ConfirmPaper.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. <template>
  2. <div class="mark confirm-paper">
  3. <div class="mark-header">
  4. <div v-if="student" class="mark-header-part">
  5. <div class="header-noun">
  6. <span>课程名称:</span>
  7. <span> {{ student.courseName }}({{ student.courseCode }})</span>
  8. </div>
  9. <div class="header-noun">
  10. <span>试卷编号:</span>
  11. <span>{{ student.paperNumber }}</span>
  12. </div>
  13. <div class="header-noun">
  14. <span>姓名:</span>
  15. <span>{{ student.studentName }}</span>
  16. </div>
  17. <div class="header-noun">
  18. <span>学号:</span>
  19. <span>{{ student?.studentCode }}</span>
  20. </div>
  21. <div v-if="studentIds.length > 1" class="header-noun">
  22. <span>进度:</span>
  23. <span> {{ currentIndex }}/{{ studentIds.length }} </span>
  24. </div>
  25. </div>
  26. <div class="mark-header-part">
  27. <div class="paper-menu">
  28. <span
  29. v-for="(u, index) in student?.sheetUrls"
  30. :key="index"
  31. :class="{ 'is-active': currentImage === index }"
  32. @click="currentImage = index"
  33. >
  34. {{ index + 1 }}
  35. </span>
  36. </div>
  37. </div>
  38. </div>
  39. <div class="mark-main">
  40. <div class="mark-body">
  41. <div
  42. v-if="student && currentImage !== 0"
  43. class="page-action page-prev"
  44. title="上一张"
  45. @click="switchImageArrow({ left: true })"
  46. >
  47. <ArrowLeftOutlined />
  48. </div>
  49. <div
  50. v-if="student && currentImage !== student.sheetUrls.length - 1"
  51. class="page-action page-next"
  52. title="上一张"
  53. @click="switchImageArrow({ right: true })"
  54. >
  55. <ArrowRightOutlined />
  56. </div>
  57. <div class="mark-body-container">
  58. <div v-if="!student" class="mark-body-none">
  59. <div>
  60. <img src="@/assets/image-none-task.png" />
  61. <p>暂无数据</p>
  62. </div>
  63. </div>
  64. <div
  65. v-else
  66. class="single-image-container"
  67. :style="{ width: answerPaperScale }"
  68. >
  69. <img
  70. draggable="false"
  71. :src="curImageUrl"
  72. :style="{
  73. transform:
  74. (rotateDegree ? 'translate( 0, calc(30vh))' : '') +
  75. `rotate(${rotateDegree}deg)`,
  76. }"
  77. @click="switchImage"
  78. @contextmenu="showBigImage"
  79. />
  80. </div>
  81. </div>
  82. <ZoomPaper v-if="student" showRotate fixed @rotateRight="rotateRight" />
  83. </div>
  84. <div class="mark-board-track">
  85. <div class="paper-topics">
  86. <div
  87. v-for="group in answersComputed"
  88. :key="group.mainNumber"
  89. class="paper-topic"
  90. >
  91. <h2 class="paper-topic-title">
  92. {{ group.mainNumber }}、{{ group.mainTitle }} ({{
  93. group.subs.length
  94. }})
  95. </h2>
  96. <div class="paper-topic-body">
  97. <div
  98. v-for="question in group.subs"
  99. :key="question.subNumber"
  100. class="paper-topic-question"
  101. >
  102. <span class="question-number">{{ question.subNumber }} </span>
  103. <a-input
  104. class="normal-input"
  105. :class="{
  106. 'long-input': question.type
  107. ? !['SINGLE', 'TRUE_OR_FALSE'].includes(question.type)
  108. : !group.mainTitle.match(/单选|单项|判断/),
  109. }"
  110. :value="question.answer"
  111. :maxLength="
  112. (
  113. question.type
  114. ? ['MULTIPLE'].includes(question.type)
  115. : group.mainTitle.match(/多选|多项|不定项/)
  116. )
  117. ? 100
  118. : 1
  119. "
  120. @keydown="onPreventAnswerKey"
  121. @input="changeAnswer($event, question)"
  122. @blur="changeAnswer($event, question, '#')"
  123. />
  124. </div>
  125. </div>
  126. </div>
  127. </div>
  128. <div class="board-tips">
  129. <p v-if="!allViewed">请先浏览完该学生的所有试卷</p>
  130. </div>
  131. <div :class="['board-footer', { 'is-simple': !isMultiStudent }]">
  132. <a-button
  133. class="board-submit"
  134. size="medium"
  135. type="primary"
  136. :disabled="!allViewed || !student?.upload"
  137. @click="saveStudentAnswer"
  138. >
  139. 保存
  140. </a-button>
  141. <div v-if="isMultiStudent" class="student-switch">
  142. <a-button :disabled="isFirst" @click="getPreviousStudent">
  143. 上一份
  144. </a-button>
  145. <a-button :disabled="isLast" @click="getNextStudent">
  146. 下一份
  147. </a-button>
  148. </div>
  149. </div>
  150. </div>
  151. </div>
  152. </div>
  153. </template>
  154. <script lang="ts" setup>
  155. import {
  156. studentObjectiveConfirmData,
  157. saveStudentObjectiveConfirmData,
  158. } from "@/api/confirmPage";
  159. import { message } from "ant-design-vue";
  160. import { onMounted, watch } from "vue";
  161. import "viewerjs/dist/viewer.css";
  162. import Viewer from "viewerjs";
  163. import { store } from "@/store/store";
  164. import ZoomPaper from "@/components/ZoomPaper.vue";
  165. import { useTimers } from "@/setups/useTimers";
  166. import { ArrowLeftOutlined, ArrowRightOutlined } from "@ant-design/icons-vue";
  167. import vls from "@/utils/storage";
  168. import { StudentObjectiveInfo } from "@/types";
  169. const { addTimeout } = useTimers();
  170. const studentIds = $ref(vls.get("check-students", []));
  171. onMounted(async () => {
  172. if (studentIds.length === 0) {
  173. void message.info("没有需要处理的考生,请返回。");
  174. return;
  175. }
  176. await getNextStudent();
  177. });
  178. let currentStudentId = $ref("");
  179. const currentIndex = $computed(() => studentIds.indexOf(currentStudentId));
  180. const isFirst = $computed(() => currentIndex === 0);
  181. const isLast = $computed(() => currentIndex === studentIds.length - 1);
  182. const isMultiStudent = $computed(() => studentIds.length > 1);
  183. const curImageUrl = $computed(() =>
  184. student ? student.sheetUrls[currentImage].url : ""
  185. );
  186. let student: StudentObjectiveInfo | null = $ref(null);
  187. /** 后台数据错误,停止整个页面的流程 */
  188. let dataError = $ref(false);
  189. const answersComputed = $computed(() => {
  190. let mains = student?.answers.map((v) => ({
  191. mainTitle: "",
  192. mainNumber: v.mainNumber,
  193. subs: [v],
  194. }));
  195. const mSet = new Set();
  196. mains = mains?.filter((v) => {
  197. if (!mSet.has(v.mainNumber)) {
  198. mSet.add(v.mainNumber);
  199. v.subs = [];
  200. return true;
  201. }
  202. });
  203. mains?.forEach((v) => {
  204. v.mainTitle = student?.titles[v.mainNumber] ?? "";
  205. v.subs =
  206. student?.answers.filter((v2) => v2.mainNumber === v.mainNumber) ?? [];
  207. });
  208. return mains;
  209. });
  210. async function getNextStudent() {
  211. if (isLast) return;
  212. student = await getStudent(studentIds[currentIndex + 1]);
  213. }
  214. async function getPreviousStudent() {
  215. if (isFirst) return;
  216. student = await getStudent(studentIds[currentIndex - 1]);
  217. }
  218. async function getStudent(studentId: string) {
  219. const res = await studentObjectiveConfirmData(studentId).catch(() => {
  220. dataError = true;
  221. });
  222. if (dataError) {
  223. void message.error(res.message, 24 * 60 * 60);
  224. throw new Error("取学生信息出错: " + res.message);
  225. }
  226. const stu = res.data;
  227. stu.sheetUrls = [
  228. { index: 1, url: "/1-1.jpg" },
  229. { index: 2, url: "/1-2.jpg" },
  230. ];
  231. currentStudentId = stu.studentId;
  232. currentImage = 0;
  233. browsedImageIndexes = [0];
  234. return stu;
  235. }
  236. const allowKey = [
  237. "Delete",
  238. "Backspace",
  239. "ArrowLeft",
  240. "ArrowRight",
  241. "#",
  242. "Shift",
  243. "[A-Za-z]",
  244. ];
  245. const allowKeyRef = new RegExp(allowKey.join("|"));
  246. function onPreventAnswerKey(e: KeyboardEvent) {
  247. console.log(e);
  248. if (!allowKeyRef.test(e.key)) {
  249. e.preventDefault();
  250. }
  251. }
  252. function changeAnswer(event: Event, question: string, defaultValue?: string) {
  253. const target = event.target as HTMLInputElement;
  254. student.answers = student.answers.map((v) => {
  255. if (
  256. v.mainNumber === question.mainNumber &&
  257. v.subNumber === question.subNumber
  258. ) {
  259. v.answer = target?.value.toUpperCase().trim() || defaultValue || "";
  260. }
  261. return v;
  262. });
  263. }
  264. let loading = false;
  265. async function saveStudentAnswer() {
  266. if (!student) return;
  267. if (loading) return;
  268. loading = true;
  269. const data = {
  270. studentId: student.studentId,
  271. answers: student.answers.map((v) => v.answer || "#").join(","),
  272. };
  273. // if (!answers.match(/^(#*,*[A-Z]*)+$/g)) {
  274. // void message.error("答案只能是#和大写英文字母");
  275. // return;
  276. // }
  277. const res = await saveStudentObjectiveConfirmData(data).catch(() => false);
  278. loading = false;
  279. if (!res) {
  280. void message.error("保存失败,请刷新页面。");
  281. } else {
  282. void message.success("保存成功");
  283. await getNextStudent();
  284. }
  285. }
  286. //#region : 显示大图,供查看和翻转
  287. let currentImage = $ref(0);
  288. let browsedImageIndexes = $ref([0]);
  289. let allViewed = $computed(() => {
  290. let indexes = Array.from(new Set(browsedImageIndexes));
  291. return indexes.length == (student?.sheetUrls || []).length;
  292. });
  293. watch(
  294. () => currentImage,
  295. () => {
  296. browsedImageIndexes.push(currentImage);
  297. }
  298. );
  299. function switchImageArrow({
  300. left = false,
  301. right = false,
  302. }: {
  303. left?: boolean;
  304. right?: boolean;
  305. }) {
  306. if (left) {
  307. if (currentImage > 0) {
  308. currentImage--;
  309. }
  310. }
  311. if (right) {
  312. if (currentImage < student.sheetUrls.length - 1) {
  313. currentImage++;
  314. }
  315. }
  316. }
  317. function switchImage(event: MouseEvent) {
  318. const image = event.target as HTMLImageElement;
  319. const layerX: number = (event as any).layerX;
  320. if (layerX * 2 < image.width) {
  321. if (currentImage > 0) {
  322. currentImage--;
  323. }
  324. } else {
  325. if (currentImage < student.sheetUrls.length - 1) {
  326. currentImage++;
  327. }
  328. }
  329. }
  330. const showBigImage = (event: MouseEvent) => {
  331. event.preventDefault();
  332. // console.log(event);
  333. let viewer: Viewer = null as unknown as Viewer;
  334. viewer && viewer.destroy();
  335. viewer = new Viewer((event.target as HTMLElement).parentElement, {
  336. // inline: true,
  337. viewed() {
  338. viewer.zoomTo(1);
  339. },
  340. hidden() {
  341. viewer.destroy();
  342. },
  343. zIndex: 1000000,
  344. });
  345. viewer.show();
  346. };
  347. //#endregion : 显示大图,供查看和翻转
  348. //#region : 放大缩小和之后的滚动
  349. const answerPaperScale = $computed(() => {
  350. // 放大、缩小不影响页面之前的滚动条定位
  351. let percentWidth = 0;
  352. let percentTop = 0;
  353. const container = document.querySelector<HTMLDivElement>(
  354. ".mark-body-container"
  355. );
  356. if (container) {
  357. const { scrollLeft, scrollTop, scrollWidth, scrollHeight } = container;
  358. percentWidth = scrollLeft / scrollWidth;
  359. percentTop = scrollTop / scrollHeight;
  360. }
  361. addTimeout(() => {
  362. if (container) {
  363. const { scrollWidth, scrollHeight } = container;
  364. container.scrollTo({
  365. left: scrollWidth * percentWidth,
  366. top: scrollHeight * percentTop,
  367. });
  368. }
  369. }, 10);
  370. const scale = store.setting.uiSetting["answer.paper.scale"];
  371. return scale * 100 + "%";
  372. });
  373. //#endregion : 放大缩小和之后的滚动
  374. //#region rotateRight
  375. let rotateDegree = $ref(0);
  376. function rotateRight() {
  377. rotateDegree = (rotateDegree + 90) % 360;
  378. }
  379. //#endregion
  380. </script>