瀏覽代碼

feat: 仲裁调整

zhangjie 8 月之前
父節點
當前提交
8066f6d6fe

+ 29 - 72
src/render/styles/pages.less

@@ -248,10 +248,14 @@
   &-img {
     height: 100%;
     overflow: auto;
-    img {
-      display: block;
+
+    &-box {
       width: calc(100% - 150px * 2);
       margin: 0 auto;
+      position: relative;
+    }
+    img {
+      display: block;
       height: auto;
       max-height: none;
     }
@@ -279,76 +283,6 @@
     cursor: move;
     top: 0;
   }
-
-  &-modal {
-    position: absolute;
-    width: 1002px;
-    top: 8px;
-    left: 50%;
-    margin-left: -501px;
-    background: #f2f3f5;
-    box-shadow: 0px 10px 10px 0px rgba(54, 61, 89, 0.2);
-    border-radius: 12px;
-    border: 1px solid @background-color;
-    padding: 16px;
-
-    .modal-box {
-      min-height: 88px;
-      height: 100%;
-      background: #ffffff;
-      border-radius: 6px;
-      border: 1px solid #e5e5e5;
-      padding: 16px;
-      color: @text-color1;
-      overflow: hidden;
-
-      .box-title {
-        height: 22px;
-        font-weight: 400;
-        font-size: 14px;
-        color: @text-color3;
-        line-height: 22px;
-        margin-bottom: 6px;
-      }
-      .box-cont {
-        height: 28px;
-        font-weight: 500;
-        font-size: 20px;
-        line-height: 28px;
-      }
-
-      &.is-btn {
-        cursor: pointer;
-
-        &:hover {
-          background: #e8f3ff;
-          color: @brand-color;
-        }
-      }
-    }
-
-    .modal-origin {
-      background-color: @background-color;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      img {
-        height: 40px;
-      }
-    }
-
-    .modal-options {
-      line-height: 54px;
-      text-align: center;
-      .ant-btn {
-        margin: 0 8px;
-        padding-left: 8px;
-        padding-right: 8px;
-        text-align: center;
-        min-width: 32px;
-      }
-    }
-  }
 }
 
 // review
@@ -944,6 +878,9 @@
     &.is-col1 {
       width: 122px;
     }
+    &.is-col2 {
+      width: 244px;
+    }
   }
 
   .modal-box {
@@ -1029,6 +966,26 @@
     }
   }
 }
