zhangjie 3 сар өмнө
parent
commit
0638e0a067

+ 35 - 42
src/api/markPage.ts

@@ -1,4 +1,4 @@
-import { store } from "@/store/app";
+import { useMarkStore } from "@/markStore";
 import { httpApp } from "@/plugins/axiosApp";
 import {
   Setting,
@@ -11,6 +11,8 @@ import {
 } from "@/types";
 import vls from "@/utils/storage";
 
+const markStore = useMarkStore();
+
 const getMarkInfo = () => {
   return vls.get("mark", {});
 };
@@ -103,18 +105,8 @@ export async function getHistoryTask({
 }
 
 /** 保存评卷任务(正常保存) */
-export async function saveTask() {
-  if (!store.currentTask?.markResult) return;
-
-  let markResult = store.currentTask.markResult;
-  markResult.problem = false;
-  markResult.problemType = undefined;
-  markResult.problemRemark = undefined;
-  markResult.unselective = false;
-  markResult.spent = Date.now() - store.currentTask.__markStartTime;
-  markResult = { ...markResult };
-
-  return httpApp.post<CommonResponse>("/api/mark/saveTask", markResult, {
+export async function saveTask(datas: MarkResult) {
+  return httpApp.post<CommonResponse>("/api/mark/saveTask", datas, {
     setGlobalMask: true,
     params: getMarkInfo(),
   });
@@ -152,44 +144,45 @@ export async function doSwitchGroup(markerId: number) {
   );
 }
 
+// TODO: 需要优化: 问题卷按试题设置
 /** 评卷用户选择试卷的问题类型 */
-export async function doProblemType({ problemType, problemRemark }) {
-  if (!store.currentTask?.markResult) return;
-
-  let markResult = store.currentTask?.markResult;
-  markResult.problem = true;
-  markResult.unselective = false;
-  markResult.problemType = problemType;
-  markResult.problemRemark = problemRemark || undefined;
-  markResult.markerScore = null;
-  markResult.scoreList = [];
-  markResult.specialTagList = [];
-  markResult.trackList = [];
-
-  markResult.spent = Date.now() - store.currentTask.__markStartTime;
-  markResult = { ...markResult };
-
-  return httpApp.post<CommonResponse>("/api/mark/saveTask", markResult, {
+export async function doProblemType({
+  problemType,
+  problemRemark,
+}: {
+  problemType: string;
+  problemRemark?: string;
+}) {
+  if (!markStore.currentTask?.markResult) return;
+
+  const datas = { ...markStore.currentTask?.markResult };
+  datas.problem = true;
+  datas.unselective = false;
+  datas.problemType = problemType;
+  datas.problemRemark = problemRemark || undefined;
+  datas.markerScore = null;
+  datas.scoreList = [];
+  datas.specialTagList = [];
+  datas.trackList = [];
+  datas.spent = Date.now() - markStore.currentTask.__markStartTime;
+
+  return httpApp.post<CommonResponse>("/api/mark/saveTask", datas, {
     params: getMarkInfo(),
   });
 }
 
 /** 评卷用户选择试卷的为未选做 */
 export async function doUnselectiveType() {
-  if (!store.currentTask?.markResult) return;
-
-  let markResult = store.currentTask?.markResult;
-  markResult.problem = false;
-  markResult.unselective = true;
-  markResult.markerScore = -1;
-  markResult.scoreList = [];
-  markResult.specialTagList = [];
-  markResult.trackList = [];
+  if (!markStore.currentTask?.markResult) return;
 
-  markResult.spent = Date.now() - store.currentTask.__markStartTime;
-  markResult = { ...markResult };
+  const datas = { ...markStore.currentTask?.markResult };
+  datas.markerScore = -1;
+  datas.scoreList = [];
+  datas.specialTagList = [];
+  datas.trackList = [];
+  datas.spent = Date.now() - markStore.currentTask.__markStartTime;
 
-  return httpApp.post<CommonResponse>("/api/mark/saveTask", markResult, {
+  return httpApp.post<CommonResponse>("/api/mark/saveTask", datas, {
     params: getMarkInfo(),
   });
 }

+ 2 - 0
src/features/mark/composables/useMakeTrack.ts

@@ -103,6 +103,8 @@ export default function useMakeTrack() {
 
     const target = event.target as HTMLImageElement;
     const track: SpecialTag = {
+      mainNumber: markStore.currentQuestion?.mainNumber,
+      subNumber: markStore.currentQuestion?.subNumber,
       tagName: markStore.currentSpecialTag,
       tagType: markStore.currentSpecialTagType,
       offsetIndex: item.indexInSliceUrls,

+ 55 - 29
src/features/mark/composables/useMarkSubmit.ts

@@ -4,33 +4,37 @@ import { message } from "ant-design-vue";
 import { isNumber } from "lodash-es";
 import { h } from "vue";
 import EventBus from "@/plugins/eventBus";
-import type { Question } from "@/types";
+import type { Question, MarkResult } from "@/types";
 import useStatus from "./useStatus";
 import { useMarkTask } from "./useMarkTask";
+import { deepCopy } from "@/utils/utils";
 
 export function useMarkSubmit() {
   const markStore = useMarkStore();
   const { updateStatus } = useStatus();
   const { nextTask } = useMarkTask();
 
-  const validateScore = (markResult: any) => {
+  // 检查分数
+  const validateScore = (markResult: MarkResult) => {
     const errors: Array<{ question: Question; index: number; error: string }> =
       [];
     markResult.scoreList.forEach((score: number, index: number) => {
       if (!markStore.currentTask) return;
       const question = markStore.currentTask.questionList[index]!;
+      const { maxScore, minScore, mainNumber, subNumber, questionName } =
+        question;
       let error;
       if (!isNumber(score)) {
-        error = `${question.mainNumber}-${question.subNumber}${
-          question.questionName ? "(" + question.questionName + ")" : ""
+        error = `${mainNumber}-${subNumber}${
+          questionName ? "(" + questionName + ")" : ""
         } 没有给分,不能提交。`;
-      } else if (isNumber(question.maxScore) && score > question.maxScore) {
-        error = `${question.mainNumber}-${question.subNumber}${
-          question.questionName ? "(" + question.questionName + ")" : ""
+      } else if (isNumber(maxScore) && score > maxScore) {
+        error = `${mainNumber}-${subNumber}${
+          questionName ? "(" + questionName + ")" : ""
         } 给分大于最高分不能提交。`;
-      } else if (isNumber(question.minScore) && score < question.minScore) {
-        error = `${question.mainNumber}-${question.subNumber}${
-          question.questionName ? "(" + question.questionName + ")" : ""
+      } else if (isNumber(minScore) && score < minScore) {
+        error = `${mainNumber}-${subNumber}${
+          questionName ? "(" + questionName + ")" : ""
         } 给分小于最低分不能提交。`;
       }
       if (error) {
@@ -40,21 +44,15 @@ export function useMarkSubmit() {
     return errors;
   };
 
-  const saveTaskToServer = async () => {
-    if (!markStore.currentTask) return;
-    const markResult = markStore.currentTask.markResult;
-    if (!markResult) return;
-
-    const mkey = "save_task_key";
+  // 检查评卷任务
+  function checkMarkResult(markResult: MarkResult): boolean {
     const errors = validateScore(markResult);
-
     if (errors.length !== 0) {
       console.log(errors);
       const msg = errors.map((v) => h("div", `${v.error}`));
       void message.warning({
         content: h("span", ["校验失败", ...msg]),
         duration: 10,
-        key: mkey,
       });
       return;
     }
@@ -64,13 +62,11 @@ export function useMarkSubmit() {
         markStore.currentTask.questionList.length ||
       !markResult.scoreList.every((s) => isNumber(s))
     ) {
-      console.error({ content: "markResult格式不正确,缺少分数", key: mkey });
+      console.error({ content: "markResult格式不正确,缺少分数" });
       return;
     }
 
-    if (!markStore.isTrackMode) {
-      markResult.trackList = [];
-    } else {
+    if (markStore.isTrackMode) {
       const trackScores =
         markResult.trackList
           .map((t) => Math.round((t.score || 0) * 100))
@@ -79,7 +75,6 @@ export function useMarkSubmit() {
         void message.error({
           content: "轨迹分与总分不一致,请检查。",
           duration: 3,
-          key: mkey,
         });
         return;
       }
@@ -93,21 +88,51 @@ export function useMarkSubmit() {
         void message.error({
           content: "强制标记已开启,请至少使用一个标记。",
           duration: 5,
-          key: mkey,
         });
         return;
       }
     }
 
+    return true;
+  }
+
+  // 获取保存评卷任务的数据
+  function getSaveTaskResult() {
+    const datas = deepCopy(markStore.currentTask.markResult);
+    datas.spent = Date.now() - markStore.currentTask.__markStartTime;
+    datas.questionList = datas.questionList.map((q) => {
+      q.trackList = datas.trackList.filter(
+        (t) => t.mainNumber === q.mainNumber && t.subNumber === q.subNumber
+      );
+      q.specialTagList = datas.specialTagList.filter(
+        (t) => t.mainNumber === q.mainNumber && t.subNumber === q.subNumber
+      );
+      return q;
+    }) as Question[];
+    return datas;
+  }
+
+  // 保存评卷任务
+  const saveTaskToServer = async () => {
+    if (!markStore.currentTask) return;
+    const markResult = markStore.currentTask.markResult;
+    if (!markResult) return;
+
+    if (!checkMarkResult(markResult)) return;
+
+    if (!markStore.isTrackMode) {
+      markResult.trackList = [];
+    }
+
     console.log("save task to server");
-    void message.loading({ content: "保存评卷任务...", key: mkey });
-    const res = await saveTask();
+    void message.loading({ content: "保存评卷任务..." });
+    const res = await saveTask(getSaveTaskResult());
     if (!res) return;
 
     // 故意不在此处同步等待,因为不必等待
     updateStatus().catch((e) => console.log("保存任务后获取status出错", e));
     if (res.data.success && markStore.currentTask) {
-      void message.success({ content: "保存成功", key: mkey, duration: 2 });
+      void message.success({ content: "保存成功", duration: 2 });
       if (!markStore.historyOpen) {
         markStore.currentTask = undefined;
         markStore.tasks.shift();
@@ -118,18 +143,18 @@ export function useMarkSubmit() {
     } else if (!res.data.success) {
       void message.error({
         content: "提交失败,请刷新页面",
-        key: mkey,
         duration: 10,
       });
       return;
     } else if (!markStore.currentTask) {
-      void message.warn({ content: "暂无新任务", key: mkey, duration: 10 });
+      void message.warn({ content: "暂无新任务", duration: 10 });
     }
 
     // 获取下一个任务
     void nextTask();
   };
 
+  // 全部零分提交
   const allZeroSubmit = async () => {
     const markResult = markStore.currentTask?.markResult;
     if (!markResult) return;
@@ -152,6 +177,7 @@ export function useMarkSubmit() {
     }
   };
 
+  // 未选做提交
   const unselectiveSubmit = async () => {
     const markResult = markStore.currentTask?.markResult;
     if (!markResult) return;

+ 9 - 11
src/features/mark/composables/useTaskWatch.ts

@@ -1,4 +1,5 @@
 import { watch } from "vue";
+import { useMarkStore } from "@/store";
 
 export default function useTaskWatch() {
   const markStore = useMarkStore();
@@ -26,13 +27,13 @@ export default function useTaskWatch() {
 
         task.__markStartTime = Date.now();
         const statusValue = markStore.setting.statusValue;
-        const { taskId, studentId } = task;
+        const { examId, studentId, paperNumber } = task;
         task.markResult = {
-          statusValue: statusValue,
-          taskId: taskId,
-          studentId: studentId,
+          examId,
+          paperNumber,
+          studentId,
           spent: 0,
-
+          statusValue,
           trackList: task.questionList
             .map((q) =>
               q.headerTrack && q.headerTrack.length
@@ -40,14 +41,11 @@ export default function useTaskWatch() {
                 : q.trackList
             )
             .flat(),
-          specialTagList: [...(task.specialTagList ?? [])],
+          specialTagList: task.questionList
+            .map((q) => q.specialTagList || [])
+            .flat(),
           scoreList: task.questionList.map((q) => q.score),
           markerScore: null, // 后期通过 scoreList 自动更新
-
-          problem: false,
-          problemType: "",
-          problemRemark: "",
-          unselective: false,
         };
         task.markResult.trackList.forEach((t) => {
           if (t.unanswered) {

+ 6 - 8
src/features/mark/stores/mark.ts

@@ -4,15 +4,13 @@ import { Setting, MarkStore, AdminPageSetting, Task } from "@/types";
 const useMarkStore = defineStore("mark", {
   state: (): MarkStore => ({
     setting: {
-      mode: "TRACK",
       examType: "SCAN_IMAGE",
+      mode: "TRACK",
       questionModal: "MULTI",
       forceMode: false,
       sheetView: false,
-      autoScroll: false,
       sheetConfig: [],
       enableAllZero: false,
-      enableSplit: true,
       fileServer: "",
       userName: "",
       subject: <Setting["subject"]>{},
@@ -21,28 +19,27 @@ const useMarkStore = defineStore("mark", {
         "answer.paper.scale": 1,
         "score.board.collapse": false,
         "normal.mode": "keyboard",
+        "score.fontSize.scale": 1,
         "paper.modal": false,
         "answer.modal": false,
         "minimap.modal": false,
         "specialTag.modal": false,
         "shortCut.modal": false,
-        "score.fontSize.scale": 1,
       },
       statusValue: null,
       problemTypes: [],
-      groupNumber: -987654, // 默认不可能的值
-      groupTitle: "",
       topCount: 0,
       splitConfig: [],
       prefetchCount: 3,
       startTime: 0,
       endTime: 0,
       selective: false,
+      autoScroll: false,
+      enableSplit: true,
+      showObjectiveScore: false,
     },
     status: <MarkStore["status"]>{},
-    groups: [],
     tasks: [],
-    message: null,
     currentTask: undefined,
     // 主观题检查时,缓存已经修改过的试题
     currentTaskModifyQuestion: {},
@@ -61,6 +58,7 @@ const useMarkStore = defineStore("mark", {
     globalMask: false,
     // 任务渲染锁状态
     renderLock: false,
+    message: null,
   }),
 
   getters: {

+ 68 - 52
src/types/index.ts

@@ -16,7 +16,6 @@ type SingleSheetConfig = SplitConfig;
 export type PictureSlice = SplitConfig;
 export interface MarkStore {
   setting: Setting;
-  groups: Array<Group>;
   status: {
     /** 个人评卷数量 */
     personCount: number;
@@ -46,7 +45,6 @@ export interface MarkStore {
   removeScoreTracks: Array<Track>;
   /** 聚焦这些tracks */
   focusTracks: Array<Track>;
-  message: string | null;
   /** 缩略图设置滚动到宽度的百分比 */
   minimapScrollToX?: number;
   /** 缩略图设置滚动到高度的百分比 */
@@ -59,6 +57,8 @@ export interface MarkStore {
   globalMask: boolean;
   /** 任务渲染锁状态 */
   renderLock: boolean;
+  /** 消息 */
+  message: string | null;
 }
 
 export interface Setting {
@@ -72,14 +72,10 @@ export interface Setting {
   forceMode: boolean;
   /** 是否显示原图功能 */
   sheetView: boolean;
-  /** 是否自动跳转 */
-  autoScroll: boolean;
   /** 原图遮盖规则 */
   sheetConfig: Array<SingleSheetConfig>;
   /** 是否开启全零分 */
   enableAllZero: boolean;
-  /** 是否允许裁切 */
-  enableSplit: boolean;
   /** 图片服务地址 */
   fileServer: string;
   /** 评卷员姓名 */
@@ -94,10 +90,6 @@ export interface Setting {
   statusValue: "TRIAL" | "FORMAL" | null;
   /** 问题卷类型 */
   problemTypes: Array<{ code: string; name: string }>;
-  /** 当前评卷分组号 */
-  groupNumber: number;
-  /** 当前评卷分组名称 */
-  groupTitle: string;
   /** 推荐老师评卷的数量,到达这个数量提示老师 */
   topCount: number;
   /** 使用裁切整图时的裁切配置 [0,1]|[0,0.3,0.25,0.55], */
@@ -110,16 +102,25 @@ export interface Setting {
   endTime: number;
   /** 是否是未选做类型 */
   selective: boolean;
+  /** 是否自动跳转 */
+  autoScroll: boolean;
+  /** 是否允许裁切 */
+  enableSplit: boolean;
+  /** 评卷员页头是否展示客观分 */
+  showObjectiveScore?: boolean;
   /** 可回评数量是否有限制数 */
   remarkCount?: any;
   /** 是否展示双评的轨迹 */
   doubleTrack?: boolean;
   /** 全卷复核页面是否要滑到页面底部才允许点击复核按钮 */
   inspectScroll?: boolean;
-  /** 评卷员页头是否展示客观分 */
-  showObjectiveScore?: boolean;
 }
 
+interface UrlData {
+  url: string;
+  paperType: string;
+  filePathVo: string | null;
+}
 /** 科目信息(试卷和答案功能) */
 interface RawSubject {
   /** 科目名称 */
@@ -127,17 +128,9 @@ interface RawSubject {
   /** 科目编号 */
   code: string;
   /** 答案 url */
-  answerUrl: {
-    url: string;
-    paperType: string;
-    filePathVo: string;
-  };
+  answerUrl: UrlData[] | null;
   /** 试卷 url */
-  paperUrl: {
-    url: string;
-    paperType: string;
-    filePathVo: string | null;
-  };
+  paperUrl: UrlData[] | null;
 }
 
 /** 科目信息(试卷和答案功能)增加前端自定义 questions字段*/
@@ -194,22 +187,24 @@ interface RawTask {
   studentName: string;
   /** 学生编号 */
   studentCode: string;
+  /** 课程名称 */
+  courseName: string;
+  /** 课程编号 */
+  courseCode: string;
   /** 考试编号 */
-  examNumber: string;
+  // examNumber: string;
   /** 试卷编号 */
   paperNumber: string;
   /** 试卷类型 */
   paperType: string;
-  /** 裁切图url */
-  sliceUrls: Array<string>;
-  /** 最高显示优先级,有sliceConfig就用sliceConfig,否则使用sheetConfig */
-  sliceConfig: Array<PictureSlice>;
   /** sliceUrls为空,则是多媒体阅卷,显示JSON */
   jsonUrl: string;
+  /** 最高显示优先级,有sliceConfig就用sliceConfig,否则使用sheetConfig */
+  sliceConfig: Array<PictureSlice>;
+  /** 裁切图url */
+  sliceUrls: Array<string>;
   // 试题列表
   questionList: Array<Question>;
-  // 特殊标记列表
-  specialTagList: Array<SpecialTag>;
   // 科组长特殊标记列表
   headerTagList?: any;
   /** 原图url */
@@ -329,7 +324,8 @@ interface RawQuestion {
     userId: string;
     userName: string;
   }>;
-  specialTagList: null;
+  // 特殊标记列表
+  specialTagList: Array<SpecialTag> | null;
   // 打回原因
   rejectReason: string | null;
   // 打回前给分数据
@@ -353,6 +349,8 @@ export interface Track {
   subNumber: string;
   /** 前端使用,暂时用不着,赋0 */
   number: number;
+  /** 评分数 */
+  score: number;
   /** 第几张图 */
   offsetIndex: number;
   /** 左上角为原点 */
@@ -361,8 +359,6 @@ export interface Track {
   /** 相对slice的位置比例 */
   positionX: number;
   positionY: number;
-  /** 评分数 */
-  score: number;
   /** 是否此处未作答,未作答时,score默认是-0分 */
   unanswered: boolean;
   userId?: string;
@@ -375,6 +371,10 @@ export interface Track {
 
 /** 特殊标记数据 */
 export interface SpecialTag {
+  /** 大题号 */
+  mainNumber: number;
+  /** 小题号,当前api中只有number // 特殊标记中没有 */
+  subNumber: string;
   /** 第几张图 */
   offsetIndex: number;
   /** 左上角为原点(原图的原点),及相对原图的位置比例 */
@@ -387,24 +387,20 @@ export interface SpecialTag {
   tagName: string;
   tagType: "TEXT" | "CIRCLE" | "RIGHT" | "WRONG" | "HALF_RIGTH" | "LINE";
   // 分组号
-  groupNumber?: number;
   userId?: number;
   color?: string;
   isByMultMark?: boolean;
 }
 
 export interface UISetting {
-  /** 给分面板展示形态 */
-  "score.board.collapse": boolean;
-  /** 0.2 gap */
   /** 试卷缩放比例 */
   "answer.paper.scale": number;
-  /**
-   * 给分模式
-   * "keyboard": 键盘模式
-   * "mouse": 鼠标模式
-   *  */
+  /** 给分面板展示形态 */
+  "score.board.collapse": boolean;
+  /** 给分模式 "keyboard": 键盘模式 "mouse": 鼠标模式 */
   "normal.mode": "keyboard" | "mouse";
+  /** 分数标记缩放比例 */
+  "score.fontSize.scale": number;
   /** 弹窗显示试卷 */
   "paper.modal": boolean;
   /** 弹窗显示答案 */
@@ -415,18 +411,17 @@ export interface UISetting {
   "specialTag.modal": boolean;
   /** 弹窗显示快捷键配置 */
   "shortCut.modal": boolean;
-  /** 0.1 gap */
-  /** 分数标记缩放比例 */
-  "score.fontSize.scale": number;
 }
 
-export interface MarkResult {
-  taskId: number;
-  studentId: number;
-  statusValue: "TRIAL" | "FORMAL" | null;
-  /** 毫秒单位 */
-  spent: number;
-
+export interface MarkResultQuestion {
+  /** 任务序号 */
+  taskId: string;
+  /** 试题号 */
+  questionId: string;
+  /** 大题号 */
+  mainNumber: number;
+  /** 小题号 */
+  subNumber: number;
   // 轨迹 or 键盘
   markerScore: number | null;
   /** 轨迹列表 */
@@ -435,7 +430,6 @@ export interface MarkResult {
   scoreList: Array<number | null>;
   /** 轨迹和键盘都需要 */
   specialTagList: Array<SpecialTag>;
-
   /** 问题卷 */
   problem: boolean;
   /** 问题卷类型 */
@@ -445,6 +439,28 @@ export interface MarkResult {
   /** 当前task是否为学生未选做 */
   unselective: boolean;
 }
+export interface MarkResult {
+  /** 考试id */
+  examId: string;
+  /** 试卷编号 */
+  paperNumber: string;
+  /** 学生id */
+  studentId: number;
+  /** 状态值 */
+  statusValue: "TRIAL" | "FORMAL" | null;
+  /** 毫秒单位 */
+  spent: number;
+  /** 试题列表 */
+  questionList: Array<MarkResultQuestion>;
+  /** 轨迹列表 */
+  trackList: Array<Track>;
+  /** 特殊标记列表 */
+  specialTagList: Array<SpecialTag>;
+  /** 给分列表 */
+  scoreList: Array<number | null>;
+  /** 评卷员分数 */
+  markerScore: number | null;
+}
 
 /** 前端自用,用来渲染裁切图 */
 export interface SliceImage {

+ 9 - 0
src/utils/utils.ts

@@ -4,6 +4,15 @@
 // 把store.currentTask当做 weakRef ,当它不存在时,就丢弃它所有的图片
 // const weakedMapImages = new WeakMap<Object, Map<string, HTMLImageElement>>();
 
+/**
+ * 深拷贝
+ * @param obj 需要拷贝的对象
+ * @returns 拷贝后的对象
+ */
+export function deepCopy(obj: any) {
+  return JSON.parse(JSON.stringify(obj));
+}
+
 /**
  * 异步获取图片
  * @param url 完整的图片路径