Browse Source

Merge branch 'dev_2.0.0' of http://git.qmth.com.cn/scan-central/client-admin into dev_2.0.0

刘洋 9 months ago
parent
commit
e159ce85c6

+ 6 - 6
pnpm-lock.yaml

@@ -6,7 +6,7 @@ specifiers:
   '@babel/plugin-transform-runtime': ^7.11.5
   '@babel/plugin-transform-runtime': ^7.11.5
   '@babel/preset-env': ^7.11.5
   '@babel/preset-env': ^7.11.5
   '@babel/preset-react': ^7.10.4
   '@babel/preset-react': ^7.10.4
-  '@qmth/ui': ^1.0.15
+  '@qmth/ui': ^1.0.16
   '@types/crypto-js': ^4.2.2
   '@types/crypto-js': ^4.2.2
   '@types/lodash-es': ^4.17.12
   '@types/lodash-es': ^4.17.12
   '@types/mockjs': ^1.0.10
   '@types/mockjs': ^1.0.10
@@ -58,8 +58,7 @@ specifiers:
 
 
 dependencies:
 dependencies:
   '@ant-design/icons-vue': 7.0.1_vue@3.5.0
   '@ant-design/icons-vue': 7.0.1_vue@3.5.0
-  '@qmth/ui': 1.0.15_typescript@5.5.4
-  '@types/spark-md5': 3.0.4
+  '@qmth/ui': 1.0.16_typescript@5.5.4
   '@vueuse/core': 10.11.1_vue@3.5.0
   '@vueuse/core': 10.11.1_vue@3.5.0
   axios: 1.7.7
   axios: 1.7.7
   core-js: 3.38.1
   core-js: 3.38.1
@@ -86,6 +85,7 @@ devDependencies:
   '@types/lodash-es': 4.17.12
   '@types/lodash-es': 4.17.12
   '@types/mockjs': 1.0.10
   '@types/mockjs': 1.0.10
   '@types/node': 20.16.3
   '@types/node': 20.16.3
+  '@types/spark-md5': 3.0.4
   '@vitejs/plugin-vue': 4.6.2_vite@4.5.3+vue@3.5.0
   '@vitejs/plugin-vue': 4.6.2_vite@4.5.3+vue@3.5.0
   '@vitejs/plugin-vue-jsx': 3.0.1_vite@4.5.3+vue@3.5.0
   '@vitejs/plugin-vue-jsx': 3.0.1_vite@4.5.3+vue@3.5.0
   '@vue/compiler-sfc': 3.5.0
   '@vue/compiler-sfc': 3.5.0
@@ -2386,8 +2386,8 @@ packages:
     resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==}
     resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==}
     dev: true
     dev: true
 
 
-  /@qmth/ui/1.0.15_typescript@5.5.4:
-    resolution: {integrity: sha512-DegHhMMsQcaP4fWLCx7ForGmIdr/PNuk7fc4QmVvHLrwJQGwwzVQ5FVABDr6DUjcmvS+qLEtfJU2jjG0fpHnlA==}
+  /@qmth/ui/1.0.16_typescript@5.5.4:
+    resolution: {integrity: sha512-MvMD/V0Ta78t7sdjImGFZDENGXvn1x+/Pdpx6PC+sVoN3SlvhCh2Suulk9+AraNCAtDdKUsAWZNvIG6VWu30/g==}
     dependencies:
     dependencies:
       ant-design-vue: 4.2.1_vue@3.5.0
       ant-design-vue: 4.2.1_vue@3.5.0
       element-resize-detector: 1.2.4
       element-resize-detector: 1.2.4
