CheckComputer.vue 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009
  1. <template>
  2. <div style="max-width: 800px; margin: 30px auto">
  3. <Steps :current="current" size="small">
  4. <Step title="网速"></Step>
  5. <Step title="时钟"></Step>
  6. <Step title="摄像头"></Step>
  7. <Step title="声音"></Step>
  8. <Step title="微信小程序"></Step>
  9. <Step title="检测结果"></Step>
  10. </Steps>
  11. <div v-if="current === 0" key="0" class="section">
  12. <div class="list">
  13. <table>
  14. <tbody class="list-row">
  15. <tr class="list-header qm-primary-strong-text">
  16. <td class="first-td">检查项</td>
  17. <td>值</td>
  18. <td>状态</td>
  19. </tr>
  20. <tr>
  21. <td>电脑当前下载速度</td>
  22. <td>{{ network.downlink }}Mb</td>
  23. <td>
  24. <div v-if="network.downlinkStatus">
  25. <Icon class="pass-check" type="md-checkmark" />
  26. </div>
  27. <div v-else>
  28. <Icon
  29. class="fail-cross"
  30. title="下载速度不佳"
  31. type="md-close"
  32. />
  33. </div>
  34. </td>
  35. </tr>
  36. <tr>
  37. <td>电脑当前网络延迟</td>
  38. <td>{{ network.rrt }}毫秒</td>
  39. <td>
  40. <div v-if="network.rrtStatus">
  41. <Icon class="pass-check" type="md-checkmark" />
  42. </div>
  43. <div v-else>
  44. <Icon
  45. class="fail-cross"
  46. title="网络延迟较大"
  47. type="md-close"
  48. />
  49. </div>
  50. </td>
  51. </tr>
  52. </tbody>
  53. </table>
  54. </div>
  55. </div>
  56. <div v-if="current === 1" key="1" class="section">
  57. <div class="list">
  58. <table>
  59. <tbody class="list-row">
  60. <tr class="list-header qm-primary-strong-text">
  61. <td class="first-td">检查项</td>
  62. <td>值</td>
  63. <td>状态</td>
  64. </tr>
  65. <!-- <tr>
  66. <td>电脑当前时间</td>
  67. <td>{{ timeCurrent }} (北京时间)</td>
  68. <td></td>
  69. </tr> -->
  70. <!-- <tr>
  71. <td>电脑是否时间准确</td>
  72. <td>{{ timeDifference }}</td>
  73. </tr> -->
  74. <tr>
  75. <td>电脑时区</td>
  76. <td>{{ time.currentTimeZone }}</td>
  77. <td>
  78. <div v-if="time.timeZoneStatus">
  79. <Icon class="pass-check" type="md-checkmark" />
  80. </div>
  81. <div v-else>
  82. <Icon
  83. class="fail-cross"
  84. title="请将电脑设置为北京时区"
  85. type="md-close"
  86. />
  87. </div>
  88. </td>
  89. </tr>
  90. <tr>
  91. <td>电脑时钟频率</td>
  92. <td>
  93. <div v-if="time.clockRateStateResolved">
  94. {{ time.clockRateDiff > 3 ? "时钟过慢" : "正常" }}
  95. </div>
  96. <div v-else>
  97. <PulseLoader />
  98. </div>
  99. </td>
  100. <td>
  101. <div v-if="time.clockRateStateResolved">
  102. <Icon
  103. v-if="time.clockRateStatus"
  104. class="pass-check"
  105. type="md-checkmark"
  106. />
  107. <Icon
  108. v-else
  109. class="fail-cross"
  110. title="请更换电脑"
  111. type="md-close"
  112. />
  113. </div>
  114. <div v-else>
  115. <PulseLoader />
  116. </div>
  117. </td>
  118. </tr>
  119. </tbody>
  120. </table>
  121. </div>
  122. </div>
  123. <div v-show="current === 2" key="2" class="section">
  124. <div>
  125. <div style="display: flex">
  126. <video
  127. id="video"
  128. ref="video"
  129. width="400"
  130. height="300"
  131. autoplay
  132. ></video>
  133. <div
  134. v-if="camera.openCameraResolved && camera.openCameraStatus"
  135. style="margin-left: 50px; margin-top: 100px"
  136. >
  137. <Button
  138. type="warning"
  139. @click="
  140. camera.identityResolved = true;
  141. camera.identityStatus = false;
  142. "
  143. >
  144. 图像中不是电脑操作者本人
  145. </Button>
  146. <div style="width: 30px; height: 30px"></div>
  147. <Button
  148. type="primary"
  149. @click="
  150. camera.identityResolved = true;
  151. camera.identityStatus = true;
  152. "
  153. >
  154. 图像中是电脑操作者本人&nbsp;&nbsp;&nbsp;&nbsp;
  155. </Button>
  156. </div>
  157. </div>
  158. </div>
  159. <div class="list">
  160. <table>
  161. <tbody class="list-row">
  162. <tr class="list-header qm-primary-strong-text">
  163. <td class="first-td">检查项</td>
  164. <td>值</td>
  165. <td>状态</td>
  166. </tr>
  167. <tr>
  168. <td>摄像头正常启用</td>
  169. <td>
  170. <div v-if="camera.openCameraResolved">
  171. {{ camera.openCameraStatus ? "正常" : "请检查摄像头" }}
  172. </div>
  173. <div v-else>
  174. <PulseLoader />
  175. </div>
  176. </td>
  177. <td>
  178. <div v-if="camera.openCameraResolved">
  179. <div v-if="camera.openCameraStatus">
  180. <Icon class="pass-check" type="md-checkmark" />
  181. </div>
  182. <div v-else>
  183. <Icon
  184. class="fail-cross"
  185. title="请检查摄像头"
  186. type="md-close"
  187. />
  188. </div>
  189. </div>
  190. <div v-else>
  191. <PulseLoader />
  192. </div>
  193. </td>
  194. </tr>
  195. <tr>
  196. <td>视频显示的是电脑操作者本人</td>
  197. <td>
  198. <div
  199. v-if="
  200. (camera.openCameraResolved && !camera.openCameraStatus) ||
  201. camera.identityResolved
  202. "
  203. >
  204. {{ camera.identityStatus ? "正常" : "请检查摄像头" }}
  205. </div>
  206. <div v-else>
  207. <PulseLoader />
  208. </div>
  209. </td>
  210. <td>
  211. <div
  212. v-if="
  213. (camera.openCameraResolved && !camera.openCameraStatus) ||
  214. camera.identityResolved
  215. "
  216. >
  217. <div v-if="camera.identityStatus">
  218. <Icon class="pass-check" type="md-checkmark" />
  219. </div>
  220. <div v-else>
  221. <Icon
  222. class="fail-cross"
  223. title="请检查摄像头"
  224. type="md-close"
  225. />
  226. </div>
  227. </div>
  228. <div v-else>
  229. <PulseLoader />
  230. </div>
  231. </td>
  232. </tr>
  233. </tbody>
  234. </table>
  235. </div>
  236. </div>
  237. <div
  238. v-show="current === 3"
  239. key="3"
  240. class="section"
  241. style="text-align: center"
  242. >
  243. <div>
  244. <div style="display: flex; margin-bottom: 30px">
  245. <audio
  246. src="https://ecs-static.qmth.com.cn/check-audio.mp3"
  247. controls
  248. nodownload
  249. @loadeddata="
  250. sound.downloadResolved = true;
  251. sound.downloadStatus = true;
  252. "
  253. @error="
  254. sound.downloadResolved = true;
  255. sound.downloadStatus = false;
  256. "
  257. />
  258. <div style="margin-left: 30px; display: flex">
  259. <Button
  260. type="warning"
  261. title="或者听不到声音"
  262. @click="
  263. sound.playedStatusResolved = true;
  264. sound.playedStatus = false;
  265. "
  266. >
  267. 不能播放声音
  268. </Button>
  269. <div style="width: 30px; height: 30px"></div>
  270. <Button
  271. type="primary"
  272. @click="
  273. sound.playedStatusResolved = true;
  274. sound.playedStatus = true;
  275. "
  276. >
  277. 能够播放声音
  278. </Button>
  279. </div>
  280. </div>
  281. </div>
  282. <div class="list">
  283. <table>
  284. <tbody class="list-row">
  285. <tr class="list-header qm-primary-strong-text">
  286. <td class="first-td">检查项</td>
  287. <td>值</td>
  288. <td>状态</td>
  289. </tr>
  290. <tr>
  291. <td>文件下载</td>
  292. <td>
  293. <div v-if="sound.downloadResolved">
  294. {{ sound.downloadStatus ? "正常" : "出错" }}
  295. </div>
  296. <div v-else>
  297. <PulseLoader />
  298. </div>
  299. </td>
  300. <td>
  301. <div v-if="sound.downloadResolved">
  302. <div v-if="sound.downloadStatus">
  303. <Icon class="pass-check" type="md-checkmark" />
  304. </div>
  305. <div v-else>
  306. <Icon class="fail-cross" title="下载出错" type="md-close" />
  307. </div>
  308. </div>
  309. <div v-else>
  310. <PulseLoader />
  311. </div>
  312. </td>
  313. </tr>
  314. <tr>
  315. <td>声音播放</td>
  316. <td>
  317. <div v-if="sound.playedStatusResolved">
  318. {{ sound.playedStatus ? "正常" : "出错" }}
  319. </div>
  320. <div v-else>
  321. <PulseLoader />
  322. </div>
  323. </td>
  324. <td>
  325. <div v-if="sound.playedStatusResolved">
  326. <div v-if="sound.playedStatus">
  327. <Icon class="pass-check" type="md-checkmark" />
  328. </div>
  329. <div v-else>
  330. <Icon
  331. class="fail-cross"
  332. title="不能播放声音"
  333. type="md-close"
  334. />
  335. </div>
  336. </div>
  337. <div v-else>
  338. <PulseLoader />
  339. </div>
  340. </td>
  341. </tr>
  342. </tbody>
  343. </table>
  344. </div>
  345. </div>
  346. <div v-show="current === 4" key="4" class="section">
  347. <div>
  348. <div style="display: flex">
  349. <div>
  350. <div v-if="wechat.qrValue" style="display: flex">
  351. <qrcode
  352. :value="wechat.qrValue"
  353. :options="{ width: 200 }"
  354. style="margin-left: -10px"
  355. ></qrcode>
  356. <div style="margin-top: 10px">
  357. <div style="font-size: 30px">
  358. 请使用<span style="font-weight: 900; color: #1e90ff"
  359. >微信</span
  360. >扫描二维码后,在微信小程序上录音,并上传文件。
  361. </div>
  362. <div
  363. v-if="wechat.qrScanned"
  364. style="margin-top: 30px; font-size: 30px"
  365. >
  366. {{ wechat.studentAnswer ? "已上传" : "已扫描" }}
  367. <Icon type="md-checkmark" />
  368. </div>
  369. </div>
  370. </div>
  371. <div v-else>正在获取二维码...</div>
  372. </div>
  373. </div>
  374. <div
  375. class="audio-answer audio-answer-line-height"
  376. style="margin-top: 20px; text-align: left"
  377. >
  378. <span class="audio-answer-line-height">上传文件:</span>
  379. <audio
  380. v-if="wechat.studentAnswer"
  381. class="audio-answer-line-height"
  382. controls
  383. controlsList="nodownload"
  384. :src="wechat.studentAnswer"
  385. />
  386. <span v-else class="audio-answer-line-height">未上传文件</span>
  387. </div>
  388. <div style="margin-top: 30px; display: flex; margin-bottom: 30px">
  389. <Button
  390. type="warning"
  391. title="扫码不成功"
  392. @click="
  393. wechat.qrScannedResolved = true;
  394. wechat.qrScanned = false;
  395. "
  396. >
  397. 不能正确扫描二维码
  398. </Button>
  399. <div style="width: 30px; height: 30px"></div>
  400. <Button
  401. type="warning"
  402. title="上传不成功"
  403. @click="
  404. wechat.uploadResolved = true;
  405. wechat.uploadStatus = false;
  406. "
  407. >
  408. 上传不成功
  409. </Button>
  410. </div>
  411. </div>
  412. <div class="list">
  413. <table>
  414. <tbody class="list-row">
  415. <tr class="list-header qm-primary-strong-text">
  416. <td class="first-td">检查项</td>
  417. <td>值</td>
  418. <td>状态</td>
  419. </tr>
  420. <tr>
  421. <td>扫描二维码</td>
  422. <td>
  423. <div v-if="wechat.qrScannedResolved">
  424. {{ wechat.qrScanned ? "正常" : "出错" }}
  425. </div>
  426. <div v-else>
  427. <PulseLoader />
  428. </div>
  429. </td>
  430. <td>
  431. <div v-if="wechat.qrScannedResolved">
  432. <div v-if="wechat.qrScanned">
  433. <Icon class="pass-check" type="md-checkmark" />
  434. </div>
  435. <div v-else>
  436. <Icon class="fail-cross" title="扫描出错" type="md-close" />
  437. </div>
  438. </div>
  439. <div v-else>
  440. <PulseLoader />
  441. </div>
  442. </td>
  443. </tr>
  444. <tr>
  445. <td>上传录音</td>
  446. <td>
  447. <div v-if="wechat.uploadResolved">
  448. {{ wechat.uploadStatus ? "正常" : "出错" }}
  449. </div>
  450. <div v-else>
  451. <PulseLoader />
  452. </div>
  453. </td>
  454. <td>
  455. <div v-if="wechat.uploadResolved">
  456. <div v-if="wechat.uploadStatus">
  457. <Icon class="pass-check" type="md-checkmark" />
  458. </div>
  459. <div v-else>
  460. <Icon class="fail-cross" type="md-close" />
  461. </div>
  462. </div>
  463. <div v-else>
  464. <PulseLoader />
  465. </div>
  466. </td>
  467. </tr>
  468. </tbody>
  469. </table>
  470. </div>
  471. </div>
  472. <div v-show="current === 5" key="5" class="section">
  473. <div class="list">
  474. <table>
  475. <tbody class="list-row">
  476. <tr class="list-header qm-primary-strong-text">
  477. <td class="first-td">检查项</td>
  478. <td>结果</td>
  479. </tr>
  480. <tr>
  481. <td>网速</td>
  482. <td>
  483. <div v-if="step1Status">
  484. <Icon class="pass-check" type="md-checkmark" />
  485. </div>
  486. <div v-else>
  487. <Icon class="fail-cross" type="md-close" />
  488. </div>
  489. </td>
  490. </tr>
  491. <tr>
  492. <td>时钟</td>
  493. <td>
  494. <div v-if="step2StatusResolved">
  495. <div v-if="step2Status">
  496. <Icon class="pass-check" type="md-checkmark" />
  497. </div>
  498. <div v-else>
  499. <Icon class="fail-cross" type="md-close" />
  500. </div>
  501. </div>
  502. <div v-else>
  503. <PulseLoader />
  504. </div>
  505. </td>
  506. </tr>
  507. <tr>
  508. <td>摄像头</td>
  509. <td>
  510. <div v-if="step3StatusResolved">
  511. <div v-if="step3Status">
  512. <Icon class="pass-check" type="md-checkmark" />
  513. </div>
  514. <div v-else>
  515. <Icon class="fail-cross" type="md-close" />
  516. </div>
  517. </div>
  518. <div v-else class="fail-cross">
  519. 请在“摄像头”步骤进行人工确认!
  520. </div>
  521. </td>
  522. </tr>
  523. <tr>
  524. <td>声音</td>
  525. <td>
  526. <div v-if="step4StatusResolved">
  527. <div v-if="step4Status">
  528. <Icon class="pass-check" type="md-checkmark" />
  529. </div>
  530. <div v-else>
  531. <Icon class="fail-cross" type="md-close" />
  532. </div>
  533. </div>
  534. <div v-else class="fail-cross">
  535. 请在“声音”步骤进行人工确认!
  536. </div>
  537. </td>
  538. </tr>
  539. <tr>
  540. <td>微信小程序</td>
  541. <td>
  542. <div v-if="step5StatusResolved">
  543. <div v-if="step5Status">
  544. <Icon class="pass-check" type="md-checkmark" />
  545. </div>
  546. <div v-else>
  547. <Icon class="fail-cross" type="md-close" />
  548. </div>
  549. </div>
  550. <div v-else class="fail-cross">
  551. 请在“微信小程序”步骤进行人工确认!
  552. </div>
  553. </td>
  554. </tr>
  555. </tbody>
  556. </table>
  557. </div>
  558. <div style="color: red">
  559. <div v-if="!step1Status" key="a">
  560. 检查网络是否连接,路由器是否正常工作。
  561. </div>
  562. <div v-if="step2StatusResolved && !step2Status" key="b">
  563. 请调整电脑时间和社区与北京时间一致。
  564. </div>
  565. <div v-if="step3StatusResolved && !step3Status" key="c">
  566. 请确认摄像头连接线正常,能正常工作,关闭杀毒软件、关闭摄像头滤镜软件;请确认您的电脑是否为双摄摄像头,启用的摄像头是否正确。
  567. </div>
  568. <div v-if="step4StatusResolved && !step4Status" key="d">
  569. 请确认音箱连接正常,调整音量开关及大小。
  570. </div>
  571. <div v-if="step5StatusResolved && !step5Status" key="e">
  572. 请确认微信已登录并连接网络。
  573. </div>
  574. <div
  575. v-if="
  576. !step1Status ||
  577. (step2StatusResolved && !step2Status) ||
  578. (step3StatusResolved && !step3Status) ||
  579. (step4StatusResolved && !step4Status) ||
  580. (step5StatusResolved && !step5Status)
  581. "
  582. key="f"
  583. >
  584. 请按提示检查并调试,调试后可再次进行环境检测。
  585. </div>
  586. </div>
  587. </div>
  588. <div style="margin-top: 30px; text-align: center">
  589. <Button type="primary" :disabled="current === 0" @click="previous">
  590. 上一步
  591. </Button>
  592. <div style="width: 30px; height: 1px; display: inline-block"></div>
  593. <Button type="primary" :disabled="current === 5" @click="next">
  594. 下一步
  595. </Button>
  596. <div style="width: 30px; height: 1px; display: inline-block"></div>
  597. <Button
  598. v-if="current === 5"
  599. key="xxx"
  600. type="primary"
  601. @click="() => $emit('on-close')"
  602. >
  603. 进入考试
  604. </Button>
  605. </div>
  606. </div>
  607. </template>
  608. <script>
  609. import moment from "moment";
  610. // import "moment-duration-format";
  611. import VueQrcode from "@chenfengyuan/vue-qrcode";
  612. import PulseLoader from "vue-spinner/src/PulseLoader.vue";
  613. import {
  614. openWS,
  615. closeWsWithoutReconnect,
  616. } from "@/features/OnlineExam/Examing/ws";
  617. import { createNamespacedHelpers } from "vuex";
  618. const { mapState } = createNamespacedHelpers("examingHomeModule");
  619. const CLOCK_RATE_TIMEOUT = 10;
  620. // console.log(
  621. // moment.duration(-new Date().getTimezoneOffset(), "minutes").format("hh:mm")
  622. // );
  623. export default {
  624. name: "CheckComputer",
  625. components: {
  626. qrcode: VueQrcode,
  627. PulseLoader,
  628. },
  629. data() {
  630. return {
  631. current: 0,
  632. // status: ["process", "wait", "wait", "wait", "wait"],
  633. network: {
  634. downlink: navigator.connection.downlink,
  635. downlinkStatus: navigator.connection.downlink > 0.5,
  636. rrt: navigator.connection.rtt,
  637. rrtStatus: navigator.connection.rtt < 1000,
  638. },
  639. time: {
  640. // nowDate: Date.now(),
  641. // timeDifference: 0,
  642. currentTimeZone: moment().format("Z"),
  643. timeZoneStatus: new Date().getTimezoneOffset() / 60 === -8,
  644. clockRateDiff: null,
  645. clockRateStateResolved: false,
  646. clockRateStatus: false,
  647. },
  648. camera: {
  649. openCameraResolved: false,
  650. openCameraStatus: false,
  651. identityStatus: false,
  652. identityResolved: false,
  653. },
  654. sound: {
  655. downloadResolved: false,
  656. downloadStatus: false,
  657. playedStatusResolved: false,
  658. playedStatus: false,
  659. },
  660. wechat: {
  661. qrValue: " ",
  662. qrScannedResolved: false,
  663. qrScanned: false,
  664. uploadResolved: false,
  665. uploadStatus: false,
  666. studentAnswer: null,
  667. examRecordDataId: null,
  668. },
  669. };
  670. },
  671. computed: {
  672. ...mapState([
  673. "questionQrCode",
  674. "questionQrCodeScanned",
  675. "questionAnswerFileUrl",
  676. ]),
  677. timeCurrent() {
  678. return moment(this.nowDate)
  679. .utcOffset("+08:00")
  680. .format("YYYY-MM-DD HH:mm:ssZZ");
  681. },
  682. step1Status() {
  683. return this.network.downlinkStatus && this.network.rrtStatus;
  684. },
  685. step2StatusResolved() {
  686. return this.time.clockRateStateResolved;
  687. },
  688. step2Status() {
  689. return this.time.timeZoneStatus && this.time.clockRateStatus;
  690. },
  691. step3StatusResolved() {
  692. return this.camera.identityResolved && this.camera.openCameraResolved;
  693. },
  694. step3Status() {
  695. return this.camera.identityStatus && this.camera.openCameraStatus;
  696. },
  697. step4StatusResolved() {
  698. return this.sound.downloadResolved && this.sound.playedStatusResolved;
  699. },
  700. step4Status() {
  701. return this.sound.downloadStatus && this.sound.playedStatus;
  702. },
  703. step5StatusResolved() {
  704. return this.wechat.qrScannedResolved && this.wechat.uploadResolved;
  705. },
  706. step5Status() {
  707. return this.wechat.qrScanned && this.wechat.uploadStatus;
  708. },
  709. },
  710. watch: {
  711. // questionQrCode(value) {
  712. // // console.log(this.examQuestion.studentAnswer);
  713. // // console.log("watch", value);
  714. // this.wechat.qrValue = value.qrCode;
  715. // this.wechat.examRecordDataId = decodeURIComponent(value.qrCode).match(
  716. // /&examRecordDataId=(\d+)&/
  717. // )[1];
  718. // },
  719. questionQrCodeScanned() {
  720. // console.log(this.examQuestion.studentAnswer);
  721. // console.log("watch", value);
  722. this.wechat.qrScanned = true;
  723. this.wechat.qrScannedResolved = true;
  724. },
  725. questionAnswerFileUrl(value) {
  726. // console.log(this.examQuestion.studentAnswer);
  727. // console.log("watch", value);
  728. const examRecordDataId = this.wechat.examRecordDataId;
  729. for (const q of value) {
  730. if (!q.saved) {
  731. let acknowledgeStatus = "CONFIRMED";
  732. this.$http
  733. .post(
  734. "/api/ecs_oe_student/examControl/saveUploadedFileAcknowledgeStatus",
  735. {
  736. examRecordDataId,
  737. filePath: q.fileUrl,
  738. order: q.order,
  739. acknowledgeStatus,
  740. }
  741. )
  742. .then(() => {
  743. this.wechat.studentAnswer = q.fileUrl;
  744. this.wechat.uploadResolved = true;
  745. this.wechat.uploadStatus = true;
  746. q.saved = true;
  747. if (acknowledgeStatus === "CONFIRMED")
  748. this.$Message.info({
  749. content: "小程序作答已更新",
  750. duration: 5,
  751. closable: true,
  752. });
  753. })
  754. .catch(() => {
  755. this.$Message.error({
  756. content: "更新小程序答案失败!",
  757. duration: 15,
  758. closable: true,
  759. });
  760. });
  761. }
  762. }
  763. },
  764. },
  765. async created() {
  766. this.getNowInterval = setInterval(() => {
  767. this.nowDate = Date.now();
  768. }, 1000);
  769. openWS({});
  770. const fetchQR = async () => {
  771. const examRecordDataId = this.$store.state.user.id;
  772. const response = await this.$http.post(
  773. "/api/ecs_oe_student/examControl/getQrCode",
  774. {
  775. examRecordDataId,
  776. order: 1,
  777. transferFileType: "AUDIO",
  778. testEnv: true,
  779. }
  780. );
  781. let origin = window.location.origin.replace(
  782. /.*(\.(ea100.com.cn|exam-cloud.cn))/,
  783. `${location.protocol}//www$1`
  784. );
  785. console.debug("测试验证:", origin);
  786. if (process.env.NODE_ENV === "development") {
  787. origin = process.env.VUE_APP_API_SERVER;
  788. }
  789. this.wechat.qrValue =
  790. response.data + encodeURIComponent("&apiServer=" + origin);
  791. const trueExamRecordDataId = decodeURIComponent(response.data).match(
  792. /&examRecordDataId=(\d+)/
  793. )[1];
  794. this.wechat.examRecordDataId = trueExamRecordDataId;
  795. };
  796. if (this.wechat.qrValue) {
  797. this.getQRCodeTimeout = setTimeout(() => {
  798. fetchQR();
  799. }, 3000);
  800. }
  801. },
  802. async mounted() {
  803. let start, end;
  804. // const networkStart = performance.now();
  805. fetch("/oe-web/login", { Method: "HEAD" }).then((e) => {
  806. start = moment(e.headers.get("date"));
  807. // const networkEnd = performance.now();
  808. // this.timeDifference =
  809. // Math.abs(moment().diff(start) - (networkEnd - networkStart) / 2) <
  810. // 60 * 1000
  811. // ? "OK"
  812. // : "与服务器时间差异超过60秒";
  813. });
  814. this.checkClockRateTimeout = setTimeout(() => {
  815. fetch("/oe-web/login", { Method: "HEAD" }).then((e) => {
  816. // 可能已经离开这个页面了
  817. if (!this.time) return;
  818. end = moment(e.headers.get("date"));
  819. this.time.clockRateStateResolved = true;
  820. this.time.clockRateDiff =
  821. end.diff(start, "seconds") - CLOCK_RATE_TIMEOUT;
  822. this.time.clockRateStatus =
  823. end.diff(start, "seconds") < CLOCK_RATE_TIMEOUT + 2;
  824. });
  825. }, CLOCK_RATE_TIMEOUT * 1000);
  826. await this.openCamera();
  827. },
  828. beforeDestroy() {
  829. clearInterval(this.getNowInterval);
  830. clearTimeout(this.checkClockRateTimeout);
  831. clearTimeout(this.getQRCodeTimeout);
  832. if (this.$refs.video.srcObject) {
  833. this.$refs.video.srcObject.getTracks().forEach(function (track) {
  834. track.stop();
  835. });
  836. this.$refs.video.srcObject = null;
  837. }
  838. closeWsWithoutReconnect();
  839. },
  840. methods: {
  841. previous() {
  842. if (this.current >= 0) {
  843. this.current -= 1;
  844. }
  845. },
  846. next() {
  847. if (this.current < 5) {
  848. this.current += 1;
  849. }
  850. if (this.current === 5) {
  851. window._hmt.push([
  852. "_trackEvent",
  853. "环境检测",
  854. `网络: ${this.step1Status}; 时间: ${this.step2Status}; 摄像头: ${this.step3Status}; 声音: ${this.step4Status}; 小程序: ${this.step5Status};`,
  855. ]);
  856. }
  857. },
  858. async openCamera() {
  859. const video = this.$refs.video;
  860. if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
  861. try {
  862. console.log("启动摄像头");
  863. const stream = await navigator.mediaDevices.getUserMedia({
  864. video: {
  865. facingMode: "user",
  866. resizeMode: "crop-and-scale",
  867. width: 400,
  868. height: 300,
  869. },
  870. });
  871. if (stream) {
  872. video.srcObject = stream;
  873. try {
  874. await video.play();
  875. this.camera.openCameraStatus = true;
  876. } catch (error) {
  877. console.log("摄像头没有正常启用", error);
  878. this.$Message.error({
  879. content: "摄像头没有正常启用: " + error,
  880. duration: 15,
  881. closable: true,
  882. });
  883. window._hmt.push([
  884. "_trackEvent",
  885. "摄像头框-环境检测",
  886. "摄像头状态",
  887. "摄像头没有正常启用: " + error,
  888. ]);
  889. }
  890. } else {
  891. this.$Message.error({
  892. content: "没有可用的视频流",
  893. duration: 15,
  894. closable: true,
  895. });
  896. window._hmt.push([
  897. "_trackEvent",
  898. "摄像头框-环境检测",
  899. "摄像头状态",
  900. "没有可用的视频流",
  901. ]);
  902. }
  903. } catch (error) {
  904. console.log("无法启用摄像头", error);
  905. let errMsg;
  906. if (error.name || error.message) {
  907. errMsg = `${error.name} ${error.message}`;
  908. } else {
  909. errMsg = error;
  910. }
  911. this.$Message.error({
  912. content: "无法启用摄像头: " + errMsg,
  913. duration: 15,
  914. closable: true,
  915. });
  916. window._hmt.push([
  917. "_trackEvent",
  918. "摄像头框-环境检测",
  919. "摄像头状态",
  920. "无法启用摄像头" + errMsg,
  921. ]);
  922. } finally {
  923. this.camera.openCameraResolved = true;
  924. }
  925. } else {
  926. this.$Message.error({
  927. content: "没有找到可用的摄像头",
  928. duration: 15,
  929. closable: true,
  930. });
  931. window._hmt.push([
  932. "_trackEvent",
  933. "摄像头框-环境检测",
  934. "摄像头状态",
  935. "没有找到可用的摄像头",
  936. ]);
  937. }
  938. this.camera.openCameraResolved = true;
  939. },
  940. },
  941. };
  942. </script>
  943. <style scoped>
  944. .section {
  945. margin-top: 10px;
  946. }
  947. .list {
  948. border: 1px solid #eeeeee;
  949. border-radius: 6px;
  950. text-align: center;
  951. }
  952. .list table {
  953. width: 100%;
  954. border-collapse: collapse !important;
  955. border-spacing: 0;
  956. }
  957. .list td {
  958. border: 1px solid #eeeeee;
  959. border-radius: 6px;
  960. border-collapse: separate !important;
  961. padding: 10px;
  962. }
  963. .list .first-td {
  964. width: 50%;
  965. }
  966. .pass-check {
  967. font-size: 20px;
  968. color: green;
  969. }
  970. .fail-cross {
  971. font-size: 20px;
  972. color: red;
  973. }
  974. </style>