林旭祥 10 ماه پیش
والد
کامیت
df4f8c00ce

+ 11 - 0
package-lock.json

@@ -10,6 +10,7 @@
       "dependencies": {
         "@element-plus/icons-vue": "^2.3.1",
         "@vueuse/core": "9.5.0",
+        "animate.css": "^4.1.1",
         "axios": "^1.7.0",
         "echarts": "^5.5.0",
         "element-plus": "^2.7.3",
@@ -1473,6 +1474,11 @@
         "url": "https://github.com/sponsors/epoberezkin"
       }
     },
+    "node_modules/animate.css": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/animate.css/-/animate.css-4.1.1.tgz",
+      "integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ=="
+    },
     "node_modules/ansi-regex": {
       "version": "5.0.1",
       "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -8530,6 +8536,11 @@
         "uri-js": "^4.2.2"
       }
     },
+    "animate.css": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/animate.css/-/animate.css-4.1.1.tgz",
+      "integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ=="
+    },
     "ansi-regex": {
       "version": "5.0.1",
       "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",

+ 12 - 11
package.json

@@ -13,6 +13,7 @@
   "dependencies": {
     "@element-plus/icons-vue": "^2.3.1",
     "@vueuse/core": "9.5.0",
+    "animate.css": "^4.1.1",
     "axios": "^1.7.0",
     "echarts": "^5.5.0",
     "element-plus": "^2.7.3",
@@ -23,27 +24,27 @@
   },
   "devDependencies": {
     "@iconify/json": "^2.2.40",
-    "@vitejs/plugin-vue": "^5.0.4",
     "@types/node": "^20.14.2",
-    "@types/path-browserify": "^1.0.2",
     "@types/nprogress": "^0.2.3",
+    "@types/path-browserify": "^1.0.2",
     "@typescript-eslint/eslint-plugin": "5.56.0",
     "@typescript-eslint/parser": "5.56.0",
+    "@vitejs/plugin-vue": "^5.0.4",
     "autoprefixer": "10.4.14",
+    "eslint": "8.36.0",
+    "eslint-config-prettier": "8.8.0",
+    "eslint-plugin-prettier": "4.2.1",
+    "eslint-plugin-vue": "9.9.0",
     "sass": "^1.77.4",
     "typescript": "^5.2.2",
-    "vite": "^5.2.0",
-    "vue-tsc": "^2.0.6",
     "unplugin-auto-import": "^0.17.6",
     "unplugin-icons": "^0.19.0",
     "unplugin-vue-components": "^0.27.0",
     "unplugin-vue-setup-extend-plus": "^1.0.1",
-    "eslint": "8.36.0",
-    "eslint-config-prettier": "8.8.0",
-    "eslint-plugin-prettier": "4.2.1",
-    "eslint-plugin-vue": "9.9.0",
-    "vue-eslint-parser": "^9.4.3",
+    "vite": "^5.2.0",
     "vite-plugin-compression": "^0.5.1",
-    "vite-plugin-svg-icons": "^2.0.1"
+    "vite-plugin-svg-icons": "^2.0.1",
+    "vue-eslint-parser": "^9.4.3",
+    "vue-tsc": "^2.0.6"
   }
-}
+}

+ 0 - 48
src/animate.ts

@@ -1,48 +0,0 @@
-// 前缀
-const animatePrefix = 'animate__animated ';
-// 开启随机动画 随机动画值
-const animateList: string[] = [
-  animatePrefix + 'animate__pulse',
-  animatePrefix + 'animate__rubberBand',
-  animatePrefix + 'animate__bounceIn',
-  animatePrefix + 'animate__bounceInLeft',
-  animatePrefix + 'animate__fadeIn',
-  animatePrefix + 'animate__fadeInLeft',
-  animatePrefix + 'animate__fadeInDown',
-  animatePrefix + 'animate__fadeInUp',
-  animatePrefix + 'animate__flipInX',
-  animatePrefix + 'animate__lightSpeedInLeft',
-  animatePrefix + 'animate__rotateInDownLeft',
-  animatePrefix + 'animate__rollIn',
-  animatePrefix + 'animate__rotateInDownLeft',
-  animatePrefix + 'animate__zoomIn',
-  animatePrefix + 'animate__zoomInDown',
-  animatePrefix + 'animate__slideInLeft',
-  animatePrefix + 'animate__lightSpeedIn'
-];
-// 关闭随机动画后的默认效果
-const defaultAnimate = animatePrefix + 'animate__fadeIn';
-// 搜索隐藏显示动画
-const searchAnimate = {
-  enter: '',
-  leave: ''
-};
-
-// 菜单搜索动画
-const menuSearchAnimate = {
-  enter: animatePrefix + 'animate__fadeIn',
-  leave: animatePrefix + 'animate__fadeOut'
-};
-// logo动画
-const logoAnimate = {
-  enter: animatePrefix + 'animate__fadeIn',
-  leave: animatePrefix + 'animate__fadeOut'
-};
-
-export default {
-  animateList,
-  defaultAnimate,
-  searchAnimate,
-  menuSearchAnimate,
-  logoAnimate
-};

