run.vue 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141
  1. <template>
  2. <div>
  3. <Header @confirmExit="confirmExit"></Header>
  4. <Transition :enter-active-class="proxy?.animate.dialog.enter" :leave-active-class="proxy?.animate.dialog.leave">
  5. <div class="time" v-show="[42].includes(examState)">{{
  6. countdownNumFormat
  7. }}</div>
  8. </Transition>
  9. <div class="main">
  10. <template v-if="isLongRun">
  11. <!--长跑-->
  12. <swiper :slides-per-view="testListArr.length >= 2 ? 2 : 1" :slides-per-group="testListArr.length >= 2 ? 2 : 1"
  13. :space-between="20" :speed="1200" :modules="[Navigation]"
  14. :navigation="{ prevEl: '.swiper-button-prev', nextEl: ' .swiper-button-next' }">
  15. <swiper-slide v-for="(items, indexs) in testListArr " :key="indexs">
  16. <div class="main-left main-left2">
  17. <div class="trackItem">
  18. <TransitionGroup :enter-active-class="proxy?.animate.run.enter">
  19. <div v-for="(item, index) in items" :key="indexs + '_' + index" class="li">
  20. <div class="left">
  21. <div class="track">{{ (index + 1) + (8 * indexs) }}</div>
  22. <div class="userInfo" @click="getChooseStudent(item.track)">
  23. <div class="pic pic2" v-if="item.student_id"> <img :src="item.face_pic" /></div>
  24. <div class="pic" v-else>
  25. <img src="@/assets/images/test/profilePicture.png" />
  26. </div>
  27. <div class="nameBox">
  28. <div class="name">{{ item.student_name || "未检录" }}</div>
  29. </div>
  30. </div>
  31. </div>
  32. <div class="scoreBox">
  33. <div v-if="item.timeStr" class="score">
  34. {{ item.timeStr || "-" }}
  35. </div>
  36. <div v-if="isBackRun && item.student_id" class="turns">往返次数:<span><i>{{ item.turns || "0"
  37. }}</i></span>
  38. </div>
  39. </div>
  40. <div class="menuBtn menuBtn2" v-if="examState == 43 && item.student_id">等待开始测试</div>
  41. <div class="menuBtn menuBtn2" v-if="examState == 42 && item.student_id">正在测试</div>
  42. <div class="menuBtn" v-if="examState == 3 && !item.timeStr && item.isfinish && item.student_id">
  43. 异常
  44. </div>
  45. <div class="close" @click="close(item)" v-if="examState == 41"></div>
  46. </div>
  47. </TransitionGroup>
  48. </div>
  49. </div>
  50. </swiper-slide>
  51. </swiper>
  52. <!-- 如果需要导航按钮 -->
  53. <div v-show="testListArr.length > 2" class="swiper-button-prev swiper-btn swiper-btn-left" slot="button-prev">
  54. </div>
  55. <div v-show="testListArr.length > 2" class="swiper-button-next swiper-btn swiper-btn-right" slot="button-next">
  56. </div>
  57. </template>
  58. <template v-else>
  59. <!--短跑-->
  60. <div class="main-left">
  61. <div class="trackItem">
  62. <TransitionGroup :enter-active-class="proxy?.animate.run.enter">
  63. <div v-for="(item, index) in faceStudentList" :key="index" class="li">
  64. <div class="left">
  65. <div class="track">{{ item.track }}</div>
  66. <div class="userInfo" @click="getChooseStudent(item.track)">
  67. <div class="pic pic2" v-if="item.student_id"> <img :src="item.face_pic" /></div>
  68. <div class="pic" v-else>
  69. <img src="@/assets/images/test/profilePicture.png" />
  70. </div>
  71. <div class="nameBox">
  72. <div class="name">{{ item.student_name || "未检录" }}</div>
  73. </div>
  74. </div>
  75. </div>
  76. <div class="scoreBox">
  77. <div v-if="item.timeStr" class="score">
  78. {{ item.timeStr || "-" }}
  79. </div>
  80. <div v-if="isBackRun && item.student_id" class="turns">往返次数:<span><i>{{ item.turns || "0"
  81. }}</i></span>
  82. </div>
  83. </div>
  84. <div class="menuBtn" v-if="examState > 3 && examState < 42 && !item.student_id"
  85. @click="getChooseStudent(item.track)">检录
  86. </div>
  87. <div class="menuBtn" v-if="examState > 3 && examState < 42 && item.student_id"
  88. @click="getRetestTrackFace(item.track)">重新识别
  89. </div>
  90. <div class="menuBtn menuBtn2" v-if="examState == 43 && item.student_id">等待开始测试</div>
  91. <div class="menuBtn menuBtn2" v-if="examState == 42 && item.student_id">正在测试</div>
  92. <div class="menuBtn" v-if="examState == 3 && !item.timeStr && item.isfinish && item.student_id">
  93. 异常
  94. </div>
  95. </div>
  96. </TransitionGroup>
  97. </div>
  98. </div>
  99. <div class="main-right">
  100. <ReportList ref="reportListRef" :parameter="parameter" :showQRCode="true" />
  101. </div>
  102. </template>
  103. </div>
  104. <!-- <div>当前状态:({{ examState == 3 ? "初始化完成" : examState == 40 ? "创建测试" : examState == 41 ? "正在人脸识别":examState ==43 ? "停止人脸识别" : examState == 42 ? "正在测试" : "请初始化" }})</div> -->
  105. <div class="footerBtn">
  106. <template v-if="needStart">
  107. <div class="btn" @click="getOpenOneTestAndStartFace" v-if="examState == 3 || examState == 40">开始识别</div>
  108. <div class="btn" @click="getChooseStudent()" v-if="examState == 41 && isLongRun">检录</div>
  109. <div class="btn" @click="getStopFace" v-if="examState == 41">停止识别</div>
  110. <div class="btn startBtn" @click="getStartOneTest" v-if="examState == 43">开始测试</div>
  111. </template>
  112. <template v-else> </template>
  113. <div class="btn" @click="getConfirmEnd" v-if="examState == 42">{{ isLongRun ? '结 束' : '下一组' }}</div>
  114. <div class="btn" @click="getAgain" v-if="showTestAgain">再测一次</div>
  115. <div class="btn" @click="getRetestFace" v-if="examState == 43">重新识别</div>
  116. </div>
  117. <ChooseStudent ref="chooseStudentRef" :selectType="isLongRun ? 'multiple' : 'single'" @returnData="returnStudent" />
  118. </div>
  119. </template>
  120. <script setup name="TrainTest" lang="ts">
  121. import { useWs } from '@/utils/trainWs';
  122. // import { initWs, examEnds, openOneTest, startFace, stopFace, faceConfirmOnly, startOneTest, finishOneTest, closeOneTest, suspendFaceRecognitionChannels, resumeFaceRecognitionChannels } from '@/utils/ws'
  123. import { initSpeech, speckText, playMusic, controlMusic, speckCancel, chineseNumber } from '@/utils/speech'
  124. import dayjs from 'dayjs'
  125. import dataDictionary from "@/utils/dataDictionary"
  126. import { Swiper, SwiperSlide } from 'swiper/vue';
  127. import { Navigation } from 'swiper/modules';
  128. import 'swiper/css';
  129. import 'swiper/scss/navigation';
  130. const { initWs, examEnds, openOneTest, startFace, stopFace, faceConfirmOnly, startOneTest, finishOneTest, closeOneTest, suspendFaceRecognitionChannels, resumeFaceRecognitionChannels } = useWs();
  131. const { proxy } = getCurrentInstance() as any;
  132. const router = useRouter();
  133. const route = useRoute();
  134. const chooseStudentRef = ref();
  135. const reportListRef = ref();
  136. const data = reactive<any>({
  137. timerManager: {},//计时器管理
  138. parameter: {},//参数
  139. time: {
  140. testTime: 0,//时长
  141. countdownNum: 0,//计时
  142. },
  143. userInfo: {},//用户信息
  144. examState: 0,//当前状态
  145. resultId: null,//测试ID
  146. unit: "",//单位
  147. needStart: false,//是否需要按钮
  148. faceStudentList: [],//跑道和人信息
  149. currentTrack: null,//当前跑道
  150. showTestAgain: false,//再测一次按钮
  151. isLongRun: false,//是否为长跑项目
  152. isBackRun: false,//是否为折返跑项目
  153. sid: null,//WS的id
  154. });
  155. const { timerManager, parameter, time, userInfo, examState, resultId, faceStudentList, currentTrack, unit, needStart, showTestAgain, isLongRun, isBackRun, sid } = toRefs(data);
  156. /**
  157. * 接收消息
  158. */
  159. const getMessage = (e: any) => {
  160. //console.log("WS响应:", e)
  161. //获取sid
  162. if (e.cmd === 'mySid') {
  163. console.log("e.data.sid", e.data.sid)
  164. sid.value = e.data.sid;
  165. }
  166. //实时状态
  167. if (e.cmd === 'exam_status') {
  168. examState.value = e.data;
  169. }
  170. //工作站状态
  171. if (e.cmd === 'init_result') {
  172. // if (isLongRun.value) {
  173. // let num = 80;
  174. // let list: any = [];
  175. // for (let i = 1; i <= num; i++) {
  176. // list.push({ track: i });
  177. // }
  178. // faceStudentList.value = list
  179. // }
  180. }
  181. //获取跑道配置
  182. if (e.cmd === 'exam_config') {
  183. let num = e.data.num_of_tracks;
  184. let list: any = [];
  185. for (let i = 1; i <= num; i++) {
  186. list.push({ track: i });
  187. }
  188. faceStudentList.value = list
  189. }
  190. //测试违规
  191. if (e.cmd === 'warning_result') {
  192. }
  193. //后端播报语音
  194. if (e.cmd === 'return_audio_msg') {
  195. }
  196. //错误提示
  197. if (e.cmd === 'info_result') {
  198. proxy?.$modal.msgError(e.data.message);
  199. }
  200. //错误提示
  201. if (e.cmd === 'error_result') {
  202. proxy?.$modal.msgError(e.data.message);
  203. }
  204. //测试中违规提示
  205. if (e.cmd === 'warning_notify') {
  206. }
  207. //断线状态
  208. if (e.cmd === 'disconnect_request') {
  209. if (e.data.message) {
  210. speckText(e.data.message);
  211. }
  212. getExit();
  213. }
  214. //状态变更
  215. if (e.cmd === 'set_exam_state') {
  216. examState.value = e.data;
  217. if (e.data === 3) {
  218. initProject();
  219. if (!isLongRun && showTestAgain.value) {
  220. reportListRef.value.getIniReportList();
  221. }
  222. }
  223. if (e.data === 40) {
  224. cleanData();
  225. }
  226. if (e.data == 41) {
  227. }
  228. if (e.data == 43) {
  229. }
  230. if (e.data == 42) {
  231. }
  232. }
  233. //新建测试后返回信息,获取result_id
  234. if (e.cmd === 'open_one_test_ack') {
  235. resultId.value = e.data.result_id;
  236. }
  237. //人脸识别状态
  238. if (e.cmd === 'face_check_result') {
  239. let obj = e.data[0];
  240. currentTrack.value = obj.track;
  241. returnStudent(obj)
  242. }
  243. //测试结束结果
  244. if (e.cmd === 'oneresult') {
  245. if (e.data.length) {
  246. let data = e.data;
  247. getAchievement(data)
  248. }
  249. }
  250. //结果生成完成(视频图片)
  251. if (e.cmd === 'static_urls_finished') {
  252. }
  253. //选择学生或测试结束后返回的数据
  254. if (e.cmd === 'result_info') {
  255. }
  256. };
  257. /**
  258. * 开始识别
  259. */
  260. const getOpenOneTestAndStartFace = async () => {
  261. if (examState.value > 3) {
  262. await closeOneTest();
  263. }
  264. await openOneTest();
  265. await startFace();
  266. };
  267. /**
  268. * 停止人脸识别
  269. */
  270. const getStopFace = async () => {
  271. if (examState.value != 41) {
  272. return false;
  273. }
  274. let list = faceStudentList.value.filter((item: any) => {
  275. return item.student_id;
  276. })
  277. if (!list.length) {
  278. proxy?.$modal.msgWarning("请选择人员!");
  279. return false;
  280. }
  281. await stopFace();
  282. getFaceConfirmOnly();
  283. };
  284. /**
  285. * 确定人脸信息
  286. */
  287. const getFaceConfirmOnly = (data?: any) => {
  288. let list = [];
  289. if (data) {
  290. faceStudentList.value = data;
  291. list = data.filter((item: any) => {
  292. return item.student_id;
  293. });
  294. } else {
  295. list = faceStudentList.value.filter((item: any) => {
  296. return item.student_id;
  297. });
  298. }
  299. //短跑播报跑道
  300. if (data && !isLongRun.value) {
  301. let speechList = list.map((item: any) => {
  302. return `第${item.track == 2 ? '二' : item.track}道, ${item.student_name}`
  303. }).join();
  304. speckText(speechList);
  305. }
  306. //长跑自动拼接跑道
  307. if (isLongRun.value) {
  308. list = list.map((item: any, index: any) => {
  309. item.track = index + 1;
  310. return item;
  311. })
  312. }
  313. //重组数据
  314. list = list.map((item: any) => {
  315. let obj = {
  316. result_id: item.result_id,
  317. student_id: item.student_id,
  318. student_name: item.student_name,
  319. gender: item.gender,
  320. track: item.track
  321. }
  322. return obj;
  323. })
  324. faceConfirmOnly(list, () => {
  325. });
  326. };
  327. /**
  328. * 重新识别
  329. */
  330. const getRetestFace = () => {
  331. proxy?.$modal.confirm("确定重新识别吗?").then(() => {
  332. if (needStart.value == false) {
  333. //自动流程项目重新识别直接返回3
  334. closeOneTest();
  335. } else {
  336. //手动流程项目重新识别43返回41,42返回3
  337. if (examState.value == 43) {
  338. cleanData();
  339. startFace();
  340. } else {
  341. closeOneTest();
  342. }
  343. }
  344. }).finally(() => {
  345. });
  346. };
  347. /**
  348. * 开始测试
  349. */
  350. const getStartOneTest = () => {
  351. if (examState.value != 43) {
  352. return false;
  353. }
  354. let list = faceStudentList.value.filter((item: any) => {
  355. return item.student_id;
  356. })
  357. if (!list.length) {
  358. proxy?.$modal.msgWarning("请选择人员!");
  359. return false;
  360. }
  361. //停止播报;
  362. speckCancel()
  363. //和工作站搭配时差版
  364. //提前发送开始的时间
  365. let advanceTime = 1000;
  366. //各就位+枪声是7秒左右,5.26秒是播枪声
  367. let myTime = 7010;
  368. //播放音频和遮罩
  369. let myText = "各就位,预备!";
  370. let loading = ElLoading.service({ text: myText, background: 'rgba(0, 0, 0, 0.8)', customClass: `sports ${parameter.value.project}` });
  371. speckText(myText);
  372. setTimeout(() => {
  373. startOneTest(data == null, () => {
  374. });
  375. loading?.close();
  376. }, advanceTime)
  377. setTimeout(() => {
  378. //显示再测一次按钮
  379. showTestAgain.value = true;
  380. //计时项目才开
  381. if (needStart.value == true) {
  382. //时间为0的为正计时,大于0的为倒计时
  383. if (time.value.testTime == 0) {
  384. getCounting("+");
  385. } else {
  386. getCounting("-");
  387. }
  388. } else {
  389. speckText("请开始测试");
  390. }
  391. }, myTime)
  392. // 立即开版本
  393. // startOneTest(data == null, () => {
  394. // //显示再测一次按钮
  395. // showTestAgain.value = true;
  396. // //计时项目才开
  397. // if (needStart.value == true) {
  398. // //时间为0的为正计时,大于0的为倒计时
  399. // if (time.value.testTime == 0) {
  400. // getCounting("+");
  401. // } else {
  402. // getCounting("-");
  403. // }
  404. // } else {
  405. // speckText("请开始测试");
  406. // }
  407. // })
  408. };
  409. /**
  410. * 再测一次
  411. */
  412. const getAgain = async () => {
  413. getClearTimer();
  414. let loading = ElLoading.service({ text: '请稍等...', background: 'rgba(0, 0, 0, 0.8)', customClass: `sports ${parameter.value.project}` });
  415. //预存测试人员
  416. let student = faceStudentList.value.map((item: any) => {
  417. let obj = {
  418. face_pic: item.face_pic,
  419. student_id: item.student_id,
  420. student_name: item.student_name,
  421. gender: item.gender,
  422. track: item.track,
  423. }
  424. return obj;
  425. });
  426. //测试中
  427. if (examState.value == 42) {
  428. await finishOneTest();
  429. }
  430. //其他状态
  431. if (examState.value > 3) {
  432. await closeOneTest();
  433. }
  434. //重新走一次流程
  435. await openOneTest();
  436. await startFace();
  437. await stopFace();
  438. getFaceConfirmOnly(student);
  439. loading?.close();
  440. let loadingTime = setTimeout(() => {
  441. loading?.close();
  442. clearTimeout(loadingTime);
  443. }, 10000);
  444. };
  445. /**
  446. * 确认退出
  447. */
  448. const getConfirmEnd = async () => {
  449. let txt = isLongRun.value ? '是否结束' : '是否开始下一组?'
  450. await proxy?.$modal.confirm(txt);
  451. let loading = ElLoading.service({ text: '请稍等...', background: 'rgba(0, 0, 0, 0.8)', customClass: `sports ${parameter.value.project}` });
  452. //测试中
  453. if (examState.value == 42) {
  454. await finishOneTest();
  455. }
  456. //其他状态
  457. if (examState.value > 3) {
  458. await closeOneTest();
  459. }
  460. if (!isLongRun.value) {
  461. //重新走一次流程
  462. await openOneTest();
  463. await startFace();
  464. }
  465. loading?.close();
  466. let loadingTime = setTimeout(() => {
  467. loading?.close();
  468. clearTimeout(loadingTime);
  469. }, 10000);
  470. };
  471. /**
  472. * 确认退出
  473. */
  474. const confirmExit = () => {
  475. proxy?.$modal.confirm("确定退出吗?").then(() => {
  476. getExit();
  477. }).finally(() => {
  478. });
  479. };
  480. /**
  481. * 退出
  482. */
  483. const getExit = () => {
  484. getClearTimer();//清除计时器
  485. examEnds();//通知工作站关闭
  486. speckCancel()//停止播报;
  487. window.onbeforeunload = null;//移除事件处理器
  488. router.push({ path: '/' });//跳转
  489. };
  490. /**
  491. * 清空定时任务
  492. */
  493. const getClearTimer = (data?: any) => {
  494. if (data) {
  495. //清除指定
  496. clearInterval(timerManager.value[data])
  497. timerManager.value[data] = null;
  498. } else {
  499. //清除全部
  500. for (let key in timerManager.value) {
  501. if (timerManager.value.hasOwnProperty(key)) {
  502. clearInterval(timerManager.value[key])
  503. timerManager.value[key] = null;
  504. }
  505. }
  506. }
  507. };
  508. /**
  509. * 选择学生
  510. */
  511. const getChooseStudent = (track?: number) => {
  512. if (examState.value < 41) {
  513. if (needStart.value) {
  514. proxy?.$modal.msgWarning("请点击开始识别");
  515. } else {
  516. proxy?.$modal.msgWarning("请稍等...");
  517. }
  518. return false;
  519. }
  520. if (examState.value == 43) {
  521. proxy?.$modal.msgWarning("请点击重新识别");
  522. return false;
  523. }
  524. currentTrack.value = track;
  525. // stopFace();
  526. chooseStudentRef.value.open();
  527. };
  528. /**
  529. * 返回被选学生
  530. */
  531. const returnStudent = (data: any) => {
  532. if (isLongRun.value) {
  533. //长跑
  534. longStudent(data)
  535. } else {
  536. //短跑
  537. sprintStudent(data)
  538. }
  539. };
  540. /**
  541. * 处理短跑返回被选学生
  542. */
  543. const sprintStudent = (data: any) => {
  544. if (data == undefined || !data) {
  545. return false;
  546. }
  547. suspendFaceRecognitionChannels(currentTrack.value);//停止识别
  548. let obj = {
  549. result_id: resultId.value,
  550. face_pic: data.face_pic || data.logo_url,
  551. student_id: data.id || data.student_id,
  552. student_name: data.name,
  553. gender: data.gender,
  554. }
  555. //可能已检录的先清除
  556. let oldIndex = faceStudentList.value.findIndex((item: any) => {
  557. return data.student_id == item.student_id;
  558. })
  559. if (oldIndex != -1) {
  560. faceStudentList.value[oldIndex] = { track: faceStudentList.value[oldIndex].track };
  561. }
  562. //赋值
  563. let newIndex = faceStudentList.value.findIndex((item: any) => {
  564. return currentTrack.value == item.track;
  565. })
  566. faceStudentList.value[newIndex] = { ...faceStudentList.value[newIndex], ...obj };
  567. if (!isLongRun.value) {
  568. speckText(`第${currentTrack.value == 2 ? '二' : currentTrack.value}道, ${data.name}`);
  569. }
  570. currentTrack.value = null;
  571. };
  572. /**
  573. * 处理长跑返回被选学生
  574. */
  575. const longStudent = (data: any) => {
  576. let ids = faceStudentList.value.map((item: any, index: any) => {
  577. return item.student_id;
  578. });
  579. //排除掉已经存在的
  580. let newList = data.filter((item: any, index: any) => {
  581. return !ids.includes(item.id)
  582. })
  583. let list = newList.map((item: any, index: any) => {
  584. let obj = {
  585. result_id: resultId.value,
  586. face_pic: item.face_pic || item.logo_url,
  587. student_id: item.id,
  588. student_name: item.name,
  589. gender: item.gender,
  590. }
  591. return obj;
  592. })
  593. faceStudentList.value.push(...list)
  594. };
  595. /**
  596. * 重新识别指定跑道
  597. */
  598. const getRetestTrackFace = (data: any) => {
  599. if (data == undefined || !data) {
  600. return false;
  601. }
  602. resumeFaceRecognitionChannels(data);//重新识别
  603. let obj = {
  604. student_id: null,
  605. track: data,
  606. isfinish: false,
  607. }
  608. let myIndex = faceStudentList.value.findIndex((item: any) => {
  609. return item.track == data;
  610. })
  611. faceStudentList.value[myIndex] = obj;
  612. };
  613. /**
  614. * 清除历史记录
  615. */
  616. const cleanData = () => {
  617. time.value.countdownNum = time.value.testTime;
  618. showTestAgain.value = false;
  619. if (isLongRun.value) {
  620. faceStudentList.value = [];
  621. } else {
  622. //重置全部
  623. let list = faceStudentList.value.map((item: any) => {
  624. let obj = {
  625. student_id: null,
  626. track: item.track,
  627. isfinish: false,
  628. }
  629. return obj;
  630. })
  631. faceStudentList.value = list;
  632. }
  633. };
  634. /**
  635. * 自动初始化项目
  636. */
  637. const initProject = () => {
  638. //停止计时
  639. getClearTimer("countdownTimer");
  640. };
  641. /**
  642. * 倒计时
  643. */
  644. const getCounting = (type: string) => {
  645. timerManager.value.countdownTimer = setInterval(() => {
  646. //正计时
  647. if (type == "+") {
  648. time.value.countdownNum++;
  649. }
  650. //倒计时
  651. if (type == "-") {
  652. if (time.value.countdownNum <= 0) {
  653. getClearTimer("countdownTimer");
  654. } else {
  655. time.value.countdownNum--;
  656. }
  657. }
  658. }, 1000);
  659. };
  660. /**
  661. * 成绩
  662. */
  663. const getAchievement = (data: any) => {
  664. if (!data) {
  665. return;
  666. }
  667. let dataList = data.map((item: any) => {
  668. if (item.track && item.times != 0) {
  669. if (typeof item.times === "string") {
  670. item.times = JSON.parse(item.times);
  671. }
  672. if (typeof item.times === "object") {
  673. item.timeStr = proxy?.$utils.runTime(item.times[item.times.length - 1], false, isLongRun.value);
  674. item.turns = Math.floor(item.times.length / 2);
  675. if (item.times.length) {
  676. item.timeTotal = item.times.reduce((total: any, num: any) => {
  677. return total + Number(num);
  678. });
  679. } else {
  680. item.timeTotal = 0;
  681. }
  682. } else {
  683. item.timeStr = proxy?.$utils.runTime(item.times, false, isLongRun.value);
  684. item.turns = 0;
  685. item.timeTotal = 0;
  686. }
  687. }
  688. return item
  689. });
  690. faceStudentList.value.forEach((item: any, index: any) => {
  691. let obj = dataList.find((items: any) => {
  692. return parseInt(item.track) == parseInt(items.track);
  693. })
  694. //加this.result_id == obj.result_id是避免抢跑的时候上一把成绩没返回会被覆盖的可能,要新的ID才能赋值
  695. if (obj != undefined && resultId.value == obj.result_id) {
  696. if (
  697. parseInt(obj.track) === parseInt(item.track) ||
  698. isLongRun.value
  699. ) {
  700. faceStudentList.value[index].timeStr = obj.timeStr
  701. faceStudentList.value[index].times = obj.times
  702. faceStudentList.value[index].timeTotal = obj.timeTotal
  703. faceStudentList.value[index].tid_num = obj.tid_num
  704. faceStudentList.value[index].score = obj.score
  705. faceStudentList.value[index].isfinish = obj.isfinish
  706. faceStudentList.value[index].turns = obj.turns
  707. faceStudentList.value[index].result_id = obj.result_id
  708. } else {
  709. faceStudentList.value[index].trackFalse = true;
  710. faceStudentList.value[index].timeStr = "跑道错误";
  711. }
  712. }
  713. });
  714. };
  715. /**
  716. * 移除长跑待测试列表
  717. */
  718. const close = (data: any) => {
  719. faceStudentList.value = JSON.parse(JSON.stringify(faceStudentList.value)).filter((item: any) => {
  720. return item.student_id != data.student_id;
  721. })
  722. };
  723. /**
  724. * 长跑分页并按圈数排序
  725. */
  726. const testListArr: any = computed(() => {
  727. // 按圈数分组排序
  728. let list = faceStudentList.value.filter((item: any) => {
  729. return item.student_id;
  730. });
  731. let myArr: any = [];
  732. JSON.parse(JSON.stringify(list)).forEach((item: any, index: number) => {
  733. let myIndex = 0;
  734. if (item.times != undefined) {
  735. myIndex = item.times.length;
  736. }
  737. if (myArr[myIndex] == undefined) {
  738. myArr[myIndex] = [];
  739. }
  740. myArr[myIndex].push(item);
  741. if (myArr[myIndex]) {
  742. myArr[myIndex].sort((a: any, b: any) => {
  743. let val1 = a.timeTotal;
  744. let val2 = b.timeTotal;
  745. return val1 - val2;
  746. });
  747. }
  748. })
  749. let myList = [];
  750. if (myArr.length) {
  751. for (let i = myArr.length - 1; i >= 0; --i) {
  752. if (myArr.hasOwnProperty(i)) {
  753. myList.push(...myArr[i]);
  754. }
  755. }
  756. } else {
  757. myList = list;
  758. }
  759. // 分页
  760. let myFaceStudentList = myList;
  761. let arrList: any = [];
  762. let num = 8;
  763. let myLength = Math.ceil(myFaceStudentList.length / num);
  764. for (let i = 0; i < myLength; i++) {
  765. arrList[i] = [];
  766. for (let j = 0; j < myFaceStudentList.length; j++) {
  767. if (j >= i * num && j < (i + 1) * num) {
  768. arrList[i].push(myFaceStudentList[j])
  769. }
  770. }
  771. }
  772. return arrList;
  773. });
  774. /**
  775. * 时间转换
  776. */
  777. const countdownNumFormat = computed(() => {
  778. return proxy?.$utils.timeFormat(time.value.countdownNum);
  779. });
  780. onBeforeMount(() => {
  781. parameter.value = route.query;
  782. let project = parameter.value.project;
  783. let area = parameter.value.area;
  784. parameter.value.examId = `${project}_${area}`; //项目+区
  785. if (parameter.value.time) {
  786. time.value.testTime = parameter.value.time
  787. }
  788. time.value.countdownNum = time.value.testTime;
  789. let myInfo: any = localStorage.getItem("userInfo");
  790. userInfo.value = JSON.parse(myInfo);
  791. let dic: any = dataDictionary;
  792. unit.value = dic.unit[project];
  793. if (parameter.value.gesture == 'true') {
  794. parameter.value.gesture = true
  795. } else {
  796. parameter.value.gesture = false
  797. }
  798. //是否折返跑
  799. if (project.slice(0, 3) === "run" &&
  800. project.includes("x")) {
  801. isBackRun.value = true
  802. }
  803. //是否长跑
  804. if (project.replace('run', '') > 799) {
  805. isLongRun.value = true
  806. }
  807. //需要开始按钮的项目
  808. needStart.value = true;
  809. //加载WS
  810. initWs({ parameter: parameter.value, testTime: time.value.testTime }, (data: any) => {
  811. getMessage(data);
  812. });
  813. //初始化语音
  814. initSpeech();
  815. //刷新关闭
  816. window.onbeforeunload = function (e) {
  817. var confirmationMessage = "刷新/关闭页面将会关闭页面,是否确认退出页面?";
  818. (e || window.event).returnValue = confirmationMessage; // 兼容 Gecko + IE
  819. let bUrl = import.meta.env.VITE_APP_BASE_API;
  820. let classId = parameter.value.classes;
  821. let project = parameter.value.project;
  822. let area = parameter.value.area;
  823. let examId = `${project}_${area}`;
  824. let mySid = sid.value;
  825. let token: any = localStorage.getItem("token")
  826. let formData = new FormData();
  827. formData.append("exam_id", examId);
  828. formData.append("class_id", classId);
  829. formData.append("token", token);
  830. formData.append("sid", mySid);
  831. navigator.sendBeacon(bUrl + "/exam/close_exam", formData)
  832. return confirmationMessage; // 兼容 Gecko + Webkit, Safari, Chrome
  833. };
  834. })
  835. onBeforeUnmount(() => {
  836. getExit();
  837. })
  838. </script>
  839. <style scoped lang="scss">
  840. $topPadding: 5.19rem;
  841. $waiPadding: 6.51rem;
  842. .time {
  843. width: 20vh;
  844. height: 20vh;
  845. line-height: 20vh;
  846. border-radius: 50%;
  847. color: #FF9402;
  848. font-size: 7.5vh;
  849. text-align: center;
  850. background-image: url("@/assets/images/test/time.png");
  851. background-position: center;
  852. background-repeat: no-repeat;
  853. background-size: 100% 100%;
  854. position: absolute;
  855. right: 50%;
  856. top: -4.5vh;
  857. margin-right: -10vh;
  858. font-family: 'Saira-BlackItalic';
  859. }
  860. .main {
  861. width: calc(100% - ($waiPadding * 2));
  862. height: 70vh;
  863. padding-top: 10rem;
  864. margin: 0 auto;
  865. display: flex;
  866. justify-content: space-between;
  867. overflow: hidden;
  868. .main-left {
  869. width: 71.5%;
  870. height: 100%;
  871. display: flex;
  872. background: linear-gradient(58deg, #092941 -85%, #2A484B 96%);
  873. box-shadow: inset 0px 1px 0px 2px rgba(255, 255, 255, 0.4);
  874. border-radius: 1.6rem;
  875. .trackItem {
  876. width: 100%;
  877. overflow-y: scroll;
  878. padding: 0px 10px;
  879. &::-webkit-scrollbar {
  880. width: 10px;
  881. }
  882. &::-webkit-scrollbar-thumb {
  883. border-width: 2px;
  884. border-radius: 4px;
  885. border-style: dashed;
  886. border-color: transparent;
  887. background-color: rgba(26, 62, 78, 0.9);
  888. background-clip: padding-box;
  889. }
  890. &::-webkit-scrollbar-button:hover {
  891. border-radius: 6px;
  892. background: rgba(26, 62, 78, 1);
  893. }
  894. .li {
  895. display: flex;
  896. align-items: center;
  897. justify-content: space-between;
  898. border-bottom: 1px solid #475557;
  899. height: calc(100%/8);
  900. box-sizing: border-box;
  901. padding: 0px 20px 0px 50px;
  902. .left {
  903. display: flex;
  904. align-items: center;
  905. .track {
  906. width: 5rem;
  907. color: #13ED84;
  908. font-size: 2rem;
  909. font-family: 'Saira-ExtraBold';
  910. margin-right: 1rem
  911. }
  912. .pic {
  913. width: 6.7vh;
  914. height: 6.7vh;
  915. border-radius: 50%;
  916. display: flex;
  917. justify-content: center;
  918. align-items: center;
  919. overflow: hidden;
  920. margin-right: 15px;
  921. img {
  922. width: 100%;
  923. }
  924. }
  925. .pic2 {
  926. box-sizing: border-box;
  927. border: 1px solid rgba(255, 255, 255, 0.5);
  928. }
  929. .userInfo {
  930. display: flex;
  931. justify-content: flex-start;
  932. align-items: center;
  933. .nameBox {
  934. width: 8rem;
  935. .name {
  936. font-size: 1.5rem;
  937. color: #ffffff;
  938. }
  939. }
  940. }
  941. }
  942. .scoreBox {
  943. display: flex;
  944. align-items: center;
  945. .score {
  946. color: #ffffff;
  947. font-size: 2rem;
  948. font-family: 'Saira-ExtraBold';
  949. }
  950. .turns {
  951. margin-left: 3rem;
  952. color: #ffffff;
  953. display: flex;
  954. align-items: center;
  955. span {
  956. font: 1.5rem;
  957. margin-left: 5px;
  958. }
  959. i {
  960. font-style: normal;
  961. font-size: 1.8rem;
  962. font-family: 'Saira-ExtraBold';
  963. }
  964. }
  965. }
  966. .menuBtn {
  967. width: auto;
  968. padding: 0 1.2rem;
  969. height: 2.8rem;
  970. line-height: 2.8rem;
  971. border-radius: 1.4rem;
  972. color: #ffffff;
  973. font-size: 1.5rem;
  974. background: linear-gradient(180deg, #FFB200 0%, #ED7905 72%);
  975. box-shadow: inset 0px 1px 0px 1px rgba(255, 255, 255, 0.5);
  976. display: flex;
  977. align-items: center;
  978. cursor: pointer;
  979. }
  980. .menuBtn2 {
  981. color: #1A293A;
  982. background: radial-gradient(159% 126% at 5% 93%, #8EFFA9 0%, #07FFE7 100%);
  983. box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.1874), inset 0px 1px 0px 2px rgba(255, 255, 255, 0.3);
  984. }
  985. .close {
  986. width: 2rem;
  987. height: 2rem;
  988. }
  989. }
  990. }
  991. }
  992. .main-left2 {
  993. width: 100%;
  994. .trackItem {
  995. .li {
  996. .scoreBox {
  997. .score {
  998. font-size: 1.8rem;
  999. }
  1000. }
  1001. }
  1002. }
  1003. }
  1004. .main-right {
  1005. width: 27%;
  1006. border-radius: 1.6rem;
  1007. background: linear-gradient(29deg, #092941 -82%, #2A484B 94%);
  1008. box-shadow: inset 0px 1px 0px 2px rgba(255, 255, 255, 0.4);
  1009. display: flex;
  1010. flex-direction: column;
  1011. overflow: hidden;
  1012. }
  1013. .swiper-btn {
  1014. width: 2.5rem;
  1015. height: 2.5rem;
  1016. display: block;
  1017. &::after {
  1018. display: none;
  1019. }
  1020. }
  1021. .swiper-btn-left {
  1022. background: url("@/assets/images/ranking/btn-left.png") left center no-repeat;
  1023. background-size: 100% 100%;
  1024. }
  1025. .swiper-btn-right {
  1026. background: url("@/assets/images/ranking/btn-right.png") left center no-repeat;
  1027. background-size: 100% 100%;
  1028. }
  1029. }
  1030. .footerBtn {
  1031. width: 100%;
  1032. padding: 0 calc(13.02rem /2);
  1033. box-sizing: border-box;
  1034. position: fixed;
  1035. bottom: 3vh;
  1036. display: flex;
  1037. justify-content: end;
  1038. .btn {
  1039. width: 14.6vw;
  1040. height: 6.1vh;
  1041. line-height: 6.1vh;
  1042. font-size: 3vh;
  1043. color: #1A293A;
  1044. text-align: center;
  1045. border-radius: 1vh;
  1046. margin-left: 20px;
  1047. cursor: pointer;
  1048. background: radial-gradient(159% 126% at 5% 93%, #8EFFA9 0%, #07FFE7 100%);
  1049. box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.1874), inset 1px 1px 1px 1px rgba(255, 255, 255, 0.3);
  1050. &:hover {
  1051. background: #8EFFA9;
  1052. }
  1053. }
  1054. .startBtn {
  1055. color: #ffffff;
  1056. background: radial-gradient(159% 126% at 5% 93%, #F99F02 0%, #ED7905 100%);
  1057. box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.1874), inset 1px 1px 1px 1px rgba(255, 255, 255, 0.3);
  1058. &:hover {
  1059. background: #F99F02;
  1060. }
  1061. }
  1062. }
  1063. </style>