Browse Source

feat: reject and arbitrate

zhangjie 4 months ago
parent
commit
3153126e8b

+ 77 - 70
src/features/arbitrate/Arbitrate.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="mark arbitrate">
-    <mark-header :isSingleStudent="isSingleStudent" />
+    <arbitrate-header :isSingleStudent="isSingleStudent" />
     <mark-tool :actions="['minimap', 'sizeScale', 'imgScale']" />
     <div class="mark-main">
       <mark-history
@@ -10,47 +10,44 @@
         :groupNumber="groupNumber + ''"
         :getHistory="getArbitrateHistory"
       />
-      <ArbitrateMarkList />
-      <mark-body @error="renderError" />
+      <arbitrate-mark-list />
+      <arbitrate-body @error="renderError" />
       <mark-board-track
-        v-if="store.isTrackMode"
+        v-if="markStore.isTrackMode"
         @unselectiveSubmit="saveTaskToServer(true)"
         @submit="saveTaskToServerByTrack"
       />
-      <template v-if="!store.isTrackMode">
+      <template v-if="!markStore.isTrackMode">
         <mark-board-key-board
-          v-if="store.shouldShowMarkBoardKeyBoard"
+          v-if="markStore.shouldShowMarkBoardKeyBoard"
           @submit="saveTaskToServer(false)"
           @unselectiveSubmit="saveTaskToServer(true)"
         />
         <mark-board-mouse
-          v-if="store.shouldShowMarkBoardMouse"
+          v-if="markStore.shouldShowMarkBoardMouse"
           @submit="saveTaskToServer(false)"
           @unselectiveSubmit="saveTaskToServer(true)"
         />
       </template>
     </div>
   </div>
-  <AnswerModal />
-  <PaperModal />
-  <MinimapModal />
-  <MarkBoardTrackDialog v-if="store.isTrackMode" @submit="saveTaskToServer" />
+  <!-- modal -->
+  <modal-answer />
+  <modal-paper />
+  <modal-minimap />
+  <!-- other -->
+  <mark-board-track-dialog
+    v-if="markStore.isTrackMode"
+    @submit="saveTaskToServer"
+  />
 </template>
 
 <script setup lang="ts">
 import { onMounted, watch, h } from "vue";
-import { store } from "@/store/app";
-import MarkHeader from "./MarkHeader.vue";
-import MarkTool from "../mark/MarkTool.vue";
-import MarkBody from "./MarkBody.vue";
-import MarkBoardKeyBoard from "@/features/mark/MarkBoardKeyBoard.vue";
-import MarkBoardMouse from "@/features/mark/MarkBoardMouse.vue";
-import MinimapModal from "@/features/mark/MinimapModal.vue";
-import MarkHistory from "@/features/mark/MarkHistory.vue";
-import MarkBoardTrack from "@/features/mark/MarkBoardTrack.vue";
-import MarkBoardTrackDialog from "@/features/mark/MarkBoardTrackDialog.vue";
-
 import { message } from "ant-design-vue";
