zhangjie пре 1 месец
родитељ
комит
8cf5e80828

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

@@ -74,6 +74,7 @@ export interface DataCheckListItem {
   examNumber: string;
   studentCode: string;
   name: string;
+  checked: boolean;
   subjectCode: string;
   subjectName: string;
   packageCode: string;

+ 14 - 0
src/render/store/modules/dataCheck/index.ts

@@ -7,12 +7,24 @@ import {
 } from "@/ap/types/dataCheck";
 import { dataCheckOmrFieldEdit } from "@/ap/dataCheck";
 
+interface TaskItem {
+  id: string;
+  type: "examNumber" | "question";
+  questionIndex: number;
+  paperIndex: number;
+  pageIndex: number;
+  studentId: string;
+  finished: boolean;
+}
+
 interface DataCheckState {
   imageType: ImageType;
   curPage: StudentPage | null;
   curPageIndex: number;
   curStudent: DataCheckListItem | null;
   curStudentIndex: number;
+  taskList: TaskItem[];
+  curTask: TaskItem | null;
 }
 
 type UpdateFieldParams = Pick<DataCheckOmrFieldEditParams, "field" | "value">;
@@ -40,6 +52,8 @@ export const useDataCheckStore = defineStore("dataCheck", {
     curPageIndex: -1,
     curStudent: null,
     curStudentIndex: -1,
+    taskList: [],
+    curTask: null,
   }),
 
   getters: {

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

@@ -766,6 +766,17 @@
     flex-direction: column;
     justify-content: space-between;
 
+    &-head {
+      flex-grow: 0;
+      flex-shrink: 0;
+      padding: 8px 16px;
+      background-color: #fff;
+
+      .ant-tabs-nav {
+        margin-bottom: 0;
+      }
+    }
+
     &-body {
       flex-grow: 2;
       padding: 8px;
@@ -959,6 +970,8 @@
         position: absolute;
         top: 0;
         border: 2px solid @brand-color;
+        line-height: 1;
+        text-align: center;
         cursor: pointer;
 
         &:hover {

+ 1 - 1
src/render/views/ExamNumberCheck/CheckAction.vue

@@ -23,7 +23,7 @@
             />
           </a-form-item>
         </a-form>
-        <div>
+        <div style="margin-left: 80px">
           <a-button class="m-r-8px" type="primary" @click="onSearch"
             >查询</a-button
           >

+ 12 - 2
src/render/views/ExamNumberCheck/EditExamNumberDialog.vue

@@ -1,5 +1,15 @@
 <template>
-  <a-drawer v-model:open="visible" :width="380" title="输入准考证号">
+  <a-modal
+    v-model:open="visible"
+    :width="380"
+    style="bottom: 0"
+    :footer="false"
+    :closable="false"
+    :mask="false"
+    :maskClosable="false"
+    :keyboard="false"
+    title="输入准考证号"
+  >
     <a-form ref="formRef" :label-col="{ style: { width: '80px' } }">
       <a-form-item label="准考证号">
         <div class="exam-number" style="margin-bottom: 10px">
@@ -12,7 +22,7 @@
         <a-button type="primary" @click="confirm">保存</a-button>
       </a-form-item>
     </a-form>
-  </a-drawer>
+  </a-modal>
 </template>
 
 <script setup lang="ts">

+ 3 - 3
src/render/views/ExamNumberCheck/QuestionPanel.vue

@@ -7,7 +7,7 @@
           :danger="!checkExamNumber(info.examNumber)"
           >{{ info.examNumber }}</a-button
         >
-        <a-button @click="onEditPaperType">
+        <a-button @click="onEditExamNumber">
           <template #icon><SwapOutlined /></template>
         </a-button>
       </a-form-item>
@@ -129,7 +129,7 @@ const toUpperCase = () => {
 };
 
 function getQuestionNo(index: number) {
-  const no = index + 1;
+  const no = index + 26;
   return no < 10 ? `0${no}` : `${no}`;
 }
 function checkQuestionError(cont: string) {
@@ -211,7 +211,7 @@ function onSaveQuesion() {
 
 // edit exam number
 const editExamNumberDialogRef = ref();
-function onEditPaperType() {
+function onEditExamNumber() {
   if (!dataCheckStore.curPage) return;
   editExamNumberDialogRef.value?.open();
 }

+ 45 - 0
src/render/views/ExamNumberCheck/ScanImage/ConfirmTips.vue

@@ -0,0 +1,45 @@
+<template>
+  <a-modal v-model:open="visible" title="处理提示">
+    <template #footer>
+      <a-button key="back" @click="close">取消</a-button>
+      <a-button key="submit" type="primary" @click="handleOk">确定</a-button>
+    </template>
+    <p>当前题卡异常处理完成,回车进入下一张题卡</p>
+  </a-modal>
+</template>
+<script lang="ts" setup>
+import { watch, onUnmounted } from "vue";
+import useModal from "@/hooks/useModal";
+
+defineOptions({
+  name: "ConfirmTips",
+});
+const emit = defineEmits(["confirm"]);
+
+/* modal */
+const { visible, open, close } = useModal();
+defineExpose({ open, close });
+
+const handleOk = () => {
+  emit("confirm");
+  close();
+};
+
+const handleKeyDown = (event: KeyboardEvent) => {
+  if (event.key === "Enter") {
+    handleOk();
+  }
+};
+
+watch(visible, (newValue) => {
+  if (newValue) {
+    document.addEventListener("keydown", handleKeyDown);
+  } else {
+    document.removeEventListener("keydown", handleKeyDown);
+  }
+});
+
+onUnmounted(() => {
+  document.removeEventListener("keydown", handleKeyDown);
+});
+</script>

+ 7 - 2
src/render/views/ExamNumberCheck/ScanImage/RecogEditDialog.vue

@@ -34,7 +34,9 @@
                 class="select-option"
                 :style="getOptionStyle(index)"
                 @click="selectOption(option)"
-              ></div>
+              >
+                {{ option }}
+              </div>
             </div>
           </div>
         </div>
@@ -111,7 +113,7 @@ const recogTitle = computed(() => {
 
 const recogTitleDesc = computed(() => {
   if (props.recogData.type === "question") {
-    return `#${props.recogData.index}`;
+    return `#${props.recogData.index + 25}`;
   }
   return "--";
 });
@@ -132,6 +134,9 @@ function getOptionStyle(index: number): Record<string, any> {
   return {
     width: `${optionSize.w}px`,
     height: `${optionSize.h}px`,
+    fontSize: `${optionSize.h * 0.8}px`,
+    fontWeight: "bold",
+    color: borderColor,
     left: `${optionSize.x}px`,
     top: `${optionSize.y}px`,
     borderColor,

+ 62 - 0
src/render/views/ExamNumberCheck/ScanImage/index.vue

@@ -80,6 +80,14 @@
     @confirm="onRecogEditConfirm"
     @close="clearCurBlock"
   />
+  <!-- EditExamNumberDialog -->
+  <EditExamNumberDialog
+    ref="editExamNumberDialogRef"
+    :data="info.examNumber"
+    @confirm="examNumberModified"
+  />
+  <!-- ConfirmTips -->
+  <ConfirmTips ref="confirmTipsRef" @confirm="tipConfirmHandle" />
 </template>
 
 <script setup lang="ts">
@@ -109,9 +117,12 @@ import {
 import { useUserStore, useDataCheckStore } from "@/store";
 import { abc } from "@/constants/enumerate";
 import useUpload from "../useUpload";
+import useTask from "../useTask";
 
 import FillAreaSetDialog from "./FillAreaSetDialog.vue";
 import RecogEditDialog from "./RecogEditDialog.vue";
+import EditExamNumberDialog from "../EditExamNumberDialog.vue";
+import ConfirmTips from "./ConfirmTips.vue";
 import ImportBtn from "@/components/ImportBtn/index.vue";
 import { debounce } from "lodash-es";
 
@@ -130,6 +141,8 @@ const userStore = useUserStore();
 const dataCheckStore = useDataCheckStore();
 
 const { save } = useUpload();
+const { updatePageTasks, updateNextUnfinishTask, taskFinisheHandle } =
+  useTask();
 
 const curPage = computed(() => dataCheckStore.curPage);
 const updateSheetData = computed(() => {
@@ -277,6 +290,7 @@ function updateRecogList() {
   });
 
   parseRecogBlocks();
+  updatePageTasks(dataCheckStore.curPage);
 }
 // recogBlocks
 const recogBlocks = ref<RecogBlock[]>([]);
@@ -335,6 +349,31 @@ function parseRecogBlocks() {
   });
 }
 
+// task
+const confirmTipsRef = ref();
+function taskFinished() {
+  if (!dataCheckStore.curPage) return;
+  taskFinisheHandle();
+  if (dataCheckStore.curTask) return;
+  confirmTipsRef.value?.open();
+}
+function tipConfirmHandle() {
+  emit("next");
+}
+
+// edit exam number
+const editExamNumberDialogRef = ref();
+function onEditExamNumber() {
+  if (!dataCheckStore.curPage) return;
+  editExamNumberDialogRef.value?.open();
+}
+async function examNumberModified(examNumber: string) {
+  if (!dataCheckStore.curStudent) return;
+
+  dataCheckStore.modifyExamNumber(examNumber);
+  await save();
+}
+
 // area click
 const recogEditDialogRef = ref();
 async function onAreaClick(data: RecogBlock) {
@@ -437,6 +476,29 @@ watch(
     deep: true,
   }
 );
+
+watch(
+  () => dataCheckStore.curTask,
+  (val) => {
+    if (!val) {
+      editExamNumberDialogRef.value?.close();
+      recogEditDialogRef.value?.close();
+      return;
+    }
+    if (val.type === "examNumber") {
+      editExamNumberDialogRef.value?.open();
+    } else {
+      const questionRecog = recogList.value.find(
+        (item) => item.index === val.questionIndex
+      );
+      if (!questionRecog) return;
+      onAreaClick(questionRecog);
+    }
+  },
+  {
+    immediate: true,
+  }
+);
 </script>
 
 <style lang="less" scoped>

+ 4 - 33
src/render/views/ExamNumberCheck/api.ts

@@ -1,4 +1,4 @@
-import { DataCheckListResult } from "@/ap/types/dataCheck";
+import { StudentCheckItem } from "./types";
 import { useAppStore } from "@/store";
 import { randomCode } from "@/utils/tool";
 import axios from "axios";
@@ -8,51 +8,22 @@ const appStore = useAppStore();
 
 export const allCheckList = async (
   params: AllCheckFilter
-): Promise<DataCheckListResult> => {
+): Promise<StudentCheckItem[]> => {
   // if (!params.isCheck && !params.imageName) {
   //   return Promise.resolve([]);
   // }
-  console.log(params);
 
   const fname = params.isCheck ? "check" : "success";
   const url = `${appStore.serverUrl}/${fname}.csv?${randomCode()}`;
-  let students = await fetchAndParseData(url);
+  let students: StudentCheckItem[] = await fetchAndParseData(url);
   if (params.imageName) {
     students = students.filter((item) => item.imageName === params.imageName);
   }
   const cacheList = await fetchCacheList();
-  const data = (students || []).map((item: any) => {
-    const studentId = `${item.imageName}_${randomCode(8)}`;
-    const questions = (item.smda ? item.smda.split("|") : []).map((item) =>
-      item.trim().replace(/[\.\?]/g, "")
-    );
+  const data = (students || []).map((item) => {
     return {
       ...item,
-      id: studentId,
-      examNumber: item.zkzh,
       checked: cacheList.includes(item.imageName),
-      papers: [
-        {
-          id: `${studentId}_1`,
-          pages: [
-            {
-              index: 1,
-              sheetUri: `${appStore.serverUrl}/pic/${item.imageName}.jpg`,
-              absent: null,
-              breach: null,
-              paperType: null,
-              question: {
-                result: questions,
-                type: "FILL_AREA",
-              },
-              selective: null,
-              recogData: null,
-              recogUri: `${appStore.serverUrl}/omr/${item.imageName}.json`,
-              recogDpi: 150,
-            },
-          ],
-        },
-      ],
     };
   });
   return Promise.resolve(data);

+ 80 - 11
src/render/views/ExamNumberCheck/index.vue

@@ -1,6 +1,12 @@
 <template>
   <div class="data-check exam-number-check">
     <div class="check-menu">
+      <div class="check-menu-head">
+        <a-tabs v-model:menuKey="menuKey" size="small" @change="menuChange">
+          <a-tab-pane key="examNumber" tab="准考证号"></a-tab-pane>
+          <a-tab-pane key="question" tab="客观题"></a-tab-pane>
+        </a-tabs>
+      </div>
       <div class="check-menu-body">
         <ul>
           <li
@@ -50,12 +56,13 @@ import {
 
 import { DataCheckListFilter, DataCheckListItem } from "@/ap/types/dataCheck";
 import { allCheckList, fetchAndParseData, getExamNumberInfo } from "./api";
-import { StudentPage, AllCheckFilter } from "./types";
+import { StudentPage, AllCheckFilter, StudentCheckItem } from "./types";
 import { useDataCheckStore, useAppStore } from "@/store";
 
 import SimplePagination from "@/components/SimplePagination/index.vue";
 import ScanImage from "./ScanImage/index.vue";
 import CheckAction from "./CheckAction.vue";
+import { randomCode } from "@/utils/tool";
 
 defineOptions({
   name: "DataCheck",
@@ -65,12 +72,18 @@ const dataCheckStore = useDataCheckStore();
 const appStore = useAppStore();
 dataCheckStore.resetInfo();
 
+const menuKey = ref("examNumber");
+
 let searchModel = {} as DataCheckListFilter;
 const pageNumber = ref(1);
-const pageSize = ref(20);
+const pageSize = ref(50);
 const total = ref(0);
 const pageCount = ref(0);
-const allStudentList = ref<DataCheckListItem[]>([]);
+const menuStudents = ref<{
+  examNumber: StudentCheckItem[];
+  question: StudentCheckItem[];
+}>({ examNumber: [], question: [] });
+const allStudentList = ref<StudentCheckItem[]>([]);
 const studentList = ref<DataCheckListItem[]>([]);
 const dataList = ref<StudentPage[]>([]);
 const loading = ref(false);
@@ -80,20 +93,62 @@ async function getAllStudents(data: AllCheckFilter) {
   const res = await allCheckList(data).catch(() => null);
   loading.value = false;
   if (!res) return;
+  menuStudents.value = {
+    examNumber: res.filter(
+      (item) => !item.info || item.info.includes("准考证号")
+    ),
+    question: res.filter((item) => item.info.includes("客观题")),
+  };
 
   allStudentList.value = res;
-  total.value = res.length;
-  pageCount.value = Math.ceil(total.value / pageSize.value);
 }
 
 async function getList() {
-  studentList.value = allStudentList.value.slice(
-    (pageNumber.value - 1) * pageSize.value,
-    pageNumber.value * pageSize.value
+  studentList.value = buildStudentList(
+    allStudentList.value.slice(
+      (pageNumber.value - 1) * pageSize.value,
+      pageNumber.value * pageSize.value
+    )
   );
 
   parseStudentPageList(studentList.value);
 }
+function buildStudentList(dataList: StudentCheckItem[]): DataCheckListItem[] {
+  const data: DataCheckListItem[] = (dataList || []).map((item) => {
+    const studentId = `${item.imageName}_${randomCode(8)}`;
+    const questions = (item.smda ? item.smda.split("|") : []).map((item) =>
+      item.trim().replace(/[\.\?]/g, "")
+    );
+    return {
+      ...item,
+      id: studentId,
+      examNumber: item.zkzh,
+      papers: [
+        {
+          id: `${studentId}_1`,
+          pages: [
+            {
+              index: 1,
+              sheetUri: `${appStore.serverUrl}/pic/${item.imageName}.jpg`,
+              absent: null,
+              breach: null,
+              paperType: null,
+              question: {
+                result: questions,
+                type: "FILL_AREA",
+              },
+              selective: null,
+              recogData: null,
+              recogUri: `${appStore.serverUrl}/omr/${item.imageName}.json`,
+              recogDpi: 150,
+            },
+          ],
+        },
+      ],
+    };
+  });
+  return data;
+}
 
 function parseStudentPageList(students: DataCheckListItem[]) {
   dataList.value = [] as StudentPage[];
@@ -127,6 +182,17 @@ function parseStudentPageList(students: DataCheckListItem[]) {
   });
 }
 
+// menu
+async function menuChange(key: string) {
+  menuKey.value = key;
+  allStudentList.value = menuStudents.value[menuKey.value];
+  total.value = allStudentList.value.length;
+  pageCount.value = Math.ceil(total.value / pageSize.value);
+  pageNumber.value = 1;
+  getList();
+  await onSelectStudent(0);
+}
+
 // table
 function onChangeListPage(index: number) {
   pageNumber.value = index;
@@ -134,10 +200,11 @@ function onChangeListPage(index: number) {
   selectPage(0);
 }
 async function onSearch(data: AllCheckFilter) {
-  pageNumber.value = 1;
   await getAllStudents(data);
-  getList();
-  await onSelectStudent(0);
+  const mk = menuStudents.value["examNumber"].length
+    ? "examNumber"
+    : "question";
+  await menuChange(mk);
 }
 
 // student
@@ -230,6 +297,8 @@ async function selectPage(index: number) {
     curPageIndex: index,
   });
 
+  updatePageTasks(dataList.value[index]);
+
   if (!dataCheckStore.curPage) return;
 
   const curStudent = studentList.value[

+ 8 - 0
src/render/views/ExamNumberCheck/types.ts

@@ -25,3 +25,11 @@ export interface AllCheckFilter {
   imageName: string;
   isCheck: boolean;
 }
+
+export interface StudentCheckItem {
+  imageName: string;
+  zkzh: string;
+  smda: string;
+  info: string;
+  checked: boolean;
+}

+ 77 - 0
src/render/views/ExamNumberCheck/useTask.ts

@@ -0,0 +1,77 @@
+import { useDataCheckStore, useAppStore } from "@/store";
+import { StudentPage } from "./types";
+
+export default function useTask() {
+  const dataCheckStore = useDataCheckStore();
+
+  function updatePageTasks(curPage: StudentPage) {
+    if (!curPage) {
+      dataCheckStore.setInfo({
+        taskList:[],
+        curTask: null,
+      });
+      return;
+    }
+  
+    if (menuKey.value === "examNumber") {
+      const taskList = [
+        {
+          id: `examNumber_${curPage.studentId}`
+          type: "examNumber",
+          questionIndex: 0,
+          paperIndex: 0,
+          pageIndex: 0,
+          studentId: curPage.studentId,
+          finished: false,
+        },
+      ];
+      dataCheckStore.setInfo({
+        taskList,
+        curTask: taskList[0],
+      });
+      return;
+    }
+  
+    const taskList = [];
+    curPage.question?.result.map((item, index) => {
+      if (!item || item.length > 1) {
+        taskList.push({
+          id: `question_${curPage.studentId}_${curPage.paperIndex}_${curPage.pageIndex}_${index}`,
+          type: "question",
+          questionIndex: index,
+          paperIndex: curPage.paperIndex,
+          pageIndex: curPage.pageIndex,
+          studentId: curPage.studentId,
+          finished: false,
+        });
+      }
+    });
+  
+    dataCheckStore.setInfo({
+      taskList,
+      curTask: taskList[0],
+    });
+  }
+
+  
+
+  function updateNextUnfinishTask() {
+    const task = dataCheckStore.taskList.find((item) => !item.finished);
+    dataCheckStore.curTask = task || null;
+  }
+
+  function taskFinisheHandle() {
+    const curTask = dataCheckStore.curTask;
+    if (!curTask) {
+      return;
+    }
+    curTask.finished = true;
+    updateNextUnfinishTask();
+  }
+
+  return {
+    updatePageTasks,
+    updateNextUnfinishTask,
+    taskFinisheHandle,
+  };
+}