浏览代码

日常开发

林旭祥 2 周之前
父节点
当前提交
a5fbb62bdb

+ 4 - 1
src/api/module/ranking.ts

@@ -18,10 +18,13 @@ export default {
   },
 
   sunshineRunRanking: (data: any) => {
+    let ip = localStorage.getItem('ip');
+    let url = `http://${ip}`
     return req({
+      baseURL:ip?url:import.meta.env.VITE_APP_BASE_API,
       url: '/v2/sunlight/rankdata/list',
       method: 'get',
-      data: data
+      data: data,
     });
   }
 };

+ 511 - 0
src/components/SquareGame/index.vue

@@ -0,0 +1,511 @@
+<template>
+    <div id="main_box">
+      <canvas ref="can1" id="myCanvas1" class="can"></canvas>
+      <canvas ref="can2" id="myCanvas2" class="can"></canvas>
+      <button id="login_btn" @click="loginGame" v-if="!beginGame && !gameOver">开始游戏</button>
+      <div id="score" v-if="beginGame && !gameOver">得分: {{ Math.floor(score) }}</div>
+      <div id="game_title" v-if="!beginGame && !gameOver">方&nbsp;块&nbsp;跳&nbsp;跃</div>
+      <div id="state" v-if="gameOver">游戏结束,得分:{{ Math.floor(score) }}</div>
+      <span
+        id="square_main"
+        v-if="mySquare && mySquare.isLive"
+        :style="{ left: mySquare.x + 'px', top: mySquare.y + 'px', background: mySquare.color }"
+      ></span>
+      <div id="colorChangeTime_div" v-if="showColorChange">{{ changeTimeArrays[changeColorIndex] }}</div>
+    </div>
+  </template>
+  
+  <script setup lang="ts">
+  import { ref, onMounted, onBeforeUnmount } from 'vue';
+  
+  interface BackgroundObj {
+    x: number[];
+    y: number[];
+    w: number[];
+    h: number[];
+    isLive: boolean[];
+    num: number;
+    init: () => void;
+    draw: () => void;
+    cloudBorn: () => void;
+  }
+  
+  interface SquareObj {
+    x: number[];
+    y: number[];
+    w: number[];
+    h: number[];
+    color: string[];
+    isLive: boolean[];
+    num: number;
+    init: () => void;
+    draw: () => void;
+    squareBorn: () => void;
+  }
+  
+  interface MySquareObj {
+    x: number;
+    y: number;
+    isLive: boolean;
+    l: number;
+    color: string;
+    toDownSpeed: number;
+    toVSpeed: number;
+    init: () => void;
+    jump: () => void;
+    toDown: () => void;
+    draw: () => void;
+  }
+  
+  interface BeginAnimObj {
+    x: number;
+    y: number;
+    isLive: boolean;
+    l: number;
+    color: string;
+    toDownSpeed: number;
+    toVSpeed: number;
+    init: () => void;
+    jump: () => void;
+    toDown: () => void;
+    draw: () => void;
+  }
+  
+  const can1 = ref<HTMLCanvasElement | null>(null);
+  const can2 = ref<HTMLCanvasElement | null>(null);
+  const ctx1 = ref<CanvasRenderingContext2D | null>(null);
+  const ctx2 = ref<CanvasRenderingContext2D | null>(null);
+  const canWidth = ref(820);
+  const canHeight = ref(640);
+  const maxHeight = ref(window.innerHeight);
+  const maxWidth = ref(window.innerWidth);
+  
+  const beginGame = ref(false);
+  const gameOver = ref(false);
+  const backrgound = ref<BackgroundObj | null>(null);
+  const squares = ref<SquareObj | null>(null);
+  const mySquare = ref<MySquareObj | null>(null);
+  const beginAnim = ref<BeginAnimObj | null>(null);
+  const lastTime = ref(Date.now());
+  const changeColorTime = ref(Date.now());
+  const changeTimeArrays = ["10", "9", "8", "7", "6", "5", "4", "3", "2", "1", "颜色变化!"];
+  const changeColorIndex = ref(0);
+  const totalSpeed = ref(3);
+  const isStep = ref(false);
+  const colorChangeTimeW = ref(0);
+  const score = ref(0);
+  const showColorChange = ref(false);
+  
+  const squareColor = ["#FF6688", "#00FF00", "#3399FF", "#FFDAB9", "blueviolet"];
+  
+  const init = () => {
+    score.value = 0;
+    beginGame.value = false;
+    gameOver.value = false;
+    isStep.value = false;
+    lastTime.value = Date.now();
+  
+    canWidth.value = maxWidth.value !== canWidth.value ? maxWidth.value : canWidth.value;
+    canHeight.value = maxHeight.value !== canHeight.value ? maxHeight.value : canHeight.value;
+  
+    can1.value!.width = canWidth.value;
+    can2.value!.width = canWidth.value;
+    can1.value!.height = canHeight.value;
+    can2.value!.height = canHeight.value;
+  
+    colorChangeTimeW.value = (canHeight.value > canWidth.value ? canWidth.value : canHeight.value) * 0.5;
+  
+    backrgound.value = {
+      x: [],
+      y: [],
+      w: [],
+      h: [],
+      isLive: [],
+      num: 3,
+      init() {
+        for (let i = 0; i < this.num; i++) {
+          if (i === 0) {
+            this.x[i] = canWidth.value * 0.1 * Math.random();
+          } else {
+            this.x[i] = this.x[i - 1] + this.w[i - 1] + canWidth.value * 0.2 * i * Math.random();
+          }
+          this.w[i] = canWidth.value * 0.08 + canHeight.value * Math.random() * 0.3;
+          this.h[i] = canHeight.value * 0.08 + canWidth.value * Math.random() * 0.3;
+          this.y[i] = canHeight.value * 0.2 * Math.random();
+          this.isLive[i] = true;
+        }
+      },
+      draw() {
+        ctx1.value!.clearRect(0, 0, canWidth.value, canHeight.value);
+        ctx1.value!.save();
+        for (let i = 0; i < this.num; i++) {
+          if (this.isLive[i]) {
+            this.x[i] -= 0.1 * totalSpeed.value;
+            ctx1.value!.fillStyle = "#ffffff";
+            // Draw cloud logic (simplified)
+            // ... (original cloud drawing logic here)
+          }
+          if (this.x[i] + this.w[i] < 0) {
+            this.isLive[i] = false;
+          }
+        }
+        ctx1.value!.restore();
+        this.cloudBorn();
+      },
+      cloudBorn() {
+        // Cloud reborn logic (simplified)
+        // ... (original cloud reborn logic here)
+      }
+    };
+  
+    squares.value = {
+      x: [],
+      y: [],
+      w: [],
+      h: [],
+      color: [],
+      isLive: [],
+      num: 12,
+      init() {
+        for (let i = 0; i < this.num; i++) {
+          this.color[i] = squareColor[Math.floor(Math.random() * squareColor.length)];
+          if (i === 0) {
+            this.x[i] = 0;
+          } else {
+            this.x[i] = this.x[i - 1] + this.w[i - 1] + 1;
+          }
+          this.h[i] = canHeight.value * 0.3 + canHeight.value * 0.25 * Math.random();
+          this.w[i] = canWidth.value * 0.15 + canWidth.value * 0.06 * Math.random();
+          this.y[i] = canHeight.value - this.h[i];
+          this.isLive[i] = true;
+        }
+      },
+      draw() {
+        for (let i = 0; i < this.num; i++) {
+          if (this.isLive[i]) {
+            ctx2.value!.fillStyle = this.color[i];
+            ctx2.value!.fillRect(this.x[i], this.y[i], this.w[i], this.h[i]);
+            this.x[i] -= totalSpeed.value;
+          }
+          if (this.x[i] + this.w[i] < 0) {
+            this.isLive[i] = false;
+          }
+        }
+        this.squareBorn();
+      },
+      squareBorn() {
+        // Square reborn logic (simplified)
+        // ... (original square reborn logic here)
+      }
+    };
+  
+    mySquare.value = {
+      x: 0,
+      y: 0,
+      isLive: true,
+      l: 40,
+      color: squareColor[0],
+      toDownSpeed: 0,
+      toVSpeed: 0,
+      init() {
+        this.x = 0;
+        this.y = 0;
+        this.isLive = true;
+        this.l = 40;
+        this.toDownSpeed = 0;
+        this.toVSpeed = 0;
+        this.color = squareColor[0];
+      },
+      jump() {
+        if (this.isLive) {
+          this.toDownSpeed = -15;
+          this.toVSpeed = 2;
+        }
+      },
+      toDown() {
+        if (this.isLive) {
+          this.toDownSpeed += 9.8 * 1 * 0.06;
+          this.y += this.toDownSpeed;
+          this.x += this.toVSpeed;
+          if (this.y + this.l > canHeight.value) {
+            this.isLive = false;
+          }
+        }
+      },
+      draw() {
+        if (this.isLive) {
+          const now = Date.now();
+          if (now - changeColorTime.value > 1000) {
+            changeColorIndex.value = ++changeColorIndex.value % changeTimeArrays.length;
+            const strColor = squareColor[Math.floor(Math.random() * squareColor.length)];
+            document.getElementById("colorChangeTime_div")!.style.color = strColor;
+            document.getElementById("colorChangeTime_div")!.innerHTML = changeTimeArrays[changeColorIndex.value];
+            if (changeColorIndex.value === 10) {
+              document.getElementById("colorChangeTime_div")!.style.fontSize = colorChangeTimeW.value * 0.15 + "px";
+              this.color = strColor;
+            } else {
+              document.getElementById("colorChangeTime_div")!.style.fontSize = colorChangeTimeW.value * 0.3 + "px";
+            }
+            changeColorTime.value = now;
+          }
+          ctx2.value!.fillStyle = this.color;
+          ctx2.value!.fillRect(this.x, this.y, this.l, this.l);
+          ctx2.value!.strokeStyle = "#ffffff";
+          ctx2.value!.strokeRect(this.x, this.y, this.l, this.l);
+          if (this.x < -100) {
+            this.isLive = false;
+          }
+        } else {
+          restartInit();
+        }
+        this.toDown();
+      }
+    };
+  
+    beginAnim.value = {
+      x: 0,
+      y: 0,
+      isLive: true,
+      l: 40,
+      color: squareColor[0],
+      toDownSpeed: 0,
+      toVSpeed: canWidth.value * 0.021,
+      init() {
+        this.isLive = true;
+        this.x = 0;
+        this.y = 0;
+        this.l = 40;
+        this.toDownSpeed = 0;
+        this.toVSpeed = canWidth.value * 0.021;
+        this.color = squareColor[0];
+      },
+      jump() {
+        if (this.isLive) {
+          this.toDownSpeed = -this.toDownSpeed;
+          this.toVSpeed = canWidth.value * 0.021 * 0.5;
+          this.x += this.toVSpeed;
+          if (this.toVSpeed + 2 < 3) {
+            this.toVSpeed += 1;
+          }
+        }
+      },
+      toDown() {
+        if (this.isLive) {
+          this.toDownSpeed += 9.8 * 1 * 0.06;
+          this.y += this.toDownSpeed;
+          this.x += this.toVSpeed;
+          if (this.y > canHeight.value) {
+            this.isLive = false;
+          }
+        }
+      },
+      draw() {
+        if (this.isLive) {
+          const squareMain = document.getElementById("square_main")!;
+          squareMain.style.left = this.x + "px";
+          squareMain.style.top = this.y + "px";
+          if (
+            this.y + this.l > canHeight.value * 0.35 &&
+            this.x + this.l * 0.5 < canWidth.value * 0.5 + 120 &&
+            this.x + this.l * 0.5 > canWidth.value * 0.5 - 120
+          ) {
+            this.jump();
+            document.getElementById("game_title")!.innerHTML = "方&nbsp;块&nbsp;<i style='font-size: 46px;color:#FF6688;'>跳&nbsp;</i>跃";
+          }
+          this.toDown();
+        } else {
+          const squareMain = document.getElementById("square_main")!;
+          squareMain.style.display = "none";
+        }
+      }
+    };
+  
+    backrgound.value.init();
+    squares.value.init();
+    mySquare.value.init();
+    beginAnim.value.init();
+  };
+  
+  const restart = () => {
+    backrgound.value!.init();
+    squares.value!.init();
+    mySquare.value!.init();
+    lastTime.value = Date.now();
+    document.getElementById("state")!.style.display = "none";
+    document.getElementById("game_title")!.style.display = "none";
+    document.getElementById("login_btn")!.style.display = "none";
+    document.getElementById("score")!.style.display = "block";
+    score.value = 0;
+    totalSpeed.value = 3;
+  };
+  
+  const gameloop = () => {
+    backrgound.value!.draw();
+    ctx2.value!.clearRect(0, 0, canWidth.value, canHeight.value);
+    ctx2.value!.save();
+    if (beginAnim.value!.isLive && Date.now() - lastTime.value > 1000) {
+      beginAnim.value!.draw();
+    }
+  
+    if (beginGame.value && !gameOver.value) {
+      document.getElementById("score")!.innerHTML = "得分:" + Math.floor(score.value);
+      addSpeed();
+      squares.value!.draw();
+      mySquare.value!.draw();
+      // squaresToMy(); // Collision detection logic needs to be adapted for Vue
+    }
+    ctx2.value!.restore();
+  };
+  
+  const loginGame = () => {
+    changeColorTime.value = Date.now();
+    document.getElementById("colorChangeTime_div")!.style.display = "block";
+    document.getElementById("colorChangeTime_div")!.style.color = squareColor[Math.floor(Math.random() * squareColor.length)];
+    document.getElementById("colorChangeTime_div")!.innerHTML = changeTimeArrays[changeColorIndex.value];
+    if (!gameOver.value && !beginGame.value) {
+      document.getElementById("score")!.style.display = "block";
+      document.getElementById("game_title")!.style.display = "none";
+      document.getElementById("square_main")!.style.display = "none";
+      beginGame.value = true;
+      document.getElementById("login_btn")!.style.display = "none";
+    } else if (gameOver.value && !beginGame.value) {
+      restart();
+      gameOver.value = false;
+      beginGame.value = true;
+    }
+    beginAnim.value!.isLive = false;
+    document.getElementById("square_main")!.style.display = "none";
+  };
+  
+  const addSpeed = () => {
+    totalSpeed.value += 0.04 * 0.05;
+    score.value += 0.04;
+  };
+  
+  const restartInit = () => {
+    changeColorIndex.value = 0;
+    document.getElementById("colorChangeTime_div")!.style.display = "none";
+    beginAnim.value!.init();
+    beginGame.value = false;
+    gameOver.value = true;
+    lastTime.value = Date.now();
+    document.getElementById("game_title")!.innerHTML = "方&nbsp;块&nbsp;跳&nbsp;跃";
+    document.getElementById("score")!.style.display = "none";
+    document.getElementById("game_title")!.style.display = "block";
+    document.getElementById("login_btn")!.style.display = "block";
+    document.getElementById("login_btn")!.innerHTML = "再来一次";
+    document.getElementById("state")!.style.display = "block";
+    document.getElementById("state")!.innerHTML = "游戏结束,得分:" + Math.floor(score.value);
+  };
+  
+  onMounted(() => {
+    ctx1.value = can1.value!.getContext("2d");
+    ctx2.value = can2.value!.getContext("2d");
+    init();
+    const loop = setInterval(gameloop, 20);
+    onBeforeUnmount(() => clearInterval(loop));
+  });
+  </script>
+  
+  <style scoped>
+  /* CSS styles from the original HTML */
+  #main_box {
+    margin: 0px auto;
+    width: 420px;
+    height: 580px;
+    background: skyblue;
+    position: relative;
+    overflow: hidden;
+    z-index: 9999;
+  }
+  .can {
+    position: absolute;
+    top: 0px;
+    left: 0px;
+  }
+  #score {
+    width: auto;
+    height: 40px;
+    font-size: 22px;
+    color: #7fffd4;
+    position: absolute;
+    top: 10px;
+    left: 10px;
+  }
+  #game_title {
+    display: none;
+    position: absolute;
+    width: 240px;
+    height: 80px;
+    line-height: 80px;
+    text-align: center;
+    top: 35%;
+    left: 50%;
+    margin-left: -120px;
+    font-size: 42px;
+    font-family: "century gothic";
+    font-weight: bolder;
+    text-shadow: 4px 3px 2px black;
+    color: #fff;
+  }
+  #login_btn {
+    display: none;
+    position: absolute;
+    bottom: 100px;
+    left: 50%;
+    width: 100px;
+    height: 40px;
+    margin-left: -50px;
+    line-height: 40px;
+    text-align: center;
+    border-radius: 12px;
+    border: 2px solid #ffffff;
+    font-size: 18px;
+    color: #000000;
+    font-weight: normal;
+    background: peachpuff;
+  }
+  #login_btn:hover {
+    color: #fff;
+  }
+  #login_btn:active {
+    background: #ffffff;
+    color: #000000;
+  }
+  #square_main {
+    width: 40px;
+    height: 40px;
+    position: absolute;
+    border: 2px solid #ffffff;
+    display: none;
+    background: #ff6688;
+  }
+  #state {
+    width: 280px;
+    height: 60px;
+    font-size: 18px;
+    font-weight: bolder;
+    text-shadow: #fdf5e6;
+    color: blueviolet;
+    font-family: "arial narrow";
+    position: absolute;
+    top: 45%;
+    left: 50%;
+    line-height: 60px;
+    text-align: center;
+    margin-left: -140px;
+    display: none;
+  }
+  #colorChangeTime_div {
+    width: 200px;
+    height: 50px;
+    line-height: 50px;
+    position: absolute;
+    text-align: center;
+    top: 3%;
+    left: 50%;
+    display: none;
+    color: #ffffff;
+    font-size: 40px;
+  }
+  </style>

