Browse Source

仲裁页面增加轨迹模式

刘洋 1 year ago
parent
commit
368fc2e1e3

+ 5 - 1
src/api/arbitratePage.ts

@@ -107,7 +107,9 @@ export async function saveArbitrateTask(
   studentId: string,
   markerScore: number,
   scoreList: Array<number>,
-  unselective: boolean
+  unselective: boolean,
+  trackList?: any,
+  specialTagList?: any
 ) {
   return httpApp.post<CommonResponse>("/admin/exam/arbitrate/saveTask", {
     libraryId,
@@ -115,5 +117,7 @@ export async function saveArbitrateTask(
     markerScore,
     scoreList,
     unselective,
+    trackList,
+    specialTagList,
   });
 }

BIN
src/assets/trackmode.png


+ 1 - 1
src/components/CommonMarkHeader.vue

@@ -122,7 +122,7 @@
         </span>
       </div>
     </div>
-
+    <slot name="modeControl"> </slot>
     <div class="tw-flex tw-place-items-center">
       <UserOutlined class="icon-font icon-with-text" />{{
         store.setting.userName

+ 39 - 12
src/features/arbitrate/Arbitrate.vue

@@ -10,16 +10,21 @@
       />
       <ArbitrateMarkList />
       <mark-body @error="renderError" />
-      <mark-board-key-board
-        v-if="store.shouldShowMarkBoardKeyBoard"
-        @submit="saveTaskToServer(false)"
-        @unselectiveSubmit="saveTaskToServer(true)"
-      />
-      <mark-board-mouse
-        v-if="store.shouldShowMarkBoardMouse"
-        @submit="saveTaskToServer(false)"
-        @unselectiveSubmit="saveTaskToServer(true)"
-      />
+      <template v-if="store.isTrackMode">
+        <mark-board-track @submit="saveTaskToServerByTrack" />
+      </template>
+      <template v-else>
+        <mark-board-key-board
+          v-if="store.shouldShowMarkBoardKeyBoard"
+          @submit="saveTaskToServer(false)"
+          @unselectiveSubmit="saveTaskToServer(true)"
+        />
+        <mark-board-mouse
+          v-if="store.shouldShowMarkBoardMouse"
+          @submit="saveTaskToServer(false)"
+          @unselectiveSubmit="saveTaskToServer(true)"
+        />
+      </template>
     </div>
   </div>
   <AnswerModal />
@@ -37,6 +42,7 @@ import MarkBoardKeyBoard from "@/features/mark/MarkBoardKeyBoard.vue";
 import MarkBoardMouse from "@/features/mark/MarkBoardMouse.vue";
 import MinimapModal from "@/features/mark/MinimapModal.vue";
 import MarkHistory from "@/features/mark/MarkHistory.vue";
+import MarkBoardTrack from "@/features/mark/MarkBoardTrack.vue";
 import { message } from "ant-design-vue";
 import {
   clearArbitrateTask,
@@ -88,6 +94,15 @@ async function updateSetting() {
     splitConfig,
     enableSplit,
   });
+  /*****************************根据本地临时会话存储的mode内容********************************* */
+  let arbitrateLocalMode = sessionStorage.getItem("arbitrate_local_mode");
+  if (
+    arbitrateLocalMode &&
+    (arbitrateLocalMode === "TRACK" || arbitrateLocalMode === "COMMON")
+  ) {
+    store.setting.mode = arbitrateLocalMode;
+  }
+  /************************************************************** */
   store.setting.selective = settingRes.data.selective;
 
   if (store.setting.subject?.paperUrl && store.isMultiMedia) {
@@ -162,7 +177,17 @@ async function getOneOfStuTask() {
   return getOneOfArbitrateTask(subjectCode, groupNumber);
 }
 
-const saveTaskToServer = async (unselective: boolean) => {
+const saveTaskToServerByTrack = async () => {
+  let trackList = store.currentTask.markResult.trackList;
+  let specialTagList = store.currentTask.markResult.specialTagList;
+  await saveTaskToServer(false, trackList, specialTagList);
+};
+
+const saveTaskToServer = async (
+  unselective: boolean,
+  trackList?: any,
+  specialTagList?: any
+) => {
   if (!store.currentTask) return;
   const markResult = store.currentTask.markResult;
   if (!markResult) return;
@@ -220,7 +245,9 @@ const saveTaskToServer = async (unselective: boolean) => {
       store.currentTask.studentId + "",
       store.currentTask.markResult.markerScore,
       store.currentTask.markResult.scoreList,
-      false
+      false,
+      trackList,
+      specialTagList
     );
   }
   if (res.data.success && store.currentTask) {

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

@@ -1,10 +1,300 @@
 <template>
-  <CommonMarkBody v-if="store" @error="$emit('error')" />
+  <CommonMarkBody
+    v-if="store"
+    :hasMarkResultToRender="true"
+    :makeTrack="makeTrack"
+    @error="$emit('error')"
+  />
+  <div class="cursor">
+    <div class="cursor-border">
+      <span class="text">{{
+        store.currentSpecialTag ||
+        (Object.is(store.currentScore, -0) ? "空" : store.currentScore)
+      }}</span>
+    </div>
+  </div>
 </template>
 
 <script setup lang="ts">
 import CommonMarkBody from "@/features/mark/CommonMarkBody.vue";
 import { store } from "@/store/store";
+import CustomCursor from "custom-cursor.js";
+import { onMounted, onUnmounted, watch } from "vue";
+import { SliceImage, SpecialTag, Track } from "@/types";
+
+const makeScoreTrack = (
+  event: MouseEvent,
+  item: SliceImage,
+  maxSliceWidth: number,
+  theFinalHeight: number
+) => {
+  // console.log(item);
+  if (!store.currentQuestion || typeof store.currentScore === "undefined")
+    return;
+  const target = event.target as HTMLImageElement;
+  const track: Track = {
+    mainNumber: store.currentQuestion?.mainNumber,
+    subNumber: store.currentQuestion?.subNumber,
+    score: store.currentScore,
+    unanswered: Object.is(store.currentScore, -0),
+    offsetIndex: item.indexInSliceUrls,
+    offsetX: Math.round(
+      event.offsetX * (target.naturalWidth / target.width) + item.dx
+    ),
+    offsetY: Math.round(
+      event.offsetY * (target.naturalHeight / target.height) + item.dy
+    ),
+    positionX: -1,
+    positionY: -1,
+    number: -1,
+  };
+  track.positionX = (track.offsetX - item.dx) / maxSliceWidth;
+  track.positionY =
+    (track.offsetY - item.dy + item.accumTopHeight) / theFinalHeight;
+
+  // const isIllegalRange = (testNum: number, min: number, max: number) => {
+  //   return testNum < min || testNum > max;
+  // };
+
+  // // 检测有问题,此处没有给原图的宽高,如果有的话,要稍微修改下数据类型
+  // // 但其实下面也做了一个基本检测
+  // if (
+  //   isIllegalRange(track.offsetX, 0, target.naturalWidth) ||
+  //   isIllegalRange(track.offsetY, 0, target.naturalHeight) ||
+  //   isIllegalRange(track.positionX, 0, 1) ||
+  //   isIllegalRange(track.positionY, 0, 1)
+  // ) {
+  //   console.error(
+  //     "错误的track",
+  //     track,
+  //     target.naturalWidth,
+  //     target.naturalHeight
+  //   );
+  //   void message.error("系统错误,请联系管理员!");
+  // }
+
+  if (track.offsetX > item.effectiveWidth + item.dx) {
+    console.log("不在有效宽度内,轨迹不生效");
+    return;
+  }
+  if (
+    item.trackList.some((t) => {
+      return (
+        Math.pow(Math.abs(t.offsetX - track.offsetX), 2) +
+          Math.pow(Math.abs(t.offsetY - track.offsetY), 2) <
+        500
+      );
+    })
+  ) {
+    console.log("两个轨迹相距过近");
+    return;
+  }
+  // 是否保留当前的轨迹分
+  const questionScore =
+    store.currentTask &&
+    store.currentQuestion &&
+    store.currentTask.markResult.scoreList[store.currentQuestion.__index];
+  const ifKeepScore =
+    Math.round(
+      store.currentQuestion.maxScore * 1000 -
+        (questionScore || 0) * 1000 -
+        store.currentScore * 2 * 1000
+    ) / 1000;
+  if (ifKeepScore < 0 && store.currentScore > 0) {
+    store.currentScore = undefined;
+  }
+  const markResult = store.currentTaskEnsured.markResult;
+  const maxNumber =
+    markResult.trackList.length === 0
+      ? 0
+      : Math.max(...markResult.trackList.map((t) => t.number));
+  track.number = maxNumber + 1;
+  // console.log(
+  //   maxNumber,
+  //   track.number,
+  //   markResult.trackList.map((t) => t.number),
+  //   Math.max(...markResult.trackList.map((t) => t.number))
+  // );
+  markResult.trackList = [...markResult.trackList, track];
+  const { __index, mainNumber, subNumber } = store.currentQuestion;
+  markResult.scoreList[__index] =
+    markResult.trackList
+      .filter((t) => t.mainNumber === mainNumber && t.subNumber === subNumber)
+      .map((t) => t.score)
+      .reduce((acc, v) => (acc += Math.round(v * 1000)), 0) / 1000;
+  item.trackList.push(track);
+};
+
+const makeSpecialTagTrack = (
+  event: MouseEvent,
+  item: SliceImage,
+  maxSliceWidth: number,
+  theFinalHeight: number
+) => {
+  // console.log(item);
+  if (!store.currentTask || typeof store.currentSpecialTag === "undefined")
+    return;
+  const target = event.target as HTMLImageElement;
+  const track: SpecialTag = {
+    tagName: store.currentSpecialTag,
+    offsetIndex: item.indexInSliceUrls,
+    offsetX: Math.round(
+      event.offsetX * (target.naturalWidth / target.width) + item.dx
+    ),
+    offsetY: Math.round(
+      event.offsetY * (target.naturalHeight / target.height) + item.dy
+    ),
+    positionX: -1,
+    positionY: -1,
+  };
+  track.positionX = (track.offsetX - item.dx) / maxSliceWidth;
+  track.positionY =
+    (track.offsetY - item.dy + item.accumTopHeight) / theFinalHeight;
+
+  // const isIllegalRange = (testNum: number, min: number, max: number) => {
+  //   return testNum < min || testNum > max;
+  // };
+
+  // if (
+  //   isIllegalRange(track.offsetX, 0, target.naturalWidth) ||
+  //   isIllegalRange(track.offsetY, 0, target.naturalHeight) ||
+  //   isIllegalRange(track.positionX, 0, 1) ||
+  //   isIllegalRange(track.positionY, 0, 1)
+  // ) {
+  //   console.error("错误的track", track);
+  //   void message.error("系统错误,请联系管理员!");
+  // }
+  // if (track.offsetX > item.effectiveWidth + item.dx) {
+  //   console.log("不在有效宽度内,轨迹不生效");
+  //   return;
+  // }
+  if (
+    item.tagList.some((t) => {
+      return (
+        Math.pow(Math.abs(t.offsetX - track.offsetX), 2) +
+          Math.pow(Math.abs(t.offsetY - track.offsetY), 2) <
+        500
+      );
+    })
+  ) {
+    console.log("两个轨迹相距过近");
+    return;
+  }
+  store.currentTaskEnsured.markResult.specialTagList.push(track);
+  item.tagList.push(track);
+};
+
+const makeTrack = (
+  event: MouseEvent,
+  item: SliceImage,
+  maxSliceWidth: number,
+  theFinalHeight: number
+) => {
+  if (store.setting.uiSetting["specialTag.modal"] && store.currentSpecialTag) {
+    makeSpecialTagTrack(event, item, maxSliceWidth, theFinalHeight);
+  } else {
+    makeScoreTrack(event, item, maxSliceWidth, theFinalHeight);
+  }
+};
+
+watch(
+  () => store.setting.mode,
+  () => {
+    const shouldHide = store.setting.mode === "COMMON";
+    if (shouldHide) {
+      // console.log("hide cursor", theCursor);
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+      theCursor && theCursor.destroy();
+    } else {
+      if (document.querySelector(".cursor")) {
+        // console.log("show cursor", theCursor);
+        // theCursor && theCursor.enable();
+        // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+        theCursor = new CustomCursor(".cursor", {
+          focusElements: [
+            {
+              selector: ".mark-body-container",
+              focusClass: "cursor--focused-view",
+            },
+          ],
+        }).initialize();
+      }
+    }
+  }
+);
+let theCursor = null as any;
+onMounted(() => {
+  if (store.isTrackMode) {
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+    theCursor = new CustomCursor(".cursor", {
+      focusElements: [
+        {
+          selector: ".mark-body-container",
+          focusClass: "cursor--focused-view",
+        },
+      ],
+    }).initialize();
+  }
+});
+
+onUnmounted(() => {
+  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+  theCursor && theCursor.destroy();
+});
 
 defineEmits(["error"]);
 </script>
+<style scoped>
+.cursor {
+  color: #ff5050;
+  display: none;
+  pointer-events: none;
+  user-select: none;
+  top: 0;
+  left: 0;
+  position: fixed;
+  will-change: transform;
+  z-index: 1000;
+}
+
+.cursor-border {
+  position: absolute;
+  box-sizing: border-box;
+  align-items: center;
+  border: 1px solid #ff5050;
+  border-radius: 50%;
+  display: flex;
+  justify-content: center;
+  height: 0px;
+  width: 0px;
+  left: 0;
+  top: 0;
+  transform: translate(-50%, -50%);
+  transition: all 360ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+
+.cursor.cursor--initialized {
+  display: block;
+}
+
+.cursor .text {
+  font-size: 2rem;
+  opacity: 0;
+  transition: opacity 80ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+
+.cursor.cursor--off-screen {
+  opacity: 0;
+}
+
+.cursor.cursor--focused .cursor-border,
+.cursor.cursor--focused-view .cursor-border {
+  width: 90px;
+  height: 90px;
+}
+
+.cursor.cursor--focused-view .text {
+  opacity: 1;
+  transition: opacity 360ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+</style>

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

@@ -13,6 +13,43 @@
       <span class="header-small-text">已处理</span>
       <span class="highlight-text">{{ store.status.markedCount ?? "-" }}</span>
     </span>
+    <template #modeControl>
+      <div class="tw-flex">
+        <a-dropdown class="header-bg-color">
+          <template v-if="!store.setting.forceMode" #overlay>
+            <a-menu>
+              <a-menu-item key="1" @click="toggleSettingMode">
+                {{ exchangeModeName }}
+              </a-menu-item>
+            </a-menu>
+          </template>
+          <a-button
+            style="
+              color: rgba(255, 255, 255, 0.5);
+              border: none;
+              display: flex;
+              align-items: center;
+            "
+          >
+            <img
+              src="../../assets/trackmode.png"
+              style="
+                width: 11px;
+                height: 12px;
+                display: inline;
+                margin-right: 2px;
+              "
+            />
+            {{ modeName }}
+            <!-- <div
+              v-if="!store.setting.forceMode"
+              class="dropdown-triangle"
+            ></div> -->
+            <div class="dropdown-triangle"></div>
+          </a-button>
+        </a-dropdown>
+      </div>
+    </template>
   </CommonMarkHeader>
 </template>
 
@@ -21,6 +58,37 @@ import { store } from "@/store/store";
 import { useRoute } from "vue-router";
 import { clearArbitrateTask } from "@/api/arbitratePage";
 import CommonMarkHeader from "@/components/CommonMarkHeader.vue";
+const modeName = $computed(() =>
+  store.setting.mode === "TRACK" ? "轨迹模式" : "普通模式"
+);
+
+const exchangeModeName = $computed(() =>
+  store.setting.mode === "TRACK" ? "普通模式" : "轨迹模式"
+);
+async function toggleSettingMode() {
+  if (store.isTrackMode) {
+    store.setting.mode = "COMMON";
+  } else {
+    store.setting.mode = "TRACK";
+  }
+
+  // store.currentTask.markResult.trackList = store.currentTask.questionList
+  //   .map((q) => q.trackList)
+  //   .flat();
+  // store.currentTask.markResult.specialTagList = [
+  //   ...(store.currentTask.specialTagList ?? []),
+  // ];
+  // store.currentTask.markResult.scoreList = store.currentTask.questionList.map(
+  //   (q) => q.score
+  // );
+  sessionStorage.setItem("arbitrate_local_mode", store.setting.mode);
+
+  const body = document.querySelector("body");
+  if (body) body.innerHTML = "重新加载中...";
+  // 等待一秒后,重新加载页面
+  await new Promise((resolve) => setTimeout(resolve, 1000));
+  window.location.reload();
+}
 
 const route = useRoute();
 let isSingleStudent = !!route.query.historyId;
@@ -41,3 +109,11 @@ let clearTasks = clearArbitrateTask.bind(
   groupNumber
 );
 </script>
+<style>
+.header-bg-color {
+  background-color: var(--header-bg-color);
+}
+.header-bg-color.ant-btn:hover {
+  background-color: var(--app-ant-select-bg-override-color) !important;
+}
+</style>

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

@@ -126,7 +126,7 @@
                 {{
                   isCurrentQuestion(question)
                     ? scoreStr
-                    : store.currentTask.markResult.scoreList[index]
+                    : store.currentTask.markResult?.scoreList[index]
                 }}
               </div>
             </div>