run.vue 33 KB

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