Browse Source

feat: 缺考校验

zhangjie 9 months ago
parent
commit
903d2c1983

+ 91 - 0
src/render/ap/absentCheck.ts

@@ -0,0 +1,91 @@
+import { request } from "@/utils/request";
+
+import {
+  AbsentCheckListParams,
+  AbsentCheckListFilter,
+  AbsentCheckListResult,
+  ExamStatusSaveParams,
+  ExamStatusSaveResult,
+  AbsentResetParams,
+} from "./types/absentCheck";
+import { RequestActionResult } from "./types/common";
+
+// 缺考校验列表
+export const absentCheckList = (
+  data: AbsentCheckListParams
+): Promise<AbsentCheckListResult> =>
+  request({
+    url: "/api/admin/check/exam-status/list",
+    method: "post",
+    data,
+  });
+
+// 提交
+export const examStatusSave = (
+  data: ExamStatusSaveParams
+): Promise<ExamStatusSaveResult> =>
+  request({
+    url: "/api/admin/check/exam-status/save",
+    method: "post",
+    data,
+  });
+
+// 按考生导出缺考校验
+export const absentCheckStudentExport = (
+  data: AbsentCheckListFilter
+): Promise<AxiosResponse<Blob>> =>
+  request({
+    url: "/api/admin/check/exam-status/student/export",
+    method: "post",
+    data,
+    responseType: "blob",
+  });
+
+// 按考场导出缺考校验
+export const absentCheckRoomExport = (
+  data: AbsentCheckListFilter
+): Promise<AxiosResponse<Blob>> =>
+  request({
+    url: "/api/admin/check/exam-status/exam-room/export",
+    method: "post",
+    data,
+    responseType: "blob",
+  });
+
+// 缺考校验导入模版
+export const absentTemplateDownload = (): Promise<AxiosResponse<Blob>> =>
+  request({
+    url: "/api/admin/check/exam-status/template",
+    method: "post",
+    data,
+    responseType: "blob",
+  });
+
+// 缺考校验导入进度
+export const absentImportStatus = (
+  examId: number
+): Promise<{ synching: boolean }> =>
+  request({
+    url: "/api/admin/check/exam-status/import/status",
+    method: "post",
+    data,
+  });
+
+// 缺考校验导入进度
+export const absentReset = (
+  data: AbsentResetParams
+): Promise<{ synching: boolean }> =>
+  request({
+    url: "/api/admin/check/exam-status/reset",
+    method: "post",
+    data,
+  });
+
+export const absentResetStatus = (
+  examId: number
+): Promise<{ synching: boolean }> =>
+  request({
+    url: "/api/admin/check/exam-status/reset/status",
+    method: "post",
+    data,
+  });

+ 4 - 2
src/render/ap/dataCheck.ts

@@ -23,21 +23,23 @@ export const dataCheckList = (
 // 按考生导出答题卡扫描详情
 export const dataCheckStudentExport = (
   data: DataCheckListFilter
-): Promise<{ url: string }> =>
+): Promise<AxiosResponse<Blob>> =>
   request({
     url: "/api/admin/scan/answer/student/export",
     method: "post",
     data,
+    responseType: "blob",
   });
 
 // 按考场导出答题卡扫描详情
 export const dataCheckRoomExport = (
   data: DataCheckListFilter
-): Promise<{ url: string }> =>
+): Promise<AxiosResponse<Blob>> =>
   request({
     url: "/api/admin/scan/answer/exam-room/export",
     method: "post",
     data,
+    responseType: "blob",
   });
 
 // 按类型修改卷型、识别结果等

+ 0 - 17
src/render/ap/examStatusCheck.ts

@@ -1,17 +0,0 @@
-import { request } from "@/utils/request";
-
-import {
-  ExamStatusSaveParams,
-  ExamStatusSaveResult,
-} from "./types/examStatusCheck";
-import { RequestActionResult } from "./types/common";
-
-// 提交
-export const examStatusSave = (
-  data: ExamStatusSaveParams
-): Promise<ExamStatusSaveResult> =>
-  request({
-    url: "/api/admin/check/exam-status/save",
-    method: "post",
-    data,
-  });

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

@@ -0,0 +1,31 @@
+import { PageResult, PageParams, ExamSubjectParams } from "./common";
+import { DataCheckListItem, ExamStatus } from "./dataCheck";
+
+export interface AbsentCheckListFilter {
+  examId: number;
+  examNumber: string;
+  studentCode: string;
+  name: string;
+  subjectCode: string;
+  examStatus: ExamStatus;
+}
+
+export type AbsentCheckListParams = PageParams<AbsentCheckListFilter>;
+
+export type AbsentCheckListResult = PageResult<DataCheckListItem>;
+
+export interface ExamStatusSaveParams {
+  id: number;
+  examStatus: ExamStatus;
+}
+
+export interface ExamStatusSaveResult {
+  examStatus: ExamStatus;
+}
+
+export type ImportType = "COMPARE" | "OVERRIDE";
+
+export interface AbsentResetParams {
+  examId: number;
+  examNumberFillCount: number;
+}

+ 0 - 10
src/render/ap/types/examStatusCheck.ts

@@ -1,10 +0,0 @@
-import { ExamStatus } from "./dataCheck";
-
-export interface ExamStatusSaveParams {
-  id: number;
-  examStatus: ExamStatus;
-}
-
-export interface ExamStatusSaveResult {
-  examStatus: ExamStatus;
-}

BIN
src/render/assets/imgs/bg-compare.png


BIN
src/render/assets/imgs/bg-overwrite.png


+ 4 - 4
src/render/components/SelectCourse/index.vue

@@ -25,14 +25,14 @@ defineOptions({
 const props = withDefaults(
   defineProps<{
     examId: number;
-    modelValue: string;
+    value: string;
     placeholder?: string;
   }>(),
   {
     placeholder: "请选择",
   }
 );
-const emit = defineEmits(["update:modelValue", "change"]);
+const emit = defineEmits(["update:value", "change"]);
 const attrs = useAttrs();
 
 const fieldNames = { label: "subjectName", value: "subjectCode" };
@@ -50,12 +50,12 @@ const onChange = () => {
   const selectedData = optionList.value.filter(
     (item) => selected.value === item.subjectCode
   );
-  emit("update:modelValue", selected.value);
+  emit("update:value", selected.value);
   emit("change", selectedData[0]);
 };
 
 watch(
-  () => props.modelValue,
+  () => props.value,
   (val) => {
     selected.value = val;
   },

+ 8 - 0
src/render/router/routes.ts

@@ -65,6 +65,14 @@ const routes: RouteRecordRaw[] = [
           },
         ],
       },
+      {
+        path: "absent-check",
+        name: "AbsentCheck",
+        component: () => import("@/views/AbsentCheck/index.vue"),
+        meta: {
+          title: "缺考校验",
+        },
+      },
       {
         path: "data-check",
         name: "DataCheck",

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

@@ -0,0 +1,257 @@
+<template>
+  <div class="check-action">
+    <a-collapse
+      v-model:activeKey="panelKey"
+      :bordered="false"
+      expandIconPosition="end"
+    >
+      <a-collapse-panel key="1">
+        <template #header><FilterFilled />搜索条件 </template>
+        <a-form :label-col="{ style: { width: '83px' } }">
+          <a-form-item label="科目">
+            <select-course
+              v-model:value="searchModel.subjectCode"
+              :exam-id="userStore.curExam.id"
+              style="width: 150px"
+            ></select-course>
+          </a-form-item>
+          <a-form-item label="查找条件">
+            <a-select
+              v-model:value="searchDataCheckType"
+              placeholder="请选择"
+              :options="dataCheckOptions"
+            ></a-select>
+          </a-form-item>
+          <a-form-item label="准考证号">
+            <div class="exam-number">
+              <a-textarea
+                v-model:value="searchModel.examNumber"
+                placeholder="请输入"
+                :auto-size="{ minRows: 1, maxRows: 1 }"
+              ></a-textarea>
+              <div class="number-suffix">{{ examNumberCountCont }}</div>
+            </div>
+            <a-button class="ant-simple m-l-8px" type="link">查看全部</a-button>
+          </a-form-item>
+
+          <a-row>
+            <a-col :span="16">
+              <a-form-item label="姓名">
+                <a-input
+                  v-model:value="searchModel.name"
+                  placeholder="请输入"
+                  style="width: 150px"
+                ></a-input>
+              </a-form-item>
+            </a-col>
+            <a-col :span="8">
+              <a-form-item>
+                <a-button class="m-r-8px" type="primary" @click="onSearch"
+                  >查询</a-button
+                >
+                <a-button @click="onExport">导出</a-button>
+              </a-form-item>
+            </a-col>
+          </a-row>
+        </a-form>
+      </a-collapse-panel>
+      <a-collapse-panel key="2">
+        <template #header><PictureFilled />图片类别 </template>
+
+        <a-radio-group v-model:value="imageType" @change="onImageTypeChange">
+          <a-radio
+            v-for="item in imageTypeOptions"
+            :key="item.value"
+            :value="item.value"
+            >{{ item.label }}</a-radio
+          >
+        </a-radio-group>
+      </a-collapse-panel>
+      <a-collapse-panel key="3">
+        <template #header><IdcardFilled />题卡信息 </template>
+
+        <QuestionPanel
+          v-model:questions="questions"
+          :info="questionInfo"
+          :simple="isSliceImage"
+          @change="onQuestionsChange"
+          @exam-status-change="onExamStatusChange"
+        />
+      </a-collapse-panel>
+      <a-collapse-panel key="4">
+        <template #header><SettingFilled />缺考数据设置 </template>
+
+        <a-button type="primary" :rotate="90" @click="onImport">
+          <template #icon><SelectOutlined /></template>导入缺考名单
+        </a-button>
+        <a-button :rotate="270" @click="onReset">
+          <template #icon><RedoOutlined /></template>重新生成缺考数据
+        </a-button>
+      </a-collapse-panel>
+    </a-collapse>
+  </div>
+
+  <!-- ExportTypeDialog -->
+  <ExportTypeDialog ref="exportTypeDialogRef" @confirm="onExportConfirm" />
+  <!-- ResetDialog -->
+  <ResetDialog ref="resetDialogRef" />
+  <!-- ImportTypeDialog -->
+  <ImportTypeDialog ref="importTypeDialogRef" />
+</template>
+
+<script setup lang="ts">
+import { computed, reactive, ref, watch } from "vue";
+import {
+  FilterFilled,
+  PictureFilled,
+  IdcardFilled,
+  SettingFilled,
+  SelectOutlined,
+  RedoOutlined,
+} from "@ant-design/icons-vue";
+import { message } from "ant-design-vue";
+
+import { useUserStore, useDataCheckStore } from "@/store";
+import { AbsentCheckListFilter, ImportType } from "@/ap/types/absentCheck";
+import { ExamStatus } from "@/ap/types/dataCheck";
+
+import {
+  absentCheckStudentExport,
+  absentCheckRoomExport,
+  examStatusSave,
+} from "@/ap/absentCheck";
+import useDictOption from "@/hooks/dictOption";
+import useLoading from "@/hooks/useLoading";
+import { downloadByApi } from "@/utils/download";
+
+import ExportTypeDialog from "../Review/ExportTypeDialog.vue";
+import ResetDialog from "./ResetDialog.vue";
+import ImportTypeDialog from "./ImportTypeDialog.vue";
+import QuestionPanel from "../DataCheck/QuestionPanel.vue";
+
+defineOptions({
+  name: "CheckAction",
+});
+
+const emit = defineEmits(["search"]);
+
+const userStore = useUserStore();
+const dataCheckStore = useDataCheckStore();
+
+const { optionList: dataCheckOptions } = useDictOption("DATA_CHECK_TYPE");
+const { optionList: imageTypeOptions } = useDictOption("IMAGE_TYPE");
+const panelKey = ref(["1", "2", "3", "4"]);
+
+// search
+const initSearchModel = {
+  examId: userStore.curExam.id,
+  examNumber: "",
+  studentCode: "",
+  name: "",
+  subjectCode: "",
+  examStatus: "",
+};
+const searchModel = reactive<AbsentCheckListFilter>({ ...initSearchModel });
+const searchDataCheckType = ref();
+const imageType = ref(dataCheckStore.imageType);
+const actionType = ref("common");
+
+// imageType
+const isSliceImage = computed(() => {
+  return dataCheckStore.imageType === "SLICE";
+});
+
+const examNumberCountCont = computed(() => {
+  const examNumbers = (searchModel.examNumber || "")
+    .split("\n")
+    .filter((item) => item);
+  return `${examNumbers.length}/100`;
+});
+
+function onSearch() {
+  emit("search", searchModel);
+}
+
+function onImageTypeChange() {
+  dataCheckStore.setInfo({
+    imageType: imageType.value,
+  });
+}
+
+// question panel
+const questionInfo = computed(() => {
+  return {
+    examNumber: dataCheckStore.curStudent?.examNumber,
+    name: dataCheckStore.curStudent?.name,
+    examSite: dataCheckStore.curStudent?.examSite,
+    seatNumber: dataCheckStore.curStudent?.seatNumber,
+    paperType: dataCheckStore.curStudent?.paperType,
+  };
+});
+
+const questions = ref([]);
+watch(
+  () => dataCheckStore.curPageIndex,
+  (val, oldval) => {
+    if (val !== oldval) {
+      questions.value = [...dataCheckStore.curPage?.question];
+    }
+  }
+);
+
+async function onQuestionsChange() {
+  if (!dataCheckStore.curPage) return;
+  dataCheckStore.curPage.question = [...questions.value];
+
+  await dataCheckStore
+    .updateField({
+      field: "QUESTION",
+      value: questions.value,
+    })
+    .catch(() => {});
+}
+
+async function onExamStatusChange(val: ExamStatus) {
+  if (!dataCheckStore.curStudent) return;
+
+  await examStatusSave({ id: dataCheckStore.curStudent.id, examStatus: val });
+  dataCheckStore.curStudent.examStatus = val;
+}
+
+// 导出
+const { loading: downloading, setLoading } = useLoading();
+const exportTypeDialogRef = ref();
+function onExport(type: string) {
+  if (downloading.value) return;
+  actionType.value = type;
+  exportTypeDialogRef.value?.open();
+}
+
+async function onExportConfirm(type: "student" | "room") {
+  if (downloading.value) return;
+
+  setLoading(true);
+  const func =
+    type === "student" ? absentCheckStudentExport : absentCheckRoomExport;
+
+  const res = await downloadByApi(() => func(searchModel)).catch((e: Error) => {
+    message.error(e.message || "下载失败,请重新尝试!");
+  });
+  setLoading(false);
+
+  if (!res) return;
+  message.success("导出成功!");
+}
+
+// 导入
+const importTypeDialogRef = ref();
+function onImport() {
+  importTypeDialogRef.value?.open();
+}
+
+// 重置
+const resetDialogRef = ref();
+function onReset() {
+  resetDialogRef.value?.open();
+}
+</script>

+ 178 - 0
src/render/views/AbsentCheck/ImportTypeDialog.vue

@@ -0,0 +1,178 @@
+<template>
+  <a-modal
+    v-model:open="visible"
+    :width="456"
+    style="top: 10vh"
+    :footer="null"
+    title="导入缺考名单"
+  >
+    <a-row :gutter="16">
+      <a-col :span="12">
+        <import-btn
+          :upload-url="uploadUrl"
+          :upload-data="updateData"
+          :disabled="importing"
+          @upload-success="checkUploadStatus"
+          @valid-error="onValidError"
+        >
+          <div class="type-box" @click="seleted('COMPARE')">
+            <img src="@/assets/imgs/bg-compare.png" alt="交叉对比" />
+            <p>交叉对比</p>
+            <p>CROSS-CONTRAST</p>
+          </div>
+        </import-btn>
+      </a-col>
+      <a-col :span="12">
+        <import-btn
+          :upload-url="uploadUrl"
+          :upload-data="updateData"
+          :disabled="importing"
+          @upload-success="startLoopSync"
+          @valid-error="onValidError"
+        >
+          <div class="type-box" @click="seleted('OVERRIDE')">
+            <img src="@/assets/imgs/bg-overwrite.png" alt="覆盖数据" />
+            <p>覆盖数据</p>
+            <p>OVERWRITE THE DATA</p>
+          </div>
+        </import-btn>
+      </a-col>
+    </a-row>
+    <div class="m-t-16px">
+      <a-button type="primary" :loading="loading" @click="onDownloadTemp">
+        <template #icon><DownloadOutlined /></template>
+        下载缺考名单模版
+      </a-button>
+    </div>
+  </a-modal>
+</template>
+
+<script setup lang="ts">
+import {
+  FilterFilled,
+  PictureFilled,
+  IdcardFilled,
+  DownloadOutlined,
+} from "@ant-design/icons-vue";
+import { computed, ref } from "vue";
+import useModal from "@/hooks/useModal";
+import { ImportType } from "@/ap/types/absentCheck";
+import { absentImportStatus } from "@/ap/absentCheck";
+import { absentTemplateDownload } from "@/ap/absentCheck";
+import { downloadByApi } from "@/utils/download";
+import useLoading from "@/hooks/useLoading";
+import useLoop from "@/hooks/useLoop";
+
+import ImportBtn from "@/components/ImportBtn/index.vue";
+import { useUserStore } from "@/store";
+import { message } from "@qmth/ui";
+
+defineOptions({
+  name: "ImportTypeDialog",
+});
+
+/* modal */
+const { visible, open, close } = useModal();
+defineExpose({ open, close });
+
+const userStore = useUserStore();
+
+const uploadUrl = "/api/admin/scan/answer/sheet/update";
+const uploadMode = ref("" as ImportType);
+
+const updateData = computed(() => {
+  return {
+    examId: userStore.curExam.id,
+    mode: uploadMode.value,
+  };
+});
+
+const { loading: importing, setLoading: setImportLoading } = useLoading();
+
+function seleted(type: ImportType) {
+  console.log(type);
+  uploadMode.value = type;
+  setImportLoading(true);
+  // emit("confirm", type);
+}
+
+const { start: startLoopSync, stop: stopLoopSync } = useLoop(
+  checkUploadStatus,
+  1000
+);
+
+async function checkUploadStatus() {
+  const res = await absentImportStatus(userStore.curExam.id).catch(() => {});
+
+  if (res) {
+    if (!res.synching) {
+      stopLoopSync();
+      setImportLoading(false);
+      message.success("导入成功!");
+      close();
+    }
+  } else {
+    setImportLoading(false);
+    stopLoopSync();
+    message.error("导入错误!");
+  }
+}
+
+function onValidError(data: { message: string }) {
+  message.error(message);
+  setImportLoading(false);
+}
+
+// 下载模板
+const { loading, setLoading } = useLoading();
+async function onDownloadTemp() {
+  if (loading.value) return;
+
+  setLoading(true);
+  const res = await downloadByApi(() => absentTemplateDownload()).catch(
+    (e: Error) => {
+      message.error(e.message || "下载失败,请重新尝试!");
+    }
+  );
+  setLoading(false);
+  if (!res) return;
+  message.success("下载成功!");
+}
+</script>
+
+<style lang="less" scoped>
+.type-box {
+  height: 140px;
+  border-radius: 6px;
+  padding: 26px;
+  border: 1px solid @border-color1;
+  text-align: center;
+  cursor: pointer;
+
+  &:hover {
+    background: #f3f4f6;
+  }
+
+  > img {
+    display: block;
+    height: 35px;
+    width: 32px;
+    margin: 0 auto;
+    margin-bottom: 12px;
+  }
+
+  > p:nth-of-type(1) {
+    height: 24px;
+    font-weight: 500;
+    font-size: 16px;
+    color: #14161a;
+    line-height: 24px;
+  }
+  > p:nth-of-type(2) {
+    height: 18px;
+    font-size: 10px;
+    color: #8c8c8c;
+    line-height: 18px;
+  }
+}
+</style>

+ 108 - 0
src/render/views/AbsentCheck/ResetDialog.vue

@@ -0,0 +1,108 @@
+<template>
+  <a-modal
+    v-model:open="visible"
+    :width="456"
+    style="top: 10vh"
+    title="重新生成缺考数据"
+  >
+    <div>
+      <h3>生成条件</h3>
+      <p>1.客观题未作答+主观题有作答的考生;</p>
+      <p>2.客观题未作答+主观题有作答+卡1或卡2任一题卡准考证号</p>
+      <p>
+        填涂≧
+        <a-input-number
+          v-model:value="examNumberFillCount"
+          :min="1"
+          :max="999"
+          :precision="0"
+          :controls="false"
+        ></a-input-number>
+        位+有条码的考生。
+      </p>
+    </div>
+
+    <template #footer>
+      <div class="box-justify">
+        <div></div>
+
+        <div>
+          <a-button type="text" @click="close">取消</a-button>
+          <a-button type="primary" @click="confirm">确认</a-button>
+        </div>
+      </div>
+    </template>
+  </a-modal>
+</template>
+
+<script setup lang="ts">
+import { computed, ref } from "vue";
+import useModal from "@/hooks/useModal";
+import { absentResetStatus, absentReset } from "@/ap/absentCheck";
+import { absentTemplateDownload } from "@/ap/absentCheck";
+import useLoading from "@/hooks/useLoading";
+import useLoop from "@/hooks/useLoop";
+
+import { useUserStore } from "@/store";
+import { message } from "@qmth/ui";
+
+defineOptions({
+  name: "ImportTypeDialog",
+});
+
+/* modal */
+const { visible, open, close } = useModal();
+defineExpose({ open, close });
+
+const userStore = useUserStore();
+
+const examNumberFillCount = ref<number>();
+
+const { loading, setLoading } = useLoading();
+
+const { start: startLoopSync, stop: stopLoopSync } = useLoop(
+  checkUploadStatus,
+  1000
+);
+
+async function checkUploadStatus() {
+  const res = await absentResetStatus(userStore.curExam.id).catch(() => {});
+
+  if (res) {
+    if (!res.synching) {
+      stopLoopSync();
+      setLoading(false);
+      message.success("缺考校验重新生成成功!");
+      close();
+    }
+  } else {
+    setLoading(false);
+    stopLoopSync();
+    message.error("缺考校验重新生成错误!");
+  }
+}
+
+async function confirm() {
+  if (!examNumberFillCount.value) {
+    message.error("请输入位数");
+    return;
+  }
+
+  if (loading.value) return;
+  setLoading(true);
+
+  const res = await absentReset({
+    examId: userStore.curExam.id,
+    examNumberFillCount: examNumberFillCount.value,
+  }).catch(() => {});
+
+  if (!res) {
+    setLoading(false);
+    return;
+  }
+
+  startLoopSync();
+}
+</script>
+
+<style lang="less" scoped></style>

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

@@ -0,0 +1,191 @@
+<template>
+  <div class="data-check">
+    <div class="check-menu">
+      <div class="check-menu-body">
+        <ul>
+          <li
+            v-for="item in studentList"
+            :key="item.id"
+            @click="onSelectStudent(item)"
+          >
+            {{ item.examNumber }}
+          </li>
+        </ul>
+      </div>
+      <div class="check-menu-page">
+        <SimplePagination
+          :total="total"
+          :page-size="pageSize"
+          @change="onChangeListPage"
+        />
+      </div>
+    </div>
+    <div class="check-body">
+      <ScanImage
+        v-if="dataCheckStore.curPage && isOriginImage && recogList.length"
+        :key="dataCheckStore.curPage.kid"
+        :img-src="dataCheckStore.curPage.sheetUri"
+        :recog-data="recogList"
+        @prev="onPrevPage"
+        @next="onNextPage"
+        @recog-block-modified="onRecogEditConfirm"
+      />
+      <SliceImage v-if="dataCheckStore.curPage && !isOriginImage" />
+    </div>
+
+    <CheckAction @search="onSearch" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, computed } from "vue";
+import { message } from "ant-design-vue";
+import { CaretLeftOutlined, CaretRightOutlined } from "@ant-design/icons-vue";
+
+import { AbsentCheckListFilter } from "@/ap/types/absentCheck";
+import { DataCheckListItem } from "@/ap/types/dataCheck";
+import { absentCheckList } from "@/ap/absentCheck";
+import { StudentPage } from "./types";
+import { useDataCheckStore } from "@/store";
+
+import SimplePagination from "@/components/SimplePagination/index.vue";
+import ScanImage from "../DataCheck/ScanImage/index.vue";
+import SliceImage from "../DataCheck/SliceImage/index.vue";
+import CheckAction from "./CheckAction.vue";
+
+defineOptions({
+  name: "AbsentCheck",
+});
+
+const dataCheckStore = useDataCheckStore();
+
+let searchModel = {} as AbsentCheckListFilter;
+const pageNumber = ref(1);
+const pageSize = ref(20);
+const total = ref(0);
+const studentList = ref<DataCheckListItem[]>([]);
+const dataList = ref<StudentPage[]>([]);
+const loading = ref(false);
+
+async function getList() {
+  loading.value = true;
+  const datas = {
+    ...searchModel,
+    pageNumber: pageNumber.value,
+    pageSize: pageSize.value,
+  };
+  const res = await absentCheckList(datas).catch(() => null);
+  loading.value = false;
+  if (!res) return;
+
+  total.value = res.totalCount;
+  studentList.value = res.result;
+
+  parseStudentPageList(res.result);
+}
+
+function parseStudentPageList(students: DataCheckListItem[]) {
+  dataList.value = [];
+
+  students.forEach((student, studentIndex) => {
+    student.papers.forEach((paper, paperIndex) => {
+      if (!paper.pages) return;
+      paper.pages.forEach((page, pageIndex) => {
+        dataList.value.push({
+          ...page,
+          paperId: paper.id as number,
+          pageIndex,
+          paperIndex,
+          studentIndex,
+          studentId: student.id,
+          examId: searchModel.examId,
+          kid: `${student.id}-${studentIndex}-${paperIndex}-${pageIndex}`,
+        });
+      });
+    });
+  });
+}
+
+function onSelectStudent(record: DataCheckListItem) {
+  const pageIndex = dataList.value.findIndex(
+    (item) => item.studentId === record.id
+  );
+  if (pageIndex === -1) return;
+
+  selectPage(pageIndex);
+}
+
+// imageType
+const isOriginImage = computed(() => {
+  return dataCheckStore.imageType === "ORIGIN";
+});
+
+// table
+async function onChangeListPage(index: number) {
+  pageNumber.value = index;
+  await getList();
+  selectPage(0);
+}
+async function onSearch(datas: AbsentCheckListFilter) {
+  searchModel = { ...datas };
+  pageNumber.value = 1;
+  await getList();
+  selectPage(0);
+}
+
+// page
+const curStudentInfo = ref({
+  examNumber: "",
+  name: "",
+  examSite: "",
+  seatNumber: "",
+  paperType: "",
+});
+const curPageQuestions = ref<string[]>([]);
+
+function selectPage(index: number) {
+  dataCheckStore.setInfo({
+    curPage: dataList.value[index],
+    curPageIndex: index,
+  });
+
+  if (!dataCheckStore.curPage) return;
+
+  const curStudent = studentList.value[
+    dataCheckStore.curPage.studentIndex
+  ] as DataCheckListItem;
+  dataCheckStore.setInfo({ curStudent });
+}
+
+async function onPrevPage() {
+  if (dataCheckStore.curPageIndex <= 0) {
+    if (pageNumber.value === 1) {
+      message.error("没有上一张了");
+      return;
+    }
+
+    pageNumber.value--;
+    await getList();
+    selectPage(dataList.value.length - 1);
+    return;
+  }
+
+  selectPage(dataCheckStore.curPageIndex - 1);
+}
+
+async function onNextPage() {
+  if (dataCheckStore.curPageIndex >= dataList.value.length - 1) {
+    if (pageNumber.value >= total.value) {
+      message.error("没有下一张了");
+      return;
+    }
+
+    pageNumber.value++;
+    await getList();
+    selectPage(0);
+    return;
+  }
+
+  selectPage(dataCheckStore.curPageIndex + 1);
+}
+</script>

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

@@ -194,6 +194,7 @@
               <div
                 class="flex items-center cursor-pointer"
                 :style="{ color: token.colorPrimary }"
+                @click="toPage('AbsentCheck')"
               >
                 <span>进入</span>
                 <RightOutlined />

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

@@ -181,7 +181,7 @@ import { useUserStore, useDataCheckStore } from "@/store";
 import { DataCheckListFilter, ExamStatus } from "@/ap/types/dataCheck";
 import { SubjectItem } from "@/ap/types/base";
 import { dataCheckStudentExport, dataCheckRoomExport } from "@/ap/dataCheck";
-import { examStatusSave } from "@/ap/examStatusCheck";
+import { examStatusSave } from "@/ap/absentCheck";
 import useDictOption from "@/hooks/dictOption";
 import useLoading from "@/hooks/useLoading";
 import { ImageType, booleanOptionList } from "@/constants/enumerate";

+ 0 - 4
src/render/views/DataCheck/ScanImage/index.vue

@@ -88,10 +88,6 @@ import FillAreaSetDialog from "./FillAreaSetDialog.vue";
 import RecogEditDialog from "./RecogEditDialog.vue";
 import ImportBtn from "@/components/ImportBtn/index.vue";
 
-import { objAssign } from "@/utils/tool";
-
-import { omit } from "lodash-es";
-
 defineOptions({
   name: "ScanImage",
 });

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

@@ -48,8 +48,8 @@ import { StudentPage } from "./types";
 import { useDataCheckStore } from "@/store";
 
 import SimplePagination from "@/components/SimplePagination/index.vue";
-import ScanImage from "@/components/ScanImage/index.vue";
-import SliceImage from "@/components/SliceImage/index.vue";
+import ScanImage from "./ScanImage/index.vue";
+import SliceImage from "./SliceImage/index.vue";
 import CheckAction from "./CheckAction.vue";
 
 defineOptions({