+ 3 - 1
src/style.css → src/assets/index.scss

@@ -1,3 +1,5 @@
+@import './transition.scss';
+@import 'animate.css';
 :root {
   font-size: calc(100vw / 106);
 }
@@ -14,4 +16,4 @@ ol {
 
 ul li {
   list-style: none;
-}
+}

+ 49 - 0
src/assets/transition.scss

@@ -0,0 +1,49 @@
+// global transition css
+
+/* fade */
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity 0.28s;
+}
+
+.fade-enter,
+.fade-leave-active {
+  opacity: 0;
+}
+
+/* fade-transform */
+.fade-transform--move,
+.fade-transform-leave-active,
+.fade-transform-enter-active {
+  transition: all 0.5s;
+}
+
+.fade-transform-enter {
+  opacity: 0;
+  transform: translateX(-30px);
+}
+
+.fade-transform-leave-to {
+  opacity: 0;
+  transform: translateX(30px);
+}
+
+/* breadcrumb transition */
+.breadcrumb-enter-active,
+.breadcrumb-leave-active {
+  transition: all 0.5s;
+}
+
+.breadcrumb-enter,
+.breadcrumb-leave-active {
+  opacity: 0;
+  transform: translateX(20px);
+}
+
+.breadcrumb-move {
+  transition: all 0.5s;
+}
+
+.breadcrumb-leave-active {
+  position: absolute;
+}

+ 59 - 40
src/components/ChooseStudent/index.vue

@@ -1,45 +1,54 @@
 <template>
-  <div class="optionWindow" v-if="optionWindow.show">
-    <div class="box">
-      <div class="top">
-        <div>手动选择学生</div>
-        <div @click="close">关闭</div>
-      </div>
-      <div class="content">
-        <div class="searchBox">
-          <el-select v-model="optionForm.grade" :popper-append-to-body="false" placeholder="年级" clearable>
-            <el-option v-for="item in gradeList" :key="item.value" :label="item.label" :value="item.value" />
-          </el-select>
-          <el-select v-model="optionForm.class" :popper-append-to-body="false" placeholder="班级" clearable>
-            <el-option v-for="item in classData" :key="item.value" :label="item.name" :value="item.id" />
-          </el-select>
-          <el-input v-model="optionForm.name" placeholder="请输入学生姓名" clearable />
-          <el-button type="primary" @click="getStudent">搜索</el-button>
-        </div>
-        <div class="tableBox">
-          <el-table :data="tableData" border class="table" @row-click="handleSelectionChange" @row-dblclick="confirm"
-            highlight-current-row>
-            <el-table-column label="头像" width="120">
-              <template #default="scope">
-                <el-avatar :src="scope.row.logo_url || scope.row.face_pic" />
-              </template>
-            </el-table-column>
-            <el-table-column prop="className" label="班级" width="180" />
-            <el-table-column prop="name" label="姓名" width="180" />
-            <el-table-column prop="genderName" label="性别" />
-            <el-table-column prop="student_number" label="学号" />
-          </el-table>
-
-          <el-pagination layout="sizes, prev, pager, next" :total="page.total" :page-size="page.size"
-            :page-sizes="[20, 60, 120]" :current-page="page.page" @size-change="handleSizeChange"
-            @current-change="handleCurrentChange" />
+  <div>
+    <transition :enter-active-class="proxy?.animate.searchAnimate2.enter"
+      :leave-active-class="proxy?.animate.searchAnimate2.leave">
+      <div class="mask" v-if="optionWindow.show"></div>
+    </transition>
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter"
+      :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div class="optionWindow" v-if="optionWindow.show">
+        <div class="box">
+          <div class="top">
+            <div>手动选择学生</div>
+            <div @click="close">关闭</div>
+          </div>
+          <div class="content">
+            <div class="searchBox">
+              <el-select v-model="optionForm.grade" :popper-append-to-body="false" placeholder="年级" clearable>
+                <el-option v-for="item in gradeList" :key="item.value" :label="item.label" :value="item.value" />
+              </el-select>
+              <el-select v-model="optionForm.class" :popper-append-to-body="false" placeholder="班级" clearable>
+                <el-option v-for="item in classData" :key="item.value" :label="item.name" :value="item.id" />
+              </el-select>
+              <el-input v-model="optionForm.name" placeholder="请输入学生姓名" clearable />
+              <el-button type="primary" @click="getStudent">搜索</el-button>
+            </div>
+            <div class="tableBox">
+              <el-table :data="tableData" border class="table" @row-click="handleSelectionChange"
+                @row-dblclick="confirm" highlight-current-row>
+                <el-table-column label="头像" width="120">
+                  <template #default="scope">
+                    <el-avatar :src="scope.row.logo_url || scope.row.face_pic" />
+                  </template>
+                </el-table-column>
+                <el-table-column prop="className" label="班级" width="180" />
+                <el-table-column prop="name" label="姓名" width="180" />
+                <el-table-column prop="genderName" label="性别" />
+                <el-table-column prop="student_number" label="学号" />
+              </el-table>
+
+              <el-pagination layout="sizes, prev, pager, next" :total="page.total" :page-size="page.size"
+                :page-sizes="[20, 60, 120]" :current-page="page.page" @size-change="handleSizeChange"
+                @current-change="handleCurrentChange" />
+            </div>
+          </div>
+          <div class="bottom">
+            <div @click="close">取消</div>
+            <div @click="confirm">确定</div>
+          </div>
         </div>
       </div>