+.recog-arbitrate-edit-dialog {
+  top: 110px !important;
+  left: 16px !important;
+  right: 16px !important;
+  bottom: auto !important;
+  min-height: 216px;
+  overflow: hidden !important;
+  border-radius: 12px;
+  box-shadow: 0px 10px 10px 0px rgba(54, 61, 89, 0.2);
+
+  .ant-modal {
+    width: auto !important;
+    transform-origin: 0 !important;
+    top: 0 !important;
+    padding: 0 !important;
+  }
+  .ant-modal-body {
+    padding: 0;
+  }
+}
 
 .cut-image-dialog {
   .ant-modal-content {

+ 9 - 8
src/render/utils/recog/recog.ts

@@ -27,7 +27,7 @@ export interface RecognizeArea {
   result: string[];
   multiple: boolean;
   // 当前区域的切图
-  sliceImg: string;
+  areaImg: string;
 }
 
 export interface RecogDataField {
@@ -79,7 +79,6 @@ export interface ImageRecogData extends RecognizeArea {
 }
 
 export interface RecogBlock extends RecognizeArea {
-  areaImg: string;
   fillAreaStyle: Record<string, any>;
   fillOptionStyles: Array<Record<string, any>>;
   [k: string]: any;
@@ -112,7 +111,7 @@ export function parseDetailSize(
     optionSizes: [],
     result: [],
     multiple: false,
-    sliceImg: "",
+    areaImg: "",
   };
 
   if (!data) return result;
@@ -140,11 +139,13 @@ export function parseDetailSize(
   const xs = result.fillPosition.map((item) => item.x);
   const maxX = maxNum(xs);
   const minX = minNum(xs);
+  const areaOffsetX = 1.5 * result.fillSize.w;
+  const areaOffsetY = 0.4 * result.fillSize.h;
   result.fillArea = {
-    x: minX,
-    y: result.fillPosition[0].y,
-    w: maxX - minX + result.fillSize.w,
-    h: result.fillSize.h,
+    x: minX - areaOffsetX,
+    y: result.fillPosition[0].y - areaOffsetY,
+    w: maxX - minX + result.fillSize.w + 2 * areaOffsetX,
+    h: result.fillSize.h + 2 * areaOffsetY,
   };
 
   result.optionSizes = result.fillPosition.map((item, index) => {
@@ -164,7 +165,7 @@ export function parseDetailSize(
     const options = abc.substring(0, data.fill_position.length).split("");
     // 空用“#”表示
     result.options = ["#", ...options];
-    console.log(result.options, options, fillResultList);
+    // console.log(result.options, options, fillResultList);
     result.result = isJingHao
       ? ["#"]
       : options.filter((r, ind) => fillResultList[ind] === 1);

+ 23 - 11
src/render/views/DataCheck/ScanImage/RecogEditDialog.vue

@@ -6,7 +6,9 @@
     :closable="false"
     :mask="false"
     :maskClosable="false"
+    :keyboard="false"
     wrapClassName="recog-edit-dialog"
+    :afterClose="afterClose"
   >
     <div class="recog-edit">
       <div class="recog-row">
@@ -29,10 +31,7 @@
               <div
                 v-for="(option, index) in recogData.options.slice(1)"
                 :key="option"
-                :class="[
-                  'select-option',
-                  { 'is-active': selectResult.includes(option) },
-                ]"
+                class="select-option"
                 :style="getOptionStyle(index)"
                 @click="selectOption(option)"
               ></div>
@@ -81,7 +80,7 @@ import { message } from "ant-design-vue";
 import useModal from "@/hooks/useModal";
 import { RecogBlock } from "@/utils/recog/recog";
 import { getBoxImageSize } from "@/utils/tool";
-import { transform } from "lodash-es";
+import { useUserStore } from "@/store";
 
 defineOptions({
   name: "RecogEditDialog",
@@ -95,7 +94,8 @@ const props = defineProps<{
   recogData: RecogBlock;
 }>();
 
-const emit = defineEmits(["confirm"]);
+const emit = defineEmits(["confirm", "close"]);
+const userStore = useUserStore();
 
 const selectResult = ref([] as string[]);
 
@@ -124,14 +124,17 @@ const recogResult = computed(() => {
 });
 
 function getOptionStyle(index: number): Record<string, any> {
-  const offTop = props.recogData.fillSize.h;
-  const offLeft = props.recogData.fillSize.w;
   const optionSize = props.recogData.optionSizes[index];
+  const option = props.recogData.options[index + 1];
+  const borderColor = selectResult.value.includes(option)
+    ? userStore.recogFillSet.fillColor
+    : userStore.recogFillSet.unfillColor;
   return {
     width: `${optionSize.w}px`,
     height: `${optionSize.h}px`,
-    left: `${optionSize.x + offLeft}px`,
-    top: `${offTop}px`,
+    left: `${optionSize.x}px`,
+    top: `${optionSize.y}px`,
+    borderColor,
   };
 }
 
@@ -197,10 +200,16 @@ function removeKeyEvent() {
 }
 
 function keyEventHandle(e: KeyboardEvent) {
-  if (e.keyCode == 13 && !e.repeat) {
+  if (e.repeat) return;
+
+  if (e.keyCode == 13) {
     e.preventDefault();
     onConfirm();
     return;
+  } else if (e.code === "Escape") {
+    e.preventDefault();
+    close();
+    return;
   }
 }
 
@@ -214,6 +223,9 @@ function onConfirm() {
   close();
 }
 
+function afterClose() {
+  emit("close");
+}
 // init
 watch(
   () => visible.value,

+ 31 - 9
src/render/views/DataCheck/ScanImage/index.vue

@@ -19,7 +19,10 @@
         <div
           v-for="(item, index) in recogBlocks"
           :key="index"
-          class="recog-block"
+          :class="[
+            'recog-block',
+            { 'is-active': curRecogBlock?.index === item.index },
+          ]"
           :style="item.fillAreaStyle"
           @click="onAreaClick(item)"
         >
@@ -73,6 +76,7 @@
     ref="recogEditDialogRef"
     :recog-data="curRecogBlock"
     @confirm="onRecogEditConfirm"
+    @close="clearCurBlock"
   />
 </template>
 
@@ -297,16 +301,9 @@ const recogEditDialogRef = ref();
 async function onAreaClick(data: RecogBlock) {
   if (!curPage.value) return;
   curRecogBlock.value = data;
-  // 基于 fillArea 四周扩展一个 fillSize 尺寸
-  const area = {
-    x: data.fillArea.x - data.fillSize.w,
-    y: data.fillArea.y - data.fillSize.h,
-    w: data.fillArea.w + data.fillSize.w * 2,
-    h: data.fillArea.h + data.fillSize.h * 2,
-  };
   curRecogBlock.value.areaImg = await getSliceFileUrl(
     curPage.value.sheetUri,
-    area
+    data.fillArea
   );
   nextTick(() => {
     recogEditDialogRef.value?.open();
@@ -330,6 +327,10 @@ async function onRecogEditConfirm(result: string[]) {
   }
 }
 
+function clearCurBlock() {
+  curRecogBlock.value = null;
+}
+
 // img action
 function onZoomIn() {
   const scale = imageSize.value.scale;
@@ -484,5 +485,26 @@ watch(
     position: absolute;
     z-index: 2;
   }
+
+  .recog-block {
+    cursor: pointer;
+    &:hover {
+      background-color: rgba(241, 214, 110, 0.3);
+    }
+    &.is-active {
+      background-color: rgba(241, 214, 110, 0.3);
+      &::after {
+        content: "";
+        display: block;
+        position: absolute;
+        z-index: 1;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        border: 1px dashed #000;
+      }
+    }
+  }
 }
 </style>

+ 257 - 0
src/render/views/RecognizeCheck/RecogArbitrateEditDialog.vue

@@ -0,0 +1,257 @@
+<template>
+  <a-modal
+    v-model:open="visible"
+    width="100%"
+    :footer="false"
+    :closable="false"
+    :mask="false"
+    :maskClosable="false"
+    :keyboard="false"
+    wrapClassName="recog-arbitrate-edit-dialog"
+  >
+    <div class="recog-edit">
+      <div class="recog-row">
+        <div class="recog-col is-static is-col2">
+          <div class="modal-box">
+            <p class="box-title">{{ recogTitle }}</p>
+            <p class="box-cont">{{ recogTitleDesc }}</p>
+          </div>
+        </div>
+        <div class="recog-col is-grow">
+          <div class="modal-box modal-origin">
+            <div class="modal-origin-body" :style="areaImgStyle">
+              <img
+                v-if="recogData.areaImg"
+                ref="areaImgRef"
+                :src="recogData.areaImg"
+                alt="截图"
+                @load="areaImgLoad"
+              />
+              <template v-if="recogData.type === 'question'">
+                <div
+                  v-for="(option, index) in recogData.options.slice(1)"
+                  :key="option"
+                  class="select-option"
+                  :style="getOptionStyle(index)"
+                  @click="selectOption(option)"
+                ></div>
+              </template>
+            </div>
+          </div>
+        </div>
+        <div class="recog-col is-static is-col2">
+          <div class="modal-box is-btn" @click="changePrevTaskDetail">
+            <p class="box-title">左键</p>
+            <p class="box-cont">上一个</p>
+          </div>
+        </div>
+      </div>
+      <div class="recog-row">
+        <div class="recog-col is-static is-col1">
+          <div class="modal-box">
+            <p class="box-title">一评结果</p>
+            <p class="box-cont">{{ recogData.result1 }}</p>
+          </div>
+        </div>
+        <div class="recog-col is-static is-col1">
+          <div class="modal-box">
+            <p class="box-title">二评结果</p>
+            <p class="box-cont">{{ recogData.result2 }}</p>
+          </div>
+        </div>
+        <div class="recog-col is-grow">
+          <div class="modal-box modal-options">
+            <a-button
+              v-for="option in recogData.options"
+              :key="option"
+              :type="selectResult.includes(option) ? 'primary' : 'default'"
+              @click="selectOption(option)"
+              >{{ option === "#" ? "空" : option }}</a-button
+            >
+          </div>
+        </div>
+        <div class="recog-col is-static is-col2">
+          <div class="modal-box is-btn" @click="onConfirm">
+            <p class="box-title">Enter键</p>
+            <p class="box-cont">下一个</p>
+          </div>
+        </div>
+      </div>
+    </div>
+  </a-modal>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch, onBeforeUnmount } from "vue";
+import { message } from "ant-design-vue";
+import useModal from "@/hooks/useModal";
+
+import { RecognizeArbitrateTaskDetail } from "@/ap/types/recognizeCheck";
+import { getBoxImageSize } from "@/utils/tool";
+import { useUserStore } from "@/store";
+
+defineOptions({
+  name: "RecogArbitrateEditDialog",
+});
+
+/* modal */
+const { visible, open, close } = useModal();
+defineExpose({ open, close });
+
+const props = defineProps<{
+  recogData: RecognizeArbitrateTaskDetail;
+}>();
+
+const emit = defineEmits(["confirm", "prev"]);
+const userStore = useUserStore();
+
+const selectResult = ref([] as string[]);
+
+const titles = {
+  question: "客观题",
+  absent: "缺考",
+  breach: "违纪",
+  paperType: "卷型",
+};
+const recogTitle = computed(() => {
+  return titles[props.recogData.type];
+});
+
+const recogTitleDesc = computed(() => {
+  if (props.recogData.type === "question") {
+    return `#${props.recogData.index}`;
+  }
+  return "--";
+});
+
+function getOptionStyle(index: number): Record<string, any> {
+  const optionSize = props.recogData.optionSizes[index];
+  const option = props.recogData.options[index + 1];
+  const borderColor = selectResult.value.includes(option)
+    ? userStore.recogFillSet.fillColor
+    : userStore.recogFillSet.unfillColor;
+  return {
+    width: `${optionSize.w}px`,
+    height: `${optionSize.h}px`,
+    left: `${optionSize.x}px`,
+    top: `${optionSize.y}px`,
+    borderColor,
+  };
+}
+
+const areaImgRef = ref();
+const areaImgStyle = ref({});
+function areaImgLoad() {
+  const areaImgDom = areaImgRef.value as HTMLImageElement;
+  const boxDom = areaImgDom.parentNode?.parentNode as HTMLDivElement;
+
+  const imgSize = getBoxImageSize({
+    box: {
+      width: boxDom.offsetWidth - 22,
+      height: boxDom.offsetHeight - 22,
+    },
+    img: {
+      width: areaImgDom.naturalWidth,
+      height: areaImgDom.naturalHeight,
+    },
+    rotate: 0,
+  });
+  const rate = imgSize.width / areaImgDom.naturalWidth;
+  areaImgStyle.value = {
+    width: `${areaImgDom.naturalWidth}px`,
+    height: `${areaImgDom.naturalHeight}px`,
+    transform: `scale(${rate}) translate(-50%, -50%)`,
+  };
+}
+
+function selectOption(option: string) {
+  if (!props.recogData) return;
+
+  // 单选直接赋值
+  if (!props.recogData.multiple) {
+    selectResult.value = [option];
+    return;
+  }
+
+  // 多选情况
+  // 空直接赋值,空值与其他互斥
+  if (option === "#") {
+    selectResult.value = ["#"];
+    return;
+  }
+
+  let result = selectResult.value.filter((item) => item !== "#");
+  if (result.includes(option)) {
+    result = result.filter((item) => item !== option);
+  } else {
+    result.push(option);
+  }
+  // 保证result的顺序和options的顺序是一致的
+  selectResult.value = props.recogData.options.filter((item) =>
+    result.includes(item)
+  );
+}
+
+// 键盘事件
+function registKeyEvent() {
+  document.addEventListener("keydown", keyEventHandle);
+}
+function removeKeyEvent() {
+  document.removeEventListener("keydown", keyEventHandle);
+}
+
+function keyEventHandle(e: KeyboardEvent) {
+  if (e.repeat) return;
+
+  if (e.keyCode == 13) {
+    e.preventDefault();
+    onConfirm();
+    return;
+  } else if (e.code === "ArrowLeft") {
+    e.preventDefault();
+    emit("prev");
+    return;
+  }
+}
+
+function onConfirm() {
+  if (!selectResult.value.length) {
+    message.error("请完成仲裁结果!");
+    return;
+  }
+
+  emit("confirm", selectResult.value);
+}
+
+// init
+watch(
+  () => visible.value,
+  (val) => {
+    if (val) {
+      modalOpenHandle();
+    } else {
+      removeKeyEvent();
+    }
+  },
+  {
+    immediate: true,
+  }
+);
+
+watch(
+  () => props.recogData,
+  (val) => {
+    if (!val) return;
+    selectResult.value = [...props.recogData.result];
+  }
+);
+
+function modalOpenHandle() {
+  selectResult.value = [...props.recogData.result];
+  registKeyEvent();
+}
+
+onBeforeUnmount(() => {
+  removeKeyEvent();
+});
+</script>

+ 29 - 130
src/render/views/RecognizeCheck/RecognizeArbitrate.vue

@@ -17,79 +17,18 @@
     <div class="arbitrate-body">
       <RecognizeImage
         v-if="curArbitrateTaskDetail"
-        :img-src="curArbitrateTaskDetail.uri"
+        :recog-data="curArbitrateTaskDetail"
       />
-
-      <!-- arbitrate action modal -->
-      <div v-if="curArbitrateTaskDetail" class="arbitrate-modal">
-        <a-row align="top" :gutter="8" class="m-b-8px">
-          <a-col :span="6">
-            <div class="modal-box">
-              <p
-                v-if="curArbitrateTaskDetail.type === 'question'"
-                class="box-title"
-              >
-                客观题
-              </p>
-              <p
-                v-else-if="curArbitrateTaskDetail.type === 'paperType'"
-                class="box-title"
-              >
-                卷型
-              </p>
-              <p class="box-cont">{{ curTaskDetailName }}</p>
-            </div>
-          </a-col>
-          <a-col :span="12">
-            <div class="modal-box modal-origin">
-              <img :src="curArbitrateTaskDetail.sliceImg" alt="区域原图" />
-            </div>
-          </a-col>
-          <a-col :span="6">
-            <div class="modal-box is-btn" @click="changePrevTaskDetail">
-              <p class="box-title">左键</p>
-              <p class="box-cont">上一个</p>
-            </div>
-          </a-col>
-        </a-row>
-        <a-row align="stretch" :gutter="8">
-          <a-col :span="3">
-            <div class="modal-box">
-              <p class="box-title">一评结果</p>
-              <p class="box-cont">{{ curArbitrateTaskDetail.result1 }}</p>
-            </div>
-          </a-col>
-          <a-col :span="3">
-            <div class="modal-box">
-              <p class="box-title">二评结果</p>
-              <p class="box-cont">{{ curArbitrateTaskDetail.result2 }}</p>
-            </div>
-          </a-col>
-          <a-col :span="12">
-            <div class="modal-box modal-options">
-              <a-button
-                v-for="option in curArbitrateTaskDetail.options"
-                :key="option"
-                :type="
-                  curArbitrateTaskDetail.result.includes(option)
-                    ? 'primary'
-                    : 'default'
-                "
-                @click="selectOption(option)"
-                >{{ option }}</a-button
-              >
-            </div>
-          </a-col>
-          <a-col :span="6">
-            <div class="modal-box is-btn" @click="onConfirm">
-              <p class="box-title">Enter键</p>
-              <p class="box-cont">下一个</p>
-            </div>
-          </a-col>
-        </a-row>
-      </div>
     </div>
   </div>
+
+  <RecogArbitrateEditDialog
+    v-if="curArbitrateTaskDetail"
+    ref="recogArbitrateEditDialogRef"
+    :recog-data="curArbitrateTaskDetail"
+    @prev="changePrevTaskDetail"
+    @confirm="onConfirm"
+  />
 </template>
 
 <script setup lang="ts">
@@ -112,6 +51,7 @@ import { message } from "ant-design-vue";
 import { useUserStore } from "@/store";
 
 import RecognizeImage from "./RecognizeImage.vue";
+import RecogArbitrateEditDialog from "./RecogArbitrateEditDialog.vue";
 
 import { getBaseDataConfig } from "@/ap/baseDataConfig";
 import {
@@ -138,6 +78,7 @@ const route = useRoute();
 const userStore = useUserStore();
 const groupId = route.params.groupId ? Number(route.params.groupId) : 0;
 
+const recogArbitrateEditDialogRef = ref();
 const paperTypeBarcodeContentAll = ref<any>([]);
 const paperTypeBarcodeContent = ref<string[]>([]);
 async function getConfig() {
@@ -311,7 +252,7 @@ async function setCurTaskDetail() {
   if (!val) return;
 
   curArbitrateTaskDetail.value = val;
-  curArbitrateTaskDetail.value.sliceImg = await getSliceFileUrl(
+  curArbitrateTaskDetail.value.areaImg = await getSliceFileUrl(
     val.uri,
     val.fillArea
   );
@@ -417,64 +358,11 @@ async function getNextTask() {
   setCurTaskDetail();
 }
 
-function selectOption(option: string) {
-  if (!curArbitrateTaskDetail.value) return;
-
-  // 单选直接赋值
-  if (!curArbitrateTaskDetail.value.multiple) {
-    curArbitrateTaskDetail.value.result = [option];
-    return;
-  }
-
-  // 多选情况
-  // 空直接赋值,空值与其他互斥
-  if (option === "#") {
-    curArbitrateTaskDetail.value.result = ["#"];
-    return;
-  }
-
-  let result = curArbitrateTaskDetail.value.result.filter(
-    (item) => item !== "#"
-  );
-  if (result.includes(option)) {
-    result = result.filter((item) => item !== option);
-  } else {
-    result.push(option);
-  }
-  // 保证result的顺序和options的顺序是一致的
-  curArbitrateTaskDetail.value.result =
-    curArbitrateTaskDetail.value.options.filter((item) =>
-      result.includes(item)
-    );
-}
-
-// 键盘事件
-function registKeyEvent() {
-  document.addEventListener("keydown", keyEventHandle);
-}
-function removeKeyEvent() {
-  document.removeEventListener("keydown", keyEventHandle);
-}
-
-function keyEventHandle(e: KeyboardEvent) {
-  if (e.keyCode == 13) {
-    e.preventDefault();
-    onConfirm();
-    return;
-  } else if (e.code === "ArrowLeft") {
-    e.preventDefault();
-    changePrevTaskDetail();
-    return;
-  }
-}
-
 // 保存任务详情信息
-async function onConfirm() {
-  if (!curArbitrateTaskDetail.value?.result.length) {
-    message.error("请完成仲裁结果!");
-    return;
-  }
+async function onConfirm(result: string[]) {
+  if (!curArbitrateTaskDetail.value) return;
 
+  curArbitrateTaskDetail.value.result = result;
   await changeNextTaskDetail();
 }
 
@@ -538,7 +426,7 @@ async function submitCurTask() {
     };
     return npage;
   });
-  console.log(pages);
+  // console.log(pages);
 
   await recognizeArbitrateSave({ id: curTask.value.id, pages }).catch(
     () => false
@@ -560,15 +448,26 @@ onMounted(async () => {
   await releaseTask();
   await getConfig();
   initData();
-  registKeyEvent();
 });
 
 onBeforeUnmount(() => {
   releaseTask();
-  removeKeyEvent();
 });
 
 const subjectCode = computed(() => {
   return curTask.value?.subjectCode || "";
 });
+
+watch(
+  () => curArbitrateTaskDetail.value,
+  (val) => {
+    nextTick(() => {
+      if (val) {
+        recogArbitrateEditDialogRef.value?.open();
+      } else {
+        recogArbitrateEditDialogRef.value?.close();
+      }
+    });
+  }
+);
 </script>

+ 101 - 4
src/render/views/RecognizeCheck/RecognizeImage.vue

@@ -1,6 +1,18 @@
 <template>
   <div ref="arbitrateImgRef" class="arbitrate-img" @scroll="onImgScroll">
-    <img :src="imgUri" alt="扫描结果" @load="onImgLoad" />
+    <div class="arbitrate-img-box">
+      <img ref="imgRef" :src="imgUri" alt="扫描结果" @load="onImgLoad" />
+      <div class="img-recogs">
+        <div class="recog-block is-active" :style="fillAreaStyle">
+          <div
+            v-for="(option, oindex) in fillOptionStyles"
+            :key="oindex"
+            :style="option"
+            class="recog-item"
+          ></div>
+        </div>
+      </div>
+    </div>
   </div>
   <div ref="imgThumbRef" class="arbitrate-img-thumb">
     <img :src="imgUri" alt="扫描结果" />
@@ -17,22 +29,27 @@
 </template>
 
 <script setup lang="ts">
-import { computed, ref, reactive, onMounted } from "vue";
+import { computed, ref, reactive, onMounted, nextTick, watch } from "vue";
 import { vEleMoveDirective } from "@/directives/eleMove";
 import { getFileUrl } from "@/utils/tool";
+import { RecognizeArbitrateTaskDetail } from "@/ap/types/recognizeCheck";
+import { useUserStore } from "@/store";
 
 defineOptions({
   name: "RecognizeImage",
 });
 
 const props = defineProps<{
-  imgSrc: string;
+  recogData: RecognizeArbitrateTaskDetail;
 }>();
 
+const userStore = useUserStore();
+
 const arbitrateImgRef = ref();
+const imgRef = ref();
 
 const imgUri = computed(() => {
-  return getFileUrl(props.imgSrc);
+  return getFileUrl(props.recogData.uri);
 });
 
 // img
@@ -46,10 +63,66 @@ function updateImgAreaSize() {
   areaSize.height = areaHeight;
 }
 
+const fillAreaStyle = ref({} as Record<string, any>);
+const fillOptionStyles = ref([] as Array<Record<string, any>>);
+function updateRecogStyle() {
+  const imgDom = imgRef.value as HTMLImageElement;
+  const rate = imgDom.clientWidth / imgDom.naturalWidth;
+
+  const { unfillColor, fillColor, borderWidth } = userStore.recogFillSet;
+  const curBorderWidth = Math.max(1, borderWidth * rate);
+
+  fillAreaStyle.value = {
+    position: "absolute",
+    left: `${props.recogData.fillArea.x * rate}px`,
+    top: `${props.recogData.fillArea.y * rate}px`,
+    width: `${props.recogData.fillArea.w * rate}px`,
+    height: `${props.recogData.fillArea.h * rate}px`,
+    zIndex: 9,
+  };
+  if (props.recogData.type !== "question") {
+    fillOptionStyles.value = [];
+    return;
+  }
+
+  fillOptionStyles.value = props.recogData.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,
+      border: "",
+    };
+
+    if (op.filled) {
+      opStyle.border = `${curBorderWidth}px solid ${fillColor}`;
+    } else {
+      opStyle.border = `${curBorderWidth}px solid ${unfillColor}`;
+    }
+
+    return opStyle;
+  });
+}
+
 function onImgLoad() {
   updateImgAreaSize();
+
+  nextTick(() => {
+    updateRecogStyle();
+  });
 }
 
+watch(
+  () => props.recogData,
+  (val, oldval) => {
+    if (val && oldval && val.uri === oldval.uri) {
+      updateRecogStyle();
+    }
+  }
+);
+
 let areaIsMoving = false;
 function onImgScroll(e: Event) {
   if (areaIsMoving) {
@@ -101,3 +174,27 @@ function moveAreaStop(pos: MovePos) {
   areaIsMoving = false;
 }
 </script>
+
+<style lang="less" scoped>
+.arbitrate-img {
+  .recog-block {
+    &:hover {
+      background-color: rgba(241, 214, 110, 0.3);
+    }
+    &.is-active {
+      background-color: rgba(241, 214, 110, 0.3);
+      &::after {
+        content: "";
+        display: block;
+        position: absolute;
+        z-index: 1;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        border: 1px dashed #000;
+      }
+    }
+  }
+}
+</style>