chenhao 3 жил өмнө
parent
commit
836e0ef4d7

+ 6 - 5
src/api/jsonMark.ts

@@ -1,6 +1,5 @@
 import { httpApp } from "@/plugins/axiosApp";
-import {
-  RichTextQuestion,
+import type {
   MarkStore,
   StudentAnswer,
   OExamPaperJSON,
@@ -31,7 +30,9 @@ export async function getPaper(store: MarkStore) {
         body: q.body,
         parentBody: q.parentBody,
         answer: [q.answer],
-      } as RichTextQuestion;
+        objective: null,
+        options: null,
+      };
       store.setting.subject.questions.push(tempQuestion);
     }
   } else {
@@ -48,7 +49,7 @@ export async function getPaper(store: MarkStore) {
               answer: order3.answer,
               objective: order3.objective,
               options: order3.options,
-            } as RichTextQuestion;
+            };
             store.setting.subject.questions.push(tempQuestion);
           }
         } else {
@@ -59,7 +60,7 @@ export async function getPaper(store: MarkStore) {
             answer: order2.answer,
             objective: order2.objective,
             options: order2.options,
-          } as RichTextQuestion;
+          };
           store.setting.subject.questions.push(tempQuestion);
         }
       }

+ 2 - 2
src/features/admin/confirmPaper/ConfirmPaper.vue