-      <div class="bottom">
-        <div @click="close">取消</div>
-        <div @click="confirm">确定</div>
-      </div>
-    </div>
+    </transition>
   </div>
 </template>
 <script setup lang="ts">
@@ -182,17 +191,27 @@ defineExpose({
 })
 </script>
 <style lang="scss" scoped>
-.optionWindow {
+.mask {
   position: fixed;
   height: 100vh;
   width: 100vw;
   top: 0;
   left: 0;
   background: rgba(0, 0, 0, 0.3);
+  z-index: 998;
+}
+
+.optionWindow {
+  position: fixed;
+  height: 100vh;
+  width: 100vw;
+  top: 0;
+  left: 0;
   display: flex;
   justify-content: center;
   align-items: center;
   color: #FFFFFF;
+  z-index: 999;
 
   .box {
     width: 50%;

+ 92 - 72
src/components/OptionWindow/index.vue

@@ -1,87 +1,97 @@
 <template>
-  <div class="optionWindow" v-if="optionWindow.show">
-    <div class="box">
-      <div class="top">
-        <div @click="close">关闭</div>
-      </div>
-      <div class="content">
-        <div>
-          <div>
-            请选择测试区域、请选择设备组
-            <div @click="getAllArea" :class="{ 'active': chooseAllState }">全选</div>
+  <div>
+    <transition :enter-active-class="proxy?.animate.searchAnimate2.enter"
+      :leave-active-class="proxy?.animate.searchAnimate2.leave">
+      <div class="mask" v-if="optionWindow.show"></div>
+    </transition>
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter"
+      :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div class="optionWindow" v-if="optionWindow.show">
+        <div class="box">
+          <div class="top">
+            <div @click="close">关闭</div>
           </div>
-          <div style="display: flex;flex-wrap: wrap;">
-            <div style="margin:0 20px;" v-for="(item, index) in areaList"
-              :class="{ 'select': chooseArea.includes(item.key), 'ing': item.value != '0' }" :key="index" class="li"
-              hover-class="li-hover" @click="getChooseArea(item)">
-              <div>{{ item.name }}</div>
+          <div class="content">
+            <div>
+              <div>
+                请选择测试区域、请选择设备组
+                <div @click="getAllArea" :class="{ 'active': chooseAllState }">全选</div>
+              </div>
+              <div style="display: flex;flex-wrap: wrap;">
+                <div style="margin:0 20px;" v-for="(item, index) in areaList"
+                  :class="{ 'select': chooseArea.includes(item.key), 'ing': item.value != '0' }" :key="index" class="li"
+                  hover-class="li-hover" @click="getChooseArea(item)">
+                  <div>{{ item.name }}</div>
+                </div>
+              </div>
             </div>
-          </div>
-        </div>
-        <div>
-          <div>评分标准</div>
-          <div style="display: flex;flex-wrap: wrap;">
-            <div style="margin:0 20px;" v-for="(item, index) in standardList"
-              :class="{ 'select': optionForm.standard == item.value }" :key="index" class="li" hover-class="li-hover"
-              @click="getChooseStandard(item)">
-              <div>{{ item.label }}</div>
+            <div>
+              <div>评分标准</div>
+              <div style="display: flex;flex-wrap: wrap;">
+                <div style="margin:0 20px;" v-for="(item, index) in standardList"
+                  :class="{ 'select': optionForm.standard == item.value }" :key="index" class="li"
+                  hover-class="li-hover" @click="getChooseStandard(item)">
+                  <div>{{ item.label }}</div>
+                </div>
+              </div>
+            </div>
+            <div>
+              <div>举手识别</div>
+              <div>
+                <el-switch v-model="optionForm.gesture" :active-value="true" :inactive-value="false" />
+              </div>
             </div>
-          </div>
-        </div>
-        <div>
-          <div>举手识别</div>
-          <div>
-            <el-switch v-model="optionForm.gesture" :active-value="true" :inactive-value="false" />
-          </div>
-        </div>
 
-        <div>
-          <div>接收心率</div>
-          <div>
-            <el-switch v-model="optionForm.hasHB" :active-value="true" :inactive-value="false" />
-          </div>
-        </div>
-        <div>
-          <div>时长</div>
-          <div>
-            <el-select v-model="optionForm.time" size="small" :popper-append-to-body="false" placeholder="请选择">
-              <el-option v-for="item in timeList" :key="item.value" :label="item.label" :value="item.value" />
-            </el-select>
-          </div>
-        </div>
-        <div>
-          <div>设置运动量目标</div>
-          <div>
-            平均心率:
-            <div style="display: flex;">
-              <el-input v-model="optionForm.standHBL" size="small" />
-              ~
-              <el-input v-model="optionForm.standHBH" size="small" />
+            <div>
+              <div>接收心率</div>
+              <div>
+                <el-switch v-model="optionForm.hasHB" :active-value="true" :inactive-value="false" />
+              </div>
             </div>
-          </div>
-          <div>
-            预警心率:
             <div>
-              <el-input v-model="optionForm.highHB" size="small" />
+              <div>时长</div>
+              <div>
+                <el-select v-model="optionForm.time" size="small" :popper-append-to-body="false" placeholder="请选择">
+                  <el-option v-for="item in timeList" :key="item.value" :label="item.label" :value="item.value" />
+                </el-select>
+              </div>
+            </div>
+            <div>
+              <div>设置运动量目标</div>
+              <div>
+                平均心率:
+                <div style="display: flex;">
+                  <el-input v-model="optionForm.standHBL" size="small" />
+                  ~
+                  <el-input v-model="optionForm.standHBH" size="small" />
+                </div>
+              </div>
+              <div>
+                预警心率:
+                <div>
+                  <el-input v-model="optionForm.highHB" size="small" />
+                </div>
+              </div>
+            </div>
+            <div>
+              <div>音乐</div>
+              <div>
+                <el-select v-model="optionForm.music" size="small" :popper-append-to-body="false" placeholder="请选择"
+                  clearable>
+                  <el-option v-for="item in musicList" :key="item.id" :label="item.name" :value="item.url" />
+                </el-select>
+              </div>
             </div>
           </div>
-        </div>
-        <div>
-          <div>音乐</div>
-          <div>
-            <el-select v-model="optionForm.music" size="small" :popper-append-to-body="false" placeholder="请选择"
-              clearable>
-              <el-option v-for="item in musicList" :key="item.id" :label="item.name" :value="item.url" />
-            </el-select>
+          <div class="bottom">
+            <div @click="close">取消</div>
+            <div @click="confirm">确定</div>
           </div>
         </div>
       </div>
-      <div class="bottom">
-        <div @click="close">取消</div>
-        <div @click="confirm">确定</div>
-      </div>
-    </div>
+    </transition>
   </div>
+
 </template>
 <script setup lang="ts">
 import useAppStore from '@/store/modules/app';
@@ -273,17 +283,27 @@ defineExpose({
 })
 </script>
 <style lang="scss" scoped>
-.optionWindow {
+.mask {
   position: fixed;
   height: 100vh;
   width: 100vw;
   top: 0;
   left: 0;
   background: rgba(0, 0, 0, 0.3);
+  z-index: 998;
+}
+
+.optionWindow {
+  position: fixed;
+  height: 100vh;
+  width: 100vw;
+  top: 0;
+  left: 0;
   display: flex;
   justify-content: center;
   align-items: center;
   color: #FFFFFF;
+  z-index: 999;
 
   .box {
     width: 50%;

+ 2 - 2
src/main.ts

@@ -1,12 +1,12 @@
 import { createApp } from 'vue';
-import './style.css';
+import '@/assets/index.scss';
 import App from './App.vue';
 import router from './router';
 import store from './store';
 import api from './api';
 import utils from './utils';
 import plugins from './plugins/index';
-import animate from './animate';
+import animate from './utils/animate';
 import './router/permission';
 
 // 自定义指令

+ 7 - 0
src/types/module.d.ts

@@ -0,0 +1,7 @@
+import type animate from '@/utils/animate';
+
+declare module '@vue/runtime-core' {
+  interface ComponentCustomProperties {
+    animate: typeof animate;
+  }
+}

+ 18 - 0
src/utils/animate.ts

@@ -0,0 +1,18 @@
+//预览地址 https://animate.style/
+// 前缀
+const animatePrefix = 'animate__animated ';
+
+const searchAnimate = {
+  enter: animatePrefix + 'animate__fadeInDown',
+  leave: animatePrefix + 'animate__fadeOutUp'
+};
+
+const searchAnimate2 = {
+  enter: animatePrefix + 'animate__fadeIn',
+  leave: animatePrefix + 'animate__fadeOut'
+};
+
+export default {
+  searchAnimate,
+  searchAnimate2
+};

+ 20 - 11
src/utils/index.ts

@@ -1,10 +1,8 @@
-import dataDictionary from "./dataDictionary"
+import dataDictionary from './dataDictionary';
 
 let utils = {
-
   /**
    * 参数处理
-   * @param {*} params  参数
    */
   tansParams: (params: any) => {
     let result = '';
@@ -30,14 +28,13 @@ let utils = {
 
   /**
    * 参数处理
-   * @param {*} params  参数
    */
   getProject: (data: any, type: number) => {
     //格式化数据
     let list = [];
     for (let key in data) {
-      let arrList = key.split("@@")
-      let newArr = arrList[0].split("_");
+      let arrList = key.split('@@');
+      let newArr = arrList[0].split('_');
       newArr.push(data[key]);
       list.push(newArr);
     }
@@ -52,18 +49,30 @@ let utils = {
             key: item[1],
             name: item[1],
             value: item[2]
-          })
+          });
         }
-      })
+      });
       let obj: any = {
         key: key,
         name: project[key],
         area: area
-      }
+      };
       newList.push(obj);
     }
     return newList;
   },
 
