Michael Wang před 4 roky
rodič
revize
130c824852

+ 35 - 4
src/components/mark/Mark.vue

@@ -4,13 +4,17 @@
     <div class="flex gap-1">
       <mark-history />
       <mark-body />
-      <mark-board-track @submit="saveTaskToServer" />
+      <mark-board-track v-if="showMarkBoardTrack" @submit="saveTaskToServer" />
+      <mark-board-key-board
+        v-if="showMarkBoardKeyBoard"
+        @submit="saveTaskToServer"
+      />
     </div>
   </div>
 </template>
 
 <script lang="ts">
-import { defineComponent, onMounted, onUnmounted, watch } from "vue";
+import { computed, defineComponent, onMounted, watch } from "vue";
 import {
   clearMarkTask,
   getGroup,
@@ -26,6 +30,8 @@ import MarkBody from "./MarkBody.vue";
 import { useTimers } from "@/setups/useTimers";
 import MarkHistory from "./MarkHistory.vue";
 import MarkBoardTrack from "./MarkBoardTrack.vue";
+import { ModeEnum, Setting } from "@/types";
+import MarkBoardKeyBoard from "./MarkBoardKeyBoard.vue";
 
 export default defineComponent({
   name: "Mark",
@@ -34,6 +40,7 @@ export default defineComponent({
     MarkBody,
     MarkHistory,
     MarkBoardTrack,
+    MarkBoardKeyBoard,
   },
   setup: () => {
     const { addInterval } = useTimers();
@@ -44,7 +51,15 @@ export default defineComponent({
 
     async function updateSetting() {
       const settingRes = await getSetting();
-      settingRes.data.uiSetting["answer.paper.scale"] ||= 1;
+      // settingRes.data.uiSetting["answer.paper.scale"] ||= 1;
+      // TODO: 要求后台清空旧数据 重置旧设置
+      if (settingRes.data.uiSetting["image.view.scale"]) {
+        settingRes.data.uiSetting = {
+          "answer.paper.scale": 1,
+          "score.board.collapse": false,
+          "normal.mode": "keyboard",
+        } as Setting["uiSetting"];
+      }
       store.setting = settingRes.data;
     }
     async function updateStatus() {
@@ -92,6 +107,17 @@ export default defineComponent({
       { deep: true }
     );
 
+    const showMarkBoardTrack = computed(() => {
+      return store.setting.mode === ModeEnum.TRACK;
+    });
+
+    const showMarkBoardKeyBoard = computed(() => {
+      return (
+        store.setting.mode === ModeEnum.COMMON &&
+        store.setting.uiSetting["normal.mode"] === "keyboard"
+      );
+    });
+
     const saveTaskToServer = async () => {
       console.log("save task to server");
       const res = (await saveTask()) as any;
@@ -109,7 +135,12 @@ export default defineComponent({
       }
     };
 
-    return { store, saveTaskToServer };
+    return {
+      store,
+      saveTaskToServer,
+      showMarkBoardTrack,
+      showMarkBoardKeyBoard,
+    };
   },
 });
 </script>

+ 204 - 0
src/components/mark/MarkBoardKeyBoard.vue

@@ -0,0 +1,204 @@
+<template>
+  <div
+    v-if="store.currentTask"
+    style="
+      max-width: 250px;
+      min-width: 250px;
+      border: 1px solid grey;
+      padding-left: 6px;
+      padding-right: 6px;
+    "
+  >
+    <div>
+      <h1 class="text-3xl text-center" @click="toggleKeyMouse">键盘给分</h1>
+    </div>
+    <div>
+      <h1 class="text-3xl text-center">
+        总分:{{ store.currentMarkResult?.markerScore || 0 }}
+      </h1>
+    </div>
+
+    <div v-if="store.currentTask && store.currentTask.questionList">
+      <template
+        v-for="(question, index) in store.currentTask?.questionList"
+        :key="index"
+      >
+        <div
+          @click="chooseQuestion(question)"
+          class="question rounded p-1 mb-2"
+          :class="isCurrentQuestion(question) && 'current-question'"
+        >
+          <div class="flex justify-between">
+            <div>
+              <div>
+                {{ question.title }} {{ question.mainNumber }}-{{
+                  question.subNumber
+                }}
+              </div>
+              <div class="text-center text-3xl">
+                {{ question.score || 0 }}
+              </div>
+            </div>
+            <div>
+              <div class="text-center">间隔{{ question.intervalScore }}分</div>
+              <div class="text-3xl">
+                {{ question.minScore }} ~ {{ question.maxScore }}
+              </div>
+            </div>
+          </div>
+        </div>
+      </template>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { Question } from "@/types";
+import { isNumber } from "lodash";
+import { computed, defineComponent, onMounted, onUnmounted, watch } from "vue";
+import { findCurrentTaskMarkResult, store } from "./store";
+
+export default defineComponent({
+  name: "MarkBoardKeyBoard",
+  emits: ["submit"],
+  setup(props, { emit }) {
+    function toggleKeyMouse() {
+      if (store.setting.uiSetting["normal.mode"] === "keyboard") {
+        store.setting.uiSetting["normal.mode"] = "mouse";
+      } else {
+        store.setting.uiSetting["normal.mode"] = "keyboard";
+      }
+    }
+    const markResult = findCurrentTaskMarkResult();
+
+    const questionScoreSteps = computed(() => {
+      const question = store.currentQuestion;
+      if (!question) return [];
+
+      const steps = [];
+      for (
+        let i = 0;
+        i <= question.maxScore - question.score;
+        i += question.intervalScore
+      ) {
+        steps.push(i);
+      }
+      if ((question.maxScore - question.score) % question.intervalScore !== 0) {
+        steps.push(question.maxScore - question.score);
+      }
+
+      return steps;
+    });
+
+    function isCurrentQuestion(question: Question) {
+      return (
+        store.currentQuestion?.mainNumber === question.mainNumber &&
+        store.currentQuestion?.subNumber === question.subNumber
+      );
+    }
+    watch(
+      () => store.currentTask,
+      () => {
+        store.currentQuestion = undefined;
+        store.currentScore = undefined;
+      }
+    );
+    watch(
+      () => store.currentQuestion,
+      () => {
+        store.currentScore = undefined;
+      }
+    );
+    function chooseQuestion(question: Question) {
+      store.currentQuestion = question;
+    }
+
+    let keyPressTimestamp = 0;
+    let keys: string[] = [];
+    function numberKeyListener(event: KeyboardEvent) {
+      // console.log(event);
+      if (!store.currentQuestion || !store.currentTask) return;
+
+      // 处理Enter跳下一题或submit
+      if (event.key === "Enter") {
+        if (!isNumber(store.currentQuestion.score)) {
+          // 当前题赋分不通过,Enter无效
+          return;
+        }
+        const idx = store.currentTask?.questionList.findIndex(
+          (q) =>
+            q.mainNumber === store.currentQuestion?.mainNumber &&
+            q.subNumber === store.currentQuestion.subNumber
+        );
+        if (idx + 1 === store.currentTask?.questionList.length) {
+          submit();
+        } else {
+          chooseQuestion(store.currentTask.questionList[idx + 1]);
+        }
+        keys = [];
+        return;
+      }
+
+      if (event.timeStamp - keyPressTimestamp > 1.5 * 1000) {
+        keys = [];
+      }
+      keyPressTimestamp = event.timeStamp;
+      keys.push(event.key);
+      if (isNaN(parseFloat(keys.join("")))) {
+        keys = [];
+      }
+      if (event.key === "Escape") {
+        keys = [];
+      }
+      const score = parseFloat(keys.join(""));
+      if (isNumber(score) && questionScoreSteps.value.includes(score)) {
+        store.currentQuestion.score = score;
+      }
+    }
+    onMounted(() => {
+      document.addEventListener("keydown", numberKeyListener);
+    });
+    onUnmounted(() => {
+      document.removeEventListener("keydown", numberKeyListener);
+    });
+
+    function submit() {
+      emit("submit");
+    }
+
+    return {
+      store,
+      toggleKeyMouse,
+      markResult,
+      isCurrentQuestion,
+      chooseQuestion,
+      questionScoreSteps,
+      submit,
+    };
+  },
+});
+</script>
+
+<style scoped>
+.question {
+  min-width: 80px;
+  border: 1px solid grey;
+}
+.current-question {
+  border: 1px solid yellowgreen;
+  background-color: lightblue;
+}
+.single-score {
+  width: 30px;
+  height: 30px;
+  display: grid;
+  place-content: center;
+
+  border: 1px solid black;
+  border-radius: 5px;
+}
+.current-score {
+  border: 1px solid yellowgreen;
+  background-color: lightblue;
+}
+</style>

+ 1 - 1
src/components/mark/MarkBoardTrack.vue

@@ -7,7 +7,7 @@
       min-width: 250px;
       border: 1px solid grey;
       padding-left: 6px;
-      padding-left: 6px;
+      padding-right: 6px;
     "
   >
     <div>

+ 56 - 10
src/components/mark/MarkHeader.vue

@@ -1,38 +1,58 @@
 <template>
   <div
-    class="flex gap-4 justify-between items-center bg-blue-100"
+    class="flex gap-4 justify-between items-center header-bg"
     style="z-index: 10000; position: relative; font-size: 16px; height: 40px"
   >
     <div>
-      <a href="/mark/subject-select">{{ store.setting.subject?.name }}</a>
+      <a
+        style="color: white; text-decoration: underline"
+        href="/mark/subject-select"
+        >{{ store.setting.subject?.name }}</a
+      >
     </div>
     <div v-if="store.setting.statusValue === 'TRIAL'">试评</div>
     <div class="flex gap-1">
-      <div>编号:{{ store.currentTask?.studentCode }}</div>
+      <div>
+        编号<span class="highlight-text">{{
+          store.currentTask?.studentCode
+        }}</span>
+      </div>
       <div
         v-if="store.currentTask && store.currentTask.objectiveScore !== null"
       >
-        客观分{{ store.currentTask.objectiveScore }}
+        客观分<span class="highlight-text">{{
+          store.currentTask.objectiveScore
+        }}</span>
       </div>
     </div>
     <ul class="flex gap-2 mb-0">
-      <li>已评{{ store.status.markedCount }}</li>
-      <li v-if="store.setting.topCount">分配{{ store.setting.topCount }}</li>
-      <li>未评{{ store.status.totalCount - store.status.markedCount }}</li>
+      <li>
+        已评<span class="highlight-text">{{ store.status.markedCount }}</span>
+      </li>
+      <li v-if="store.setting.topCount">
+        分配<span class="highlight-text">{{ store.setting.topCount }}</span>
+      </li>
+      <li>
+        未评<span class="highlight-text">{{
+          store.status.totalCount - store.status.markedCount
+        }}</span>
+      </li>
       <li
         :title="`问题卷${store.status.problemCount}\n待仲裁${store.status.arbitrateCount}`"
         style="line-height: 20px"
       >
         <QuestionCircleOutlined :style="{ 'font-size': '20px' }" />
       </li>
-      <li>进度{{ progress }}%</li>
+      <li>
+        进度<span class="highlight-text">{{ progress }}%</span>
+      </li>
     </ul>
     <ul class="flex gap-2 mb-0">
       <li @click="upScale" title="放大" style="line-height: 20px">
         <PlusCircleOutlined
           :style="{
             'font-size': '20px',
-            color: greaterThanOneScale ? 'green' : 'black',
+            color: greaterThanOneScale ? 'red' : 'white',
           }"
         />
       </li>
@@ -40,7 +60,7 @@
         <MinusCircleOutlined
           :style="{
             'font-size': '20px',
-            color: lessThanOneScale ? 'green' : 'black',
+            color: lessThanOneScale ? 'red' : 'white',
           }"
         />
       </li>
@@ -48,6 +68,9 @@
         <FullscreenOutlined :style="{ 'font-size': '20px' }" />
       </li>
     </ul>
+    <div @click="toggleSettingMode" style="line-height: 20px">
+      {{ modeName }} {{ store.setting.forceMode ? "" : "(切换)" }}
+    </div>
     <div @click="toggleHistory" style="line-height: 20px" title="回看">
       <HistoryOutlined :style="{ 'font-size': '20px' }" />
     </div>
@@ -95,6 +118,7 @@ import {
   ClockCircleOutlined,
   QuestionCircleOutlined,
 } from "@ant-design/icons-vue";
+import { ModeEnum } from "@/types";
 
 export default defineComponent({
   name: "MarkHeader",
@@ -109,6 +133,16 @@ export default defineComponent({
     QuestionCircleOutlined,
   },
   setup() {
+    const modeName = computed(() =>
+      store.setting.mode === ModeEnum.TRACK ? "轨迹模式" : "普通模式"
+    );
+    function toggleSettingMode() {
+      if (store.setting.mode === ModeEnum.TRACK) {
+        store.setting.mode = ModeEnum.COMMON;
+      } else {
+        store.setting.mode = ModeEnum.TRACK;
+      }
+    }
     const progress = computed(() => {
       const { totalCount, markedCount } = store.status;
       if (totalCount <= 0) return 0;
@@ -171,6 +205,8 @@ export default defineComponent({
 
     return {
       store,
+      modeName,
+      toggleSettingMode,
       progress,
       group,
       upScale,
@@ -184,3 +220,13 @@ export default defineComponent({
   },
 });
 </script>
+
+<style scoped>
+.header-bg {
+  background-color: #5d6d7d;
+  color: white;
+}
+.highlight-text {
+  color: #ffe400;
+}
+</style>

+ 28 - 8
src/components/mark/store.ts

@@ -14,7 +14,11 @@ const obj = {
     marker: <Setting["marker"]>{},
     subject: <Setting["subject"]>{},
     forceSpecialTag: false,
-    uiSetting: <Setting["uiSetting"]>{},
+    uiSetting: {
+      "answer.paper.scale": 1,
+      "score.board.collapse": false,
+      "normal.mode": "keyboard",
+    },
     statusValue: "FORMAL",
     problemTypes: [],
     groupNumber: 0,
@@ -104,13 +108,14 @@ watch(
       // );
       const cq = store.currentQuestion;
       if (cq) {
-        cq.score = markResult.trackList
-          .filter(
-            (v) =>
-              v.mainNumber === cq.mainNumber && v.subNumber === cq.subNumber
-          )
-          .map((v) => v.score)
-          .reduce((acc, v) => (acc += v));
+        cq.score =
+          markResult.trackList
+            .filter(
+              (v) =>
+                v.mainNumber === cq.mainNumber && v.subNumber === cq.subNumber
+            )
+            .map((v) => v.score)
+            .reduce((acc, v) => (acc += v * 100), 0) / 100;
       }
       markResult.scoreList = scoreList as number[];
       // const sortScore = orderBy(markResult.trackList, ['mainNumber', 'subNumber', 'score']);
@@ -127,3 +132,18 @@ watch(
   },
   { deep: true }
 );
+
+watch(
+  () => store.currentQuestion?.score,
+  () => {
+    if (store.setting.mode === ModeEnum.COMMON) {
+      const markResult = findCurrentTaskMarkResult();
+      if (markResult && store.currentTask) {
+        const scoreList = store.currentTask.questionList.map((q) => q.score);
+        markResult.scoreList = scoreList as number[];
+        markResult.markerScore =
+          scoreList.reduce((acc, v) => (acc += v * 100), 0) / 100;
+      }
+    }
+  }
+);

+ 1 - 0
src/types/index.ts

@@ -138,6 +138,7 @@ interface PictureSlice {
 export interface UISetting {
   "score.board.collapse": boolean;
   "answer.paper.scale": number; // 0.2 gap
+  "normal.mode": "keyboard" | "mouse";
 }
 
 export interface MarkResult {