Jelajahi Sumber

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

刘洋 7 bulan lalu
induk
melakukan
ab50226f42
28 mengubah file dengan 683 tambahan dan 334 penghapusan
  1. 1 0
      src/render/ap/imageCheck.ts
  2. 1 0
      src/render/ap/types/absentCheck.ts
  3. 2 0
      src/render/ap/types/audit.ts
  4. 31 22
      src/render/components/ElementResize/index.vue
  5. 4 6
      src/render/store/modules/dataCheck/index.ts
  6. 43 73
      src/render/styles/pages.less
  7. 10 10
      src/render/utils/recog/recog.ts
  8. 10 0
      src/render/views/AbsentCheck/CheckAction.vue
  9. 0 1
      src/render/views/AbsentCheck/index.vue
  10. 21 2
      src/render/views/Audit/Main/index.vue
  11. 2 2
      src/render/views/CurExam/index.vue
  12. 12 2
      src/render/views/DataCheck/CheckAction.vue
  13. 23 27
      src/render/views/DataCheck/ModifyPaperType.vue
  14. 40 26
      src/render/views/DataCheck/QuestionPanel.vue
  15. 1 0
      src/render/views/DataCheck/ScanImage/FillAreaSetDialog.vue
  16. 23 11
      src/render/views/DataCheck/ScanImage/RecogEditDialog.vue
  17. 40 9
      src/render/views/DataCheck/ScanImage/index.vue
  18. 1 1
      src/render/views/DataCheck/SliceImage/CutImageDialog.vue
  19. 0 1
      src/render/views/DataCheck/index.vue
  20. 261 0
      src/render/views/RecognizeCheck/RecogArbitrateEditDialog.vue
  21. 29 130
      src/render/views/RecognizeCheck/RecognizeArbitrate.vue
  22. 102 4
      src/render/views/RecognizeCheck/RecognizeImage.vue
  23. 5 1
      src/render/views/ResultExport/BreachImport.vue
  24. 3 3
      src/render/views/ResultExport/MarkSite.vue
  25. 12 0
      src/render/views/ResultExport/ModifyMarkSite.vue
  26. 2 0
      src/render/views/ResultExport/TaskProgressDialog.vue
  27. 2 1
      src/render/views/Review/ReviewMarkPan.vue
  28. 2 2
      src/render/views/Review/index.vue

+ 1 - 0
src/render/ap/imageCheck.ts

@@ -19,6 +19,7 @@ export const imageCheckList = (
     url: "/api/admin/check/image/list",
     method: "post",
     params: data,
+    loading: true,
   });
 
 export const imageCheckFailedList = (

+ 1 - 0
src/render/ap/types/absentCheck.ts

@@ -8,6 +8,7 @@ export interface AbsentCheckListFilter {
   name: string;
   subjectCode: string;
   examStatus: string;
+  status: string;
 }
 
 export type AbsentCheckListParams = PageParams<AbsentCheckListFilter>;

+ 2 - 0
src/render/ap/types/audit.ts

@@ -15,6 +15,8 @@ export interface ExamOverviewResult {
   assignedCheck: {
     todoCount: number;
   };
+  // 是否开启实时审核
+  enableSyncVerify: boolean;
 }
 
 export interface AuditBatchStudentPaper {

+ 31 - 22
src/render/components/ElementResize/index.vue

@@ -138,7 +138,10 @@ function initControlPoints() {
     return {
       classes: ["control-point", `control-point-${type}`],
       movePoint: actions[type],
-      movePointOver: moveOver,
+      movePointOver: (data: PositionData) => {
+        actions[type](data);
+        movePointOver();
+      },
     };
   });
 }
