刘洋 9 місяців тому
батько
коміт
4e69a1f9fc
35 змінених файлів з 1336 додано та 192 видалено
  1. 1 1
      package.json
  2. 83 0
      src/render/ap/baseDataConfig.ts
  3. 6 1
      src/render/ap/exam.ts
  4. 77 0
      src/render/ap/mock/baseDataConfig.ts
  5. 43 1
      src/render/ap/mock/exam.ts
  6. 2 0
      src/render/ap/mock/index.ts
  7. 32 0
      src/render/ap/mock/system.ts
  8. 14 0
      src/render/ap/system.ts
  9. 1 1
      src/render/components/FooterInfo/index.vue
  10. 116 0
      src/render/components/ImportButton/index.vue
  11. 3 1
      src/render/components/ImportDialog/index.vue
  12. 2 0
      src/render/components/register.ts
  13. 10 0
      src/render/constants/enums.ts
  14. 1 1
      src/render/hooks/useTable.ts
  15. 5 2
      src/render/store/modules/app/index.ts
  16. 1 1
      src/render/store/modules/user/index.ts
  17. 3 0
      src/render/styles/base.less
  18. 0 1
      src/render/utils/request.ts
  19. 8 0
      src/render/utils/tool.ts
  20. 42 15
      src/render/views/BaseDataConfig/AddUserDialog.vue
  21. 38 12
      src/render/views/BaseDataConfig/CardImport.vue
  22. 51 0
      src/render/views/BaseDataConfig/ImportCardDialog.vue
  23. 37 6
      src/render/views/BaseDataConfig/ResetPasswordDialog.vue
  24. 154 53
      src/render/views/BaseDataConfig/ScanParams.vue
  25. 75 16
      src/render/views/BaseDataConfig/UserManage.vue
  26. 35 8
      src/render/views/CurExam/AddExamDialog.vue
  27. 71 18
      src/render/views/CurExam/index.vue
  28. 37 13
      src/render/views/Login/AdminLogin.vue
  29. 18 14
      src/render/views/Login/index.vue
  30. 56 22
      src/render/views/ScanManage/ScanCheckMiss.vue
  31. 1 1
      src/render/views/ScanManage/ScanProcess.vue
  32. 122 0
      src/render/views/ScanManage/StuInfo.vue
  33. 157 0
      src/render/views/ScanManage/WorkStatistics.vue
  34. 29 4
      src/render/views/ScanManage/index.vue
  35. 5 0
      types/app.d.ts

+ 1 - 1
package.json

@@ -73,4 +73,4 @@
     "webpack-cli": "^5.1.4",
     "webpack-dev-server": "^4.15.1"
   }
-}
+}

+ 83 - 0
src/render/ap/baseDataConfig.ts

@@ -0,0 +1,83 @@
+import { request } from "@/utils/request";
+import { getFileMD5 } from "@/utils/crypto";
+import { obj2formData } from "@/utils/tool";
+
+export const addOrEditExam = (params: { id?: string | number; name: string }) =>
+  request({
+    url: "/api/admin/exam/save",
+    params,
+  });
+
+export const getBaseDataConfig = (params: { examId: any }) =>
+  request({
+    url: "/api/admin/exam/config/info",
+    params,
+  });
+export const saveBaseDataConfig = (data: {
+  examId: any;
+  paperTypeBarcodeContent: string[];
+  imageCheckRatio: string | number;
+  imageCheckOrder: string;
+  enableSyncVerify: boolean;
+  scannerAssignedMaxCount: string | number;
+  scannerAssignedVerifyPassword: string;
+}) =>
+  request({
+    url: "/api/admin/exam/config/save",
+    data,
+  });
+
+export const getUserList = (params: { role: string } & PageBaseParams) =>
+  request({
+    url: "/api/admin/user/page",
+    params,
+  });
+
+export const toggleUserStatus = (params: { userId: number; enable: boolean }) =>
+  request({
+    url: "/api/admin/user/toggle",
+    params,
+    loading: true,
+  });
+
+export const addUser = (params: {
+  role: string;
+  loginName: string;
+  password: string;
+}) =>
+  request({
+    url: "/api/admin/user/save",
+    params,
+    loading: true,
+  });
+
+export const resetUserPwd = (params: { userId: number; password: string }) =>
+  request({
+    url: "/api/admin/user/password/reset",
+    params,
+    loading: true,
+  });
+
+export const getCardList = (params: { examId: number } & PageBaseParams) =>
+  request({
+    url: "/api/admin/card/page",
+    params,
+  });
+
+export const importCard = async (params: {
+  examId: number;
+  subjectCode: string;
+  remark?: string;
+  file: File;
+}) => {
+  const md5 = await getFileMD5(params.file);
+  const formData = obj2formData(params);
+  return request({
+    url: "/api/admin/card/import",
+    data: formData,
+    headers: {
+      md5,
+      "Content-Type": "multipart/form-data",
+    },
+  });
+};

+ 6 - 1
src/render/ap/exam.ts

@@ -7,6 +7,11 @@ export const getExamList = (data: {
 }) =>
   request({
     url: "/api/admin/exam/list",
-    method: "post",
     data,
   });
+
+export const getExamOverview = (params: { examId: number }) =>
+  request({
+    url: "/api/admin/exam/overview",
+    params,
+  });

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

@@ -0,0 +1,77 @@
+import Mock from "mockjs";
+
+Mock.mock(/\/api\/admin\/exam\/save/, "post", {
+  updateTime: 1,
+});
+Mock.mock(/\/api\/admin\/exam\/config\/info/, "post", {
+  examId: 1,
+  //卷型条码内容
+  paperTypeBarcodeContent: [],
+  //图片抽查比例
+  imageCheckRatio: 5,
+  //图片抽查顺序
+  imageCheckOrder: "DESC", // DESC,ASC
+  //是否开启实时审核
+  enableSyncVerify: true,
+  //人工绑定锁屏数量
+  scannerAssignedMaxCount: 5,
+  //人工绑定解锁密码
+  scannerAssignedVerifyPassword: "",
+});
+Mock.mock(/\/api\/admin\/exam\/config\/save/, "post", {
+  updateTime: 1,
+});
+
+Mock.mock(/\/api\/admin\/user\/page/, "post", {
+  result: [
+    {
+      id: 1,
+      loginName: "aaa",
+      role: "SCHOOL_ADMIN",
+      roleName: "管理员a",
+      enable: true, //启用禁用
+    },
+    {
+      id: 2,
+      loginName: "bbb",
+      role: "SCHOOL_ADMIN",
+      roleName: "管理员b",
+      enable: false, //启用禁用
+    },
+    {
+      id: 3,
+      loginName: "ccc",
+      role: "AUDITOR",
+      roleName: "审核员",
+      enable: false, //启用禁用
+    },
+  ],
+  totalCount: 3,
+  pageCount: 1,
+});
+Mock.mock(/\/api\/admin\/user\/toggle/, "post", {
+  updateTime: 1,
+});
+Mock.mock(/\/api\/admin\/user\/save/, "post", {
+  updateTime: 1,
+});
+Mock.mock(/\/api\/admin\/user\/password\/reset/, "post", {
+  updateTime: 1,
+});
+Mock.mock(/\/api\/admin\/card\/page/, "post", {
+  result: [
+    {
+      examId: 1,
+      number: 666,
+      subjectCode: 1,
+      subjectName: "某科目名称",
+      parameter: "{a:123}",
+      paperCount: 2,
+      remark: "这是一串备注",
+      updateTime: 1726724528841,
+      url: "https://www.baidu.com",
+    },
+  ],
+  totalCount: 3,
+  pageCount: 1,
+});

+ 43 - 1
src/render/ap/mock/exam.ts

