|
@@ -0,0 +1,401 @@
|
|
|
|
+<template>
|
|
|
|
+ <div class="mark">
|
|
|
|
+ <div class="mark-header">
|
|
|
|
+ <div class="mark-header-part">
|
|
|
|
+ <template v-if="store.currentTask">
|
|
|
|
+ <div class="header-noun">
|
|
|
|
+ <span>课程名称:</span>
|
|
|
|
+ <span>
|
|
|
|
+ {{ store.currentTask.courseName }}({{
|
|
|
|
+ store.currentTask.courseCode
|
|
|
|
+ }})</span
|
|
|
|
+ >
|
|
|
|
+ </div>
|
|
|
|
+ <div class="header-noun">
|
|
|
|
+ <span>试卷编号:</span>
|
|
|
|
+ <span>{{ store.currentTask.paperNumber }}</span>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="header-noun">
|
|
|
|
+ <span>姓名:</span>
|
|
|
|
+ <span>{{ store.currentTask.studentName }}</span>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="header-noun">
|
|
|
|
+ <span>学号:</span>
|
|
|
|
+ <span>{{ store.currentTask?.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 v-if="isMultiStudent" class="task-switch">
|
|
|
|
+ <a-button
|
|
|
|
+ :disabled="isFirst"
|
|
|
|
+ size="small"
|
|
|
|
+ @click="getPreviousStudent"
|
|
|
|
+ >
|
|
|
|
+ 上一份
|
|
|
|
+ </a-button>
|
|
|
|
+ <a-button
|
|
|
|
+ :disabled="isLast"
|
|
|
|
+ size="small"
|
|
|
|
+ style="margin-left: 8px"
|
|
|
|
+ @click="getNextStudent"
|
|
|
|
+ >
|
|
|
|
+ 下一份
|
|
|
|
+ </a-button>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="header-text-btn header-logout" @click="logout">
|
|
|
|
+ <img class="header-icon" src="@/assets/icons/icon-return.svg" />返回
|
|
|
|
+ </div>
|
|
|
|
+ <a-tooltip placement="bottomRight">
|
|
|
|
+ <template #title>弹出给分板</template>
|
|
|
|
+ <div
|
|
|
|
+ :class="[
|
|
|
|
+ 'header-menu',
|
|
|
|
+ { 'is-toggled': store.isScoreBoardVisible && store.currentTask },
|
|
|
|
+ ]"
|
|
|
|
+ @click="store.toggleScoreBoard"
|
|
|
|
+ >
|
|
|
|
+ <img src="@/assets/icons/icon-right-menu.svg" class="header-icon" />
|
|
|
|
+ </div>
|
|
|
|
+ </a-tooltip>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <mark-tool @allZeroSubmit="allZeroSubmit" />
|
|
|
|
+ <div class="mark-main">
|
|
|
|
+ <mark-body @error="removeBrokenTask" />
|
|
|
|
+ <mark-board-track v-if="store.isTrackMode" @submit="saveTaskToServer" />
|
|
|
|
+ <mark-board-key-board
|
|
|
|
+ v-if="store.shouldShowMarkBoardKeyBoard"
|
|
|
|
+ @submit="saveTaskToServer"
|
|
|
|
+ @allZeroSubmit="allZeroSubmit"
|
|
|
|
+ />
|
|
|
|
+ <mark-board-mouse
|
|
|
|
+ v-if="store.shouldShowMarkBoardMouse"
|
|
|
|
+ @submit="saveTaskToServer"
|
|
|
|
+ @allZeroSubmit="allZeroSubmit"
|
|
|
|
+ />
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ <MinimapModal />
|
|
|
|
+ <AllPaperModal />
|
|
|
|
+ <SheetViewModal />
|
|
|
|
+ <SpecialTagModal />
|
|
|
|
+ <ShortCutModal />
|
|
|
|
+ <MarkBoardTrackDialog
|
|
|
|
+ v-if="store.isTrackMode"
|
|
|
|
+ @submit="saveTaskToServer"
|
|
|
|
+ @allZeroSubmit="allZeroSubmit"
|
|
|
|
+ />
|
|
|
|
+</template>
|
|
|
|
+
|
|
|
|
+<script setup lang="ts">
|
|
|
|
+import { onMounted, watch, h } from "vue";
|
|
|
|
+import {
|
|
|
|
+ studentSubjectiveConfirmData,
|
|
|
|
+ saveStudentSubjectiveConfirmData,
|
|
|
|
+} from "@/api/checkPage";
|
|
|
|
+import { updateUISetting } from "@/api/markPage";
|
|
|
|
+import { store } from "@/store/store";
|
|
|
|
+import MarkTool from "../mark/MarkTool.vue";
|
|
|
|
+import MarkBody from "./MarkBody.vue";
|
|
|
|
+import MarkBoardTrack from "../mark/MarkBoardTrack.vue";
|
|
|
|
+import type { Question } from "@/types";
|
|
|
|
+import MarkBoardKeyBoard from "../mark/MarkBoardKeyBoard.vue";
|
|
|
|
+import MarkBoardMouse from "../mark/MarkBoardMouse.vue";
|
|
|
|
+import { debounce, isEmpty, isNumber } from "lodash-es";
|
|
|
|
+import { message } from "ant-design-vue";
|
|
|
|
+import MinimapModal from "../mark/MinimapModal.vue";
|
|
|
|
+import AllPaperModal from "../mark/AllPaperModal.vue";
|
|
|
|
+import SheetViewModal from "../mark/SheetViewModal.vue";
|
|
|
|
+import SpecialTagModal from "../mark/SpecialTagModal.vue";
|
|
|
|
+import ShortCutModal from "../mark/ShortCutModal.vue";
|
|
|
|
+import { calcSum } from "@/utils/utils";
|
|
|
|
+import MarkBoardTrackDialog from "../mark/MarkBoardTrackDialog.vue";
|
|
|
|
+import vls from "@/utils/storage";
|
|
|
|
+
|
|
|
|
+const studentIds = $ref(vls.get("check-students", []));
|
|
|
|
+
|
|
|
|
+const currentIndex = $computed(() =>
|
|
|
|
+ studentIds.indexOf(store.currentTask?.studentId)
|
|
|
|
+);
|
|
|
|
+const isFirst = $computed(() => currentIndex === 0);
|
|
|
|
+const isLast = $computed(() => currentIndex === studentIds.length - 1);
|
|
|
|
+const isMultiStudent = $computed(() => studentIds.length > 1);
|
|
|
|
+
|
|
|
|
+onMounted(async () => {
|
|
|
|
+ if (studentIds.length === 0) {
|
|
|
|
+ void message.info("没有需要处理的考生,请返回。");
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ updateSetting();
|
|
|
|
+ await getNextStudent();
|
|
|
|
+});
|
|
|
|
+
|
|
|
|
+const logout = () => {
|
|
|
|
+ window.history.go(-1);
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+async function getNextStudent() {
|
|
|
|
+ console.log(currentIndex);
|
|
|
|
+
|
|
|
|
+ if (isLast) return;
|
|
|
|
+ await updateTask(studentIds[currentIndex + 1]);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+async function getPreviousStudent() {
|
|
|
|
+ if (isFirst) return;
|
|
|
|
+ await updateTask(studentIds[currentIndex - 1]);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function updateSetting() {
|
|
|
|
+ let uiSetting = vls.get("user", { uiSetting: "" }).uiSetting;
|
|
|
|
+ // 初次使用时,重置并初始化uisetting
|
|
|
|
+ if (isEmpty(uiSetting)) {
|
|
|
|
+ uiSetting = {
|
|
|
|
+ "answer.paper.scale": 1,
|
|
|
|
+ "score.board.collapse": false,
|
|
|
|
+ "normal.mode": "keyboard",
|
|
|
|
+ "score.fontSize.scale": 1,
|
|
|
|
+ "paper.modal": false,
|
|
|
|
+ "answer.modal": false,
|
|
|
|
+ "minimap.modal": false,
|
|
|
|
+ "specialTag.modal": false,
|
|
|
|
+ "shortCut.modal": false,
|
|
|
|
+ };
|
|
|
|
+ } else {
|
|
|
|
+ uiSetting = JSON.parse(uiSetting);
|
|
|
|
+ }
|
|
|
|
+ store.setting.uiSetting = uiSetting;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+let taskQuestionInfo = {};
|
|
|
|
+function updateTaskGroupInfo() {
|
|
|
|
+ taskQuestionInfo = {};
|
|
|
|
+ if (!store.currentTask) return;
|
|
|
|
+
|
|
|
|
+ store.currentTask.questionList.forEach((question) => {
|
|
|
|
+ const qno = `${question.mainNumber * 1000}${question.subNumber}`;
|
|
|
|
+ taskQuestionInfo[qno] = {
|
|
|
|
+ groupNumber: question.groupNumber,
|
|
|
|
+ score: question.score,
|
|
|
|
+ };
|
|
|
|
+ });
|
|
|
|
+}
|
|
|
|
+async function updateTask(studentId) {
|
|
|
|
+ const res = await studentSubjectiveConfirmData(studentId);
|
|
|
|
+ if (!res.data) {
|
|
|
|
+ store.message = res.message || "数据错误";
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ res.data.taskId = res.data.studentId;
|
|
|
|
+ const newTask = res.data;
|
|
|
|
+ // newTask.sheetUrls = newTask.sheetUrls || [];
|
|
|
|
+ newTask.sheetUrls = ["/1-1.jpg", "/1-2.jpg"];
|
|
|
|
+ newTask.sliceUrls = [...newTask.sheetUrls];
|
|
|
|
+ newTask.specialTagList = newTask.headerTagList.length
|
|
|
|
+ ? newTask.headerTagList
|
|
|
|
+ : newTask.specialTagList;
|
|
|
|
+ store.currentTask = newTask;
|
|
|
|
+ updateTaskGroupInfo();
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const __debounceUpdate = debounce(() => {
|
|
|
|
+ updateUISetting("", store.setting.uiSetting).catch((e) =>
|
|
|
|
+ console.log("保存设置出错", e)
|
|
|
|
+ );
|
|
|
|
+}, 3000);
|
|
|
|
+watch(
|
|
|
|
+ () => [store.setting.uiSetting],
|
|
|
|
+ () => {
|
|
|
|
+ __debounceUpdate();
|
|
|
|
+ },
|
|
|
|
+ { deep: true }
|
|
|
|
+);
|
|
|
|
+
|
|
|
|
+const removeBrokenTask = () => {
|
|
|
|
+ console.log("removeBrokenTask");
|
|
|
|
+
|
|
|
|
+ // store.currentTask = undefined;
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+const allZeroSubmit = async () => {
|
|
|
|
+ const markResult = store.currentTask?.markResult;
|
|
|
|
+ if (!markResult) return;
|
|
|
|
+
|
|
|
|
+ const { markerScore, scoreList, trackList, specialTagList } = markResult;
|
|
|
|
+ markResult.markerScore = 0;
|
|
|
|
+ const ss = new Array(store.currentTaskEnsured.questionList.length);
|
|
|
|
+ markResult.scoreList = ss.fill(0);
|
|
|
|
+ markResult.trackList = [];
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ await saveTaskToServer();
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.log("error restore");
|
|
|
|
+ } finally {
|
|
|
|
+ // console.log({ markerScore, scoreList, trackList });
|
|
|
|
+ markResult.markerScore = markerScore;
|
|
|
|
+ markResult.scoreList = scoreList;
|
|
|
|
+ markResult.trackList = trackList;
|
|
|
|
+ markResult.specialTagList = specialTagList;
|
|
|
|
+ }
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+const getMarkData = () => {
|
|
|
|
+ if (!store.currentTask?.markResult) return {};
|
|
|
|
+
|
|
|
|
+ let markResult = store.currentTask.markResult;
|
|
|
|
+
|
|
|
|
+ let commomData = {
|
|
|
|
+ status: markResult.status,
|
|
|
|
+ spent: Date.now() - store.currentTask.__markStartTime,
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ let groupMap = {};
|
|
|
|
+ markResult.trackList.forEach((track) => {
|
|
|
|
+ const { groupNumber } =
|
|
|
|
+ taskQuestionInfo[`${track.mainNumber * 1000}${track.subNumber}`];
|
|
|
|
+ if (!groupMap[groupNumber]) {
|
|
|
|
+ groupMap[groupNumber] = { groupNumber, trackList: [] };
|
|
|
|
+ }
|
|
|
|
+ groupMap[groupNumber].trackList.push(track);
|
|
|
|
+ });
|
|
|
|
+ markResult.specialTagList.forEach((track) => {
|
|
|
|
+ const { groupNumber } = track;
|
|
|
|
+ if (!groupMap[groupNumber]) {
|
|
|
|
+ groupMap[groupNumber] = { groupNumber, specialTagList: [] };
|
|
|
|
+ }
|
|
|
|
+ groupMap[groupNumber].specialTagList.push(track);
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ let groups = Object.values(groupMap).map((item) => {
|
|
|
|
+ let qScore = {};
|
|
|
|
+ item.trackList.forEach((track) => {
|
|
|
|
+ const qno = `${track.mainNumber * 1000}${track.subNumber}`;
|
|
|
|
+ qScore[qno] = (qScore[qno] || 0) + track.score;
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ // 过滤分数未修改小题的分组
|
|
|
|
+ const groupChanged = Object.entries(qScore).some(
|
|
|
|
+ ([qno, score]) => score !== taskQuestionInfo[qno].score
|
|
|
|
+ );
|
|
|
|
+ if (!groupChanged) return null;
|
|
|
|
+
|
|
|
|
+ let qScoreList = Object.entries(qScore).map(([qno, score]) => {
|
|
|
|
+ return {
|
|
|
|
+ qno: qno * 1,
|
|
|
|
+ score,
|
|
|
|
+ };
|
|
|
|
+ });
|
|
|
|
+ qScoreList.sort((a, b) => a.qno - b.qno);
|
|
|
|
+ const scoreList = qScoreList.map((item) => item.score);
|
|
|
|
+
|
|
|
|
+ return {
|
|
|
|
+ ...commomData,
|
|
|
|
+ ...item,
|
|
|
|
+ scoreList,
|
|
|
|
+ markerScore: calcSum(scoreList),
|
|
|
|
+ studentId: store.currentTask.studentId,
|
|
|
|
+ };
|
|
|
|
+ });
|
|
|
|
+ groups = groups.filter((group) => group);
|
|
|
|
+
|
|
|
|
+ return {
|
|
|
|
+ studentId: store.currentTask.studentId,
|
|
|
|
+ groups,
|
|
|
|
+ };
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+const saveTaskToServer = async () => {
|
|
|
|
+ if (!store.currentTask) return;
|
|
|
|
+ const markResult = store.currentTask.markResult;
|
|
|
|
+ if (!markResult) return;
|
|
|
|
+
|
|
|
|
+ const mkey = "save_task_key";
|
|
|
|
+
|
|
|
|
+ type SubmitError = {
|
|
|
|
+ question: Question;
|
|
|
|
+ index: number;
|
|
|
|
+ error: string;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const errors: SubmitError[] = [];
|
|
|
|
+ markResult.scoreList.forEach((score, index) => {
|
|
|
|
+ if (!store.currentTask) return;
|
|
|
|
+ const question = store.currentTask.questionList[index]!;
|
|
|
|
+ let error;
|
|
|
|
+ if (!isNumber(score)) {
|
|
|
|
+ error = `${question.mainNumber}-${question.subNumber}${
|
|
|
|
+ question.questionName ? "(" + question.questionName + ")" : ""
|
|
|
|
+ } 没有给分,不能提交。`;
|
|
|
|
+ } else if (isNumber(question.maxScore) && score > question.maxScore) {
|
|
|
|
+ error = `${question.mainNumber}-${question.subNumber}${
|
|
|
|
+ question.questionName ? "(" + question.questionName + ")" : ""
|
|
|
|
+ } 给分大于最高分不能提交。`;
|
|
|
|
+ } else if (isNumber(question.minScore) && score < question.minScore) {
|
|
|
|
+ error = `${question.mainNumber}-${question.subNumber}${
|
|
|
|
+ question.questionName ? "(" + question.questionName + ")" : ""
|
|
|
|
+ } 给分小于最低分不能提交。`;
|
|
|
|
+ }
|
|
|
|
+ if (error) {
|
|
|
|
+ errors.push({ question, index, error });
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ if (errors.length !== 0) {
|
|
|
|
+ console.log(errors);
|
|
|
|
+ const msg = errors.map((v) => h("div", `${v.error}`));
|
|
|
|
+ void message.warning({
|
|
|
|
+ content: h("span", ["校验失败", ...msg]),
|
|
|
|
+ duration: 10,
|
|
|
|
+ key: mkey,
|
|
|
|
+ });
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (
|
|
|
|
+ markResult.scoreList.length !== store.currentTask.questionList.length ||
|
|
|
|
+ !markResult.scoreList.every((s) => isNumber(s))
|
|
|
|
+ ) {
|
|
|
|
+ console.error({ content: "markResult格式不正确,缺少分数", key: mkey });
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (!store.isTrackMode) {
|
|
|
|
+ markResult.trackList = [];
|
|
|
|
+ } else {
|
|
|
|
+ const trackScores =
|
|
|
|
+ markResult.trackList
|
|
|
|
+ .map((t) => Math.round((t.score || 0) * 100))
|
|
|
|
+ .reduce((acc, s) => acc + s, 0) / 100;
|
|
|
|
+ if (trackScores !== markResult.markerScore) {
|
|
|
|
+ void message.error({
|
|
|
|
+ content: "轨迹分与总分不一致,请检查。",
|
|
|
|
+ duration: 3,
|
|
|
|
+ key: mkey,
|
|
|
|
+ });
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ console.log("save task to server");
|
|
|
|
+ void message.loading({ content: "保存检查任务...", key: mkey });
|
|
|
|
+
|
|
|
|
+ const data = getMarkData();
|
|
|
|
+ const res = await saveStudentSubjectiveConfirmData(data).catch(() => false);
|
|
|
|
+ if (!res) return;
|
|
|
|
+ if (res.data.success) {
|
|
|
|
+ void message.success({ content: "保存成功", key: mkey, duration: 2 });
|
|
|
|
+ store.currentTask = undefined;
|
|
|
|
+ } else {
|
|
|
|
+ console.log(res.data.message);
|
|
|
|
+ void message.error({ content: res.data.message, key: mkey, duration: 5 });
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ await getNextStudent();
|
|
|
|
+};
|
|
|
|
+</script>
|