Sfoglia il codice sorgente

merge dev分分分支,解决冲突

Johnson 7 anni fa
parent
commit
3f41a435dc
58 ha cambiato i file con 1427 aggiunte e 584 eliminazioni
  1. 6 6
      mock/modules/auth/index.js
  2. 11 5
      mock/modules/course/index.js
  3. 0 57
      mock/modules/doc/index.js
  4. 4 4
      mock/modules/group/index.js
  5. 89 0
      mock/modules/review/index.js
  6. 0 12
      mock/modules/tag/index.js
  7. 3 3
      mock/modules/user/index.js
  8. 2 3
      mock/routes.js
  9. 0 67
      mock/serializers/DocSerializer.js
  10. 0 10
      mock/serializers/DocSimpleSerializer.js
  11. 0 15
      mock/serializers/TagSerializer.js
  12. 2 0
      mock/serializers/review/ReviewResultSerializer.js
  13. 4 1
      mock/serializers/review/ReviewSimpleSerializer.js
  14. 1 1
      mock/serializers/user/UserSerializer.js
  15. 1 22
      src/App.vue
  16. 1 0
      src/api/_prefix.js
  17. 4 4
      src/api/auth.js
  18. 11 0
      src/api/course.js
  19. 150 0
      src/api/review.js
  20. BIN
      src/assets/login_background.png
  21. 11 19
      src/assets/scss/app.scss
  22. 64 0
      src/components/Card/index.vue
  23. 2 0
      src/data/userType.js
  24. 0 8
      src/layouts/ExampleLayout.vue
  25. 87 0
      src/layouts/HomeLayout/HomeHeader/index.vue
  26. 30 0
      src/layouts/HomeLayout/index.vue
  27. 30 9
      src/layouts/LoginLayout.vue
  28. 104 0
      src/layouts/courseLayouts/LayoutStructure.vue
  29. 15 0
      src/layouts/courseLayouts/StudentCourseLayout.vue
  30. 48 0
      src/layouts/courseLayouts/StudentMenu.vue
  31. 15 0
      src/layouts/courseLayouts/TeacherCourseLayout.vue
  32. 40 0
      src/layouts/courseLayouts/TeacherMenu.vue
  33. 20 45
      src/router/index.js
  34. 7 5
      src/router/name.js
  35. 107 0
      src/router/routes.js
  36. 0 62
      src/store/auth.module.js
  37. 3 3
      src/store/index.js
  38. 26 0
      src/store/modules/course.module.js
  39. 28 0
      src/store/modules/user.module.js
  40. 1 6
      src/store/type/actions.type.js
  41. 1 7
      src/store/type/mutations.type.js
  42. 0 83
      src/store/user.module.js
  43. 12 0
      src/util/auth.js
  44. 2 2
      src/util/profileStorage.js
  45. 37 28
      src/util/request.js
  46. 0 12
      src/util/tokenStorage.js
  47. 0 3
      src/views/About.vue
  48. 0 15
      src/views/Home.vue
  49. 93 67
      src/views/login/Login.vue
  50. 194 0
      src/views/login/Register.vue
  51. 3 0
      src/views/student/Code/index.vue
  52. 3 0
      src/views/student/Dashboard/index.vue
  53. 3 0
      src/views/student/Document/index.vue
  54. 3 0
      src/views/student/Group/index.vue
  55. 39 0
      src/views/student/Home/index.vue
  56. 68 0
      src/views/student/Review/index.vue
  57. 3 0
      src/views/teacher/Dashboard/index.vue
  58. 39 0
      src/views/teacher/Home/index.vue

+ 6 - 6
mock/modules/auth/index.js

@@ -1,6 +1,6 @@
 const express = require("express");
 const router = express.Router();
