瀏覽代碼

feat: 普通模式默认添加轨迹

zhangjie 2 月之前
父節點
當前提交
f28f7f5a42

+ 6 - 5
src/features/check-subjective/MarkBodyBase.vue

@@ -21,7 +21,7 @@
         :class="[`rotate-board-${rotateBoard}`]"
       >
         <template
-          v-for="(item, index) in sliceImagesWithTrackList"
+          v-for="(item, index) in markStore.sliceImagesWithTrackList"
           :key="index"
         >
           <div class="single-image-container">
@@ -90,8 +90,9 @@ const { answerPaperScale } = useBodyScroll({
   shortCut: true,
   autoScroll: true,
 });
-const { rotateBoard, sliceImagesWithTrackList, maxSliceWidth, theFinalHeight } =
+const { rotateBoard, maxSliceWidth, theFinalHeight, addRenderWatch } =
   useSliceTrack(hasMarkResultToRender);
+addRenderWatch();
 
 // 图片拖动。在轨迹模式下,仅当没有选择分数时可用。
 const { dragContainer } = useDraggable();
@@ -111,7 +112,7 @@ function initWatch() {
   // 清除分数轨迹
   watchEffect(() => {
     for (const track of markStore.removeScoreTracks) {
-      for (const sliceImage of sliceImagesWithTrackList) {
+      for (const sliceImage of markStore.sliceImagesWithTrackList) {
         sliceImage.markerTrackList = sliceImage.markerTrackList.filter(
           (t) =>
             !(
@@ -129,7 +130,7 @@ function initWatch() {
   // 清除特殊标记轨迹
   watchEffect(() => {
     if (!markStore.currentTask) return;
-    for (const sliceImage of sliceImagesWithTrackList) {
+    for (const sliceImage of markStore.sliceImagesWithTrackList) {
       sliceImage.tagList = sliceImage.tagList.filter((t) =>
         markStore.currentTaskEnsured.markResult?.markerTagList.find(
           (st) =>
@@ -140,7 +141,7 @@ function initWatch() {
       );
     }
     if (markStore.currentTaskEnsured.markResult?.markerTagList.length === 0) {
-      for (const sliceImage of sliceImagesWithTrackList) {
+      for (const sliceImage of markStore.sliceImagesWithTrackList) {
         sliceImage.tagList = [];
       }
     }

+ 14 - 21
src/features/check-subjective/composables/useSliceTrack.ts

@@ -1,5 +1,4 @@
 import { ref, watch } from "vue";
-import type { SliceImage } from "@/types";
 import { useMarkStore } from "@/store";
 import { loadImage } from "@/utils/utils";
 import EventBus from "@/plugins/eventBus";
@@ -11,7 +10,6 @@ export default function useSliceTrack() {
   const { addTrackColorAttr, addHeaderTrackColorAttr, addTagColorAttr } =
     useTrackColor();
 
-  const sliceImagesWithTrackList = $ref<SliceImage[]>([]);
   const maxSliceWidth = ref(0); // 最大的裁切块宽度,图片容器以此为准
   const theFinalHeight = ref(0); // 最终宽度,用来定位轨迹在第几张图片,不包括image-seperator高度
 
@@ -26,7 +24,7 @@ export default function useSliceTrack() {
 
     maxSliceWidth.value = Math.max(...images.map((i) => i.naturalWidth));
     theFinalHeight.value = Math.max(...images.map((i) => i.naturalHeight));
-    sliceImagesWithTrackList.splice(0);
+    markStore.sliceImagesWithTrackList.splice(0);
 
     const trackLists = (markStore.currentTask.questionList || [])
       .map((q) => {
@@ -57,7 +55,7 @@ export default function useSliceTrack() {
         (t) => t.offsetIndex === indexInSliceUrls
       );
 
-      sliceImagesWithTrackList.push({
+      markStore.sliceImagesWithTrackList.push({
         url,
         indexInSliceUrls,
         markerTrackList: thisImageTrackList,
@@ -89,7 +87,7 @@ export default function useSliceTrack() {
       markStore.globalMask = true;
       await processImage();
     } catch (error) {
-      sliceImagesWithTrackList.splice(0);
+      markStore.sliceImagesWithTrackList.splice(0);
       console.trace("render error ", error);
       // 图片加载出错,自动加载下一个任务
       EventBus.emit("body-render-error");
@@ -99,21 +97,16 @@ export default function useSliceTrack() {
     }
   };
 
-  // 在阻止渲染的情况下,watchEffect收集不到 store.currentTask 的依赖,会导致本组件不再更新
-  watch(
-    () => markStore.currentTask,
-    () => {
-      setTimeout(renderPaperAndMark, 50);
-    }
-  );
-
-  watch(
-    () => sliceImagesWithTrackList,
-    () => {
-      EventBus.emit("draw-change", sliceImagesWithTrackList);
-    },
-    { deep: true }
-  );
+  function addRenderWatch() {
+    // 监听 currentTask 变化,重新渲染
+    // 在阻止渲染的情况下,watchEffect收集不到 store.currentTask 的依赖,会导致本组件不再更新
+    watch(
+      () => markStore.currentTask,
+      () => {
+        setTimeout(renderPaperAndMark, 50);
+      }
+    );
+  }
 
-  return { maxSliceWidth, theFinalHeight, sliceImagesWithTrackList };
+  return { maxSliceWidth, theFinalHeight, addRenderWatch };
 }

+ 7 - 8
src/features/mark/MarkBodyBase.vue

@@ -24,7 +24,7 @@
         :class="[`rotate-board-${rotateBoard}`]"
       >
         <template
-          v-for="(item, index) in sliceImagesWithTrackList"
+          v-for="(item, index) in markStore.sliceImagesWithTrackList"
           :key="index"
         >
           <div class="single-image-container">
@@ -93,11 +93,10 @@ const { answerPaperScale } = useBodyScroll({
   shortCut: true,
   autoScroll: true,
 });
-const { rotateBoard, sliceImagesWithTrackList } = useSliceTrack(
-  hasMarkResultToRender
-);
-const { showBigImage } = useBigImage();
+const { rotateBoard, addRenderWatch } = useSliceTrack(hasMarkResultToRender);
+addRenderWatch();
 
+const { showBigImage } = useBigImage();
 // 在轨迹模式下,仅当没有选择分数时可用。
 const { dragContainer } = useDraggable();
 
@@ -116,7 +115,7 @@ function initWatch() {
   // 清除分数轨迹
   watchEffect(() => {
     for (const track of markStore.removeScoreTracks) {
-      for (const sliceImage of sliceImagesWithTrackList) {
+      for (const sliceImage of markStore.sliceImagesWithTrackList) {
         sliceImage.markerTrackList = sliceImage.markerTrackList.filter(
           (t) =>
             !(
@@ -134,7 +133,7 @@ function initWatch() {
   // 清除特殊标记轨迹
   watchEffect(() => {
     if (!markStore.currentTask) return;
-    for (const sliceImage of sliceImagesWithTrackList) {
+    for (const sliceImage of markStore.sliceImagesWithTrackList) {
       sliceImage.tagList = sliceImage.tagList.filter((t) =>
         markStore.currentTaskEnsured.markResult?.markerTagList.find(
           (st) =>
@@ -145,7 +144,7 @@ function initWatch() {
       );
     }
     if (markStore.currentTaskEnsured.markResult?.markerTagList.length === 0) {
-      for (const sliceImage of sliceImagesWithTrackList) {
+      for (const sliceImage of markStore.sliceImagesWithTrackList) {
         sliceImage.tagList = [];
       }
     }

+ 109 - 1
src/features/mark/composables/useMakeTrack.ts

@@ -1,9 +1,14 @@
-import { SliceImage, SpecialTag, Track } from "@/types";
+import { SliceImage, SpecialTag, Track, Question, SplitConfig } from "@/types";
 import { useMarkStore } from "@/store";
+import { randomCode } from "@/utils/utils";
 
 export default function useMakeTrack() {
   const markStore = useMarkStore();
 
+  function getTrackId(data: { mainNumber: number; subNumber: number }): string {
+    return `${data.mainNumber}-${data.subNumber}-${randomCode()}`;
+  }
+
   // 标记分数轨迹
   function makeScoreTrack(event: MouseEvent, item: SliceImage) {
     if (
@@ -15,6 +20,10 @@ export default function useMakeTrack() {
 
     const target = event.target as HTMLImageElement;
     const track: Track = {
+      id: getTrackId({
+        mainNumber: markStore.currentQuestion?.mainNumber,
+        subNumber: markStore.currentQuestion?.subNumber,
+      }),
       mainNumber: markStore.currentQuestion?.mainNumber,
       subNumber: markStore.currentQuestion?.subNumber,
       score: markStore.currentScore,
@@ -89,6 +98,10 @@ export default function useMakeTrack() {
 
     const target = event.target as HTMLImageElement;
     const track: SpecialTag = {
+      id: getTrackId({
+        mainNumber: markStore.currentQuestion?.mainNumber,
+        subNumber: markStore.currentQuestion?.subNumber,
+      }),
       mainNumber: markStore.currentQuestion?.mainNumber,
       subNumber: markStore.currentQuestion?.subNumber,
       tagName: markStore.currentSpecialTag,
@@ -131,9 +144,104 @@ export default function useMakeTrack() {
     }
   }
 
+  function getRandomPosition(area: SplitConfig) {
+    const { x, y, w, h } = area;
+    return {
+      x: x + w / 4 + (Math.random() * w) / 2,
+      y: y + h / 4 + (Math.random() * h) / 2,
+    };
+  }
+
+  function checkPointInArea(
+    point: { x: number; y: number },
+    area: { x: number; y: number; w: number; h: number }
+  ): boolean {
+    return (
+      point.x >= area.x &&
+      point.x <= area.x + area.w &&
+      point.y >= area.y &&
+      point.y <= area.y + area.h
+    );
+  }
+
+  // 标记普通模式轨迹
+  function makeCommonTrack(question: Question, score: number) {
+    if (!markStore.currentTask || question.problem) return;
+
+    markStore.currentQuestion = question;
+    const markResult = markStore.currentTaskEnsured.markResult;
+    markResult.scoreList[question.__index] = score;
+    const questionTrack = markResult.markerTrackList.find(
+      (t) =>
+        t.mainNumber === question.mainNumber &&
+        t.subNumber === question.subNumber
+    );
+    // 存在轨迹时直接使用已有的轨迹位置
+    if (questionTrack) {
+      questionTrack.score = score;
+      // 更新渲染轨迹中的分值
+      let viewTrack = null;
+      markStore.sliceImagesWithTrackList.forEach((item) => {
+        if (viewTrack || item.indexInSliceUrls !== questionTrack.offsetIndex)
+          return;
+        viewTrack = item.markerTrackList.find((t) => {
+          return t.id === questionTrack.id;
+        });
+        if (!viewTrack) return;
+        viewTrack.score = score;
+      });
+      return;
+    }
+
+    // 通过题目设置的第一个评卷区,随机生成一个轨迹
+    const { picList } = question;
+    const area = picList[0] || null;
+    if (!area) return;
+
+    const imageSize = markStore.currentTaskSliceImages[area.i];
+    if (!imageSize) return;
+
+    // 不存在时,在第一个评卷区的中心50%的区域随机一个点
+    const { x, y } = getRandomPosition(area);
+    const track: Track = {
+      id: getTrackId({
+        mainNumber: question.mainNumber,
+        subNumber: question.subNumber,
+      }),
+      mainNumber: question.mainNumber,
+      subNumber: question.subNumber,
+      score,
+      unanswered: false,
+      offsetIndex: area.i,
+      offsetX: imageSize.width * x,
+      offsetY: imageSize.height * y,
+      number: -1,
+    };
+    markResult.markerTrackList.push(track);
+    // 将轨迹更新到渲染轨迹中
+    markStore.sliceImagesWithTrackList.forEach((item) => {
+      if (item.indexInSliceUrls !== area.i) return;
+
+      if (
+        checkPointInArea(
+          { x: track.offsetX, y: track.offsetY },
+          {
+            x: item.dx,
+            y: item.dy,
+            w: item.sliceImageWidth,
+            h: item.sliceImageHeight,
+          }
+        )
+      ) {
+        item.markerTrackList.push(track);
+      }
+    });
+  }
+
   return {
     makeScoreTrack,
     makeSpecialTagTrack,
     makeTrack,
+    makeCommonTrack,
   };
 }

+ 0 - 7
src/features/mark/composables/useMarkSubmit.ts

@@ -138,9 +138,6 @@ export default function useMarkSubmit() {
         q.markerScore = q.markerTrackList.reduce((acc, t) => {
           return acc + (t.score || 0);
         }, 0);
-        if (!markStore.isTrackMode) {
-          q.markerScore = datas.scoreList[q.__index];
-        }
         return q;
       }) as Question[];
 
@@ -160,10 +157,6 @@ export default function useMarkSubmit() {
 
     if (!checkMarkResult(markResult)) return;
 
-    if (!markStore.isTrackMode) {
-      markResult.markerTrackList = [];
-    }
-
     console.log("save task to server");
     void message.loading({ content: "保存评卷任务..." });
     const res = await saveTask(getSaveTaskResult()).catch(() => false);

+ 27 - 27
src/features/mark/composables/useSliceTrack.ts

@@ -14,7 +14,6 @@ export default function useSliceTrack(hasMarkResultToRender = false) {
   const { addTimeout } = useTimers();
 
   const rotateBoard = ref(0);
-  const sliceImagesWithTrackList = $ref<SliceImage[]>([]);
   const maxSliceWidth = ref(0); // 最大的裁切块宽度,图片容器以此为准
   const theFinalHeight = ref(0); // 最终宽度,用来定位轨迹在第几张图片,不包括image-seperator高度
 
@@ -33,12 +32,19 @@ export default function useSliceTrack(hasMarkResultToRender = false) {
     if (markStore.currentTask.sliceConfig.some((v) => v.i > sliceNum)) {
       console.warn("裁切图设置的数量小于该学生的总图片数量");
     }
+    markStore.currentTaskSliceImages = {};
     markStore.currentTask.sliceConfig =
       markStore.currentTask.sliceConfig.filter((v) => v.i <= sliceNum);
     for (const sliceConfig of markStore.currentTask.sliceConfig) {
       // 在原图基础上设置遮盖区之后的图,用作制作阅卷裁切图的原图
       const url = markStore.currentTask.sliceUrls[sliceConfig.i - 1];
       const image = await loadImage(url);
+      if (!markStore.currentTaskSliceImages[sliceConfig.i]) {
+        markStore.currentTaskSliceImages[sliceConfig.i] = {
+          width: image.naturalWidth,
+          height: image.naturalHeight,
+        };
+      }
       images[sliceConfig.i] = image;
       const { x, y, w, h } = sliceConfig;
       x < 0 && (sliceConfig.x = 0);
@@ -137,17 +143,16 @@ export default function useSliceTrack(hasMarkResultToRender = false) {
     }
 
     // console.log("render: ", store.currentTask.secretNumber);
-    if (sliceImagesWithTrackList.length === 0) {
+    if (markStore.sliceImagesWithTrackList.length === 0) {
       // 初次渲染,不做动画
-      sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
-      // 没抽象好,这里不好做校验
-      // const renderedTrackAndTagNumber = sliceImagesWithTrackList.map(s => s.markerTrackList.length + s.tagList.length).reduce((p,c) => p+ c);
-      // if(renderedTrackAndTagNumber === thisIma)
+      markStore.sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
     } else {
       rotateBoard.value = 1;
       setTimeout(() => {
-        sliceImagesWithTrackList.splice(0);
-        sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
+        markStore.sliceImagesWithTrackList.splice(0);
+        markStore.sliceImagesWithTrackList.push(
+          ...tempSliceImagesWithTrackList
+        );
         setTimeout(() => {
           rotateBoard.value = 0;
         }, 300);
@@ -301,8 +306,8 @@ export default function useSliceTrack(hasMarkResultToRender = false) {
 
     rotateBoard.value = 1;
     addTimeout(() => {
-      sliceImagesWithTrackList.splice(0);
-      sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
+      markStore.sliceImagesWithTrackList.splice(0);
+      markStore.sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
       addTimeout(() => {
         rotateBoard.value = 0;
       }, 300);
@@ -337,7 +342,7 @@ export default function useSliceTrack(hasMarkResultToRender = false) {
         await processSplitConfig();
       }
     } catch (error) {
-      sliceImagesWithTrackList.splice(0);
+      markStore.sliceImagesWithTrackList.splice(0);
       console.trace("render error ", error);
       // 图片加载出错,自动加载下一个任务
       EventBus.emit("body-render-error");
@@ -347,26 +352,21 @@ export default function useSliceTrack(hasMarkResultToRender = false) {
     }
   };
 
-  // 在阻止渲染的情况下,watchEffect收集不到 store.currentTask 的依赖,会导致本组件不再更新
-  watch(
-    () => markStore.currentTask,
-    () => {
-      setTimeout(renderPaperAndMark, 50);
-    }
-  );
-
-  watch(
-    () => sliceImagesWithTrackList,
-    () => {
-      EventBus.emit("draw-change", sliceImagesWithTrackList);
-    },
-    { deep: true }
-  );
+  function addRenderWatch() {
+    // 监听 currentTask 变化,重新渲染
+    // 在阻止渲染的情况下,watchEffect收集不到 store.currentTask 的依赖,会导致本组件不再更新
+    watch(
+      () => markStore.currentTask,
+      () => {
+        setTimeout(renderPaperAndMark, 50);
+      }
+    );
+  }
 
   return {
     rotateBoard,
     maxSliceWidth,
     theFinalHeight,
-    sliceImagesWithTrackList,
+    addRenderWatch,
   };
 }

+ 5 - 5
src/features/mark/scoring/MarkBoardKeyBoard.vue

@@ -146,6 +146,7 @@ import { isNumber } from "lodash-es";
 import { ref, onMounted, onUnmounted, watch } from "vue";
 import { useMarkStore } from "@/store";
 import useAutoChooseFirstQuestion from "../composables/useAutoChooseFirstQuestion";
+import useMakeTrack from "../composables/useMakeTrack";
 import { message } from "ant-design-vue";
 
 const emit = defineEmits([
@@ -162,6 +163,7 @@ const {
   isArbitrated,
   getArbitratedStatusName,
 } = useAutoChooseFirstQuestion();
+const { makeCommonTrack } = useMakeTrack();
 const hasModifyScore = ref(false);
 
 const markStore = useMarkStore();
@@ -302,11 +304,9 @@ function numberKeyListener(event: KeyboardEvent) {
       void message.error({ content: "输入的分数不在有效间隔内", duration: 5 });
       return;
     }
-    const { __index } = markStore.currentQuestion;
-    markStore.currentTask.markResult.scoreList[__index] = score;
-    //
-    // scoreStr = "";
-    // console.log("give score", score);
+    makeCommonTrack(markStore.currentQuestion, score);
+
+    // 跳到下一题
     const idx = indexOfCurrentQuestion();
     if (idx + 1 < markStore.currentTask.questionList.length) {
       chooseQuestion(markStore.currentTask.questionList[idx + 1]);

+ 4 - 7
src/features/mark/scoring/MarkBoardMouse.vue

@@ -133,6 +133,7 @@ import { ref } from "vue";
 import type { Question } from "@/types";
 import { useMarkStore } from "@/store";
 import useAutoChooseFirstQuestion from "../composables/useAutoChooseFirstQuestion";
+import useMakeTrack from "../composables/useMakeTrack";
 
 const markStore = useMarkStore();
 const {
@@ -141,6 +142,7 @@ const {
   isArbitrated,
   getArbitratedStatusName,
 } = useAutoChooseFirstQuestion();
+const { makeCommonTrack } = useMakeTrack();
 
 const emit = defineEmits([
   "submit",
@@ -153,9 +155,7 @@ const props = defineProps<{ isCheckAnswer?: boolean }>();
 const hasModifyScore = ref(false);
 
 // 切换给分模式
-const normalMode = ref(
-  markStore.setting.uiSetting["normal.mode"] || "keyboard"
-);
+const normalMode = ref(markStore.setting.uiSetting["normal.mode"] || "mouse");
 function normalModeChange(value: string) {
   markStore.setting.uiSetting["normal.mode"] = value;
 }
@@ -170,10 +170,7 @@ function chooseScore(question: Question, score: number) {
   // 只要修改了分值,就当做已修改
   hasModifyScore.value = true;
 
-  markStore.currentQuestion = question;
-  const { __index } = markStore.currentQuestion;
-  markStore.currentTask &&
-    (markStore.currentTask.markResult.scoreList[__index] = score);
+  makeCommonTrack(question, score);
 }
 function isCurrentScore(question: Question, score: number) {
   const { __index } = question;

+ 5 - 12
src/features/mark/scoring/MarkBoardTrack.vue

@@ -208,8 +208,6 @@ import { useMarkStore } from "@/store";
 import useAutoChooseFirstQuestion from "../composables/useAutoChooseFirstQuestion";
 import useDragSplitPane from "../composables/useDragSplitPane";
 import useFocusTracks from "../composables/useFocusTracks";
-import EventBus from "@/plugins/eventBus";
-import { cloneDeep } from "lodash-es";
 
 const props = defineProps<{ modal?: boolean; isCheckAnswer?: boolean }>();
 const emit = defineEmits(["submit", "unselectiveSubmit", "checkSubmit"]);
@@ -256,17 +254,16 @@ const rightBlur = () => {
   removeFocusTrack();
 };
 const positioning = (question: Question) => {
-  // addFocusTrack(undefined, question.mainNumber, question.subNumber, true, list);
-  console.log(
-    "sliceImagesWithTrackListCopy:",
-    sliceImagesWithTrackListCopy.value
-  );
+  // console.log(
+  //   "sliceImagesWithTrackListCopy:",
+  //   markStore.sliceImagesWithTrackListCopy
+  // );
   addFocusTrack(
     undefined,
     question.mainNumber,
     question.subNumber,
     true,
-    sliceImagesWithTrackListCopy.value || []
+    markStore.sliceImagesWithTrackListCopy || []
   );
   activeRightMenuItem.value = null;
 };
@@ -277,10 +274,6 @@ const {
   isArbitrated,
   getArbitratedStatusName,
 } = useAutoChooseFirstQuestion();
-let sliceImagesWithTrackListCopy = ref([]);
-EventBus.on("draw-change", (list: any) => {
-  sliceImagesWithTrackListCopy.value = cloneDeep(list);
-});
 
 // 切换题目是清空上一题的分数
 watch(

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

@@ -18,7 +18,7 @@ const useMarkStore = defineStore("mark", {
       uiSetting: {
         "answer.paper.scale": 1,
         "score.board.collapse": false,
-        "normal.mode": "keyboard",
+        "normal.mode": "",
         "score.fontSize.scale": 1,
         "paper.modal": false,
         "answer.modal": false,
@@ -42,6 +42,8 @@ const useMarkStore = defineStore("mark", {
     curStatus: {},
     tasks: [],
     currentTask: undefined,
+    currentTaskSliceImages: undefined,
+    sliceImagesWithTrackList: [],
     // 主观题检查时,缓存已经修改过的试题
     currentTaskModifyQuestion: {},
     currentQuestion: undefined,

+ 0 - 1
src/plugins/eventBus.ts

@@ -2,7 +2,6 @@ import mitt from "mitt";
 
 type Events = {
   "should-reload-history": undefined;
-  "draw-change": any;
   "body-render-error": any;
 };
 

+ 27 - 18
src/types/index.ts

@@ -47,6 +47,8 @@ export interface MarkStore {
   tasks: Array<Task>;
   /** 用来切换task,还有回看 */
   currentTask?: Task;
+  currentTaskSliceImages?: Record<number, { width: number; height: number }>;
+  sliceImagesWithTrackList: SliceImage[];
   currentQuestion?: Question;
   currentScore?: number;
   currentSpecialTag?: string;
@@ -317,7 +319,7 @@ interface RawQuestion {
   /** 大题号 */
   mainNumber: number;
   /** 小题号 */
-  subNumber: string;
+  subNumber: number;
   /** 分数间隔 */
   intervalScore: number;
   /** 默认分数 */
@@ -330,6 +332,9 @@ interface RawQuestion {
   maxScore: number;
   /** 限制最小分数 */
   minScore: number;
+  picList: SplitConfig[];
+  // 1-单选,2-多选,3-判断,4-填空,5-问答
+  questionType: 1 | 2 | 3 | 4 | 5;
   /** 未计分 */
   uncalculate: boolean;
   /** 是否自己评卷,false时为他人评卷,给分板置灰 */
@@ -377,10 +382,12 @@ export interface ColorMap {
 }
 /** 轨迹数据 */
 export interface Track {
+  // 轨迹id: ${mainNumber}_${subNumber}_${randomCode(16)} */
+  id: string;
   /** 大题号 */
   mainNumber: number;
   /** 小题号,当前api中只有number  */
-  subNumber: string;
+  subNumber: number;
   /** 前端使用,暂时用不着,赋0 */
   number: number;
   /** 评分数 */
@@ -402,10 +409,12 @@ export interface Track {
 
 /** 特殊标记数据 */
 export interface SpecialTag {
+  // 轨迹id: ${mainNumber}_${subNumber}_${randomCode(16)} */
+  id: string;
   /** 大题号 */
   mainNumber: number;
   /** 小题号,当前api中只有number */
-  subNumber: string;
+  subNumber: number;
   /** 第几张图 */
   offsetIndex: number;
   /** 左上角为原点(原图的原点),及相对原图的位置比例 */
@@ -597,7 +606,7 @@ export interface RichTextBlockJSON {
 
 export interface StudentAnswer {
   mainNumber: number;
-  subNumber: string;
+  subNumber: number;
   subIndex: string;
   answer: Array<RichTextJSON> | null;
 }
@@ -605,7 +614,7 @@ export interface StudentAnswer {
 /** 云平台试卷格式 */
 export type ECSPaperJSON = {
   mainNumber: number;
-  subNumber: string;
+  subNumber: number;
   body: RichTextJSON;
   parentBody: RichTextJSON | null;
   answer: RichTextJSON;
@@ -647,7 +656,7 @@ export type StudentObjectiveInfo = {
   sheetUrls: Array<{ index: number; url: string; recogData: string }>;
   answers: Array<{
     mainNumber: number;
-    subNumber: string;
+    subNumber: number;
     answer: string;
     exist: boolean;
     questionType: string;
@@ -674,7 +683,7 @@ export type StudentSubjectiveInfo = {
   sheetUrls: Array<{ index: number; url: string }>;
   answers: Array<{
     mainNumber: number;
-    subNumber: string;
+    subNumber: number;
     answer: string;
     exist: boolean;
     questionType: string;
@@ -710,14 +719,14 @@ export interface CardData {
   content: string;
 }
 
-export interface SubjectiveQuestion extends RawQuestion {
-  picList: SplitConfig[];
-  // 1-单选,2-多选,3-判断,4-填空,5-问答
-  questionType: 1 | 2 | 3 | 4 | 5;
-}
+// export interface SubjectiveQuestion extends RawQuestion {
+//   picList: SplitConfig[];
+//   // 1-单选,2-多选,3-判断,4-填空,5-问答
+//   questionType: 1 | 2 | 3 | 4 | 5;
+// }
 export interface objectiveQuestion {
   mainNumber: number;
-  subNumber: string;
+  subNumber: number;
   answer: string;
   exist: boolean;
   standardAnswer: string;
@@ -739,7 +748,7 @@ export interface StudentTrackTask {
   objectiveScore: number;
   subjectiveScore: number;
   sheetUrls: Array<{ index: number; url: string; recogData: string }>;
-  subjectiveQuestions: SubjectiveQuestion[];
+  subjectiveQuestions: RawQuestion[];
   objectiveQuestions: objectiveQuestion[];
   cardContent: string;
 }
@@ -750,7 +759,7 @@ export interface MarkDetailUserItem {
   userName: string;
   prename: string;
   color: string;
-  scores: Array<{ subNumber: string; score: number }>;
+  scores: Array<{ subNumber: number; score: number }>;
   score: number;
 }
 export type UserMapType = Record<string, MarkDetailUserItem>;
@@ -765,7 +774,7 @@ export interface QuestionArea {
 }
 export interface MarkDetailItem {
   mainNumber: number;
-  subNumber: string;
+  subNumber: number;
   isFillQuestion: boolean;
   score: number;
   maxScore: number;
@@ -776,7 +785,7 @@ export interface MarkDetailItem {
 
 export interface AnswerTagItem {
   mainNumber: number;
-  subNumber: string;
+  subNumber: number;
   answer: string;
   style: Record<string, string>;
 }
@@ -792,7 +801,7 @@ export interface ObjectiveAnswerTagItem {
 
 export interface TrackSummaryItem {
   mainNumber: number;
-  subNumber: string;
+  subNumber: number;
   score: number;
   markerName: string;
 }