Bladeren bron

feat: 阅卷调整

zhangjie 3 maanden geleden
bovenliggende
commit
a2df7ed8f6

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "stmms-web",
-  "version": "3.3.0",
+  "version": "3.4.4.",
   "private": "true",
   "scripts": {
     "start": "vite --host 0.0.0.0",

+ 11 - 5
src/api/markPage.ts

@@ -30,11 +30,11 @@ export async function getSetting() {
 }
 
 /** 获取评卷状态 */
-export async function getStatus() {
+export async function getStatus(questionModel: Setting["questionModal"]) {
   return httpApp.post<MarkStore["status"]>(
     "/api/mark/getStatus",
     {},
-    { params: getMarkInfo() }
+    { params: { questionModel, ...getMarkInfo() } }
   );
 }
 
@@ -48,14 +48,19 @@ export async function getGroup() {
 }
 
 /** 获取评卷任务 */
-export async function getTask() {
-  return httpApp.post<Task>("/api/mark/getTask", {}, { params: getMarkInfo() });
+export async function getTask(questionModal: Setting["questionModal"]) {
+  return httpApp.post<Task>(
+    "/api/mark/getTask",
+    {},
+    { params: { questionModal, ...getMarkInfo() } }
+  );
 }
 
 /** 更新评卷UI */
 export async function updateUISetting(
   mode?: Setting["mode"],
-  uiSetting?: UISetting
+  uiSetting?: UISetting,
+  questionModal?: Setting["questionModal"]
 ) {
   return httpApp.post<void>(
     "/api/mark/updateSetting",
@@ -64,6 +69,7 @@ export async function updateUISetting(
       params: {
         mode: mode || undefined,
         uiSetting: uiSetting ? JSON.stringify(uiSetting) : undefined,
+        questionModal: questionModal || undefined,
         ...getMarkInfo(),
       },
     }

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

@@ -78,8 +78,11 @@ import ModalAnswer from "../mark/modals/ModalAnswer.vue";
 import ModalPaper from "../mark/modals/ModalPaper.vue";
 import ModalMinimap from "../mark/modals/ModalMinimap.vue";
 
-const markStore = useMarkStore();
+// composables
+import useTaskWatch from "../mark/composables/useTaskWatch";
 
+const markStore = useMarkStore();
+useTaskWatch();
 const { paperNumber, groupNumber, examId, arbitrateId } = vls.get(
   "arbitrate",
   {}

+ 2 - 0
src/features/check-subjective/CheckSubjective.vue

@@ -150,6 +150,7 @@ import ModalShortCut from "../mark/modals/ModalShortCut.vue";
 // composables
 import useTask from "./composables/useTask";
 import useSetting from "./composables/useSetting";
+import useTaskWatch from "../mark/composables/useTaskWatch";
 
 const {
   examId,
@@ -167,6 +168,7 @@ vls.set("mark", {
 });
 
 const markStore = useMarkStore();
+useTaskWatch();
 const {
   getNextStudent,
   getPreviousStudent,

+ 5 - 1
src/features/check-subjective/composables/useSliceTrack.ts

@@ -10,7 +10,11 @@ export default function useSliceTrack() {
   const emit = defineEmits(["error"]);
 
   const markStore = useMarkStore();
-  const { addTrackColorAttr, addSpecialTrackColorAttr } = useTrackColor();
+  const {
+    addTrackColorAttr,
+    addHeaderTrackColorAttr,
+    addSpecialTrackColorAttr,
+  } = useTrackColor();
 
   const sliceImagesWithTrackList = $ref<SliceImage[]>([]);
   const maxSliceWidth = ref(0); // 最大的裁切块宽度,图片容器以此为准

+ 8 - 0
src/features/check-subjective/composables/useTrackColor.ts

@@ -39,8 +39,16 @@ export default function useTrackColor() {
     });
   }
 
+  function addHeaderTrackColorAttr(headerTrack: any): any {
+    return headerTrack.map((item: any) => {
+      item.color = "green";
+      return item;
+    });
+  }
+
   return {
     addTrackColorAttr,
     addSpecialTrackColorAttr,
+    addHeaderTrackColorAttr,
   };
 }

+ 4 - 4
src/features/mark/Mark.vue

@@ -81,16 +81,17 @@ import ModalSheetView from "./modals/ModalSheetView.vue";
 import ModalShortCut from "./modals/ModalShortCut.vue";
 
 // composables
+import useTaskWatch from "./composables/useTaskWatch";
 import useMarkTask from "./composables/useMarkTask";
 import useSetting from "./composables/useSetting";
 import useStatus from "./composables/useStatus";
 import useMarkSubmit from "./composables/useMarkSubmit";
 
 const markStore = useMarkStore();
+useTaskWatch();
 const { updateMarkTask, nextTask, removeBrokenTask } = useMarkTask();
 const { updateSetting } = useSetting();
-const { statusSpinning, loadingStatusSpinning, updateStatus, updateGroups } =
-  useStatus();
+const { statusSpinning, loadingStatusSpinning, updateStatus } = useStatus();
 const { saveTaskToServer, allZeroSubmit, unselectiveSubmit } = useMarkSubmit();
 
 const { addInterval } = useTimers();
@@ -98,9 +99,8 @@ onMounted(async () => {
   let result = true;
   try {
     await updateMarkTask();
-    await updateStatus();
     await updateSetting();
-    await updateGroups();
+    await updateStatus();
     await nextTask();
   } catch (error) {
     loadingStatusSpinning.value = false;

+ 2 - 1
src/features/mark/MarkHistory.vue

@@ -83,10 +83,11 @@ import { watch } from "vue";
 import { message } from "ant-design-vue";
 import { useMarkStore } from "@/store";
 import EventBus from "@/plugins/eventBus";
-import { preDrawImageHistory } from "@/utils/utils";
+import useDraw from "./composables/useDraw";
 
 const limitPageSize = 20;
 const markStore = useMarkStore();
+const { preDrawImageHistory } = useDraw();
 
 const {
   title = "回评",

+ 383 - 0
src/features/mark/composables/useDraw.ts

@@ -0,0 +1,383 @@
+import { useMarkStore } from "@/store";
+import { PictureSlice, Task } from "@/types";
+import { loadImage } from "@/utils/utils";
+
+// 存放裁切图的ObjectUrls
+let objectUrlMap = new Map<string, string>();
+const OBJECT_URLS_MAP_MAX_SIZE =
+  window.APP_OPTIONS?.OBJECT_URLS_MAP_MAX_SIZE ?? 100;
+
+// 清理缓存的过时数据(清除头10张),First in first out
+function cacheFIFO() {
+  if (objectUrlMap.size > OBJECT_URLS_MAP_MAX_SIZE) {
+    const ary = [...objectUrlMap.entries()];
+    const toRelease = ary.splice(0, 10);
+    // 为了避免部分图片还没显示就被revoke了,这里做一个延迟revoke
+    // 此处有个瑕疵,缩略图的显示与试卷不是同时显示,是有可能被清除了的,只能让用户刷新了。 => 见下面的fix
+    for (const u of toRelease) {
+      // 如果当前图片仍在引用 objectUrl , 则将其放入缓存中
+      if (document.querySelector(`img[src="${u[1]}"]`)) {
+        ary.push(u);
+      } else {
+        URL.revokeObjectURL(u[1]);
+      }
+    }
+    objectUrlMap = new Map(ary);
+  }
+}
+
+export default function useDraw() {
+  const markStore = useMarkStore();
+
+  async function getDataUrlForSliceConfig(
+    image: HTMLImageElement,
+    sliceConfig: PictureSlice,
+    maxSliceWidth: number,
+    urlForCache: string
+  ) {
+    const { i, x, y, w, h } = sliceConfig;
+    const key = `${urlForCache}-${i}-${x}-${y}-${w}-${h}`;
+
+    if (objectUrlMap.get(key)) {
+      console.log("cached slice objectUrl");
+      return objectUrlMap.get(key);
+    }
+
+    const canvas = document.createElement("canvas");
+    canvas.width = Math.max(sliceConfig.w, maxSliceWidth);
+    canvas.height = sliceConfig.h;
+    const ctx = canvas.getContext("2d");
+    if (!ctx) {
+      console.log('canvas.getContext("2d") error');
+      throw new Error("canvas ctx error");
+    }
+    // drawImage 画图软件透明色
+    ctx.drawImage(
+      image,
+      sliceConfig.x,
+      sliceConfig.y,
+      sliceConfig.w,
+      sliceConfig.h,
+      0,
+      0,
+      sliceConfig.w,
+      sliceConfig.h
+    );
+    // console.log(image, canvas.height, sliceConfig, ctx);
+    // console.log(canvas.toDataURL());
+
+    // 如果用toBlob,则产生异步,而且URL.createObjectURL还会需要手动释放
+    // const dataurl = canvas.toDataURL();
+    const blob: Blob = await new Promise((res) => {
+      canvas.toBlob((b) => res(b));
+    });
+    const dataurl = URL.createObjectURL(blob);
+
+    cacheFIFO();
+
+    objectUrlMap.set(key, dataurl);
+
+    return dataurl;
+  }
+
+  async function getDataUrlForSplitConfig(
+    image: HTMLImageElement,
+    config: [number, number],
+    maxSliceWidth: number,
+    urlForCache: string
+  ) {
+    const [start, end] = config;
+    const key = `${urlForCache}-${start}-${end}`;
+
+    if (objectUrlMap.get(key)) {
+      console.log("cached split objectUrl");
+      return objectUrlMap.get(key);
+    }
+
+    const width = image.naturalWidth * (end - start);
+    const canvas = document.createElement("canvas");
+    canvas.width = Math.max(width, maxSliceWidth);
+    canvas.height = image.naturalHeight;
+    const ctx = canvas.getContext("2d");
+    if (!ctx) {
+      console.log('canvas.getContext("2d") error');
+      throw new Error("canvas ctx error");
+    }
+    // drawImage 画图软件透明色
+    ctx.drawImage(
+      image,
+      image.naturalWidth * start,
+      0,
+      image.naturalWidth * end,
+      image.naturalHeight,
+      0,
+      0,
+      image.naturalWidth * end,
+      image.naturalHeight
+    );
+
+    // 如果用toBlob,则产生异步,而且URL.createObjectURL还会需要手动释放
+    // const dataurl = canvas.toDataURL();
+    const blob: Blob = await new Promise((res) => {
+      canvas.toBlob((b) => res(b));
+    });
+    const dataurl = URL.createObjectURL(blob);
+    cacheFIFO();
+
+    objectUrlMap.set(key, dataurl);
+    return dataurl;
+  }
+
+  async function getDataUrlForCoverConfig(
+    image: HTMLImageElement,
+    configs: PictureSlice[]
+  ) {
+    const key = `${image.src}-slice`;
+    if (objectUrlMap.get(key)) {
+      return objectUrlMap.get(key);
+    }
+
+    const canvas = document.createElement("canvas");
+    canvas.width = image.naturalWidth;
+    canvas.height = image.naturalHeight;
+    const ctx = canvas.getContext("2d");
+    if (!ctx) {
+      console.log('canvas.getContext("2d") error');
+      throw new Error("canvas ctx error");
+    }
+    ctx.drawImage(image, 0, 0);
+    ctx.fillStyle = "#ffffff";
+    configs.forEach((config) => {
+      ctx.fillRect(config.x, config.y, config.w, config.h);
+    });
+
+    const blob: Blob = await new Promise((res) => {
+      canvas.toBlob((b) => res(b));
+    });
+    const dataurl = URL.createObjectURL(blob);
+    cacheFIFO();
+    objectUrlMap.set(key, dataurl);
+    return dataurl;
+  }
+
+  async function preDrawImage(_currentTask: Task | undefined) {
+    // console.log("preDrawImage=>curTask:", _currentTask);
+
+    if (!_currentTask?.taskId) return;
+
+    let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准
+
+    const hasSliceConfig = _currentTask?.sliceConfig?.length;
+
+    const images = [];
+
+    if (hasSliceConfig) {
+      // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
+      const sliceNum = _currentTask.sliceUrls.length;
+      if (_currentTask.sliceConfig.some((v) => v.i > sliceNum)) {
+        console.warn("裁切图设置的数量小于该学生的总图片数量");
+      }
+      _currentTask.sliceConfig = _currentTask.sliceConfig.filter(
+        (v) => v.i <= sliceNum
+      );
+      for (const sliceConfig of _currentTask.sliceConfig) {
+        const url = _currentTask.sliceUrls[sliceConfig.i - 1];
+        const image = await loadImage(url);
+        images[sliceConfig.i] = image;
+        const { x, y, w, h } = sliceConfig;
+        x < 0 && (sliceConfig.x = 0);
+        y < 0 && (sliceConfig.y = 0);
+        if (sliceConfig.w === 0 && sliceConfig.h === 0) {
+          // 选择整图时,w/h 为0
+          sliceConfig.w = image.naturalWidth;
+          sliceConfig.h = image.naturalHeight;
+        }
+        if (x <= 1 && y <= 1 && sliceConfig.w <= 1 && sliceConfig.h <= 1) {
+          sliceConfig.x = image.naturalWidth * x;
+          sliceConfig.y = image.naturalHeight * y;
+          sliceConfig.w = image.naturalWidth * w;
+          sliceConfig.h = image.naturalHeight * h;
+        }
+      }
+
+      maxSliceWidth = Math.max(..._currentTask.sliceConfig.map((v) => v.w));
+      // 用来保存sliceImage在整个图片容器中(不包括image-seperator)的高度范围
+      for (const sliceConfig of _currentTask.sliceConfig) {
+        const url = _currentTask.sliceUrls[sliceConfig.i - 1];
+        const image = images[sliceConfig.i];
+
+        try {
+          await getDataUrlForSliceConfig(
+            image,
+            sliceConfig,
+            maxSliceWidth,
+            url
+          );
+        } catch (error) {
+          console.log("preDrawImage failed: ", error);
+        }
+      }
+    } else {
+      for (const url of _currentTask.sliceUrls) {
+        const image = await loadImage(url);
+        images.push(image);
+      }
+
+      const splitConfigPairs = markStore.setting.splitConfig.reduce<
+        [number, number][]
+      >((a, v, index) => {
+        index % 2 === 0 ? a.push([v, -1]) : (a.at(-1)![1] = v);
+        return a;
+      }, []);
+
+      const maxSplitConfig = Math.max(...markStore.setting.splitConfig);
+      maxSliceWidth =
+        Math.max(...images.map((v) => v.naturalWidth)) * maxSplitConfig;
+
+      for (const url of _currentTask.sliceUrls) {
+        for (const config of splitConfigPairs) {
+          const indexInSliceUrls = _currentTask.sliceUrls.indexOf(url) + 1;
+          const image = images[indexInSliceUrls - 1];
+
+          try {
+            await getDataUrlForSplitConfig(image, config, maxSliceWidth, url);
+          } catch (error) {
+            console.log("preDrawImage failed: ", error);
+          }
+        }
+      }
+    }
+  }
+
+  async function processSliceUrls(_currentTask: Task | undefined) {
+    if (!_currentTask?.taskId) return;
+
+    const getNum = (num) => Math.max(Math.min(1, num), 0);
+
+    const sheetUrls = _currentTask.sheetUrls || [];
+    const sheetConfig = (markStore.setting.sheetConfig || []).map((item) => {
+      return { ...item };
+    });
+
+    const urls = [];
+    for (let i = 0; i < sheetUrls.length; i++) {
+      const url = sheetUrls[i];
+      const configs = sheetConfig.filter((item) => item.i === i + 1);
+      if (!configs.length) {
+        urls[i] = url;
+        continue;
+      }
+      const image = await loadImage(url);
+      configs.forEach((item) => {
+        item.x = image.naturalWidth * getNum(item.x);
+        item.y = image.naturalHeight * getNum(item.y);
+        item.w = image.naturalWidth * getNum(item.w);
+        item.h = image.naturalHeight * getNum(item.h);
+      });
+
+      urls[i] = await getDataUrlForCoverConfig(image, configs);
+    }
+    return urls;
+  }
+
+  async function preDrawImageHistory(_currentTask: Task | undefined) {
+    console.log("preDrawImageHistory=>curTask:", _currentTask);
+
+    if (!_currentTask?.taskId) return;
+
+    let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准
+
+    const hasSliceConfig = _currentTask?.sliceConfig?.length;
+    // _currentTask.sheetUrls = ["/1-1.jpg", "/1-2.jpg"];
+    _currentTask.sliceUrls = await processSliceUrls(_currentTask);
+
+    const images = [];
+
+    if (hasSliceConfig) {
+      // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
+      const sliceNum = _currentTask.sliceUrls.length;
+      if (_currentTask.sliceConfig.some((v) => v.i > sliceNum)) {
+        console.warn("裁切图设置的数量小于该学生的总图片数量");
+      }
+      _currentTask.sliceConfig = _currentTask.sliceConfig.filter(
+        (v) => v.i <= sliceNum
+      );
+      for (const sliceConfig of _currentTask.sliceConfig) {
+        const url = _currentTask.sliceUrls[sliceConfig.i - 1];
+        const image = await loadImage(url);
+        images[sliceConfig.i] = image;
+        const { x, y, w, h } = sliceConfig;
+        x < 0 && (sliceConfig.x = 0);
+        y < 0 && (sliceConfig.y = 0);
+        if (sliceConfig.w === 0 && sliceConfig.h === 0) {
+          // 选择整图时,w/h 为0
+          sliceConfig.w = image.naturalWidth;
+          sliceConfig.h = image.naturalHeight;
+        }
+        if (x <= 1 && y <= 1 && sliceConfig.w <= 1 && sliceConfig.h <= 1) {
+          sliceConfig.x = image.naturalWidth * x;
+          sliceConfig.y = image.naturalHeight * y;
+          sliceConfig.w = image.naturalWidth * w;
+          sliceConfig.h = image.naturalHeight * h;
+        }
+      }
+
+      maxSliceWidth = Math.max(..._currentTask.sliceConfig.map((v) => v.w));
+
+      // 用来保存sliceImage在整个图片容器中(不包括image-seperator)的高度范围
+      for (const sliceConfig of _currentTask.sliceConfig) {
+        const url = _currentTask.sliceUrls[sliceConfig.i - 1];
+        const image = images[sliceConfig.i];
+
+        try {
+          await getDataUrlForSliceConfig(
+            image,
+            sliceConfig,
+            maxSliceWidth,
+            url
+          );
+        } catch (error) {
+          console.log("preDrawImage failed: ", error);
+        }
+      }
+    } else {
+      for (const url of _currentTask.sliceUrls) {
+        const image = await loadImage(url);
+        images.push(image);
+      }
+
+      const splitConfigPairs = markStore.setting.splitConfig.reduce<
+        [number, number][]
+      >((a, v, index) => {
+        index % 2 === 0 ? a.push([v, -1]) : (a.at(-1)![1] = v);
+        return a;
+      }, []);
+
+      const maxSplitConfig = Math.max(...markStore.setting.splitConfig);
+      maxSliceWidth =
+        Math.max(...images.map((v) => v.naturalWidth)) * maxSplitConfig;
+
+      for (const url of _currentTask.sliceUrls) {
+        for (const config of splitConfigPairs) {
+          const indexInSliceUrls = _currentTask.sliceUrls.indexOf(url) + 1;
+          const image = images[indexInSliceUrls - 1];
+
+          try {
+            await getDataUrlForSplitConfig(image, config, maxSliceWidth, url);
+          } catch (error) {
+            console.log("preDrawImage failed: ", error);
+          }
+        }
+      }
+    }
+  }
+
+  return {
+    getDataUrlForSliceConfig,
+    getDataUrlForSplitConfig,
+    getDataUrlForCoverConfig,
+    preDrawImage,
+    processSliceUrls,
+    preDrawImageHistory,
+  };
+}

+ 8 - 4
src/features/mark/composables/useMarkTask.ts

@@ -1,12 +1,15 @@
 import { clearMarkTask, getTask } from "@/api/markPage";
 import { useMarkStore } from "@/store";
-import { processSliceUrls } from "@/utils/utils";
 import { nextTick, watch } from "vue";
-import { useTaskRejection } from "./useTaskRejection";
+import useTaskRejection from "./useTaskRejection";
+import useDraw from "./useDraw";
+import useStatus from "./useStatus";
 
 export default function useMarkTask() {
   const markStore = useMarkStore();
   const { showRejectedReason } = useTaskRejection();
+  const { updateStatus } = useStatus();
+  const { processSliceUrls } = useDraw();
 
   let preDrawing = false;
 
@@ -15,8 +18,8 @@ export default function useMarkTask() {
   }
 
   async function updateTask() {
-    const res = await getTask();
-    if (res.data?.taskId) {
+    const res = await getTask(markStore.setting.questionModal);
+    if (res.data) {
       const newTask = res.data;
       newTask.sheetUrls = newTask.sheetUrls || [];
       try {
@@ -42,6 +45,7 @@ export default function useMarkTask() {
     } else {
       markStore.message = res.message || "数据错误";
     }
+
     if (!res.data && markStore.message === "成功")
       markStore.message = "当前无评卷任务";
   }

+ 3 - 6
src/features/mark/composables/useSliceTrack.ts

@@ -2,18 +2,15 @@ import { message } from "ant-design-vue";
 import { ref } from "vue";
 import type { SliceImage } from "@/types";
 import { useMarkStore } from "@/store";
-import {
-  getDataUrlForSliceConfig,
-  getDataUrlForSplitConfig,
-  loadImage,
-} from "@/utils/utils";
+import { loadImage } from "@/utils/utils";
 import EventBus from "@/plugins/eventBus";
-
+import useDraw from "./useDraw";
 // 计算裁切图和裁切图上的分数轨迹和特殊标记轨迹
 export default function useSliceTrack() {
   const emit = defineEmits(["error"]);
 
   const markStore = useMarkStore();
+  const { getDataUrlForSliceConfig, getDataUrlForSplitConfig } = useDraw();
 
   const rotateBoard = ref(0);
   const sliceImagesWithTrackList = $ref<SliceImage[]>([]);

+ 2 - 8
src/features/mark/composables/useStatus.ts

@@ -1,5 +1,5 @@
 import { useMarkStore } from "@/store";
-import { getStatus, getGroup } from "@/api/markPage";
+import { getStatus } from "@/api/markPage";
 import { useTimers } from "@/setups/useTimers";
 import { ref, watch } from "vue";
 
@@ -13,15 +13,10 @@ export default function useStatus() {
   const loadingStatusSpinning = ref(true);
 
   async function updateStatus() {
-    const res = await getStatus();
+    const res = await getStatus(markStore.setting.questionModal);
     markStore.setInfo({ status: res.data });
   }
 
-  async function updateGroups() {
-    const res = await getGroup();
-    markStore.setInfo({ groups: res.data });
-  }
-
   function setupStatusWatch() {
     watch(
       () => markStore.setting.statusValue,
@@ -40,7 +35,6 @@ export default function useStatus() {
     statusSpinning,
     loadingStatusSpinning,
     updateStatus,
-    updateGroups,
     setupStatusWatch,
   };
 }

+ 46 - 46
src/features/mark/composables/useTaskRejection.ts

@@ -2,54 +2,54 @@ import { Modal } from "ant-design-vue";
 import { h } from "vue";
 import type { Task } from "@/types";
 
-export function useTaskRejection() {
+export default function useTaskRejection() {
   const showRejectedReason = (task: Task) => {
-    if (task.rejected && task.rejectReason) {
-      const [reasonType, reasonDesc] = task.rejectReason.split(":");
-      Modal.info({
-        title: null,
-        closable: false,
-        maskClosable: false,
-        centered: true,
-        icon: null,
-        okText: "知道了",
-        wrapClassName: "custom-modal-info",
-        zIndex: 9999,
-        bodyStyle: {
-          padding: "15px",
-        },
-        content: () =>
-          h(
-            "div",
-            {
-              style: {
-                fontSize: "14px",
-                color: "var(--app-main-text-color)",
-              },
+    if (!task || !task.rejectReason || !task.rejectReason) return;
+
+    const [reasonType, reasonDesc] = task.rejectReason.split(":");
+    Modal.info({
+      title: null,
+      closable: false,
+      maskClosable: false,
+      centered: true,
+      icon: null,
+      okText: "知道了",
+      wrapClassName: "custom-modal-info",
+      zIndex: 9999,
+      bodyStyle: {
+        padding: "15px",
+      },
+      content: () =>
+        h(
+          "div",
+          {
+            style: {
+              fontSize: "14px",
+              color: "var(--app-main-text-color)",
             },
-            [
-              h("div", { style: { marginBottom: "8px" } }, [
-                h(
-                  "span",
-                  { style: { fontWeight: "bold", marginRight: "0.5em" } },
-                  "打回原因: "
-                ),
-                h("span", reasonType),
-              ]),
-              h("div", { style: { fontWeight: "bold" } }, "详情描述: "),
-              h("div", { style: { padding: "4px 2px" } }, reasonDesc),
-              h("div", { style: { marginBottom: "8px" } }, [
-                h(
-                  "span",
-                  { style: { fontWeight: "bold", marginRight: "0.5em" } },
-                  "给分记录: "
-                ),
-                h("span", task.rejectScoreList),
-              ]),
-            ]
-          ),
-      });
-    }
+          },
+          [
+            h("div", { style: { marginBottom: "8px" } }, [
+              h(
+                "span",
+                { style: { fontWeight: "bold", marginRight: "0.5em" } },
+                "打回原因: "
+              ),
+              h("span", reasonType),
+            ]),
+            h("div", { style: { fontWeight: "bold" } }, "详情描述: "),
+            h("div", { style: { padding: "4px 2px" } }, reasonDesc),
+            h("div", { style: { marginBottom: "8px" } }, [
+              h(
+                "span",
+                { style: { fontWeight: "bold", marginRight: "0.5em" } },
+                "给分记录: "
+              ),
+              h("span", task.rejectScoreList),
+            ]),
+          ]
+        ),
+    });
   };
 
   return {

+ 78 - 0
src/features/mark/composables/useTaskWatch.ts

@@ -0,0 +1,78 @@
+import { watch } from "vue";
+
+export default function useTaskWatch() {
+  const markStore = useMarkStore();
+
+  watch(
+    () => markStore.currentTask,
+    () => {
+      // 初始化 task.markResult ,始终保证 task 下有 markResult
+      // 1. 评卷时,如果没有 markResult ,则初始化一个 markResult 给它
+      // 1. 回评时,先清空它的 markResult ,然后初始化一个 markResult 给它
+      if (!markStore.currentTask) return;
+
+      const task = markStore.currentTask;
+      if (task.previous && task.markResult) {
+        task.markResult = undefined;
+      }
+      if (!task.markResult) {
+        // 管理后台可能不设置 questionList, 而且它不用 markResult
+        if (!task.questionList) {
+          task.questionList = [];
+          // return;
+        }
+        // 初始化 __index
+        task.questionList.forEach((q, i, ar) => (ar[i].__index = i));
+
+        task.__markStartTime = Date.now();
+        const statusValue = markStore.setting.statusValue;
+        const { taskId, studentId } = task;
+        task.markResult = {
+          statusValue: statusValue,
+          taskId: taskId,
+          studentId: studentId,
+          spent: 0,
+
+          trackList: task.questionList
+            .map((q) =>
+              q.headerTrack && q.headerTrack.length
+                ? q.headerTrack
+                : q.trackList
+            )
+            .flat(),
+          specialTagList: [...(task.specialTagList ?? [])],
+          scoreList: task.questionList.map((q) => q.score),
+          markerScore: null, // 后期通过 scoreList 自动更新
+
+          problem: false,
+          problemType: "",
+          problemRemark: "",
+          unselective: false,
+        };
+        task.markResult.trackList.forEach((t) => {
+          if (t.unanswered) {
+            t.score = -0;
+          }
+        });
+      }
+    }
+  );
+
+  // 唯一根据 scoreList 自动更新 markerScore
+  watch(
+    () => markStore.currentTask?.markResult.scoreList,
+    () => {
+      if (!markStore.currentTask) return;
+      const scoreList = markStore.currentTask.markResult.scoreList.filter(
+        (v) => v !== null
+      );
+      const result =
+        scoreList.length === 0
+          ? null
+          : scoreList.reduce((acc, v) => (acc += Math.round(v * 1000)), 0) /
+            1000;
+      markStore.currentTask.markResult.markerScore = result;
+    },
+    { deep: true }
+  );
+}

+ 7 - 1
src/features/mark/modals/ModalAnswer.vue

@@ -8,7 +8,8 @@
     @close="close"
   >
     <object
-      :data="markStore.setting.subject.answerUrl"
+      v-if="url"
+      :data="url"
       type="application/pdf"
       frameBorder="0"
       scrolling="auto"
@@ -21,10 +22,15 @@
 
 <script setup lang="ts">
 import { useMarkStore } from "@/store";
+import { computed } from "vue";
 
 const markStore = useMarkStore();
 
 const close = () => {
   markStore.setting.uiSetting["answer.modal"] = false;
 };
+
+const url = computed(() => {
+  return markStore.setting.subject.answerUrl?.url || "";
+});
 </script>

+ 7 - 2
src/features/mark/modals/ModalPaper.vue

@@ -8,7 +8,8 @@
     @close="close"
   >
     <object
-      :data="markStore.setting.subject.paperUrl"
+      v-if="url"
+      :data="url"
       type="application/pdf"
       frameBorder="0"
       scrolling="auto"
@@ -21,10 +22,14 @@
 
 <script setup lang="ts">
 import { useMarkStore } from "@/store";
-
+import { computed } from "vue";
 const markStore = useMarkStore();
 
 const close = () => {
   markStore.setting.uiSetting["paper.modal"] = false;
 };
+
+const url = computed(() => {
+  return markStore.setting.subject.paperUrl?.url || "";
+});
 </script>

+ 7 - 6
src/features/mark/stores/mark.ts

@@ -6,6 +6,7 @@ const useMarkStore = defineStore("mark", {
     setting: {
       mode: "TRACK",
       examType: "SCAN_IMAGE",
+      questionModal: "MULTI",
       forceMode: false,
       sheetView: false,
       autoScroll: false,
@@ -153,13 +154,13 @@ const useMarkStore = defineStore("mark", {
       };
 
       const fileServer = this.setting.fileServer;
-      if (this.setting.subject?.answerUrl) {
-        this.setting.subject.answerUrl =
-          fileServer + this.setting.subject?.answerUrl;
+      if (this.setting.subject?.answerUrl?.url) {
+        this.setting.subject.answerUrl.url =
+          fileServer + this.setting.subject?.answerUrl.url;
       }
-      if (this.setting.subject?.paperUrl) {
-        this.setting.subject.paperUrl =
-          fileServer + this.setting.subject?.paperUrl;
+      if (this.setting.subject?.paperUrl?.url) {
+        this.setting.subject.paperUrl.url =
+          fileServer + this.setting.subject?.paperUrl.url;
       }
     },
     toggleHistory(): void {

+ 1 - 0
src/features/mark/toolbar/MarkChangeProfileDialog.vue

@@ -36,6 +36,7 @@
 </template>
 
 <script setup lang="ts">
+// 暂时也不需要修改个人信息
 import { changeUserInfo, doLogout } from "@/api/markPage";
 import { message } from "ant-design-vue";
 import { reactive, watchEffect } from "vue";

+ 0 - 23
src/features/mark/toolbar/MarkHeader.vue

@@ -133,18 +133,6 @@
           />
         </div>
       </a-dropdown>
-      <div
-        class="header-text-btn"
-        :title="
-          markStore.setting.groupTitle + '-' + markStore.setting.groupNumber
-        "
-        @click="openSwitchGroupModal"
-      >
-        <img class="header-icon" src="@/assets/icons/icon-group.svg" />{{
-          "分组:" + markStore.setting.groupNumber
-        }}
-        <CaretDownOutlined v-if="markStore.groups.length > 1" class="a-icon" />
-      </div>
       <div class="header-text-btn">
         <img class="header-icon" src="@/assets/icons/icon-user.svg" />{{
           markStore.setting.userName
@@ -174,8 +162,6 @@
       </div>
     </div>
   </div>
-  <!-- <MarkChangeProfileDialog /> -->
-  <MarkSwitchGroupDialog ref="switchGroupRef" />
 </template>
 
 <script setup lang="ts">
@@ -187,9 +173,6 @@ import { isNumber } from "lodash-es";
 import { useMarkStore } from "@/store";
 import { doLogout, updateUISetting, clearMarkTask } from "@/api/markPage";
 
-// import MarkChangeProfileDialog from "./MarkChangeProfileDialog.vue";
-import MarkSwitchGroupDialog from "./MarkSwitchGroupDialog.vue";
-
 const props = defineProps<{ showTotalScore?: boolean }>();
 
 const markStore = useMarkStore();
@@ -247,12 +230,6 @@ const logout = async () => {
   doLogout();
 };
 
-type ShowModalFunc = () => void;
-let switchGroupRef = $ref<InstanceType<typeof MarkSwitchGroupDialog>>();
-const openSwitchGroupModal = () => {
-  (switchGroupRef.showModal as ShowModalFunc)();
-};
-
 watchEffect(() => {
   if (
     isNumber(markStore.setting.topCount) &&

+ 1 - 0
src/features/mark/toolbar/MarkSwitchGroupDialog.vue

@@ -40,6 +40,7 @@
   </a-modal>
 </template>
 <script setup lang="ts">
+// 已经没有分组概念了,这里暂时废弃
 import { getGroup } from "@/api/markPage";
 import { onUpdated } from "vue";
 import { useMarkStore } from "@/store";

+ 54 - 7
src/features/mark/toolbar/MarkTool.vue

@@ -10,15 +10,15 @@
         <p>全卷</p>
       </div>
       <div
-        v-if="checkValid('minimap')"
+        v-if="checkValid('paper') && markStore.setting.subject.paperUrl"
         :class="[
           'mark-tool-item',
-          { 'is-active': markStore.setting.uiSetting['minimap.modal'] },
+          { 'is-active': markStore.setting.uiSetting['answer.modal'] },
         ]"
-        @click="toThumbnail"
+        @click="toPaper"
       >
-        <img src="@/assets/icons/icon-thumbnail.svg" />
-        <p>缩略图</p>
+        <img src="@/assets/icons/icon-answer.svg" />
+        <p>试卷</p>
       </div>
       <div
         v-if="checkValid('answer') && markStore.setting.subject.answerUrl"
@@ -31,6 +31,17 @@
         <img src="@/assets/icons/icon-answer.svg" />
         <p>答案</p>
       </div>
+      <div
+        v-if="checkValid('minimap')"
+        :class="[
+          'mark-tool-item',
+          { 'is-active': markStore.setting.uiSetting['minimap.modal'] },
+        ]"
+        @click="toThumbnail"
+      >
+        <img src="@/assets/icons/icon-thumbnail.svg" />
+        <p>缩略图</p>
+      </div>
       <div
         v-if="checkValid('issuePaper')"
         class="mark-tool-item"
@@ -136,6 +147,10 @@
       </div>
     </div>
     <div>
+      <div class="mark-tool-item" @click="toSwitchQuestionModal">
+        <img src="@/assets/icons/icon-eye-green.svg" />
+        <p>{{ questionModalName }}</p>
+      </div>
       <div class="mark-tool-item" @click="toEyecare">
         <img src="@/assets/icons/icon-eye-green.svg" />
         <p>护眼模式</p>
@@ -182,14 +197,17 @@
 import { computed, onMounted, onUnmounted } from "vue";
 import { useMarkStore } from "@/store";
 import { Modal } from "ant-design-vue";
+import { updateUISetting } from "@/api/markPage";
+
 import MarkProblemDialog from "./MarkProblemDialog.vue";
 
 const markStore = useMarkStore();
 
 /**
  * allPage:全卷
- * minimap:缩略图
+ * paper:试卷
  * answer:答案
+ * minimap:缩略图
  * issuePaper:问题试卷
  * sizeScale:标记大小
  * shortCut:快捷键
@@ -202,7 +220,7 @@ const { actions = [] } = defineProps<{
 
 type ShowModalFunc = () => void;
 
-const checkValid = (name) => {
+const checkValid = (name: string) => {
   if (!actions.length) return true;
   return actions.includes(name);
 };
@@ -221,6 +239,10 @@ const toAnswer = () => {
   markStore.setting.uiSetting["answer.modal"] =
     !markStore.setting.uiSetting["answer.modal"];
 };
+const toPaper = () => {
+  markStore.setting.uiSetting["paper.modal"] =
+    !markStore.setting.uiSetting["paper.modal"];
+};
 const toIssuePaper = () => {
   (problemRef.showModal as ShowModalFunc)();
 };
@@ -277,6 +299,31 @@ function toEyecare() {
   }
   window.localStorage.setItem("eyecareMode", eyecareMode);
 }
+// 阅卷模式切换
+const questionModalName = computed(() => {
+  return markStore.setting.questionModal === "SINGLE"
+    ? "按小题阅卷"
+    : "阅全部题目";
+});
+const toSwitchQuestionModal = () => {
+  const qname =
+    markStore.setting.questionModal === "SINGLE" ? "阅全部题目" : "按小题阅卷";
+
+  Modal.confirm({
+    title: "操作警告",
+    content: `确定切换为${qname}吗?`,
+    async onOk() {
+      markStore.setting.questionModal =
+        markStore.setting.questionModal === "SINGLE" ? "MULTI" : "SINGLE";
+      await updateUISetting(
+        markStore.setting.mode,
+        markStore.setting.uiSetting,
+        markStore.setting.questionModal
+      );
+      window.location.reload();
+    },
+  });
+};
 
 function clearLatestTagOfCurrentTask() {
   if (!markStore.currentTask?.markResult) return;

+ 45 - 29
src/types/index.ts

@@ -66,6 +66,8 @@ export interface Setting {
   examType: "SCAN_IMAGE" | "MULTI_MEDIA";
   /** 阅卷模式 TRACK | COMMON */
   mode: "TRACK" | "COMMON";
+  /** 试题评阅方式 SINGLE:单题阅,MULTI:多题阅 */
+  questionModal: "SINGLE" | "MULTI";
   /** 是否允许模式切换,true为不允许 */
   forceMode: boolean;
   /** 是否显示原图功能 */
@@ -91,7 +93,7 @@ export interface Setting {
   /** 只显示试评名称 TRIAL("试评"), FORMAL("正评"), FINISH("结束"): 结束状态不会在评卷端出现 */
   statusValue: "TRIAL" | "FORMAL" | null;
   /** 问题卷类型 */
-  problemTypes: Array<{ id: number; name: string }>;
+  problemTypes: Array<{ code: string; name: string }>;
   /** 当前评卷分组号 */
   groupNumber: number;
   /** 当前评卷分组名称 */
@@ -124,10 +126,18 @@ interface RawSubject {
   name: string;
   /** 科目编号 */
   code: string;
-  /** 学生答案json url */
-  answerUrl: string;
-  /** 试卷json url */
-  paperUrl: string;
+  /** 答案 url */
+  answerUrl: {
+    url: string;
+    paperType: string;
+    filePathVo: string;
+  };
+  /** 试卷 url */
+  paperUrl: {
+    url: string;
+    paperType: string;
+    filePathVo: string | null;
+  };
 }
 
 /** 科目信息(试卷和答案功能)增加前端自定义 questions字段*/
@@ -174,12 +184,12 @@ export interface Group {
 }
 
 interface RawTask {
-  taskId: string;
+  // 考试id
+  examId: string;
   /** 学生ID */
   studentId: string;
   /** 任务编号 */
   secretNumber: string;
-  /** 后端处理是否显示 */
   /** 学生名称 */
   studentName: string;
   /** 学生编号 */
@@ -187,42 +197,39 @@ interface RawTask {
   /** 考试编号 */
   examNumber: string;
   /** 试卷编号 */
-  paperNumber?: string;
+  paperNumber: string;
   /** 试卷类型 */
-  paperType?: string;
-  /** 一般不要用此处的subject,优先用setting.subject */
-  subject?: Subject;
+  paperType: string;
   /** 裁切图url */
   sliceUrls: Array<string>;
   /** 最高显示优先级,有sliceConfig就用sliceConfig,否则使用sheetConfig */
   sliceConfig: Array<PictureSlice>;
   /** sliceUrls为空,则是多媒体阅卷,显示JSON */
   jsonUrl: string;
+  // 试题列表
   questionList: Array<Question>;
+  // 特殊标记列表
   specialTagList: Array<SpecialTag>;
-
+  // 科组长特殊标记列表
+  headerTagList?: any;
   /** 原图url */
   sheetUrls: Array<string>;
   /** 客观分 复核也用到 */
   objectiveScore: number;
-
   /** 评卷总分 */
   markerScore: number;
   /** 评卷时间 */
   markerTime: number;
   /** 复核有用 */
   inspectTime?: number;
-  /** 是否自评,暂时用不着 */
-  self: boolean;
   /** 是否回评 */
   previous: boolean;
   /** 是否是打回 */
   rejected: boolean;
   /** 打回原因 冒号拼接的 */
-  rejectReason: string;
   message: string | null;
-  rejectScoreList?: any;
-  headerTagList?: any;
+  /** 评卷员名称 */
+  markerName: string | null;
 }
 
 type ElementType =
@@ -284,8 +291,10 @@ export interface Task extends RawTask {
 }
 
 interface RawQuestion {
-  /** 分组序号 */
-  groupNumber: number;
+  /** 任务序号 */
+  taskId: string;
+  /** 试题号 */
+  questionId: string;
   /** 大题号 */
   mainNumber: number;
   /** 小题号 */
@@ -294,20 +303,23 @@ interface RawQuestion {
   intervalScore: number;
   /** 默认分数 */
   defaultScore: number;
-  /** 限制最小分数 */
-  minScore: number;
-  /** 限制最大分数 */
-  maxScore: number;
   /** 题目名称 */
   title: string;
-  /** 轨迹列表 */
-  trackList: Array<Track>;
   /** 得分;null的值时是为打回时可以被评卷修改的;null也是从未评分过的情况,要通过rejected来判断 */
   score: number | null;
+  /** 限制最大分数 */
+  maxScore: number;
+  /** 限制最小分数 */
+  minScore: number;
   /** 未计分 */
   uncalculate: boolean;
-  /** 选做题分组 */
-  selectiveIndex: number | null;
+  /** 是否自己评卷,false时为他人评卷,给分板置灰 */
+  selfMark: boolean;
+  // 任务状态: WAIT_ARBITRATE:待仲裁,PROBLEM:问题卷
+  status: "WAIT_ARBITRATE" | "PROBLEM" | "WAITING" | "MARKED";
+  /** 轨迹列表 */
+  trackList: Array<Track>;
+  headerTrack?: Array<Track>;
   /** 无轨迹情况下评卷员打分信息 */
   markerList: null | Array<{
     // 是否是科组长
@@ -317,9 +329,13 @@ interface RawQuestion {
     userId: string;
     userName: string;
   }>;
+  specialTagList: null;
+  // 打回原因
+  rejectReason: string | null;
+  // 打回前给分数据
+  rejectScoreList: number[] | null;
   rejected?: boolean;
   questionName?: string;
-  headerTrack?: Array<Track>;
 }
 export interface Question extends RawQuestion {
   /** question 在 task 里面的 index ,用来对应 scoreList 的 score */

+ 0 - 375
src/utils/utils.ts

@@ -1,6 +1,3 @@
-import { store } from "@/store/app";
-import { PictureSlice, Task } from "@/types";
-
 // 打开cache后,会造成没有 vue devtools 时,canvas缓存错误,暂时不知道原因
 // 通过回看的测试,打开回看,再关闭回看,稍等一会儿再打开回看,确实可以看到该缓存时缓存了,该丢弃时丢弃了
 
@@ -26,378 +23,6 @@ export async function loadImage(url: string): Promise<HTMLImageElement> {
   });
 }
 
-// 存放裁切图的ObjectUrls
-let objectUrlMap = new Map<string, string>();
-const OBJECT_URLS_MAP_MAX_SIZE =
-  window.APP_OPTIONS?.OBJECT_URLS_MAP_MAX_SIZE ?? 100;
-
-export async function getDataUrlForSliceConfig(
-  image: HTMLImageElement,
-  sliceConfig: PictureSlice,
-  maxSliceWidth: number,
-  urlForCache: string
-) {
-  const { i, x, y, w, h } = sliceConfig;
-  const key = `${urlForCache}-${i}-${x}-${y}-${w}-${h}`;
-
-  if (objectUrlMap.get(key)) {
-    console.log("cached slice objectUrl");
-    return objectUrlMap.get(key);
-  }
-
-  const canvas = document.createElement("canvas");
-  canvas.width = Math.max(sliceConfig.w, maxSliceWidth);
-  canvas.height = sliceConfig.h;
-  const ctx = canvas.getContext("2d");
-  if (!ctx) {
-    console.log('canvas.getContext("2d") error');
-    throw new Error("canvas ctx error");
-  }
-  // drawImage 画图软件透明色
-  ctx.drawImage(
-    image,
-    sliceConfig.x,
-    sliceConfig.y,
-    sliceConfig.w,
-    sliceConfig.h,
-    0,
-    0,
-    sliceConfig.w,
-    sliceConfig.h
-  );
-  // console.log(image, canvas.height, sliceConfig, ctx);
-  // console.log(canvas.toDataURL());
-
-  // 如果用toBlob,则产生异步,而且URL.createObjectURL还会需要手动释放
-  // const dataurl = canvas.toDataURL();
-  const blob: Blob = await new Promise((res) => {
-    canvas.toBlob((b) => res(b));
-  });
-  const dataurl = URL.createObjectURL(blob);
-
-  cacheFIFO();
-
-  objectUrlMap.set(key, dataurl);
-
-  return dataurl;
-}
-
-// 清理缓存的过时数据(清除头10张),First in first out
-function cacheFIFO() {
-  if (objectUrlMap.size > OBJECT_URLS_MAP_MAX_SIZE) {
-    const ary = [...objectUrlMap.entries()];
-    const toRelease = ary.splice(0, 10);
-    // 为了避免部分图片还没显示就被revoke了,这里做一个延迟revoke
-    // 此处有个瑕疵,缩略图的显示与试卷不是同时显示,是有可能被清除了的,只能让用户刷新了。 => 见下面的fix
-    for (const u of toRelease) {
-      // 如果当前图片仍在引用 objectUrl , 则将其放入缓存中
-      if (document.querySelector(`img[src="${u[1]}"]`)) {
-        ary.push(u);
-      } else {
-        // console.log("revoke ", u[1]);
-        URL.revokeObjectURL(u[1]);
-      }
-    }
-    objectUrlMap = new Map(ary);
-  }
-}
-
-export async function getDataUrlForSplitConfig(
-  image: HTMLImageElement,
-  config: [number, number],
-  maxSliceWidth: number,
-  urlForCache: string
-) {
-  const [start, end] = config;
-  const key = `${urlForCache}-${start}-${end}`;
-
-  if (objectUrlMap.get(key)) {
-    console.log("cached split objectUrl");
-    return objectUrlMap.get(key);
-  }
-
-  const width = image.naturalWidth * (end - start);
-  const canvas = document.createElement("canvas");
-  canvas.width = Math.max(width, maxSliceWidth);
-  canvas.height = image.naturalHeight;
-  const ctx = canvas.getContext("2d");
-  if (!ctx) {
-    console.log('canvas.getContext("2d") error');
-    throw new Error("canvas ctx error");
-  }
-  // drawImage 画图软件透明色
-  ctx.drawImage(
-    image,
-    image.naturalWidth * start,
-    0,
-    image.naturalWidth * end,
-    image.naturalHeight,
-    0,
-    0,
-    image.naturalWidth * end,
-    image.naturalHeight
-  );
-
-  // 如果用toBlob,则产生异步,而且URL.createObjectURL还会需要手动释放
-  // const dataurl = canvas.toDataURL();
-  const blob: Blob = await new Promise((res) => {
-    canvas.toBlob((b) => res(b));
-  });
-  const dataurl = URL.createObjectURL(blob);
-  cacheFIFO();
-
-  objectUrlMap.set(key, dataurl);
-  return dataurl;
-}
-
-export async function getDataUrlForCoverConfig(
-  image: HTMLImageElement,
-  configs: PictureSlice[]
-) {
-  const key = `${image.src}-slice`;
-  if (objectUrlMap.get(key)) {
-    return objectUrlMap.get(key);
-  }
-
-  const canvas = document.createElement("canvas");
-  canvas.width = image.naturalWidth;
-  canvas.height = image.naturalHeight;
-  const ctx = canvas.getContext("2d");
-  if (!ctx) {
-    console.log('canvas.getContext("2d") error');
-    throw new Error("canvas ctx error");
-  }
-  ctx.drawImage(image, 0, 0);
-  ctx.fillStyle = "#ffffff";
-  configs.forEach((config) => {
-    ctx.fillRect(config.x, config.y, config.w, config.h);
-  });
-
-  const blob: Blob = await new Promise((res) => {
-    canvas.toBlob((b) => res(b));
-  });
-  const dataurl = URL.createObjectURL(blob);
-  cacheFIFO();
-  objectUrlMap.set(key, dataurl);
-  return dataurl;
-}
-
-export async function preDrawImage(_currentTask: Task | undefined) {
-  // console.log("preDrawImage=>curTask:", _currentTask);
-
-  if (!_currentTask?.taskId) return;
-
-  let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准
-
-  // const hasSliceConfig = store.currentTask?.sliceConfig?.length;
-  const hasSliceConfig = _currentTask?.sliceConfig?.length;
-
-  const images = [];
-
-  if (hasSliceConfig) {
-    // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
-    const sliceNum = _currentTask.sliceUrls.length;
-    if (_currentTask.sliceConfig.some((v) => v.i > sliceNum)) {
-      console.warn("裁切图设置的数量小于该学生的总图片数量");
-    }
-    _currentTask.sliceConfig = _currentTask.sliceConfig.filter(
-      (v) => v.i <= sliceNum
-    );
-    for (const sliceConfig of _currentTask.sliceConfig) {
-      const url = _currentTask.sliceUrls[sliceConfig.i - 1];
-      const image = await loadImage(url);
-      images[sliceConfig.i] = image;
-      const { x, y, w, h } = sliceConfig;
-      x < 0 && (sliceConfig.x = 0);
-      y < 0 && (sliceConfig.y = 0);
-      if (sliceConfig.w === 0 && sliceConfig.h === 0) {
-        // 选择整图时,w/h 为0
-        sliceConfig.w = image.naturalWidth;
-        sliceConfig.h = image.naturalHeight;
-      }
-      if (x <= 1 && y <= 1 && sliceConfig.w <= 1 && sliceConfig.h <= 1) {
-        sliceConfig.x = image.naturalWidth * x;
-        sliceConfig.y = image.naturalHeight * y;
-        sliceConfig.w = image.naturalWidth * w;
-        sliceConfig.h = image.naturalHeight * h;
-      }
-    }
-
-    maxSliceWidth = Math.max(..._currentTask.sliceConfig.map((v) => v.w));
-    // 用来保存sliceImage在整个图片容器中(不包括image-seperator)的高度范围
-    for (const sliceConfig of _currentTask.sliceConfig) {
-      const url = _currentTask.sliceUrls[sliceConfig.i - 1];
-      const image = images[sliceConfig.i];
-
-      try {
-        await getDataUrlForSliceConfig(image, sliceConfig, maxSliceWidth, url);
-      } catch (error) {
-        console.log("preDrawImage failed: ", error);
-      }
-    }
-  } else {
-    for (const url of _currentTask.sliceUrls) {
-      const image = await loadImage(url);
-      images.push(image);
-    }
-
-    const splitConfigPairs = store.setting.splitConfig.reduce<
-      [number, number][]
-    >((a, v, index) => {
-      index % 2 === 0 ? a.push([v, -1]) : (a.at(-1)![1] = v);
-      return a;
-    }, []);
-
-    const maxSplitConfig = Math.max(...store.setting.splitConfig);
-    maxSliceWidth =
-      Math.max(...images.map((v) => v.naturalWidth)) * maxSplitConfig;
-
-    for (const url of _currentTask.sliceUrls) {
-      for (const config of splitConfigPairs) {
-        const indexInSliceUrls = _currentTask.sliceUrls.indexOf(url) + 1;
-        const image = images[indexInSliceUrls - 1];
-
-        try {
-          await getDataUrlForSplitConfig(image, config, maxSliceWidth, url);
-        } catch (error) {
-          console.log("preDrawImage failed: ", error);
-        }
-      }
-    }
-  }
-}
-
-export async function processSliceUrls(_currentTask: Task | undefined) {
-  if (!_currentTask?.taskId) return;
-
-  const getNum = (num) => Math.max(Math.min(1, num), 0);
-
-  const sheetUrls = _currentTask.sheetUrls || [];
-  const sheetConfig = (store.setting.sheetConfig || []).map((item) => {
-    return { ...item };
-  });
-
-  const urls = [];
-  for (let i = 0; i < sheetUrls.length; i++) {
-    const url = sheetUrls[i];
-    const configs = sheetConfig.filter((item) => item.i === i + 1);
-    if (!configs.length) {
-      urls[i] = url;
-      continue;
-    }
-    const image = await loadImage(url);
-    configs.forEach((item) => {
-      item.x = image.naturalWidth * getNum(item.x);
-      item.y = image.naturalHeight * getNum(item.y);
-      item.w = image.naturalWidth * getNum(item.w);
-      item.h = image.naturalHeight * getNum(item.h);
-    });
-
-    urls[i] = await getDataUrlForCoverConfig(image, configs);
-  }
-  return urls;
-}
-
-export async function preDrawImageHistory(_currentTask: Task | undefined) {
-  console.log("preDrawImageHistory=>curTask:", _currentTask);
-
-  if (!_currentTask?.taskId) return;
-
-  let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准
-
-  // const hasSliceConfig = store.currentTask?.sliceConfig?.length;
-  const hasSliceConfig = _currentTask?.sliceConfig?.length;
-  // _currentTask.sheetUrls = ["/1-1.jpg", "/1-2.jpg"];
-  _currentTask.sliceUrls = await processSliceUrls(_currentTask);
-
-  const images = [];
-
-  if (hasSliceConfig) {
-    // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
-    const sliceNum = _currentTask.sliceUrls.length;
-    if (_currentTask.sliceConfig.some((v) => v.i > sliceNum)) {
-      console.warn("裁切图设置的数量小于该学生的总图片数量");
-    }
-    _currentTask.sliceConfig = _currentTask.sliceConfig.filter(
-      (v) => v.i <= sliceNum
-    );
-    for (const sliceConfig of _currentTask.sliceConfig) {
-      const url = _currentTask.sliceUrls[sliceConfig.i - 1];
-      const image = await loadImage(url);
-      images[sliceConfig.i] = image;
-      const { x, y, w, h } = sliceConfig;
-      x < 0 && (sliceConfig.x = 0);
-      y < 0 && (sliceConfig.y = 0);
-      if (sliceConfig.w === 0 && sliceConfig.h === 0) {
-        // 选择整图时,w/h 为0
-        sliceConfig.w = image.naturalWidth;
-        sliceConfig.h = image.naturalHeight;
-      }
-      if (x <= 1 && y <= 1 && sliceConfig.w <= 1 && sliceConfig.h <= 1) {
-        sliceConfig.x = image.naturalWidth * x;
-        sliceConfig.y = image.naturalHeight * y;
-        sliceConfig.w = image.naturalWidth * w;
-        sliceConfig.h = image.naturalHeight * h;
-      }
-    }
-
-    maxSliceWidth = Math.max(..._currentTask.sliceConfig.map((v) => v.w));
-
-    // 用来保存sliceImage在整个图片容器中(不包括image-seperator)的高度范围
-    for (const sliceConfig of _currentTask.sliceConfig) {
-      const url = _currentTask.sliceUrls[sliceConfig.i - 1];
-      const image = images[sliceConfig.i];
-
-      try {
-        await getDataUrlForSliceConfig(image, sliceConfig, maxSliceWidth, url);
-      } catch (error) {
-        console.log("preDrawImage failed: ", error);
-      }
-    }
-  } else {
-    for (const url of _currentTask.sliceUrls) {
-      const image = await loadImage(url);
-      images.push(image);
-    }
-
-    const splitConfigPairs = store.setting.splitConfig.reduce<
-      [number, number][]
-    >((a, v, index) => {
-      index % 2 === 0 ? a.push([v, -1]) : (a.at(-1)![1] = v);
-      return a;
-    }, []);
-
-    const maxSplitConfig = Math.max(...store.setting.splitConfig);
-    maxSliceWidth =
-      Math.max(...images.map((v) => v.naturalWidth)) * maxSplitConfig;
-
-    for (const url of _currentTask.sliceUrls) {
-      for (const config of splitConfigPairs) {
-        const indexInSliceUrls = _currentTask.sliceUrls.indexOf(url) + 1;
-        const image = images[indexInSliceUrls - 1];
-
-        try {
-          await getDataUrlForSplitConfig(image, config, maxSliceWidth, url);
-        } catch (error) {
-          console.log("preDrawImage failed: ", error);
-        }
-      }
-    }
-  }
-}
-
-export function addFileServerPrefixToTask(rawTask: Task): Task {
-  const newTask = JSON.parse(JSON.stringify(rawTask)) as Task;
-  return newTask;
-}
-
-export function addHeaderTrackColorAttr(headerTrack: any): any {
-  return headerTrack.map((item: any) => {
-    item.color = "green";
-    return item;
-  });
-}
-
 /**
  * 获取随机code,默认获取16位
  * @param {Number} len 推荐8的倍数