ExamingHome.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790
  1. <template>
  2. <div v-if="exam && examQuestion()" :key="exam.id" class="container">
  3. <div class="header">
  4. <RemainTime></RemainTime>
  5. <OverallProgress :exam-question-list="examQuestionList"></OverallProgress>
  6. <div>
  7. {{ this.$store.state.user.displayName }} -&nbsp;
  8. {{ this.$store.state.user.studentCodeList.join(",") }}
  9. </div>
  10. <QuestionFilters :exam-question-list="examQuestionList"></QuestionFilters>
  11. <i-button class="qm-primary-button" @click="submitPaper">交卷</i-button>
  12. </div>
  13. <div id="examing-home-question" class="main">
  14. <QuestionView :exam-question="examQuestion()"></QuestionView>
  15. <ArrowNavView
  16. :previous-question-order="previousQuestionOrder"
  17. :next-question-order="nextQuestionOrder"
  18. ></ArrowNavView>
  19. </div>
  20. <div :class="['side', 'side-row-size']">
  21. <div :class="['question-nav', !faceEnable && 'question-nav-long']">
  22. <QuestionNavView :paper-struct="paperStruct" />
  23. </div>
  24. <div v-if="faceEnable" class="camera">
  25. <FaceRecognition
  26. v-if="faceEnable"
  27. width="400"
  28. height="300"
  29. :show-recognize-button="false"
  30. />
  31. </div>
  32. </div>
  33. <Modal
  34. v-model="showFaceId"
  35. :mask-closable="false"
  36. :closable="false"
  37. width="800"
  38. :styles="{ top: '10px' }"
  39. >
  40. <FaceId v-if="showFaceId" @closeFaceId="closeFaceId" />
  41. <p slot="footer"></p>
  42. </Modal>
  43. <FaceTracking v-if="faceEnable && PRODUCTION" />
  44. </div>
  45. <div v-else>
  46. 正在等待数据返回...
  47. <i-button v-if="timeouted" class="qm-primary-button" @click="reloadPage">
  48. 重试
  49. </i-button>
  50. </div>
  51. </template>
  52. <script>
  53. import RemainTime from "./RemainTime.vue";
  54. import OverallProgress from "./OverallProgress.vue";
  55. import QuestionFilters from "./QuestionFilters.vue";
  56. import QuestionView from "./QuestionView.vue";
  57. import ArrowNavView from "./ArrowNavView.vue";
  58. import QuestionNavView from "./QuestionNavView.vue";
  59. import FaceTracking from "./FaceTracking.vue";
  60. import FaceId from "./FaceId.vue";
  61. import FaceRecognition from "../../../components/FaceRecognition/FaceRecognition";
  62. import { openWS, closeWsWithoutReconnect } from "./ws.js";
  63. import { createNamespacedHelpers } from "vuex";
  64. const { mapState, mapMutations } = createNamespacedHelpers("examingHomeModule");
  65. import { DOMAINS_CAN_UPLOAD_PHOTOS } from "@/constants/constants";
  66. export default {
  67. name: "ExamingHome",
  68. components: {
  69. RemainTime,
  70. OverallProgress,
  71. QuestionFilters,
  72. QuestionView,
  73. ArrowNavView,
  74. QuestionNavView,
  75. FaceRecognition,
  76. FaceId,
  77. FaceTracking,
  78. },
  79. data() {
  80. return {
  81. showFaceId: false,
  82. faceEnable: false,
  83. timeouted: false,
  84. PRODUCTION: process.env.NODE_ENV === "production",
  85. };
  86. },
  87. computed: {
  88. ...mapState([
  89. "exam",
  90. "paperStruct",
  91. "examQuestionList",
  92. "snapNow",
  93. "snapProcessingCount",
  94. "shouldSubmitPaper",
  95. "remainTime",
  96. "questionAnswerFileUrl",
  97. "uploadModalVisible",
  98. ]),
  99. previousQuestionOrder: vm => {
  100. if (vm.examQuestion().order > 1) {
  101. return vm.examQuestion().order - 1;
  102. } else {
  103. return null;
  104. }
  105. },
  106. nextQuestionOrder: vm => {
  107. if (vm.examQuestion().order < vm.examQuestionList.length) {
  108. return vm.examQuestion().order + 1;
  109. } else {
  110. return null;
  111. }
  112. },
  113. },
  114. watch: {
  115. $route: function() {
  116. this.examQuestion();
  117. },
  118. shouldSubmitPaper() {
  119. this.realSubmitPaper();
  120. },
  121. questionAnswerFileUrl(value) {
  122. // console.log(this.examQuestion.studentAnswer);
  123. // console.log("watch", value);
  124. const examRecordDataId = this.$route.params.examRecordDataId;
  125. const that = this;
  126. for (const q of value) {
  127. if (!q.saved) {
  128. let acknowledgeStatus = "CONFIRMED";
  129. // 目前只针对音频题有丢弃的可能
  130. if (
  131. q.transferFileType === "PIC" &&
  132. (q.order != this.$route.params.order || !this.uploadModalVisible)
  133. ) {
  134. acknowledgeStatus = "DISCARDED";
  135. }
  136. this.$http
  137. .post(
  138. "/api/ecs_oe_student/examControl/saveUploadedFileAcknowledgeStatus",
  139. {
  140. examRecordDataId,
  141. filePath: q.fileUrl,
  142. order: q.order,
  143. acknowledgeStatus,
  144. }
  145. )
  146. .then(() => {
  147. if (q.transferFileType === "AUDIO") {
  148. that.updateExamQuestion({
  149. order: q.order,
  150. studentAnswer: q.fileUrl,
  151. });
  152. } else if (
  153. acknowledgeStatus === "CONFIRMED" &&
  154. q.transferFileType === "PIC"
  155. ) {
  156. that.updatePicture(q);
  157. }
  158. q.saved = true;
  159. if (acknowledgeStatus === "CONFIRMED")
  160. this.$Message.info({
  161. content: "小程序作答已更新",
  162. duration: 5,
  163. closable: true,
  164. });
  165. })
  166. .catch(() => {
  167. this.$Message.error({
  168. content: "更新小程序答案失败!",
  169. duration: 15,
  170. closable: true,
  171. });
  172. });
  173. }
  174. }
  175. },
  176. // examQuestionList(val, oldVal) {
  177. // // console.log(val, oldVal);
  178. // }
  179. },
  180. async created() {
  181. this.timeoutTimeout = setTimeout(() => (this.timeouted = true), 30 * 1000);
  182. // 仅在线上使用活体检测
  183. if (
  184. process.env.NODE_ENV === "production" &&
  185. /^\d+$/.test(this.$route.query.faceVerifyMinute)
  186. ) {
  187. const enoughTimeForFaceId = this.remainTime // 如果remainTime取到了的话
  188. ? this.remainTime / (60 * 1000) - 1 > this.$route.query.faceVerifyMinute
  189. : true;
  190. if (!enoughTimeForFaceId) return;
  191. this.faceIdMsgTimeout = setTimeout(() => {
  192. // this.serverLog("debug/S-002001", "活体检测前抓拍");
  193. this.toggleSnapNow();
  194. this.$Message.info({
  195. content: "30秒后开始活体检测",
  196. duration: 15,
  197. closable: true,
  198. });
  199. }, this.$route.query.faceVerifyMinute * 60 * 1000 - 30 * 1000); // 活体检测提醒
  200. this.faceIdDivTimeout = setTimeout(() => {
  201. // this.serverLog("debug/S-003001", "准备弹出活体检测框");
  202. this.showFaceId = true;
  203. }, this.$route.query.faceVerifyMinute * 60 * 1000); // 定时做活体检测
  204. // }, 1 * 1000); // 定时做活体检测
  205. }
  206. // for test
  207. // setTimeout(() => {
  208. // this.showFaceId = true;
  209. // // this.$Modal.remove();
  210. // // }, this.$route.query.faceVerifyMinute * 60 * 1000); // 定时做活体检测
  211. // }, 5 * 1000); // 定时做活体检测
  212. let faceEnable;
  213. try {
  214. faceEnable = await this.$http.get(
  215. "/api/ecs_exam_work/exam/examOrgPropertyFromCache4StudentSession/" +
  216. this.$route.params.examId +
  217. `/IS_FACE_ENABLE`
  218. );
  219. } catch (error) {
  220. this.$Message.error({
  221. content: "获取人脸检测设置失败",
  222. duration: 15,
  223. closable: true,
  224. });
  225. this.logout("?LogoutReason=获取人脸检测设置失败");
  226. return;
  227. }
  228. if (
  229. faceEnable.data.IS_FACE_ENABLE &&
  230. JSON.parse(faceEnable.data.IS_FACE_ENABLE)
  231. ) {
  232. this.faceEnable = true;
  233. // setTimeout(() => {
  234. // this.serverLog("debug/S-002001", "进入考试后60秒内抓拍");
  235. // this.toggleSnapNow(); // 开启抓拍才在进入考试时抓拍一张
  236. // }, 60 * 1000); // 60内秒钟后抓拍
  237. let initSnapshotTrialTimes = 0;
  238. this.initSnapInterval = setInterval(() => {
  239. if (this.exam || initSnapshotTrialTimes > 12) {
  240. clearInterval(this.initSnapInterval);
  241. this.serverLog(
  242. "debug/S-002001",
  243. "进入考试后60秒内抓拍" + `(${(initSnapshotTrialTimes + 1) * 5}秒)`
  244. );
  245. this.toggleSnapNow(); // 开启抓拍才在进入考试时抓拍一张
  246. } else {
  247. initSnapshotTrialTimes++;
  248. }
  249. }, 15 * 1000);
  250. // let initSnapshotTrialTimes = 0;
  251. // const initSnapshot = setTimeout(() => {
  252. // if (this.exam || initSnapshotTrialTimes < 6) {
  253. // this.serverLog("debug/S-002001", "进入考试后60秒内抓拍");
  254. // this.toggleSnapNow(); // 开启抓拍才在进入考试时抓拍一张
  255. // } else {
  256. // setTimeout(() => initSnapshot(), 5 * 1000);
  257. // }
  258. // }, 5 * 1000); // 60内秒钟后抓拍
  259. try {
  260. const snapshotInterval = await this.$http.get(
  261. "/api/ecs_exam_work/exam/examOrgPropertyFromCache4StudentSession/" +
  262. this.$route.params.examId +
  263. `/SNAPSHOT_INTERVAL`
  264. );
  265. if (snapshotInterval.data) {
  266. // 考务设置抓拍间隔
  267. this.snapInterval = setInterval(() => {
  268. this.serverLog(
  269. "debug/S-002001",
  270. "根据抓拍间隔抓拍:抓拍间隔=" +
  271. snapshotInterval.data.SNAPSHOT_INTERVAL +
  272. "分钟"
  273. );
  274. this.toggleSnapNow();
  275. }, snapshotInterval.data.SNAPSHOT_INTERVAL * 60 * 1000);
  276. }
  277. } catch (error) {
  278. this.$Message.error({
  279. content: "获取人脸抓拍间隔设置失败",
  280. duration: 15,
  281. closable: true,
  282. });
  283. this.logout("?LogoutReason=获取人脸抓拍间隔设置失败");
  284. return;
  285. }
  286. }
  287. try {
  288. await this.initData();
  289. } catch (error) {
  290. this.$Message.error({
  291. content: "获取试卷信息失败,退出登录",
  292. duration: 15,
  293. closable: true,
  294. });
  295. this.logout("?LogoutReason=获取试卷信息失败");
  296. return;
  297. }
  298. this.submitInterval = setInterval(
  299. () => this.answerAllQuestions(),
  300. 5 * 1000 // 10秒检查是否有更改需要提交答案
  301. );
  302. },
  303. async mounted() {
  304. // iview bug: https://github.com/iview/iview/issues/4061
  305. // document.body.style = "";
  306. window._hmt.push(["_trackEvent", "正在考试页面", "进入页面"]);
  307. if (typeof nodeRequire != "undefined") {
  308. try {
  309. var fs = window.nodeRequire("fs");
  310. if (fs.existsSync("multiCamera.exe")) {
  311. await new Promise((resolve, reject) => {
  312. window.nodeRequire("node-cmd").get("multiCamera.exe", () => {
  313. try {
  314. let cameraInfos = fs.readFileSync("CameraInfo.txt", "utf-8");
  315. if (cameraInfos && cameraInfos.trim()) {
  316. cameraInfos = cameraInfos.trim();
  317. cameraInfos = cameraInfos.replace(/\r\n/g, "");
  318. cameraInfos = cameraInfos.replace(/\n/g, "");
  319. console.log(cameraInfos);
  320. this.serverLog("debug/S-001001", cameraInfos);
  321. }
  322. resolve();
  323. } catch (error) {
  324. reject("读取摄像头列表失败");
  325. }
  326. });
  327. });
  328. }
  329. } catch (error) {
  330. console.log(error);
  331. }
  332. }
  333. },
  334. beforeRouteUpdate(from, to, next) {
  335. window._hmt.push(["_trackEvent", "正在考试页面", "题目切换"]);
  336. if (process.env.NODE_ENV === "development") {
  337. console.log("beforeRouteUpdate from: " + this.$route.fullPath);
  338. }
  339. this.answerAllQuestions();
  340. next();
  341. },
  342. beforeDestroy() {
  343. clearTimeout(this.timeoutTimeout);
  344. clearInterval(this.submitInterval);
  345. clearInterval(this.initSnapInterval);
  346. clearInterval(this.snapInterval);
  347. clearTimeout(this.faceIdMsgTimeout);
  348. clearTimeout(this.faceIdDivTimeout);
  349. closeWsWithoutReconnect();
  350. this.updateExamState({
  351. exam: null,
  352. paperStruct: null,
  353. examQuestionList: null,
  354. questionAnswerFileUrl: [],
  355. pictureAnswer: {},
  356. });
  357. this.$Modal.remove();
  358. },
  359. // beforeRouteUpdate(to, from, next) {
  360. // this.updateQuestion(next);
  361. // },
  362. methods: {
  363. ...mapMutations([
  364. "updateExamState",
  365. "updateExamQuestion",
  366. "toggleSnapNow",
  367. "updateExamResult",
  368. "resetExamQuestionDirty",
  369. "updatePicture",
  370. ]),
  371. async initData() {
  372. const [
  373. examData,
  374. paperStructData,
  375. examQuestionListData,
  376. ] = await Promise.all([
  377. this.$http.get("/api/ecs_exam_work/exam/" + this.$route.params.examId),
  378. this.$http.get(
  379. "/api/ecs_oe_student/examRecordPaperStruct/getExamRecordPaperStruct?examRecordDataId=" +
  380. this.$route.params.examRecordDataId
  381. ),
  382. this.$http.get("/api/ecs_oe_student/examQuestion/findExamQuestionList"),
  383. ]);
  384. const [exam, paperStruct] = [examData.data, paperStructData.data];
  385. let examQuestionList = examQuestionListData.data;
  386. if (
  387. exam === undefined ||
  388. paperStruct === undefined ||
  389. examQuestionListData === undefined
  390. ) {
  391. this.$Message.error({
  392. content: "获取试卷信息失败",
  393. duration: 15,
  394. closable: true,
  395. });
  396. this.logout("?LogoutReason=获取试卷信息失败");
  397. return;
  398. }
  399. if (exam.examType === "PRACTICE") {
  400. const practiceType = (await this.$http.get(
  401. "/api/ecs_exam_work/exam/examOrgPropertyFromCache4StudentSession/" +
  402. this.$route.params.examId +
  403. `/PRACTICE_TYPE`
  404. )).data;
  405. this.practiceType = practiceType.PRACTICE_TYPE; // IN_PRACTICE NO_ANSWER
  406. exam.practiceType = practiceType.PRACTICE_TYPE;
  407. }
  408. try {
  409. let freezeTimeData = await this.$http.get(
  410. "/api/ecs_exam_work/exam/examOrgPropertyFromCache4StudentSession/" +
  411. this.$route.params.examId +
  412. `/FREEZE_TIME`
  413. );
  414. exam.freezeTime =
  415. freezeTimeData.data.FREEZE_TIME &&
  416. JSON.parse(freezeTimeData.data.FREEZE_TIME);
  417. } catch (error) {
  418. console.log("获取考试冻结时间失败--忽略");
  419. }
  420. // parentQuestionBody
  421. // questionUnitWrapperList
  422. // questionBody => from examQuestionList
  423. // questionUnitList =>
  424. // studentAnswer
  425. // rightAnswer
  426. // init subNumber
  427. let questionId = null;
  428. let i = 1;
  429. examQuestionList = examQuestionList.map(eq => {
  430. if (questionId == eq.questionId) {
  431. eq.subNumber = i++;
  432. } else {
  433. i = 1;
  434. questionId = eq.questionId;
  435. eq.subNumber = i++;
  436. }
  437. return eq;
  438. });
  439. let groupOrder = 1;
  440. let mainNumber = 0;
  441. examQuestionList = examQuestionList.map(eq => {
  442. if (mainNumber == eq.mainNumber) {
  443. eq.groupOrder = groupOrder++;
  444. } else {
  445. mainNumber = eq.mainNumber;
  446. groupOrder = 1;
  447. eq.groupOrder = groupOrder++;
  448. }
  449. const questionWrapperList =
  450. paperStruct.defaultPaper.questionGroupList[eq.mainNumber - 1]
  451. .questionWrapperList;
  452. const groupName =
  453. paperStruct.defaultPaper.questionGroupList[eq.mainNumber - 1]
  454. .groupName;
  455. const groupTotal = questionWrapperList.reduce(
  456. (accumulator, questionWrapper) =>
  457. accumulator + questionWrapper.questionUnitWrapperList.length,
  458. 0
  459. );
  460. eq.groupName = groupName;
  461. eq.groupTotal = groupTotal;
  462. return eq;
  463. });
  464. examQuestionList = examQuestionList.map(eq => {
  465. const paperStructQuestion = paperStruct.defaultPaper.questionGroupList[
  466. eq.mainNumber - 1
  467. ].questionWrapperList.find(q => q.questionId === eq.questionId);
  468. return Object.assign(eq, {
  469. limitedPlayTimes: paperStructQuestion.limitedPlayTimes,
  470. });
  471. });
  472. this.updateExamState({
  473. exam: exam,
  474. paperStruct: paperStruct,
  475. examQuestionList: examQuestionList,
  476. allAudioPlayTimes: JSON.parse(examQuestionList[0].audioPlayTimes) || [],
  477. questionAnswerFileUrl: [],
  478. pictureAnswer: {},
  479. });
  480. // console.log(examQuestionList);
  481. // console.log(examQuestionList.find(v => v.answerType === "SINGLE_AUDIO"));
  482. const shouldOpenWS =
  483. examQuestionList.find(v => v.answerType === "SINGLE_AUDIO") ||
  484. DOMAINS_CAN_UPLOAD_PHOTOS.includes(this.$store.state.user.schoolDomain);
  485. if (shouldOpenWS) {
  486. // console.log("have single");
  487. const examRecordDataId = this.$route.params.examRecordDataId;
  488. openWS({ examRecordDataId });
  489. }
  490. },
  491. updateQuestion: async function(next) {
  492. // 初始化套题的答案,为回填部分选项做准备
  493. // for (let q of this.examQuestionList) {
  494. // if (q.subQuestionList.length > 0) {
  495. // q.studentAnswer = [];
  496. // for (let sq of q.subQuestionList) {
  497. // q.studentAnswer.push(sq.studentAnswer);
  498. // }
  499. // }
  500. // }
  501. next && next();
  502. if (!this.exam) return;
  503. },
  504. closeFaceId() {
  505. this.showFaceId = false;
  506. },
  507. async answerAllQuestions(ignoreDirty) {
  508. const answers = this.examQuestionList
  509. .filter(eq => (ignoreDirty ? true : eq.dirty))
  510. .filter(eq => eq.getQuestionContent)
  511. .map(eq => {
  512. return Object.assign(
  513. {
  514. order: eq.order,
  515. studentAnswer: eq.studentAnswer,
  516. },
  517. eq.audioPlayTimes && { audioPlayTimes: eq.audioPlayTimes },
  518. eq.isSign && { isSign: eq.isSign }
  519. );
  520. });
  521. if (answers.length > 0) {
  522. try {
  523. await this.$http.post(
  524. "/api/ecs_oe_student/examQuestion/submitQuestionAnswer",
  525. answers
  526. );
  527. this.resetExamQuestionDirty();
  528. // 提交成功,返回true,供最后提交时判断。自动提交失败,不暂停。
  529. return true;
  530. } catch (error) {
  531. console.log(error);
  532. this.$Message.error({
  533. content: "提交答案失败",
  534. duration: 15,
  535. closable: true,
  536. });
  537. window._hmt.push([
  538. "_trackEvent",
  539. "正在考试页面",
  540. "提交答案失败",
  541. error.message +
  542. " |||| " +
  543. (((error.response || {}).data || {}).desc || ""),
  544. ]);
  545. this.serverLog(
  546. "debug/S-008001",
  547. `提交答案失败 => 考试剩余时间:${this.remainTime / 1000}`
  548. );
  549. }
  550. }
  551. },
  552. async submitPaper() {
  553. try {
  554. // 交卷前强制提交所有答案
  555. const ret = await this.answerAllQuestions(true);
  556. if (!ret) {
  557. // 提交答案失败,停止交卷逻辑。
  558. return;
  559. }
  560. } catch (error) {
  561. return;
  562. }
  563. if (
  564. this.exam.freezeTime &&
  565. this.remainTime >
  566. (this.exam.duration - this.exam.freezeTime) * 60 * 1000
  567. ) {
  568. this.$Message.info({
  569. content: `考试开始${this.exam.freezeTime}分钟后才允许交卷。`,
  570. duration: 5,
  571. closable: true,
  572. });
  573. return;
  574. }
  575. const answered = this.examQuestionList.filter(
  576. q => q.studentAnswer !== null
  577. ).length;
  578. const unanswered = this.examQuestionList.filter(
  579. q => q.studentAnswer === null
  580. ).length;
  581. const signed = this.examQuestionList.filter(q => q.isSign).length;
  582. const showConfirmTime = Date.now();
  583. this.$Modal.confirm({
  584. title: "确认交卷",
  585. content: `<p>已答题目:${answered}</p><p>未答题目:${unanswered}</p><p>标记题目:${signed}</p>`,
  586. onOk: () => {
  587. this.realSubmitPaper(showConfirmTime);
  588. },
  589. });
  590. },
  591. async realSubmitPaper(showConfirmTime = 0) {
  592. this.$Spin.show({
  593. render: () => {
  594. return <div style="font-size: 44px">正在交卷,请耐心等待...</div>;
  595. },
  596. });
  597. if (this.faceEnable) {
  598. this.serverLog("debug/S-002001", "交卷前抓拍");
  599. this.toggleSnapNow();
  600. }
  601. // 确保抓拍指令在交卷前执行,同时确保5秒间隔提交答案的指令执行了
  602. let delay = 5 - (Date.now() - showConfirmTime) / 1000;
  603. if (delay < 0) {
  604. // 如果用户已经看确认框超过5秒,或者不是由确认框进来的,不延迟
  605. delay = 0;
  606. }
  607. // 给抓拍照片多一秒处理时间
  608. delay = delay + 1;
  609. setTimeout(() => this.realSubmitPaperStep2(), delay * 1000);
  610. this.submitCount = 1;
  611. },
  612. async realSubmitPaperStep2() {
  613. if (this.snapProcessingCount > 0) {
  614. this.submitCount++;
  615. if (this.submitCount < 200) {
  616. // 一分钟后,强制交卷
  617. setTimeout(() => this.realSubmitPaperStep2(), 300);
  618. return;
  619. }
  620. }
  621. if (this.submitLock) {
  622. return;
  623. } else {
  624. this.submitLock = true;
  625. }
  626. if (this.$route.name !== "OnlineExamingHome") {
  627. // 非考试页,不在交卷
  628. this.$Spin.hide();
  629. return;
  630. }
  631. try {
  632. const examId = this.$route.params.examId;
  633. const examRecordDataId = this.$route.params.examRecordDataId;
  634. const res = await this.$http.get(
  635. "/api/ecs_oe_student/examControl/endExam"
  636. );
  637. if (res.status === 200) {
  638. this.$router.replace({
  639. path: `/online-exam/exam/${examId}/examRecordData/${examRecordDataId}/end`,
  640. });
  641. // 确保交卷成功后,不会再次交卷
  642. this.submitLock = true;
  643. this.$Spin.hide();
  644. return;
  645. } else {
  646. this.$Message.error({
  647. content: "交卷失败",
  648. duration: 15,
  649. closable: true,
  650. });
  651. }
  652. this.submitLock = false;
  653. } catch (e) {
  654. this.$Message.error({
  655. content: "交卷失败",
  656. duration: 15,
  657. closable: true,
  658. });
  659. console.log(e);
  660. }
  661. this.submitLock = false;
  662. this.$Spin.hide();
  663. },
  664. examQuestion() {
  665. return (
  666. this.examQuestionList &&
  667. this.examQuestionList.find(
  668. eq => eq.order == this.$route.params.order // number == string
  669. )
  670. );
  671. },
  672. reloadPage() {
  673. window._hmt.push([
  674. "_trackEvent",
  675. "正在考试页面",
  676. "页面加载失败",
  677. "reload",
  678. ]);
  679. window.location.reload();
  680. },
  681. },
  682. };
  683. </script>
  684. <style scoped>
  685. .container {
  686. display: grid;
  687. grid-template-areas:
  688. "header header"
  689. "main side";
  690. grid-template-rows: 80px minmax(0, 1fr);
  691. grid-template-columns: 1fr 400px;
  692. height: 100vh;
  693. width: 100vw;
  694. }
  695. .header {
  696. display: grid;
  697. align-items: center;
  698. justify-items: center;
  699. grid-template-columns: 200px 280px 1fr 300px 100px;
  700. grid-area: header;
  701. height: 80px;
  702. background-color: #f5f5f5;
  703. }
  704. .main {
  705. display: grid;
  706. grid-area: main;
  707. grid-template-rows: 1fr 50px;
  708. }
  709. .side {
  710. display: grid;
  711. grid-area: side;
  712. grid-template-rows: 1fr 250px;
  713. background-color: #f5f5f5;
  714. }
  715. .side-row-size {
  716. grid-template-rows: 1fr;
  717. }
  718. .question-nav {
  719. overflow-y: scroll;
  720. max-height: calc(100vh - 300px);
  721. }
  722. .question-nav-long {
  723. max-height: calc(100vh - 100px);
  724. }
  725. .camera {
  726. align-self: flex-end;
  727. justify-self: flex-end;
  728. }
  729. @media screen and (max-height: 768px) {
  730. .container {
  731. grid-template-rows: 50px minmax(0, 1fr);
  732. }
  733. .header {
  734. height: 50px;
  735. }
  736. .side {
  737. grid-template-rows: minmax(0, 1fr) 200px;
  738. }
  739. }
  740. </style>
  741. <style>
  742. #examing-home-question img {
  743. max-width: 100%;
  744. height: auto !important;
  745. }
  746. </style>