-}
-export default utils
+  /**
+   * 时间格式
+   */
+  timeFormat: (val: number) => {
+    let h = parseInt(((val / 60 / 60) % 24).toString());
+    let m = parseInt(((val / 60) % 60).toString());
+    let s = parseInt((val % 60).toString());
+    if (h > 0) {
+      m = h * 60 + m;
+    }
+    return `${m}:${s}`;
+  }
+};
+export default utils;

+ 27 - 15
src/utils/ws.ts

@@ -181,30 +181,42 @@ export const stopFace = () => {
 /**
  * 确认并提交人脸
  */
-export const faceConfirmOnly = (data: any) => {
+export const faceConfirmOnly = (data: any, callback?: any) => {
   let examId = parameter.examId;
-  sendMessage('msgfrom_frontend', {
-    data: {
-      cmd: 'face_confirm_only',
-      exam_id: examId,
-      result_id: data.result_id,
-      student_id: data.student_id,
-      gender: data.gender
+  sendMessage(
+    'msgfrom_frontend',
+    {
+      data: {
+        cmd: 'face_confirm_only',
+        exam_id: examId,
+        result_id: data.result_id,
+        student_id: data.student_id,
+        gender: data.gender
+      }
+    },
+    () => {
+      callback();
     }
-  });
+  );
 };
 
 /**
  * 开始测试
  */
-export const startOneTest = () => {
+export const startOneTest = (callback?: any) => {
   let examId = parameter.examId;
-  sendMessage('msgfrom_frontend', {
-    data: {
-      cmd: 'start_one_test',
-      exam_id: examId
+  sendMessage(
+    'msgfrom_frontend',
+    {
+      data: {
+        cmd: 'start_one_test',
+        exam_id: examId
+      }
+    },
+    () => {
+      callback();
     }
-  });
+  );
 };
 
 /**

+ 1 - 2
src/views/home/index.vue

@@ -64,8 +64,7 @@ const getLogout = () => {
 //跳转
 const getJump = (url: string, name: string) => {
   if (url == undefined || !url) {
-    let message = "正在开发,敬请期待!";
-    ElMessage({ message: message, type: 'error', duration: 5 * 1000 });
+    proxy?.$modal.msgWarning(`正在开发,敬请期待!`);
     return false;
   }
   router.push({ path: url });

+ 259 - 61
src/views/train/test.vue

@@ -1,25 +1,32 @@
 <template>
   <div>
+    <div>{{
+      countdownNumFormat
+    }}</div>
     <div><el-avatar :src="faceCheckStu.logo_url || faceCheckStu.face_pic" @click="getChooseStudent" /></div>
     <div>{{ faceCheckStu.name }}</div>
     <div>成绩:{{ currentResultObj.count }} {{ unit }}</div>
     <div>得分:{{ currentResultObj.score }}</div>
-    <div @click="getProcess">走一套流程({{ examState == 3 ? "创建测试" : examState == 40 ? "开始人脸识别" : examState == 41 ? "停止人脸识别"
+    <div>中断/犯规:{{ currentResultObj.back_num }}</div>
+    <div v-if="backReason.length">
+      <div>违规项</div>
+      <div v-for="(item, index) in backReason" :key="index">{{ item }}</div>
+    </div>
+    <div>当前状态:({{ examState == 3 ? "创建测试" : examState == 40 ? "开始识别" : examState == 41 ? "停止人脸识别"
       :
       examState ==
         43 ? "开始测试" : examState == 42 ? "正在测试" : "请初始化" }})</div>
-    <div @click="getOpenOneTestAndStartFace" v-if="examState == 3 || examState == 40">1、开始人脸识别</div>
-
-    <div @click="getChooseStudent">2、选择学生</div>
-    <div @click="getStopFace" v-if="examState == 41">3、停止人脸识别</div>
-
-    <div @click="getStartOneTest" v-if="examState == 43">4、开始测试</div>
-
-    <div @click="getRetest" v-if="examState == 3 || examState == 42">再测一次</div>
-
+    <!-- <div @click="getProcess">走一套流程</div> -->
 
+    <div v-if="needStart">
+      <div @click="getOpenOneTestAndStartFace" v-if="examState == 3 || examState == 40">1、开始识别</div>
+      <div @click="getStopFace" v-if="examState == 41">3、停止人脸识别</div>
+      <div @click="getStartOneTest" v-if="examState == 43">5、开始测试</div>
+    </div>
+    <div @click="getChooseStudent" v-if="examState == 41 || (examState == 43 && !faceCheckStu.student_id)">2、选择学生</div>
+    <div @click="getRetestFace" v-if="examState == 43 || examState == 42">4、重新识别</div>
+    <div @click="getRetest" v-if="(examState == 3 && faceCheckStu.student_id) || examState == 42">再测一次</div>
     <div @click="confirmExit">退出</div>
-
     <ChooseStudent ref="chooseStudentRef" @back="backStudent" />
   </div>
 </template>
@@ -35,16 +42,21 @@ const data = reactive<any>({
   timerManager: {},//计时器管理
   parameter: {},//参数
   testTime: 60,//时长
+  countdownNum: 0,//计时
   userInfo: {},//用户信息
   examState: 0,//当前状态
-  result_id: null,//测试ID
+  resultId: null,//测试ID
   currentResultObj: {},//成绩
-  unit: "",//单位
   faceCheckStu: {},//人脸信息
+  unit: "",//单位
+  backReason: [],//犯规项
+  needStart: false,//是否需要按钮
 });
-const { timerManager, parameter, testTime, userInfo, examState, result_id, currentResultObj, unit, faceCheckStu } = toRefs(data);
+const { timerManager, parameter, testTime, countdownNum, userInfo, examState, resultId, currentResultObj, unit, faceCheckStu, backReason, needStart } = toRefs(data);
 
-//接收消息
+/**
+ * 接收消息
+*/
 const getMessage = (e: any) => {
   console.log("WS响应:", e)
   //实时状态
@@ -53,6 +65,7 @@ const getMessage = (e: any) => {
   }
   //工作站状态
   if (e.cmd === 'init_result') {
+
   }
   //测试违规
   if (e.cmd === 'warning_result') {
@@ -64,11 +77,11 @@ const getMessage = (e: any) => {
   }
   //错误提示
   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') {
@@ -81,35 +94,48 @@ const getMessage = (e: any) => {
   //状态变更
   if (e.cmd === 'set_exam_state') {
     examState.value = e.data;
+    if (e.data === 3) {
+      //停止计时
+      getStopCountdown();
+      //不需要按钮的自动进入下一步
+      if (needStart.value == false) {
+        setTimeout(() => {
+          //再加一个判断以免和再测一次冲突
+          if (examState.value == 3) {
+            getOpenOneTestAndStartFace();
+          }
+        }, 6000)
+      }
+    }
+    if (e.data === 40) {
+      cleanData();
+    }
+    if (e.data == 41) {
+
+    }
+    if (e.data == 43) {
+
+    }
+    if (e.data == 42) {
+
+    }
   }
   //新建测试后返回信息,获取result_id
   if (e.cmd === 'open_one_test_ack') {
-    currentResultObj.value = {};
-    result_id.value = e.data.result_id
+    resultId.value = e.data.result_id;
   }
   //人脸识别状态
   if (e.cmd === 'face_check_result') {
     faceCheckStu.value = e.data[0] || e.data;
+    getStopFace();
   }
   //测试结束结果
   if (e.cmd === 'oneresult') {
     if (e.data.length) {
       let data = e.data[0];
       console.log("成绩", data);
-      let dic: any = dataDictionary;
-      let type = parameter.value.project;
-      let count =
-        data?.[dic.typeResultKey[type]]?.toFixed(0);
-      if (["trijump", "solidball", "shotput", "longjump"].includes(type)) {
-        count =
-          data?.[dic.typeResultKey[type]]?.toFixed(2);
-        count = Math.round(count) / 100;
-      }
-      data.count = count || "0";
-      data.score = data.score || "0";
-      currentResultObj.value = data;
+      getAchievement(data)
     }
-
   }
   //结果生成完成(视频图片)
   if (e.cmd === 'static_urls_finished') {
@@ -119,26 +145,27 @@ const getMessage = (e: any) => {
   }
 };
 
-/**
- * 正常流程
-*/
-const getProcess = () => {
-  if (examState.value == 3) {
-    openOneTest();
-  }
-  if (examState.value == 40) {
-    startFace();
-  }
-  if (examState.value == 41) {
-    getStopFace();
-  }
-  if (examState.value == 43) {
-    startOneTest();
-  }
-};
+
+// /**
+//  * 正常流程
+// */
+// const getProcess = () => {
+//   if (examState.value == 3) {
+//     openOneTest();
+//   }
+//   if (examState.value == 40) {
+//     startFace();
+//   }
+//   if (examState.value == 41) {
+//     getStopFace();
+//   }
+//   if (examState.value == 43) {
+//     getStartOneTest();
+//   }
+// };
 
 /**
- * 开始人脸识别
+ * 开始识别
 */
 const getOpenOneTestAndStartFace = () => {
   if (examState.value == 3) {
@@ -158,24 +185,59 @@ const getOpenOneTestAndStartFace = () => {
 */
 const getStopFace = () => {
   stopFace();
+  if (faceCheckStu.value.student_id) {
+    getFaceConfirmOnly();
+  }
+};
+
+/**
+ * 确定人脸信息
+*/
+const getFaceConfirmOnly = () => {
   faceConfirmOnly({
-    result_id: result_id.value,
+    result_id: resultId.value,
     student_id: faceCheckStu.value.student_id,
     gender: faceCheckStu.value.gender
+  }, () => {
+    //不需要按钮的自动进入下一步
+    if (needStart.value == false) {
+      getStartOneTest();
+    }
   });
 };
 
 /**
- * 停止人脸识别
+ * 重新识别
+*/
+const getRetestFace = () => {
+  if (needStart.value == false) {
+    //不需要手动操作的项目重新识别直接返回3
+    closeOneTest();
+  } else {
+    //需要手动操作的项目重新识别返回41
+    startFace();
+  }
+};
+
+/**
+ * 开始测试
 */
 const getStartOneTest = () => {
-  startOneTest()
+  if (!faceCheckStu.value.student_id) {
+    proxy?.$modal.msgError("请选择人员!");
+    return false;
+  }
+  startOneTest(() => {
+    getCountdown();
+  })
 };
 
 /**
  * 再测一次
 */
 const getRetest = () => {
+  //预存测试人员
+  let student = JSON.parse(JSON.stringify(faceCheckStu.value));
   //测试中
   if (examState.value == 42) {
     finishOneTest();
@@ -196,6 +258,9 @@ const getRetest = () => {
       getStopFace();
     }
     if (examState.value == 43) {
+      faceCheckStu.value = student;
+      getFaceConfirmOnly();
+      //停止自动执行
       clearInterval(timerManager.value.retest)
       timerManager.value.retest = null;
     }
@@ -209,7 +274,6 @@ const confirmExit = () => {
   proxy?.$modal.confirm("确定退出吗?").then(() => {
     getExit();
   }).finally(() => {
-
   });
 };
 
@@ -238,30 +302,164 @@ const getClearTimer = () => {
  * 选择学生
 */
 const getChooseStudent = () => {
-  if (examState.value != 41) {
-    return false;
+  if (examState.value == 41 || (examState.value == 43 && !faceCheckStu.value.student_id)) {
+    chooseStudentRef.value.open();
   }
-  chooseStudentRef.value.open();
 };
 
 /**
- * 被选学生
+ * 返回被选学生
 */
 const backStudent = (data: any) => {
   faceCheckStu.value = data;
+  getStopFace();
+};
+
+/**
+ * 返回被选学生
+*/
+const cleanData = () => {
+  countdownNum.value = testTime.value;
+  faceCheckStu.value = {};
+  currentResultObj.value = {};
+  backReason.value = [];
 };
 
+/**
+ * 时间转换
+*/
+const countdownNumFormat = computed(() => {
+  return proxy?.$utils.timeFormat(countdownNum.value);
+});
+
+/**
+ * 计时
+*/
+const getCountdown = () => {
+  timerManager.value.countdownTimer = setInterval(() => {
+    if (countdownNum.value <= 0) {
+      getStopCountdown();
+    } else {
+      countdownNum.value--;
+    }
+  }, 1000);
+};
+
+/**
+ * 停止计时
+*/
+const getStopCountdown = () => {
+  clearInterval(timerManager.value.countdownTimer);
+  timerManager.value.countdownTimer = null;
+};
+
+
+/**
+ * 成绩
+*/
+const getAchievement = (data: any) => {
+  let dic: any = dataDictionary;
+  let type = parameter.value.project;
+  let count =
+    data?.[dic.typeResultKey[type]]?.toFixed(0);
+  if (["trijump", "solidball", "shotput", "longjump"].includes(type)) {
+    count =
+      data?.[dic.typeResultKey[type]]?.toFixed(2);
+    count = Math.round(count) / 100;
+  }
+  data.count = count || "0";
+  data.score = data.score || "0";
+  currentResultObj.value = data;
+  //违规处理
+  let arr = backReason.value;
+  if (["situp", "pullup", "sidepullup", "jumprope", "jumpingjack", "jump", "longjump", "verticaljump"]
+    .indexOf(type) > -1) {
+    if (["pullup", "situp", "jumprope", "jumpingjack"].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 = "踩线违规";
+        arr.push(txt)
+      }
+      if (data?.singleleg_jump_check == 0) {
+        let txt = "单脚跳违规";
+        arr.push(txt)
+      }
+      if (data?.outside_check == 0) {
+        let txt = "跳出测试区违规";
+        arr.push(txt)
+      }
+    }
+    if (
+      data?.elbow_check == false
+    ) {
+      let txt = "肘部违规";
+      arr.push(txt);
+    }
+    if (
+      ["situp", "pullup"].indexOf(type) > -1 &&
+      data?.knee_check === false
+    ) {
+      let txt = "腿部违规";
+      if (!arr.includes(txt)) {
+        arr.push(txt);
+      }
+    }
+    if (["situp"].indexOf(type) > -1 && data?.hand_check === false) {
+      let txt = "手部违规";
+      if (!arr.includes(txt)) {
+        arr.push(txt);
+      }
+    }
+    if (
+      ["pullup"].indexOf(type) > -1 &&
+      data?.["0"]?.elbow_check === false
+    ) {
+      let txt = "手部违规";
+      if (!arr.includes(txt)) {
+        arr.push(txt);
+      }
+    }
+    if (["situp"].indexOf(type) > -1 && data?.["0"]?.back_check === false) {
+      let txt = "背部违规";
+      if (!arr.includes(txt)) {
+        arr.push(txt);
+      }
+    }
+    if (
+      ["sidepullup", "situp"].indexOf(type) > -1 &&
+      data?.["0"]?.hip_check === false
+    ) {
+      let txt = "臀部违规";
+      if (!arr.includes(txt)) {
+        arr.push(txt);
+      }
+    }
+  }
+  backReason.value = arr;
+};
+
+
 onMounted(() => {
   parameter.value = route.query;
-  parameter.value.examId = `${parameter.value.project}_${parameter.value.area}`; //项目+区
+  let project = parameter.value.project;
+  let area = parameter.value.area;
+  parameter.value.examId = `${project}_${area}`; //项目+区
   if (parameter.value.time) {
     testTime.value = parameter.value.time
   }
   let myInfo: any = localStorage.getItem("userInfo");
   userInfo.value = JSON.parse(myInfo);
   let dic: any = dataDictionary;
-  let type = parameter.value.project;
-  unit.value = dic.unit[type]
+  unit.value = dic.unit[project];
+  //需要开始按钮的项目
+  if (["jumprope", "skiprope", "jumpingjack", "situp"].includes(project)) {
+    needStart.value = true;
+  }
   //加载WS
   initWs({ parameter: parameter.value, testTime: testTime.value }, (data: any) => {
     getMessage(data);