@@ -405,9 +405,9 @@ const answerPaperScale = $computed(() => {
   // 放大、缩小不影响页面之前的滚动条定位
   let percentWidth = 0;
   let percentTop = 0;
-  const container = document.querySelector(
+  const container = document.querySelector<HTMLDivElement>(
     ".mark-body-container"
-  ) as HTMLDivElement;
+  );
   if (container) {
     const { scrollLeft, scrollTop, scrollWidth, scrollHeight } = container;
     percentWidth = scrollLeft / scrollWidth;

+ 19 - 21
src/features/mark/CommonMarkBody.vue

@@ -121,9 +121,9 @@ const { addTimeout } = useTimers();
 watch(
   () => [store.minimapScrollToX, store.minimapScrollToY],
   () => {
-    const container = document.querySelector(
+    const container = document.querySelector<HTMLDivElement>(
       ".mark-body-container"
-    ) as HTMLDivElement;
+    );
     addTimeout(() => {
       if (
         container &&
@@ -144,9 +144,12 @@ watch(
 
 //#region : 快捷键定位
 const scrollContainerByKey = (e: KeyboardEvent) => {
-  const container = document.querySelector(
+  const container = document.querySelector<HTMLDivElement>(
     ".mark-body-container"
-  ) as HTMLDivElement;
+  );
+  if (!container) {
+    return;
+  }
   if (e.key === "w") {
     container.scrollBy({ top: -100, behavior: "smooth" });
   } else if (e.key === "s") {
@@ -216,7 +219,7 @@ async function processSliceConfig() {
     ? markResult.specialTagList ?? []
     : store.currentTask.specialTagList ?? [];
 
-  const tempSliceImagesWithTrackList = [] as Array<SliceImage>;
+  const tempSliceImagesWithTrackList: Array<SliceImage> = [];
   for (const sliceConfig of store.currentTask.sliceConfig) {
     accumBottomHeight += sliceConfig.h;
     const url = store.currentTask.sliceUrls[sliceConfig.i - 1];
@@ -314,8 +317,8 @@ async function processSplitConfig() {
     store.setting.splitConfig = [0, 1];
   }
   const splitConfigPairs = store.setting.splitConfig
-    .map((v, index, ary) => (index % 2 === 0 ? [v, ary[index + 1]] : false))
-    .filter((v) => v) as unknown as Array<[number, number]>;
+    .filter((v, index) => index % 2 === 0)
+    .map<[number, number]>((v, index, ary) => [v, ary[index + 1]]);
 
   // 最大的 splitConfig 的宽度
   const maxSplitConfig = Math.max(
@@ -345,7 +348,7 @@ async function processSplitConfig() {
 
   let accumTopHeight = 0;
   let accumBottomHeight = 0;
-  const tempSliceImagesWithTrackList = [] as Array<SliceImage>;
+  const tempSliceImagesWithTrackList: SliceImage[] = [];
   const trackLists = hasMarkResultToRender
     ? markResult.trackList
     : (store.currentTask.questionList || []).map((q) => q.trackList).flat();
@@ -674,22 +677,21 @@ onUnmounted(() => {
 // }, 1000);
 
 //#region autoScroll自动跳转
-let oldFirstScoreContainer: HTMLDivElement;
+let oldFirstScoreContainer: HTMLDivElement | null;
 watch(
   () => store.currentTask,
   () => {
     if (store.setting.autoScroll) {
       // 给任务清理和动画留一点时间
-      oldFirstScoreContainer = document.querySelector(
-        ".score-container"
-      ) as HTMLDivElement;
+      oldFirstScoreContainer =
+        document.querySelector<HTMLDivElement>(".score-container");
       oldFirstScoreContainer?.scrollIntoView({ behavior: "smooth" });
       addTimeout(scrollToFirstScore, 1000);
     } else {
-      const container = document.querySelector(
+      const container = document.querySelector<HTMLDivElement>(
         ".mark-body-container"
-      ) as HTMLDivElement;
-      container.scrollTo({ top: 0, left: 0, behavior: "smooth" });
+      );
+      container?.scrollTo({ top: 0, left: 0, behavior: "smooth" });
     }
   }
 );
@@ -698,12 +700,8 @@ function scrollToFirstScore() {
     window.requestAnimationFrame(scrollToFirstScore);
   }
   addTimeout(() => {
-    let firstScore = document.querySelector(
-      ".score-container"
-    ) as HTMLDivElement;
-    if (firstScore) {
-      firstScore?.scrollIntoView({ behavior: "smooth" });
-    }
+    let firstScore = document.querySelector<HTMLDivElement>(".score-container");
+    firstScore?.scrollIntoView({ behavior: "smooth" });
   }, 1000);
 }
 //#endregion

+ 8 - 3
src/features/mark/Mark.vue

@@ -71,7 +71,7 @@ import MarkBody from "./MarkBody.vue";
 import { useTimers } from "@/setups/useTimers";
 import MarkHistory from "./MarkHistory.vue";
 import MarkBoardTrack from "./MarkBoardTrack.vue";
-import type { Setting, Question } from "@/types";
+import type { Question } from "@/types";
 import MarkBoardKeyBoard from "./MarkBoardKeyBoard.vue";
 import MarkBoardMouse from "./MarkBoardMouse.vue";
 import { debounce, isEmpty, isNumber } from "lodash-es";
@@ -122,7 +122,12 @@ async function updateSetting() {
       "score.board.collapse": false,
       "normal.mode": "keyboard",
       "score.fontSize.scale": 1,
-    } as Setting["uiSetting"];
+      "paper.modal": false,
+      "answer.modal": false,
+      "minimap.modal": false,
+      "specialTag.modal": false,
+      "shortCut.modal": false,
+    };
   }
   store.setting = settingRes.data;
   if (store.setting.subject?.answerUrl) {
@@ -308,7 +313,7 @@ const saveTaskToServer = async () => {
     error: string;
   };
 
-  const errors = [] as Array<SubmitError>;
+  const errors: SubmitError[] = [];
   markResult.scoreList.forEach((score, index) => {
     if (!store.currentTask) return;
     const question = store.currentTask.questionList[index]!;

+ 2 - 2
src/features/mark/MarkBoardTrack.vue

@@ -320,12 +320,12 @@ function numberKeyListener(event: KeyboardEvent) {
       (q) =>
         q.mainNumber === store.currentQuestion?.mainNumber &&
         q.subNumber === store.currentQuestion.subNumber
-    );
+    ) || -1;
   }
 
   // tab 循环答题列表
   if (event.key === "Tab") {
-    const idx = indexOfCurrentQuestion() as number;
+    const idx = indexOfCurrentQuestion();
     if (idx >= 0 && store.currentTask) {
       const len = store.currentTask.questionList.length;
       chooseQuestion(store.currentTask.questionList[(idx + 1) % len]);

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

@@ -202,8 +202,8 @@ EventBus.on("should-reload-history", () => {
         markerId,
         markerScore,
       });
-      if (res.data) {
-        let data = cloneDeep(res.data) as Array<Task>;
+      if (res?.data) {
+        let data = cloneDeep(res.data);
         data = data.map(addFileServerPrefixToTask);
         if (store.currentTask) {
           // 这种方式(对象被重新构造了)能查找到index,我也很惊讶
@@ -249,8 +249,8 @@ async function updateHistoryTask({
     markerScore,
   });
   loading = false;
-  if (res.data) {
-    let data = cloneDeep(res.data) as Array<Task>;
+  if (res?.data) {
+    let data = cloneDeep(res.data) ;
     data = data.map(addFileServerPrefixToTask);
     store.historyTasks = data;
     replaceCurrentTask(store.historyTasks[0]);

+ 25 - 38
src/features/mark/MultiMediaMarkBody.vue

@@ -68,7 +68,7 @@ import { getStudentAnswerJSON } from "@/api/jsonMark";
 import { store } from "@/store/store";
 import { onUpdated, watch } from "vue";
 import { renderRichText } from "@/utils/renderJSON";
-import type { RichTextJSON } from "@/types";
+import type { RichTextJSON, QuestionForRender } from "@/types";
 import "viewerjs/dist/viewer.css";
 import Viewer from "viewerjs";
 import { useRoute } from "vue-router";
@@ -78,18 +78,6 @@ const isSeePaper = route.name === "StudentTrack";
 const showScore = (question: QuestionForRender) =>
   route.name !== "Mark" && question.totalScore;
 
-interface QuestionForRender {
-  unionOrder: string;
-  parentBody: RichTextJSON | null;
-  body: RichTextJSON;
-  studentAnswer: Array<RichTextJSON> | null;
-  standardAnswer: Array<RichTextJSON> | null;
-  score: number | null;
-  totalScore: number;
-  objective: boolean | null;
-  options: Array<{ number: number; body: RichTextJSON }>;
-}
-
 let questions: QuestionForRender[] = $ref([]);
 async function updateStudentAnswerJSON() {
   return getStudentAnswerJSON(store.currentTask?.jsonUrl as string);
@@ -124,8 +112,6 @@ watch(
     // 查看原卷显示全部题目
     if (isSeePaper) {
       for (const questionBody of store.setting.subject.questions || []) {
-        const questionForRender = {} as QuestionForRender;
-
         const [mainNumber, subNumber] = questionBody.unionOrder.split("-");
         const stuAns = stuAnswers.find(
           (v) =>
@@ -144,22 +130,20 @@ watch(
           (v) =>
             [v.mainNumber, v.subNumber].join("-") === questionBody.unionOrder
         );
-
-        questionForRender.unionOrder = questionBody.unionOrder;
-        questionForRender.parentBody = questionBody.parentBody;
-        questionForRender.body = questionBody.body;
-        questionForRender.options = questionBody.options;
-        questionForRender.objective = questionBody.objective;
-        questionForRender.standardAnswer = questionBody.answer;
-        questionForRender.studentAnswer = stuAns.answer;
-        questionForRender.score = taskQuestion?.score ?? null;
-        questionForRender.totalScore = taskQuestion?.maxScore || 0;
-        questions.push(questionForRender);
+        questions.push({
+          unionOrder: questionBody.unionOrder,
+          parentBody: questionBody.parentBody,
+          body: questionBody.body,
+          options: questionBody.options,
+          objective: questionBody.objective,
+          standardAnswer: questionBody.answer,
+          studentAnswer: stuAns.answer,
+          score: taskQuestion?.score ?? null,
+          totalScore: taskQuestion?.maxScore || 0,
+        });
       }
     } else {
       for (const taskQuestion of store.currentTask?.questionList || []) {
-        const questionForRender = {} as QuestionForRender;
-
         const { mainNumber, subNumber } = taskQuestion;
         const questionBody = store.setting.subject.questions.find(
           (ques) => ques.unionOrder === `${mainNumber}-${subNumber}`
@@ -180,14 +164,17 @@ watch(
           answer: [],
         };
 
-        questionForRender.unionOrder = questionBody.unionOrder;
-        questionForRender.parentBody = questionBody.parentBody;
-        questionForRender.body = questionBody.body;
-        questionForRender.standardAnswer = questionBody.answer;
-        questionForRender.studentAnswer = stuAns.answer;
-        questionForRender.score = taskQuestion.score;
-        questionForRender.totalScore = taskQuestion.maxScore;
-        questions.push(questionForRender);
+        questions.push({
+          unionOrder: questionBody.unionOrder,
+          parentBody: questionBody.parentBody,
+          body: questionBody.body,
+          options: questionBody.options,
+          objective: questionBody.objective,
+          standardAnswer: questionBody.answer,
+          studentAnswer: stuAns.answer,
+          score: taskQuestion.score,
+          totalScore: taskQuestion.maxScore,
+        });
       }
     }
   },
@@ -195,7 +182,7 @@ watch(
 );
 
 const indexToABCD = (index: number) => "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[index - 1];
-const renderObjective = (ans: any) => {
+const renderObjective = (ans: QuestionForRender["studentAnswer"]) => {
   if (typeof ans === "boolean") {
     return ans ? "A" : "B";
   } else if (Array.isArray(ans) && typeof ans[0] === "boolean") {
@@ -213,7 +200,7 @@ let viewer: Viewer = null as unknown as Viewer;
 onUpdated(() => {
   viewer && viewer.destroy();
   viewer = new Viewer(
-    document.querySelector(".rich-text-question-container") as HTMLElement,
+    document.querySelector<HTMLElement>(".rich-text-question-container")!,
     // document.querySelector("#app") as HTMLElement,
     {
       // inline: true,

+ 1 - 1
src/store/store.ts

@@ -27,7 +27,7 @@ const initState: MarkStore = {
       "shortCut.modal": false,
       "score.fontSize.scale": 1,
     },
-    statusValue: null as unknown as Setting["statusValue"],
+    statusValue: null,
     problemTypes: [],
     groupNumber: -987654, // 默认不可能的值
     groupTitle: "",

+ 90 - 52
src/types/index.ts

@@ -1,3 +1,19 @@
+import type { AxiosResponse } from "axios";
+interface SplitConfig {
+  /** index of sheets */
+  i: number;
+  /** 覆盖区域的width */
+  w: number;
+  /** 覆盖区域的height */
+  h: number;
+  /** 从哪里开始覆盖 左上角为 (0, 0) */
+  x: number;
+  /** 从哪里开始覆盖 左上角为 (0, 0) */
+  y: number;
+}
+
+type SingleSheetConfig = SplitConfig;
+export type PictureSlice = SplitConfig;
 export interface MarkStore {
   setting: Setting;
   groups: Array<Group>;
@@ -64,19 +80,14 @@ export interface Setting {
   fileServer: string;
   /** 评卷员姓名 */
   userName: string;
-  subject: {
-    /** 科目信息(试卷和答案功能)*/
-    name: string;
-    code: string;
-    answerUrl: string;
-    paperUrl: string;
-    questions: Array<RichTextQuestion>;
-  };
+  /** 科目信息(试卷和答案功能)*/
+  subject: Subject;
   /** 强制标记是否开启 */
   forceSpecialTag: boolean;
+  /** 评卷小工具控制字段 */
   uiSetting: UISetting;
   /** 只显示试评名称 TRIAL("试评"), FORMAL("正评"), FINISH("结束"): 结束状态不会在评卷端出现 */
-  statusValue: "TRIAL" | "FORMAL";
+  statusValue: "TRIAL" | "FORMAL" | null;
   /** 问题卷类型 */
   problemTypes: Array<{ id: number; name: string }> | [];
   /** 当前评卷分组号 */
@@ -97,6 +108,24 @@ export interface Setting {
   selective: boolean;
 }
 
+/** 科目信息(试卷和答案功能) */
+interface RawSubject {
+  /** 科目名称 */
+  name: string;
+  /** 科目编号 */
+  code: string;
+  /** 学生答案json url */
+  answerUrl: string;
+  /** 试卷json url */
+  paperUrl: string;
+}
+
+/** 科目信息(试卷和答案功能)增加前端自定义 questions字段*/
+interface Subject extends RawSubject {
+  /** 前端自定义questions字段, 在多媒体阅卷模式下使用 */
+  questions: Array<RichTextQuestion>;
+}
+
 // setting for admin page
 export interface AdminPageSetting {
   /** 扫描图片或者多媒体,多媒体只允许 common mode */
@@ -105,14 +134,8 @@ export interface AdminPageSetting {
   fileServer: string;
   /** 管理员姓名 */
   userName: string;
-  subject: {
-    /** 科目信息(试卷和答案功能) */
-    name: string;
-    code: string;
-    answerUrl: string;
-    paperUrl: string;
-    questions: Array<RichTextQuestion>;
-  };
+  /** 科目信息 */
+  subject: Subject;
   /** 使用裁切整图时的裁切配置 [0,1]|[0,0.3,0.25,0.55], */
   splitConfig: Array<number>;
   enableSplit: boolean;
@@ -125,22 +148,11 @@ export interface AdminPageSettingForImport extends AdminPageSetting {
   message: string;
 }
 
-interface SingleSheetConfig {
-  /** index of sheets */
-  i: number;
-  /** 覆盖区域的width */
-  w: number;
-  /** 覆盖区域的height */
-  h: number;
-  /** 从哪里开始覆盖 左上角为 (0, 0) */
-  x: number;
-  /** 从哪里开始覆盖 左上角为 (0, 0) */
-  y: number;
-}
-
 export interface Group {
   markerId: number;
+  /** 分组编号 */
   number: number;
+  /** 分组title */
   title: string;
   /** 总评卷数量 */
   markedCount: number;
@@ -148,22 +160,21 @@ export interface Group {
   totalCount: number;
 }
 
-export interface Task {
+interface RawTask {
   libraryId: number;
+  /** 学生ID */
   studentId: number;
+  /** 任务编号 */
   secretNumber: string;
   /** 后端处理是否显示 */
+  /** 学生名称 */
   studentName: string;
+  /** 学生编号 */
   studentCode: string;
+  /** 考试编号 */
   examNumber: string;
-  subject?: {
-    /** 一般不要用此处的subject,优先用setting.subject */
-    name: string;
-    code: string;
-    answerUrl: string;
-    paperUrl: string;
-    questions: Array<RichTextQuestion>;
-  };
+  /** 一般不要用此处的subject,优先用setting.subject */
+  subject?: Subject;
   /** 裁切图url */
   sliceUrls: Array<string>;
   /** 最高显示优先级,有sliceConfig就用sliceConfig,否则使用sheetConfig */
@@ -191,14 +202,16 @@ export interface Task {
   /** 是否是打回 */
   rejected: boolean;
   message: string | null;
+}
 
+export interface Task extends RawTask {
   /** 评卷结果,在task第一次被访问时自动添加,watch currentTask */
   markResult: MarkResult;
   /** 前端自用,用于标记阅卷开始时间和计算spent */
   __markStartTime: number;
 }
 
-export interface Question {
+interface RawQuestion {
   /** 分组序号 */
   groupNumber: number;
   /** 大题号 */
@@ -207,8 +220,11 @@ export interface Question {
   subNumber: string;
   /** 分数间隔 */
   intervalScore: number;
+  /** 默认分数 */
   defaultScore: number;
+  /** 限制最小分数 */
   minScore: number;
+  /** 限制最大分数 */
   maxScore: number;
   /** 题目名称 */
   title: string;
@@ -216,10 +232,13 @@ export interface Question {
   trackList: Array<Track>;
   /** 得分;null的值时是为打回时可以被评卷修改的;null也是从未评分过的情况,要通过rejected来判断 */
   score: number | null;
+}
+export interface Question extends RawQuestion {
   /** question 在 task 里面的 index ,用来对应 scoreList 的 score */
   __index: number;
 }
 
+/** 轨迹数据 */
 export interface Track {
   /** 大题号 */
   mainNumber: number;
@@ -235,11 +254,13 @@ export interface Track {
   /** 相对slice的位置比例 */
   positionX: number;
   positionY: number;
+  /** 评分数 */
   score: number;
   /** 是否此处未作答,未作答时,score默认是-0分 */
   unanswered: boolean;
 }
 
+/** 特殊标记数据 */
 export interface SpecialTag {
   /** 第几张图 */
   offsetIndex: number;
@@ -253,45 +274,52 @@ export interface SpecialTag {
   tagName: string;
 }
 
-export interface PictureSlice {
-  /** 从1开始 */
-  i: number;
-  w: number;
-  h: number;
-  x: number;
-  y: number;
-}
-
 export interface UISetting {
+  /** 给分面板展示形态 */
   "score.board.collapse": boolean;
   /** 0.2 gap */
+  /** 试卷缩放比例 */
   "answer.paper.scale": number;
+  /**
+   * 给分模式
+   * "keyboard": 键盘模式
+   * "mouse": 鼠标模式
+   *  */
   "normal.mode": "keyboard" | "mouse";
+  /** 弹窗显示试卷 */
   "paper.modal": boolean;
+  /** 弹窗显示答案 */
   "answer.modal": boolean;
+  /** 弹窗显示缩略图 */
   "minimap.modal": boolean;
+  /** 弹窗显示特殊字符 */
   "specialTag.modal": boolean;
+  /** 弹窗显示快捷键配置 */
   "shortCut.modal": boolean;
   /** 0.1 gap */
+  /** 分数标记缩放比例 */
   "score.fontSize.scale": number;
 }
 
 export interface MarkResult {
   libraryId: number;
   studentId: number;
-  statusValue: string;
+  statusValue: "TRIAL" | "FORMAL" | null;
   /** 毫秒单位 */
   spent: number;
 
   // 轨迹 or 键盘
   markerScore: number | null;
+  /** 轨迹列表 */
   trackList: Array<Track>;
+  /** 给分列表 */
   scoreList: Array<number | null>;
   /** 轨迹和键盘都需要 */
   specialTagList: Array<SpecialTag>;
 
   /** 问题卷 */
   problem: boolean;
+  /** 问题卷类型ID */
   problemTypeId: number;
   /** 当前task是否为学生未选做 */
   unselective: boolean;
@@ -341,7 +369,9 @@ export interface HistoryQueryParams {
 }
 
 export interface GetHistory {
-  (historyQuery: HistoryQueryParams): any;
+  (historyQuery: HistoryQueryParams): Promise<
+    AxiosResponse<Task[]> | undefined
+  >;
 }
 
 export interface CommonResponse {
@@ -367,8 +397,16 @@ export interface RichTextQuestion {
   parentBody: RichTextJSON | null;
   answer: Array<RichTextJSON> | null;
   objective: boolean | null;
-  options: Array<{ number: number; body: RichTextJSON }>;
+  options: Array<{ number: number; body: RichTextJSON }> | null;
+}
+
+export interface QuestionForRender extends Omit<RichTextQuestion, "answer"> {
+  studentAnswer: RichTextQuestion['answer'];
+  standardAnswer: RichTextQuestion['answer'];
+  score: number | null;
+  totalScore: number;
 }
+
 export interface RichTextJSON {
   sections: RichTextSectionJSON[];
 }

+ 2 - 2
src/utils/utils.ts

@@ -216,8 +216,8 @@ export async function preDrawImage(_currentTask: Task) {
     }
 
     const splitConfigPairs = store.setting.splitConfig
-      .map((v, index, ary) => (index % 2 === 0 ? [v, ary[index + 1]] : false))
-      .filter((v) => v) as unknown as Array<[number, number]>;
+      .filter((v, index) => index % 2 === 0)
+      .map<[number, number]>((v, index, ary) => [v, ary[index + 1]]);
 
     const maxSplitConfig = Math.max(...store.setting.splitConfig);
     maxSliceWidth =