浏览代码

数据检查 & 人工确认

Michael Wang 3 年之前
父节点
当前提交
ba2d48815f

+ 2 - 0
components.d.ts

@@ -16,6 +16,8 @@ declare module '@vue/runtime-core' {
     AModal: typeof import('ant-design-vue/es')['Modal']
     APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
     APopover: typeof import('ant-design-vue/es')['Popover']
+    ARadio: typeof import('ant-design-vue/es')['Radio']
+    ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
     ASlider: typeof import('ant-design-vue/es')['Slider']
     ASpin: typeof import('ant-design-vue/es')['Spin']
     ASwitch: typeof import('ant-design-vue/es')['Switch']

+ 13 - 1
src/components/ZoomPaper.vue

@@ -2,6 +2,13 @@
   <div
     class="tw-flex tw-flex-col tw-gap-2 zoom-container tw-place-content-center"
   >
+    <RotateRightOutlined
+      v-if="props.showRotate"
+      class="icon-font-size-20 tw-cursor-pointer"
+      style="color: white"
+      title="向右旋转"
+      @click="$emit('rotateRight')"
+    />
     <ZoomInOutlined
       class="icon-font-size-20 tw-cursor-pointer"
       :style="{
@@ -32,10 +39,14 @@ import {
   ZoomInOutlined,
   ZoomOutOutlined,
   FullscreenOutlined,
+  RotateRightOutlined,
 } from "@ant-design/icons-vue";
 import { computed, onMounted, onUnmounted } from "vue";
 import { store } from "@/store/store";
 
+const props = defineProps<{ showRotate?: boolean }>();
+defineEmits(["rotateRight"]);
+
 const upScale = () => {
   const s = store.setting.uiSetting["answer.paper.scale"];
   if (s < 3)
@@ -79,7 +90,8 @@ onUnmounted(() => {
   bottom: 10px;
   right: 320px;
   width: 40px;
-  height: 100px;
+  /* height: 100px; */
+  padding: 10px 0;
   border-radius: 10px;
 }
 .icon-font-size-20 {

+ 279 - 57
src/features/admin/confirmPaper/ConfirmPaper.vue

@@ -1,19 +1,56 @@
 <template>
-  <div>
-    <header class="tw-flex">
-      <div>进度:{{ currentIndex }}/{{ allIds.length }}</div>
-      <div>姓名:{{ student?.name }}</div>
-      <div>准考证号:{{ student?.examNumber }}</div>
-      <div>学号:{{ student?.studentCode }}</div>
-      <div>科目:{{ student?.subjectCode }}-{{ student?.subjectName }}</div>
-      <div>客观分:{{ student?.objectiveScore }}</div>
-      <div>主观分:{{ student?.subjectiveScore }}</div>
-      <div></div>
+  <div class="tw-h-screen">
+    <header
+      class="tw-flex tw-gap-2 tw-justify-between tw-items-center header-container"
+    >
+      <div class="tw-ml-2">
+        进度:<span class="highlight-text">
+          {{ currentIndex }}/{{ allIds.length }}
+        </span>
+      </div>
+      <div>
+        姓名:<span class="highlight-text">{{ student?.name }}</span>
+      </div>
+      <div>
+        准考证号:<span class="highlight-text">{{ student?.examNumber }}</span>
+      </div>
+      <div>
+        学号:<span class="highlight-text">{{ student?.studentCode }}</span>
+      </div>
+      <div>
+        科目:<span class="highlight-text">
+          {{ student?.subjectCode }}-{{ student?.subjectName }}
+        </span>
+      </div>
+      <div>
+        客观分:<span class="highlight-text">{{
+          student?.objectiveScore
+        }}</span>
+      </div>
+      <div>
+        主观分:<span class="highlight-text">{{
+          student?.subjectiveScore
+        }}</span>
+      </div>
+      <div class="tw-flex tw-items-center tw-gap-2 tw-mx-8">
+        <span
+          v-for="(u, index) in student?.sheetUrls"
+          :key="index"
+          class="tw-cursor-pointer"
+          :class="currentImage === index && 'highlight-text'"
+          @click="currentImage = index"
+        >
+          {{ index + 1 }}
+        </span>
+      </div>
     </header>
 
-    <div>
-      <div>
-        <div>
+    <div class="tw-flex" style="height: calc(100% - 56px)">
+      <div
+        style="flex: 0 1 420px"
+        class="tw-flex tw-flex-col tw-justify-between"
+      >
+        <div class="tw-m-2">
           <div>
             是否缺考:
             <a-radio-group v-if="student" v-model:value="student.absent">
@@ -21,7 +58,7 @@
               <a-radio :value="false">否</a-radio>
             </a-radio-group>
           </div>
-          <div>
+          <div class="tw-my-2">
             试卷类型:
             <a-input
               v-if="student"
@@ -30,51 +67,103 @@
               style="width: 40px"
             />
           </div>
-        </div>
 
-        <div v-if="student?.answers">
-          <div v-for="group in answersComputed" :key="group.mainNumber">
-            <h2>
-              {{ group.mainNumber }}、{{ group.mainTitle }} ({{
-                group.subs.length
-              }})
-            </h2>
-            <div class="tw-flex tw-gap-4">
-              <div v-for="question in group.subs" :key="question.subNumber">
-                <span>{{ question.subNumber }}</span>
-                <a-input
-                  :value="question.answer"
-                  style="width: 40px"
-                  :maxLength="
-                    group.mainTitle.match(/多选|多项|不定项/) ? 100 : 1
-                  "
-                  @input="($event) => changeAnswer($event, question)"
-                  @blur="($event) => changeAnswer($event, question, '#')"
-                />
+          <div v-if="student?.answers" class="tw-mt-4">
+            <div
+              v-for="group in answersComputed"
+              :key="group.mainNumber"
+              class="tw-mt-2"
+            >
+              <h2>
+                {{ group.mainNumber }}、{{ group.mainTitle }} ({{
+                  group.subs.length
+                }})
+              </h2>
+              <div class="tw-flex tw-gap-4">
+                <div v-for="question in group.subs" :key="question.subNumber">
+                  <span>{{ question.subNumber }}. </span>
+                  <a-input
+                    :value="question.answer"
+                    style="width: 40px"
+                    :maxLength="
+                      group.mainTitle.match(/多选|多项|不定项/) ? 100 : 1
+                    "
+                    @input="changeAnswer($event, question)"
+                    @blur="changeAnswer($event, question, '#')"
+                  />
+                </div>
               </div>
             </div>
           </div>
         </div>
 
-        <div>
-          <a-button @click="saveStudentAnswer">保存</a-button>
-          <a-button :disabled="currentIndex === 1" @click="getPreviousStudent"
-            >上一份</a-button
-          >
-          <a-button
-            :disabled="currentIndex === allIds.length"
-            @click="getNextStudent"
-            >下一份</a-button
-          >
+        <div class="tw-flex tw-justify-between tw-bg-white tw-p-4">
+          <a-button type="primary" shape="round" @click="saveStudentAnswer">
+            保存
+          </a-button>
+          <div>
+            <a-button
+              shape="round"
+              :disabled="currentIndex === 1"
+              class="tw-mr-4"
+              @click="getPreviousStudent"
+            >
+              上一份
+            </a-button>
+            <a-button
+              shape="round"
+              :disabled="currentIndex === allIds.length"
+              @click="getNextStudent"
+            >
+              下一份
+            </a-button>
+          </div>
         </div>
       </div>
 
-      <div>
-        <div>
-          <img v-for="item in student?.sheetUrls" :key="item" :src="item" />
+      <div style="flex: 1" class="mark-body-container tw-relative">
+        <ArrowLeftOutlined
+          v-if="student"
+          class="tw-cursor-pointer tw-absolute"
+          style="top: 45%; left: 20px; z-index: 1; font-size: 40px"
+          :style="{
+            color: currentImage === 0 ? 'white' : 'blueviolet',
+          }"
+          title="上一张"
+          @click="switchImageArrow({ left: true })"
+        />
+        <ArrowRightOutlined
+          v-if="student"
+          class="tw-cursor-pointer tw-absolute"
+          style="top: 45%; right: 20px; z-index: 1; font-size: 40px"
+          :style="{
+            color:
+              currentImage === student.sheetUrls.length - 1
+                ? 'white'
+                : 'blueviolet',
+          }"
+          title="上一张"
+          @click="switchImageArrow({ right: true })"
+        />
+        <div :style="{ width: answerPaperScale }">
+          <img
+            v-for="(item, index) in student?.sheetUrls"
+            :key="item"
+            class="tw-object-cover"
+            :src="item"
+            :style="{
+              display: index === currentImage ? 'block' : 'none',
+              rotate: rotateDegree + 'deg',
+              translate: rotateDegree ? '0 calc(30vh)' : '',
+            }"
+            @click="switchImage"
+            @contextmenu="showBigImage"
+          />
         </div>
       </div>
     </div>
+
+    <ZoomPaper v-if="student" showRotate @rotateRight="rotateRight" />
   </div>
 </template>
 
@@ -84,6 +173,14 @@ import { message } from "ant-design-vue";
 import { onMounted, reactive } from "vue";
 import { useRoute } from "vue-router";
 import { CheckSetting, StudentInfo } from "./check";
+import "viewerjs/dist/viewer.css";
+import Viewer from "viewerjs";
+import { store } from "@/store/store";
+import ZoomPaper from "@/components/ZoomPaper.vue";
+import { useTimers } from "@/setups/useTimers";
+import { ArrowLeftOutlined, ArrowRightOutlined } from "@ant-design/icons-vue";
+
+const { addTimeout } = useTimers();
 
 const route = useRoute();
 const checkType = route.query.checkType;
@@ -91,6 +188,8 @@ const queryId = route.query.queryId as string;
 let pageType: "DATA_CHECK" | "HAND_CHECK" = "HAND_CHECK";
 if (queryId) {
   pageType = "DATA_CHECK";
+  sessionStorage.setItem(queryId, localStorage.getItem(queryId) || "[]");
+  localStorage.removeItem(queryId);
 }
 
 onMounted(async () => {
@@ -145,12 +244,12 @@ const answersComputed = $computed(() => {
 async function getSetting() {
   let res: any;
   if (pageType === "DATA_CHECK") {
-    const q: Record<string, string> = JSON.parse(
-      sessionStorage.getItem(queryId) || "{}"
+    const query: Array<{ name: string; value: string }> = JSON.parse(
+      sessionStorage.getItem(queryId) || "[]"
     );
     const form = new FormData();
-    for (const [k, v] of Object.entries(q)) {
-      form.append(k, v + "");
+    for (const v of query) {
+      form.append(v.name, v.value + "");
     }
     res = await httpApp.post("/admin/exam/check/answer/getSetting", form);
   } else {
@@ -179,14 +278,15 @@ async function getStudent(studentId: number) {
   ).data;
   stu?.sheetUrls.forEach((v, i, a) => (a[i] = setting.fileServer + v));
   currentStudentId = stu.id;
+  currentImage = 0;
 
   // for dev
-  stu.answers = [
-    { mainNumber: 1, subNumber: "1", answer: "A" },
-    { mainNumber: 1, subNumber: "2", answer: "B" },
-    { mainNumber: 2, subNumber: "1", answer: "#" },
-  ];
-  stu.titles = { 1: "单选题", 2: "多选题" };
+  // stu.answers = [
+  //   { mainNumber: 1, subNumber: "1", answer: "A" },
+  //   { mainNumber: 1, subNumber: "2", answer: "B" },
+  //   { mainNumber: 2, subNumber: "1", answer: "#" },
+  // ];
+  // stu.titles = { 1: "单选题", 2: "多选题" };
   return stu;
 }
 
@@ -242,4 +342,126 @@ async function saveStudentAnswer() {
     void message.success("所有考生已处理完毕。");
   }
 }
+
+//#region : 显示大图,供查看和翻转
+let currentImage = $ref(0);
+function switchImageArrow({
+  left = false,
+  right = false,
+}: {
+  left?: boolean;
+  right?: boolean;
+}) {
+  if (left) {
+    if (currentImage > 0) {
+      currentImage--;
+    }
+  }
+  if (right) {
+    if (currentImage < student!.sheetUrls.length - 1) {
+      currentImage++;
+    }
+  }
+}
+
+function switchImage(event: MouseEvent) {
+  const image = event.target as HTMLImageElement;
+  const layerX: number = (event as any).layerX;
+  if (layerX * 2 < image.width) {
+    if (currentImage > 0) {
+      currentImage--;
+    }
+  } else {
+    if (currentImage < student!.sheetUrls.length - 1) {
+      currentImage++;
+    }
+  }
+}
+
+const showBigImage = (event: MouseEvent) => {
+  event.preventDefault();
+  // console.log(event);
+  let viewer: Viewer = null as unknown as Viewer;
+  viewer && viewer.destroy();
+  viewer = new Viewer((event.target as HTMLElement).parentElement!, {
+    // inline: true,
+    viewed() {
+      viewer.zoomTo(1);
+    },
+    hidden() {
+      viewer.destroy();
+    },
+    zIndex: 1000000,
+  });
+  viewer.show();
+};
+//#endregion : 显示大图,供查看和翻转
+
+//#region : 放大缩小和之后的滚动
+const answerPaperScale = $computed(() => {
+  // 放大、缩小不影响页面之前的滚动条定位
+  let percentWidth = 0;
+  let percentTop = 0;
+  const container = document.querySelector(
+    ".mark-body-container"
+  ) as HTMLDivElement;
+  if (container) {
+    const { scrollLeft, scrollTop, scrollWidth, scrollHeight } = container;
+    percentWidth = scrollLeft / scrollWidth;
+    percentTop = scrollTop / scrollHeight;
+  }
+
+  addTimeout(() => {
+    if (container) {
+      const { scrollWidth, scrollHeight } = container;
+      container.scrollTo({
+        left: scrollWidth * percentWidth,
+        top: scrollHeight * percentTop,
+      });
+    }
+  }, 10);
+  const scale = store.setting.uiSetting["answer.paper.scale"];
+  return scale * 100 + "%";
+});
+//#endregion : 放大缩小和之后的滚动
+
+//#region rotateRight
+let rotateDegree = $ref(0);
+function rotateRight() {
+  rotateDegree = (rotateDegree + 90) % 360;
+}
+//#endregion
 </script>
+
+<style scoped>
+.header-container {
+  position: relative;
+  height: 56px;
+  line-height: 16px;
+
+  background-color: var(--header-bg-color);
+  color: rgba(255, 255, 255, 0.5);
+}
+.highlight-text {
+  color: white;
+  font-size: var(--app-title-font-size);
+}
+
+.mark-body-container {
+  position: relative;
+  min-height: calc(100vh - 56px);
+  height: calc(100vh - 56px);
+  overflow: auto;
+  /* background-size: 8px 8px;
+  background-image: linear-gradient(to right, #e7e7e7 4px, transparent 4px),
+    linear-gradient(to bottom, transparent 4px, #e7e7e7 4px); */
+  background-color: var(--app-container-bg-color);
+  background-image: linear-gradient(45deg, #e0e0e0 25%, transparent 25%),
+    linear-gradient(-45deg, #e0e0e0 25%, transparent 25%),
+    linear-gradient(45deg, transparent 75%, #e0e0e0 75%),
+    linear-gradient(-45deg, transparent 75%, #e0e0e0 75%);
+  background-size: 20px 20px;
+  background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
+  transform: inherit;
+}
+</style>

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

@@ -187,6 +187,7 @@
         <div
           class="single-score tw-cursor-pointer tw-font-bold"
           :class="Object.is(store.currentScore, -0) && 'current-score'"
+          title="按 # 可以选中"
           @click="chooseScore(-0)"
         >

+ 1 - 1
src/router/index.ts

@@ -51,7 +51,7 @@ const routes = [
   {
     // 数据检查
     path: "/admin/exam/check/answer/start",
-    name: "ConfirmPaper",
+    name: "ConfirmData",
     component: () => import("@/features/admin/confirmPaper/ConfirmPaper.vue"),
   },
   {