Ver Fonte

merge dev

Johnson há 7 anos atrás
pai
commit
f3d3f24941
50 ficheiros alterados com 2203 adições e 401 exclusões
  1. 6 0
      mock/modules/group/index.js
  2. 113 38
      mock/modules/review/index.js
  3. 2 2
      mock/serializers/course/GroupSerializer.js
  4. 10 6
      mock/serializers/review/CheckListItemSerializer.js
  5. 42 0
      mock/serializers/review/CheckListResultItemSerializer.js
  6. 19 20
      mock/serializers/review/ReviewDetailSerializer.js
  7. 37 0
      mock/serializers/review/ReviewDocDetailSerializer.js
  8. 36 0
      mock/serializers/review/ReviewDocResultSerializer.js
  9. 22 0
      mock/serializers/review/ReviewDocSimpleSerializer.js
  10. 46 39
      mock/serializers/review/ReviewResultSerializer.js
  11. 15 17
      mock/serializers/review/ReviewSimpleSerializer.js
  12. 1 1
      mock/serializers/user/UserSerializer.js
  13. 9 0
      src/api/group.js
  14. 142 74
      src/api/review.js
  15. 7 0
      src/assets/scss/app.scss
  16. 0 0
      src/assets/undraw_having_fun.svg
  17. 53 0
      src/components/CardJudgeItem/index.vue
  18. 143 0
      src/components/CheckList/ResultCheckListItem.vue
  19. 185 0
      src/components/CheckList/index.vue
  20. 5 0
      src/components/PageHeader/index.scss
  21. 21 0
      src/components/ReviewStateTag/index.vue
  22. 38 0
      src/data/checkListItemState.js
  23. 37 0
      src/data/reviewState.js
  24. 1 1
      src/layouts/SettingLayout.vue
  25. 6 0
      src/layouts/TeacherMainLayout.vue
  26. 1 1
      src/layouts/courseLayouts/LayoutStructure.vue
  27. 10 0
      src/mixins/time.js
  28. 50 4
      src/router/routes.js
  29. 0 2
      src/views/student/Code/detail.vue
  30. 247 1
      src/views/student/Group/index.vue
  31. 1 1
      src/views/student/Home/index.vue
  32. 29 0
      src/views/student/Review/ReviewDetail/ReviewDocs/index.vue
  33. 90 0
      src/views/student/Review/ReviewDetail/ReviewSelfDetail/index.vue
  34. 28 15
      src/views/student/Review/ReviewDetail/index.vue
  35. 142 0
      src/views/student/Review/ReviewDocDetail/index.vue
  36. 0 0
      src/views/student/Review/ReviewSelf/index.vue
  37. 8 3
      src/views/student/Review/index.vue
  38. 0 1
      src/views/teacher/Code/detail.vue
  39. 0 23
      src/views/teacher/Code/index.vue
  40. 2 0
      src/views/teacher/Document/index.vue
  41. 2 8
      src/views/teacher/Question/index.vue
  42. 1 1
      src/views/teacher/Question/questionDetail.vue
  43. 1 1
      src/views/teacher/Result/index.vue
  44. 114 0
      src/views/teacher/Review/ReviewDetail/Detail/index.vue
  45. 121 0
      src/views/teacher/Review/ReviewDetail/GroupArgueDetail/index.vue
  46. 100 0
      src/views/teacher/Review/ReviewDetail/GroupDocDetail/index.vue
  47. 55 0
      src/views/teacher/Review/ReviewDetail/Groups/index.vue
  48. 142 0
      src/views/teacher/Review/ReviewDetail/index.vue
  49. 0 142
      src/views/teacher/Review/assignmentList.vue
  50. 63 0
      src/views/teacher/Review/index.vue

+ 6 - 0
mock/modules/group/index.js

@@ -20,4 +20,10 @@ router
     })
   );
 
+router.route("/:groupId").get((req, res) =>
+  res.send({
+    data: groupSerializer(req.params.groupId)
+  })
+);
+
 module.exports = router;

+ 113 - 38
mock/modules/review/index.js

