ExamingHome.vue 22 KB

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