Procházet zdrojové kódy

Merge branch 'feature/review-bay' of SEEC-II-FrontEnd-Team/SEEC-II-FrontEnd into develop

WuXinyu před 7 roky
rodič
revize
e473f6d0ec

+ 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;

+ 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
   };
 };

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

@@ -0,0 +1,40 @@
+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 小组做出的备注
+ */
+
+/**
+ * @description 生成checkListResultItem
+ * @returns {CheckListResultItemType}
+ */
+module.exports = ({ id = makeId() }) => {
+  /**
+   * @return {ResultByGroupType}
+   */
+  const groupResult = () => ({
+    groupId: makeId(),
+    level: true,
+    comment: "小组做出的备注"
+  });
+
+  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,

+ 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}/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;
+}
+

+ 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>

+ 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>

+ 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();

+ 9 - 0
src/mixins/time.js

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

+ 4 - 0
src/router/routes.js

@@ -137,6 +137,10 @@ export default [
         path: "review/:reviewId",
         component: () => import("@/views/student/Review/ReviewDetail")
       },
+      {
+        path: "review/:reviewId/doc/:docReviewId",
+        component: () => import("@/views/student/Review/ReviewDocDetail")
+      },
       {
         path: "code",
         component: () => import("@/views/student/Code")

+ 33 - 8
src/views/student/Review/ReviewDetail/index.vue

@@ -9,15 +9,37 @@
       <template slot="title">
         {{ detail.name }}
       </template>
+      <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="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>
-      <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>
+      <div class="columns is-multiline is-desktop">
+        <div
+          :key="item.docReviewId"
+          v-for="item in detail.documents"
+          class="column is-4"
+        >
+          <router-link :to="`${$route.path}/doc/${item.docReviewId}`">
+            <card hoverable> {{ item.group.name }} </card>
+          </router-link>
         </div>
-        <div class="review-body__right"></div>
       </div>
     </page-body>
   </div>
@@ -25,15 +47,18 @@
 <script>
 import PageHeader from "@/components/PageHeader";
 import PageBody from "@/components/PageBody/index";
-import VueMarkdown from "vue-markdown";
+import Card from "@/components/Card";
 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, Card },
   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>

+ 7 - 2
src/views/student/Review/index.vue

@@ -11,8 +11,11 @@
     <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>
+          <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