run.vue 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154
  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 (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. let loading = ElLoading.service({ text: '请稍等...', background: 'rgba(0, 0, 0, 0.8)', customClass: `sports ${parameter.value.project}` });
  414. //预存测试人员
  415. let student = faceStudentList.value.map((item: any) => {
  416. let obj = {
  417. face_pic: item.face_pic,
  418. student_id: item.student_id,
  419. student_name: item.student_name,
  420. gender: item.gender,
  421. track: item.track,
  422. }
  423. return obj;
  424. });
  425. //测试中
  426. if (examState.value == 42) {
  427. await finishOneTest();
  428. }
  429. //其他状态
  430. if (examState.value > 3) {
  431. await closeOneTest();
  432. }
  433. //重新走一次流程
  434. await openOneTest();
  435. await startFace();
  436. await stopFace();
  437. getFaceConfirmOnly(student);
  438. loading?.close();
  439. let loadingTime = setTimeout(() => {
  440. loading?.close();
  441. clearTimeout(loadingTime);
  442. }, 10000);
  443. };
  444. /**
  445. * 确认退出
  446. */
  447. const getConfirmEnd = async () => {
  448. let txt = isLongRun.value ? '是否结束' : '是否开始下一组?'
  449. await proxy?.$modal.confirm(txt);
  450. let loading = ElLoading.service({ text: '请稍等...', background: 'rgba(0, 0, 0, 0.8)', customClass: `sports ${parameter.value.project}` });
  451. //测试中
  452. if (examState.value == 42) {
  453. await finishOneTest();
  454. }
  455. //其他状态
  456. if (examState.value > 3) {
  457. await closeOneTest();
  458. }
  459. if (!isLongRun.value) {
  460. //重新走一次流程
  461. await openOneTest();
  462. await startFace();
  463. }
  464. loading?.close();
  465. let loadingTime = setTimeout(() => {
  466. loading?.close();
  467. clearTimeout(loadingTime);
  468. }, 10000);
  469. };
  470. /**
  471. * 确认退出
  472. */
  473. const confirmExit = () => {
  474. proxy?.$modal.confirm("确定退出吗?").then(() => {
  475. getExit();
  476. }).finally(() => {
  477. });
  478. };
  479. /**
  480. * 退出
  481. */
  482. const getExit = () => {
  483. getClearTimer();//清除计时器
  484. examEnds();//通知工作站关闭
  485. speckCancel()//停止播报;
  486. window.onbeforeunload = null;//移除事件处理器
  487. router.push({ path: '/' });//跳转
  488. };
  489. /**
  490. * 清空定时任务
  491. */
  492. const getClearTimer = (data?: any) => {
  493. if (data) {
  494. //清除指定
  495. clearInterval(timerManager.value[data])
  496. timerManager.value[data] = null;
  497. } else {
  498. //清除全部
  499. for (let key in timerManager.value) {
  500. if (timerManager.value.hasOwnProperty(key)) {
  501. clearInterval(timerManager.value[key])
  502. timerManager.value[key] = null;
  503. }
  504. }
  505. }
  506. };
  507. /**
  508. * 选择学生
  509. */
  510. const getChooseStudent = (track?: number) => {
  511. if (examState.value < 41) {
  512. if (needStart.value) {
  513. proxy?.$modal.msgWarning("请点击开始识别");
  514. } else {
  515. proxy?.$modal.msgWarning("请稍等...");
  516. }
  517. return false;
  518. }
  519. if (examState.value == 43) {
  520. proxy?.$modal.msgWarning("请点击重新识别");
  521. return false;
  522. }
  523. currentTrack.value = track;
  524. stopFace();
  525. chooseStudentRef.value.open();
  526. };
  527. /**
  528. * 返回被选学生
  529. */
  530. const returnStudent = (data: any) => {
  531. if (isLongRun.value) {
  532. //长跑
  533. longStudent(data)
  534. } else {
  535. //短跑
  536. sprintStudent(data)
  537. }
  538. };
  539. /**
  540. * 处理短跑返回被选学生
  541. */
  542. const sprintStudent = (data: any) => {
  543. if (data == undefined || !data) {
  544. return false;
  545. }
  546. suspendFaceRecognitionChannels(currentTrack.value);//停止识别
  547. let obj = {
  548. result_id: resultId.value,
  549. face_pic: data.face_pic || data.logo_url,
  550. student_id: data.id || data.student_id,
  551. student_name: data.name,
  552. gender: data.gender,
  553. }
  554. //可能已检录的先清除
  555. let oldIndex = faceStudentList.value.findIndex((item: any) => {
  556. return data.student_id == item.student_id;
  557. })
  558. if (oldIndex != -1) {
  559. faceStudentList.value[oldIndex] = { track: faceStudentList.value[oldIndex].track };
  560. }
  561. //赋值
  562. let newIndex = faceStudentList.value.findIndex((item: any) => {
  563. return currentTrack.value == item.track;
  564. })
  565. faceStudentList.value[newIndex] = { ...faceStudentList.value[newIndex], ...obj };
  566. if (!isLongRun.value) {
  567. speckText(`第${currentTrack.value == 2 ? '二' : currentTrack.value}道, ${data.name}`);
  568. }
  569. currentTrack.value = null;
  570. };
  571. /**
  572. * 处理长跑返回被选学生
  573. */
  574. const longStudent = (data: any) => {
  575. let ids = faceStudentList.value.map((item: any, index: any) => {
  576. return item.student_id;
  577. });
  578. //排除掉已经存在的
  579. let newList = data.filter((item: any, index: any) => {
  580. return !ids.includes(item.id)
  581. })
  582. let list = newList.map((item: any, index: any) => {
  583. let obj = {
  584. result_id: resultId.value,
  585. face_pic: item.face_pic || item.logo_url,
  586. student_id: item.id,
  587. student_name: item.name,
  588. gender: item.gender,
  589. }
  590. return obj;
  591. })
  592. faceStudentList.value.push(...list)
  593. };
  594. /**
  595. * 重新识别指定跑道
  596. */
  597. const getRetestTrackFace = (data: any) => {
  598. if (data == undefined || !data) {
  599. return false;
  600. }
  601. resumeFaceRecognitionChannels(data);//重新识别
  602. let obj = {
  603. student_id: null,
  604. track: data,
  605. isfinish: false,
  606. }
  607. let myIndex = faceStudentList.value.findIndex((item: any) => {
  608. return item.track == data;
  609. })
  610. faceStudentList.value[myIndex] = obj;
  611. };
  612. /**
  613. * 清除历史记录
  614. */
  615. const cleanData = () => {
  616. time.value.countdownNum = time.value.testTime;
  617. showTestAgain.value = false;
  618. if (isLongRun.value) {
  619. faceStudentList.value = [];
  620. } else {
  621. //重置全部
  622. let list = faceStudentList.value.map((item: any) => {
  623. let obj = {
  624. student_id: null,
  625. track: item.track,
  626. isfinish: false,
  627. }
  628. return obj;
  629. })
  630. faceStudentList.value = list;
  631. }
  632. };
  633. /**
  634. * 自动初始化项目
  635. */
  636. const initProject = () => {
  637. //停止计时
  638. getClearTimer("countdownTimer");
  639. };
  640. /**
  641. * 时间转换
  642. */
  643. const countdownNumFormat = computed(() => {
  644. return proxy?.$utils.timeFormat(time.value.countdownNum);
  645. });
  646. /**
  647. * 倒计时
  648. */
  649. const getCounting = (type: string) => {
  650. timerManager.value.countdownTimer = setInterval(() => {
  651. //正计时
  652. if (type == "+") {
  653. time.value.countdownNum++;
  654. }
  655. //倒计时
  656. if (type == "-") {
  657. if (time.value.countdownNum <= 0) {
  658. getClearTimer("countdownTimer");
  659. } else {
  660. time.value.countdownNum--;
  661. }
  662. }
  663. }, 1000);
  664. };
  665. /**
  666. * 成绩
  667. */
  668. const getAchievement = (data: any) => {
  669. if (!data) {
  670. return;
  671. }
  672. let dataList = data.map((item: any) => {
  673. if (item.track && item.times != 0) {
  674. if (typeof item.times === "string") {
  675. item.times = JSON.parse(item.times);
  676. }
  677. if (typeof item.times === "object") {
  678. item.timeStr = proxy?.$utils.runTime(item.times[item.times.length - 1], false, isLongRun.value);
  679. item.turns = Math.floor(item.times.length / 2);
  680. if (item.times.length) {
  681. item.timeTotal = item.times.reduce((total: any, num: any) => {
  682. return total + Number(num);
  683. });
  684. } else {
  685. item.timeTotal = 0;
  686. }
  687. } else {
  688. item.timeStr = proxy?.$utils.runTime(item.times, false, isLongRun.value);
  689. item.turns = 0;
  690. item.timeTotal = 0;
  691. }
  692. }
  693. return item
  694. });
  695. faceStudentList.value.forEach((item: any, index: any) => {
  696. let obj = dataList.find((items: any) => {
  697. return parseInt(item.track) == parseInt(items.track);
  698. })
  699. //加this.result_id == obj.result_id是避免抢跑的时候上一把成绩没返回会被覆盖的可能,要新的ID才能赋值
  700. if (obj != undefined && resultId.value == obj.result_id) {
  701. if (
  702. parseInt(obj.track) === parseInt(item.track) ||
  703. isLongRun.value
  704. ) {
  705. faceStudentList.value[index].timeStr = obj.timeStr
  706. faceStudentList.value[index].times = obj.times
  707. faceStudentList.value[index].timeTotal = obj.timeTotal
  708. faceStudentList.value[index].tid_num = obj.tid_num
  709. faceStudentList.value[index].score = obj.score
  710. faceStudentList.value[index].isfinish = obj.isfinish
  711. faceStudentList.value[index].turns = obj.turns
  712. faceStudentList.value[index].result_id = obj.result_id
  713. } else {
  714. faceStudentList.value[index].trackFalse = true;
  715. faceStudentList.value[index].timeStr = "跑道错误";
  716. }
  717. }
  718. });
  719. };
  720. /**
  721. * 当天成绩列表
  722. */
  723. const close = (data: any) => {
  724. faceStudentList.value = JSON.parse(JSON.stringify(faceStudentList.value)).filter((item: any) => {
  725. return item.student_id != data.student_id;
  726. })
  727. };
  728. /**
  729. * 将测试列表分页
  730. */
  731. const testListArr: any = computed(() => {
  732. let list: any = [];
  733. let num = 8;
  734. let myLength = Math.ceil(faceStudentList.value.length / num);
  735. for (let i = 0; i < myLength; i++) {
  736. list[i] = [];
  737. for (let j = 0; j < faceStudentList.value.length; j++) {
  738. if (j >= i * num && j < (i + 1) * num) {
  739. list[i].push(faceStudentList.value[j])
  740. }
  741. }
  742. }
  743. console.log("111", list)
  744. return list;
  745. });
  746. /**
  747. * 长跑名单
  748. */
  749. const faceStudentListLongRun: any = computed(() => {
  750. if (isLongRun.value) {
  751. return false;
  752. }
  753. console.log("嘻嘻嘻0", faceStudentList.value)
  754. let list = faceStudentList.value.filter((item: any) => {
  755. return item.student_id;
  756. });
  757. //按圈数分组排序
  758. let arr: any = [];
  759. JSON.parse(JSON.stringify(list)).forEach((item: any, index: number) => {
  760. let myIndex = 0;
  761. if (item.times != undefined) {
  762. myIndex = item.times.length;
  763. }
  764. if (arr[myIndex] == undefined) {
  765. arr[myIndex] = [];
  766. }
  767. arr[myIndex].push(item);
  768. if (arr[myIndex]) {
  769. arr[myIndex].sort((a: any, b: any) => {
  770. let val1 = a.timeTotal;
  771. let val2 = b.timeTotal;
  772. return val1 - val2;
  773. });
  774. }
  775. })
  776. console.log("嘻嘻嘻", arr);
  777. if (arr.length) {
  778. console.log("嘻嘻嘻1")
  779. let myList = [];
  780. for (let i = arr.length - 1; i >= 0; --i) {
  781. if (arr.hasOwnProperty(i)) {
  782. myList.push(...arr[i]);
  783. }
  784. }
  785. console.log("嘻嘻嘻3", list)
  786. return myList;
  787. } else {
  788. console.log("嘻嘻嘻2", list)
  789. return list;
  790. }
  791. });
  792. onBeforeMount(() => {
  793. parameter.value = route.query;
  794. let project = parameter.value.project;
  795. let area = parameter.value.area;
  796. parameter.value.examId = `${project}_${area}`; //项目+区
  797. if (parameter.value.time) {
  798. time.value.testTime = parameter.value.time
  799. }
  800. time.value.countdownNum = time.value.testTime;
  801. let myInfo: any = localStorage.getItem("userInfo");
  802. userInfo.value = JSON.parse(myInfo);
  803. let dic: any = dataDictionary;
  804. unit.value = dic.unit[project];
  805. if (parameter.value.gesture == 'true') {
  806. parameter.value.gesture = true
  807. } else {
  808. parameter.value.gesture = false
  809. }
  810. //是否折返跑
  811. if (project.slice(0, 3) === "run" &&
  812. project.includes("x")) {
  813. isBackRun.value = true
  814. }
  815. //是否长跑
  816. if (project.replace('run', '') > 799) {
  817. isLongRun.value = true
  818. }
  819. //需要开始按钮的项目
  820. needStart.value = true;
  821. //加载WS
  822. initWs({ parameter: parameter.value, testTime: time.value.testTime }, (data: any) => {
  823. getMessage(data);
  824. });
  825. //初始化语音
  826. initSpeech();
  827. //刷新关闭
  828. window.onbeforeunload = function (e) {
  829. var confirmationMessage = "刷新/关闭页面将会关闭页面,是否确认退出页面?";
  830. (e || window.event).returnValue = confirmationMessage; // 兼容 Gecko + IE
  831. let bUrl = import.meta.env.VITE_APP_BASE_API;
  832. let classId = parameter.value.classes;
  833. let project = parameter.value.project;
  834. let area = parameter.value.area;
  835. let examId = `${project}_${area}`;
  836. let mySid = sid.value;
  837. let token: any = localStorage.getItem("token")
  838. let formData = new FormData();
  839. formData.append("exam_id", examId);
  840. formData.append("class_id", classId);
  841. formData.append("token", token);
  842. formData.append("sid", mySid);
  843. navigator.sendBeacon(bUrl + "/exam/close_exam", formData)
  844. return confirmationMessage; // 兼容 Gecko + Webkit, Safari, Chrome
  845. };
  846. })
  847. onBeforeUnmount(() => {
  848. getExit();
  849. })
  850. </script>
  851. <style scoped lang="scss">
  852. $topPadding: 5.19rem;
  853. $waiPadding: 6.51rem;
  854. .time {
  855. width: 20vh;
  856. height: 20vh;
  857. line-height: 20vh;
  858. border-radius: 50%;
  859. color: #FF9402;
  860. font-size: 7.5vh;
  861. text-align: center;
  862. background-image: url("@/assets/images/test/time.png");
  863. background-position: center;
  864. background-repeat: no-repeat;
  865. background-size: 100% 100%;
  866. position: absolute;
  867. right: 50%;
  868. top: -4.5vh;
  869. margin-right: -10vh;
  870. font-family: 'Saira-BlackItalic';
  871. }
  872. .main {
  873. width: calc(100% - ($waiPadding * 2));
  874. height: 70vh;
  875. padding-top: 10rem;
  876. margin: 0 auto;
  877. display: flex;
  878. justify-content: space-between;
  879. overflow: hidden;
  880. .main-left {
  881. width: 71.5%;
  882. height: 100%;
  883. display: flex;
  884. background: linear-gradient(58deg, #092941 -85%, #2A484B 96%);
  885. box-shadow: inset 0px 1px 0px 2px rgba(255, 255, 255, 0.4);
  886. border-radius: 1.6rem;
  887. .trackItem {
  888. width: 100%;
  889. overflow-y: scroll;
  890. padding: 0px 10px;
  891. &::-webkit-scrollbar {
  892. width: 10px;
  893. }
  894. &::-webkit-scrollbar-thumb {
  895. border-width: 2px;
  896. border-radius: 4px;
  897. border-style: dashed;
  898. border-color: transparent;
  899. background-color: rgba(26, 62, 78, 0.9);
  900. background-clip: padding-box;
  901. }
  902. &::-webkit-scrollbar-button:hover {
  903. border-radius: 6px;
  904. background: rgba(26, 62, 78, 1);
  905. }
  906. .li {
  907. display: flex;
  908. align-items: center;
  909. justify-content: space-between;
  910. border-bottom: 1px solid #475557;
  911. height: calc(100%/8);
  912. box-sizing: border-box;
  913. padding: 0px 20px 0px 50px;
  914. .left {
  915. display: flex;
  916. align-items: center;
  917. .track {
  918. width: 5rem;
  919. color: #13ED84;
  920. font-size: 2rem;
  921. font-family: 'Saira-ExtraBold';
  922. margin-right: 1rem
  923. }
  924. .pic {
  925. width: 6.7vh;
  926. height: 6.7vh;
  927. border-radius: 50%;
  928. display: flex;
  929. justify-content: center;
  930. align-items: center;
  931. overflow: hidden;
  932. margin-right: 15px;
  933. img {
  934. width: 100%;
  935. }
  936. }
  937. .pic2 {
  938. box-sizing: border-box;
  939. border: 1px solid rgba(255, 255, 255, 0.5);
  940. }
  941. .userInfo {
  942. display: flex;
  943. justify-content: flex-start;
  944. align-items: center;
  945. .nameBox {
  946. width: 8rem;
  947. .name {
  948. font-size: 1.5rem;
  949. color: #ffffff;
  950. }
  951. }
  952. }
  953. }
  954. .scoreBox {
  955. display: flex;
  956. align-items: center;
  957. .score {
  958. color: #ffffff;
  959. font-size: 2rem;
  960. font-family: 'Saira-ExtraBold';
  961. }
  962. .turns {
  963. margin-left: 3rem;
  964. color: #ffffff;
  965. display: flex;
  966. align-items: center;
  967. span {
  968. font: 1.5rem;
  969. margin-left: 5px;
  970. }
  971. i {
  972. font-style: normal;
  973. font-size: 1.8rem;
  974. font-family: 'Saira-ExtraBold';
  975. }
  976. }
  977. }
  978. .menuBtn {
  979. width: auto;
  980. padding: 0 1.2rem;
  981. height: 2.8rem;
  982. line-height: 2.8rem;
  983. border-radius: 1.4rem;
  984. color: #ffffff;
  985. font-size: 1.5rem;
  986. background: linear-gradient(180deg, #FFB200 0%, #ED7905 72%);
  987. box-shadow: inset 0px 1px 0px 1px rgba(255, 255, 255, 0.5);
  988. display: flex;
  989. align-items: center;
  990. cursor: pointer;
  991. }
  992. .menuBtn2 {
  993. color: #1A293A;
  994. background: radial-gradient(159% 126% at 5% 93%, #8EFFA9 0%, #07FFE7 100%);
  995. box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.1874), inset 0px 1px 0px 2px rgba(255, 255, 255, 0.3);
  996. }
  997. .close {
  998. width: 2rem;
  999. height: 2rem;
  1000. }
  1001. }
  1002. }
  1003. }
  1004. .main-left2 {
  1005. width: 100%;
  1006. .trackItem {
  1007. .li {
  1008. .scoreBox {
  1009. .score {
  1010. font-size: 1.8rem;
  1011. }
  1012. }
  1013. }
  1014. }
  1015. }
  1016. .main-right {
  1017. width: 27%;
  1018. border-radius: 1.6rem;
  1019. background: linear-gradient(29deg, #092941 -82%, #2A484B 94%);
  1020. box-shadow: inset 0px 1px 0px 2px rgba(255, 255, 255, 0.4);
  1021. display: flex;
  1022. flex-direction: column;
  1023. overflow: hidden;
  1024. }
  1025. .swiper-btn {
  1026. width: 2.5rem;
  1027. height: 2.5rem;
  1028. display: block;
  1029. &::after {
  1030. display: none;
  1031. }
  1032. }
  1033. .swiper-btn-left {
  1034. background: url("@/assets/images/ranking/btn-left.png") left center no-repeat;
  1035. background-size: 100% 100%;
  1036. }
  1037. .swiper-btn-right {
  1038. background: url("@/assets/images/ranking/btn-right.png") left center no-repeat;
  1039. background-size: 100% 100%;
  1040. }
  1041. }
  1042. .footerBtn {
  1043. width: 100%;
  1044. padding: 0 calc(13.02rem /2);
  1045. box-sizing: border-box;
  1046. position: fixed;
  1047. bottom: 3vh;
  1048. display: flex;
  1049. justify-content: end;
  1050. .btn {
  1051. width: 14.6vw;
  1052. height: 6.1vh;
  1053. line-height: 6.1vh;
  1054. font-size: 3vh;
  1055. color: #1A293A;
  1056. text-align: center;
  1057. border-radius: 1vh;
  1058. margin-left: 20px;
  1059. cursor: pointer;
  1060. background: radial-gradient(159% 126% at 5% 93%, #8EFFA9 0%, #07FFE7 100%);
  1061. box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.1874), inset 1px 1px 1px 1px rgba(255, 255, 255, 0.3);
  1062. &:hover {
  1063. background: #8EFFA9;
  1064. }
  1065. }
  1066. .startBtn {
  1067. color: #ffffff;
  1068. background: radial-gradient(159% 126% at 5% 93%, #F99F02 0%, #ED7905 100%);
  1069. box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.1874), inset 1px 1px 1px 1px rgba(255, 255, 255, 0.3);
  1070. &:hover {
  1071. background: #F99F02;
  1072. }
  1073. }
  1074. }
  1075. </style>