+ 1 - 0
src/router/index.ts

@@ -20,6 +20,7 @@ const router = createRouter({
         { path: '/train/run', component: () => import('@/views/train/run.vue') },
         { path: '/train/multiple', component: () => import('@/views/train/multiple.vue') },
         { path: '/train/device', component: () => import('@/views/train/device.vue') },
+        { path: '/train/game', component: () => import('@/views/train/game.vue') },
         { path: '/test', component: () => import('@/views/test/index.vue') },
         { path: '/set', component: () => import('@/views/set/index.vue') },
         { path: '/set/config', component: () => import('@/views/set/config.vue') },

+ 2 - 0
src/types/components.d.ts

@@ -33,6 +33,7 @@ declare module 'vue' {
     ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
     ElUpload: typeof import('element-plus/es')['ElUpload']
     FaceWindow: typeof import('./../components/FaceWindow/index.vue')['default']
+    Game: typeof import('./../components/Game/index.vue')['default']
     Header: typeof import('./../components/Header/index.vue')['default']
     JumpRopeGame: typeof import('./../components/JumpRopeGame/index.vue')['default']
     MultipleItem: typeof import('./../components/MultipleItem/index.vue')['default']
@@ -43,6 +44,7 @@ declare module 'vue' {
     ReportWindow: typeof import('./../components/ReportWindow/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
+    SquareGame: typeof import('./../components/SquareGame/index.vue')['default']
     WorkstationWindow: typeof import('./../components/WorkstationWindow/index.vue')['default']
   }
   export interface ComponentCustomProperties {

+ 2 - 1
src/utils/sunshineRunWs.ts

@@ -13,7 +13,8 @@ import useAppStore from '@/store/modules/app';
 // });
 
 export function useSunshineRunSocket() {
-  const address: any = import.meta.env.VITE_APP_BASE_API;
+  let ip = localStorage.getItem('ip');
+  const address: any = ip?ip:import.meta.env.VITE_APP_BASE_API;
   const token: any = localStorage.getItem('token');
   const deviceid: any = localStorage.getItem('deviceid');
   const myToken: any = 'JWT ' + token;

+ 10 - 0
src/views/home/index.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="home">
+    <!-- <SquareGame></SquareGame> -->
     <Header :showClose="false" :showTool="true" logoClass="logo2"></Header>
     <div class="menu">
       <div class="left">
@@ -17,6 +18,7 @@
           <div class="li" @click="getJump('/gesture', '竞赛')"><img src="@/assets/images/home/competition.png" /></div>
           <div class="li" @click="getJump('/course', '课程')"><img src="@/assets/images/home/course.png" /></div>
           <div class="li" @click="getJump('/set', '设置')"><img src="@/assets/images/home/set.png" /></div>
+          <!-- <div class="li" @click="getJump('/train/game', '游戏')"><img src="@/assets/images/analysis/skiprope.png" /></div> -->
         </div>
       </div>
     </div>
@@ -89,6 +91,14 @@ const getJump = (url: string, name: string) => {
         }
       })
       .finally(() => {});
+  } else if (url == '/train/game') {
+    router.push({ path: url,query: {
+      project:'jumprope',
+      time:60,
+      area:1,
+      gesture:'true',
+      classes:'1'
+    } });
   } else {
     router.push({ path: url });
   }

+ 1 - 0
src/views/login/index.vue

@@ -129,6 +129,7 @@ onMounted(() => {
   if (username) {
     loginForm.value.username = username;
   }
+  localStorage.removeItem('ip');
   // if (password) {
   //   let myPassword = CryptoJS.AES.decrypt(password, 'trops').toString(CryptoJS.enc.Utf8);
   //   loginForm.value.password = myPassword;

+ 3 - 2
src/views/login/mobile.vue

@@ -58,7 +58,8 @@ const data = reactive<DataType>({
   handcontroller: '',
   loginForm: {
     username: '',
-    password: '', deviceid: '',
+    password: '',
+    deviceid: '',
   },
   loading: false,
 });
@@ -139,7 +140,7 @@ const getCmdtest = async (data: any) => {
 
 onMounted(() => {
   let sid = route.query.sid;
-  let deviceid = route.query.deviceid;
+  let deviceid = route.query.deviceid as any;
   if (sid == undefined && !sid) {
     proxy?.$modal.msgError("缺少SID,请重新扫码!");
     return false;

+ 1 - 0
src/views/login/qrcode.vue

@@ -201,6 +201,7 @@ onMounted(() => {
   //   let myPassword = CryptoJS.AES.decrypt(password, 'trops').toString(CryptoJS.enc.Utf8);
   //   loginForm.value.password = myPassword;
   // }
+  localStorage.removeItem('ip');
   if (token && deviceid) {
     //跳转
     router.push({ path: '/gesture' });

+ 14 - 2
src/views/login/sunshineRun.vue

@@ -13,6 +13,10 @@
               <input class="login-input login-input-password" type="password" autocomplete="off" placeholder="请输入密码"
                 v-model.trim="loginForm.password" />
             </div>
+            <div class="login-item">
+              <input class="login-input login-input-username" type="text" placeholder="请输入IP"
+                v-model.trim="loginForm.ip" />
+            </div>
             <div @click="getLogin" class="login-btn">
               <el-icon class="is-loading" v-if="loading">
                 <Loading />
@@ -36,6 +40,7 @@ interface DataType {
   loginForm: {
     username: string,
     password: string,
+    ip: string,
   },
   loading: boolean,
   key: number,
@@ -44,7 +49,8 @@ interface DataType {
 const data = reactive<DataType>({
   loginForm: {
     username: '',
-    password: ''
+    password: '',
+    ip: ''
   },
   loading: false,
   key: 0,
@@ -56,6 +62,7 @@ const { loginForm, loading, key } = toRefs(data);
 const getLogin = () => {
   let username = loginForm.value.username;
   let password = loginForm.value.password;
+  let ip = loginForm.value.ip;
   if (!username || !password) {
     return false;
   }
@@ -69,10 +76,11 @@ const getLogin = () => {
     if (res.access_token) {
       //保存token
       let token = res.access_token;
-      let passwordStr = CryptoJS.AES.encrypt(password, 'trops').toString();
+      let passwordStr = CryptoJS.AES.encrypt(password, 'trops').toString(); 
       localStorage.setItem("token", token);
       localStorage.setItem('username', username);
       localStorage.setItem('password', passwordStr);
+      localStorage.setItem('ip', ip)
       router.push({ path: '/sunshineRun' });
       getUserInfo();
     }
@@ -108,6 +116,7 @@ onMounted(() => {
   // }
   let username = localStorage.getItem('username');
   let password = localStorage.getItem('password');
+  let ip = localStorage.getItem('ip');
   if (username) {
     loginForm.value.username = username;
   }
@@ -115,6 +124,9 @@ onMounted(() => {
     let myPassword = CryptoJS.AES.decrypt(password, 'trops').toString(CryptoJS.enc.Utf8);
     loginForm.value.password = myPassword;
   }
+  if (ip) {
+    loginForm.value.ip = ip;
+  }
 })
 </script>
 

+ 1571 - 0
src/views/train/game.vue

@@ -0,0 +1,1571 @@
+<template>
+  <div class="test">
+    <Header @confirmExit="confirmExit" @setMusic="setMusic"></Header>
+    <div class="main">
+      <div class="main-left">
+        <div class="main-left-top">
+          <div class="top-left" @click="getChooseStudent">
+            <div class="top-left-center">
+              <div class="pic" :class="{ 'pic2': faceCheckStu.student_id }" v-if="faceCheckStu.student_id">
+                <img :src="faceCheckStu.face_pic || faceCheckStu.logo_url" />
+              </div>
+              <div class="pic" v-else>
+                <img src="@/assets/images/test/profilePicture.png" />
+              </div>
+              <div class="name" :class="{ 'name2': faceCheckStu.student_id }">
+                {{ faceCheckStu.student_id ? faceCheckStu.name : "虚位以待" }}
+              </div>
+            </div>
+          </div>
+          <div class="top-right">
+            <Transition :enter-active-class="proxy?.animate.dialog.enter" :leave-active-class="proxy?.animate.dialog.leave">
+              <div class="time" v-show="needStart && [42].includes(examState) && !['basketballv1', 'footballv1'].includes(parameter.project)">
+                {{
+                  time.countdownNum
+                }}
+              </div>
+            </Transition>
+            <div class="tips" v-if="examState == 41">
+              <img v-if="parameter.gesture" src="@/assets/images/test/ready1.png" />
+              <img v-if="!parameter.gesture" src="@/assets/images/test/ready2.png" />
+            </div>
+            <div
+              class="complete"
+              :class="{ 'complete2': needStart && [42].includes(examState) }"
+              v-if="faceCheckStu.student_id && time.ready <= 0 && examState != 43 && examState != 41"
+            >
+              <div class="scoreBox">
+                <div class="score" v-if="currentResultObj?.count && currentResultObj.count>=0">{{ currentResultObj.count }}</div>
+                <div class="prompt" v-if="currentResultObj?.count && currentResultObj.count==0 && examState == 42">请开始测试</div>
+                <div class="unit" v-if="!['basketballv1', 'footballv1'].includes(parameter.project) && currentResultObj.count && !needStart">
+                  {{ unit }}
+                </div>
+              </div>
+              <div class="fractionViolation">
+                <div class="fraction">
+                  <div class="lable">得分:</div>
+                  <div class="value">{{ currentResultObj.score || "" }}</div>
+                </div>
+                <div class="violation">
+                  <div class="lable">
+                    {{ ['jumprope', 'jumpingjack', 'highknees'].includes(parameter.project) ? '中断' : '犯规'
+                    }}
+                  </div>
+                  <div class="value">{{ currentResultObj.back_num || 0 }}</div>
+                </div>
+              </div>
+            </div>
+            <div class="foulBox" v-if="examState == 42 && backReason.length">
+              <Transition :enter-active-class="proxy?.animate.mask.enter" :leave-active-class="proxy?.animate.mask.leave">
+                <div class="foul" v-show="backReasonStr ? true : false">
+                  <div class="lable">!</div>
+                  <div class="value">{{ backReasonStr }}</div>
+                </div>
+              </Transition>
+            </div>
+
+            <div v-show="examState == 43 && time.ready">
+              <div class="readyBox">
+                <div class="value" :class="{ 'transparent': time.ready > 5 }">{{ time.ready }}</div>
+                <div class="lable">倒计时</div>
+              </div>
+            </div>
+
+            <div v-show="examState == 43 && faceCheckStu.student_id && !time.ready && readyState">
+              <div class="readyBoxBefore">
+                <div class="item" v-if="parameter.handcontroller">
+                  <div><img src="@/assets/images/test/jushou.png" /></div>
+                  <div class="lable">
+                    <div>请举左手开始</div>
+                  </div>
+                </div>
+                <div class="item" v-else>
+                  <div><img src="@/assets/images/test/bujushou.png" /></div>
+                  <div class="lable">
+                    <div>请点击开始</div>
+                  </div>
+                </div>
+              </div>
+            </div>
+            <div v-show="examState == 43 && !faceCheckStu.student_id">
+              <div class="btn btn2" @click="getChooseStudent">点击重新识别</div>
+            </div>
+            <div class="btn" @click="getReady" v-if="needStart && examState == 43 && faceCheckStu.student_id && !time.ready && readyState">开 始</div>
+            <!-- <div v-if="needStart"> -->
+            <!-- <div class="btn" @click="getOpenOneTestAndStartFace" v-if="examState < 41">开始识别</div> -->
+            <!-- <div class="btn" @click="getStopFace" v-if="examState == 41 && !parameter.gesture">停止人脸识别</div> -->
+            <!-- <div class="btn" @click="getStartOneTest" v-if="examState == 43">开始测试</div> -->
+            <!-- <div @click="getRetestFace" v-if="examState == 43 || examState == 42">重新识别</div> -->
+            <!-- </div> -->
+            <!-- <div>当前状态:({{ examState == 3 ? "初始化完成" : examState == 40 ? "创建测试" : examState == 41 ? "正在人脸识别":examState == 43 ? "停止人脸识别" : examState == 42 ? "正在测试" : "请初始化" }})</div> -->
+            <!-- <div @click="getAgain" v-if="examState == 42 || showTestAgain">再测一次</div> -->
+          </div>
+          <i></i>
+        </div>
+        <div class="main-left-bottom">
+          <div class="bottom-left">
+            <div class="tips"><img src="/src/assets/images/test/tips.png" /></div>
+            <div class="pic"><img :src="'static/images/tips/' + parameter.project + '.png'" /></div>
+          </div>
+          <div class="bottom-right" v-html="dic.projectNote[parameter.project]"></div>
+        </div>
+      </div>
+      <div class="main-right">
+        <ReportList ref="reportListRef" :parameter="parameter" :showQRCode="true" />
+      </div>
+    </div>
+    <FaceWindow ref="faceWindowRef" :faceCheckStu="faceCheckStu" :gesture="parameter.gesture" />
+    <ChooseStudent ref="chooseStudentRef" @returnData="returnStudent" />
+    <JumpRopeGame ref="gameContainer" v-if="['test'].includes(parameter.project) && (!readyState || [42].includes(examState))" />
+    <div class="close" @click="confirmExit"></div>
+  </div>
+</template>
+
+<script setup name="TrainTest" lang="ts">
+import useAppStore from '@/store/modules/app';
+import { useWs } from '@/utils/trainWs';
+// import { initWs, examEnds, openOneTest, startFace, stopFace, faceConfirmOnly, startOneTest, finishOneTest, closeOneTest, suspendFaceRecognitionChannels, resumeFaceRecognitionChannels } from '@/utils/ws'
+import { initSpeech, speckText, playMusic, controlMusic, speckCancel, chineseNumber } from '@/utils/speech';
+import { useWebSocket } from '@/utils/handWs';
+import dayjs from 'dayjs';
+import dataDictionary from '@/utils/dataDictionary';
+const { handWs, startDevice, startHand, stateHand } = useWebSocket();
+const {
+  initWs,
+  examEnds,
+  openOneTest,
+  startFace,
+  stopFace,
+  faceConfirmOnly,
+  startOneTest,
+  finishOneTest,
+  closeOneTest,
+  suspendFaceRecognitionChannels,
+  resumeFaceRecognitionChannels
+} = useWs();
+const { proxy } = getCurrentInstance() as any;
+const router = useRouter();
+const route = useRoute();
+const faceWindowRef = ref();
+const chooseStudentRef = ref();
+const reportListRef = ref();
+const gameContainer = ref();
+const myInfo: any = localStorage.getItem('userInfo');
+const dic: any = dataDictionary;
+const data = reactive<any>({
+  timerManager: {}, //计时器管理
+  parameter: {}, //参数
+  time: {
+    testTime: 60, //时长
+    countdownNum: 0, //计时
+    ready: 0, //预备
+    exit: 0 //退出倒计时
+  },
+  userInfo: {}, //用户信息
+  examState: 0, //当前状态
+  resultId: null, //测试ID
+  currentResultObj: {}, //成绩
+  faceCheckStu: {}, //人脸信息
+  unit: '', //单位
+  backReason: [], //犯规项
+  backReasonStr: '', //犯规提示
+  needStart: false, //是否需要按钮
+  showTestAgain: false, //再测一次按钮
+  readyState: true, //倒计时按钮状态
+  exitStatus: 0, //退出响应次数
+  sid: null, //WS的id
+  deviceInfo: {}, //设备信息
+  musicList: [], //音乐列表
+});
+const {
+  timerManager,
+  parameter,
+  time,
+  userInfo,
+  examState,
+  resultId,
+  faceCheckStu,
+  currentResultObj,
+  unit,
+  backReason,
+  backReasonStr,
+  needStart,
+  showTestAgain,
+  readyState,
+  exitStatus,
+  sid,
+  deviceInfo,
+  musicList
+} = toRefs(data);
+
+/**
+ * 接收消息
+ */
+const getMessage = (e: any) => {
+  //console.log("WS响应:", e)
+  //获取sid
+  if (e.cmd === 'mySid') {
+    console.log('e.data.sid', e.data.sid);
+    sid.value = e.data.sid;
+  }
+  //实时状态
+  if (e.cmd === 'exam_status') {
+    examState.value = e.data;
+  }
+  //工作站状态
+  if (e.cmd === 'init_result') {
+  }
+  //测试违规
+  if (e.cmd === 'warning_result') {
+    console.log('eeeeeeeeeee', e);
+    if ((e.status + '')[0] === '2') {
+      proxy?.$modal.msgError(e.data.message);
+      speckText(e.data.message);
+    }
+  }
+  //后端播报语音
+  if (e.cmd === 'return_audio_msg') {
+    if (e.data.message) {
+      proxy?.$modal.msgError(e.data.message);
+      speckText(e.data.message);
+    }
+  }
+  //错误提示
+  if (e.cmd === 'info_result') {
+    proxy?.$modal.msgError(e.data.message);
+  }
+  //错误提示
+  if (e.cmd === 'error_result') {
+    proxy?.$modal.msgError(e.data.message);
+  }
+  //测试中违规提示
+  if (e.cmd === 'warning_notify') {
+    let message = e.data?.message;
+    if (message) {
+      proxy?.$modal.msgError(message);
+      speckText(message);
+    }
+    if (message == '工作站已断开!') {
+      getExit();
+    }
+  }
+  //断线状态
+  if (e.cmd === 'disconnect_request') {
+    let message = e.data.message;
+    if (message) {
+      proxy?.$modal.msgError(`${message}`);
+      speckText(message);
+    }
+    getExit();
+  }
+  //状态变更
+  if (e.cmd === 'set_exam_state') {
+    examState.value = e.data;
+    if (e.data === 3) {
+      initProject();
+    }
+    if (e.data === 40) {
+      cleanData();
+    }
+    if (e.data == 41) {
+      getFaceWindow(true);
+    }
+    if (e.data == 43) {
+    }
+    if (e.data == 42) {
+      getClearTimer('readyTimer');
+      time.value.ready = 0;
+    }
+  }
+  //新建测试后返回信息,获取result_id
+  if (e.cmd === 'open_one_test_ack') {
+    resultId.value = e.data.result_id;
+  }
+  //人脸识别状态
+  if (e.cmd === 'face_check_result') {
+    let myData = e.data[0] || e.data;
+    returnStudent(myData);
+  }
+  //测试结束结果
+  if (e.cmd === 'oneresult') {
+    if (e.data.length) {
+      let data = e.data[0];
+      getAchievement(data);
+    }
+  }
+  //结果生成完成(视频图片)
+  if (e.cmd === 'static_urls_finished') {
+  }
+  //选择学生或测试结束后返回的数据
+  if (e.cmd === 'result_info') {
+  }
+};
+
+/**
+ * 开始识别
+ */
+const getOpenOneTestAndStartFace = async () => {
+  if (examState.value > 3) {
+    await closeOneTest();
+  }
+  await openOneTest();
+  await startFace();
+};
+
+/**
+ * 停止人脸识别
+ */
+const getStopFace = async () => {
+  // 旧版识别成功直接43了这里先屏蔽
+  // if (examState.value != 41) {
+  //   return false;
+  // }
+  getClearTimer('face');
+  if (needStart.value) {
+    let txt = parameter.value.handcontroller ? ',请举左手开始测试' : ',请准备';
+    speckText(faceCheckStu.value.name + txt);
+  }
+  if (examState.value == 41) {
+    await stopFace();
+  }
+  if (faceCheckStu.value.student_id) {
+    getFaceConfirmOnly();
+  }
+};
+
+/**
+ * 确定人脸信息
+ */
+const getFaceConfirmOnly = (data?: any) => {
+  if (data) {
+    faceCheckStu.value = data;
+  }
+  faceConfirmOnly(
+    {
+      result_id: resultId.value,
+      student_id: faceCheckStu.value.student_id,
+      gender: faceCheckStu.value.gender
+    },
+    () => {
+      faceWindowRef.value?.close();
+      //不需要按钮的自动进入下一步
+      if (needStart.value == false) {
+        getStartOneTest();
+      }
+    }
+  );
+};
+
+/**
+ * 重新识别
+ */
+const getRetestFace = () => {
+  proxy?.$modal
+    .confirm('确定重新识别吗?')
+    .then(() => {
+      cleanData();
+      if (needStart.value == false) {
+        //自动流程项目重新识别直接返回3
+        closeOneTest();
+      } else {
+        //手动流程项目重新识别43返回41,42返回3
+        if (examState.value == 43) {
+          startFace();
+        } else if (examState.value == 42) {
+          finishOneTest();
+        } else {
+          closeOneTest();
+        }
+      }
+    })
+    .finally(() => {});
+};
+
+/**
+ * 开始测试
+ */
+const getStartOneTest = () => {
+  if (examState.value != 43 || !faceCheckStu.value.student_id) {
+    return false;
+  }
+  if (!faceCheckStu.value.student_id) {
+    proxy?.$modal.msgWarning('请选择人员!');
+    return false;
+  }
+  startOneTest(null, () => {
+    //显示再测一次按钮
+    showTestAgain.value = true;
+    //停止播报;
+    speckCancel();
+    //计时项目才开
+    if (needStart.value == true) {
+      speckText('哨声');
+      if (parameter.value.music && musicList.value.length) {
+        let obj = musicList.value.find((item: any) => {
+          return item.id == parameter.value.music;
+        });
+        if (obj != undefined) {
+          playMusic(obj.url);
+        }
+      }
+      //时间为0的为正计时,大于0的为倒计时
+      if (time.value.testTime == 0) {
+        getCounting('+');
+      } else {
+        getCounting('-');
+      }
+    } else {
+      speckText(faceCheckStu.value.name + ',请开始测试');
+    }
+  });
+};
+
+/**
+ * 再测一次
+ */
+const getAgain = async () => {
+  let txt = '是否再测一次?';
+  await proxy?.$modal.confirm(txt);
+  getClearTimer();
+  //预存测试人员
+  let student = JSON.parse(JSON.stringify(faceCheckStu.value));
+  //测试中
+  if (examState.value == 42) {
+    await finishOneTest();
+  }
+  //其他状态
+  if (examState.value > 3) {
+    await closeOneTest();
+  }
+  //重新走一次流程
+  await openOneTest();
+  await startFace();
+  await stopFace();
+  getFaceConfirmOnly(student);
+};
+
+/**
+ * 确认退出
+ */
+const confirmExit = () => {
+  let handcontroller_id = parameter.value.handcontroller;
+  proxy?.$modal
+    .confirm(handcontroller_id ? `请保持两秒确认退出` : `确定退出吗?`)
+    .then(() => {
+      getExit();
+    })
+    .finally(() => {});
+};
+
+/**
+ * 退出
+ */
+const getExit = () => {
+  getClearTimer(); //清除计时器
+  examEnds(); //通知工作站关闭
+  speckCancel(); //停止播报
+  window.onbeforeunload = null; //移除事件处理器
+  let handcontroller_id = parameter.value.handcontroller;
+  if (handcontroller_id) {
+    router.push({ path: '/gesture' });
+  } else {
+    if (parameter.value.taskId) {
+      router.push({ path: '/test' });
+    } else {
+      router.push({ path: '/train' });
+    }
+  }
+};
+
+/**
+ * 清空定时任务
+ */
+const getClearTimer = (data?: any) => {
+  if (data) {
+    //清除指定
+    clearInterval(timerManager.value[data]);
+    timerManager.value[data] = null;
+  } else {
+    //清除全部
+    for (let key in timerManager.value) {
+      if (timerManager.value.hasOwnProperty(key)) {
+        clearInterval(timerManager.value[key]);
+        timerManager.value[key] = null;
+      }
+    }
+  }
+};
+
+/**
+ * 选择学生
+ */
+const getChooseStudent = () => {
+  if (examState.value < 41) {
+    proxy?.$modal.msgWarning('请等待');
+  }
+  if (examState.value == 41) {
+    stopFace();
+    chooseStudentRef.value.open();
+    //然后定时自动关闭
+    setTimeout(() => {
+      faceWindowRef.value.close();
+    }, 3000);
+  }
+  if (examState.value == 43) {
+    getRetestFace();
+  }
+  if (examState.value == 42) {
+    getRetestFace();
+    // proxy?.$modal.msgWarning(`正在测试请结束后再操作,当前状态:${examState.value}`);
+    // return false;
+  }
+};
+
+/**
+ * 返回被选学生
+ */
+const returnStudent = (data: any) => {
+  speckCancel();
+  chooseStudentRef.value.close();
+  faceCheckStu.value = data;
+  faceWindowRef.value.open();
+  //然后定时自动关闭
+  setTimeout(() => {
+    faceWindowRef.value.close();
+  }, 1000);
+  getStopFace();
+};
+
+/**
+ * 清除历史记录
+ */
+const cleanData = () => {
+  time.value.countdownNum = time.value.testTime;
+  showTestAgain.value = false;
+  faceCheckStu.value = {};
+  currentResultObj.value = {};
+  backReason.value = [];
+};
+
+/**
+ * 自动初始化项目
+ */
+const initProject = () => {
+  //停止计时
+  getClearTimer('countdownTimer');
+  //恢复倒计时按钮状态
+  readyState.value = true;
+  //自动项目定时进入下一步
+  let time = 0;
+  //控制新建测试的时间,第一次快,之后就慢
+  if (!faceCheckStu.value.student_id) {
+    time = 1000;
+  } else {
+    time = 6000;
+  }
+  setTimeout(() => {
+    //再加一个判断以免和再测一次冲突
+    if (examState.value == 3) {
+      getOpenOneTestAndStartFace();
+    }
+  }, time);
+};
+
+/**
+ * 倒计时
+ */
+const getCounting = (type: string) => {
+  timerManager.value.countdownTimer = setInterval(() => {
+    //正计时
+    if (type == '+') {
+      time.value.countdownNum++;
+    }
+    //倒计时
+    if (type == '-') {
+      if (time.value.countdownNum <= 0) {
+        getClearTimer('countdownTimer');
+      } else {
+        time.value.countdownNum--;
+      }
+    }
+  }, 1000);
+};
+
+/**
+ * 人脸窗口
+ */
+const getFaceWindow = (data: boolean, num: number = 0) => {
+  let total = num + 1; //叠加三次后不再播放
+  let txt = parameter.value.gesture === true ? '请举右手看摄像头人脸识别' : '请看摄像头进行人脸识别';
+  speckText(txt);
+  //data=true为弹出框,data=false为不要弹出框
+  if (data) {
+    faceWindowRef.value.open();
+    //然后定时自动关闭
+    setTimeout(() => {
+      if (examState.value == 41 && faceWindowRef.value?.faceState == true) {
+        faceWindowRef.value.close();
+      }
+    }, 3000);
+  }
+  //定时检查如果一直停留在人脸识别就提示
+  let timeout = 16000;
+  timerManager.value.face = setInterval(() => {
+    getClearTimer('face');
+    if (examState.value == 41 && total < 3) {
+      getFaceWindow(false, total);
+    }
+  }, timeout);
+};
+
+/**
+ * 成绩
+ */
+const getAchievement = (data: any) => {
+  //console.log("成绩", data);
+  let type = parameter.value.project;
+  let result = data?.[dic.typeResultKey[type]] || 0;
+  let count = null;
+  if (['trijump', 'solidball', 'shotput', 'longjump'].includes(type)) {
+    count = (Math.round(result) / 100).toFixed(2);
+  } else if (['basketballv1', 'footballv1'].includes(type)) {
+    count = proxy?.$utils.runTime(result, true, 1);
+  } else {
+    count = result;
+  }
+  data.count = count || 0;
+  data.score = data.score || '0';
+  currentResultObj.value = data;
+  //违规处理
+  let arr = backReason.value;
+  if (['situp', 'pullup', 'sidepullup', 'jumprope', 'jumpingjack', 'highknees', 'jump', 'longjump', 'verticaljump'].indexOf(type) > -1) {
+    if (['pullup', 'situp', 'jumprope', 'jumpingjack', 'highknees'].indexOf(type) > -1) {
+      currentResultObj.value.back_num = data?.all_failed_num;
+    }
+    if (type === 'sidepullup') {
+      currentResultObj.value.back_num = data?.['0']?.hip_failed_num;
+    }
+    if (['jump', 'longjump', 'verticaljump'].includes(type)) {
+      if (data?.startline_check == 0) {
+        let txt = '踩线违规';
+        speckText(txt);
+        arr.push(txt);
+      }
+      if (data?.singleleg_jump_check == 0) {
+        let txt = '单脚跳违规';
+        speckText(txt);
+        arr.push(txt);
+      }
+      if (data?.outside_check == 0) {
+        let txt = '跳出测试区违规';
+        speckText(txt);
+        arr.push(txt);
+      }
+    }
+    if (data?.elbow_check == false) {
+      let txt = '肘部违规';
+      speckText(txt);
+      arr.push(txt);
+    }
+    if (['situp', 'pullup'].indexOf(type) > -1 && data?.knee_check === false) {
+      let txt = '腿部违规';
+      speckText(txt);
+      if (!arr.includes(txt)) {
+      }
+      arr.push(txt);
+    }
+    if (['situp'].indexOf(type) > -1 && data?.hand_check === false) {
+      let txt = '手部违规';
+      speckText(txt);
+      if (!arr.includes(txt)) {
+      }
+      arr.push(txt);
+    }
+    if (['pullup'].indexOf(type) > -1 && data?.['0']?.elbow_check === false) {
+      let txt = '手部违规';
+      speckText(txt);
+      if (!arr.includes(txt)) {
+      }
+      arr.push(txt);
+    }
+    if (['situp'].indexOf(type) > -1 && data?.['0']?.back_check === false) {
+      let txt = '背部违规';
+      speckText(txt);
+      if (!arr.includes(txt)) {
+      }
+      arr.push(txt);
+    }
+    if (['sidepullup', 'situp'].indexOf(type) > -1 && data?.['0']?.hip_check === false) {
+      let txt = '臀部违规';
+      speckText(txt);
+      if (!arr.includes(txt)) {
+      }
+      arr.push(txt);
+    }
+  }
+  backReason.value = arr;
+  if (data.isfinish) {
+    if (['jump'].includes(type) && backReason.value.length) {
+      speckText('请重新测试');
+      return false;
+    }
+    if (['basketballv1', 'footballv1'].includes(type)) {
+      speckText(
+        faceCheckStu?.value.name +
+          '成绩为' +
+          (chineseNumber(proxy?.$utils.runTime(data?.[dic.typeResultKey[type]], false, 0, 1)) || 0) +
+          ',请下一位准备!' || ''
+      );
+    } else {
+      speckText(faceCheckStu?.value.name + '成绩为' + (chineseNumber(count) || 0) + unit.value + ',请下一位准备!' || '');
+    }
+    reportListRef.value.getIniReportList();
+    faceWindowRef.value.open();
+    //然后定时自动关闭
+    setTimeout(() => {
+      faceWindowRef.value.close('right');
+    }, 1000);
+  }
+};
+
+/**
+ * 准备开始
+ */
+const getReady = () => {
+  if (needStart.value && examState.value == 43 && !time.value.ready && readyState.value) {
+    if (time.value.ready) {
+      return false;
+    }
+    speckCancel();
+    readyState.value = false;
+    time.value.ready = 6;
+    timerManager.value.readyTimer = setInterval(() => {
+      time.value.ready--;
+      if (time.value.ready <= 0) {
+        getClearTimer('readyTimer');
+        getStartOneTest();
+      } else {
+        speckText(time.value.ready);
+      }
+    }, 1000);
+  }
+};
+
+/**
+ * 获取音乐
+ */
+const getMusic = async () => {
+  const list: any = useAppStore().getMusic();
+  if (list.length) {
+    musicList.value = list;
+  } else {
+    await proxy?.$http.train.musicList().then((res: any) => {
+      if (res.data.length > 0) {
+        let myList: any = res.data;
+        musicList.value = myList;
+        useAppStore().setMusic(myList);
+      }
+    });
+  }
+};
+
+/**
+ * 设置音乐
+ */
+const setMusic = async (data:any) => {
+  //console.log("data",data)
+  parameter.value.music = data;
+};
+
+/**
+ * 获取设备项目
+ */
+const getDevice = async () => {
+  let deviceid = localStorage.getItem('deviceid') || '';
+  if (deviceid) {
+    startDevice({ deviceid: deviceid });
+  } else {
+    proxy?.$modal.msgError(`缺少设备信息请重新登录!`);
+    await proxy?.$http.common.logout({}).then((res: any) => {});
+    proxy?.$modal?.closeLoading();
+    //清空缓存
+    // localStorage.clear();
+    localStorage.removeItem('token');
+    localStorage.removeItem('userInfo');
+    //跳转
+    router.push({ path: '/login/qrcode' });
+  }
+};
+
+/**
+ * 加载手势WS
+ */
+const initHand = () => {
+  handWs((e: any) => {
+    if (router.currentRoute.value.path != '/train/test' || parameter.value.handcontroller == undefined || examState.value == 0) {
+      return false;
+    }
+    console.log('eeeee', e);
+    if (e?.wksid) {
+      //获取设备项目
+      getDevice();
+    }
+    //接收设备信息
+    if (e?.device_info) {
+      deviceInfo.value = e.device_info;
+      let handcontroller_id = deviceInfo.value.handcontroller_id;
+      stateHand(handcontroller_id);
+    }
+    //获取手势状态
+    if (e?.cmd == 'get_handcontroller_state' && e?.state == 0) {
+      let handcontroller_id = deviceInfo.value.handcontroller_id;
+      startHand(handcontroller_id);
+    }
+    //刷新
+    if (e?.data?.result == 'refresh') {
+      getExit();
+      //刷新
+      window.location.reload();
+    }
+    //没初始化完成不监听手势动作
+    if (examState.value == 0) {
+      return false;
+    }
+    //左滑动
+    // if (e?.data?.result == "next_item") {
+    //   proxy?.$modal.msgSuccess('手势指令:左滑动');
+    //   if(examState.value == 43 && time.value.ready){
+    //     return false;
+    //   }
+    //   if (examState.value == 43 || examState.value == 42) {
+    //     speckCancel();//停止播报
+    //     if (needStart.value == false) {
+    //       //自动流程项目重新识别直接返回3
+    //       closeOneTest();
+    //     } else {
+    //       //手动流程项目重新识别43返回41,42返回3
+    //       if (examState.value == 43) {
+    //         cleanData();
+    //         startFace();
+    //       } else {
+    //         closeOneTest();
+    //       }
+    //     }
+    //   }
+    // }
+    //举左手
+    if (e?.data?.result == 'left_hand') {
+      //proxy?.$modal.msgSuccess('手势指令:举左手');
+      //举左手确认退出
+      // if (exitStatus.value) {
+      //   exitStatus.value = 0;
+      //   //确认退出
+      //   let keyEvent: any = null;
+      //   keyEvent = new KeyboardEvent('keydown', {
+      //     key: 'Enter', // 键值
+      //     code: 'Enter', // 键盘代码
+      //     keyCode: 13, // 旧的键盘代码
+      //     which: 13, // 新的键盘代码
+      //     shiftKey: false, // 是否按下Shift键
+      //     ctrlKey: false, // 是否按下Ctrl键
+      //     metaKey: false, // 是否按下Meta键(Win键或Command键)
+      //     bubbles: true, // 事件是否冒泡
+      //     cancelable: true // 是否可以取消事件的默认行为
+      //   });
+      //   document.activeElement?.dispatchEvent(keyEvent);
+      //   return false;
+      // }
+      //开始识别
+      if (needStart.value && examState.value < 41) {
+        getOpenOneTestAndStartFace();
+        return false;
+      }
+      //停止人脸识别
+      // if (needStart.value && examState.value == 41) {
+      //   getStopFace();
+      // }
+      //开始测试
+      if (examState.value == 43) {
+        if (needStart.value) {
+          getReady();
+        } else {
+          getStartOneTest();
+        }
+        return false;
+      }
+    }
+    //退出
+    // if (e?.data?.result == "exit") {
+    //   proxy?.$modal.msgSuccess('手势指令:交叉手');
+    //   // console.log("exitStatus.value", exitStatus.value)
+    //   if (exitStatus.value == 0) {
+    //     speckText("请保持两秒确认退出");
+    //     //第一次才弹出
+    //     confirmExit();
+    //     setTimeout(() => {
+    //       let keyEvent: any = null;
+    //       let myKey = null;
+    //       //如果交叉手两秒后返回超过4次就确认退出
+    //       if (exitStatus.value >= 4) {
+    //         myKey = 'Enter';
+    //       } else {
+    //         myKey = 'Esc';
+    //       }
+    //       if (myKey == 'Esc') {
+    //         keyEvent = new KeyboardEvent('keydown', {
+    //           key: 'Escape', // 键值
+    //           code: 'Escape', // 键盘代码
+    //           keyCode: 27, // 旧的键盘代码
+    //           which: 27, // 新的键盘代码
+    //           shiftKey: false, // 是否按下Shift键
+    //           ctrlKey: false, // 是否按下Ctrl键
+    //           metaKey: false, // 是否按下Meta键(Win键或Command键)
+    //           bubbles: true, // 事件是否冒泡
+    //           cancelable: true // 是否可以取消事件的默认行为
+    //         });
+    //         exitStatus.value = 0;
+    //       }
+    //       if (myKey == 'Enter') {
+    //         keyEvent = new KeyboardEvent('keydown', {
+    //           key: 'Enter', // 键值
+    //           code: 'Enter', // 键盘代码
+    //           keyCode: 13, // 旧的键盘代码
+    //           which: 13, // 新的键盘代码
+    //           shiftKey: false, // 是否按下Shift键
+    //           ctrlKey: false, // 是否按下Ctrl键
+    //           metaKey: false, // 是否按下Meta键(Win键或Command键)
+    //           bubbles: true, // 事件是否冒泡
+    //           cancelable: true // 是否可以取消事件的默认行为
+    //         });
+    //       }
+    //       document.activeElement?.dispatchEvent(keyEvent);
+    //     }, 2500)
+    //   }
+    //   exitStatus.value = exitStatus.value + 1
+    // }
+    // if (e?.data?.result == "exit") {
+    //   console.log("exitStatus.value", exitStatus.value)
+    //   if (exitStatus.value == 0) {
+    //     exitStatus.value = 1
+    //     speckText("请5秒内举左手确认退出");
+    //     //第一次才弹出
+    //     confirmExit();
+    //     time.value.exit = 6;
+    //     timerManager.value.exitTimer = setInterval(() => {
+    //       time.value.exit--;
+    //       console.log("取消倒计时", time.value.exit)
+    //       proxy?.$modal.msgWarning(`取消倒计时:${time.value.exit}`)
+    //       if (time.value.exit == 0) {
+    //         exitStatus.value = 0;
+    //         getClearTimer("exitTimer");
+    //         let keyEvent: any = null;
+    //         keyEvent = new KeyboardEvent('keydown', {
+    //           key: 'Escape', // 键值
+    //           code: 'Escape', // 键盘代码
+    //           keyCode: 27, // 旧的键盘代码
+    //           which: 27, // 新的键盘代码
+    //           shiftKey: false, // 是否按下Shift键
+    //           ctrlKey: false, // 是否按下Ctrl键
+    //           metaKey: false, // 是否按下Meta键(Win键或Command键)
+    //           bubbles: true, // 事件是否冒泡
+    //           cancelable: true // 是否可以取消事件的默认行为
+    //         });
+    //         document.activeElement?.dispatchEvent(keyEvent);
+    //       }
+    //     }, 1000);
+    //   }
+    // }
+  });
+};
+
+/**
+ * 输出犯规
+ */
+watch(
+  () => backReason.value.length,
+  (v) => {
+    backReasonStr.value = backReason.value[backReason.value.length - 1];
+    setTimeout(() => {
+      backReasonStr.value = '';
+    }, 1500);
+  },
+  { immediate: true }
+);
+
+/**
+ * 播报时间
+ */
+watch(
+  () => time.value.countdownNum,
+  (newData) => {
+    if (examState.value != 42) {
+      return false;
+    }
+    if (newData >= 30 && newData < time.value.testTime) {
+      if (newData % 30 == 0) {
+        speckText(`还有${newData}秒`);
+      }
+    }
+    if (newData == 10) {
+      speckText('还有10秒');
+    }
+    if (newData <= 5) {
+      speckText(newData);
+    }
+    if (newData == 0) {
+      speckText('哨声');
+    }
+  },
+  { immediate: true }
+);
+
+/**
+ * 成绩整数播报
+ */
+watch(
+  () => currentResultObj.value,
+  (newData: any, oldData: any) => {
+    if (examState.value != 42 || newData.count <= 0) {
+      return false;
+    }
+    let project = parameter.value.project;
+    //引体向上比较慢所以都播报
+    if (['pullup'].includes(project) && newData.count > 0 && oldData.back_num == oldData.back_num) {
+      speckText(newData.count);
+    }
+    if (
+      ['situp', 'sidepullup', 'jumprope', 'jumpingjack', 'highknees'].includes(project) &&
+      newData.count > 0 &&
+      newData.count % 10 == 0 &&
+      oldData.back_num == oldData.back_num
+    ) {
+      speckText(newData.count);
+    }
+  },
+  { immediate: true }
+);
+
+/**
+ * 播报时间
+ */
+watch(
+  () => currentResultObj.value.count,
+  (newData) => {
+    if (newData > 0 && ['jumprope'].includes(parameter.value.project)) {
+      //let frameRate = 12;
+      //gameContainer.value.sports(frameRate);
+    }
+  },
+  { immediate: true }
+);
+
+/**
+ * 时间转换
+ */
+// const countdownNumFormat = computed(() => {
+//   return time.value.countdownNum;
+//   //return proxy?.$utils.timeFormat(time.value.countdownNum);
+// });
+
+onBeforeMount(() => {
+  parameter.value = route.query;
+  let project = parameter.value.project;
+  let area = parameter.value.area;
+  parameter.value.examId = `${project}_${area}`; //项目+区
+  if (parameter.value.time) {
+    time.value.testTime = parameter.value.time;
+  }
+  time.value.countdownNum = time.value.testTime;
+  userInfo.value = JSON.parse(myInfo);
+  unit.value = dic.unit[project];
+  if (parameter.value.gesture == 'true') {
+    parameter.value.gesture = true;
+  } else {
+    parameter.value.gesture = false;
+  }
+  //需要开始按钮的项目
+  let myList = ['situp', 'jumprope', 'jumpingjack', 'highknees', 'footballv1', 'basketballv1', 'pingpong'];
+  if (myList.includes(project)) {
+    needStart.value = true;
+  }
+  //加载WS
+  initWs({ parameter: parameter.value, testTime: time.value.testTime }, (data: any) => {
+    getMessage(data);
+  });
+  //初始化语音
+  initSpeech();
+  //初始化手势
+  initHand();
+  //加载音乐
+  getMusic();
+  //刷新关闭
+  window.onbeforeunload = function (e) {
+    var confirmationMessage = '刷新/关闭页面将会关闭页面,是否确认退出页面?';
+    (e || window.event).returnValue = confirmationMessage; // 兼容 Gecko + IE
+    let bUrl = import.meta.env.VITE_APP_BASE_API;
+    let classId = parameter.value.classes;
+    let project = parameter.value.project;
+    let area = parameter.value.area;
+    let examId = `${project}_${area}`;
+    let mySid = sid.value;
+    let token: any = localStorage.getItem('token');
+    let formData = new FormData();
+    formData.append('exam_id', examId);
+    formData.append('class_id', classId);
+    formData.append('token', token);
+    formData.append('sid', mySid);
+    navigator.sendBeacon(bUrl + '/exam/close_exam', formData);
+    return confirmationMessage; // 兼容 Gecko + Webkit, Safari, Chrome
+  };
+});
+
+onBeforeUnmount(() => {
+  getExit();
+});
+</script>
+
+<style lang="scss" scoped>
+$topPadding: 5.19rem;
+$waiPadding: 6.51rem;
+
+.main {
+  width: calc(100% - ($waiPadding * 2));
+  height: 78vh;
+  padding-top: 10rem;
+  margin: 0 auto;
+  display: flex;
+  justify-content: space-between;
+  overflow: hidden;
+
+  .main-left {
+    width: 71.5%;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+
+    .main-left-top {
+      display: flex;
+      justify-content: space-between;
+      height: 55.8%;
+      position: relative;
+
+      .top-left {
+        width: 37.4%;
+        height: 100%;
+        border-radius: 1.6rem;
+        background: radial-gradient(122% 126% at 97% 6%, #35ffc6 0%, #00ffe8 100%);
+        text-align: center;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        cursor: pointer;
+
+        .top-left-center {
+          .pic {
+            width: 22.3vh;
+            height: 22.3vh;
+            border-radius: 50%;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            overflow: hidden;
+            margin: 0 auto 2vh auto;
+
+            img {
+              width: 100%;
+            }
+          }
+
+          .pic2 {
+            box-sizing: border-box;
+            border: 0.44rem solid rgba(26, 41, 58, 0.6315);
+          }
+
+          .name {
+            width: 100%;
+            color: #1a293a;
+            font-size: 2.21rem;
+          }
+
+          .name2 {
+            padding: 0 0.3rem;
+            border-radius: 1.1rem;
+            background: radial-gradient(96% 96% at 2% 32%, #ffffff 0%, #fcfdfd 54%, #e1e4e7 100%);
+            box-shadow: inset 0px 1px 0px 2px rgba(255, 255, 255, 0.9046), inset 0px 3px 6px 0px rgba(0, 0, 0, 0.0851);
+          }
+        }
+      }
+
+      .top-right {
+        width: 62%;
+        height: 100%;
+        border-radius: 1.6rem;
+        opacity: 1;
+        background: #ffffff;
+        box-sizing: border-box;
+        border: 0.55rem solid #13ed84;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        flex-direction: column;
+        position: relative;
+
+        .time {
+          width: 28vh;
+          height: 28vh;
+          line-height: 28vh;
+          border-radius: 50%;
+          color: #ff9402;
+          font-size: 11vh;
+          text-align: center;
+          background-image: url('@/assets/images/test/time.png');
+          background-position: center;
+          background-repeat: no-repeat;
+          background-size: 100% 100%;
+          position: absolute;
+          right: -1.5vh;
+          top: -11vh;
+          font-family: 'Saira-BlackItalic';
+        }
+
+        .tips {
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          height: 100%;
+
+          img {
+            max-height: 80%;
+            max-height: 80%;
+          }
+        }
+
+        .complete {
+          width: 100%;
+          display: flex;
+          justify-content: center;
+          flex-direction: column;
+
+          .scoreBox {
+            height: 10vh;
+            color: #1a293a;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            margin-bottom: 5vh;
+
+            .score {
+              font-size: 8.5rem;
+              line-height: 8.5rem;
+              font-family: 'Saira-BlackItalic';
+            }
+
+            .prompt {
+              font-size: 3.5rem;
+              line-height: 3.5rem;
+            }
+
+            .unit {
+              font-size: 2rem;
+              margin-left: 10px;
+            }
+          }
+
+          .fractionViolation {
+            height: 10vh;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+
+            .fraction {
+              height: 10vh;
+              line-height: 10vh;
+              border-radius: 5vh;
+              display: flex;
+              align-items: center;
+              padding: 0 6%;
+              background: linear-gradient(138deg, #38536c 21%, #1a293a 75%);
+              box-shadow: inset 0px 1px 13px 0px rgba(255, 255, 255, 0.9452);
+
+              .lable {
+                font-size: 4vh;
+                color: #13ed84;
+              }
+
+              .value {
+                font-size: 7vh;
+                color: #00ffe8;
+                font-family: 'Saira-BlackItalic';
+                min-width: 7vh;
+              }
+            }
+
+            .violation {
+              height: 6.1vh;
+              line-height: 6.1vh;
+              border-radius: 4vh;
+              border: 0.25rem solid #ed7905;
+              display: flex;
+              align-items: center;
+              margin-left: 11px;
+              padding: 3px;
+              box-sizing: content-box;
+
+              .lable {
+                font-size: 1.2rem;
+                color: #ffffff;
+                width: 6.1vh;
+                height: 6.1vh;
+                line-height: 6.1vh;
+                background: #ed7905;
+                border-radius: 50%;
+
+                text-align: center;
+              }
+
+              .value {
+                margin-left: 1.5vh;
+                font-size: 3.2rem;
+                color: #ed7905;
+                font-family: 'Saira-BlackItalic';
+                min-width: 6vh;
+              }
+            }
+          }
+        }
+
+        .complete2 {
+          padding-left: 20%;
+          padding-top: 5vh;
+
+          .scoreBox {
+            margin-bottom: 3vh;
+            justify-content: left;
+          }
+
+          .fractionViolation {
+            justify-content: left;
+
+            .fraction {
+              padding: 0 8%;
+            }
+          }
+        }
+
+        .foulBox {
+          height: calc(4.2vh + 0.5rem + 6px);
+          overflow: hidden;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          padding-top: 3vh;
+
+          .foul {
+            height: 4.2vh;
+            line-height: 4.2vh;
+            border-radius: 3vh;
+            border: 0.25rem solid #ed7905;
+            display: flex;
+            align-items: center;
+            margin-left: 11px;
+            padding: 3px;
+            box-sizing: content-box;
+
+            .lable {
+              font-size: 2rem;
+              color: #ffffff;
+              width: 4.2vh;
+              height: 4.2vh;
+              line-height: 4.2vh;
+              background: #ed7905;
+              border-radius: 50%;
+              text-align: center;
+            }
+
+            .value {
+              margin-left: 1.5vh;
+              font-size: 2rem;
+              color: #ed7905;
+              font-family: 'Saira-BlackItalic';
+              min-width: 6vh;
+              padding: 0 10px;
+            }
+          }
+        }
+
+        .readyBoxBefore {
+          display: flex;
+          justify-content: center;
+          font-size: 2.5rem;
+          color: #1a293a;
+          padding-top: 4vh;
+          line-height: 0;
+          margin-bottom: 2vh;
+
+          .item {
+            display: flex;
+            justify-content: center;
+
+            .lable {
+              display: flex;
+              align-items: center;
+              margin-left: 10px;
+            }
+          }
+
+          img {
+            height: 20vh;
+          }
+        }
+
+        .readyBox {
+          text-align: center;
+          color: #1a293a;
+
+          .value {
+            font-size: 8.5rem;
+            line-height: 8.5rem;
+            font-family: 'Saira-BlackItalic';
+          }
+
+          .lable {
+            font-size: 3.5rem;
+          }
+
+          .transparent {
+            opacity: 0;
+          }
+        }
+
+        .btn {
+          font-size: 2.21rem;
+          color: #ffffff;
+          text-align: center;
+          width: 50%;
+          line-height: 8vh;
+          line-height: 8vh;
+          border-radius: 15px;
+          opacity: 1;
+          background: radial-gradient(159% 126% at 5% 93%, #f99f02 0%, #ed7905 100%);
+          box-shadow: 3px 6px 4px 1px rgba(0, 0, 0, 0.1874), inset 0px 1px 0px 2px rgba(255, 255, 255, 0.3);
+          cursor: pointer;
+        }
+
+        .btn2 {
+          width: auto;
+          padding: 0 10px;
+        }
+
+        .exitBtn {
+          color: #ffffff;
+          background: radial-gradient(159% 126% at 5% 93%, #931b1b 0%, #ec5624 0%, #ff6860 76%);
+          box-shadow: 3px 6px 4px 1px rgba(0, 0, 0, 0.1874), inset 0px 1px 0px 2px rgba(255, 255, 255, 0.5577);
+
+          &:hover {
+            background: #ec5624;
+          }
+        }
+      }
+
+      i {
+        width: 4vw;
+        height: 4vw;
+        display: block;
+        position: absolute;
+        top: 50%;
+        left: 37.5%;
+        margin-top: calc(4vw * -0.5);
+        margin-left: calc(4vw * -0.5);
+        background-image: url('@/assets/images/test/yuan.png');
+        background-position: center;
+        background-repeat: no-repeat;
+        background-size: 100% 100%;
+        border-radius: 50%;
+        flex-shrink: 0;
+        transition: all 0.5s;
+      }
+    }
+
+    .main-left-bottom {
+      display: flex;
+      justify-content: space-between;
+      height: calc(100% - 55.8% - 3vh);
+      overflow: hidden;
+
+      .bottom-left {
+        width: 58%;
+        padding-right: 1rem;
+        display: flex;
+        flex-direction: column;
+
+        .tips {
+          height: 2.8vh;
+
+          img {
+            height: 100%;
+          }
+        }
+
+        .pic {
+          text-align: center;
+          width: 100%;
+          height: 100%;
+          display: flex;
+          justify-content: center;
+          overflow: hidden;
+
+          img {
+            max-width: 100%;
+            max-height: 100%;
+          }
+        }
+      }
+
+      .bottom-right {
+        width: 41%;
+        height: 100%;
+        overflow-y: scroll;
+        color: #f9f9f9;
+        font-size: 1.1rem;
+        line-height: 1.6rem;
+
+        &::-webkit-scrollbar {
+          width: 10px;
+        }
+
+        &::-webkit-scrollbar-thumb {
+          border-width: 2px;
+          border-radius: 4px;
+          border-style: dashed;
+          border-color: transparent;
+          background-color: rgba(26, 62, 78, 0.9);
+          background-clip: padding-box;
+        }
+
+        &::-webkit-scrollbar-button:hover {
+          border-radius: 6px;
+          background: rgba(26, 62, 78, 1);
+        }
+      }
+    }
+  }
+
+  .main-right {
+    width: 27%;
+    border-radius: 1.6rem;
+    background: linear-gradient(29deg, #092941 -82%, #2a484b 94%);
+    box-shadow: inset 0px 1px 0px 2px rgba(255, 255, 255, 0.4);
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+  }
+}
+.close {
+  position: absolute;
+  // right: calc($waiPadding - 3.2rem);
+  left: auto;
+  right: calc($topPadding / 2 - 3.2rem / 4);
+  top: auto;
+  bottom: calc($topPadding / 2 - 3.2rem / 4);
+}
+</style>