Sfoglia il codice sorgente

数据检查 & 人工确认 完成逻辑

Michael Wang 3 anni fa
parent
commit
a5484d40ed

+ 245 - 0
src/features/admin/confirmPaper/ConfirmPaper.vue

@@ -0,0 +1,245 @@
+<template>
+  <div>
+    <header class="tw-flex">
+      <div>进度:{{ currentIndex }}/{{ allIds.length }}</div>
+      <div>姓名:{{ student?.name }}</div>
+      <div>准考证号:{{ student?.examNumber }}</div>
+      <div>学号:{{ student?.studentCode }}</div>
+      <div>科目:{{ student?.subjectCode }}-{{ student?.subjectName }}</div>
+      <div>客观分:{{ student?.objectiveScore }}</div>
+      <div>主观分:{{ student?.subjectiveScore }}</div>
+      <div></div>
+    </header>
+
+    <div>
+      <div>
+        <div>
+          <div>
+            是否缺考:
+            <a-radio-group v-if="student" v-model:value="student.absent">
+              <a-radio :value="true">是</a-radio>
+              <a-radio :value="false">否</a-radio>
+            </a-radio-group>
+          </div>
+          <div>
+            试卷类型:
+            <a-input
+              v-if="student"
+              v-model:value="student.paperType"
+              :maxlength="1"
+              style="width: 40px"
+            />
+          </div>
+        </div>
+
+        <div v-if="student?.answers">
+          <div v-for="group in answersComputed" :key="group.mainNumber">
+            <h2>
+              {{ group.mainNumber }}、{{ group.mainTitle }} ({{
+                group.subs.length
+              }})
+            </h2>
+            <div class="tw-flex tw-gap-4">
+              <div v-for="question in group.subs" :key="question.subNumber">
+                <span>{{ question.subNumber }}</span>
+                <a-input
+                  :value="question.answer"
+                  style="width: 40px"
+                  :maxLength="
+                    group.mainTitle.match(/多选|多项|不定项/) ? 100 : 1
+                  "
+                  @input="($event) => changeAnswer($event, question)"
+                  @blur="($event) => changeAnswer($event, question, '#')"
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div>
+          <a-button @click="saveStudentAnswer">保存</a-button>
+          <a-button :disabled="currentIndex === 1" @click="getPreviousStudent"
+            >上一份</a-button
+          >
+          <a-button
+            :disabled="currentIndex === allIds.length"
+            @click="getNextStudent"
+            >下一份</a-button
+          >
+        </div>
+      </div>
+
+      <div>
+        <div>
+          <img v-for="item in student?.sheetUrls" :key="item" :src="item" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { httpApp } from "@/plugins/axiosApp";
+import { message } from "ant-design-vue";
+import { onMounted, reactive } from "vue";
+import { useRoute } from "vue-router";
+import { CheckSetting, StudentInfo } from "./check";
+
+const route = useRoute();
+const checkType = route.query.checkType;
+const queryId = route.query.queryId as string;
+let pageType: "DATA_CHECK" | "HAND_CHECK" = "HAND_CHECK";
+if (queryId) {
+  pageType = "DATA_CHECK";
+}
+
+onMounted(async () => {
+  await getSetting();
+  if (setting.studentIds.length === 0) {
+    void message.info("没有需要处理的考生,请返回。");
+    return;
+  }
+  await getNextStudent();
+});
+
+let setting: CheckSetting = reactive({
+  fileServer: "",
+  studentIds: [],
+  studentIdsDone: [],
+});
+
+const allIds = $computed(() => [
+  ...setting.studentIdsDone,
+  ...setting.studentIds,
+]);
+
+let currentStudentId = $ref(-1);
+
+const currentIndex = $computed(() => allIds.indexOf(currentStudentId) + 1);
+
+let student: StudentInfo | null = $ref(null);
+
+const answersComputed = $computed(() => {
+  let mains = student?.answers.map((v) => ({
+    mainTitle: "",
+    mainNumber: v.mainNumber,
+    subs: [v],
+  }));
+  const mSet = new Set();
+  mains = mains?.filter((v) => {
+    if (!mSet.has(v.mainNumber)) {
+      mSet.add(v.mainNumber);
+      v.subs = [];
+      return true;
+    }
+  });
+  mains?.forEach((v) => {
+    v.mainTitle = student?.titles[v.mainNumber] ?? "";
+    v.subs =
+      student?.answers.filter((v2) => v2.mainNumber === v.mainNumber) ?? [];
+  });
+  // console.log(mains);
+  return mains;
+});
+
+async function getSetting() {
+  let res: any;
+  if (pageType === "DATA_CHECK") {
+    const q: Record<string, string> = JSON.parse(
+      sessionStorage.getItem(queryId) || "{}"
+    );
+    const form = new FormData();
+    for (const [k, v] of Object.entries(q)) {
+      form.append(k, v + "");
+    }
+    res = await httpApp.post("/admin/exam/check/answer/getSetting", form);
+  } else {
+    res = await httpApp.get(
+      `/admin/exam/check/student/getSetting?checkType=${checkType}`
+    );
+  }
+
+  setting.fileServer = res.data.fileServer;
+  setting.studentIds = res.data.studentIds;
+}
+
+async function getNextStudent() {
+  const wantedIndex = allIds.indexOf(currentStudentId);
+  student = await getStudent(allIds[wantedIndex + 1]);
+}
+
+async function getPreviousStudent() {
+  const wantedIndex = allIds.indexOf(currentStudentId);
+  student = await getStudent(allIds[wantedIndex - 1]);
+}
+
+async function getStudent(studentId: number) {
+  const stu: StudentInfo = await (
+    await httpApp.get(`/admin/exam/check/answer/info?studentId=${studentId}`)
+  ).data;
+  stu?.sheetUrls.forEach((v, i, a) => (a[i] = setting.fileServer + v));
+  currentStudentId = stu.id;
+
+  // for dev
+  stu.answers = [
+    { mainNumber: 1, subNumber: "1", answer: "A" },
+    { mainNumber: 1, subNumber: "2", answer: "B" },
+    { mainNumber: 2, subNumber: "1", answer: "#" },
+  ];
+  stu.titles = { 1: "单选题", 2: "多选题" };
+  return stu;
+}
+
+function changeAnswer(
+  event: Event,
+  question: StudentInfo["answers"][0],
+  defaultValue?: string
+) {
+  // console.log(question, event.target.value);
+  student!.answers = student!.answers.map((v) => {
+    if (
+      v.mainNumber === question.mainNumber &&
+      v.subNumber === question.subNumber
+    ) {
+      v.answer =
+        (<HTMLInputElement>event.target!).value.toUpperCase() ||
+        defaultValue ||
+        "";
+    }
+    return v;
+  });
+}
+
+async function saveStudentAnswer() {
+  if (!student) return;
+  const form = new FormData();
+  form.append("studentId", student.id + "");
+  const answers = student.answers.map((v) => v.answer).join(",");
+  if (!answers.match(/^(#*[A-Z]*)+$/g)) {
+    void message.error("答案只能是#和大写英文字母");
+    return;
+  }
+  form.append("answers", answers);
+
+  const extra = pageType === "DATA_CHECK";
+  extra && form.append("absent", student.absent + "");
+  extra && form.append("paperType", student.paperType);
+  if (extra) {
+    if (!student.paperType.match(/^#|[A-Z]$/)) {
+      void message.error("试卷类型只能是#和大写英文字母");
+      return;
+    }
+  }
+
+  const url = extra
+    ? // 数据检查
+      "/admin/exam/check/answer/save"
+    : `/admin/exam/check/student/save`;
+  await httpApp.post(url, form).catch(() => message.error("保存失败"));
+  void message.success("保存成功");
+
+  if (setting.studentIds.length === 0) {
+    void message.success("所有考生已处理完毕。");
+  }
+}
+</script>

+ 28 - 0
src/features/admin/confirmPaper/check.d.ts

@@ -0,0 +1,28 @@
+export type CheckSetting = {
+  fileServer: string;
+  studentIds: number[];
+  studentIdsDone: number[];
+};
+
+export type StudentInfo = {
+  id: number;
+  name: string;
+  examNumber: string;
+  studentCode: string;
+  subjectCode: string;
+  subjectName: string;
+  objectiveScore: number;
+  subjectiveScore: number;
+  upload: boolean;
+  absent: boolean;
+  paperType: string;
+  sheetUrls: [string];
+  answers: {
+    mainNumber: number;
+    subNumber: string;
+    answer: string;
+  }[];
+  titles: { [index: number]: string };
+};
+
+export {};

+ 12 - 0
src/router/index.ts

@@ -48,6 +48,18 @@ const routes = [
     name: "TrialRoute",
     component: () => import("@/features/library/libraryTrack/LibraryTrack.vue"),
   },
+  {
+    // 数据检查
+    path: "/admin/exam/check/answer/start",
+    name: "ConfirmPaper",
+    component: () => import("@/features/admin/confirmPaper/ConfirmPaper.vue"),
+  },
+  {
+    // 人工确认
+    path: "/admin/exam/check/student/start",
+    name: "ConfirmPaper",
+    component: () => import("@/features/admin/confirmPaper/ConfirmPaper.vue"),
+  },
   {
     path: "/:pathMatch(.*)*",
     name: "NotFound",