index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  1. <template>
  2. <div class="game">
  3. <Header @confirmExit="getExit" :showTool="false" closeClass="close2"></Header>
  4. <div class="menu" v-if="projectList.length" :class="projectList.length <= 12 ? 'menu1' : 'menu2'"
  5. :key="projectList.length">
  6. <swiper :slides-per-view="6" :modules="[Grid]" :grid="{
  7. fill: projectList.length <= 12 ? 'row' : 'column',
  8. rows: 2,
  9. }" :space-between="20" :slides-per-group="12">
  10. <swiper-slide v-for="(item, index) in projectList" :key="index" @click="getJump(item)">
  11. <div class="li">
  12. <div>
  13. <div class="pic"><img :src="'static/images/game/' + item.exam_name + '.png'" /></div>
  14. <div class="name">
  15. {{ item.exam_name_cn }}
  16. </div>
  17. </div>
  18. </div>
  19. </swiper-slide>
  20. </swiper>
  21. </div>
  22. <Transition :enter-active-class="proxy?.animate.rankingWindow.enter"
  23. :leave-active-class="proxy?.animate.rankingWindow.leave">
  24. <div class="gameWindow" v-if="start">
  25. <!---人体姿态识别-->
  26. <div class="columns" v-if="currentGame == 'bodyposecontroller'">
  27. <template v-if="currentGameArea.length == 2">
  28. <div class="item left">
  29. <HumanBody ref="humanBodyLeftRef" type="left" :currentGameArea="currentGameArea[0]"
  30. :areaStateList="areaStateList"></HumanBody>
  31. </div>
  32. <div class="item right">
  33. <HumanBody ref="humanBodyRightRef" type="right" :currentGameArea="currentGameArea[1]"
  34. :areaStateList="areaStateList"></HumanBody>
  35. </div>
  36. </template>
  37. <template v-else>
  38. <HumanBody ref="humanBodyLeftRef" type="left" :currentGameArea="currentGameArea[0]"
  39. :areaStateList="areaStateList"></HumanBody>
  40. </template>
  41. </div>
  42. <!---篮球投篮-->
  43. <div class="columns" v-if="currentGame == 'game_basketball'">
  44. <div class="item left">
  45. <Basketball ref="basketballLeftRef" type="left" @confirmExit="getExitGame"
  46. :currentGameArea="currentGameArea[0]" :areaStateList="areaStateList"></Basketball>
  47. </div>
  48. <div class="item right">
  49. <Basketball ref="basketballRightRef" type="right" @confirmExit="getExitGame"
  50. :currentGameArea="currentGameArea[1]" :areaStateList="areaStateList"></Basketball>
  51. </div>
  52. </div>
  53. <!---足球带球-->
  54. <div class="columns" v-if="currentGame == 'game_football'">
  55. <div class="item left">
  56. <Football ref="footballLeftRef" type="left" @confirmExit="getExitGame" :currentGameArea="currentGameArea[0]"
  57. :areaStateList="areaStateList">
  58. </Football>
  59. </div>
  60. <div class="item right">
  61. <Football ref="footballRightRef" type="right" @confirmExit="getExitGame"
  62. :currentGameArea="currentGameArea[1]" :areaStateList="areaStateList">
  63. </Football>
  64. </div>
  65. </div>
  66. <!---切水果-->
  67. <div class="columns" v-if="currentGame == 'game_fruit'">
  68. <Fruit ref="fruitRef" @confirmExit="getExitGame" :currentGameArea="currentGameArea[0]"
  69. :areaStateList="areaStateList">
  70. </Fruit>
  71. </div>
  72. </div>
  73. </Transition>
  74. <div class="resumeGame" v-if="resumeGame" @click="getResumeGame">继续游戏</div>
  75. <div class="close" v-if="start" @click="getExitGame"></div>
  76. <ActionConfirmWindow ref="actionConfirmRef" @confirmExit="getExitGame({ type: 1 })" @confirmStart="getStartGame">
  77. </ActionConfirmWindow>
  78. <div class="time" v-if="timerNum > 0">
  79. <div>{{ timerNum }}</div>
  80. </div>
  81. </div>
  82. </template>
  83. <script setup name="GameIndex" lang="ts">
  84. import { initSpeech, speckText, playMusic, controlMusic, speckCancel, chineseNumber } from '@/utils/speech';
  85. import { Swiper, SwiperSlide } from 'swiper/vue';
  86. import { Grid } from 'swiper/modules';
  87. import 'swiper/css';
  88. import 'swiper/css/grid';
  89. import { useWebSocket } from '@/utils/bodyposeWs';
  90. import HumanBody from "./components/humanBody.vue";
  91. import Basketball from "./components/basketball.vue";
  92. import Football from "./components/football.vue";
  93. import Fruit from "./components/fruit.vue";
  94. const router = useRouter();
  95. const { proxy } = getCurrentInstance() as any;
  96. const humanBodyLeftRef = ref();
  97. const humanBodyRightRef = ref();
  98. const basketballLeftRef = ref();
  99. const basketballRightRef = ref();
  100. const footballLeftRef = ref();
  101. const footballRightRef = ref();
  102. const fruitRef = ref();
  103. const actionConfirmRef = ref();
  104. const { bodyposeWs, startDevice, checkBodypose, openBodypose, terminateBodypose, suspendBodypose, resumeBodypose, getBodyposeState, closeWS } = useWebSocket();
  105. const data = reactive<any>({
  106. projectList: [],//项目列表
  107. wsState: false,//WS状态
  108. bodyposeState: false,//姿态识别窗口状态
  109. deviceInfo: {},//设备信息
  110. currentGame: "",//当前游戏
  111. currentGameArea: [],//当前游戏区
  112. start: false,//是否开始游戏
  113. areaStateList: [],//各游戏就绪状态
  114. resumeGame: false,//游戏结束显示继续游戏
  115. timer: null,
  116. timerNum: null,
  117. });
  118. const { projectList, wsState, bodyposeState, deviceInfo, currentGame, currentGameArea, start, areaStateList, resumeGame, timer, timerNum } = toRefs(data);
  119. /**
  120. * 监听数据变化
  121. */
  122. watch(
  123. () => areaStateList.value,
  124. (newData, oldData) => {
  125. //如果当前都是结束了就再重新弹窗
  126. let state = newData.every((item: any) => {
  127. return item.gameover;
  128. })
  129. if (state) {
  130. // start.value = false;
  131. // actionConfirmRef.value.getOpen(currentGame.value, currentGameArea.value);
  132. resumeGame.value = true;
  133. }
  134. },
  135. { deep: true }
  136. );
  137. /**
  138. * 初始化
  139. */
  140. const getInit = async () => {
  141. console.log("触发姿态识别")
  142. let deviceid = localStorage.getItem('deviceid') || '';
  143. if (!deviceid) {
  144. proxy?.$modal.msgError(`请重新登录绑定设备号后使用`);
  145. getExit();
  146. return false;
  147. }
  148. bodyposeState.value = true;
  149. if (wsState.value) {
  150. proxy?.$modal.msgWarning(`操作过快,请稍后重试`);
  151. setTimeout(() => {
  152. bodyposeState.value = false;
  153. }, 1000)
  154. return false;
  155. }
  156. bodyposeWs((e: any) => {
  157. if (e?.wksid) {
  158. wsState.value = true;
  159. //获取设备信息
  160. startDevice({ deviceid: deviceid });
  161. console.log("获取设备信息")
  162. }
  163. if (e?.type == 'fe_device_init_result') {
  164. //接收设备信息并发送请求
  165. if (e?.device_info) {
  166. deviceInfo.value = e.device_info;
  167. let list = deviceInfo.value.project_list.filter((item: any) => {
  168. return ['bodyposecontroller', 'game_basketball', 'game_football', 'game_fruit'].includes(item.exam_name)
  169. })
  170. projectList.value = list;
  171. if (import.meta.env.APP_ENV != 'pro') {
  172. // projectList.value.push({ exam_name_cn: "单个17点位(区号在主表)", exam_name: "test" })
  173. }
  174. } else {
  175. proxy?.$modal.msgError(`设备信息缺失,请重新登录绑定设备号后使用`);
  176. }
  177. }
  178. if (e?.cmd == 'check_bodyposecontroller_available') {
  179. let area = e.ctrl_name.replace('bodyposecontroller_', '');
  180. if (e?.code == 0) {
  181. getBodyposeState(area);
  182. }
  183. }
  184. if (e?.cmd == 'get_bodyposecontroller_state') {
  185. let area = e.ctrl_name.replace('bodyposecontroller_', '');
  186. //state说明: 0:关闭,3:空闲,36:工作中
  187. if ([3, 36].includes(e.state)) {
  188. terminateBodypose(area);
  189. } else {
  190. openBodypose(area);
  191. }
  192. }
  193. if (e?.type == 'bodyposecontroller_result') {
  194. if (!currentGame.value) {
  195. return false;
  196. }
  197. //传给预备窗口
  198. actionConfirmRef.value.getInit(e);
  199. if (!start.value) {
  200. return false;
  201. }
  202. let area = e.ctrl_name.replace('bodyposecontroller_', '');
  203. //传给游戏窗口
  204. if (currentGame.value == 'bodyposecontroller') {
  205. if (currentGameArea.value.length == 2) {
  206. if (area == currentGameArea.value[0]) {
  207. humanBodyLeftRef.value.getInit(e);
  208. }
  209. if (area == currentGameArea.value[1]) {
  210. humanBodyRightRef.value.getInit(e);
  211. }
  212. } else {
  213. if (area == currentGameArea.value[0]) {
  214. humanBodyLeftRef.value.getInit(e);
  215. }
  216. }
  217. }
  218. if (currentGame.value == 'game_basketball') {
  219. if (area == currentGameArea.value[0]) {
  220. basketballLeftRef.value.getInit(e);
  221. }
  222. if (area == currentGameArea.value[1]) {
  223. basketballRightRef.value.getInit(e);
  224. }
  225. }
  226. if (currentGame.value == 'game_football') {
  227. if (area == currentGameArea.value[0]) {
  228. footballLeftRef.value.getInit(e);
  229. }
  230. if (area == currentGameArea.value[1]) {
  231. footballRightRef.value.getInit(e);
  232. }
  233. }
  234. if (currentGame.value == 'game_fruit') {
  235. fruitRef.value.getInit(e);
  236. }
  237. }
  238. if (e?.cmd == 'terminate_bodyposecontroller') {
  239. }
  240. if (e?.type == 'disconnect') {
  241. getExit();
  242. }
  243. });
  244. };
  245. //打开窗口
  246. const getJump = (data: any) => {
  247. console.log("222", data)
  248. if (data.exam_name == 'test') {
  249. router.push({ path: '/game/test' });
  250. } else {
  251. currentGame.value = data.exam_name;
  252. currentGameArea.value = data.area_test_id?.split(",") || [];
  253. actionConfirmRef.value.getOpen(currentGame.value, currentGameArea.value);
  254. currentGameArea.value.forEach((item: any) => {
  255. checkBodypose(item);
  256. })
  257. }
  258. };
  259. /**
  260. * 退出
  261. */
  262. const getExit = () => {
  263. // router.go(-1);
  264. router.push({ path: '/home' });
  265. };
  266. /**
  267. * 退出游戏
  268. */
  269. const getExitGame = (data: any) => {
  270. if (data.type == 1) {
  271. //浮窗退出
  272. console.log("currentGameArea.value", currentGameArea.value)
  273. currentGameArea.value.forEach((item: any) => {
  274. terminateBodypose(item);
  275. })
  276. currentGame.value = "";
  277. start.value = false;
  278. resumeGame.value = false;
  279. clearInterval(timer.value);
  280. timer.value = null;
  281. } else if (data.type == 2) {
  282. //游戏结束显示下一场
  283. let myIndex = areaStateList.value.findIndex((item: any) => {
  284. return item.area == data.area;
  285. })
  286. areaStateList.value[myIndex].gameover = true;
  287. } else {
  288. //游戏中退出
  289. proxy?.$modal.confirm("确定退出吗?").then(() => {
  290. currentGameArea.value.forEach((item: any) => {
  291. terminateBodypose(item);
  292. })
  293. currentGame.value = "";
  294. start.value = false;
  295. resumeGame.value = false;
  296. clearInterval(timer.value);
  297. timer.value = null;
  298. }).finally(() => {
  299. });
  300. }
  301. };
  302. /**
  303. * 开始游戏
  304. */
  305. const getStartGame = (data: any) => {
  306. areaStateList.value = data;
  307. start.value = false;
  308. resumeGame.value = false;
  309. setTimeout(() => {
  310. start.value = true;
  311. })
  312. };
  313. /**
  314. * 继续游戏
  315. */
  316. const getResumeGame = () => {
  317. areaStateList.value.forEach((item: any, index: number) => {
  318. areaStateList.value[index].gameover = false;
  319. })
  320. resumeGame.value = false;
  321. timerNum.value = 5;
  322. speckText(timerNum.value);
  323. timer.value = setInterval(() => {
  324. timerNum.value--;
  325. speckText(timerNum.value);
  326. if (timerNum.value == 0) {
  327. clearInterval(timer.value);
  328. timer.value = null;
  329. if (currentGame.value == 'game_basketball') {
  330. basketballLeftRef.value.getResumeGame();
  331. basketballRightRef.value.getResumeGame();
  332. }
  333. if (currentGame.value == 'game_football') {
  334. footballLeftRef.value.getResumeGame();
  335. footballRightRef.value.getResumeGame();
  336. }
  337. if (currentGame.value == 'game_fruit') {
  338. fruitRef.value.getResumeGame();
  339. }
  340. }
  341. }, 1000)
  342. };
  343. onBeforeMount(async () => {
  344. //初始化语音
  345. initSpeech();
  346. getInit();
  347. });
  348. onMounted(() => {
  349. });
  350. onBeforeUnmount(() => {
  351. closeWS();
  352. });
  353. </script>
  354. <style lang="scss" scoped>
  355. $topPadding: 5.19rem;
  356. $waiPadding: 6.51rem;
  357. .menu {
  358. width: calc(100% - ($waiPadding * 2));
  359. height: 72vh;
  360. padding-top: 10rem;
  361. margin: 0 auto;
  362. display: flex;
  363. .li {
  364. // width: calc((100% / 6) - 1rem + (1rem/6));
  365. // margin-right: 1rem;
  366. // margin-bottom: 1rem;
  367. width: 100%;
  368. height: calc((72vh / 2) - 20px);
  369. padding: 2.2vh 0;
  370. border-radius: 1.6rem;
  371. box-sizing: border-box;
  372. box-shadow: inset 0px 1px 0px 2px rgba(255, 255, 255, 0.9046), inset 0px 3px 6px 0px rgba(0, 0, 0, 0.0851);
  373. display: flex;
  374. justify-content: center;
  375. align-items: center;
  376. cursor: pointer;
  377. .pic {
  378. width: 11.36vw;
  379. height: 11.36vw;
  380. border-radius: 50%;
  381. background: radial-gradient(78% 78% at 53% 50%, #07121a 0%, #2a4256 49%, #5180a9 100%);
  382. box-shadow: 0px 0px 2px 2px #ffffff;
  383. margin-bottom: 2.5vh;
  384. overflow: hidden;
  385. display: flex;
  386. align-items: center;
  387. justify-content: center;
  388. flex-shrink: 0;
  389. img {
  390. max-width: 88%;
  391. max-height: 88%;
  392. transition: all 1s;
  393. }
  394. }
  395. .name {
  396. width: 100%;
  397. font-size: 2.1rem;
  398. color: #1a293a;
  399. text-align: center;
  400. }
  401. &:hover {
  402. img {
  403. transform: translateY(-0.5vw);
  404. }
  405. }
  406. }
  407. .swiper-slide {
  408. border-radius: 1.6rem;
  409. overflow: hidden;
  410. }
  411. }
  412. .menu1 {
  413. align-items: center;
  414. .swiper-slide {
  415. margin-bottom: 20px;
  416. .li {
  417. background: radial-gradient(96% 96% at 2% 32%, #ffffff 0%, #fcfdfd 54%, #e1e4e7 100%);
  418. }
  419. &:nth-child(2),
  420. &:nth-child(4),
  421. &:nth-child(6),
  422. &:nth-child(7),
  423. &:nth-child(9),
  424. &:nth-child(11) {
  425. .li {
  426. background: radial-gradient(167% 126% at 97% 6%, #35ffc6 0%, #00ffe8 100%);
  427. }
  428. }
  429. }
  430. }
  431. .menu2 {
  432. display: flex;
  433. .swiper-slide {
  434. &:nth-child(1),
  435. &:nth-child(4),
  436. &:nth-child(5),
  437. &:nth-child(8),
  438. &:nth-child(9),
  439. &:nth-child(12),
  440. &:nth-child(13),
  441. &:nth-child(16),
  442. &:nth-child(17),
  443. &:nth-child(20),
  444. &:nth-child(21),
  445. &:nth-child(24),
  446. &:nth-child(25),
  447. &:nth-child(28),
  448. &:nth-child(29),
  449. &:nth-child(32),
  450. &:nth-child(33),
  451. &:nth-child(36),
  452. &:nth-child(37),
  453. &:nth-child(40),
  454. &:nth-child(41),
  455. &:nth-child(44) {
  456. .li {
  457. background: radial-gradient(96% 96% at 2% 32%, #ffffff 0%, #fcfdfd 54%, #e1e4e7 100%);
  458. }
  459. }
  460. &:nth-child(2),
  461. &:nth-child(3),
  462. &:nth-child(6),
  463. &:nth-child(7),
  464. &:nth-child(10),
  465. &:nth-child(11),
  466. &:nth-child(14),
  467. &:nth-child(15),
  468. &:nth-child(18),
  469. &:nth-child(19),
  470. &:nth-child(22),
  471. &:nth-child(23),
  472. &:nth-child(26),
  473. &:nth-child(27),
  474. &:nth-child(30),
  475. &:nth-child(31),
  476. &:nth-child(34),
  477. &:nth-child(35),
  478. &:nth-child(38),
  479. &:nth-child(39),
  480. &:nth-child(42),
  481. &:nth-child(43) {
  482. .li {
  483. background: radial-gradient(167% 126% at 97% 6%, #35ffc6 0%, #00ffe8 100%);
  484. }
  485. }
  486. }
  487. }
  488. .gameWindow {
  489. width: 100vw;
  490. height: 100vh;
  491. position: absolute;
  492. z-index: 990;
  493. left: 0;
  494. top: 0;
  495. display: flex;
  496. .columns {
  497. display: flex;
  498. flex-wrap: wrap;
  499. width: 100%;
  500. .item {
  501. flex: 1;
  502. display: flex;
  503. box-sizing: border-box;
  504. width: 50%;
  505. }
  506. .left {
  507. border-right: 10px solid #07121a;
  508. }
  509. .right {
  510. border-left: 10px solid #07121a;
  511. }
  512. }
  513. }
  514. .close {
  515. z-index: 991;
  516. position: fixed;
  517. bottom: 35px;
  518. left: 50%;
  519. margin-left: -1.6rem;
  520. }
  521. .resumeGame {
  522. width: 10rem;
  523. height: 3.2rem;
  524. line-height: 3.2rem;
  525. font-size: 1.8rem;
  526. color: #1a293a;
  527. text-align: center;
  528. z-index: 991;
  529. position: fixed;
  530. bottom: calc(35px + 3.2rem + 25px);
  531. left: 50%;
  532. margin-left: -5rem;
  533. cursor: pointer;
  534. border-radius: 8px;
  535. background: radial-gradient(159% 126% at 5% 93%, #8effa9 0%, #07ffe7 100%);
  536. box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.1874), inset 0px 1px 0px 1px rgba(255, 255, 255, 0.3);
  537. &:hover {
  538. background: #8effa9;
  539. }
  540. }
  541. .time {
  542. position: absolute;
  543. left: 0;
  544. top: 0;
  545. width: 100%;
  546. height: 100%;
  547. background: rgba(0, 0, 0, 0.5);
  548. color: #ffffff;
  549. font-size: 7rem;
  550. display: flex;
  551. align-items: center;
  552. justify-content: center;
  553. z-index: 998;
  554. }
  555. ::v-deep(.menu) {
  556. .swiper-horizontal {
  557. width: 100%;
  558. }
  559. }
  560. @media screen and (max-width: 1450px) {
  561. .menu {
  562. .li {
  563. .name {
  564. font-size: 1.6rem;
  565. }
  566. .pic {
  567. width: 10vw;
  568. height: 10vw;
  569. }
  570. }
  571. }
  572. }
  573. </style>