123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485 |
- <template>
- <div v-if="!dataError" class="tw-h-screen">
- <header
- class="tw-flex tw-gap-2 tw-justify-between tw-items-center header-container"
- >
- <div class="tw-ml-2">
- 进度:<span class="highlight-text">
- {{ currentIndex }}/{{ allIds.length }}
- </span>
- </div>
- <div>
- 姓名:<span class="highlight-text">{{ student?.name }}</span>
- </div>
- <div>
- 准考证号:<span class="highlight-text">{{ student?.examNumber }}</span>
- </div>
- <div>
- 学号:<span class="highlight-text">{{ student?.studentCode }}</span>
- </div>
- <div>
- 科目:<span class="highlight-text">
- {{ student?.subjectCode }}-{{ student?.subjectName }}
- </span>
- </div>
- <div>
- 客观分:<span class="highlight-text">{{
- student?.objectiveScore
- }}</span>
- </div>
- <div>
- 主观分:<span class="highlight-text">{{
- student?.subjectiveScore
- }}</span>
- </div>
- <div class="tw-flex tw-items-center tw-gap-2 tw-mx-8">
- <span
- v-for="(u, index) in student?.sheetUrls"
- :key="index"
- class="tw-cursor-pointer"
- :class="currentImage === index && 'highlight-text'"
- @click="currentImage = index"
- >
- {{ index + 1 }}
- </span>
- </div>
- </header>
- <div class="tw-flex" style="height: calc(100% - 56px)">
- <div
- style="flex: 0 1 420px; overflow: auto"
- class="tw-flex tw-flex-col tw-justify-between"
- >
- <div class="tw-m-2 tw-flex-1 tw-overflow-auto">
- <div v-if="pageType === 'DATA_CHECK'">
- 是否缺考:
- <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 v-if="pageType === 'DATA_CHECK'" class="tw-my-2">
- 试卷类型:
- <a-input
- v-if="student"
- v-model:value="student.paperType"
- :maxlength="1"
- style="width: 40px"
- />
- </div>
- <div v-if="student?.answers" class="tw-mt-4">
- <div
- v-for="group in answersComputed"
- :key="group.mainNumber"
- class="tw-mt-2"
- >
- <h2>
- {{ group.mainNumber }}、{{ group.mainTitle }} ({{
- group.subs.length
- }})
- </h2>
- <div class="tw-flex tw-flex-wrap 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="changeAnswer($event, question)"
- @blur="changeAnswer($event, question, '#')"
- />
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="tw-flex tw-justify-between tw-bg-white tw-p-4">
- <a-button
- :disabled="!student?.upload"
- type="primary"
- shape="round"
- @click="saveStudentAnswer"
- >
- 保存
- </a-button>
- <div>
- <a-button
- shape="round"
- :disabled="currentIndex <= 1"
- class="tw-mr-4"
- @click="getPreviousStudent"
- >
- 上一份
- </a-button>
- <a-button
- shape="round"
- :disabled="currentIndex === allIds.length"
- @click="getNextStudent"
- >
- 下一份
- </a-button>
- </div>
- </div>
- </div>
- <div style="flex: 1" class="mark-body-container tw-relative">
- <ArrowLeftOutlined
- v-if="student && currentImage !== 0"
- class="tw-cursor-pointer tw-absolute"
- style="top: 45%; left: 20px; z-index: 1; font-size: 40px"
- title="上一张"
- @click="switchImageArrow({ left: true })"
- />
- <ArrowRightOutlined
- v-if="student && currentImage !== student.sheetUrls.length - 1"
- class="tw-cursor-pointer tw-absolute"
- style="top: 45%; right: 20px; z-index: 1; font-size: 40px"
- title="上一张"
- @click="switchImageArrow({ right: true })"
- />
- <div :style="{ width: answerPaperScale }">
- <img
- v-for="(item, index) in student?.sheetUrls"
- :key="item"
- class="tw-object-cover"
- :src="item"
- :style="{
- display: index === currentImage ? 'block' : 'none',
- transform:
- (rotateDegree ? 'translate( 0, calc(30vh))' : '') +
- `rotate(${rotateDegree}deg)`,
- }"
- @click="switchImage"
- @contextmenu="showBigImage"
- />
- </div>
- <ZoomPaper v-if="student" fixed showRotate @rotateRight="rotateRight" />
- </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";
- import "viewerjs/dist/viewer.css";
- import Viewer from "viewerjs";
- import { store } from "@/store/store";
- import ZoomPaper from "@/components/ZoomPaper.vue";
- import { useTimers } from "@/setups/useTimers";
- import { ArrowLeftOutlined, ArrowRightOutlined } from "@ant-design/icons-vue";
- const { addTimeout } = useTimers();
- const route = useRoute();
- // 使用 location.search 替代
- // 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";
- sessionStorage.setItem(queryId, localStorage.getItem(queryId) || "[]");
- localStorage.removeItem(queryId);
- }
- 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);
- /** 后台数据错误,停止整个页面的流程 */
- let dataError = $ref(false);
- 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 query: Array<{ name: string; value: string }> = JSON.parse(
- sessionStorage.getItem(queryId) || "[]"
- );
- const form = new FormData();
- for (const v of query) {
- form.append(v.name, v.value + "");
- }
- res = await httpApp.post("/admin/exam/check/answer/getSetting", form);
- } else {
- const form = new FormData();
- form.append("checkType", route.query.checkType as string);
- form.append("subjectCode", route.query.subjectCode as string);
- form.append("examSite", route.query.examSite as string);
- res = await httpApp.post(`/admin/exam/check/student/getSetting`, form);
- }
- setting.fileServer = res.data.fileServer;
- setting.studentIds = res.data.studentIds;
- }
- async function getNextStudent() {
- const wantedIndex = allIds.indexOf(currentStudentId);
- if (allIds[wantedIndex + 1]) {
- 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;
- currentImage = 0;
- if (!stu.success) {
- void message.error(stu.message, 24 * 60 * 60);
- dataError = true;
- throw new Error("取学生信息出错: " + stu.message);
- }
- // 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().trim() ||
- 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`;
- const res = await httpApp
- .post(url, form)
- .catch(() => message.error("保存失败-接口调用失败"));
- if (!res.data) {
- void message.error("保存失败,请刷新页面。");
- } else {
- void message.success("保存成功");
- await getNextStudent()
- }
- if (setting.studentIds.length === 0) {
- void message.success("所有考生已处理完毕。");
- }
- }
- //#region : 显示大图,供查看和翻转
- let currentImage = $ref(0);
- function switchImageArrow({
- left = false,
- right = false,
- }: {
- left?: boolean;
- right?: boolean;
- }) {
- if (left) {
- if (currentImage > 0) {
- currentImage--;
- }
- }
- if (right) {
- if (currentImage < student!.sheetUrls.length - 1) {
- currentImage++;
- }
- }
- }
- function switchImage(event: MouseEvent) {
- const image = event.target as HTMLImageElement;
- const layerX: number = (event as any).layerX;
- if (layerX * 2 < image.width) {
- if (currentImage > 0) {
- currentImage--;
- }
- } else {
- if (currentImage < student!.sheetUrls.length - 1) {
- currentImage++;
- }
- }
- }
- const showBigImage = (event: MouseEvent) => {
- event.preventDefault();
- // console.log(event);
- let viewer: Viewer = null as unknown as Viewer;
- viewer && viewer.destroy();
- viewer = new Viewer((event.target as HTMLElement).parentElement!, {
- // inline: true,
- viewed() {
- viewer.zoomTo(1);
- },
- hidden() {
- viewer.destroy();
- },
- zIndex: 1000000,
- });
- viewer.show();
- };
- //#endregion : 显示大图,供查看和翻转
- //#region : 放大缩小和之后的滚动
- const answerPaperScale = $computed(() => {
- // 放大、缩小不影响页面之前的滚动条定位
- let percentWidth = 0;
- let percentTop = 0;
- const container = document.querySelector<HTMLDivElement>(
- ".mark-body-container"
- );
- if (container) {
- const { scrollLeft, scrollTop, scrollWidth, scrollHeight } = container;
- percentWidth = scrollLeft / scrollWidth;
- percentTop = scrollTop / scrollHeight;
- }
- addTimeout(() => {
- if (container) {
- const { scrollWidth, scrollHeight } = container;
- container.scrollTo({
- left: scrollWidth * percentWidth,
- top: scrollHeight * percentTop,
- });
- }
- }, 10);
- const scale = store.setting.uiSetting["answer.paper.scale"];
- return scale * 100 + "%";
- });
- //#endregion : 放大缩小和之后的滚动
- //#region rotateRight
- let rotateDegree = $ref(0);
- function rotateRight() {
- rotateDegree = (rotateDegree + 90) % 360;
- }
- //#endregion
- </script>
- <style scoped>
- .header-container {
- position: relative;
- height: 56px;
- line-height: 16px;
- background-color: var(--header-bg-color);
- color: rgba(255, 255, 255, 0.5);
- }
- .highlight-text {
- color: white;
- font-size: var(--app-title-font-size);
- }
- .mark-body-container {
- position: relative;
- min-height: calc(100vh - 56px);
- height: calc(100vh - 56px);
- overflow: auto;
- /* background-size: 8px 8px;
- background-image: linear-gradient(to right, #e7e7e7 4px, transparent 4px),
- linear-gradient(to bottom, transparent 4px, #e7e7e7 4px); */
- background-color: var(--app-container-bg-color);
- background-image: linear-gradient(45deg, #e0e0e0 25%, transparent 25%),
- linear-gradient(-45deg, #e0e0e0 25%, transparent 25%),
- linear-gradient(45deg, transparent 75%, #e0e0e0 75%),
- linear-gradient(-45deg, transparent 75%, #e0e0e0 75%);
- background-size: 20px 20px;
- background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
- transform: inherit;
- }
- </style>
|