zhangjie 4 месяцев назад
Родитель
Сommit
24d27f9c70
28 измененных файлов с 241 добавлено и 3960 удалено
  1. 109 183
      src/features/check-subjective/CheckSubjective.vue
  2. 2 1
      src/features/check-subjective/MarkBody.vue
  3. 51 0
      src/features/check-subjective/composables/useSetting.ts
  4. 71 0
      src/features/check-subjective/composables/useTask.ts
  5. 0 224
      src/features/library/inspect/LibraryInspect.vue
  6. 0 258
      src/features/library/inspect/MarkBoardInspect.vue
  7. 0 11
      src/features/library/inspect/MarkBody.vue
  8. 0 32
      src/features/library/inspect/MarkHeader.vue
  9. 0 115
      src/features/library/inspect/ReviewReturnDialog.vue
  10. 0 99
      src/features/library/libraryTrack/LibraryTrack.vue
  11. 0 10
      src/features/library/libraryTrack/MarkBody.vue
  12. 0 7
      src/features/library/libraryTrack/MarkHeader.vue
  13. 0 11
      src/features/library/quality/MarkBody.vue
  14. 0 7
      src/features/library/quality/MarkHeader.vue
  15. 0 74
      src/features/library/quality/Quality.vue
  16. 7 5
      src/features/mark/Mark.vue
  17. 0 141
      src/features/student/importInspect/ImportInspect.vue
  18. 0 225
      src/features/student/importInspect/MarkBoardInspect.vue
  19. 0 45
      src/features/student/importInspect/MarkHeader.vue
  20. 0 227
      src/features/student/scoreVerify/MarkBoardInspect.vue
  21. 0 57
      src/features/student/scoreVerify/MarkHeader.vue
  22. 0 173
      src/features/student/scoreVerify/ScoreVerify.vue
  23. 0 309
      src/features/student/scoreVerify/markBody.vue
  24. 0 351
      src/features/student/studentInspect/MarkBoardInspect.vue
  25. 0 1051
      src/features/student/studentInspect/MarkBody.vue
  26. 0 30
      src/features/student/studentInspect/MarkHeader.vue
  27. 0 273
      src/features/student/studentInspect/StudentInspect.vue
  28. 1 41
      src/router/index.ts

+ 109 - 183
src/features/check-subjective/CheckSubjective.vue

@@ -2,26 +2,26 @@
   <div class="mark">
     <div class="mark-header">
       <div class="mark-header-part">
-        <template v-if="store.currentTask">
+        <template v-if="markStore.currentTask">
           <div class="header-noun">
             <span>课程名称:</span>
             <span>