@@ -2,7 +2,7 @@ import Mock from "mockjs";
 Mock.mock("/api/admin/exam/list", "post", {
   result: [
     {
-      id: 123,
+      id: 1,
       schoolName: "学校名称",
       name: "考试名称",
       mode: "COMMON",
@@ -13,3 +13,45 @@ Mock.mock("/api/admin/exam/list", "post", {
   totalCount: 100,
   pageCount: 10,
 });
+
+Mock.mock("/api/admin/exam/overview?examId=1", "post", {
+  //基础数据
+  basic: {
+    subjectCount: 123,
+    cardCount: 2,
+    studentCount: 1,
+  },
+  //扫描管理
+  scan: {
+    scannedCount: 1,
+    unexistCount: 1,
+    scannedRate: "100%",
+    imageCheckCount: 1,
+    imageCheckTodoCount: 2,
+    imageCheckRate: "50%",
+  },
+  //复核校验
+  assignedCheck: {
+    auditorFinishCount: 1,
+    auditorTodoCount: 1,
+    auditorFinishRate: "50%",
+    adminFinishCount: 1,
+    adminTodoCount: 1,
+    adminFinishRate: "50%",
+  },
+  //识别对照
+  omr: {
+    suspectFinishCount: 1,
+    suspectTodoCount: 1,
+    suspectFinishRate: "50%",
+    customizeFinishCount: 1,
+    customizeTodoCount: 1,
+    customizeFinishRate: "50%",
+  },
+  //缺考校验
+  absentCheck: {
+    okCount: 1,
+    absentCount: 1,
+    todoCount: 1,
+  },
+});

+ 2 - 0
src/render/ap/mock/index.ts

@@ -1 +1,3 @@
 import "./exam.ts";
+import "./system.ts";
+import "./baseDataConfig.ts";

+ 32 - 0
src/render/ap/mock/system.ts

@@ -0,0 +1,32 @@
+import Mock from "mockjs";
+Mock.mock("/api/status", "post", {
+  //文件URI前缀
+  //最终完整URL为http://{host}:{port}/{fileUriPrefix}/{uri}
+  //{host}和{port}为当前配置的服务端地址,{uri}为接口返回的文件路径
+  fileUriPrefix: "cet",
+  version: "1.0.0",
+  time: 10,
+  //当前请求客户端ip地址
+  clientIP: "xxx",
+  //若没有客户端升级包,则此字段为null
+  client: {
+    version: "1.0.3",
+    uri: "xxxx.zip",
+    md5: "摘要信息",
+    updateTime: 123456789,
+  },
+  //当前系统模式:MARKINGCLOUD,STANDALONE
+  systemMode: "MARKINGCLOUD",
+});
+Mock.mock("/api/user/login?loginName=1&password=2", "post", {
+  role: "SCHOOL_ADMIN", //SCHOOL_ADMIN|SCAN_ADMIN|AUDITOR
+  name: "管理员",
+  sessionId: "abcdefg123456",
+  accessToken: "xxxxxxoooooo",
+});
+Mock.mock("/api/user/login?loginName=2&password=2", "post", {
+  role: "SCAN_ADMIN", //SCHOOL_ADMIN|SCAN_ADMIN|AUDITOR
+  name: "审核员",
+  sessionId: "abcdefg123456",
+  accessToken: "xxxxxxoooooo",
+});

+ 14 - 0
src/render/ap/system.ts

@@ -0,0 +1,14 @@
+import { request } from "@/utils/request";
+
+export const getServerStatus = () =>
+  request({
+    url: "/api/status",
+    loading: true,
+  });
+
+export const adminLogin = (params: { loginName: string; password: string }) =>
+  request({
+    url: "/api/user/login",
+    params,
+    loading: true,
+  });

+ 1 - 1
src/render/components/FooterInfo/index.vue

@@ -47,7 +47,7 @@ const appStore = useAppStore();
 const timeStr = ref("");
 const renderTimeStr = () => {
   timeStr.value = dateFormat(
-    Date.now() + appStore.timeDiff,
+    Date.now() + (appStore.serverStatus?.time || 0),
     "yyyy-MM-dd HH:mm:ss"
   );
 };

+ 116 - 0
src/render/components/ImportButton/index.vue

@@ -0,0 +1,116 @@
+<template>
+  <a-upload
+    ref="uploadBtn"
+    :headers="headers"
+    :custom-request="customRequest"
+    :disabled="disabled || loading"
+    :before-upload="beforeUpload"
+    :file-list="fileList"
+    @change="fileChange"
+  >
+    <qm-button :loading="loading">
+      <template #icon><ImportOutlined /></template>{{ btnText }}
+    </qm-button>
+  </a-upload>
+</template>
+<script name="ImportButton" lang="ts" setup>
+import { ref, isRef, Ref, Reactive } from "vue";
+import type { UploadProps } from "@qmth/ui";
+import { getFileMD5 } from "@/utils/crypto";
+import { request } from "@/utils/request";
+import { ImportOutlined } from "@ant-design/icons-vue";
+import type { AxiosError, AxiosProgressEvent } from "axios";
+
+const emit = defineEmits(["success", "fileChange"]);
+const props = withDefaults(
+  defineProps<
+    {
+      maxSize?: number;
+      disabled?: boolean;
+      fileParamsName?: string;
+      btnText?: string;
+      uploadParams?: Record<string, any> | Ref | Reactive<any>;
+    } & UploadProps
+  >(),
+  {
+    maxSize: 20 * 1024 * 1024,
+    fileParamsName: "file",
+    disabled: false,
+    btnText: "点击导入",
+    uploadParams: () => ({}),
+  }
+);
+const fileList = ref<UploadProps["fileList"]>([]);
+const uploadBtn = ref();
+const headers = ref<any>({ "Content-Type": "multipart/form-data", md5: "" });
+const loading = ref(false);
+const uploadProgress = ref(0);
+
+const beforeUpload: UploadProps["beforeUpload"] = async (file) => {
+  if (file.size > props.maxSize) {
+    window.$message.error("文件大小不能大于20M!");
+    return false;
+  }
+  if (!props.action) {
+    // fileList.value = [...(fileList.value || []), file];
+    fileList.value = [file]; //暂时只考虑单文件上传
+    return false;
+  }
+  const md5 = await getFileMD5(file);
+  headers.value.md5 = md5;
+
+  return true;
+};
+
+const customRequest: UploadProps["customRequest"] | undefined = !props.action
+  ? undefined
+  : (option) => {
+      const { file, data } = option;
+      const formData = new FormData();
+      const paramData: Record<string, any> = data || {};
+      Object.entries(paramData).forEach(([k, v]) => {
+        formData.append(k, v);
+      });
+      formData.append(props.fileParamsName, file as File);
+
+      request({
+        url: option.action as string,
+        data: formData,
+        headers: option.headers,
+        onUploadProgress: (data: AxiosProgressEvent) => {
+          uploadProgress.value = data.total
+            ? Math.floor((100 * data.loaded) / data.total)
+            : 0;
+        },
+      })
+        .then((res: any) => {
+          // 所有excel导入的特殊处理
+          if (res.hasError) {
+            const failRecords = res.failRecords;
+            const message = failRecords
+              .map((item: any) => `第${item.lineNum}行:${item.msg}`)
+              .join("。");
+
+            window.$message.error(message);
+            return;
+          }
+          emit("success");
+        })
+        .catch((error: AxiosError<{ message: string }> | null) => {
+          window.$message.error(error?.response?.data?.message || "上传失败!");
+        });
+    };
+
+const fileChange = ({ file }: any) => {
+  //   const formData = new FormData();
+  //   const paramData: any = isRef(props.uploadParams)
+  //     ? { ...(props.uploadParams?.value || {}) }
+  //     : { ...props.uploadParams };
+  //   Object.keys(paramData).forEach((key: string) => {
+  //     formData.append(key, paramData[key]);
+  //   });
+  //   formData.append(props.fileParamsName, file as File);
+  emit("fileChange", file as File);
+};
+</script>
+<style lang="less" scoped></style>

+ 3 - 1
src/render/components/ImportDialog/index.vue

@@ -38,7 +38,7 @@
           </p>
         </template>
       </a-form-item>
-      <a-form-item label="下载模板">
+      <a-form-item label="下载模板" v-if="showDownload">
         <a-button :loading="downloading" @click="onDownloadTemplate">
           <template #icon><DownloadOutlined /></template>{{ downloadBtnTitle }}
         </a-button>
@@ -97,6 +97,7 @@ const props = withDefaults(
     downloadFilename?: string;
     downloadHandle?: PromiseFunc;
     beforeSubmitHandle?: PromiseFunc;
+    showDownload?: boolean;
   }>(),
   {
     title: "文件上传",
@@ -110,6 +111,7 @@ const props = withDefaults(
     autoUpload: true,
     disabled: false,
     downloadBtnTitle: "导入模板",
+    showDownload: true,
   }
 );
 

+ 2 - 0
src/render/components/register.ts

@@ -24,6 +24,7 @@ import FooterInfo from "./FooterInfo/index.vue";
 import SelectCourse from "./SelectCourse/index.vue";
 import MyQuote from "./MyQuote/index.vue";
 import Accordion from "./Accordion/index.vue";
+import ImportButton from "./ImportButton/index.vue";
 
 use([
   CanvasRenderer,
@@ -50,5 +51,6 @@ export default {
     Vue.component("SelectCourse", SelectCourse);
     Vue.component("MyQuote", MyQuote);
     Vue.component("Accordion", Accordion);
+    Vue.component("ImportButton", ImportButton);
   },
 };

+ 10 - 0
src/render/constants/enums.ts

@@ -0,0 +1,10 @@
+export const enum2Options = (obj: Record<string, any>) => {
+  return Object.keys(obj).map((k) => {
+    return { value: k, label: obj[k] };
+  });
+};
+
+export const ROLES = {
+  SCHOOL_ADMIN: "管理员",
+  AUDITOR: "审核员",
+};

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

@@ -6,7 +6,7 @@ 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>,
-  initAutoFetch = false
+  initAutoFetch = true
 ) {
   const pageNumber = ref(1);
   const pageSize = ref(10);

+ 5 - 2
src/render/store/modules/app/index.ts

@@ -4,16 +4,19 @@ import { defineStore } from "pinia";
 export const useAppStore = defineStore<"app", any, any, any>("app", {
   persist: {
     storage: sessionStorage,
-    paths: ["timeDiff"],
+    paths: ["serverStatus"],
   },
   state: () => ({
-    timeDiff: 0,
+    serverStatus: null,
     loadingStatus: {
       spinning: false,
       tip: "",
     },
   }),
   actions: {
+    setServerStatus(obj: any) {
+      this.serverStatus = obj;
+    },
     setState(data: any) {
       this.$patch(data);
     },

+ 1 - 1
src/render/store/modules/user/index.ts

@@ -3,7 +3,7 @@ import router from "@/router";
 
 export const useUserStore = defineStore<
   "user",
-  { curExam: Exam; imageCheckLoopTime: number },
+  { curExam: Exam | null; imageCheckLoopTime: number; userInfo: any },
   any,
   any
 >("user", {

+ 3 - 0
src/render/styles/base.less

@@ -33,3 +33,6 @@ body {
 .page-box {
   padding: 20px;
 }
+.qm-low-form .ant-form-item {
+  margin-bottom: 24px !important;
+}

+ 0 - 1
src/render/utils/request.ts

@@ -51,7 +51,6 @@ function createService() {
   // HTTP request 拦截器
   service.interceptors.request.use(
     (config: any) => {
-      console.log("request config", config);
       setAuth(config);
       const appStore = useAppStore();
       if (config.loading) {

+ 8 - 0
src/render/utils/tool.ts

@@ -316,3 +316,11 @@ export function minNum(dataList: number[]): number {
   if (!dataList.length) return 0;
   return Math.min.apply(null, dataList);
 }
+
+export const obj2formData = (obj: Record<string, any>): FormData => {
+  const formData = new FormData();
+  Object.keys(obj).forEach((key: string) => {
+    formData.append(key, obj[key]);
+  });
+  return formData;
+};

+ 42 - 15
src/render/views/BaseDataConfig/AddUserDialog.vue

@@ -1,42 +1,69 @@
 <template>
-  <my-modal v-model:open="visible" title="新增用户">
-    <qm-low-form :params="params" :fields="fields" :label-width="80">
+  <my-modal
+    v-model:open="visible"
+    title="新增用户"
+    :width="400"
+    @ok="submitHandle"
+  >
+    <qm-low-form
+      :params="params"
+      :fields="fields"
+      :label-width="80"
+      :rules="rules"
+      ref="form"
+    >
     </qm-low-form>
   </my-modal>
 </template>
 <script name="AddUserDialog" lang="ts" setup>
-import { ref } from "vue";
+import { ref, reactive } from "vue";
+import { ROLES, enum2Options } from "@/constants/enums";
+import { addUser } from "@/ap/baseDataConfig";
+
+const form = ref();
 const visible = defineModel();
+const emit = defineEmits(["success"]);
 
-//todo 入参名
-const params = ref({
-  a: "1",
-  b: "",
-  c: "",
+const params = reactive({
+  role: "SCHOOL_ADMIN",
+  loginName: "",
+  password: "",
 });
 const fields = ref([
   {
-    prop: "a",
+    prop: "role",
     label: "角色",
     colSpan: 24,
     type: "radio",
     attrs: {
-      options: [
-        { label: "管理员", value: "1" },
-        { label: "审核员", value: "2" },
-      ],
+      options: enum2Options(ROLES),
     },
   },
   {
-    prop: "b",
+    prop: "loginName",
     label: "账号",
     colSpan: 24,
   },
   {
-    prop: "c",
+    prop: "password",
     label: "初始密码",
     colSpan: 24,
+    type: "password",
   },
 ]);
+const rules = {
+  loginName: [{ required: true, message: "请输入账号" }],
+  password: [{ required: true, message: "请输入初始密码" }],
+};
+const submitHandle = () => {
+  form.value.formRef.validate().then(() => {
+    addUser(params).then((res: any) => {
+      console.log("mock 新建用户", res);
+      window.$message.success("操作成功");
+      visible.value = false;
+      emit("success");
+    });
+  });
+};
 </script>
 <style lang="less" scoped></style>

+ 38 - 12
src/render/views/BaseDataConfig/CardImport.vue

@@ -1,7 +1,15 @@
 <template>
   <div class="card-import">
-    <qm-low-form :fields="fields"> </qm-low-form>
-    <a-table :data-source="tableData" :columns="columns" size="middle" bordered>
+    <qm-low-form :fields="fields"></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.key === 'operation'">
           <qm-button type="link" @click="editRow(record)">修改</qm-button>
@@ -14,13 +22,24 @@
       v-if="showAddDialog"
       :cur-row="curRow"
     ></AddCardDialog>
+    <ImportCardDialog
+      v-model="showImportCardDialog"
+      v-if="showImportCardDialog"
+    ></ImportCardDialog>
   </div>
 </template>
 <script name="CardImport" lang="ts" setup>
-import { ref } from "vue";
+import { ref, computed, reactive } from "vue";
 import AddCardDialog from "./AddCardDialog.vue";
 import type { TableColumnsType } from "@qmth/ui";
+import useTable from "@/hooks/useTable";
+import { getCardList } from "@/ap/baseDataConfig";
+import { useUserStore } from "@/store";
+import ImportCardDialog from "./ImportCardDialog.vue";
+
+const userStore = useUserStore();
 
+const showImportCardDialog = ref(false);
 const showAddDialog = ref(false);
 const curRow = ref(null);
 const editRow = (row: any) => {
@@ -35,6 +54,9 @@ const fields = ref([
         text: "导入卡格式",
         attrs: {
           style: { marginLeft: 0 },
+          onClick: () => {
+            showImportCardDialog.value = true;
+          },
         },
       },
       {
@@ -49,34 +71,38 @@ const fields = ref([
     ],
   },
 ]);
-const tableData = ref([{ a: 1, b: 2, c: 3, d: 4, e: 5 }]);
+
+const { dataList, pagination, loading, getList, toPage } = useTable(
+  getCardList,
+  { examId: userStore.curExam?.id },
+  true
+);
 const columns: TableColumnsType = [
   {
-    title: "序号",
-    dataIndex: "index",
+    title: "号",
+    dataIndex: "number",
     key: "index",
     width: 100,
-    customRender: ({ index }) => `${index + 1}`,
   },
   {
     title: "科目",
-    dataIndex: "a",
+    dataIndex: "subjectName",
   },
   {
     title: "属性",
-    dataIndex: "b",
+    dataIndex: "parameter",
   },
   {
     title: "张数",
-    dataIndex: "c",
+    dataIndex: "paperCount",
   },
   {
     title: "备注",
-    dataIndex: "d",
+    dataIndex: "remark",
   },
   {
     title: "更新时间",
-    dataIndex: "e",
+    dataIndex: "updateTime",
   },
   {
     title: "操作",

+ 51 - 0
src/render/views/BaseDataConfig/ImportCardDialog.vue

@@ -0,0 +1,51 @@
+<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>
+    </qm-low-form>
+  </my-modal>
+</template>
+<script name="ImportCardDialog" lang="ts" setup>
+import { ref, reactive } from "vue";
+import { importCard } from "@/ap/baseDataConfig";
+
+const form = ref();
+const visible = defineModel();
+const emit = defineEmits(["success"]);
+
+const params = reactive({
+  subjectCode: "",
+  remark: "",
+  file: null,
+});
+const fields = ref([
+  { prop: "subjectCode", cell: "subjectCode", label: "科目题卡", colSpan: 24 },
+  { prop: "remark", type: "textarea", label: "备注", colSpan: 24 },
+  { prop: "file", cell: "file", label: "选择卡格式", colSpan: 24 },
+]);
+
+const rules = {
+  subjectCode: [{ required: true, message: "请选择科目" }],
+  file: [{ required: true, message: "请上传文件" }],
+};
+const getFile = (file: any) => {
+  params.file = file;
+};
+const submitHandle = () => {
+  form.value.formRef.validate().then(() => {});
+};
+</script>
+<style lang="less" scoped></style>

+ 37 - 6
src/render/views/BaseDataConfig/ResetPasswordDialog.vue

@@ -1,27 +1,58 @@
 <template>
-  <my-modal v-model:open="visible" title="重置密码" :width="450">
-    <qm-low-form :params="params" :fields="fields" :label-width="80">
+  <my-modal
+    v-model:open="visible"
+    title="重置密码"
+    :width="400"
+    @ok="submitHandle"
+  >
+    <qm-low-form
+      :params="params"
+      :fields="fields"
+      :label-width="80"
+      :rules="rules"
+      ref="form"
+    >
     </qm-low-form>
   </my-modal>
 </template>
 <script name="ResetPasswordDialog" lang="ts" setup>
 import { ref } from "vue";
 import { setValueFromObj } from "@/utils/tool";
-const visible = defineModel();
+import { resetUserPwd } from "@/ap/baseDataConfig";
+
+const form = ref();
 
+const visible = defineModel();
+const emit = defineEmits(["success"]);
 const props = defineProps<{ curRow: any }>();
-//todo 入参名
 const params = ref({
-  a: "",
+  password: "",
 });
 params.value = setValueFromObj(params.value, props.curRow);
 const fields = ref([
   {
-    prop: "a",
+    prop: "password",
     label: "新密码",
     colSpan: 24,
     type: "password",
   },
 ]);
+const rules = {
+  password: [{ required: true, message: "请输入初始密码" }],
+};
+
+const submitHandle = () => {
+  form.value.formRef.validate().then(() => {
+    resetUserPwd({
+      password: params.value.password,
+      userId: props.curRow?.id,
+    }).then((res: any) => {
+      console.log("mock 新建用户", res);
+      window.$message.success("操作成功");
+      visible.value = false;
+      emit("success");
+    });
+  });
+};
 </script>
 <style lang="less" scoped></style>

+ 154 - 53
src/render/views/BaseDataConfig/ScanParams.vue

@@ -4,20 +4,32 @@
       <qm-low-form
         :fields="fields"
         :params="params"
-        :label-width="150"
         labelAlign="left"
+        ref="form"
+        :rules="rules"
       >
-        <template #a>
+        <template #label1></template>
+        <template #paperTypeBarcodeContentItem>
           <div class="flex items-center">
-            <a-input style="width: 200px" v-model:value="params.a" />
-            <qm-button class="m-l-8px" :icon="h(PlusCircleOutlined)"
+            <a-input
+              style="width: 200px"
+              v-model:value="params.paperTypeBarcodeContentItem"
+            />
+            <qm-button
+              class="m-l-8px"
+              :icon="h(PlusCircleOutlined)"
+              @click="addTag"
+              :disabled="!params.paperTypeBarcodeContentItem"
               >新增</qm-button
             >
           </div>
-          <div class="p-t-10px p-b-10px">
+          <div
+            class="p-t-10px p-b-10px"
+            v-if="!!params.paperTypeBarcodeContent.length"
+          >
             <div
               class="tag"
-              v-for="(item, index) in tagList"
+              v-for="(item, index) in params.paperTypeBarcodeContent"
               :key="item"
               :style="{ color: token.colorPrimary }"
             >
@@ -26,92 +38,181 @@
             </div>
           </div>
         </template>
-
-        <template #b>
+        <template #label2></template>
+        <template #imageCheckRatio>
           <div class="flex items-center">
-            <div class="flex items-center m-r-50px ccbl">
-              <span>抽查比例:</span>
-              <a-input-number v-model:value="params.b" :max="100" :min="0" />
-              <span>%</span>
-            </div>
-            <div class="flex items-center jcsx">
-              <span>检查顺序:</span>
-              <a-select v-mode:value="params.c" style="width: 200px"></a-select>
-            </div>
+            <a-input-number
+              v-model:value="params.imageCheckRatio"
+              :max="100"
+              :min="0"
+            />
+            <span>%</span>
           </div>
         </template>
-        <template #e>
+        <template #scannerAssignedMaxCount>
           <div class="flex items-center">
-            <div class="flex items-center m-r-50px ccbl">
-              <span>锁屏数量:</span>
-              <a-input-number v-model:value="params.e" :max="100" :min="0" />
-              <span>张</span>
-            </div>
-            <div class="flex items-center jcsx">
-              <span>解锁密码:</span>
-              <a-input-password
-                v-mode:value="params.f"
-                style="width: 200px"
-              ></a-input-password>
-            </div>
+            <a-input-number
+              v-model:value="params.scannerAssignedMaxCount"
+              :max="100"
+              :min="0"
+            />
+            <span>张</span>
           </div>
         </template>
       </qm-low-form>
 
       <div class="btns">
         <!-- <qm-button :icon="h(EditOutlined)">编辑</qm-button> -->
-        <qm-button type="primary">保存</qm-button>
+        <qm-button type="primary" @click="save">保存</qm-button>
       </div>
     </div>
   </div>
 </template>
 <script name="ScanParams" lang="ts" setup>
-import { ref, h } from "vue";
+import { ref, h, onMounted, reactive } from "vue";
 import { PlusCircleOutlined } from "@ant-design/icons-vue";
 import useToken from "@/hooks/useToken";
 import { EditOutlined } from "@ant-design/icons-vue";
+import { getBaseDataConfig, saveBaseDataConfig } from "@/ap/baseDataConfig";
+import { useUserStore } from "@/store";
+
+const form = ref();
+
+const userStore = useUserStore();
 const { token } = useToken();
-//todo 入参名
-const params = ref({
-  a: "",
-  b: "",
-  c: "",
-  d: "1",
-  e: "",
-  f: "",
+const getData = () => {
+  getBaseDataConfig({ examId: userStore.curExam?.id }).then((res: any) => {
+    params.paperTypeBarcodeContent = res.paperTypeBarcodeContent || [];
+    params.imageCheckRatio = res.imageCheckRatio;
+    params.imageCheckOrder = res.imageCheckOrder;
+    params.enableSyncVerify = res.enableSyncVerify;
+    params.scannerAssignedMaxCount = res.scannerAssignedMaxCount;
+    params.scannerAssignedVerifyPassword = res.scannerAssignedVerifyPassword;
+  });
+};
+onMounted(() => {
+  getData();
+});
+const params = reactive<any>({
+  paperTypeBarcodeContentItem: "",
+  paperTypeBarcodeContent: [],
+  imageCheckRatio: "",
+  imageCheckOrder: "",
+  enableSyncVerify: true,
+  scannerAssignedMaxCount: "",
+  scannerAssignedVerifyPassword: "",
 });
 const fields = ref([
   {
-    cell: "a",
+    cell: "label1",
     label: "卷型条码内容",
-    colSpan: 24,
+    colSpan: 6,
+    labelWidth: 130,
+  },
+  {
+    prop: "paperTypeBarcodeContentItem",
+    cell: "paperTypeBarcodeContentItem",
+    label: "",
+    labelWidth: 0,
+    colSpan: 18,
   },
   {
-    cell: "b",
+    cell: "label2",
     label: "图片检查",
-    colSpan: 24,
+    colSpan: 6,
+    labelWidth: 130,
+  },
+  {
+    prop: "imageCheckRatio",
+    cell: "imageCheckRatio",
+    label: "抽查比例",
+    colSpan: 9,
+  },
+  {
+    prop: "imageCheckOrder",
+    type: "select",
+    attrs: {
+      options: [
+        { value: "DESC", label: "最新扫描批次" },
+        { value: "ASC", label: "批次顺序" },
+      ],
+    },
+    label: "检查顺序",
+    colSpan: 9,
   },
   {
-    prop: "d",
+    cell: "label3",
     label: "实时审核",
-    colSpan: 24,
+    colSpan: 6,
+    labelWidth: 130,
+  },
+  {
+    prop: "enableSyncVerify",
+    label: "",
+    colSpan: 18,
     type: "radio",
     attrs: {
       options: [
-        { label: "是", value: "1" },
-        { label: "否", value: "0" },
+        { label: "是", value: true },
+        { label: "否", value: false },
       ],
     },
   },
   {
-    cell: "e",
+    cell: "label3",
     label: "人工绑定锁屏控制",
-    colSpan: 24,
+    colSpan: 6,
+    labelWidth: 130,
+  },
+  {
+    prop: "scannerAssignedMaxCount",
+    cell: "scannerAssignedMaxCount",
+    label: "锁屏数量",
+    colSpan: 9,
+  },
+  {
+    prop: "scannerAssignedVerifyPassword",
+    type: "password",
+    colSpan: 9,
+    label: "解锁密码",
   },
 ]);
-const tagList = ref(["666666", "888888", "999999"]);
+
+const addTag = () => {
+  params.paperTypeBarcodeContent.push(params.paperTypeBarcodeContentItem);
+  params.paperTypeBarcodeContentItem = "";
+};
 const delTag = (index: number) => {
-  tagList.value.splice(index, 1);
+  params.paperTypeBarcodeContent.splice(index, 1);
+};
+
+const rules = {
+  paperTypeBarcodeContentItem: [
+    {
+      validator: async () => {
+        if (!params.paperTypeBarcodeContent.length) {
+          return Promise.reject("卷型条码内容至少一条");
+        } else {
+          return Promise.resolve();
+        }
+      },
+    },
+  ],
+  imageCheckRatio: [{ required: true, message: "请输入抽查比例" }],
+  scannerAssignedMaxCount: [{ required: true, message: "请输入锁屏数量" }],
+  scannerAssignedVerifyPassword: [
+    { required: true, message: "请输入解锁密码" },
+  ],
+};
+const save = () => {
+  form.value.formRef.validate().then(() => {
+    let p: any = { ...params };
+    delete p.paperTypeBarcodeContentItem;
+    saveBaseDataConfig(p).then((res: any) => {
+      console.log("mock 保存基础数据配置", res);
+      window.$message.success("保存成功");
+    });
+  });
 };
 </script>
 <style lang="less" scoped>
@@ -139,7 +240,7 @@ const delTag = (index: number) => {
       height: 32px;
       padding: 0 10px;
       border-radius: 16px;
-      line-height: 32px;
+      line-height: 30px;
       background: #e8f3ff;
       display: inline-block;
       margin-right: 8px;

+ 75 - 16
src/render/views/BaseDataConfig/UserManage.vue

@@ -1,38 +1,74 @@
 <template>
   <div class="user-manage">
-    <qm-low-form :params="searchParams" :fields="searchFields"></qm-low-form>
-    <a-table :data-source="tableData" :columns="columns" size="middle" bordered>
+    <qm-low-form :params="params" :fields="fields"></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.key === 'enable'">
+          <a-tag :bordered="false" color="success" v-if="record[column.key]"
+            >启用</a-tag
+          >
+          <a-tag :bordered="false" color="error" v-if="!record[column.key]"
+            >禁用</a-tag
+          >
+        </template>
         <template v-if="column.key === 'operation'">
-          <qm-button type="link">启用</qm-button>
-          <qm-button type="link">禁用</qm-button>
+          <qm-button
+            type="link"
+            :disabled="record.enable"
+            @click="toggleStatus(record, true)"
+            >启用</qm-button
+          >
+          <qm-button
+            type="link"
+            :disabled="!record.enable"
+            @click="toggleStatus(record, false)"
+            >禁用</qm-button
+          >
           <qm-button type="link" @click="resetPwd(record)">重置密码</qm-button>
         </template>
       </template>
     </a-table>
-    <AddUserDialog v-model="showAddDialog" v-if="showAddDialog"></AddUserDialog>
+    <AddUserDialog
+      v-model="showAddDialog"
+      v-if="showAddDialog"
+      @success="toPage(1)"
+    ></AddUserDialog>
     <ResetPasswordDialog
       v-model="showResetPwdDialog"
       v-if="showResetPwdDialog"
       :cur-row="curRow"
+      @success="toPage(1)"
     ></ResetPasswordDialog>
   </div>
 </template>
 <script name="UserManage" lang="ts" setup>
-import { ref } from "vue";
+import { ref, reactive } from "vue";
 import AddUserDialog from "./AddUserDialog.vue";
 import ResetPasswordDialog from "./ResetPasswordDialog.vue";
 import type { TableColumnsType } from "@qmth/ui";
-//todo 入参名
-const searchParams = ref({
-  a: "",
+import useTable from "@/hooks/useTable";
+import { getUserList } from "@/ap/baseDataConfig";
+import { toggleUserStatus } from "@/ap/baseDataConfig";
+import { ROLES, enum2Options } from "@/constants/enums";
+const params = reactive({
+  role: "",
 });
-const searchFields = ref([
+const fields = ref([
   {
-    prop: "a",
+    prop: "role",
     type: "select",
     colSpan: 3,
     label: "角色",
+    attrs: {
+      options: enum2Options(ROLES),
+    },
   },
   {
     type: "buttons",
@@ -43,6 +79,9 @@ const searchFields = ref([
           type: "default",
         },
         text: "查询",
+        onClick: () => {
+          toPage(1);
+        },
       },
       {
         text: "新增用户",
@@ -53,8 +92,11 @@ const searchFields = ref([
     ],
   },
 ]);
-
-const tableData = ref([{ a: 1, b: 2, c: 3 }]);
+const { dataList, pagination, loading, getList, toPage } = useTable(
+  getUserList,
+  params,
+  true
+);
 const columns: TableColumnsType = [
   {
     title: "序号",
@@ -65,15 +107,16 @@ const columns: TableColumnsType = [
   },
   {
     title: "角色",
-    dataIndex: "a",
+    dataIndex: "roleName",
   },
   {
     title: "账号",
-    dataIndex: "b",
+    dataIndex: "loginName",
   },
   {
     title: "状态",
-    dataIndex: "c",
+    dataIndex: "enable",
+    key: "enable",
   },
   {
     title: "操作",
@@ -89,6 +132,22 @@ const resetPwd = (record: any) => {
   curRow.value = record;
   showResetPwdDialog.value = true;
 };
+const toggleStatus = (row: any, status: boolean) => {
+  let str = status ? "启用" : "禁用";
+  window.$confirm({
+    title: "系统提示",
+    content: `确认${str}该用户吗?`,
+    onOk() {
+      toggleUserStatus({ userId: row.id, enable: status }).then((res: any) => {
+        console.log("mock 启用/禁用用户", res);
+        window.$message.success("操作成功");
+      });
+    },
+    onCancel() {
+      console.log("Cancel");
+    },
+  });
+};
 </script>
 <style lang="less" scoped>
 .user-manage {

+ 35 - 8
src/render/views/CurExam/AddExamDialog.vue

@@ -1,24 +1,51 @@
 <template>
-  <my-modal v-model:open="visible" title="新建考试">
-    <qm-low-form :params="params" :fields="fields" :label-width="80">
-      <template #b> CET </template>
+  <my-modal
+    v-model:open="visible"
+    :title="IS_EDIT ? '编辑' : '新建' + '考试'"
+    @ok="handleOk"
+  >
+    <qm-low-form
+      :params="params"
+      :fields="fields"
+      :label-width="80"
+      :rules="rules"
+      ref="form"
+    >
+      <template #mode> CET </template>
     </qm-low-form>
   </my-modal>
 </template>
 <script name="AddExam" lang="ts" setup>
-import { ref } from "vue";
+import { ref, computed } from "vue";
 import { setValueFromObj } from "@/utils/tool";
+import { addOrEditExam } from "@/ap/baseDataConfig";
+
+const form = ref();
+const IS_EDIT = computed(() => !!props.curRow?.id);
 const visible = defineModel();
 const props = defineProps<{ curRow: any }>();
 
-//todo 入参名
+const rules = {
+  name: [{ required: true, message: "请输入考试名称" }],
+};
 const params = ref({
-  a: "",
+  name: "",
 });
 params.value = setValueFromObj(params.value, props.curRow);
 const fields = ref([
-  { prop: "a", label: "名称", colSpan: 24 },
-  { colSpan: 24, cell: "b", label: "扫描模式" },
+  { prop: "name", label: "名称", colSpan: 24 },
+  { colSpan: 24, cell: "mode", label: "扫描模式" },
 ]);
+const handleOk = () => {
+  form.value.formRef.validate().then(() => {
+    addOrEditExam({ name: params.value.name, id: props.curRow?.id }).then(
+      (res: any) => {
+        console.log("mock 创建或编辑考试:", res);
+        window.$message.success("操作成功");
+        visible.value = false;
+      }
+    );
+  });
+};
 </script>
 <style lang="less" scoped></style>

+ 71 - 18
src/render/views/CurExam/index.vue

@@ -59,9 +59,13 @@
               </div>
             </div>
             <div class="body">
-              <div class="option">科目数量:</div>
-              <div class="option">卡格式:</div>
-              <div class="option">考生人数:</div>
+              <div class="option">
+                科目数量:{{ allData.basic?.subjectCount }}
+              </div>
+              <div class="option">卡格式:{{ allData.basic?.cardCount }}</div>
+              <div class="option">
+                考生人数:{{ allData.basic?.studentCount }}
+              </div>
             </div>
           </div>
           <div class="module">
@@ -84,11 +88,19 @@
             <div class="body">
               <div class="option">
                 扫描进度:
-                <p>已扫描:,未扫描:,完成比:0%</p>
+                <p>
+                  已扫描:{{ allData.scan?.scannedCount }},未扫描:{{
+                    allData.scan?.unexistCount
+                  }},完成比:{{ allData.scan?.scannedRate }}
+                </p>
               </div>
               <div class="option">
                 图片审核:
-                <p>已扫描:,未扫描:,完成比:0%</p>
+                <p>
+                  已扫描:{{ allData.scan?.imageCheckCount }},未扫描:{{
+                    allData.scan?.imageCheckTodoCount
+                  }},完成比:{{ allData.scan?.imageCheckRate }}
+                </p>
               </div>
             </div>
           </div>
@@ -112,11 +124,23 @@
             <div class="body">
               <div class="option">
                 审核员
-                <p>已完成:,待完成:,完成比:0%</p>
+                <p>
+                  已完成:{{
+                    allData.assignedCheck?.auditorFinishCount
+                  }},待完成:{{
+                    allData.assignedCheck?.auditorTodoCount
+                  }},完成比:{{ allData.assignedCheck?.auditorFinishRate }}
+                </p>
               </div>
               <div class="option">
                 管理员
-                <p>已完成:,待完成:,完成比:0%</p>
+                <p>
+                  已完成:{{
+                    allData.assignedCheck?.adminFinishCount
+                  }},待完成:{{
+                    allData.assignedCheck?.adminTodoCount
+                  }},完成比:{{ allData.assignedCheck?.adminFinishRate }}
+                </p>
               </div>
             </div>
           </div>
@@ -140,11 +164,19 @@
             <div class="body">
               <div class="option">
                 识别嫌疑
-                <p>待处理:,已处理:,完成比:0%</p>
+                <p>
+                  待处理:{{ allData.omr?.suspectTodoCount }},已处理:{{
+                    allData.omr?.suspectFinishCount
+                  }},完成比:{{ allData.omr?.suspectFinishRate }}
+                </p>
               </div>
               <div class="option">
                 自定义
-                <p>待处理:,已处理:,完成比:0%</p>
+                <p>
+                  待处理:{{ allData.omr?.customizeTodoCount }},已处理:{{
+                    allData.omr?.customizeFinishCount
+                  }},完成比:{{ allData.omr?.customizeFinishRate }}
+                </p>
                 <p>待生成:</p>
               </div>
             </div>
@@ -168,9 +200,15 @@
               </div>
             </div>
             <div class="body">
-              <div class="option">正常数量:</div>
-              <div class="option">缺考:</div>
-              <div class="option">缺考待确认数量:</div>
+              <div class="option">
+                正常数量:{{ allData.absentCheck?.okCount }}
+              </div>
+              <div class="option">
+                缺考:{{ allData.absentCheck?.absentCount }}
+              </div>
+              <div class="option">
+                缺考待确认数量:{{ allData.absentCheck?.todoCount }}
+              </div>
             </div>
           </div>
           <div class="module">
@@ -271,6 +309,7 @@ import {
   PlusCircleOutlined,
   RightOutlined,
 } from "@ant-design/icons-vue";
+import { getExamOverview } from "@/ap/exam";
 import useToken from "@/hooks/useToken";
 
 const { token } = useToken();
@@ -292,20 +331,32 @@ const curExam = computed(() => {
   return userStore.curExam;
 });
 const choosedExamId = ref();
-watch(curExam, (exam: Exam) => {
-  choosedExamId.value = exam.id;
+const allData = ref<any>({});
+const getAllCardData = () => {
+  getExamOverview({ examId: choosedExamId.value }).then((res: any) => {
+    console.log("mock 考试概览数据:", res);
+    allData.value = res || {};
+  });
+};
+if (curExam.value?.id) {
+  choosedExamId.value = curExam.value.id;
+  getAllCardData();
+}
+watch(curExam, (exam: Exam | null) => {
+  choosedExamId.value = exam?.id;
+});
+
+watch(choosedExamId, (val: number | string) => {
+  getAllCardData();
 });
 const examList = ref<Exam[]>([]);
 const showExamListModal = ref(false);
 const _getExamList = () => {
   getExamList({ enable: true, pageNumber: 1, pageSize: 10000 }).then(
     (res: any) => {
+      console.log("mock 考试列表:", res);
       if (res?.result?.length) {
-        console.log("考试列表:", res.result);
         examList.value = res.result || [];
-        if (showExamListModal.value) {
-          choosedExamId.value = res.result[0]?.id;
-        }
       } else {
         examList.value = [];
       }
@@ -385,6 +436,7 @@ watch(showExamListModal, (val: boolean) => {
           padding: 15px;
           .body {
             padding: 20px 4px;
+            height: 170px;
             .option {
               position: relative;
               color: @text-color1;
@@ -447,6 +499,7 @@ watch(showExamListModal, (val: boolean) => {
   .operate-box {
     height: 54px;
     padding: 0 16px;
+    border-bottom: 1px solid #e5e6eb;
     .lf {
       .no {
         min-width: 70px;

+ 37 - 13
src/render/views/Login/AdminLogin.vue

@@ -5,7 +5,9 @@
     <qm-low-form
       :fields="fields"
       :params="params"
-      :labelWidth="1"
+      :labelWidth="20"
+      :rules="rules"
+      ref="loginForm"
     ></qm-low-form>
     <div class="text-center">
       <qm-button type="link" @click="emit('toIndex', 2)"
@@ -25,13 +27,22 @@
 import { reactive, ref, h } from "vue";
 import { useRouter } from "vue-router";
 import { GlobalOutlined } from "@ant-design/icons-vue";
+import { adminLogin } from "@/ap/system";
+import { useUserStore } from "@/store";
+
+const userStore = useUserStore();
+
+const loginForm = ref();
 const emit = defineEmits(["toIndex"]);
-//todo 入参名非正式
-const params = reactive({ a: "" });
+const params = reactive({ loginName: "", password: "" });
 const router = useRouter();
+const rules = {
+  loginName: [{ required: true, message: "请输入用户名", trigger: "change" }],
+  password: [{ required: true, message: "请输入密码", trigger: "change" }],
+};
 const fields = ref([
   {
-    prop: "a",
+    prop: "loginName",
     colSpan: 24,
     attrs: {
       placeholder: "输入你的账号",
@@ -40,7 +51,7 @@ const fields = ref([
     },
   },
   {
-    prop: "b",
+    prop: "password",
     type: "password",
     colSpan: 24,
     attrs: {
@@ -56,14 +67,27 @@ const fields = ref([
       block: true,
       size: "large",
       onClick: () => {
-        //todo模拟登录请求...
-        if (params.a === "audit") {
-          router.push({ name: "Audit" });
-          window.electronApi.changeWinSize("big");
-          return;
-        }
-        router.push({ name: "CurExam" });
-        window.electronApi.changeWinSize("big");
+        loginForm.value.formRef
+          .validate()
+          .then(() => {
+            adminLogin(params).then((res: any) => {
+              console.log("mock 登录:", res);
+              userStore.setUserInfo(res);
+              let routeName =
+                res.role === "SCHOOL_ADMIN"
+                  ? "CurExam"
+                  : res.role === "SCAN_ADMIN"
+                  ? "Audit"
+                  : "";
+              if (routeName) {
+                router.push({ name: routeName });
+                window.electronApi.changeWinSize("big");
+              }
+            });
+          })
+          .catch((error: any) => {
+            console.log("验证不通过", error);
+          });
       },
     },
   },

+ 18 - 14
src/render/views/Login/index.vue

@@ -6,10 +6,7 @@
       <div class="right-box h-full">
         <IpSet v-if="curStepIndex == 0" @next="toNext"></IpSet>
 
-        <EnvCheck
-          v-if="curStepIndex == 1"
-          @mounted="checkEnvHandler"
-        ></EnvCheck>
+        <EnvCheck v-if="curStepIndex == 1" @mounted="checkEnvHandle"></EnvCheck>
         <LoginWays v-if="curStepIndex == 2" @next="toNext"> </LoginWays>
         <AdminLogin v-if="curStepIndex == 3" @to-index="toIndex"> </AdminLogin>
       </div>
@@ -24,6 +21,11 @@ import IpSet from "./IpSet.vue";
 import LoginWays from "./LoginWays.vue";
 import AdminLogin from "./AdminLogin.vue";
 import { local } from "@/utils/tool";
+import { useAppStore } from "@/store";
+import { getServerStatus } from "@/ap/system";
+
+const appStore = useAppStore();
+
 const curStepIndex = ref(0);
 const toNext = () => {
   curStepIndex.value++;
@@ -37,17 +39,19 @@ onMounted(() => {
   }
 });
 const envCheckLoading = ref(false);
-const checkEnvHandler = () => {
-  //todo 环境检测,发起请求
+const checkEnvHandle = () => {
   envCheckLoading.value = true;
-  //先定时器模拟请求
-  setTimeout(() => {
-    envCheckLoading.value = false;
-    curStepIndex.value++;
-
-    //todo如果请求异常,则清除本地local里的baseUrl
-    //...
-  }, 1000);
+  getServerStatus()
+    .then((res: any) => {
+      console.log("mock 环境检测:", res);
+      appStore.setServerStatus(res);
+      envCheckLoading.value = false;
+      curStepIndex.value++;
+    })
+    .catch(() => {
+      //todo 真实接口下,解除该注释
+      // local.remove("baseUrl");
+    });
 };
 const willClose = () => {
   if (curStepIndex.value == 1 && envCheckLoading.value) {

+ 56 - 22
src/render/views/ScanManage/ScanCheckMiss.vue

@@ -1,23 +1,62 @@
 <template>
-  <div class="scan-process h-full">
-    <qm-low-form :params="searchParams" :fields="searchFields"></qm-low-form>
+  <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>
     </a-table>
   </div>
 </template>
-<script name="ScanProcess" lang="ts" setup>
+<script name="ScanCheckMiss" lang="ts" setup>
 import { ref, computed } from "vue";
 import type { TableColumnsType } from "@qmth/ui";
 //todo 入参名
 const searchParams = ref({
   a: "",
+  b: "",
+  c: "",
+  d: "",
+  e: "",
+  f: "",
 });
 const searchFields = ref([
   {
     prop: "a",
     type: "select",
-    colSpan: 4,
-    label: "选择科目",
+    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",
@@ -26,6 +65,12 @@ const searchFields = ref([
       {
         text: "查询",
       },
+      {
+        text: "导出",
+        attrs: {
+          type: "default",
+        },
+      },
     ],
   },
 ]);
@@ -37,36 +82,25 @@ const columns: TableColumnsType = [
     dataIndex: "a",
   },
   {
-    title: "进度",
+    title: "考点",
     dataIndex: "b",
   },
   {
-    title: "考生总数",
+    title: "校区",
     dataIndex: "c",
   },
   {
-    title: "已扫描",
+    title: "考场号",
     dataIndex: "d",
   },
   {
-    title: "扫描",
+    title: "扫描状态",
     dataIndex: "e",
   },
-  {
-    title: "预计剩余时间",
-    dataIndex: "f",
-  },
 ];
 </script>
 <style lang="less" scoped>
-.image-view {
-  .image-wrap {
-    height: 100%;
-    border-left: 1px solid #e5e6eb;
-    border-right: 1px solid #e5e6eb;
-    background: #eceef1;
-    margin-left: 284px;
-    margin-right: 400px;
-  }
+.scan-check-miss {
+  padding: 20px;
 }
 </style>

+ 1 - 1
src/render/views/ScanManage/ScanProcess.vue

@@ -59,7 +59,7 @@ const columns: TableColumnsType = [
 ];
 </script>
 <style lang="less" scoped>
-.image-view {
+.scan-process {
   padding: 20px;
 }
 </style>

+ 122 - 0
src/render/views/ScanManage/StuInfo.vue

@@ -0,0 +1,122 @@
+<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>
+    </a-table>
+  </div>
+</template>
+<script name="StuInfo" lang="ts" setup>
+import { ref, computed } from "vue";
+import type { TableColumnsType } from "@qmth/ui";
+//todo 入参名
+const searchParams = ref({
+  a: "",
+  b: "",
+  c: "",
+  d: "",
+  e: "",
+  f: "",
+});
+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: "查询",
+      },
+      {
+        text: "导出",
+        attrs: {
+          type: "default",
+        },
+      },
+    ],
+  },
+]);
+
+const tableData = ref([{ a: 1, b: 2, c: 3 }]);
+const columns: TableColumnsType = [
+  {
+    title: "科目",
+    dataIndex: "a",
+  },
+  {
+    title: "准考证号",
+    dataIndex: "b",
+  },
+  {
+    title: "姓名",
+    dataIndex: "c",
+  },
+  {
+    title: "考点",
+    dataIndex: "d",
+  },
+  {
+    title: "校区",
+    dataIndex: "e",
+  },
+  {
+    title: "考场号",
+    dataIndex: "f",
+  },
+  {
+    title: "卷袋号",
+    dataIndex: "g",
+  },
+  {
+    title: "扫描状态",
+    dataIndex: "h",
+  },
+  {
+    title: "扫描账号",
+    dataIndex: "i",
+  },
+];
+</script>
+<style lang="less" scoped>
+.stu-info {
+  padding: 20px;
+}
+</style>

+ 157 - 0
src/render/views/ScanManage/WorkStatistics.vue

@@ -0,0 +1,157 @@
+<template>
+  <div class="work-statistics h-full">
+    <qm-low-form
+      :params="searchParams"
+      :fields="searchFields"
+      :label-width="80"
+    ></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 VueEcharts from "vue-echarts";
+import { graphic } from "echarts";
+
+//todo 入参名
+const searchParams = ref({
+  a: "",
+  b: "",
+  c: "",
+});
+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: "查询",
+      },
+      {
+        text: "导出",
+        attrs: {
+          type: "default",
+        },
+      },
+    ],
+  },
+]);
+
+const chartOptions = computed(() => {
+  // const xData = result.value?.map((item) => item.markingGroupLeader) || []
+  const xData = ["scan1", "scan2", "scan3", "scan4", "scan5"];
+  return {
+    grid: {
+      top: 50,
+      bottom: 15,
+      left: 30,
+      right: 30,
+      containLabel: true,
+    },
+    legend: {
+      top: 10,
+      itemWidth: 14,
+      data: ["扫描量"],
+    },
+    tooltip: {
+      trigger: "item",
+      triggerOn: "mousemove",
+    },
+    xAxis: {
+      axisLine: { show: false },
+      axisTick: { show: false },
+      splitLine: { show: false },
+      axisLabel: {
+        rotate: 30,
+        // interval: 0,
+      },
+      data: xData,
+    },
+    yAxis: {
+      axisTick: { show: false },
+      type: "value",
+    },
+    dataZoom: [
+      {
+        show: xData.length > 20,
+        realtime: true,
+        height: 20,
+        bottom: 10,
+        start: 0,
+        end: 2000 / xData.length,
+        brushSelect: false,
+      },
+    ],
+    series: [
+      {
+        name: "扫描量",
+        type: "bar",
+        itemStyle: {
+          normal: {
+            color: new graphic.LinearGradient(0, 0, 0, 1, [
+              {
+                offset: 0,
+                color: "#4080ff",
+              },
+              {
+                offset: 1,
+                color: "#C6E1FF",
+              },
+            ]),
+          },
+        },
+        // data: result.value?.map((item) => item.totalCount) || [],
+        data: [10, 30, 100, 70, 56],
+        label: {
+          show: true,
+          color: "#444",
+          fontSize: 10,
+          position: "top",
+          formatter({ value }: { value: number }) {
+            return value > 0 ? `${value}` : "";
+          },
+        },
+        barWidth: xData.length <= 20 ? "20" : "16",
+        barCategoryGap: "50%",
+        barGap: "0%",
+      },
+    ],
+  };
+});
+</script>
+<style lang="less" scoped>
+.work-statistics {
+  padding: 20px;
+  .chart-wrap {
+    height: calc(100% - 50px);
+  }
+}
+</style>

+ 29 - 4
src/render/views/ScanManage/index.vue

@@ -7,9 +7,15 @@
       <a-tab-pane key="2" tab="扫描进度"
         ><ScanProcess></ScanProcess>
       </a-tab-pane>
-      <a-tab-pane key="3" tab="工作量统计"> </a-tab-pane>
-      <a-tab-pane key="4" tab="扫描查漏"> </a-tab-pane>
-      <a-tab-pane key="5" tab="考生信息"> </a-tab-pane>
+      <a-tab-pane key="3" tab="工作量统计"
+        ><WorkStatistics></WorkStatistics>
+      </a-tab-pane>
+      <a-tab-pane key="4" tab="扫描查漏">
+        <ScanCheckMiss></ScanCheckMiss>
+      </a-tab-pane>
+      <a-tab-pane key="5" tab="考生信息">
+        <StuInfo></StuInfo>
+      </a-tab-pane>
     </a-tabs>
   </div>
 </template>
@@ -17,6 +23,25 @@
 import { ref } from "vue";
 import ImageView from "./ImageView.vue";
 import ScanProcess from "./ScanProcess.vue";
+import ScanCheckMiss from "./ScanCheckMiss.vue";
+import WorkStatistics from "./WorkStatistics.vue";
+import StuInfo from "./StuInfo.vue";
 const activeKey = ref("1");
 </script>
-<style lang="less" scoped></style>
+<style lang="less" scoped>
+.scan-manage {
+  :deep(.ant-tabs) {
+    height: 100%;
+    .ant-tabs-nav {
+      padding: 8px 15px 0 15px;
+      margin-bottom: 0;
+    }
+    .ant-tabs-content-holder {
+      overflow: auto;
+      .ant-tabs-content {
+        height: 100%;
+      }
+    }
+  }
+}
+</style>

+ 5 - 0
types/app.d.ts

@@ -6,3 +6,8 @@ interface Exam {
   schoolName: string;
   updateTime: number;
 }
+
+interface PageBaseParams {
+  pageNumber: number;
+  pageSize: number;
+}