OnlineExamList.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. <template>
  2. <div class="list">
  3. <table>
  4. <tbody class="list-row">
  5. <tr class="list-header qm-primary-strong-text">
  6. <td>课程</td>
  7. <td v-if="!isEpcc" key="cc">层次</td>
  8. <td v-if="!isEpcc" key="zy">专业</td>
  9. <td>考试开放时间</td>
  10. <td>剩余考试次数</td>
  11. <td style="max-width: 200px">操作</td>
  12. </tr>
  13. <tr v-for="course in courses" :key="course.examId + course.courseId">
  14. <td>{{ course.courseName }}</td>
  15. <td v-if="!isEpcc" key="cc">{{ course.courseLevel }}</td>
  16. <td v-if="!isEpcc" key="zy">{{ course.specialtyName }}</td>
  17. <td>
  18. {{ course.startTime }} <br />
  19. ~ <br />
  20. {{ course.endTime }}
  21. </td>
  22. <td>{{ course.allowExamCount }}</td>
  23. <td style="min-width: 180px">
  24. <div
  25. style="display: grid; grid-template-columns: repeat( auto-fit, minmax(100px, 1fr) );
  26. grid-gap: 10px"
  27. >
  28. <i-button
  29. class="qm-primary-button qm-primary-button-padding-fix"
  30. :disabled="disableTheCourse(course)"
  31. @click="
  32. () => {
  33. if (isEpcc) {
  34. raceEnter(course);
  35. } else {
  36. enterExam(course);
  37. }
  38. }
  39. "
  40. >
  41. 进入考试{{ isEpcc && countdown > 0 ? `(${countdown})` : "" }}
  42. </i-button>
  43. <i-poptip
  44. v-if="!isEpcc"
  45. :trigger="course.isObjScoreView ? 'hover' : 'click'"
  46. placement="left"
  47. class="online-exam-list-override-poptip"
  48. @on-popper-show="cid = course.courseId"
  49. @on-popper-hide="cid = null"
  50. >
  51. <i-button
  52. class="qm-primary-button qm-primary-button-padding-fix"
  53. style="width: 100%"
  54. :disabled="!course.isObjScoreView"
  55. >
  56. 客观分
  57. </i-button>
  58. <ecs-online-exam-result-list
  59. slot="content"
  60. :popper-show="cid === course.courseId"
  61. :exam-student-id="course.examStudentId"
  62. ></ecs-online-exam-result-list>
  63. </i-poptip>
  64. </div>
  65. </td>
  66. </tr>
  67. </tbody>
  68. </table>
  69. <Spin v-if="spinShow" size="large" fix>{{ processingMessage }}</Spin>
  70. <OnlineExamFaceCheckModal
  71. :open="faceCheckModalOpen"
  72. :course="selectedCourse"
  73. ></OnlineExamFaceCheckModal>
  74. <Modal
  75. ref="checkEnvModal"
  76. v-model="shouldShowCheckEnvModal"
  77. title="环境检测"
  78. footer-hide
  79. width="800"
  80. :closable="false"
  81. :mask-closable="false"
  82. >
  83. <CheckComputer
  84. v-if="shouldShowCheckEnvModal"
  85. @on-close="resumeEnterExam"
  86. />
  87. </Modal>
  88. </div>
  89. </template>
  90. <script>
  91. import { createNamespacedHelpers } from "vuex";
  92. import OnlineExamResultList from "./OnlineExamResultList.vue";
  93. import OnlineExamFaceCheckModal from "./OnlineExamFaceCheckModal.vue";
  94. import moment from "moment";
  95. import {
  96. mapState as globalMapState,
  97. mapGetters as globalMapGetters,
  98. } from "vuex";
  99. const { mapState, mapMutations } = createNamespacedHelpers("examHomeModule");
  100. import CheckComputer from "./CheckComputer";
  101. export default {
  102. name: "EcsOnlineList",
  103. components: {
  104. "ecs-online-exam-result-list": OnlineExamResultList,
  105. OnlineExamFaceCheckModal,
  106. CheckComputer,
  107. },
  108. props: {
  109. courses: {
  110. type: Array,
  111. default() {
  112. return [];
  113. },
  114. },
  115. },
  116. data() {
  117. return {
  118. now: new Date(),
  119. selectedCourse: null,
  120. spinShow: false,
  121. processingMessage: "",
  122. cid: null,
  123. shouldShowCheckEnvModal: false,
  124. countdown: 0,
  125. };
  126. },
  127. computed: {
  128. ...globalMapState(["user", "timeDifference"]),
  129. ...mapState(["faceCheckModalOpen"]),
  130. ...globalMapGetters(["isEpcc"]),
  131. },
  132. created() {
  133. this.getNow();
  134. this.intervalID = setInterval(() => this.getNow(), 1000);
  135. },
  136. beforeDestroy() {
  137. this.toggleFaceCheckModal(false);
  138. clearInterval(this.intervalID);
  139. clearInterval(this.countdownInterval);
  140. },
  141. methods: {
  142. ...mapMutations(["toggleFaceCheckModal"]),
  143. getNow() {
  144. this.now = Date.now() + this.timeDifference;
  145. },
  146. courseInBetween(course) {
  147. return moment(this.now).isBetween(
  148. moment(course.startTime),
  149. moment(course.endTime)
  150. );
  151. },
  152. disableTheCourse(course) {
  153. return (
  154. !this.courseInBetween(course) ||
  155. course.allowExamCount < 1 ||
  156. (this.isEpcc && this.countdown > 0)
  157. );
  158. },
  159. raceEnter(course) {
  160. const successRatePerMinute = [
  161. 0.1,
  162. 0.15,
  163. 0.2,
  164. 0.25,
  165. 0.3,
  166. 0.35,
  167. 0.4,
  168. 0.45,
  169. 0.5,
  170. 0.6,
  171. 0.7,
  172. 0.8,
  173. 0.9,
  174. 1,
  175. ]; // 30秒步进
  176. const minutesAfterCourseStart = Math.floor(
  177. moment(this.getNow()).diff(moment(course.startTime), "seconds") / 30
  178. );
  179. let idx = 0;
  180. if (minutesAfterCourseStart < 0) {
  181. idx = 0;
  182. } else if (
  183. minutesAfterCourseStart >= 0 &&
  184. minutesAfterCourseStart < successRatePerMinute.length
  185. ) {
  186. idx = minutesAfterCourseStart;
  187. } else {
  188. idx = successRatePerMinute.length - 1;
  189. }
  190. if (Math.random() > 1 - successRatePerMinute[idx]) {
  191. if (minutesAfterCourseStart < 120) {
  192. window._hmt.push([
  193. "_trackEvent",
  194. "在线考试列表页面",
  195. "摇号进入考试-courseId-" + course.examId,
  196. minutesAfterCourseStart / 2 + "分进入",
  197. ]);
  198. } else {
  199. window._hmt.push([
  200. "_trackEvent",
  201. "在线考试列表页面",
  202. "摇号进入考试-courseId-" + course.examId,
  203. "60分后进入",
  204. ]);
  205. }
  206. this.enterExam(course);
  207. // return true;
  208. } else {
  209. this.$Modal.warning({
  210. title: "提示",
  211. content: "考试人员过多,请稍等2分钟再试。",
  212. onOk: () => {
  213. clearInterval(this.countdownInterval);
  214. this.countdown = 10;
  215. this.countdownInterval = setInterval(() => {
  216. this.countdown--;
  217. }, 1 * 1000);
  218. // this.enterExam(course);
  219. },
  220. });
  221. // this.$Message.warning({
  222. // content: "考试人员过多,请稍等2分钟再试",
  223. // duration: 10,
  224. // closable: true,
  225. // });
  226. // return false;
  227. }
  228. },
  229. async enterExam(course, alreadyChecked) {
  230. this.spinShow = true;
  231. this.processingMessage = "正在检测断点续考信息...";
  232. try {
  233. const alreadyInExam = await this.checkExamInProgress();
  234. if (alreadyInExam) {
  235. this.spinShow = false;
  236. window._hmt.push([
  237. "_trackEvent",
  238. "在线考试列表页面",
  239. "断点续考",
  240. "进入",
  241. ]);
  242. return;
  243. }
  244. } catch (error) {
  245. this.spinShow = false;
  246. return;
  247. }
  248. this.spinShow = true;
  249. this.processingMessage = "正在检测IP合法性...";
  250. try {
  251. const ipLimit = (await this.$http.get(
  252. "/api/ecs_exam_work/exam/ipLimit/" + course.examId
  253. )).data;
  254. // sleep function: await new Promise(resolve => setTimeout(() => resolve(), 3000));
  255. if (ipLimit.limited) {
  256. window._hmt.push(["_trackEvent", "在线考试列表页面", "IP受限"]);
  257. this.spinShow = false;
  258. this.$Message.error({
  259. content: "IP受限,请到中心指定地点进行考试!",
  260. duration: 15,
  261. closable: true,
  262. });
  263. return;
  264. }
  265. } catch (error) {
  266. this.$Message.error({
  267. content: "查询IP限制出错!",
  268. duration: 15,
  269. closable: true,
  270. });
  271. this.spinShow = false;
  272. return;
  273. }
  274. this.processingMessage = "正在获取考试设置...";
  275. if (!alreadyChecked) {
  276. let checkEnv = null;
  277. try {
  278. checkEnv = await this.$http.get(
  279. "/api/ecs_exam_work/exam/examOrgPropertyFromCache4StudentSession/" +
  280. course.examId +
  281. `/CHECK_ENVIRONMENT`
  282. );
  283. if (checkEnv.data.CHECK_ENVIRONMENT === "true") {
  284. const skipCheck = await new Promise(resolve => {
  285. this.$Modal.confirm({
  286. title: "进行环境检测",
  287. content:
  288. "环境检测可以检测电脑的硬件配置、网络速度和常用操作。环境检测不通过的话,可能影响考试的正常进行。",
  289. okText: "进行检测",
  290. cancelText: "跳过检测",
  291. onOk: () => {
  292. // sessionStorage.setItem(
  293. // "computer_env_ok_save_course_id",
  294. // course.courseId
  295. // );
  296. // this.$router.push("/check-computer");
  297. // this.$refs.checkEnvModal.show();
  298. this.shouldShowCheckEnvModal = true;
  299. this.selectedCourse = course;
  300. resolve();
  301. },
  302. onCancel: () => {
  303. resolve(true);
  304. },
  305. });
  306. });
  307. if (!skipCheck) {
  308. this.spinShow = false;
  309. return;
  310. }
  311. }
  312. } catch (error) {
  313. this.spinShow = false;
  314. this.$Message.error({
  315. content: "查询考试的环境检测设置属性出错!",
  316. duration: 15,
  317. closable: true,
  318. });
  319. return;
  320. }
  321. }
  322. if (course.faceEnable) {
  323. // if 人脸检测 && 没有底照,提示,并返回
  324. if (!this.user.photoPath) {
  325. this.spinShow = false;
  326. window._hmt.push(["_trackEvent", "在线考试列表页面", "无底照"]);
  327. this.$Message.info(
  328. "本场考试需要进行人脸检测,但是您没有上传底照,请联系老师!"
  329. );
  330. return;
  331. }
  332. let faceLiveness = null;
  333. try {
  334. faceLiveness = await this.$http.get(
  335. "/api/ecs_exam_work/exam/examOrgPropertyFromCache4StudentSession/" +
  336. course.examId +
  337. `/IS_FACE_VERIFY`
  338. );
  339. } catch (error) {
  340. this.spinShow = false;
  341. this.$Message.error({
  342. content: "查询考试的人脸检测设置属性出错!",
  343. duration: 15,
  344. closable: true,
  345. });
  346. return;
  347. }
  348. if (
  349. faceLiveness.data.IS_FACE_VERIFY &&
  350. JSON.parse(faceLiveness.data.IS_FACE_VERIFY)
  351. ) {
  352. this.processingMessage = "正在检测底照是否满足活体检测标准...";
  353. let checkBasePhoto;
  354. try {
  355. checkBasePhoto = (await this.$http.get(
  356. "/api/ecs_oe_student/examFaceLivenessVerify/checkFaceLiveness" +
  357. `?${this.user.token}` // 考生采用相同的机器考试,使用不同的请求的缓存
  358. )).data;
  359. this.spinShow = false;
  360. if (!checkBasePhoto.success) {
  361. this.$Message.error(
  362. "您上传的底照不符合活体检测的要求,请联系老师!"
  363. );
  364. return;
  365. }
  366. } catch (error) {
  367. this.spinShow = false;
  368. this.$Message.error({
  369. content: "查询检测底照是否满足活体检测标准的接口出错!",
  370. duration: 15,
  371. closable: true,
  372. });
  373. return;
  374. }
  375. }
  376. this.spinShow = false;
  377. // open face check modal, then
  378. // if 人脸识别失败 && 考试开启强制人脸识别 return
  379. // if 人脸识别失败 && 考试未开启强制人脸识别
  380. // 让学生手动确认进入考试,若取消,则返回
  381. this.selectedCourse = course;
  382. window._hmt.push([
  383. "_trackEvent",
  384. "在线考试列表页面",
  385. "人脸识别框",
  386. "弹出框",
  387. ]);
  388. this.toggleFaceCheckModal(true);
  389. } else {
  390. this.spinShow = false;
  391. window._hmt.push([
  392. "_trackEvent",
  393. "在线考试列表页面",
  394. "进入考试",
  395. "无人脸检测",
  396. ]);
  397. this.$router.push(
  398. `/online-exam/exam/${course.examId}/overview?examStudentId=${course.examStudentId}`
  399. );
  400. }
  401. },
  402. // eslint-disable-next-line
  403. async faceCheckResultCallback(course, faceMatched) {
  404. // if faceMatched
  405. },
  406. async resumeEnterExam() {
  407. this.shouldShowCheckEnvModal = false;
  408. this.enterExam(this.selectedCourse, true);
  409. },
  410. },
  411. // watch: {
  412. // courses(value) {
  413. // if (value.length > 0) {
  414. // let courseId = sessionStorage.getItem("computer_env_ok_save_course_id");
  415. // if (courseId !== null) {
  416. // courseId = +courseId; // 转为数字
  417. // const course = value.find(v => v.courseId === courseId);
  418. // if (course) {
  419. // this.enterExam(course, true);
  420. // sessionStorage.removeItem("computer_env_ok_save_course_id");
  421. // }
  422. // }
  423. // }
  424. // },
  425. // },
  426. };
  427. </script>
  428. <style scoped>
  429. .list {
  430. border: 1px solid #eeeeee;
  431. border-radius: 6px;
  432. }
  433. .list table {
  434. width: 100%;
  435. border-collapse: collapse !important;
  436. border-spacing: 0;
  437. }
  438. .list td {
  439. border: 1px solid #eeeeee;
  440. border-radius: 6px;
  441. border-collapse: separate !important;
  442. padding: 10px;
  443. }
  444. </style>
  445. <style>
  446. .online-exam-list-override-poptip .ivu-poptip-rel {
  447. width: 100%;
  448. }
  449. </style>