2
0
刘洋 10 сар өмнө
parent
commit
7a4c8acc9b

+ 1 - 1
src/api/arbitratePage.ts

@@ -116,7 +116,7 @@ export async function saveArbitrateTask(
     studentId,
     markerScore,
     scoreList,
-    unselective,
+    // unselective,
     trackList,
     specialTagList,
   });

+ 15 - 2
src/api/markPage.ts

@@ -1,5 +1,6 @@
 import { store } from "@/store/store";
 import { httpApp } from "@/plugins/axiosApp";
+import { cloneDeep } from "lodash-es";
 import {
   Setting,
   UISetting,
@@ -68,14 +69,26 @@ export async function getHistoryTask({
 /** 保存评卷任务(正常保存) */
 export async function saveTask() {
   if (!store.currentTask?.markResult) return;
+  console.log("store.currentTask", store.currentTask);
 
   let markResult = store.currentTask.markResult;
   markResult.problem = false;
   markResult.unselective = false;
   markResult.spent = Date.now() - store.currentTask.__markStartTime;
   markResult = { ...markResult };
-
-  return httpApp.post<CommonResponse>("/mark/saveTask", markResult, {
+  console.log("markResult", markResult);
+
+  const data = cloneDeep(markResult);
+  delete data.unselective;
+  const len = store.currentTask?.questionList?.length || 0;
+  for (let i = 0; i < len; i++) {
+    if (store.currentTask.questionList[i].hasSetUnselective) {
+      data.scoreList[i] = -1;
+    }
+  }
+  console.log("saveTask接口参数", data);
+
+  return httpApp.post<CommonResponse>("/mark/saveTask", data, {
     setGlobalMask: true,
   });
 }

+ 8 - 0
src/components/CommonMarkHeader.vue

@@ -86,6 +86,12 @@
               />
             </td>
           </tr>
+          <tr v-if="showAllPaper">
+            <td>全卷</td>
+            <td>
+              <a-switch v-model:checked="store.allPaperModal" />
+            </td>
+          </tr>
           <tr>
             <td>缩略图</td>
             <td>
@@ -184,6 +190,7 @@ const {
   showPaperAndAnswer = false,
   showScoreBoard = false,
   notShowAnswer = false,
+  showAllPaper = false,
 } = defineProps<{
   isSingleStudent?: boolean;
   clearTasks?: () => Promise<AxiosResponse<void, any>>;
@@ -191,6 +198,7 @@ const {
   showScoreBoard?: boolean;
   notShowHistoryToggle?: boolean;
   notShowAnswer?: boolean;
+  showAllPaper?: boolean;
 }>();
 
 async function updateClearTask() {

+ 18 - 18
src/devLoginParams.ts

@@ -12,12 +12,12 @@
 // export const LOGIN_CONFIG = {
 //   isAdmin: false,
 //   forceChange: true,
-//   loginName: "1-339-5-1",
+//   loginName: "1-339-2-1",
 //   // loginName: "liuyang",
 //   password: "123456",
 //   examId: "1",
 //   // markerId: "438",
-//   markerId: "147",
+//   markerId: "597",
 // };
 /** 244 评卷员 */
 // export const LOGIN_CONFIG = {
@@ -77,32 +77,32 @@
 //   // markerId: "483",
 // };
 /** 224 管理员 */
-// export const LOGIN_CONFIG = {
-//   isAdmin: true,
-//   forceChange: true,
-//   loginName: "admin-test",
-//   password: "123456",
-//   examId: "1",
-//   markerId: null,
-// };
 export const LOGIN_CONFIG = {
   isAdmin: true,
   forceChange: true,
-  // loginName: "fh161301",
-  loginName: "admin-jhdx",
-  password: "jhdx!@#",
-  examId: "1274",
+  loginName: "admin-test",
+  password: "123456",
+  examId: "1",
   markerId: null,
 };
+// export const LOGIN_CONFIG = {
+//   isAdmin: true,
+//   forceChange: true,
+//   // loginName: "fh161301",
+//   loginName: "admin-jhdx",
+//   password: "jhdx!@#",
+//   examId: "1274",
+//   markerId: null,
+// };
 
 /** 255 评卷员 */
 // export const LOGIN_CONFIG = {
 //   isAdmin: false,
 //   forceChange: true,
-//   loginName: "spj111-02",
+//   loginName: "6-X-M_pj01",
 //   password: "123456",
-//   examId: "348",
-//   markerId: "3717",
+//   examId: "345",
+//   markerId: "3581",
 // };
 
 /** 225 管理员 */
@@ -112,7 +112,7 @@ export const LOGIN_CONFIG = {
 //   forceChange: true,
 //   loginName: "admin031",
 //   password: "123456",
-//   examId: "386",
+//   examId: "345",
 //   markerId: null,
 // };
 

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

@@ -66,7 +66,7 @@ import { getArbitrateHistory } from "@/api/arbitratePage";
 import EventBus from "@/plugins/eventBus";
 import { addFileServerPrefixToTask, preDrawImage } from "@/utils/utils";
 import { isNumber } from "lodash-es";
-import type { Question } from "@/types";
+import type { Question, Task } from "@/types";
 
 const arbitrateIndex = computed(() => {
   return store.currentTask?.arbitrateIndex == null
@@ -136,6 +136,19 @@ async function updateStatus() {
   const res = await getArbitrateTaskStatus(subjectCode, groupNumber);
   if (res.data.valid) Object.assign(store.status, res.data);
 }
+
+const selectiveHandler = (t: Task) => {
+  t.questionList = (t.questionList || []).map((q: any) => {
+    if (q?.score == -1) {
+      q.score = void 0;
+      q.selective = true;
+      q.hasSetUnselective = true;
+    }
+    return q;
+  });
+  return t;
+};
+
 async function updateTask() {
   const mkey = "fetch_task_key";
   void message.info({ content: "获取任务中...", duration: 1.5, key: mkey });
@@ -156,6 +169,7 @@ async function updateTask() {
     if (store.isScanImage && !!t) {
       await preDrawImage(t);
     }
+    selectiveHandler(t);
     store.currentTask = t;
   } else {
     store.message = res.data.message;
@@ -237,7 +251,7 @@ const saveTaskToServer = async (
       if (!store.currentTask) return;
       const question = store.currentTask.questionList[index];
       let error;
-      if (!isNumber(score)) {
+      if (!isNumber(score) && !question.hasSetUnselective) {
         error = `${question.mainNumber}-${question.subNumber}${
           question.questionName ? "(" + question.questionName + ")" : ""
         } 没有给分,不能提交。`;
@@ -268,14 +282,24 @@ const saveTaskToServer = async (
         key: mkey,
       });
     }
+    console.log("store.currentTask.markResult", store.currentTask.markResult);
+    const len = store.currentTask?.questionList?.length || 0;
+    let scoreList = [...store.currentTask.markResult.scoreList];
+    for (let i = 0; i < len; i++) {
+      if (store.currentTask.questionList[i].hasSetUnselective) {
+        scoreList[i] = -1;
+      }
+    }
+    // return;
     res = await saveArbitrateTask(
       store.currentTask.libraryId + "",
       store.currentTask.studentId + "",
       store.currentTask.markResult.markerScore,
       // store.currentTask.markResult.scoreList,
-      store.currentTask.markResult.scoreList.filter(
-        (score: any) => score !== null
-      ),
+      // store.currentTask.markResult.scoreList.filter(
+      //   (score: any) => score !== null
+      // ),
+      scoreList.filter((score: any) => score !== null),
       false,
       trackList,
       specialTagList

+ 55 - 37
src/features/arbitrate/ArbitrateMarkList.vue

@@ -1,47 +1,64 @@
 <template>
-  <div class="container tw-mt-6 tw-mr-2">
-    <div v-for="(markDetail, index) of list" :key="index">
-      <div
-        class="tw-mb-4 tw-py-2"
-        style="background-color: white; border-radius: 5px"
-      >
-        <div class="tw-flex" style="color: var(--app-small-header-text-color)">
-          <div class="col-1">评卷员</div>
-          <div class="col-2">{{ markDetail.markerName }}</div>
-        </div>
-        <div class="tw-flex" style="color: var(--app-bold-text-color)">
-          <div class="col-1 tw-font-bold">时间</div>
-          <div class="col-2">
-            {{
-              markDetail.markerTime &&
-              $filters.datetimeFilter(markDetail.markerTime)
-            }}
+  <div class="container tw-pt-6 tw-mr-2">
+    <div style="height: calc(100% - 50px); overflow: auto">
+      <div v-for="(markDetail, index) of list" :key="index">
+        <div
+          class="tw-mb-4 tw-py-2"
+          style="background-color: white; border-radius: 5px"
+        >
+          <div
+            class="tw-flex"
+            style="color: var(--app-small-header-text-color)"
+          >
+            <div class="col-1">评卷员</div>
+            <div class="col-2">{{ markDetail.markerName }}</div>
           </div>
-        </div>
-        <div class="tw-flex" style="color: var(--app-bold-text-color)">
-          <div class="col-1 tw-font-bold">总分</div>
-          <div class="col-2">
-            {{
-              markDetail.totalScore === -1 ? "未选做" : markDetail.totalScore
-            }}
+          <div class="tw-flex" style="color: var(--app-bold-text-color)">
+            <div class="col-1 tw-font-bold">时间</div>
+            <div class="col-2">
+              {{
+                markDetail.markerTime &&
+                $filters.datetimeFilter(markDetail.markerTime)
+              }}
+            </div>
           </div>
-        </div>
-        <div class="tw-flex" style="color: var(--app-bold-text-color)">
-          <div class="col-1 tw-font-bold">详情</div>
-          <div class="col-2">
-            <span
-              v-for="(item, i) in str2Arr(markDetail.scoreList)"
-              :key="item"
-              :class="[{ active: isActive(i) }, 'score-single']"
-              >{{ item
-              }}<span class="split">{{
-                i == str2Arr(markDetail.scoreList).length - 1 ? "" : " , "
-              }}</span>
-            </span>
+          <div class="tw-flex" style="color: var(--app-bold-text-color)">
+            <div class="col-1 tw-font-bold">总分</div>
+            <div class="col-2">
+              {{
+                markDetail.totalScore === -1 ? "未选做" : markDetail.totalScore
+              }}
+            </div>
+          </div>
+          <div class="tw-flex" style="color: var(--app-bold-text-color)">
+            <div class="col-1 tw-font-bold">详情</div>
+            <div class="col-2">
+              <span
+                v-for="(item, i) in str2Arr(markDetail.scoreList)"
+                :key="item"
+                :class="[{ active: isActive(i) }, 'score-single']"
+                >{{ item == "-1" ? "-" : item
+                }}<span class="split">{{
+                  i == str2Arr(markDetail.scoreList).length - 1 ? "" : " , "
+                }}</span>
+              </span>
+            </div>
           </div>
         </div>
       </div>
     </div>
+    <div
+      style="
+        font-size: 12px;
+        color: red;
+        height: 50px;
+        padding-left: 10px;
+        display: flex;
+        align-items: center;
+      "
+    >
+      仲裁给分只能对标红的小题给分
+    </div>
   </div>
 </template>
 
@@ -88,6 +105,7 @@ watch(
 <style lang="less" scoped>
 .container {
   min-width: 200px;
+  // height: 100vh;
 }
 .container .col-1 {
   width: 50px;

+ 1 - 0
src/features/arbitrate/MarkBody.vue

@@ -123,6 +123,7 @@ const makeScoreTrack = (
       .map((t) => t.score)
       .reduce((acc, v) => (acc += Math.round(v * 1000)), 0) / 1000;
   item.trackList.push(track);
+  store.currentQuestion.hasSetUnselective = false;
 };
 
 const makeSpecialTagTrack = (

+ 1 - 0
src/features/arbitrate/MarkHeader.vue

@@ -3,6 +3,7 @@
     :isSingleStudent="isSingleStudent"
     :clearTasks="clearTasks"
     showPaperAndAnswer
+    showAllPaper
   >
     <span>
       <span class="header-small-text">待处理</span>

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

@@ -168,6 +168,10 @@ async function updateTask() {
     //     h: 0.52344,
     //   },
     // ];
+    // rawTask.questionList = [
+    //   { ...rawTask.questionList[0], selective: true },
+    //   { ...rawTask.questionList[0], subNumber: "2", selective: false },
+    // ];
     const newTask = addFileServerPrefixToTask(rawTask);
 
     try {
@@ -450,7 +454,7 @@ const saveTaskToServer = async () => {
     if (!store.currentTask) return;
     const question = store.currentTask.questionList[index]!;
     let error;
-    if (!isNumber(score)) {
+    if (!isNumber(score) && !question.hasSetUnselective) {
       error = `${question.mainNumber}-${question.subNumber}${
         question.questionName ? "(" + question.questionName + ")" : ""
       } 没有给分,不能提交。`;
@@ -480,7 +484,10 @@ const saveTaskToServer = async () => {
 
   if (
     markResult.scoreList.length !== store.currentTask.questionList.length ||
-    !markResult.scoreList.every((s) => isNumber(s))
+    // !markResult.scoreList.every((s) => isNumber(s))
+    !markResult.scoreList.every((s, i) => {
+      return isNumber(s) || store.currentTask.questionList[i].hasSetUnselective;
+    })
   ) {
     console.error({ content: "markResult格式不正确,缺少分数", key: mkey });
     return;
@@ -493,7 +500,10 @@ const saveTaskToServer = async () => {
       markResult.trackList
         .map((t) => Math.round((t.score || 0) * 100))
         .reduce((acc, s) => acc + s, 0) / 100;
-    if (trackScores !== markResult.markerScore) {
+    if (
+      trackScores !== markResult.markerScore &&
+      !(trackScores === 0 && markResult.markerScore === null)
+    ) {
       void message.error({
         content: "轨迹分与总分不一致,请检查。",
         duration: 3,

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

@@ -65,7 +65,7 @@
               <span>全零分</span>
             </a-button>
           </a-popconfirm>
-          <a-popconfirm
+          <!-- <a-popconfirm
             v-if="store.setting.selective"
             title="确定是未选做?"
             :overlayStyle="{ width: '200px' }"
@@ -78,7 +78,7 @@
             >
               <span>未选做</span>
             </a-button>
-          </a-popconfirm>
+          </a-popconfirm> -->
         </div>
 
         <qm-button

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

@@ -57,7 +57,7 @@
               <span>全零分</span>
             </a-button>
           </a-popconfirm>
-          <a-popconfirm
+          <!-- <a-popconfirm
             v-if="store.setting.selective"
             title="确定是未选做?"
             :overlayStyle="{ width: '200px' }"
@@ -70,7 +70,7 @@
             >
               <span>未选做</span>
             </a-button>
-          </a-popconfirm>
+          </a-popconfirm> -->
         </div>
 
         <qm-button

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

@@ -52,7 +52,7 @@
               <span>全零分</span>
             </a-button>
           </a-popconfirm>
-          <a-popconfirm
+          <!-- <a-popconfirm
             v-if="store.setting.selective"
             title="确定是未选做?"
             :overlayStyle="{ width: '200px' }"
@@ -65,7 +65,7 @@
             >
               <span>未选做</span>
             </a-button>
-          </a-popconfirm>
+          </a-popconfirm> -->
         </div>
 
         <qm-button
@@ -175,6 +175,14 @@
                 </span>
               </transition-group>
             </div>
+            <a-button
+              v-if="question.selective"
+              :type="question.hasSetUnselective ? 'primary' : 'default'"
+              size="small"
+              class="set-unselect"
+              @click="setUnselect(question, index)"
+              >未选做</a-button
+            >
           </div>
         </template>
       </div>
@@ -614,6 +622,22 @@ function clearAllMarksOfCurrentQuetion() {
   markResult.scoreList[__index] = null;
 }
 
+function setUnselect(question: Question, index: number) {
+  if (!question.hasSetUnselective) {
+    const markResult = store.currentTask.markResult;
+    store.removeScoreTracks = markResult.trackList.filter(
+      (q) =>
+        q.mainNumber === question?.mainNumber &&
+        q.subNumber === question?.subNumber
+    );
+    markResult.trackList = markResult.trackList.filter(
+      (q) => !store.removeScoreTracks.includes(q)
+    );
+    markResult.scoreList[index] = null;
+  }
+  question.hasSetUnselective = !question.hasSetUnselective;
+}
+
 function submit() {
   emit("submit");
 }

+ 1 - 0
src/features/mark/MarkBody.vue

@@ -192,6 +192,7 @@ const makeScoreTrack = (
       .map((t) => t.score)
       .reduce((acc, v) => (acc += Math.round(v * 1000)), 0) / 1000;
   item.trackList.push(track);
+  store.currentQuestion.hasSetUnselective = false;
 };
 
 const makeSpecialTagTrack = (

+ 1 - 1
src/features/mark/MarkDrawTrack.vue

@@ -123,7 +123,7 @@ watch(
             find?.offsetY || topTrack.offsetY
           }-${find?.offsetX || topTrack.offsetX}`
         )
-        ?.scrollIntoView({ behavior: "smooth" });
+        ?.scrollIntoView({ behavior: "smooth", block: "center" });
     }
   },
   {

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

@@ -209,6 +209,22 @@ import { cloneDeep } from "lodash-es";
 import EventBus from "@/plugins/eventBus";
 import { addFileServerPrefixToTask, preDrawImageHistory } from "@/utils/utils";
 import { message } from "ant-design-vue";
+
+const selectiveHandler = (d: any) => {
+  let data = d.map((item: any) => {
+    item.questionList = (item.questionList || []).map((q: any) => {
+      if (q?.score == -1) {
+        q.score = void 0;
+        q.selective = true;
+        q.hasSetUnselective = true;
+      }
+      return q;
+    });
+    return item;
+  });
+  return data;
+};
+
 const remarkCount = computed(() => {
   return store.setting?.remarkCount;
 });
@@ -338,6 +354,7 @@ EventBus.on("should-reload-history", () => {
       if (res?.data) {
         // let data = cloneDeep(res.data);
         let data = remarkCount.value === 0 ? [] : cloneDeep(res.data);
+        data = selectiveHandler(data);
         if (
           typeof remarkCount.value == "number" &&
           remarkCount.value > 0 &&
@@ -399,6 +416,7 @@ async function updateHistoryTask({
   if (res?.data) {
     // let data = cloneDeep(res.data);
     let data = remarkCount.value === 0 ? [] : cloneDeep(res.data);
+    data = selectiveHandler(data);
     if (
       typeof remarkCount.value == "number" &&
       remarkCount.value > 0 &&

+ 1 - 1
src/features/student/studentInspect/MarkHeader.vue

@@ -4,7 +4,7 @@
     :clearTasks="clearTasks"
     showScoreBoard
     showPaperAndAnswer
-    notShowAnswer
+    showAllPaper
   >
     <span>
       <span class="header-small-text">待复核</span>

+ 228 - 225
src/store/store.ts

@@ -1,225 +1,228 @@
-import { Setting, MarkStore, AdminPageSetting, Task } from "@/types";
-import { watch } from "vue";
-import { defineStore } from "pinia";
-
-const initState: MarkStore = {
-  setting: {
-    mode: "TRACK",
-    examType: "SCAN_IMAGE",
-    forceMode: false,
-    sheetView: false,
-    autoScroll: false,
-    sheetConfig: [],
-    enableAllZero: false,
-    enableSplit: true,
-    fileServer: "",
-    userName: "",
-    subject: <Setting["subject"]>{},
-    forceSpecialTag: false,
-    uiSetting: {
-      "answer.paper.scale": 1,
-      "score.board.collapse": false,
-      "normal.mode": "keyboard",
-      "paper.modal": false,
-      "answer.modal": false,
-      "minimap.modal": false,
-      "specialTag.modal": false,
-      "shortCut.modal": false,
-      "score.fontSize.scale": 1,
-    },
-    statusValue: null,
-    problemTypes: [],
-    groupNumber: -987654, // 默认不可能的值
-    groupTitle: "",
-    topCount: 0,
-    splitConfig: [],
-    prefetchCount: 3,
-    startTime: 0,
-    endTime: 0,
-    selective: false,
-  },
-  status: <MarkStore["status"]>{},
-  groups: [],
-  tasks: [],
-  message: null,
-  currentTask: undefined,
-  currentQuestion: undefined,
-  currentScore: undefined,
-  currentSpecialTag: undefined,
-  historyOpen: false,
-  historyTasks: [],
-  removeScoreTracks: [],
-  focusTracks: [],
-  maxModalZIndex: 1020,
-  minimapScrollToX: 0,
-  minimapScrollToY: 0,
-  allPaperModal: false,
-  sheetViewModal: false,
-  globalMask: false,
-};
-
-const useMarkStore = defineStore("mark", {
-  state: () => {
-    return initState;
-  },
-  getters: {
-    /** 获得statusValue的中文名 */
-    getStatusValueName() {
-      const st = store.setting.statusValue;
-      if (!st) return "";
-      if (st === "FORMAL") return "正评";
-      if (st === "TRIAL") return "试评";
-      return "";
-    },
-    /** 当前任务。确保不为空,需在上文已经检查过 store.currentTask 不为空 */
-    currentTaskEnsured(): Task {
-      return store.currentTask;
-    },
-    /** 是否是评卷端的轨迹模式 */
-    isTrackMode(): boolean {
-      return store.setting.mode && store.setting.mode === "TRACK";
-    },
-    /** 评卷端的轨迹模式显示轨迹 && 管理后台都显示轨迹 */
-    shouldShowTrack(): boolean {
-      // FIXME: 不是最优雅的方式来判断是否是阅卷端
-      const isWebMark = location.pathname === "/web/mark";
-      return !isWebMark || this.isTrackMode;
-    },
-    /* 是否是扫描阅卷 */
-    isScanImage(): boolean {
-      return this.setting.examType === "SCAN_IMAGE";
-    },
-    isMultiMedia(): boolean {
-      return this.setting.examType === "MULTI_MEDIA";
-    },
-    /* 返回正在评卷的状态 '' | 回评 | 打回 */
-    getMarkStatus(): string {
-      if (!this.currentTask) return "";
-      if (this.currentTask.previous) return "回评";
-      if (this.currentTask.rejected) return "打回";
-
-      return store.getStatusValueName;
-    },
-    shouldShowMarkBoardKeyBoard(): boolean {
-      return (
-        store.setting.mode === "COMMON" &&
-        store.setting.uiSetting["normal.mode"] === "keyboard"
-      );
-    },
-    shouldShowMarkBoardMouse(): boolean {
-      return (
-        store.setting.mode === "COMMON" &&
-        store.setting.uiSetting["normal.mode"] === "mouse"
-      );
-    },
-    isScoreBoardCollapsed(): boolean {
-      return store.setting.uiSetting["score.board.collapse"];
-    },
-    isScoreBoardVisible(): boolean {
-      return !store.setting.uiSetting["score.board.collapse"];
-    },
-  },
-  actions: {
-    initSetting(adminPageSetting: AdminPageSetting): void {
-      Object.assign(this.setting, adminPageSetting, {
-        mode: "COMMON" as Setting["mode"],
-        uiSetting: {
-          "answer.paper.scale": 1,
-          "score.board.collapse": false,
-          "normal.mode": "keyboard",
-          "score.fontSize.scale": 1,
-        } as Setting["uiSetting"],
-      });
-      const fileServer = this.setting.fileServer;
-      if (this.setting.subject?.answerUrl) {
-        this.setting.subject.answerUrl =
-          fileServer + this.setting.subject?.answerUrl;
-      }
-      if (this.setting.subject?.paperUrl) {
-        this.setting.subject.paperUrl =
-          fileServer + this.setting.subject?.paperUrl;
-      }
-    },
-    toggleHistory(): void {
-      this.historyOpen = !this.historyOpen;
-    },
-    toggleScoreBoard(): void {
-      this.setting.uiSetting["score.board.collapse"] =
-        !this.setting.uiSetting["score.board.collapse"];
-    },
-  },
-});
-
-export let store = null as unknown as ReturnType<typeof useMarkStore>;
-
-export const initMarkStore = () => {
-  store = useMarkStore();
-
-  watch(
-    () => store.currentTask,
-    () => {
-      // 初始化 task.markResult ,始终保证 task 下有 markResult
-      // 1. 评卷时,如果没有 markResult ,则初始化一个 markResult 给它
-      // 1. 回评时,先清空它的 markResult ,然后初始化一个 markResult 给它
-      if (!store.currentTask) return;
-
-      const task = store.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 = store.setting.statusValue;
-        const { libraryId, studentId } = task;
-        task.markResult = {
-          statusValue: statusValue,
-          libraryId: libraryId,
-          studentId: studentId,
-          spent: 0,
-
-          trackList: task.questionList.map((q) => q.trackList).flat(),
-          specialTagList: [...(task.specialTagList ?? [])],
-          scoreList: task.questionList.map((q) => q.score),
-          markerScore: null, // 后期通过 scoreList 自动更新
-
-          problem: false,
-          problemTypeId: 0,
-          unselective: false,
-        };
-        task.markResult.trackList.forEach((t) => {
-          if (t.unanswered) {
-            t.score = -0;
-          }
-        });
-      }
-    }
-  );
-
-  // 唯一根据 scoreList 自动更新 markerScore
-  watch(
-    () => store.currentTask?.markResult.scoreList,
-    () => {
-      if (!store.currentTask) return;
-      const scoreList = store.currentTask.markResult.scoreList.filter(
-        (v) => v !== null
-      );
-      const result =
-        scoreList.length === 0
-          ? null
-          : scoreList.reduce((acc, v) => (acc += Math.round(v * 1000)), 0) /
-            1000;
-      store.currentTask.markResult.markerScore = result;
-    },
-    { deep: true }
-  );
-
-  // scoreList 被 trackList 和用户手动更新
-};
+import { Setting, MarkStore, AdminPageSetting, Task } from "@/types";
+import { watch } from "vue";
+import { defineStore } from "pinia";
+
+const initState: MarkStore = {
+  setting: {
+    mode: "TRACK",
+    examType: "SCAN_IMAGE",
+    forceMode: false,
+    sheetView: false,
+    autoScroll: false,
+    sheetConfig: [],
+    enableAllZero: false,
+    enableSplit: true,
+    fileServer: "",
+    userName: "",
+    subject: <Setting["subject"]>{},
+    forceSpecialTag: false,
+    uiSetting: {
+      "answer.paper.scale": 1,
+      "score.board.collapse": false,
+      "normal.mode": "keyboard",
+      "paper.modal": false,
+      "answer.modal": false,
+      "minimap.modal": false,
+      "specialTag.modal": false,
+      "shortCut.modal": false,
+      "score.fontSize.scale": 1,
+    },
+    statusValue: null,
+    problemTypes: [],
+    groupNumber: -987654, // 默认不可能的值
+    groupTitle: "",
+    topCount: 0,
+    splitConfig: [],
+    prefetchCount: 3,
+    startTime: 0,
+    endTime: 0,
+    selective: false,
+  },
+  status: <MarkStore["status"]>{},
+  groups: [],
+  tasks: [],
+  message: null,
+  currentTask: undefined,
+  currentQuestion: undefined,
+  currentScore: undefined,
+  currentSpecialTag: undefined,
+  historyOpen: false,
+  historyTasks: [],
+  removeScoreTracks: [],
+  focusTracks: [],
+  maxModalZIndex: 1020,
+  minimapScrollToX: 0,
+  minimapScrollToY: 0,
+  allPaperModal: false,
+  sheetViewModal: false,
+  globalMask: false,
+};
+
+const useMarkStore = defineStore("mark", {
+  state: () => {
+    return initState;
+  },
+  getters: {
+    /** 获得statusValue的中文名 */
+    getStatusValueName() {
+      const st = store.setting.statusValue;
+      if (!st) return "";
+      if (st === "FORMAL") return "正评";
+      if (st === "TRIAL") return "试评";
+      return "";
+    },
+    /** 当前任务。确保不为空,需在上文已经检查过 store.currentTask 不为空 */
+    currentTaskEnsured(): Task {
+      return store.currentTask;
+    },
+    /** 是否是评卷端的轨迹模式 */
+    isTrackMode(): boolean {
+      return store.setting.mode && store.setting.mode === "TRACK";
+    },
+    /** 评卷端的轨迹模式显示轨迹 && 管理后台都显示轨迹 */
+    shouldShowTrack(): boolean {
+      // FIXME: 不是最优雅的方式来判断是否是阅卷端
+      const isWebMark = location.pathname === "/web/mark";
+      return !isWebMark || this.isTrackMode;
+    },
+    /* 是否是扫描阅卷 */
+    isScanImage(): boolean {
+      return this.setting.examType === "SCAN_IMAGE";
+    },
+    isMultiMedia(): boolean {
+      return this.setting.examType === "MULTI_MEDIA";
+    },
+    /* 返回正在评卷的状态 '' | 回评 | 打回 */
+    getMarkStatus(): string {
+      if (!this.currentTask) return "";
+      if (this.currentTask.previous) return "回评";
+      if (this.currentTask.rejected) return "打回";
+
+      return store.getStatusValueName;
+    },
+    shouldShowMarkBoardKeyBoard(): boolean {
+      return (
+        store.setting.mode === "COMMON" &&
+        store.setting.uiSetting["normal.mode"] === "keyboard"
+      );
+    },
+    shouldShowMarkBoardMouse(): boolean {
+      return (
+        store.setting.mode === "COMMON" &&
+        store.setting.uiSetting["normal.mode"] === "mouse"
+      );
+    },
+    isScoreBoardCollapsed(): boolean {
+      return store.setting.uiSetting["score.board.collapse"];
+    },
+    isScoreBoardVisible(): boolean {
+      return !store.setting.uiSetting["score.board.collapse"];
+    },
+  },
+  actions: {
+    initSetting(adminPageSetting: AdminPageSetting): void {
+      Object.assign(this.setting, adminPageSetting, {
+        mode: "COMMON" as Setting["mode"],
+        uiSetting: {
+          "answer.paper.scale": 1,
+          "score.board.collapse": false,
+          "normal.mode": "keyboard",
+          "score.fontSize.scale": 1,
+        } as Setting["uiSetting"],
+      });
+      const fileServer = this.setting.fileServer;
+      if (this.setting.subject?.answerUrl) {
+        this.setting.subject.answerUrl =
+          fileServer + this.setting.subject?.answerUrl;
+      }
+      if (this.setting.subject?.paperUrl) {
+        this.setting.subject.paperUrl =
+          fileServer + this.setting.subject?.paperUrl;
+      }
+    },
+    toggleHistory(): void {
+      this.historyOpen = !this.historyOpen;
+    },
+    toggleScoreBoard(): void {
+      this.setting.uiSetting["score.board.collapse"] =
+        !this.setting.uiSetting["score.board.collapse"];
+    },
+  },
+});
+
+export let store = null as unknown as ReturnType<typeof useMarkStore>;
+
+export const initMarkStore = () => {
+  store = useMarkStore();
+
+  watch(
+    () => store.currentTask,
+    () => {
+      // 初始化 task.markResult ,始终保证 task 下有 markResult
+      // 1. 评卷时,如果没有 markResult ,则初始化一个 markResult 给它
+      // 1. 回评时,先清空它的 markResult ,然后初始化一个 markResult 给它
+      if (!store.currentTask) return;
+
+      const task = store.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 = store.setting.statusValue;
+        const { libraryId, studentId } = task;
+        task.markResult = {
+          statusValue: statusValue,
+          libraryId: libraryId,
+          studentId: studentId,
+          spent: 0,
+
+          trackList: task.questionList.map((q) => q.trackList).flat(),
+          specialTagList: [...(task.specialTagList ?? [])],
+          scoreList: task.questionList.map((q) => q.score),
+          markerScore: null, // 后期通过 scoreList 自动更新
+
+          problem: false,
+          problemTypeId: 0,
+          unselective: false,
+        };
+        task.markResult.trackList.forEach((t) => {
+          if (t.unanswered) {
+            t.score = -0;
+          }
+        });
+      }
+    }
+  );
+
+  // 唯一根据 scoreList 自动更新 markerScore
+  watch(
+    () => store.currentTask?.markResult.scoreList,
+    () => {
+      if (!store.currentTask) return;
+      const scoreList = store.currentTask.markResult.scoreList.filter(
+        (v) => v !== null
+      );
+      const result =
+        scoreList.length === 0
+          ? null
+          : // : scoreList.reduce((acc, v) => (acc += Math.round(v * 1000)), 0) /
+            scoreList.reduce(
+              (acc, v) => (acc += Math.round((v || 0) * 1000)),
+              0
+            ) / 1000;
+      store.currentTask.markResult.markerScore = result;
+    },
+    { deep: true }
+  );
+
+  // scoreList 被 trackList 和用户手动更新
+};

+ 11 - 0
src/styles/global.css

@@ -58,3 +58,14 @@ button.ant-btn span[role="img"] {
 button.ant-btn-primary {
   background-color: var(--app-primary-button-bg-color);
 }
+.set-unselect {
+  position: absolute !important;
+  bottom: 3px;
+  right: 3px;
+  font-size: 12px !important;
+  
+}
+.set-unselect.ant-btn-primary {
+  background-color: #faad14 !important;
+  border-color: #faad14 !important;
+}

+ 2 - 0
src/types/index.ts

@@ -257,6 +257,8 @@ interface RawQuestion {
   rejected?: boolean;
   questionName?: string;
   headerTrack?: any;
+  selective?: any;
+  hasSetUnselective?: any;
 }
 export interface Question extends RawQuestion {
   /** question 在 task 里面的 index ,用来对应 scoreList 的 score */

+ 3 - 3
vite.config.ts

@@ -3,10 +3,10 @@ import vue from "@vitejs/plugin-vue";
 import ViteComponents from "unplugin-vue-components/vite";
 import { AntDesignVueResolver } from "unplugin-vue-components/resolvers";
 
-// const SERVER_URL = "http://192.168.10.224";
-const SERVER_URL = "https://www.markingcloud.com";
+// const SERVER_URL = "http://192.168.10.225";
+// const SERVER_URL = "https://www.markingcloud.com";
 // const SERVER_URL = "http://192.168.11.103:8090";
-// const SERVER_URL = "http://192.168.11.81:8090";
+const SERVER_URL = "http://192.168.11.201:8000";
 
 const path = require("path");