刘洋 преди 9 месеца
родител
ревизия
effbd7ba1a

+ 1 - 1
src/render/ap/mock/baseDataConfig.ts

@@ -142,7 +142,7 @@ Mock.mock(/\/api\/admin\/scanner\/workload/, "post", [
     answerScanCount: 100,
   },
 ]);
-Mock.mock(/\/api\/admin\/exam_site\/list/, "post", [
+Mock.mock(/\/api\/admin\/exam-site\/list/, "post", [
   {
     code: "1",
     name: "考点一",

+ 12 - 1
src/render/ap/scanManage.ts

@@ -43,6 +43,17 @@ export const batchStudentList = (params: { batchId: any }) =>
     params,
   });
 
+//查询考生答题卡扫描详情
+export const getStuCardDetail = (params: {
+  batchId: number | string;
+  studentId: number | string | undefined;
+}) =>
+  request({
+    url: "/api/admin/student/answer",
+    params,
+    loading: true,
+  });
+
 export const scanProcessData = (params: {
   examId: number | undefined;
   subjectCode: string;
@@ -75,7 +86,7 @@ export const exportWorkStatistics = (params: {
 
 export const getSiteList = (params: { examId: number | undefined }) =>
   request({
-    url: "/api/admin/exam_site/list",
+    url: "/api/admin/exam-site/list",
     params,
   });
 

+ 2 - 2
src/render/components/Accordion/index.vue

@@ -91,11 +91,12 @@ watch(
       border-bottom: 1px solid #e5e5e5;
       background: linear-gradient(180deg, #ffffff 0%, #f2f3f5 100%);
       padding: 0 16px;
+      cursor: pointer;
       .title-box {
         .title {
           color: @text-color1;
           font-size: 14px;
-          font-weight: 500;
+          font-weight: bold;
         }
       }
       .collapse-icon {
@@ -108,7 +109,6 @@ watch(
       }
     }
     .body {
-      background-color: red;
       transition: all 0.2s;
       overflow: hidden;
     }

+ 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.serverStatus?.time || 0),
+    Date.now() + (Number(appStore.timeDiff) || 0),
     "yyyy-MM-dd HH:mm:ss"
   );
 };

+ 123 - 0
src/render/components/VerticalDateRange/index.vue

@@ -0,0 +1,123 @@
+<template>
+  <div class="vertical-date-range flex items-center justify-between">
+    <div>
+      <div class="flex items-center">
+        <span class="label">开始时间:</span>
+        <date-picker
+          v-model:value="startTime"
+          placeholder="开始时间"
+          :show-time="showTimeStart"
+          :size="props.size || 'large'"
+          :locale="locale"
+          :disabled-date="disabledDateStart"
+          :show-now="false"
+        ></date-picker>
+      </div>
+      <div class="flex items-center mt-8px">
+        <span class="label">结束时间:</span>
+        <date-picker
+          v-model:value="endTime"
+          placeholder="结束时间"
+          :show-time="showTimeEnd"
+          :size="props.size || 'large'"
+          :locale="locale"
+          :disabled-date="disabledDateEnd"
+          :show-now="false"
+        ></date-picker>
+      </div>
+    </div>
+    <qm-button style="height: 100%" type="primary">搜索</qm-button>
+  </div>
+</template>
+<script lang="ts" name="VerticalDateRange" setup>
+import { DatePicker, FormItemRest } from "ant-design-vue";
+import locale from "ant-design-vue/es/date-picker/locale/zh_CN";
+import dayjs from "dayjs";
+import "dayjs/locale/zh-cn";
+import { ref, watch } from "vue";
+import { useVModel } from "@/hooks/useVModal";
+
+const emit = defineEmits(["change"]);
+const props = withDefaults(
+  defineProps<{
+    modelValue: any;
+    size?: "large" | "middle" | "small";
+    splitText?: string;
+    valueFormat?: "timestamp" | string;
+    type?: "date" | "datetime";
+  }>(),
+  {
+    modelValue: () => [],
+    size: "middle",
+    splitText: "至",
+    // valueFormat: "YYYYMMDDHHmmss",
+    valueFormat: "timestamp",
+    type: "datetime",
+  }
+);
+const time = useVModel(props);
+const _time = ref([]);
+
+const startTime = ref();
+const endTime = ref();
+const showTimeStart =
+  props.type === "datetime"
+    ? { defaultValue: dayjs("00:00:00", "HH:mm:ss") }
+    : undefined;
+const showTimeEnd =
+  props.type === "datetime"
+    ? { defaultValue: dayjs("23:59:59", "HH:mm:ss") }
+    : undefined;
+
+watch([startTime, endTime], (arr: any) => {
+  _time.value = arr.map((item: any) => (item ? item.toDate() : undefined));
+  time.value = arr.map((item: any) =>
+    item
+      ? props.valueFormat === "timestamp"
+        ? props.type === "datetime"
+          ? item.toDate().getTime()
+          : item.startOf("date").toDate().getTime()
+        : props.type === "datetime"
+        ? item.format(props.valueFormat)
+        : item.endOf("date").format(props.valueFormat)
+      : undefined
+  );
+  emit("change", [...time.value]);
+});
+const disabledDateStart = (current: any) => {
+  if (!_time.value[0] && !_time.value[1]) {
+    return false;
+  } else {
+    if (_time.value[1]) {
+      return current > _time.value[1];
+    } else {
+      return false;
+    }
+  }
+};
+const disabledDateEnd = (current: any) => {
+  if (!_time.value[0] && !_time.value[1]) {
+    return false;
+  } else {
+    if (_time.value[0]) {
+      return current < _time.value[0];
+    } else {
+      return false;
+    }
+  }
+};
+</script>
+<style lang="less" scoped>
+.vertical-date-range {
+  width: 100%;
+  span.label {
+    color: @text-color2;
+  }
+}
+.split-txt {
+  margin-left: 11px;
+  margin-right: 11px;
+  font-size: 14px;
+  color: rgba(0, 0, 0, 0.88);
+}
+</style>

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

@@ -8,3 +8,7 @@ export const ROLES = {
   SCHOOL_ADMIN: "管理员",
   AUDITOR: "审核员",
 };
+export const IMAGE_TYPE = {
+  ORIGIN: "原图",
+  SLICE: "裁切图",
+};

+ 28 - 0
src/render/hooks/useVModal.ts

@@ -0,0 +1,28 @@
+import { getCurrentInstance, ref, watch } from "vue";
+
+export const useVModel = <T extends Record<string, any>, K extends keyof T>(
+  props: T,
+  key: K = "modelValue" as K
+) => {
+  const instance = getCurrentInstance();
+  const prop = ref<T[K]>(props[key]);
+  const event = `update:${key as string}`;
+  if (instance) {
+    watch(
+      () => props[key],
+      () => {
+        prop.value = props[key];
+      },
+      { deep: true }
+    );
+    const emits = instance.emit;
+    watch(
+      prop,
+      (v) => {
+        emits(event, v);
+      },
+      { deep: true }
+    );
+  }
+  return prop;
+};

+ 3 - 1
src/render/store/modules/app/index.ts

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

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

@@ -22,7 +22,6 @@ function initSyncTime(serverTime: number, localTime = Date.now()) {
 }
 
 function fetchTime() {
-  console.log("time diff", initServerTime - initLocalTime);
   return Date.now() + (initServerTime - initLocalTime || 0);
 }
 

+ 7 - 1
src/render/views/DataCheck/QuestionPanel.vue

@@ -24,7 +24,11 @@
           </a-button>
         </template>
       </a-descriptions-item>
-      <a-descriptions-item v-if="!simple" label="缺考" :span="4">
+      <a-descriptions-item
+        v-if="!simple && !hideMissExamRadio"
+        label="缺考"
+        :span="4"
+      >
         <a-radio-group
           v-model:value="examStatus"
           name="examStatus"
@@ -86,10 +90,12 @@ const props = withDefaults(
     questions: string[];
     info: QuestionInfo;
     simple: boolean;
+    hideMissExamRadio?: boolean;
   }>(),
   {
     questions: () => [],
     simple: false,
+    hideMissExamRadio: false,
   }
 );
 const emit = defineEmits(["update:questions", "change", "examStatusChange"]);

+ 25 - 21
src/render/views/Login/AdminLogin.vue

@@ -8,6 +8,7 @@
       :labelWidth="20"
       :rules="rules"
       ref="loginForm"
+      :search="loginHandle"
     ></qm-low-form>
     <div class="text-center">
       <qm-button type="link" @click="emit('toIndex', 2)"
@@ -40,6 +41,29 @@ const rules = {
   loginName: [{ required: true, message: "请输入用户名", trigger: "change" }],
   password: [{ required: true, message: "请输入密码", trigger: "change" }],
 };
+const loginHandle = () => {
+  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);
+    });
+};
 const fields = ref([
   {
     prop: "loginName",
@@ -67,27 +91,7 @@ const fields = ref([
       block: true,
       size: "large",
       onClick: () => {
-        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);
-          });
+        loginHandle();
       },
     },
   },

+ 17 - 2
src/render/views/Login/LoginWays.vue

@@ -34,18 +34,26 @@
         <footer-info></footer-info>
       </div>
     </div>
+    <qm-button
+      :icon="h(GlobalOutlined)"
+      class="reset-btn"
+      @click="emit('toIndex', 0)"
+    >
+      连接设置</qm-button
+    >
   </div>
 </template>
 <script name="LoginWays" lang="ts" setup>
-import { ref } from "vue";
+import { ref, h } from "vue";
 import {
   CheckCircleFilled,
   CloseCircleFilled,
   SwapRightOutlined,
+  GlobalOutlined,
 } from "@ant-design/icons-vue";
 import FooterInfo from "@/components/FooterInfo/index.vue";
 
-const emit = defineEmits(["next"]);
+const emit = defineEmits(["next", "toIndex"]);
 const activeName = ref("scan");
 
 const nextStep = () => {
@@ -60,6 +68,13 @@ const nextStep = () => {
 <style lang="less" scoped>
 .login-ways {
   padding-top: 120px;
+  position: relative;
+  :deep(.reset-btn) {
+    position: absolute;
+    top: 25px;
+    right: 65px;
+    z-index: 1;
+  }
   .level2 {
     .text-right {
       padding-right: 24px;

+ 2 - 1
src/render/views/Login/index.vue

@@ -7,7 +7,8 @@
         <IpSet v-if="curStepIndex == 0" @next="toNext"></IpSet>
 
         <EnvCheck v-if="curStepIndex == 1" @mounted="checkEnvHandle"></EnvCheck>
-        <LoginWays v-if="curStepIndex == 2" @next="toNext"> </LoginWays>
+        <LoginWays v-if="curStepIndex == 2" @next="toNext" @to-index="toIndex">
+        </LoginWays>
         <AdminLogin v-if="curStepIndex == 3" @to-index="toIndex"> </AdminLogin>
       </div>
     </div>

+ 166 - 29
src/render/views/ScanManage/ImageView.vue

@@ -24,6 +24,8 @@
             @click="toggleListType('level3', 3)"
             >批次</span
           >
+          <i>&gt;</i>
+          <span :class="{ active: chooseLevel >= 4 }">考生</span>
         </div>
       </div>
       <div class="bottom">
@@ -40,40 +42,93 @@
     </div>
     <div class="info-wrap">
       <Accordion :arr="arr">
-        <template #one>11111</template>
+        <template #one>
+          <div class="flex accordion-body">
+            <VerticalDateRange v-model="timeArr" />
+          </div>
+        </template>
+        <template #two>
+          <div class="accordion-body">
+            <a-radio-group
+              v-model:value="imageType"
+              @change="onImageTypeChange"
+            >
+              <a-radio
+                v-for="item in imageTypeOptions"
+                :key="item.value"
+                :value="item.value"
+                >{{ item.label }}</a-radio
+              >
+            </a-radio-group>
+          </div>
+        </template>
+        <template #three>
+          <div class="accordion-body">
+            <QuestionPanel
+              v-model:questions="questions"
+              :info="questionInfo"
+              :simple="isSliceImage"
+              @change="onQuestionsChange"
+              :hideMissExamRadio="true"
+            />
+          </div>
+        </template>
       </Accordion>
     </div>
-    <div class="image-wrap"></div>
+    <div class="image-wrap">
+      <ScanImage
+        v-if="dataCheckStore.curPage && isOriginImage"
+        @prev="onPrevPage"
+        @next="onNextPage"
+      />
+      <SliceImage v-if="dataCheckStore.curPage && !isOriginImage" />
+    </div>
   </div>
 </template>
 <script name="ImageView" lang="ts" setup>
-import { ref, computed, onMounted, reactive } from "vue";
-import { useRequest } from "vue-request";
-import { useUserStore } from "@/store";
+import { ref, computed, onMounted, watch } from "vue";
+import VerticalDateRange from "@/components/VerticalDateRange/index.vue";
+import { useUserStore, useDataCheckStore } from "@/store";
+import { IMAGE_TYPE, enum2Options } from "@/constants/enums";
+import QuestionPanel from "../DataCheck/QuestionPanel.vue";
+import ScanImage from "../DataCheck/ScanImage/index.vue";
+import SliceImage from "../DataCheck/SliceImage/index.vue";
 import {
   batchSubjectList,
   batchDeviceList,
   batchList,
   batchStudentList,
+  getStuCardDetail,
 } from "@/ap/scanManage";
 
+const imageTypeOptions = enum2Options(IMAGE_TYPE);
+const dataCheckStore = useDataCheckStore();
+function onImageTypeChange() {
+  dataCheckStore.setInfo({
+    imageType: imageType.value,
+  });
+}
+
+const timeArr = ref([]);
+const timeParams = computed<any>(() => {
+  return { startTime: timeArr.value[0], endTime: timeArr.value[1] };
+});
 const userStore = useUserStore();
 const examId = computed(() => userStore.curExam?.id);
-// const { runAsync: run1 } = useRequest(batchSubjectList);
-// const { runAsync: run2 } = useRequest(batchDeviceList);
-// const { runAsync: run3 } = useRequest(batchList);
-// const { runAsync: run4 } = useRequest(batchStudentList);
 const _batchSubjectList = () => {
-  batchSubjectList({ examId: examId.value }).then((res: any) => {
-    leftList.value = res || [];
-    listType.value = "level1";
-  });
+  batchSubjectList({ examId: examId.value, ...timeParams.value }).then(
+    (res: any) => {
+      leftList.value = res || [];
+      listType.value = "level1";
+    }
+  );
 };
 
 const _batchDeviceList = () => {
   batchDeviceList({
     examId: examId.value,
     subjectCode: curSubject.value.value,
+    ...timeParams.value,
   }).then((res: any) => {
     leftList.value = res || [];
     listType.value = "level2";
@@ -84,22 +139,43 @@ const _batchList = () => {
     examId: examId.value,
     subjectCode: curSubject.value.value,
     device: curDevice.value.value,
+    ...timeParams.value,
   }).then((res: any) => {
     leftList.value = res || [];
     listType.value = "level3";
   });
 };
 const _batchStudentList = () => {
-  batchStudentList({ batchId: curSubject.value.value }).then((res: any) => {
+  batchStudentList({ batchId: curBatch.value.value }).then((res: any) => {
     leftList.value = res || [];
     listType.value = "level4";
   });
 };
+const _getStuCardDetail = () => {
+  getStuCardDetail({
+    batchId: curBatch.value.value,
+    studentId: curStu.value.studentId,
+  }).then((res: any) => {
+    curStuCardData.value = res || {};
+    dataCheckStore.setInfo({
+      curPage: curStuCardData.value as any,
+      curPageIndex: -1,
+    });
+
+    if (!dataCheckStore.curPage) return;
+    dataCheckStore.setInfo({ curStudent: curStuCardData.value as any });
+  });
+};
+
 onMounted(() => {
   _batchSubjectList();
 });
 
-const arr = ref([{ title: "搜索条件", name: "one", open: true }]);
+const arr = ref([
+  { title: "搜索条件", name: "one", open: true },
+  { title: "图片类别", name: "two", open: true },
+  { title: "题卡信息", name: "three", open: true },
+]);
 
 const listType = ref("level1");
 const fieldNames = computed(() => {
@@ -124,6 +200,12 @@ const leftList = ref<any>([]);
 const curSubject = ref({ label: "", value: "" });
 const curDevice = ref({ label: "", value: "" });
 const curBatch = ref({ label: "", value: "" });
+const curStu = ref({
+  examNumber: "",
+  studentId: void 0,
+  studentName: "",
+});
+const curStuCardData = ref({});
 
 const chooseLeft = (item: any, index: number) => {
   activeIndex.value = index;
@@ -136,6 +218,13 @@ const chooseLeft = (item: any, index: number) => {
   } else if (listType.value === "level3") {
     curBatch.value = { value: item.batchId, label: item.batchId };
     _batchStudentList();
+  } else if (listType.value === "level4") {
+    curStu.value = {
+      examNumber: item.examNumber,
+      studentId: item.studentId,
+      studentName: item.studentName,
+    };
+    _getStuCardDetail();
   }
 };
 const toggleListType = (type: string, num: number) => {
@@ -143,8 +232,8 @@ const toggleListType = (type: string, num: number) => {
     return;
   }
   const clearObj = { value: "", label: "" };
+  leftList.value = [];
   if (type === "level1") {
-    leftList.value = [];
     curSubject.value = { ...clearObj };
     curDevice.value = { ...clearObj };
     curBatch.value = { ...clearObj };
@@ -153,23 +242,66 @@ const toggleListType = (type: string, num: number) => {
     curDevice.value = { ...clearObj };
     curBatch.value = { ...clearObj };
     _batchDeviceList();
+  } else if (type === "level3") {
+    curBatch.value = { ...clearObj };
+    _batchList();
   }
 };
 const activeIndex = ref();
-// const imgList = ref([
-//   {
-//     url: "https://cet-test.markingtool.cn/file/slice/1/2/10101/1/031/1/101011221203102-1.jpg",
-//   },
-//   {
-//     url: "https://cet-test.markingtool.cn/file/slice/1/2/10101/1/031/1/101011221203101-1.jpg",
-//   },
-//   {
-//     url: "https://cet-test.markingtool.cn/file/slice/1/2/10100/1/035/1/101001221203502-1.jpg",
-//   },
-// ]);
+
+const imageType = ref(dataCheckStore.imageType);
+// imageType
+const isOriginImage = computed(() => {
+  return dataCheckStore.imageType === "ORIGIN";
+});
+
+// imageType
+const isSliceImage = computed(() => {
+  return dataCheckStore.imageType === "SLICE";
+});
+
+// question panel
+const questionInfo = computed(() => {
+  return {
+    examNumber: dataCheckStore.curStudent?.examNumber,
+    name: dataCheckStore.curStudent?.name,
+    examSite: dataCheckStore.curStudent?.examSite,
+    seatNumber: dataCheckStore.curStudent?.seatNumber,
+    paperType: dataCheckStore.curStudent?.paperType,
+  };
+});
+
+const questions = ref<any>([]);
+watch(
+  () => dataCheckStore.curPageIndex,
+  (val, oldval) => {
+    if (val !== oldval) {
+      questions.value = [...(dataCheckStore.curPage?.question || [])];
+    }
+  }
+);
+
+async function onQuestionsChange() {
+  if (!dataCheckStore.curPage) return;
+  dataCheckStore.curPage.question = [...questions.value];
+
+  await dataCheckStore
+    .updateField({
+      field: "QUESTION",
+      value: questions.value,
+    })
+    .catch(() => {});
+}
+
+async function onPrevPage() {}
+
+async function onNextPage() {}
 </script>
 <style lang="less" scoped>
 .image-view {
+  .accordion-body {
+    padding: 12px 16px;
+  }
   .image-wrap {
     height: 100%;
     border-left: 1px solid #e5e6eb;
@@ -216,16 +348,21 @@ const activeIndex = ref();
         }
         span {
           color: @text-color3;
+          &:not(:last-child) {
+            cursor: pointer;
+          }
           &.active {
             color: @text-color1;
+            font-weight: bold;
           }
         }
       }
       .result-list {
         background: #f2f3f5;
         border-radius: 6px;
-        padding: 6px 8px;
-        line-height: 20px;
+        padding: 0 8px;
+        height: 32px;
+        line-height: 32px;
         color: @text-color1;
       }
     }