-const UserSerializer = require("../../serializers/user/UserSerializer");
+const userSerializer = require("../../serializers/user/UserSerializer");
 
 router.route("/login").post((req, res) => {
   //登录
@@ -8,7 +8,7 @@ router.route("/login").post((req, res) => {
   const setSendValue = role => {
     res.append("Authorization", token);
     res.send({
-      data: UserSerializer(role)
+      data: userSerializer(role)
     });
   };
   const { body = {} } = req;
@@ -19,19 +19,19 @@ router.route("/login").post((req, res) => {
     case "teacher":
       setSendValue("TEACHER");
       break;
-    case "error":
+    case "error@123.com":
       res.status(418).send({ message: "用户名密码不匹配" });
       break;
     default:
-      setSendValue("STUDENT");
+      setSendValue();
       break;
   }
 });
 
 router.route("/register").post((req, res) => {
   //注册
-  res.send({ data: UserSerializer() });
-  // res.status(418).send({ message: '用户名已存在' });
+  res.send({ data: userSerializer() });
+  // res.status(400).send({ message: "用户名已存在" });
 });
 
 module.exports = router;

+ 11 - 5
mock/modules/course/index.js

@@ -1,17 +1,17 @@
 const express = require("express");
 const router = express.Router();
-const CourseSerializer = require("../../serializers/course/CourseSerializer");
+const courseSerializer = require("../../serializers/course/CourseSerializer");
 
 router
   .route("/teacher")
   .get((req, res) => {
     res.send({
-      data: [CourseSerializer(), CourseSerializer()]
+      data: [courseSerializer(), courseSerializer()]
     });
   })
   .post((req, res) => {
     res.send({
-      data: CourseSerializer()
+      data: courseSerializer()
     });
   });
 
@@ -20,14 +20,20 @@ router
   .get((req, res) => {
     //修改用户信息
     res.send({
-      data: [CourseSerializer(), CourseSerializer()]
+      data: [courseSerializer(), courseSerializer()]
     });
   })
   .post((req, res) => {
     //修改用户信息
     res.send({
-      data: CourseSerializer()
+      data: courseSerializer()
     });
   });
 
+router.route("/course/:courseId").get((req, res) => {
+  res.send({
+    data: courseSerializer()
+  });
+});
+
 module.exports = router;

+ 0 - 57
mock/modules/doc/index.js

@@ -1,57 +0,0 @@
-const express = require("express");
-const router = express.Router();
-const DocSimpleSerializer = require("../../serializers/DocSimpleSerializer");
-const DocSerializer = require("../../serializers/DocSerializer");
-const DocResultSerializer = require("../../serializers/review/ReviewResultSerializer");
-const CheckListItemSerializer = require("../../serializers/review/CheckListItemSerializer");
-
-router.route("").get((req, res) => {
-  //获得文档列表
-  res.send({
-    data: [
-      DocSimpleSerializer(req.query.self),
-      DocSimpleSerializer(),
-      DocSimpleSerializer(),
-      DocSimpleSerializer(),
-      DocSimpleSerializer()
-    ]
-  });
-});
-
-router.route("/:docId").get((req, res) => {
-  //获得文档详情
-  res.send({
-    data: DocSerializer()
-  });
-});
-
-router
-  .route("/:docId/checklist")
-  .get((req, res) => {
-    res.send({
-      data: [
-        CheckListItemSerializer(),
-        CheckListItemSerializer(),
-        CheckListItemSerializer(),
-        CheckListItemSerializer()
-      ]
-    });
-  })
-  .post((req, res) => {
-    res.send({
-      data: [
-        CheckListItemSerializer(),
-        CheckListItemSerializer(),
-        CheckListItemSerializer(),
-        CheckListItemSerializer()
-      ]
-    });
-  });
-
-router.route("/:docId/result").get((req, res) => {
-  res.send({
-    data: [DocResultSerializer(), DocResultSerializer(true)] //Result
-  });
-});
-
-module.exports = router;

+ 4 - 4
mock/modules/group/index.js

@@ -1,22 +1,22 @@
 const express = require("express");
 const router = express.Router();
-const GroupSerializer = require("../../serializers/course/GroupSerializer");
+const groupSerializer = require("../../serializers/course/GroupSerializer");
 
 router
   .route("")
   .get((req, res) =>
     res.send({
-      data: [GroupSerializer()]
+      data: [groupSerializer()]
     })
   )
   .post((req, res) =>
     res.send({
-      data: GroupSerializer()
+      data: groupSerializer()
     })
   )
   .put((req, res) =>
     res.send({
-      data: GroupSerializer()
+      data: groupSerializer()
     })
   );
 

+ 89 - 0
mock/modules/review/index.js

@@ -0,0 +1,89 @@
+const express = require("express");
+const router = express.Router();
+const reviewSimpleSerializer = require("../../serializers/review/ReviewSimpleSerializer");
+const reviewDetailSerializer = require("../../serializers/review/ReviewDetailSerializer");
+const checkListItemSerializer = require("../../serializers/review/CheckListItemSerializer");
+const reviewResultSerializer = require("../../serializers/review/ReviewResultSerializer");
+
+router.route("/review").post((req, res) =>
+  res.send({
+    data: reviewSimpleSerializer()
+  })
+);
+
+router.route("/argue").post((req, res) => res.send({}));
+
+router.route("/teacher/course/:courseId/review").get((req, res) =>
+  res.send({
+    data: [reviewSimpleSerializer(), reviewSimpleSerializer()]
+  })
+);
+
+router.route("/teacher/review/:reviewId").get((req, res) =>
+  res.send({
+    data: reviewDetailSerializer()
+  })
+);
+
+router.route("/student/course/:courseId/review").get((req, res) =>
+  res.send({
+    data: [reviewSimpleSerializer(), reviewSimpleSerializer()]
+  })
+);
+
+router
+  .route("/student/review/:reviewId")
+  .get((req, res) =>
+    res.send({
+      data: reviewDetailSerializer()
+    })
+  )
+  .post((req, res) =>
+    res.send({
+      data: [checkListItemSerializer(), checkListItemSerializer()]
+    })
+  );
+
+router.route("/student/course/:courseId/result").get((req, res) =>
+  res.send({
+    data: [reviewSimpleSerializer(), reviewSimpleSerializer()]
+  })
+);
+
+router.route("/student/review/:reviewId/result").get((req, res) =>
+  res.send({
+    data: reviewResultSerializer()
+  })
+);
+
+router.route("/sum/:reviewId").get((req, res) =>
+  res.send({
+    detail: reviewDetailSerializer(),
+    finishPercentage: 0.7,
+    studentIdCheckListAcceptNumMap: {
+      1: 1,
+      2: 3
+    }
+  })
+);
+
+router.route("/sum/:reviewId/level").post((req, res) => {
+  res.send({});
+});
+
+router.route("/sum/:reviewId/argue").get((req, res) =>
+  res.send({
+    data: [reviewSimpleSerializer(), reviewSimpleSerializer()]
+  })
+);
+
+router
+  .route("/sum/:reviewId/argue/:groupId")
+  .get((req, res) =>
+    res.send({
+      data: reviewResultSerializer()
+    })
+  )
+  .post((req, res) => res.send({}));
+
+module.exports = router;

+ 0 - 12
mock/modules/tag/index.js

@@ -1,12 +0,0 @@
-const express = require("express");
-const router = express.Router();
-const TagSerializer = require("../../serializers/TagSerializer");
-
-router.route("").get((req, res) => {
-  //获得用户标签列表
-  res.send({
-    data: [TagSerializer(), TagSerializer(), TagSerializer()]
-  });
-});
-
-module.exports = router;

+ 3 - 3
mock/modules/user/index.js

@@ -1,11 +1,11 @@
 const express = require("express");
 const router = express.Router();
-const UserSerializer = require("../../serializers/user/UserSerializer");
+const userSerializer = require("../../serializers/user/UserSerializer");
 
 router.route("/:userId").get((req, res) => {
   //获得用户信息
   res.send({
-    data: UserSerializer(),
+    data: userSerializer(),
     abilities: {
       update: true
     }
@@ -15,7 +15,7 @@ router.route("/:userId").get((req, res) => {
 router.route("/:userId").post((req, res) => {
   //修改用户信息
   res.send({
-    data: UserSerializer(),
+    data: userSerializer(),
     abilities: {
       update: true
     }

+ 2 - 3
mock/routes.js

@@ -1,13 +1,12 @@
 const express = require("express");
 const router = express.Router();
 
-router.use("/login", require("./modules/auth"));
+router.use("/auth", require("./modules/auth"));
 router.use("/user", require("./modules/user"));
-// router.use("/tag", require("./modules/tag"));
-// router.use("/doc", require("./modules/doc"));
 router.use("/course", require("./modules/course"));
 router.use("/group", require("./modules/group"));
 router.use("/question", require("./modules/question"));
 router.use("/assignment", require("./modules/assignment"));
+router.use("/review", require("./modules/review"));
 
 module.exports = router;

+ 0 - 67
mock/serializers/DocSerializer.js

@@ -1,67 +0,0 @@
-const makeId = require("../util/makeId");
-const tagSerializer = require("./TagSerializer");
-
-module.exports = (filename = "需求规格说明文档") => {
-  return {
-    id: makeId(),
-    filename,
-    owner: tagSerializer(),
-    content:
-      "# 需求规格说明文档\n\n" +
-      "> 参照语雀API基本要求\n" +
-      "\n" +
-      "## 基本路径\n" +
-      "* `/api/v2/`\n" +
-      "\n" +
-      "## HTTP Verbs\n" +
-      "\n" +
-      "| Verb | Description |\n" +
-      "| :--- | :--- |\n" +
-      "| GET | 用于获取数据 |\n" +
-      "| POST | 用于创建数据 |\n" +
-      "| PUT | 用于修改部分数据,例如一个文档标题,正文 |\n" +
-      "| DELETE | 用于删除数据 |\n" +
-      "\n" +
-      "## 用户认证\n" +
-      "使用 Token 机制来实现用户认证。\n" +
-      "在请求的 HTTP Headers 传入 `Authorization` 带入用户的 Token 信息,用于认证。\n" +
-      "\n" +
-      "文档会在需要用户权限的api中标识 __[需要认证] __\n" +
-      "\n" +
-      "## HTTP 状态码\n" +
-      "* 200 - 成功\n" +
-      "* 400 - 请求的参数不正确,或缺少必要信息,请对比文档\n" +
-      "* 401 - 需要用户认证的接口用户信息不正确,即__Token错__\n" +
-      "* 403 - 缺少对应功能的权限,即__权限不足__\n" +
-      "* 404 - 数据不存在,或未开放\n" +
-      "* 500 - 服务器异常\n" +
-      "\n" +
-      "## 返回数据格式[示例]\n" +
-      "* JSON 格式\n" +
-      "\n" +
-      "```json\n" +
-      "{\n" +
-      '    "data": {\n' +
-      '        "id": 10,\n' +
-      '        "slug": "weekly",\n' +
-      '        "name": "技术周刊",\n' +
-      "     },\n" +
-      '    "abilities": {\n' +
-      '        "update": false,\n' +
-      '        "destroy": false\n' +
-      "    },\n" +
-      '    "meta": {\n' +
-      '        "liked": false,\n' +
-      '        "followed": false,\n' +
-      "    }\n" +
-      "}\n" +
-      "```\n" +
-      "\n" +
-      "* id: 每个数据都会有的,Resource 的唯一编号,后续很多地方你可能需要用它查询\n" +
-      "* abilities: 表述当前登陆者对于此资源的权限\n" +
-      "* meta: 一些附加信息,例如是否赞过,是否关注过\n" +
-      "\n" +
-      "## DateTime 格式\n" +
-      "DateTime 使用 [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) 标准格式,请按照标准方式进行转换。\n"
-  };
-};

+ 0 - 10
mock/serializers/DocSimpleSerializer.js

@@ -1,10 +0,0 @@
-const makeId = require("../util/makeId");
-const tagSerializer = require("./TagSerializer");
-
-module.exports = (filename = "需求规格说明文档") => {
-  return {
-    id: makeId(),
-    filename,
-    owner: tagSerializer()
-  };
-};

+ 0 - 15
mock/serializers/TagSerializer.js

@@ -1,15 +0,0 @@
-const makeId = require("../util/makeId");
-const userSerializer = require("./user/UserSerializer");
-
-module.exports = (type = "GROUP", name = "TagBaker") => {
-  return {
-    id: makeId(),
-    type,
-    shareLink:
-      "https://bequank.oss-cn-beijing.aliyuncs.com/web-gallery/pixiv39957555.jpg?x-oss-process=style/avatar",
-    name,
-    owner: [userSerializer(), userSerializer()],
-    createdAt: "2010-10-21",
-    updatedAt: "2010-11-21"
-  };
-};

+ 2 - 0
mock/serializers/review/ReviewResultSerializer.js

@@ -8,6 +8,7 @@ const checkListItemSerializer = require("./CheckListItemSerializer");
  * @property {number|string} id ReviewResultSerializer
  * @property {ReviewDetailSerializerType} detail 文档详情
  * @property {string} level 文档等级
+ * @property {number} score 文档分数
  * @property {CheckListResult[]} checkListResult 互评作业的结果
  */
 
@@ -58,6 +59,7 @@ const reviewResultSerializer = () => {
     id: makeId(),
     detail: reviewDetailSerializer(),
     level: "A",
+    score: 80,
     checkListResult: [checkListResultItem(), checkListResultItem()]
   };
 };

+ 4 - 1
mock/serializers/review/ReviewSimpleSerializer.js

@@ -1,6 +1,7 @@
 const makeId = require("../../util/makeId");
 const courseSerializer = require("../course/CourseSerializer");
 const dayjs = require("dayjs");
+const groupSerializer = require("../course/GroupSerializer");
 
 /**
  * @description 互评checkListItem详细实体类
@@ -10,6 +11,7 @@ const dayjs = require("dayjs");
  * @property {CourseSerializerType} course 所属课程
  * @property {number} beginAt 开始时间
  * @property {number} endAt 截止时间
+ * @property {GroupSerializerType} group 所属小组
  */
 
 /**
@@ -22,7 +24,8 @@ const reviewSimpleSerializer = () => {
     name: "该项评价的内容, String",
     course: courseSerializer(),
     beginAt: dayjs("2018-10-21").unix(),
-    endAt: dayjs("2018-12-21").unix()
+    endAt: dayjs("2018-12-21").unix(),
+    group: groupSerializer()
   };
 };
 

+ 1 - 1
mock/serializers/user/UserSerializer.js

@@ -14,7 +14,7 @@ const makeId = require("../../util/makeId");
  * @param role
  * @returns {UserSerializerType}
  */
-module.exports = (role = "STUDENT") => {
+module.exports = (role = "TEACHER") => {
   return {
     id: makeId(),
     role: role,

+ 1 - 22
src/App.vue

@@ -1,30 +1,9 @@
 <template>
-  <div id="app">
-    <!--<div id="nav">-->
-    <!--<router-link to="/">Home</router-link> |-->
-    <!--<router-link to="/about">About</router-link>-->
-    <!--</div>-->
-    <router-view />
-  </div>
+  <div id="app"><router-view /></div>
 </template>
 
 <style lang="scss">
 #app {
-  /*font-family: "Avenir", Helvetica, Arial, sans-serif;*/
-  /*-webkit-font-smoothing: antialiased;*/
-  /*-moz-osx-font-smoothing: grayscale;*/
-  /*text-align: center;*/
-  /*color: #2c3e50;*/
   background: #f4f7fb;
 }
-/*#nav {*/
-/*padding: 30px;*/
-/*a {*/
-/*font-weight: bold;*/
-/*color: #2c3e50;*/
-/*&.router-link-exact-active {*/
-/*color: #42b983;*/
-/*}*/
-/*}*/
-/*}*/
 </style>

+ 1 - 0
src/api/_prefix.js

@@ -8,3 +8,4 @@ export const COURSE_MODULE = `${API_VERSION}/course`;
 export const GROUP_MODULE = `${API_VERSION}/group`;
 export const QUESTION_MODULE = `${API_VERSION}/question`;
 export const ASSIGNMENT_MODULE = `${API_VERSION}/assignment`;
+export const REVIEW_MODULE = `${API_VERSION}/review`;

+ 4 - 4
src/api/auth.js

@@ -19,18 +19,18 @@ export const login = ({ username, password }) => {
 
 /**
  * 注册 register
- * @param {string} nickname
+ * @param {string|null} nickname
  * @param {string} username
  * @param {string} password
  * @returns {Promise<{data:UserSerializerType}>}
  */
-export const register = ({ nickname, username, password }) => {
+export const register = ({ nickname = null, username, password }) => {
   return request(`${AUTH_MODULE}/register`, {
     method: "POST",
     body: {
       nickname: nickname || null,
-      username: username || null,
-      password: password || null
+      username,
+      password
     }
   });
 };

+ 11 - 0
src/api/course.js

@@ -22,6 +22,7 @@ export const getStudentCourses = () => {
  * @param {string} name
  * @param {string} code
  * @returns {Promise<{data:CourseSerializerType}>}
+ * @throws {StatusException} "课程名重复" | "课程选课码重复"
  */
 export const postTeacherCourse = ({ name, code }) => {
   return request(`${COURSE_MODULE}/teacher`, {
@@ -37,6 +38,7 @@ export const postTeacherCourse = ({ name, code }) => {
  * 学生加入课程
  * @param {string} code
  * @returns {Promise<{data:CourseSerializerType}>}
+ * @throws {StatusException} "选课码不存在" | "已经加入课程"
  */
 export const postStudentCourse = code => {
   return request(`${COURSE_MODULE}/student`, {
@@ -46,3 +48,12 @@ export const postStudentCourse = code => {
     }
   });
 };
+
+/**
+ * 获得课程详情
+ * @param courseId
+ * @returns {Promise<{data:CourseSerializerType}>}
+ */
+export const getCourseDetail = courseId => {
+  return request(`${COURSE_MODULE}/course/${courseId}`);
+};

+ 150 - 0
src/api/review.js

@@ -0,0 +1,150 @@
+import request from "@/util/request";
+import { REVIEW_MODULE } from "./_prefix";
+
+/**
+ * 新建互评作业
+ * @param {number} homeworkId 作业id
+ * @param {number} beginAt 开始时间
+ * @param {number} endAt 截止时间
+ * @param {number} taskNumber 互评任务数
+ * @returns {Promise<ReviewSimpleSerializerType>}
+ * @throws {StatusException} 输入不合法
+ */
+export const createReview = ({ homeworkId, beginAt, endAt, taskNumber }) => {
+  return request(`${REVIEW_MODULE}/review`, {
+    method: "POST",
+    body: { homeworkId, beginAt, endAt, taskNumber }
+  });
+};
+
+/**
+ * 学生申请解释互评作业
+ * @param homeworkId
+ * @param checkListItemId
+ * @param judgeItemId
+ * @param remark
+ * @returns {Promise<{}>}
+ * @throws {StatusException} 输入不合法
+ */
+export const argueReviewHomework = ({
+  homeworkId,
+  checkListItemId,
+  judgeItemId,
+  remark
+}) => {
+  return request(`${REVIEW_MODULE}/argue`, {
+    method: "POST",
+    body: { homeworkId, checkListItemId, judgeItemId, remark }
+  });
+};
+
+/**
+ * 老师查看互评作业列表
+ * @param {string|number} courseId
+ * @return {Promise<{data:ReviewSimpleSerializerType[]}>}
+ */
+export const getTeacherCourseReviewList = courseId => {
+  return request(`${REVIEW_MODULE}/teacher/course/${courseId}/review`);
+};
+
+/**
+ * 老师查看互评作业详情
+ * @param {string|number} reviewId
+ * @return {Promise<{data:ReviewDetailSerializerType}>}
+ */
+export const getReviewDetail = reviewId => {
+  return request(`${REVIEW_MODULE}/teacher/review/${reviewId}`);
+};
+
+/**
+ * 学生查看互评作业列表
+ * @param {string|number} courseId
+ * @return {Promise<{data:ReviewSimpleSerializerType[]}>}
+ */
+export const getStudentCourseReviewList = courseId => {
+  return request(`${REVIEW_MODULE}/student/course/${courseId}/review`);
+};
+
+/**
+ * 学生做互评作业
+ * @param {string|number} reviewId
+ * @param {{data:CheckListItemSerializerType}} checkListBody
+ * @return {Promise<{data:CheckListItemSerializerType[]}>}
+ * @throws {StatusException} 输入不合法
+ */
+export const postStudentReviewWork = (reviewId, checkListBody) => {
+  return request(`${REVIEW_MODULE}/student/review/${reviewId}`, {
+    method: "POST",
+    body: checkListBody
+  });
+};
+
+/**
+ * 学生查看互评结果列表
+ * @param {string|number} courseId
+ * @return {Promise<{data:ReviewSimpleSerializerType[]}>}
+ */
+export const getStudentReviewResultList = courseId => {
+  return request(`${REVIEW_MODULE}/student/course/${courseId}/result`);
+};
+
+/**
+ * 学生查看某次互评结果
+ * @param {string|number} reviewId
+ * @return {Promise<{data:ReviewResultSerializerType}>}
+ */
+export const getStudentReviewResultDetail = reviewId => {
+  return request(`${REVIEW_MODULE}/student/review/${reviewId}/result`);
+};
+
+/**
+ * 老师查看互评作业汇总结果
+ * @param {string|number} reviewId
+ * @return {Promise<{detail:ReviewDetailSerializerType,finishPercentage:number,studentIdCheckListAcceptNumMap:*}>}
+ */
+export const getReviewSumResult = reviewId => {
+  return request(`${REVIEW_MODULE}/sum/${reviewId}`);
+};
+
+/**
+ * 老师更新作业评分标准
+ * @param {string|number} reviewId
+ * @param {*} level
+ * @return {Promise<{}>}
+ * @throws {StatusException} 输入不合法
+ */
+export const postReviewSumLevel = (reviewId, level) => {
+  return request(`${REVIEW_MODULE}/sum/${reviewId}/level`, {
+    method: "POST",
+    body: level
+  });
+};
+
+/**
+ * 老师获得需要人工复核的文档列表
+ * @param {string|number} reviewId
+ * @return {Promise<ReviewSimpleSerializerType[]>}
+ */
+export const getTeacherArgueList = reviewId => {
+  return request(`${REVIEW_MODULE}/sum/${reviewId}/argue`);
+};
+
+/**
+ * 老师获得需要人工复核的文档详情
+ * @param {string|number} reviewId
+ * @param {string|number} groupId
+ * @return {Promise<{data:ReviewResultSerializerType}>}
+ */
+export const getTeacherArgueDetail = (reviewId, groupId) => {
+  return request(`${REVIEW_MODULE}/sum/${reviewId}/argue/${groupId}`);
+};
+
+/**
+ * 老师提交需要复核文档的结果
+ * @param {string|number} reviewId
+ * @param {string|number} groupId
+ * @return {Promise<{}>}
+ */
+export const postTeacherArgueResult = (reviewId, groupId) => {
+  return request(`${REVIEW_MODULE}/sum/${reviewId}/argue/${groupId}`);
+};

BIN
src/assets/login_background.png


+ 11 - 19
src/assets/scss/app.scss

@@ -1,28 +1,20 @@
 @import "~bulma/sass/utilities/initial-variables";
 @import "~bulma/sass/utilities/functions";
-// 1. Set your own initial variables and derived 
-//    variables in _variables.scss
 @import "variables";
 
-// 2. Setup your Custom Colors
-$linkedin: #0077b5;
-$linkedin-invert: findColorInvert($linkedin);
-$twitter: #55acee;
-$twitter-invert: findColorInvert($twitter);
-$github: #333;
-$github-invert: findColorInvert($github);
-
 @import "~bulma/sass/utilities/derived-variables";
 
-// 3. Add new color variables to the color map.
-$addColors: (
-  "twitter":($twitter, $twitter-invert),
-  "linkedin": ($linkedin, $linkedin-invert),
-  "github": ($github, $github-invert)
-);
-$colors: map-merge($colors, $addColors);
-
 @import "~bulma";
 @import "~buefy/src/scss/buefy";
 
-// 4. Provide custom buefy overrides and site styles here
+
+$border-color:#b9c7d8;
+$default-border:1px solid #c9d8ea;
+
+.has-text-primary{
+  color:#209cee !important;
+}
+
+.input{
+  box-shadow: none !important;
+}

+ 64 - 0
src/components/Card/index.vue

@@ -0,0 +1,64 @@
+<template>
+  <div
+    class="custom-card"
+    :class="{ hoverable: hoverable }"
+    :style="{ padding: hasPadding ? '20px' : 0 }"
+  >
+    <slot></slot>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    hoverable: {
+      type: Boolean,
+      default: false
+    },
+    hasPadding: {
+      type: Boolean,
+      default: true
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../assets/scss/app";
+
+.custom-card {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  background: #ffffff;
+  border: $default-border;
+  color: #152935;
+}
+
+.hoverable {
+  cursor: pointer;
+  position: relative;
+  text-decoration: none;
+  z-index: 1;
+}
+.hoverable:after {
+  pointer-events: none;
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  content: "";
+  box-sizing: content-box;
+  top: -5px;
+  left: -5px;
+  padding: 5px;
+  box-shadow: 0 0 0 4px #8897a2;
+  transition: transform 0.2s, opacity 0.2s;
+  transform: scaleY(0.9) scaleX(0.95);
+  opacity: 0;
+}
+
+.hoverable:hover:after {
+  transform: scale(1);
+  opacity: 1;
+}
+</style>

+ 2 - 0
src/data/userType.js

@@ -0,0 +1,2 @@
+export const TEACHER = "TEACHER";
+export const STUDENT = "STUDENT";

+ 0 - 8
src/layouts/ExampleLayout.vue

@@ -1,8 +0,0 @@
-<template>
-  <div>
-    <div></div>
-    <router-view></router-view>
-  </div>
-</template>
-
-<style lang="scss"></style>

+ 87 - 0
src/layouts/HomeLayout/HomeHeader/index.vue

@@ -0,0 +1,87 @@
+<template>
+  <nav class="navbar nav-section">
+    <div class="container">
+      <div class="navbar-brand">
+        <a class="navbar-item header-logo"> MOOC </a>
+        <a class="navbar-item"><b-icon pack="fab" icon="github"></b-icon></a>
+        <a class="navbar-item"><b-icon pack="fab" icon="twitter"></b-icon></a>
+      </div>
+
+      <div class="navbar-menu">
+        <div class="navbar-end">
+          <b-dropdown position="is-bottom-left">
+            <a class="navbar-item" slot="trigger">
+              <span>{{ name }}</span>
+              <b-icon icon="menu-down"></b-icon>
+            </a>
+
+            <b-dropdown-item custom>
+              以 <b>{{ name }}</b> 的身份登录
+            </b-dropdown-item>
+            <hr class="dropdown-divider" />
+            <b-dropdown-item value="settings">
+              <b-icon icon="settings"></b-icon>
+              个人设置
+            </b-dropdown-item>
+            <b-dropdown-item value="logout">
+              <b-icon icon="logout"></b-icon>
+              登出
+            </b-dropdown-item>
+          </b-dropdown>
+        </div>
+      </div>
+    </div>
+  </nav>
+</template>
+
+<script>
+import { mapState } from "vuex";
+import { FETCH_PROFILE } from "@/store/type/actions.type";
+
+export default {
+  computed: {
+    ...mapState({
+      profile: state => state.user.profile
+    }),
+    name() {
+      const { nickname, username } = this.profile;
+      return nickname ? nickname : username;
+    }
+  },
+  async mounted() {
+    await this.$store.dispatch(FETCH_PROFILE, this.profile.id);
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../../assets/scss/app";
+
+.nav-section {
+  border-bottom: $default-border;
+}
+
+@media screen and (min-width: 1088px) {
+  .navbar {
+    min-height: 4.25rem;
+  }
+}
+.header-logo {
+  font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
+    Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", Helvetica, Arial,
+    sans-serif;
+  font-weight: bold;
+  font-size: 1.7em;
+  color: #3d70b2;
+  padding-right: 20px;
+  a {
+    color: #3d70b2;
+  }
+}
+.nav-active {
+  background-color: #fafafa;
+}
+
+@media screen {
+}
+</style>

+ 30 - 0
src/layouts/HomeLayout/index.vue

@@ -0,0 +1,30 @@
+<template>
+  <section class="main-body">
+    <home-header />
+    <router-view class="body-section" />
+    <footer class="footer" style="background: transparent">
+      <div class="content has-text-centered">@nju software</div>
+    </footer>
+  </section>
+</template>
+
+<script>
+import HomeHeader from "@/layouts/HomeLayout/HomeHeader";
+export default {
+  components: {
+    HomeHeader
+  }
+};
+</script>
+
+<style lang="scss">
+/*@import "bulma/sass/utilities/_all.sass";*/
+.main-body {
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  .body-section {
+    flex: 1;
+  }
+}
+</style>

+ 30 - 9
src/layouts/LoginLayout.vue

@@ -1,22 +1,28 @@
 <template>
   <section class="main-body">
     <section class="main-body-section">
-      <div class="logo">MOOC</div>
+      <div class="logo">欢迎使用 MOOC</div>
       <div class="login_container">
         <div class="container__main"><router-view /></div>
       </div>
+      <div class="login-footer">
+        <div class="content has-text-centered" style="padding-bottom: 20px">
+          @nju software
+        </div>
+      </div>
     </section>
-    <footer class="footer is-transparent" style="background: #f4f7fb">
-      <div class="content has-text-centered">@nju software</div>
-    </footer>
+    <!--<footer class="footer is-transparent" style="background: #f4f7fb">-->
+    <!--<div class="content has-text-centered">@nju software</div>-->
+    <!--</footer>-->
   </section>
 </template>
 
 <style lang="scss" scoped>
 .logo {
   text-align: center;
-  padding: 20vh 0 0;
-  font-size: 40px;
+  padding: 10vh 0 0;
+  font-size: 34px;
+  font-weight: bold;
   /*<!--color: $logo-color;-->*/
   /*<!--font-size: $logo-font-size;-->*/
   /*<!--font-family: $logo-font-family;-->*/
@@ -38,11 +44,26 @@
 }
 .main-body {
   min-height: 100vh;
+  min-width: 100vw;
   display: flex;
-  flex-direction: column;
-  background: #f4f7fb;
+  flex-direction: row-reverse;
+  /*background: #f4f7fb;*/
+  background-image: url("../assets/login_background.png");
+  background-size: cover;
   .main-body-section {
-    flex: 1;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    width: 55%;
+    background-color: #ffffff;
   }
 }
+.login-footer {
+  flex: 1;
+  display: flex;
+  flex-direction: column-reverse;
+  position: relative;
+  width: 100%;
+  background: transparent;
+}
 </style>

+ 104 - 0
src/layouts/courseLayouts/LayoutStructure.vue

@@ -0,0 +1,104 @@
+<template>
+  <div class="course-layout">
+    <div class="menu-section">
+      <div class="menu-sticky">
+        <div class="header-logo">MOOC</div>
+        <div class="course-detail">
+          {{ course.name }}
+          <b-loading :is-full-page="false" :active.sync="isLoading"></b-loading>
+        </div>
+        <slot />
+      </div>
+    </div>
+    <div class="router-section"><router-view /></div>
+  </div>
+</template>
+
+<script>
+import { FETCH_CURRENT_COURSE } from "@/store/type/actions.type";
+import { mapState } from "vuex";
+
+export default {
+  computed: {
+    ...mapState({
+      course: state => state.course.currentCourse
+    })
+  },
+  data() {
+    return {
+      isLoading: false
+    };
+  },
+  async mounted() {
+    const { courseId } = this.$route.params;
+    this.isLoading = true;
+    await this.$store.dispatch(FETCH_CURRENT_COURSE, courseId);
+    this.isLoading = false;
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../assets/scss/app.scss";
+
+$menu-width: 300px;
+$menu-padding: 24px;
+$screen-width: 1450px;
+$left-padding: "(100% -" #{$screen-width} "-" #{$menu-padding} "* 2) / 2";
+$left-width: #{$left-padding} + #{$menu-width};
+$left-padding-calc: calc(#{$left-padding});
+$left-width-calc: calc(#{$left-width});
+$top-section-height: 4.25rem;
+
+.course-layout {
+  display: flex;
+  min-height: 100vh;
+  justify-content: stretch;
+  position: relative;
+}
+
+.router-section {
+  padding: 24px;
+  flex-grow: 1;
+  max-width: 1000px;
+}
+.menu-section {
+  min-width: $menu-width;
+  width: $left-width-calc;
+  background: white;
+  border-right: 1px solid #c9d8ea;
+  padding-left: $left-padding-calc;
+  .menu-sticky {
+    width: $menu-width;
+    position: sticky;
+    overflow-y: auto;
+    padding: 0 $menu-padding;
+    top: 0;
+    height: 100vh;
+
+    .header-logo {
+      font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont,
+        "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue",
+        Helvetica, Arial, sans-serif;
+      font-weight: bold;
+      font-size: 1.7em;
+      color: #3d70b2;
+      padding-right: 20px;
+      line-height: $top-section-height;
+      a {
+        color: #3d70b2;
+      }
+    }
+    .course-detail {
+      position: relative;
+      padding: 15px;
+      background: #f4f7fb;
+      border: 1px solid #c9d8ea;
+      border-radius: 3px;
+      box-shadow: 0 1px 2px rgba(201, 216, 234, 0.5);
+      margin-bottom: 1em;
+      min-height: 54px;
+    }
+  }
+}
+</style>

+ 15 - 0
src/layouts/courseLayouts/StudentCourseLayout.vue

@@ -0,0 +1,15 @@
+<template>
+  <layout-structure> <student-menu /></layout-structure>
+</template>
+
+<script>
+import StudentMenu from "./StudentMenu";
+import LayoutStructure from "./LayoutStructure";
+
+export default {
+  components: {
+    StudentMenu,
+    LayoutStructure
+  }
+};
+</script>

+ 48 - 0
src/layouts/courseLayouts/StudentMenu.vue

@@ -0,0 +1,48 @@
+<template>
+  <aside class="menu">
+    <p class="menu-label">总览</p>
+    <ul class="menu-list">
+      <li>
+        <router-link active-class="is-active" :to="`${prefix}/dashboard`">
+          工作台
+        </router-link>
+      </li>
+    </ul>
+    <p class="menu-label">作业</p>
+    <ul class="menu-list">
+      <li>
+        <router-link active-class="is-active" :to="`${prefix}/code`">
+          代码作业
+        </router-link>
+      </li>
+      <li>
+        <router-link active-class="is-active" :to="`${prefix}/document`"
+          >文档作业</router-link
+        >
+      </li>
+      <li>
+        <router-link active-class="is-active" :to="`${prefix}/review`"
+          >评审作业</router-link
+        >
+      </li>
+    </ul>
+    <p class="menu-label">小组</p>
+    <ul class="menu-list">
+      <li>
+        <router-link active-class="is-active" :to="`${prefix}/group`"
+          >我的小组</router-link
+        >
+      </li>
+    </ul>
+  </aside>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      prefix: `/course-student/${this.$route.params.courseId}`
+    };
+  }
+};
+</script>

+ 15 - 0
src/layouts/courseLayouts/TeacherCourseLayout.vue

@@ -0,0 +1,15 @@
+<template>
+  <layout-structure> <teacher-menu /></layout-structure>
+</template>
+
+<script>
+import TeacherMenu from "./TeacherMenu";
+import LayoutStructure from "./LayoutStructure";
+
+export default {
+  components: {
+    TeacherMenu,
+    LayoutStructure
+  }
+};
+</script>

+ 40 - 0
src/layouts/courseLayouts/TeacherMenu.vue

@@ -0,0 +1,40 @@
+<template>
+  <aside class="menu">
+    <p class="menu-label">总览</p>
+    <ul class="menu-list">
+      <li>
+        <router-link active-class="is-active" :to="`${prefix}`"
+          >工作台</router-link
+        >
+      </li>
+    </ul>
+    <p class="menu-label">作业</p>
+    <ul class="menu-list">
+      <li>
+        <router-link active-class="is-active" :to="`${prefix}/code`"
+          >代码作业</router-link
+        >
+      </li>
+      <li>
+        <router-link active-class="is-active" :to="`${prefix}/document`"
+          >文档作业</router-link
+        >
+      </li>
+      <li>
+        <router-link active-class="is-active" :to="`${prefix}/review`"
+          >评审作业</router-link
+        >
+      </li>
+    </ul>
+  </aside>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      prefix: `/course-teacher/${this.$route.params.courseId}`
+    };
+  }
+};
+</script>

+ 20 - 45
src/router/index.js

@@ -1,57 +1,32 @@
 import Vue from "vue";
 import Router from "vue-router";
-import {
-  LOGIN_ROUTER,
-  // REGISTER_ROUTER,
-  HOME_ROUTER,
-  // MIXER_ROUTER
-  QUESTION_ROUTER
-} from "@/router/name";
+import routes from "./routes";
+import { judgeLogin } from "@/util/auth";
+import { LOGIN } from "@/router/name";
 
 Vue.use(Router);
 
 const router = new Router({
   mode: "history",
   base: process.env.BASE_URL,
-  routes: [
-    {
-      path: "/login",
-      component: () => import("@/layouts/LoginLayout"),
-      meta: { requiresAuth: false },
-      children: [
-        // {
-        //   name: REGISTER_ROUTER,
-        //   path: "/register",
-        //   component: () => import("@/views/Register"),
-        //   meta: { requiresAuth: false }
-        // },
-        {
-          name: LOGIN_ROUTER,
-          path: "",
-          component: () => import("@/views/login/Login.vue"),
-          meta: { requiresAuth: false }
-        }
-      ]
-    },
-    {
-      name: HOME_ROUTER,
-      path: "/",
-      component: () => import("@/views/Home"),
-      meta: { requiresAuth: true }
-    },
-    {
-      name: QUESTION_ROUTER,
-      path: "/question",
-      component: () => import("@/layouts/TeacherMainLayout.vue"),
-      meta: { requiresAuth: true }
+  routes
+});
+
+router.beforeEach((to, from, next) => {
+  if (to.matched.some(record => record.meta.requiresAuth)) {
+    // this route requires auth, check if logged in
+    // if not, redirect to login page.
+    if (!judgeLogin()) {
+      next({
+        name: LOGIN,
+        query: { redirect: to.fullPath }
+      });
+    } else {
+      next();
     }
-    // {
-    //   name: MIXER_ROUTER,
-    //   path: "/mixer",
-    //   component: () => import("@/views/Mixer"),
-    //   meta: { requiresAuth: true }
-    // }
-  ]
+  } else {
+    next();
+  }
 });
 
 export default router;

+ 7 - 5
src/router/name.js

@@ -1,5 +1,7 @@
-export const LOGIN_ROUTER = "login";
-export const REGISTER_ROUTER = "register";
-export const HOME_ROUTER = "home";
-export const MIXER_ROUTER = "mixer";
-export const QUESTION_ROUTER = "question";
+export const LOGIN = "LOGIN";
+export const REGISTER = "REGISTER";
+export const HOME = "HOME";
+export const TEACHER = "TEACHER";
+export const STUDENT = "STUDENT";
+export const TEACHER_COURSE = "TEACHER_COURSE";
+export const STUDENT_COURSE = "STUDENT_COURSE";

+ 107 - 0
src/router/routes.js

@@ -0,0 +1,107 @@
+import {
+  HOME,
+  LOGIN,
+  REGISTER,
+  STUDENT,
+  STUDENT_COURSE,
+  TEACHER,
+  TEACHER_COURSE
+} from "@/router/name";
+import { judgeLogin } from "@/util/auth";
+import { getUserProfile } from "@/util/profileStorage";
+
+export default [
+  {
+    path: "/login",
+    component: () => import("@/layouts/LoginLayout"),
+    meta: { requiresAuth: false },
+    children: [
+      {
+        name: REGISTER,
+        path: "/register",
+        component: () => import("@/views/login/Register.vue")
+      },
+      {
+        name: LOGIN,
+        path: "",
+        component: () => import("@/views/login/Login.vue")
+      }
+    ]
+  },
+  {
+    path: "/teacher",
+    component: () => import("@/layouts/HomeLayout"),
+    meta: { requiresAuth: true },
+    children: [
+      {
+        name: TEACHER,
+        path: "",
+        component: () => import("@/views/teacher/Home")
+      }
+    ]
+  },
+  {
+    path: "/course-teacher/:courseId",
+    component: () => import("@/layouts/courseLayouts/TeacherCourseLayout.vue"),
+    meta: { requiresAuth: true },
+    children: [
+      {
+        name: TEACHER_COURSE,
+        path: "dashboard",
+        component: () => import("@/views/teacher/Dashboard")
+      }
+    ]
+  },
+  {
+    path: "/course-student/:courseId",
+    component: () => import("@/layouts/courseLayouts/StudentCourseLayout.vue"),
+    meta: { requiresAuth: true },
+    children: [
+      {
+        name: STUDENT_COURSE,
+        path: "dashboard",
+        component: () => import("@/views/student/Dashboard")
+      },
+      {
+        path: "code",
+        component: () => import("@/views/student/Code")
+      },
+      {
+        path: "document",
+        component: () => import("@/views/student/Document")
+      },
+      {
+        path: "review",
+        component: () => import("@/views/student/Review")
+      },
+      {
+        path: "code",
+        component: () => import("@/views/student/Code")
+      }
+    ]
+  },
+  {
+    path: "/student",
+    component: () => import("@/layouts/HomeLayout"),
+    meta: { requiresAuth: true },
+    children: [
+      {
+        name: STUDENT,
+        path: "",
+        component: () => import("@/views/student/Home")
+      }
+    ]
+  },
+  {
+    path: "/",
+    name: HOME,
+    redirect: () => {
+      if (judgeLogin()) {
+        const { role } = getUserProfile();
+        return { name: role };
+      } else {
+        return { name: LOGIN };
+      }
+    }
+  }
+];

+ 0 - 62
src/store/auth.module.js

@@ -1,62 +0,0 @@
-import { login, register } from "@/api/auth";
-import { LOGIN, LOGOUT, REGISTER } from "@/store/type/actions.type";
-import {
-  SET_AUTH,
-  SET_LOGIN_ERROR,
-  REMOVE_AUTH,
-  SET_REGISTER_ERROR,
-  SET_PROFILE
-} from "@/store/type/mutations.type";
-import { getToken, destroyToken, saveToken } from "@/util/tokenStorage";
-
-const state = {
-  isAuthenticated: !!getToken(),
-  isLoginError: false,
-  isRegisterError: false
-};
-
-const actions = {
-  async [LOGIN](context, credentials) {
-    try {
-      const { token, user } = await login(credentials);
-      context.commit(SET_PROFILE, user);
-      context.commit(SET_AUTH, { token, profile: user });
-      context.commit(SET_LOGIN_ERROR, false);
-    } catch (e) {
-      context.commit(SET_LOGIN_ERROR, true);
-    }
-  },
-  [LOGOUT](context) {
-    context.commit(REMOVE_AUTH);
-  },
-  async [REGISTER](context, credentials) {
-    try {
-      await register(credentials);
-    } catch (e) {
-      context.commit(SET_REGISTER_ERROR, true);
-    }
-  }
-};
-
-const mutations = {
-  [SET_LOGIN_ERROR](state, error) {
-    state.isLoginError = error;
-  },
-  [SET_REGISTER_ERROR](state, error) {
-    state.isRegisterError = error;
-  },
-  [SET_AUTH](state, { token, profile }) {
-    state.isAuthenticated = true;
-    saveToken(token, profile);
-  },
-  [REMOVE_AUTH](state) {
-    state.isAuthenticated = false;
-    destroyToken();
-  }
-};
-
-export default {
-  state,
-  actions,
-  mutations
-};

+ 3 - 3
src/store/index.js

@@ -1,14 +1,14 @@
 import Vue from "vue";
 import Vuex from "vuex";
 
-import auth from "./auth.module";
-import user from "./user.module";
+import user from "./modules/user.module";
+import course from "./modules/course.module";
 
 Vue.use(Vuex);
 
 export default new Vuex.Store({
   modules: {
     user,
-    auth
+    course
   }
 });

+ 26 - 0
src/store/modules/course.module.js

@@ -0,0 +1,26 @@
+import * as ACTIONS from "@/store/type/actions.type";
+import * as MUTATIONS from "@/store/type/mutations.type.js";
+import { getCourseDetail } from "@/api/course";
+
+const courseState = {
+  currentCourse: {}
+};
+
+const actions = {
+  async [ACTIONS.FETCH_CURRENT_COURSE](context, courseId) {
+    const { data } = await getCourseDetail(courseId);
+    context.commit(MUTATIONS.SET_CURRENT_COURSE, data);
+  }
+};
+
+const mutations = {
+  [MUTATIONS.SET_CURRENT_COURSE](state, courseSerializer = {}) {
+    state.currentCourse = courseSerializer;
+  }
+};
+
+export default {
+  state: courseState,
+  actions,
+  mutations
+};

+ 28 - 0
src/store/modules/user.module.js

@@ -0,0 +1,28 @@
+import * as ACTIONS from "@/store/type/actions.type";
+import * as MUTATIONS from "@/store/type/mutations.type.js";
+import { getUserProfile, saveProfile } from "@/util/profileStorage";
+import { fetchUserProfile } from "@/api/user";
+
+const userState = {
+  profile: getUserProfile() || {}
+};
+
+const actions = {
+  async [ACTIONS.FETCH_PROFILE](context, userId) {
+    const { data } = await fetchUserProfile(userId);
+    context.commit(MUTATIONS.SET_PROFILE, data);
+  }
+};
+
+const mutations = {
+  [MUTATIONS.SET_PROFILE](state, userSerializer = {}) {
+    state.profile = userSerializer;
+    saveProfile(userSerializer);
+  }
+};
+
+export default {
+  state: userState,
+  actions,
+  mutations
+};

+ 1 - 6
src/store/type/actions.type.js

@@ -1,7 +1,2 @@
-export const LOGIN = "login";
-export const REGISTER = "register";
 export const FETCH_PROFILE = "fetchProfile";
-export const FETCH_PHOTOS = "fetchPhotos";
-export const POST_PHOTOS = "postPhotos";
-export const LOGOUT = "logout";
-export const ORDER_BY = "orderBy";
+export const FETCH_CURRENT_COURSE = "fetchCurrentCourse";

+ 1 - 7
src/store/type/mutations.type.js

@@ -1,8 +1,2 @@
-export const SET_LOGIN_ERROR = "setLoginError";
-export const SET_REGISTER_ERROR = "setRegisterError";
-export const SET_AUTH = "setAuthorization";
 export const SET_PROFILE = "setProfile";
-export const REMOVE_AUTH = "removeAuthorization";
-export const SET_PHOTOS = "setPhotos";
-export const SET_ORDER = "setOrder";
-export const SET_LAST_SEARCH = "setLastSearch";
+export const SET_CURRENT_COURSE = "setCurrentCourse";

+ 0 - 83
src/store/user.module.js

@@ -1,83 +0,0 @@
-import { fetchUserAuthProfile } from "@/api/user";
-import {
-  FETCH_PROFILE,
-  FETCH_PHOTOS,
-  POST_PHOTOS,
-  ORDER_BY
-} from "@/store/type/actions.type";
-import {
-  SET_PROFILE,
-  SET_PHOTOS,
-  SET_ORDER,
-  SET_LAST_SEARCH
-} from "@/store/type/mutations.type";
-import { fetchUserPhotos, postUserPhotos } from "@/api/photo";
-
-const LATEST = "LATEST";
-
-const state = {
-  profile: {
-    id: undefined,
-    username: undefined,
-    nickname: undefined
-  },
-  photos: [],
-  orderType: LATEST,
-  searchBy: -1,
-  lastSearch: -1
-};
-
-const actions = {
-  async [FETCH_PROFILE](context) {
-    if (context.state.profile) {
-      const newProfile = await fetchUserAuthProfile(context.state.profile.id);
-      context.commit(SET_PROFILE, newProfile);
-    }
-  },
-  async [FETCH_PHOTOS](context, tag = -1) {
-    let searchTag = tag;
-    if (tag === "") {
-      searchTag = -1;
-    }
-    const photos = await fetchUserPhotos({
-      tag: searchTag,
-      sort: context.state.orderType
-    });
-    context.commit(SET_PHOTOS, photos);
-    context.commit(SET_LAST_SEARCH, tag);
-  },
-  async [POST_PHOTOS](context, { files, onload }) {
-    if (context.state.profile) {
-      postUserPhotos(context.state.profile.id, files, onload);
-    }
-  },
-  async [ORDER_BY](context, type) {
-    context.commit(SET_ORDER, type);
-    const photos = await fetchUserPhotos({
-      tag: context.state.lastSearch,
-      sort: type
-    });
-    context.commit(SET_PHOTOS, photos);
-  }
-};
-
-const mutations = {
-  [SET_PROFILE](state, profile) {
-    state.profile = profile;
-  },
-  [SET_PHOTOS](state, photos = []) {
-    state.photos = photos;
-  },
-  [SET_ORDER](state, type) {
-    state.orderType = type;
-  },
-  [SET_LAST_SEARCH](state, search) {
-    state.lastSearch = search;
-  }
-};
-
-export default {
-  state,
-  actions,
-  mutations
-};

+ 12 - 0
src/util/auth.js

@@ -0,0 +1,12 @@
+import { getToken } from "@/util/tokenStorage";
+import { getUserProfile } from "@/util/profileStorage";
+
+/**
+ * 判断是否登陆
+ * @return {boolean}
+ */
+export const judgeLogin = () => {
+  const token = getToken();
+  const profile = getUserProfile();
+  return token && profile && typeof profile === "object" && profile.role;
+};

+ 2 - 2
src/util/profileStorage.js

@@ -9,7 +9,7 @@ export const saveProfile = profile =>
 
 /**
  * 获得用户信息
- * @return {UserSerializerType} 用户信息
+ * @return {UserSerializerType|null} 用户信息
  * @exception {Error} profile not found
  */
 export const getUserProfile = () => {
@@ -18,7 +18,7 @@ export const getUserProfile = () => {
   try {
     profile = JSON.parse(profileText);
   } catch (e) {
-    throw new Error("profile not found");
+    return null;
   }
   return profile;
 };

+ 37 - 28
src/util/request.js

@@ -1,36 +1,39 @@
 // ant-design-pro request.js file with MIT license
 import "whatwg-fetch";
 import router from "@/router";
-import { LOGIN_ROUTER } from "@/router/name";
+import { LOGIN } from "@/router/name";
 
-const codeMessage = {
-  200: "服务器成功返回请求的数据。",
-  201: "新建或修改数据成功。",
-  202: "一个请求已经进入后台排队(异步任务)。",
-  204: "删除数据成功。",
-  400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。",
-  401: "用户没有权限(令牌、用户名、密码错误)。",
-  403: "用户得到授权,但是访问是被禁止的。",
-  404: "发出的请求针对的是不存在的记录,服务器没有进行操作。",
-  406: "请求的格式不可得。",
-  410: "请求的资源被永久删除,且不会再得到的。",
-  422: "当创建一个对象时,发生一个验证错误。",
-  500: "服务器发生错误,请检查服务器。",
-  502: "网关错误。",
-  503: "服务不可用,服务器暂时过载或维护。",
-  504: "网关超时。"
-};
+/**
+ * @description error
+ * @typedef {Object} StatusException
+ * @property {string} name status code
+ * @property {string} message 消息
+ * @property {{code:string,message:string,body:{code:number,message:string}}} response 返回值
+ */
 
-function checkStatus(response) {
+/**
+ * 判断是否抛异常
+ * @param response
+ * @returns {*}
+ * @throws StatusException
+ */
+const checkStatus = async response => {
   if (response.status >= 200 && response.status < 300) {
     return response;
   }
-  const errortext = codeMessage[response.status] || response.message;
-  const error = new Error(errortext);
+  let responseBody = {};
+  try {
+    responseBody = await response.json();
+  } catch (e) {
+    console.log(e);
+  }
+  console.log(responseBody);
+  const error = new Error();
   error.name = response.status;
-  error.response = response;
+  error.message = responseBody.message;
+  error.body = responseBody;
   throw error;
-}
+};
 
 /**
  * Requests a URL, returning a promise.
@@ -38,6 +41,7 @@ function checkStatus(response) {
  * @param  {string} url       The URL we want to request
  * @param  {{method:string,body}} [options] The options we want to pass to "fetch"
  * @return {Promise<*>}           An object containing either "data" or "err"
+ * @throws StatusException
  */
 export default async function request(url, options) {
   const defaultOptions = {
@@ -67,18 +71,23 @@ export default async function request(url, options) {
 
   const response = await fetch(url, newOptions);
   try {
-    checkStatus(response);
+    await checkStatus(response);
   } catch (e) {
-    if (e.name !== 418) {
-      router.push({ name: LOGIN_ROUTER });
+    if (e.name === 401) {
+      router.push({ name: LOGIN });
     } else {
-      throw new Error(e);
+      throw e;
     }
   }
 
   if (newOptions.method === "DELETE" || response.status === 204) {
     return response.text();
   }
+  const Authorization = await response.headers.get("Authorization");
+  const body = await response.json();
+  const header = {
+    Authorization
+  };
 
-  return response.json();
+  return { ...body, _header: header };
 }

+ 0 - 12
src/util/tokenStorage.js

@@ -1,5 +1,4 @@
 const ID_TOKEN_KEY = "id_token";
-const USER_PROFILE = "user_profile";
 
 /**
  * 获得token
@@ -18,14 +17,3 @@ export const saveToken = token =>
   token && window.localStorage.setItem(ID_TOKEN_KEY, token);
 
 export const destroyToken = () => window.localStorage.removeItem(ID_TOKEN_KEY);
-
-export const getUserProfile = () => {
-  const profileText = window.localStorage.getItem(USER_PROFILE);
-  let profile = {};
-  try {
-    profile = JSON.parse(profileText);
-  } catch (e) {
-    profile = {};
-  }
-  return profile;
-};

+ 0 - 3
src/views/About.vue

@@ -1,3 +0,0 @@
-<template>
-  <div class="about"><h1>This is an about page</h1></div>
-</template>

+ 0 - 15
src/views/Home.vue

@@ -1,15 +0,0 @@
-<template>
-  <div></div>
-</template>
-
-<script>
-// @ is an alias to /src
-// import HelloWorld from "@/components/HelloWorld.vue";
-
-export default {
-  name: "home"
-  // components: {
-  //   HelloWorld
-  // }
-};
-</script>

+ 93 - 67
src/views/login/Login.vue

@@ -1,117 +1,143 @@
 <template>
   <div>
-    <div class="intro">Sign in</div>
+    <div class="intro">登录</div>
+    <b-notification type="is-danger" :active.sync="isServerError">
+      {{ errorMassage }}
+    </b-notification>
     <b-field
-      label="Username"
-      :type="!$v.username.required ? 'is-danger' : ''"
-      :message="!$v.username.required ? 'username required' : ''"
+      label="邮箱"
+      :type="$v.username.$error ? 'is-danger' : ''"
+      :message="$v.username.$error ? '请输入邮箱' : ''"
     >
-      <b-input v-model="username" placeholder="input username here"></b-input>
+      <b-input
+        type="email"
+        v-model="$v.username.$model"
+        placeholder="在此输入邮箱"
+      ></b-input>
     </b-field>
 
-    <b-field label="Password">
+    <b-field
+      label="密码"
+      :type="$v.password.$error ? 'is-danger' : ''"
+      :message="$v.password.$error ? '请输入密码' : ''"
+    >
       <b-input
         type="password"
-        v-model="password"
-        placeholder="input password here"
+        v-model="$v.password.$model"
+        placeholder="在此输入密码"
         password-reveal
       >
       </b-input>
     </b-field>
-    <b-field>
-      <p class="control level" style="padding-top: 20px">
-        <button
-          style="width:40%"
-          :class="{ 'is-loading': submitting }"
-          class="button is-link"
-          @click="loginAction"
-        >
-          登 陆
-        </button>
-        <router-link
-          :to="{ name: 'register' }"
-          :class="{ 'is-text': true }"
-          class="button"
-          type="submit"
-          >没有账户,注册
-        </router-link>
-      </p>
-    </b-field>
+    <!--<p class="control level" style="padding-top: 20px">-->
+    <div style="padding-top: 40px;">
+      <button
+        :class="{ 'is-loading': submitting }"
+        class="login"
+        @click="loginAction"
+      >
+        登 陆
+      </button>
+    </div>
+    <div>
+      <router-link
+        :to="{ name: 'REGISTER' }"
+        :class="{ 'is-text': true }"
+        class="register"
+        type="submit"
+        >没有账户,注册
+      </router-link>
+    </div>
+    <!--</p>-->
   </div>
 </template>
 
 <script>
-import { required } from "vuelidate/lib/validators";
-import { getTeacherQuestionType } from "../../api/question";
+import { required, email } from "vuelidate/lib/validators";
+import { login } from "@/api/auth";
+import { SET_PROFILE } from "@/store/type/mutations.type";
+import { destroyToken, saveToken } from "@/util/tokenStorage";
+import { HOME } from "@/router/name";
 
 export default {
   data() {
     return {
       username: "",
       password: "",
-      submitting: false
+      submitting: false,
+      errorMassage: null,
+      isServerError: false
     };
   },
   validations: {
     username: {
-      required
+      required,
+      email
     },
     password: {
       required
     }
   },
-  computed: {},
+  mounted() {
+    destroyToken();
+  },
   methods: {
-    loginAction() {
-      //this.$router.push("");
-      //请求mock数据
-      getTeacherQuestionType().then(res => {
-        console.log(res);
-      });
+    async loginAction() {
+      this.$v.$touch();
+      if (!this.$v.$invalid) {
+        const { username = "", password = "" } = this;
+        try {
+          this.submitting = true;
+          const {
+            data,
+            _header: { Authorization }
+          } = await login({ username, password });
+          saveToken(Authorization);
+          this.$store.commit(SET_PROFILE, data);
+          this.$router.push({ name: HOME });
+        } catch (e) {
+          this.errorMassage = e.message;
+          this.isServerError = true;
+          this.submitting = false;
+        }
+      }
     }
   }
-
-  // @State((state: RootState) => state.auth.isLoginError)
-  // public isLoginError: boolean;
-  // public mounted() {
-  //   this.$store.dispatch(LOGOUT);
-  //   Validator.extend("truthy", {
-  //     getMessage: (field: string): string => "两次输入的密码不相同",
-  //     validate: (value: string): boolean => value === this.credentials.password
-  //   });
-  // }
-  // public closeError() {
-  //   this.$store.commit(SET_LOGIN_ERROR, false);
-  // }
-  // public submit() {
-  //   this.$validator.validateAll().then(async result => {
-  //     if (result) {
-  //       this.submitting = true;
-  //       await this.$store.dispatch(LOGIN, this.credentials);
-  //       if (!this.isLoginError) {
-  //         this.$router.push({ name: "home" });
-  //       } else {
-  //         this.submitting = false;
-  //       }
-  //     }
-  //   });
-  // }
 };
 </script>
 
 <style lang="scss" scoped>
 .intro {
-  /*<!--color: $oc-gray-7;-->*/
   text-align: center;
   font-size: 25px;
-  padding: 10px 0 30px;
+  padding: 20px 0 20px;
 }
 .link {
-  /*<!--color: $oc-blue-5;-->*/
   text-decoration: none;
 }
 .action-section {
   display: flex;
   justify-content: space-between;
 }
+.register {
+  color: #7396fd;
+  margin-top: 20px;
+  padding: 10px 0px 10px 0px;
+  font-size: 1rem;
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  border: 1px solid #82b1ed;
+}
+.login {
+  color: #ffffff;
+  padding: 10px 0px 10px 0px;
+  display: flex;
+  justify-content: center;
+  font-size: 1rem;
+  width: 100%;
+  background: linear-gradient(to right, #82b1ed, #b073fd);
+  border-radius: 4px;
+  border: 0px;
+}
 </style>

+ 194 - 0
src/views/login/Register.vue

@@ -0,0 +1,194 @@
+<template>
+  <div>
+    <div class="intro">注册</div>
+    <b-notification type="is-danger" :active.sync="isServerError">
+      {{ errorMassage }}
+    </b-notification>
+    <b-field
+      label="昵称"
+      :type="$v.nickname.$error ? 'is-danger' : ''"
+      :message="$v.nickname.$error ? '昵称须由长度为4至16的字母或数字组成' : ''"
+    >
+      <b-input
+        v-model="$v.nickname.$model"
+        placeholder="在此输入昵称"
+      ></b-input>
+    </b-field>
+    <b-field
+      label="邮箱"
+      :type="$v.username.$error ? 'is-danger' : ''"
+      :message="
+        $v.username.$error
+          ? [
+              !$v.username.email ? '邮箱格式不正确' : undefined,
+              !$v.username.required ? '请输入用户名' : undefined
+            ]
+          : ''
+      "
+    >
+      <b-input
+        v-model="$v.username.$model"
+        type="email"
+        placeholder="在此输入邮箱"
+      ></b-input>
+    </b-field>
+    <b-field
+      label="密码"
+      :type="$v.password.$error ? 'is-danger' : ''"
+      :message="
+        $v.password.$error
+          ? [
+              !$v.password.complex
+                ? '密码至少为8位,且要包含字母和数字'
+                : undefined,
+              !$v.password.required ? '请输入密码' : undefined
+            ]
+          : ''
+      "
+    >
+      <b-input
+        v-model="$v.password.$model"
+        type="password"
+        placeholder="在此输入密码"
+        password-reveal
+      ></b-input>
+    </b-field>
+
+    <b-field
+      label="再次输入密码"
+      :type="$v.passwordAgain.$error ? 'is-danger' : ''"
+      :message="$v.passwordAgain.$error ? '密码需一致' : ''"
+    >
+      <b-input
+        type="password"
+        v-model="$v.passwordAgain.$model"
+        placeholder="在这里重复密码"
+        password-reveal
+      >
+      </b-input>
+    </b-field>
+    <div style="padding-top: 20px;">
+      <button
+        :class="{ 'is-loading': submitting }"
+        class="register"
+        @click="registerAction"
+      >
+        注 册
+      </button>
+    </div>
+
+    <router-link
+      :to="{ name: 'LOGIN' }"
+      :class="{ 'is-text': true }"
+      class="login"
+      type="submit"
+      >已有账户,登陆
+    </router-link>
+  </div>
+</template>
+
+<script>
+import {
+  required,
+  alphaNum,
+  minLength,
+  maxLength,
+  email,
+  sameAs
+} from "vuelidate/lib/validators";
+import { register } from "@/api/auth";
+import { LOGIN } from "@/router/name";
+
+export default {
+  data() {
+    return {
+      nickname: "",
+      username: "",
+      password: "",
+      email: "",
+      passwordAgain: "",
+      submitting: false,
+      errorMassage: null,
+      isServerError: false
+    };
+  },
+  validations: {
+    nickname: {
+      alphaNum,
+      minLength: minLength(4),
+      maxLength: maxLength(16)
+    },
+    username: {
+      required,
+      email
+    },
+    password: {
+      required,
+      complex: password =>
+        /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/.test(password)
+    },
+    passwordAgain: {
+      sameAsPassword: sameAs("password")
+    }
+  },
+  methods: {
+    async registerAction() {
+      this.$v.$touch();
+      if (!this.$v.$invalid) {
+        this.submitting = true;
+        const { username = "", password = "", nickname = null } = this;
+        try {
+          await register({ username, password, nickname });
+          this.$toast.open({
+            message: `用户「${
+              nickname === null ? username : nickname
+            }」注册成功`,
+            type: "is-success"
+          });
+          this.$router.push({ name: LOGIN });
+        } catch (e) {
+          this.errorMassage = e.message;
+          this.isServerError = true;
+          this.submitting = false;
+        }
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.intro {
+  text-align: center;
+  font-size: 25px;
+  padding: 20px 0 10px;
+}
+.link {
+  text-decoration: none;
+}
+.action-section {
+  display: flex;
+  justify-content: space-between;
+}
+.register {
+  color: #ffffff;
+  padding: 10px 0px 10px 0px;
+  display: flex;
+  justify-content: center;
+  font-size: 1rem;
+  width: 100%;
+  background: linear-gradient(to right, #82b1ed, #b073fd);
+  border-radius: 4px;
+  border: 0px;
+}
+.login {
+  color: #7396fd;
+  margin-top: 20px;
+  padding: 10px 0px 10px 0px;
+  font-size: 1rem;
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  border: 1px solid #82b1ed;
+}
+</style>

+ 3 - 0
src/views/student/Code/index.vue

@@ -0,0 +1,3 @@
+<template>
+  <div></div>
+</template>

+ 3 - 0
src/views/student/Dashboard/index.vue

@@ -0,0 +1,3 @@
+<template>
+  <div></div>
+</template>

+ 3 - 0
src/views/student/Document/index.vue

@@ -0,0 +1,3 @@
+<template>
+  <div></div>
+</template>

+ 3 - 0
src/views/student/Group/index.vue

@@ -0,0 +1,3 @@
+<template>
+  <div></div>
+</template>

+ 39 - 0
src/views/student/Home/index.vue

@@ -0,0 +1,39 @@
+<template>
+  <div>
+    <section class="hero">
+      <div class="hero-body">
+        <div class="container">
+          <h1 class="title">我的课程</h1>
+          <h2 class="subtitle">展示我加入的全部课程</h2>
+        </div>
+      </div>
+    </section>
+    <div class="container">
+      <div class="columns is-multiline is-desktop">
+        <div :key="item.id" v-for="item in courses" class="column is-4">
+          <router-link :to="`/course-student/${item.id}/dashboard`">
+            <card hoverable>{{ item.name }}</card>
+          </router-link>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Card from "@/components/Card";
+import { getStudentCourses } from "@/api/course";
+export default {
+  components: {
+    Card
+  },
+  data() {
+    return {
+      courses: []
+    };
+  },
+  mounted() {
+    getStudentCourses().then(value => (this.courses = value.data));
+  }
+};
+</script>

+ 68 - 0
src/views/student/Review/index.vue

@@ -0,0 +1,68 @@
+<template>
+  <div>
+    <nav class="breadcrumb" aria-label="breadcrumbs">
+      <ul>
+        <li><router-link :to="{ name: 'HOME' }">所有课程</router-link></li>
+        <li>
+          <router-link :to="`/student-course/${course.id}/dashboard`">{{
+            course.name
+          }}</router-link>
+        </li>
+        <li class="is-active">
+          <router-link :to="`/student-course/${course.id}/review`"
+            >评审作业</router-link
+          >
+        </li>
+      </ul>
+    </nav>
+    <div style="margin: 30px 0">
+      <h1 class="title">评审作业</h1>
+      <div class="subtitle">您仍剩余 {{ review.length }} 份评审作业待完成</div>
+    </div>
+
+    <div class="tabs">
+      <ul>
+        <li class="is-active"><a>待完成</a></li>
+        <li><a>需复合</a></li>
+      </ul>
+    </div>
+    <div class="columns is-multiline is-desktop">
+      <div :key="item.id" v-for="item in review" class="column is-4">
+        <card hoverable>{{ item.name }}</card>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapState } from "vuex";
+import { getStudentCourseReviewList } from "@/api/review";
+import Card from "@/components/Card";
+export default {
+  components: {
+    Card
+  },
+  computed: {
+    ...mapState({
+      course: state => state.course.currentCourse
+    })
+  },
+  data() {
+    return {
+      courses: [],
+      review: []
+    };
+  },
+  mounted() {
+    getStudentCourseReviewList(this.courses.id).then(
+      value => (this.review = value.data)
+    );
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.top-nav-section {
+  padding: 0 40px;
+}
+</style>

+ 3 - 0
src/views/teacher/Dashboard/index.vue

@@ -0,0 +1,3 @@
+<template>
+  <div></div>
+</template>

+ 39 - 0
src/views/teacher/Home/index.vue

@@ -0,0 +1,39 @@
+<template>
+  <div>
+    <section class="hero">
+      <div class="hero-body">
+        <div class="container">
+          <h1 class="title">我的课程</h1>
+          <h2 class="subtitle">展示我开设的全部课程</h2>
+        </div>
+      </div>
+    </section>
+    <div class="container">
+      <div class="columns is-multiline is-desktop">
+        <div :key="item.id" v-for="item in courses" class="column is-4">
+          <router-link :to="`/course-teacher/${item.id}/dashboard`">
+            <card hoverable>{{ item.name }}</card>
+          </router-link>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Card from "@/components/Card";
+import { getTeacherCourses } from "@/api/course";
+export default {
+  components: {
+    Card
+  },
+  data() {
+    return {
+      courses: []
+    };
+  },
+  mounted() {
+    getTeacherCourses().then(value => (this.courses = value.data));
+  }
+};
+</script>