-              {{ store.currentTask.courseName }}({{
-                store.currentTask.courseCode
+              {{ markStore.currentTask.courseName }}({{
+                markStore.currentTask.courseCode
               }})</span
             >
           </div>
           <div class="header-noun">
             <span>试卷编号:</span>
-            <span>{{ store.currentTask.paperNumber }}</span>
+            <span>{{ markStore.currentTask.paperNumber }}</span>
           </div>
           <div class="header-noun">
             <span>姓名:</span>
-            <span>{{ store.currentTask.studentName }}</span>
+            <span>{{ markStore.currentTask.studentName }}</span>
           </div>
           <div class="header-noun">
             <span>学号:</span>
-            <span>{{ store.currentTask?.studentCode }}</span>
+            <span>{{ markStore.currentTask?.studentCode }}</span>
           </div>
           <div v-if="studentIds.length > 1" class="header-noun">
             <span>进度:</span>
@@ -47,14 +47,17 @@
         <div class="header-text-btn header-logout" @click="logout">
           <img class="header-icon" src="@/assets/icons/icon-return.svg" />返回
         </div>
-        <a-tooltip v-if="store.isTrackMode" placement="bottomRight">
+        <a-tooltip v-if="markStore.isTrackMode" placement="bottomRight">
           <template #title>弹出给分板</template>
           <div
             :class="[
               'header-menu',
-              { 'is-toggled': store.isScoreBoardVisible && store.currentTask },
+              {
+                'is-toggled':
+                  markStore.isScoreBoardVisible && markStore.currentTask,
+              },
             ]"
-            @click="store.toggleScoreBoard"
+            @click="markStore.toggleScoreBoard"
           >
             <img src="@/assets/icons/icon-right-menu.svg" class="header-icon" />
           </div>
@@ -78,20 +81,20 @@
     <div class="mark-main">
       <mark-body @error="removeBrokenTask" />
       <mark-board-track
-        v-if="store.isTrackMode"
+        v-if="markStore.isTrackMode"
         isCheckAnswer
         @submit="saveTaskToServer"
         @checkSubmit="checkTask"
       />
       <mark-board-key-board
-        v-if="store.shouldShowMarkBoardKeyBoard"
+        v-if="markStore.shouldShowMarkBoardKeyBoard"
         isCheckAnswer
         @submit="saveTaskToServer"
         @allZeroSubmit="allZeroSubmit"
         @checkSubmit="checkTask"
       />
       <mark-board-mouse
-        v-if="store.shouldShowMarkBoardMouse"
+        v-if="markStore.shouldShowMarkBoardMouse"
         isCheckAnswer
         @submit="saveTaskToServer"
         @allZeroSubmit="allZeroSubmit"
@@ -99,14 +102,16 @@
       />
     </div>
   </div>
-  <AnswerModal />
-  <MinimapModal />
-  <AllPaperModal />
-  <SheetViewModal />
-  <SpecialTagModal />
-  <ShortCutModal />
-  <MarkBoardTrackDialog
-    v-if="store.isTrackMode"
+  <!-- modal -->
+  <modal-answer />
+  <modal-paper />
+  <modal-minimap />
+  <modal-all-paper />
+  <modal-sheet-view />
+  <modal-short-cut />
+  <!-- other -->
+  <mark-board-track-dialog
+    v-if="markStore.isTrackMode"
     isCheckAnswer
     @submit="saveTaskToServer"
     @allZeroSubmit="allZeroSubmit"
@@ -114,32 +119,43 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, watch, h } from "vue";
+import { onMounted, h } from "vue";
+import { message } from "ant-design-vue";
 import {
-  studentSubjectiveConfirmData,
   saveStudentSubjectiveConfirmData,
   saveStudentSubjectiveCheck,
 } from "@/api/checkPage";
-import { doLogout, updateUISetting, getSetting } from "@/api/markPage";
-import { store } from "@/store/app";
-import MarkTool from "../mark/MarkTool.vue";
-import MarkBody from "./MarkBody.vue";
-import MarkBoardTrack from "../mark/MarkBoardTrack.vue";
+import { doLogout } from "@/api/markPage";
+import { useMarkStore } from "@/store";
 import type { Question } from "@/types";
-import MarkBoardKeyBoard from "../mark/MarkBoardKeyBoard.vue";
-import MarkBoardMouse from "../mark/MarkBoardMouse.vue";
-import { debounce, isEmpty, isNumber } from "lodash-es";
-import { message } from "ant-design-vue";
-import AnswerModal from "../mark/AnswerModal.vue";
-import MinimapModal from "../mark/MinimapModal.vue";
-import AllPaperModal from "../mark/AllPaperModal.vue";
-import SheetViewModal from "../mark/modals/SheetViewModal.vue";
-import SpecialTagModal from "../mark/modals/SpecialTagModal.vue";
-import ShortCutModal from "../mark/modals/ShortCutModal.vue";
-import MarkBoardTrackDialog from "../mark/MarkBoardTrackDialog.vue";
+import { isNumber } from "lodash-es";
 import vls from "@/utils/storage";
 
-const { examId, paperNumber, studentIds } = vls.get("check-students", {
+// components
+import MarkTool from "../mark/MarkTool.vue";
+import MarkBody from "./MarkBody.vue";
+// scoring
+import MarkBoardTrack from "../mark/scoring/MarkBoardTrack.vue";
+import MarkBoardKeyBoard from "../mark/scoring/MarkBoardKeyBoard.vue";
+import MarkBoardMouse from "../mark/scoring/MarkBoardMouse.vue";
+import MarkBoardTrackDialog from "../mark/scoring/MarkBoardTrackDialog.vue";
+// modals
+import ModalAnswer from "../mark/modals/ModalAnswer.vue";
+import ModalPaper from "../mark/modals/ModalPaper.vue";
+import ModalMinimap from "../mark/modals/ModalMinimap.vue";
+import ModalAllPaper from "../mark/modals/ModalAllPaper.vue";
+import ModalSheetView from "../mark/modals/ModalSheetView.vue";
+import ModalShortCut from "../mark/modals/ModalShortCut.vue";
+
+// composables
+import useTask from "./composables/useTask";
+import useSetting from "./composables/useSetting";
+
+const {
+  examId,
+  paperNumber,
+  studentIds: sids,
+} = vls.get("check-students", {
   examId: "",
   paperNumber: "",
   studentIds: [],
@@ -150,11 +166,16 @@ vls.set("mark", {
   groupNumber: 1,
 });
 
-let currentStudentId = $ref("");
-const currentIndex = $computed(() => studentIds.indexOf(currentStudentId));
-const isFirst = $computed(() => currentIndex === 0);
-const isLast = $computed(() => currentIndex === studentIds.length - 1);
-const isMultiStudent = $computed(() => studentIds.length > 1);
+const markStore = useMarkStore();
+const {
+  getNextStudent,
+  getPreviousStudent,
+  studentIds,
+  taskQuestionInfo,
+  isMultiStudent,
+} = useTask(sids);
+
+const { updateSetting } = useSetting();
 
 onMounted(async () => {
   if (studentIds.length === 0) {
@@ -169,99 +190,18 @@ const logout = () => {
   doLogout();
 };
 
-async function getNextStudent() {
-  if (isLast) {
-    void message.warning("已经是最后一份!");
-    return;
-  }
-  await updateTask(studentIds[currentIndex + 1]);
-}
-
-async function getPreviousStudent() {
-  if (isFirst) {
-    void message.warning("已经是第一份!");
-    return;
-  }
-  await updateTask(studentIds[currentIndex - 1]);
-}
-
-async function updateSetting() {
-  const settingRes = await getSetting();
-  // 初次使用时,重置并初始化uisetting
-  if (isEmpty(settingRes.data.uiSetting)) {
-    settingRes.data.uiSetting = {
-      "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,
-    };
-  } else {
-    settingRes.data.uiSetting = JSON.parse(settingRes.data.uiSetting);
-  }
-  settingRes.data.doubleTrack = true;
-  store.setting = settingRes.data;
-}
-
-let taskQuestionInfo = {};
-function updateTaskGroupInfo() {
-  taskQuestionInfo = {};
-  if (!store.currentTask) return;
-
-  store.currentTask.questionList.forEach((question) => {
-    const qno = `${question.mainNumber * 1000}${question.subNumber}`;
-    taskQuestionInfo[qno] = {
-      groupNumber: question.groupNumber,
-      score: question.score,
-    };
-  });
-}
-async function updateTask(studentId) {
-  const res = await studentSubjectiveConfirmData(studentId);
-  if (!res.data) {
-    store.message = res.message || "数据错误";
-    return;
-  }
-  res.data.taskId = res.data.studentId;
-  const newTask = res.data;
-  newTask.sheetUrls = newTask.sheetUrls || [];
-  // newTask.sheetUrls = ["/1-1.jpg", "/1-2.jpg"];
-  newTask.sliceUrls = [...newTask.sheetUrls];
-  store.currentTask = newTask;
-  currentStudentId = studentId;
-  updateTaskGroupInfo();
-}
-
-const __debounceUpdate = debounce(() => {
-  updateUISetting(store.setting.mode, store.setting.uiSetting).catch((e) =>
-    console.log("保存设置出错", e)
-  );
-}, 3000);
-watch(
-  () => [store.setting.uiSetting],
-  () => {
-    __debounceUpdate();
-  },
-  { deep: true }
-);
-
 const removeBrokenTask = () => {
   console.log("removeBrokenTask");
-
   // store.currentTask = undefined;
 };
 
 const allZeroSubmit = async () => {
-  const markResult = store.currentTask?.markResult;
+  const markResult = markStore.currentTask?.markResult;
   if (!markResult) return;
 
   const { markerScore, scoreList, trackList, specialTagList } = markResult;
   markResult.markerScore = 0;
-  const ss = new Array(store.currentTaskEnsured.questionList.length);
+  const ss = new Array(markStore.currentTaskEnsured.questionList.length);
   markResult.scoreList = ss.fill(0);
   markResult.trackList = [];
 
@@ -279,18 +219,18 @@ const allZeroSubmit = async () => {
 };
 
 const getMarkData = () => {
-  if (!store.currentTask?.markResult) return {};
+  if (!markStore.currentTask?.markResult) return {};
 
-  let markResult = store.currentTask.markResult;
+  let markResult = markStore.currentTask.markResult;
 
   let commomData = {
     status: markResult.status,
-    spent: Date.now() - store.currentTask.__markStartTime,
+    spent: Date.now() - markStore.currentTask.__markStartTime,
   };
 
-  if (!store.isTrackMode) {
+  if (!markStore.isTrackMode) {
     const groupMap = {};
-    store.currentTask.questionList.forEach((question) => {
+    markStore.currentTask.questionList.forEach((question) => {
       const { __index, groupNumber } = question;
       if (!groupMap[groupNumber]) {
         groupMap[groupNumber] = {
@@ -300,15 +240,15 @@ const getMarkData = () => {
           specialTagList: [],
           markerScore: [],
           scoreList: [],
-          studentId: store.currentTask.studentId,
+          studentId: markStore.currentTask.studentId,
         };
       }
       groupMap[groupNumber].scoreList[__index] =
-        store.currentTask.markResult.scoreList[__index];
+        markStore.currentTask.markResult.scoreList[__index];
       groupMap[groupNumber].markerScore.push({
         mainNumber: question.mainNumber,
         subNumber: question.subNumber,
-        score: store.currentTask.markResult.scoreList[__index],
+        score: markStore.currentTask.markResult.scoreList[__index],
       });
     });
 
@@ -317,7 +257,7 @@ const getMarkData = () => {
       return group;
     });
     return {
-      studentId: store.currentTask.studentId,
+      studentId: markStore.currentTask.studentId,
       groups,
     };
   }
@@ -347,9 +287,9 @@ const getMarkData = () => {
     groupMap[groupNumber].specialTagList.push(track);
   });
 
-  const modifiedQuestions = Object.keys(store.currentTaskModifyQuestion).filter(
-    (k) => store.currentTaskModifyQuestion[k]
-  );
+  const modifiedQuestions = Object.keys(
+    markStore.currentTaskModifyQuestion
+  ).filter((k) => markStore.currentTaskModifyQuestion[k]);
 
   let groups = Object.values(groupMap).map((item) => {
     let qScore = {};
@@ -375,34 +315,23 @@ const getMarkData = () => {
       ...item,
       trackList,
       markerScore: Object.values(qScore),
-      studentId: store.currentTask.studentId,
+      studentId: markStore.currentTask.studentId,
     };
   });
   groups = groups.filter((group) => group);
 
   return {
-    studentId: store.currentTask.studentId,
+    studentId: markStore.currentTask.studentId,
     groups,
   };
 };
 
-const saveTaskToServer = async () => {
-  if (!store.currentTask) return;
-  const markResult = store.currentTask.markResult;
-  if (!markResult) return;
-
-  const mkey = "save_task_key";
-
-  type SubmitError = {
-    question: Question;
-    index: number;
-    error: string;
-  };
-
-  const errors: SubmitError[] = [];
-  markResult.scoreList.forEach((score, index) => {
-    if (!store.currentTask) return;
-    const question = store.currentTask.questionList[index]!;
+const validateScore = (markResult: any) => {
+  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]!;
     let error;
     if (!isNumber(score)) {
       error = `${question.mainNumber}-${question.subNumber}${
@@ -421,6 +350,23 @@ const saveTaskToServer = async () => {
       errors.push({ question, index, error });
     }
   });
+  return errors;
+};
+
+const saveTaskToServer = async () => {
+  if (!markStore.currentTask) return;
+  const markResult = markStore.currentTask.markResult;
+  if (!markResult) return;
+
+  const mkey = "save_task_key";
+
+  type SubmitError = {
+    question: Question;
+    index: number;
+    error: string;
+  };
+
+  const errors: SubmitError[] = validateScore(markResult);
   if (errors.length !== 0) {
     console.log(errors);
     const msg = errors.map((v) => h("div", `${v.error}`));
@@ -433,33 +379,13 @@ const saveTaskToServer = async () => {
   }
 
   if (
-    markResult.scoreList.length !== store.currentTask.questionList.length ||
+    markResult.scoreList.length !== markStore.currentTask.questionList.length ||
     !markResult.scoreList.every((s) => isNumber(s))
   ) {
     console.error({ content: "markResult格式不正确,缺少分数", key: mkey });
     return;
   }
 
-  // if (!store.isTrackMode) {
-  //   markResult.trackList = [];
-  // } else {
-  //   const trackScores =
-  //     markResult.trackList
-  //       .map((t) => Math.round((t.score || 0) * 100))
-  //       .reduce((acc, s) => acc + s, 0) / 100;
-  //   // console.log(markResult.trackList);
-  //   // console.log(trackScores, markResult.markerScore);
-
-  //   if (trackScores !== markResult.markerScore) {
-  //     void message.error({
-  //       content: "轨迹分与总分不一致,请检查。",
-  //       duration: 3,
-  //       key: mkey,
-  //     });
-  //     return;
-  //   }
-  // }
-
   const data = getMarkData();
   const res = await saveStudentSubjectiveConfirmData(data).catch(() => false);
   if (!res) return;
@@ -483,16 +409,16 @@ const saveTaskToServer = async () => {
     void message.success({ content: "保存成功", key: mkey, duration: 2 });
   }
 
-  store.currentTask = undefined;
+  markStore.currentTask = undefined;
   await getNextStudent();
 };
 
 // 直接点击复核,不做修改
 const checkTask = async () => {
-  if (!store.currentTask) return;
+  if (!markStore.currentTask) return;
 
   const res = await saveStudentSubjectiveCheck({
-    studentId: store.currentTask.studentId,
+    studentId: markStore.currentTask.studentId,
     examId,
     paperNumber,
   }).catch(() => false);
@@ -518,7 +444,7 @@ const checkTask = async () => {
   } else {
     void message.success({ content: "保存成功", key: mkey, duration: 2 });
   }
-  store.currentTask = undefined;
+  markStore.currentTask = undefined;
   await getNextStudent();
 };
 </script>

+ 2 - 1
src/features/check-subjective/MarkBody.vue

@@ -9,7 +9,8 @@
 
 <script setup lang="ts">
 import MarkBodyCursor from "../mark/MarkBodyCursor.vue";
-import MarkBodyBase from "../mark/MarkBodyBase.vue";
+import MarkBodyBase from "../MarkBodyBase.vue";
+
 import useMakeTrack from "../mark/composables/useMakeTrack";
 
 defineEmits(["error"]);

+ 51 - 0
src/features/check-subjective/composables/useSetting.ts

@@ -0,0 +1,51 @@
+import { useMarkStore } from "@/store";
+import { getSetting, updateUISetting } from "@/api/markPage";
+import { debounce, isEmpty } from "lodash-es";
+import { watch } from "vue";
+
+export default function useSetting() {
+  const markStore = useMarkStore();
+
+  async function updateSetting() {
+    const settingRes = await getSetting();
+    if (isEmpty(settingRes.data.uiSetting)) {
+      settingRes.data.uiSetting = {
+        "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,
+      };
+    } else {
+      settingRes.data.uiSetting = JSON.parse(settingRes.data.uiSetting);
+    }
+    settingRes.data.sheetConfig = settingRes.data.sheetConfig
+      ? JSON.parse(settingRes.data.sheetConfig)
+      : [];
+    settingRes.data.doubleTrack = true;
+    markStore.setting = settingRes.data;
+  }
+
+  const debouncedUpdateUISetting = debounce(() => {
+    updateUISetting(markStore.setting.mode, markStore.setting.uiSetting).catch(
+      (e) => console.log("保存设置出错", e)
+    );
+  }, 3000);
+
+  watch(
+    () => [markStore.setting.uiSetting, markStore.setting.mode],
+    () => {
+      debouncedUpdateUISetting();
+    },
+    { deep: true }
+  );
+
+  return {
+    updateSetting,
+    debouncedUpdateUISetting,
+  };
+}

+ 71 - 0
src/features/check-subjective/composables/useTask.ts

@@ -0,0 +1,71 @@
+import { message } from "ant-design-vue";
+import { studentSubjectiveConfirmData } from "@/api/checkPage";
+import { useMarkStore } from "@/store";
+
+export default function useTask(ids: string[]) {
+  const markStore = useMarkStore();
+  const studentIds = $ref(ids);
+
+  let currentStudentId = $ref("");
+  const currentIndex = $computed(() => studentIds.indexOf(currentStudentId));
+  const isFirst = $computed(() => currentIndex === 0);
+  const isLast = $computed(() => currentIndex === studentIds.length - 1);
+  const isMultiStudent = $computed(() => studentIds.length > 1);
+
+  async function getNextStudent() {
+    if (isLast) {
+      void message.warning("已经是最后一份!");
+      return;
+    }
+    await updateTask(studentIds[currentIndex + 1]);
+  }
+
+  async function getPreviousStudent() {
+    if (isFirst) {
+      void message.warning("已经是第一份!");
+      return;
+    }
+    await updateTask(studentIds[currentIndex - 1]);
+  }
+
+  async function updateTask(studentId) {
+    const res = await studentSubjectiveConfirmData(studentId);
+    if (!res.data) {
+      markStore.message = res.message || "数据错误";
+      return;
+    }
+    res.data.taskId = res.data.studentId;
+    const newTask = res.data;
+    newTask.sheetUrls = newTask.sheetUrls || [];
+    newTask.sliceUrls = [...newTask.sheetUrls];
+    markStore.currentTask = newTask;
+    currentStudentId = studentId;
+    updateTaskGroupInfo();
+  }
+
+  let taskQuestionInfo = $ref({});
+  function updateTaskGroupInfo() {
+    taskQuestionInfo = {};
+    if (!markStore.currentTask) return;
+
+    markStore.currentTask.questionList.forEach((question) => {
+      const qno = `${question.mainNumber * 1000}${question.subNumber}`;
+      taskQuestionInfo[qno] = {
+        groupNumber: question.groupNumber,
+        score: question.score,
+      };
+    });
+  }
+
+  return {
+    getNextStudent,
+    getPreviousStudent,
+    taskQuestionInfo,
+    studentIds,
+    currentStudentId,
+    currentIndex,
+    isFirst,
+    isLast,
+    isMultiStudent,
+  };
+}

+ 0 - 224
src/features/library/inspect/LibraryInspect.vue

@@ -1,224 +0,0 @@
-<template>
-  <div class="my-container">
-    <mark-header />
-    <div class="tw-flex tw-gap-1">
-      <mark-history
-        v-if="!isSingleStudent"
-        :subjectCode="subjectCode"
-        :groupNumber="groupNumber"
-        orderTimeField="inspect_time"
-        :getHistory="getLibraryInspectedHistoryOfLibraryInspect"
-      />
-      <mark-body @error="renderError" />
-      <MarkBoardInspect @inspect="saveTaskToServer" @reject="rejectQuestions" />
-    </div>
-  </div>
-  <MinimapModal />
-</template>
-
-<script setup lang="ts">
-import { onMounted, watch } from "vue";
-import { store } from "@/store/app";
-import MarkHeader from "./MarkHeader.vue";
-import MinimapModal from "@/features/mark/MinimapModal.vue";
-import { useRoute } from "vue-router";
-import MarkBody from "./MarkBody.vue";
-import MarkHistory from "@/features/mark/MarkHistory.vue";
-import MarkBoardInspect from "./MarkBoardInspect.vue";
-import type { Question } from "@/types";
-import { message } from "ant-design-vue";
-import {
-  clearInspectedTaskOfLibraryInspect,
-  getInspectedTaskStatusOfLibraryInspect,
-  getLibraryInspectedHistoryOfLibraryInspect,
-  getOneOfInspectedTaskOfLibraryInspect,
-  rejectInspectedTaskOfLibraryInspect,
-  saveInspectedTaskOfLibraryInspect,
-} from "@/api/libraryInspectPage";
-import { getPaper } from "@/api/jsonMark";
-import { getAdminPageSetting } from "@/api/inspectPage";
-import EventBus from "@/plugins/eventBus";
-import { addFileServerPrefixToTask } from "@/utils/utils";
-
-const route = useRoute();
-let isSingleStudent = !!route.query.taskId;
-const {
-  subjectCode,
-  groupNumber,
-  taskId,
-  markerId,
-  examNumber,
-  secretNumber,
-  startScore,
-  endScore,
-  questionScore,
-  unselective,
-} = route.query as {
-  subjectCode: string;
-  groupNumber: string;
-  taskId: string; // TODO: for未来单一任务
-  markerId: string;
-  examNumber: string;
-  secretNumber: string;
-  startScore: string;
-  endScore: string;
-  questionScore: string;
-  unselective: string;
-};
-
-async function updateClearTask() {
-  await clearInspectedTaskOfLibraryInspect(subjectCode, groupNumber);
-}
-
-async function updateSetting() {
-  const settingRes = await getAdminPageSetting(subjectCode);
-
-  const { examType, fileServer, subject, userName, splitConfig, enableSplit } =
-    settingRes.data;
-  store.initSetting({
-    examType,
-    fileServer,
-    subject,
-    userName,
-    splitConfig,
-    enableSplit,
-  });
-
-  if (store.setting.subject?.paperUrl && store.isMultiMedia) {
-    await getPaper(store);
-  }
-}
-async function updateStatus() {
-  const res = await getInspectedTaskStatusOfLibraryInspect({
-    subjectCode,
-    groupNumber,
-    markerId,
-    examNumber,
-    secretNumber,
-    startScore,
-    endScore,
-    questionScore,
-    unselective,
-  });
-  if (res.data.valid) Object.assign(store.status, res.data);
-}
-async function updateTask() {
-  const mkey = "fetch_task_key";
-  void message.info({ content: "获取任务中...", duration: 1.5, key: mkey });
-  let res = await getOneOfStuTask();
-  void message.success({
-    content: res.data.studentId ? "获取成功" : "无任务",
-    key: mkey,
-  });
-
-  if (res.data.studentId) {
-    let rawTask = res.data;
-    store.currentTask = addFileServerPrefixToTask(rawTask);
-  } else {
-    store.message = res.data.message;
-  }
-}
-
-watch(
-  () => store.historyOpen,
-  async () => {
-    if (!store.historyOpen) {
-      await updateClearTask();
-      await fetchTask();
-    }
-  }
-);
-
-async function fetchTask() {
-  !isSingleStudent && (await updateStatus());
-  await updateTask();
-}
-
-onMounted(async () => {
-  await updateClearTask();
-  await updateSetting();
-  await fetchTask();
-});
-
-async function getOneOfStuTask() {
-  return getOneOfInspectedTaskOfLibraryInspect({
-    subjectCode,
-    groupNumber,
-    markerId,
-    examNumber,
-    secretNumber,
-    startScore,
-    endScore,
-    questionScore,
-    unselective,
-  });
-}
-
-const realLibraryId = $computed(
-  () => (isSingleStudent ? taskId : store.currentTask?.taskId) as string
-);
-const saveTaskToServer = async () => {
-  console.log("save inspect task to server");
-  const mkey = "save_task_key";
-  void message.loading({ content: "保存评卷任务...", key: mkey });
-  const res = await saveInspectedTaskOfLibraryInspect(realLibraryId);
-  if (res.data.success && store.currentTask) {
-    void message.success({ content: "复核成功", key: mkey, duration: 2 });
-    if (!store.historyOpen) {
-      store.currentTask = undefined;
-      if (!isSingleStudent) await fetchTask();
-    } else {
-      EventBus.emit("should-reload-history");
-    }
-  } else if (res.data.message) {
-    console.log(res.data.message);
-    void message.error({ content: res.data.message, key: mkey, duration: 10 });
-  } else if (!store.currentTask) {
-    void message.warn({ content: "暂无新任务", key: mkey, duration: 10 });
-  }
-};
-
-const rejectQuestions = async ({
-  questions,
-  reason = "",
-}: {
-  questions: Array<Question>;
-  reason: string;
-}) => {
-  if (!store.currentTask) return;
-  const mkey = "reject_task_key";
-  void message.loading({ content: "打回评卷任务...", key: mkey });
-  const res = await rejectInspectedTaskOfLibraryInspect(
-    store.currentTask.taskId + "",
-    questions,
-    reason
-  );
-  if (res.data.success) {
-    store.currentTask = undefined;
-    void message.success({ content: "打回成功", key: mkey, duration: 2 });
-    if (!store.historyOpen) {
-      store.currentTask = undefined;
-      if (!isSingleStudent) await fetchTask();
-    } else {
-      EventBus.emit("should-reload-history");
-    }
-  } else if (res.data.message) {
-    console.log(res.data.message);
-    void message.error({ content: res.data.message, key: mkey, duration: 10 });
-  } else if (!store.currentTask) {
-    void message.warn({ content: "暂无新任务", key: mkey, duration: 10 });
-  }
-};
-
-const renderError = () => {
-  store.currentTask = undefined;
-  store.message = "加载失败,请重新加载。";
-};
-</script>
-
-<style scoped>
-.my-container {
-  width: 100%;
-  overflow: clip;
-}
-</style>

+ 0 - 258
src/features/library/inspect/MarkBoardInspect.vue

@@ -1,258 +0,0 @@
-<template>
-  <div
-    v-if="store.currentTask"
-    class="mark-board-track-container"
-    :class="[store.isScoreBoardCollapsed ? 'hide' : 'show']"
-  >
-    <div class="top-container tw-flex-shrink-0 tw-flex tw-items-center">
-      <div class="tw-flex tw-flex-col tw-flex-1 tw-text-center">
-        <div class="tw-flex tw-justify-center">
-          <img
-            src="../../mark/images/totalscore.png"
-            style="width: 13px; height: 16px"
-          />
-        </div>
-        <div>试卷总分</div>
-      </div>
-      <div class="tw-flex-1" style="font-size: 40px">
-        {{ markerScore > 0 ? markerScore : 0 }}
-      </div>
-    </div>
-
-    <div v-if="groups" class="tw-flex-grow tw-overflow-auto tw-my-5">
-      <template v-for="(groupNumber, index) in groups" :key="index">
-        <div class="tw-mb-4 tw-bg-white tw-p-4">
-          <div
-            class="tw-flex tw-justify-between tw-place-items-center hover:tw-bg-gray-200"
-            @mouseover="addFocusTrack(groupNumber, undefined, undefined)"
-            @mouseleave="removeFocusTrack"
-          >
-            <span class="secondary-text">分组 {{ groupNumber }}</span>
-            <input
-              class="tw-my-auto"
-              title="打回"
-              type="checkbox"
-              :checked="groupChecked(groupNumber)"
-              @click="groupClicked(groupNumber)"
-            />
-          </div>
-          <div v-if="questions">
-            <template v-for="(question, index2) in questions" :key="index2">
-              <div
-                v-if="question.groupNumber === groupNumber"
-                :class="{ 'is-rejected': question.rejected }"
-                class="question tw-flex tw-place-items-center tw-mb-1 tw-font-bold hover:tw-bg-gray-200"
-                @mouseover="
-                  addFocusTrack(
-                    undefined,
-                    question.mainNumber,
-                    question.subNumber
-                  )
-                "
-                @mouseleave="removeFocusTrack"
-              >
-                <span class="tw-flex-1">
-                  {{ question.title }} {{ question.mainNumber }}-{{
-                    question.subNumber
-                  }}
-                </span>
-                <span class="tw-flex-1 tw-text-center">
-                  {{ question.score === -1 ? "未选做" : question.score || 0 }}
-                </span>
-                <input
-                  :disabled="question.score === -1"
-                  title="打回"
-                  type="checkbox"
-                  :checked="questionChecked(question)"
-                  @change="questionCheckChanged(question)"
-                />
-              </div>
-            </template>
-          </div>
-        </div>
-      </template>
-    </div>
-
-    <div class="tw-flex tw-flex-shrink-0 tw-justify-center">
-      <!-- <qm-button
-        v-if="
-          store.currentTask.inspectTime && store.currentTask.inspectTime > 0
-        "
-        type="primary"
-        class="full-width-btn undo-btn"
-        @click="reject"
-      >
-        打回
-      </qm-button> -->
-      <qm-button
-        v-if="store.currentTask && store.historyOpen"
-        type="primary"
-        class="full-width-btn undo-btn"
-        @click="reject"
-      >
-        打回
-      </qm-button>
-      <qm-button
-        v-else-if="checkedQuestions.length === 0"
-        type="primary"
-        class="full-width-btn"
-        @click="inspect"
-      >
-        复核
-      </qm-button>
-      <qm-button
-        v-else
-        type="primary"
-        class="full-width-btn undo-btn"
-        @click="reject"
-        >打回</qm-button
-      >
-    </div>
-  </div>
-  <review-return-dialog
-    v-model:visible="reviewReturnVisible"
-    @confirmReturn="onConfirmReturn"
-  />
-</template>
-
-<script setup lang="ts">
-import type { Question } from "@/types";
-import { message } from "ant-design-vue";
-import { reactive, watch } from "vue";
-import { store } from "@/store/app";
-import useFocusTracks from "@/features/mark/composables/useFocusTracks";
-import ReviewReturnDialog from "./ReviewReturnDialog.vue";
-
-const emit = defineEmits(["inspect", "reject"]);
-
-const { addFocusTrack, removeFocusTrack } = useFocusTracks();
-
-let checkedQuestions: Question[] = reactive([]);
-let reviewReturnVisible = $ref(false);
-watch(
-  () => store.currentTask,
-  () => {
-    checkedQuestions.splice(0);
-  }
-);
-const groups = $computed(() => {
-  const gs = store.currentTaskEnsured.questionList.map((q) => q.groupNumber);
-  return [...new Set(gs)].sort((a, b) => a - b);
-});
-
-const questions = $computed(() => {
-  const qs = store.currentTaskEnsured.questionList;
-  return qs;
-});
-
-const markerScore = $computed(() => store.currentTaskEnsured.markerScore || 0);
-
-function addToCheckedQuestion(question: Question) {
-  checkedQuestions.push(question);
-}
-function removeCheckedQuestion(question: Question) {
-  const idx = checkedQuestions.indexOf(question);
-  checkedQuestions.splice(idx, 1);
-}
-function groupChecked(groupNumber: number) {
-  return (
-    checkedQuestions.filter((q) => q.groupNumber === groupNumber).length ===
-    questions.filter((q) => q.groupNumber === groupNumber).length
-  );
-}
-
-function questionChecked(question: Question) {
-  return checkedQuestions.includes(question);
-}
-
-function questionCheckChanged(question: Question) {
-  const checked = questionChecked(question);
-  if (checked) {
-    removeCheckedQuestion(question);
-  } else {
-    addToCheckedQuestion(question);
-  }
-}
-
-function groupClicked(groupNumber: number) {
-  if (groupChecked(groupNumber)) {
-    checkedQuestions
-      .filter((q) => q.groupNumber === groupNumber)
-      .forEach((q) => {
-        const idx = checkedQuestions.indexOf(q);
-        checkedQuestions.splice(idx, 1);
-      });
-  } else {
-    questions
-      .filter((q) => q.groupNumber === groupNumber)
-      .forEach((q) => {
-        if (!questionChecked(q)) checkedQuestions.push(q);
-      });
-  }
-}
-
-function reject() {
-  if (checkedQuestions.length === 0) {
-    void message.warn({ content: "请先选择试题。" });
-    return;
-  }
-  reviewReturnVisible = true;
-  // emit("reject", checkedQuestions);
-}
-
-function inspect() {
-  emit("inspect");
-}
-
-function onConfirmReturn(reason: string) {
-  emit("reject", { questions: checkedQuestions, reason });
-}
-</script>
-
-<style lang="less" scoped>
-.mark-board-track-container {
-  display: flex;
-  flex-direction: column;
-  max-width: 290px;
-  min-width: 290px;
-  max-height: calc(100vh - 56px);
-  padding: 20px;
-  z-index: 1001;
-  transition: margin-right 0.5s;
-  color: var(--app-small-header-text-color);
-}
-.mark-board-track-container.show {
-  margin-right: 0;
-}
-.mark-board-track-container.hide {
-  margin-right: -290px;
-}
-
-.top-container {
-  background-color: var(--app-container-bg-color);
-  height: 86px;
-  border-radius: 5px;
-
-  color: white;
-  background-color: var(--app-primary-button-bg-color);
-}
-.total-score {
-  color: var(--app-main-text-color);
-  font-size: 32px;
-}
-.question {
-  min-width: 80px;
-  background-color: var(--app-container-bg-color);
-  &.is-rejected {
-    background-color: yellow;
-  }
-}
-.full-width-btn {
-  width: 100%;
-  border-radius: 20px;
-}
-.undo-btn {
-  background-color: var(--app-undo-button-bg-color);
-  border-color: var(--app-undo-button-bg-color);
-}
-</style>

+ 0 - 11
src/features/library/inspect/MarkBody.vue

@@ -1,11 +0,0 @@
-<template>
-  <MarkBodyBase v-if="store" @error="$emit('error')" />
-</template>
-
-<script setup lang="ts">
-import MarkBodyBase from "@/features/mark/MarkBodyBase.vue";
-
-import { store } from "@/store/app";
-
-defineEmits(["error"]);
-</script>

+ 0 - 32
src/features/library/inspect/MarkHeader.vue

@@ -1,32 +0,0 @@
-<template>
-  <CommonMarkHeader
-    :isSingleStudent="isSingleStudent"
-    :clearTasks="clearTasks"
-    showScoreBoard
-  >
-    <span>
-      <span class="header-small-text">待复核</span
-      ><span class="highlight-text">{{ store.status.totalCount ?? "-" }}</span>
-    </span>
-  </CommonMarkHeader>
-</template>
-
-<script setup lang="ts">
-import { store } from "@/store/app";
-import { useRoute } from "vue-router";
-import { clearInspectedTaskOfLibraryInspect } from "@/api/libraryInspectPage";
-import CommonMarkHeader from "@/components/CommonMarkHeader.vue";
-
-const route = useRoute();
-let isSingleStudent = !!route.query.studentId;
-const { subjectCode, groupNumber } = route.query as {
-  subjectCode: string;
-  groupNumber: string;
-};
-
-let clearTasks = clearInspectedTaskOfLibraryInspect.bind(
-  null,
-  subjectCode,
-  groupNumber
-);
-</script>

+ 0 - 115
src/features/library/inspect/ReviewReturnDialog.vue

@@ -1,115 +0,0 @@
-<template>
-  <a-modal
-    v-bind="$attrs"
-    title="打回"
-    :maskClosable="false"
-    :footer="false"
-    :zIndex="6000"
-  >
-    <div ref="modalContenBox">
-      <a-form
-        autocomplete="off"
-        :model="reasonOption"
-        :labelCol="{span: 5}"
-        @keydown.stop=""
-        @keypress.stop=""
-        @finish="onConfirm"
-      >
-        <a-form-item label="打回原因" name="reason" required>
-          <a-select
-            v-model:value="reasonOption.reason"
-            placeholder="选择打回原因"
-            :virtual="false"
-            :getPopupContainer="() => modalContenBox!"
-            :options="reasonOptions"
-          >
-          </a-select>
-        </a-form-item>
-        <a-form-item label="详情描述" name="desc">
-          <a-textarea
-            v-model:value="reasonOption.desc"
-            placeholder="详情描述(限制50字以内)"
-            :maxlength="50"
-          ></a-textarea>
-        </a-form-item>
-        <a-form-item>
-          <a-row justify="center">
-            <a-col>
-              <a-button type="primary" htmlType="submit">打回</a-button>
-            </a-col>
-            <a-col :offset="2">
-              <a-button type="ghost" @click="onCancel">取消</a-button>
-            </a-col>
-          </a-row>
-        </a-form-item>
-      </a-form>
-    </div>
-  </a-modal>
-</template>
-
-<script setup lang="ts">
-import { reactive, useAttrs, watch, ref } from "vue";
-
-interface ReturnInfo {
-  reason?: string;
-  desc: string;
-}
-
-/** select 定位层级较低, 所以在弹窗中使用一个div容器, 将select 组件的 dorpdown 挂载在div中, 避免dropdown被modal挡住 */
-const modalContenBox = ref<HTMLElement>();
-
-const emit = defineEmits(["confirmReturn"]);
-
-/** 打回原因元数据 */
-const reasonOptions = ["给分较高", "给分较低", "判分错误", "其它"].map((v) => ({
-  value: v,
-}));
-
-/** 初始化表单数据 */
-const getInitialFormData = (): ReturnInfo => {
-  return {
-    reason: undefined,
-    desc: "",
-  };
-};
-
-const reasonOption = reactive<ReturnInfo>(getInitialFormData());
-
-watch(
-  () => modalContenBox,
-  () => {
-    console.log(modalContenBox);
-  }
-);
-
-/** 弹窗关闭时 重置form表单数据 */
-const attrs = useAttrs();
-watch(
-  () => attrs.visible,
-  (v) => {
-    if (!v) {
-      Object.assign(reasonOption, getInitialFormData());
-    }
-  }
-);
-
-/** 确定打回 */
-const onConfirm = () => {
-  emit(
-    "confirmReturn",
-    Object.values(reasonOption)
-      .map((s) => s || "")
-      .join(":")
-  );
-  onCancel();
-};
-
-/** 取消打回 */
-const onCancel = (e?: MouseEvent) => {
-  (attrs["onUpdate:visible"] as (e: boolean) => void)?.(false);
-  (attrs.onChange as (e: boolean) => void)?.(false);
-  (attrs.onCancel as (e?: MouseEvent) => void)?.(e);
-};
-</script>
-
-<style scoped></style>

+ 0 - 99
src/features/library/libraryTrack/LibraryTrack.vue

@@ -1,99 +0,0 @@
-<template>
-  <div class="my-container">
-    <mark-header />
-    <div class="tw-flex tw-gap-1">
-      <mark-body @error="renderError" />
-    </div>
-  </div>
-  <MinimapModal />
-</template>
-
-<script setup lang="ts">
-import { onMounted } from "vue";
-import { store } from "@/store/app";
-import MarkHeader from "./MarkHeader.vue";
-import MinimapModal from "@/features/mark/MinimapModal.vue";
-import { useRoute } from "vue-router";
-import MarkBody from "./MarkBody.vue";
-import { message } from "ant-design-vue";
-import {
-  getSingleLibraryTask,
-  getSingleLibraryTaskTrial,
-} from "@/api/libraryTrackPage";
-import { getAdminPageSetting } from "@/api/inspectPage";
-import { getPaper } from "@/api/jsonMark";
-import { addFileServerPrefixToTask } from "@/utils/utils";
-
-const route = useRoute();
-let taskId = route.query.taskId;
-let subjectCode = route.query.subjectCode;
-
-async function updateSetting() {
-  const settingRes = await getAdminPageSetting(subjectCode as string);
-  const { examType, fileServer, subject, userName, splitConfig, enableSplit } =
-    settingRes.data;
-  store.initSetting({
-    examType,
-    fileServer,
-    subject,
-    userName,
-    splitConfig,
-    enableSplit,
-  });
-
-  if (store.setting.subject?.paperUrl && store.isMultiMedia) {
-    await getPaper(store);
-  }
-}
-
-async function updateTask() {
-  const mkey = "fetch_task_key";
-  void message.info({ content: "获取任务中...", duration: 1.5, key: mkey });
-  let res = await getSingleStuTask();
-  void message.success({
-    content: res.data.task.studentId ? "获取成功" : "无任务",
-    key: mkey,
-  });
-
-  if (res.data.task.studentId) {
-    store.setting.fileServer = res.data.fileServer;
-    store.setting.splitConfig = res.data.splitConfig;
-    store.setting.groupNumber = res.data.groupNumber;
-
-    let rawTask = res.data.task;
-    store.currentTask = addFileServerPrefixToTask(rawTask);
-  } else {
-    // store.message = res.data.message;
-    console.log("请求异常");
-  }
-}
-
-async function fetchTask() {
-  await updateTask();
-}
-
-onMounted(async () => {
-  await updateSetting();
-  await fetchTask();
-});
-
-async function getSingleStuTask() {
-  if (route.name === "TrialRoute") {
-    return getSingleLibraryTaskTrial(taskId as string);
-  } else {
-    return getSingleLibraryTask(taskId as string);
-  }
-}
-
-const renderError = () => {
-  store.currentTask = undefined;
-  store.message = "加载失败,请重新加载。";
-};
-</script>
-
-<style scoped>
-.my-container {
-  width: 100%;
-  overflow: clip;
-}
-</style>

+ 0 - 10
src/features/library/libraryTrack/MarkBody.vue

@@ -1,10 +0,0 @@
-<template>
-  <MarkBodyBase v-if="store" @error="$emit('error')" />
-</template>
-
-<script setup lang="ts">
-import MarkBodyBase from "@/features/mark/MarkBodyBase.vue";
-import { store } from "@/store/app";
-
-defineEmits(["error"]);
-</script>

+ 0 - 7
src/features/library/libraryTrack/MarkHeader.vue

@@ -1,7 +0,0 @@
-<template>
-  <CommonMarkHeader></CommonMarkHeader>
-</template>
-
-<script setup lang="ts">
-import CommonMarkHeader from "@/components/CommonMarkHeader.vue";
-</script>

+ 0 - 11
src/features/library/quality/MarkBody.vue

@@ -1,11 +0,0 @@
-<template>
-  <MarkBodyBase v-if="store" @error="$emit('error')" />
-</template>
-
-<script setup lang="ts">
-import MarkBodyBase from "@/features/mark/MarkBodyBase.vue";
-
-import { store } from "@/store/app";
-
-defineEmits(["error"]);
-</script>

+ 0 - 7
src/features/library/quality/MarkHeader.vue

@@ -1,7 +0,0 @@
-<template>
-  <CommonMarkHeader showPaperAndAnswer></CommonMarkHeader>
-</template>
-
-<script setup lang="ts">
-import CommonMarkHeader from "@/components/CommonMarkHeader.vue";
-</script>

+ 0 - 74
src/features/library/quality/Quality.vue

@@ -1,74 +0,0 @@
-<template>
-  <div class="my-container">
-    <mark-header />
-    <div class="tw-flex tw-gap-1">
-      <mark-history
-        title="给分记录"
-        :subjectCode="subjectCode"
-        :markerId="markerId"
-        :markerScore="markerScore"
-        :getHistory="getQualityHistory"
-      />
-      <mark-body @error="renderError" />
-    </div>
-  </div>
-  <AnswerModal />
-  <PaperModal />
-  <MinimapModal />
-</template>
-
-<script setup lang="ts">
-import { onMounted } from "vue";
-import { store } from "@/store/app";
-import MarkHeader from "./MarkHeader.vue";
-import { useRoute } from "vue-router";
-import MarkBody from "./MarkBody.vue";
-import MarkHistory from "@/features/mark/MarkHistory.vue";
-import { getAdminPageSetting } from "@/api/inspectPage";
-import MinimapModal from "@/features/mark/MinimapModal.vue";
-import PaperModal from "@/features/mark/PaperModal.vue";
-import AnswerModal from "@/features/mark/AnswerModal.vue";
-import { getPaper } from "@/api/jsonMark";
-import { getQualityHistory } from "@/api/qualityPage";
-
-const route = useRoute();
-const { subjectCode, markerId, markerScore } = route.query as {
-  subjectCode: string;
-  markerId: string;
-  markerScore: string;
-};
-
-async function updateSetting() {
-  const settingRes = await getAdminPageSetting(subjectCode);
-  const { examType, fileServer, subject, userName, splitConfig, enableSplit } =
-    settingRes.data;
-  store.initSetting({
-    examType,
-    fileServer,
-    subject,
-    userName,
-    splitConfig,
-    enableSplit,
-  });
-
-  if (store.setting.subject?.paperUrl && store.isMultiMedia) {
-    await getPaper(store);
-  }
-}
-
-onMounted(async () => {
-  await updateSetting();
-  store.historyOpen = true;
-});
-
-const renderError = () => {
-  store.currentTask = undefined;
-  store.message = "加载失败,请重新加载。";
-};
-</script>
-
-<style scoped>
-.my-container {
-  width: 100%;
-}
-</style>

+ 7 - 5
src/features/mark/Mark.vue

@@ -6,18 +6,18 @@
       <mark-history showSearch :getHistory="getHistoryTask" />
       <mark-body @error="removeBrokenTask" />
       <mark-board-track
-        v-if="store.isTrackMode"
+        v-if="markStore.isTrackMode"
         @submit="saveTaskToServer"
         @unselectiveSubmit="unselectiveSubmit"
       />
       <mark-board-key-board
-        v-if="store.shouldShowMarkBoardKeyBoard"
+        v-if="markStore.shouldShowMarkBoardKeyBoard"
         @submit="saveTaskToServer"
         @allZeroSubmit="allZeroSubmit"
         @unselectiveSubmit="unselectiveSubmit"
       />
       <mark-board-mouse
-        v-if="store.shouldShowMarkBoardMouse"
+        v-if="markStore.shouldShowMarkBoardMouse"
         @submit="saveTaskToServer"
         @allZeroSubmit="allZeroSubmit"
         @unselectiveSubmit="unselectiveSubmit"
@@ -33,7 +33,7 @@
   <modal-short-cut />
   <!-- other -->
   <mark-board-track-dialog
-    v-if="store.isTrackMode"
+    v-if="markStore.isTrackMode"
     @submit="saveTaskToServer"
     @allZeroSubmit="allZeroSubmit"
     @unselectiveSubmit="unselectiveSubmit"
@@ -49,7 +49,7 @@
       class="tw-text-8xl tw-flex tw-items-center tw-justify-center tw-text-red-500"
       data-test="status-spin"
     >
-      {{ store.getStatusValueName }}
+      {{ markStore.getStatusValueName }}
     </div>
   </a-spin>
 </template>
@@ -58,6 +58,7 @@
 import { onMounted } from "vue";
 import { useTimers } from "@/setups/useTimers";
 import { getHistoryTask } from "@/api/markPage";
+import { useMarkStore } from "@/store";
 
 // components
 import MarkHeader from "./MarkHeader.vue";
@@ -85,6 +86,7 @@ import useSetting from "./composables/useSetting";
 import useStatus from "./composables/useStatus";
 import useMarkSubmit from "./composables/useMarkSubmit";
 
+const markStore = useMarkStore();
 const { updateMarkTask, nextTask, removeBrokenTask } = useMarkTask();
 const { updateSetting } = useSetting();
 const { statusSpinning, loadingStatusSpinning, updateStatus, updateGroups } =

+ 0 - 141
src/features/student/importInspect/ImportInspect.vue

@@ -1,141 +0,0 @@
-<template>
-  <div class="my-container">
-    <mark-header />
-    <div class="tw-flex tw-gap-1">
-      <mark-body origImageUrls="sheetUrls" @error="renderError" />
-      <MarkBoardInspect
-        :tagged="isCurrentTagged"
-        :isFirst="isFirst"
-        :isLast="isLast"
-        @makeTag="saveTaskToServer"
-        @fetchTask="fetchTask"
-      />
-    </div>
-  </div>
-  <MinimapModal />
-</template>
-
-<script setup lang="ts">
-import { onMounted } from "vue";
-import {
-  getInspectedSettingOfImportInspect,
-  getSingleInspectedTaskOfImportInspect,
-  saveInspectedTaskOfImportInspect,
-} from "@/api/importInspectPage";
-import { store } from "@/store/app";
-import MarkHeader from "./MarkHeader.vue";
-import MinimapModal from "@/features/mark/MinimapModal.vue";
-import { useRoute } from "vue-router";
-import MarkBody from "../studentInspect/MarkBody.vue";
-import MarkBoardInspect from "./MarkBoardInspect.vue";
-import type { AdminPageSetting } from "@/types";
-import { message } from "ant-design-vue";
-import { addFileServerPrefixToTask } from "@/utils/utils";
-
-const route = useRoute();
-const { studentId } = route.query as {
-  studentId: string;
-};
-
-let studentIds: number[] = $ref([]);
-let tagIds: number[] = $ref([]);
-let currentStudentId = $ref(0);
-
-async function updateSetting() {
-  const settingRes = await getInspectedSettingOfImportInspect(studentId);
-  const { examType, fileServer } = settingRes.data;
-  store.initSetting({ examType, fileServer } as AdminPageSetting);
-  store.status.totalCount = settingRes.data.inspectCount;
-  store.status.markedCount = 0;
-
-  if (!settingRes.data.inspectCount) {
-    store.message = settingRes.data.message;
-  } else {
-    studentIds = settingRes.data.studentIds;
-    tagIds = settingRes.data.tagIds;
-  }
-}
-// 要通过fetchTask调用
-async function updateTask() {
-  if (!currentStudentId) {
-    return;
-  }
-  const mkey = "fetch_task_key";
-  void message.info({ content: "获取任务中...", duration: 1.5, key: mkey });
-  let res = await getSingleInspectedTaskOfImportInspect("" + currentStudentId);
-  void message.success({
-    content: res.data.studentId ? "获取成功" : "无任务",
-    key: mkey,
-  });
-
-  if (res.data.studentId) {
-    let rawTask = res.data;
-    store.currentTask = addFileServerPrefixToTask(rawTask);
-  } else {
-    store.message = res.data.message;
-  }
-}
-
-const isCurrentTagged = $computed(() => tagIds.includes(currentStudentId));
-const isFirst = $computed(() => studentIds.indexOf(currentStudentId) === 0);
-const isLast = $computed(
-  () => studentIds.indexOf(currentStudentId) === studentIds.length - 1
-);
-
-async function fetchTask(next: boolean, init?: boolean) {
-  if (init) {
-    currentStudentId = studentIds[0];
-  } else if (isLast && next) {
-    return; // currentStudentId是最后一个不调用
-  } else if (isFirst && !next) {
-    return; // currentStudentId是第一个不调用
-  } else {
-    currentStudentId =
-      studentIds[studentIds.indexOf(currentStudentId) + (next ? 1 : -1)];
-  }
-  if (!currentStudentId) return; // 无currentStudentId不调用
-  store.status.markedCount = studentIds.indexOf(currentStudentId) + 1;
-  await updateTask();
-}
-
-onMounted(async () => {
-  await updateSetting();
-  await fetchTask(true, true);
-});
-
-const saveTaskToServer = async () => {
-  const mkey = "save_task_key";
-  void message.loading({ content: "标记评卷任务...", key: mkey });
-  const res = await saveInspectedTaskOfImportInspect(
-    currentStudentId + "",
-    !isCurrentTagged + ""
-  );
-  if (res.data.success) {
-    void message.success({
-      content: isCurrentTagged ? "取消标记成功" : "标记成功",
-      key: mkey,
-      duration: 2,
-    });
-    if (isCurrentTagged) {
-      tagIds.splice(tagIds.indexOf(currentStudentId), 1);
-    } else {
-      tagIds.push(currentStudentId);
-    }
-  } else {
-    console.log(res.data.message);
-    void message.error({ content: res.data.message, key: mkey, duration: 10 });
-  }
-};
-
-const renderError = () => {
-  store.currentTask = undefined;
-  store.message = "加载失败,请重新加载。";
-};
-</script>
-
-<style scoped>
-.my-container {
-  width: 100%;
-  overflow: clip;
-}
-</style>

+ 0 - 225
src/features/student/importInspect/MarkBoardInspect.vue

@@ -1,225 +0,0 @@
-<template>
-  <div
-    v-if="store.currentTask"
-    class="mark-board-track-container"
-    :class="[store.isScoreBoardCollapsed ? 'hide' : 'show']"
-  >
-    <div class="top-container tw-flex-shrink-0 tw-flex tw-items-center">
-      <div class="tw-flex tw-flex-col tw-flex-1 tw-text-center">
-        <div class="tw-flex tw-justify-center">
-          <img
-            src="../../mark/images/totalscore.png"
-            style="width: 13px; height: 16px"
-          />
-        </div>
-        <div>试卷总分</div>
-      </div>
-      <div class="tw-flex-1" style="font-size: 40px">
-        {{ markerScore > 0 ? markerScore : 0 }}
-      </div>
-      <div
-        class="star"
-        :class="props.tagged ? 'star-yes' : 'star-no'"
-        @click="makeTag(!props.tagged)"
-      ></div>
-    </div>
-
-    <div v-if="groups" class="tw-flex-grow tw-overflow-auto tw-my-5">
-      <template v-for="(groupNumber, index) in groups" :key="index">
-        <div class="tw-mb-4 tw-bg-white tw-p-4 tw-pl-5 tw-pr-3">
-          <div
-            class="tw-flex tw-justify-between tw-place-items-center hover:tw-bg-gray-200"
-            @mouseover="addFocusTrack(groupNumber, undefined, undefined)"
-            @mouseleave="removeFocusTrack"
-          >
-            <span class="secondary-text">分组 {{ groupNumber }}</span>
-          </div>
-          <div v-if="questions">
-            <template v-for="(question, index2) in questions" :key="index2">
-              <div
-                v-if="question.groupNumber === groupNumber"
-                class="question tw-flex tw-place-items-center tw-mb-1 tw-font-bold hover:tw-bg-gray-200"
-                :class="{ uncalculate: question.uncalculate }"
-                @mouseover="
-                  addFocusTrack(
-                    undefined,
-                    question.mainNumber,
-                    question.subNumber
-                  )
-                "
-                @mouseleave="removeFocusTrack"
-              >
-                <a-tooltip placement="left">
-                  <template #title>
-                    <span>未计入总分</span>
-                  </template>
-                  <MinusCircleFilled class="uncalculate-icon" />
-                </a-tooltip>
-                <span class="question-title">
-                  {{ question.title }} {{ question.mainNumber }}-{{
-                    question.subNumber
-                  }}
-                </span>
-                <span class="tw-text-center question-score">
-                  {{ question.score === -1 ? "未选做" : question.score || 0 }}
-                </span>
-              </div>
-            </template>
-          </div>
-        </div>
-      </template>
-    </div>
-
-    <div class="tw-flex tw-flex-shrink-0 tw-justify-center tw-gap-4">
-      <a-button
-        type="primary"
-        class="full-width-btn"
-        :disabled="props.isFirst"
-        @click="fetchTask(false)"
-      >
-        上一个
-      </a-button>
-      <a-button
-        type="primary"
-        class="full-width-btn"
-        :disabled="props.isLast"
-        @click="fetchTask(true)"
-        >下一个</a-button
-      >
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import type { Question } from "@/types";
-import { reactive, watch } from "vue";
-import { store } from "@/store/app";
-import useFocusTracks from "@/features/mark/composables/useFocusTracks";
-import { MinusCircleFilled } from "@ant-design/icons-vue";
-
-const emit = defineEmits(["makeTag", "fetchTask"]);
-const props = defineProps<{
-  tagged: boolean;
-  isFirst: boolean;
-  isLast: boolean;
-}>();
-let checkedQuestions: Question[] = reactive([]);
-
-const { addFocusTrack, removeFocusTrack } = useFocusTracks();
-
-watch(
-  () => store.currentTask,
-  () => {
-    checkedQuestions.splice(0);
-  }
-);
-const groups = $computed(() => {
-  const gs = store.currentTaskEnsured.questionList.map((q) => q.groupNumber);
-  return [...new Set(gs)].sort((a, b) => a - b);
-});
-
-const questions = $computed(() => {
-  const qs = store.currentTaskEnsured.questionList;
-  return qs;
-});
-
-const markerScore = $computed(() => store.currentTaskEnsured.markerScore || 0);
-
-function fetchTask(next: boolean) {
-  emit("fetchTask", next);
-}
-
-function makeTag(isTag: boolean) {
-  emit("makeTag", isTag);
-}
-</script>
-
-<style scoped>
-.mark-board-track-container {
-  display: flex;
-  flex-direction: column;
-  max-width: 290px;
-  min-width: 290px;
-  max-height: calc(100vh - 56px);
-  padding: 20px;
-  z-index: 1001;
-  transition: margin-right 0.5s;
-  color: var(--app-small-header-text-color);
-}
-.mark-board-track-container.show {
-  margin-right: 0;
-}
-.mark-board-track-container.hide {
-  margin-right: -290px;
-}
-
-.top-container {
-  background-color: var(--app-container-bg-color);
-  height: 86px;
-  border-radius: 5px;
-
-  color: white;
-  background-color: var(--app-primary-button-bg-color);
-}
-.question {
-  min-width: 80px;
-  background-color: var(--app-container-bg-color);
-}
-.question-title {
-  flex: 1;
-}
-.question-score {
-  flex-basis: 56px;
-  padding: 0 3px;
-}
-
-.question.uncalculate {
-  position: relative;
-}
-
-.question .uncalculate-icon {
-  display: none;
-  color: red;
-  position: absolute;
-  font-size: 15px;
-  left: -16px;
-  top: 0.3em;
-}
-
-.question.uncalculate .uncalculate-icon {
-  display: block;
-}
-
-.full-width-btn {
-  width: 100%;
-  border-radius: 20px;
-}
-
-.star {
-  margin-top: -30px;
-  margin-right: 20px;
-  width: 30px;
-  height: 30px;
-  cursor: pointer;
-
-  clip-path: polygon(
-    50% 0%,
-    61% 35%,
-    98% 35%,
-    68% 57%,
-    79% 91%,
-    50% 70%,
-    21% 91%,
-    32% 57%,
-    2% 35%,
-    39% 35%
-  );
-}
-
-.star.star-yes {
-  background-color: yellowgreen;
-}
-.star.star-no {
-  background-color: white;
-}
-</style>

+ 0 - 45
src/features/student/importInspect/MarkHeader.vue

@@ -1,45 +0,0 @@
-<template>
-  <CommonMarkHeader
-    :isSingleStudent="isSingleStudent"
-    :clearTasks="clearTasks"
-    showScoreBoard
-    :notShowHistoryToggle="true"
-  >
-    <slot name="taskInfo">
-      <div>
-        <span class="header-small-text">学号</span>
-        <span class="highlight-text">
-          {{ store.currentTask?.studentCode ?? "-" }}
-        </span>
-      </div>
-      <div>
-        <span class="header-small-text">姓名</span>
-        <span class="highlight-text">
-          {{ store.currentTask?.studentName ?? "-" }}
-        </span>
-      </div>
-    </slot>
-    <span>
-      <span class="header-small-text">待复核</span>
-      <span class="highlight-text">{{
-        store.status.totalCount - store.status.markedCount ?? "-"
-      }}</span>
-    </span>
-  </CommonMarkHeader>
-</template>
-
-<script setup lang="ts">
-import { clearInspectedTask } from "@/api/inspectPage";
-import { store } from "@/store/app";
-import { useRoute } from "vue-router";
-import CommonMarkHeader from "@/components/CommonMarkHeader.vue";
-
-const route = useRoute();
-let isSingleStudent = !!route.query.studentId;
-const { studentId, subjectCode } = route.query as {
-  studentId: string;
-  subjectCode: string;
-};
-
-let clearTasks = clearInspectedTask.bind(null, studentId, subjectCode);
-</script>

+ 0 - 227
src/features/student/scoreVerify/MarkBoardInspect.vue

@@ -1,227 +0,0 @@
-<template>
-  <div
-    v-if="store.currentTask"
-    class="mark-board-track-container"
-    :class="[store.isScoreBoardCollapsed ? 'hide' : 'show']"
-  >
-    <div class="top-container tw-flex-shrink-0 tw-flex tw-items-center">
-      <div class="tw-flex tw-flex-col tw-flex-1 tw-text-center">
-        <div class="tw-flex tw-justify-center">
-          <img
-            src="../../mark/images/totalscore.png"
-            style="width: 13px; height: 16px"
-          />
-        </div>
-        <div>试卷总分</div>
-      </div>
-      <div class="tw-flex-1" style="font-size: 40px">
-        {{ markerScore > 0 ? markerScore : 0 }}
-      </div>
-      <div
-        class="star"
-        :class="props.tagged ? 'star-yes' : 'star-no'"
-        @click="makeTag(!props.tagged)"
-      ></div>
-    </div>
-
-    <div v-if="groups" class="tw-flex-grow tw-overflow-auto tw-my-5">
-      <template v-for="(groupNumber, index) in groups" :key="index">
-        <div class="tw-mb-4 tw-bg-white tw-p-4 tw-pl-5 tw-pr-3">
-          <div
-            class="tw-flex tw-justify-between tw-place-items-center hover:tw-bg-gray-200"
-            @mouseover="addFocusTrack(groupNumber, undefined, undefined)"
-            @mouseleave="removeFocusTrack"
-          >
-            <span class="secondary-text">分组 {{ groupNumber }}</span>
-          </div>
-          <div v-if="questions">
-            <template v-for="(question, index2) in questions" :key="index2">
-              <div
-                v-if="question.groupNumber === groupNumber"
-                class="question tw-flex tw-place-items-center tw-mb-1 tw-font-bold hover:tw-bg-gray-200"
-                :class="{ uncalculate: question.uncalculate }"
-                @mouseover="
-                  addFocusTrack(
-                    undefined,
-                    question.mainNumber,
-                    question.subNumber
-                  )
-                "
-                @mouseleave="removeFocusTrack"
-              >
-                <a-tooltip placement="left">
-                  <template #title>
-                    <span>未计入总分</span>
-                  </template>
-                  <MinusCircleFilled class="uncalculate-icon" />
-                </a-tooltip>
-                <span class="question-title">
-                  {{ question.title }} {{ question.mainNumber }}-{{
-                    question.subNumber
-                  }}
-                </span>
-                <span class="tw-text-center question-score">
-                  {{ question.score === -1 ? "未选做" : question.score || 0 }}
-                </span>
-              </div>
-            </template>
-          </div>
-        </div>
-      </template>
-    </div>
-
-    <div class="tw-flex tw-flex-shrink-0 tw-justify-center tw-gap-4">
-      <a-button
-        type="primary"
-        class="full-width-btn"
-        :disabled="props.isFirst"
-        @click="fetchTask(false)"
-      >
-        上一个
-      </a-button>
-      <a-button
-        type="primary"
-        class="full-width-btn"
-        :disabled="props.isLast"
-        @click="fetchTask(true)"
-        >下一个</a-button
-      >
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import type { Question } from "@/types";
-import { reactive, watch } from "vue";
-import { store } from "@/store/app";
-import useFocusTracks from "@/features/mark/composables/useFocusTracks";
-import { MinusCircleFilled } from "@ant-design/icons-vue";
-
-const emit = defineEmits(["makeTag", "fetchTask"]);
-const props = defineProps<{
-  tagged: boolean;
-  isFirst: boolean;
-  isLast: boolean;
-}>();
-let checkedQuestions: Question[] = reactive([]);
-
-const { addFocusTrack, removeFocusTrack } = useFocusTracks();
-
-watch(
-  () => store.currentTask,
-  () => {
-    checkedQuestions.splice(0);
-  }
-);
-const groups = $computed(() => {
-  const gs = (store.currentTaskEnsured?.questionList || []).map(
-    (q) => q.groupNumber
-  );
-  return [...new Set(gs)].sort((a, b) => a - b);
-});
-
-const questions = $computed(() => {
-  const qs = store.currentTaskEnsured.questionList;
-  return qs;
-});
-
-const markerScore = $computed(() => store.currentTaskEnsured.markerScore || 0);
-
-function fetchTask(next: boolean) {
-  emit("fetchTask", next);
-}
-
-function makeTag(isTag: boolean) {
-  emit("makeTag", isTag);
-}
-</script>
-
-<style scoped>
-.mark-board-track-container {
-  display: flex;
-  flex-direction: column;
-  max-width: 290px;
-  min-width: 290px;
-  max-height: calc(100vh - 56px);
-  padding: 20px;
-  z-index: 1001;
-  transition: margin-right 0.5s;
-  color: var(--app-small-header-text-color);
-}
-.mark-board-track-container.show {
-  margin-right: 0;
-}
-.mark-board-track-container.hide {
-  margin-right: -290px;
-}
-
-.top-container {
-  background-color: var(--app-container-bg-color);
-  height: 86px;
-  border-radius: 5px;
-
-  color: white;
-  background-color: var(--app-primary-button-bg-color);
-}
-.question {
-  min-width: 80px;
-  background-color: var(--app-container-bg-color);
-}
-.question-title {
-  flex: 1;
-}
-.question-score {
-  flex-basis: 56px;
-  padding: 0 3px;
-}
-
-.question.uncalculate {
-  position: relative;
-}
-
-.question .uncalculate-icon {
-  display: none;
-  color: red;
-  position: absolute;
-  font-size: 15px;
-  left: -16px;
-  top: 0.3em;
-}
-
-.question.uncalculate .uncalculate-icon {
-  display: block;
-}
-
-.full-width-btn {
-  width: 100%;
-  border-radius: 20px;
-}
-
-.star {
-  margin-top: -30px;
-  margin-right: 20px;
-  width: 30px;
-  height: 30px;
-  cursor: pointer;
-
-  clip-path: polygon(
-    50% 0%,
-    61% 35%,
-    98% 35%,
-    68% 57%,
-    79% 91%,
-    50% 70%,
-    21% 91%,
-    32% 57%,
-    2% 35%,
-    39% 35%
-  );
-}
-
-.star.star-yes {
-  background-color: yellowgreen;
-}
-.star.star-no {
-  background-color: white;
-}
-</style>

+ 0 - 57
src/features/student/scoreVerify/MarkHeader.vue

@@ -1,57 +0,0 @@
-<template>
-  <CommonMarkHeader
-    :isSingleStudent="isSingleStudent"
-    :clearTasks="clearTasks"
-    showScoreBoard
-    showPaperAndAnswer
-    :notShowHistoryToggle="true"
-  >
-    <slot name="taskInfo">
-      <div>
-        <span class="header-small-text">学号</span>
-        <span class="highlight-text">
-          {{ store.currentTask?.studentCode ?? "-" }}
-        </span>
-      </div>
-      <div>
-        <span class="header-small-text">姓名</span>
-        <span class="highlight-text">
-          {{ store.currentTask?.studentName ?? "-" }}
-        </span>
-      </div>
-    </slot>
-    <span>
-      <span class="header-small-text">待校验</span>
-      <span class="highlight-text">{{
-        store.status.totalCount - store.status.markedCount || "-"
-      }}</span>
-    </span>
-
-    <template v-if="route.query?.studentId" #studentInfo
-      ><div class="highlight-text">
-        考生:
-        {{
-          store.currentTask?.studentCode +
-          " - " +
-          store.currentTask?.studentName
-        }}
-      </div></template
-    >
-  </CommonMarkHeader>
-</template>
-
-<script setup lang="ts">
-import { clearInspectedTask } from "@/api/inspectPage";
-import { store } from "@/store/app";
-import { useRoute } from "vue-router";
-import CommonMarkHeader from "@/components/CommonMarkHeader.vue";
-
-const route = useRoute();
-let isSingleStudent = !!route.query.studentId;
-const { studentId, subjectCode } = route.query as {
-  studentId: string;
-  subjectCode: string;
-};
-
-let clearTasks = clearInspectedTask.bind(null, studentId, subjectCode);
-</script>

+ 0 - 173
src/features/student/scoreVerify/ScoreVerify.vue

@@ -1,173 +0,0 @@
-<template>
-  <div class="my-container">
-    <mark-header />
-    <div class="tw-flex tw-gap-1">
-      <mark-body origImageUrls="sheetUrls" @error="renderError" />
-      <MarkBoardInspect
-        :tagged="isCurrentTagged"
-        :isFirst="isFirst"
-        :isLast="isLast"
-        @makeTag="saveTaskToServer"
-        @fetchTask="fetchTask"
-      />
-    </div>
-  </div>
-  <MinimapModal />
-  <PaperModal />
-</template>
-
-<script setup lang="ts">
-import { onMounted, ref } from "vue";
-// import {
-//   getInspectedSettingOfImportInspect,
-//   getSingleInspectedTaskOfImportInspect,
-//   saveInspectedTaskOfImportInspect,
-// } from "@/api/importInspectPage";
-import {
-  getInspectedSettingOfImportInspect,
-  getSingleInspectedTaskOfImportInspect,
-  saveInspectedTaskOfImportInspect,
-} from "@/api/scoreVerify";
-import { store } from "@/store/app";
-import MarkHeader from "./MarkHeader.vue";
-import MinimapModal from "@/features/mark/MinimapModal.vue";
-import PaperModal from "@/features/mark/PaperModal.vue";
-import { useRoute } from "vue-router";
-// import MarkBody from "../studentInspect/MarkBody.vue";
-import MarkBody from "./markBody.vue";
-import MarkBoardInspect from "./MarkBoardInspect.vue";
-import type { AdminPageSetting } from "@/types";
-import { message } from "ant-design-vue";
-import { addFileServerPrefixToTask } from "@/utils/utils";
-
-const route = useRoute();
-const { studentId } = route.query as {
-  studentId: string | number;
-};
-
-let studentIds: (number | string)[] = $ref([]);
-// let tagIds: number[] = $ref([]);
-let currentStudentId = $ref<string | number>(0);
-const fileServer = ref("");
-
-async function updateSetting() {
-  const settingRes = await getInspectedSettingOfImportInspect(
-    studentId as string
-  );
-  const { examType, fileServer, doubleTrack } = settingRes.data;
-  store.initSetting({ examType, fileServer, doubleTrack } as AdminPageSetting);
-  // store.status.totalCount = settingRes.data.inspectCount;
-  // store.status.markedCount = 0;
-
-  // if (!settingRes.data.inspectCount) {
-  //   store.message = settingRes.data.message;
-  // } else {
-  if (studentId) {
-    studentIds = [studentId];
-  } else {
-    studentIds = settingRes.data.studentIds || [];
-  }
-  if (!studentIds.length) {
-    await message.warning("没有数据需要校验");
-  }
-  // tagIds = settingRes.data.tagIds;
-  // }
-  return fileServer;
-}
-// 要通过fetchTask调用
-async function updateTask() {
-  if (!currentStudentId) {
-    return;
-  }
-  const mkey = "fetch_task_key";
-  void message.info({ content: "获取任务中...", duration: 1.5, key: mkey });
-  let res = await getSingleInspectedTaskOfImportInspect("" + currentStudentId);
-  void message.success({
-    content: res.data.task?.studentId ? "获取成功" : "无任务",
-    key: mkey,
-  });
-  isCurrentTagged = !!res.data.flagged;
-  store.setting.subject.paperUrl = res.data.paperUrl
-    ? fileServer.value + res.data.paperUrl
-    : "";
-  if (res.data.task?.studentId) {
-    let rawTask = res.data.task;
-    store.currentTask = addFileServerPrefixToTask(rawTask);
-  } else {
-    store.message = res.data.message;
-  }
-}
-let isCurrentTagged = $ref(false);
-
-// const isCurrentTagged = $computed(() => tagIds.includes(currentStudentId));
-const isFirst = $computed(() => studentIds.indexOf(currentStudentId) === 0);
-const isLast = $computed(
-  () => studentIds.indexOf(currentStudentId) === studentIds.length - 1
-);
-
-async function fetchTask(next: boolean, init?: boolean) {
-  if (init) {
-    currentStudentId = studentIds[0];
-  } else if (isLast && next) {
-    return; // currentStudentId是最后一个不调用
-  } else if (isFirst && !next) {
-    return; // currentStudentId是第一个不调用
-  } else {
-    currentStudentId =
-      studentIds[studentIds.indexOf(currentStudentId) + (next ? 1 : -1)];
-  }
-  if (!currentStudentId) return; // 无currentStudentId不调用
-  store.status.totalCount = studentIds.length;
-  // store.status.markedCount = studentIds.indexOf(currentStudentId) + 1;
-  await updateTask();
-  if (!store.status.markedCountStuIds) {
-    store.status.markedCountStuIds = [currentStudentId];
-  } else {
-    store.status.markedCountStuIds = Array.from(
-      new Set([...store.status.markedCountStuIds, currentStudentId])
-    );
-  }
-  store.status.markedCount = store.status.markedCountStuIds.length;
-}
-onMounted(async () => {
-  fileServer.value = await updateSetting();
-  await fetchTask(true, true);
-});
-
-const saveTaskToServer = async () => {
-  const mkey = "save_task_key";
-  void message.loading({ content: "标记评卷任务...", key: mkey });
-  const res = await saveInspectedTaskOfImportInspect(
-    currentStudentId + "",
-    !isCurrentTagged + ""
-  );
-  if (res.data.success) {
-    void message.success({
-      content: isCurrentTagged ? "取消标记成功" : "标记成功",
-      key: mkey,
-      duration: 2,
-    });
-    isCurrentTagged = !isCurrentTagged;
-    // if (isCurrentTagged) {
-    //   tagIds.splice(tagIds.indexOf(currentStudentId), 1);
-    // } else {
-    //   tagIds.push(currentStudentId);
-    // }
-  } else {
-    console.log(res.data.message);
-    void message.error({ content: res.data.message, key: mkey, duration: 10 });
-  }
-};
-
-const renderError = () => {
-  store.currentTask = undefined;
-  store.message = "加载失败,请重新加载。";
-};
-</script>
-
-<style scoped>
-.my-container {
-  width: 100%;
-  overflow: clip;
-}
-</style>

+ 0 - 309
src/features/student/scoreVerify/markBody.vue

@@ -1,309 +0,0 @@
-<template>
-  <div
-    ref="dragContainer"
-    class="mark-body-container tw-flex-auto tw-p-2 tw-pt-0"
-    @scroll="viewScroll"
-  >
-    <div v-if="!store.currentTask" class="tw-text-center none-tip">
-      {{ store.message }}
-    </div>
-    <div
-      v-else-if="!sliceImagesWithTrackList.length"
-      class="tw-text-center none-tip"
-      style="color: red"
-    >
-      考生答卷未上传
-    </div>
-    <div v-else :style="{ width: answerPaperScale }" class="tw-pt-2">
-      <div
-        v-for="(item, index) in sliceImagesWithTrackList"
-        :key="index"
-        class="single-image-container"
-        :style="{
-          width: item.width,
-        }"
-      >
-        <img :src="item.url" draggable="false" />
-        <MarkDrawTrack
-          :trackList="item.trackList"
-          :specialTagList="item.tagList"
-          :sliceImageHeight="item.originalImageHeight"
-          :sliceImageWidth="item.originalImageWidth"
-          :dx="0"
-          :dy="0"
-        />
-        <hr class="image-seperator" />
-      </div>
-    </div>
-    <ZoomPaper v-if="store.isScanImage && sliceImagesWithTrackList.length" />
-  </div>
-</template>
-
-<script setup lang="ts">
-import { reactive, watch } from "vue";
-import { store } from "@/store/app";
-import MarkDrawTrack from "@/features/mark/MarkDrawTrack.vue";
-import type { SpecialTag, Track, ColorMap } from "@/types";
-import { useTimers } from "@/setups/useTimers";
-import { loadImage, addHeaderTrackColorAttr } from "@/utils/utils";
-import useDraggable from "@/features/mark/composables/useDraggable";
-import ZoomPaper from "@/components/ZoomPaper.vue";
-
-interface SliceImage {
-  url: string;
-  trackList: Array<Track>;
-  tagList: Array<SpecialTag>;
-  originalImageWidth: number;
-  originalImageHeight: number;
-  width: string; // 图片在整个图片列表里面的宽度比例
-}
-
-const { origImageUrls = "sliceUrls" } = defineProps<{
-  origImageUrls?: "sheetUrls" | "sliceUrls";
-}>();
-const emit = defineEmits(["error", "getIsMultComments", "getScrollStatus"]);
-
-const { dragContainer } = useDraggable();
-const viewScroll = () => {
-  if (
-    dragContainer.value.scrollTop + dragContainer.value.offsetHeight + 50 >=
-    dragContainer.value.scrollHeight
-  ) {
-    emit("getScrollStatus");
-  }
-};
-const { addTimeout } = useTimers();
-
-let sliceImagesWithTrackList: SliceImage[] = reactive([]);
-let maxImageWidth = 0;
-
-function addTrackColorAttr(tList: Track[]): Track[] {
-  let markerIds: (number | undefined)[] = tList
-    .map((v) => v.markerId)
-    .filter((x) => !!x);
-  markerIds = Array.from(new Set(markerIds));
-  // markerIds.sort();
-  let colorMap: ColorMap = {};
-  for (let i = 0; i < markerIds.length; i++) {
-    const mId: any = markerIds[i];
-    if (i == 0) {
-      colorMap[mId + ""] = "red";
-    } else if (i == 1) {
-      colorMap[mId + ""] = "blue";
-    } else if (i > 1) {
-      colorMap[mId + ""] = "gray";
-    }
-  }
-  if (Object.keys(colorMap).length > 1) {
-    emit("getIsMultComments", true);
-  }
-  tList = tList.map((item: Track) => {
-    item.color = colorMap[item.markerId + ""] || "gray";
-    item.isByMultMark = markerIds.length > 1;
-    return item;
-  });
-  return tList;
-}
-
-function addTagColorAttr(tList: SpecialTag[]): SpecialTag[] {
-  let markerIds: (number | undefined)[] = tList
-    .map((v) => v.markerId)
-    .filter((x) => !!x);
-  markerIds = Array.from(new Set(markerIds));
-  // markerIds.sort();
-  let colorMap: ColorMap = {};
-  for (let i = 0; i < markerIds.length; i++) {
-    const mId: any = markerIds[i];
-    if (i == 0) {
-      colorMap[mId + ""] = "red";
-    } else if (i == 1) {
-      colorMap[mId + ""] = "blue";
-    } else if (i > 1) {
-      colorMap[mId + ""] = "gray";
-    }
-  }
-  tList = tList.map((item: SpecialTag) => {
-    item.color = colorMap[item.markerId + ""] || "gray";
-    item.isByMultMark = markerIds.length > 1;
-    return item;
-  });
-  return tList;
-}
-
-async function processImage() {
-  if (!store.currentTask) return;
-
-  const images = [];
-  const urls = store.currentTask[origImageUrls] || [];
-  for (const url of urls) {
-    const image = await loadImage(url);
-    images.push(image);
-  }
-
-  maxImageWidth = Math.max(...images.map((i) => i.naturalWidth));
-
-  for (const url of urls) {
-    const indexInSliceUrls = urls.indexOf(url) + 1;
-    const image = images[indexInSliceUrls - 1];
-
-    const trackLists = (store.currentTask.questionList || [])
-      // .map((q) => q.trackList)
-      .map((q) => {
-        let tList = q.trackList;
-
-        return q.headerTrack?.length
-          ? addHeaderTrackColorAttr(q.headerTrack)
-          : addTrackColorAttr(tList);
-      })
-      .flat();
-    const thisImageTrackList = trackLists.filter(
-      (t) => t.offsetIndex === indexInSliceUrls
-    );
-
-    const thisImageTagList = store.currentTask.headerTagList?.length
-      ? addHeaderTrackColorAttr(
-          (store.currentTask.headerTagList || []).filter(
-            (t) => t.offsetIndex === indexInSliceUrls
-          )
-        )
-      : addTagColorAttr(
-          (store.currentTask.specialTagList || []).filter(
-            (t) => t.offsetIndex === indexInSliceUrls
-          )
-        );
-
-    // const thisImageTagList = addTagColorAttr(
-    //   (store.currentTask.specialTagList || []).filter(
-    //     (t) => t.offsetIndex === indexInSliceUrls
-    //   )
-    // );
-
-    sliceImagesWithTrackList.push({
-      url,
-      trackList: thisImageTrackList,
-      tagList: thisImageTagList,
-      originalImageWidth: image.naturalWidth,
-      originalImageHeight: image.naturalHeight,
-      width: (image.naturalWidth / maxImageWidth) * 100 + "%",
-    });
-  }
-}
-
-// should not render twice at the same time
-let renderLock = false;
-const renderPaperAndMark = async () => {
-  if (renderLock) {
-    console.log("上个任务还未渲染完毕,稍等一秒再尝试渲染");
-    await new Promise((res) => setTimeout(res, 1000));
-    await renderPaperAndMark();
-    return;
-  }
-  renderLock = true;
-  sliceImagesWithTrackList.splice(0);
-
-  if (!store.currentTask) {
-    renderLock = false;
-    return;
-  }
-
-  try {
-    store.globalMask = true;
-    await processImage();
-  } catch (error) {
-    sliceImagesWithTrackList.splice(0);
-    console.log("render error ", error);
-    // 图片加载出错,自动加载下一个任务
-    emit("error");
-  } finally {
-    await new Promise((res) => setTimeout(res, 500));
-    store.globalMask = false;
-    renderLock = false;
-  }
-};
-
-watch(() => store.currentTask, renderPaperAndMark);
-
-watch(
-  (): (number | undefined)[] => [
-    store.minimapScrollToX,
-    store.minimapScrollToY,
-  ],
-  () => {
-    const container = document.querySelector<HTMLDivElement>(
-      ".mark-body-container"
-    );
-    addTimeout(() => {
-      if (
-        container &&
-        typeof store.minimapScrollToX === "number" &&
-        typeof store.minimapScrollToY === "number"
-      ) {
-        const { scrollWidth, scrollHeight } = container;
-        container.scrollTo({
-          top: scrollHeight * store.minimapScrollToY,
-          left: scrollWidth * store.minimapScrollToX,
-          behavior: "smooth",
-        });
-      }
-    }, 10);
-  }
-);
-
-const answerPaperScale = $computed(() => {
-  // 放大、缩小不影响页面之前的滚动条定位
-  let percentWidth = 0;
-  let percentTop = 0;
-  const container = document.querySelector(".mark-body-container");
-  if (container) {
-    const { scrollLeft, scrollTop, scrollWidth, scrollHeight } = container;
-    percentWidth = scrollLeft / scrollWidth;
-    percentTop = scrollTop / scrollHeight;
-  }
-
-  addTimeout(() => {
-    if (container) {
-      const { scrollWidth, scrollHeight } = container;
-      container.scrollTo({
-        left: scrollWidth * percentWidth,
-        top: scrollHeight * percentTop,
-      });
-    }
-  }, 10);
-  const scale = store.setting.uiSetting["answer.paper.scale"];
-  return scale * 100 + "%";
-});
-</script>
-
-<style scoped>
-.mark-body-container .none-tip {
-  height: 100%;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  font-size: 28px;
-}
-.mark-body-container {
-  height: calc(100vh - 56px);
-  overflow: auto;
-  background-color: var(--app-container-bg-color);
-  background-image: linear-gradient(45deg, #e0e0e0 25%, transparent 25%),
-    linear-gradient(-45deg, #e0e0e0 25%, transparent 25%),
-    linear-gradient(45deg, transparent 75%, #e0e0e0 75%),
-    linear-gradient(-45deg, transparent 75%, #e0e0e0 75%);
-  background-size: 20px 20px;
-  background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
-  transform: inherit;
-
-  cursor: grab;
-  user-select: none;
-}
-.mark-body-container img {
-  width: 100%;
-}
-.single-image-container {
-  position: relative;
-}
-.image-seperator {
-  border: 2px solid rgba(120, 120, 120, 0.1);
-}
-</style>

+ 0 - 351
src/features/student/studentInspect/MarkBoardInspect.vue

@@ -1,351 +0,0 @@
-<template>
-  <div
-    v-if="store.currentTask"
-    class="mark-board-track-container"
-    :class="[store.isScoreBoardCollapsed ? 'hide' : 'show']"
-  >
-    <div class="top-container tw-flex-shrink-0 tw-flex tw-items-center">
-      <div class="tw-flex tw-flex-col tw-flex-1 tw-text-center">
-        <div class="tw-flex tw-justify-center">
-          <img
-            src="../../mark/images/totalscore.png"
-            style="width: 13px; height: 16px"
-          />
-        </div>
-        <div>试卷总分</div>
-      </div>
-      <div class="tw-flex-1" style="font-size: 40px">
-        {{ markerScore > 0 ? markerScore : 0 }}
-      </div>
-    </div>
-
-    <div v-if="groups" class="question-list tw-flex-grow tw-overflow-auto">
-      <template
-        v-for="({ groupNumber, selectiveIndex }, index) in groups"
-        :key="index"
-      >
-        <div class="question-item tw-mb-4 tw-bg-white tw-p-4 tw-pl-5 tw-pr-3">
-          <div v-if="selectiveIndex" class="question-item-select-group">
-            {{ selectiveIndex }}
-          </div>
-          <div
-            class="tw-flex tw-justify-between tw-place-items-center hover:tw-bg-gray-200"
-            @mouseover="willAddFocusTrack(groupNumber, undefined, undefined)"
-            @mouseleave="removeFocusTrack"
-          >
-            <span class="secondary-text">分组 {{ groupNumber }}</span>
-            <input
-              class="tw-my-auto"
-              title="打回"
-              type="checkbox"
-              :checked="groupChecked(groupNumber)"
-              @click="groupClicked(groupNumber)"
-            />
-          </div>
-          <div v-if="questions">
-            <template v-for="(question, index2) in questions" :key="index2">
-              <div
-                v-if="question.groupNumber === groupNumber"
-                class="question tw-flex tw-place-items-center tw-mb-1 tw-font-bold hover:tw-bg-gray-200"
-                :class="{
-                  uncalculate: question.uncalculate,
-                  'is-rejected': question.rejected,
-                }"
-                @mouseover="
-                  willAddFocusTrack(
-                    undefined,
-                    question.mainNumber,
-                    question.subNumber
-                  )
-                "
-                @mouseleave="removeFocusTrack"
-              >
-                <a-tooltip placement="left">
-                  <template #title>
-                    <span>未计入总分</span>
-                  </template>
-                  <MinusCircleFilled class="uncalculate-icon" />
-                </a-tooltip>
-                <span class="question-title">
-                  {{ question.title }} {{ question.mainNumber }}-{{
-                    question.subNumber
-                  }}
-                </span>
-                <span class="tw-text-center question-score">
-                  {{ question.score === -1 ? "未选做" : question.score || 0 }}
-                </span>
-                <input
-                  :disabled="question.score === -1"
-                  title="打回"
-                  type="checkbox"
-                  :checked="questionChecked(question)"
-                  @change="questionCheckChanged(question)"
-                />
-              </div>
-            </template>
-          </div>
-        </div>
-      </template>
-    </div>
-
-    <div class="tw-flex tw-flex-shrink-0 tw-justify-center">
-      <qm-button
-        v-if="
-          store.currentTask.inspectTime && store.currentTask.inspectTime > 0
-        "
-        type="primary"
-        class="full-width-btn undo-btn"
-        @click="reject"
-      >
-        打回
-      </qm-button>
-      <template v-else-if="checkedQuestions.length === 0">
-        <qm-button
-          v-if="hasScrollToBottom"
-          type="primary"
-          class="full-width-btn"
-          @click="inspect"
-        >
-          复核
-        </qm-button>
-        <a-tooltip v-else placement="top">
-          <template #title>请先浏览至试卷底部</template>
-          <div style="width: 100%">
-            <a-button
-              disabled
-              class="full-width-btn"
-              style="display: block; width: 100%"
-            >
-              复核
-            </a-button>
-          </div>
-        </a-tooltip>
-      </template>
-
-      <qm-button
-        v-else
-        type="primary"
-        class="full-width-btn undo-btn"
-        @click="reject"
-        >打回</qm-button
-      >
-    </div>
-  </div>
-  <review-return-dialog
-    v-model:visible="reviewReturnVisible"
-    @confirmReturn="onConfirmReturn"
-  />
-</template>
-
-<script setup lang="ts">
-import type { Question } from "@/types";
-import { message } from "ant-design-vue";
-import { MinusCircleFilled } from "@ant-design/icons-vue";
-import { reactive, watch } from "vue";
-import { store } from "@/store/app";
-import useFocusTracks from "@/features/mark/composables/useFocusTracks";
-import ReviewReturnDialog from "@/features/library/inspect/ReviewReturnDialog.vue";
-
-const { addFocusTrack, removeFocusTrack } = useFocusTracks();
-
-const willAddFocusTrack = (
-  groupNumber: number | undefined,
-  mainNumber: number | undefined,
-  subNumber: string | undefined
-) => {
-  addFocusTrack(groupNumber, mainNumber, subNumber);
-};
-
-const { hasScrollToBottom } = defineProps<{
-  hasScrollToBottom: boolean;
-}>();
-const emit = defineEmits(["inspect", "reject"]);
-let checkedQuestions: Question[] = reactive([]);
-let reviewReturnVisible = $ref(false);
-watch(
-  () => store.currentTask,
-  () => {
-    checkedQuestions.splice(0);
-  }
-);
-const groups = $computed(() => {
-  const gs = store.currentTaskEnsured.questionList.reduce((gs, q) => {
-    if (!gs[q.groupNumber]) {
-      gs[q.groupNumber] = {
-        groupNumber: q.groupNumber,
-        selectiveIndex: q.selectiveIndex,
-      };
-    }
-    return gs;
-  }, {} as Record<number, { groupNumber: number; selectiveIndex: number | null }>);
-  return Object.values(gs);
-});
-
-const questions = $computed(() => {
-  const qs = store.currentTaskEnsured.questionList;
-  return qs;
-});
-
-const markerScore = $computed(() => store.currentTaskEnsured.markerScore || 0);
-
-function addToCheckedQuestion(question: Question) {
-  checkedQuestions.push(question);
-}
-function removeCheckedQuestion(question: Question) {
-  const idx = checkedQuestions.indexOf(question);
-  checkedQuestions.splice(idx, 1);
-}
-function groupChecked(groupNumber: number) {
-  return (
-    checkedQuestions.filter((q) => q.groupNumber === groupNumber).length ===
-    questions?.filter((q) => q.groupNumber === groupNumber).length
-  );
-}
-
-function questionChecked(question: Question) {
-  return checkedQuestions.includes(question);
-}
-
-function questionCheckChanged(question: Question) {
-  const checked = questionChecked(question);
-  if (checked) {
-    removeCheckedQuestion(question);
-  } else {
-    addToCheckedQuestion(question);
-  }
-}
-
-function groupClicked(groupNumber: number) {
-  if (groupChecked(groupNumber)) {
-    checkedQuestions
-      .filter((q) => q.groupNumber === groupNumber)
-      .forEach((q) => {
-        const idx = checkedQuestions.indexOf(q);
-        checkedQuestions.splice(idx, 1);
-      });
-  } else {
-    questions
-      ?.filter((q) => q.groupNumber === groupNumber)
-      .forEach((q) => {
-        if (!questionChecked(q)) checkedQuestions.push(q);
-      });
-  }
-}
-
-function reject() {
-  if (checkedQuestions.length === 0) {
-    void message.warn({ content: "请先选择试题。" });
-    return;
-  }
-  reviewReturnVisible = true;
-  // emit("reject", checkedQuestions);
-}
-
-function inspect() {
-  emit("inspect");
-}
-
-function onConfirmReturn(reason: string) {
-  emit("reject", { questions: checkedQuestions, reason });
-}
-</script>
-
-<style lang="less" scoped>
-.mark-board-track-container {
-  display: flex;
-  flex-direction: column;
-  max-width: 290px;
-  min-width: 290px;
-  max-height: calc(100vh - 56px);
-  z-index: 1001;
-  transition: margin-right 0.5s;
-  color: var(--app-small-header-text-color);
-  padding-bottom: 20px;
-}
-.mark-board-track-container.show {
-  margin-right: 0;
-}
-.mark-board-track-container.hide {
-  margin-right: -290px;
-}
-
-.top-container {
-  background-color: var(--app-container-bg-color);
-  height: 86px;
-  border-radius: 5px;
-  margin: 20px;
-  margin-bottom: 0;
-  color: white;
-  background-color: var(--app-primary-button-bg-color);
-}
-
-.question-list {
-  padding: 20px;
-}
-
-.question-item {
-  border-radius: 5px;
-  position: relative;
-}
-
-/** 选做题分组 */
-.question-item-select-group {
-  position: absolute;
-  left: -12px;
-  top: -12px;
-  width: 24px;
-  height: 24px;
-  background: #d3d9e6;
-  border-radius: 12px 12px 4px 12px;
-  color: #435488;
-  text-align: center;
-  line-height: 24px;
-  font-size: 12px;
-  font-weight: bold;
-}
-
-.total-score {
-  color: var(--app-main-text-color);
-  font-size: 32px;
-}
-.question {
-  min-width: 80px;
-  background-color: var(--app-container-bg-color);
-  &.is-rejected {
-    background-color: yellow;
-  }
-}
-.question-title {
-  flex: 1;
-}
-.question-score {
-  flex-basis: 56px;
-  padding: 0 3px;
-}
-
-.question.uncalculate {
-  position: relative;
-}
-
-.question .uncalculate-icon {
-  display: none;
-  color: red;
-  position: absolute;
-  font-size: 15px;
-  left: -16px;
-  top: 0.3em;
-}
-
-.question.uncalculate .uncalculate-icon {
-  display: block;
-}
-
-.full-width-btn {
-  width: 100%;
-  border-radius: 20px;
-}
-.undo-btn {
-  background-color: var(--app-undo-button-bg-color);
-  border-color: var(--app-undo-button-bg-color);
-}
-</style>

+ 0 - 1051
src/features/student/studentInspect/MarkBody.vue

@@ -1,1051 +0,0 @@
-<template>
-  <div class="mark-body" @scroll="viewScroll">
-    <div ref="dragContainer" class="mark-body-container">
-      <div v-if="!markStore.currentTask" class="mark-body-none">
-        <div>
-          <img src="@/assets/image-none-task.png" />
-          <p>
-            {{ markStore.message }}
-          </p>
-        </div>
-      </div>
-      <div
-        v-else-if="markStore.isScanImage"
-        :style="{ width: answerPaperScale }"
-      >
-        <div
-          v-for="(item, index) in sliceImagesWithTrackList"
-          :key="index"
-          class="single-image-container"
-        >
-          <img :src="item.url" draggable="false" />
-          <MarkDrawTrack
-            :trackList="item.trackList"
-            :specialTagList="item.tagList"
-            :sliceImageHeight="item.originalImageHeight"
-            :sliceImageWidth="item.originalImageWidth"
-            :dx="0"
-            :dy="0"
-          />
-          <template v-if="!onlyTrack">
-            <!-- 客观题答案标记 -->
-            <template v-if="item.answerTags">
-              <div
-                v-for="(tag, tindex) in item.answerTags"
-                :key="`tag-${tindex}`"
-                :style="tag.style"
-              >
-                {{ tag.answer }}
-              </div>
-            </template>
-            <!-- 试题评分明细 -->
-            <template v-if="item.markDetail">
-              <div
-                v-for="(minfo, mindex) in item.markDetail"
-                :key="`mark-${mindex}`"
-                :style="minfo.style"
-                class="mark-info"
-              >
-                <div v-if="minfo.isFillQuestion">
-                  <div
-                    v-for="user in minfo.users"
-                    :key="user.userId"
-                    :style="{ color: user.color }"
-                  >
-                    <p>{{ user.prename }}:{{ user.userName }},评分:</p>
-                    <p>
-                      {{
-                        user.scores
-                          .map((s) => `${s.subNumber}:${s.score}分`)
-                          .join(",")
-                      }}
-                    </p>
-                  </div>
-                </div>
-                <div v-else>
-                  <p
-                    v-for="user in minfo.users"
-                    :key="user.userId"
-                    :style="{ color: user.color }"
-                  >
-                    {{ user.prename }}:{{ user.userName }},评分:{{
-                      user.score
-                    }}
-                  </p>
-                </div>
-                <h3>得分:{{ minfo.score }},满分:{{ minfo.maxScore }}</h3>
-              </div>
-            </template>
-            <!-- 客观题 -->
-            <template v-if="item.objectiveAnswerTags">
-              <div
-                v-for="tag in item.objectiveAnswerTags"
-                :key="tag.id"
-                class="mark-objective"
-                :style="tag.style"
-              >
-                得分:{{ tag.score }},满分:{{ tag.totalScore }}
-              </div>
-            </template>
-            <!-- 模式4的summary -->
-            <template v-if="item.summarys && item.summarys.length">
-              <div class="summary-detail">
-                <table>
-                  <tr>
-                    <th>主观题号</th>
-                    <th>分数</th>
-                    <th>评卷员</th>
-                  </tr>
-                  <tr v-for="(sinfo, sindex) in item.summarys" :key="sindex">
-                    <td>{{ sinfo.mainNumber }}-{{ sinfo.subNumber }}</td>
-                    <td>{{ sinfo.score }}</td>
-                    <td>{{ sinfo.markerName }}</td>
-                  </tr>
-                </table>
-              </div>
-            </template>
-
-            <!-- 总分 -->
-            <div class="mark-total">
-              总分:{{ totalScore }},主观题得分:{{
-                subjectiveScore
-              }},客观题得分:{{ objectiveScore }}
-            </div>
-          </template>
-          <hr class="image-seperator" />
-        </div>
-      </div>
-      <div v-else>未知数据</div>
-
-      <div v-if="!sliceImagesWithTrackList.length" class="mark-body-none">
-        <div>
-          <img src="@/assets/image-none-task.png" />
-          <p>
-            {{ markStore.message }}
-          </p>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { reactive, watch } from "vue";
-import { useMarkStore } from "@/store";
-import MarkDrawTrack from "@/features/mark/MarkDrawTrack.vue";
-import type {
-  SpecialTag,
-  Track,
-  ColorMap,
-  PaperRecogData,
-  Question,
-} from "@/types";
-import { useTimers } from "@/setups/useTimers";
-import {
-  loadImage,
-  addHeaderTrackColorAttr,
-  calcSumPrecision,
-  maxNum,
-  toPrecision,
-} from "@/utils/utils";
-import useDraggable from "@/features/mark/composables/useDraggable";
-
-interface SliceImage {
-  url: string;
-  trackList: Array<Track>;
-  tagList: Array<SpecialTag>;
-  originalImageWidth: number;
-  originalImageHeight: number;
-  width: string; // 图片在整个图片列表里面的宽度比例
-  answerTags?: AnswerTagItem[];
-  markDetail?: MarkDetailItem[];
-  objectiveAnswerTags?: ObjectiveAnswerTagItem[];
-  summarys?: SummaryItem[];
-}
-
-const { origImageUrls = "sliceUrls", onlyTrack = false } = defineProps<{
-  origImageUrls?: "sheetUrls" | "sliceUrls";
-  onlyTrack?: boolean;
-}>();
-const emit = defineEmits(["error", "getIsMultComments", "getScrollStatus"]);
-
-const { dragContainer } = useDraggable();
-const markStore = useMarkStore();
-
-const viewScroll = () => {
-  if (
-    dragContainer.value.scrollTop + dragContainer.value.offsetHeight + 50 >=
-    dragContainer.value.scrollHeight
-  ) {
-    emit("getScrollStatus");
-  }
-};
-const { addTimeout } = useTimers();
-
-const totalScore = $computed(() => {
-  return markStore.currentTask?.markerScore || 0;
-});
-const objectiveScore = $computed(() => {
-  return markStore.currentTask?.objectiveScore || 0;
-});
-const subjectiveScore = $computed(() => {
-  return toPrecision(totalScore - objectiveScore);
-});
-
-let sliceImagesWithTrackList: SliceImage[] = reactive([]);
-let maxImageWidth = 0;
-
-function addTrackColorAttr(tList: Track[]): Track[] {
-  let markerIds: (number | undefined)[] = tList
-    .map((v) => v.userId)
-    .filter((x) => !!x);
-  markerIds = Array.from(new Set(markerIds));
-  // markerIds.sort();
-  let colorMap: ColorMap = {};
-  for (let i = 0; i < markerIds.length; i++) {
-    const mId: any = markerIds[i];
-    if (i == 0) {
-      colorMap[mId + ""] = "red";
-    } else if (i == 1) {
-      colorMap[mId + ""] = "blue";
-    } else if (i > 1) {
-      colorMap[mId + ""] = "gray";
-    }
-  }
-  if (Object.keys(colorMap).length > 1) {
-    emit("getIsMultComments", true);
-  }
-  tList = tList.map((item: Track) => {
-    item.color = colorMap[item.userId + ""] || "red";
-    item.isByMultMark = markerIds.length > 1;
-    return item;
-  });
-  return tList;
-}
-
-function addTagColorAttr(tList: SpecialTag[]): SpecialTag[] {
-  let markerIds: (number | undefined)[] = tList
-    .map((v) => v.userId)
-    .filter((x) => !!x);
-  markerIds = Array.from(new Set(markerIds));
-  // markerIds.sort();
-  let colorMap: ColorMap = {};
-  for (let i = 0; i < markerIds.length; i++) {
-    const mId: any = markerIds[i];
-    if (i == 0) {
-      colorMap[mId + ""] = "red";
-    } else if (i == 1) {
-      colorMap[mId + ""] = "blue";
-    } else if (i > 1) {
-      colorMap[mId + ""] = "gray";
-    }
-  }
-  tList = tList.map((item: SpecialTag) => {
-    item.color = colorMap[item.userId + ""] || "red";
-    item.isByMultMark = markerIds.length > 1;
-    return item;
-  });
-  return tList;
-}
-
-async function processImage() {
-  if (!markStore.currentTask) return;
-
-  const images = [];
-  const urls = markStore.currentTask[origImageUrls] || [];
-  if (!urls.length) return;
-  for (const url of urls) {
-    const image = await loadImage(url);
-    images.push(image);
-  }
-
-  maxImageWidth = Math.max(...images.map((i) => i.naturalWidth));
-
-  const trackLists = (markStore.currentTask.questionList || [])
-    // .map((q) => q.trackList)
-    .map((q) => {
-      let tList = q.trackList;
-      return q.headerTrack?.length
-        ? addHeaderTrackColorAttr(q.headerTrack)
-        : addTrackColorAttr(tList);
-    })
-    .flat();
-
-  // 解析各试题答题区域以及评分
-  const markDetailList = parseMarkDetailList();
-  // 解析客观题的得分情况,按大题统计
-  const objectiveAnswerTagList = parseObjectiveAnswerTags();
-
-  for (const url of urls) {
-    const indexInSliceUrls = urls.indexOf(url) + 1;
-    const image = images[indexInSliceUrls - 1];
-
-    const thisImageTrackList = trackLists.filter(
-      (t) => t.offsetIndex === indexInSliceUrls
-    );
-    const thisImageTagList = markStore.currentTask.headerTagList?.length
-      ? addHeaderTrackColorAttr(
-          (markStore.currentTask.headerTagList || []).filter(
-            (t) => t.offsetIndex === indexInSliceUrls
-          )
-        )
-      : addTagColorAttr(
-          (markStore.currentTask.specialTagList || []).filter(
-            (t) => t.offsetIndex === indexInSliceUrls
-          )
-        );
-    const answerTags = paserRecogData(image, indexInSliceUrls - 1);
-
-    sliceImagesWithTrackList.push({
-      url,
-      trackList: thisImageTrackList,
-      tagList: thisImageTagList,
-      originalImageWidth: image.naturalWidth,
-      originalImageHeight: image.naturalHeight,
-      width: (image.naturalWidth / maxImageWidth) * 100 + "%",
-      answerTags,
-      markDetail: markDetailList[indexInSliceUrls - 1],
-      objectiveAnswerTags: objectiveAnswerTagList[indexInSliceUrls - 1],
-    });
-  }
-
-  // 无答题卡,模式4
-  if (!markStore.currentTask.cardData?.length) {
-    const summarys = parseMode4Data();
-    if (summarys && summarys.length) {
-      sliceImagesWithTrackList[0].summarys = summarys;
-    }
-  }
-}
-
-// 解析客观题答案展示位置
-interface AnswerTagItem {
-  mainNumber: number;
-  subNumber: string;
-  answer: string;
-  style: Record<string, string>;
-}
-function paserRecogData(imgDom: HTMLImageElement, imageIndex): AnswerTagItem[] {
-  if (
-    !markStore.currentTask.recogDatas?.length ||
-    !markStore.currentTask.recogDatas[imageIndex]
-  )
-    return [];
-
-  const answerMap = markStore.currentTask.answerMap || {};
-  const { naturalWidth, naturalHeight } = imgDom;
-  const recogData: PaperRecogData = JSON.parse(
-    window.atob(markStore.currentTask.recogDatas[imageIndex])
-  );
-  const answerTags: AnswerTagItem[] = [];
-  // const optionsBlocks = [];
-  recogData.question.forEach((question) => {
-    question.fill_result.forEach((result) => {
-      const tagSize = result.fill_size[1];
-      const fillPositions = result.fill_position.map((pos) => {
-        return pos.split(",").map((n) => n * 1);
-      });
-
-      const offsetLt = result.fill_size.map((item) => item * 0.4);
-      const tagLeft =
-        maxNum(fillPositions.map((pos) => pos[0])) +
-        result.fill_size[0] -
-        offsetLt[0];
-      const tagTop = fillPositions[0][1] - offsetLt[1];
-
-      const { answer, isRight } =
-        answerMap[`${result.main_number}_${result.sub_number}`] || {};
-
-      answerTags.push({
-        mainNumber: result.main_number,
-        subNumber: result.sub_number,
-        answer,
-        style: {
-          height: ((100 * tagSize) / naturalHeight).toFixed(4) + "%",
-          fontSize: ((100 * 20) / tagSize).toFixed(4) + "%",
-          left: ((100 * tagLeft) / naturalWidth).toFixed(4) + "%",
-          top: ((100 * tagTop) / naturalHeight).toFixed(4) + "%",
-          position: "absolute",
-          color: isRight ? "#05b575" : "#f53f3f",
-          fontWeight: 600,
-          lineHeight: 1,
-          zIndex: 9,
-        },
-      });
-
-      // 测试:选项框
-      // fillPositions.forEach((fp, index) => {
-      //   optionsBlocks.push({
-      //     mainNumber: result.main_number,
-      //     subNumber: result.sub_number,
-      //     filled: !!result.fill_option[index],
-      //     style: {
-      //       width:
-      //         ((100 * result.fill_size[0]) / naturalWidth).toFixed(4) + "%",
-      //       height:
-      //         ((100 * result.fill_size[1]) / naturalHeight).toFixed(4) + "%",
-      //       left:
-      //         ((100 * (fp[0] - offsetLt[0])) / naturalWidth).toFixed(4) + "%",
-      //       top:
-      //         ((100 * (fp[1] - offsetLt[1])) / naturalHeight).toFixed(4) + "%",
-      //       position: "absolute",
-      //       border: "1px solid #f53f3f",
-      //       background: result.fill_option[index]
-      //         ? "rgba(245, 63, 63, 0.5)"
-      //         : "transparent",
-      //       zIndex: 9,
-      //     },
-      //   });
-      // });
-    });
-  });
-
-  return answerTags;
-}
-
-interface QuestionItem {
-  mainNumber: number;
-  subNumber: number | string;
-}
-interface QuestionArea {
-  i: number;
-  x: number;
-  y: number;
-  w: number;
-  h: number;
-  qStruct: string;
-}
-function parseQuestionAreas(questions: QuestionItem[]) {
-  if (!questions.length || !markStore.currentTask.cardData?.length) return [];
-
-  let pictureConfigs: QuestionArea[] = [];
-  const structs = questions.map(
-    (item) => `${item.mainNumber}_${item.subNumber}`
-  );
-  markStore.currentTask.cardData.forEach((page, pindex) => {
-    page.exchange.answer_area.forEach((area) => {
-      const [x, y, w, h] = area.area;
-      const qStruct = `${area.main_number}_${area.sub_number}`;
-
-      const pConfig: QuestionArea = {
-        i: pindex + 1,
-        x,
-        y,
-        w,
-        h,
-        qStruct,
-      };
-
-      if (typeof area.sub_number === "number") {
-        if (!structs.includes(qStruct)) return;
-        pictureConfigs.push(pConfig);
-        return;
-      }
-      // 复合区域处理,比如填空题,多个小题合并为一个区域
-      if (typeof area.sub_number === "string") {
-        const areaStructs = area.sub_number
-          .split(",")
-          .map((subNumber) => `${area.main_number}_${subNumber}`);
-        if (
-          structs.some((struct) => areaStructs.includes(struct)) &&
-          !pictureConfigs.find((item) => item.qStruct === qStruct)
-        ) {
-          pictureConfigs.push(pConfig);
-        }
-      }
-    });
-  });
-  // console.log(pictureConfigs);
-
-  // 合并相邻区域
-  pictureConfigs.sort((a, b) => {
-    return a.i - b.i || a.x - b.x || a.y - b.y;
-  });
-  let combinePictureConfigList: QuestionArea[] = [];
-  let prevConfig = null;
-  pictureConfigs.forEach((item, index) => {
-    if (!index) {
-      prevConfig = { ...item };
-      combinePictureConfigList.push(prevConfig);
-      return;
-    }
-
-    const elasticRate = 0.01;
-    if (
-      prevConfig.i === item.i &&
-      prevConfig.y + prevConfig.h + elasticRate >= item.y &&
-      prevConfig.w === item.w &&
-      prevConfig.x === item.x
-    ) {
-      prevConfig.h = item.y + item.h - prevConfig.y;
-    } else {
-      prevConfig = { ...item };
-      combinePictureConfigList.push(prevConfig);
-    }
-  });
-  // console.log(combinePictureConfigList);
-  return combinePictureConfigList;
-}
-
-// 获取属于填空题的试题号
-function getFillLines() {
-  if (!markStore.currentTask.cardData?.length) return {};
-
-  const questions: Record<number, string[]> = {};
-  markStore.currentTask.cardData.forEach((page) => {
-    page.columns.forEach((column) => {
-      column.elements.forEach((element) => {
-        if (element.type !== "FILL_LINE") return;
-
-        if (!questions[element.topicNo]) questions[element.topicNo] = [];
-
-        for (let i = 0; i < element.questionsCount; i++) {
-          questions[element.topicNo].push(
-            `${element.topicNo}_${element.startNumber + i}`
-          );
-        }
-      });
-    });
-  });
-  return questions;
-}
-
-// 解析各试题答题区域以及评分
-interface MarkDetailUserItem {
-  userId: string;
-  userName: string;
-  prename: string;
-  color: string;
-  scores: Array<{ subNumber: string; score: number }>;
-  score: number;
-}
-type UserMapType = Record<string, MarkDetailUserItem>;
-interface MarkDetailItem {
-  mainNumber: number;
-  subNumber: string;
-  isFillQuestion: boolean;
-  score: number;
-  maxScore: number;
-  users: MarkDetailUserItem[];
-  area: QuestionArea;
-  style: Record<string, string>;
-}
-
-function parseMarkDetailList(): Array<MarkDetailItem[]> {
-  const dataList: Array<MarkDetailItem[]> = [];
-  const questions = markStore.currentTask.questionList || [];
-
-  const fillQues = getFillLines();
-  let fillQuestions = [] as Question[];
-  let otherQuestions = questions;
-  if (Object.keys(fillQues).length) {
-    const fillQNos = Object.values(fillQues).flat();
-    fillQuestions = questions.filter((q) =>
-      fillQNos.includes(`${q.mainNumber}_${q.subNumber}`)
-    );
-    otherQuestions = questions.filter(
-      (q) => !fillQNos.includes(`${q.mainNumber}_${q.subNumber}`)
-    );
-  }
-
-  // 填空题:合并所有小题为一个区域
-  Object.values(fillQues).forEach((qnos) => {
-    const groupQuestions = fillQuestions.filter((q) =>
-      qnos.includes(`${q.mainNumber}_${q.subNumber}`)
-    );
-    const areas = parseQuestionAreas(groupQuestions);
-    if (!areas.length) return;
-    const area = { ...areas[0] };
-    const imgIndex = area.i - 1;
-    if (!dataList[imgIndex]) {
-      dataList[imgIndex] = [];
-    }
-
-    const userMap: UserMapType = {};
-    // 大题分成两个部分给两个人评 与 大题被两人同时评 是不一样的
-    const isDoubleMark = !groupQuestions.some((question) => {
-      let userIds = question.trackList.map((track) => track.userId);
-      if (
-        !userIds.length &&
-        question.markerList &&
-        question.markerList.length
-      ) {
-        userIds = question.markerList
-          .filter((marker) => !marker.header)
-          .map((marker) => marker.userId);
-      }
-      const uids = new Set(userIds);
-      return uids.size === 1;
-    });
-    groupQuestions.forEach((question) => {
-      question.trackList.forEach((track) => {
-        if (!userMap[track.userId]) {
-          userMap[track.userId] = {
-            userId: track.userId,
-            userName: track.userName,
-            color: track.color || "red",
-            prename: "",
-            scores: [],
-            score: 0,
-          };
-        }
-        const existUserScore = userMap[track.userId].scores.find(
-          (s) => s.subNumber === track.subNumber
-        );
-        if (existUserScore) {
-          existUserScore.score += track.score;
-        } else {
-          userMap[track.userId].scores.push({
-            score: track.score,
-            subNumber: track.subNumber,
-          });
-        }
-      });
-
-      // 普通模式没有轨迹
-      if (
-        !question.trackList.length &&
-        question.markerList &&
-        question.markerList.length
-      ) {
-        question.markerList
-          .filter((marker) => !marker.header)
-          .forEach((marker) => {
-            if (!userMap[marker.userId]) {
-              userMap[marker.userId] = {
-                userId: marker.userId,
-                userName: marker.userName,
-                color: marker.header ? "green" : "red",
-                prename: "",
-                scores: [],
-                score: 0,
-              };
-            }
-            userMap[marker.userId].scores.push({
-              score: marker.score,
-              subNumber: question.subNumber,
-            });
-          });
-      }
-    });
-
-    const users = Object.values(userMap).map((user, index) => {
-      const zhs = ["一", "二", "三"];
-      const prename = isDoubleMark ? `${zhs[index] || ""}评` : "评卷员";
-      return {
-        ...user,
-        prename,
-        score: calcSumPrecision(user.scores.map((s) => s.score)),
-      };
-    });
-
-    const score = calcSumPrecision(
-      groupQuestions.map((item) => item.score || 0)
-    );
-    const maxScore = calcSumPrecision(
-      groupQuestions.map((item) => item.maxScore)
-    );
-
-    dataList[imgIndex].push({
-      mainNumber: groupQuestions[0].mainNumber,
-      subNumber: "",
-      isFillQuestion: true,
-      score,
-      maxScore,
-      users,
-      area,
-      style: {
-        position: "absolute",
-        left: (100 * area.x).toFixed(4) + "%",
-        top: (100 * area.y).toFixed(4) + "%",
-        width: (100 * area.w).toFixed(4) + "%",
-        fontSize: "14px",
-        lineHeight: 1,
-        zIndex: 9,
-      },
-    });
-  });
-
-  // 其他试题
-  otherQuestions.forEach((question) => {
-    const areas = parseQuestionAreas([question]);
-    const area = { ...areas[0] };
-    const imgIndex = area.i - 1;
-    if (!dataList[imgIndex]) {
-      dataList[imgIndex] = [];
-    }
-
-    const userMap: UserMapType = {};
-    const isArbitration = Boolean(question.headerTrack?.length);
-    const tList = isArbitration ? question.headerTrack : question.trackList;
-    tList.forEach((track) => {
-      if (!userMap[track.userId]) {
-        userMap[track.userId] = {
-          userId: track.userId,
-          userName: track.userName,
-          color: track.color || "red",
-          prename: "",
-          scores: [],
-          score: 0,
-        };
-      }
-      userMap[track.userId].scores.push({
-        score: track.score,
-        subNumber: track.subNumber,
-      });
-    });
-
-    const isDoubleMark = Object.keys(userMap).length > 1;
-    const zhs = ["一", "二", "三"];
-    let users = Object.values(userMap).map((user, index) => {
-      let prename = "";
-      if (isArbitration) {
-        prename = "仲裁";
-      } else {
-        prename = isDoubleMark ? `${zhs[index] || ""}评` : "评卷员";
-      }
-      return {
-        ...user,
-        prename,
-        score: calcSumPrecision(user.scores.map((s) => s.score)),
-      };
-    });
-
-    // 普通模式没有轨迹
-    if (!tList.length && question.markerList && question.markerList.length) {
-      let markers = question.markerList.filter((marker) => marker.header);
-      if (!markers.length) {
-        markers = question.markerList.filter((marker) => !marker.header);
-      }
-      users = markers.map((item, index) => {
-        return {
-          userId: item.userId,
-          userName: item.userName,
-          color: item.header ? "green" : "red",
-          prename: markers.length > 1 ? `${zhs[index] || ""}评` : "评卷员",
-          scores: [],
-          score: item.score,
-        };
-      });
-    }
-
-    dataList[imgIndex].push({
-      mainNumber: question.mainNumber,
-      subNumber: question.subNumber,
-      isFillQuestion: false,
-      score: question.score,
-      maxScore: question.maxScore,
-      users,
-      area,
-      style: {
-        position: "absolute",
-        left: (100 * area.x).toFixed(4) + "%",
-        top: (100 * area.y).toFixed(4) + "%",
-        width: (100 * area.w).toFixed(4) + "%",
-        fontSize: "14px",
-        lineHeight: 1,
-        zIndex: 9,
-      },
-    });
-  });
-
-  return dataList;
-}
-
-// 解析客观题区域总分
-interface ObjectiveAnswerTagItem {
-  id: string;
-  mainNumber: number;
-  subNumbers: string;
-  score: number;
-  totalScore: number;
-  style: Record<string, string | number>;
-}
-function parseObjectiveAnswerTags() {
-  const objectiveAnswerTags: Array<ObjectiveAnswerTagItem[]> = [];
-
-  if (
-    !markStore.currentTask.cardData?.length ||
-    !markStore.currentTask.answerMap
-  )
-    return objectiveAnswerTags;
-
-  markStore.currentTask.cardData.forEach((page, pindex) => {
-    if (!objectiveAnswerTags[pindex]) objectiveAnswerTags[pindex] = [];
-
-    page.columns.forEach((column) => {
-      column.elements.forEach((element) => {
-        if (element.type !== "FILL_QUESTION") return;
-
-        const ogroup = objectiveAnswerTags.find((tgroup) =>
-          tgroup.some((oitem) => oitem.id === element.parent.id)
-        );
-        if (ogroup) return;
-
-        const parent = element.parent;
-        const oaTagItem: ObjectiveAnswerTagItem = {
-          id: parent.id,
-          mainNumber: parent.topicNo,
-          subNumbers: `${parent.startNumber}~${
-            parent.startNumber + parent.questionsCount - 1
-          }`,
-          score: 0,
-          totalScore: 0,
-          style: {
-            position: "absolute",
-            left: 0,
-            top: 0,
-            textAlign: "right",
-            width: "44%",
-            fontSize: "20px",
-            fontWeight: "bold",
-            color: "#f53f3f",
-            lineHeight: 1,
-            zIndex: 9,
-          },
-        };
-
-        let area = [0, 0];
-        page.exchange.fill_area.forEach((fa) => {
-          fa.items.forEach((fitem) => {
-            if (
-              fitem.main_number === oaTagItem.mainNumber &&
-              fitem.sub_number === parent.startNumber
-            ) {
-              area = fitem.options[0];
-            }
-          });
-        });
-
-        const left = (100 * (area[0] - 0.015)).toFixed(4);
-        const top = (100 * (area[1] - 0.04)).toFixed(4);
-        oaTagItem.style.left = `${left}%`;
-        oaTagItem.style.top = `${top}%`;
-
-        const questions: Array<{ score: number; totalScore: number }> = [];
-        for (let i = 0; i < parent.questionsCount; i++) {
-          const qans = markStore.currentTask.answerMap[
-            `${parent.topicNo}_${i + parent.startNumber}`
-          ] || { score: 0, totalScore: 0 };
-          questions[i] = {
-            score: qans.score,
-            totalScore: qans.totalScore,
-          };
-        }
-
-        oaTagItem.score = calcSumPrecision(questions.map((q) => q.score || 0));
-        oaTagItem.totalScore = calcSumPrecision(
-          questions.map((q) => q.totalScore || 0)
-        );
-
-        objectiveAnswerTags[pindex].push(oaTagItem);
-      });
-    });
-  });
-
-  return objectiveAnswerTags;
-}
-
-// 模式4的解析
-interface SummaryItem {
-  mainNumber: number;
-  subNumber: string;
-  score: number;
-  markerName: string;
-}
-function parseMode4Data(): SummaryItem[] {
-  // 只有单评才展示summary
-  const isDoubleMark = (markStore.currentTask.questionList || []).some(
-    (question) => {
-      let userIds = question.trackList.map((track) => track.userId);
-      if (
-        !userIds.length &&
-        question.markerList &&
-        question.markerList.length
-      ) {
-        userIds = question.markerList
-          .filter((marker) => !marker.header)
-          .map((marker) => marker.userId);
-      }
-      const uids = new Set(userIds);
-      return uids.size === 2;
-    }
-  );
-  if (isDoubleMark) return [];
-
-  return (markStore.currentTask.questionList || []).map((q) => {
-    let markerName = "";
-    if (q.headerTrack && q.headerTrack.length) {
-      markerName = q.headerTrack[0].userName;
-    } else if (q.trackList && q.trackList.length) {
-      markerName = q.trackList[0].userName;
-    } else if (q.markerList && q.markerList.length) {
-      let markers = q.markerList.filter((marker) => marker.header);
-      if (!markers.length) {
-        markers = q.markerList.filter((marker) => !marker.header);
-      }
-      if (markers.length) markerName = markers[0].userName;
-    }
-    return {
-      mainNumber: q.mainNumber,
-      subNumber: q.subNumber,
-      score: q.score,
-      markerName,
-    };
-  });
-}
-
-// should not render twice at the same time
-let renderLock = false;
-const renderPaperAndMark = async () => {
-  if (renderLock) {
-    console.log("上个任务还未渲染完毕,稍等一秒再尝试渲染");
-    await new Promise((res) => setTimeout(res, 1000));
-    await renderPaperAndMark();
-    return;
-  }
-  renderLock = true;
-  sliceImagesWithTrackList.splice(0);
-
-  if (!markStore.currentTask) {
-    renderLock = false;
-    return;
-  }
-
-  try {
-    markStore.globalMask = true;
-    await processImage();
-  } catch (error) {
-    sliceImagesWithTrackList.splice(0);
-    console.log("render error ", error);
-    // 图片加载出错,自动加载下一个任务
-    emit("error");
-  } finally {
-    await new Promise((res) => setTimeout(res, 500));
-    markStore.globalMask = false;
-    renderLock = false;
-  }
-};
-
-watch(() => markStore.currentTask, renderPaperAndMark);
-
-watch(
-  (): (number | undefined)[] => [
-    markStore.minimapScrollToX,
-    markStore.minimapScrollToY,
-  ],
-  () => {
-    const container = document.querySelector<HTMLDivElement>(
-      ".mark-body-container"
-    );
-    addTimeout(() => {
-      if (
-        container &&
-        typeof markStore.minimapScrollToX === "number" &&
-        typeof markStore.minimapScrollToY === "number"
-      ) {
-        const { scrollWidth, scrollHeight } = container;
-        container.scrollTo({
-          top: scrollHeight * markStore.minimapScrollToY,
-          left: scrollWidth * markStore.minimapScrollToX,
-          behavior: "smooth",
-        });
-      }
-    }, 10);
-  }
-);
-
-const answerPaperScale = $computed(() => {
-  // 放大、缩小不影响页面之前的滚动条定位
-  let percentWidth = 0;
-  let percentTop = 0;
-  const container = document.querySelector(".mark-body-container");
-  if (container) {
-    const { scrollLeft, scrollTop, scrollWidth, scrollHeight } = container;
-    percentWidth = scrollLeft / scrollWidth;
-    percentTop = scrollTop / scrollHeight;
-  }
-
-  addTimeout(() => {
-    if (container) {
-      const { scrollWidth, scrollHeight } = container;
-      container.scrollTo({
-        left: scrollWidth * percentWidth,
-        top: scrollHeight * percentTop,
-      });
-    }
-  }, 10);
-  const scale = markStore.setting.uiSetting["answer.paper.scale"];
-  return scale * 100 + "%";
-});
-</script>
-
-<style scoped>
-.mark-body-container {
-  overflow: auto;
-  background-color: var(--app-container-bg-color);
-  background-image: linear-gradient(45deg, #e0e0e0 25%, transparent 25%),
-    linear-gradient(-45deg, #e0e0e0 25%, transparent 25%),
-    linear-gradient(45deg, transparent 75%, #e0e0e0 75%),
-    linear-gradient(-45deg, transparent 75%, #e0e0e0 75%);
-  background-size: 20px 20px;
-  background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
-  transform: inherit;
-
-  cursor: grab;
-  user-select: none;
-}
-.mark-body-container img {
-  width: 100%;
-}
-.single-image-container {
-  position: relative;
-}
-.image-seperator {
-  border: 2px solid rgba(120, 120, 120, 0.1);
-}
-.mark-info {
-  display: flex;
-  justify-content: space-between;
-}
-.mark-info h3 {
-  font-size: 20px;
-  font-weight: bold;
-  line-height: 1;
-  color: #f53f3f;
-}
-.mark-info p {
-  margin: 0;
-  line-height: 20px;
-  font-weight: bold;
-}
-.mark-total {
-  font-size: 20px;
-  font-weight: bold;
-  position: absolute;
-  top: 1%;
-  left: 15%;
-  z-index: 9;
-  color: #f53f3f;
-}
-.summary-detail {
-  position: absolute;
-  width: 45%;
-  left: 5%;
-  top: 11%;
-  height: 84%;
-  z-index: 9;
-  color: #f53f3f;
-  font-weight: 600;
-}
-.summary-detail table {
-  border-spacing: 0;
-  border-collapse: collapse;
-  text-align: left;
-}
-.summary-detail table td,
-.summary-detail table th {
-  padding: 0 10px;
-  line-height: 24px;
-}
-</style>

+ 0 - 30
src/features/student/studentInspect/MarkHeader.vue

@@ -1,30 +0,0 @@
-<template>
-  <CommonMarkHeader
-    :isSingleStudent="isSingleStudent"
-    :clearTasks="clearTasks"
-    showScoreBoard
-    showPaperAndAnswer
-    notShowAnswer
-  >
-    <span>
-      <span class="header-small-text">待复核</span>
-      <span class="highlight-text">{{ store.status.totalCount ?? "-" }}</span>
-    </span>
-  </CommonMarkHeader>
-</template>
-
-<script setup lang="ts">
-import { clearInspectedTask } from "@/api/inspectPage";
-import { store } from "@/store/app";
-import { useRoute } from "vue-router";
-import CommonMarkHeader from "@/components/CommonMarkHeader.vue";
-
-const route = useRoute();
-let isSingleStudent = !!route.query.studentId;
-const { studentId, subjectCode } = route.query as {
-  studentId: string;
-  subjectCode: string;
-};
-
-let clearTasks = clearInspectedTask.bind(null, studentId, subjectCode);
-</script>

+ 0 - 273
src/features/student/studentInspect/StudentInspect.vue

@@ -1,273 +0,0 @@
-<template>
-  <div class="my-container">
-    <mark-header />
-    <div class="tw-flex tw-gap-1">
-      <mark-history
-        v-if="!isSingleStudent"
-        :subjectCode="subjectCode"
-        orderTimeField="inspect_time"
-        :getHistory="getInspectedHistory"
-      />
-      <mark-body
-        @error="renderError"
-        @getIsMultComments="getIsMultComments"
-        @getScrollStatus="getScrollStatus"
-      />
-      <MarkBoardInspect
-        :isMultComments="isMultComments"
-        :hasScrollToBottom="hasScrollToBottom"
-        @inspect="saveTaskToServer"
-        @reject="rejectQuestions"
-      />
-    </div>
-  </div>
-  <MinimapModal />
-  <PaperModal />
-</template>
-
-<script setup lang="ts">
-import { onMounted, watch } from "vue";
-import {
-  clearInspectedTask,
-  getAdminPageSetting,
-  getInspectedTaskStatus,
-  getOneOfInspectedTask,
-  getSingleInspectedTask,
-  rejectInspectedTask,
-  saveInspectedTask,
-} from "@/api/inspectPage";
-import { store } from "@/store/app";
-import MarkHeader from "./MarkHeader.vue";
-import MinimapModal from "@/features/mark/MinimapModal.vue";
-import { useRoute } from "vue-router";
-import MarkBody from "./MarkBody.vue";
-import MarkHistory from "@/features/mark/MarkHistory.vue";
-import MarkBoardInspect from "./MarkBoardInspect.vue";
-import type { Question } from "@/types";
-import { message } from "ant-design-vue";
-import { getPaper } from "@/api/jsonMark";
-import { getInspectedHistory } from "@/api/inspectPage";
-import EventBus from "@/plugins/eventBus";
-import { addFileServerPrefixToTask } from "@/utils/utils";
-import PaperModal from "../../mark/PaperModal.vue";
-
-let isMultComments = $ref(false); //是否双评
-const getIsMultComments = (bool: boolean) => {
-  isMultComments = bool;
-};
-let hasScrollToBottom = $ref(false);
-const getScrollStatus = () => {
-  hasScrollToBottom = true;
-};
-watch(
-  () => store.setting,
-  () => {
-    hasScrollToBottom = !store.setting?.inspectScroll;
-  },
-  { deep: true }
-);
-const route = useRoute();
-let isSingleStudent = !!route.query.studentId;
-const {
-  studentId,
-  subjectCode,
-  startScore,
-  endScore,
-  mainNumber,
-  mainStartScore,
-  mainEndScore,
-  questionScore,
-  selectiveStatus,
-  secretNumber,
-} = route.query as {
-  studentId: string;
-  subjectCode: string;
-  startScore: string;
-  endScore: string;
-  mainNumber: string;
-  mainStartScore: string;
-  mainEndScore: string;
-  questionScore: string;
-  selectiveStatus: string;
-  secretNumber: string;
-};
-
-async function updateClearTask() {
-  await clearInspectedTask(studentId, subjectCode);
-}
-
-async function updateSetting() {
-  const settingRes = await getAdminPageSetting(subjectCode);
-  const {
-    examType,
-    fileServer,
-    subject,
-    userName,
-    splitConfig,
-    enableSplit,
-    doubleTrack,
-    inspectScroll,
-  } = settingRes.data;
-  store.initSetting({
-    examType,
-    fileServer,
-    subject,
-    userName,
-    splitConfig,
-    enableSplit,
-    doubleTrack,
-    inspectScroll,
-  });
-  if (store.setting.subject?.paperUrl && store.isMultiMedia) {
-    await getPaper(store);
-  }
-}
-async function updateStatus() {
-  const res = await getInspectedTaskStatus({
-    subjectCode,
-    mainNumber,
-    startScore,
-    endScore,
-    mainStartScore,
-    mainEndScore,
-    questionScore,
-    selectiveStatus,
-    secretNumber,
-  });
-  if (res.data.valid) Object.assign(store.status, res.data);
-}
-async function updateTask() {
-  const mkey = "fetch_task_key";
-  void message.info({ content: "获取任务中...", duration: 1.5, key: mkey });
-  let res;
-  if (isSingleStudent) {
-    res = await getSingleStuTask();
-  } else {
-    res = await getOneOfStuTask();
-  }
-  void message.success({
-    content:
-      res.data.studentId && !res.data.inspectTime ? "获取成功" : "无任务",
-    key: mkey,
-  });
-
-  // if (res.data.studentId) {
-  if (res.data.studentId && !res.data.inspectTime) {
-    let rawTask = res.data;
-    store.currentTask = addFileServerPrefixToTask(rawTask);
-    if (res.data.subject) {
-      store.setting.subject = Object.assign(
-        res.data.subject,
-        store.setting.subject || {}
-      );
-    }
-  } else {
-    store.message = res.data.message;
-  }
-}
-
-watch(
-  () => store.historyOpen,
-  async () => {
-    if (!store.historyOpen) {
-      await updateClearTask();
-      await updateSetting();
-      await fetchTask();
-    }
-  }
-);
-async function fetchTask() {
-  !isSingleStudent && (await updateStatus());
-  await updateTask();
-}
-
-onMounted(async () => {
-  await updateClearTask();
-  await updateSetting();
-  await fetchTask();
-});
-
-async function getSingleStuTask() {
-  return getSingleInspectedTask(studentId);
-}
-
-async function getOneOfStuTask() {
-  return getOneOfInspectedTask({
-    subjectCode,
-    mainNumber,
-    startScore,
-    endScore,
-    mainStartScore,
-    mainEndScore,
-    questionScore,
-    selectiveStatus,
-    secretNumber,
-  });
-}
-
-const realStudentId = $computed(
-  () => (isSingleStudent ? studentId : store.currentTask?.studentId) as string
-);
-const saveTaskToServer = async () => {
-  console.log("save inspect task to server");
-  const mkey = "save_task_key";
-  void message.loading({ content: "保存评卷任务...", key: mkey });
-  const res = await saveInspectedTask(realStudentId);
-  if (res.data.success && store.currentTask) {
-    void message.success({ content: "复核成功", key: mkey, duration: 2 });
-    if (!store.historyOpen) {
-      store.currentTask = undefined;
-      if (!isSingleStudent) await fetchTask();
-    } else {
-      EventBus.emit("should-reload-history");
-    }
-  } else if (res.data.message) {
-    console.log(res.data.message);
-    void message.error({ content: res.data.message, key: mkey, duration: 10 });
-  } else if (!store.currentTask) {
-    void message.warn({ content: "暂无新任务", key: mkey, duration: 10 });
-  }
-};
-
-const rejectQuestions = async ({
-  questions,
-  reason = "",
-}: {
-  questions: Array<Question>;
-  reason: string;
-}) => {
-  const mkey = "reject_task_key";
-  void message.loading({ content: "打回评卷任务...", key: mkey });
-  const res = await rejectInspectedTask(realStudentId, questions, reason);
-  if (res.data.success && store.currentTask) {
-    store.currentTask = undefined;
-    void message.success({ content: "打回成功", key: mkey, duration: 2 });
-    if (!store.historyOpen) {
-      store.currentTask = undefined;
-      if (!isSingleStudent) {
-        await fetchTask();
-        hasScrollToBottom = !store.setting?.inspectScroll;
-      }
-    } else {
-      EventBus.emit("should-reload-history");
-    }
-  } else if (res.data.message) {
-    // console.log(res.data.message);
-    void message.error({ content: res.data.message, key: mkey, duration: 10 });
-  } else if (!store.currentTask) {
-    void message.warn({ content: "暂无新任务", key: mkey, duration: 10 });
-  }
-};
-
-const renderError = () => {
-  store.currentTask = undefined;
-  store.message = "加载失败,请重新加载。";
-};
-</script>
-
-<style scoped>
-.my-container {
-  width: 100%;
-  overflow: clip;
-}
-</style>

+ 1 - 41
src/router/index.ts

@@ -37,45 +37,6 @@ const routes = [
     name: "Reject",
     component: () => import("@/features/reject/Reject.vue"),
   },
-  // old page
-  // {
-  //   // 整卷批量复核
-  //   path: "/admin/exam/inspected/start",
-  //   component: () =>
-  //     import("@/features/student/studentInspect/StudentInspect.vue"),
-  // },
-  // {
-  //   // 批量导入复核
-  //   path: "/admin/exam/inspected/import/start",
-  //   component: () =>
-  //     import("@/features/student/importInspect/ImportInspect.vue"),
-  // },
-  // {
-  //   // 成绩校验
-  //   path: "/admin/exam/score/verify/start",
-  //   component: () => import("@/features/student/scoreVerify/ScoreVerify.vue"),
-  // },
-  // {
-  //   // 任务批量复核
-  //   path: "/admin/exam/library/inspected/start",
-  //   component: () => import("@/features/library/inspect/LibraryInspect.vue"),
-  // },
-  // {
-  //   // 质量分析
-  //   path: "/admin/exam/quality",
-  //   component: () => import("@/features/library/quality/Quality.vue"),
-  // },
-  // {
-  //   // 评卷管理-任务管理-轨迹图
-  //   path: "/admin/exam/track/library",
-  //   component: () => import("@/features/library/libraryTrack/LibraryTrack.vue"),
-  // },
-  // {
-  //   // 试评任务轨迹
-  //   path: "/admin/exam/track/trialLibrary",
-  //   name: "TrialRoute",
-  //   component: () => import("@/features/library/libraryTrack/LibraryTrack.vue"),
-  // },
   {
     path: "/:pathMatch(.*)*",
     name: "NotFound",
@@ -87,9 +48,8 @@ const routes = [
 // You can pass in additional options here, but let's
 // keep it simple for now.
 const router = createRouter({
-  // 4. Provide the history implementation to use. We are using the hash history for simplicity here.
   history: createWebHistory("mark"),
-  routes, // short for `routes: routes`
+  routes,
 });
 
 export default router;