zhangjie 4 ماه پیش
والد
کامیت
e950db8af9

+ 4 - 1
src/features/mark/MarkBodyBase.vue

@@ -97,7 +97,10 @@ const {
 }>();
 
 const markStore = useMarkStore();
-const { answerPaperScale } = useBodyScroll();
+const { answerPaperScale } = useBodyScroll({
+  shortCut: true,
+  autoScroll: true,
+});
 const { sliceImagesWithTrackList, maxSliceWidth, theFinalHeight } =
   useSliceTrack();
 

+ 49 - 37
src/features/mark/composables/useBodyScroll.ts

@@ -2,15 +2,23 @@ import { useTimers } from "@/setups/useTimers";
 import { useMarkStore } from "@/store";
 import { onMounted, onUnmounted, watch } from "vue";
 
-export default function useBodyScroll() {
+export default function useBodyScroll({
+  shortCut = false,
+  autoScroll = false,
+  containerSelector = ".mark-body-container",
+}: {
+  shortCut?: boolean;
+  autoScroll?: boolean;
+  container?: string;
+} = {}) {
   const { addTimeout } = useTimers();
   const markStore = useMarkStore();
 
   function getContainer(): HTMLDivElement | null {
-    return document.querySelector<HTMLDivElement>(".mark-body-container");
+    return document.querySelector<HTMLDivElement>(containerSelector);
   }
 
-  //缩略图定位
+  // 缩略图定位
   watch(
     () => [markStore.minimapScrollToX, markStore.minimapScrollToY],
     () => {
@@ -33,45 +41,49 @@ export default function useBodyScroll() {
   );
 
   // 快捷键定位
-  const scrollContainerByKey = (e: KeyboardEvent) => {
-    const container = getContainer();
-    if (!container) return;
+  if (shortCut) {
+    const scrollContainerByKey = (e: KeyboardEvent) => {
+      const container = getContainer();
+      if (!container) return;
 
-    if (e.key === "w") {
-      container.scrollBy({ top: -100, behavior: "smooth" });
-    } else if (e.key === "s") {
-      container.scrollBy({ top: 100, behavior: "smooth" });
-    } else if (e.key === "a") {
-      container.scrollBy({ left: -100, behavior: "smooth" });
-    } else if (e.key === "d") {
-      container.scrollBy({ left: 100, behavior: "smooth" });
-    }
-  };
+      if (e.key === "w") {
+        container.scrollBy({ top: -100, behavior: "smooth" });
+      } else if (e.key === "s") {
+        container.scrollBy({ top: 100, behavior: "smooth" });
+      } else if (e.key === "a") {
+        container.scrollBy({ left: -100, behavior: "smooth" });
+      } else if (e.key === "d") {
+        container.scrollBy({ left: 100, behavior: "smooth" });
+      }
+    };
 
-  onMounted(() => {
-    document.addEventListener("keypress", scrollContainerByKey);
-  });
-  onUnmounted(() => {
-    document.removeEventListener("keypress", scrollContainerByKey);
-  });
+    onMounted(() => {
+      document.addEventListener("keypress", scrollContainerByKey);
+    });
+    onUnmounted(() => {
+      document.removeEventListener("keypress", scrollContainerByKey);
+    });
+  }
 
   //  autoScroll自动跳转
-  let oldFirstScoreContainer: HTMLDivElement | null = null;
-  watch(
-    () => markStore.currentTask,
-    () => {
-      if (markStore.setting.autoScroll) {
-        // 给任务清理和动画留一点时间
-        oldFirstScoreContainer =
-          document.querySelector<HTMLDivElement>(".score-container");
-        oldFirstScoreContainer?.scrollIntoView({ behavior: "smooth" });
-        addTimeout(scrollToFirstScore, 1000);
-      } else {
-        const container = getContainer();
-        container?.scrollTo({ top: 0, left: 0, behavior: "smooth" });
+  if (autoScroll) {
+    let oldFirstScoreContainer: HTMLDivElement | null = null;
+    watch(
+      () => markStore.currentTask,
+      () => {
+        if (markStore.setting.autoScroll) {
+          // 给任务清理和动画留一点时间
+          oldFirstScoreContainer =
+            document.querySelector<HTMLDivElement>(".score-container");
+          oldFirstScoreContainer?.scrollIntoView({ behavior: "smooth" });
+          addTimeout(scrollToFirstScore, 1000);
+        } else {
+          const container = getContainer();
+          container?.scrollTo({ top: 0, left: 0, behavior: "smooth" });
+        }
       }
-    }
-  );
+    );
+  }
 
   function scrollToFirstScore() {
     if (markStore.renderLock) {

+ 47 - 39
src/features/student/studentInspect/MarkBody.vue

@@ -1,15 +1,18 @@
 <template>
   <div class="mark-body" @scroll="viewScroll">
     <div ref="dragContainer" class="mark-body-container">
-      <div v-if="!store.currentTask" class="mark-body-none">
+      <div v-if="!markStore.currentTask" class="mark-body-none">
         <div>
           <img src="@/assets/image-none-task.png" />
           <p>
-            {{ store.message }}
+            {{ markStore.message }}
           </p>
         </div>
       </div>
-      <div v-else-if="store.isScanImage" :style="{ width: answerPaperScale }">
+      <div
+        v-else-if="markStore.isScanImage"
+        :style="{ width: answerPaperScale }"
+      >
         <div
           v-for="(item, index) in sliceImagesWithTrackList"
           :key="index"
@@ -118,7 +121,7 @@
         <div>
           <img src="@/assets/image-none-task.png" />
           <p>
-            {{ store.message }}
+            {{ markStore.message }}
           </p>
         </div>
       </div>
@@ -128,7 +131,7 @@
 
 <script setup lang="ts">
 import { reactive, watch } from "vue";
-import { store } from "@/store/app";
+import { useMarkStore } from "@/store";
 import MarkDrawTrack from "@/features/mark/MarkDrawTrack.vue";
 import type {
   SpecialTag,
@@ -167,6 +170,8 @@ const { origImageUrls = "sliceUrls", onlyTrack = false } = defineProps<{
 const emit = defineEmits(["error", "getIsMultComments", "getScrollStatus"]);
 
 const { dragContainer } = useDraggable();
+const markStore = useMarkStore();
+
 const viewScroll = () => {
   if (
     dragContainer.value.scrollTop + dragContainer.value.offsetHeight + 50 >=
@@ -178,10 +183,10 @@ const viewScroll = () => {
 const { addTimeout } = useTimers();
 
 const totalScore = $computed(() => {
-  return store.currentTask?.markerScore || 0;
+  return markStore.currentTask?.markerScore || 0;
 });
 const objectiveScore = $computed(() => {
-  return store.currentTask?.objectiveScore || 0;
+  return markStore.currentTask?.objectiveScore || 0;
 });
 const subjectiveScore = $computed(() => {
   return toPrecision(totalScore - objectiveScore);
@@ -244,10 +249,10 @@ function addTagColorAttr(tList: SpecialTag[]): SpecialTag[] {
 }
 
 async function processImage() {
-  if (!store.currentTask) return;
+  if (!markStore.currentTask) return;
 
   const images = [];
-  const urls = store.currentTask[origImageUrls] || [];
+  const urls = markStore.currentTask[origImageUrls] || [];
   if (!urls.length) return;
   for (const url of urls) {
     const image = await loadImage(url);
@@ -256,7 +261,7 @@ async function processImage() {
 
   maxImageWidth = Math.max(...images.map((i) => i.naturalWidth));
 
-  const trackLists = (store.currentTask.questionList || [])
+  const trackLists = (markStore.currentTask.questionList || [])
     // .map((q) => q.trackList)
     .map((q) => {
       let tList = q.trackList;
@@ -278,14 +283,14 @@ async function processImage() {
     const thisImageTrackList = trackLists.filter(
       (t) => t.offsetIndex === indexInSliceUrls
     );
-    const thisImageTagList = store.currentTask.headerTagList?.length
+    const thisImageTagList = markStore.currentTask.headerTagList?.length
       ? addHeaderTrackColorAttr(
-          (store.currentTask.headerTagList || []).filter(
+          (markStore.currentTask.headerTagList || []).filter(
             (t) => t.offsetIndex === indexInSliceUrls
           )
         )
       : addTagColorAttr(
-          (store.currentTask.specialTagList || []).filter(
+          (markStore.currentTask.specialTagList || []).filter(
             (t) => t.offsetIndex === indexInSliceUrls
           )
         );
@@ -305,7 +310,7 @@ async function processImage() {
   }
 
   // 无答题卡,模式4
-  if (!store.currentTask.cardData?.length) {
+  if (!markStore.currentTask.cardData?.length) {
     const summarys = parseMode4Data();
     if (summarys && summarys.length) {
       sliceImagesWithTrackList[0].summarys = summarys;
@@ -322,15 +327,15 @@ interface AnswerTagItem {
 }
 function paserRecogData(imgDom: HTMLImageElement, imageIndex): AnswerTagItem[] {
   if (
-    !store.currentTask.recogDatas?.length ||
-    !store.currentTask.recogDatas[imageIndex]
+    !markStore.currentTask.recogDatas?.length ||
+    !markStore.currentTask.recogDatas[imageIndex]
   )
     return [];
 
-  const answerMap = store.currentTask.answerMap || {};
+  const answerMap = markStore.currentTask.answerMap || {};
   const { naturalWidth, naturalHeight } = imgDom;
   const recogData: PaperRecogData = JSON.parse(
-    window.atob(store.currentTask.recogDatas[imageIndex])
+    window.atob(markStore.currentTask.recogDatas[imageIndex])
   );
   const answerTags: AnswerTagItem[] = [];
   // const optionsBlocks = [];
@@ -411,13 +416,13 @@ interface QuestionArea {
   qStruct: string;
 }
 function parseQuestionAreas(questions: QuestionItem[]) {
-  if (!questions.length || !store.currentTask.cardData?.length) return [];
+  if (!questions.length || !markStore.currentTask.cardData?.length) return [];
 
   let pictureConfigs: QuestionArea[] = [];
   const structs = questions.map(
     (item) => `${item.mainNumber}_${item.subNumber}`
   );
-  store.currentTask.cardData.forEach((page, pindex) => {
+  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}`;
@@ -484,10 +489,10 @@ function parseQuestionAreas(questions: QuestionItem[]) {
 
 // 获取属于填空题的试题号
 function getFillLines() {
-  if (!store.currentTask.cardData?.length) return {};
+  if (!markStore.currentTask.cardData?.length) return {};
 
   const questions: Record<number, string[]> = {};
-  store.currentTask.cardData.forEach((page) => {
+  markStore.currentTask.cardData.forEach((page) => {
     page.columns.forEach((column) => {
       column.elements.forEach((element) => {
         if (element.type !== "FILL_LINE") return;
@@ -528,7 +533,7 @@ interface MarkDetailItem {
 
 function parseMarkDetailList(): Array<MarkDetailItem[]> {
   const dataList: Array<MarkDetailItem[]> = [];
-  const questions = store.currentTask.questionList || [];
+  const questions = markStore.currentTask.questionList || [];
 
   const fillQues = getFillLines();
   let fillQuestions = [] as Question[];
@@ -759,10 +764,13 @@ interface ObjectiveAnswerTagItem {
 function parseObjectiveAnswerTags() {
   const objectiveAnswerTags: Array<ObjectiveAnswerTagItem[]> = [];
 
-  if (!store.currentTask.cardData?.length || !store.currentTask.answerMap)
+  if (
+    !markStore.currentTask.cardData?.length ||
+    !markStore.currentTask.answerMap
+  )
     return objectiveAnswerTags;
 
-  store.currentTask.cardData.forEach((page, pindex) => {
+  markStore.currentTask.cardData.forEach((page, pindex) => {
     if (!objectiveAnswerTags[pindex]) objectiveAnswerTags[pindex] = [];
 
     page.columns.forEach((column) => {
@@ -816,7 +824,7 @@ function parseObjectiveAnswerTags() {
 
         const questions: Array<{ score: number; totalScore: number }> = [];
         for (let i = 0; i < parent.questionsCount; i++) {
-          const qans = store.currentTask.answerMap[
+          const qans = markStore.currentTask.answerMap[
             `${parent.topicNo}_${i + parent.startNumber}`
           ] || { score: 0, totalScore: 0 };
           questions[i] = {
@@ -847,7 +855,7 @@ interface SummaryItem {
 }
 function parseMode4Data(): SummaryItem[] {
   // 只有单评才展示summary
-  const isDoubleMark = (store.currentTask.questionList || []).some(
+  const isDoubleMark = (markStore.currentTask.questionList || []).some(
     (question) => {
       let userIds = question.trackList.map((track) => track.userId);
       if (
@@ -865,7 +873,7 @@ function parseMode4Data(): SummaryItem[] {
   );
   if (isDoubleMark) return [];
 
-  return (store.currentTask.questionList || []).map((q) => {
+  return (markStore.currentTask.questionList || []).map((q) => {
     let markerName = "";
     if (q.headerTrack && q.headerTrack.length) {
       markerName = q.headerTrack[0].userName;
@@ -899,13 +907,13 @@ const renderPaperAndMark = async () => {
   renderLock = true;
   sliceImagesWithTrackList.splice(0);
 
-  if (!store.currentTask) {
+  if (!markStore.currentTask) {
     renderLock = false;
     return;
   }
 
   try {
-    store.globalMask = true;
+    markStore.globalMask = true;
     await processImage();
   } catch (error) {
     sliceImagesWithTrackList.splice(0);
@@ -914,17 +922,17 @@ const renderPaperAndMark = async () => {
     emit("error");
   } finally {
     await new Promise((res) => setTimeout(res, 500));
-    store.globalMask = false;
+    markStore.globalMask = false;
     renderLock = false;
   }
 };
 
-watch(() => store.currentTask, renderPaperAndMark);
+watch(() => markStore.currentTask, renderPaperAndMark);
 
 watch(
   (): (number | undefined)[] => [
-    store.minimapScrollToX,
-    store.minimapScrollToY,
+    markStore.minimapScrollToX,
+    markStore.minimapScrollToY,
   ],
   () => {
     const container = document.querySelector<HTMLDivElement>(
@@ -933,13 +941,13 @@ watch(
     addTimeout(() => {
       if (
         container &&
-        typeof store.minimapScrollToX === "number" &&
-        typeof store.minimapScrollToY === "number"
+        typeof markStore.minimapScrollToX === "number" &&
+        typeof markStore.minimapScrollToY === "number"
       ) {
         const { scrollWidth, scrollHeight } = container;
         container.scrollTo({
-          top: scrollHeight * store.minimapScrollToY,
-          left: scrollWidth * store.minimapScrollToX,
+          top: scrollHeight * markStore.minimapScrollToY,
+          left: scrollWidth * markStore.minimapScrollToX,
           behavior: "smooth",
         });
       }
@@ -967,7 +975,7 @@ const answerPaperScale = $computed(() => {
       });
     }
   }, 10);
-  const scale = store.setting.uiSetting["answer.paper.scale"];
+  const scale = markStore.setting.uiSetting["answer.paper.scale"];
   return scale * 100 + "%";
 });
 </script>

+ 0 - 7
src/features/student/studentTrack/MarkHeader.vue

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

+ 21 - 18
src/features/student/studentTrack/StudentTrack.vue → src/features/track/Track.vue

@@ -1,26 +1,26 @@
 <template>
   <div class="mark">
     <div class="mark-header">
-      <div v-if="store.currentTask" class="mark-header-part">
+      <div v-if="markStore.currentTask" class="mark-header-part">
         <div class="header-noun">
           <span>课程名称:</span>
           <span>
-            {{ store.currentTask.subject.name }}({{
-              store.currentTask.subject.code
+            {{ markStore.currentTask.subject.name }}({{
+              markStore.currentTask.subject.code
             }})</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>
       <div class="mark-header-part">
@@ -32,27 +32,30 @@
     <mark-tool :actions="['minimap', 'sizeScale', 'imgScale']" />
 
     <div class="mark-main">
-      <mark-body @error="renderError" />
+      <track-body @error="renderError" />
     </div>
   </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 "../studentInspect/MarkBody.vue";
 import { message } from "ant-design-vue";
+import { useMarkStore } from "@/store";
+import { Task } from "@/types";
+import vls from "@/utils/storage";
+
 import {
   getSingleStudentTaskOfStudentTrack,
   getSingleStudentCardData,
 } from "@/api/studentTrackPage";
 import { studentObjectiveConfirmData } from "@/api/checkPage";
-import vls from "@/utils/storage";
 import { doLogout } from "@/api/markPage";
-import { Task } from "@/types";
+
+import MarkTool from "../mark/toolbar/MarkTool.vue";
+import TrackBody from "./TrackBody.vue";
 
 const studentId = $ref(vls.get("check-students", ""));
+const markStore = useMarkStore();
 
 function logout() {
   doLogout();
@@ -68,7 +71,7 @@ async function updateTask() {
 
     if (!res.data.sheetUrls?.length) {
       rawTask.sheetUrls = rawTask.sheetUrls || [];
-      store.message = "暂无数据";
+      markStore.message = "暂无数据";
     } else {
       // 获取客观题选项信息
       const objectiveRes = await studentObjectiveConfirmData(studentId);
@@ -102,11 +105,11 @@ async function updateTask() {
       code: rawTask.courseCode,
       name: rawTask.courseName,
     };
-    store.currentTask = rawTask;
+    markStore.currentTask = rawTask;
 
-    store.setting.doubleTrack = true;
+    markStore.setting.doubleTrack = true;
   } else {
-    store.message = res.data.message;
+    markStore.message = res.data.message;
   }
 
   void message.success({
@@ -124,7 +127,7 @@ onMounted(async () => {
 });
 
 const renderError = () => {
-  store.currentTask = undefined;
-  store.message = "加载失败,请重新加载。";
+  markStore.currentTask = undefined;
+  markStore.message = "加载失败,请重新加载。";
 };
 </script>

+ 376 - 0
src/features/track/TrackBody.vue

@@ -0,0 +1,376 @@
+<template>
+  <div class="mark-body" @scroll="viewScrollHandle">
+    <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 type { SpecialTag, Track } from "@/types";
+import { loadImage, toPrecision } from "@/utils/utils";
+import MarkDrawTrack from "@/features/mark/MarkDrawTrack.vue";
+
+import useDraggable from "@/features/mark/composables/useDraggable";
+import useTrackTag from "./composables/useTrackTag";
+import type {
+  AnswerTagItem,
+  MarkDetailItem,
+  ObjectiveAnswerTagItem,
+  SummaryItem,
+} from "./composables/useTrackTag";
+import useBodyScroll from "@/features/mark/composables/useBodyScroll";
+import useTrack from "./composables/useTrack";
+
+const { answerPaperScale } = useBodyScroll();
+const {
+  parseMarkDetailList,
+  paserRecogData,
+  parseObjectiveAnswerTags,
+  parseMode4Data,
+} = useTrackTag();
+const { addTrackColorAttr, addTagColorAttr, addHeaderTrackColorAttr } =
+  useTrack();
+
+const totalScore = $computed(() => {
+  return markStore.currentTask?.markerScore || 0;
+});
+const objectiveScore = $computed(() => {
+  return markStore.currentTask?.objectiveScore || 0;
+});
+const subjectiveScore = $computed(() => {
+  return toPrecision(totalScore - objectiveScore);
+});
+
+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 viewScrollHandle = () => {
+  if (
+    dragContainer.value.scrollTop + dragContainer.value.offsetHeight + 50 >=
+    dragContainer.value.scrollHeight
+  ) {
+    emit("getScrollStatus");
+  }
+};
+
+// render task
+let sliceImagesWithTrackList: SliceImage[] = reactive([]);
+let maxImageWidth = 0;
+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;
+    }
+  }
+}
+
+// should not render twice at the same time
+const renderPaperAndMark = async () => {
+  if (markStore.renderLock) {
+    console.log("上个任务还未渲染完毕,稍等一秒再尝试渲染");
+    await new Promise((res) => setTimeout(res, 1000));
+    await renderPaperAndMark();
+    return;
+  }
+  markStore.renderLock = true;
+  sliceImagesWithTrackList.splice(0);
+
+  if (!markStore.currentTask) {
+    markStore.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;
+    markStore.renderLock = false;
+  }
+};
+
+watch(() => markStore.currentTask, renderPaperAndMark);
+</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>

+ 69 - 0
src/features/track/composables/useTrack.ts

@@ -0,0 +1,69 @@
+import type { SpecialTag, Track, ColorMap } from "@/types";
+
+export default function useTrack() {
+  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();
+    const 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();
+    const 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;
+  }
+
+  function addHeaderTrackColorAttr(headerTrack: Track[]): Track[] {
+    return headerTrack.map((item: Track) => {
+      item.color = "green";
+      return item;
+    });
+  }
+
+  return {
+    addTrackColorAttr,
+    addTagColorAttr,
+    addHeaderTrackColorAttr,
+  };
+}

+ 601 - 0
src/features/track/composables/useTrackTag.ts

@@ -0,0 +1,601 @@
+import { useMarkStore } from "@/store";
+import type { PaperRecogData, Question } from "@/types";
+import { calcSumPrecision, maxNum } from "@/utils/utils";
+
+export interface MarkDetailUserItem {
+  userId: string;
+  userName: string;
+  prename: string;
+  color: string;
+  scores: Array<{ subNumber: string; score: number }>;
+  score: number;
+}
+export type UserMapType = Record<string, MarkDetailUserItem>;
+export interface MarkDetailItem {
+  mainNumber: number;
+  subNumber: string;
+  isFillQuestion: boolean;
+  score: number;
+  maxScore: number;
+  users: MarkDetailUserItem[];
+  area: QuestionArea;
+  style: Record<string, string>;
+}
+
+export interface QuestionItem {
+  mainNumber: number;
+  subNumber: number | string;
+}
+export interface QuestionArea {
+  i: number;
+  x: number;
+  y: number;
+  w: number;
+  h: number;
+  qStruct: string;
+}
+
+export interface AnswerTagItem {
+  mainNumber: number;
+  subNumber: string;
+  answer: string;
+  style: Record<string, string>;
+}
+
+export interface ObjectiveAnswerTagItem {
+  id: string;
+  mainNumber: number;
+  subNumbers: string;
+  score: number;
+  totalScore: number;
+  style: Record<string, string | number>;
+}
+
+export interface SummaryItem {
+  mainNumber: number;
+  subNumber: string;
+  score: number;
+  markerName: string;
+}
+
+export default function useTrackTag() {
+  const markStore = useMarkStore();
+
+  // 解析识别数据
+  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;
+  }
+
+  // 解析各试题答题区域
+  function parseQuestionAreas(questions: QuestionItem[]) {
+    if (!questions.length || !markStore.currentTask.cardData?.length) return [];
+
+    const 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;
+    });
+    const 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;
+  }
+
+  // 解析各试题答题区域以及评分
+  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;
+  }
+
+  // 解析客观题区域总分
+  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的解析
+  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,
+      };
+    });
+  }
+
+  return {
+    parseMarkDetailList,
+    paserRecogData,
+    parseObjectiveAnswerTags,
+    parseMode4Data,
+  };
+}

+ 46 - 45
src/router/index.ts

@@ -1,28 +1,29 @@
 import { createRouter, createWebHistory } from "vue-router";
-import Mark from "@/features/mark/Mark.vue";
-import ObjectiveAnswer from "@/features/check/ObjectiveAnswer.vue";
-import SubjectiveAnswer from "@/features/check/SubjectiveAnswer.vue";
 
 const routes = [
   { path: "/", redirect: { name: "StudentTrack" } },
-  { path: "/mark", component: Mark, name: "Mark" },
+  {
+    path: "/mark",
+    name: "Mark",
+    component: () => import("@/features/mark/Mark.vue"),
+  },
   {
     // 客观题检查
     path: "/check/objective-answer",
     name: "CheckObjectiveAnswer",
-    component: ObjectiveAnswer,
+    component: () => import("@/features/check/ObjectiveAnswer.vue"),
   },
   {
     // 主观题检查
     path: "/check/subjective-answer",
     name: "CheckSubjectiveAnswer",
-    component: SubjectiveAnswer,
+    component: () => import("@/features/check/SubjectiveAnswer.vue"),
   },
   {
     // 成绩查询-试卷轨迹
     path: "/track/student",
     name: "StudentTrack",
-    component: () => import("@/features/student/studentTrack/StudentTrack.vue"),
+    component: () => import("@/features/track/Track.vue"),
   },
   {
     // 仲裁
@@ -37,44 +38,44 @@ const routes = [
     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: "/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",