林旭祥 před 10 měsíci
rodič
revize
1dbf9ddf59

+ 1 - 1
src/components/ChooseStudent/index.vue

@@ -26,7 +26,7 @@
                 @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" />
+                    <el-avatar :src="scope.row.face_pic ||scope.row.logo_url" />
                   </template>
                 </el-table-column>
                 <el-table-column prop="className" label="班级" width="180" />

+ 5 - 1
src/components/OptionWindow/index.vue

@@ -266,7 +266,11 @@ const confirm = (data: any) => {
     ElMessage({ message: message, type: 'error', duration: 3 * 1000 });
     return false;
   }
-  router.push({ path: '/train/test', query: optionForm.value });
+  if (["run50", "run70", "run100", "run200", "run400", "run800", "run1000", "run15x4", "run10x4", "run50x8"].includes(project.value.key)) {
+    router.push({ path: '/train/run', query: optionForm.value });
+  } else {
+    router.push({ path: '/train/test', query: optionForm.value });
+  }
 };
 
 onMounted(() => {

+ 12 - 11
src/router/index.ts

@@ -1,14 +1,15 @@
-import { createRouter, createWebHashHistory } from 'vue-router'
+import { createRouter, createWebHashHistory } from 'vue-router';
 
 const router = createRouter({
-    history: createWebHashHistory(),
-    routes: [
-        { path: "/", component: () => import("@/views/home/index.vue") },
-        { path: "/home", component: () => import("@/views/home/index.vue") },
-        { path: "/login", component: () => import("@/views/login/index.vue") },
-        { path: "/train", component: () => import("@/views/train/index.vue") },
-        { path: '/train/test', component: () => import("@/views/train/test.vue") }
-    ],
-})
+  history: createWebHashHistory(),
+  routes: [
+    { path: '/', component: () => import('@/views/home/index.vue') },
+    { path: '/home', component: () => import('@/views/home/index.vue') },
+    { path: '/login', component: () => import('@/views/login/index.vue') },
+    { path: '/train', component: () => import('@/views/train/index.vue') },
+    { path: '/train/test', component: () => import('@/views/train/test.vue') },
+    { path: '/train/run', component: () => import('@/views/train/run.vue') }
+  ]
+});
 
-export default router;
+export default router;

+ 2 - 3
src/utils/speech.ts

@@ -95,19 +95,19 @@ export const speckText = (text: any) => {
     哨声: 'shaosheng.mp3',
     哨声2: 'shaosheng2.mp3'
   };
+  speechText = '';
   console.log('播报', text);
+
   if (obj[text]) {
     //用本地文件
     let url = `./static/audio/${obj[text]}`;
     myAudio = new Audio(url);
     myAudio.play();
-    speechText = '';
   } else {
     if (browserSupport == true) {
       //用TTS
       speech.speak({ text: text.toString() }).then(() => {
         speech.cancel(); //播放结束后调用
-        speechText = '';
       });
     } else {
       //用百度语音
@@ -115,7 +115,6 @@ export const speckText = (text: any) => {
       let url = `https://tsn.baidu.com/text2audio?tex=${encodeURI(text)}&tok=${tok}&cuid=baike&lan=ZH&ctp=1&vol=15&rate=32&per=0&spd=7&pit=4`;
       myAudio = new Audio(url);
       myAudio.play();
-      speechText = '';
     }
   }
 };

+ 57 - 25
src/utils/ws.ts

@@ -1,19 +1,22 @@
 import io from 'socket.io-client';
 const address: any = import.meta.env.VITE_APP_BASE_API;
 const token: any = localStorage.getItem('token');
-let myInfo: any = localStorage.getItem('userInfo');
+const myInfo: any = localStorage.getItem('userInfo');
 let socket: any = {}; //ws实例对象
 let timerManager: any = {}; //计时器管理
 let parameter: any = {}; //参数
 let testTime: number = 60; //默认时长
 let userInfo: any = JSON.parse(myInfo); //用户信息
 let beatTime: number = 10000; //心跳频率