@@ -159,6 +162,9 @@ function fetchValidSizePos(
 ) {
   if (sizeData.w <= props.minWidth) {
     sizeData.w = props.minWidth;
+
+    if (actionType.includes("l"))
+      sizeData.x = lastSizePos.x + lastSizePos.w - sizeData.w;
   }
   if (props.maxWidth !== 0 && sizeData.w >= props.maxWidth) {
     sizeData.w = props.maxWidth;
@@ -166,6 +172,9 @@ function fetchValidSizePos(
 
   if (sizeData.h <= props.minHeight) {
     sizeData.h = props.minHeight;
+
+    if (actionType.includes("t"))
+      sizeData.y = lastSizePos.y + lastSizePos.h - sizeData.h;
   }
   if (props.maxHeight !== 0 && sizeData.h >= props.maxHeight) {
     sizeData.h = props.maxHeight;
@@ -185,23 +194,29 @@ function fetchValidSizePos(
   if (fitParentTypeWidth.value) {
     if (sizeData.x <= 0) {
       sizeData.x = 0;
-      if (actionType.includes("l")) sizeData.w = lastSizePos.w;
+      if (actionType.includes("l")) sizeData.w = lastSizePos.w + lastSizePos.x;
     }
 
-    if (sizeData.x + sizeData.w > parentNodeSize.w) {
-      sizeData.x = lastSizePos.x;
-      sizeData.w = parentNodeSize.w - sizeData.x;
+    if (sizeData.x + sizeData.w >= parentNodeSize.w) {
+      if (actionType === "move") {
+        sizeData.x = parentNodeSize.w - sizeData.w;
+      } else {
+        sizeData.w = parentNodeSize.w - sizeData.x;
+      }
     }
   }
 
   if (fitParentTypeHeight.value) {
     if (sizeData.y <= 0) {
       sizeData.y = 0;
-      if (actionType.includes("t")) sizeData.h = lastSizePos.h;
+      if (actionType.includes("t")) sizeData.h = lastSizePos.h + lastSizePos.y;
     }
-    if (sizeData.y + sizeData.h > parentNodeSize.h) {
-      sizeData.y = lastSizePos.y;
-      sizeData.h = parentNodeSize.h - sizeData.y;
+    if (sizeData.y + sizeData.h >= parentNodeSize.h) {
+      if (actionType === "move") {
+        sizeData.y = parentNodeSize.h - sizeData.h;
+      } else {
+        sizeData.h = parentNodeSize.h - sizeData.y;
+      }
     }
   }
 
@@ -295,19 +310,9 @@ function moveRightBottomPoint({ left, top }: PositionData) {
   emitChange();
 }
 
-function moveOver({ left, top }: PositionData) {
-  const sp = {
-    ...sizePos,
-    ...{
-      x: left + sizePosOrigin.x,
-      y: top + sizePosOrigin.y,
-    },
-  };
-  objModifyAssign(sizePos, fetchValidSizePos(sp, "move"));
-
+function movePointOver() {
   objModifyAssign(sizePosOrigin, sizePos);
   objModifyAssign(lastSizePos, sizePos);
-  emitChange();
   emit("resizeOver", sizePos);
 }
 
@@ -329,9 +334,13 @@ function moveElement({ left, top }: PositionData) {
   emitChange();
 }
 
-function moveElementOver(pos: PositionData) {
+function moveElementOver({ left, top }: PositionData) {
   if (!props.move) return;
-  moveOver(pos);
+  moveElement({ left, top });
+
+  objModifyAssign(sizePosOrigin, sizePos);
+  objModifyAssign(lastSizePos, sizePos);
+  emit("resizeOver", sizePos);
 }
 
 function emitChange() {

+ 4 - 6
src/render/store/modules/dataCheck/index.ts

@@ -61,8 +61,8 @@ export const useDataCheckStore = defineStore("dataCheck", {
       const params = {
         examId: this.curPage.examId,
         examNumber: this.curStudent.examNumber,
-        paperNumber: this.curPage.paperNumber,
-        pageIndex: this.curPage.pageIndex + 1,
+        paperNumber: data.field === "PAPER_TYPE" ? 1 : this.curPage.paperNumber,
+        pageIndex: data.field === "PAPER_TYPE" ? 1 : this.curPage.pageIndex + 1,
         subjectCode: this.curStudent.subjectCode,
         ...data,
       };
@@ -81,12 +81,10 @@ export const useDataCheckStore = defineStore("dataCheck", {
         this.curPage.sheetUri = uri;
       }
     },
-    modifyPaperType(data: UpdatePaperType) {
+    modifyPaperType(paperType: string) {
       if (!this.curStudent) return;
-      const { paperType, pageIndex, paperIndex } = data;
       this.curStudent.paperType = paperType || "#";
-      this.curStudent.papers[paperIndex].pages[pageIndex].paperType.result =
-        paperType;
+      this.curStudent.papers[0].pages[0].paperType.result = paperType;
     },
   },
   persist: {

+ 43 - 73
src/render/styles/pages.less

@@ -55,6 +55,19 @@
       display: flex;
       flex-direction: column;
       justify-content: space-between;
+      position: relative;
+
+      &.is-disabled::after {
+        content: "";
+        display: block;
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        z-index: 9;
+        background-color: rgba(255, 255, 255, 0.5);
+      }
 
       &-head,
       &-foot {
@@ -248,10 +261,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 +296,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
@@ -556,7 +503,7 @@
       > p {
         font-weight: 500;
         font-size: 16px;
-        color: @warning-color;
+        color: @error-color;
         line-height: 24px;
         margin-bottom: 8px;
 
@@ -944,6 +891,9 @@
     &.is-col1 {
       width: 122px;
     }
+    &.is-col2 {
+      width: 244px;
+    }
   }
 
   .modal-box {
@@ -1029,6 +979,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 {

+ 10 - 10
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,13 +111,12 @@ export function parseDetailSize(
     optionSizes: [],
     result: [],
     multiple: false,
-    sliceImg: "",
+    areaImg: "",
   };
 
   if (!data) return result;
 
-  const fillResultList =
-    fillResult && fillResult.length ? fillResult : data.fill_option;
+  const fillResultList = fillResult && fillResult.length ? fillResult : [];
 
   result.fillSize = {
     w: data.fill_size[0],
@@ -140,11 +138,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 +164,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);

+ 10 - 0
src/render/views/AbsentCheck/CheckAction.vue

@@ -201,6 +201,7 @@ const initSearchModel = {
   name: "",
   subjectCode: "",
   examStatus: "",
+  status: "SCANNED",
 };
 const searchModel = reactive<AbsentCheckListFilter>({ ...initSearchModel });
 const imageType = ref(dataCheckStore.imageType);
@@ -295,6 +296,15 @@ const { loading: downloading, setLoading } = useLoading();
 const exportTypeDialogRef = ref();
 function onExport() {
   if (downloading.value) return;
+
+  if (!searchModel.subjectCode) {
+    message.error("请选择科目");
+    return;
+  }
+  if (!searchModel.examStatus || searchModel.examStatus === "OK") {
+    message.error("请选择异常条件");
+    return;
+  }
   exportTypeDialogRef.value?.open();
 }
 

+ 0 - 1
src/render/views/AbsentCheck/index.vue

@@ -75,7 +75,6 @@ async function getList() {
     ...searchModel,
     pageNumber: pageNumber.value,
     pageSize: pageSize.value,
-    status: "SCANNED",
   };
   const res = await absentCheckList(datas).catch(() => null);
   loading.value = false;

+ 21 - 2
src/render/views/Audit/Main/index.vue

@@ -18,7 +18,12 @@
     </div>
     <div class="home-body">
       <template v-if="overviewData">
-        <div class="audit-box">
+        <div
+          :class="[
+            'audit-box',
+            { 'is-disabled': !overviewData.enableSyncVerify },
+          ]"
+        >
           <div class="audit-box-head">
             <h4>实时审核</h4>
           </div>
@@ -36,7 +41,12 @@
           </div>
         </div>
 
-        <div class="audit-box">
+        <div
+          :class="[
+            'audit-box',
+            { 'is-disabled': overviewData.enableSyncVerify },
+          ]"
+        >
           <div class="audit-box-head">
             <h4>
               复核校验<span style="font-weight: normal">(人工绑定审核)</span>
@@ -141,6 +151,15 @@ async function getOverviewData() {
 }
 
 function toPage(name: string) {
+  if (!overviewData.value) return;
+
+  if (
+    (!overviewData.value.enableSyncVerify && name === "IntimeAudit") ||
+    (overviewData.value.enableSyncVerify && name === "ReviewAudit")
+  ) {
+    return;
+  }
+
   router.push({ name });
 }
 

+ 2 - 2
src/render/views/CurExam/index.vue

@@ -126,7 +126,7 @@
             </div>
             <div class="body">
               <div class="option">
-                审核员<span style="font-weight: normal">(人工绑定审核)</span>
+                审核员<span style="font-weight: normal">(第一遍审核)</span>
                 <p>
                   已完成:{{
                     allData.assignedCheck?.auditorFinishCount
@@ -136,7 +136,7 @@
                 </p>
               </div>
               <div class="option">
-                管理员
+                管理员<span style="font-weight: normal">(第二遍审核)</span>
                 <p>
                   已完成:{{
                     allData.assignedCheck?.adminFinishCount

+ 12 - 2
src/render/views/DataCheck/CheckAction.vue

@@ -131,7 +131,7 @@
                 <a-button class="m-r-8px" type="primary" @click="onCustomSearch"
                   >查询</a-button
                 >
-                <a-button @click="onExport('custom')">导出</a-button>
+                <!-- <a-button @click="onExport('custom')">导出</a-button> -->
               </a-form-item>
             </a-col>
           </a-row>
@@ -235,7 +235,7 @@ const fieldNames = { label: "name", value: "code" };
 // search
 const initSearchModel = {
   examId: userStore.curExam.id,
-  status: "",
+  status: "SCANNED",
   examStatus: "",
   examNumber: "",
   studentCode: "",
@@ -424,6 +424,16 @@ const { loading: downloading, setLoading } = useLoading();
 const exportTypeDialogRef = ref();
 function onExport(type: string) {
   if (downloading.value) return;
+
+  const params = type === "common" ? searchModel : customSearchModel;
+  if (!params.subjectCode) {
+    message.error("请选择科目");
+    return;
+  }
+  if (!searchDataCheckType.value) {
+    message.error("请选择条件");
+    return;
+  }
   actionType.value = type;
   exportTypeDialogRef.value?.open();
 }

+ 23 - 27
src/render/views/DataCheck/ModifyPaperType.vue

@@ -9,16 +9,11 @@
     <template #title> 更改卷型号 </template>
 
     <a-form ref="formRef" :label-col="{ style: { width: '90px' } }">
+      <a-form-item label="最终结果">
+        <span class="color-brand">{{ areaResultDisplay }}</span>
+      </a-form-item>
       <a-form-item label="识别结果">
-        <a-input
-          v-if="editing"
-          v-model:value="paperType"
-          style="width: 100px"
-        />
-        <span v-else class="color-brand">{{ areaResult || "#" }}</span>
-        <qm-button class="ml-15px" @click="toggleEditing">{{
-          editing ? "取消" : "编辑"
-        }}</qm-button>
+        <span class="color-brand">{{ ocrResult }}</span>
       </a-form-item>
       <a-form-item label="识别图片">
         <div class="paper-type-img">
@@ -41,6 +36,12 @@
           >
             {{ item }}
           </li>
+          <li
+            :class="['type-item', { 'is-active': paperType === '?' }]"
+            @click="selectPaperType('?')"
+          >
+            异常
+          </li>
         </ul>
       </a-form-item>
     </a-form>
@@ -48,7 +49,7 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref, watch } from "vue";
+import { computed, onMounted, ref, watch } from "vue";
 import { message } from "ant-design-vue";
 
 import { getBaseDataConfig } from "@/ap/baseDataConfig";
@@ -60,7 +61,6 @@ defineOptions({
 });
 
 const dataCheckStore = useDataCheckStore();
-const editing = ref(false);
 /* modal */
 const { visible, open, close } = useModal();
 defineExpose({ open, close });
@@ -68,6 +68,7 @@ defineExpose({ open, close });
 const props = defineProps<{
   areaImg: string;
   areaResult: string;
+  ocrResult: string;
 }>();
 
 const emit = defineEmits(["confirm"]);
@@ -76,9 +77,14 @@ const paperTypeBarcodeContent = ref<string[]>([]);
 const userStore = useUserStore();
 const paperType = ref("");
 
+const areaResultDisplay = computed(() => {
+  if (props.areaResult === "#") return "空";
+  if (props.areaResult === "?") return "异常";
+  return props.areaResult;
+});
+
 async function getConfig() {
   const res = await getBaseDataConfig({ examId: userStore.curExam.id });
-  // paperTypeBarcodeContent.value = res.paperTypeBarcodeContent || [];
   paperTypeBarcodeContent.value =
     (res.paperTypeBarcodeContent || []).find((item: any) => {
       return item.code == dataCheckStore.curStudent?.subjectCode;
@@ -88,23 +94,13 @@ async function getConfig() {
 function selectPaperType(val: string) {
   paperType.value = val;
 }
-let oldPaperType: string = "";
-const toggleEditing = () => {
-  if (!editing.value) {
-    oldPaperType = paperType.value;
-  } else {
-    paperType.value = oldPaperType;
-  }
-  editing.value = !editing.value;
-};
+
 function confirm() {
-  // if (!paperType.value) {
-  //   message.error("请选择卷型");
-  //   return;
-  // }
+  if (!paperType.value) {
+    message.error("请选择卷型");
+    return;
+  }
   emit("confirm", paperType.value);
-  editing.value = false;
-  oldPaperType = "";
   close();
 }
 

+ 40 - 26
src/render/views/DataCheck/QuestionPanel.vue

@@ -15,12 +15,10 @@
       </a-descriptions-item>
       <a-descriptions-item label="卷型号" :span="6">
         <template v-if="simple">
-          {{ info.paperType || "#" }}
+          {{ paperTypeDisplay }}
         </template>
         <template v-else>
-          <a-button class="ant-gray m-r-4px">{{
-            info.paperType || "#"
-          }}</a-button>
+          <a-button class="ant-gray m-r-4px">{{ paperTypeDisplay }}</a-button>
           <a-button v-if="paperTypeArea && editable" @click="onEditPaperType">
             <template #icon><SwapOutlined /></template>
           </a-button>
@@ -95,6 +93,7 @@
     ref="modifyPaperTypeRef"
     :area-img="paperTypeImg"
     :area-result="paperTypeResult"
+    :ocr-result="paperTypeOrcResult"
     @confirm="paperTypeModified"
   />
 </template>
@@ -230,50 +229,56 @@ const modifyPaperTypeRef = ref();
 const paperTypeArea = ref<AreaSize | null>(null);
 const paperTypeImg = ref("");
 const paperTypeResult = ref("");
-async function onEditPaperType() {
+const paperTypeOrcResult = ref("");
+const paperTypeType = ref("");
+const paperTypeDisplay = computed(() => {
+  if (paperTypeResult.value === "#") return "空";
+  if (paperTypeResult.value === "?") return "异常";
+  return paperTypeResult.value;
+});
+function onEditPaperType() {
   if (!dataCheckStore.curPage) return;
-
-  if (paperTypeArea.value) {
-    paperTypeImg.value = await getSliceFileUrl(
-      dataCheckStore.curPage.sheetUri,
-      paperTypeArea.value
-    );
-    paperTypeResult.value = dataCheckStore.curPage.paperType.result;
-  } else {
-    paperTypeImg.value = "";
-  }
   modifyPaperTypeRef.value?.open();
 }
 const curPage = computed(() => dataCheckStore.curPage);
 async function paperTypeModified(paperType: string) {
-  if (!dataCheckStore.curPage) return;
-  dataCheckStore.modifyPaperType({
-    paperIndex: curPage.value?.paperIndex as number,
-    pageIndex: curPage.value?.pageIndex as number,
-    paperType: paperType || "#",
-  });
+  if (!dataCheckStore.curStudent) return;
+
   await dataCheckStore.updateField({
     field: "PAPER_TYPE",
     value: JSON.stringify({
-      ...dataCheckStore.curPage.paperType,
+      type: paperTypeType.value,
       result: paperType || "#",
     }),
   });
-  dataCheckStore.curPage.paperType.result = paperType;
+  dataCheckStore.modifyPaperType(paperType);
+  paperTypeResult.value = paperType;
 }
 
 watch(
-  () => dataCheckStore.curPage?.recogData,
-  (val) => {
+  () => dataCheckStore.curStudent,
+  async (val) => {
     paperTypeArea.value = null;
     if (!val) return;
 
-    const regdata = parseRecogData(val);
+    // 只取第一张第一页的数据
+    const curStudentFirstPage = val.papers[0]?.pages[0];
+    if (!curStudentFirstPage) return;
+
+    paperTypeResult.value = curStudentFirstPage.paperType.result;
+    paperTypeType.value = curStudentFirstPage.paperType.type;
+    const recogData = curStudentFirstPage.recogData;
+    const regdata = parseRecogData(recogData);
     if (!regdata) return;
 
+    // console.log(regdata);
+    paperTypeOrcResult.value = regdata.paperType.content;
+    if (!paperTypeOrcResult.value) paperTypeOrcResult.value = "空";
+
     const rect = regdata.paperType.rect || null;
     if (!rect) {
       paperTypeArea.value = null;
+      paperTypeImg.value = "";
       return;
     }
 
@@ -286,6 +291,15 @@ watch(
           h: rect[3],
         }
       : null;
+
+    if (paperTypeArea.value) {
+      paperTypeImg.value = await getSliceFileUrl(
+        curStudentFirstPage.sheetUri,
+        paperTypeArea.value
+      );
+    } else {
+      paperTypeImg.value = "";
+    }
   },
   {
     immediate: true,

+ 1 - 0
src/render/views/DataCheck/ScanImage/FillAreaSetDialog.vue

@@ -163,6 +163,7 @@ function modalOpenHandle() {
   border-radius: 6px;
   margin-right: 8px;
   cursor: pointer;
+  border: 1px solid #b0b0b0;
 
   &:hover {
     opacity: 0.8;

+ 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,

+ 40 - 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;
@@ -387,6 +388,15 @@ watch(
   (val) => {
     if (!val) return;
     updateRecogList();
+
+    if (!curRecogBlock.value) return;
+
+    const curBlock = curRecogBlock.value;
+    const block = recogBlocks.value.find(
+      (item) => item.type === curBlock.type && curBlock.index
+    );
+    if (!block) return;
+    curRecogBlock.value = block;
   },
   {
     deep: true,
@@ -484,5 +494,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>

+ 1 - 1
src/render/views/DataCheck/SliceImage/CutImageDialog.vue

@@ -22,7 +22,7 @@
           class="element-resize-act"
           :active="['r', 'rb', 'b', 'lb', 'l', 'lt', 't', 'rt']"
         >
-          <div class="image-selection" :style="selectionStyle"></div>
+          <!-- <div class="image-selection" :style="selectionStyle"></div> -->
         </element-resize>
       </div>
 

+ 0 - 1
src/render/views/DataCheck/index.vue

@@ -74,7 +74,6 @@ async function getList() {
     ...searchModel,
     pageNumber: pageNumber.value,
     pageSize: pageSize.value,
-    status: "SCANNED",
   };
   const res = await dataCheckList(datas).catch(() => null);
   loading.value = false;

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

@@ -0,0 +1,261 @@
+<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="onPrev">
+            <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);
+}
+
+function onPrev() {
+  emit("prev");
+}
+
+// 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>

+ 102 - 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) {
@@ -58,6 +131,7 @@ function onImgScroll(e: Event) {
   }
 
   const imgBoxDom = arbitrateImgRef.value as HTMLDivElement;
+  if (!imgBoxDom) return;
   const scrollTop = imgBoxDom.scrollTop;
   const imgDom = imgBoxDom.firstChild as HTMLImageElement;
   const { clientHeight } = imgThumbRef.value as HTMLDivElement;
@@ -101,3 +175,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>

+ 5 - 1
src/render/views/ResultExport/BreachImport.vue

@@ -34,7 +34,11 @@
   />
 
   <!-- TaskProgressDialog -->
-  <TaskProgressDialog ref="taskProgressDialogRef" :task="curExportTask" />
+  <TaskProgressDialog
+    ref="taskProgressDialogRef"
+    :task="curExportTask"
+    @finished="getData"
+  />
 </template>
 
 <script setup lang="ts">

+ 3 - 3
src/render/views/ResultExport/MarkSite.vue

@@ -1,9 +1,9 @@
 <template>
-  <div class="m-b-16px">
+  <!-- <div class="m-b-16px">
     <qm-button type="primary" :icon="h(PlusCircleOutlined)" @click="onAdd"
       >新增评卷点信息</qm-button
     >
-  </div>
+  </div> -->
 
   <a-table
     :columns="columns"
@@ -16,7 +16,7 @@
     <template #bodyCell="{ column, index }">
       <template v-if="column.dataIndex === 'operation'">
         <qm-button type="link" @click="onEdit(index)">编辑</qm-button>
-        <qm-button type="link" @click="onDelete(index)">删除</qm-button>
+        <!-- <qm-button type="link" @click="onDelete(index)">删除</qm-button> -->
       </template>
     </template>
   </a-table>

+ 12 - 0
src/render/views/ResultExport/ModifyMarkSite.vue

@@ -18,12 +18,14 @@
         <select-course
           v-model:value="formData.subjectCode"
           :exam-id="userStore.curExam.id"
+          :disabled="isEdit"
         ></select-course>
       </a-form-item>
       <a-form-item name="paperType" label="条码值">
         <a-input
           v-model:value="formData.paperType"
           placeholder="请输入"
+          :disabled="isEdit"
         ></a-input>
       </a-form-item>
       <a-form-item name="oddNumber" label="奇数考场评卷点代码">
@@ -113,6 +115,11 @@ const rules: FormRules<keyof MarkSiteSaveParams> = {
       message: "请输入",
       trigger: "change",
     },
+    {
+      pattern: /^[0-9]{1,6}$/,
+      message: "只能输入最多6个数字",
+      trigger: "change",
+    },
   ],
   evenNumber: [
     {
@@ -120,6 +127,11 @@ const rules: FormRules<keyof MarkSiteSaveParams> = {
       message: "请输入",
       trigger: "change",
     },
+    {
+      pattern: /^[0-9]{1,6}$/,
+      message: "只能输入最多6个数字",
+      trigger: "change",
+    },
   ],
 };
 

+ 2 - 0
src/render/views/ResultExport/TaskProgressDialog.vue

@@ -43,6 +43,7 @@ const props = defineProps<{
   };
   downloadHandle?: PromiseFunc;
 }>();
+const emit = defineEmits(["finished"]);
 
 const curProgress = ref(0);
 
@@ -83,6 +84,7 @@ async function getProgress() {
   // 文件生成成功,开始下载
   if (result.status === "SUCCESS") {
     stop();
+    emit("finished");
     if (props.downloadHandle) {
       await props.downloadHandle();
     }

+ 2 - 1
src/render/views/Review/ReviewMarkPan.vue

@@ -92,8 +92,9 @@ function moveAreaStop(pos: MovePos) {
 
 function updateStaticSize() {
   const panDom = reviewMarkPanRef.value as HTMLDivElement;
+  if (!panDom) return;
   const parentDom = panDom.parentElement as HTMLDivElement;
-  if (!panDom || !parentDom) return;
+  if (!parentDom) return;
   panWidth = panDom.clientWidth;
   panHeight = panDom.clientHeight;
   limitWidth = parentDom.clientWidth;

+ 2 - 2
src/render/views/Review/index.vue

@@ -2,8 +2,8 @@
   <div class="review">
     <div class="review-head">
       <h2 v-if="reviewStore.curTask" class="review-title">
-        {{ reviewStore.curTask.examNumber }} - {{ reviewStore.curTask.name }} -
-        {{ userStore.curExam?.name }}
+        {{ reviewStore.curTask.subjectName }} -
+        {{ reviewStore.curTask.examNumber }} - {{ reviewStore.curTask.name }}
       </h2>
       <h2 v-else class="review-title">-</h2>