@@ -2641,7 +2641,7 @@ packages:
 
 
   /@types/spark-md5/3.0.4:
   /@types/spark-md5/3.0.4:
     resolution: {integrity: sha512-qtOaDz+IXiNndPgYb6t1YoutnGvFRtWSNzpVjkAPCfB2UzTyybuD4Tjgs7VgRawum3JnJNRwNQd4N//SvrHg1Q==}
     resolution: {integrity: sha512-qtOaDz+IXiNndPgYb6t1YoutnGvFRtWSNzpVjkAPCfB2UzTyybuD4Tjgs7VgRawum3JnJNRwNQd4N//SvrHg1Q==}
-    dev: false
+    dev: true
 
 
   /@types/verror/1.10.10:
   /@types/verror/1.10.10:
     resolution: {integrity: sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg==}
     resolution: {integrity: sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg==}

+ 2 - 50
src/render/ap/types/recognizeCheck.ts

@@ -1,4 +1,5 @@
 import { PageResult, PageParams, ExamSubjectParams } from "./common";
 import { PageResult, PageParams, ExamSubjectParams } from "./common";
+import { RecognizeArea } from "@/utils/recog/recog";
 
 
 export interface RecognizeConditionItem {
 export interface RecognizeConditionItem {
   code: string;
   code: string;
@@ -130,60 +131,11 @@ export interface RecognizeArbitrateHistoryParams {
   next?: boolean;
   next?: boolean;
 }
 }
 
 
-// 扫描数据相关
-// 参考: /views/RecognizeCheck/data/recogData.json
-export interface RecognizeArbitrateTaskDetail {
-  type: "absent" | "breach" | "paperType" | "question";
+export interface RecognizeArbitrateTaskDetail extends RecognizeArea {
   index: number;
   index: number;
-  fillPosition: Array<{ x: number; y: number }>;
-  fillSize: {
-    w: number;
-    h: number;
-  };
-  fillArea: { x: number; y: number; w: number; h: number };
-  options: string[];
-  result: string[];
   result1: string;
   result1: string;
   result2: string;
   result2: string;
-  multiple: boolean;
   pageIndex: number;
   pageIndex: number;
   groupId: number;
   groupId: number;
   arbitrate: boolean;
   arbitrate: boolean;
 }
 }
-
-export interface RecogDataField {
-  field: string;
-  index: number;
-  content: string;
-  wrong_flag: number;
-  fill_result: RecogDataFillResult[];
-  type: string;
-  rect?: [number, number, number, number];
-}
-
-export interface RecogDataFillResult {
-  main_number: number;
-  sub_number: number;
-  // 所有试题的统一序号
-  index?: number;
-  single: number;
-  fill_option: number[];
-  suspect_flag: number;
-  fill_position: string[];
-  fill_size: [number, number];
-}
-
-export interface RecogDataType {
-  algorithm: number;
-  page_index: number;
-  error_flag: number;
-  blank_flag: number;
-  angle: number;
-  examNumber: RecogDataField;
-  absent: RecogDataField;
-  breach: RecogDataField;
-  pageNumber: RecogDataField;
-  paperType: RecogDataField;
-  question: RecogDataField[];
-  block_struct: number[];
-}

+ 1 - 0
src/render/components.d.ts

@@ -9,6 +9,7 @@ declare module 'vue' {
   export interface GlobalComponents {
   export interface GlobalComponents {
     AButton: typeof import('@qmth/ui')['Button']
     AButton: typeof import('@qmth/ui')['Button']
     ACard: typeof import('@qmth/ui')['Card']
     ACard: typeof import('@qmth/ui')['Card']
+    ACheckbox: typeof import('@qmth/ui')['Checkbox']
     ACol: typeof import('@qmth/ui')['Col']
     ACol: typeof import('@qmth/ui')['Col']
     ACollapse: typeof import('@qmth/ui')['Collapse']
     ACollapse: typeof import('@qmth/ui')['Collapse']
     ACollapsePanel: typeof import('@qmth/ui')['CollapsePanel']
     ACollapsePanel: typeof import('@qmth/ui')['CollapsePanel']

+ 142 - 0
src/render/components/ScanImage/FillAreaSetDialog.vue

@@ -0,0 +1,142 @@
+<template>
+  <a-modal v-model:open="visible" :width="334" centered @ok="confirm">
+    <template #title> 填涂区设置 </template>
+
+    <a-form
+      ref="formRef"
+      :model="formData"
+      :rules="rules"
+      :label-col="{ style: { width: '60px' } }"
+    >
+      <a-form-item label="未填涂">
+        <div class="color-block color-unfill"></div>
+        <a-checkbox v-model:checked="formData.unfillShow">显示</a-checkbox>
+      </a-form-item>
+      <a-form-item label="已填涂">
+        <div class="color-block color-fill"></div>
+        <a-checkbox v-model:checked="formData.fillShow">显示</a-checkbox>
+      </a-form-item>
+      <a-form-item name="borderWidth" label="宽度">
+        <a-input-number
+          v-model:value="formData.borderWidth"
+          style="width: 40px; margin-right: 4px"
+          :min="1"
+          :max="9"
+          :precision="0"
+          :controls="false"
+        />
+        <span class="input-tips">像素</span>
+      </a-form-item>
+    </a-form>
+
+    <template #footer>
+      <div class="box-justify">
+        <a-button type="link" @click="initSet">恢复默认值</a-button>
+
+        <div>
+          <a-button type="text" @click="close">取消</a-button>
+          <a-button type="primary" @click="confirm">确认</a-button>
+        </div>
+      </div>
+    </template>
+  </a-modal>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, watch } from "vue";
+import { message } from "ant-design-vue";
+
+import useModal from "@/hooks/useModal";
+import { useUserStore } from "@/store";
+import { objModifyAssign } from "@/utils/tool";
+
+defineOptions({
+  name: "FillAreaSetDialog",
+});
+
+/* modal */
+const { visible, open, close } = useModal();
+defineExpose({ open, close });
+
+const userStore = useUserStore();
+
+const defaultFormData = {
+  fillColor: "#f53f3f ",
+  fillShow: true,
+  unfillColor: "#165DFF",
+  unfillShow: false,
+  borderWidth: 2,
+};
+
+const emit = defineEmits(["modified"]);
+
+type FormDataType = typeof defaultFormData;
+const formRef = ref();
+const formData: UnwrapRef<FormDataType> = reactive({
+  ...defaultFormData,
+});
+const rules: FormRules<keyof FormDataType> = {
+  borderWidth: [
+    {
+      required: true,
+      message: "请输入",
+      trigger: "change",
+    },
+  ],
+};
+
+function initSet() {
+  objModifyAssign(formData, defaultFormData);
+}
+
+async function confirm() {
+  const valid = await formRef.value?.validate().catch(() => false);
+  if (!valid) return;
+
+  userStore.setRecogFillSet(formData);
+
+  message.success("保存成功!");
+  emit("modified", formData);
+  close();
+}
+
+/* init modal */
+watch(
+  () => visible.value,
+  (val) => {
+    if (val) {
+      modalOpenHandle();
+    }
+  },
+  {
+    immediate: true,
+  }
+);
+
+function modalOpenHandle() {
+  objModifyAssign(formData, userStore.recogFillSet || defaultFormData);
+}
+</script>
+
+<style lang="less" scoped>
+.color-block {
+  display: inline-block;
+  vertical-align: middle;
+  width: 32px;
+  height: 32px;
+  border-radius: 6px;
+  margin-right: 8px;
+
+  &.color-unfill {
+    background: #165dff;
+  }
+  &.color-fill {
+    background: #f53f3f;
+  }
+}
+.input-tips {
+  display: inline-block;
+  vertical-align: middle;
+  margin-top: 3px;
+}
+</style>

+ 0 - 0
src/render/views/RecognizeCheck/data/202302040117-1.jpg → src/render/components/ScanImage/data/paper.jpg


+ 360 - 0
src/render/components/ScanImage/index.vue

@@ -0,0 +1,360 @@
+<template>
+  <div ref="elRef" class="scan-image">
+    <div
+      class="img-body"
+      :style="imageStyle"
+      v-ele-move-directive.prevent.stop="{
+        moveElement: onMoveImg,
+        emitOriginLeftTop: true,
+      }"
+    >
+      <img ref="imgRef" src="./data/paper.jpg" alt="p" @load="initImageSize" />
+      <div class="img-recogs">
+        <div
+          v-for="(item, index) in recogBlocks"
+          :key="index"
+          class="recog-block"
+          :style="item.fillAreaStyle"
+          @click="onAreaClick(item)"
+        >
+          <div
+            v-for="(option, oindex) in item.fillOptionStyles"
+            :key="oindex"
+            :style="option"
+            class="recog-item"
+          ></div>
+        </div>
+      </div>
+    </div>
+    <div class="img-guide">
+      <div class="img-guide-icon is-left"><LeftOutlined /></div>
+      <div class="img-guide-icon is-right"><RightOutlined /></div>
+    </div>
+    <div class="img-actions">
+      <ul>
+        <li @click="onZoomIn"><ZoomInOutlined /></li>
+        <li @click="onZoomOut"><ZoomOutOutlined /></li>
+        <li @click="onZoomNormal">1:1</li>
+        <li @click="onSetRecogStyle"><BgColorsOutlined /></li>
+      </ul>
+    </div>
+    <div class="img-change" @click="onChangeImage"><PictureFilled /></div>
+  </div>
+
+  <!-- FillAreaSetDialog -->
+  <FillAreaSetDialog ref="fillAreaSetDialogRef" @modified="parseRecogBlocks" />
+</template>
+
+<script setup lang="ts">
+import {
+  ZoomInOutlined,
+  ZoomOutOutlined,
+  BgColorsOutlined,
+  LeftOutlined,
+  RightOutlined,
+  PictureFilled,
+} from "@ant-design/icons-vue";
+import { computed, nextTick, ref } from "vue";
+import { objAssign } from "@/utils/tool";
+import { vEleMoveDirective } from "@/directives/eleMove";
+import { RecognizeArea } from "@/utils/recog/recog";
+import { useUserStore } from "@/store";
+
+import FillAreaSetDialog from "./FillAreaSetDialog.vue";
+
+defineOptions({
+  name: "ScanImage",
+});
+
+interface ImageRecogData extends RecognizeArea {
+  [k: string]: any;
+}
+
+const props = defineProps<{
+  imgSrc: string;
+  recogData: ImageRecogData[];
+}>();
+
+const emit = defineEmits(["area-click"]);
+
+const userStore = useUserStore();
+
+const elRef = ref();
+const imgRef = ref();
+const imageSize = ref({
+  width: 0,
+  height: 0,
+  top: 0,
+  left: 0,
+  scale: 1,
+});
+
+const imageStyle = computed(() => {
+  return {
+    width: `${imageSize.value.width}px`,
+    height: `${imageSize.value.height}px`,
+    top: `${imageSize.value.top}px`,
+    left: `${imageSize.value.left}px`,
+    transform: `scale(${imageSize.value.scale}, ${imageSize.value.scale})`,
+  };
+});
+
+function initImageSize() {
+  const imgDom = imgRef.value as HTMLImageElement;
+  const elDom = elRef.value as HTMLDivElement;
+
+  const imgSize = getImageSizePos({
+    win: {
+      width: elDom.clientWidth,
+      height: elDom.clientHeight,
+    },
+    img: {
+      width: imgDom.naturalWidth,
+      height: imgDom.naturalHeight,
+    },
+    rotate: 0,
+  });
+  imageSize.value = objAssign(imageSize.value, imgSize);
+
+  nextTick(() => {
+    parseRecogBlocks();
+  });
+}
+
+interface AreaSize {
+  width: number;
+  height: number;
+}
+function getImageSizePos({
+  win,
+  img,
+  rotate,
+}: {
+  win: AreaSize;
+  img: AreaSize;
+  rotate: number;
+}) {
+  const imageSize = {
+    width: 0,
+    height: 0,
+    top: 0,
+    left: 0,
+  };
+  const isHorizontal = !!(rotate % 180);
+
+  const rateWin = isHorizontal
+    ? win.height / win.width
+    : win.width / win.height;
+  const hwin = isHorizontal
+    ? {
+        width: win.height,
+        height: win.width,
+      }
+    : win;
+
+  const rateImg = img.width / img.height;
+
+  if (rateImg <= rateWin) {
+    imageSize.height = Math.min(hwin.height, img.height);
+    imageSize.width = Math.floor((imageSize.height * img.width) / img.height);
+  } else {
+    imageSize.width = Math.min(hwin.width, img.width);
+    imageSize.height = Math.floor((imageSize.width * img.height) / img.width);
+  }
+  imageSize.left = (win.width - imageSize.width) / 2;
+  imageSize.top = (win.height - imageSize.height) / 2;
+  return imageSize;
+}
+
+// recogBlocks
+interface RecogBlock extends ImageRecogData {
+  fillAreaStyle: Record<string, any>;
+  fillOptionStyles: Array<Record<string, any>>;
+}
+
+const recogBlocks = ref<RecogBlock[]>([]);
+function parseRecogBlocks() {
+  const imgDom = imgRef.value as HTMLImageElement;
+  const rate = imgDom.clientWidth / imgDom.naturalWidth;
+
+  const { unfillColor, unfillShow, fillColor, fillShow, borderWidth } =
+    userStore.recogFillSet;
+  const curBorderWidth = Math.max(1, borderWidth * rate);
+
+  recogBlocks.value = props.recogData.map((item) => {
+    const nitem: RecogBlock = { ...item };
+
+    nitem.fillAreaStyle = {
+      position: "absolute",
+      left: `${item.fillArea.x * rate}px`,
+      top: `${item.fillArea.y * rate}px`,
+      width: `${item.fillArea.w * rate}px`,
+      height: `${item.fillArea.h * rate}px`,
+      zIndex: 9,
+    };
+    nitem.fillOptionStyles = item.optionSizes
+      .map((op) => {
+        const opStyle = {
+          position: "absolute",
+          left: `${op.x * rate}px`,
+          top: `${op.y * rate}px`,
+          width: `${op.w * rate}px`,
+          height: `${op.h * rate}px`,
+          zIndex: 9,
+        };
+
+        if (op.filled && fillShow) {
+          opStyle.border = `${curBorderWidth}px solid ${fillColor}`;
+          return opStyle;
+        }
+
+        if (!op.filled && unfillShow) {
+          opStyle.border = `${curBorderWidth}px solid ${unfillColor}`;
+          return opStyle;
+        }
+
+        return;
+      })
+      .filter((item) => item);
+
+    return nitem;
+  });
+}
+
+function onAreaClick(data: RecogBlock) {
+  console.log(data);
+
+  emit("area-click", data);
+}
+
+// img action
+function onZoomIn() {
+  const scale = imageSize.value.scale;
+  if (scale >= 2) return;
+
+  imageSize.value.scale = Math.min(2, scale * 1.2);
+}
+function onZoomOut() {
+  const scale = imageSize.value.scale;
+  if (scale <= 1) return;
+
+  imageSize.value.scale = Math.max(1, scale * 0.8);
+}
+function onZoomNormal() {
+  initImageSize();
+  imageSize.value.scale = 1;
+}
+
+interface PosSize {
+  left: number;
+  top: number;
+}
+function onMoveImg({ left, top }: PosSize) {
+  imageSize.value.left = left;
+  imageSize.value.top = top;
+}
+
+// change image
+function onChangeImage() {
+  // TODO:
+}
+
+// set recog style
+const fillAreaSetDialogRef = ref();
+function onSetRecogStyle() {
+  fillAreaSetDialogRef.value?.open();
+}
+</script>
+
+<style lang="less" scoped>
+.scan-image {
+  overflow: hidden;
+  position: relative;
+  height: 100%;
+
+  .img-guide {
+    &-icon {
+      position: absolute;
+      top: 50%;
+      width: 28px;
+      height: 32px;
+      margin-top: -16px;
+      background: #ffffff;
+      border-radius: 6px;
+      border: 1px solid @border-color1;
+      line-height: 32px;
+      text-align: center;
+      z-index: 9;
+      cursor: pointer;
+
+      &:hover {
+        background-color: #e8f3ff;
+        border-color: @brand-color;
+        color: @brand-color;
+      }
+
+      &.is-left {
+        left: 12px;
+      }
+      &.is-right {
+        right: 12px;
+      }
+    }
+  }
+
+  .img-change {
+    position: absolute;
+    top: 12px;
+    right: 12px;
+    width: 32px;
+    height: 32px;
+    line-height: 32px;
+    background: #e8f3ff;
+    border-radius: 6px;
+    border: 1px solid #bedaff;
+    color: #4080ff;
+    text-align: center;
+    cursor: pointer;
+    z-index: 9;
+
+    &:hover {
+      opacity: 0.8;
+    }
+  }
+
+  .img-actions {
+    position: absolute;
+    bottom: 12px;
+    right: 12px;
+    background: rgba(89, 89, 89, 0.6);
+    border-radius: 8px;
+    padding: 4px 8px;
+    z-index: 9;
+
+    li {
+      display: inline-block;
+      vertical-align: middle;
+      width: 26px;
+      height: 26px;
+      border-radius: 6px;
+      line-height: 26px;
+      text-align: center;
+      color: #fff;
+      font-size: 16px;
+      cursor: pointer;
+      &:not(:last-child) {
+        margin-right: 4px;
+      }
+
+      &:hover {
+        background: rgba(89, 89, 89, 0.6);
+      }
+    }
+  }
+
+  .img-body {
+    position: absolute;
+    z-index: 2;
+  }
+}
+</style>

+ 2 - 1
src/render/components/SimplePagination/index.vue

@@ -58,7 +58,8 @@ function onChange(page: number, pageSize: number) {
     color: #d9d9d9;
     color: #d9d9d9;
   }
   }
   .ant-pagination-total-text {
   .ant-pagination-total-text {
-    margin-right: 40px;
+    width: 70px;
+    overflow: hidden;
   }
   }
   .ant-pagination-options-quick-jumper {
   .ant-pagination-options-quick-jumper {
     margin: 0;
     margin: 0;

+ 24 - 4
src/render/directives/eleMove.ts

@@ -4,11 +4,13 @@ interface BindValue {
   moveElement?: ({ left, top }: { left: number; top: number }) => void;
   moveElement?: ({ left, top }: { left: number; top: number }) => void;
   moveStop?: ({ left, top }: { left: number; top: number }) => void;
   moveStop?: ({ left, top }: { left: number; top: number }) => void;
   moveStart?: (e: MouseEvent) => void;
   moveStart?: (e: MouseEvent) => void;
+  emitOriginLeftTop?: boolean;
 }
 }
 
 
 export const vEleMoveDirective: Directive<HTMLElement, BindValue> = {
 export const vEleMoveDirective: Directive<HTMLElement, BindValue> = {
   mounted(el, { value, modifiers }) {
   mounted(el, { value, modifiers }) {
     let [_x, _y] = [0, 0];
     let [_x, _y] = [0, 0];
+    let [ox, oy] = [0, 0];
     // 只允许鼠标左键触发
     // 只允许鼠标左键触发
     const moveHandle = (e: MouseEvent) => {
     const moveHandle = (e: MouseEvent) => {
       if (e.button !== 0) return;
       if (e.button !== 0) return;
@@ -19,8 +21,16 @@ export const vEleMoveDirective: Directive<HTMLElement, BindValue> = {
         e.stopPropagation();
         e.stopPropagation();
       }
       }
 
 
-      const left = e.pageX - _x;
-      const top = e.pageY - _y;
+      let left = 0;
+      let top = 0;
+
+      if (value.emitOriginLeftTop) {
+        left = ox + e.pageX - _x;
+        top = oy + e.pageY - _y;
+      } else {
+        left = e.pageX - _x;
+        top = e.pageY - _y;
+      }
 
 
       value.moveElement && value.moveElement({ left, top });
       value.moveElement && value.moveElement({ left, top });
     };
     };
@@ -34,8 +44,16 @@ export const vEleMoveDirective: Directive<HTMLElement, BindValue> = {
         e.stopPropagation();
         e.stopPropagation();
       }
       }
 
 
-      const left = e.pageX - _x;
-      const top = e.pageY - _y;
+      let left = 0;
+      let top = 0;
+
+      if (value.emitOriginLeftTop) {
+        left = ox + e.pageX - _x;
+        top = oy + e.pageY - _y;
+      } else {
+        left = e.pageX - _x;
+        top = e.pageY - _y;
+      }
 
 
       value.moveStop && value.moveStop({ left, top });
       value.moveStop && value.moveStop({ left, top });
       document.removeEventListener("mousemove", moveHandle);
       document.removeEventListener("mousemove", moveHandle);
@@ -52,6 +70,8 @@ export const vEleMoveDirective: Directive<HTMLElement, BindValue> = {
       }
       }
       _x = e.pageX;
       _x = e.pageX;
       _y = e.pageY;
       _y = e.pageY;
+      ox = el.offsetLeft;
+      oy = el.offsetTop;
       value.moveStart && value.moveStart(e);
       value.moveStart && value.moveStart(e);
 
 
       document.addEventListener("mousemove", moveHandle);
       document.addEventListener("mousemove", moveHandle);

+ 25 - 2
src/render/store/modules/user/index.ts

@@ -1,9 +1,22 @@
 import { defineStore } from "pinia";
 import { defineStore } from "pinia";
 import router from "@/router";
 import router from "@/router";
 
 
+interface RecogFillSetType {
+  fillColor: string;
+  fillShow: boolean;
+  unfillColor: string;
+  unfillShow: boolean;
+  borderWidth: number;
+}
+
 export const useUserStore = defineStore<
 export const useUserStore = defineStore<
   "user",
   "user",
-  { curExam: Exam | null; imageCheckLoopTime: number; userInfo: any },
+  {
+    curExam: Exam | null;
+    imageCheckLoopTime: number;
+    userInfo: any;
+    recogFillSet: RecogFillSetType;
+  },
   any,
   any,
   any
   any
 >("user", {
 >("user", {
@@ -14,13 +27,20 @@ export const useUserStore = defineStore<
     },
     },
     {
     {
       storage: localStorage,
       storage: localStorage,
-      paths: ["curExam", "imageCheckLoopTime"],
+      paths: ["curExam", "imageCheckLoopTime", "recogFillSet"],
     },
     },
   ],
   ],
   state: () => ({
   state: () => ({
     userInfo: null,
     userInfo: null,
     curExam: null,
     curExam: null,
     imageCheckLoopTime: 0,
     imageCheckLoopTime: 0,
+    recogFillSet: {
+      fillColor: "#f53f3f ",
+      fillShow: true,
+      unfillColor: "#165DFF",
+      unfillShow: false,
+      borderWidth: 2,
+    },
   }),
   }),
   actions: {
   actions: {
     setUserInfo(info: any) {
     setUserInfo(info: any) {
@@ -32,6 +52,9 @@ export const useUserStore = defineStore<
     setImageCheckLoopTime(imageCheckLoopTime: number) {
     setImageCheckLoopTime(imageCheckLoopTime: number) {
       this.imageCheckLoopTime = imageCheckLoopTime;
       this.imageCheckLoopTime = imageCheckLoopTime;
     },
     },
+    setRecogFillSet(recogFillSet: RecogFillSetType) {
+      this.recogFillSet = recogFillSet;
+    },
     setState(data: any) {
     setState(data: any) {
       this.$patch(data);
       this.$patch(data);
     },
     },

+ 6 - 0
src/render/styles/base.less

@@ -15,6 +15,12 @@ body {
   line-height: 28px;
   line-height: 28px;
 }
 }
 
 
+.box-justify {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
 // color
 // color
 .color-success {
 .color-success {
   color: @success-color;
   color: @success-color;

+ 59 - 0
src/render/styles/pages.less

@@ -798,3 +798,62 @@
     }
     }
   }
   }
 }
 }
+
+// data-check
+.data-check {
+  height: 100%;
+  display: flex;
+  justify-content: space-between;
+  align-items: stretch;
+
+  .check-menu {
+    flex-grow: 0;
+    flex-shrink: 0;
+    width: 278px;
+    border-right: 1px solid @border-color1;
+
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+
+    &-body {
+      flex-grow: 2;
+      padding: 8px;
+      overflow-x: hidden;
+      overflow-y: auto;
+
+      li {
+        height: 24px;
+        line-height: 24px;
+        padding: 0 8px;
+        border-radius: 6px;
+        margin-bottom: 4px;
+        cursor: pointer;
+
+        &.is-active,
+        &:hover {
+          background: #e8f3ff;
+        }
+      }
+    }
+    &-page {
+      flex-grow: 0;
+      flex-shrink: 0;
+      padding: 8px 16px;
+      border-top: 1px solid @border-color1;
+    }
+  }
+  .check-action {
+    flex-grow: 0;
+    flex-shrink: 0;
+    width: 382px;
+    border-left: 1px solid @border-color1;
+    overflow-x: hidden;
+    overflow-y: auto;
+    background-color: #fff;
+  }
+  .check-body {
+    flex-grow: 2;
+    background-color: @background-color;
+  }
+}

+ 0 - 0
src/render/views/RecognizeCheck/data/recogData.json → src/render/utils/recog/data.json


+ 142 - 0
src/render/utils/recog/recog.ts

@@ -0,0 +1,142 @@
+import { maxNum, minNum } from "@/utils/tool";
+
+// recognize ---start >
+// 扫描数据相关
+// 参考:./data.json
+export type RecogAreaType = "absent" | "breach" | "paperType" | "question";
+
+export interface RecogOptionSize {
+  x: number;
+  y: number;
+  w: number;
+  h: number;
+  filled: boolean;
+}
+export interface RecognizeArea {
+  index: number;
+  type: RecogAreaType;
+  fillPosition: Array<{ x: number; y: number }>;
+  fillSize: {
+    w: number;
+    h: number;
+  };
+  fillArea: { x: number; y: number; w: number; h: number };
+  options: string[];
+  optionSizes: RecogOptionSize[];
+  result: string[];
+  multiple: boolean;
+}
+
+export interface RecogDataField {
+  field: string;
+  index: number;
+  content: string;
+  wrong_flag: number;
+  fill_result: RecogDataFillResult[];
+  type: string;
+  rect?: [number, number, number, number];
+}
+
+export interface RecogDataFillResult {
+  main_number: number;
+  sub_number: number;
+  // 所有试题的统一序号
+  index?: number;
+  single: number;
+  fill_option: number[];
+  suspect_flag: number;
+  fill_position: string[];
+  fill_size: [number, number];
+}
+
+export interface RecogDataType {
+  algorithm: number;
+  page_index: number;
+  error_flag: number;
+  blank_flag: number;
+  angle: number;
+  examNumber: RecogDataField;
+  absent: RecogDataField;
+  breach: RecogDataField;
+  pageNumber: RecogDataField;
+  paperType: RecogDataField;
+  question: RecogDataField[];
+  block_struct: number[];
+}
+// recognize ---end >
+
+export function parseRecogData(data: string) {
+  if (!data) return null;
+  const precogData = window.atob(data);
+  const recogData: RecogDataType | null = precogData
+    ? JSON.parse(precogData)
+    : null;
+  return recogData;
+}
+
+const abc = "abcdefghijklmnopqrstuvwxyz".toUpperCase();
+export function parseDetailSize(
+  data: RecogDataFillResult,
+  type: RecogAreaType,
+  index: number
+): RecognizeArea {
+  const result: RecognizeArea = {
+    index,
+    type,
+    fillPosition: [],
+    fillSize: { w: 0, h: 0 },
+    fillArea: { x: 0, y: 0, w: 0, h: 0 },
+    options: [],
+    optionSizes: [],
+    result: [],
+    multiple: false,
+  };
+
+  if (!data) return result;
+
+  result.fillPosition = data.fill_position.map((item) => {
+    const size = item.split(",");
+    return {
+      x: size[0] ? Number(size[0]) : 0,
+      y: size[1] ? Number(size[1]) : 0,
+    };
+  });
+  result.fillSize = {
+    w: data.fill_size[0],
+    h: data.fill_size[1],
+  };
+  const xs = result.fillPosition.map((item) => item.x);
+  const maxX = maxNum(xs);
+  const minX = minNum(xs);
+  result.fillArea = {
+    x: minX,
+    y: result.fillPosition[0].y,
+    w: maxX - minX,
+    h: result.fillPosition[0].y,
+  };
+
+  result.optionSizes = result.fillPosition.map((item, index) => {
+    let filled = false;
+    if (type !== "paperType") {
+      filled = data.fill_option[index] === 1;
+    }
+    return {
+      x: item.x - result.fillArea.x,
+      y: item.y - result.fillArea.y,
+      filled,
+      ...result.fillSize,
+    };
+  });
+
+  if (type === "question") {
+    const options = abc.substring(0, data.fill_position.length).split("");
+    // 空用“#”表示
+    result.options = ["#", ...options];
+    result.result = data.fill_option
+      .map((item, index) => (item ? abc[index] : ""))
+      .filter((item) => item);
+    result.multiple = true;
+  }
+
+  return result;
+}

+ 1 - 1
src/render/views/Audit/ImageCheck/index.vue

@@ -51,7 +51,7 @@
     </div>
     </div>
     <div class="audit-body">
     <div class="audit-body">
       <!-- <img src="" alt=""> -->
       <!-- <img src="" alt=""> -->
-      <img src="../../RecognizeCheck/data/202302040117-1.jpg" />
+      <img src="@/components/ScanImage/data/paper.jpg" />
     </div>
     </div>
     <div class="audit-topinfo">
     <div class="audit-topinfo">
       <a-space :size="6">
       <a-space :size="6">

+ 1 - 1
src/render/views/Audit/InTime/index.vue

@@ -75,7 +75,7 @@
             v-for="(page, pindex) in paper.pages"
             v-for="(page, pindex) in paper.pages"
             :key="pindex"
             :key="pindex"
             class="paper-img"
             class="paper-img"
-            src="../../RecognizeCheck/data/202302040117-1.jpg"
+            src="@/components/ScanImage/data/paper.jpg"
           />
           />
           <!-- <img
           <!-- <img
             v-for="(page, pindex) in paper.pages"
             v-for="(page, pindex) in paper.pages"

+ 46 - 25
src/render/views/DataCheck/index.vue

@@ -7,50 +7,43 @@
         </ul>
         </ul>
       </div>
       </div>
       <div class="check-menu-page">
       <div class="check-menu-page">
-        <div class="page-total">共{{ pagination.total }}页</div>
-        <div class="page-jumper">
-          <a-button @click="onPrevPage">
-            <template #icon> <CaretLeftOutlined /></template>
-          </a-button>
-          <a-button @click="onNextPage">
-            <template #icon> <CaretRightOutlined /></template>
-          </a-button>
-          <span>
-            前往
-            <a-input-number
-              v-model:value="formData.oddNumber"
-              :min="1"
-              :max="10"
-              :precision="0"
-              :controls="false"
-            ></a-input-number>
-          </span>
-        </div>
+        <SimplePagination
+          :total="pagination.total"
+          :page-size="pagination.pageSize"
+          @change="toPage"
+        />
       </div>
       </div>
     </div>
     </div>
-    <div class="check-body"></div>
+    <div class="check-body">
+      <ScanImage v-if="curRecogData.length" :recog-data="curRecogData" />
+    </div>
     <div class="check-action"></div>
     <div class="check-action"></div>
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { ref, onMounted } from "vue";
-import { useRoute, useRouter } from "vue-router";
-import type { TableProps } from "ant-design-vue";
+import { ref, reactive, onMounted } from "vue";
 import { CaretLeftOutlined, CaretRightOutlined } from "@ant-design/icons-vue";
 import { CaretLeftOutlined, CaretRightOutlined } from "@ant-design/icons-vue";
 
 
 import useTable from "@/hooks/useTable";
 import useTable from "@/hooks/useTable";
 
 
 import { DataCheckListFilter, DataCheckListItem } from "@/ap/types/dataCheck";
 import { DataCheckListFilter, DataCheckListItem } from "@/ap/types/dataCheck";
 import { dataCheckList } from "@/ap/dataCheck";
 import { dataCheckList } from "@/ap/dataCheck";
+import { useUserStore } from "@/store";
+
+import SimplePagination from "@/components/SimplePagination/index.vue";
+import ScanImage from "@/components/ScanImage/index.vue";
+import recogSampleData from "@/utils/recog/data.json";
+import { parseDetailSize } from "@/utils/recog/recog";
 
 
 defineOptions({
 defineOptions({
   name: "DataCheck",
   name: "DataCheck",
 });
 });
 
 
+const userStore = useUserStore();
+
 const searchModel = reactive({
 const searchModel = reactive({
-  examId: Number(route.params.examId),
-  subjectCode: route.params.subjectCode,
+  examId: userStore.curExam.id,
 });
 });
 
 
 const { dataList, loading, pagination, getList, toPage, setPageSize } =
 const { dataList, loading, pagination, getList, toPage, setPageSize } =
@@ -76,4 +69,32 @@ function onNextPage() {
   }
   }
   toPage(current);
   toPage(current);
 }
 }
+
+const curRecogData = ref([]);
+// 试题
+let index = 0;
+recogSampleData.question.forEach((gGroup) => {
+  gGroup.fill_result.forEach((qRecog) => {
+    qRecog.index = ++index;
+
+    curRecogData.value.push(parseDetailSize(qRecog, "question", qRecog.index));
+  });
+});
+console.log(curRecogData.value);
+
+// TODO: 测试数据
+dataList.value = "#"
+  .repeat(30)
+  .split("")
+  .map((item, index) => {
+    return {
+      id: index + 1,
+      examNumber: `3600802404012${index}`,
+      name: `考生名${index}`,
+      studentCode: `36008${index}`,
+      subjectCode: "科目代码",
+      subjectName: "科目名称",
+      seatNumber: "11",
+    };
+  });
 </script>
 </script>

+ 14 - 72
src/render/views/RecognizeCheck/RecognizeArbitrate.vue

@@ -77,7 +77,14 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { computed, ref, reactive, onMounted, onBeforeUnmount } from "vue";
+import {
+  computed,
+  ref,
+  reactive,
+  onMounted,
+  onBeforeUnmount,
+  nextTick,
+} from "vue";
 import { useRoute, useRouter } from "vue-router";
 import { useRoute, useRouter } from "vue-router";
 import {
 import {
   CheckCircleFilled,
   CheckCircleFilled,
@@ -97,12 +104,9 @@ import {
 import {
 import {
   RecognizeArbitrateItem,
   RecognizeArbitrateItem,
   RecognizeArbitrateTaskDetail,
   RecognizeArbitrateTaskDetail,
-  RecogDataType,
-  RecogDataFillResult,
   RecognizeArbitrateSavePage,
   RecognizeArbitrateSavePage,
 } from "@/ap/types/recognizeCheck";
 } from "@/ap/types/recognizeCheck";
-import { maxNum, minNum } from "@/utils/tool";
-import { nextTick } from "vue";
+import { parseRecogData, parseDetailSize } from "@/utils/recog/recog";
 
 
 defineOptions({
 defineOptions({
   name: "RecognizeArbitrate",
   name: "RecognizeArbitrate",
@@ -179,18 +183,12 @@ function parseDetails(
 ): RecognizeArbitrateTaskDetail[] {
 ): RecognizeArbitrateTaskDetail[] {
   const details: RecognizeArbitrateTaskDetail[] = [];
   const details: RecognizeArbitrateTaskDetail[] = [];
   data.pages.forEach((page) => {
   data.pages.forEach((page) => {
-    if (!page.recogData) return;
-    const precogData = window.atob(page.recogData);
-    const recogData: RecogDataType | null = precogData
-      ? JSON.parse(precogData)
-      : null;
+    const recogData = parseRecogData(page.recogData);
     if (!recogData) return;
     if (!recogData) return;
 
 
     // 缺考
     // 缺考
     details.push({
     details.push({
-      index: 0,
-      ...parseDetailSize(recogData.absent.fill_result[0], "absent"),
-      multiple: false,
+      ...parseDetailSize(recogData.absent.fill_result[0], "absent", 0),
       result1: page.absent ? page.absent[0] : "",
       result1: page.absent ? page.absent[0] : "",
       result2: page.absent ? page.absent[1] : "",
       result2: page.absent ? page.absent[1] : "",
       pageIndex: page.index,
       pageIndex: page.index,
@@ -200,9 +198,7 @@ function parseDetails(
 
 
     // 违纪
     // 违纪
     details.push({
     details.push({
-      index: 0,
-      ...parseDetailSize(recogData.breach.fill_result[0], "breach"),
-      multiple: false,
+      ...parseDetailSize(recogData.breach.fill_result[0], "breach", 0),
       result1: page.breach ? page.breach[0] : "",
       result1: page.breach ? page.breach[0] : "",
       result2: page.breach ? page.breach[1] : "",
       result2: page.breach ? page.breach[1] : "",
       pageIndex: page.index,
       pageIndex: page.index,
@@ -212,9 +208,7 @@ function parseDetails(
 
 
     // 试卷类型
     // 试卷类型
     details.push({
     details.push({
-      index: 0,
-      ...parseDetailSize(recogData.paperType.fill_result[0], "paperType"),
-      multiple: false,
+      ...parseDetailSize(recogData.paperType.fill_result[0], "paperType", 0),
       result1: page.paperType ? page.paperType[0] : "",
       result1: page.paperType ? page.paperType[0] : "",
       result2: page.paperType ? page.paperType[1] : "",
       result2: page.paperType ? page.paperType[1] : "",
       pageIndex: page.index,
       pageIndex: page.index,
@@ -231,9 +225,7 @@ function parseDetails(
         const arbitrate = questionResult && questionResult.length >= 2;
         const arbitrate = questionResult && questionResult.length >= 2;
 
 
         details.push({
         details.push({
-          index: qRecog.index,
-          ...parseDetailSize(qRecog, "question"),
-          multiple: true,
+          ...parseDetailSize(qRecog, "question", qRecog.index),
           result: arbitrate ? [] : questionResult,
           result: arbitrate ? [] : questionResult,
           result1: questionResult ? questionResult[0] : "",
           result1: questionResult ? questionResult[0] : "",
           result2: questionResult ? questionResult[1] : "",
           result2: questionResult ? questionResult[1] : "",
@@ -248,56 +240,6 @@ function parseDetails(
   return details;
   return details;
 }
 }
 
 
-const abc = "abcdefghijklmnopqrstuvwxyz".toUpperCase();
-type DetailSizeResult = Pick<
-  RecognizeArbitrateTaskDetail,
-  "fillPosition" | "fillSize" | "fillArea" | "options" | "type" | "result"
->;
-function parseDetailSize(
-  data: RecogDataFillResult,
-  type: string
-): DetailSizeResult {
-  const result: DetailSizeResult = {
-    type,
-    fillPosition: [],
-    fillSize: { w: 0, h: 0 },
-    fillArea: { x: 0, y: 0, w: 0, h: 0 },
-    options: [],
-    result: [],
-  };
-
-  if (!data) return result;
-
-  result.fillPosition = data.fill_position.map((item) => {
-    const size = item.split(",");
-    return {
-      x: size[0] ? Number(size[0]) : 0,
-      y: size[1] ? Number(size[1]) : 0,
-    };
-  });
-  result.fillSize = {
-    w: data.fill_size[0],
-    h: data.fill_size[1],
-  };
-  const xs = result.fillPosition.map((item) => item.x);
-  const maxX = maxNum(xs);
-  const minX = minNum(xs);
-  result.fillArea = {
-    x: minX + result.fillSize.w + 10,
-    y: result.fillPosition[0].y - 5,
-    w: maxX - minX + result.fillSize.w + 20,
-    h: result.fillPosition[0].y + result.fillSize.h + 5,
-  };
-
-  if (type === "question") {
-    const options = abc.substring(0, data.fill_position.length).split("");
-    // 空用“#”表示
-    result.options = ["#", ...options];
-  }
-
-  return result;
-}
-
 // 任务执行流程 ----------------- start>
 // 任务执行流程 ----------------- start>
 function setCurTaskDetail() {
 function setCurTaskDetail() {
   curArbitrateTaskDetail.value =
   curArbitrateTaskDetail.value =

+ 6 - 2
src/render/views/RecognizeCheck/RecognizeImage.vue

@@ -1,10 +1,14 @@
 <template>
 <template>
   <div ref="arbitrateImgRef" class="arbitrate-img" @scroll="onImgScroll">
   <div ref="arbitrateImgRef" class="arbitrate-img" @scroll="onImgScroll">
-    <img src="./data/202302040117-1.jpg" alt="扫描结果" @load="onImgLoad" />
+    <img
+      src="@/components/ScanImage/data/paper.jpg"
+      alt="扫描结果"
+      @load="onImgLoad"
+    />
     <!-- <img :src="imgSrc" alt="扫描结果" @load="onImgLoad" /> -->
     <!-- <img :src="imgSrc" alt="扫描结果" @load="onImgLoad" /> -->
   </div>
   </div>
   <div ref="imgThumbRef" class="arbitrate-img-thumb">
   <div ref="imgThumbRef" class="arbitrate-img-thumb">
-    <img src="./data/202302040117-1.jpg" alt="扫描结果" />
+    <img src="@/components/ScanImage/data/paper.jpg" alt="扫描结果" />
     <!-- <img :src="imgSrc" alt="扫描结果" /> -->
     <!-- <img :src="imgSrc" alt="扫描结果" /> -->
     <div
     <div
       class="arbitrate-img-area"
       class="arbitrate-img-area"

+ 1 - 1
src/render/views/Review/ReviewImage.vue

@@ -13,7 +13,7 @@
       >
       >
         <!-- TODO:测试数据 -->
         <!-- TODO:测试数据 -->
         <img
         <img
-          src="../RecognizeCheck/data/202302040117-1.jpg"
+          src="@/components/ScanImage/data/paper.jpg"
           :alt="`第${index + 1}页`"
           :alt="`第${index + 1}页`"
         />
         />
         <!-- <img :src="item.pages[0].sheetUri" :alt="`第${index + 1}页`" /> -->
         <!-- <img :src="item.pages[0].sheetUri" :alt="`第${index + 1}页`" /> -->