@@ -4,86 +4,161 @@ const reviewSimpleSerializer = require("../../serializers/review/ReviewSimpleSer
 const reviewDetailSerializer = require("../../serializers/review/ReviewDetailSerializer");
 const checkListItemSerializer = require("../../serializers/review/CheckListItemSerializer");
 const reviewResultSerializer = require("../../serializers/review/ReviewResultSerializer");
+const reviewDocResultSerializer = require("../../serializers/review/ReviewDocResultSerializer");
+const reviewDocDetailSerializer = require("../../serializers/review/ReviewDocDetailSerializer");
 
+const groupSerializer = require("../../serializers/course/GroupSerializer");
+
+// ====================== TEACHER ======================
+
+// 新建互评作业
 router.route("/review").post((req, res) =>
   res.send({
-    data: reviewSimpleSerializer()
+    data: reviewSimpleSerializer({})
   })
 );
 
-router.route("/argue").post((req, res) => res.send({}));
-
+// 老师查看互评作业列表
 router.route("/teacher/course/:courseId/review").get((req, res) =>
   res.send({
-    data: [reviewSimpleSerializer(), reviewSimpleSerializer()]
+    data: [
+      reviewSimpleSerializer({ state: "DEFAULT" }),
+      reviewSimpleSerializer({ state: "REVIEWING" }),
+      reviewSimpleSerializer({ state: "ARGUING" }),
+      reviewSimpleSerializer({ state: "SOLVING" }),
+      reviewSimpleSerializer({ state: "END" })
+    ]
   })
 );
 
-router.route("/teacher/review/:reviewId").get((req, res) =>
+// 老师查看互评作业详情
+router.route("/teacher/review/:reviewId").get((req, res) => {
+  const reviewId = req.params.reviewId;
+  res.send({
+    data: reviewDetailSerializer({ reviewId })
+  });
+});
+
+// 老师查看互评作业汇总结果
+router.route("/sum/:reviewId").get((req, res) =>
   res.send({
-    data: reviewDetailSerializer()
+    data: reviewResultSerializer({ reviewId: req.params.reviewId })
   })
 );
 
-router.route("/student/course/:courseId/review").get((req, res) =>
+// 老师查看某次互评作业所有小组
+router.route("/sum/:reviewId/group").get((req, res) =>
+  res.send({
+    data: [
+      groupSerializer(),
+      groupSerializer(),
+      groupSerializer(),
+      groupSerializer(),
+      groupSerializer()
+    ]
+  })
+);
+
+// 老师查看某次互评作业一个小组的详细结果
+router.route("/sum/:reviewId/group/:groupId").get((req, res) =>
   res.send({
-    data: [reviewSimpleSerializer(), reviewSimpleSerializer()]
+    data: reviewDocResultSerializer()
   })
 );
 
 router
-  .route("/student/review/:reviewId")
+  .route("/sum/:reviewId/argue/:groupId")
+  // 老师开始对某次互评作业一个小组进行评价
   .get((req, res) =>
     res.send({
-      data: reviewDetailSerializer()
+      data: reviewDocDetailSerializer({})
     })
   )
-  .post((req, res) =>
-    res.send({
-      data: [checkListItemSerializer(), checkListItemSerializer()]
-    })
-  );
+  // 老师提交对某次互评作业一个小组的评价结果
+  .post((req, res) => {
+    res.send(req.body);
+  });
 
-router.route("/student/course/:courseId/result").get((req, res) =>
-  res.send({
-    data: [reviewSimpleSerializer(), reviewSimpleSerializer()]
-  })
-);
+// 老师更新作业评分标准
+router.route("/sum/:reviewId/level").post((req, res) => {
+  res.send({});
+});
 
-router.route("/student/review/:reviewId/result").get((req, res) =>
+// ====================== STUDENT ======================
+
+// 学生查看老师发布的互评作业列表
+router.route("/student/course/:courseId/review").get((req, res) =>
   res.send({
-    data: reviewResultSerializer()
+    data: [
+      reviewSimpleSerializer({ state: "DEFAULT" }),
+      reviewSimpleSerializer({ state: "REVIEWING" }),
+      reviewSimpleSerializer({ state: "ARGUING" }),
+      reviewSimpleSerializer({ state: "SOLVING" }),
+      reviewSimpleSerializer({ state: "END" })
+    ]
   })
 );
 
-router.route("/sum/:reviewId").get((req, res) =>
+// 学生查看互评作业详情
+router.route("/student/review/:reviewId").get((req, res) =>
   res.send({
-    detail: reviewDetailSerializer(),
-    finishPercentage: 0.7,
-    studentIdCheckListAcceptNumMap: {
-      1: 1,
-      2: 3
-    }
+    data: reviewDetailSerializer({ reviewId: req.params.reviewId })
   })
 );
 
-router.route("/sum/:reviewId/level").post((req, res) => {
-  res.send({});
-});
+router
+  .route("/student/review/:reviewId/doc-review/:docReviewId")
+  // 学生获取某次互评作业中某个文档
+  .get((req, res) =>
+    res.send({
+      data: reviewDocDetailSerializer(req.params.docReviewId)
+    })
+  )
+  // 学生提交某次互评作业中的某个文档
+  .post((req, res) =>
+    res.send({
+      data: [
+        checkListItemSerializer({}),
+        checkListItemSerializer({}),
+        checkListItemSerializer({}),
+        checkListItemSerializer({}),
+        checkListItemSerializer({})
+      ]
+    })
+  );
 
-router.route("/sum/:reviewId/argue").get((req, res) =>
+// 学生选择查看某次互评作业中自己的文档
+router.route("/student/review/:reviewId/result").get((req, res) =>
   res.send({
-    data: [reviewSimpleSerializer(), reviewSimpleSerializer()]
+    data: reviewDocResultSerializer()
   })
 );
 
+// 学生对某个组的某条checklist提出argue
+router.route("/student/argue").post((req, res) => res.send({}));
+
+// 学生(评审方)判断是否愿意处理argue
 router
-  .route("/sum/:reviewId/argue/:groupId")
+  .route("/student/review/:reviewId/checklist/:checkListId/argue/acceptation")
+  .post((req, res) =>
+    res.send({
+      data: [checkListItemSerializer({})]
+    })
+  );
+
+// 学生(评审方)提交argue [标记为已处理完成]
+router
+  .route("/student/review/:reviewId/checklist/:checkListId/argue/end")
   .get((req, res) =>
     res.send({
-      data: reviewResultSerializer()
+      data: [
+        checkListItemSerializer({}),
+        checkListItemSerializer({}),
+        checkListItemSerializer({}),
+        checkListItemSerializer({}),
+        checkListItemSerializer({})
+      ]
     })
-  )
-  .post((req, res) => res.send({}));
+  );
 
 module.exports = router;

+ 2 - 2
mock/serializers/course/GroupSerializer.js

@@ -14,9 +14,9 @@ const userSerializer = require("../user/UserSerializer");
  * @description 生成课程信息
  * @returns {GroupSerializerType}
  */
-module.exports = () => {
+module.exports = (id = makeId()) => {
   return {
-    id: makeId(),
+    id,
     name: "tag-maker",
     code: "xxxxx",
     members: [

+ 10 - 6
mock/serializers/review/CheckListItemSerializer.js

@@ -3,23 +3,27 @@ const makeId = require("../../util/makeId");
 /**
  * @description 互评checkListItem详细实体类
  * @typedef {Object} CheckListItemSerializerType
- * @property {number|string} fid item id
+ * @property {number|string} id checkListItem id
  * @property {string} content 评价内容
- * @property {string} level 用户评级
+ * @property {boolean} level 用户评级
  * @property {string} comment 用户备注
  * @property {string} explain 老师说明
+ * @property {string} remark 学生申请argue的备注
+ * @property {string} state ["DEFAULT","SUBMITTED","PENDING","SOLVING","ARGUED"]
  */
 
 /**
  * @description 生成checkListItem
  * @returns {CheckListItemSerializerType}
  */
-module.exports = () => {
+module.exports = ({ id = makeId(), state = "DEFAULT" }) => {
   return {
-    fid: makeId(),
+    id,
     content: "该项评价的内容, String",
-    level: "用户的备注, String",
+    level: false,
     comment: "用户的评级",
-    explain: "老师的说明,String"
+    explain: "老师的说明,String",
+    remark: "学生申请argue的备注",
+    state
   };
 };

+ 42 - 0
mock/serializers/review/CheckListResultItemSerializer.js

@@ -0,0 +1,42 @@
+const makeId = require("../../util/makeId");
+
+/**
+ * @description 互评CheckListResultItem详细实体类
+ * @typedef {Object} CheckListResultItemType
+ * @property {number|string} id checkListItem id
+ * @property {string} content 评价内容
+ * @property {string} explain 老师说明
+ * @property {ResultByGroupType[]} results 各个小组的评价
+ */
+
+/**
+ * @description 各个小组的评价
+ * @typedef {Object} ResultByGroupType
+ * @property {number|string} groupId 小组 id
+ * @property {boolean} level 小组做出的评价
+ * @property {string} comment 小组做出的备注
+ * @property {boolean} isArgued 是否已经申请过重评
+ */
+
+/**
+ * @description 生成checkListResultItem
+ * @returns {CheckListResultItemType}
+ */
+module.exports = ({ id = makeId() }) => {
+  /**
+   * @return {ResultByGroupType}
+   */
+  const groupResult = () => ({
+    groupId: makeId(),
+    level: true,
+    comment: "小组做出的备注",
+    isArgued: false
+  });
+
+  return {
+    id,
+    content: "该项评价的内容, String",
+    explain: "老师的说明,String",
+    results: [groupResult(), groupResult(), groupResult()]
+  };
+};

+ 19 - 20
mock/serializers/review/ReviewDetailSerializer.js

@@ -1,37 +1,36 @@
-const makeLongMarkdown = require("../../util/makeLongMarkdown");
 const makeId = require("../../util/makeId");
-const courseSerializer = require("../course/CourseSerializer");
 const dayjs = require("dayjs");
-const checkListItemSerializer = require("./CheckListItemSerializer");
-const groupSerializer = require("../course/GroupSerializer");
+const reviewDocSimpleSerializer = require("./ReviewDocSimpleSerializer");
 
 /**
- * @description 互评checkListItem详细实体类
+ * @description 互评作业详情
  * @typedef {Object} ReviewDetailSerializerType
- * @property {number|string} id 互评作业id
+ * @property {number|string} reviewId 老师布置一次互评作业id
  * @property {string} name 对应的文档作业名称
- * @property {CourseSerializerType} course 所属课程
- * @property {number} beginAt 开始时间
- * @property {number} endAt 截止时间
- * @property {string} docContent 作业内容
- * @property {CheckListItemSerializerType[]} checkList 评价列表
- * @property {GroupSerializerType} group 所属小组
+ * @property {number} reviewDdl 互评结束时间
+ * @property {number} argueDdl argue结束时间
+ * @property {number} endDdl 最后结束时间
+ * @property {string} state ["DEFAULT","REVIEWING","ARGUING","SOLVING","END"]
+ * @property {ReviewDocSimpleSerializerType[]} documents 文档评价的实例概况列表
  */
 
 /**
  * @description 生成ReviewDetailSerializer
  * @returns ReviewDetailSerializerType
  */
-const reviewDetailSerializer = () => {
+const reviewDetailSerializer = ({ reviewId = makeId(), state = "DEFAULT" }) => {
   return {
-    id: makeId(),
+    reviewId,
     name: "该项评价的内容, String",
-    course: courseSerializer(),
-    beginAt: dayjs("2018-10-21").unix(),
-    endAt: dayjs("2018-12-21").unix(),
-    docContent: makeLongMarkdown(),
-    checkList: [checkListItemSerializer(), checkListItemSerializer()],
-    group: groupSerializer()
+    reviewDdl: dayjs("2018-9-21").unix(),
+    argueDdl: dayjs("2018-10-21").unix(),
+    endDdl: dayjs("2018-12-21").unix(),
+    state,
+    documents: [
+      reviewDocSimpleSerializer(),
+      reviewDocSimpleSerializer(),
+      reviewDocSimpleSerializer()
+    ]
   };
 };
 

+ 37 - 0
mock/serializers/review/ReviewDocDetailSerializer.js

@@ -0,0 +1,37 @@
+const makeLongMarkdown = require("../../util/makeLongMarkdown");
+const makeId = require("../../util/makeId");
+const checkListItemSerializer = require("./CheckListItemSerializer");
+const groupSerializer = require("../course/GroupSerializer");
+const reviewDetailSerializer = require("./ReviewDetailSerializer");
+
+/**
+ * @description 一次文档评价实例的详情
+ * @typedef {Object} ReviewDocDetailSerializerType
+ * @property {number|string} docReviewId 一次文档评价的实例id
+ * @property {ReviewDetailSerializerType} review 互评作业详情
+ * @property {GroupSerializerType} group 作者小组
+ * @property {string} docContent 文档内容
+ * @property {CheckListItemSerializerType[]} checkList check列表
+ */
+
+/**
+ * @description 生成ReviewDocDetailSerializer一次文档评价实例的详情
+ * @returns {ReviewDocDetailSerializerType}
+ */
+const reviewDocDetailSerializer = (docReviewId = makeId()) => {
+  return {
+    docReviewId,
+    review: reviewDetailSerializer({}),
+    group: groupSerializer(),
+    docContent: makeLongMarkdown(),
+    checkList: [
+      checkListItemSerializer({ state: "DEFAULT" }),
+      checkListItemSerializer({ state: "SUBMITTED" }),
+      checkListItemSerializer({ state: "PENDING" }),
+      checkListItemSerializer({ state: "SOLVING" }),
+      checkListItemSerializer({ state: "ARGUED" })
+    ]
+  };
+};
+
+module.exports = reviewDocDetailSerializer;

+ 36 - 0
mock/serializers/review/ReviewDocResultSerializer.js

@@ -0,0 +1,36 @@
+const makeLongMarkdown = require("../../util/makeLongMarkdown");
+const makeId = require("../../util/makeId");
+const checkListResultItemSerializer = require("./CheckListResultItemSerializer");
+const groupSerializer = require("../course/GroupSerializer");
+const reviewDetailSerializer = require("./ReviewDetailSerializer");
+
+/**
+ * @description 一次文档评价实例的详情
+ * @typedef {Object} ReviewDocResultSerializerType
+ * @property {number|string} docReviewId 一次文档评价的实例id
+ * @property {ReviewDetailSerializerType} review 互评作业详情
+ * @property {string} docContent 文档内容
+ * @property {CheckListResultItemType[]} checkListResult check结果列表
+ */
+
+/**
+ * @description 生成ReviewDocResultSerializer一次文档评价实例的详情
+ * @returns {ReviewDocResultSerializerType}
+ */
+const reviewDocDetailSerializer = (docReviewId = makeId()) => {
+  return {
+    docReviewId,
+    review: reviewDetailSerializer({}),
+    level: groupSerializer(),
+    docContent: makeLongMarkdown(),
+    checkListResult: [
+      checkListResultItemSerializer({}),
+      checkListResultItemSerializer({}),
+      checkListResultItemSerializer({}),
+      checkListResultItemSerializer({}),
+      checkListResultItemSerializer({})
+    ]
+  };
+};
+
+module.exports = reviewDocDetailSerializer;

+ 22 - 0
mock/serializers/review/ReviewDocSimpleSerializer.js

@@ -0,0 +1,22 @@
+const makeId = require("../../util/makeId");
+const groupSerializer = require("../course/GroupSerializer");
+
+/**
+ * @description 一次文档评价的实例概况
+ * @typedef {Object} ReviewDocSimpleSerializerType
+ * @property {number|string} docReviewId 一次文档评价的实例id
+ * @property {GroupSerializerType} group 作者小组
+ */
+
+/**
+ * @description 生成ReviewDocSimpleSerializer一次文档评价的实例概况
+ * @returns {ReviewDocSimpleSerializerType}
+ */
+const reviewDocSimpleSerializer = (id = makeId()) => {
+  return {
+    docReviewId: id,
+    group: groupSerializer()
+  };
+};
+
+module.exports = reviewDocSimpleSerializer;

+ 46 - 39
mock/serializers/review/ReviewResultSerializer.js

@@ -1,67 +1,74 @@
 const makeId = require("../../util/makeId");
-const reviewDetailSerializer = require("./ReviewDetailSerializer");
-const checkListItemSerializer = require("./CheckListItemSerializer");
+const dayjs = require("dayjs");
 
 /**
- * @description 互评ReviewResultSerializer详细实体类
+ * @description 互评作业结果汇总
  * @typedef {Object} ReviewResultSerializerType
- * @property {number|string} id ReviewResultSerializer
- * @property {ReviewDetailSerializerType} detail 文档详情
- * @property {string} level 文档等级
- * @property {number} score 文档分数
- * @property {CheckListResult[]} checkListResult 互评作业的结果
+ * @property {number|string} reviewId 老师布置一次互评作业id
+ * @property {string} name 对应的文档作业名称
+ * @property {number} reviewDdl 互评结束时间
+ * @property {number} argueDdl argue结束时间
+ * @property {number} endDdl 最后结束时间
+ * @property {string} state ["DEFAULT","REVIEWING","ARGUING","SOLVING","END"]
+ * @property {Number} completeRate (int)(完成率*100)
+ * @property {PassedCDFType[]} passedCDF cdf图
+ * @property {CheckListAnalysedItemSerializerType[]} checklistItems check汇总
  */
 
 /**
- * @description 详细评价结果CheckListResult
- * @typedef {Object} CheckListResult
- * @property {string|number} id id
- * @property {CheckListItemSerializerType} checkListItem
- * @property {GroupCheckResult[]} results 小组作出的评价
+ * @description 互评作业结果汇总
+ * @typedef {Object} PassedCDFType
+ * @property {number} passedNumber
+ * @property {number} passedRatio
  */
 
 /**
- * @description 各个小组的评价 groupCheckResult
- * @typedef {Object} GroupCheckResult
- * @property {string|number} judgeItemId id
- * @property {number} level 小组作出的评价
- * @property {string} comment 小组作出的备注
+ * @description
+ * @typedef {Object} CheckListAnalysedItemSerializerType
+ * @property {number|string} id checkListItem id
+ * @property {string} content 评价内容
+ * @property {number} passedNum 通过量
+ * @property {string} explain 老师说明
  */
 
 /**
- * @description 生成DocResultSerializer
+ * @description 生成 ReviewResultSerializerType 互评作业结果汇总
  * @returns {ReviewResultSerializerType}
  */
-const reviewResultSerializer = () => {
+const reviewSimpleSerializer = ({ reviewId = makeId(), state = "DEFAULT" }) => {
   /**
-   * @return {GroupCheckResult}
+   * @return {PassedCDFType}
    */
-  const groupCheckResultItem = (level = 0) => ({
-    judgeItemId: makeId(),
-    level: level,
-    comment: "我是备注我是小组备注"
+  const passedCDF = (passedNumber = 30) => ({
+    passedNumber,
+    passedRatio: passedNumber
   });
 
   /**
-   * @returns {CheckListResult}
+   * @return {CheckListAnalysedItemSerializerType}
    */
-  const checkListResultItem = () => ({
+  const checkListAnalysedItemSerializer = () => ({
     id: makeId(),
-    checkListItem: checkListItemSerializer(),
-    results: [
-      groupCheckResultItem(),
-      groupCheckResultItem(1),
-      groupCheckResultItem()
-    ]
+    content: "评价内容",
+    passedNum: 10,
+    explain: "老师的说明说明说明说明"
   });
 
   return {
-    id: makeId(),
-    detail: reviewDetailSerializer(),
-    level: "A",
-    score: 80,
-    checkListResult: [checkListResultItem(), checkListResultItem()]
+    reviewId,
+    name: "需求规格说明文档",
+    reviewDdl: dayjs("2018-9-21").unix(),
+    argueDdl: dayjs("2018-10-21").unix(),
+    endDdl: dayjs("2018-12-21").unix(),
+    state,
+    completeRate: 89,
+    passedCDF: [passedCDF(), passedCDF(), passedCDF()],
+    checklistItems: [
+      checkListAnalysedItemSerializer(),
+      checkListAnalysedItemSerializer(),
+      checkListAnalysedItemSerializer()
+    ]
   };
 };
 
-module.exports = reviewResultSerializer;
+module.exports = reviewSimpleSerializer;

+ 15 - 17
mock/serializers/review/ReviewSimpleSerializer.js

@@ -1,31 +1,29 @@
 const makeId = require("../../util/makeId");
-const courseSerializer = require("../course/CourseSerializer");
 const dayjs = require("dayjs");
-const groupSerializer = require("../course/GroupSerializer");
 
 /**
- * @description 互评checkListItem详细实体类
+ * @description 互评作业概况
  * @typedef {Object} ReviewSimpleSerializerType
- * @property {number|string} id 互评作业id
+ * @property {number|string} reviewId 老师布置一次互评作业id
  * @property {string} name 对应的文档作业名称
- * @property {CourseSerializerType} course 所属课程
- * @property {number} beginAt 开始时间
- * @property {number} endAt 截止时间
- * @property {GroupSerializerType} group 所属小组
+ * @property {number} reviewDdl 互评结束时间
+ * @property {number} argueDdl argue结束时间
+ * @property {number} endDdl 最后结束时间
+ * @property {string} state ["DEFAULT","REVIEWING","ARGUING","SOLVING","END"]
  */
 
 /**
- * @description 生成ReviewSimpleSerializerType
- * @returns ReviewSimpleSerializerType
+ * @description 生成 ReviewSimpleSerializerType 互评作业概况
+ * @returns {ReviewSimpleSerializerType}
  */
-const reviewSimpleSerializer = () => {
+const reviewSimpleSerializer = ({ reviewId = makeId(), state = "DEFAULT" }) => {
   return {
-    id: makeId(),
-    name: "该项评价的内容, String",
-    course: courseSerializer(),
-    beginAt: dayjs("2018-10-21").unix(),
-    endAt: dayjs("2018-12-21").unix(),
-    group: groupSerializer()
+    reviewId,
+    name: "需求规格说明文档",
+    reviewDdl: dayjs("2018-9-21").unix(),
+    argueDdl: dayjs("2018-10-21").unix(),
+    endDdl: dayjs("2018-12-21").unix(),
+    state
   };
 };
 

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

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

+ 9 - 0
src/api/group.js

@@ -39,3 +39,12 @@ export const putStudentGroup = code => {
     }
   });
 };
+
+/**
+ * 获得小组详情
+ * @param groupId
+ * @returns {Promise<{data:GroupSerializerType}>}
+ */
+export const getGroupDetail = groupId => {
+  return request(`${GROUP_MODULE}/${groupId}`);
+};

+ 142 - 74
src/api/review.js

@@ -1,40 +1,30 @@
 import request from "@/util/request";
 import { REVIEW_MODULE } from "./_prefix";
 
+// ====================== TEACHER ======================
+
 /**
  * 新建互评作业
- * @param {number} homeworkId 作业id
- * @param {number} beginAt 开始时间
- * @param {number} endAt 截止时间
+ * @param {number} documentHwId 作业id
+ * @param {number} startAt 开始时间
+ * @param {number} reviewDdl 互评结束时间
+ * @param {number} argueDdl argue结束时间
+ * @param {number} endDdl 最后结束时间
  * @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
+export const createReview = ({
+  documentHwId,
+  startAt,
+  reviewDdl,
+  argueDdl,
+  endDdl,
+  taskNumber
 }) => {
-  return request(`${REVIEW_MODULE}/argue`, {
+  return request(`${REVIEW_MODULE}/review`, {
     method: "POST",
-    body: { homeworkId, checkListItemId, judgeItemId, remark }
+    body: { documentHwId, startAt, reviewDdl, argueDdl, endDdl, taskNumber }
   });
 };
 
@@ -57,103 +47,181 @@ export const getTeacherReviewDetail = reviewId => {
 };
 
 /**
- * 学生查看互评作业列表
- * @param {string|number} courseId
- * @return {Promise<{data:ReviewSimpleSerializerType[]}>}
+ * 老师查看互评作业汇总结果
+ * @param {string|number} reviewId
+ * @return {Promise<{data:ReviewResultSerializerType}>}
  */
-export const getStudentCourseReviewList = courseId => {
-  return request(`${REVIEW_MODULE}/student/course/${courseId}/review`);
+export const getTeacherReviewResult = reviewId => {
+  return request(`${REVIEW_MODULE}/sum/${reviewId}`);
 };
 
 /**
- * 学生查看互评作业详情
+ * 老师查看某次互评作业所有小组
+ * @param reviewId
+ * @return {Promise<{data:GroupSerializerType}>}
+ */
+export const getTeacherReviewAllGroups = reviewId => {
+  return request(`${REVIEW_MODULE}/sum/${reviewId}/group`);
+};
+
+/**
+ * 老师查看某次互评作业一个小组的详细结果
+ * @param reviewId
+ * @param groupId
+ * @return {Promise<{data:ReviewDocResultSerializerType}>}
+ */
+export const getTeacherReviewGroupDetail = (reviewId, groupId) => {
+  return request(`${REVIEW_MODULE}/sum/${reviewId}/group/${groupId}`);
+};
+
+/**
+ * 老师开始对某次互评作业一个小组进行评价
  * @param {string|number} reviewId
- * @return {Promise<{data:ReviewDetailSerializerType}>}
+ * @param {string|number} groupId
+ * @return {Promise<{data:ReviewDocDetailSerializerType}>}
  */
-export const getStudentReviewDetail = reviewId => {
-  return request(`${REVIEW_MODULE}/student/review/${reviewId}`);
+export const getTeacherStartArgue = (reviewId, groupId) => {
+  return request(`${REVIEW_MODULE}/sum/${reviewId}/argue/${groupId}`);
 };
 
 /**
- * 学生做互评作业
+ * 老师提交对某次互评作业一个小组的评价结果
  * @param {string|number} reviewId
- * @param {{data:CheckListItemSerializerType}} checkListBody
- * @return {Promise<{data:CheckListItemSerializerType[]}>}
+ * @param {string|number} groupId
+ * @param {CheckListItemSerializerType[]} checkList
+ * @return {Promise<{checkList:CheckListItemSerializerType[]}>}
+ */
+export const postTeacherArgueResult = (reviewId, groupId, checkList) => {
+  return request(`${REVIEW_MODULE}/sum/${reviewId}/argue/${groupId}`, {
+    method: "POST",
+    body: { checkList }
+  });
+};
+
+/**
+ * 老师更新作业评分标准
+ * @param {string|number} reviewId
+ * @param {*} level
+ * @return {Promise<{}>}
  * @throws {StatusException} 输入不合法
  */
-export const postStudentReviewWork = (reviewId, checkListBody) => {
-  return request(`${REVIEW_MODULE}/student/review/${reviewId}`, {
+export const postReviewSumLevel = (reviewId, level) => {
+  return request(`${REVIEW_MODULE}/sum/${reviewId}/level`, {
     method: "POST",
-    body: checkListBody
+    body: level
   });
 };
 
+// ====================== STUDENT ======================
+
 /**
- * 学生查看互评结果列表
+ * 学生查看老师发布的互评作业列表
  * @param {string|number} courseId
  * @return {Promise<{data:ReviewSimpleSerializerType[]}>}
  */
-export const getStudentReviewResultList = courseId => {
-  return request(`${REVIEW_MODULE}/student/course/${courseId}/result`);
+export const getStudentCourseReviewList = courseId => {
+  return request(`${REVIEW_MODULE}/student/course/${courseId}/review`);
 };
 
 /**
- * 学生查看某次互评结果
+ * 学生查看互评作业详情
  * @param {string|number} reviewId
- * @return {Promise<{data:ReviewResultSerializerType}>}
+ * @return {Promise<{data:ReviewDetailSerializerType}>}
  */
-export const getStudentReviewResultDetail = reviewId => {
-  return request(`${REVIEW_MODULE}/student/review/${reviewId}/result`);
+export const getStudentReviewDetail = reviewId => {
+  return request(`${REVIEW_MODULE}/student/review/${reviewId}`);
 };
 
 /**
- * 老师查看互评作业汇总结果
+ * 学生获取某次互评作业中某个文档
  * @param {string|number} reviewId
- * @return {Promise<{detail:ReviewDetailSerializerType,finishPercentage:number,studentIdCheckListAcceptNumMap:*}>}
+ * @param {string|number} docReviewId
+ * @return {Promise<{data:ReviewDocDetailSerializerType}>}
  */
-export const getReviewSumResult = reviewId => {
-  return request(`${REVIEW_MODULE}/sum/${reviewId}`);
+export const getStudentReviewDocDetail = (reviewId, docReviewId) => {
+  return request(
+    `${REVIEW_MODULE}/student/review/${reviewId}/doc-review/${docReviewId}`
+  );
 };
 
 /**
- * 老师更新作业评分标准
+ * 学生提交某次互评作业中的某个文档
  * @param {string|number} reviewId
- * @param {*} level
- * @return {Promise<{}>}
+ * @param {string|number} docReviewId
+ * @param {{data:CheckListItemSerializerType[]}} checkListBody
+ * @return {Promise<{data:CheckListItemSerializerType[]}>}
  * @throws {StatusException} 输入不合法
  */
-export const postReviewSumLevel = (reviewId, level) => {
-  return request(`${REVIEW_MODULE}/sum/${reviewId}/level`, {
-    method: "POST",
-    body: level
-  });
+export const postStudentReviewWork = (reviewId, docReviewId, checkListBody) => {
+  return request(
+    `${REVIEW_MODULE}/student/review/${reviewId}/doc-review/${docReviewId}`,
+    {
+      method: "POST",
+      body: checkListBody
+    }
+  );
 };
 
 /**
- * 老师获得需要人工复核的文档列表
+ * 学生选择查看某次互评作业中自己的文档
  * @param {string|number} reviewId
- * @return {Promise<ReviewSimpleSerializerType[]>}
+ * @return {Promise<{data:ReviewDocResultSerializerType}>}
  */
-export const getTeacherArgueList = reviewId => {
-  return request(`${REVIEW_MODULE}/sum/${reviewId}/argue`);
+export const getStudentReviewSelfDetail = reviewId => {
+  return request(`${REVIEW_MODULE}/student/review/${reviewId}/result`);
 };
 
 /**
- * 老师获得需要人工复核的文档详情
+ * 学生对某个组的某条checklist提出argue
+ * @param {string|number} reviewId 作业id
+ * @param {string|number} id 某个checkList的id
+ * @param {string|number} groupId 某个小组的id
+ * @param {string} remark 评论
+ * @returns {Promise<{}>}
+ * @throws {StatusException} 输入不合法 | 已评审,不可重复评审
+ */
+export const postStudentArgueReview = ({
+  reviewId,
+  id,
+  groupId,
+  remark = ""
+}) => {
+  return request(`${REVIEW_MODULE}/student/argue`, {
+    method: "POST",
+    body: { reviewId, id, groupId, remark }
+  });
+};
+
+/**
+ * 学生(评审方)判断是否愿意处理argue
  * @param {string|number} reviewId
- * @param {string|number} groupId
- * @return {Promise<{data:ReviewResultSerializerType}>}
+ * @param {string|number} checkListId
+ * @param {boolean} accept
+ * @return {Promise<{data:CheckListItemSerializerType[]}>}
  */
-export const getTeacherArgueDetail = (reviewId, groupId) => {
-  return request(`${REVIEW_MODULE}/sum/${reviewId}/argue/${groupId}`);
+export const postStudentArgueAcceptation = (reviewId, checkListId, accept) => {
+  return request(
+    `${REVIEW_MODULE}/student/review/${reviewId}/checklist/${checkListId}/argue/acceptation`,
+    {
+      method: "POST",
+      body: { accept }
+    }
+  );
 };
 
 /**
- * 老师提交需要复核文档的结果
+ * 学生(评审方)提交argue [标记为已处理完成]
  * @param {string|number} reviewId
- * @param {string|number} groupId
- * @return {Promise<{}>}
+ * @param {string|number} checkListId
+ * @param {CheckListItemSerializerType} checkListItem
+ * @return {Promise<{data:CheckListItemSerializerType[]}>}
  */
-export const postTeacherArgueResult = (reviewId, groupId) => {
-  return request(`${REVIEW_MODULE}/sum/${reviewId}/argue/${groupId}`);
+export const postStudentArgueEnd = (reviewId, checkListId, checkListItem) => {
+  return request(
+    `${REVIEW_MODULE}/student/review/${reviewId}/checklist/${checkListId}/argue/end`,
+    {
+      method: "POST",
+      body: { data: checkListItem }
+    }
+  );
 };

+ 7 - 0
src/assets/scss/app.scss

@@ -80,3 +80,10 @@ $default-margin-between:1.5rem;
   /*min-height: 80vh;*/
   background-color: #ffffff;
 }
+
+.content blockquote {
+  background-color: #fbfbfb;
+  border-left: 3px solid #ced9ec;
+  padding: 1em;
+}
+

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
src/assets/undraw_having_fun.svg


+ 53 - 0
src/components/CardJudgeItem/index.vue

@@ -0,0 +1,53 @@
+<template>
+  <div
+    :class="{
+      'judge-section': true,
+      'judge-success': success,
+      'judge-primary': state === 'SOLVING',
+      'judge-warning': state === 'PENDING',
+      'judge-end': state === 'ARGUED'
+    }"
+  >
+    <slot></slot>
+  </div>
+</template>
+
+<script>
+import { DEFAULT } from "@/data/checkListItemState";
+
+export default {
+  name: "CardJudgeItem",
+  props: {
+    success: {
+      type: Boolean,
+      default: false
+    },
+    state: {
+      type: String,
+      default: DEFAULT
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../assets/scss/app";
+.judge-section {
+  background: $white;
+  border-left: 6px solid $danger;
+  position: relative;
+}
+
+.judge-success {
+  border-left: 6px solid $success;
+}
+.judge-primary {
+  border-left: 6px solid $primary;
+}
+.judge-warning {
+  border-left: 6px solid $warning;
+}
+.judge-end {
+  border-left: 6px solid $grey;
+}
+</style>

+ 143 - 0
src/components/CheckList/ResultCheckListItem.vue

@@ -0,0 +1,143 @@
+<template>
+  <card-judge-item :success="pass">
+    <div class="result-section">
+      <strong>{{ checkResultItem.content }}</strong>
+      <div
+        :class="{ 'group-line--hover': !result.isArgued }"
+        class="group-line"
+        v-for="result in checkResultItem.results"
+        :key="result.rid"
+        @click="argue(result)"
+      >
+        <strong style="padding-right:10px;color:grey"
+          >+ id:{{ result.groupId + "\t" }}</strong
+        >
+        <span class="state state-success" v-if="result.level">PASS</span>
+        <span class="state state-error" v-else>ERROR</span>
+        <span class="comments">{{ result.comment }}</span>
+        <b-tag
+          v-if="result.isArgued"
+          type="is-warning"
+          class="state"
+          style="margin-left: 10px;"
+          >已申请
+        </b-tag>
+      </div>
+    </div>
+
+    <b-modal :active.sync="showModal" :width="400">
+      <div class="card">
+        <div class="title">申请重判</div>
+        <div class="content">
+          <b-field label="理由">
+            <b-input maxlength="200" type="textarea" v-model="remark"></b-input>
+          </b-field>
+
+          <a
+            class="button is-link"
+            :class="{ 'is-loading': submitLoading }"
+            @click="submit()"
+          >
+            提交重判申请
+          </a>
+        </div>
+      </div>
+    </b-modal>
+  </card-judge-item>
+</template>
+<script>
+import CardJudgeItem from "@/components/CardJudgeItem/index";
+export default {
+  name: "ResultCheckListItem",
+  components: { CardJudgeItem },
+  props: {
+    checkResultItem: {
+      type: Object,
+      default: () => ({
+        results: []
+      })
+    },
+    arguable: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      selectedResult: {},
+      showModal: false,
+      remark: "",
+      submitLoading: false
+    };
+  },
+  computed: {
+    pass() {
+      const { results = [] } = this.checkResultItem;
+      return results.filter(result => result.level === false).length === 0;
+    }
+  },
+  methods: {
+    argue(result) {
+      if (this.arguable && !result.isArgued) {
+        this.selectedResult = result;
+        this.showModal = true;
+      }
+    },
+    async submit() {
+      this.submitLoading = true;
+      await this.$emit("argue", {
+        id: this.checkResultItem.id,
+        groupId: this.selectedResult.groupId,
+        remark: this.remark
+      });
+      this.submitLoading = false;
+      this.showModal = false;
+      this.remark = "";
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../assets/scss/app";
+.result-section {
+  padding: $default-margin;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.12);
+}
+
+.card {
+  padding: $default-margin-between;
+  border-radius: $default-radius;
+}
+
+.state {
+  padding: 2px 5px;
+  font-weight: bold;
+  font-size: 12px;
+  border-radius: 3px;
+  color: white;
+}
+
+.state-success {
+  background: $success;
+}
+.state-error {
+  background: $danger;
+}
+.comments {
+  padding-left: 10px;
+  display: inline-block;
+  color: $grey;
+  font-size: 14px;
+}
+
+.group-line {
+  padding: 2px 0;
+  border-radius: $default-radius;
+}
+
+.group-line--hover:hover {
+  cursor: pointer;
+  background-color: #f4f4f4;
+}
+</style>

+ 185 - 0
src/components/CheckList/index.vue

@@ -0,0 +1,185 @@
+<template>
+  <div>
+    <card-judge-item
+      v-for="item in value"
+      :key="item.id"
+      :success="item.level"
+      :state="item.state"
+    >
+      <div>
+        <b-checkbox
+          class="checkbox-section"
+          v-model="item.level"
+          :type="item.state === 'SOLVING' ? 'is-primary' : 'is-success'"
+          :disabled="item.state === 'PENDING' || item.state === 'ARGUED'"
+        >
+          <b-tooltip :label="item.explain" position="is-top">
+            {{ item.content }}
+          </b-tooltip>
+        </b-checkbox>
+        <b-tooltip :label="`申诉备注 : ${item.remark}`" type="is-warning">
+          <b-tag v-if="item.state === 'PENDING'" type="is-warning">
+            学生申诉
+          </b-tag>
+          <b-tag v-if="item.state === 'SOLVING'" type="is-primary">
+            处理申诉
+          </b-tag>
+        </b-tooltip>
+      </div>
+
+      <input
+        v-model="item.comment"
+        class="input-custom"
+        :class="{
+          'input-custom--success': item.level,
+          'input-custom--danger': !item.level,
+          'input-custom--primary': item.state === 'SOLVING',
+          'input-custom--warning': item.state === 'PENDING',
+          'input-custom--end': item.state === 'ARGUED'
+        }"
+        :disabled="item.state === 'PENDING' || item.state === 'ARGUED'"
+        type="text"
+        placeholder="填写备注"
+        style="box-shadow:none;border-color:white"
+      />
+      <div
+        class="action-section--warning action-section "
+        v-if="item.state === 'PENDING'"
+      >
+        <span
+          @click="acceptArgue(item)"
+          :class="{ 'is-loading': acceptArgueLoading }"
+          class="button is-warning"
+          >接受申诉</span
+        >
+        <span
+          @click="refuseArgue(item)"
+          :class="{ 'is-loading': refuseArgueLoading }"
+          class="button is-danger"
+          >拒绝申诉</span
+        >
+      </div>
+      <div
+        class="action-section--primary action-section "
+        v-if="item.state === 'SOLVING'"
+      >
+        <span
+          @click="submitSolution(item)"
+          :class="{ 'is-loading': submitSolutionLoading }"
+          class="button is-primary"
+          >提交处理好的申诉</span
+        >
+      </div>
+    </card-judge-item>
+  </div>
+</template>
+
+<script>
+import CardJudgeItem from "@/components/CardJudgeItem/index";
+export default {
+  name: "CheckListItem",
+  components: { CardJudgeItem },
+  props: {
+    value: {
+      type: Array,
+      default: () => []
+    }
+  },
+  data() {
+    return {
+      acceptArgueLoading: false,
+      refuseArgueLoading: false,
+      submitSolutionLoading: false
+    };
+  },
+  watch: {
+    value: {
+      handler(newValue) {
+        this.$emit("onchange", newValue);
+      },
+      deep: true
+    }
+  },
+  methods: {
+    async acceptArgue(item) {
+      this.acceptArgueLoading = true;
+      await this.$emit("accept-argue", item);
+      this.acceptArgueLoading = false;
+    },
+    async refuseArgue(item) {
+      this.refuseArgueLoading = true;
+      await this.$emit("refuse-argue", item);
+      this.refuseArgueLoading = false;
+    },
+    async submitSolution(item) {
+      this.submitSolutionLoading = true;
+      await this.$emit("submit-solution", item);
+      this.submitSolutionLoading = false;
+    }
+    // getColor(state) {}
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../assets/scss/app";
+.checkbox-section {
+  padding: $default-margin;
+}
+
+.argue-section {
+  padding: $default-margin;
+  background-color: $warning;
+}
+
+.action-section {
+  .button {
+    border-radius: 0;
+  }
+
+  &--warning {
+    .button {
+      width: 50%;
+    }
+  }
+
+  &--primary {
+    .button {
+      width: 100%;
+    }
+  }
+}
+
+.input-custom {
+  padding: $default-margin;
+  width: 100%;
+  border: 0;
+  box-shadow: none;
+  color: hsl(0, 0%, 21%);
+  font-size: $size-normal;
+
+  &--danger {
+    background-color: #fff8fa;
+  }
+
+  &--success {
+    background-color: #ebfff2;
+  }
+
+  &--primary {
+    background-color: lighten($primary, 30%);
+  }
+
+  &--warning {
+    background-color: lighten($warning, 30%);
+  }
+
+  &--end {
+    background-color: lighten($grey, 50%);
+  }
+
+  &::placeholder {
+    /*color: hsl(0, 0%, 96%);*/
+  }
+}
+</style>

+ 5 - 0
src/components/PageHeader/index.scss

@@ -4,6 +4,11 @@
   background: white;
   padding: $default-margin-between $default-margin-between*1.5 0 $default-margin-between*1.5;
   box-shadow: $default-shadow-lighter;
+
+  .tabs a,
+  .tabs ul{
+    border-bottom-color: white;
+  }
   .wide {
     max-width: 1200px;
     margin: auto;

+ 21 - 0
src/components/ReviewStateTag/index.vue

@@ -0,0 +1,21 @@
+<template>
+  <div class="tag" :class="tagDetail.styledClass">{{ tagDetail.name }}</div>
+</template>
+
+<script>
+import { reviewStateHelperFectory, DEFAULT } from "../../data/reviewState";
+export default {
+  name: "ReviewStateTag",
+  props: {
+    state: {
+      type: String,
+      default: DEFAULT
+    }
+  },
+  computed: {
+    tagDetail() {
+      return reviewStateHelperFectory.getStateHelper(this.state);
+    }
+  }
+};
+</script>

+ 38 - 0
src/data/checkListItemState.js

@@ -0,0 +1,38 @@
+export const DEFAULT = "DEFAULT";
+export const SUBMITTED = "SUBMITTED";
+export const PENDING = "PENDING";
+export const SOLVING = "SOLVING";
+export const ARGUED = "ARGUED";
+
+export const ALL_STATE = [DEFAULT, SUBMITTED, PENDING, SOLVING, ARGUED];
+
+/**
+ * @typedef CheckListItemStateHelper
+ * @property {string} name
+ * @property {string} styledClass
+ */
+
+class CheckListItemStateHelperFactory {
+  /**
+   * @param state
+   * @return {ReviewStateHelper}
+   */
+  getStateHelper = state => {
+    switch (state) {
+      case DEFAULT:
+        return { name: "未评价", styledClass: "is-light" };
+      case SUBMITTED:
+        return { name: "已评价", styledClass: "is-success" };
+      case PENDING:
+        return { name: "待处理重判", styledClass: "is-warning" };
+      case SOLVING:
+        return { name: "处理重判期间", styledClass: "is-info" };
+      case ARGUED:
+        return { name: "重判已结束", styledClass: "is-danger" };
+      default:
+        console.log("checklist item state not found : ", state);
+    }
+  };
+}
+
+export const checkListItemStateHelperFactory = new CheckListItemStateHelperFactory();

+ 37 - 0
src/data/reviewState.js

@@ -0,0 +1,37 @@
+export const DEFAULT = "DEFAULT";
+export const REVIEWING = "REVIEWING";
+export const ARGUING = "ARGUING";
+export const SOLVING = "SOLVING";
+export const END = "END";
+export const ALL_STATE = [DEFAULT, REVIEWING, ARGUING, SOLVING, END];
+
+/**
+ * @typedef ReviewStateHelper
+ * @property {string} name
+ * @property {string} styledClass
+ */
+
+class ReviewStateHelperFactory {
+  /**
+   * @param state
+   * @return {ReviewStateHelper}
+   */
+  getStateHelper = state => {
+    switch (state) {
+      case DEFAULT:
+        return { name: "未开始", styledClass: "is-light" };
+      case REVIEWING:
+        return { name: "正在进行", styledClass: "is-success" };
+      case ARGUING:
+        return { name: "申请重判期间", styledClass: "is-warning" };
+      case SOLVING:
+        return { name: "处理重判期间", styledClass: "is-info" };
+      case END:
+        return { name: "已结束", styledClass: "is-danger" };
+      default:
+        console.log("Review state not found : ", state);
+    }
+  };
+}
+
+export const reviewStateHelperFectory = new ReviewStateHelperFactory();

+ 1 - 1
src/layouts/SettingLayout.vue

@@ -2,7 +2,7 @@
   <div class="section">
     <div class="container">
       <div class="columns is-desktop">
-        <div class="column is-3">
+        <div class="column is-3-desktop">
           <aside class="menu">
             <ul class="menu-list">
               <li>

+ 6 - 0
src/layouts/TeacherMainLayout.vue

@@ -0,0 +1,6 @@
+<template>
+  <div>
+    <div></div>
+    <router-view></router-view>
+  </div>
+</template>

+ 1 - 1
src/layouts/courseLayouts/LayoutStructure.vue

@@ -87,7 +87,7 @@ $top-section-height: 4.25rem;
 .menu-section {
   //min-width: $menu-width;
   //width: $left-width-calc;
-  max-width: 300px;
+  width: 280px;
   /*background: white;*/
   /*border-right: 3px solid rgb(159, 148, 254);*/
   padding-left: $left-padding-calc;

+ 10 - 0
src/mixins/time.js

@@ -0,0 +1,10 @@
+import dayjs from "dayjs";
+
+export default {
+  methods: {
+    displayUnixTime(time) {
+      if (!time) return;
+      return dayjs.unix(time).format("YYYY年 MM月 DD日 HH:mm:ss");
+    }
+  }
+};

+ 50 - 4
src/router/routes.js

@@ -99,11 +99,33 @@ export default [
       },
       {
         path: "review",
-        component: () => import("@/views/teacher/Review/assignmentList")
+        component: () => import("@/views/teacher/Review")
       },
       {
-        path: "review/:id",
-        component: () => import("@/views/teacher/Review/assignment")
+        path: "review/:reviewId",
+        component: () => import("@/views/teacher/Review/ReviewDetail"),
+        children: [
+          {
+            path: "",
+            component: () =>
+              import("@/views/teacher/Review/ReviewDetail/Detail")
+          },
+          {
+            path: "groups",
+            component: () =>
+              import("@/views/teacher/Review/ReviewDetail/Groups")
+          }
+        ]
+      },
+      {
+        path: "review/:reviewId/groups/:groupId",
+        component: () =>
+          import("@/views/teacher/Review/ReviewDetail/GroupDocDetail")
+      },
+      {
+        path: "review/:reviewId/groups/:groupId/argue",
+        component: () =>
+          import("@/views/teacher/Review/ReviewDetail/GroupArgueDetail")
       }
     ]
   },
@@ -139,7 +161,31 @@ export default [
       },
       {
         path: "review/:reviewId",
-        component: () => import("@/views/student/Review/ReviewDetail")
+        component: () => import("@/views/student/Review/ReviewDetail"),
+        children: [
+          {
+            path: "self",
+            component: () =>
+              import("@/views/student/Review/ReviewDetail/ReviewSelfDetail")
+          },
+          {
+            path: "",
+            component: () =>
+              import("@/views/student/Review/ReviewDetail/ReviewDocs")
+          }
+        ]
+      },
+      {
+        path: "review/:reviewId/doc/:docReviewId",
+        component: () => import("@/views/student/Review/ReviewDocDetail")
+      },
+      {
+        path: "code",
+        component: () => import("@/views/student/Code")
+      },
+      {
+        path: "group",
+        component: () => import("@/views/student/Group")
       }
     ]
   },

+ 0 - 2
src/views/student/Code/detail.vue

@@ -47,5 +47,3 @@ export default {
   }
 };
 </script>
-
-<style scoped></style>

+ 247 - 1
src/views/student/Group/index.vue

@@ -1,3 +1,249 @@
 <template>
-  <div></div>
+  <div>
+    <page-header :routes="[{ name: '我的小组' }]">
+      <template slot="title">
+        我的小组 {{ this.groups.length > 0 && `「${this.groups[0].name}」` }}
+      </template>
+    </page-header>
+    <page-body>
+      <div v-if="groups.length > 0">
+        <div class="page-section">
+          <div class="columns is-desktop">
+            <div class="column">
+              <h5 class="title is-5">id:</h5>
+              <div class="subtitle is-6">{{ groups[0].id }}</div>
+            </div>
+            <div class="column">
+              <h5 class="title is-5">名称:</h5>
+              <div class="subtitle is-6">{{ groups[0].name }}</div>
+            </div>
+            <div class="column">
+              <h5 class="title is-5">选组码:</h5>
+              <div class="subtitle is-6">{{ groups[0].code }}</div>
+            </div>
+          </div>
+        </div>
+        <div class="divider">
+          <div class="columns is-multiline is-desktop">
+            <div
+              class="column is-3-desktop"
+              :key="member.id"
+              v-for="member in groups[0].members"
+            >
+              <card>
+                <h5 class="title is-5">成员:「{{ member.nickname }}」</h5>
+                <div class="subtitle is-6">{{ member.username }}</div>
+              </card>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div
+        v-if="!isLoadingGroup && groups.length === 0"
+        style="padding: 50px"
+        class="columns is-desktop  is-vcentered"
+      >
+        <div class="column is-three-fifths-desktop">
+          <img src="@/assets/undraw_having_fun.svg" alt="" />
+        </div>
+        <div class="column">
+          <div class="title is-spaced">您还尚未加入任何小组</div>
+          <p class="subtitle">加入或创建小组,一起迎接挑战!</p>
+          <div class="buttons">
+            <a
+              @click="() => (this.showCreateModal = true)"
+              class="button is-primary"
+              >创建小组
+            </a>
+            <a
+              @click="() => (this.showJoinModal = true)"
+              class="button is-primary is-outlined"
+              >加入小组
+            </a>
+          </div>
+        </div>
+      </div>
+    </page-body>
+    <b-modal :active.sync="showCreateModal" :width="400">
+      <div class="card">
+        <div class="title">创建小组</div>
+        <b-notification type="is-danger" :active.sync="isServerError">
+          {{ errorMassage }}
+        </b-notification>
+        <div class="content">
+          <b-field
+            label="名称"
+            :type="$v.name.$error ? 'is-danger' : ''"
+            :message="
+              $v.name.$error
+                ? [
+                    !$v.name.basicChar
+                      ? '名称只能由大小写字母,数字,- 与 _ 组成'
+                      : undefined,
+                    !$v.name.required ? '请输入名称' : undefined
+                  ]
+                : ''
+            "
+          >
+            <b-input maxlength="50" v-model="$v.name.$model"></b-input>
+          </b-field>
+
+          <a
+            class="button is-primary"
+            :class="{ 'is-loading': submitLoading }"
+            @click="createGroup()"
+          >
+            创建小组
+          </a>
+        </div>
+      </div>
+    </b-modal>
+    <b-modal :active.sync="showJoinModal" :width="400">
+      <div class="card">
+        <div class="title">加入小组</div>
+        <b-notification type="is-danger" :active.sync="isServerError">
+          {{ errorMassage }}
+        </b-notification>
+        <div class="content">
+          <b-field
+            label="选组码"
+            :type="$v.code.$error ? 'is-danger' : ''"
+            :message="
+              $v.name.$error
+                ? [!$v.code.required ? '请输入选组码' : undefined]
+                : ''
+            "
+          >
+            <b-input v-model="$v.code.$model"></b-input>
+          </b-field>
+          <a
+            class="button is-primary"
+            :class="{ 'is-loading': submitLoading }"
+            @click="joinGroup()"
+          >
+            申请加入小组
+          </a>
+        </div>
+      </div>
+    </b-modal>
+  </div>
 </template>
+<script>
+import PageHeader from "@/components/PageHeader";
+import PageBody from "@/components/PageBody/index";
+import Card from "@/components/Card";
+import { required } from "vuelidate/lib/validators";
+import {
+  getStudentGroup,
+  postStudentGroup,
+  putStudentGroup
+} from "@/api/group";
+export default {
+  components: { PageBody, PageHeader, Card },
+  data() {
+    return {
+      showCreateModal: false,
+      showJoinModal: false,
+      name: "",
+      code: "",
+      isServerError: false,
+      errorMassage: "",
+      submitLoading: false,
+      isLoadingGroup: false,
+      groups: []
+    };
+  },
+  validations: {
+    name: {
+      required,
+      basicChar: name => /^[a-zA-Z0-9_-]*$/.test(name)
+    },
+    code: {
+      required
+    }
+  },
+  watch: {
+    showCreateModal(newValue) {
+      if (newValue === false) {
+        this.onModalClosed();
+      }
+    },
+    showJoinModal(newValue) {
+      if (newValue === false) {
+        this.onModalClosed();
+      }
+    }
+  },
+  mounted() {
+    this.loadGroup();
+  },
+  methods: {
+    async createGroup() {
+      this.$v.$touch();
+      if (!this.$v.name.$invalid) {
+        try {
+          this.submitLoading = true;
+          this.isServerError = false;
+          await postStudentGroup(this.name);
+          this.$toast.open({
+            message: `小组「${this.name}」创建成功`,
+            type: "is-success"
+          });
+          this.showCreateModal = false;
+          this.loadGroup();
+        } catch (e) {
+          this.errorMassage = e.message;
+          this.isServerError = true;
+        } finally {
+          this.submitLoading = false;
+        }
+      }
+    },
+    async joinGroup() {
+      this.$v.$touch();
+      if (!this.$v.code.$invalid) {
+        try {
+          this.submitLoading = true;
+          this.isServerError = false;
+          await putStudentGroup(this.code);
+          this.$toast.open({
+            message: `小组加入成功`,
+            type: "is-success"
+          });
+          this.showJoinModal = false;
+          this.loadGroup();
+        } catch (e) {
+          this.errorMassage = e.message;
+          this.isServerError = true;
+        } finally {
+          this.submitLoading = false;
+        }
+      }
+    },
+    onModalClosed() {
+      this.isServerError = false;
+      this.submitLoading = false;
+    },
+    async loadGroup() {
+      this.isLoadingGroup = true;
+      const { courseId } = this.$route.params;
+      const { data } = await getStudentGroup(courseId);
+      this.groups = data;
+      this.isLoadingGroup = false;
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../../assets/scss/app";
+.card {
+  padding: $default-margin-between;
+  border-radius: $default-radius;
+}
+
+.divider {
+  padding-top: $default-margin-between;
+}
+</style>

+ 1 - 1
src/views/student/Home/index.vue

@@ -10,7 +10,7 @@
     </section>
     <div class="container">
       <div class="columns is-multiline is-desktop">
-        <div :key="item.id" v-for="item in courses" class="column is-4">
+        <div :key="item.id" v-for="item in courses" class="column is-4-desktop">
           <router-link :to="`/course-student/${item.id}/dashboard`">
             <card hoverable>{{ item.name }}</card>
           </router-link>

+ 29 - 0
src/views/student/Review/ReviewDetail/ReviewDocs/index.vue

@@ -0,0 +1,29 @@
+<template>
+  <div class="columns is-multiline is-desktop">
+    <div
+      :key="item.docReviewId"
+      v-for="item in docs"
+      class="column is-4-desktop"
+    >
+      <router-link :to="`${$route.path}/doc/${item.docReviewId}`">
+        <card hoverable> {{ item.group.name }} </card>
+      </router-link>
+    </div>
+  </div>
+</template>
+
+<script>
+import Card from "@/components/Card";
+
+export default {
+  components: {
+    Card
+  },
+  props: {
+    docs: {
+      type: Array,
+      default: () => []
+    }
+  }
+};
+</script>

+ 90 - 0
src/views/student/Review/ReviewDetail/ReviewSelfDetail/index.vue

@@ -0,0 +1,90 @@
+<template>
+  <div>
+    <div class="columns">
+      <div class="column is-8">
+        <div class="page-section">
+          <div v-if="detail.docContent" class="content">
+            <vue-markdown>{{ detail.docContent }}</vue-markdown>
+          </div>
+        </div>
+      </div>
+      <div class="column is-4">
+        <div class="right-part">
+          <result-check-list-item
+            v-for="item in detail.checkListResult"
+            :arguable="true"
+            :key="item.id"
+            :check-result-item="item"
+            @argue="argue"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import VueMarkdown from "vue-markdown";
+import {
+  getStudentReviewSelfDetail,
+  postStudentArgueReview
+} from "@/api/review";
+import timeMixins from "@/mixins/time";
+import ResultCheckListItem from "@/components/CheckList/ResultCheckListItem";
+export default {
+  components: {
+    ResultCheckListItem,
+    VueMarkdown
+  },
+  data() {
+    return {
+      detail: {
+        review: {},
+        checkListResult: []
+      },
+      submitLoading: false
+    };
+  },
+  mixins: [timeMixins],
+  mounted() {
+    const { reviewId } = this.$route.params;
+    getStudentReviewSelfDetail(reviewId).then(
+      resp => (this.detail = resp.data)
+    );
+  },
+  methods: {
+    async argue(item) {
+      const { reviewId } = this.$route.params;
+      try {
+        const { data } = await postStudentArgueReview({ reviewId, ...item });
+        this.detail.checkList = data;
+      } catch (e) {
+        this.$toast.open({
+          message: `重复的申请评审是不被允许的`,
+          position: "is-bottom",
+          type: "is-danger"
+        });
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../../../../assets/scss/app";
+.right-part {
+  margin-bottom: -1.5rem;
+  margin-right: -1.5rem;
+  background: white;
+  border-top: 1px solid #eff2f7;
+  /*position: sticky;*/
+  /*top: 0;*/
+  /*max-height: 100vh;*/
+  /*overflow-y: auto;*/
+  box-shadow: $default-shadow;
+}
+
+.action-button {
+  width: 100%;
+  border-radius: 0;
+}
+</style>

+ 28 - 15
src/views/student/Review/ReviewDetail/index.vue

@@ -1,39 +1,52 @@
 <template>
   <div>
     <page-header
-      :routes="[
-        { name: '互评作业', path: 'review' },
-        { name: '文档详情', path: $route.path }
+      :routes="[{ name: '互评作业', path: 'review' }, { name: '文档详情' }]"
+      :tab-list="[
+        { name: '待评文档', path: `review/${$route.params.reviewId}` },
+        { name: '我的文档', path: `review/${$route.params.reviewId}/self` }
       ]"
     >
       <template slot="title">
         {{ detail.name }}
       </template>
-    </page-header>
-    <page-body>
-      <div class="review-body">
-        <div class="review-body__left page-section">
-          <div v-if="detail.docContent" class="content">
-            <vue-markdown>{{ detail.docContent }}</vue-markdown>
-          </div>
+      <template slot="content">
+        <div>
+          共需要处理 {{ detail.documents && detail.documents.length }} 组作业
+        </div>
+      </template>
+      <template slot="action">
+        <div><review-state-tag :state="detail.state"></review-state-tag></div>
+      </template>
+      <template slot="extraContent">
+        <div class="tag is-light" v-if="detail.state === 'REVIEWING'">
+          互评结束时间 : {{ displayUnixTime(detail.reviewDdl) }}
         </div>
-        <div class="review-body__right"></div>
-      </div>
-    </page-body>
+        <div class="tag is-light" v-if="detail.state === 'ARGUING'">
+          申诉结束时间 : {{ displayUnixTime(detail.argueDdl) }}
+        </div>
+        <div class="tag is-light" v-if="detail.state === 'SOLVING'">
+          处理申诉结束时间 : {{ displayUnixTime(detail.endDdl) }}
+        </div>
+      </template>
+    </page-header>
+    <page-body> <router-view :docs="detail.documents" /> </page-body>
   </div>
 </template>
 <script>
 import PageHeader from "@/components/PageHeader";
 import PageBody from "@/components/PageBody/index";
-import VueMarkdown from "vue-markdown";
 import { getStudentReviewDetail } from "@/api/review";
+import timeMixins from "@/mixins/time";
+import ReviewStateTag from "@/components/ReviewStateTag/index";
 export default {
-  components: { PageBody, PageHeader, VueMarkdown },
+  components: { ReviewStateTag, PageBody, PageHeader },
   data() {
     return {
       detail: {}
     };
   },
+  mixins: [timeMixins],
   mounted() {
     const { reviewId } = this.$route.params;
     getStudentReviewDetail(reviewId).then(resp => (this.detail = resp.data));

+ 142 - 0
src/views/student/Review/ReviewDocDetail/index.vue

@@ -0,0 +1,142 @@
+<template>
+  <div>
+    <page-header
+      :routes="[
+        { name: '互评作业', path: 'review' },
+        { name: '文档详情', path: `review/${$route.params.reviewId}` },
+        { name: '被评审小组文档', path: $route.path }
+      ]"
+    >
+      <template slot="title">
+        {{ detail.review.name }}
+      </template>
+      <template slot="action">
+        <div>
+          <review-state-tag :state="detail.review.state"></review-state-tag>
+        </div>
+      </template>
+    </page-header>
+    <page-body>
+      <div class="columns">
+        <div class="column is-8">
+          <div class="page-section">
+            <div v-if="detail.docContent" class="content">
+              <vue-markdown>{{ detail.docContent }}</vue-markdown>
+            </div>
+          </div>
+        </div>
+        <div class="column is-4">
+          <div class="right-part">
+            <div
+              class="button  is-medium is-primary action-button"
+              :class="{ 'is-loading': submitLoading }"
+              @click="submitCheckList"
+            >
+              提 交
+            </div>
+            <check-list
+              v-model="detail.checkList"
+              @submit-solution="submitSolution"
+              @accept-argue="acceptArgue"
+              @refuse-argue="refuseArgue"
+            />
+          </div>
+        </div>
+      </div>
+    </page-body>
+  </div>
+</template>
+<script>
+import PageHeader from "@/components/PageHeader";
+import PageBody from "@/components/PageBody/index";
+import VueMarkdown from "vue-markdown";
+import {
+  getStudentReviewDocDetail,
+  postStudentArgueAcceptation,
+  postStudentArgueEnd,
+  postStudentReviewWork
+} from "@/api/review";
+import timeMixins from "@/mixins/time";
+import ReviewStateTag from "@/components/ReviewStateTag/index";
+import CheckList from "@/components/CheckList/index";
+export default {
+  components: {
+    CheckList,
+    ReviewStateTag,
+    PageBody,
+    PageHeader,
+    VueMarkdown
+  },
+  data() {
+    return {
+      detail: {
+        review: {},
+        checkList: []
+      },
+      submitLoading: false
+    };
+  },
+  mixins: [timeMixins],
+  mounted() {
+    const { reviewId, docReviewId } = this.$route.params;
+    getStudentReviewDocDetail(reviewId, docReviewId).then(
+      resp => (this.detail = resp.data)
+    );
+  },
+  methods: {
+    submitCheckList() {
+      const { reviewId, docReviewId } = this.$route.params;
+      this.submitLoading = true;
+      postStudentReviewWork(reviewId, docReviewId, this.detail.checkList).then(
+        resp => {
+          this.submitLoading = false;
+          this.detail.checkList = resp.data;
+        }
+      );
+    },
+    async submitSolution(item) {
+      const { reviewId } = this.$route.params;
+      const { data } = await postStudentArgueEnd(reviewId, item.id, item);
+      this.detail.checkList = data;
+    },
+    async refuseArgue(item) {
+      const { reviewId } = this.$route.params;
+      const { data } = await postStudentArgueAcceptation(
+        reviewId,
+        item.id,
+        false
+      );
+      this.detail.checkList = data;
+    },
+    async acceptArgue(item) {
+      const { reviewId } = this.$route.params;
+      const { data } = await postStudentArgueAcceptation(
+        reviewId,
+        item.id,
+        true
+      );
+      this.detail.checkList = data;
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../../../assets/scss/app";
+.right-part {
+  margin-bottom: -1.5rem;
+  margin-right: -1.5rem;
+  background: white;
+  border-top: 1px solid #eff2f7;
+  position: sticky;
+  top: 0;
+  max-height: 100vh;
+  overflow-y: auto;
+  box-shadow: $default-shadow;
+}
+
+.action-button {
+  width: 100%;
+  border-radius: 0;
+}
+</style>

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


+ 8 - 3
src/views/student/Review/index.vue

@@ -10,9 +10,12 @@
     </page-header>
     <page-body>
       <div class="columns is-multiline is-desktop">
-        <div :key="item.id" v-for="item in review" class="column is-4">
-          <router-link :to="`${$route.path}/${item.id}`">
-            <card hoverable>{{ item.name }}</card>
+        <div :key="item.id" v-for="item in review" class="column is-4-desktop">
+          <router-link :to="`${$route.path}/${item.reviewId}`">
+            <card hoverable>
+              {{ item.name }}
+              <div><review-state-tag :state="item.state" /></div>
+            </card>
           </router-link>
         </div>
       </div>
@@ -26,8 +29,10 @@ import { getStudentCourseReviewList } from "@/api/review";
 import Card from "@/components/Card";
 import PageHeader from "@/components/PageHeader";
 import PageBody from "@/components/PageBody/index";
+import ReviewStateTag from "@/components/ReviewStateTag/index";
 export default {
   components: {
+    ReviewStateTag,
     PageBody,
     PageHeader,
     Card

+ 0 - 1
src/views/teacher/Code/detail.vue

@@ -196,7 +196,6 @@ export default {
   }
 };
 </script>
-
 <style scoped>
 .breadcrumb a {
   /*color: #7957d5;*/

+ 0 - 23
src/views/teacher/Code/index.vue

@@ -119,29 +119,6 @@
 import PageHeader from "@/components/PageHeader";
 import PageBody from "@/components/PageBody/index";
 import { getCodeAssignmentListByTeacher } from "@/api/assignment";
-// const codeData = [
-//   {
-//     id: 1,
-//     name: "购物车web项目",
-//     description: "1111111111111这里是文档描述",
-//     startAt: 1547107174000,
-//     endAt: 1547971174000
-//   },
-//   {
-//     id: 2,
-//     name: "电影票web项目",
-//     description: "222222222222这里是文档描述",
-//     startAt: 1547107174000,
-//     endAt: 1547971174000
-//   },
-//   {
-//     id: 3,
-//     name: "SEECweb项目",
-//     description: "3333333333333333这里是文档描述",
-//     startAt: 1547107174000,
-//     endAt: 1547971174000
-//   }
-// ];
 export default {
   components: {
     PageBody,

+ 2 - 0
src/views/teacher/Document/index.vue

@@ -204,6 +204,7 @@ export default {
     createDocAssignment() {
       this.isCardModalActive = false;
       if (this.docAssignment.id === -1) {
+        // 新建作业
         var itemsCount = this.data.length;
         this.data.push({
           id: itemsCount + 1,
@@ -213,6 +214,7 @@ export default {
           endAt: this.docAssignment.endAt.getTime()
         });
       } else {
+        // 修改作业
         for (var item of this.data) {
           if (item.id === this.docAssignment.id) {
             item.name = this.docAssignment.name;

+ 2 - 8
src/views/teacher/Question/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div>
-    <page-header :routes="[{ name: `查看题目`, path: 'question' }]">
+    <page-header :routes="[{ name: `查看题目`, path: 'question/' }]">
       <template slot="title">
         查看题目
       </template>
@@ -44,13 +44,7 @@
         </div>
         <!--题目table-->
         <div class="question-list">
-          <b-table
-            :data="data"
-            paginated
-            per-page="10"
-            detailed
-            detail-key="id"
-          >
+          <b-table :data="data" detailed detail-key="id">
             <template slot-scope="props">
               <b-table-column field="id" label="ID" width="40" numeric sortable>
                 {{ props.row.id }}

+ 1 - 1
src/views/teacher/Question/questionDetail.vue

@@ -2,7 +2,7 @@
   <div>
     <page-header
       :routes="[
-        { name: '查看题目', path: 'question' },
+        { name: '查看题目', path: 'question/' },
         { name: `${questionName}`, path: `question/123` }
       ]"
     >

+ 1 - 1
src/views/teacher/Result/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div>
-    <page-header :routes="[{ name: '成绩管理', path: 'result' }]">
+    <page-header :routes="[{ name: '成绩管理', path: 'result/' }]">
       <template slot="title">
         全部作业成绩
       </template>

+ 114 - 0
src/views/teacher/Review/ReviewDetail/Detail/index.vue

@@ -0,0 +1,114 @@
+<template>
+  <div>
+    <div class="page-section">
+      <div class="columns is-desktop">
+        <div class="column">
+          <h5 class="title is-5">id:</h5>
+          <div class="subtitle is-6">{{ detail.reviewId }}</div>
+        </div>
+        <div class="column">
+          <h5 class="title is-5">名称:</h5>
+          <div class="subtitle is-6">{{ detail.name }}</div>
+        </div>
+        <div class="column">
+          <h5 class="title is-5">状态:</h5>
+          <div class="subtitle is-6">
+            <review-state-tag :state="detail.state" />
+          </div>
+        </div>
+      </div>
+      <div class="columns is-desktop">
+        <div class="column">
+          <h5 class="title is-5">互评结束时间:</h5>
+          <div class="subtitle is-6">
+            {{ displayUnixTime(detail.reviewDdl) }}
+          </div>
+        </div>
+        <div class="column">
+          <h5 class="title is-5">重申申请结束时间:</h5>
+          <div class="subtitle is-6">
+            {{ displayUnixTime(detail.argueDdl) }}
+          </div>
+        </div>
+        <div class="column">
+          <h5 class="title is-5">重申结束时间:</h5>
+          <div class="subtitle is-6">{{ displayUnixTime(detail.endDdl) }}</div>
+        </div>
+      </div>
+    </div>
+    <div class="space">
+      <div class="columns is-desktop">
+        <div class="column">
+          <div class="page-section">
+            <h5 class="title is-5">完成率: {{ sum.completeRate }}%</h5>
+            <progress
+              class="progress is-success"
+              :value="sum.completeRate"
+              max="100"
+              >{sum.completeRate}%</progress
+            >
+          </div>
+        </div>
+        <div class="column">
+          <div class="page-section">
+            <h5 class="title is-5">checkList 通过情况:</h5>
+            <div
+              class="checklist-item"
+              :key="item.id"
+              v-for="item in sum.checklistItems"
+            >
+              <div>id: {{ item.id }}</div>
+              <b-tag type="is-success" style="margin-right: 5px"
+                >通过 : {{ item.passedNum }}</b-tag
+              >
+              <span>{{ item.content }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import ReviewStateTag from "@/components/ReviewStateTag/index";
+import timeMixins from "@/mixins/time";
+import { getTeacherReviewResult } from "@/api/review";
+export default {
+  components: { ReviewStateTag },
+  mixins: [timeMixins],
+  props: {
+    detail: {
+      type: Object,
+      default: () => ({ completeRate: 0 })
+    }
+  },
+  data() {
+    return {
+      sum: { passedCDF: [], checklistItems: [] }
+    };
+  },
+  mounted() {
+    getTeacherReviewResult(this.$route.params.reviewId).then(
+      value => (this.sum = value.data)
+    );
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../../../../assets/scss/app";
+.divider {
+  margin: $default-margin 0;
+  height: 1px;
+  background-color: #eaeaea;
+}
+
+.space {
+  padding-top: $default-margin-between;
+}
+
+.checklist-item {
+  margin: $default-margin 0;
+}
+</style>

+ 121 - 0
src/views/teacher/Review/ReviewDetail/GroupArgueDetail/index.vue

@@ -0,0 +1,121 @@
+<template>
+  <div>
+    <page-header
+      :routes="[
+        { name: '互评作业', path: 'review' },
+        { name: '文档详情', path: `review/${$route.params.reviewId}` },
+        {
+          name: '小组作业',
+          path: `review/${$route.params.reviewId}/groups/${
+            $route.params.groupId
+          }`
+        },
+        {
+          name: '重评作业'
+        }
+      ]"
+    >
+      <template slot="title">
+        {{ groupDetail.name }}
+      </template>
+      <template slot="action">
+        <router-link
+          :to="$route.path.substring(0, $route.path.length - '/argue'.length)"
+          class="button is-link"
+        >
+          返回小组详情
+        </router-link>
+      </template>
+    </page-header>
+    <page-body>
+      <div class="columns">
+        <div class="column is-8">
+          <div class="page-section">
+            <div v-if="detail.docContent" class="content">
+              <vue-markdown>{{ detail.docContent }}</vue-markdown>
+            </div>
+          </div>
+        </div>
+        <div class="column is-4">
+          <div class="right-part">
+            <div
+              class="button is-medium is-primary action-button"
+              :class="{ 'is-loading': submitLoading }"
+              @click="submitCheckList"
+            >
+              提 交
+            </div>
+            <check-list v-model="detail.checkList" />
+          </div>
+        </div>
+      </div>
+    </page-body>
+  </div>
+</template>
+<script>
+import VueMarkdown from "vue-markdown";
+import { getTeacherStartArgue, postTeacherArgueResult } from "@/api/review";
+import timeMixins from "@/mixins/time";
+import PageHeader from "@/components/PageHeader";
+import PageBody from "@/components/PageBody/index";
+import { getGroupDetail } from "@/api/group";
+import CheckList from "@/components/CheckList/index";
+export default {
+  components: {
+    PageBody,
+    PageHeader,
+    CheckList,
+    VueMarkdown
+  },
+  data() {
+    return {
+      detail: {
+        review: {},
+        checkList: []
+      },
+      groupDetail: {},
+      submitLoading: false
+    };
+  },
+  mixins: [timeMixins],
+  mounted() {
+    const { reviewId, groupId } = this.$route.params;
+    getGroupDetail(groupId).then(value => (this.groupDetail = value.data));
+    getTeacherStartArgue(reviewId, groupId).then(
+      resp => (this.detail = resp.data)
+    );
+  },
+  methods: {
+    submitCheckList() {
+      const { reviewId, groupId } = this.$route.params;
+      this.submitLoading = true;
+      postTeacherArgueResult(reviewId, groupId, this.detail.checkList).then(
+        resp => {
+          this.submitLoading = false;
+          this.detail.checkList = resp.checkList;
+        }
+      );
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../../../../assets/scss/app";
+.right-part {
+  margin-bottom: -1.5rem;
+  margin-right: -1.5rem;
+  background: white;
+  border-top: 1px solid #eff2f7;
+  /*position: sticky;*/
+  /*top: 0;*/
+  /*max-height: 100vh;*/
+  /*overflow-y: auto;*/
+  box-shadow: $default-shadow;
+}
+
+.action-button {
+  width: 100%;
+  border-radius: 0;
+}
+</style>

+ 100 - 0
src/views/teacher/Review/ReviewDetail/GroupDocDetail/index.vue

@@ -0,0 +1,100 @@
+<template>
+  <div>
+    <page-header
+      :routes="[
+        { name: '互评作业', path: 'review' },
+        { name: '文档详情', path: `review/${$route.params.reviewId}` },
+        {
+          name: '小组作业',
+          path: `review/${$route.params.reviewId}/groups/${
+            $route.params.groupId
+          }`
+        }
+      ]"
+    >
+      <template slot="title">
+        {{ groupDetail.name }}
+      </template>
+      <template slot="action">
+        <router-link :to="`${$route.path}/argue`" class="button is-primary">
+          开始重评
+        </router-link>
+      </template>
+    </page-header>
+    <page-body>
+      <div class="columns">
+        <div class="column is-8">
+          <div class="page-section">
+            <div v-if="detail.docContent" class="content">
+              <vue-markdown>{{ detail.docContent }}</vue-markdown>
+            </div>
+          </div>
+        </div>
+        <div class="column is-4">
+          <div class="right-part">
+            <result-check-list-item
+              v-for="item in detail.checkListResult"
+              :key="item.id"
+              :check-result-item="item"
+            />
+          </div>
+        </div>
+      </div>
+    </page-body>
+  </div>
+</template>
+<script>
+import VueMarkdown from "vue-markdown";
+import { getTeacherReviewGroupDetail } from "@/api/review";
+import timeMixins from "@/mixins/time";
+import ResultCheckListItem from "@/components/CheckList/ResultCheckListItem";
+import PageHeader from "@/components/PageHeader";
+import PageBody from "@/components/PageBody/index";
+import { getGroupDetail } from "@/api/group";
+export default {
+  components: {
+    PageBody,
+    PageHeader,
+    ResultCheckListItem,
+    VueMarkdown
+  },
+  data() {
+    return {
+      detail: {
+        review: {},
+        checkListResult: []
+      },
+      groupDetail: {},
+      submitLoading: false
+    };
+  },
+  mixins: [timeMixins],
+  mounted() {
+    const { reviewId, groupId } = this.$route.params;
+    getGroupDetail(groupId).then(value => (this.groupDetail = value.data));
+    getTeacherReviewGroupDetail(reviewId, groupId).then(
+      resp => (this.detail = resp.data)
+    );
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../../../../assets/scss/app";
+.right-part {
+  margin-bottom: -1.5rem;
+  margin-right: -1.5rem;
+  background: white;
+  border-top: 1px solid #eff2f7;
+  /*position: sticky;*/
+  /*top: 0;*/
+  /*max-height: 100vh;*/
+  /*overflow-y: auto;*/
+  box-shadow: $default-shadow;
+}
+
+.action-button {
+  width: 100%;
+  border-radius: 0;
+}
+</style>

+ 55 - 0
src/views/teacher/Review/ReviewDetail/Groups/index.vue

@@ -0,0 +1,55 @@
+<template>
+  <div>
+    <div class="input-area">
+      <b-input v-model="filter" size="is-medium" placeholder="输入小组名来搜索">
+      </b-input>
+    </div>
+    <div class="columns is-multiline is-desktop">
+      <div
+        :key="item.id"
+        v-for="item in availableGroups"
+        class="column is-4-desktop"
+      >
+        <router-link :to="`${$route.path}/${item.id}`">
+          <card hoverable> {{ item.name }} </card>
+        </router-link>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Card from "@/components/Card";
+export default {
+  components: {
+    Card
+  },
+  props: {
+    groups: {
+      type: Array,
+      default: () => []
+    }
+  },
+  data() {
+    return {
+      filter: ""
+    };
+  },
+  computed: {
+    availableGroups() {
+      if (this.filter === "") return this.groups;
+
+      return this.groups.filter(group => group.name.indexOf(this.filter) >= 0);
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../../../../assets/scss/app";
+.input-area {
+  margin: 0 auto;
+  max-width: 600px;
+  padding-bottom: $default-margin-between;
+}
+</style>

+ 142 - 0
src/views/teacher/Review/ReviewDetail/index.vue

@@ -0,0 +1,142 @@
+<template>
+  <div>
+    <page-header
+      :routes="[{ name: '互评作业', path: 'review' }, { name: '文档详情' }]"
+      :tab-list="[
+        { name: '作业详情', path: `review/${$route.params.reviewId}` },
+        { name: '小组列表', path: `review/${$route.params.reviewId}/groups` }
+      ]"
+    >
+      <template slot="title">
+        {{ detail.name }}
+      </template>
+      <template slot="content">
+        <div>共有 {{ groups.length }} 组小组参与互评</div>
+      </template>
+      <template slot="action">
+        <div @click="() => (this.showModal = true)" class="button is-primary">
+          制定评分标准
+        </div>
+      </template>
+      <template slot="extraContent">
+        <div class="tag is-light" v-if="detail.state === 'REVIEWING'">
+          互评结束时间 : {{ displayUnixTime(detail.reviewDdl) }}
+        </div>
+        <div class="tag is-light" v-if="detail.state === 'ARGUING'">
+          申诉结束时间 : {{ displayUnixTime(detail.argueDdl) }}
+        </div>
+        <div class="tag is-light" v-if="detail.state === 'SOLVING'">
+          处理申诉结束时间 : {{ displayUnixTime(detail.endDdl) }}
+        </div>
+      </template>
+    </page-header>
+    <page-body> <router-view :detail="detail" :groups="groups" /> </page-body>
+    <b-modal :active.sync="showModal" :width="400">
+      <div class="card">
+        <div class="title">每个等级通过数字</div>
+        <div class="content">
+          <b-field label="A">
+            <b-input type="number" v-model="grade.A"></b-input>
+          </b-field>
+          <b-field label="B">
+            <b-input type="number" v-model="grade.B"></b-input>
+          </b-field>
+          <b-field label="C">
+            <b-input type="number" v-model="grade.C"></b-input>
+          </b-field>
+          <b-field label="D">
+            <b-input type="number" v-model="grade.D"></b-input>
+          </b-field>
+          <b-field label="E">
+            <b-input type="number" v-model="grade.E"></b-input>
+          </b-field>
+          <b-field label="F">
+            <b-input type="number" v-model="grade.F"></b-input>
+          </b-field>
+
+          <a
+            class="button is-link"
+            :class="{ 'is-loading': submitLoading }"
+            @click="submit()"
+          >
+            更新评分标准
+          </a>
+        </div>
+      </div>
+    </b-modal>
+  </div>
+</template>
+<script>
+import PageHeader from "@/components/PageHeader";
+import PageBody from "@/components/PageBody/index";
+import {
+  getTeacherReviewAllGroups,
+  getTeacherReviewDetail,
+  postReviewSumLevel
+} from "@/api/review";
+import timeMixins from "@/mixins/time";
+export default {
+  components: { PageBody, PageHeader },
+  data() {
+    return {
+      detail: {},
+      groups: [],
+      showModal: false,
+      grade: {
+        A: 0,
+        B: 0,
+        C: 0,
+        D: 0,
+        E: 0,
+        F: 0
+      },
+      submitLoading: false
+    };
+  },
+  mixins: [timeMixins],
+  mounted() {
+    const { reviewId } = this.$route.params;
+    getTeacherReviewDetail(reviewId).then(resp => (this.detail = resp.data));
+    getTeacherReviewAllGroups(reviewId).then(resp => (this.groups = resp.data));
+  },
+  methods: {
+    async submit() {
+      const { reviewId } = this.$route.params;
+      this.submitLoading = true;
+      try {
+        await postReviewSumLevel(reviewId, this.grade);
+      } catch (e) {
+        this.$toast.open({
+          message: e.message,
+          type: "is-error"
+        });
+      }
+      this.submitLoading = false;
+      this.showModal = false;
+      this.$toast.open({
+        message: "制定成功",
+        type: "is-success"
+      });
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../../../assets/scss/app";
+.review-body {
+  display: flex;
+
+  &__left {
+    flex-grow: 1;
+  }
+
+  &__right {
+    min-width: 300px;
+  }
+}
+.card {
+  padding: $default-margin-between;
+  border-radius: $default-radius;
+}
+</style>

+ 0 - 142
src/views/teacher/Review/assignmentList.vue

@@ -1,142 +0,0 @@
-<template>
-  <div>
-    <page-header :routes="[{ name: '互评作业', path: 'review' }]">
-      <template slot="title">
-        全部互评作业
-      </template>
-      <template slot="content">
-        当前有3份互评作业进行中
-      </template>
-    </page-header>
-    <page-body>
-      <div class="page-section">
-        <!--<div class="submit-state">-->
-        <!--<span>提交情况</span>-->
-        <!--<progress class="progress is-link" value="30" max="100">40%</progress>-->
-        <!--50/200-->
-        <!--</div>-->
-        <div class="homework-list">
-          <section>
-            <b-tabs type="is-boxed">
-              <b-tab-item label="进行中的作业">
-                <router-link
-                  :to="`/course-teacher/${this.course.id}/review/123`"
-                  ><b-table
-                    :data="homeworkList"
-                    :columns="columns"
-                    :hoverable="true"
-                  ></b-table
-                ></router-link>
-              </b-tab-item>
-              <b-tab-item label="已结束作业">
-                <div class="info">暂无已结束作业</div>
-              </b-tab-item>
-            </b-tabs>
-          </section>
-        </div>
-      </div>
-    </page-body>
-  </div>
-</template>
-<script>
-import { mapState } from "vuex";
-import PageHeader from "@/components/PageHeader";
-import PageBody from "@/components/PageBody/index";
-const homeworkList = [
-  {
-    id: 1,
-    name: "物流系统需求规格说明文档",
-    beginAt: "2018-12-29",
-    endAt: "2019-01-06",
-    group: 2
-  },
-  {
-    id: 2,
-    name: "物流系统用例图",
-    beginAt: "2018-12-29",
-    endAt: "2019-01-06",
-    group: 3
-  },
-  {
-    id: 3,
-    name: "物流系统体系架构文档",
-    beginAt: "2018-12-29",
-    endAt: "2019-01-06",
-    group: 4
-  }
-];
-
-const columns = [
-  {
-    field: "id",
-    label: "ID",
-    numeric: true
-  },
-  {
-    field: "name",
-    label: "作业名",
-    centered: true
-  },
-  {
-    field: "beginAt",
-    label: "开始时间",
-    centered: true
-  },
-  {
-    field: "endAt",
-    label: "结束时间",
-    centered: true
-  }
-];
-
-export default {
-  components: {
-    PageBody,
-    PageHeader
-  },
-  computed: {
-    ...mapState({
-      course: state => state.course.currentCourse
-    })
-  },
-  data() {
-    return {
-      homeworkList,
-      columns
-    };
-  },
-  mounted() {}
-};
-</script>
-<style lang="scss" scoped>
-.breadcrumb a {
-  font-weight: bold;
-}
-.data-table {
-  width: 100%;
-  padding: 30px;
-  /*min-height: 80vh;*/
-  background-color: #ffffff;
-  .submit-state {
-    display: flex;
-    align-items: center;
-    font-size: 1rem;
-    font-weight: bold;
-    span {
-      margin-right: 1.5rem;
-    }
-    progress {
-      width: 50%;
-      margin-right: 1.5rem;
-    }
-  }
-  .homework-list {
-    .info {
-      display: flex;
-      justify-content: center;
-      margin: 0.75rem;
-      font-weight: bold;
-    }
-  }
-}
-</style>

+ 63 - 0
src/views/teacher/Review/index.vue

@@ -0,0 +1,63 @@
+<template>
+  <div>
+    <page-header :routes="[{ name: '互评作业', path: 'review' }]">
+      <template slot="title">
+        互评作业
+      </template>
+      <template slot="content">
+        共有 {{ review.length }} 份互评作业
+      </template>
+    </page-header>
+    <page-body>
+      <div class="columns is-multiline is-desktop">
+        <div :key="item.id" v-for="item in review" class="column is-4-desktop">
+          <router-link :to="`${$route.path}/${item.reviewId}`">
+            <card hoverable>
+              {{ item.name }}
+              <div><review-state-tag :state="item.state" /></div>
+            </card>
+          </router-link>
+        </div>
+      </div>
+    </page-body>
+  </div>
+</template>
+
+<script>
+import { mapState } from "vuex";
+import { getTeacherCourseReviewList } from "@/api/review";
+import Card from "@/components/Card";
+import PageHeader from "@/components/PageHeader";
+import PageBody from "@/components/PageBody/index";
+import ReviewStateTag from "@/components/ReviewStateTag/index";
+export default {
+  components: {
+    ReviewStateTag,
+    PageBody,
+    PageHeader,
+    Card
+  },
+  computed: {
+    ...mapState({
+      course: state => state.course.currentCourse
+    })
+  },
+  data() {
+    return {
+      courses: [],
+      review: []
+    };
+  },
+  mounted() {
+    getTeacherCourseReviewList(this.courses.id).then(
+      value => (this.review = value.data)
+    );
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.top-nav-section {
+  padding: 0 40px;
+}
+</style>

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff