Parcourir la source

提交点代码

刘洋 il y a 9 mois
Parent
commit
d4a8bc9a55

+ 1 - 0
package.json

@@ -33,6 +33,7 @@
     "v3-drag-zoom": "^1.1.20",
     "vue": "^3.4.32",
     "vue-echarts": "^7.0.0-beta.0",
+    "vue-request": "^2.0.4",
     "vue-router": "^4.2.4"
   },
   "devDependencies": {

+ 133 - 0
src/render/ap/mock/baseDataConfig.ts

@@ -95,3 +95,136 @@ Mock.mock(/\/api\/admin\/student\/count/, "post", [
     studentCount: 5,
   },
 ]);
+Mock.mock(/\/api\/admin\/student\/import\/config/, "post", {
+  //考试年度
+  year: 2024,
+  //考次
+  yearHalf: 1,
+});
+Mock.mock(/\/api\/admin\/student\/import\/save/, "post", {
+  updateTime: 1,
+});
+Mock.mock(/\/api\/admin\/student\/clear/, "post", {
+  updateTime: 1,
+});
+Mock.mock(/\/api\/admin\/student\/import/, "post", {
+  taskId: 1,
+});
+Mock.mock(/\/api\/admin\/subject\/list/, "post", [
+  {
+    code: "1",
+    name: "科目一",
+  },
+]);
+Mock.mock(/\/api\/admin\/subject\/scan\/progress/, "post", {
+  total: {
+    progress: "10%",
+    studentCount: 22,
+    unexistCount: 10,
+    scannedCount: 12,
+    estimation: 1000000,
+  },
+  subjects: [
+    {
+      subjectCode: 1,
+      subjectName: "科目一",
+      progress: "10%",
+      studentCount: 22,
+      scannedCount: 12,
+      unexistCount: 10,
+      estimation: 1000000,
+    },
+  ],
+});
+Mock.mock(/\/api\/admin\/scanner\/workload/, "post", [
+  {
+    device: "192.168.0.1",
+    answerScanCount: 100,
+  },
+]);
+Mock.mock(/\/api\/admin\/exam_site\/list/, "post", [
+  {
+    code: "1",
+    name: "考点一",
+  },
+]);
+Mock.mock(/\/api\/admin\/campus\/list/, "post", [
+  {
+    code: "1",
+    name: "校区一",
+  },
+]);
+Mock.mock(/\/api\/admin\/exam-room\/scanned\/page/, "post", {
+  result: [
+    {
+      subjectCode: "1",
+      subjectName: "科目一",
+      examSite: 1,
+      campusCode: 1,
+      examRoom: 1,
+      scanned: "已扫描",
+    },
+  ],
+  totalCount: 10,
+  pageCount: 5,
+});
+
+Mock.mock(/\/api\/admin\/student\/page/, "post", {
+  result: [
+    {
+      id: 1,
+      examNumber: "111",
+      name: "刘洋",
+      studentCode: "111",
+      subjectCode: "1",
+      subjectName: "科目一",
+      packageCode: "222",
+      campusCode: "1",
+      campusName: "1",
+      examSite: "1",
+      examSiteName: "1",
+      examRoom: "考场一",
+      seatNumber: "座位号1",
+      scanned: "已扫描",
+      device: "设备一",
+    },
+  ],
+  totalCount: 10,
+  pageCount: 5,
+});
+
+Mock.mock(/\/api\/admin\/batch\/subject\/list/, "post", [
+  {
+    subjectCode: "1",
+    subjectName: "科目一",
+  },
+  {
+    subjectCode: "2",
+    subjectName: "科目二",
+  },
+]);
+Mock.mock(/\/api\/admin\/batch\/device\/list/, "post", [
+  {
+    device: "1",
+    deviceName: "设备一",
+  },
+  {
+    device: "2",
+    deviceName: "设备二",
+  },
+]);
+Mock.mock(/\/api\/admin\/batch\/list/, "post", [
+  {
+    batchId: "1",
+    createTime: 1727081842375,
+  },
+  {
+    batchId: "2",
+    createTime: 1727081842375,
+  },
+]);
+Mock.mock(/\/api\/admin\/batch\/student\/list/, "post", [
+  {
+    batchId: "1",
+  },
+]);

+ 1 - 0
src/render/ap/mock/scanManage.ts

@@ -0,0 +1 @@
+import Mock from "mockjs";

+ 167 - 0
src/render/ap/scanManage.ts

@@ -0,0 +1,167 @@
+import { request } from "@/utils/request";
+import { getFileMD5 } from "@/utils/crypto";
+import { obj2formData } from "@/utils/tool";
+
+//获取扫描批次科目列表
+export const batchSubjectList = (params: {
+  examId: any;
+  startTime?: number;
+  endTime?: number;
+}) =>
+  request({
+    url: "/api/admin/batch/subject/list",
+    params,
+  });
+//根据科目查询扫描批次机器列表
+export const batchDeviceList = (params: {
+  examId: any;
+  subjectCode: string;
+  startTime?: number;
+  endTime?: number;
+}) =>
+  request({
+    url: "/api/admin/batch/device/list",
+    params,
+  });
+
+//根据机器查询批次列表
+export const batchList = (params: {
+  examId: any;
+  subjectCode: string;
+  device: string;
+  startTime?: number;
+  endTime?: number;
+}) =>
+  request({
+    url: "/api/admin/batch/list",
+    params,
+  });
+//根据批次查询考生列表
+export const batchStudentList = (params: { batchId: any }) =>
+  request({
+    url: "/api/admin/batch/student/list",
+    params,
+  });
+
+export const getStuImportSet = (params: { examId: number }) =>
+  request({
+    url: "/api/admin/student/import/config",
+    params,
+  });
+
+export const saveStuImportSet = (params: {
+  examId: number;
+  year: number | string;
+  yearHalf: number | string;
+}) =>
+  request({
+    url: "/api/admin/student/import/config/save",
+    params,
+  });
+
+export const clearStuData = (params: { examId: number; subjectCode: string }) =>
+  request({
+    url: "/api/admin/student/clear",
+    params,
+  });
+
+export const importStu = async (params: {
+  examId: number;
+  file: File | null;
+}) => {
+  const md5 = await getFileMD5(params.file as File);
+  const formData = obj2formData(params);
+  return request({
+    url: "/api/admin/student/import",
+    data: formData,
+    headers: {
+      md5,
+      "Content-Type": "multipart/form-data",
+    },
+  });
+};
+
+export const scanProcessData = (params: {
+  examId: number | undefined;
+  subjectCode: string;
+}) =>
+  request({
+    url: "/api/admin/subject/scan/progress",
+    params,
+    loading: true,
+  });
+
+export const getWorkStatistics = (params: {
+  examId: number | undefined;
+  startTime: number | undefined;
+  endTime: number | undefined;
+}) =>
+  request({
+    url: "/api/admin/scanner/workload",
+    params,
+    loading: true,
+  });
+export const exportWorkStatistics = (params: {
+  examId: number | undefined;
+  startTime: number | undefined;
+  endTime: number | undefined;
+}) =>
+  request({
+    url: "/api/admin/scanner/workload/export",
+    params,
+  });
+
+export const getSiteList = (params: { examId: number | undefined }) =>
+  request({
+    url: "/api/admin/exam_site/list",
+    params,
+  });
+
+export const getCampusList = (params: { examId: number | undefined }) =>
+  request({
+    url: "/api/admin/campus/list",
+    params,
+  });
+export const getScannedList = (
+  params: {
+    examId: number | undefined;
+    campusCode?: string;
+    subjectCode?: string;
+    province?: string;
+    examSite?: string;
+    examRoom?: string;
+    scanned?: boolean | null;
+  } & PageBaseParams
+) =>
+  request({
+    url: "/api/admin/exam-room/scanned/page",
+    params,
+  });
+
+export const exportScanned = (params: {
+  examId: any;
+  campusCode?: string;
+  subjectCode?: string;
+  province?: string;
+  examSite?: string;
+  examRoom?: string;
+  scanned?: boolean | null;
+}) =>
+  request({
+    url: "/api/admin/exam-room/scanned/export",
+    params,
+  });
+
+export const getStuPage = (params: {
+  examId: any;
+  campusCode?: string;
+  subjectCode?: string;
+  name?: string;
+  examSite?: string;
+  examRoom?: string;
+  packageCode?: string;
+}) =>
+  request({
+    url: "/api/admin/student/page",
+    params,
+  });

+ 5 - 1
src/render/ap/system.ts

@@ -2,7 +2,7 @@ import { request } from "@/utils/request";
 
 export const getServerStatus = () =>
   request({
-    url: "/api/status",
+    url: "/api/sys/status",
     loading: true,
   });
 
@@ -12,3 +12,7 @@ export const adminLogin = (params: { loginName: string; password: string }) =>
     params,
     loading: true,
   });
+export const getSubjectList = (params: { examId: number | null }) =>
+  request({
+    url: "/api/admin/subject/list",
+  });

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

@@ -32,13 +32,17 @@ declare module 'vue' {
     ASpaceCompact: typeof import('@qmth/ui')['SpaceCompact']
     ASpin: typeof import('@qmth/ui')['Spin']
     ATable: typeof import('@qmth/ui')['Table']
+    ATableSummaryCell: typeof import('@qmth/ui')['TableSummaryCell']
+    ATableSummaryRow: typeof import('@qmth/ui')['TableSummaryRow']
     ATabPane: typeof import('@qmth/ui')['TabPane']
     ATabs: typeof import('@qmth/ui')['Tabs']
     ATag: typeof import('@qmth/ui')['Tag']
     ATextarea: typeof import('@qmth/ui')['Textarea']
+    ATypographyText: typeof import('@qmth/ui')['TypographyText']
     AUpload: typeof import('@qmth/ui')['Upload']
     QmButton: typeof import('@qmth/ui')['QmButton']
     QmConfigProvider: typeof import('@qmth/ui')['QmConfigProvider']
+    QmDateRangePicker: typeof import('@qmth/ui')['QmDateRangePicker']
     QmLowForm: typeof import('@qmth/ui')['QmLowForm']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']

+ 3 - 3
src/render/components/ImportButton/index.vue

@@ -8,13 +8,13 @@
     :file-list="fileList"
     @change="fileChange"
   >
-    <qm-button :loading="loading">
-      <template #icon><ImportOutlined /></template>{{ btnText }}
+    <qm-button :loading="loading" :icon="h(ImportOutlined)">
+      {{ btnText }}
     </qm-button>
   </a-upload>
 </template>
 <script name="ImportButton" lang="ts" setup>
-import { ref, isRef, Ref, Reactive } from "vue";
+import { ref, h, Ref, Reactive } from "vue";
 import type { UploadProps } from "@qmth/ui";
 import { getFileMD5 } from "@/utils/crypto";
 import { request } from "@/utils/request";

+ 60 - 0
src/render/components/SelectSubject/index.vue

@@ -0,0 +1,60 @@
+<template>
+  <a-select
+    v-model:value="selected"
+    :placeholder="placeholder"
+    :options="optionList"
+    filter-option
+    :multiple="false"
+    :field-names="fieldNames"
+    v-bind="attrs"
+    @change="onChange"
+  >
+  </a-select>
+</template>
+
+<script setup name="SelectSubject" lang="ts">
+import { ref, useAttrs, watch } from "vue";
+import { getSubjectList } from "@/ap/system";
+
+const props = withDefaults(
+  defineProps<{
+    examId: number;
+    modelValue: string;
+    placeholder?: string;
+  }>(),
+  {
+    placeholder: "请选择",
+  }
+);
+const emit = defineEmits(["update:modelValue", "change"]);
+const attrs = useAttrs();
+
+const fieldNames = { label: "name", value: "code" };
+
+const selected = ref("");
+const optionList = ref([]);
+const search = async () => {
+  optionList.value = [];
+  const resData = await getSubjectList({ examId: props.examId });
+  optionList.value = resData || [];
+};
+search();
+
+const onChange = () => {
+  const selectedData = optionList.value.filter(
+    (item: any) => selected.value === item.code
+  );
+  emit("update:modelValue", selected.value);
+  emit("change", selectedData[0]);
+};
+
+watch(
+  () => props.modelValue,
+  (val) => {
+    selected.value = val;
+  },
+  {
+    immediate: true,
+  }
+);
+</script>

+ 3 - 3
src/render/hooks/useTable.ts

@@ -1,11 +1,11 @@
-import { reactive, ref } from "vue";
+import { reactive, ref, isRef, Ref } from "vue";
 import type { UnwrapRef } from "vue";
 import { PageResult } from "@/ap/types/common";
 import type { TablePaginationConfig } from "ant-design-vue";
 
 export default function useTable<T extends Record<string, any>>(
   apiFunc: (data: any) => Promise<PageResult<T>>,
-  searchModel: Record<string, any>,
+  searchModel: Record<string, any> | Ref,
   initAutoFetch = true
 ) {
   const pageNumber = ref(1);
@@ -17,7 +17,7 @@ export default function useTable<T extends Record<string, any>>(
   async function getList() {
     loading.value = true;
     const datas = {
-      ...searchModel,
+      ...(isRef(searchModel || {}) ? searchModel.value : searchModel),
       pageNumber: pageNumber.value,
       pageSize: pageSize.value,
     };

+ 1 - 1
src/render/main.ts

@@ -9,7 +9,7 @@ import "@qmth/ui/lib/style.css";
 import "v3-drag-zoom/dist/style.css";
 import "virtual:uno.css";
 import "./styles/index.less";
-import "./ap/mock/index.ts";
+// import "./ap/mock/index.ts";
 
 window.$message = message;
 window.$notification = notification;

+ 0 - 5
src/render/views/BaseDataConfig/CardImport.vue

@@ -12,7 +12,6 @@
     >
       <template #bodyCell="{ column, record }">
         <template v-if="column.key === 'operation'">
-          <qm-button type="link" @click="editRow(record)">修改</qm-button>
           <qm-button type="link" @click="deleteRow(record)">删除</qm-button>
         </template>
       </template>
@@ -36,10 +35,6 @@ import ImportCardDialog from "./ImportCardDialog.vue";
 const userStore = useUserStore();
 
 const showImportCardDialog = ref(false);
-const curRow = ref(null);
-const editRow = (row: any) => {
-  curRow.value = row;
-};
 const fields = ref([
   {
     type: "buttons",

+ 59 - 11
src/render/views/BaseDataConfig/SetImportParamsDialog.vue

@@ -1,34 +1,82 @@
 <template>
-  <my-modal v-model:open="visible" title="设置导入参数" :width="400">
-    <qm-low-form :params="params" :fields="fields" :label-width="80">
+  <my-modal
+    v-model:open="visible"
+    title="设置导入参数"
+    :width="400"
+    @ok="submit"
+  >
+    <qm-low-form
+      :params="params"
+      :fields="fields"
+      :label-width="80"
+      :rules="rules"
+      ref="form"
+    >
     </qm-low-form>
   </my-modal>
 </template>
 <script name="SetImportParamsDialog" lang="ts" setup>
 import { ref } from "vue";
-import { setValueFromObj } from "@/utils/tool";
+import { useUserStore } from "@/store";
+import { saveStuImportSet } from "@/ap/baseDataConfig";
 const visible = defineModel();
-const props = defineProps<{ curRow: any }>();
-
-//todo 入参名
+const userStore = useUserStore();
+const form = ref();
+const emit = defineEmits(["success"]);
 const params = ref({
-  a: "",
-  b: "",
+  year: "",
+  yearHalf: "",
 });
-//   params.value = setValueFromObj(params.value, props.curRow);
 const fields = ref([
   {
-    prop: "a",
+    prop: "year",
     label: "考试年度",
     colSpan: 24,
     type: "select",
+    attrs: {
+      options: [
+        { value: "2020", label: "2020" },
+        { value: "2021", label: "2021" },
+        { value: "2022", label: "2022" },
+        { value: "2023", label: "2023" },
+        { value: "2024", label: "2024" },
+        { value: "2025", label: "2025" },
+        { value: "2026", label: "2026" },
+        { value: "2027", label: "2027" },
+        { value: "2028", label: "2028" },
+        { value: "2029", label: "2029" },
+      ],
+    },
   },
   {
-    prop: "b",
+    prop: "yearHalf",
     label: "考次",
     colSpan: 24,
     type: "select",
+    attrs: {
+      options: [
+        { value: 1, label: "上半年" },
+        { value: 2, label: "下半年" },
+      ],
+    },
   },
 ]);
+const rules = {
+  year: { required: true, message: "请选择考试年度" },
+  yearHalf: { required: true, message: "请选择考次" },
+};
+const submit = () => {
+  form.value.formRef.validate().then(() => {
+    saveStuImportSet({
+      ...params.value,
+      examId: userStore.curExam?.id as number,
+    }).then((res: any) => {
+      console.log("mock 保存考生导入参数设置", res);
+      window.$message.success("操作成功");
+      visible.value = false;
+      emit("success");
+    });
+  });
+};
 </script>
 <style lang="less" scoped></style>

+ 48 - 10
src/render/views/BaseDataConfig/StuImport.vue

@@ -8,7 +8,9 @@
     <a-table :data-source="dataList" :columns="columns" size="middle" bordered>
       <template #bodyCell="{ column, record }">
         <template v-if="column.key === 'operation'">
-          <qm-button type="link" @click="">导入</qm-button>
+          <qm-button type="link" @click="openImportDialog(record)"
+            >导入</qm-button
+          >
           <qm-button type="link" @click="clear(record)">清空</qm-button>
         </template>
       </template>
@@ -17,6 +19,11 @@
       v-model="showSetParamsDialog"
       v-if="showSetParamsDialog"
     ></SetImportParamsDialog>
+    <StuImportFileDialog
+      v-model="showStuImportFileDialog"
+      v-if="showStuImportFileDialog"
+    >
+    </StuImportFileDialog>
   </div>
 </template>
 <script name="StuImport" lang="ts" setup>
@@ -25,10 +32,26 @@ import SetImportParamsDialog from "./SetImportParamsDialog.vue";
 import useTable from "@/hooks/useTable";
 import { getStuList } from "@/ap/baseDataConfig";
 import { useUserStore } from "@/store";
+import { getStuImportSet, clearStuData } from "@/ap/baseDataConfig";
+import StuImportFileDialog from "./StuImportFileDialog.vue";
 import type { TableColumnsType } from "@qmth/ui";
 
 const userStore = useUserStore();
 const showSetParamsDialog = ref(false);
+const showStuImportFileDialog = ref(false);
+const year = ref();
+const yearHalf = ref();
+
+const _getStuImportSet = () => {
+  getStuImportSet({ examId: userStore.curExam?.id as number }).then(
+    (res: any) => {
+      console.log("mock 考生导入参数设置获取", res);
+      year.value = res?.year;
+      yearHalf.value = res?.yearHalf;
+    }
+  );
+};
+_getStuImportSet();
 const fields = ref([
   {
     type: "button",
@@ -53,15 +76,15 @@ const columns: TableColumnsType = [
   },
   {
     title: "科目代码",
-    dataIndex: "a",
+    dataIndex: "subjectCode",
   },
   {
     title: "科目名称",
-    dataIndex: "b",
+    dataIndex: "subjectName",
   },
   {
     title: "已导入考生数",
-    dataIndex: "c",
+    dataIndex: "studentCount",
   },
   {
     title: "操作",
@@ -69,20 +92,35 @@ const columns: TableColumnsType = [
     width: 300,
   },
 ];
-const { dataList, pagination, loading, getList, toPage } = useTable(
-  getStuList,
-  { examId: userStore.curExam?.id },
-  true
-);
+const dataList = ref([]);
+const search = () => {
+  getStuList({ examId: userStore.curExam?.id as number }).then((res: any) => {
+    console.log("mock 考生列表", res);
+    dataList.value = res || [];
+  });
+};
+search();
 const clear = (row: any) => {
   window.$confirm({
     title: () => "系统通知",
     content: () => "请确认是否立即删除?",
     onOk() {
-      //todo 执行删除接口
+      clearStuData({
+        examId: userStore.curExam?.id as number,
+        subjectCode: row.subjectCode,
+      }).then(() => {
+        console.log("mock 删除考生单条数据");
+        window.$message.success("操作成功");
+        search();
+      });
     },
   });
 };
+const curRow = ref(null);
+const openImportDialog = (row: any) => {
+  curRow.value = row;
+  showStuImportFileDialog.value = true;
+};
 </script>
 <style lang="less" scoped>
 .stu-import {

+ 67 - 0
src/render/views/BaseDataConfig/StuImportFileDialog.vue

@@ -0,0 +1,67 @@
+<template>
+  <my-modal
+    v-model:open="visible"
+    title="考生导入"
+    :width="600"
+    @ok="submitHandle"
+  >
+    <qm-low-form
+      :params="params"
+      :fields="fields"
+      :label-width="110"
+      :rules="rules"
+      ref="form"
+    >
+      <template #file>
+        <ImportButton @fileChange="getFile" />
+      </template>
+      <template #download>
+        <qm-button :icon="h(VerticalAlignBottomOutlined)" @click="downloadTpl"
+          >考生导入模板</qm-button
+        >
+      </template>
+    </qm-low-form>
+  </my-modal>
+</template>
+<script name="StuImportFileDialog" lang="ts" setup>
+import { ref, reactive, h } from "vue";
+import { importStu } from "@/ap/baseDataConfig";
+import { VerticalAlignBottomOutlined } from "@ant-design/icons-vue";
+import { downloadByCrossUrl } from "@/utils/tool";
+import { useUserStore } from "@/store";
+
+const userStore = useUserStore();
+const form = ref();
+const visible = defineModel();
+const emit = defineEmits(["success"]);
+
+const params = reactive({
+  file: null,
+});
+const fields = ref([
+  { prop: "file", cell: "file", label: "选择文件", colSpan: 24 },
+  { cell: "download", label: "下载模板", colSpan: 24 },
+]);
+
+const rules = {
+  file: [{ required: true, message: "请上传文件" }],
+};
+const getFile = (file: any) => {
+  params.file = file;
+};
+const submitHandle = () => {
+  form.value.formRef.validate().then(() => {
+    importStu({ examId: userStore.curExam?.id as number, ...params }).then(
+      (res: any) => {
+        console.log("mock 考生导入", res);
+        window.$message.success("考生导入成功");
+        visible.value = false;
+      }
+    );
+  });
+};
+const downloadTpl = () => {
+  downloadByCrossUrl("/api/admin/student/import/template", "考生导入模板");
+};
+</script>
+<style lang="less" scoped></style>

+ 150 - 72
src/render/views/ScanManage/ScanCheckMiss.vue

@@ -1,103 +1,181 @@
 <template>
   <div class="scan-check-miss h-full">
-    <qm-low-form
-      :params="searchParams"
-      :fields="searchFields"
-      :label-width="80"
-    ></qm-low-form>
-    <a-table :data-source="tableData" :columns="columns" size="middle" bordered>
+    <qm-low-form :params="params" :fields="fields" :label-width="80">
+      <template #subjectCode>
+        <SelectSubject
+          v-model="params.subjectCode"
+          :exam-id="userStore.curExam?.id as number"
+        ></SelectSubject>
+      </template>
+    </qm-low-form>
+    <a-table
+      :data-source="dataList"
+      :columns="columns"
+      size="middle"
+      bordered
+      :loading="loading"
+      :pagination="pagination"
+    >
+      <template #bodyCell="{ column, record }">
+        <template v-if="column.dataIndex === 'scanned'">
+          {{ record[column.dataIndex] }}
+        </template>
+      </template>
     </a-table>
   </div>
 </template>
 <script name="ScanCheckMiss" lang="ts" setup>
-import { ref, computed } from "vue";
+import { ref, reactive, computed } from "vue";
+import SelectSubject from "@/components/SelectSubject/index.vue";
+import { useUserStore } from "@/store";
+import { useRequest } from "vue-request";
+import useTable from "@/hooks/useTable";
+import { downloadByApi } from "@/utils/download";
+
+import {
+  getSiteList,
+  getCampusList,
+  getScannedList,
+  exportScanned,
+} from "@/ap/baseDataConfig";
 import type { TableColumnsType } from "@qmth/ui";
-//todo 入参名
-const searchParams = ref({
-  a: "",
-  b: "",
-  c: "",
-  d: "",
-  e: "",
-  f: "",
+const userStore = useUserStore();
+
+//考点下拉
+const { data: examSiteOptions, run: runSite } = useRequest(getSiteList);
+const { data: examCampusOptions, run: runCampus } = useRequest(getCampusList);
+runSite({ examId: userStore.curExam?.id });
+runCampus({ examId: userStore.curExam?.id });
+
+const params = reactive({
+  subjectCode: "",
+  province: "",
+  examSite: "",
+  campusCode: "",
+  examRoom: "",
+  scanned: null,
 });
-const searchFields = ref([
-  {
-    prop: "a",
-    type: "select",
-    colSpan: 3,
-    label: "科目",
-  },
-  {
-    prop: "b",
-    type: "select",
-    colSpan: 3,
-    label: "省份",
-  },
-  {
-    prop: "c",
-    type: "select",
-    colSpan: 3,
-    label: "考点",
-  },
-  {
-    prop: "d",
-    type: "select",
-    colSpan: 3,
-    label: "校区",
-  },
-  {
-    prop: "e",
-    type: "select",
-    colSpan: 3,
-    label: "考场号",
-  },
-  {
-    prop: "f",
-    type: "select",
-    colSpan: 3,
-    label: "扫描状态",
-  },
-  {
-    type: "buttons",
-    colSpan: 5,
-    children: [
-      {
-        text: "查询",
+const transParams = computed(() => {
+  return { ...params, examId: userStore.curExam?.id };
+});
+const { dataList, pagination, loading, getList, toPage } = useTable(
+  getScannedList,
+  transParams,
+  true
+);
+const fields = computed(() => {
+  return [
+    {
+      cell: "subjectCode",
+      type: "select",
+      colSpan: 3,
+      label: "科目",
+    },
+    {
+      prop: "province",
+      type: "select",
+      colSpan: 3,
+      label: "省份",
+    },
+    {
+      prop: "examSite",
+      type: "select",
+      colSpan: 3,
+      label: "考点",
+      attrs: {
+        options: examSiteOptions.value || [],
+        fieldNames: { label: "name", value: "code" },
       },
-      {
-        text: "导出",
-        attrs: {
-          type: "default",
-        },
+    },
+    {
+      prop: "campusCode",
+      type: "select",
+      colSpan: 3,
+      label: "校区",
+      attrs: {
+        options: examCampusOptions.value || [],
+        fieldNames: { label: "name", value: "code" },
       },
-    ],
-  },
-]);
+    },
+    {
+      prop: "examRoom",
+      type: "select",
+      colSpan: 3,
+      label: "考场号",
+    },
+    {
+      prop: "scanned",
+      type: "select",
+      colSpan: 3,
+      label: "扫描状态",
+      attrs: {
+        options: [
+          { value: true, label: "已扫描" },
+          { value: false, label: "未扫描" },
+        ],
+        allowClear: true,
+      },
+    },
+    {
+      type: "buttons",
+      colSpan: 5,
+      children: [
+        {
+          text: "查询",
+        },
+        {
+          text: "导出",
+          attrs: {
+            type: "default",
+            loading: exportLoading.value,
+          },
+          onClick: () => {
+            exportFile();
+          },
+        },
+      ],
+    },
+  ];
+});
 
-const tableData = ref([{ a: 1, b: 2, c: 3 }]);
 const columns: TableColumnsType = [
   {
     title: "科目",
-    dataIndex: "a",
+    dataIndex: "subjectName",
   },
   {
     title: "考点",
-    dataIndex: "b",
+    dataIndex: "examSite",
   },
   {
     title: "校区",
-    dataIndex: "c",
+    dataIndex: "campusCode",
   },
   {
     title: "考场号",
-    dataIndex: "d",
+    dataIndex: "examRoom",
   },
   {
     title: "扫描状态",
-    dataIndex: "e",
+    dataIndex: "scanned",
   },
 ];
+const exportLoading = ref(false);
+const exportFile = async () => {
+  if (exportLoading.value) return;
+  exportLoading.value = true;
+
+  downloadByApi(() => exportScanned(transParams.value))
+    .then(() => {
+      window.$message.success("导出成功!");
+    })
+    .catch((e: Error) => {
+      window.$message.error(e.message || "下载失败,请重新尝试!");
+    })
+    .finally(() => {
+      exportLoading.value = false;
+    });
+};
 </script>
 <style lang="less" scoped>
 .scan-check-miss {

+ 64 - 13
src/render/views/ScanManage/ScanProcess.vue

@@ -1,23 +1,69 @@
 <template>
   <div class="scan-process h-full">
-    <qm-low-form :params="searchParams" :fields="searchFields"></qm-low-form>
+    <qm-low-form :params="params" :fields="searchFields">
+      <template #subjectCode>
+        <SelectSubject
+          v-model="params.subjectCode"
+          :exam-id="userStore.curExam?.id as number"
+        ></SelectSubject>
+      </template>
+    </qm-low-form>
     <a-table :data-source="tableData" :columns="columns" size="middle" bordered>
+      <template #summary>
+        <a-table-summary-row>
+          <a-table-summary-cell>整体进度</a-table-summary-cell>
+          <a-table-summary-cell>
+            <span class="blue">{{ totals.progress }}</span>
+          </a-table-summary-cell>
+          <a-table-summary-cell>
+            <span class="blue">{{ totals.studentCount }}</span>
+          </a-table-summary-cell>
+          <a-table-summary-cell>
+            <span class="blue">{{ totals.scannedCount }}</span>
+          </a-table-summary-cell>
+          <a-table-summary-cell>
+            <span class="blue">{{ totals.unexistCount }}</span>
+          </a-table-summary-cell>
+          <a-table-summary-cell>
+            <span class="blue">{{ totals.estimation }}</span>
+          </a-table-summary-cell>
+        </a-table-summary-row>
+      </template>
     </a-table>
   </div>
 </template>
 <script name="ScanProcess" lang="ts" setup>
-import { ref, computed } from "vue";
+import { ref, reactive, onMounted } from "vue";
+import { scanProcessData } from "@/ap/baseDataConfig";
+import { useUserStore } from "@/store";
+import SelectSubject from "@/components/SelectSubject/index.vue";
 import type { TableColumnsType } from "@qmth/ui";
-//todo 入参名
-const searchParams = ref({
-  a: "",
+const userStore = useUserStore();
+const params = reactive({
+  subjectCode: "",
+});
+const tableData = ref([]);
+const totals = ref<any>({});
+const search = () => {
+  scanProcessData({
+    examId: userStore.curExam?.id,
+    subjectCode: params.subjectCode,
+  }).then((res: any) => {
+    console.log("mock 科目扫描进度查询", res);
+    totals.value = res?.total || {};
+    tableData.value = res?.subjects || [];
+  });
+};
+onMounted(() => {
+  search();
 });
 const searchFields = ref([
   {
-    prop: "a",
+    prop: "subjectCode",
     type: "select",
     colSpan: 4,
     label: "选择科目",
+    cell: "subjectCode",
   },
   {
     type: "buttons",
@@ -25,41 +71,46 @@ const searchFields = ref([
     children: [
       {
         text: "查询",
+        onClick: () => {
+          search();
+        },
       },
     ],
   },
 ]);
 
-const tableData = ref([{ a: 1, b: 2, c: 3 }]);
 const columns: TableColumnsType = [
   {
     title: "科目",
-    dataIndex: "a",
+    dataIndex: "subjectName",
   },
   {
     title: "进度",
-    dataIndex: "b",
+    dataIndex: "progress",
   },
   {
     title: "考生总数",
-    dataIndex: "c",
+    dataIndex: "studentCount",
   },
   {
     title: "已扫描",
-    dataIndex: "d",
+    dataIndex: "scannedCount",
   },
   {
     title: "未扫描",
-    dataIndex: "e",
+    dataIndex: "unexistCount",
   },
   {
     title: "预计剩余时间",
-    dataIndex: "f",
+    dataIndex: "estimation",
   },
 ];
 </script>
 <style lang="less" scoped>
 .scan-process {
   padding: 20px;
+  .blue {
+    color: #165dff;
+  }
 }
 </style>

+ 114 - 77
src/render/views/ScanManage/StuInfo.vue

@@ -1,117 +1,154 @@
 <template>
   <div class="stu-info h-full">
-    <qm-low-form
-      :params="searchParams"
-      :fields="searchFields"
-      :label-width="80"
-    ></qm-low-form>
-    <a-table :data-source="tableData" :columns="columns" size="middle" bordered>
+    <qm-low-form :params="params" :fields="fields" :label-width="80">
+      <template #subjectCode>
+        <SelectSubject
+          v-model="params.subjectCode"
+          :exam-id="userStore.curExam?.id as number"
+        ></SelectSubject>
+      </template>
+    </qm-low-form>
+    <a-table
+      :data-source="dataList"
+      :columns="columns"
+      size="middle"
+      bordered
+      :loading="loading"
+      :pagination="pagination"
+    >
     </a-table>
   </div>
 </template>
 <script name="StuInfo" lang="ts" setup>
-import { ref, computed } from "vue";
+import { ref, reactive, computed } from "vue";
+import { useUserStore } from "@/store";
+import { useRequest } from "vue-request";
+import useTable from "@/hooks/useTable";
+import SelectSubject from "@/components/SelectSubject/index.vue";
+import { getSiteList, getCampusList, getStuPage } from "@/ap/baseDataConfig";
 import type { TableColumnsType } from "@qmth/ui";
-//todo 入参名
-const searchParams = ref({
-  a: "",
-  b: "",
-  c: "",
-  d: "",
-  e: "",
-  f: "",
+const userStore = useUserStore();
+//考点下拉
+const { data: examSiteOptions, run: runSite } = useRequest(getSiteList);
+const { data: examCampusOptions, run: runCampus } = useRequest(getCampusList);
+runSite({ examId: userStore.curExam?.id });
+runCampus({ examId: userStore.curExam?.id });
+const params = reactive({
+  subjectCode: "",
+  examNumber: "",
+  name: "",
+  examSite: "",
+  campusCode: "",
+  packageCode: "",
 });
-const searchFields = ref([
-  {
-    prop: "a",
-    type: "select",
-    colSpan: 3,
-    label: "科目",
-  },
-  {
-    prop: "b",
-    type: "select",
-    colSpan: 3,
-    label: "准考证号",
-  },
-  {
-    prop: "c",
-    type: "select",
-    colSpan: 3,
-    label: "姓名",
-  },
-  {
-    prop: "d",
-    type: "select",
-    colSpan: 3,
-    label: "考点",
-  },
-  {
-    prop: "e",
-    type: "select",
-    colSpan: 3,
-    label: "校区",
-  },
-  {
-    prop: "f",
-    type: "select",
-    colSpan: 3,
-    label: "卷袋号",
-  },
-  {
-    type: "buttons",
-    colSpan: 5,
-    children: [
-      {
-        text: "查询",
+const transParams = computed(() => {
+  return { ...params, examId: userStore.curExam?.id };
+});
+const { dataList, pagination, loading, getList, toPage } = useTable(
+  getStuPage,
+  transParams,
+  true
+);
+const fields = computed(() => {
+  return [
+    {
+      cell: "subjectCode",
+      type: "select",
+      colSpan: 3,
+      label: "科目",
+    },
+    {
+      prop: "examNumber",
+      colSpan: 3,
+      label: "准考证号",
+    },
+    {
+      prop: "name",
+      colSpan: 3,
+      label: "姓名",
+    },
+    {
+      prop: "examSite",
+      type: "select",
+      colSpan: 3,
+      label: "考点",
+      attrs: {
+        options: examSiteOptions.value || [],
+        fieldNames: { label: "name", value: "code" },
       },
-      {
-        text: "导出",
-        attrs: {
-          type: "default",
-        },
+    },
+    {
+      prop: "campusCode",
+      type: "select",
+      colSpan: 3,
+      label: "校区",
+      attrs: {
+        options: examCampusOptions.value || [],
+        fieldNames: { label: "name", value: "code" },
       },
-    ],
-  },
-]);
+    },
+    {
+      prop: "packageCode",
+      colSpan: 3,
+      label: "卷袋号",
+    },
+    {
+      type: "buttons",
+      colSpan: 5,
+      children: [
+        {
+          text: "查询",
+          onClick: () => {
+            toPage(1);
+          },
+        },
+        {
+          text: "导出",
+          attrs: {
+            type: "default",
+          },
+        },
+      ],
+    },
+  ];
+});
 
-const tableData = ref([{ a: 1, b: 2, c: 3 }]);
 const columns: TableColumnsType = [
   {
     title: "科目",
-    dataIndex: "a",
+    dataIndex: "subjectCode",
   },
   {
     title: "准考证号",
-    dataIndex: "b",
+    dataIndex: "examNumber",
   },
   {
     title: "姓名",
-    dataIndex: "c",
+    dataIndex: "name",
   },
   {
     title: "考点",
-    dataIndex: "d",
+    dataIndex: "examSiteName",
   },
   {
     title: "校区",
-    dataIndex: "e",
+    dataIndex: "campusName",
   },
   {
-    title: "考场",
-    dataIndex: "f",
+    title: "考场",
+    dataIndex: "examRoom",
   },
   {
     title: "卷袋号",
-    dataIndex: "g",
+    dataIndex: "packageCode",
   },
   {
     title: "扫描状态",
-    dataIndex: "h",
+    dataIndex: "scanned",
   },
   {
     title: "扫描账号",
-    dataIndex: "i",
+    dataIndex: "device",
   },
 ];
 </script>

+ 83 - 56
src/render/views/ScanManage/WorkStatistics.vue

@@ -1,73 +1,85 @@
 <template>
   <div class="work-statistics h-full">
-    <qm-low-form
-      :params="searchParams"
-      :fields="searchFields"
-      :label-width="80"
-    ></qm-low-form>
+    <qm-low-form :params="params" :fields="fields" :label-width="80">
+      <template #timeArr>
+        <qm-date-range-picker
+          v-model="params.timeArr"
+          value-format="timestamp"
+        ></qm-date-range-picker>
+      </template>
+    </qm-low-form>
     <div class="chart-wrap">
       <vue-echarts :option="chartOptions" autoresize></vue-echarts>
     </div>
   </div>
 </template>
 <script name="WorkStatistics" lang="ts" setup>
-import { ref, computed } from "vue";
+import { ref, computed, reactive, onMounted } from "vue";
+import { getWorkStatistics, exportWorkStatistics } from "@/ap/baseDataConfig";
+import { useUserStore } from "@/store";
 import VueEcharts from "vue-echarts";
 import { graphic } from "echarts";
+import { downloadByApi } from "@/utils/download";
 
-//todo 入参名
-const searchParams = ref({
-  a: "",
-  b: "",
-  c: "",
+const userStore = useUserStore();
+const params = reactive({
+  timeArr: [],
 });
-const searchFields = ref([
-  {
-    prop: "a",
-    type: "date",
-    colSpan: 4,
-    label: "开始时间",
-    attrs: {
-      showTime: true,
-      format: "YYYY-MM-DD HH:mm:ss",
-    },
-  },
-  {
-    prop: "b",
-    type: "date",
-    colSpan: 4,
-    label: "结束时间",
-    attrs: {
-      showTime: true,
-      format: "YYYY-MM-DD HH:mm:ss",
-    },
-  },
-  {
-    prop: "c",
-    type: "select",
-    colSpan: 3,
-    label: "排序方式",
-  },
-  {
-    type: "buttons",
-    colSpan: 5,
-    children: [
-      {
-        text: "查询",
+const transParams = computed(() => {
+  return {
+    startTime: params.timeArr[0],
+    endTime: params.timeArr[1],
+    examId: userStore.curExam?.id,
+  };
+});
+const data = ref([]);
+const search = () => {
+  getWorkStatistics(transParams.value).then((res: any) => {
+    console.log("mock 扫描员工作量统计", res);
+    data.value = res || [];
+  });
+};
+onMounted(() => {
+  search();
+});
+const fields = computed(() => {
+  return [
+    {
+      cell: "timeArr",
+      type: "date",
+      label: "开始时间",
+      attrs: {
+        showTime: true,
+        format: "YYYY-MM-DD HH:mm:ss",
       },
-      {
-        text: "导出",
-        attrs: {
-          type: "default",
+    },
+    {
+      type: "buttons",
+      colSpan: 5,
+      children: [
+        {
+          text: "查询",
+          onClick: () => {
+            search();
+          },
         },
-      },
-    ],
-  },
-]);
+        {
+          text: "导出",
+          attrs: {
+            type: "default",
+            loading: exportLoading.value,
+          },
+          onClick: () => {
+            exportFile();
+          },
+        },
+      ],
+    },
+  ];
+});
 
 const chartOptions = computed(() => {
-  // const xData = result.value?.map((item) => item.markingGroupLeader) || []
-  const xData = ["scan1", "scan2", "scan3", "scan4", "scan5"];
+  const xData = data.value.map((item: any) => item.device);
   return {
     grid: {
       top: 50,
@@ -128,8 +140,7 @@ const chartOptions = computed(() => {
             ]),
           },
         },
-        // data: result.value?.map((item) => item.totalCount) || [],
-        data: [10, 30, 100, 70, 56],
+        data: data.value.map((item: any) => item.answerScanCount),
         label: {
           show: true,
           color: "#444",
@@ -146,6 +157,22 @@ const chartOptions = computed(() => {
     ],
   };
 });
+const exportLoading = ref(false);
+const exportFile = async () => {
+  if (exportLoading.value) return;
+  exportLoading.value = true;
+
+  downloadByApi(() => exportWorkStatistics(transParams.value))
+    .then(() => {
+      window.$message.success("导出成功!");
+    })
+    .catch((e: Error) => {
+      window.$message.error(e.message || "下载失败,请重新尝试!");
+    })
+    .finally(() => {
+      exportLoading.value = false;
+    });
+};
 </script>
 <style lang="less" scoped>
 .work-statistics {