diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d34034c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.dockerignore +Dockerfile +docker-compose.yml \ No newline at end of file diff --git a/.gitignore b/.gitignore index 66fd13c..5f9b7ca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Dependency directories (remove the comment below to include it) +# vendor/ + +### Go template # Binaries for programs and plugins *.exe *.exe~ @@ -13,3 +17,16 @@ # Dependency directories (remove the comment below to include it) # vendor/ + +*.zip +*.tmp + +.idea +.vscode + +tmp + +### Config file + +conf/app-dev.conf +conf/app-prod.conf \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..645bab0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM golang:1.16-rc-buster +WORKDIR /openpbl +RUN go env -w CGO_ENABLED=0 GOPROXY=https://goproxy.io,direct GOOS=linux GOARCH=amd64 \ + && apt update && apt install sudo \ + && wget https://nodejs.org/dist/v12.22.0/node-v12.22.0-linux-x64.tar.gz \ + && sudo tar xf node-v12.22.0-linux-x64.tar.gz \ + && sudo apt install wait-for-it +ENV PATH=$PATH:/openpbl/node-v12.22.0-linux-x64/bin +RUN npm install -g yarn + +COPY openpbl-landing/package.json /openpbl/openpbl-landing/package.json +RUN cd openpbl-landing && yarn install + +COPY openpbl-landing /openpbl/openpbl-landing +RUN cd openpbl-landing && yarn build && rm -rf node_modules + +COPY ./ /openpbl +RUN cd /openpbl && go build main.go + + +FROM alpine:3.7 +COPY --from=0 /openpbl / +COPY --from=0 /usr/bin/wait-for-it / +RUN set -eux \ + && sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories \ + && apk update \ + && apk upgrade \ + && apk add bash +ENV RUNMODE=prod +CMD ./wait-for-it openpbl-db:3308 && ./main \ No newline at end of file diff --git a/README.md b/README.md index 8b211b9..8220ffb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,77 @@ # OpenPBL System of PBL. + + +## 开发 + +### 后端 + +新建开发配置文件 + +`vim conf/app-dev.conf` + +配置文件内容参考 + +`conf/app.conf` + +### 前端 + +新建开发配置文件 + +`vim openpbl-landing/.env.development` + +配置文件内容参考 + +`openpbl-landing/.env` + + +## 部署 + +### 后端 beego + +新建部署配置文件 + +`vim conf/app-prod.conf` + +配置文件内容参考 + +`conf/app.conf` + +``` +appname = OpenPBL +httpaddr = 127.0.0.1 +autorender = false +copyrequestbody = true +EnableDocs = true +SessionOn = true +copyrequestbody = true + +httpport = 5000 +driverName = mysql +dataSourceName = root:root@tcp(db:3306)/ # docker-compose 部署环境下 localhost 改为 db +dbName = openpbl_db + +casdoorEndpoint = http://localhost:8000 +clientId = # casdoor 应用 id +clientSecret = # casdoor 应用 secret +jwtSecret = # jwt 加密密钥 +casdoorOrganization = "openct" # casdoor 应用所属组织 +``` + +### 前端 react + +新建部署配置文件 + +`vim openpbl-landing/.env.production` + +配置文件内容参考 + +`openpbl-landing/.env` + +```dotenv +REACT_APP_BASE_URL='http://localhost:5000/api' +REACT_APP_OSS_REGION: 'oss-cn-hangzhou' # 阿里云 oss region +REACT_APP_OSS_ACCESSKEYID: '123' # oss accesskeyId +REACT_APP_OSS_ACCESSKEYSECRET: '123' # oss accessKeySecret +REACT_APP_OSS_BUCKET: 'bucket' # oss bucket +``` \ No newline at end of file diff --git a/conf/app.conf b/conf/app.conf new file mode 100644 index 0000000..6dff7f6 --- /dev/null +++ b/conf/app.conf @@ -0,0 +1,21 @@ +appname = OpenPBL +httpaddr = 127.0.0.1 +autorender = false +copyrequestbody = true +EnableDocs = true +SessionOn = true +copyrequestbody = true + +runmode = dev + +httpport = 5000 +driverName = mysql +dataSourceName = root:root@tcp(localhost:3306)/ +dbName = openpbl_db + +jwtSecret = CasdoorSecret + +casdoorEndpoint = http://localhost:8000 +clientId= +clientSecret= +casdoorOrganization="openct" diff --git a/controllers/auth.go b/controllers/auth.go new file mode 100644 index 0000000..e2427b5 --- /dev/null +++ b/controllers/auth.go @@ -0,0 +1,140 @@ +package controllers + +import ( + "OpenPBL/util" + "fmt" + "github.com/astaxie/beego" + "github.com/casdoor/casdoor-go-sdk/auth" +) + +type AuthController struct { + beego.Controller +} + +func InitCasdoor() { + var CasdoorEndpoint = beego.AppConfig.String("casdoorEndpoint") + var ClientId = beego.AppConfig.String("clientId") + var ClientSecret = beego.AppConfig.String("clientSecret") + var JwtSecret = beego.AppConfig.String("jwtSecret") + var CasdoorOrganization = beego.AppConfig.String("casdoorOrganization") + auth.InitConfig(CasdoorEndpoint, ClientId, ClientSecret, JwtSecret, CasdoorOrganization) +} + + +func (c *AuthController) GetSessionUser() *auth.Claims { + s := c.GetSession("user") + if s == nil { + return nil + } + claims := &auth.Claims{} + err := util.JsonToStruct(s.(string), claims) + if err != nil { + panic(err) + } + + return claims +} + +func (c *AuthController) SetSessionUser(claims *auth.Claims) { + if claims == nil { + c.DelSession("user") + return + } + + s := util.StructToJson(claims) + + c.SetSession("user", s) +} + +type Response struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data"` +} + +// Login +// @Title +// @Description +// @Param code body string true "code" +// @Param state body string true "state" +// @Success 200 {int} models.Teacher.Id +// @Failure 403 body is empty +// @router /login [post] +func (c *AuthController) Login() { + code := c.Input().Get("code") + state := c.Input().Get("state") + token, err := auth.GetOAuthToken(code, state) + if err != nil { + c.Data["json"] = Response{ + Code: 403, + Msg: err.Error(), + } + c.ServeJSON() + return + } + claims, err := auth.ParseJwtToken(token.AccessToken) + if err != nil { + c.Data["json"] = Response{ + Code: 403, + Msg: err.Error(), + } + c.ServeJSON() + return + } + + fmt.Println(claims.StandardClaims) + + claims.AccessToken = token.AccessToken + c.SetSessionUser(claims) + + resp := &Response{ + Code: 200, + Msg: "登录成功", + Data: claims, + } + c.Data["json"] = resp + c.ServeJSON() +} + +// Logout +// @Title +// @Description +// @Success 200 {object} Response +// @router /logout [post] +func (c *AuthController) Logout() { + var resp Response + c.SetSessionUser(nil) + resp = Response{ + Code: 200, + Msg: "", + } + c.Data["json"] = resp + c.ServeJSON() +} + +// GetAccount +// @Title +// @Description +// @Success 200 {object} Response +// @Failure 401 {object} Response +// @router /account [get] +func (c *AuthController) GetAccount() { + var resp Response + user := c.GetSessionUser() + if user == nil { + resp = Response{ + Code: 404, + Msg: "账号不存在", + } + c.Data["json"] = resp + c.ServeJSON() + return + } + resp = Response{ + Code: 200, + Msg: "", + Data: user, + } + c.Data["json"] = resp + c.ServeJSON() +} diff --git a/controllers/chapter.go b/controllers/chapter.go new file mode 100644 index 0000000..ec01d59 --- /dev/null +++ b/controllers/chapter.go @@ -0,0 +1,188 @@ +package controllers + +import ( + "OpenPBL/models" + "strconv" +) + +type ChaptersResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Chapters []models.Outline `json:"chapters"` + ShowMinute bool `json:"showMinute"` +} + +// GetProjectChapters +// @Title +// @Description +// @Param pid path string true "project id" +// @Success 200 {object} []models.Outline +// @Failure 403 body is empty +// @router /:id/chapters [get] +func (p *ProjectController) GetProjectChapters() { + user := p.GetSessionUser() + if user == nil { + p.Data["json"] = ChaptersResponse{ + Code: 401, + Msg: "请先登录", + } + p.ServeJSON() + return + } + uid := "" + show := false + if user.Tag == "student" { + uid = user.Username + show = true + } + if user.Tag == "teacher" { + uid = p.GetString("studentId") + if uid != "" { + show = true + } + } + pid := p.GetString(":id") + outline, err := models.GetChaptersByPid(pid, uid) + if err != nil { + p.Data["json"] = ChaptersResponse{ + Code: 400, + Msg: err.Error(), + Chapters: make([]models.Outline, 0), + } + } else { + p.Data["json"] = ChaptersResponse{ + Code: 200, + Chapters: outline, + ShowMinute: show, + } + } + p.ServeJSON() +} +// CreateProjectChapter +// @Title +// @Description +// @Param body body models.Chapter true "" +// @Success 200 {object} Response +// @Failure 401 +// @router /:id/chapter [post] +func (p *ProjectController) CreateProjectChapter() { + pid, err := p.GetInt64(":id") + num, err := p.GetInt("chapterNumber") + chapter := &models.Chapter{ + ProjectId: pid, + ChapterName: p.GetString("chapterName"), + ChapterNumber: num, + } + if err != nil { + p.Data["json"] = Response{ + Code: 400, + Msg: err.Error(), + } + } + err = chapter.Create() + if err != nil { + p.Data["json"] = Response{ + Code: 400, + Msg: err.Error(), + } + } else { + p.Data["json"] = Response{ + Code: 200, + Msg: "添加成功", + Data: strconv.FormatInt(chapter.Id, 10), + } + } + p.ServeJSON() +} + +// UpdateProjectChapter +// @Title +// @Description +// @Param body body models.Chapter true "" +// @Success 200 {object} Response +// @Failure 401 +// @router /:projectId/chapter/:chapterId [post] +func (p *ProjectController) UpdateProjectChapter() { + cid, err := p.GetInt64(":chapterId") + pid, err := p.GetInt64(":projectId") + num, err := p.GetInt("chapterNumber") + chapter := &models.Chapter{ + Id: cid, + ProjectId: pid, + ChapterName: p.GetString("chapterName"), + ChapterNumber: num, + } + err = chapter.Update() + if err != nil { + p.Data["json"] = Response{ + Code: 400, + Msg: "更新失败", + } + } else { + p.Data["json"] = Response{ + Code: 200, + Msg: "更新成功", + Data: true, + } + } + p.ServeJSON() +} + +// DeleteProjectChapter +// @Title +// @Description +// @Param body body models.Chapter true "" +// @Success 200 {object} Response +// @Failure 401 +// @router /:projectId/chapter/:chapterId/delete [post] +func (p *ProjectController) DeleteProjectChapter() { + cid, err := p.GetInt64(":chapterId") + pid, err := p.GetInt64(":projectId") + num, err := p.GetInt("chapterNumber") + chapter := &models.Chapter{ + Id: cid, + ProjectId: pid, + ChapterName: p.GetString("chapterName"), + ChapterNumber: num, + } + err = chapter.Delete() + if err != nil { + p.Data["json"] = Response{ + Code: 400, + Msg: err.Error(), + } + } else { + p.Data["json"] = Response{ + Code: 200, + Msg: "删除成功", + Data: true, + } + } + p.ServeJSON() +} + +// ExchangeProjectChapter +// @Title +// @Description +// @Param cid path string true "" +// @Success 200 {object} Response +// @Failure 401 +// @router /:projectId/chapters/exchange [post] +func (p *ProjectController) ExchangeProjectChapter() { + cid1 := p.GetString("chapterId1") + cid2 := p.GetString("chapterId2") + + err := models.ExchangeChapters(cid1, cid2) + if err != nil { + p.Data["json"] = Response{ + Code: 400, + Msg: err.Error(), + } + } else { + p.Data["json"] = Response{ + Code: 200, + Data: true, + } + } + p.ServeJSON() +} diff --git a/controllers/evaluate.go b/controllers/evaluate.go new file mode 100644 index 0000000..083d62a --- /dev/null +++ b/controllers/evaluate.go @@ -0,0 +1,2 @@ +package controllers + diff --git a/controllers/project.go b/controllers/project.go new file mode 100644 index 0000000..d7bd398 --- /dev/null +++ b/controllers/project.go @@ -0,0 +1,538 @@ +package controllers + +import ( + "OpenPBL/models" + "OpenPBL/util" + "encoding/json" + "fmt" + "github.com/astaxie/beego" + "github.com/casdoor/casdoor-go-sdk/auth" + "strings" + "time" +) + +// ProjectController +// Operations about Projects +type ProjectController struct { + beego.Controller +} + +func (p *ProjectController) GetSessionUser() *auth.Claims { + s := p.GetSession("user") + if s == nil { + return nil + } + claims := &auth.Claims{} + err := util.JsonToStruct(s.(string), claims) + if err != nil { + panic(err) + } + return claims +} + +type ProjectResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Project models.ProjectDetail `json:"project"` +} + +// GetProjectDetail +// @Title +// @Description +// @Param id path string true "project id" +// @Success 200 {object} models.TeacherProject +// @Failure 400 +// @router /:id [get] +func (p *ProjectController) GetProjectDetail() { + pid := p.GetString(":id") + user := p.GetSessionUser() + var resp ProjectResponse + if user == nil { + resp = ProjectResponse{ + Code: 401, + Msg: "请先登录", + } + p.Data["json"] = resp + p.ServeJSON() + return + } + uid := user.Username + var err error + var project models.ProjectDetail + if user.Tag == "student" { + project, err = models.GetProjectByPidForStudent(pid, uid) + } else if user.Tag == "teacher" { + project, err = models.GetProjectByPidForTeacher(pid) + } + if err != nil { + resp = ProjectResponse{ + Code: 400, + Msg: err.Error(), + } + } else { + resp = ProjectResponse{ + Code: 200, + Project: project, + } + } + p.Data["json"] = resp + p.ServeJSON() +} + +// CreateProject +// @Title +// @Description create project +// @Success 200 {object} Response +// @Failure 401 +// @Failure 400 +// @Failure 403 +// @router / [post] +func (p *ProjectController) CreateProject() { + user := p.GetSessionUser() + var resp Response + if user == nil { + resp = Response{ + Code: 401, + Msg: "请先登录", + } + p.Data["json"] = resp + p.ServeJSON() + return + } + if user.Tag != "teacher" { + resp = Response{ + Code: 403, + Msg: "非法用户", + } + p.Data["json"] = resp + p.ServeJSON() + return + } + uid := user.Username + project := &models.Project{ + TeacherId: uid, + } + fmt.Println(project) + err := project.Create() + if err != nil { + resp = Response{ + Code: 400, + Msg: err.Error(), + } + } else { + resp = Response{ + Code: 200, + Msg: "创建成功", + Data: project.Id, + } + } + p.Data["json"] = resp + p.ServeJSON() +} + +// UpdateProject +// @Title +// @Description create project +// @Param body body models.Project true "" +// @Success 200 {int} models.Project.Id +// @Failure 403 body is empty +// @router /:id [post] +func (p *ProjectController) UpdateProject() { + user := p.GetSessionUser() + var resp Response + if user == nil { + resp = Response{ + Code: 401, + Msg: "请先登录", + } + p.Data["json"] = resp + p.ServeJSON() + return + } + if user.Tag != "teacher" { + resp = Response{ + Code: 403, + Msg: "非法用户", + } + p.Data["json"] = resp + p.ServeJSON() + return + } + uid := user.Username + pid, err := p.GetInt64(":id") + if err != nil { + resp = Response{ + Code: 400, + Msg: err.Error(), + } + p.Data["json"] = resp + p.ServeJSON() + return + } + project := &models.Project{ + Id: pid, + Image: p.GetString("image"), + ProjectTitle: p.GetString("projectTitle"), + ProjectIntroduce: p.GetString("projectIntroduce"), + ProjectGoal: p.GetString("projectGoal"), + TeacherId: uid, + Subjects: p.GetString("subjects"), + Skills: p.GetString("skills"), + } + projectSubjects, projectSkills, err := getProjectSubjectsAndSkills(pid, project.Subjects, project.Skills) + err = project.UpdateInfo(projectSubjects, projectSkills) + if err != nil { + resp = Response{ + Code: 400, + Msg: err.Error(), + Data: true, + } + } else { + resp = Response{ + Code: 200, + Msg: "更新成功", + } + } + p.Data["json"] = resp + p.ServeJSON() +} + +// UpdateProjectWeight +// @Title +// @Description create project +// @Param body body models.Project true "" +// @Success 200 {int} models.Project.Id +// @Failure 403 body is empty +// @router /:id/weight [post] +func (p *ProjectController) UpdateProjectWeight() { + user := p.GetSessionUser() + var resp Response + if user == nil { + resp = Response{ + Code: 401, + Msg: "请先登录", + } + p.Data["json"] = resp + p.ServeJSON() + return + } + if user.Tag != "teacher" { + resp = Response{ + Code: 403, + Msg: "非法用户", + } + p.Data["json"] = resp + p.ServeJSON() + return + } + pid, err := p.GetInt64(":id") + learnMinuteWeight, err := p.GetInt("learnMinuteWeight") + + if err != nil { + resp = Response{ + Code: 400, + Msg: err.Error(), + } + p.Data["json"] = resp + p.ServeJSON() + return + } + project := models.Project{ + Id: pid, + LearnMinuteWeight: learnMinuteWeight, + } + tasks := make([]models.Task, 0) + err = json.Unmarshal([]byte(p.GetString("tasks")), &tasks) + err = models.UpdateWeight(project, tasks) + if err != nil { + resp = Response{ + Code: 400, + Msg: err.Error(), + Data: true, + } + } else { + resp = Response{ + Code: 200, + Msg: "更新成功", + } + } + p.Data["json"] = resp + p.ServeJSON() +} + + +// PublishProject +// @Title +// @Description +// @Param pid path int true "" +// @Success 200 {Response} +// @Failure 400 +// @Failure 401 +// @Failure 403 +// @router /:id/publish [post] +func (u *ProjectController) PublishProject() { + pid, err := u.GetInt64(":id") + var resp Response + user := u.GetSessionUser() + if user == nil { + resp = Response{ + Code: 401, + Msg: "请先登录", + } + u.Data["json"] = resp + u.ServeJSON() + return + } + if user.Tag != "teacher" { + resp = Response{ + Code: 403, + Msg: "非法的用户", + } + u.Data["json"] = resp + u.ServeJSON() + return + } + + p := models.Project{ + Id: pid, + PublishedAt: time.Now(), + Published: true, + } + err = models.UpdatePublished(p) + if err != nil { + resp = Response{ + Code: 400, + Msg: err.Error(), + } + } else { + resp = Response{ + Code: 200, + Msg: "发布成功", + } + } + u.Data["json"] = resp + u.ServeJSON() +} + +// CloseProject +// @Title +// @Description +// @Param pid path int true "" +// @Success 200 {object} Response +// @Failure 400 +// @Failure 401 +// @Failure 403 +// @router /:id/close [post] +func (u *ProjectController) CloseProject() { + pid, err := u.GetInt64(":id") + var resp Response + user := u.GetSessionUser() + if user == nil { + resp = Response{ + Code: 401, + Msg: "请先登录", + } + u.Data["json"] = resp + u.ServeJSON() + return + } + if user.Tag != "teacher" { + resp = Response{ + Code: 403, + Msg: "非法的用户", + } + u.Data["json"] = resp + u.ServeJSON() + return + } + + p := models.Project{ + Id: pid, + ClosedAt: time.Now(), + Closed: true, + } + err = models.UpdateClosed(p) + if err != nil { + resp = Response{ + Code: 400, + Msg: err.Error(), + } + } else { + resp = Response{ + Code: 200, + Msg: "发布成功", + } + } + u.Data["json"] = resp + u.ServeJSON() +} + +// DeleteProject +// @Title +// @Description +// @Param pid path int true "" +// @Success 200 {Response} +// @Failure 400 +// @Failure 401 +// @Failure 403 +// @router /:id/delete [post] +func (u *ProjectController) DeleteProject() { + pid, err := u.GetInt64(":id") + var resp Response + user := u.GetSessionUser() + if user == nil { + resp = Response{ + Code: 401, + Msg: "请先登录", + } + u.Data["json"] = resp + u.ServeJSON() + return + } + if user.Tag != "teacher" { + resp = Response{ + Code: 403, + Msg: "非法的用户", + } + u.Data["json"] = resp + u.ServeJSON() + return + } + + p := models.Project{ + Id: pid, + } + err = p.Delete() + if err != nil { + resp = Response{ + Code: 400, + Msg: err.Error(), + } + } else { + resp = Response{ + Code: 200, + Msg: "删除成功", + } + } + u.Data["json"] = resp + u.ServeJSON() +} + +// RemoveStudent +// @Title +// @Description +// @Param pid path string true "" +// @Success 200 {object} Response +// @Failure 401 +// @router /:projectId/remove/:studentId [post] +func (u *ProjectController) RemoveStudent() { + pid, err := u.GetInt64(":projectId") + sid := u.GetString(":studentId") + var resp Response + user := u.GetSessionUser() + if user == nil { + resp = Response{ + Code: 401, + Msg: "请先登录", + } + u.Data["json"] = resp + u.ServeJSON() + return + } + l := &models.LearnProject{ + StudentId: sid, + ProjectId: pid, + } + err = l.Delete() + if err != nil { + resp = Response{ + Code: 400, + Msg: err.Error(), + } + u.Data["json"] = resp + } else { + resp = Response{ + Code: 200, + Msg: "移除成功", + } + } + u.Data["json"] = resp + u.ServeJSON() +} + + +func getProjectSubjectsAndSkills(pid int64, subjects string, skills string) (subjectList []*models.ProjectSubject, skillList []*models.ProjectSkill, err error) { + var ( + subjectL []string + skillL []string + ) + + if subjects == "" { + subjectL = make([]string, 0) + } else { + subjectL = strings.Split(subjects, ",") + } + if skills == "" { + skillL = make([]string, 0) + } else { + skillL = strings.Split(skills, ",") + } + n1 := len(subjectL) + n2 := len(skillL) + + subjectList = make([]*models.ProjectSubject, n1) + skillList = make([]*models.ProjectSkill, n2) + for i:=0; i ?", p.ProjectId, p.ChapterNumber) + fmt.Println(err) + _, err = session.Table(&Section{}).Delete(Section{ChapterId: p.Id}) + _, err = session.Table(&Chapter{}).ID(p.Id).Delete(p) + session.Commit() + return +} +func ExchangeChapters(cid1 string, cid2 string) (err error) { + _, err = adapter.Engine. + Exec("update chapter c1 join chapter c2 on (c1.id = ? and c2.id = ?) " + + "set c1.chapter_number = c2.chapter_number, c2.chapter_number = c1.chapter_number", cid1, cid2) + return +} + +func GetChaptersByPid(pid string, uid string) (outline []Outline, err error) { + var c []Chapter + err = (&Chapter{}).GetEngine(). + Where("project_id = ?", pid). + Asc("chapter_number"). + Find(&c) + outline = make([]Outline, len(c)) + for i:=0; i< len(c); i++ { + sections := make([]SectionMinute, 0) + outline[i].Chapter = c[i] + if uid == "" { + err = (&Section{}).GetEngine(). + Where("chapter_id = ?", c[i].Id). + Asc("section_number"). + Find(§ions) + } else { + err = adapter.Engine. + SQL("select * from (select * from section where chapter_id = ?) s LEFT JOIN learn_section ls on s.id = ls.section_id and ls.student_id = ? order by s.section_number", c[i].Id, uid). + Find(§ions) + } + outline[i].Sections = sections + } + return +} diff --git a/models/comment.go b/models/comment.go new file mode 100644 index 0000000..6ede7e3 --- /dev/null +++ b/models/comment.go @@ -0,0 +1,29 @@ +package models + +import "time" + +type Comment struct { + Id int64 `json:"id" xorm:"not null pk autoincr"` + + ProjectId int64 `json:"projectId" xorm:"not null index"` + UserId int64 `json:"userId" xorm:"not null index"` + IsTeacher bool `json:"isTeacher" xorm:"not null index default false"` + + Content string `json:"content" xorm:"text"` + + CreateAt time.Time `json:"createAt" xorm:"created"` + +} + +type CommentReply struct { + Id int64 `json:"id" xorm:"not null pk autoincr"` + + CommentId int64 `json:"commentId" xorm:"not null index"` + UserId int64 `json:"userId" xorm:"not null index"` + IsTeacher bool `json:"isTeacher" xorm:"not null index default false"` + + Content string `json:"content" xorm:"text"` + + CreateAt time.Time `json:"createAt" xorm:"created"` +} + diff --git a/models/evaluate.go b/models/evaluate.go new file mode 100644 index 0000000..34063a8 --- /dev/null +++ b/models/evaluate.go @@ -0,0 +1,25 @@ +package models + +type Evaluate struct { + Chapter `xorm:"extends"` + SectionOutline `xorm:"extends"` +} + + +func GetProjectEvaluate(pid string) (evaluate []Evaluate, err error) { + var c []Chapter + err = (&Chapter{}).GetEngine(). + Where("project_id = ?", pid). + Asc("chapter_number"). + Find(&c) + evaluate = make([]Evaluate, len(c)) + var outline SectionOutline + for i:=0; i< len(c); i++ { + evaluate[i].Chapter = c[i] + err = (&Section{}).GetEngine(). + Where("chapter_id = ?", c[i].Id). + Find(&outline) + evaluate[i].SectionOutline = outline + } + return +} \ No newline at end of file diff --git a/models/generateSql.go b/models/generateSql.go new file mode 100644 index 0000000..c5301a2 --- /dev/null +++ b/models/generateSql.go @@ -0,0 +1,64 @@ +package models + +import ( + "bytes" + "strings" +) + +func getSubjectExistSql(subject string) (sql string) { + if subject == "" { + return "" + } else { + s := strings.Split(subject, ",") + sql := ` + and exists ( + select project_subject.project_id from project_subject where project_subject.project_id = project.id and (` + var buf bytes.Buffer + buf.WriteString(sql) + n := len(s) + for i:=0; i ?", p.ChapterId, p.SectionNumber) + _, err = session.Table(&Section{}).ID(p.Id).Delete(p) + session.Commit() + return +} +func ExchangeSections(id1 string, id2 string) (err error) { + _, err = adapter.Engine. + Exec("update section c1 join section c2 on (c1.id = ? and c2.id = ?) " + + "set c1.section_number = c2.section_number, c2.section_number = c1.section_number", id1, id2) + return +} + +func GetSectionsByCid(cid string) (s []Section, err error) { + err = (&Section{}).GetEngine(). + Where("chapter_id = ?", cid). + Asc("section_number"). + Find(&s) + return +} + +func GetSectionDetailById(sid string) (s SectionDetail, err error) { + _, err = adapter.Engine. + Table("section"). + Where("section.id = ?", sid). + Join("INNER", "resource", "resource.section_id = section.id"). + Get(&s) + return +} \ No newline at end of file diff --git a/models/sql/GetMyProjectListBySid.sql b/models/sql/GetMyProjectListBySid.sql new file mode 100644 index 0000000..3c45d1e --- /dev/null +++ b/models/sql/GetMyProjectListBySid.sql @@ -0,0 +1,23 @@ +select * from ( + select * from project + inner join learn_project on ( + learn_project.student_id = 1 and + learn_project.learning = true and + project.id = learn_project.project_id + ) +) as project where true + and exists ( + select project_subject.project_id from project_subject + where project_subject.project_id = project.id and ( + project_subject.subject = '英语' or project_subject.subject = '数学' + ) + ) + and exists ( + select project_skill.project_id from project_skill + where project_skill.project_id = project.id and ( + project_skill.skill = '生活与职业技能' + ) + ) + and project.project_title like '%神奇%' + +order by create_at desc limit 0, 10 \ No newline at end of file diff --git a/models/sql/GetMyProjectListByTid.sql b/models/sql/GetMyProjectListByTid.sql new file mode 100644 index 0000000..61c6541 --- /dev/null +++ b/models/sql/GetMyProjectListByTid.sql @@ -0,0 +1,18 @@ +select * from project where teacher_id = 1 + and published = true + and closed = false + and exists ( + select project_subject.project_id from project_subject + where project_subject.project_id = project.id and ( + project_subject.subject = '英语' or project_subject.subject = '数学' + ) + ) + and exists ( + select project_skill.project_id from project_skill + where project_skill.project_id = project.id and ( + project_skill.skill = '生活与职业技能' + ) + ) + and project.project_title like '%神奇%' + +order by create_at desc limit 0, 10 \ No newline at end of file diff --git a/models/sql/GetPublicProjectListForStudent.sql b/models/sql/GetPublicProjectListForStudent.sql new file mode 100644 index 0000000..49a870e --- /dev/null +++ b/models/sql/GetPublicProjectListForStudent.sql @@ -0,0 +1,19 @@ +select * from ( + select * from project where project.published = true + and exists ( + select project_subject.project_id from project_subject + where project_subject.project_id = project.id and ( + project_subject.subject = '英语' or project_subject.subject = '数学' + ) + ) + and exists ( + select project_skill.project_id from project_skill + where project_skill.project_id = project.id and ( + project_skill.skill = '生活与职业技能' + ) + ) + and project.project_title like '%%' + ) as p1 left join learn_project on ( + p1.id = learn_project.project_id and learn_project.student_id = 1 + ) +order by p1.create_at desc limit 0, 10 \ No newline at end of file diff --git a/models/sql/GetPublicProjectListForTeacher.sql b/models/sql/GetPublicProjectListForTeacher.sql new file mode 100644 index 0000000..5141bd1 --- /dev/null +++ b/models/sql/GetPublicProjectListForTeacher.sql @@ -0,0 +1,16 @@ +select * from project where published = true + and exists ( + select project_subject.project_id from project_subject + where project_subject.project_id = project.id and ( + project_subject.subject = '英语' or project_subject.subject = '数学' + ) + ) + and exists ( + select project_skill.project_id from project_skill + where project_skill.project_id = project.id and ( + project_skill.skill = '生活与职业技能' + ) + ) + and project.project_title like '%神奇%' + +order by create_at desc limit 0, 10 \ No newline at end of file diff --git a/models/student.go b/models/student.go new file mode 100644 index 0000000..00e9c11 --- /dev/null +++ b/models/student.go @@ -0,0 +1,154 @@ +package models + +import ( + "strconv" + "time" + "xorm.io/xorm" +) + +type LearnProject struct { + Avatar string `json:"avatar" xorm:"text"` + Name string `json:"name"` + StudentId string `json:"studentId" xorm:"not null index pk"` + ProjectId int64 `json:"projectId" xorm:"not null index pk"` + Learning bool `json:"learning" xorm:"index default 0"` + JoinTime time.Time `json:"joinTime" xorm:"created"` +} + +type LearnSection struct { + StudentId string `json:"studentId" xorm:"not null pk"` + SectionId int64 `json:"sectionId" xorm:"not null pk"` + + LearnMinute int `json:"learnMinute" xorm:"default 0"` + LearnSecond int `json:"learnSecond" xorm:"default 0"` +} + +type LastLearn struct { + StudentId string `json:"studentId" xorm:"not null pk"` + ProjectId int64 `json:"projectId" xorm:"not null pk"` + SectionId int64 `json:"sectionId" xorm:"not null index"` + ExitAt time.Time `json:"exitAt" xorm:"updated"` +} + +type LastLearnSection struct { + LastLearn `xorm:"extends"` + Id int64 `json:"id"` + SectionName string `json:"sectionName"` + ChapterNumber int `json:"chapterNumber"` + SectionNumber int `json:"sectionNumber"` + Last bool `json:"last"` +} + +func (l *LearnProject) GetEngine() *xorm.Session { + return adapter.Engine.Table(l) +} +func (l *LearnSection) GetEngine() *xorm.Session { + return adapter.Engine.Table(l) +} +func (l *LastLearn) GetEngine() *xorm.Session { + return adapter.Engine.Table(l) +} + +func (l *LearnProject) Create() (err error) { + _, err = (&LearnProject{}).GetEngine().Insert(l) + _, err = adapter.Engine. + Exec("update project set join_num = join_num + 1 where id = ?", l.ProjectId) + return +} + +func (l *LearnProject) Update() (err error) { + _, err = (&LearnProject{}).GetEngine(). + Where("student_id = ?", l.StudentId). + Where("project_id = ?", l.ProjectId). + MustCols("learning"). + Update(l) + return +} + +func (l *LearnProject) Delete() (err error) { + _, err = (&LearnProject{}).GetEngine(). + Where("student_id = ?", l.StudentId). + Where("project_id = ?", l.ProjectId). + Delete(l) + _, err = adapter.Engine. + Exec("update project set join_num = join_num - 1 where id = ?", l.ProjectId) + return +} + +func IsLearningProject(pid string, uid string) (e bool) { + var err error + id, err := strconv.ParseInt(pid, 10, 64) + e, err = (&LearnProject{}).GetEngine().Exist(&LearnProject{ + StudentId: uid, + ProjectId: id, + Learning: true, + }) + if err != nil { + e = false + } + return +} + +func GetProjectStudents(pid string, from int, size int) (s []LearnProject, rows int64, err error) { + err = (&LearnProject{}).GetEngine(). + Where("project_id = ?", pid). + Desc("join_time"). + Limit(size, from). + Find(&s) + rows, err = (&LearnProject{}).GetEngine(). + Where("project_id = ?", pid). + Count() + return +} + +func GetLearnSection(sectionId int64, studentId string, projectId int64) (l LearnSection, err error) { + var b bool + b, err = (&LearnSection{}).GetEngine(). + Where("section_id = ?", sectionId). + Where("student_id = ?", studentId). + Get(&l) + if !b { + l.SectionId = sectionId + l.StudentId = studentId + err = (&l).Create(projectId) + } + return +} + +func GetLastLearnSection(studentId string, projectId string) (l LastLearnSection, err error) { + var b bool + b, err = (&LastLearn{}).GetEngine(). + Where("student_id = ?", studentId). + Where("project_id = ?", projectId). + Join("LEFT OUTER", Section{}, "last_learn.section_id = section.id"). + Get(&l) + l.Last = b + return +} + +func (l *LearnSection) Create(projectId int64) (err error) { + _, err = (&LearnSection{}).GetEngine().Insert(l) + _, err = (&LastLearn{}).GetEngine().Insert(&LastLearn{ + StudentId: l.StudentId, + ProjectId: projectId, + SectionId: l.SectionId, + ExitAt: time.Now(), + }) + return +} +func (l *LearnSection) Update(projectId int64) (err error) { + _, err = (&LearnSection{}).GetEngine(). + Where("student_id = ?", l.StudentId). + Where("section_id = ?", l.SectionId). + Update(l) + _, err = (&LastLearn{}).GetEngine(). + Where("student_id = ?", l.StudentId). + Where("project_id = ?", projectId). + Update(&LastLearn{ + StudentId: l.StudentId, + ProjectId: projectId, + SectionId: l.SectionId, + ExitAt: time.Now(), + }) + return +} \ No newline at end of file diff --git a/models/submit.go b/models/submit.go new file mode 100644 index 0000000..70fc62e --- /dev/null +++ b/models/submit.go @@ -0,0 +1,89 @@ +package models + +import ( + "time" + "xorm.io/xorm" +) + +type Submit struct { + Id int64 `json:"id" xorm:"not null pk autoincr"` + + ProjectId int64 `json:"projectId" xorm:"not null index"` + StudentId string `json:"studentId" xorm:"not null index"` + TaskId int64 `json:"taskId" xorm:"not null index"` + + SubmitType string `json:"submitType" xorm:"index"` + + SubmitTitle string `json:"submitTitle"` + SubmitIntroduce string `json:"submitIntroduce" xorm:"text"` + SubmitContent string `json:"submitContent" xorm:"text"` + + FilePath string `json:"filePath"` + CreateAt time.Time `json:"createAt"` + + Score int `json:"score" xorm:"default 0"` + Scored bool `json:"scored" xorm:"default false"` +} + +type Choice struct { + Id int64 `json:"id" xorm:"not null pk autoincr"` + SubmitId int64 `json:"submitId" xorm:"not null index"` + ChoiceOrder int `json:"choiceOrder"` + ChoiceOptions string `json:"choiceOptions" xorm:"text"` +} + +type SubmitDetail struct { + Submit `json:"submit" xorm:"extends"` + Choices []Choice `json:"choices" xorm:"extends"` + Submitted bool `json:"submitted"` +} + +func (p *Submit) GetEngine() *xorm.Session { + return adapter.Engine.Table(p) +} +func (c *Choice) GetEngine() *xorm.Session { + return adapter.Engine.Table(c) +} +func (c *Choice) Create() (err error) { + _, err = c.GetEngine().Insert(c) + return +} +func (c *Choice) Update() (err error) { + _, err = c.GetEngine().ID(c.Id).Update(c) + return +} + +func (p *Submit) Create(c []Choice) (err error) { + _, err = p.GetEngine().Insert(p) + if p.SubmitType == "survey" { + for i := 0; i < len(c); i ++ { + ci := &Choice{ + SubmitId: p.Id, + ChoiceOrder: c[i].ChoiceOrder, + ChoiceOptions: c[i].ChoiceOptions, + } + err = ci.Create() + } + CountSubmit(c, nil, p.TaskId) + } + return +} +func (p *Submit) Update(c []Choice) (err error) { + _, err = p.GetEngine().ID(p.Id).Update(p) + if len(c) > 0 && p.SubmitType == "survey" { + var cs []Choice + err = (&Choice{}).GetEngine(). + Where("submit_id = ?", p.Id). + Find(&cs) + for i:=0; i< len(c); i++ { + err = (&c[i]).Update() + } + CountSubmit(c, cs, p.TaskId) + } + return +} + +func CountSubmit(c []Choice, cl []Choice, taskId int64) { + + +} \ No newline at end of file diff --git a/models/survey.go b/models/survey.go new file mode 100644 index 0000000..7cd5701 --- /dev/null +++ b/models/survey.go @@ -0,0 +1,76 @@ +package models + +import ( + "xorm.io/xorm" +) + +type Survey struct { + Id int64 `json:"id" xorm:"not null pk autoincr"` + TaskId int64 `json:"taskId" xorm:"not null index"` + + SurveyTitle string `json:"surveyTitle"` + SurveyIntroduce string `json:"surveyIntroduce"` +} +type Question struct { + Id int64 `json:"id" xorm:"not null pk autoincr"` + SurveyId int64 `json:"surveyId" xorm:"not null index"` + QuestionOrder int `json:"questionOrder" xorm:"not null index"` + QuestionTitle string `json:"questionTitle"` + QuestionType string `json:"questionType"` + QuestionOptions string `json:"questionOptions" xorm:"text"` + QuestionCount string `json:"question" xorm:"text"` +} +type SurveyDetail struct { + Survey `json:"survey" xorm:"extends"` + Questions []Question `json:"questions" xorm:"extends"` +} + +func (s *Survey) GetEngine() *xorm.Session { + return adapter.Engine.Table(s) +} +func (s *Survey) Create() (err error) { + _, err = s.GetEngine().Insert(s) + return +} +func (s *Survey) Update() (err error) { + _, err = s.GetEngine().ID(s.Id).Update(s) + return +} +func (s *Survey) Delete() (err error) { + _, err = s.GetEngine().ID(s.Id).Delete(s) + return +} + +func (q *Question) GetEngine() *xorm.Session { + return adapter.Engine.Table(q) +} +func (q *Question) Create() (err error) { + _, err = q.GetEngine().Insert(q) + return +} +func (q *Question) Update() (err error) { + _, err = q.GetEngine().ID(q.Id).Update(q) + return +} +func (q *Question) Delete() (err error) { + _, err = q.GetEngine().ID(q.Id).Delete(q) + return +} + +func GetSurveyByTaskId(tid string) (s Survey, qs []Question, err error) { + _, err = (&Survey{}).GetEngine(). + Where("task_id = ?", tid). + Get(&s) + err = (&Question{}).GetEngine(). + Where("survey_id = ?", s.Id). + Asc("question_order"). + Find(&qs) + return +} + +func ExchangeQuestion(id1 string, id2 string) (err error) { + _, err = adapter.Engine. + Exec("update question t1 join question t2 on (t1.id = ? and t2.id = ?) " + + "set t1.question_order = t2.question_order, t2.question_order = t1.question_order", id1, id2) + return +} \ No newline at end of file diff --git a/models/task.go b/models/task.go new file mode 100644 index 0000000..351da6f --- /dev/null +++ b/models/task.go @@ -0,0 +1,193 @@ +package models + +import ( + "xorm.io/xorm" +) + +type Task struct { + Id int64 `json:"id" xorm:"not null pk autoincr"` + SectionId int64 `json:"sectionId" xorm:"not null index"` + ProjectId int64 `json:"projectId" xorm:"not null index"` + + SectionNumber int `json:"sectionNumber" xorm:"index"` + ChapterNumber int `json:"chapterNumber" xorm:"index"` + + TaskOrder int `json:"taskOrder"` + + TaskTitle string `json:"taskTitle"` + TaskIntroduce string `json:"taskIntroduce" xorm:"text"` + + TaskType string `json:"taskType" xorm:"index"` + TaskWeight int `json:"taskWeight"` +} + +type TaskEvaluate struct { + Task `xorm:"extends"` + Submitted bool `json:"submitted"` + Submit Submit `json:"submit"` +} + +type TaskDetail struct { + Task `xorm:"extends"` + SurveyDetail `xorm:"extends"` + SubmitDetail `xorm:"extends"` +} + + +func (t *Task) GetEngine() *xorm.Session { + return adapter.Engine.Table(t) +} + +func (t *Task) Create() (err error) { + session := adapter.Engine.NewSession() + defer session.Close() + session.Begin() + _, err = session.Insert(t) + if t.TaskType == "survey" { + _, err = session.Insert(Survey{ + TaskId: t.Id, + }) + } + session.Commit() + return +} +func (t *Task) Update() (err error) { + _, err = t.GetEngine().ID(t.Id).Update(t) + return +} +func (t *Task) Delete() (err error) { + _, err = t.GetEngine().ID(t.Id).Delete(t) + return +} + +func GetSectionTasks(sid string, uid string, learning bool) (t []TaskDetail, err error) { + err = (&Task{}).GetEngine(). + Where("section_id = ?", sid). + Asc("task_order"). + Find(&t) + var b bool + for i := 0; i < len(t); i ++ { + var s Survey + var qs []Question + var c []Choice + if t[i].TaskType == "survey" { + var m Submit + _, err = (&Survey{}).GetEngine(). + Where("task_id = ?", t[i].Id). + Get(&s) + err = (&Question{}).GetEngine(). + Where("survey_id = ?", s.Id). + Asc("question_order"). + Find(&qs) + t[i].Survey = s + t[i].Questions = qs + + if learning { + b, err = (&Submit{}).GetEngine(). + Where("task_id = ?", t[i].Id). + Where("student_id = ?", uid). + Get(&m) + if b { + t[i].Submitted = true + } + t[i].Submit = m + + err = (&Choice{}).GetEngine(). + Where("submit_id = ?", m.Id). + Asc("choice_order"). + Find(&c) + t[i].Choices = c + } + } else { + var m Submit + if learning { + b, err = (&Submit{}).GetEngine(). + Where("task_id = ?", t[i].Id). + Where("student_id = ?", uid). + Get(&m) + t[i].Submit = m + if b { + t[i].Submitted = true + } + } + } + } + return +} + +func ExchangeTasks(cid1 string, cid2 string) (err error) { + _, err = adapter.Engine. + Exec("update task t1 join task t2 on (t1.id = ? and t2.id = ?) " + + "set t1.task_order = t2.task_order, t2.task_order = t1.task_order", cid1, cid2) + return +} + + +func GetProjectTasksDetail(sid string, uid string, showSubmit bool) (t []TaskDetail, err error) { + err = (&Task{}).GetEngine(). + Where("project_id = ?", sid). + Asc("chapter_number"). + Asc("section_number"). + Asc("task_order"). + Find(&t) + var b bool + for i := 0; i < len(t); i ++ { + var s Survey + var qs []Question + var c []Choice + + if t[i].TaskType == "survey" { + var m Submit + b, err = (&Survey{}).GetEngine(). + Where("task_id = ?", t[i].Id). + Get(&s) + if b { + err = (&Question{}).GetEngine(). + Where("survey_id = ?", s.Id). + Asc("question_order"). + Find(&qs) + t[i].Questions = qs + } + t[i].Survey = s + + if showSubmit { + b, err = (&Submit{}).GetEngine(). + Where("task_id = ?", t[i].Id). + Where("student_id = ?", uid). + Get(&m) + if b { + t[i].Submitted = true + err = (&Choice{}).GetEngine(). + Where("submit_id = ?", m.Id). + Asc("choice_order"). + Find(&c) + t[i].Choices = c + } + t[i].Submit = m + } + } else { + var m Submit + if showSubmit { + b, err = (&Submit{}).GetEngine(). + Where("task_id = ?", t[i].Id). + Where("student_id = ?", uid). + Get(&m) + t[i].Submit = m + if b { + t[i].Submitted = true + } + } + } + } + return +} + +func GetProjectTasks(pid string) (t []TaskEvaluate, err error) { + err = (&Task{}).GetEngine(). + Where("project_id = ?", pid). + Asc("chapter_number"). + Asc("section_number"). + Asc("task_order"). + Find(&t) + return +} \ No newline at end of file diff --git a/openpbl-landing/.env b/openpbl-landing/.env new file mode 100644 index 0000000..95c04e0 --- /dev/null +++ b/openpbl-landing/.env @@ -0,0 +1,13 @@ +REACT_APP_BASE_URL='http://localhost:5000/api' +REACT_APP_OSS_REGION='oss-cn-hangzhou' +REACT_APP_OSS_ACCESSKEYID='' +REACT_APP_OSS_ACCESSKEYSECRET='' +REACT_APP_OSS_BUCKET='' + +REACT_APP_CASDOOR_ENDPOINT='http://localhost:8000' + +REACT_APP_CLIENT_ID='' +REACT_APP_APP_NAME='' +REACT_APP_CASDOOR_ORGANIZATION='' + +GENERATE_SOURCEMAP=false \ No newline at end of file diff --git a/openpbl-landing/.gitignore b/openpbl-landing/.gitignore new file mode 100644 index 0000000..f4bda71 --- /dev/null +++ b/openpbl-landing/.gitignore @@ -0,0 +1,31 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +.idea +.vscode + +### Config file + +.env.development +.env.production \ No newline at end of file diff --git a/openpbl-landing/README.md b/openpbl-landing/README.md new file mode 100644 index 0000000..02aac3f --- /dev/null +++ b/openpbl-landing/README.md @@ -0,0 +1,70 @@ +# Getting Started with Create React App + +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `yarn start` + +Runs the app in the development mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.\ +You will also see any lint errors in the console. + +### `yarn test` + +Launches the test runner in the interactive watch mode.\ +See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `yarn build` + +Builds the app for production to the `build` folder.\ +It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.\ +Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `yarn eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). + +### Code Splitting + +This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) + +### Analyzing the Bundle Size + +This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) + +### Making a Progressive Web App + +This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) + +### Advanced Configuration + +This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) + +### Deployment + +This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) + +### `yarn build` fails to minify + +This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) diff --git a/openpbl-landing/craco.config.js b/openpbl-landing/craco.config.js new file mode 100644 index 0000000..3207ce4 --- /dev/null +++ b/openpbl-landing/craco.config.js @@ -0,0 +1,29 @@ +const CracoLessPlugin = require('craco-less'); +const path = require("path"); +const resolve = dir => path.resolve(__dirname, dir); + +module.exports = { + plugins: [ + { + plugin: CracoLessPlugin, + options: { + lessLoaderOptions: { + lessOptions: { + javascriptEnabled: true, + }, + }, + }, + }, + ], + babel: { + plugins: [ + ['import', { libraryName: 'antd', libraryDirectory: 'es', style: 'css' }], + ['@babel/plugin-proposal-decorators', { legacy: true }] + ] + }, + webpack: { + alias: { + '@': resolve("src"), + } + } +}; diff --git a/openpbl-landing/package.json b/openpbl-landing/package.json new file mode 100644 index 0000000..52d6605 --- /dev/null +++ b/openpbl-landing/package.json @@ -0,0 +1,68 @@ +{ + "name": "openpbl-landing", + "version": "0.1.0", + "private": true, + "dependencies": { + "@ant-design/icons": "^4.6.2", + "@babel/plugin-proposal-decorators": "^7.14.5", + "@craco/craco": "^6.2.0", + "@testing-library/jest-dom": "^5.11.4", + "@testing-library/react": "^11.1.0", + "@testing-library/user-event": "^12.1.10", + "ali-oss": "^6.16.0", + "antd": "^4.16.6", + "antd-img-crop": "^3.14.2", + "axios": "^0.21.1", + "babel-plugin-import": "^1.13.3", + "craco-less": "^1.18.0", + "echarts": "^5.1.2", + "echarts-for-react": "^3.0.1", + "enquire-js": "^0.2.1", + "localStorage": "^1.0.4", + "lodash": "^4.17.21", + "prop-types": "^15.7.2", + "qs": "^6.10.1", + "rc-banner-anim": "^2.4.5", + "rc-queue-anim": "^1.8.5", + "rc-scroll-anim": "^2.7.6", + "rc-tween-one": "^2.7.3", + "react": "^17.0.2", + "react-document-title": "^2.0.3", + "react-dom": "^17.0.2", + "react-github-button": "^0.1.11", + "react-lz-editor": "^0.12.1", + "react-pdf": "^5.3.2", + "react-redux": "^7.2.4", + "react-router-dom": "^5.2.0", + "react-scripts": "4.0.3", + "serve": "^12.0.0", + "web-vitals": "^1.0.1" + }, + "scripts": { + "start": "craco start", + "build": "craco build", + "test": "craco test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "rc-animate": "^3.1.1" + } +} diff --git a/openpbl-landing/public/favicon.ico b/openpbl-landing/public/favicon.ico new file mode 100644 index 0000000..a11777c Binary files /dev/null and b/openpbl-landing/public/favicon.ico differ diff --git a/openpbl-landing/public/index.html b/openpbl-landing/public/index.html new file mode 100644 index 0000000..aa069f2 --- /dev/null +++ b/openpbl-landing/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/openpbl-landing/public/logo192.png b/openpbl-landing/public/logo192.png new file mode 100644 index 0000000..fc44b0a Binary files /dev/null and b/openpbl-landing/public/logo192.png differ diff --git a/openpbl-landing/public/logo512.png b/openpbl-landing/public/logo512.png new file mode 100644 index 0000000..a4e47a6 Binary files /dev/null and b/openpbl-landing/public/logo512.png differ diff --git a/openpbl-landing/public/manifest.json b/openpbl-landing/public/manifest.json new file mode 100644 index 0000000..080d6c7 --- /dev/null +++ b/openpbl-landing/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/openpbl-landing/public/robots.txt b/openpbl-landing/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/openpbl-landing/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/openpbl-landing/src/App.js b/openpbl-landing/src/App.js new file mode 100644 index 0000000..f346277 --- /dev/null +++ b/openpbl-landing/src/App.js @@ -0,0 +1,57 @@ +import { Route, BrowserRouter, Switch } from "react-router-dom"; + +import './App.less'; +import Home from './pages/Home/index' +import Project from "./pages/Project"; +import MyProject from "./pages/Project/MyProject"; +import ProjectInfo from "./pages/Project/ProjectInfo/index"; +import PublicProject from "./pages/Project/PublicProject"; +import LearningProject from "./pages/Project/LearningProject"; +import FinishedProject from "./pages/Project/FinishedProject"; + +import AuthCallback from "./pages/User/Auth/AuthCallback"; +import Learning from "./pages/Project/LearningPage"; +import EditInfo from "./pages/Project/CreateProject/Info"; +import EditOutlined from "./pages/Project/CreateProject/Outline" +import SectionEditPage from "./pages/Project/CreateProject/Section/SectionEditPage"; +import PreviewSection from "./pages/Project/PreviewProject/PreviewSection"; +import SurveyEditPage from "./pages/Project/CreateProject/Survey/SurveyEditPage"; +import Evidence from "./pages/Project/Evidence"; + +function App() { + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} + +export default App; diff --git a/openpbl-landing/src/App.less b/openpbl-landing/src/App.less new file mode 100644 index 0000000..63026cc --- /dev/null +++ b/openpbl-landing/src/App.less @@ -0,0 +1,40 @@ +@import '~antd/dist/antd.less'; + +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/openpbl-landing/src/App.test.js b/openpbl-landing/src/App.test.js new file mode 100644 index 0000000..1f03afe --- /dev/null +++ b/openpbl-landing/src/App.test.js @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/openpbl-landing/src/api/AuthApi.js b/openpbl-landing/src/api/AuthApi.js new file mode 100644 index 0000000..996f6a7 --- /dev/null +++ b/openpbl-landing/src/api/AuthApi.js @@ -0,0 +1,26 @@ +import request from './request' +import qs from 'qs'; + +const AuthApi = { + login(code, state) { + return request({ + url: '/auth/login', + method: 'post', + data: qs.stringify({code: code, state: state}), + }) + }, + logout() { + return request({ + url: '/auth/logout', + method: 'post' + }) + }, + getAccount() { + return request({ + url: '/auth/account', + method: 'get', + }) + } +} + +export default AuthApi \ No newline at end of file diff --git a/openpbl-landing/src/api/ChapterApi.js b/openpbl-landing/src/api/ChapterApi.js new file mode 100644 index 0000000..8db3232 --- /dev/null +++ b/openpbl-landing/src/api/ChapterApi.js @@ -0,0 +1,43 @@ +import request from "./request"; +import qs from "qs"; + +const ChapterApi = { + getProjectChapters(id, studentId) { + return request({ + url: `/project/${id}/chapters`, + method: 'get', + params: {studentId: studentId} + }) + }, + createProjectChapter(chapter) { + return request({ + url: `/project/${chapter.projectId}/chapter`, + method: 'post', + data: qs.stringify(chapter) + }) + }, + updateProjectChapter(chapter) { + return request({ + url: `/project/${chapter.projectId}/chapter/${chapter.id}`, + method: 'post', + data: qs.stringify(chapter) + }) + }, + + deleteProjectChapter(c) { + return request({ + url: `/project/${c.projectId}/chapter/${c.id}/delete`, + method: 'post', + data: qs.stringify(c) + }) + }, + exchangeProjectChapter(pid, id1, id2) { + return request({ + url: `/project/${pid}/chapters/exchange`, + method: 'post', + data: qs.stringify({chapterId1: id1, chapterId2: id2}) + }) + }, +} + +export default ChapterApi \ No newline at end of file diff --git a/openpbl-landing/src/api/ProjectApi.js b/openpbl-landing/src/api/ProjectApi.js new file mode 100644 index 0000000..bd81ce1 --- /dev/null +++ b/openpbl-landing/src/api/ProjectApi.js @@ -0,0 +1,79 @@ +import request from './request' +import qs from 'qs' + +const ProjectApi = { + getProjectDetail(id) { + return request({ + url:'/project/' + id, + method: 'get' + }) + }, + createProject(data) { + return request({ + url:'/project', + method: 'post', + data: qs.stringify(data) + }) + }, + updateProject(data, id) { + return request({ + url:'/project/' + id, + method: 'post', + data: qs.stringify(data) + }) + }, + publishProject(pid) { + return request({ + url: `/project/${pid}/publish`, + method: 'post', + }) + }, + closeProject(pid) { + return request({ + url: `/project/${pid}/close`, + method: 'post', + }) + }, + deleteProject(pid) { + return request({ + url: `/project/${pid}/delete`, + method: 'post', + }) + }, + + getSectionFiles(id) { + return request({ + url:'/project/chapter/section/files/' + id, + method: 'get', + }) + }, + createSectionFile(f) { + return request({ + url:'/project/chapter/section/file', + method: 'post', + data: qs.stringify(f) + }) + }, + + getProjectStudents(pid) { + return request({ + url: `/project/${pid}/students`, + method: 'get', + }) + }, + removeStudent(pid, sid) { + return request({ + url: `/project/${pid}/remove/${sid}`, + method: 'post' + }) + }, + updateWeight(pid, data) { + return request({ + url: `project/${pid}/weight`, + method: 'post', + data: qs.stringify(data) + }) + } +} + +export default ProjectApi \ No newline at end of file diff --git a/openpbl-landing/src/api/ProjectListApi.js b/openpbl-landing/src/api/ProjectListApi.js new file mode 100644 index 0000000..c4414f7 --- /dev/null +++ b/openpbl-landing/src/api/ProjectListApi.js @@ -0,0 +1,13 @@ +import request from './request' + +const ProjectListApi = { + getUserProjectList(mode, q) { + return request({ + url: '/project-list/' + mode, + params: q, + method: 'get' + }) + }, +} + +export default ProjectListApi \ No newline at end of file diff --git a/openpbl-landing/src/api/ResourceApi.js b/openpbl-landing/src/api/ResourceApi.js new file mode 100644 index 0000000..8cbfb4a --- /dev/null +++ b/openpbl-landing/src/api/ResourceApi.js @@ -0,0 +1,35 @@ +import request from "./request"; +import qs from 'qs' + +const ResourceApi = { + getResource(id) { + return request({ + url: `/resource/${id}`, + method: 'get', + }) + }, + createResource(q) { + return request({ + url: '/resource', + method: 'post', + data: qs.stringify(q) + }) + }, + updateResource(q) { + return request({ + url: `/${q.id}/resource`, + method: 'post', + data: qs.stringify(q) + }) + }, + updateResourceContent(id, content) { + return request({ + url: `/resource/${id}`, + method: 'post', + data: qs.stringify({content: content}) + }) + }, + +} + +export default ResourceApi \ No newline at end of file diff --git a/openpbl-landing/src/api/SectionApi.js b/openpbl-landing/src/api/SectionApi.js new file mode 100644 index 0000000..d23fcd7 --- /dev/null +++ b/openpbl-landing/src/api/SectionApi.js @@ -0,0 +1,58 @@ +import request from "./request"; +import qs from "qs"; + + +const SectionApi = { + getChapterSections(item) { + return request({ + url: `/project/${item.projectId}/chapter/${item.id}/sections`, + method: 'get', + }) + }, + createChapterSection(pid, section) { + return request({ + url: `/project/${pid}/chapter/${section.chapterId}/section`, + method: 'post', + data: qs.stringify(section) + }) + }, + updateChapterSection(pid, section) { + return request({ + url: `/project/${pid}/chapter/${section.chapterId}/section/${section.id}`, + method: 'post', + data: qs.stringify(section) + }) + }, + updateSectionsMinute(pid, sections) { + return request({ + url: `/project/${pid}/sections-minute`, + method: 'post', + data: qs.stringify(sections) + }) + }, + deleteChapterSection(pid, s) { + return request({ + url: `/project/${pid}/chapter/${s.chapterId}/section/${s.id}/delete`, + method: 'post', + data: qs.stringify(s) + }) + }, + exchangeChapterSection(chapter, id1, id2) { + return request({ + url: `/project/${chapter.projectId}/chapter/${chapter.id}/sections/exchange`, + method: 'post', + data: qs.stringify({ + sectionId1: id1, + sectionId2: id2 + }) + }) + }, + getSectionDetail(id, pid) { + return request({ + url: `/project/${pid}/section/${id}`, + method: 'get', + }) + }, +} + +export default SectionApi \ No newline at end of file diff --git a/openpbl-landing/src/api/StudentApi.js b/openpbl-landing/src/api/StudentApi.js new file mode 100644 index 0000000..15fac8c --- /dev/null +++ b/openpbl-landing/src/api/StudentApi.js @@ -0,0 +1,49 @@ +import request from './request' +import qs from 'qs' + +const StudentApi = { + learnProject(pid) { + return request({ + url: '/student/learn/' + pid, + method: 'post', + }) + }, + exitProject(pid) { + return request({ + url: '/student/exit/' + pid, + method: 'post', + }) + }, + FinishedProject(sid, pid) { + return request({ + url: '/student/finished', + method: 'post', + data: qs.stringify({ + studentId: sid, + projectId: pid, + learning: false + }) + }) + }, + getLearnSection(pid, sid) { + return request({ + url: `/student/project/${pid}/section/${sid}`, + method: 'get' + }) + }, + updateLearnSection(pid, sid, data) { + return request({ + url: `/student/project/${pid}/section/${sid}`, + method: 'post', + data: qs.stringify(data) + }) + }, + getLastLearnSection(pid) { + return request({ + url: `/student/last-learn/project/${pid}`, + method: 'get' + }) + } +} + +export default StudentApi \ No newline at end of file diff --git a/openpbl-landing/src/api/SubmitApi.js b/openpbl-landing/src/api/SubmitApi.js new file mode 100644 index 0000000..717502c --- /dev/null +++ b/openpbl-landing/src/api/SubmitApi.js @@ -0,0 +1,21 @@ +import request from "./request"; +import qs from "qs"; + +const SubmitApi = { + createSubmit(pid, tid, data) { + return request({ + url: `/project/${pid}/task/${tid}/submit`, + method: 'post', + data: qs.stringify(data) + }) + }, + updateSubmit(pid, tid, sid, data) { + return request({ + url: `/project/${pid}/task/${tid}/submit/${sid}`, + method: 'post', + data: qs.stringify(data) + }) + }, +} + +export default SubmitApi diff --git a/openpbl-landing/src/api/SurveyApi.js b/openpbl-landing/src/api/SurveyApi.js new file mode 100644 index 0000000..296ee79 --- /dev/null +++ b/openpbl-landing/src/api/SurveyApi.js @@ -0,0 +1,43 @@ +import request from "./request"; +import qs from 'qs' + +const SurveyApi = { + getSurveyDetailByTaskId(pid, tid) { + return request({ + url: `/project/${pid}/task/${tid}/survey`, + method: 'get' + }) + }, + createQuestion(pid, tid, q) { + return request({ + url: `/project/${pid}/task/${tid}/survey/${q.surveyId}/question`, + method: 'post', + data: qs.stringify(q) + }) + }, + updateQuestion(pid, tid, q) { + return request({ + url: `/project/${pid}/task/${tid}/survey/${q.surveyId}/question/${q.id}`, + method: 'post', + data: qs.stringify(q) + }) + }, + deleteQuestion(pid, tid, sid, qid) { + return request({ + url: `/project/${pid}/task/${tid}/survey/${sid}/question/${qid}/delete`, + method: 'post', + }) + }, + exchangeQuestion(pid, tid, suid, id1, id2) { + return request({ + url: `/project/${pid}/task/${tid}/survey/${suid}/questions/exchange`, + method: 'post', + data: qs.stringify({ + questionId1: id1, + questionId2: id2 + }) + }) + } +} + +export default SurveyApi \ No newline at end of file diff --git a/openpbl-landing/src/api/TaskApi.js b/openpbl-landing/src/api/TaskApi.js new file mode 100644 index 0000000..49dc997 --- /dev/null +++ b/openpbl-landing/src/api/TaskApi.js @@ -0,0 +1,58 @@ +import request from "./request"; +import qs from 'qs' + +const TaskApi = { + getSectionTasks(sid, pid) { + return request({ + url: `/project/${pid}/section/${sid}/tasks`, + method: 'get', + }) + }, + getProjectTasks(pid) { + return request({ + url: `/project/${pid}/tasks`, + method: 'get', + }) + }, + getProjectTasksDetail(pid, sid) { + return request({ + url: `/project/${pid}/tasks-detail`, + method: 'get', + params: { + studentId: sid + } + }) + }, + createTask(pid, q) { + return request({ + url: `/project/${pid}/task`, + method: 'post', + data: qs.stringify(q) + }) + }, + updateTask(pid, q) { + return request({ + url: `/project/${pid}/task/${q.id}`, + method: 'post', + data: qs.stringify(q) + }) + }, + deleteTask(pid, id) { + return request({ + url: `/project/${pid}/task/${id}/delete`, + method: 'post' + }) + }, + exchangeTask(pid, id1, id2) { + return request({ + url: `/project/${pid}/tasks/exchange`, + method: 'post', + data: qs.stringify({ + taskId1: id1, + taskId2: id2 + }) + }) + } +} + +export default TaskApi \ No newline at end of file diff --git a/openpbl-landing/src/api/request.js b/openpbl-landing/src/api/request.js new file mode 100644 index 0000000..74aad84 --- /dev/null +++ b/openpbl-landing/src/api/request.js @@ -0,0 +1,26 @@ +import axios from 'axios' +import {message} from 'antd' + +const request = axios.create({ + baseURL: process.env.REACT_APP_BASE_URL, + timeout: 10000, + withCredentials: true +}) + +request.interceptors.response.use(res=>{ + if (res.data.code === 401) { + message.error(res.data.msg) + setTimeout(()=>{ + window.location.href = '/' + }, 1000) + } else if (res.data.code === 403) { + message.error(res.data.msg) + setTimeout(()=>{ + window.location.href = '/' + }, 1000) + } else { + return res + } +}) + +export default request diff --git a/openpbl-landing/src/index.css b/openpbl-landing/src/index.css new file mode 100644 index 0000000..ec2585e --- /dev/null +++ b/openpbl-landing/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/openpbl-landing/src/index.js b/openpbl-landing/src/index.js new file mode 100644 index 0000000..ef2edf8 --- /dev/null +++ b/openpbl-landing/src/index.js @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import './index.css'; +import App from './App'; +import reportWebVitals from './reportWebVitals'; + +ReactDOM.render( + + + , + document.getElementById('root') +); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/openpbl-landing/src/logo.svg b/openpbl-landing/src/logo.svg new file mode 100644 index 0000000..9dfc1c0 --- /dev/null +++ b/openpbl-landing/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/openpbl-landing/src/pages/Home/Banner.jsx b/openpbl-landing/src/pages/Home/Banner.jsx new file mode 100644 index 0000000..a143c1a --- /dev/null +++ b/openpbl-landing/src/pages/Home/Banner.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import QueueAnim from 'rc-queue-anim'; +import TweenOne from 'rc-tween-one'; +import {Button} from 'antd'; +import BannerSVGAnim from './component/BannerSVGAnim'; + +function Banner(props) { + return ( +
+ {props.isMobile && ( + +
+ banner +
+
+ )} + +
+
+
+