+import { useMarkStore } from "@/store";
+import { isNumber } from "lodash-es";
+import type { Question } from "@/types";
 import {
   clearArbitrateTask,
   getArbitrateSetting,
@@ -59,15 +56,30 @@ import {
   getSingleArbitrateTask,
   saveArbitrateTask,
 } from "@/api/arbitratePage";
-import ArbitrateMarkList from "./ArbitrateMarkList.vue";
-import AnswerModal from "../mark/AnswerModal.vue";
-import PaperModal from "../mark/PaperModal.vue";
 import { getArbitrateHistory } from "@/api/arbitratePage";
 import EventBus from "@/plugins/eventBus";
-import { isNumber } from "lodash-es";
-import type { Question } from "@/types";
 import vls from "@/utils/storage";
 
+// components
+import ArbitrateBody from "./ArbitrateBody.vue";
+import ArbitrateHeader from "./ArbitrateHeader.vue";
+import ArbitrateMarkList from "./ArbitrateMarkList.vue";
+
+import MarkTool from "../mark/toolbar/MarkTool.vue";
+import MarkHistory from "../mark/MarkHistory.vue";
+
+// scoring
+import MarkBoardKeyBoard from "../mark/scoring/MarkBoardKeyBoard.vue";
+import MarkBoardMouse from "../mark/scoring/MarkBoardMouse.vue";
+import MarkBoardTrack from "../mark/scoring/MarkBoardTrack.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";
+
+const markStore = useMarkStore();
+
 const { paperNumber, groupNumber, examId, arbitrateId } = vls.get(
   "arbitrate",
   {}
@@ -81,10 +93,10 @@ const params = isSingleStudent
 async function updateClearTask() {
   await clearArbitrateTask(params);
 }
-
+// 更新设置
 async function updateSetting() {
   const settingRes = await getArbitrateSetting(params);
-  store.setting = Object.assign({}, store.setting, {
+  markStore.setting = Object.assign({}, markStore.setting, {
     selective: false,
     examType: "SCAN_IMAGE",
     subject: {
@@ -95,18 +107,20 @@ async function updateSetting() {
     enableSplit: false,
   });
 }
+
 async function updateStatus() {
   const res = await getArbitrateTaskStatus(params);
-  if (res.data.valid) Object.assign(store.status, res.data);
+  if (res.data.valid) Object.assign(markStore.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();
+    res = await getSingleArbitrateTask(params);
   } else {
-    res = await getOneOfStuTask();
+    res = await getOneOfArbitrateTask(params);
   }
   void message.success({
     content: res.data ? "获取成功" : "当前无评卷任务",
@@ -116,24 +130,32 @@ async function updateTask() {
   if (res.data) {
     let rawTask = res.data;
     rawTask.sheetUrls = rawTask.sheetUrls || [];
-    // rawTask.sheetUrls = ["/1-1.jpg", "/1-2.jpg"];
     rawTask.sliceUrls = [...rawTask.sheetUrls];
-    store.currentTask = rawTask;
+    markStore.currentTask = rawTask;
   } else {
-    store.message = "当前无评卷任务";
+    markStore.message = "当前无评卷任务";
   }
 }
 
 watch(
-  () => store.historyOpen,
+  () => markStore.historyOpen,
   async () => {
-    if (!store.historyOpen) {
+    if (!markStore.historyOpen) {
       await updateClearTask();
       await fetchTask();
     }
   }
 );
 
+watch(
+  () => markStore.currentTask,
+  () => {
+    // 重置当前选择的quesiton和score
+    markStore.currentQuestion = undefined;
+    markStore.currentScore = undefined;
+  }
+);
+
 async function fetchTask() {
   !isSingleStudent && (await updateStatus());
   await updateTask();
@@ -145,26 +167,10 @@ onMounted(async () => {
   await fetchTask();
 });
 
-watch(
-  () => store.currentTask,
-  () => {
-    // 重置当前选择的quesiton和score
-    store.currentQuestion = undefined;
-    store.currentScore = undefined;
-  }
-);
-
-async function getSingleStuTask() {
-  return getSingleArbitrateTask(params);
-}
-
-async function getOneOfStuTask() {
-  return getOneOfArbitrateTask(params);
-}
-
+// 保存任务
 const saveTaskToServerByTrack = async () => {
-  let trackList = store.currentTask.markResult.trackList;
-  let specialTagList = store.currentTask.markResult.specialTagList;
+  let trackList = markStore.currentTask.markResult.trackList;
+  let specialTagList = markStore.currentTask.markResult.specialTagList;
   await saveTaskToServer(false, trackList, specialTagList);
 };
 
@@ -173,8 +179,8 @@ const saveTaskToServer = async (
   trackList?: any,
   specialTagList?: any
 ) => {
-  if (!store.currentTask) return;
-  const markResult = store.currentTask.markResult;
+  if (!markStore.currentTask) return;
+  const markResult = markStore.currentTask.markResult;
   if (!markResult) return;
 
   const mkey = "save_task_key";
@@ -182,8 +188,8 @@ const saveTaskToServer = async (
   let res;
   if (unselective) {
     res = await saveArbitrateTask(
-      store.currentTask.taskId,
-      store.currentTask.studentId,
+      markStore.currentTask.taskId,
+      markStore.currentTask.studentId,
       -1,
       [],
       true
@@ -197,8 +203,8 @@ const saveTaskToServer = async (
     const errors: SubmitError[] = [];
 
     markResult.scoreList.forEach((score, index) => {
-      if (!store.currentTask) return;
-      const question = store.currentTask.questionList[index];
+      if (!markStore.currentTask) return;
+      const question = markStore.currentTask.questionList[index];
       let error;
       if (!isNumber(score)) {
         error = `${question.mainNumber}-${question.subNumber}${
@@ -227,10 +233,10 @@ const saveTaskToServer = async (
       });
     }
     res = await saveArbitrateTask(
-      store.currentTask.taskId + "",
-      store.currentTask.studentId + "",
-      store.currentTask.markResult.markerScore,
-      store.currentTask.markResult.scoreList,
+      markStore.currentTask.taskId + "",
+      markStore.currentTask.studentId + "",
+      markStore.currentTask.markResult.markerScore,
+      markStore.currentTask.markResult.scoreList,
       false,
       trackList,
       specialTagList
@@ -258,16 +264,17 @@ const saveTaskToServer = async (
   }
 
   void message.success({ content: "仲裁成功", key: mkey, duration: 2 });
-  if (!store.historyOpen) {
-    store.currentTask = undefined;
+  if (!markStore.historyOpen) {
+    markStore.currentTask = undefined;
     await fetchTask();
   } else {
     EventBus.emit("should-reload-history");
   }
 };
 
+// 渲染错误
 const renderError = () => {
-  store.currentTask = undefined;
-  store.message = "加载失败,请重新加载。";
+  markStore.currentTask = undefined;
+  markStore.message = "加载失败,请重新加载。";
 };
 </script>

+ 151 - 0
src/features/arbitrate/ArbitrateBody.vue

@@ -0,0 +1,151 @@
+<template>
+  <MarkBodyBase
+    hasMarkResultToRender
+    :makeTrack="makeTrack"
+    @error="$emit('error')"
+  />
+  <MarkBodyCursor />
+</template>
+
+<script setup lang="ts">
+import MarkBodyBase from "../mark/MarkBodyBase.vue";
+import MarkBodyCursor from "../mark/MarkBodyCursor.vue";
+import { useMarkStore } from "@/store";
+import { SliceImage, SpecialTag, Track } from "@/types";
+
+const markStore = useMarkStore();
+
+const makeScoreTrack = (
+  event: MouseEvent,
+  item: SliceImage,
+  maxSliceWidth: number,
+  theFinalHeight: number
+) => {
+  if (
+    !markStore.currentQuestion ||
+    typeof markStore.currentScore === "undefined"
+  )
+    return;
+  const target = event.target as HTMLImageElement;
+  const track: Track = {
+    mainNumber: markStore.currentQuestion?.mainNumber,
+    subNumber: markStore.currentQuestion?.subNumber,
+    score: markStore.currentScore,
+    unanswered: Object.is(markStore.currentScore, -0),
+    offsetIndex: item.indexInSliceUrls,
+    offsetX: event.offsetX * (target.naturalWidth / target.width) + item.dx,
+    offsetY: event.offsetY * (target.naturalHeight / target.height) + item.dy,
+    positionX: -1,
+    positionY: -1,
+    number: -1,
+  };
+  track.positionX = (track.offsetX - item.dx) / maxSliceWidth;
+  track.positionY =
+    (track.offsetY - item.dy + item.accumTopHeight) / theFinalHeight;
+
+  if (track.offsetX > item.effectiveWidth + item.dx) {
+    console.log("不在有效宽度内,轨迹不生效");
+    return;
+  }
+  if (
+    item.trackList.some((t) => {
+      return (
+        Math.pow(Math.abs(t.offsetX - track.offsetX), 2) +
+          Math.pow(Math.abs(t.offsetY - track.offsetY), 2) <
+        500
+      );
+    })
+  ) {
+    console.log("两个轨迹相距过近");
+    return;
+  }
+  // 是否保留当前的轨迹分
+  const questionScore =
+    markStore.currentTask &&
+    markStore.currentQuestion &&
+    markStore.currentTask.markResult.scoreList[
+      markStore.currentQuestion.__index
+    ];
+  const ifKeepScore =
+    Math.round(
+      markStore.currentQuestion.maxScore * 1000 -
+        (questionScore || 0) * 1000 -
+        markStore.currentScore * 2 * 1000
+    ) / 1000;
+  if (ifKeepScore < 0 && markStore.currentScore > 0) {
+    markStore.currentScore = undefined;
+  }
+  const markResult = markStore.currentTaskEnsured.markResult;
+  const maxNumber =
+    markResult.trackList.length === 0
+      ? 0
+      : Math.max(...markResult.trackList.map((t) => t.number));
+  track.number = maxNumber + 1;
+  markResult.trackList = [...markResult.trackList, track];
+  const { __index, mainNumber, subNumber } = markStore.currentQuestion;
+  markResult.scoreList[__index] =
+    markResult.trackList
+      .filter((t) => t.mainNumber === mainNumber && t.subNumber === subNumber)
+      .map((t) => t.score)
+      .reduce((acc, v) => (acc += Math.round(v * 1000)), 0) / 1000;
+  item.trackList.push(track);
+};
+
+const makeSpecialTagTrack = (
+  event: MouseEvent,
+  item: SliceImage,
+  maxSliceWidth: number,
+  theFinalHeight: number
+) => {
+  if (
+    !markStore.currentTask ||
+    typeof markStore.currentSpecialTag === "undefined"
+  )
+    return;
+  const target = event.target as HTMLImageElement;
+  const track: SpecialTag = {
+    tagName: markStore.currentSpecialTag,
+    offsetIndex: item.indexInSliceUrls,
+    offsetX: event.offsetX * (target.naturalWidth / target.width) + item.dx,
+    offsetY: event.offsetY * (target.naturalHeight / target.height) + item.dy,
+    positionX: -1,
+    positionY: -1,
+  };
+  track.positionX = (track.offsetX - item.dx) / maxSliceWidth;
+  track.positionY =
+    (track.offsetY - item.dy + item.accumTopHeight) / theFinalHeight;
+
+  if (
+    item.tagList.some((t) => {
+      return (
+        Math.pow(Math.abs(t.offsetX - track.offsetX), 2) +
+          Math.pow(Math.abs(t.offsetY - track.offsetY), 2) <
+        500
+      );
+    })
+  ) {
+    console.log("两个轨迹相距过近");
+    return;
+  }
+  markStore.currentTaskEnsured.markResult.specialTagList.push(track);
+  item.tagList.push(track);
+};
+
+const makeTrack = (
+  event: MouseEvent,
+  item: SliceImage,
+  maxSliceWidth: number,
+  theFinalHeight: number
+) => {
+  if (
+    markStore.setting.uiSetting["specialTag.modal"] &&
+    markStore.currentSpecialTag
+  ) {
+    makeSpecialTagTrack(event, item, maxSliceWidth, theFinalHeight);
+  } else {
+    makeScoreTrack(event, item, maxSliceWidth, theFinalHeight);
+  }
+};
+
+defineEmits(["error"]);
+</script>

+ 24 - 20
src/features/arbitrate/MarkHeader.vue → src/features/arbitrate/ArbitrateHeader.vue

@@ -1,18 +1,21 @@
 <template>
-  <div v-if="store.setting && store.setting.subject.name" class="mark-header">
+  <div
+    v-if="markStore.setting && markStore.setting.subject.name"
+    class="mark-header"
+  >
     <div class="mark-header-part">
       <div
         v-if="!isSingleStudent"
-        :class="['header-menu', { 'is-toggled': store.historyOpen }]"
-        @click="store.toggleHistory"
+        :class="['header-menu', { 'is-toggled': markStore.historyOpen }]"
+        @click="markStore.toggleHistory"
       >
         <img class="header-icon" src="@/assets/icons/icon-left-menu.svg" />回评
       </div>
-      <div class="header-subject" :title="store.setting.subject.name">
+      <div class="header-subject" :title="markStore.setting.subject.name">
         <div>
           {{
-            `${store.setting.subject.code ?? ""}-${
-              store.setting.subject.name ?? ""
+            `${markStore.setting.subject.code ?? ""}-${
+              markStore.setting.subject.name ?? ""
             }`
           }}
         </div>
@@ -21,7 +24,7 @@
         <div class="header-noun">
           <span>编号:</span>
           <span>
-            {{ store.currentTask?.secretNumber ?? "-" }}
+            {{ markStore.currentTask?.secretNumber ?? "-" }}
           </span>
         </div>
       </div>
@@ -29,16 +32,16 @@
         <span class="header-noun">
           <span>待处理:</span>
           <transition-group name="count-animation" tag="span">
-            <span :key="store.status.totalCount || 0">
-              {{ store.status.totalCount }}
+            <span :key="markStore.status.totalCount || 0">
+              {{ markStore.status.totalCount }}
             </span>
           </transition-group>
         </span>
         <span class="header-noun">
           <span>已处理:</span>
           <transition-group name="count-animation" tag="span">
-            <span :key="store.status.markedCount || 0">
-              {{ store.status.markedCount }}
+            <span :key="markStore.status.markedCount || 0">
+              {{ markStore.status.markedCount }}
             </span>
           </transition-group>
         </span>
@@ -48,19 +51,22 @@
     <div class="mark-header-part">
       <div class="header-text-btn">
         <img src="@/assets/icons/icon-track-mode.svg" class="header-icon" />
-        {{ modeName }}
+        {{ markStore.modeName }}
       </div>
       <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>
@@ -73,17 +79,15 @@
 </template>
 
 <script setup lang="ts">
-import { store } from "@/store/app";
+import { useMarkStore } from "@/store";
 import { doLogout } from "@/api/markPage";
 
+const markStore = useMarkStore();
+
 const { isSingleStudent = false } = defineProps<{
   isSingleStudent?: boolean;
 }>();
 
-const modeName = $computed(() =>
-  store.setting.mode === "TRACK" ? "轨迹模式" : "普通模式"
-);
-
 const logout = () => {
   doLogout();
 };

+ 6 - 4
src/features/arbitrate/ArbitrateMarkList.vue

@@ -33,18 +33,20 @@
 
 <script setup lang="ts">
 import { reactive, watch } from "vue";
-import { store } from "@/store/app";
+import { useMarkStore } from "@/store";
 import { getArbitrateList } from "@/api/arbitratePage";
 import { MarkDetail } from "@/types";
 
+const markStore = useMarkStore();
+
 let list: MarkDetail[] = reactive([]);
 
 watch(
-  () => store.currentTask,
+  () => markStore.currentTask,
   async () => {
-    if (store.currentTask) {
+    if (markStore.currentTask) {
       const res = await getArbitrateList(
-        store.currentTask?.taskId as unknown as string
+        markStore.currentTask?.taskId as unknown as string
       );
       if (Array.isArray(res.data)) {
         list.splice(0);

+ 0 - 291
src/features/arbitrate/MarkBody.vue

@@ -1,291 +0,0 @@
-<template>
-  <MarkBodyBase
-    :hasMarkResultToRender="true"
-    :makeTrack="makeTrack"
-    @error="$emit('error')"
-  />
-  <div class="cursor">
-    <div class="cursor-border">
-      <span class="text">{{
-        store.currentSpecialTag ||
-        (Object.is(store.currentScore, -0) ? "空" : store.currentScore)
-      }}</span>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import MarkBodyBase from "@/features/mark/MarkBodyBase.vue";
-import { store } from "@/store/app";
-import CustomCursor from "custom-cursor.js";
-import { onMounted, onUnmounted, watch } from "vue";
-import { SliceImage, SpecialTag, Track } from "@/types";
-
-const makeScoreTrack = (
-  event: MouseEvent,
-  item: SliceImage,
-  maxSliceWidth: number,
-  theFinalHeight: number
-) => {
-  // console.log(item);
-  if (!store.currentQuestion || typeof store.currentScore === "undefined")
-    return;
-  const target = event.target as HTMLImageElement;
-  const track: Track = {
-    mainNumber: store.currentQuestion?.mainNumber,
-    subNumber: store.currentQuestion?.subNumber,
-    score: store.currentScore,
-    unanswered: Object.is(store.currentScore, -0),
-    offsetIndex: item.indexInSliceUrls,
-    offsetX: event.offsetX * (target.naturalWidth / target.width) + item.dx,
-    offsetY: event.offsetY * (target.naturalHeight / target.height) + item.dy,
-    positionX: -1,
-    positionY: -1,
-    number: -1,
-  };
-  track.positionX = (track.offsetX - item.dx) / maxSliceWidth;
-  track.positionY =
-    (track.offsetY - item.dy + item.accumTopHeight) / theFinalHeight;
-
-  // const isIllegalRange = (testNum: number, min: number, max: number) => {
-  //   return testNum < min || testNum > max;
-  // };
-
-  // // 检测有问题,此处没有给原图的宽高,如果有的话,要稍微修改下数据类型
-  // // 但其实下面也做了一个基本检测
-  // if (
-  //   isIllegalRange(track.offsetX, 0, target.naturalWidth) ||
-  //   isIllegalRange(track.offsetY, 0, target.naturalHeight) ||
-  //   isIllegalRange(track.positionX, 0, 1) ||
-  //   isIllegalRange(track.positionY, 0, 1)
-  // ) {
-  //   console.error(
-  //     "错误的track",
-  //     track,
-  //     target.naturalWidth,
-  //     target.naturalHeight
-  //   );
-  //   void message.error("系统错误,请联系管理员!");
-  // }
-
-  if (track.offsetX > item.effectiveWidth + item.dx) {
-    console.log("不在有效宽度内,轨迹不生效");
-    return;
-  }
-  if (
-    item.trackList.some((t) => {
-      return (
-        Math.pow(Math.abs(t.offsetX - track.offsetX), 2) +
-          Math.pow(Math.abs(t.offsetY - track.offsetY), 2) <
-        500
-      );
-    })
-  ) {
-    console.log("两个轨迹相距过近");
-    return;
-  }
-  // 是否保留当前的轨迹分
-  const questionScore =
-    store.currentTask &&
-    store.currentQuestion &&
-    store.currentTask.markResult.scoreList[store.currentQuestion.__index];
-  const ifKeepScore =
-    Math.round(
-      store.currentQuestion.maxScore * 1000 -
-        (questionScore || 0) * 1000 -
-        store.currentScore * 2 * 1000
-    ) / 1000;
-  if (ifKeepScore < 0 && store.currentScore > 0) {
-    store.currentScore = undefined;
-  }
-  const markResult = store.currentTaskEnsured.markResult;
-  const maxNumber =
-    markResult.trackList.length === 0
-      ? 0
-      : Math.max(...markResult.trackList.map((t) => t.number));
-  track.number = maxNumber + 1;
-  // console.log(
-  //   maxNumber,
-  //   track.number,
-  //   markResult.trackList.map((t) => t.number),
-  //   Math.max(...markResult.trackList.map((t) => t.number))
-  // );
-  markResult.trackList = [...markResult.trackList, track];
-  const { __index, mainNumber, subNumber } = store.currentQuestion;
-  markResult.scoreList[__index] =
-    markResult.trackList
-      .filter((t) => t.mainNumber === mainNumber && t.subNumber === subNumber)
-      .map((t) => t.score)
-      .reduce((acc, v) => (acc += Math.round(v * 1000)), 0) / 1000;
-  item.trackList.push(track);
-};
-
-const makeSpecialTagTrack = (
-  event: MouseEvent,
-  item: SliceImage,
-  maxSliceWidth: number,
-  theFinalHeight: number
-) => {
-  // console.log(item);
-  if (!store.currentTask || typeof store.currentSpecialTag === "undefined")
-    return;
-  const target = event.target as HTMLImageElement;
-  const track: SpecialTag = {
-    tagName: store.currentSpecialTag,
-    offsetIndex: item.indexInSliceUrls,
-    offsetX: event.offsetX * (target.naturalWidth / target.width) + item.dx,
-    offsetY: event.offsetY * (target.naturalHeight / target.height) + item.dy,
-    positionX: -1,
-    positionY: -1,
-  };
-  track.positionX = (track.offsetX - item.dx) / maxSliceWidth;
-  track.positionY =
-    (track.offsetY - item.dy + item.accumTopHeight) / theFinalHeight;
-
-  // const isIllegalRange = (testNum: number, min: number, max: number) => {
-  //   return testNum < min || testNum > max;
-  // };
-
-  // if (
-  //   isIllegalRange(track.offsetX, 0, target.naturalWidth) ||
-  //   isIllegalRange(track.offsetY, 0, target.naturalHeight) ||
-  //   isIllegalRange(track.positionX, 0, 1) ||
-  //   isIllegalRange(track.positionY, 0, 1)
-  // ) {
-  //   console.error("错误的track", track);
-  //   void message.error("系统错误,请联系管理员!");
-  // }
-  // if (track.offsetX > item.effectiveWidth + item.dx) {
-  //   console.log("不在有效宽度内,轨迹不生效");
-  //   return;
-  // }
-  if (
-    item.tagList.some((t) => {
-      return (
-        Math.pow(Math.abs(t.offsetX - track.offsetX), 2) +
-          Math.pow(Math.abs(t.offsetY - track.offsetY), 2) <
-        500
-      );
-    })
-  ) {
-    console.log("两个轨迹相距过近");
-    return;
-  }
-  store.currentTaskEnsured.markResult.specialTagList.push(track);
-  item.tagList.push(track);
-};
-
-const makeTrack = (
-  event: MouseEvent,
-  item: SliceImage,
-  maxSliceWidth: number,
-  theFinalHeight: number
-) => {
-  if (store.setting.uiSetting["specialTag.modal"] && store.currentSpecialTag) {
-    makeSpecialTagTrack(event, item, maxSliceWidth, theFinalHeight);
-  } else {
-    makeScoreTrack(event, item, maxSliceWidth, theFinalHeight);
-  }
-};
-
-watch(
-  () => store.setting.mode,
-  () => {
-    const shouldHide = store.setting.mode === "COMMON";
-    if (shouldHide) {
-      // console.log("hide cursor", theCursor);
-      // eslint-disable-next-line @typescript-eslint/no-unsafe-call
-      theCursor && theCursor.destroy();
-    } else {
-      if (document.querySelector(".cursor")) {
-        // console.log("show cursor", theCursor);
-        // theCursor && theCursor.enable();
-        // eslint-disable-next-line @typescript-eslint/no-unsafe-call
-        theCursor = new CustomCursor(".cursor", {
-          focusElements: [
-            {
-              selector: ".mark-body-container",
-              focusClass: "cursor--focused-view",
-            },
-          ],
-        }).initialize();
-      }
-    }
-  }
-);
-let theCursor = null as any;
-onMounted(() => {
-  if (store.isTrackMode) {
-    // eslint-disable-next-line @typescript-eslint/no-unsafe-call
-    theCursor = new CustomCursor(".cursor", {
-      focusElements: [
-        {
-          selector: ".mark-body-container",
-          focusClass: "cursor--focused-view",
-        },
-      ],
-    }).initialize();
-  }
-});
-
-onUnmounted(() => {
-  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
-  theCursor && theCursor.destroy();
-});
-
-defineEmits(["error"]);
-</script>
-<style scoped>
-.cursor {
-  color: #ff5050;
-  display: none;
-  pointer-events: none;
-  user-select: none;
-  top: 0;
-  left: 0;
-  position: fixed;
-  will-change: transform;
-  z-index: 1000;
-}
-
-.cursor-border {
-  position: absolute;
-  box-sizing: border-box;
-  align-items: center;
-  border: 1px solid #ff5050;
-  border-radius: 50%;
-  display: flex;
-  justify-content: center;
-  height: 0px;
-  width: 0px;
-  left: 0;
-  top: 0;
-  transform: translate(-50%, -50%);
-  transition: all 360ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-
-.cursor.cursor--initialized {
-  display: block;
-}
-
-.cursor .text {
-  font-size: 2rem;
-  opacity: 0;
-  transition: opacity 80ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-
-.cursor.cursor--off-screen {
-  opacity: 0;
-}
-
-.cursor.cursor--focused .cursor-border,
-.cursor.cursor--focused-view .cursor-border {
-  width: 90px;
-  height: 90px;
-}
-
-.cursor.cursor--focused-view .text {
-  opacity: 1;
-  transition: opacity 360ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-</style>

+ 17 - 18
src/features/check/ObjectiveAnswer.vue

@@ -183,25 +183,30 @@
 </template>
 
 <script lang="ts" setup>
-import {
-  studentObjectiveConfirmData,
-  saveStudentObjectiveConfirmData,
-} from "@/api/checkPage";
 import { message } from "ant-design-vue";
+import { ArrowLeftOutlined, ArrowRightOutlined } from "@ant-design/icons-vue";
 import { onMounted, watch } from "vue";
 import "viewerjs/dist/viewer.css";
 import Viewer from "viewerjs";
-import { store } from "@/store/app";
-import ZoomPaper from "@/components/ZoomPaper.vue";
+import { StudentObjectiveInfo, PaperRecogData, AnswerTagItem } from "@/types";
+
+import {
+  studentObjectiveConfirmData,
+  saveStudentObjectiveConfirmData,
+} from "@/api/checkPage";
+import { doLogout } from "@/api/markPage";
+
+import { useMarkStore } from "@/store";
 import { useTimers } from "@/setups/useTimers";
-import { ArrowLeftOutlined, ArrowRightOutlined } from "@ant-design/icons-vue";
 import vls from "@/utils/storage";
-import { StudentObjectiveInfo, PaperRecogData } from "@/types";
-import { doLogout } from "@/api/markPage";
 import { maxNum } from "@/utils/utils";
 
+import ZoomPaper from "@/components/ZoomPaper.vue";
+
 const { addTimeout } = useTimers();
 
+const markStore = useMarkStore();
+
 const studentIds = $ref(vls.get("check-students", []));
 
 onMounted(async () => {
@@ -232,13 +237,7 @@ let student: StudentObjectiveInfo | null = $ref(null);
 let dataError = $ref(false);
 let answerMap: Record<string, { answer: string; isRight: boolean }> = {};
 
-interface AnswerTagType {
-  mainNumber: number;
-  subNumber: number;
-  answer: string;
-  style: Record<string, string>;
-}
-let answerTags = $ref<AnswerTagType[]>([]);
+let answerTags = $ref<AnswerTagItem[]>([]);
 let optionsBlocks = $ref([]);
 
 const answersComputed = $computed(() => {
@@ -543,11 +542,11 @@ const answerPaperScale = $computed(() => {
       });
     }
   }, 10);
-  const scale = store.setting.uiSetting["answer.paper.scale"];
+  const scale = markStore.setting.uiSetting["answer.paper.scale"];
   return scale * 100 + "%";
 });
 const answerPaperFontSize = $computed(() => {
-  const scale = store.setting.uiSetting["answer.paper.scale"];
+  const scale = markStore.setting.uiSetting["answer.paper.scale"];
   return scale * 14 + "px";
 });
 //#endregion : 放大缩小和之后的滚动

+ 1 - 1
src/features/mark/MarkBodyCursor.vue

@@ -31,9 +31,9 @@
 
 <script setup lang="ts">
 import { watch, onMounted, onUnmounted } from "vue";
-import { useMarkStore } from "@/store";
 import { CheckOutlined } from "@ant-design/icons-vue";
 import CustomCursor from "custom-cursor.js";
+import { useMarkStore } from "@/store";
 
 const markStore = useMarkStore();
 

+ 3 - 0
src/features/mark/stores/mark.ts

@@ -82,6 +82,9 @@ const useMarkStore = defineStore("mark", {
     isTrackMode(state: MarkStore): boolean {
       return state.setting.mode && state.setting.mode === "TRACK";
     },
+    modeName(state: MarkStore): string {
+      return state.setting.mode === "TRACK" ? "轨迹模式" : "普通模式";
+    },
     /** 评卷端的轨迹模式显示轨迹 && 管理后台都显示轨迹 */
     shouldShowTrack(state: MarkStore): boolean {
       // FIXME: 不是最优雅的方式来判断是否是阅卷端

+ 1 - 5
src/features/mark/toolbar/MarkHeader.vue

@@ -126,7 +126,7 @@
 
         <div class="header-text-btn">
           <img src="@/assets/icons/icon-track-mode.svg" class="header-icon" />
-          {{ modeName }}
+          {{ markStore.modeName }}
           <CaretDownOutlined
             v-if="!markStore.setting.forceMode"
             class="a-icon"
@@ -194,10 +194,6 @@ const props = defineProps<{ showTotalScore?: boolean }>();
 
 const markStore = useMarkStore();
 
-const modeName = $computed(() =>
-  markStore.setting.mode === "TRACK" ? "轨迹模式" : "普通模式"
-);
-
 const exchangeModeName = $computed(() =>
   markStore.setting.mode === "TRACK" ? "普通模式" : "轨迹模式"
 );

+ 8 - 9
src/features/reject/Reject.vue

@@ -33,27 +33,26 @@
     <mark-tool :actions="['minimap', 'sizeScale', 'imgScale']" />
 
     <div class="mark-main">
-      <mark-body origImageUrls="sheetUrls" onlyTrack @error="renderError" />
+      <track-body origImageUrls="sheetUrls" onlyTrack @error="renderError" />
       <reject-board v-if="store.currentTask" />
     </div>
 
-    <MinimapModal />
+    <modal-minimap />
   </div>
 </template>
 
 <script setup lang="ts">
 import { onMounted } from "vue";
 import { store } from "@/store/app";
-import MarkTool from "@/features/mark/MarkTool.vue";
-import MarkBody from "../student/studentInspect/MarkBody.vue";
-import MinimapModal from "../mark/MinimapModal.vue";
-
-import RejectBoard from "./RejectBoard.vue";
 import { message } from "ant-design-vue";
-
 import { getMarkTask } from "@/api/markPage";
-import vls from "@/utils/storage";
 import { doLogout } from "@/api/markPage";
+import vls from "@/utils/storage";
+
+import TrackBody from "../track/TrackBody.vue";
+import MarkTool from "../mark/toolbar/MarkTool.vue";
+import ModalMinimap from "../mark/modals/ModalMinimap.vue";
+import RejectBoard from "./RejectBoard.vue";
 
 // taskId
 const rejectParam = vls.get("reject", {});