test.vue 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454
  1. <template>
  2. <div class="test">
  3. <Header @confirmExit="confirmExit"></Header>
  4. <div class="main">
  5. <div class="main-left">
  6. <div class="main-left-top">
  7. <div class="top-left" @click="getChooseStudent">
  8. <div class="top-left-center">
  9. <div class="pic" :class="{ 'pic2': faceCheckStu.student_id }" v-if="faceCheckStu.student_id"> <img
  10. :src="faceCheckStu.face_pic || faceCheckStu.logo_url" /></div>
  11. <div class="pic" v-else>
  12. <img src="@/assets/images/test/profilePicture.png" />
  13. </div>
  14. <div class="name" :class="{ 'name2': faceCheckStu.student_id }">
  15. {{ faceCheckStu.student_id ? faceCheckStu.name : "虚位以待" }}
  16. </div>
  17. </div>
  18. </div>
  19. <div class="top-right">
  20. <Transition :enter-active-class="proxy?.animate.dialog.enter"
  21. :leave-active-class="proxy?.animate.dialog.leave">
  22. <div class="time"
  23. v-show="needStart && [42].includes(examState) && !['basketballv1', 'footballv1'].includes(parameter.project)">
  24. {{
  25. time.countdownNum
  26. }}</div>
  27. </Transition>
  28. <div class="tips" v-if="examState == 41">
  29. <img v-if="parameter.gesture" src="@/assets/images/test/ready1.png" />
  30. <img v-if="!parameter.gesture" src="@/assets/images/test/ready2.png" />
  31. </div>
  32. <div class="complete" :class="{ 'complete2': needStart && [42].includes(examState) }"
  33. v-if="faceCheckStu.student_id && time.ready <= 0 && examState != 43 && examState != 41">
  34. <div class="scoreBox" v-if="['basketballv1', 'footballv1'].includes(parameter.project)">
  35. <div class="score">{{ currentResultObj.count || 0 }}</div>
  36. </div>
  37. <div class="scoreBox" v-else>
  38. <div class="score">{{ currentResultObj.count || 0 }}</div>
  39. <div class="unit" v-if="currentResultObj.count && !needStart">{{ unit }}</div>
  40. </div>
  41. <div class="fractionViolation">
  42. <div class="fraction">
  43. <div class="lable">得分:</div>
  44. <div class="value">{{ currentResultObj.score || "" }}</div>
  45. </div>
  46. <div class="violation">
  47. <div class="lable">{{ ['jumprope', 'jumpingjack', 'highknees'].includes(parameter.project) ? '中断' : '犯规'
  48. }}</div>
  49. <div class="value">{{ currentResultObj.back_num || 0 }}</div>
  50. </div>
  51. </div>
  52. </div>
  53. <div class="foulBox" v-if="examState == 42 && backReason.length">
  54. <Transition :enter-active-class="proxy?.animate.mask.enter" :leave-active-class="proxy?.animate.mask.leave">
  55. <div class="foul" v-show="backReasonStr ? true : false">
  56. <div class="lable">!</div>
  57. <div class="value">{{ backReasonStr }}</div>
  58. </div>
  59. </Transition>
  60. </div>
  61. <div v-show="examState == 43 && time.ready">
  62. <div class="readyBox">
  63. <div class="value" :class="{ 'transparent': time.ready > 5 }">{{ time.ready }}</div>
  64. <div class="lable">倒计时</div>
  65. </div>
  66. </div>
  67. <div v-show="examState == 43 && faceCheckStu.student_id && !time.ready && readyState">
  68. <div class="readyBoxBefore">
  69. <div class="item" v-if="parameter.handcontroller">
  70. <div><img src="@/assets/images/test/jushou.png" /></div>
  71. <div class="lable">
  72. <div>请举左手开始</div>
  73. </div>
  74. </div>
  75. <div class="item" v-else>
  76. <div> <img src="@/assets/images/test/bujushou.png" /></div>
  77. <div class="lable">
  78. <div>请点击开始</div>
  79. </div>
  80. </div>
  81. </div>
  82. </div>
  83. <div v-show="examState == 43 && !faceCheckStu.student_id">
  84. <div class="btn btn2" @click="getChooseStudent">点击重新识别</div>
  85. </div>
  86. <div class="btn" @click="getReady"
  87. v-if="needStart && examState == 43 && faceCheckStu.student_id && !time.ready && readyState">开 始
  88. </div>
  89. <!-- <div v-if="needStart"> -->
  90. <!-- <div class="btn" @click="getOpenOneTestAndStartFace" v-if="examState < 41">开始识别</div> -->
  91. <!-- <div class="btn" @click="getStopFace" v-if="examState == 41 && !parameter.gesture">停止人脸识别</div> -->
  92. <!-- <div class="btn" @click="getStartOneTest" v-if="examState == 43">开始测试</div> -->
  93. <!-- <div @click="getRetestFace" v-if="examState == 43 || examState == 42">重新识别</div> -->
  94. <!-- </div> -->
  95. <!-- <div>当前状态:({{ examState == 3 ? "初始化完成" : examState == 40 ? "创建测试" : examState == 41 ? "正在人脸识别":examState == 43 ? "停止人脸识别" : examState == 42 ? "正在测试" : "请初始化" }})</div> -->
  96. <!-- <div @click="getAgain" v-if="examState == 42 || showTestAgain">再测一次</div> -->
  97. </div>
  98. <i></i>
  99. </div>
  100. <div class="main-left-bottom">
  101. <div class="bottom-left">
  102. <div class="tips"><img src="/src/assets/images/test/tips.png" /></div>
  103. <div class="pic"><img :src="'static/images/tips/' + parameter.project + '.png'" /></div>
  104. </div>
  105. <div class="bottom-right" v-html="dic.projectNote[parameter.project]">
  106. </div>
  107. </div>
  108. </div>
  109. <div class="main-right">
  110. <ReportList ref="reportListRef" :parameter="parameter" :showQRCode="true" />
  111. </div>
  112. </div>
  113. <FaceWindow ref="faceWindowRef" :faceCheckStu="faceCheckStu" :gesture="parameter.gesture" />
  114. <ChooseStudent ref="chooseStudentRef" @returnData="returnStudent" />
  115. </div>
  116. </template>
  117. <script setup name="TrainTest" lang="ts">
  118. import { useWs } from '@/utils/trainWs';
  119. // import { initWs, examEnds, openOneTest, startFace, stopFace, faceConfirmOnly, startOneTest, finishOneTest, closeOneTest, suspendFaceRecognitionChannels, resumeFaceRecognitionChannels } from '@/utils/ws'
  120. import { initSpeech, speckText, playMusic, controlMusic, speckCancel, chineseNumber } from '@/utils/speech'
  121. import { useWebSocket } from '@/utils/handWs';
  122. import dayjs from 'dayjs'
  123. import dataDictionary from "@/utils/dataDictionary"
  124. const { handWs, startDevice, startHand, stateHand } = useWebSocket();
  125. const { initWs, examEnds, openOneTest, startFace, stopFace, faceConfirmOnly, startOneTest, finishOneTest, closeOneTest, suspendFaceRecognitionChannels, resumeFaceRecognitionChannels } = useWs();
  126. const { proxy } = getCurrentInstance() as any;
  127. const router = useRouter();
  128. const route = useRoute();
  129. const faceWindowRef = ref();
  130. const chooseStudentRef = ref();
  131. const reportListRef = ref();
  132. const myInfo: any = localStorage.getItem("userInfo");
  133. const dic: any = dataDictionary;
  134. const data = reactive<any>({
  135. timerManager: {},//计时器管理
  136. parameter: {},//参数
  137. time: {
  138. testTime: 60,//时长
  139. countdownNum: 0,//计时
  140. ready: 0,//预备
  141. exit: 0,//退出倒计时
  142. },
  143. userInfo: {},//用户信息
  144. examState: 0,//当前状态
  145. resultId: null,//测试ID
  146. currentResultObj: {},//成绩
  147. faceCheckStu: {},//人脸信息
  148. unit: "",//单位
  149. backReason: [],//犯规项
  150. backReasonStr: "",//犯规提示
  151. needStart: false,//是否需要按钮
  152. showTestAgain: false,//再测一次按钮
  153. readyState: true,//倒计时按钮状态
  154. exitStatus: 0,//退出响应次数
  155. sid: null,//WS的id
  156. device_info: {},//设备信息
  157. });
  158. const { timerManager, parameter, time, userInfo, examState, resultId, faceCheckStu, currentResultObj, unit, backReason, backReasonStr, needStart, showTestAgain, readyState, exitStatus, sid, device_info } = toRefs(data);
  159. /**
  160. * 接收消息
  161. */
  162. const getMessage = (e: any) => {
  163. //console.log("WS响应:", e)
  164. //获取sid
  165. if (e.cmd === 'mySid') {
  166. console.log("e.data.sid", e.data.sid)
  167. sid.value = e.data.sid;
  168. }
  169. //实时状态
  170. if (e.cmd === 'exam_status') {
  171. examState.value = e.data;
  172. }
  173. //工作站状态
  174. if (e.cmd === 'init_result') {
  175. }
  176. //测试违规
  177. if (e.cmd === 'warning_result') {
  178. console.log("eeeeeeeeeee", e)
  179. if ((e.status + "")[0] === "2") {
  180. proxy?.$modal.msgError(e.data.message);
  181. speckText(e.data.message);
  182. }
  183. }
  184. //后端播报语音
  185. if (e.cmd === 'return_audio_msg') {
  186. if (e.data.message) {
  187. proxy?.$modal.msgError(e.data.message);
  188. speckText(e.data.message);
  189. }
  190. }
  191. //错误提示
  192. if (e.cmd === 'info_result') {
  193. proxy?.$modal.msgError(e.data.message);
  194. }
  195. //错误提示
  196. if (e.cmd === 'error_result') {
  197. proxy?.$modal.msgError(e.data.message);
  198. }
  199. //测试中违规提示
  200. if (e.cmd === 'warning_notify') {
  201. let message = e.data?.message;
  202. if (message) {
  203. proxy?.$modal.msgError(message);
  204. speckText(message);
  205. }
  206. if (message == "工作站已断开!") {
  207. getExit();
  208. }
  209. }
  210. //断线状态
  211. if (e.cmd === 'disconnect_request') {
  212. let message = e.data.message;
  213. if (message) {
  214. proxy?.$modal.msgError(`${message}`);
  215. speckText(message);
  216. }
  217. getExit();
  218. }
  219. //状态变更
  220. if (e.cmd === 'set_exam_state') {
  221. examState.value = e.data;
  222. if (e.data === 3) {
  223. initProject();
  224. }
  225. if (e.data === 40) {
  226. cleanData();
  227. }
  228. if (e.data == 41) {
  229. getFaceWindow(true);
  230. }
  231. if (e.data == 43) {
  232. }
  233. if (e.data == 42) {
  234. getClearTimer("readyTimer");
  235. time.value.ready = 0;
  236. }
  237. }
  238. //新建测试后返回信息,获取result_id
  239. if (e.cmd === 'open_one_test_ack') {
  240. resultId.value = e.data.result_id;
  241. }
  242. //人脸识别状态
  243. if (e.cmd === 'face_check_result') {
  244. let myData = e.data[0] || e.data;
  245. returnStudent(myData);
  246. }
  247. //测试结束结果
  248. if (e.cmd === 'oneresult') {
  249. if (e.data.length) {
  250. let data = e.data[0];
  251. getAchievement(data)
  252. }
  253. }
  254. //结果生成完成(视频图片)
  255. if (e.cmd === 'static_urls_finished') {
  256. }
  257. //选择学生或测试结束后返回的数据
  258. if (e.cmd === 'result_info') {
  259. }
  260. };
  261. /**
  262. * 开始识别
  263. */
  264. const getOpenOneTestAndStartFace = async () => {
  265. if (examState.value > 3) {
  266. await closeOneTest();
  267. }
  268. await openOneTest();
  269. await startFace();
  270. };
  271. /**
  272. * 停止人脸识别
  273. */
  274. const getStopFace = async () => {
  275. // 旧版识别成功直接43了这里先屏蔽
  276. // if (examState.value != 41) {
  277. // return false;
  278. // }
  279. getClearTimer("face");
  280. if (needStart.value) {
  281. let txt = parameter.value.handcontroller ? ",请举左手开始测试" : ",请准备";
  282. speckText(faceCheckStu.value.name + txt);
  283. }
  284. if (examState.value == 41) {
  285. await stopFace();
  286. }
  287. if (faceCheckStu.value.student_id) {
  288. getFaceConfirmOnly();
  289. }
  290. };
  291. /**
  292. * 确定人脸信息
  293. */
  294. const getFaceConfirmOnly = (data?: any) => {
  295. if (data) {
  296. faceCheckStu.value = data;
  297. }
  298. faceConfirmOnly({
  299. result_id: resultId.value,
  300. student_id: faceCheckStu.value.student_id,
  301. gender: faceCheckStu.value.gender
  302. }, () => {
  303. faceWindowRef.value?.close();
  304. //不需要按钮的自动进入下一步
  305. if (needStart.value == false) {
  306. getStartOneTest();
  307. }
  308. });
  309. };
  310. /**
  311. * 重新识别
  312. */
  313. const getRetestFace = () => {
  314. proxy?.$modal.confirm("确定重新识别吗?").then(() => {
  315. cleanData();
  316. if (needStart.value == false) {
  317. //自动流程项目重新识别直接返回3
  318. closeOneTest();
  319. } else {
  320. //手动流程项目重新识别43返回41,42返回3
  321. if (examState.value == 43) {
  322. startFace();
  323. } else {
  324. closeOneTest();
  325. }
  326. }
  327. }).finally(() => {
  328. });
  329. };
  330. /**
  331. * 开始测试
  332. */
  333. const getStartOneTest = () => {
  334. if (examState.value != 43 || !faceCheckStu.value.student_id) {
  335. return false;
  336. }
  337. if (!faceCheckStu.value.student_id) {
  338. proxy?.$modal.msgWarning("请选择人员!");
  339. return false;
  340. }
  341. startOneTest(null, () => {
  342. //显示再测一次按钮
  343. showTestAgain.value = true;
  344. //停止播报;
  345. speckCancel();
  346. //计时项目才开
  347. if (needStart.value == true) {
  348. speckText("哨声");
  349. if (parameter.value.music) {
  350. playMusic(parameter.value.music);
  351. }
  352. //时间为0的为正计时,大于0的为倒计时
  353. if (time.value.testTime == 0) {
  354. getCounting("+");
  355. } else {
  356. getCounting("-");
  357. }
  358. } else {
  359. speckText(faceCheckStu.value.name + ",请开始测试");
  360. }
  361. })
  362. };
  363. /**
  364. * 再测一次
  365. */
  366. const getAgain = async () => {
  367. let txt = '是否再测一次?'
  368. await proxy?.$modal.confirm(txt);
  369. getClearTimer();
  370. //预存测试人员
  371. let student = JSON.parse(JSON.stringify(faceCheckStu.value));
  372. //测试中
  373. if (examState.value == 42) {
  374. await finishOneTest();
  375. }
  376. //其他状态
  377. if (examState.value > 3) {
  378. await closeOneTest();
  379. }
  380. //重新走一次流程
  381. await openOneTest();
  382. await startFace();
  383. await stopFace();
  384. getFaceConfirmOnly(student);
  385. };
  386. /**
  387. * 确认退出
  388. */
  389. const confirmExit = () => {
  390. let handcontroller_id = parameter.value.handcontroller;
  391. proxy?.$modal.confirm(handcontroller_id ? `请保持两秒确认退出` : `确定退出吗?`).then(() => {
  392. getExit();
  393. }).finally(() => {
  394. });
  395. };
  396. /**
  397. * 退出
  398. */
  399. const getExit = () => {
  400. getClearTimer();//清除计时器
  401. examEnds();//通知工作站关闭
  402. speckCancel();//停止播报
  403. window.onbeforeunload = null;//移除事件处理器
  404. let handcontroller_id = parameter.value.handcontroller;
  405. if (handcontroller_id) {
  406. router.push({ path: '/gesture' });//跳转
  407. } else {
  408. router.push({ path: '/' });//跳转
  409. }
  410. };
  411. /**
  412. * 清空定时任务
  413. */
  414. const getClearTimer = (data?: any) => {
  415. if (data) {
  416. //清除指定
  417. clearInterval(timerManager.value[data])
  418. timerManager.value[data] = null;
  419. } else {
  420. //清除全部
  421. for (let key in timerManager.value) {
  422. if (timerManager.value.hasOwnProperty(key)) {
  423. clearInterval(timerManager.value[key])
  424. timerManager.value[key] = null;
  425. }
  426. }
  427. }
  428. };
  429. /**
  430. * 选择学生
  431. */
  432. const getChooseStudent = () => {
  433. if (examState.value < 41) {
  434. proxy?.$modal.msgWarning("请等待");
  435. }
  436. if (examState.value == 41) {
  437. stopFace()
  438. chooseStudentRef.value.open();
  439. //然后定时自动关闭
  440. setTimeout(() => {
  441. faceWindowRef.value.close();
  442. }, 3000)
  443. }
  444. if (examState.value == 43) {
  445. getRetestFace();
  446. }
  447. if (examState.value == 42) {
  448. proxy?.$modal.msgWarning(`正在测试请结束后再操作,当前状态:${examState.value}`);
  449. return false;
  450. }
  451. };
  452. /**
  453. * 返回被选学生
  454. */
  455. const returnStudent = (data: any) => {
  456. speckCancel();
  457. chooseStudentRef.value.close();
  458. faceCheckStu.value = data;
  459. faceWindowRef.value.open();
  460. //然后定时自动关闭
  461. setTimeout(() => {
  462. faceWindowRef.value.close();
  463. }, 1000)
  464. getStopFace();
  465. };
  466. /**
  467. * 清除历史记录
  468. */
  469. const cleanData = () => {
  470. time.value.countdownNum = time.value.testTime;
  471. showTestAgain.value = false;
  472. faceCheckStu.value = {};
  473. currentResultObj.value = {};
  474. backReason.value = [];
  475. };
  476. /**
  477. * 自动初始化项目
  478. */
  479. const initProject = () => {
  480. //停止计时
  481. getClearTimer("countdownTimer");
  482. //恢复倒计时按钮状态
  483. readyState.value = true;
  484. //自动项目定时进入下一步
  485. let time = 0;
  486. //控制新建测试的时间,第一次快,之后就慢
  487. if (!faceCheckStu.value.student_id) {
  488. time = 1000;
  489. } else {
  490. time = 6000;
  491. }
  492. setTimeout(() => {
  493. //再加一个判断以免和再测一次冲突
  494. if (examState.value == 3) {
  495. getOpenOneTestAndStartFace();
  496. }
  497. }, time)
  498. };
  499. /**
  500. * 倒计时
  501. */
  502. const getCounting = (type: string) => {
  503. timerManager.value.countdownTimer = setInterval(() => {
  504. //正计时
  505. if (type == "+") {
  506. time.value.countdownNum++;
  507. }
  508. //倒计时
  509. if (type == "-") {
  510. if (time.value.countdownNum <= 0) {
  511. getClearTimer("countdownTimer");
  512. } else {
  513. time.value.countdownNum--;
  514. }
  515. }
  516. }, 1000);
  517. };
  518. /**
  519. * 人脸窗口
  520. */
  521. const getFaceWindow = (data: boolean, num: number = 0) => {
  522. let total = num + 1;//叠加三次后不再播放
  523. let txt = parameter.value.gesture === true ? "请举右手看摄像头人脸识别" : "请看摄像头进行人脸识别";
  524. speckText(txt);
  525. //data=true为弹出框,data=false为不要弹出框
  526. if (data) {
  527. faceWindowRef.value.open();
  528. //然后定时自动关闭
  529. setTimeout(() => {
  530. if (examState.value == 41 && faceWindowRef.value?.faceState == true) {
  531. faceWindowRef.value.close();
  532. }
  533. }, 3000)
  534. }
  535. //定时检查如果一直停留在人脸识别就提示
  536. let timeout = 16000;
  537. timerManager.value.face = setInterval(() => {
  538. getClearTimer("face");
  539. if (examState.value == 41 && total < 3) {
  540. getFaceWindow(false, total);
  541. }
  542. }, timeout)
  543. };
  544. /**
  545. * 成绩
  546. */
  547. const getAchievement = (data: any) => {
  548. //console.log("成绩", data);
  549. let type = parameter.value.project;
  550. let count =
  551. data?.[dic.typeResultKey[type]]?.toFixed(0);
  552. if (["trijump", "solidball", "shotput", "longjump"].includes(type)) {
  553. count =
  554. data?.[dic.typeResultKey[type]]?.toFixed(2);
  555. count = Math.round(count) / 100;
  556. }
  557. if (["basketballv1", "footballv1"].includes(type)) {
  558. count = proxy?.$utils.runTime(data?.[dic.typeResultKey[type]], true, 1)
  559. }
  560. data.count = count || "0";
  561. data.score = data.score || "0";
  562. currentResultObj.value = data;
  563. //违规处理
  564. let arr = backReason.value;
  565. if (["situp", "pullup", "sidepullup", "jumprope", "jumpingjack", "highknees", "jump", "longjump", "verticaljump"]
  566. .indexOf(type) > -1) {
  567. if (["pullup", "situp", "jumprope", "jumpingjack", "highknees"].indexOf(type) > -1) {
  568. currentResultObj.value.back_num = data?.all_failed_num;
  569. }
  570. if (type === "sidepullup") {
  571. currentResultObj.value.back_num = data?.["0"]?.hip_failed_num;
  572. }
  573. if (['jump', 'longjump', 'verticaljump'].includes(type)) {
  574. if (data?.startline_check == 0) {
  575. let txt = "踩线违规";
  576. speckText(txt);
  577. arr.push(txt)
  578. }
  579. if (data?.singleleg_jump_check == 0) {
  580. let txt = "单脚跳违规";
  581. speckText(txt);
  582. arr.push(txt)
  583. }
  584. if (data?.outside_check == 0) {
  585. let txt = "跳出测试区违规";
  586. speckText(txt);
  587. arr.push(txt)
  588. }
  589. }
  590. if (
  591. data?.elbow_check == false
  592. ) {
  593. let txt = "肘部违规";
  594. speckText(txt);
  595. arr.push(txt);
  596. }
  597. if (
  598. ["situp", "pullup"].indexOf(type) > -1 &&
  599. data?.knee_check === false
  600. ) {
  601. let txt = "腿部违规";
  602. speckText(txt);
  603. if (!arr.includes(txt)) { }
  604. arr.push(txt);
  605. }
  606. if (["situp"].indexOf(type) > -1 && data?.hand_check === false) {
  607. let txt = "手部违规";
  608. speckText(txt);
  609. if (!arr.includes(txt)) { }
  610. arr.push(txt);
  611. }
  612. if (
  613. ["pullup"].indexOf(type) > -1 &&
  614. data?.["0"]?.elbow_check === false
  615. ) {
  616. let txt = "手部违规";
  617. speckText(txt);
  618. if (!arr.includes(txt)) { }
  619. arr.push(txt);
  620. }
  621. if (["situp"].indexOf(type) > -1 && data?.["0"]?.back_check === false) {
  622. let txt = "背部违规";
  623. speckText(txt);
  624. if (!arr.includes(txt)) { }
  625. arr.push(txt);
  626. }
  627. if (
  628. ["sidepullup", "situp"].indexOf(type) > -1 &&
  629. data?.["0"]?.hip_check === false
  630. ) {
  631. let txt = "臀部违规";
  632. speckText(txt);
  633. if (!arr.includes(txt)) { }
  634. arr.push(txt);
  635. }
  636. }
  637. backReason.value = arr;
  638. if (data.isfinish) {
  639. if (['jump'].includes(type) && backReason.value.length) {
  640. speckText("请重新测试");
  641. return false;
  642. }
  643. if (["basketballv1", "footballv1"].includes(type)) {
  644. speckText(faceCheckStu?.value.name + "成绩为" + (chineseNumber(proxy?.$utils.runTime(data?.[dic.typeResultKey[type]], false, 0,
  645. 1)) || 0) + ",请下一位准备!" || "");
  646. } else {
  647. speckText(faceCheckStu?.value.name + "成绩为" + (chineseNumber(count) || 0) + unit.value + ",请下一位准备!" || "");
  648. }
  649. reportListRef.value.getIniReportList();
  650. faceWindowRef.value.open();
  651. //然后定时自动关闭
  652. setTimeout(() => {
  653. faceWindowRef.value.close("right");
  654. }, 1000)
  655. }
  656. };
  657. /**
  658. * 准备开始
  659. */
  660. const getReady = () => {
  661. if (needStart.value && examState.value == 43 && !time.value.ready && readyState.value) {
  662. if (time.value.ready) {
  663. return false;
  664. }
  665. speckCancel();
  666. readyState.value = false;
  667. time.value.ready = 6;
  668. timerManager.value.readyTimer = setInterval(() => {
  669. time.value.ready--;
  670. if (time.value.ready <= 0) {
  671. getClearTimer("readyTimer");
  672. getStartOneTest();
  673. }else{
  674. speckText(time.value.ready);
  675. }
  676. }, 1000);
  677. }
  678. };
  679. /**
  680. * 获取设备项目
  681. */
  682. const getDevice = async () => {
  683. let deviceid = localStorage.getItem("deviceid") || '';
  684. if (deviceid) {
  685. startDevice({ deviceid: deviceid })
  686. } else {
  687. proxy?.$modal.msgError(`缺少设备信息请重新登录!`);
  688. await proxy?.$http.common.logout({}).then((res: any) => {
  689. });
  690. proxy?.$modal?.closeLoading()
  691. //清空缓存
  692. localStorage.clear();
  693. //跳转
  694. router.push({ path: '/login/qrcode' });
  695. }
  696. };
  697. /**
  698. * 加载手势WS
  699. */
  700. const initHand = () => {
  701. handWs((e: any) => {
  702. if (router.currentRoute.value.path != '/train/test' || parameter.value.handcontroller == undefined || examState.value == 0) {
  703. return false;
  704. }
  705. console.log("eeeee", e)
  706. if (e?.wksid) {
  707. //获取设备项目
  708. getDevice()
  709. }
  710. //接收设备信息
  711. if (e?.device_info) {
  712. device_info.value = e.device_info;
  713. let handcontroller_id = device_info.value.handcontroller_id;
  714. stateHand(handcontroller_id);
  715. }
  716. //获取手势状态
  717. if (e?.cmd == 'get_handcontroller_state' && e?.state == 0) {
  718. let handcontroller_id = device_info.value.handcontroller_id;
  719. startHand(handcontroller_id);
  720. }
  721. //刷新
  722. if (e?.data?.result == "refresh") {
  723. getExit();
  724. //刷新
  725. window.location.reload()
  726. }
  727. //没初始化完成不监听手势动作
  728. if (examState.value == 0) {
  729. return false;
  730. }
  731. //左滑动
  732. if (e?.data?.result == "next_item") {
  733. proxy?.$modal.msgSuccess('手势指令:左滑动');
  734. // if(examState.value == 43 && time.value.ready){
  735. // return false;
  736. // }
  737. // if (examState.value == 43 || examState.value == 42) {
  738. // speckCancel();//停止播报
  739. // if (needStart.value == false) {
  740. // //自动流程项目重新识别直接返回3
  741. // closeOneTest();
  742. // } else {
  743. // //手动流程项目重新识别43返回41,42返回3
  744. // if (examState.value == 43) {
  745. // cleanData();
  746. // startFace();
  747. // } else {
  748. // closeOneTest();
  749. // }
  750. // }
  751. // }
  752. }
  753. //举左手
  754. if (e?.data?.result == "left_hand") {
  755. proxy?.$modal.msgSuccess('手势指令:举左手');
  756. //举左手确认退出
  757. // if (exitStatus.value) {
  758. // exitStatus.value = 0;
  759. // //确认退出
  760. // let keyEvent: any = null;
  761. // keyEvent = new KeyboardEvent('keydown', {
  762. // key: 'Enter', // 键值
  763. // code: 'Enter', // 键盘代码
  764. // keyCode: 13, // 旧的键盘代码
  765. // which: 13, // 新的键盘代码
  766. // shiftKey: false, // 是否按下Shift键
  767. // ctrlKey: false, // 是否按下Ctrl键
  768. // metaKey: false, // 是否按下Meta键(Win键或Command键)
  769. // bubbles: true, // 事件是否冒泡
  770. // cancelable: true // 是否可以取消事件的默认行为
  771. // });
  772. // document.activeElement?.dispatchEvent(keyEvent);
  773. // return false;
  774. // }
  775. //开始识别
  776. if (needStart.value && examState.value < 41) {
  777. getOpenOneTestAndStartFace();
  778. return false;
  779. }
  780. //停止人脸识别
  781. // if (needStart.value && examState.value == 41) {
  782. // getStopFace();
  783. // }
  784. //开始测试
  785. if (examState.value == 43) {
  786. if (needStart.value) {
  787. getReady()
  788. } else {
  789. getStartOneTest();
  790. }
  791. return false;
  792. }
  793. }
  794. //退出
  795. if (e?.data?.result == "exit") {
  796. proxy?.$modal.msgSuccess('手势指令:交叉手');
  797. // console.log("exitStatus.value", exitStatus.value)
  798. if (exitStatus.value == 0) {
  799. speckText("请保持两秒确认退出");
  800. //第一次才弹出
  801. confirmExit();
  802. setTimeout(() => {
  803. let keyEvent: any = null;
  804. let myKey = null;
  805. //如果交叉手两秒后返回超过4次就确认退出
  806. if (exitStatus.value >= 4) {
  807. myKey = 'Enter';
  808. } else {
  809. myKey = 'Esc';
  810. }
  811. if (myKey == 'Esc') {
  812. keyEvent = new KeyboardEvent('keydown', {
  813. key: 'Escape', // 键值
  814. code: 'Escape', // 键盘代码
  815. keyCode: 27, // 旧的键盘代码
  816. which: 27, // 新的键盘代码
  817. shiftKey: false, // 是否按下Shift键
  818. ctrlKey: false, // 是否按下Ctrl键
  819. metaKey: false, // 是否按下Meta键(Win键或Command键)
  820. bubbles: true, // 事件是否冒泡
  821. cancelable: true // 是否可以取消事件的默认行为
  822. });
  823. exitStatus.value = 0;
  824. }
  825. if (myKey == 'Enter') {
  826. keyEvent = new KeyboardEvent('keydown', {
  827. key: 'Enter', // 键值
  828. code: 'Enter', // 键盘代码
  829. keyCode: 13, // 旧的键盘代码
  830. which: 13, // 新的键盘代码
  831. shiftKey: false, // 是否按下Shift键
  832. ctrlKey: false, // 是否按下Ctrl键
  833. metaKey: false, // 是否按下Meta键(Win键或Command键)
  834. bubbles: true, // 事件是否冒泡
  835. cancelable: true // 是否可以取消事件的默认行为
  836. });
  837. }
  838. document.activeElement?.dispatchEvent(keyEvent);
  839. }, 2500)
  840. }
  841. exitStatus.value = exitStatus.value + 1
  842. }
  843. // if (e?.data?.result == "exit") {
  844. // console.log("exitStatus.value", exitStatus.value)
  845. // if (exitStatus.value == 0) {
  846. // exitStatus.value = 1
  847. // speckText("请5秒内举左手确认退出");
  848. // //第一次才弹出
  849. // confirmExit();
  850. // time.value.exit = 6;
  851. // timerManager.value.exitTimer = setInterval(() => {
  852. // time.value.exit--;
  853. // console.log("取消倒计时", time.value.exit)
  854. // proxy?.$modal.msgWarning(`取消倒计时:${time.value.exit}`)
  855. // if (time.value.exit == 0) {
  856. // exitStatus.value = 0;
  857. // getClearTimer("exitTimer");
  858. // let keyEvent: any = null;
  859. // keyEvent = new KeyboardEvent('keydown', {
  860. // key: 'Escape', // 键值
  861. // code: 'Escape', // 键盘代码
  862. // keyCode: 27, // 旧的键盘代码
  863. // which: 27, // 新的键盘代码
  864. // shiftKey: false, // 是否按下Shift键
  865. // ctrlKey: false, // 是否按下Ctrl键
  866. // metaKey: false, // 是否按下Meta键(Win键或Command键)
  867. // bubbles: true, // 事件是否冒泡
  868. // cancelable: true // 是否可以取消事件的默认行为
  869. // });
  870. // document.activeElement?.dispatchEvent(keyEvent);
  871. // }
  872. // }, 1000);
  873. // }
  874. // }
  875. });
  876. };
  877. /**
  878. * 输出犯规
  879. */
  880. watch(() => backReason.value.length, (v) => {
  881. backReasonStr.value = backReason.value[backReason.value.length - 1];
  882. setTimeout(() => {
  883. backReasonStr.value = "";
  884. }, 1500)
  885. }, { immediate: true });
  886. /**
  887. * 播报时间
  888. */
  889. watch(() => time.value.countdownNum, (newData) => {
  890. if (examState.value != 42) {
  891. return false;
  892. }
  893. if (newData >= 30) {
  894. if (newData % 30 == 0) {
  895. speckText(
  896. `还有${newData}秒`
  897. );
  898. }
  899. }
  900. if (newData == 10) {
  901. speckText("还有10秒");
  902. }
  903. if (newData <= 5) {
  904. speckText(newData);
  905. }
  906. if (newData == 0) {
  907. speckText("哨声");
  908. }
  909. }, { immediate: true });
  910. /**
  911. * 成绩整数播报
  912. */
  913. watch(() => currentResultObj.value, (newData: any, oldData: any) => {
  914. if (examState.value != 42 || newData.count <= 0) {
  915. return false;
  916. }
  917. let project = parameter.value.project;
  918. //引体向上比较慢所以都播报
  919. if (
  920. ["pullup"].includes(project) &&
  921. newData.count > 0 &&
  922. oldData.back_num == oldData.back_num
  923. ) {
  924. speckText(newData.count);
  925. }
  926. if (
  927. ["situp", "sidepullup", "jumprope", "jumpingjack", "highknees"].includes(project) &&
  928. newData.count > 0 &&
  929. newData.count % 10 == 0 &&
  930. oldData.back_num == oldData.back_num
  931. ) {
  932. speckText(newData.count);
  933. }
  934. }, { immediate: true });
  935. /**
  936. * 时间转换
  937. */
  938. // const countdownNumFormat = computed(() => {
  939. // return time.value.countdownNum;
  940. // //return proxy?.$utils.timeFormat(time.value.countdownNum);
  941. // });
  942. onBeforeMount(() => {
  943. parameter.value = route.query;
  944. let project = parameter.value.project;
  945. let area = parameter.value.area;
  946. parameter.value.examId = `${project}_${area}`; //项目+区
  947. if (parameter.value.time) {
  948. time.value.testTime = parameter.value.time
  949. }
  950. time.value.countdownNum = time.value.testTime;
  951. userInfo.value = JSON.parse(myInfo);
  952. unit.value = dic.unit[project];
  953. if (parameter.value.gesture == 'true') {
  954. parameter.value.gesture = true
  955. } else {
  956. parameter.value.gesture = false
  957. }
  958. //需要开始按钮的项目
  959. let myList = ['situp', 'jumprope', 'jumpingjack', 'highknees', 'footballv1', 'basketballv1', 'pingpong'];
  960. if (myList.includes(project)) {
  961. needStart.value = true;
  962. }
  963. //加载WS
  964. initWs({ parameter: parameter.value, testTime: time.value.testTime }, (data: any) => {
  965. getMessage(data);
  966. });
  967. //初始化语音
  968. initSpeech();
  969. //初始化手势
  970. initHand();
  971. //刷新关闭
  972. window.onbeforeunload = function (e) {
  973. var confirmationMessage = "刷新/关闭页面将会关闭页面,是否确认退出页面?";
  974. (e || window.event).returnValue = confirmationMessage; // 兼容 Gecko + IE
  975. let bUrl = import.meta.env.VITE_APP_BASE_API;
  976. let classId = parameter.value.classes;
  977. let project = parameter.value.project;
  978. let area = parameter.value.area;
  979. let examId = `${project}_${area}`;
  980. let mySid = sid.value;
  981. let token: any = localStorage.getItem("token")
  982. let formData = new FormData();
  983. formData.append("exam_id", examId);
  984. formData.append("class_id", classId);
  985. formData.append("token", token);
  986. formData.append("sid", mySid);
  987. navigator.sendBeacon(bUrl + "/exam/close_exam", formData)
  988. return confirmationMessage; // 兼容 Gecko + Webkit, Safari, Chrome
  989. };
  990. })
  991. onBeforeUnmount(() => {
  992. getExit();
  993. })
  994. </script>
  995. <style lang="scss" scoped>
  996. $topPadding: 5.19rem;
  997. $waiPadding: 6.51rem;
  998. .main {
  999. width: calc(100% - ($waiPadding * 2));
  1000. height: 78vh;
  1001. padding-top: 10rem;
  1002. margin: 0 auto;
  1003. display: flex;
  1004. justify-content: space-between;
  1005. overflow: hidden;
  1006. .main-left {
  1007. width: 71.5%;
  1008. display: flex;
  1009. flex-direction: column;
  1010. justify-content: space-between;
  1011. .main-left-top {
  1012. display: flex;
  1013. justify-content: space-between;
  1014. height: 55.8%;
  1015. position: relative;
  1016. .top-left {
  1017. width: 37.4%;
  1018. height: 100%;
  1019. border-radius: 1.6rem;
  1020. background: radial-gradient(122% 126% at 97% 6%, #35FFC6 0%, #00FFE8 100%);
  1021. text-align: center;
  1022. display: flex;
  1023. align-items: center;
  1024. justify-content: center;
  1025. cursor: pointer;
  1026. .top-left-center {
  1027. .pic {
  1028. width: 22.3vh;
  1029. height: 22.3vh;
  1030. border-radius: 50%;
  1031. display: flex;
  1032. justify-content: center;
  1033. align-items: center;
  1034. overflow: hidden;
  1035. margin: 0 auto 2vh auto;
  1036. img {
  1037. width: 100%;
  1038. }
  1039. }
  1040. .pic2 {
  1041. box-sizing: border-box;
  1042. border: 0.44rem solid rgba(26, 41, 58, 0.6315);
  1043. }
  1044. .name {
  1045. width: 100%;
  1046. color: #1A293A;
  1047. font-size: 2.21rem;
  1048. }
  1049. .name2 {
  1050. padding: 0 0.3rem;
  1051. border-radius: 1.1rem;
  1052. background: radial-gradient(96% 96% at 2% 32%, #FFFFFF 0%, #FCFDFD 54%, #E1E4E7 100%);
  1053. box-shadow: inset 0px 1px 0px 2px rgba(255, 255, 255, 0.9046), inset 0px 3px 6px 0px rgba(0, 0, 0, 0.0851);
  1054. }
  1055. }
  1056. }
  1057. .top-right {
  1058. width: 62%;
  1059. height: 100%;
  1060. border-radius: 1.6rem;
  1061. opacity: 1;
  1062. background: #ffffff;
  1063. box-sizing: border-box;
  1064. border: 0.55rem solid #13ED84;
  1065. display: flex;
  1066. align-items: center;
  1067. justify-content: center;
  1068. flex-direction: column;
  1069. position: relative;
  1070. .time {
  1071. width: 28vh;
  1072. height: 28vh;
  1073. line-height: 28vh;
  1074. border-radius: 50%;
  1075. color: #FF9402;
  1076. font-size: 11vh;
  1077. text-align: center;
  1078. background-image: url("@/assets/images/test/time.png");
  1079. background-position: center;
  1080. background-repeat: no-repeat;
  1081. background-size: 100% 100%;
  1082. position: absolute;
  1083. right: -1.5vh;
  1084. top: -11vh;
  1085. font-family: 'Saira-BlackItalic';
  1086. }
  1087. .tips {
  1088. display: flex;
  1089. justify-content: center;
  1090. align-items: center;
  1091. height: 100%;
  1092. img {
  1093. max-height: 80%;
  1094. max-height: 80%;
  1095. }
  1096. }
  1097. .complete {
  1098. width: 100%;
  1099. display: flex;
  1100. justify-content: center;
  1101. flex-direction: column;
  1102. .scoreBox {
  1103. height: 10vh;
  1104. color: #1A293A;
  1105. display: flex;
  1106. align-items: center;
  1107. justify-content: center;
  1108. margin-bottom: 5vh;
  1109. .score {
  1110. font-size: 8.5rem;
  1111. line-height: 8.5rem;
  1112. font-family: 'Saira-BlackItalic';
  1113. }
  1114. .unit {
  1115. font-size: 2rem;
  1116. margin-left: 10px;
  1117. }
  1118. }
  1119. .fractionViolation {
  1120. height: 10vh;
  1121. display: flex;
  1122. align-items: center;
  1123. justify-content: center;
  1124. .fraction {
  1125. height: 10vh;
  1126. line-height: 10vh;
  1127. border-radius: 5vh;
  1128. display: flex;
  1129. align-items: center;
  1130. padding: 0 6%;
  1131. background: linear-gradient(138deg, #38536C 21%, #1A293A 75%);
  1132. box-shadow: inset 0px 1px 13px 0px rgba(255, 255, 255, 0.9452);
  1133. .lable {
  1134. font-size: 4vh;
  1135. color: #13ED84;
  1136. }
  1137. .value {
  1138. font-size: 7vh;
  1139. color: #00FFE8;
  1140. font-family: 'Saira-BlackItalic';
  1141. min-width: 7vh;
  1142. }
  1143. }
  1144. .violation {
  1145. height: 6.1vh;
  1146. line-height: 6.1vh;
  1147. border-radius: 4vh;
  1148. border: 0.25rem solid #ED7905;
  1149. display: flex;
  1150. align-items: center;
  1151. margin-left: 11px;
  1152. padding: 3px;
  1153. box-sizing: content-box;
  1154. .lable {
  1155. font-size: 1.2rem;
  1156. color: #ffffff;
  1157. width: 6.1vh;
  1158. height: 6.1vh;
  1159. line-height: 6.1vh;
  1160. background: #ED7905;
  1161. border-radius: 50%;
  1162. text-align: center;
  1163. }
  1164. .value {
  1165. margin-left: 1.5vh;
  1166. font-size: 3.2rem;
  1167. color: #ED7905;
  1168. font-family: 'Saira-BlackItalic';
  1169. min-width: 6vh;
  1170. }
  1171. }
  1172. }
  1173. }
  1174. .complete2 {
  1175. padding-left: 20%;
  1176. padding-top: 5vh;
  1177. .scoreBox {
  1178. margin-bottom: 3vh;
  1179. justify-content: left;
  1180. }
  1181. .fractionViolation {
  1182. justify-content: left;
  1183. .fraction {
  1184. padding: 0 8%;
  1185. }
  1186. }
  1187. }
  1188. .foulBox {
  1189. height: calc(4.2vh + 0.5rem + 6px);
  1190. overflow: hidden;
  1191. display: flex;
  1192. align-items: center;
  1193. justify-content: center;
  1194. padding-top: 3vh;
  1195. .foul {
  1196. height: 4.2vh;
  1197. line-height: 4.2vh;
  1198. border-radius: 3vh;
  1199. border: 0.25rem solid #ED7905;
  1200. display: flex;
  1201. align-items: center;
  1202. margin-left: 11px;
  1203. padding: 3px;
  1204. box-sizing: content-box;
  1205. .lable {
  1206. font-size: 2rem;
  1207. color: #ffffff;
  1208. width: 4.2vh;
  1209. height: 4.2vh;
  1210. line-height: 4.2vh;
  1211. background: #ED7905;
  1212. border-radius: 50%;
  1213. text-align: center;
  1214. }
  1215. .value {
  1216. margin-left: 1.5vh;
  1217. font-size: 2rem;
  1218. color: #ED7905;
  1219. font-family: 'Saira-BlackItalic';
  1220. min-width: 6vh;
  1221. padding: 0 10px;
  1222. }
  1223. }
  1224. }
  1225. .readyBoxBefore {
  1226. display: flex;
  1227. justify-content: center;
  1228. font-size: 2.5rem;
  1229. color: #1A293A;
  1230. padding-top: 4vh;
  1231. line-height: 0;
  1232. margin-bottom: 2vh;
  1233. .item {
  1234. display: flex;
  1235. justify-content: center;
  1236. .lable {
  1237. display: flex;
  1238. align-items: center;
  1239. margin-left: 10px;
  1240. }
  1241. }
  1242. img {
  1243. height: 20vh;
  1244. }
  1245. }
  1246. .readyBox {
  1247. text-align: center;
  1248. color: #1A293A;
  1249. .value {
  1250. font-size: 8.5rem;
  1251. line-height: 8.5rem;
  1252. font-family: 'Saira-BlackItalic';
  1253. }
  1254. .lable {
  1255. font-size: 3.5rem;
  1256. }
  1257. .transparent {
  1258. opacity: 0;
  1259. }
  1260. }
  1261. .btn {
  1262. font-size: 2.21rem;
  1263. color: #FFFFFF;
  1264. text-align: center;
  1265. width: 50%;
  1266. line-height: 8vh;
  1267. line-height: 8vh;
  1268. border-radius: 15px;
  1269. opacity: 1;
  1270. background: radial-gradient(159% 126% at 5% 93%, #F99F02 0%, #ED7905 100%);
  1271. box-shadow: 3px 6px 4px 1px rgba(0, 0, 0, 0.1874), inset 0px 1px 0px 2px rgba(255, 255, 255, 0.3);
  1272. cursor: pointer;
  1273. }
  1274. .btn2 {
  1275. width: auto;
  1276. padding: 0 10px;
  1277. }
  1278. }
  1279. i {
  1280. width: 4vw;
  1281. height: 4vw;
  1282. display: block;
  1283. position: absolute;
  1284. top: 50%;
  1285. left: 37.5%;
  1286. margin-top: calc(4vw * -0.5);
  1287. margin-left: calc(4vw * -0.5);
  1288. background-image: url("@/assets/images/test/yuan.png");
  1289. background-position: center;
  1290. background-repeat: no-repeat;
  1291. background-size: 100% 100%;
  1292. border-radius: 50%;
  1293. flex-shrink: 0;
  1294. transition: all 0.5s;
  1295. }
  1296. }
  1297. .main-left-bottom {
  1298. display: flex;
  1299. justify-content: space-between;
  1300. height: calc(100% - 55.8% - 3vh);
  1301. overflow: hidden;
  1302. .bottom-left {
  1303. width: 58%;
  1304. padding-right: 1rem;
  1305. display: flex;
  1306. flex-direction: column;
  1307. .tips {
  1308. height: 2.8vh;
  1309. img {
  1310. height: 100%;
  1311. }
  1312. }
  1313. .pic {
  1314. text-align: center;
  1315. width: 100%;
  1316. height: 100%;
  1317. display: flex;
  1318. justify-content: center;
  1319. overflow: hidden;
  1320. img {
  1321. max-width: 100%;
  1322. max-height: 100%;
  1323. }
  1324. }
  1325. }
  1326. .bottom-right {
  1327. width: 41%;
  1328. height: 100%;
  1329. overflow-y: scroll;
  1330. color: #F9F9F9;
  1331. font-size: 1.1rem;
  1332. line-height: 1.6rem;
  1333. &::-webkit-scrollbar {
  1334. width: 10px;
  1335. }
  1336. &::-webkit-scrollbar-thumb {
  1337. border-width: 2px;
  1338. border-radius: 4px;
  1339. border-style: dashed;
  1340. border-color: transparent;
  1341. background-color: rgba(26, 62, 78, 0.9);
  1342. background-clip: padding-box;
  1343. }
  1344. &::-webkit-scrollbar-button:hover {
  1345. border-radius: 6px;
  1346. background: rgba(26, 62, 78, 1);
  1347. }
  1348. }
  1349. }
  1350. }
  1351. .main-right {
  1352. width: 27%;
  1353. border-radius: 1.6rem;
  1354. background: linear-gradient(29deg, #092941 -82%, #2A484B 94%);
  1355. box-shadow: inset 0px 1px 0px 2px rgba(255, 255, 255, 0.4);
  1356. display: flex;
  1357. flex-direction: column;
  1358. overflow: hidden;
  1359. }
  1360. }
  1361. </style>