123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560 |
- <template>
- <div class="mark check-paper">
- <div class="mark-header">
- <div class="mark-header-part">
- <template v-if="student">
- <div class="header-noun">
- <span>课程名称:</span>
- <span> {{ student.courseName }}({{ student.courseCode }})</span>
- </div>
- <div class="header-noun">
- <span>试卷编号:</span>
- <span>{{ student.paperNumber }}</span>
- </div>
- <div class="header-noun">
- <span>姓名:</span>
- <span>{{ student.studentName }}</span>
- </div>
- <div class="header-noun">
- <span>学号:</span>
- <span>{{ student?.studentCode }}</span>
- </div>
- <div v-if="studentIds.length > 1" class="header-noun">
- <span>进度:</span>
- <span> {{ currentIndex + 1 }}/{{ studentIds.length }} </span>
- </div>
- </template>
- </div>
- <div class="mark-header-part">
- <div class="paper-menu">
- <span
- v-for="(u, index) in student?.sheetUrls"
- :key="index"
- :class="{ 'is-active': currentImage === index }"
- @click="currentImage = index"
- >
- {{ index + 1 }}
- </span>
- </div>
- <div class="header-text-btn header-logout" @click="logout">
- <img class="header-icon" src="@/assets/icons/icon-return.svg" />返回
- </div>
- </div>
- </div>
- <div class="mark-main">
- <div class="mark-body">
- <div
- v-if="student && currentImage !== 0"
- class="page-action page-prev"
- title="上一张"
- @click="switchImageArrow({ left: true })"
- >
- <ArrowLeftOutlined />
- </div>
- <div
- v-if="student && currentImage !== student.sheetUrls.length - 1"
- class="page-action page-next"
- title="上一张"
- @click="switchImageArrow({ right: true })"
- >
- <ArrowRightOutlined />
- </div>
- <div class="mark-body-container">
- <div v-if="!student" class="mark-body-none">
- <div>
- <img src="@/assets/image-none-task.png" />
- <p>暂无数据</p>
- </div>
- </div>
- <div
- v-else
- class="single-image-container"
- :style="{ width: answerPaperScale, fontSize: answerPaperFontSize }"
- >
- <img
- id="mark-body-paper"
- draggable="false"
- :src="curImageUrl"
- :style="{
- transform:
- (rotateDegree ? 'translate( 0, calc(30vh))' : '') +
- `rotate(${rotateDegree}deg)`,
- }"
- @click="switchImage"
- @contextmenu="showBigImage"
- @load="paperLoad"
- />
- <div
- v-for="(tag, tindex) in answerTags"
- :key="tindex"
- :style="tag.style"
- >
- {{ tag.answer }}
- </div>
- <div
- v-for="(tag, tindex) in optionsBlocks"
- :key="tindex + 'block'"
- :style="tag.style"
- ></div>
- </div>
- </div>
- <ZoomPaper v-if="student" showRotate fixed @rotateRight="rotateRight" />
- </div>
- <div class="mark-board-track">
- <div class="board-header no-action">
- <div class="board-header-info">
- <img src="@/assets/icons/icon-star.svg" />
- <span>总分</span>
- </div>
- <div class="board-header-score">
- <transition-group name="score-number-animation" tag="span">
- <span :key="totalScore">{{ totalScore }}</span>
- </transition-group>
- </div>
- </div>
- <div class="paper-topics">
- <div
- v-for="group in answersComputed"
- :key="group.mainNumber"
- class="paper-topic"
- >
- <h2 class="paper-topic-title">
- {{ group.mainNumber }}、{{ group.mainTitle }} ({{
- group.subs.length
- }})
- </h2>
- <div class="paper-topic-body">
- <div
- v-for="question in group.subs"
- :key="question.subNumber"
- class="paper-topic-question"
- >
- <span class="question-number">{{ question.subNumber }} </span>
- <a-input
- class="normal-input"
- :class="{
- 'long-input': question.type
- ? !['SINGLE', 'TRUE_OR_FALSE'].includes(question.type)
- : !group.mainTitle.match(/单选|单项|判断/),
- }"
- :value="question.answer"
- :maxLength="
- (
- question.type
- ? ['MULTIPLE'].includes(question.type)
- : group.mainTitle.match(/多选|多项|不定项/)
- )
- ? 100
- : 1
- "
- @keydown="onPreventAnswerKey"
- @input="changeAnswer($event, question)"
- @blur="changeAnswer($event, question, '#')"
- />
- </div>
- </div>
- </div>
- </div>
- <div :class="['board-footer', { 'is-simple': !isMultiStudent }]">
- <qm-button
- class="board-submit"
- size="medium"
- type="primary"
- :disabled="!student?.upload"
- @click="saveStudentAnswer"
- >
- 保存
- </qm-button>
- <div v-if="isMultiStudent" class="student-switch">
- <a-button :disabled="isFirst" @click="getPreviousStudent">
- 上一份
- </a-button>
- <a-button :disabled="isLast" @click="getNextStudent">
- 下一份
- </a-button>
- </div>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script lang="ts" setup>
- import { message } from "ant-design-vue";
- import { ArrowLeftOutlined, ArrowRightOutlined } from "@ant-design/icons-vue";
- import { onMounted, watch } from "vue";
- import "viewerjs/dist/viewer.css";
- import Viewer from "viewerjs";
- import { StudentObjectiveInfo, PaperRecogData, AnswerTagItem } from "@/types";
- import {
- studentObjectiveConfirmData,
- saveStudentObjectiveConfirmData,
- } from "@/api/checkPage";
- import { doLogout } from "@/api/markPage";
- import { useMarkStore } from "@/store";
- import { useTimers } from "@/setups/useTimers";
- import vls from "@/utils/storage";
- import { maxNum } from "@/utils/utils";
- import ZoomPaper from "@/components/ZoomPaper.vue";
- const { addTimeout } = useTimers();
- const markStore = useMarkStore();
- const studentIds = $ref(vls.get("check-students", []));
- onMounted(async () => {
- if (studentIds.length === 0) {
- void message.info("没有需要处理的考生,请返回。");
- return;
- }
- await getNextStudent();
- });
- let currentStudentId = $ref("");
- const currentIndex = $computed(() => studentIds.indexOf(currentStudentId));
- const isFirst = $computed(() => currentIndex === 0);
- const isLast = $computed(() => currentIndex === studentIds.length - 1);
- const isMultiStudent = $computed(() => studentIds.length > 1);
- const totalScore = $computed(() => {
- if (!student) return 0;
- return student.objectiveScore || 0;
- });
- const curImageUrl = $computed(() =>
- student ? student.sheetUrls[currentImage]?.url : ""
- );
- let student: StudentObjectiveInfo | null = $ref(null);
- /** 后台数据错误,停止整个页面的流程 */
- let dataError = $ref(false);
- let answerMap: Record<string, { answer: string; isRight: boolean }> = {};
- let answerTags = $ref<AnswerTagItem[]>([]);
- let optionsBlocks = $ref([]);
- 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) ?? [];
- });
- return mains;
- });
- function logout() {
- doLogout();
- }
- async function getNextStudent() {
- if (isLast) {
- void message.warning("已经是最后一份!");
- return;
- }
- student = await getStudent(studentIds[currentIndex + 1]);
- }
- async function getPreviousStudent() {
- if (isFirst) {
- void message.warning("已经是第一份!");
- return;
- }
- student = await getStudent(studentIds[currentIndex - 1]);
- }
- async function getStudent(studentId: string) {
- const res = await studentObjectiveConfirmData(studentId).catch(() => {
- dataError = true;
- });
- if (dataError) {
- void message.error(res.message, 24 * 60 * 60);
- throw new Error("取学生信息出错: " + res.message);
- }
- const stu = res.data as StudentObjectiveInfo;
- // stu.sheetUrls = [
- // { index: 1, url: "/1-1.jpg" },
- // { index: 2, url: "/1-2.jpg" },
- // ];
- currentStudentId = stu.studentId;
- currentImage = 0;
- browsedImageIndexes = [0];
- answerMap = {};
- stu.answers.forEach((item) => {
- answerMap[`${item.mainNumber}_${item.subNumber}`] = {
- answer: item.answer,
- isRight: item.answer === item.standardAnswer,
- };
- });
- return stu;
- }
- const allowKey = [
- "Delete",
- "Backspace",
- "ArrowLeft",
- "ArrowRight",
- "#",
- "Shift",
- "[A-Za-z]",
- ];
- const allowKeyRef = new RegExp(allowKey.join("|"));
- function onPreventAnswerKey(e: KeyboardEvent) {
- console.log(e);
- if (!allowKeyRef.test(e.key)) {
- e.preventDefault();
- }
- }
- function changeAnswer(event: Event, question: string, defaultValue?: string) {
- const target = event.target as HTMLInputElement;
- student.answers = student.answers.map((v) => {
- if (
- v.mainNumber === question.mainNumber &&
- v.subNumber === question.subNumber
- ) {
- v.answer = target?.value.toUpperCase().trim() || defaultValue || "";
- }
- return v;
- });
- }
- let loading = false;
- async function saveStudentAnswer() {
- if (!student) return;
- if (loading) return;
- loading = true;
- const data = {
- studentId: student.studentId,
- answers: student.answers.map((v) => v.answer || "#").join(","),
- };
- // if (!answers.match(/^(#*,*[A-Z]*)+$/g)) {
- // void message.error("答案只能是#和大写英文字母");
- // return;
- // }
- const res = await saveStudentObjectiveConfirmData(data).catch(() => false);
- loading = false;
- if (!res) {
- void message.error("保存失败,请刷新页面。");
- } else {
- void message.success("保存成功");
- if (!isMultiStudent) {
- window.close();
- return;
- }
- if (isLast) {
- student = await getStudent(studentIds[currentIndex]);
- } else {
- await getNextStudent();
- }
- }
- }
- function paperLoad() {
- if (!student.sheetUrls[currentImage]?.recogData) {
- answerTags = [];
- optionsBlocks = [];
- return;
- }
- const imgDom = document.getElementById("mark-body-paper");
- const { naturalWidth, naturalHeight } = imgDom;
- const recogData: PaperRecogData = JSON.parse(
- window.atob(student.sheetUrls[currentImage].recogData)
- );
- answerTags = [];
- optionsBlocks = [];
- recogData.question.forEach((question) => {
- question.fill_result.forEach((result) => {
- const tagSize = result.fill_size[1];
- const fillPositions = result.fill_position.map((pos) => {
- return pos.split(",").map((n) => n * 1);
- });
- const offsetLt = result.fill_size.map((item) => item * 0.4);
- const tagLeft =
- maxNum(fillPositions.map((pos) => pos[0])) +
- result.fill_size[0] -
- offsetLt[0];
- const tagTop = fillPositions[0][1] - offsetLt[1];
- const { answer, isRight } =
- answerMap[`${result.main_number}_${result.sub_number}`] || {};
- answerTags.push({
- mainNumber: result.main_number,
- subNumber: result.sub_number,
- answer,
- style: {
- height: ((100 * tagSize) / naturalHeight).toFixed(4) + "%",
- fontSize: ((100 * 20) / tagSize).toFixed(4) + "%",
- left: ((100 * tagLeft) / naturalWidth).toFixed(4) + "%",
- top: ((100 * tagTop) / naturalHeight).toFixed(4) + "%",
- position: "absolute",
- color: isRight ? "#05b575" : "#f53f3f",
- fontWeight: 600,
- lineHeight: 1,
- zIndex: 9,
- },
- });
- // 测试:选项框
- // fillPositions.forEach((fp, index) => {
- // optionsBlocks.push({
- // mainNumber: result.main_number,
- // subNumber: result.sub_number,
- // filled: !!result.fill_option[index],
- // style: {
- // width:
- // ((100 * result.fill_size[0]) / naturalWidth).toFixed(4) + "%",
- // height:
- // ((100 * result.fill_size[1]) / naturalHeight).toFixed(4) + "%",
- // left:
- // ((100 * (fp[0] - offsetLt[0])) / naturalWidth).toFixed(4) + "%",
- // top:
- // ((100 * (fp[1] - offsetLt[1])) / naturalHeight).toFixed(4) + "%",
- // position: "absolute",
- // border: "1px solid #f53f3f",
- // background: result.fill_option[index]
- // ? "rgba(245, 63, 63, 0.5)"
- // : "transparent",
- // zIndex: 9,
- // },
- // });
- // });
- });
- });
- }
- //#region : 显示大图,供查看和翻转
- let currentImage = $ref(0);
- let browsedImageIndexes = $ref([0]);
- // let allViewed = $computed(() => {
- // let indexes = Array.from(new Set(browsedImageIndexes));
- // return indexes.length == (student?.sheetUrls || []).length;
- // });
- watch(
- () => currentImage,
- () => {
- browsedImageIndexes.push(currentImage);
- }
- );
- 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 = markStore.setting.uiSetting["answer.paper.scale"];
- return scale * 100 + "%";
- });
- const answerPaperFontSize = $computed(() => {
- const scale = markStore.setting.uiSetting["answer.paper.scale"];
- return scale * 14 + "px";
- });
- //#endregion : 放大缩小和之后的滚动
- //#region rotateRight
- let rotateDegree = $ref(0);
- function rotateRight() {
- rotateDegree = (rotateDegree + 90) % 360;
- }
- //#endregion
- </script>
|