OpenCT

+

+ 指向5C核心素养的在线交互式高阶思维能力测评体系 +

+ + + {!props.isMobile && ( + + + + )} +
+ ); +} + +Banner.propTypes = { + isMobile: PropTypes.bool.isRequired, +}; + +export default Banner; diff --git a/openpbl-landing/src/pages/Home/Footer.jsx b/openpbl-landing/src/pages/Home/Footer.jsx new file mode 100644 index 0000000..61fb519 --- /dev/null +++ b/openpbl-landing/src/pages/Home/Footer.jsx @@ -0,0 +1,173 @@ +import React from 'react'; +import {Button, Col, Row} from 'antd'; + +function Footer() { + return ( + + ); +} + + +export default Footer; diff --git a/openpbl-landing/src/pages/Home/Page1.jsx b/openpbl-landing/src/pages/Home/Page1.jsx new file mode 100644 index 0000000..5fc94e8 --- /dev/null +++ b/openpbl-landing/src/pages/Home/Page1.jsx @@ -0,0 +1,202 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Parallax from 'rc-scroll-anim/lib/ScrollParallax'; +import TweenOne from 'rc-tween-one'; + +const {TweenOneGroup} = TweenOne; + +const featuresCN = [ + { + title: '聚焦高阶思维', + content: '基于 5C 核心素养设计', + src: 'https://gw.alipayobjects.com/zos/rmsportal/VriUmzNjDnjoFoFFZvuh.svg', + color: '#13C2C2', + shadowColor: 'rgba(19,194,194,.12)', + }, + { + title: '交互式测试', + content: '聚焦学生的过程性思维', + src: 'https://gw.alipayobjects.com/zos/rmsportal/smwQOoxCjXVbNAKMqvWk.svg', + color: '#2F54EB', + shadowColor: 'rgba(47,84,235,.12)', + }, + { + title: '情境化学习', + content: '在线的真实问题解决', + src: 'https://gw.alipayobjects.com/zos/rmsportal/hBbIHzUsSbSxrhoRFYzi.svg', + color: '#F5222D', + shadowColor: 'rgba(245,34,45,.12)', + }, + { + title: '自助组题测试模式', + content: '一线教师自助测评的组卷模式', + src: 'https://gw.alipayobjects.com/zos/rmsportal/BISfzKcCNCYFmTYcUygW.svg', + color: '#1AC44D', + shadowColor: 'rgba(26,196,77,.12)', + }, + { + title: '开放式命题系统', + content: '多元素集成的块状命题结构', + src: 'https://gw.alipayobjects.com/zos/rmsportal/XxqEexmShHOofjMYOCHi.svg', + color: '#FAAD14', + shadowColor: 'rgba(250,173,20,.12)', + }, + { + title: '即时报告反馈体系', + content: '自动化即时报告生成', + src: 'https://gw.alipayobjects.com/zos/rmsportal/JsixxWSViARJnQbAAPkI.svg', + color: '#722ED1', + shadowColor: 'rgba(114,46,209,.12)', + }, + { + title: '数据灵活管理', + content: '灵活的数据管理机制', + src: 'https://gw.alipayobjects.com/zos/rmsportal/pbmKMSFpLurLALLNliUQ.svg', + color: '#FA8C16', + shadowColor: 'rgba(250,140,22,.12)', + }, + { + title: '结构化数据分析', + content: '基于内嵌框架的自动数据分析', + src: 'https://gw.alipayobjects.com/zos/rmsportal/aLQyKyUyssIUhHTZqCIb.svg', + color: '#EB2F96', + shadowColor: 'rgba(235,45,150,.12)', + }, +]; + +const pointPos = [ + {x: -30, y: -10}, + {x: 20, y: -20}, + {x: -65, y: 15}, + {x: -45, y: 80}, + {x: 35, y: 5}, + {x: 50, y: 50, opacity: 0.2}, +]; + +class Page1 extends React.PureComponent { + static propTypes = { + isMobile: PropTypes.bool.isRequired, + } + + constructor(props) { + super(props); + this.state = { + hoverNum: null, + }; + } + + onMouseOver = (i) => { + this.setState({ + hoverNum: i, + }); + } + onMouseOut = () => { + this.setState({ + hoverNum: null, + }); + } + getEnter = (e) => { + const i = e.index; + const r = (Math.random() * 2) - 1; + const y = (Math.random() * 10) + 5; + const delay = Math.round(Math.random() * (i * 50)); + return [ + { + delay, opacity: 0.4, ...pointPos[e.index], ease: 'easeOutBack', duration: 300, + }, + { + y: r > 0 ? `+=${y}` : `-=${y}`, + duration: (Math.random() * 1000) + 2000, + yoyo: true, + repeat: -1, + }]; + } + + render() { + const {hoverNum} = this.state; + let children = [[], [], []]; + featuresCN.forEach((item, i) => { + const isHover = hoverNum === i; + const pointChild = [ + 'point-0 left', 'point-0 right', + 'point-ring', 'point-1', 'point-2', 'point-3', + ].map(className => ( + + )); + const child = ( +
  • +
    { + this.onMouseOver(i); + }} + onMouseLeave={this.onMouseOut} + > + + {(this.props.isMobile || isHover) && pointChild} + +
    + img +
    +

    {item.title}

    +

    {item.content}

    +
    +
  • + ); + children[Math.floor(i / 4)].push(child); + }); + + children = children.map((item, i) => ( +
    + {item} +
    + )); + return ( +
    +
    + {!this.props.isMobile && ( + + Feature + + )} +

    指向5C核心素养的结构体系

    +
    +
    +
    + {/**/} + {children} + {/**/} +
    +
    + ); + } +} + +export default Page1; diff --git a/openpbl-landing/src/pages/Home/Page2.jsx b/openpbl-landing/src/pages/Home/Page2.jsx new file mode 100644 index 0000000..70d8af9 --- /dev/null +++ b/openpbl-landing/src/pages/Home/Page2.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import {OverPack} from 'rc-scroll-anim'; +import QueueAnim from 'rc-queue-anim'; +import {Button} from 'antd'; + +function Page2() { + return ( +
    +
    +
    +
    +
    +

    细节功能

    + + +

    + 集成选择、填空、拖拽、可视化编程等多种题型 +

    +
    +
    + $ 可视化编程 集成块状编程系统 +
    +
    $ 嵌入拖拽组件 多模式拖拽
    +
    $ 设置选择模块 多种选择题型
    +
    $ 整合填空模块 多类型填空题
    +
    + +
    +
    +
    +
    + ); +} + +export default Page2; diff --git a/openpbl-landing/src/pages/Home/Teams2.jsx b/openpbl-landing/src/pages/Home/Teams2.jsx new file mode 100644 index 0000000..4c17dc1 --- /dev/null +++ b/openpbl-landing/src/pages/Home/Teams2.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import QueueAnim from 'rc-queue-anim'; +import {Col, Row} from 'antd'; +import OverPack from 'rc-scroll-anim/lib/ScrollOverPack'; +import {getChildrenToRender} from './utils'; + +class Teams2 extends React.PureComponent { + getBlockChildren = (data) => + data.map((item, i) => { + const {titleWrapper, image, ...$item} = item; + return ( + + + +
    + img +
    + + + + {titleWrapper.children.map(getChildrenToRender)} + + +
    + + ); + }); + + render() { + const {...props} = this.props; + const {dataSource} = props; + delete props.dataSource; + delete props.isMobile; + const listChildren = this.getBlockChildren(dataSource.block.children); + return ( +
    +
    +
    + {dataSource.titleWrapper.children.map(getChildrenToRender)} +
    + + + + {listChildren} + + + +
    +
    + ); + } +} + +export default Teams2; diff --git a/openpbl-landing/src/pages/Home/TestPage.jsx b/openpbl-landing/src/pages/Home/TestPage.jsx new file mode 100644 index 0000000..f630fb4 --- /dev/null +++ b/openpbl-landing/src/pages/Home/TestPage.jsx @@ -0,0 +1,113 @@ +import React from 'react'; +import QueueAnim from 'rc-queue-anim'; +import {TweenOneGroup} from 'rc-tween-one'; +import OverPack from 'rc-scroll-anim/lib/ScrollOverPack'; +import {Col, Row} from 'antd'; +import {page1} from './data'; + +const pointPos = [ + {x: -90, y: -20}, + {x: 35, y: -25}, + {x: -120, y: 125}, + {x: -100, y: 165}, + {x: 95, y: -5}, + {x: 90, y: 160, opacity: 0.2}, + {x: 110, y: 50}, +]; + +export default class Design extends React.PureComponent { + state = { + hoverNum: null, + } + onMouseOver = (i) => { + this.setState({ + hoverNum: i, + }); + } + + onMouseOut = () => { + this.setState({ + hoverNum: null, + }); + } + getEnter = (e) => { + const i = e.index; + const r = (Math.random() * 2) - 1; + const y = (Math.random() * 10) + 5; + const delay = Math.round(Math.random() * (i * 50)); + return [ + { + delay, opacity: 0.4, ...pointPos[e.index], ease: 'easeOutBack', duration: 300, + }, + { + y: r > 0 ? `+=${y}` : `-=${y}`, + duration: (Math.random() * 1000) + 2000, + yoyo: true, + repeat: -1, + }]; + } + + render() { + const {hoverNum} = this.state; + const {isMobile} = this.props; + const children = page1.children.map((item, i) => { + const isHover = hoverNum === i; + const pointChild = [ + 'point-ring left', 'point-ring point-ring-0 right', + 'point-0', 'point-2', 'point-1', 'point-3', 'point-2', + ].map((className, ii) => ( + + )); + return ( + + { + this.onMouseOver(i); + }} + onMouseLeave={this.onMouseOut} + > + + {(isHover || isMobile) && pointChild} + +
    + img +
    +
    {item.title}
    +

    {item.content}

    +
    + ); + }); + return ( +
    +
    +

    {page1.title}

    + {/**/} +
    +
    +
    + + + {children} + + +
    +
    ); + } +} diff --git a/openpbl-landing/src/pages/Home/component/BannerSVGAnim.jsx b/openpbl-landing/src/pages/Home/component/BannerSVGAnim.jsx new file mode 100644 index 0000000..77e4951 --- /dev/null +++ b/openpbl-landing/src/pages/Home/component/BannerSVGAnim.jsx @@ -0,0 +1,727 @@ +/* eslint-disable */ +import React from 'react'; +import TweenOne from 'rc-tween-one'; +import SvgDrawPlugin from 'rc-tween-one/lib/plugin/SvgDrawPlugin'; + +TweenOne.plugins.push(SvgDrawPlugin); + +const animate = { + scale: { + scale: 0, + opacity: 0, + type: 'from', + ease: 'easeOutQuad', + }, + alpha: { + opacity: 0, + type: 'from', + ease: 'easeOutQuad', + }, + y: { + y: 30, + opacity: 0, + type: 'from', + ease: 'easeOutQuad', + }, + y2: { + y: -30, + opacity: 0, + type: 'from', + ease: 'easeOutQuad', + }, + x: { + x: 30, + opacity: 0, + type: 'from', + ease: 'easeOutQuad', + }, + x2: { + x: -30, + opacity: 0, + type: 'from', + ease: 'easeOutQuad', + }, + draw: { + SVGDraw: 0, + type: 'from', + ease: 'easeOutQuad', + }, + loop: { + yoyo: true, + repeat: -1, + duration: 2500, + }, +}; + +export default function () { + // safari 下取不到 transform 值,,所有动画在外层增加 g 标签。 + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/openpbl-landing/src/pages/Home/data.js b/openpbl-landing/src/pages/Home/data.js new file mode 100644 index 0000000..a0a5c49 --- /dev/null +++ b/openpbl-landing/src/pages/Home/data.js @@ -0,0 +1,222 @@ +export const header = [ + { + title: '产品', + children: [ + { + title: '云凤蝶', desc: '移动建站平台', img: 'https://gw.alipayobjects.com/zos/rmsportal/fLPzRmwAurHkPDVfHHiQ.svg', link: 'https://fengdie.alipay-eco.com/intro', top: '2px', + }, + ], + }, + { + title: '设计体系', + children: [ + { + title: '设计价值观', desc: 'Design Values', img: 'https://gw.alipayobjects.com/zos/rmsportal/zMeJnhxAtpXPZAUhUKJH.svg', link: 'https://ant.design/docs/spec/values-cn', + }, + { + title: '视觉', desc: 'Visual', img: 'https://gw.alipayobjects.com/zos/rmsportal/qkNZxQRDqvFJscXVDmKp.svg', link: 'https://ant.design/docs/spec/colors-cn', + }, + { + title: '可视化', desc: 'Visualisation', img: 'https://gw.alipayobjects.com/zos/rmsportal/MrUQjZNOJhYJCSZZuJDr.svg', link: 'https://antv.alipay.com/zh-cn/vis/index.html', + }, + ], + }, + { + title: '技术方案', + children: [ + { + title: 'Ant Design', desc: '蚂蚁 UI 体系', img: 'https://gw.alipayobjects.com/zos/rmsportal/ruHbkzzMKShUpDYMEmHM.svg', link: 'https://ant.design', + }, + { + title: 'AntV', desc: '蚂蚁数据可视化解决方案', img: 'https://gw.alipayobjects.com/zos/rmsportal/crqUoMinEgjMeGGFAKzG.svg', link: 'https://antv.alipay.com', + }, + { + title: 'Egg', desc: '企业级 Node 开发框架', img: 'https://gw.alipayobjects.com/zos/rmsportal/nEEwwpmNVihZimnBAtMf.svg', link: 'https://eggjs.org', + }, + ], + }, + { + title: '关于', + children: [ + { + title: '蚂蚁金服体验科技专栏', desc: '探索极致用户体验与最佳工程实践', img: 'https://gw.alipayobjects.com/zos/rmsportal/VsVqfjYxPTJaFbPcZqMb.svg', link: 'https://zhuanlan.zhihu.com/xtech', + }, + ], + }, +]; +export const banner = [ + { + img: 'https://gw.alipayobjects.com/zos/rmsportal/cTyLQiaRrpzxFAuWwoDQ.svg', + imgMobile: 'https://gw.alipayobjects.com/zos/rmsportal/ksMYqrCyhwQNdBKReFIU.svg', + className: 'seeconf-wrap', + children: [ + { children: 'Seeking Experience & Engineering Conference', className: 'seeconf-en-name' }, + { children: '首届蚂蚁体验科技大会', className: 'seeconf-title', tag: 'h1' }, + { children: '探索极致用户体验与最佳工程实践', className: 'seeconf-cn-name' }, + { + children: '了解详细', + className: 'banner-button', + tag: 'button', + link: 'https://seeconf.alipay.com/', + }, + { children: '2018.01.06 / 中国·杭州', className: 'seeconf-time' }, + ], + }, + { + img: 'https://gw.alipayobjects.com/zos/rmsportal/cTyLQiaRrpzxFAuWwoDQ.svg', + imgMobile: 'https://gw.alipayobjects.com/zos/rmsportal/ksMYqrCyhwQNdBKReFIU.svg', + className: 'seeconf-wrap', + children: [ + { children: 'Seeking Experience & Engineering Conference', className: 'seeconf-en-name' }, + { children: '首届蚂蚁体验科技大会', className: 'seeconf-title', tag: 'h1' }, + { children: '探索极致用户体验与最佳工程实践', className: 'seeconf-cn-name' }, + { + children: '了解详细', + className: 'banner-button', + tag: 'button', + link: 'https://seeconf.alipay.com/', + }, + { children: '2018.01.06 / 中国·杭州', className: 'seeconf-time' }, + ], + }, +]; +export const page1 = { + title: '高阶思维能力测试版块', + children: [ + { + title: '数学建模', + content: 'Computational Thinking', + src: 'https://gw.alipayobjects.com/zos/rmsportal/vUxYuDdsbBBcMDxSGmwc.svg', + color: '#EB2F96', + shadowColor: 'rgba(166, 55, 112, 0.08)', + link: '/ticket-login-card', + }, + { + title: '智能计算思维', + content: 'Culture Competency', + src: 'https://gw.alipayobjects.com/zos/rmsportal/qIcZMXoztWjrnxzCNTHv.svg', + color: '#1890FF', + shadowColor: 'rgba(15, 93, 166, 0.08)', + link: 'https://ant.design/docs/spec/colors-cn', + }, + { + title: '文化理解与创新', + content: 'Critical Thinking', + src: 'https://gw.alipayobjects.com/zos/rmsportal/eLtHtrKjXfabZfRchvVT.svg', + color: '#AB33F7', + shadowColor: 'rgba(112, 73, 166, 0.08)', + link: 'https://antv.alipay.com/zh-cn/vis/index.html', + }, + { + title: '跨学科问题解决', + content: 'Creativity', + src: 'https://gw.alipayobjects.com/zos/rmsportal/QCcDSfdbCIbVSsUZJaQK.svg', + color: '#EB2F96', + shadowColor: 'rgba(166, 55, 112, 0.08)', + link: 'https://ant.design/docs/spec/values-cn', + }, + { + title: '审辩阅读', + content: 'Communication', + src: 'https://gw.alipayobjects.com/zos/rmsportal/hMSnSxMzmiGSSIXxFtNf.svg', + color: '#fadb14', + shadowColor: 'rgba(191, 188, 21, 0.08)', + link: 'https://ant.design/docs/spec/colors-cn', + }, + { + title: '创造力倾向', + content: 'Collaboration', + src: 'https://gw.alipayobjects.com/zos/rmsportal/OMEOieDFPYDcWXMpqqzd.svg', + color: '#2f54eb', + shadowColor: 'rgba(73, 101, 166, 0.08)', + link: 'https://antv.alipay.com/zh-cn/vis/index.html', + }, + ], +}; + +export const page3 = { + title: '大家都喜爱的产品', + children: [ + { + img: 'https://gw.alipayobjects.com/zos/rmsportal/iVOzVyhyQkQDhRsuyBXC.svg', + imgMobile: 'https://gw.alipayobjects.com/zos/rmsportal/HxEfljPlykWElfhidpxR.svg', + src: 'https://gw.alipayobjects.com/os/rmsportal/gCFHQneMNZMMYEdlHxqK.mp4', + }, + { + img: 'https://gw.alipayobjects.com/zos/rmsportal/iVOzVyhyQkQDhRsuyBXC.svg', + imgMobile: 'https://gw.alipayobjects.com/zos/rmsportal/HxEfljPlykWElfhidpxR.svg', + src: 'https://gw.alipayobjects.com/os/rmsportal/gCFHQneMNZMMYEdlHxqK.mp4', + }, + ], +}; + +export const page4 = { + title: '众多企业正在使用', + children: [ + 'https://gw.alipayobjects.com/zos/rmsportal/qImQXNUdQgqAKpPgzxyK.svg', // 阿里巴巴 + 'https://gw.alipayobjects.com/zos/rmsportal/LqRoouplkwgeOVjFBIRp.svg', // 蚂蚁金服 + 'https://gw.alipayobjects.com/zos/rmsportal/TLCyoAagnCGXUlbsMTWq.svg', // 人民网 + 'https://gw.alipayobjects.com/zos/rmsportal/HmCGMKcJQMwfPLNCIhOH.svg', // cisco + 'https://gw.alipayobjects.com/zos/rmsportal/aqldfFDDqRVFRxqLUZOk.svg', // GrowingIO + 'https://gw.alipayobjects.com/zos/rmsportal/rqNeEFCGFuwiDKHaVaPp.svg', // 饿了么 + 'https://gw.alipayobjects.com/zos/rmsportal/FdborlfwBxkWIqKbgRtq.svg', // 滴滴出行 + 'https://gw.alipayobjects.com/zos/rmsportal/coPmiBkAGVTuTNFVRUcg.png', // 飞凡网 + ], +}; + +export const footer = [ + { + title: '蚂蚁科技', + children: [ + { title: '蚂蚁金服开放平台', link: 'https://open.alipay.com' }, + { title: '蚂蚁体验云', link: 'https://xcloud.alipay.com' }, + { title: '蚂蚁金融云', link: 'https://www.cloud.alipay.com' }, + ], + }, + { + title: '相关会议', + children: [ + { title: 'ATEC', link: 'https://atec.antfin.com' }, + { title: 'SEE Conf', link: 'https://seeconf.alipay.com' }, + ], + }, + { + title: '联系我们', + children: [ + { title: '蚂蚁金服体验科技专栏', link: 'https://zhuanlan.zhihu.com/xtech' }, + { title: '蚂蚁金服体验科技官微', link: 'https://weibo.com/p/1005056420205486' }, + { title: 'AntV 官微', link: 'https://weibo.com/antv2017' }, + { title: 'Ant Design 专栏', link: 'https://zhuanlan.zhihu.com/antdesign' }, + ], + }, + { + title: '蚂蚁体验云', + icon: 'https://gw.alipayobjects.com/zos/rmsportal/wdarlDDcdCaVoCprCRwB.svg', + children: [ + { title: 'Ant Design', desc: '蚂蚁 UI 体系', link: 'https://ant.design' }, + { title: 'AntV', desc: '蚂蚁数据可视化方案', link: 'https://antv.alipay.com' }, + // { title: 'AntG', desc: '蚂蚁互动图形技术', link: 'http://antg.alipay.net' }, + { title: 'Egg', desc: '企业级 Node Web 开发框架', link: 'https://eggjs.org' }, + { title: '云凤蝶', desc: '移动建站平台', link: 'https://fengdie.alipay-eco.com/intro' }, + ], + }, +]; + +// 图处预加载; +if (typeof document !== 'undefined') { + const div = document.createElement('div'); + div.style.display = 'none'; + document.body.appendChild(div); + [ + 'https://gw.alipayobjects.com/zos/rmsportal/KtRzkMmxBuWCVjPbBgRY.svg', + 'https://gw.alipayobjects.com/zos/rmsportal/qIcZMXoztWjrnxzCNTHv.svg', + 'https://gw.alipayobjects.com/zos/rmsportal/eLtHtrKjXfabZfRchvVT.svg', + 'https://gw.alipayobjects.com/zos/rmsportal/iVOzVyhyQkQDhRsuyBXC.svg', + 'https://gw.alipayobjects.com/zos/rmsportal/HxEfljPlykWElfhidpxR.svg', + 'https://gw.alipayobjects.com/zos/rmsportal/wdarlDDcdCaVoCprCRwB.svg', + ].concat(page4.children).forEach((src) => { + const img = new Image(); + img.src = src; + div.appendChild(img); + }); +} diff --git a/openpbl-landing/src/pages/Home/data.source.js b/openpbl-landing/src/pages/Home/data.source.js new file mode 100644 index 0000000..6105d61 --- /dev/null +++ b/openpbl-landing/src/pages/Home/data.source.js @@ -0,0 +1,276 @@ + +export const Teams20DataSource = { + wrapper: { className: 'home-page-wrapper teams2-wrapper' }, + page: { className: 'home-page teams2' }, + OverPack: { playScale: 0.3, className: '' }, + titleWrapper: { + className: 'title-wrapper', + children: [{ name: 'title', children: '团队成员' }], + }, + block: { + className: 'block-wrapper', + gutter: 72, + children: [ + { + name: 'block0', + className: 'block', + md: 8, + xs: 24, + image: { + name: 'image', + className: 'teams2-image', + children: + 'https://gw.alipayobjects.com/mdn/rms_ae7ad9/afts/img/A*--rVR4hclJYAAAAAAAAAAABjARQnAQ', + }, + titleWrapper: { + className: 'teams2-textWrapper', + children: [ + { + name: 'title', + className: 'teams2-title', + children: '谢志勇', + }, + { + name: 'content', + className: 'teams2-job', + children: '2018级博士生', + }, + { + name: 'content1', + className: 'teams2-content', + children: '赣南师范大学研究生导师', + }, + ], + }, + }, + { + name: 'block1', + className: 'block', + md: 8, + xs: 24, + image: { + name: 'image', + className: 'teams2-image', + children: + 'https://gw.alipayobjects.com/mdn/rms_ae7ad9/afts/img/A*njqxS5Ky7CQAAAAAAAAAAABjARQnAQ', + }, + titleWrapper: { + className: 'teams2-textWrapper', + children: [ + { + name: 'title', + className: 'teams2-title', + children: '罗海风', + }, + { + name: 'content', + className: 'teams2-job', + children: '2017级博士生', + }, + { + name: 'content1', + className: 'teams2-content', + children: '高阶思维能力测试组成员', + }, + ], + }, + }, + { + name: 'block2', + className: 'block', + md: 8, + xs: 24, + image: { + name: 'image', + className: 'teams2-image', + children: + 'https://gw.alipayobjects.com/mdn/rms_ae7ad9/afts/img/A*--rVR4hclJYAAAAAAAAAAABjARQnAQ', + }, + titleWrapper: { + className: 'teams2-textWrapper', + children: [ + { name: 'title', className: 'teams2-title', children: '刘启蒙' }, + { + name: 'content', + className: 'teams2-job', + children: '中国教育创新研究院教师', + }, + { + name: 'content1', + className: 'teams2-content', + children: '高阶思维能力测试组成员', + }, + ], + }, + }, + { + name: 'block3', + className: 'block', + md: 8, + xs: 24, + image: { + name: 'image', + className: 'teams2-image', + children: + 'https://gw.alipayobjects.com/mdn/rms_ae7ad9/afts/img/A*--rVR4hclJYAAAAAAAAAAABjARQnAQ', + }, + titleWrapper: { + className: 'teams2-textWrapper', + children: [ + { name: 'title', className: 'teams2-title', children: '王田' }, + { + name: 'content', + className: 'teams2-job', + children: '2020级博士生', + }, + { + name: 'content1', + className: 'teams2-content', + children: '高阶思维能力测试组成员', + }, + ], + }, + }, + { + name: 'block4', + className: 'block', + md: 8, + xs: 24, + image: { + name: 'image', + className: 'teams2-image', + children: + 'https://gw.alipayobjects.com/mdn/rms_ae7ad9/afts/img/A*njqxS5Ky7CQAAAAAAAAAAABjARQnAQ', + }, + titleWrapper: { + className: 'teams2-textWrapper', + children: [ + { name: 'title', className: 'teams2-title', children: '董晓舒' }, + { + name: 'content', + className: 'teams2-job', + children: '2019级博士生', + }, + { + name: 'content1', + className: 'teams2-content', + children: '高阶思维能力测试组成员', + }, + ], + }, + }, + { + name: 'block5', + className: 'block', + md: 8, + xs: 24, + image: { + name: 'image', + className: 'teams2-image', + children: + 'https://gw.alipayobjects.com/mdn/rms_ae7ad9/afts/img/A*--rVR4hclJYAAAAAAAAAAABjARQnAQ', + }, + titleWrapper: { + className: 'teams2-textWrapper', + children: [ + { name: 'title', className: 'teams2-title', children: '王佳敏' }, + { + name: 'content', + className: 'teams2-job', + children: '区域教育质量健康体检项目组老师', + }, + { + name: 'content1', + className: 'teams2-content', + children: '高阶思维能力测试组成员', + }, + ], + }, + }, + { + name: 'block6', + className: 'block', + md: 8, + xs: 24, + image: { + name: 'image', + className: 'teams2-image', + children: + 'https://gw.alipayobjects.com/mdn/rms_ae7ad9/afts/img/A*--rVR4hclJYAAAAAAAAAAABjARQnAQ', + }, + titleWrapper: { + className: 'teams2-textWrapper', + children: [ + { name: 'title', className: 'teams2-title', children: '赵萍萍' }, + { + name: 'content', + className: 'teams2-job', + children: '北京师范大学博士后', + }, + { + name: 'content1', + className: 'teams2-content', + children: '高阶思维能力测试组成员', + }, + ], + }, + }, + { + name: 'block7', + className: 'block', + md: 8, + xs: 24, + image: { + name: 'image', + className: 'teams2-image', + children: + 'https://gw.alipayobjects.com/mdn/rms_ae7ad9/afts/img/A*njqxS5Ky7CQAAAAAAAAAAABjARQnAQ', + }, + titleWrapper: { + className: 'teams2-textWrapper', + children: [ + { name: 'title', className: 'teams2-title', children: '胡梦玥' }, + { + name: 'content', + className: 'teams2-job', + children: '区域教育质量健康体检项目组老师', + }, + { + name: 'content1', + className: 'teams2-content', + children: '高阶思维能力测试组成员', + }, + ], + }, + }, + { + name: 'block8', + className: 'block', + md: 8, + xs: 24, + image: { + name: 'image', + className: 'teams2-image', + children: + 'https://gw.alipayobjects.com/mdn/rms_ae7ad9/afts/img/A*--rVR4hclJYAAAAAAAAAAABjARQnAQ', + }, + titleWrapper: { + className: 'teams2-textWrapper', + children: [ + { name: 'title', className: 'teams2-title', children: '董瑶瑶' }, + { + name: 'content', + className: 'teams2-job', + children: '17级硕士生', + }, + { + name: 'content1', + className: 'teams2-content', + children: '高阶思维能力测试组成员', + }, + ], + }, + }, + ], + }, +}; diff --git a/openpbl-landing/src/pages/Home/index.jsx b/openpbl-landing/src/pages/Home/index.jsx new file mode 100644 index 0000000..ae11510 --- /dev/null +++ b/openpbl-landing/src/pages/Home/index.jsx @@ -0,0 +1,54 @@ +import React from 'react'; +import DocumentTitle from 'react-document-title'; +import {enquireScreen} from 'enquire-js'; + +import GlobalHeader from '../component/GlobalHeader/GlobalHeader'; +import Banner from './Banner'; +import Page1 from './Page1'; +import Page2 from './Page2'; +import Footer from './Footer'; +import './static/style'; +import TestPage from "./TestPage"; +import Teams2 from "./Teams2"; +import {Teams20DataSource} from './data.source'; + + +let isMobile; + +enquireScreen((b) => { + isMobile = b; +}); + +class Home extends React.PureComponent { + state = { + isMobile, + } + + componentDidMount() { + enquireScreen((b) => { + this.setState({ + isMobile: !!b, + }); + }); + } + + render() { + return ( + +
    + +
    + + + + + +
    +
    +
    +
    + ); + } +} + +export default Home; diff --git a/openpbl-landing/src/pages/Home/static/custom.less b/openpbl-landing/src/pages/Home/static/custom.less new file mode 100644 index 0000000..02b5dcd --- /dev/null +++ b/openpbl-landing/src/pages/Home/static/custom.less @@ -0,0 +1,10 @@ +body { + -webkit-font-smoothing: antialiased; +} + +.text-center { + text-align: center!important; +} +a:focus { + text-decoration: none; +} diff --git a/openpbl-landing/src/pages/Home/static/default.less b/openpbl-landing/src/pages/Home/static/default.less new file mode 100644 index 0000000..0973641 --- /dev/null +++ b/openpbl-landing/src/pages/Home/static/default.less @@ -0,0 +1,6 @@ +@import "~antd/lib/style/themes/default.less"; +@site-heading-color: #0d1a26; +@site-text-color: #314659; +@site-text-color-secondary: #697b8c; +@site-border-color-split: #ebedf0; +@border-color: rgba(229, 231, 235, 100); diff --git a/openpbl-landing/src/pages/Home/static/footer.less b/openpbl-landing/src/pages/Home/static/footer.less new file mode 100644 index 0000000..b4046f9 --- /dev/null +++ b/openpbl-landing/src/pages/Home/static/footer.less @@ -0,0 +1,86 @@ +@import './default'; + +@padding-space: 114px; + +footer.dark { + background-color: #000; + color: rgba(255, 255, 255, 0.65); + a { + color: #fff; + } + h2 { + color: rgba(255, 255, 255, 1); + & > span { + color: rgba(255, 255, 255, 1); + } + } + .bottom-bar { + border-top: 1px solid rgba(255, 255, 255, 0.25); + overflow: hidden; + } +} + +footer { + border-top: 1px solid @border-color; + clear: both; + font-size: 12px; + background: #fff; + position: relative; + z-index: 100; + color: @site-text-color; + box-shadow: 0 1000px 0 1000px #fff; + .ant-row { + text-align: center; + .footer-center { + display: inline-block; + text-align: left; + > h2 { + font-size: 14px; + margin: 0 auto 24px; + font-weight: 500; + position: relative; + > .anticon { + font-size: 16px; + position: absolute; + left: -22px; + top: 3px; + color: #aaa; + } + } + > div { + margin: 12px 0; + } + } + } + .footer-wrap { + position: relative; + padding: 86px @padding-space 70px @padding-space; + } + .bottom-bar { + border-top: 1px solid @border-color; + text-align: right; + padding: 20px @padding-space; + margin: 0; + line-height: 24px; + a { + color: rgba(255, 255, 255, 0.65); + &:hover { + color: #fff; + } + } + .translate-button { + text-align: left; + width: 200px; + margin: 0 auto; + } + } + .footer-logo { + position: relative; + top: -2px; + } + .footer-flag { + position: relative; + top: -4px; + margin-right: 8px; + } +} diff --git a/openpbl-landing/src/pages/Home/static/header.less b/openpbl-landing/src/pages/Home/static/header.less new file mode 100644 index 0000000..d146983 --- /dev/null +++ b/openpbl-landing/src/pages/Home/static/header.less @@ -0,0 +1,78 @@ +@import './default.less'; +@import "~antd/lib/style/mixins/clearfix.less"; + +@header-height: 80px; + +#header { + background-color: #fff; + position: relative; + z-index: 10; + height: 64px; +} + +#logo { + overflow: hidden; + padding-left: 40px; + float: left; + line-height: 64px; + text-decoration: none; + height: 64px; + img { + display: inline; + vertical-align: middle; + margin-right: 16px; + height: 40px; + } + span { + color: @primary-color; + outline: none; + font-size: 14px; + line-height: 28px; + } +} + +.header-meta { + .clearfix(); + padding-right: 40px; +} + +#menu { + float: right; + overflow: hidden; + height: 64px; + .ant-menu { + line-height: 60px; + } + .ant-menu-horizontal { + border-bottom: none; + & > .ant-menu-item { + border-top: 2px solid transparent; + &:hover { + border-top: 2px solid @primary-color; + border-bottom: 2px solid transparent; + } + } + & > .ant-menu-item-selected { + border-top: 2px solid @primary-color; + border-bottom: 2px solid transparent; + a { + color: @primary-color; + } + } + } +} + +#preview { + padding-top: 17px; + float: right; + margin-left: 32px; + button { + border-radius: 32px; + } +} + +#preview-button { + .ant-btn { + color: @site-text-color; + } +} diff --git a/openpbl-landing/src/pages/Home/static/home.less b/openpbl-landing/src/pages/Home/static/home.less new file mode 100644 index 0000000..31ae5d7 --- /dev/null +++ b/openpbl-landing/src/pages/Home/static/home.less @@ -0,0 +1,508 @@ +@import './default.less'; + +html, +body { + height: 100%; +} + +body { + font-family: @font-family; + line-height: 1.5; + color: @site-text-color; + font-size: 14px; + background: #fff; + transition: background 1s cubic-bezier(0.075, 0.82, 0.165, 1); + overflow-x: hidden; +} + +a { + transition: color .3s ease; + &:focus { + text-decoration: underline; + text-decoration-skip: ink; + } +} + +.home-wrapper { + width: 100%; + color: #697b8c; + .ant-btn { + min-width: 110px; + height: 40px; + border-radius: 20px; + font-size: 16px; + &:hover { + transform: translateY(-4px); + box-shadow: 0 4px 12px rgba(24, 144, 255, .4); + } + } +} + +svg g { + transform-origin: 50% 50%; + transform-box: fill-box; +} + +.banner-wrapper { + height: 526px; + width: 100%; + max-width: 1500px; + margin: auto; + position: relative; + .banner-title-wrapper { + width: 40%; + max-width: 480px; + height: 245px; + position: absolute; + top: 0; + bottom: 0; + left: 8%; + margin: auto; + z-index: 1; + > * { + will-change: transform; + } + h1 { + font-family: "Futura", "Helvetica Neue For Number", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 54px; + margin: 12px 0; + } + p { + font-size: 20px; + } + .button-wrapper { + margin-top: 64px; + line-height: 40px; + align-items: center; + display: flex; + .github-btn { + display: inline-block; + height: 28px; + .gh-btn { + height: 28px; + border-radius: 4px; + background: rgba(243, 243, 243, 1); + background: linear-gradient(to bottom, rgba(255, 255, 255, 1) 0%, rgba(243, 243, 243, 1) 100%); + border: 1px solid #ebedf0; + align-items: center; + display: flex; + padding: 0 12px; + font-size: 13px; + &:hover { + color: @primary-color; + } + .gh-ico { + margin-right: 8px; + } + } + .gh-count { + height: 28px; + line-height: 22px; + background: #fff; + border: 1px solid #ebedf0; + border-radius: 4px; + padding: 2px 8px; + font-size: 13px; + } + } + } + .title-line { + transform: translateX(-64px); + animation: bannerTitleLine 3s ease-in-out 0s infinite; + } + } + .banner-image-wrapper { + width: 45%; + max-width: 598px; + height: 324px; + position: absolute; + right: 8%; + margin: auto; + top: 0; + bottom: 0; + opacity: 0; + } +} + +.home-banner-image { + display: none; +} + +.title-line-wrapper { + height: 2px; + width: 100%; + overflow: hidden; + .title-line { + height: 100%; + width: 64px; + transform: translateX(-64px); + background: linear-gradient(to right, rgba(24, 144, 255, 0) 0%, rgba(24, 144, 255, 1) 100%); + } +} + +.home-page { + margin: 50px auto; + h2 { + margin: 0 auto 32px; + font-size: 38px; + line-height: 46px; + color: #0d1a26; + font-weight: 400; + text-align: center; + } +} + +.home-page-wrapper { + max-width: 1280px; + width: 100%; + margin: auto; + position: relative; +} + +/** page1 **/ +.page1 { + height: 1060px; +} +.page1-line.title-line-wrapper { + width: 312px; + margin: 24px auto 76px; + .title-line { + animation: page1TitleLine 3s ease-in-out 1.5s infinite; + } +} + +.page1-bg { + font-size: 320px; + color: #ebedf0; + position: absolute; + width: 100%; + text-align: center; + opacity: .25; + top: 0; + transform: translateY(960px); +} + +.page1-box-wrapper { + margin-bottom: 62px; + display: flex; + align-items: flex-start; + li { + width: 33.33%; + display: inline-block; + will-change: transform; + .page1-box { + width: 194px; + margin: auto; + text-align: center; + position: relative; + .page1-image { + width: 80px; + height: 80px; + border-radius: 40px; + margin: 20px auto 32px; + position: relative; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + background: #fff; + transition: box-shadow .3s ease-out, transform .3s ease-out; + } + &:hover .page1-image { + transform: translateY(-5px); + } + h3 { + color: #0d1a26; + font-size: 16px; + margin: 8px auto; + } + } + } +} +.page1-point-wrapper { + position: absolute; + width: 0; + left: 50%; + top: 0; + .point-0 { + width: 4px; + height: 4px; + } + .point-2, .point-ring { + width: 10px; + height: 10px; + } + .point-ring { + border-style: solid; + border-width: 1px; + background: transparent !important; + } + .point-1 { + width: 6px; + height: 6px; + } + .point-3 { + width: 15px; + height: 15px; + } + i { + display: inline-block; + border-radius: 100%; + position: absolute; + opacity: 0; + transform: translate(0, 30px); + } +} +/** page2 **/ +.page2 { + text-align: center; + height: 588px; + + .page2-content { + will-change: transform; + } +} + +.page2-line { + margin: 148px auto 24px; + width: 114px; + + .title-line { + animation: page2TitleLine 3s ease-in-out 0s infinite; + } +} + +.page-content { + width: 760px; + margin: 24px auto 32px; + line-height: 28px; +} + +.home-code { + width: 90%; + max-width: 840px; + border-radius: 4px; + background: #f2f4f5; + line-height: 28px; + margin: 16px auto; + color: #151e26; + font-size: 16px; + font-family: @code-family; + text-align: left; + padding: 20px 50px; + span { + color: #f5222d; + } +} + +.home-code-comment { + color: @site-text-color-secondary !important; +} + +@keyframes bannerTitleLine { + 0%, 25% { + transform: translateX(-64px); + } + 75%, 100% { + transform: translateX(544px); + } +} + +@keyframes page1TitleLine { + 0%, 25% { + transform: translateX(-64px); + } + 75%, 100% { + transform: translateX(376px); + } +} + +@keyframes page2TitleLine { + 0%, 25% { + transform: translateX(-64px); + } + 75%, 100% { + transform: translateX(178px); + } +} + + + + + + + + + + + + + + + + + + + + +@import "./default.less"; +@import "./custom.less"; +.page { + &-wrapper { + width: 100%; + will-change: transform; + } + width: 100%; + max-width: 1200px; + padding: 0 24px; + margin: auto; + overflow: hidden; + >h1 { + margin: 144px auto 32px; + font-size: 38px; + line-height: 46px; + color: #0d1a26; + font-weight: 400; + text-align: center; + } + >i { + width: 64px; + margin: auto; + height: 2px; + display: block; + background: rgb(22, 217, 227); + background: linear-gradient(to right, rgba(22, 217, 227, 1) 0%, rgba(22, 119, 227, 1) 100%); + } +} + +.banner-anim { + &-arrow { + opacity: 0; + transition: opacity .3s @ease-out; + } + &:hover { + .banner-anim-arrow { + opacity: 1; + } + } +} + +.banner { + background: url(https://gw.alipayobjects.com/zos/rmsportal/okhVRKJXxQpbpKGtKneS.svg) no-repeat center top; + background-size: contain; + overflow: hidden; + font-family: PingFang SC, Helvetica Neue For Number, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif; + .page { + max-width: 1248px; + } + .logo { + background: url(https://gw.alipayobjects.com/zos/rmsportal/khXpcyRYlACLokoNTzwc.svg) no-repeat; + width: 127px; + height: 110px; + margin: 86px auto 40px; + } + & &-anim { + margin: 0 auto 64px; + height: 600px; + box-shadow: 0 20px 32px rgba(1, 4, 8, .12); + background: #fff; + border-radius: 8px; + &-elem { + .banner-bg { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + overflow: hidden; + background-position: center; + background-repeat: no-repeat; + } + } + } + &-button { + .ant-btn { + color: #fff; + border: none; + } + } +} + +.page1 { + min-height: 720px; + .page { + >h1 { + margin-top: 64px; + } + } + &-item { + text-align: center; + margin-top: 96px; + &-link { + display: block; + width: 180px; + margin: auto; + } + &-img { + width: 180px; + height: 180px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 100%; + background: #fff; + position: relative; + z-index: 1; + } + &-title { + margin-top: 56px; + font-size: 20px; + color: #0D1A26; + } + p { + color: #697B8C; + margin-top: 8px; + } + } + .point-wrapper { + position: absolute; + width: 0; + left: 50%; + top: 0; + z-index: 0; + .point-0 { + width: 4px; + height: 4px; + } + .point-2 { + width: 10px; + height: 10px; + } + .point-ring { + width: 12px; + height: 12px; + border-style: solid; + border-width: 1px; + background: transparent !important; + } + .point-ring-0 { + width: 4px; + height: 4px; + } + .point-1 { + width: 12px; + height: 12px; + } + .point-3 { + width: 21px; + height: 21px; + } + i { + display: inline-block; + border-radius: 100%; + position: absolute; + opacity: 0; + transform: translate(0, 30px); + } + } +} diff --git a/openpbl-landing/src/pages/Home/static/responsive.less b/openpbl-landing/src/pages/Home/static/responsive.less new file mode 100644 index 0000000..a14ce20 --- /dev/null +++ b/openpbl-landing/src/pages/Home/static/responsive.less @@ -0,0 +1,331 @@ +@import './default.less'; + +.nav-phone-icon { + display: none; + position: absolute; + right: 30px; + top: 25px; + z-index: 1; + width: 16px; + height: 22px; + cursor: pointer; +} + +@media only screen and (min-width: 1440px) and (max-width: 1599px) { + .main-wrapper > .ant-row > .ant-col-xl-5 { + width: 274px; + } + + #header .ant-row .ant-col-xl-5 { + width: 274px; + } +} + +@media only screen and (max-width: 1300px) { + #search-box { + display: none; + } +} + +@media only screen and (max-width: @screen-xl) { + #logo { + padding: 0 40px; + } + .banner-wrapper .banner-title-wrapper { + h1 { + font-size: 36px; + } + p { + font-size: 16px; + } + } +} + +@media only screen and (max-width: @screen-lg) { + .code-boxes-col-2-1, .code-boxes-col-1-1 { + float: none; + width: 100%; + } + + .preview-image-boxes { + margin: 0 !important; + float: none; + width: 100%; + } + + .preview-image-box { + padding-left: 0; + margin: 10px 0; + } + + .banner-entry { + position: relative; + top: 30px; + left: 0; + text-align: center; + } + + .image-wrapper { + display: none; + } + + .banner-wrapper { + background-position: 40%; + } + + .content-wrapper .text-wrapper { + float: none; + text-align: center; + left: 0; + width: 100%; + padding: 0; + > p { + max-width: 100% !important; + padding: 0 40px; + } + } + + .content-wrapper.page { + min-height: 300px; + height: 300px; + } + + .banner-text-wrapper { + left: 50%; + transform: translateX(-50%); + text-align: center; + .start-button { + text-align: center; + > a { + margin: 0 4px; + } + } + .github-btn { + text-align: center; + float: none; + display: inline-block; + } + .line { + display: none; + } + } + + button.lang { + display: block; + margin: 29px auto 16px; + color: @site-text-color; + border-color: @site-text-color; + } + + div.version { + display: block; + margin: 29px auto 16px; + & > .ant-select-selection { + color: @site-text-color; + &:not(:hover) { + border-color: @site-text-color; + } + } + } + + .popover-menu { + width: 300px; + button.lang { + margin: 16px auto; + float: none; + } + div.version { + margin: 32px auto 16px; + float: none; + } + .ant-popover-inner { + overflow: hidden; + &-content { + padding: 0; + } + } + } + + .toc { + display: none; + } + + .nav-phone-icon { + display: block; + } + + .nav-phone-icon:before { + content: ""; + display: block; + border-radius: 2px; + width: 16px; + height: 2px; + background: #777; + box-shadow: 0 6px 0 0 #777, 0 12px 0 0 #777; + position: absolute; + } + + .main { + height: calc(100% - 86px); + } + + .aside-container { + float: none; + width: auto; + padding-bottom: 30px; + border-right: 0; + margin-bottom: 30px; + } + + .main-container { + padding-left: 16px; + padding-right: 16px; + margin-right: 0; + > .markdown > * { + width: 100% !important; + } + } + + .main-wrapper { + width: 100%; + border-radius: 0; + margin: 0; + } + + #footer { + text-align: center; + .footer-wrap { + padding: 40px; + } + .footer-center { + text-align: center; + } + h2 { + margin-top: 16px; + } + .bottom-bar { + text-align: center; + .translate-button { + width: auto; + text-align: center; + margin-bottom: 16px; + } + } + } + + .prev-next-nav { + margin-left: 16px; + width: ~"calc(100% - 32px)"; + } + .drawer { + .ant-menu-inline .ant-menu-item:after, + .ant-menu-vertical .ant-menu-item:after { + left: 0; + right: auto; + } + } +} + +@media only screen and (max-width: @screen-md) { + #logo { + padding: 0; + display: block; + margin-left: auto; + margin-right: auto; + float: none; + width: 200px; + } + + .header-meta { + padding-right: 80px; + } + + .home-banner-image { + display: block; + } + + .home-banner-anim { + display: none; + } + + .banner-wrapper { + width: 80%; + height: calc(~"100vh - 64px"); + overflow: hidden; + .banner-title-wrapper, .banner-image-wrapper { + display: block; + position: initial; + width: 100%; + height: auto; + } + .banner-title-wrapper { + text-align: center; + max-width: 480px; + + .button-wrapper { + text-align: center; + display: block; + margin-top: 5vh; + a { + display: inline-block; + } + .github-btn { + display: flex; + margin: 20px auto; + justify-content: center; + } + } + } + .banner-image-wrapper { + margin: 10vh auto 5vh; + } + } + .home-page { + width: 90%; + } + .home-code, .page-content { + width: 100%; + } + #footer { + .footer-wrap { + padding: 0; + } + } +} + +@media only screen and (max-width: @screen-xs) { + .page1 { + height: 2400px; + } + .page2 { + height: 628px; + } + .page1-box-wrapper { + display: block; + li { + width: 80%; + display: block; + margin: 0 auto 100px; + .page1-box { + width: 100%; + } + } + } + .banner-wrapper .banner-title-wrapper { + h1 { + font-size: 28px; + } + p { + font-size: 16px; + } + } +} + +@media only screen and (max-width: 320px) { + .home-page h2 { + font-size: 24px; + } + .page2 { + height: 648px; + } +} diff --git a/openpbl-landing/src/pages/Home/static/style.js b/openpbl-landing/src/pages/Home/static/style.js new file mode 100644 index 0000000..f3fdbf1 --- /dev/null +++ b/openpbl-landing/src/pages/Home/static/style.js @@ -0,0 +1,6 @@ +import 'react-github-button/assets/style.css'; +import './header.less'; +import './home.less'; +import './footer.less'; +import './responsive.less'; +import './teams2.less'; diff --git a/openpbl-landing/src/pages/Home/static/teams2.less b/openpbl-landing/src/pages/Home/static/teams2.less new file mode 100644 index 0000000..c434fc4 --- /dev/null +++ b/openpbl-landing/src/pages/Home/static/teams2.less @@ -0,0 +1,72 @@ +@teams2: teams2; + +.@{teams2}-wrapper { + min-height: 446px; + overflow: hidden; + + .@{teams2} { + overflow: hidden; + height: 100%; + padding: 0px 24px 64px 24px; + + >.title-wrapper { + margin: 0 auto 48px; + text-align: center; + } + + .block-wrapper { + position: relative; + height: 100%; + overflow: hidden; + padding: 20px 0; + min-height: 456px; + + .block { + margin-bottom: 48px; + vertical-align: text-top; + + &.queue-anim-leaving { + position: relative !important; + } + } + } + + &-image { + color: #404040; + width: 100%; + + img { + width: 100%; + } + } + + &-textWrapper { + padding-left: 16px; + } + + &-title { + font-size: 18px; + margin-bottom: 2px; + } + + &-job { + margin-bottom: 4px; + } + + &-job, + &-content { + font-size: 12px; + color: #919191; + } + } +} + +@media screen and (max-width: 767px) { + .@{teams2}-wrapper { + min-height: 1440px; + + .@{teams2}.home-page { + padding-bottom: 0; + } + } +} diff --git a/openpbl-landing/src/pages/Home/utils.js b/openpbl-landing/src/pages/Home/utils.js new file mode 100644 index 0000000..57cf9cb --- /dev/null +++ b/openpbl-landing/src/pages/Home/utils.js @@ -0,0 +1,18 @@ + +import React from 'react'; +import { Button } from 'antd'; + +export const isImg = /^http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?/; +export const getChildrenToRender = (item, i) => { + let tag = item.name.indexOf('title') === 0 ? 'h1' : 'div'; + tag = item.href ? 'a' : tag; + let children = typeof item.children === 'string' && item.children.match(isImg) + ? React.createElement('img', { src: item.children, alt: 'img' }) + : item.children; + if (item.name.indexOf('button') === 0 && typeof item.children === 'object') { + children = React.createElement(Button, { + ...item.children + }); + } + return React.createElement(tag, { key: i.toString(), ...item }, children); +}; diff --git a/openpbl-landing/src/pages/Project/CreateProject/Info/component/InfoEditPage.jsx b/openpbl-landing/src/pages/Project/CreateProject/Info/component/InfoEditPage.jsx new file mode 100644 index 0000000..47c45f5 --- /dev/null +++ b/openpbl-landing/src/pages/Project/CreateProject/Info/component/InfoEditPage.jsx @@ -0,0 +1,254 @@ +import React, {useEffect, useState} from "react"; +import {Button, Col, Input, message, Row, Select, Upload} from "antd"; +import {Link} from 'react-router-dom' +import ImgCrop from 'antd-img-crop'; +import '../../Outline/index.less' +import {LoadingOutlined, PlusOutlined} from "@ant-design/icons"; +import ProjectApi from "../../../../../api/ProjectApi"; + + +function InfoEditPage(obj) { + const pid = obj.pid + + const subjects = ['语文', '数学', '英语', '科学'] + const skills = ['学习与创新技能', '信息、媒体与技术技能', '生活与职业技能'] + + const [change, setChange] = useState(false) + + const [selectedSubjects, setSelectedSubjects] = useState([]) + const [selectedSkills, setSelectedSkills] = useState([]) + const [loading, setLoading] = useState(false) + const [imageUrl, setImageUrl] = useState() + const [projectTitle, setProjectTitle] = useState('') + const [projectIntroduce, setProjectIntroduce] = useState('') + const [projectGoal, setProjectGoal] = useState('') + + const stringToList = str => { + if (str === '') { + return [] + } + return str.split(',') + } + useEffect(() => { + ProjectApi.getProjectDetail(pid) + .then((res) => { + if (res.data.code === 200) { + setImageUrl(res.data.project.image) + setProjectTitle(res.data.project.projectTitle) + setProjectIntroduce(res.data.project.projectIntroduce) + setProjectGoal(res.data.project.projectGoal) + setSelectedSkills(stringToList(res.data.project.skills)) + setSelectedSubjects(stringToList(res.data.project.subjects)) + } + }) + .catch((e) => { + console.log(e) + }) + }, []) + + const changeTitle = value => { + setChange(true) + setProjectTitle(value.target.value) + } + const changeIntroduce = value => { + setChange(true) + setProjectIntroduce(value.target.value) + } + const changeGoal = value => { + setChange(true) + setProjectGoal(value.target.value) + } + + const onUploadImage = (file) => { + setChange(true) + + setLoading(true) + file = file.file; + const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'; + if (!isJpgOrPng) { + message.error('只能上传 JPG/PNG 格式文件'); + } + const isLt2M = file.size / 1024 / 1024 < 2; + if (!isLt2M) { + message.error('图片应当小于 2MB'); + } + + const r = new FileReader(); + r.addEventListener('load', () => upload(r.result)); + r.readAsDataURL(file); + } + + const upload = (file) => { + setImageUrl(file) + setLoading(true) + + } + + const onPreview = file => { + let src = file.url; + if (!src) { + src = new Promise(resolve => { + const reader = new FileReader(); + reader.readAsDataURL(file.originFileObj); + reader.onload = () => resolve(reader.result); + }); + } + const image = new Image(); + image.src = src; + const imgWindow = window.open(src); + imgWindow.document.write(image.outerHTML); + } + + const onFinish = () => { + let data = { + id: pid, + image: imageUrl, + projectTitle: projectTitle, + projectIntroduce: projectIntroduce, + projectGoal: projectGoal, + subjects: selectedSubjects.toString(), + skills: selectedSkills.toString(), + } + ProjectApi.updateProject(data, pid) + .then((res) => { + if (res.data.code === 200) { + message.success(res.data.msg) + setTimeout(()=>{ + window.location.href = `/project/${pid}/outline/edit` + }, 200) + } + }) + .catch((e) => { + console.log(e) + }) + } + const nextPage = () => { + if (checkInput()) { + if (change) { + onFinish(); + } else { + window.location.href = `/project/${pid}/outline/edit` + } + } + } + const checkInput = () => { + if (imageUrl === "") { + message.error("请上传图片") + return false + } + if (projectTitle === "") { + message.error("请输入标题") + return false + } + if (projectIntroduce === "") { + message.error("请输入简介") + return false + } + return true + } + + const handleSubjectsChange = selected => { + setChange(true) + setSelectedSubjects(selected) + } + const handleSkillsChange = selected => { + setChange(true) + setSelectedSkills(selected) + } + + return ( +
    + + 上传图片: + + + + { + imageUrl ? avatar + : +
    + {loading ? : } +
    上传图片
    +
    + } +
    +
    + +
    + + 输入标题: + + + + 输入简介: + + + + + + 输入目标: + + + + + + + 选择学科: + + + + + + 选择技能: + + + + + + + + + +
    + ) +} + +export default InfoEditPage diff --git a/openpbl-landing/src/pages/Project/CreateProject/Info/index.jsx b/openpbl-landing/src/pages/Project/CreateProject/Info/index.jsx new file mode 100644 index 0000000..b1298a2 --- /dev/null +++ b/openpbl-landing/src/pages/Project/CreateProject/Info/index.jsx @@ -0,0 +1,58 @@ +import React from "react"; +import DocumentTitle from 'react-document-title'; +import {Card, Divider, PageHeader, Steps} from "antd"; + +import '../Outline/index.less' +import InfoEditPage from "./component/InfoEditPage"; + +const {Step} = Steps; + + +class EditInfo extends React.PureComponent { + state = { + current: 0, + pid: this.props.match.params.id + } + + componentDidMount() { + } + + back = e => { + window.location.href = `/project/${this.state.pid}/info` + } + + render() { + const {pid} = this.state + return ( + +
    + this.back()} + title="返回" + subTitle="我的项目" + /> +
    + + + + + + + + +
    +
    +
    + ) + } +} + +export default EditInfo diff --git a/openpbl-landing/src/pages/Project/CreateProject/Outline/component/OutlineEditPage.jsx b/openpbl-landing/src/pages/Project/CreateProject/Outline/component/OutlineEditPage.jsx new file mode 100644 index 0000000..78ce6b7 --- /dev/null +++ b/openpbl-landing/src/pages/Project/CreateProject/Outline/component/OutlineEditPage.jsx @@ -0,0 +1,350 @@ +import React, {useEffect, useState} from "react"; +import {Button, Col, Input, Menu, message, Modal, Popconfirm, Row} from 'antd' +import {ArrowDownOutlined, ArrowUpOutlined, DeleteOutlined, EditOutlined} from '@ant-design/icons' +import {Link} from 'react-router-dom' + +import ChapterApi from "../../../../../api/ChapterApi" +import SectionApi from "../../../../../api/SectionApi" + +import util from "../../../component/Util" + +const {SubMenu} = Menu; + +function OutlineEditPage(obj) { + const pid = obj.pid + + const [chapters, setChapters] = useState([]) + const [chapterModalVisible, setChapterModalVisible] = useState(false) + const [sectionModalVisible, setSectionModalVisible] = useState(false) + + const [opt, setOpt] = useState('add') + const [chapter, setChapter] = useState({}) + const [section, setSection] = useState({}) + + const [index, setIndex] = useState('') + const [subIndex, setSubIndex] = useState('') + + const [chapterName, setChapterName] = useState('') + const [sectionName, setSectionName] = useState('') + + useEffect(() => { + getChapters() + }, []) + const getChapters = () => { + ChapterApi.getProjectChapters(pid) + .then((res) => { + if (res.data.chapters === null) { + setChapters([]) + } else { + setChapters(res.data.chapters) + } + }) + .catch((e) => { + console.log(e) + }) + } + + const addChapter = e => { + setOpt('add') + setChapterModalVisible(true) + } + const modifyChapter = (e, c, index) => { + e.stopPropagation() + + setChapter(c) + setChapterName(c.chapterName) + setIndex(index) + setOpt('modify') + setChapterModalVisible(true) + } + const addSection = (c, index) => { + setOpt('add') + setChapter(c) + setIndex(index) + setSectionModalVisible(true) + } + const modifySection = (s, index, subIndex) => { + setSection(s) + setSectionName(s.sectionName) + setIndex(index) + setSubIndex(subIndex) + setOpt('modify') + setSectionModalVisible(true) + } + const del = e => { + e.stopPropagation() + } + const cancel = e => { + e.stopPropagation() + } + const deleteChapter = (e, c, index) => { + e.stopPropagation() + ChapterApi.deleteProjectChapter(c) + .then((res) => { + if (res.data.code === 200) { + message.success(res.data.msg) + getChapters() + } + }) + .catch((e) => { + console.log(e) + }) + } + const deleteSection = (s, index, subIndex) => { + SectionApi.deleteChapterSection(pid, s) + .then((res) => { + if (res.data.code === 200) { + message.success(res.data.msg) + getChapters() + } + }) + } + const doChapter = e => { + if (chapterName === '') { + message.error('请输入章名') + return + } + if (opt === 'modify') { + let c = { + id: chapter.id, + projectId: chapter.projectId, + chapterName: chapterName, + chapterNumber: chapter.chapterNumber, + } + ChapterApi.updateProjectChapter(c) + .then((res) => { + if (res.data.code === 200) { + setChapterModalVisible(false) + message.success(res.data.msg) + + chapters[index].chapterName = chapterName + setChapters([...chapters]) + setChapterName('') + } + }) + .catch((e) => { + console.log(e) + }) + } else if (opt === 'add') { + let len = chapters.length + let l = 0 + if (len > 0) { + l = chapters[len - 1].chapterNumber + 1 + } + let cp = {chapterName: chapterName, chapterNumber: l, projectId: pid} + ChapterApi.createProjectChapter(cp) + .then((res) => { + setChapterModalVisible(false) + setChapterName('') + if (res.data.code === 200) { + cp.id = res.data.data + chapters.push(cp) + setChapters([...chapters]) + } + }) + .catch((e) => { + console.log(e) + }) + } + } + const doSection = () => { + if (sectionName === '') { + message.error('请输入小节名') + return + } + if (opt === 'modify') { + let s = { + id: section.id, + chapterId: section.chapterId, + sectionName: sectionName, + sectionNumber: section.sectionNumber, + chapterNumber: section.chapterNumber + } + SectionApi.updateChapterSection(pid, s) + .then((res) => { + if (res.data.code === 200) { + setSectionModalVisible(false) + + chapters[index].sections[subIndex].sectionName = sectionName + setChapters([...chapters]) + setSectionName('') + } + }) + .catch((e) => { + console.log(e) + }) + } else if (opt === 'add') { + let l = 0 + if (chapters[index].sections !== undefined && chapters[index].sections !== null) { + let len = chapters[index].sections.length + if (len > 0) { + l = chapters[index].sections[len - 1].sectionNumber + 1 + } + } + let sec = { + sectionName: sectionName, + sectionNumber: l, + chapterId: chapters[index].id, + chapterNumber: chapters[index].chapterNumber + } + SectionApi.createChapterSection(pid, sec) + .then((res) => { + setSectionModalVisible(false) + setSectionName('') + if (res.data.code === 200) { + sec.id = res.data.data; + if (chapters[index].sections === undefined || chapters[index].sections === null) { + chapters[index].sections = [sec] + } else { + chapters[index].sections.push(sec) + } + setChapters([...chapters]) + setChapterName('') + } + }) + .catch((e) => { + console.log(e) + }) + } + } + const cancelDoChapter = e => { + setChapterModalVisible(false) + setChapterName('') + } + const cancelDoSection = e => { + setSectionModalVisible(false) + setSectionName('') + } + + const changeChapterName = value => { + setChapterName(value.target.value) + } + const changeSectionName = value => { + setSectionName(value.target.value) + } + const exchangeChapter = (e, index, index2) => { + e.stopPropagation() + + if (index < 0 || index2 >= chapters.length) { + } else { + let id1 = chapters[index].id + let id2 = chapters[index2].id + ChapterApi.exchangeProjectChapter(pid, id1, id2) + .then((res) => { + if (res.data.code === 200) { + getChapters() + } + }) + .catch(e => { + console.log(e) + }) + } + } + const exchangeSection = (index, subIndex, subIndex2) => { + if (subIndex < 0 || subIndex2 >= chapters[index].sections.length) { + } else { + let id1 = chapters[index].sections[subIndex].id + let id2 = chapters[index].sections[subIndex2].id + SectionApi.exchangeChapterSection(chapters[index], id1, id2) + .then(res => { + if (res.data.code === 200) { + getChapters() + } + }) + .catch(e => { + console.log(e) + }) + } + } + + return ( +
    + {chapters.map((item, index) => ( + + {util.FormatChapterName(item.chapterName, item.chapterNumber)} + +
    + } + > + {(item.sections === null || item.sections === undefined) ? null : + item.sections.map((subItem, subIndex) => ( + + {util.FormatSectionName(subItem.sectionName, subItem.chapterNumber, subItem.sectionNumber)} + + + + + + + + ))} + +
    + +
    +
    + + + + + + +
    + + +
    + + +

    章名:

    + + + + +
    +
    + + +
    + + +

    小节名:

    + + + + +
    +
    +
    + ) +} + +export default OutlineEditPage diff --git a/openpbl-landing/src/pages/Project/CreateProject/Outline/index.jsx b/openpbl-landing/src/pages/Project/CreateProject/Outline/index.jsx new file mode 100644 index 0000000..1e1ced6 --- /dev/null +++ b/openpbl-landing/src/pages/Project/CreateProject/Outline/index.jsx @@ -0,0 +1,59 @@ +import React from "react"; +import DocumentTitle from 'react-document-title'; +import {Card, Divider, PageHeader, Steps} from "antd"; + +import './index.less' +import OutlineEditPage from "./component/OutlineEditPage"; + +const {Step} = Steps; + + +class EditOutline extends React.PureComponent { + state = { + current: 0, + pid: this.props.match.params.id + } + + componentDidMount() { + + } + + back = e => { + this.props.history.push(`/project/${this.state.pid}/info/`) + } + + render() { + const {pid} = this.state + return ( + +
    + this.back()} + title="返回" + subTitle="我的项目" + /> +
    + + + + + + + + +
    +
    +
    + ) + } +} + +export default EditOutline diff --git a/openpbl-landing/src/pages/Project/CreateProject/Outline/index.less b/openpbl-landing/src/pages/Project/CreateProject/Outline/index.less new file mode 100644 index 0000000..709903d --- /dev/null +++ b/openpbl-landing/src/pages/Project/CreateProject/Outline/index.less @@ -0,0 +1,5 @@ +.edit-card { + max-width: 1200px; + margin: auto; + text-align: left; +} diff --git a/openpbl-landing/src/pages/Project/CreateProject/Section/SectionEditPage.jsx b/openpbl-landing/src/pages/Project/CreateProject/Section/SectionEditPage.jsx new file mode 100644 index 0000000..20a9d40 --- /dev/null +++ b/openpbl-landing/src/pages/Project/CreateProject/Section/SectionEditPage.jsx @@ -0,0 +1,59 @@ +import React, {useEffect, useState} from "react"; +import {Button, Card, PageHeader} from "antd"; +import DocumentTitle from 'react-document-title'; +import {Link} from "react-router-dom"; + + +import SectionApi from "../../../../api/SectionApi"; +import RichWords from "./component/RichWords"; +import FileResource from "../component/FileResource"; +import StudentTask from "./component/StudentTask"; + +function SectionEditPage(obj) { + const pid = obj.match.params.pid + const sid = obj.match.params.sid + const [section, setSection] = useState({resource: {}}) + + useEffect(() => { + SectionApi.getSectionDetail(sid, pid) + .then(res => { + setSection(res.data.section) + }) + .catch(e => { + console.log(e) + }) + + }, []) + + + const back = e => { + window.location.href = `/project/${pid}/outline/edit` + } + + + return ( + +
    + +
    + +

    {section.sectionName}

    +
    + + + +
    + + + +
    +
    + ) +} + +export default SectionEditPage diff --git a/openpbl-landing/src/pages/Project/CreateProject/Section/component/RichWords.jsx b/openpbl-landing/src/pages/Project/CreateProject/Section/component/RichWords.jsx new file mode 100644 index 0000000..f40f304 --- /dev/null +++ b/openpbl-landing/src/pages/Project/CreateProject/Section/component/RichWords.jsx @@ -0,0 +1,104 @@ +import React, {useEffect, useState} from "react"; +import {Button, Card, Divider, message} from "antd"; +import LzEditor from "react-lz-editor" +import uniqBy from 'lodash/uniqBy'; +import findIndex from 'lodash/findIndex'; + +import OSSUploaderFile from "../../component/oss"; +import ResourceApi from "../../../../../api/ResourceApi"; +import "./section-edit.less" + +function RichWords(obj) { + const sid = obj.section.id + const [fileList, setFileList] = useState([]) + const [content, setContent] = useState(obj.content) + + useEffect(() => { + + }, []) + const receiveHtml = (content) => { + setContent(content) + } + + const saveContent = e => { + ResourceApi.updateResourceContent(obj.section.resource.id, content) + .then(res => { + if (res.data.code === 200) { + obj.section.resource.content = content + message.success(res.data.msg) + } else { + message.error(res.data.msg) + } + }) + .catch(e => { + console.log(e) + }) + } + + const handleChange = (changedValue) => { + let currFileList = changedValue.fileList; + console.error(JSON.stringify(changedValue)); + currFileList = currFileList.filter(f => (!f.length)); + currFileList = currFileList.map((file) => { + if (file.response) { + file.url = file.response.url; + } + if (!file.length) { + return file; + } + }); + currFileList = currFileList.filter((file) => { + const hasNoExistCurrFileInUploadedList = !~findIndex( + fileList, item => item.name === file.name, + ); + if (hasNoExistCurrFileInUploadedList) { + fileList.push(file); + } + return !!file.response || (!!file.url && file.status === 'done') || file.status === 'uploading'; + }); + currFileList = uniqBy(currFileList, 'name'); + if (!!currFileList && currFileList.length !== 0) { + setFileList(currFileList); + } + }; + + const customRequest = ({ + file, + onError, + onSuccess, + }) => { + OSSUploaderFile(file, '/openct/' + sid + '/content/', onSuccess, onError); + }; + + const uploadProps = { + onChange: handleChange, + listType: 'picture', + fileList: fileList, + customRequest: customRequest, + multiple: true, + showUploadList: true + } + + return ( + +

    文本内容

    + +
    + +
    +
    + + + +
    +
    + ) +} + +export default RichWords diff --git a/openpbl-landing/src/pages/Project/CreateProject/Section/component/StudentTask.jsx b/openpbl-landing/src/pages/Project/CreateProject/Section/component/StudentTask.jsx new file mode 100644 index 0000000..38fffa5 --- /dev/null +++ b/openpbl-landing/src/pages/Project/CreateProject/Section/component/StudentTask.jsx @@ -0,0 +1,161 @@ +import React, {useEffect, useState} from "react"; +import {Button, Card, Divider, Dropdown, Input, Menu, message, Popconfirm} from "antd"; +import {ArrowDownOutlined, ArrowUpOutlined, DeleteOutlined, UpOutlined} from "@ant-design/icons"; + +import TaskApi from "../../../../../api/TaskApi"; +import "./section-edit.less" + + +function StudentTask(obj) { + const pid = obj.pid + const sid = obj.section.id + const [tasks, setTasks] = useState([]) + useEffect(() => { + if (sid !== undefined) { + getTasks() + } + }, [sid]) + const getTasks = () => { + TaskApi.getSectionTasks(sid, pid) + .then(res => { + if (res.data.tasks === null) { + setTasks([]) + } else { + setTasks(res.data.tasks) + } + }) + } + + const saveContent = (item, index) => { + TaskApi.updateTask(pid, item) + .then(res => { + if (res.data.code === 200) { + message.success(res.data.msg) + } else { + message.error(res.data.msg) + } + }) + } + const addTask = (tp) => { + let len = tasks.length + let o = 0 + if (len > 0) { + o = tasks[len - 1].taskOrder + 1 + } + // taskType: survey file comment + let t = { + sectionId: sid, + taskOrder: o, + taskType: tp.key, + chapterNumber: obj.section.chapterNumber, + sectionNumber: obj.section.sectionNumber + } + TaskApi.createTask(pid, t) + .then(res => { + if (res.data.code === 200) { + t.id = res.data.data + tasks.push(t) + setTasks([...tasks]) + } + }) + } + + const changeTitle = (value, index) => { + tasks[index].taskTitle = value.target.value + setTasks([...tasks]) + } + const changeIntroduce = (value, index) => { + tasks[index].taskIntroduce = value.target.value + setTasks([...tasks]) + } + const deleteTask = (item, index) => { + TaskApi.deleteTask(pid, item.id) + .then(res => { + if (res.data.code === 200) { + tasks.splice(index, 1) + setTasks([...tasks]) + } else { + message.error(res.data.msg) + } + }) + .catch(e => { + console.log(e) + }) + } + const exchangeTask = (index1, index2) => { + if (index1 < 0 || index2 >= tasks.length) { + } else { + let id1 = tasks[index1].id + let id2 = tasks[index2].id + TaskApi.exchangeTask(id1, id2) + .then(res => { + if (res.data.code === 200) { + let t1 = tasks[index1] + tasks[index1] = tasks[index2] + tasks[index2] = t1 + setTasks([...tasks]) + } else { + message.error(res.data.msg) + } + }) + } + } + const gotoSurvey = item => { + window.location.href = `/project/${pid}/section/${obj.section.id}/task/${item.id}/survey/edit` + } + + return ( + +

    学生任务

    + {tasks.map((item, index) => ( +
    + +

    + {item.taskType === 'file' ? '学生上传文件' : null} + {item.taskType === 'comment' ? '学生评论' : null} + {item.taskType === 'survey' ? '学生填写问卷' : null} + + + : null + } + +

    + + + +
    +
    + ))} +
    + + 文件任务 + 评论任务 + 问卷任务 + + } + > + + +
    +
    + ) +} + +export default StudentTask diff --git a/openpbl-landing/src/pages/Project/CreateProject/Section/component/section-edit.less b/openpbl-landing/src/pages/Project/CreateProject/Section/component/section-edit.less new file mode 100644 index 0000000..89e1d11 --- /dev/null +++ b/openpbl-landing/src/pages/Project/CreateProject/Section/component/section-edit.less @@ -0,0 +1,21 @@ +.resource-card { + margin-top: 10px; + text-align: left; +} + +.card-title { + text-align: left; + font-weight: bold; + font-size: 20px; +} + +.task-title { + text-align: left; + font-size: 16px; +} + +.submit-status{ + font-weight: normal; + font-size: 14px; + margin-left: 10px; +} diff --git a/openpbl-landing/src/pages/Project/CreateProject/Survey/SurveyEditPage.jsx b/openpbl-landing/src/pages/Project/CreateProject/Survey/SurveyEditPage.jsx new file mode 100644 index 0000000..9cb1d26 --- /dev/null +++ b/openpbl-landing/src/pages/Project/CreateProject/Survey/SurveyEditPage.jsx @@ -0,0 +1,268 @@ +import React, {useEffect, useState} from "react"; +import {Affix, Card, Divider, Layout, Menu, message, PageHeader, Radio} from "antd"; +import DocumentTitle from 'react-document-title'; + +import SurveyApi from "../../../../api/SurveyApi"; + +import SingleChoice from "./component/SingleChoice"; +import MultipleChoice from "./component/MultipleChoice"; +import BlankFill from "./component/BlankFill"; +import BriefAnswer from "./component/BriefAnswer"; +import Question from "./component/Question"; +import Scale from "./component/Scale"; + +const qType = { + 'singleChoice': '单选', + 'multipleChoice': '多选', + 'blankFill': '填空', + 'briefAnswer': '简答', + 'scale5': '五点量表', + 'scale7': '七点量表' +} + +const blank = Question.blank + +function SurveyEditPage(obj) { + const pid = obj.match.params.pid + const sid = obj.match.params.sid + const tid = obj.match.params.tid + + const [survey, setSurvey] = useState({}) + const [questions, setQuestions] = useState([]) + const [editing, setEditing] = useState([]) + + useEffect(() => { + if (tid !== undefined) { + SurveyApi.getSurveyDetailByTaskId(pid, tid) + .then(res => { + if (res.data.questions === null) { + setQuestions([]) + } else { + let qs = res.data.questions + for (let i = 0; i < qs.length; i++) { + qs[i].questionOptions = qs[i].questionOptions.split(",") + } + setQuestions(qs) + setEditing(new Array(qs.length).fill(false)) + } + setSurvey(res.data.survey) + }) + } + }, [tid]) + + const createQuestion = (e) => { + let len = questions.length + let l = 0 + if (len > 0) { + l = questions[len - 1].questionOrder + 1 + } + let opt = "" + if (e.key === 'singleChoice' || e.key === 'multipleChoice') { + opt = ['选项1', '选项2'] + } else if (e.key === 'blankFill') { + opt = ['题目描述', blank] + } else if (e.key === 'briefAnswer') { + opt = ['题目描述'] + } else if (e.key === 'scale5') { + opt = ['选项1', '选项2', '选项3', '选项4', '选项5'] + } else { + opt = ['选项1', '选项2', '选项3', '选项4', '选项5', '选项6', '选项7'] + } + let q = { + surveyId: survey.id, + questionType: e.key, + questionOrder: l, + questionTitle: '标题', + questionOptions: opt + } + q.questionOptions = q.questionOptions.toString() + SurveyApi.createQuestion(pid, tid, q) + .then(res => { + if (res.data.code === 200) { + q.id = res.data.data + q.questionOptions = q.questionOptions.split(",") + questions.push(q) + setQuestions([...questions]) + } else { + message.error(res.data.msg) + } + }) + } + const saveQuestion = (item, title, opt, index) => { + let q = Object.assign({}, item) + q.questionOptions = opt.toString() + q.questionTitle = title + SurveyApi.updateQuestion(pid, tid, q) + .then(res => { + editing[index] = false + setEditing([...editing]) + if (res.data.code === 200) { + q.questionOptions = q.questionOptions.split(",") + questions[index] = q + setQuestions([...questions]) + message.success(res.data.msg) + } else { + message.error(res.data.msg) + } + }) + .catch(e => { + console.log(e) + }) + } + + const deleteQuestion = (item, index) => { + SurveyApi.deleteQuestion(pid, tid, item.surveyId, item.id) + .then(res => { + if (res.data.code === 200) { + questions.splice(index, 1) + setQuestions([...questions]) + } else { + message.error(res.data.msg) + } + }) + .catch(e => { + console.log(e) + }) + } + const exchangeQuestion = (index1, index2) => { + if (index1 < 0 || index2 >= questions.length) { + } else { + SurveyApi.exchangeQuestion(pid, tid, questions[index1].surveyId, questions[index1].id, questions[index2].id) + .then(res => { + if (res.data.code === 200) { + let q1 = questions[index1] + questions[index1] = questions[index2] + questions[index2] = q1 + setQuestions([...questions]) + } else { + message.error(res.data.msg) + } + }) + } + } + + const editQuestion = (item, index) => { + editing[index] = true + setEditing([...editing]) + } + const back = e => { + window.location.href = `/project/${pid}/section/${sid}/edit` + } + + const getType = t => { + return qType[t] + } + + return ( + +
    + +
    + + + + + + + 单选 + 多选 + + + 填空 + 简答 + + + 五点量表 + 七点量表 + + + + + +
    +

    {survey.surveyTitle}

    + {questions.map((item, index) => ( +
    + + {item.questionType === 'singleChoice' ? + + : null} + + {item.questionType === 'scale5' || item.questionType === 'scale7' ? + + : null} + + {item.questionType === 'multipleChoice' ? + + : null} + + {item.questionType === 'blankFill' ? + + : null} + + {item.questionType === 'briefAnswer' ? + + : null} +
    + ))} +
    +
    +
    +
    +
    +
    +
    + ) +} + +export default SurveyEditPage diff --git a/openpbl-landing/src/pages/Project/CreateProject/Survey/component/BlankFill.jsx b/openpbl-landing/src/pages/Project/CreateProject/Survey/component/BlankFill.jsx new file mode 100644 index 0000000..40a8389 --- /dev/null +++ b/openpbl-landing/src/pages/Project/CreateProject/Survey/component/BlankFill.jsx @@ -0,0 +1,131 @@ +import React, {useState} from "react"; +import {Button, Dropdown, Input, Menu, message, Popconfirm} from "antd"; +import { + ArrowDownOutlined, + ArrowUpOutlined, + DeleteOutlined, + EditOutlined, + MinusOutlined, + PlusOutlined, + SaveOutlined +} from "@ant-design/icons"; +import Question from "./Question"; + + +const blank = Question.blank + +function BlankFill(obj) { + const [opt, setOpt] = useState(obj.item.questionOptions) + const [title, setTitle] = useState(obj.item.questionTitle) + + const changeTitle = value => { + setTitle(value.target.value) + } + const changeOpt = (value, index) => { + opt[index] = value.target.value + setOpt([...opt]) + } + const addOpt = (key, index) => { + let v = '描述' + if (key.key === '2') { + v = ' ' + } + opt.splice(index + 1, 0, v) + setOpt([...opt]) + } + const delOpt = index => { + if (opt.length === 1) { + message.error("不能删除最后一个选项") + } else { + opt.splice(index, 1) + setOpt([...opt]) + } + } + return ( +
    +
    + {obj.editing ? +

    + +

    + : +

    {obj.item.questionTitle} +   + [{obj.getType(obj.item.questionType)}] + +

    + } +

    + {obj.editing ? +

    +

    + + {obj.editing ? +
    + {opt.map((item, index) => ( +
    + {item === blank ? + + + + : + + changeOpt(value, index)}/> + + } + +
    + ))} +
    + : +
    + {obj.item.questionOptions.map((subItem, subIndex) => ( +
    + {subItem === blank ? + + + + : + {subItem}} +
    + ))} +
    + } +
    + ) +} + +export default BlankFill \ No newline at end of file diff --git a/openpbl-landing/src/pages/Project/CreateProject/Survey/component/BriefAnswer.jsx b/openpbl-landing/src/pages/Project/CreateProject/Survey/component/BriefAnswer.jsx new file mode 100644 index 0000000..4324b63 --- /dev/null +++ b/openpbl-landing/src/pages/Project/CreateProject/Survey/component/BriefAnswer.jsx @@ -0,0 +1,72 @@ +import React, {useState} from "react"; +import {Button, Input, Popconfirm} from "antd"; +import {ArrowDownOutlined, ArrowUpOutlined, DeleteOutlined, EditOutlined, SaveOutlined} from "@ant-design/icons"; + + +function BriefAnswer(obj) { + const [opt, setOpt] = useState(obj.item.questionOptions) + const [title, setTitle] = useState(obj.item.questionTitle) + + const changeTitle = value => { + setTitle(value.target.value) + } + const changeQ = v => { + opt[0] = v.target.value + setOpt([...opt]) + } + return ( +
    +
    + {obj.editing ? +

    + +

    + : +

    {obj.item.questionTitle} +   + [{obj.getType(obj.item.questionType)}] + +

    + } +

    + {obj.editing ? +

    +

    + + {obj.editing ? +
    + + +
    + : +
    +

    {obj.item.questionOptions[0]}

    + +
    + } + +
    + ) +} + +export default BriefAnswer \ No newline at end of file diff --git a/openpbl-landing/src/pages/Project/CreateProject/Survey/component/MultipleChoice.jsx b/openpbl-landing/src/pages/Project/CreateProject/Survey/component/MultipleChoice.jsx new file mode 100644 index 0000000..8668180 --- /dev/null +++ b/openpbl-landing/src/pages/Project/CreateProject/Survey/component/MultipleChoice.jsx @@ -0,0 +1,106 @@ +import React, {useState} from "react"; +import {Button, Checkbox, Input, message, Popconfirm, Radio} from "antd"; +import { + ArrowDownOutlined, + ArrowUpOutlined, + DeleteOutlined, + EditOutlined, + MinusOutlined, + PlusOutlined, + SaveOutlined +} from "@ant-design/icons"; + +function MultipleChoice(obj) { + const [opt, setOpt] = useState(obj.item.questionOptions) + const [title, setTitle] = useState(obj.item.questionTitle) + + const changeTitle = value => { + setTitle(value.target.value) + } + const changeOpt = (value, index) => { + opt[index] = value.target.value + setOpt([...opt]) + } + const addOpt = index => { + opt.splice(index + 1, 0, '') + setOpt([...opt]) + } + const delOpt = index => { + if (opt.length === 1) { + message.error("不能删除最后一个选项") + } else { + opt.splice(index, 1) + setOpt([...opt]) + } + } + return ( +
    +
    + {obj.editing ? +

    + +

    + : +

    {obj.item.questionTitle} +   + [{obj.getType(obj.item.questionType)}] + +

    + } +

    + {obj.editing ? +

    +

    + + {obj.editing ? +
    + {opt.map((item, index) => ( +
    + + changeOpt(value, index)}/> + +
    + ))} +
    + : +
    + + {obj.item.questionOptions.map((subItem, subIndex) => ( +
    + + {subItem} + +
    + ))} +
    +
    + } +
    + ) + +} + +export default MultipleChoice \ No newline at end of file diff --git a/openpbl-landing/src/pages/Project/CreateProject/Survey/component/Question.js b/openpbl-landing/src/pages/Project/CreateProject/Survey/component/Question.js new file mode 100644 index 0000000..e8844d0 --- /dev/null +++ b/openpbl-landing/src/pages/Project/CreateProject/Survey/component/Question.js @@ -0,0 +1,4 @@ +const blank = ' ' +export default { + blank, +} \ No newline at end of file diff --git a/openpbl-landing/src/pages/Project/CreateProject/Survey/component/Scale.jsx b/openpbl-landing/src/pages/Project/CreateProject/Survey/component/Scale.jsx new file mode 100644 index 0000000..86fcb4c --- /dev/null +++ b/openpbl-landing/src/pages/Project/CreateProject/Survey/component/Scale.jsx @@ -0,0 +1,104 @@ +import React, {useState} from "react"; +import {Button, Input, message, Popconfirm, Radio} from "antd"; +import { + ArrowDownOutlined, + ArrowUpOutlined, + DeleteOutlined, + EditOutlined, + MinusOutlined, PlusOutlined, + SaveOutlined +} from "@ant-design/icons"; + +function Scale(obj) { + const [opt, setOpt] = useState(obj.item.questionOptions) + const [title, setTitle] = useState(obj.item.questionTitle) + + const changeTitle = value => { + setTitle(value.target.value) + } + const changeOpt = (value, index) => { + opt[index] = value.target.value + setOpt([...opt]) + } + const addOpt = index => { + opt.splice(index + 1, 0, '') + setOpt([...opt]) + } + const delOpt = index => { + if (opt.length === 1) { + message.error("不能删除最后一个选项") + } else { + opt.splice(index, 1) + setOpt([...opt]) + } + } + return ( +
    +
    + {obj.editing ? +

    + +

    + : +

    {obj.item.questionTitle} +   + [{obj.getType(obj.item.questionType)}] + +

    + } +

    + {obj.editing ? +

    +

    + + {obj.editing ? +
    + + {opt.map((item, index) => ( +
    + + changeOpt(value, index)}/> + + +
    + ))} +
    +
    + : +
    + + {obj.item.questionOptions.map((subItem, subIndex) => ( +
    + + {subItem} + +
    + ))} +
    +
    + } +
    + ) +} + +export default Scale \ No newline at end of file diff --git a/openpbl-landing/src/pages/Project/CreateProject/Survey/component/SingleChoice.jsx b/openpbl-landing/src/pages/Project/CreateProject/Survey/component/SingleChoice.jsx new file mode 100644 index 0000000..499bc96 --- /dev/null +++ b/openpbl-landing/src/pages/Project/CreateProject/Survey/component/SingleChoice.jsx @@ -0,0 +1,108 @@ +import React, {useState} from "react"; +import {Button, Input, message, Popconfirm, Radio} from "antd"; +import { + ArrowDownOutlined, + ArrowUpOutlined, + DeleteOutlined, + EditOutlined, + MinusOutlined, + PlusOutlined, + SaveOutlined +} from "@ant-design/icons"; + +function SingleChoice(obj) { + const [opt, setOpt] = useState(obj.item.questionOptions) + const [title, setTitle] = useState(obj.item.questionTitle) + + const changeTitle = value => { + setTitle(value.target.value) + } + const changeOpt = (value, index) => { + opt[index] = value.target.value + setOpt([...opt]) + } + const addOpt = index => { + opt.splice(index + 1, 0, '') + setOpt([...opt]) + } + const delOpt = index => { + if (opt.length === 1) { + message.error("不能删除最后一个选项") + } else { + opt.splice(index, 1) + setOpt([...opt]) + } + } + return ( +
    +
    + {obj.editing ? +

    + +

    + : +

    {obj.item.questionTitle} +   + [{obj.getType(obj.item.questionType)}] + +

    + } +

    + {obj.editing ? +

    +

    + + {obj.editing ? +
    + + {opt.map((item, index) => ( +
    + + changeOpt(value, index)}/> + +
    + ))} +
    +
    + : +
    + + {obj.item.questionOptions.map((subItem, subIndex) => ( +
    + + {subItem} + +
    + ))} +
    +
    + } +
    + ) + +} + +export default SingleChoice \ No newline at end of file diff --git a/openpbl-landing/src/pages/Project/CreateProject/component/FileResource.jsx b/openpbl-landing/src/pages/Project/CreateProject/component/FileResource.jsx new file mode 100644 index 0000000..ca4fd4b --- /dev/null +++ b/openpbl-landing/src/pages/Project/CreateProject/component/FileResource.jsx @@ -0,0 +1,98 @@ +import React, {useEffect, useState} from "react"; +import {Button, Card, Divider, Input, message, Upload} from "antd"; +import {InboxOutlined} from "@ant-design/icons"; + + +import "../Section/component/section-edit.less" +import ResourceApi from "../../../../api/ResourceApi"; + +const {Dragger} = Upload; + +function FileResource(obj) { + + const [fileTitle, setFileTitle] = useState('') + const [fileIntroduce, setFileIntroduce] = useState('') + const [filePath, setFilePath] = useState('') + + useEffect(() => { + setFileTitle(obj.section.resource.fileTitle) + setFileIntroduce(obj.section.resource.fileIntroduce) + setFilePath(obj.section.resource.filePath) + }, [fileTitle, obj.section.resource.fileIntroduce, obj.section.resource.filePath, obj.section.resource.fileTitle]) + + const onUploadFile = e => { + } + const saveContent = e => { + let q = { + id: obj.section.resource.id, + fileTitle: fileTitle, + fileIntroduce: fileIntroduce, + filePath: filePath + } + ResourceApi.updateResource(q) + .then(res => { + if (res.data.code === 200) { + message.success(res.data.msg) + } else { + message.error(res.data.msg) + } + }) + .catch(e => { + console.log(e) + }) + } + const changeTitle = value => { + setFileTitle(value.target.value) + } + const changeIntroduce = value => { + setFileIntroduce(value.target.value) + } + + const props = { + name: 'file', + multiple: false, + action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76', + onChange(info) { + const {status} = info.file; + if (status !== 'uploading') { + console.log(info.file, info.fileList); + } + if (status === 'done') { + setFilePath(info.file.name) + message.success(`${info.file.name} 上传成功`); + } else if (status === 'error') { + message.error(`${info.file.name} 上传失败`); + } + }, + onDrop(e) { + console.log('删除文件', e.dataTransfer.files); + setFilePath('') + }, + }; + + return ( + +

    上传文件资源

    + + + + + +

    + +

    +

    点击或拖动文件上传

    +

    + 多个文件打包上传 +

    +
    +
    + + + +
    +
    + ) +} + +export default FileResource diff --git a/openpbl-landing/src/pages/Project/CreateProject/component/oss.js b/openpbl-landing/src/pages/Project/CreateProject/component/oss.js new file mode 100644 index 0000000..1e26c88 --- /dev/null +++ b/openpbl-landing/src/pages/Project/CreateProject/component/oss.js @@ -0,0 +1,30 @@ +import OSS from 'ali-oss' + +const ossclient = new OSS({ + region: process.env.REACT_APP_OSS_REGION, + accessKeyId: process.env.REACT_APP_OSS_ACCESSKEYID, + accessKeySecret: process.env.REACT_APP_OSS_ACCESSKEYSECRET, + bucket: process.env.REACT_APP_OSS_BUCKET, +}); + +function buildFileName(path, postfix) { + return path + new Date().getTime() + postfix; +} + +function OSSUploaderFile(file, path, onSucess, onError) { + console.error(file); + const index = file.name.lastIndexOf('.'); + + if (index === -1) { + return onError('file error.'); + } + const postfix = file.name.substr(index); + + ossclient.put(buildFileName(path, postfix), file) + .then((ret) => { + onSucess(ret, file); + }) + .catch(onError); +} + +export default OSSUploaderFile; diff --git a/openpbl-landing/src/pages/Project/EditingProject/index.jsx b/openpbl-landing/src/pages/Project/EditingProject/index.jsx new file mode 100644 index 0000000..3d7ae5a --- /dev/null +++ b/openpbl-landing/src/pages/Project/EditingProject/index.jsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import ProjectList from '../component/ProjectList'; + +class EditingProject extends React.PureComponent { + render() { + return ( + + ); + } +} + +export default EditingProject; diff --git a/openpbl-landing/src/pages/Project/Evidence/component/StudentEvidence.jsx b/openpbl-landing/src/pages/Project/Evidence/component/StudentEvidence.jsx new file mode 100644 index 0000000..aa29da4 --- /dev/null +++ b/openpbl-landing/src/pages/Project/Evidence/component/StudentEvidence.jsx @@ -0,0 +1,218 @@ +import React, {useEffect, useState} from "react"; +import {Collapse, Divider, Input, List, Progress, message, InputNumber, Tooltip, Button} from "antd"; + +import TaskApi from "../../../../api/TaskApi"; +import ChapterApi from "../../../../api/ChapterApi"; +import TaskCard from "../../PreviewProject/component/TaskCard"; +import util from "../../component/Util" +import SubmitApi from "../../../../api/SubmitApi"; + +function StudentEvidence(obj) { + const pid = obj.project === undefined ? obj.pid : obj.project.id + const studentId = obj.studentId === undefined ? "" : obj.studentId + const [tasks, setTasks] = useState([]) + const [learning, setLearning] = useState(false) + const [editable, setEditable] = useState(false) + const [teacherScore, setTeacherScore] = useState(false) + const [chapters, setChapters] = useState([]) + const [showMinute, setShowMinute] = useState(false) + + useEffect(() => { + getChapters() + getTasks() + }, []); + const getChapters = () => { + ChapterApi.getProjectChapters(pid, studentId) + .then((res) => { + if (res.data.chapters === null) { + setChapters([]) + } else { + setChapters(res.data.chapters) + setShowMinute(res.data.showMinute) + } + }) + .catch(e => { + console.log(e) + }) + } + const getTasks = () => { + TaskApi.getProjectTasksDetail(pid, studentId) + .then(res => { + if (res.data.code === 200) { + if (res.data.tasks === null) { + setTasks([]) + } else { + let t = res.data.tasks + for (let i = 0; i < t.length; i++) { + if (t[i].questions !== undefined && t[i].questions != null) { + for (let j = 0; j < t[i].questions.length; j++) { + t[i].questions[j].questionOptions = t[i].questions[j].questionOptions.split(",") + } + } else { + t[i].questions = [] + } + if (t[i].choices !== undefined && t[i].choices != null) { + for (let j = 0; j < t[i].choices.length; j++) { + t[i].choices[j].choiceOptions = t[i].choices[j].choiceOptions.split(",") + } + } else { + t[i].choices = [] + for (let j = 0; j < t[i].questions.length; j++) { + t[i].choices.push({ + choiceOptions: [], + choiceOrder: j + }) + } + } + } + setTasks(t) + setLearning(res.data.learning) + setEditable(res.data.editable) + setTeacherScore(res.data.teacherScore) + } + } + }) + .catch(e => { + console.log(e) + }) + } + const changeScore = (v, index) => { + tasks[index].submit.score = v + setTasks([...tasks]) + } + const saveScore = (index) => { + tasks[index].submit.scored = true + SubmitApi.updateSubmit(pid, tasks[index].id, tasks[index].submit.id, tasks[index].submit) + .then(res=>{ + if (res.data.code === 200) { + message.success(res.data.msg) + getTasks() + } else { + message.error(res.data.msg) + } + }) + .catch(e=>{console.log(e)}) + } + const getScore = (score, weight) => { + return (score * weight / 100).toFixed(2) + } + const setTaskItem = (item, index) => { + tasks[index] = item + setTasks([...tasks]) + } + return ( +
    + +

    章节学习时长

    +
    + {chapters.map((item, index) => ( +
    +

    + {util.FormatChapterName(item.chapterName, item.chapterNumber)} +

    + {(item.sections === null || item.sections === undefined) ? + null + : + <> + ( + + {util.FormatSectionName(item.sectionName, item.chapterNumber, item.sectionNumber)} + {showMinute ? + <> + + + + + 学习进度: + {item.learnMinute} : {item.learnSecond} /  + {item.sectionMinute} + + + : null} + + ) + } + />
    + + } +
    + ))} + + +

    学生任务

    +
    + + + {tasks.map((item, index) => ( + + {util.FormatSectionName(item.taskTitle, item.chapterNumber, item.sectionNumber)} + + {item.submit.scored ? + 已打分 + : + 未打分 + } + + + {item.submitted ? + 已提交 + : + 未提交 + } + + + 权重:{item.taskWeight} + + + 得分:{getScore(item.submit.score, item.taskWeight)} / {item.taskWeight} + + + } + > + {teacherScore ? +
    + + + + + changeScore(v, index)} min={0} max={100} /> / 100 + + +

    教师评分:

    +
    +
    + +
    + : null + } + + +
    + ))} +
    +
    + ) +} + +export default StudentEvidence \ No newline at end of file diff --git a/openpbl-landing/src/pages/Project/Evidence/index.jsx b/openpbl-landing/src/pages/Project/Evidence/index.jsx new file mode 100644 index 0000000..0f475ab --- /dev/null +++ b/openpbl-landing/src/pages/Project/Evidence/index.jsx @@ -0,0 +1,57 @@ +import React, {useEffect} from "react"; +import {Card, PageHeader} from "antd"; +import DocumentTitle from 'react-document-title'; +import StudentEvidence from "./component/StudentEvidence"; + +function Evidence(obj) { + const pid = obj.match.params.pid + const sid = obj.match.params.sid + + useEffect(() => { + }, []) + + const back = e => { + window.location.href = `/project/${pid}/info?menu=student-admin` + } + + return ( + +
    + +
    +
    + + + +
    +
    +
    +
    + ) +} + +export default Evidence \ No newline at end of file diff --git a/openpbl-landing/src/pages/Project/FinishedProject/index.jsx b/openpbl-landing/src/pages/Project/FinishedProject/index.jsx new file mode 100644 index 0000000..dccf2f9 --- /dev/null +++ b/openpbl-landing/src/pages/Project/FinishedProject/index.jsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import ProjectList from '../component/ProjectList'; + +class FinishedProject extends React.PureComponent { + render() { + return ( + + ); + } +} + +export default FinishedProject; diff --git a/openpbl-landing/src/pages/Project/LearningPage/index.jsx b/openpbl-landing/src/pages/Project/LearningPage/index.jsx new file mode 100644 index 0000000..8785dc5 --- /dev/null +++ b/openpbl-landing/src/pages/Project/LearningPage/index.jsx @@ -0,0 +1,58 @@ +import React from 'react'; +import {Card, message, PageHeader, Progress, Upload} from 'antd'; +import {Document, Page} from 'react-pdf'; +import ProjectApi from "../../../api/ProjectApi"; + + +class Learning extends React.PureComponent { + state = { + pid: this.props.match.params.pid, + cid: this.props.match.params.cid, + sid: this.props.match.params.sid, + } + + componentDidMount() { + + } + + onDocumentLoadSuccess = (numPages) => { + this.setState({ + numPages, + }); + }; + onUploadFile = (info) => { + if (info.file.status !== 'uploading') { + console.log(info.file, info.fileList); + } + if (info.file.status === 'done') { + message.success(`${info.file.name} 文件上传成功`); + } else if (info.file.status === 'error') { + message.error(`${info.file.name} 文件上传失败`); + } + } + back = e => { + console.log('back') + this.props.history.push('/project/info/' + this.state.pid) + } + + render() { + const { pid, cid, sid } = this.state + return ( +
    + this.back()} + title="返回" + subTitle="项目信息" + /> +
    + +

    介绍

    +
    +
    +
    + ); + } +} + +export default Learning; diff --git a/openpbl-landing/src/pages/Project/LearningProject/index.jsx b/openpbl-landing/src/pages/Project/LearningProject/index.jsx new file mode 100644 index 0000000..8ed6201 --- /dev/null +++ b/openpbl-landing/src/pages/Project/LearningProject/index.jsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import ProjectList from '../component/ProjectList'; + +class LearningProject extends React.PureComponent { + render() { + return ( + + ); + } +} + +export default LearningProject; diff --git a/openpbl-landing/src/pages/Project/MyProject/index.jsx b/openpbl-landing/src/pages/Project/MyProject/index.jsx new file mode 100644 index 0000000..b37c813 --- /dev/null +++ b/openpbl-landing/src/pages/Project/MyProject/index.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import DocumentTitle from 'react-document-title'; + +import GlobalHeader from '../../component/GlobalHeader/GlobalHeader'; +import MenuBar from "../component/MenuBar"; +import localStorage from "localStorage"; + +class MyProject extends React.PureComponent { + state = { + type: localStorage.getItem('type'), + } + render() { + const {type} = this.state + return ( + +
    + +
    + {type === 'teacher' ? + + : + + } +
    +
    +
    + ); + } +} + + +export default MyProject; diff --git a/openpbl-landing/src/pages/Project/PreviewProject/PreviewSection.jsx b/openpbl-landing/src/pages/Project/PreviewProject/PreviewSection.jsx new file mode 100644 index 0000000..40039b9 --- /dev/null +++ b/openpbl-landing/src/pages/Project/PreviewProject/PreviewSection.jsx @@ -0,0 +1,197 @@ +import React, {useEffect, useState} from "react"; +import {Card, PageHeader, Input, Upload, message, Button, BackTop} from "antd"; +import DocumentTitle from 'react-document-title'; +import {InboxOutlined} from '@ant-design/icons' + +import SectionApi from "../../../api/SectionApi"; +import "../CreateProject/Section/component/section-edit.less" +import "./preview.less" +import TaskApi from "../../../api/TaskApi"; +import util from "../component/Util" +import StudentApi from "../../../api/StudentApi"; +import TaskCard from "./component/TaskCard"; + +function PreviewSection(obj) { + let url = new URLSearchParams(obj.location.search) + const backUrl = url.get('back') + + const sid = obj.match.params.sid + const pid = obj.match.params.pid + const [section, setSection] = useState({resource:{}}) + const [tasks, setTasks] = useState([]) + const [learning, setLearning] = useState(false) + const [editable, setEditable] = useState(false) + + const [minute, setMinute] = useState(0) + const [second, setSecond] = useState(0) + const [timer, setTimer] = useState(null) + let s = 0 + let m = 0 + + useEffect(()=>{ + getSectionDetail() + getTasks() + }, []) + useEffect(()=>{ + window.onbeforeunload = leave + }, []) + const leave = () => { + if (timer != null) { + clearTimeout(timer) + } + let data = { + learnMinute: m, + learnSecond: s + } + StudentApi.updateLearnSection(pid, sid, data) + .then(res=>{ + }) + .catch(e=>{console.log(e)}) + window.removeEventListener("onbeforeunload", leave) + } + + const getSectionDetail = () => { + SectionApi.getSectionDetail(sid, pid) + .then(res=>{ + setSection(res.data.section) + }) + .catch(e=>{console.log(e)}) + } + const getTasks = () => { + TaskApi.getSectionTasks(sid, pid) + .then(res=>{ + if (res.data.tasks === null) { + setTasks([]) + } else { + let t = res.data.tasks + for (let i = 0; i < t.length; i++) { + if (t[i].questions !== undefined && t[i].questions != null) { + for (let j = 0; j < t[i].questions.length; j++) { + t[i].questions[j].questionOptions = t[i].questions[j].questionOptions.split(",") + } + } else { + t[i].questions = [] + } + if (t[i].choices !== undefined && t[i].choices != null) { + for (let j = 0; j < t[i].choices.length; j++) { + t[i].choices[j].choiceOptions = t[i].choices[j].choiceOptions.split(",") + } + } else { + t[i].choices = [] + for (let j=0; j{console.log(e)}) + } + const getTimer = () => { + StudentApi.getLearnSection(pid, sid) + .then(res=>{ + if (res.data.code === 200) { + setSecond(res.data.data.learnSecond) + setMinute(res.data.data.learnMinute) + s = res.data.data.learnSecond + m = res.data.data.learnMinute + } + }) + .catch(e=>{console.log(e)}) + + setTimeout(count, 1000) + } + const count = () => { + if (s === 30) { + s = 0 + m ++ + setMinute(m) + } else { + s++ + } + setSecond(s) + setTimeout(count, 1000) + } + const back = e => { + if (backUrl === undefined || backUrl === null) { + window.location.href = `/project/${pid}/section/${sid}/edit` + } else { + window.location.href = backUrl + } + } + const setTaskItem = (item, index) => { + tasks[index] = item + setTasks([...tasks]) + } + + return ( + +
    + + +
    + +

    + {util.FormatSectionName(section.sectionName, section.chapterNumber, section.sectionNumber)} +

    + {learning ? + {minute} : {second} + : null} +
    + +
    + + +

    文件资源

    +

    {section.resource.fileTitle}

    +

    {section.resource.fileIntroduce}

    +
    + {tasks.map((item, index)=>( + +

    学生任务 + {item.submitted ? + + 权重占比 {item.taskWeight} %  已提交  {util.FilterTime(item.submit.createAt)} + + : + + 权重占比 {item.taskWeight} %  未提交 + + } +

    + + +
    + )) + } +

    +
    + +) +} + +export default PreviewSection diff --git a/openpbl-landing/src/pages/Project/PreviewProject/component/FillSurvey.jsx b/openpbl-landing/src/pages/Project/PreviewProject/component/FillSurvey.jsx new file mode 100644 index 0000000..137bac0 --- /dev/null +++ b/openpbl-landing/src/pages/Project/PreviewProject/component/FillSurvey.jsx @@ -0,0 +1,139 @@ +import React from "react"; +import {Button, Checkbox, Divider, Input, Radio, message} from "antd"; +import qs from 'qs' + +import Question from "../../CreateProject/Survey/component/Question" +import "../preview.less" +import "../../CreateProject/Section/component/section-edit.less" +import SubmitApi from "../../../../api/SubmitApi"; + +const blank = Question.blank + +function FillSurvey(obj) { + const submitSurvey = e => { + obj.item.submit.submitType = 'survey' + let data = Object.assign({}, obj.item.submit) + let c = [...obj.item.choices] + for (let i=0; i{ + if (res.data.code === 200) { + message.success(res.data.msg) + obj.getTasks() + } else { + message.error(res.data.msg) + } + }) + .catch(e=>{console.log(e)}) + } + const updateSurvey = e => { + obj.item.submit.submitType = 'survey' + let data = Object.assign({}, obj.item.submit) + let c = [...obj.item.choices] + for (let i=0; i{ + if (res.data.code === 200) { + message.success(res.data.msg) + obj.getTasks() + } else { + message.error(res.data.msg) + } + }) + .catch(e=>{console.log(e)}) + + } + const changeCheckBox = (v, subIndex) => { + obj.item.choices[subIndex].choiceOptions = v + obj.setTaskItem(obj.item, obj.index) + } + const changeRadio = (v, subIndex) => { + obj.item.choices[subIndex].choiceOptions[0] = v.target.value + obj.setTaskItem(obj.item, obj.index) + } + const changeBlankFill = (v, subIndex, optIndex) => { + obj.item.choices[subIndex].choiceOptions[optIndex] = v.target.value + } + const changeBriefAnswer = (v, subIndex) => { + obj.item.choices[subIndex].choiceOptions[0] = v.target.value + } + return ( +
    +

    {obj.item.survey.surveyTitle}

    + +
    + {obj.item.questions.map((subItem, subIndex)=>( +
    + {subItem.questionType==='singleChoice' || subItem.questionType==='scale5' || subItem.questionType==='scale7' ? +
    +

    {subItem.questionTitle}

    + changeRadio(v, subIndex)}> + {subItem.questionOptions.map((optItem, optIndex) => ( +
    + + {optItem} + +
    + ))} +
    +
    + : null} + {subItem.questionType==='multipleChoice' ? +
    +

    {subItem.questionTitle}

    + changeCheckBox(v, subIndex)}> + {subItem.questionOptions.map((optItem, optIndex) => ( +
    + + {optItem} + +
    + ))} +
    +
    + : null} + {subItem.questionType==='blankFill' ? +
    +

    {subItem.questionTitle}

    + {subItem.questionOptions.map((optItem, optIndex) => ( +
    + {optItem === blank ? + + changeBlankFill(v, subIndex, optIndex)} style={{borderBottom: '2px solid black'}} bordered={false}/> + + : + {optItem}} +
    + ))} +
    + : null} + + {subItem.questionType==='briefAnswer' ? +
    +

    {subItem.questionOptions[0]}

    + changeBriefAnswer(v, subIndex)}/> +
    + : null} + + +
    + ))} +
    +
    + {obj.item.submitted ? + + : + + } +
    +
    + ) +} + +export default FillSurvey \ No newline at end of file diff --git a/openpbl-landing/src/pages/Project/PreviewProject/component/TaskCard.jsx b/openpbl-landing/src/pages/Project/PreviewProject/component/TaskCard.jsx new file mode 100644 index 0000000..4f62866 --- /dev/null +++ b/openpbl-landing/src/pages/Project/PreviewProject/component/TaskCard.jsx @@ -0,0 +1,106 @@ +import React from "react"; +import {Button, Input, message, Upload} from "antd"; +import {InboxOutlined} from "@ant-design/icons"; +import FillSurvey from "./FillSurvey"; +import SubmitApi from "../../../../api/SubmitApi"; + + +function TaskCard(obj) { + + const updateComment = (item, index) => { + SubmitApi.updateSubmit(obj.pid, item.id, item.submit.id, item.submit) + .then(res=>{ + if (res.data.code === 200) { + message.success(res.data.msg) + obj.getTasks() + } else { + message.error(res.data.msg) + } + }) + .catch(e=>{console.log(e)}) + } + const submitComment = (item, index) => { + item.submit.submitType = item.taskType + SubmitApi.createSubmit(obj.pid, item.id, item.submit) + .then(res=>{ + if (res.data.code === 200) { + message.success(res.data.msg) + obj.getTasks() + } else { + message.error(res.data.msg) + } + }) + .catch(e=>{console.log(e)}) + } + const changeComment = (v, index) => { + obj.item.submit.submitContent = v.target.value + obj.setTaskItem(obj.item, index) + } + + const props = { + name: 'file', + multiple: true, + action: '', + onChange(info) { + const { status } = info.file; + if (status !== 'uploading') { + console.log(info.file, info.fileList); + } + if (status === 'done') { + message.success(`${info.file.name} 上传成功`); + } else if (status === 'error') { + message.error(`${info.file.name} 上传失败`); + } + }, + onDrop(e) { + console.log('Dropped files', e.dataTransfer.files); + }, + }; + + return ( + <> +

    {obj.item.taskTitle}

    +

    {obj.item.taskIntroduce}

    + {obj.item.taskType === 'file' ? +
    + +

    + +

    +

    点击或拖动文件上传

    +

    hint +

    +
    +
    + : null + } + {obj.item.taskType === 'comment' ? +
    + changeComment(v, obj.index)} /> +
    + {obj.item.submitted ? + + : + + } +
    +
    + : null + } + {obj.item.taskType === 'survey' ? + + : null + } + + ) +} + +export default TaskCard \ No newline at end of file diff --git a/openpbl-landing/src/pages/Project/PreviewProject/preview.less b/openpbl-landing/src/pages/Project/PreviewProject/preview.less new file mode 100644 index 0000000..d906411 --- /dev/null +++ b/openpbl-landing/src/pages/Project/PreviewProject/preview.less @@ -0,0 +1,7 @@ +.survey{ + max-width: 1200px; + padding: 20px; + margin: auto; + background-color: rgba(244, 231, 174, 0.07); + border-radius: 10px; +} \ No newline at end of file diff --git a/openpbl-landing/src/pages/Project/ProjectInfo/component/ProjectComment.jsx b/openpbl-landing/src/pages/Project/ProjectInfo/component/ProjectComment.jsx new file mode 100644 index 0000000..ba11986 --- /dev/null +++ b/openpbl-landing/src/pages/Project/ProjectInfo/component/ProjectComment.jsx @@ -0,0 +1,155 @@ +import React, {createElement, useEffect, useState} from 'react'; +import {DislikeOutlined, LikeOutlined} from '@ant-design/icons'; +import {Avatar, Button, Comment, Form, Input, Pagination, Tooltip} from 'antd'; +import QueueAnim from 'rc-queue-anim'; + +const commentData = [ + { + avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png', + author: 'aaaa', + content: '评论内容', + date: '2021-6-6', + likes: 1, + dislikes: 0 + }, + { + avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png', + author: 'aaaa', + content: '评论内容', + date: '2021-6-6', + likes: 0, + dislikes: 1 + }, + { + avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png', + author: 'aaaa', + content: '评论内容', + date: '2021-6-6', + likes: 1, + dislikes: 0 + }, + { + avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png', + author: 'aaaa', + content: '评论内容', + date: '2021-6-6', + likes: 0, + dislikes: 0 + }, + { + avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png', + author: 'aaaa', + content: '评论内容', + date: '2021-6-6', + likes: 0, + dislikes: 0 + }, +]; + +const {TextArea} = Input; + +function like(item) { + console.log(item); +} + +function dislike(item) { + console.log(item); +} + +function ProjectComment(obj) { + const [project, setProject] = useState({}); + const [commentList, setCommentList] = useState([]); + const [actions, setActions] = useState([]); + const [page, setPage] = useState(1); + const [total, setTotal] = useState(0); + const [value, setValue] = useState(''); + const [submitting, setSubmitting] = useState(false); + + const updateCommentList = (p, size) => { + // TODO axios.get comment list + console.log(p, size); + setCommentList(commentData); + setTotal(100); + setPage(p); + }; + useEffect(() => { + setProject(obj.project); + updateCommentList(1, 10); + }, []); + + const onSubmit = () => { + console.log(value); + setSubmitting(true); + setTimeout(() => { + setSubmitting(false); + setValue(''); + }, 500); + }; + + const onChange = (v) => { + setValue(v.target.value); + }; + + return ( + +
    + +