-let loading: any = null;
+let beatNumber: number = 0; //心跳次数
+let loading: any = null; //遮罩层
+let version: string = ''; //ws接口版本v2表示单ws多项目
 
 export const initWs = (data: any, callback: any) => {
   loading = ElLoading.service({ text: '正在初始化,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
   parameter = data.parameter;
   testTime = data.testTime;
+  version = data.version || '';
   socket = io(address + '/midexam', {
     transports: ['websocket', 'polling'],
     query: {
@@ -30,6 +33,7 @@ export const initWs = (data: any, callback: any) => {
     callback(e);
     //实时状态
     if (e.cmd === 'exam_status') {
+      beatNumber++;
     }
     //工作站状态
     if (e.cmd === 'init_result') {
@@ -90,7 +94,7 @@ export const initWs = (data: any, callback: any) => {
 
 /**
  * 发送命令
- * @param type发送类型:
+ * @param type发送类型:v2版是单WS多人的项目
  * msgfrom_frontend:测试中命令交互,
  * get_exam_status:心跳,
  * exam_ends:结束测试,
@@ -116,6 +120,10 @@ export const initWs = (data: any, callback: any) => {
 export const sendMessage = (type: string, data: any, callback?: () => void) => {
   if (socket.connected) {
     callback = callback || function () {};
+    //版本2就拼接进去
+    if (version == 'v2') {
+      type = type + '_' + version;
+    }
     socket.emit(type, JSON.stringify(data), callback);
   }
 };
@@ -182,17 +190,29 @@ export const stopFace = () => {
  * 确认并提交人脸
  */
 export const faceConfirmOnly = (data: any, callback?: any) => {
+  console.log(Array.isArray(data));
   let examId = parameter.examId;
+  let myData = null;
+  if (Array.isArray(data)) {
+    //数组类型
+    myData = {
+      cmd: 'face_confirm_only',
+      data
+    };
+  } else {
+    //对象类型
+    myData = {
+      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
-      }
+      data: myData
     },
     () => {
       callback();
@@ -245,21 +265,6 @@ export const closeOneTest = () => {
   });
 };
 
-/**
- * 心跳
- */
-const getNetWork = () => {
-  timerManager.beat = setInterval(() => {
-    let examId = parameter.examId;
-    let wk_id = parameter.wk_id;
-    sendMessage('get_exam_status', {
-      exam_id: examId,
-      wk_id: wk_id,
-      school_id: userInfo.school_id || null
-    });
-  }, beatTime);
-};
-
 /**
  * 某道识别到了人就停止某道的识别
  */
@@ -286,6 +291,33 @@ export const resumeFaceRecognitionChannels = (data: any) => {
   });
 };
 
+/**
+ * 心跳
+ */
+export const getNetWork = (callback?: any) => {
+  timerManager.beat = setInterval(() => {
+    let examId = parameter.examId;
+    let wk_id = parameter.wk_id;
+    sendMessage(
+      'get_exam_status',
+      {
+        exam_id: examId,
+        wk_id: wk_id,
+        school_id: userInfo.school_id || null
+      },
+      () => {
+        //如果心跳停止了就退出去
+        let beforBeatNumber = JSON.parse(JSON.stringify(beatNumber));
+        setTimeout(() => {
+          if (beforBeatNumber == beatNumber) {
+            callback();
+          }
+        }, 5000);
+      }
+    );
+  }, beatTime);
+};
+
 /**
  * 关闭项目
  */

+ 426 - 0
src/views/train/run.vue

@@ -0,0 +1,426 @@
+<template>
+  <div>
+    <div v-if="needStart">{{
+      countdownNumFormat
+    }}</div>
+    <div class="trackItem">
+      <div v-for="(item, index) in faceStudentList" :key="index" class="li">
+        <div>{{ item.track }}</div>
+        <div><el-avatar :src="item.face_pic" @click="getChooseStudent(item.track)" />
+        </div>
+        <div>{{ item.student_name || "未检录" }}</div>
+        <div class="menuBtn" v-if="examState <= 42" @click="getChooseStudent(item.track)">检录
+        </div>
+
+      </div>
+    </div>
+    <div>当前状态:({{ examState == 3 ? "初始化完成" : examState == 40 ? "创建测试" : examState == 41 ? "正在人脸识别"
+      :
+      examState ==
+        43 ? "停止人脸识别" : examState == 42 ? "正在测试" : "请初始化" }})</div>
+    <div @click="getRetestFace" v-if="examState == 43 || examState == 42">4、重新识别</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="getRetest" v-if="examState == 42">再测一次</div>
+    <div @click="confirmExit">退出</div>
+    <ChooseStudent ref="chooseStudentRef" @returnData="returnStudent" />
+  </div>
+</template>
+
+<script setup name="TrainTest" lang="ts">
+import { initSpeech, speckText, speckCancel, chineseNumber } from '@/utils/speech'
+import { initWs, examEnds, openOneTest, startFace, stopFace, faceConfirmOnly, startOneTest, finishOneTest, closeOneTest, getNetWork, suspendFaceRecognitionChannels, resumeFaceRecognitionChannels } from '@/utils/ws'
+import dataDictionary from "@/utils/dataDictionary"
+const { proxy } = getCurrentInstance() as any;
+const router = useRouter();
+const route = useRoute();
+const chooseStudentRef = ref();
+const data = reactive<any>({
+  timerManager: {},//计时器管理
+  parameter: {},//参数
+  time: {
+    testTime: 0,//时长
+    countdownNum: 0,//计时
+  },
+  userInfo: {},//用户信息
+  examState: 0,//当前状态
+  resultId: null,//测试ID
+  currentResultObj: {},//成绩
+  unit: "",//单位
+  backReason: [],//犯规项
+  needStart: false,//是否需要按钮
+  faceStudentList: [],//跑道和人信息
+  currentTrack: null,//当前跑道
+});
+const { timerManager, parameter, time, userInfo, examState, resultId, currentResultObj, unit, backReason, needStart, faceStudentList, currentTrack } = toRefs(data);
+
+/**
+ * 接收消息
+*/
+const getMessage = (e: any) => {
+  console.log("WS响应:", e)
+  //实时状态
+  if (e.cmd === 'exam_status') {
+    examState.value = e.data;
+  }
+  //工作站状态
+  if (e.cmd === 'init_result') {
+
+  }
+
+  //获取跑道配置
+  if (e.cmd === 'exam_config') {
+    let list: any = [];
+    for (let i = 1; i <= e.data.num_of_tracks; i++) {
+      list.push({ track: i });
+    }
+    faceStudentList.value = list
+  }
+
+  //测试违规
+  if (e.cmd === 'warning_result') {
+
+  }
+  //后端播报语音
+  if (e.cmd === 'return_audio_msg') {
+
+  }
+  //错误提示
+  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') {
+
+  }
+  //断线状态
+  if (e.cmd === 'disconnect_request') {
+    speckText(e.data.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) {
+    }
+    if (e.data == 43) {
+
+    }
+    if (e.data == 42) {
+
+    }
+  }
+  //新建测试后返回信息,获取result_id
+  if (e.cmd === 'open_one_test_ack') {
+    resultId.value = e.data.result_id;
+  }
+  //人脸识别状态
+  if (e.cmd === 'face_check_result') {
+  }
+  //测试结束结果
+  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 = () => {
+  if (examState.value == 3) {
+    openOneTest();
+  }
+  timerManager.value.startFace = setInterval(() => {
+    if (examState.value == 40) {
+      getClearTimer("startFace");
+      startFace();
+    }
+  }, 250);
+};
+
+/**
+ * 停止人脸识别
+*/
+const getStopFace = () => {
+  stopFace();
+  getFaceConfirmOnly();
+};
+
+/**
+ * 确定人脸信息
+*/
+const getFaceConfirmOnly = () => {
+  faceConfirmOnly(faceStudentList.value, () => {
+  });
+};
+
+/**
+ * 重新识别
+*/
+const getRetestFace = () => {
+  if (needStart.value == false) {
+    //自动流程项目重新识别直接返回3
+    closeOneTest();
+  } else {
+    //手动流程项目重新识别43返回41,42返回3
+    if (examState.value == 43) {
+      cleanData();
+      startFace();
+    } else {
+      closeOneTest();
+    }
+  }
+};
+
+/**
+ * 开始测试
+*/
+const getStartOneTest = () => {
+  // if (!faceCheckStu.value.student_id) {
+  //   proxy?.$modal.msgError("请选择人员!");
+  //   return false;
+  // }
+  startOneTest(() => {
+    //计时项目才开
+    if (needStart.value == true) {
+      //时间为0的为正计时,大于0的为倒计时
+      if (time.value.testTime == 0) {
+        getCounting("+");
+      } else {
+        getCounting("-");
+      }
+    } else {
+      speckCancel()//停止播报;
+      speckText("请开始测试");
+    }
+  })
+};
+
+/**
+ * 再测一次
+*/
+const getRetest = () => {
+  //预存测试人员
+  let student = JSON.parse(JSON.stringify(faceStudentList.value));
+  //测试中
+  if (examState.value == 42) {
+    finishOneTest();
+  }
+  //已测完
+  if (examState.value == 3) {
+    openOneTest();
+  }
+  //获取最新状态逐步发送命令
+  timerManager.value.retest = setInterval(() => {
+    if (examState.value == 3) {
+      openOneTest();
+    }
+    if (examState.value == 40) {
+      startFace();
+    }
+    if (examState.value == 41) {
+      getStopFace();
+    }
+    if (examState.value == 43) {
+      faceStudentList.value = student;
+      getFaceConfirmOnly();
+      //停止自动执行
+      getClearTimer("retest");
+    }
+  }, 250);
+};
+
+/**
+ * 确认退出
+*/
+const confirmExit = () => {
+  proxy?.$modal.confirm("确定退出吗?").then(() => {
+    getExit();
+  }).finally(() => {
+  });
+};
+
+/**
+ * 退出
+*/
+const getExit = () => {
+  getClearTimer();//清除计时器
+  examEnds();//通知工作站关闭
+  speckCancel()//停止播报;
+  router.push({ path: '/' });//跳转
+};
+
+/**
+ * 清空定时任务
+*/
+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 = (track: number) => {
+  if (examState.value < 41) {
+    proxy?.$modal.msgError("请点击开始人脸识别");
+    return false;
+  }
+  currentTrack.value = track;
+  chooseStudentRef.value.open();
+};
+
+/**
+ * 返回被选学生
+*/
+const returnStudent = (data: any) => {
+  let obj = {
+    result_id: resultId.value,
+    face_pic: data.face_pic || data.logo_url,
+    student_id: data.id,
+    student_name: data.name,
+    gender: data.gender,
+  }
+  //可能已检录的先清除
+  let oldIndex = faceStudentList.value.findIndex((item: any) => {
+    return data.student_id == item.student_id;
+  })
+  if (oldIndex != -1) {
+    faceStudentList.value[oldIndex] = { track: faceStudentList.value[oldIndex].track };
+  }
+  //赋值
+  let newIndex = faceStudentList.value.findIndex((item: any) => {
+    return currentTrack.value == item.track;
+  })
+  faceStudentList.value[newIndex] = { ...faceStudentList.value[newIndex], ...obj };
+  speckText(`第${currentTrack.value == 2 ? '二' : currentTrack.value}道, ${data.name}`);
+  currentTrack.value = null;
+};
+
+/**
+ * 清除历史记录
+*/
+const cleanData = () => {
+  time.value.countdownNum = time.value.testTime;
+};
+
+/**
+ * 自动初始化项目
+*/
+const initProject = () => {
+  //停止计时
+  getClearTimer("countdownTimer");
+};
+
+/**
+ * 时间转换
+*/
+const countdownNumFormat = computed(() => {
+  return proxy?.$utils.timeFormat(time.value.countdownNum);
+});
+
+/**
+ * 倒计时
+*/
+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 getAchievement = (data: any) => {
+};
+
+onMounted(() => {
+  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;
+  let myInfo: any = localStorage.getItem("userInfo");
+  userInfo.value = JSON.parse(myInfo);
+  let dic: any = dataDictionary;
+  unit.value = dic.unit[project];
+  if (parameter.value.gesture == 'true') {
+    parameter.value.gesture = true
+  } else {
+    parameter.value.gesture = false
+  }
+  //需要开始按钮的项目
+  needStart.value = true;
+  //加载WS
+  initWs({ parameter: parameter.value, testTime: time.value.testTime }, (data: any) => {
+    getMessage(data);
+  });
+  getNetWork(() => {
+    getExit();
+  });
+  initSpeech();
+})
+
+onUnmounted(() => {
+  getExit();
+})
+</script>
+
+<style scoped lang="scss">
+.trackItem {
+  .li {
+    display: flex;
+
+    .menuBtn {
+      margin-left: 20px;
+    }
+  }
+}
+</style>

+ 72 - 66
src/views/train/test.vue

@@ -20,7 +20,6 @@
       :
       examState ==
         43 ? "停止人脸识别" : examState == 42 ? "正在测试" : "请初始化" }})</div>
-    <!-- <div @click="getProcess">走一套流程</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 v-if="needStart">
@@ -37,7 +36,7 @@
 
 <script setup name="TrainTest" lang="ts">
 import { initSpeech, speckText, speckCancel, chineseNumber } from '@/utils/speech'
-import { initWs, examEnds, openOneTest, startFace, stopFace, faceConfirmOnly, startOneTest, finishOneTest, closeOneTest, suspendFaceRecognitionChannels, resumeFaceRecognitionChannels } from '@/utils/ws'
+import { initWs, examEnds, openOneTest, startFace, stopFace, faceConfirmOnly, startOneTest, finishOneTest, closeOneTest, getNetWork, suspendFaceRecognitionChannels, resumeFaceRecognitionChannels } from '@/utils/ws'
 import dataDictionary from "@/utils/dataDictionary"
 const { proxy } = getCurrentInstance() as any;
 const router = useRouter();
@@ -47,8 +46,10 @@ const chooseStudentRef = ref();
 const data = reactive<any>({
   timerManager: {},//计时器管理
   parameter: {},//参数
-  testTime: 60,//时长
-  countdownNum: 0,//计时
+  time: {
+    testTime: 60,//时长
+    countdownNum: 0,//计时
+  },
   userInfo: {},//用户信息
   examState: 0,//当前状态
   resultId: null,//测试ID
@@ -58,7 +59,7 @@ const data = reactive<any>({
   backReason: [],//犯规项
   needStart: false,//是否需要按钮
 });
-const { timerManager, parameter, testTime, countdownNum, userInfo, examState, resultId, currentResultObj, unit, faceCheckStu, backReason, needStart } = toRefs(data);
+const { timerManager, parameter, time, userInfo, examState, resultId, currentResultObj, unit, faceCheckStu, backReason, needStart } = toRefs(data);
 
 /**
  * 接收消息
@@ -95,6 +96,9 @@ const getMessage = (e: any) => {
   }
   //断线状态
   if (e.cmd === 'disconnect_request') {
+    if (e.data.message) {
+      speckText(e.data.message);
+    }
     getExit();
   }
   //状态变更
@@ -107,7 +111,7 @@ const getMessage = (e: any) => {
       cleanData();
     }
     if (e.data == 41) {
-      getFaceWindow();
+      getFaceWindow(true);
     }
     if (e.data == 43) {
 
@@ -141,25 +145,6 @@ 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) {
-//     getStartOneTest();
-//   }
-// };
-
 /**
  * 开始识别
 */
@@ -169,8 +154,7 @@ const getOpenOneTestAndStartFace = () => {
   }
   timerManager.value.startFace = setInterval(() => {
     if (examState.value == 40) {
-      clearInterval(timerManager.value.startFace)
-      timerManager.value.startFace = null;
+      getClearTimer("startFace");
       startFace();
     }
   }, 250);
@@ -195,7 +179,7 @@ const getFaceConfirmOnly = () => {
     student_id: faceCheckStu.value.student_id,
     gender: faceCheckStu.value.gender
   }, () => {
-    faceWindowRef?.value.close();
+    faceWindowRef.value?.close();
     //不需要按钮的自动进入下一步
     if (needStart.value == false) {
       getStartOneTest();
@@ -232,8 +216,14 @@ const getStartOneTest = () => {
   startOneTest(() => {
     //计时项目才开
     if (needStart.value == true) {
-      getCountdown();
+      //时间为0的为正计时,大于0的为倒计时
+      if (time.value.testTime == 0) {
+        getCounting("+");
+      } else {
+        getCounting("-");
+      }
     } else {
+      speckCancel()//停止播报;
       speckText(faceCheckStu.value.name + ",请开始测试");
     }
   })
@@ -268,8 +258,7 @@ const getRetest = () => {
       faceCheckStu.value = student;
       getFaceConfirmOnly();
       //停止自动执行
-      clearInterval(timerManager.value.retest)
-      timerManager.value.retest = null;
+      getClearTimer("retest");
     }
   }, 250);
 };
@@ -297,11 +286,18 @@ const getExit = () => {
 /**
  * 清空定时任务
 */
-const getClearTimer = () => {
-  for (let key in timerManager.value) {
-    if (timerManager.value.hasOwnProperty(key)) {
-      clearInterval(timerManager.value[key])
-      timerManager.value[key] = null;
+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;
+      }
     }
   }
 };
@@ -328,7 +324,7 @@ const returnStudent = (data: any) => {
  * 清除历史记录
 */
 const cleanData = () => {
-  countdownNum.value = testTime.value;
+  time.value.countdownNum = time.value.testTime;
   faceCheckStu.value = {};
   currentResultObj.value = {};
   backReason.value = [];
@@ -339,10 +335,11 @@ const cleanData = () => {
 */
 const initProject = () => {
   //停止计时
-  getStopCountdown();
+  getClearTimer("countdownTimer");
   //自动项目定时进入下一步
   if (needStart.value == false) {
     let time = 0;
+    //控制新建测试的时间,第一次快,之后就慢
     if (!faceCheckStu.value.student_id) {
       time = 1000;
     } else {
@@ -361,44 +358,52 @@ const initProject = () => {
  * 时间转换
 */
 const countdownNumFormat = computed(() => {
-  return proxy?.$utils.timeFormat(countdownNum.value);
+  return proxy?.$utils.timeFormat(time.value.countdownNum);
 });
 
 /**
- * 计时
+ * 计时
 */
-const getCountdown = () => {
+const getCounting = (type: string) => {
   timerManager.value.countdownTimer = setInterval(() => {
-    if (countdownNum.value <= 0) {
-      getStopCountdown();
-    } else {
-      countdownNum.value--;
+    //正计时
+    if (type == "+") {
+      time.value.countdownNum++;
+    }
+    //倒计时
+    if (type == "-") {
+      if (time.value.countdownNum <= 0) {
+        getClearTimer("countdownTimer");
+      } else {
+        time.value.countdownNum--;
+      }
     }
   }, 1000);
 };
 
-/**
- * 停止计时
-*/
-const getStopCountdown = () => {
-  clearInterval(timerManager.value.countdownTimer);
-  timerManager.value.countdownTimer = null;
-};
-
 /**
  * 人脸窗口
 */
-const getFaceWindow = () => {
+const getFaceWindow = (data: boolean) => {
   let txt = parameter.value.gesture ? "请举手看摄像头人脸识别" : "请看摄像头进行人脸识别";
   speckText(txt);
-  faceWindowRef.value.open();
-  //然后定时自动关闭
-  setTimeout(() => {
-    if (examState.value == 41 && faceWindowRef.value.faceState == true) {
-      faceWindowRef?.value.close();
+  //data=true为弹出框,data=false为不要弹出框
+  if (data) {
+    faceWindowRef.value.open();
+    //然后定时自动关闭
+    setTimeout(() => {
+      if (examState.value == 41 && faceWindowRef.value.faceState == true) {
+        faceWindowRef.value?.close();
+      }
+    }, 3000)
+  }
+  //定时检查如果一直停留在人脸识别就提示
+  timerManager.value.face = setInterval(() => {
+    getClearTimer("face");
+    if (examState.value == 41) {
+      getFaceWindow(false);
     }
-  }, 3000)
-
+  }, 15000)
 };
 
 /**
@@ -489,7 +494,6 @@ const getAchievement = (data: any) => {
     }
   }
   backReason.value = arr;
-
   if (data.isfinish) {
     if (['jump'].includes(type) && backReason.value.length) {
       speckText("请重新测试");
@@ -497,7 +501,6 @@ const getAchievement = (data: any) => {
     }
     speckText(faceCheckStu?.value.name + "成绩为" + (chineseNumber(count) || 0) + unit.value + ",请下一位准备!" || "");
   }
-
 };
 
 
@@ -507,9 +510,9 @@ onMounted(() => {
   let area = parameter.value.area;
   parameter.value.examId = `${project}_${area}`; //项目+区
   if (parameter.value.time) {
-    testTime.value = parameter.value.time
+    time.value.testTime = parameter.value.time
   }
-  countdownNum.value = testTime.value;
+  time.value.countdownNum = time.value.testTime;
   let myInfo: any = localStorage.getItem("userInfo");
   userInfo.value = JSON.parse(myInfo);
   let dic: any = dataDictionary;
@@ -524,9 +527,12 @@ onMounted(() => {
     needStart.value = true;
   }
   //加载WS
-  initWs({ parameter: parameter.value, testTime: testTime.value }, (data: any) => {
+  initWs({ parameter: parameter.value, testTime: time.value.testTime }, (data: any) => {
     getMessage(data);
   });
+  getNetWork(() => {
+    getExit();
+  });
   initSpeech();
 })