Forráskód Böngészése

仲裁及轨迹图

zhangjie 1 éve
szülő
commit
5647417b69

+ 3 - 3
src/api/arbitratePage.ts

@@ -77,7 +77,7 @@ export async function getArbitrateSetting(params: paramType) {
 
 /** 批量仲裁历史 */
 export async function getArbitrateHistory({
-  paperNumber,
+  subjectCode,
   groupNumber,
   examId,
   pageNumber = 1,
@@ -86,14 +86,14 @@ export async function getArbitrateHistory({
   sort = "DESC",
   secretNumber = null,
 }) {
-  if (!paperNumber) return;
+  if (!subjectCode) return;
 
   return httpApp.post<Task[]>(
     "/api/admin/mark/arbitrate/getHistory",
     {},
     {
       params: {
-        paperNumber,
+        paperNumber: subjectCode,
         pageSize,
         order,
         sort,

+ 5 - 3
src/api/studentTrackPage.ts

@@ -3,7 +3,9 @@ import { Task } from "@/types";
 
 /** 查看单个学生的试卷轨迹 */
 export async function getSingleStudentTaskOfStudentTrack(studentId: string) {
-  const form = new FormData();
-  studentId && form.append("studentId", studentId);
-  return httpApp.post<Task>("/api/admin/exam/track/student", form);
+  return httpApp.post<Task>(
+    "/api/admin/mark/track/getTask",
+    {},
+    { params: { studentId } }
+  );
 }

+ 23 - 23
src/devLoginParams.ts

@@ -1,34 +1,34 @@
 // 192.168.11.167:7001
 // 评卷员
-export const LOGIN_CONFIG = {
-  isAdmin: false,
-  loginName: "admin",
-  password: "12345678",
-  arbitrate: {
-    // historyId: "1722865426532139010",
-    examId: "455729804847611904",
-    groupNumber: 2,
-    paperNumber: "bh111002",
-  },
-  // mark: {
-  //   examId: "455729804847611904",
-  //   paperNumber: "bh111002",
-  //   groupNumber: 2,
-  // },
-  schoolCode: "test-school-1",
-};
 // export const LOGIN_CONFIG = {
-//   isAdmin: true,
-//   loginName: "admin1",
+//   isAdmin: false,
+//   loginName: "admin",
 //   password: "12345678",
+//   arbitrate: {
+//     // historyId: "1722865426532139010",
+//     examId: "455729804847611904",
+//     groupNumber: 2,
+//     paperNumber: "bh111002",
+//   },
 //   // mark: {
-//   //   examId: "437537682336260096",
-//   //   paperNumber: "bh102101",
-//   //   groupNumber: 1,
+//   //   examId: "455729804847611904",
+//   //   paperNumber: "bh111002",
+//   //   groupNumber: 2,
 //   // },
 //   schoolCode: "test-school-1",
-//   studentIds: "448443226152513536",
 // };
+export const LOGIN_CONFIG = {
+  isAdmin: true,
+  loginName: "admin1",
+  password: "12345678",
+  // mark: {
+  //   examId: "437537682336260096",
+  //   paperNumber: "bh102101",
+  //   groupNumber: 1,
+  // },
+  schoolCode: "test-school-1",
+  studentIds: "448443226152513536",
+};
 // export const LOGIN_CONFIG = {
 //   isAdmin: true,
 //   loginName: "admin",

+ 5 - 1
src/features/arbitrate/Arbitrate.vue

@@ -5,8 +5,9 @@
     <div class="mark-main">
       <mark-history
         v-if="!isSingleStudent"
+        :examId="examId"
         :subjectCode="paperNumber"
-        :groupNumber="groupNumber"
+        :groupNumber="groupNumber + ''"
         :getHistory="getArbitrateHistory"
       />
       <ArbitrateMarkList />
@@ -33,6 +34,7 @@
   <AnswerModal />
   <PaperModal />
   <MinimapModal />
+  <MarkBoardTrackDialog v-if="store.isTrackMode" @submit="saveTaskToServer" />
 </template>
 
 <script setup lang="ts">
@@ -46,6 +48,8 @@ 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 {
   clearArbitrateTask,

+ 0 - 119
src/features/arbitrate/Markhea1.vue

@@ -1,119 +0,0 @@
-<template>
-  <CommonMarkHeader
-    :isSingleStudent="isSingleStudent"
-    :clearTasks="clearTasks"
-    showPaperAndAnswer
-    showScoreBoard
-  >
-    <span>
-      <span class="header-small-text">待处理</span>
-      <span class="highlight-text">{{ store.status.totalCount ?? "-" }}</span>
-    </span>
-    <span>
-      <span class="header-small-text">已处理</span>
-      <span class="highlight-text">{{ store.status.markedCount ?? "-" }}</span>
-    </span>
-    <template #modeControl>
-      <div class="tw-flex">
-        <!-- <a-dropdown class="header-bg-color"> -->
-
-        <a-button
-          class="header-bg-color"
-          style="
-            color: rgba(255, 255, 255, 0.5);
-            border: none;
-            display: flex;
-            align-items: center;
-          "
-        >
-          <img
-            src="../../assets/trackmode.png"
-            style="
-              width: 11px;
-              height: 12px;
-              display: inline;
-              margin-right: 2px;
-            "
-          />
-          {{ modeName }}
-        </a-button>
-        <!-- </a-dropdown> -->
-      </div>
-    </template>
-  </CommonMarkHeader>
-</template>
-
-<script setup lang="ts">
-import { store } from "@/store/store";
-import { useRoute } from "vue-router";
-import { clearArbitrateTask } from "@/api/arbitratePage";
-import CommonMarkHeader from "@/components/CommonMarkHeader.vue";
-const modeName = $computed(() =>
-  store.setting.mode === "TRACK" ? "轨迹模式" : "普通模式"
-);
-
-const exchangeModeName = $computed(() =>
-  store.setting.mode === "TRACK" ? "普通模式" : "轨迹模式"
-);
-async function toggleSettingMode() {
-  if (store.isTrackMode) {
-    store.setting.mode = "COMMON";
-  } else {
-    store.setting.mode = "TRACK";
-  }
-
-  // store.currentTask.markResult.trackList = store.currentTask.questionList
-  //   .map((q) => q.trackList)
-  //   .flat();
-  // store.currentTask.markResult.specialTagList = [
-  //   ...(store.currentTask.specialTagList ?? []),
-  // ];
-  // store.currentTask.markResult.scoreList = store.currentTask.questionList.map(
-  //   (q) => q.score
-  // );
-  sessionStorage.setItem("arbitrate_local_mode", store.setting.mode);
-
-  const body = document.querySelector("body");
-  if (body) body.innerHTML = "重新加载中...";
-  // 等待一秒后,重新加载页面
-  await new Promise((resolve) => setTimeout(resolve, 1000));
-  window.location.reload();
-}
-
-const route = useRoute();
-let isSingleStudent = !!route.query.historyId;
-const {
-  subjectCode,
-  groupNumber,
-  historyId: taskId,
-} = route.query as {
-  subjectCode: string;
-  groupNumber: string;
-  historyId: string;
-};
-
-let clearTasks = clearArbitrateTask.bind(
-  null,
-  taskId,
-  subjectCode,
-  groupNumber
-);
-</script>
-<style>
-.header-bg-color {
-  background-color: var(--header-bg-color);
-}
-.header-bg-color.ant-btn:hover {
-  background-color: var(--app-ant-select-bg-override-color) !important;
-}
-.header-bg-color.ant-btn:focus {
-  background-color: transparent;
-}
-.dropdown-triangle {
-  background-color: #8c8d9b;
-  width: 7px;
-  height: 5px;
-  clip-path: polygon(0 0, 100% 0, 50% 100%);
-  margin-left: 4px;
-}
-</style>

+ 4 - 0
src/features/mark/MarkHistory.vue

@@ -94,6 +94,7 @@ const {
   groupNumber = undefined,
   markerId = undefined,
   markerScore = undefined,
+  examId = undefined,
   getHistory,
 } = defineProps<{
   title?: string;
@@ -103,6 +104,7 @@ const {
   groupNumber?: string;
   markerId?: string;
   markerScore?: string;
+  examId?: string;
   getHistory: GetHistory;
 }>();
 let searchType = $ref("1");
@@ -185,6 +187,7 @@ EventBus.on("should-reload-history", () => {
         groupNumber,
         markerId,
         markerScore,
+        examId,
         pageSize: limitPageSize,
       });
       if (res?.data) {
@@ -230,6 +233,7 @@ async function updateHistoryTask({
     groupNumber,
     markerId,
     markerScore,
+    examId,
   };
   let key = searchType == "1" ? "secretNumber" : "markerScore";
   params[key] = secretNumber || undefined;

+ 286 - 287
src/features/student/studentInspect/MarkBody.vue

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

+ 48 - 17
src/features/student/studentTrack/StudentTrack.vue

@@ -1,9 +1,38 @@
 <template>
-  <div class="my-container">
-    <mark-header />
-    <div class="tw-flex tw-gap-1">
-      <mark-body v-if="store.isScanImage" @error="renderError" />
-      <CommonMarkBody v-else @error="renderError" />
+  <div class="mark">
+    <div class="mark-header">
+      <div v-if="store.currentTask" class="mark-header-part">
+        <div class="header-noun">
+          <span>课程名称:</span>
+          <span>
+            {{ store.currentTask.subject.name }}({{
+              store.currentTask.subject.code
+            }})</span
+          >
+        </div>
+        <div class="header-noun">
+          <span>试卷编号:</span>
+          <span>{{ store.currentTask.paperNumber }}</span>
+        </div>
+        <div class="header-noun">
+          <span>姓名:</span>
+          <span>{{ store.currentTask.studentName }}</span>
+        </div>
+        <div class="header-noun">
+          <span>学号:</span>
+          <span>{{ store.currentTask.studentCode }}</span>
+        </div>
+      </div>
+      <div class="mark-header-part">
+        <div class="header-text-btn header-logout" @click="logout">
+          <img class="header-icon" src="@/assets/icons/icon-return.svg" />返回
+        </div>
+      </div>
+    </div>
+    <mark-tool :actions="['minimap', 'sizeScale', 'imgScale']" />
+
+    <div class="mark-main">
+      <mark-body @error="renderError" />
     </div>
   </div>
   <MinimapModal />
@@ -12,17 +41,18 @@
 <script setup lang="ts">
 import { onMounted } from "vue";
 import { store } from "@/store/store";
-import MarkHeader from "./MarkHeader.vue";
 import MinimapModal from "@/features/mark/MinimapModal.vue";
+import MarkTool from "@/features/mark/MarkTool.vue";
 import MarkBody from "../studentInspect/MarkBody.vue";
-import CommonMarkBody from "@/features/mark/CommonMarkBody.vue";
 import { message } from "ant-design-vue";
 import { getSingleStudentTaskOfStudentTrack } from "@/api/studentTrackPage";
-// import { getPaper } from "@/api/jsonMark";
-import { addFileServerPrefixToTask } from "@/utils/utils";
 import vls from "@/utils/storage";
 
-const studentId = $ref(vls.get("check-student", ""));
+const studentId = $ref(vls.get("check-students", ""));
+
+function logout() {
+  window.history.go(-1);
+}
 
 async function updateTask() {
   const mkey = "fetch_task_key";
@@ -35,7 +65,14 @@ async function updateTask() {
 
   if (res.data.studentId) {
     let rawTask = res.data;
-    store.currentTask = addFileServerPrefixToTask(rawTask);
+    // newTask.sheetUrls = newTask.sheetUrls || [];
+    rawTask.sheetUrls = ["/1-1.jpg", "/1-2.jpg"];
+    rawTask.sliceUrls = [...rawTask.sheetUrls];
+    rawTask.subject = {
+      code: rawTask.courseCode,
+      name: rawTask.courseName,
+    };
+    store.currentTask = rawTask;
   } else {
     store.message = res.data.message;
   }
@@ -54,9 +91,3 @@ const renderError = () => {
   store.message = "加载失败,请重新加载。";
 };
 </script>
-
-<style scoped>
-.my-container {
-  width: 100%;
-}
-</style>

+ 2 - 2
src/router/index.ts

@@ -4,7 +4,7 @@ import ObjectiveAnswer from "@/features/check/ObjectiveAnswer.vue";
 import SubjectiveAnswer from "@/features/check/SubjectiveAnswer.vue";
 
 const routes = [
-  { path: "/", redirect: { name: "Arbitrate" } },
+  { path: "/", redirect: { name: "StudentTrack" } },
   { path: "/mark", component: Mark, name: "Mark" },
   {
     // 客观题检查
@@ -26,7 +26,7 @@ const routes = [
   },
   {
     // 仲裁
-    path: "/admin/exam/arbitrate/start",
+    path: "/arbitrate",
     name: "Arbitrate",
     component: () => import("@/features/arbitrate/Arbitrate.vue"),
   },

+ 2 - 0
src/types/index.ts

@@ -185,6 +185,8 @@ interface RawTask {
   studentCode: string;
   /** 考试编号 */
   examNumber: string;
+  /** 试卷编号 */
+  paperNumber?: string;
   /** 一般不要用此处的subject,优先用setting.subject */
   subject?: Subject;
   /** 裁切图url */