林旭祥 1 місяць тому
батько
коміт
a8e52df136

+ 99 - 0
src/api/module/deploy.ts

@@ -0,0 +1,99 @@
+import req from '../request';
+
+export default {
+    checkStreamClosd: (data:any) => {
+        return req({
+            url: '/school/check_stream_closed',
+            method: 'post',
+            data: data,
+        });
+    },
+
+    checkCaliStreamClosd: (data:any) => {
+        return req({
+            url: '/school/check_cailistream_closed',
+            method: 'post',
+            data: data,
+        });
+    },
+
+    closeExamForce: (data:any) => {
+        return req({
+            url: '/school/close_exam_force',
+            method: 'post',
+            data: data,
+        });
+    },
+
+    closeLocalStream: (data:any) => {
+        return req({
+            url: '/exam/close_push_stream',
+            method: 'post',
+            data: data,
+        });
+    },
+
+    getExamSnapShot: (data:any) => {
+        return req({
+            url: '/exam/snap_pics',
+            method: 'post',
+            data: data,
+        });
+    },
+
+    pushLocalStream: (data:any) => {
+        return req({
+            url: '/exam/push_stream',
+            method: 'post',
+            data: data,
+        });
+    },
+
+    checkPushStream: (data:any) => {
+        return req({
+            url: '/exam/check_push_stream',
+            method: 'post',
+            data: data,
+        });
+    },
+
+    upExamSettings: (data:any) => {
+        return req({
+            url: '/school/update_gpu_exam_setting',
+            method: 'post',
+            data: data,
+        });
+    },
+
+    getExamSettings: (data:any) => {
+        return req({
+            url: '/school/get_gpu_exam_setting',
+            method: 'post',
+            data: data,
+        });
+    },
+
+    saveAnchorPt: (data:any) => {
+        return req({
+            url: '/exam/gpu_anchor_update',
+            method: 'post',
+            data: data,
+        });
+    },
+
+    checkCaliRes: (data:any) => {
+        return req({
+            url: '/school/check_cali_result',
+            method: 'post',
+            data: data,
+        });
+    },
+
+    closeCaliStream: (data:any) => {
+        return req({
+            url: '/exam/close_cali_stream',
+            method: 'post',
+            data: data,
+        });
+    },
+};

+ 8 - 0
src/assets/styles/func.scss

@@ -0,0 +1,8 @@
+// px数值转vh
+@function getvh($num) {
+  @return ($num / 1080 * 100vh);
+}
+// px数值转vw
+@function getvw($num) {
+  @return ($num / 1920 * 100vw);
+}

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

@@ -246,7 +246,7 @@ const open = async (data: any) => {
       optionForm.value.music = musicList.value[0].url; //默认第一首
     }
   } else if (['heartbeat'].includes(project.value.key)) {
-    optionForm.value.time = 2700; //默认45分钟
+    optionForm.value.time = 45; //默认45分钟
   } else {
     optionForm.value.gesture = false;
   }
@@ -407,6 +407,9 @@ const confirm = () => {
   } else if (['run50', 'run70', 'run100', 'run200', 'run400', 'run800', 'run1000', 'run15x4', 'run10x4', 'run50x8'].includes(project.value.key)) {
     //跑步项目
     router.push({ path: '/train/run', query: optionForm.value });
+  } else if (['skiprope','heartbeat'].includes(project.value.key)) {
+    //设备项目
+    router.push({ path: '/train/device', query: optionForm.value });
   } else {
     //单项
     router.push({ path: '/train/test', query: optionForm.value });

+ 2 - 0
src/router/index.ts

@@ -19,9 +19,11 @@ const router = createRouter({
         { path: '/train/test', component: () => import('@/views/train/test.vue') },
         { 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: '/test', component: () => import('@/views/test/index.vue') },
         { path: '/set', component: () => import('@/views/set/index.vue') },
         { path: '/set/config', component: () => import('@/views/set/config.vue') },
+        { path: '/set/deploy', component: () => import('@/views/set/deploy.vue') },
         { path: '/ranking', component: () => import('@/views/ranking/index.vue') },
         { path: '/course', component: () => import('@/views/course/index.vue') },
         { path: '/gesture', component: () => import('@/views/gesture/index.vue') },

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

@@ -9,6 +9,30 @@ declare module 'vue' {
   export interface GlobalComponents {
     AddItemWindow: typeof import('./../components/AddItemWindow/index.vue')['default']
     ChooseStudent: typeof import('./../components/ChooseStudent/index.vue')['default']
+    ElAvatar: typeof import('element-plus/es')['ElAvatar']
+    ElButton: typeof import('element-plus/es')['ElButton']
+    ElCollapse: typeof import('element-plus/es')['ElCollapse']
+    ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
+    ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition']
+    ElContainer: typeof import('element-plus/es')['ElContainer']
+    ElDialog: typeof import('element-plus/es')['ElDialog']
+    ElForm: typeof import('element-plus/es')['ElForm']
+    ElFormItem: typeof import('element-plus/es')['ElFormItem']
+    ElHeader: typeof import('element-plus/es')['ElHeader']
+    ElIcon: typeof import('element-plus/es')['ElIcon']
+    ElImage: typeof import('element-plus/es')['ElImage']
+    ElInput: typeof import('element-plus/es')['ElInput']
+    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
+    ElLink: typeof import('element-plus/es')['ElLink']
+    ElMain: typeof import('element-plus/es')['ElMain']
+    ElOption: typeof import('element-plus/es')['ElOption']
+    ElPagination: typeof import('element-plus/es')['ElPagination']
+    ElPopover: typeof import('element-plus/es')['ElPopover']
+    ElSelect: typeof import('element-plus/es')['ElSelect']
+    ElSwitch: typeof import('element-plus/es')['ElSwitch']
+    ElTable: typeof import('element-plus/es')['ElTable']
+    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
+    ElUpload: typeof import('element-plus/es')['ElUpload']
     FaceWindow: typeof import('./../components/FaceWindow/index.vue')['default']
     Header: typeof import('./../components/Header/index.vue')['default']
     JumpRopeGame: typeof import('./../components/JumpRopeGame/index.vue')['default']
@@ -20,4 +44,7 @@ declare module 'vue' {
     RouterView: typeof import('vue-router')['RouterView']
     WorkstationWindow: typeof import('./../components/WorkstationWindow/index.vue')['default']
   }
+  export interface ComponentCustomProperties {
+    vLoading: typeof import('element-plus/es')['ElLoadingDirective']
+  }
 }

+ 1382 - 1
src/utils/dataDictionary.ts

@@ -489,6 +489,1387 @@ let data = {
     jumprope: {},
     skiprope: {},
     heartbeat: {},
-  }
+  },
+
+  anchorLabelObj: {
+    cam_path: "测试摄像头",
+    anchor_path: "测试布点文件",
+    cam_face_path: "人脸摄像头",
+    anchor_face_path: "人脸布点文件",
+    flag_calibrate: "测试相机矫正",
+    cam_param_path: "矫正文件",
+    flag_check_person: "干扰提示",
+    flag_check_stline: "踩线提示",
+    distance_ground: "地面总长度",
+    distance_ground_between_lines: "地线间隔",
+    length_ground_line: "地线宽度",
+    flag_save: "保存视频",
+    flag_display: "画面显示",
+    distance_air_between_lines: "高线间隔",
+    length_air_line: "高线高度",
+    test_time: "测试时长(s)",
+    flag_check_back: "背部检测",
+    flag_check_knee: "膝盖检测",
+    flag_check_hand: "手部检测",
+    flag_check_elbow: "肘部检测",
+    flag_check_hip: "臀部检测",
+    difficulty: "难度系数",
+    num_of_tracks: "跑道数",
+    num_of_tracks_startline: "起跑线跑道数",
+    num_of_tracks_endline: "终点线跑道数",
+    delay_cam_second: "摄像头延时",
+    delay_cmd_second: "其他延时",
+    cam_0_path: "辅助摄像头",
+    anchor_0_path: "辅助布点文件",
+    distance_per_round: "每圈长度(m)",
+    flag_check_singleleg_jump: "单脚跳检测",
+    distance_maximum: "最长距离",
+    height_maximum: "最大高度",
+  },
+
+
+  anchorSetsObj: {
+    basic: {
+      names: ["Btl", "Btr", "Bbr", "Bbl", "Ftl", "Ftr", "Fbr", "Fbl"],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [4, 5],
+        [5, 6],
+        [6, 7],
+        [7, 4],
+      ],
+      required: { cam_path: "", anchor_path: "", cam_fps: 25 },
+      advanced: {},
+      reserved: {
+        flag_save: 1,
+        flag_display: 0,
+        cam_face_path: "",
+        anchor_face_path: "",
+        cam_face_fps: 25,
+      },
+    },
+    situp: {
+      names: ["Btl", "Btr", "Bbr", "Bbl", "Ftl", "Ftr", "Fbr", "Fbl"],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [4, 5],
+        [5, 6],
+        [6, 7],
+        [7, 4],
+      ],
+      advanced: {
+        test_time: 60,
+        flag_check_back: 1,
+        flag_check_hip: 1,
+        flag_check_knee: 1,
+        flag_check_hand: 0,
+        flag_check_elbow: 0,
+      },
+      reserved: {},
+    },
+    pullup: {
+      names: ["Btl", "Btr", "Bbr", "Bbl", "Ftl", "Ftr", "Fbr", "Fbl", "ls", "le"],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [4, 5],
+        [5, 6],
+        [6, 7],
+        [7, 4],
+        [8, 9],
+      ],
+      advanced: {
+        flag_check_knee: 1,
+        flag_check_elbow: 1,
+      },
+    },
+  
+    sidepullup: {
+      names: ["Btl", "Btr", "Bbr", "Bbl", "Ftl", "Ftr", "Fbr", "Fbl", "ls", "le"],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [4, 5],
+        [5, 6],
+        [6, 7],
+        [7, 4],
+        [8, 9],
+      ],
+      advanced: {
+        flag_check_hip: 1,
+      },
+    },
+  
+    jumprope: {
+      names: ["Btl", "Btr", "Bbr", "Bbl", "Ftl", "Ftr", "Fbr", "Fbl"],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [4, 5],
+        [5, 6],
+        [6, 7],
+        [7, 4],
+      ],
+      advanced: {
+        test_time: 60,
+      },
+    },
+    highknees: {
+      names: ["Btl", "Btr", "Bbr", "Bbl", "Ftl", "Ftr", "Fbr", "Fbl"],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [4, 5],
+        [5, 6],
+        [6, 7],
+        [7, 4],
+      ],
+      advanced: {
+        test_time: 60,
+      },
+    },
+  
+    volleyballv1: {
+    },
+  
+    squat: {
+      names: ["Btl", "Btr", "Bbr", "Bbl", "Ftl", "Ftr", "Fbr", "Fbl"],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [4, 5],
+        [5, 6],
+        [6, 7],
+        [7, 4],
+      ],
+      advanced: {
+        test_time: 60,
+      },
+    },
+    jumpleftright: {
+      names: ["Btl", "Btr", "Bbr", "Bbl", "Ftl", "Ftr", "Fbr", "Fbl"],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [4, 5],
+        [5, 6],
+        [6, 7],
+        [7, 4],
+      ],
+      advanced: {
+        test_time: 60,
+      },
+    },
+    crossjump: {
+      names: ["Btl", "Btr", "Bbr", "Bbl", "Ftl", "Ftr", "Fbr", "Fbl"],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [4, 5],
+        [5, 6],
+        [6, 7],
+        [7, 4],
+      ],
+      advanced: {
+        test_time: 60,
+      },
+    },
+    kneeupclap: {
+      names: ["Btl", "Btr", "Bbr", "Bbl", "Ftl", "Ftr", "Fbr", "Fbl"],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [4, 5],
+        [5, 6],
+        [6, 7],
+        [7, 4],
+      ],
+      advanced: {
+        test_time: 60,
+      },
+    },
+  
+    jumpingjack: {},
+    jump: {
+      names: [
+        "Btl",
+        "Btr",
+        "Bbr",
+        "Bbl",
+        "Ftl",
+        "Ftr",
+        "Fbr",
+        "Fbl",
+        "l0s",
+        "l0e",
+        "l1s",
+        "l1e",
+        "l2s",
+        "l2e",
+        "l3s",
+        "l3e",
+        "l4s",
+        "l4e",
+        "l5s",
+        "l5e",
+        "l6s",
+        "l6e",
+        "h0s",
+        "h0e",
+        "h1s",
+        "h1e",
+        "h2s",
+        "h2e",
+        "h3s",
+        "h3e",
+        "h4s",
+        "h4e",
+        "h5s",
+        "h5e",
+        "h6s",
+        "h6e",
+      ],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [4, 5],
+        [5, 6],
+        [6, 7],
+        [7, 4],
+        [8, 9],
+        [10, 11],
+        [12, 13],
+        [14, 15],
+        [16, 17],
+        [18, 19],
+        [20, 21],
+        [22, 23],
+        [24, 25],
+        [26, 27],
+        [28, 29],
+        [30, 31],
+        [32, 33],
+        [34, 35],
+      ],
+      advanced: {
+        cam_param_path: "",
+        flag_calibrate: 0,
+        flag_check_stline: 0,
+        flag_check_person: 1,
+        flag_check_singleleg_jump: 0,
+      },
+      reserved: {
+        distance_ground_between_lines: 50,
+        length_ground_line: 80,
+        distance_air_between_lines: 50,
+        length_air_line: 50,
+      },
+    },
+  
+    longjump: {
+      names: [
+        "Btl",
+        "Btr",
+        "Bbr",
+        "Bbl",
+        "Ftl",
+        "Ftr",
+        "Fbr",
+        "Fbl",
+        "l0s",
+        "l0e",
+        "l1s",
+        "l1e",
+        "l2s",
+        "l2e",
+        "l3s",
+        "l3e",
+        "l4s",
+        "l4e",
+        "l5s",
+        "l5e",
+        "l6s",
+        "l6e",
+        "h0s",
+        "h0e",
+        "h1s",
+        "h1e",
+        "h2s",
+        "h2e",
+        "h3s",
+        "h3e",
+        "h4s",
+        "h4e",
+        "h5s",
+        "h5e",
+        "h6s",
+        "h6e",
+      ],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [4, 5],
+        [5, 6],
+        [6, 7],
+        [7, 4],
+        [8, 9],
+        [10, 11],
+        [12, 13],
+        [14, 15],
+        [16, 17],
+        [18, 19],
+        [20, 21],
+        [22, 23],
+        [24, 25],
+        [26, 27],
+        [28, 29],
+        [30, 31],
+        [32, 33],
+        [34, 35],
+      ],
+      required: {
+        distance_maximum: 800,
+      },
+      advanced: {
+        cam_param_path: "",
+        flag_calibrate: 0,
+        flag_check_stline: 0,
+        flag_check_person: 1,
+      },
+      reserved: {
+        distance_ground_between_lines: 50,
+        length_ground_line: 80,
+        distance_air_between_lines: 50,
+        length_air_line: 50,
+      },
+    },
+  
+    verticaljump: {
+      // 这个要配置【基础设置】才会有显示
+      required: {
+        cam_path: "",
+        anchor_path: "",
+        cam_fps: 25,
+        cam_param_path: "1",
+        flag_calibrate: 1,
+        height_maximum: 300,
+      },
+      advanced: {},
+      reserved: {
+        flag_save: 1,
+        flag_display: 0,
+        cam_face_path: "",
+        anchor_face_path: "",
+        cam_face_fps: 25,
+        height_air_between_lines: 50,
+      },
+    },
+  
+    trijump: {
+      names: [
+        "Btl",
+        "Btr",
+        "Bbr",
+        "Bbl",
+        "Ftl",
+        "Ftr",
+        "Fbr",
+        "Fbl",
+        "l0s",
+        "l0e",
+        "l1s",
+        "l1e",
+        "l2s",
+        "l2e",
+        "l3s",
+        "l3e",
+        "l4s",
+        "l4e",
+        "l5s",
+        "l5e",
+        "l6s",
+        "l6e",
+        "l7s",
+        "l7e",
+        "l8s",
+        "l8e",
+        "l9s",
+        "l9e",
+        "l10s",
+        "l10e",
+        "l11s",
+        "l11e",
+        "l12s",
+        "l12e",
+        "l13s",
+        "l13e",
+        "l14s",
+        "l14e",
+        "l15s",
+        "l15e",
+        "l16s",
+        "l16e",
+        "l17s",
+        "l17e",
+        "l18s",
+        "l18e",
+        "h0s",
+        "h0e",
+        "h2s",
+        "h2e",
+        "h4s",
+        "h4e",
+        "h6s",
+        "h6e",
+        "h8s",
+        "h8e",
+        "h10s",
+        "h10e",
+        "h12s",
+        "h12e",
+        "h14s",
+        "h14e",
+        "h16s",
+        "h16e",
+        "h18s",
+        "h18e",
+      ],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [4, 5],
+        [5, 6],
+        [6, 7],
+        [7, 4],
+        [8, 9],
+        [10, 11],
+        [12, 13],
+        [14, 15],
+        [16, 17],
+        [18, 19],
+        [20, 21],
+        [22, 23],
+        [24, 25],
+        [26, 27],
+        [28, 29],
+        [30, 31],
+        [32, 33],
+        [34, 35],
+        [36, 37],
+        [38, 39],
+        [40, 41],
+        [42, 43],
+        [44, 45],
+        [46, 47],
+        [48, 49],
+        [50, 51],
+        [52, 53],
+        [54, 55],
+        [56, 57],
+        [58, 59],
+        [60, 61],
+        [62, 63],
+        [64, 65],
+      ],
+      advanced: {
+        cam_param_path: "",
+        flag_calibrate: 0,
+        flag_check_stline: 0,
+        flag_check_person: 1,
+        cam_face_path: "",
+        anchor_face_path: "",
+        cam_face_fps: 25,
+      },
+      reserved: {
+        flag_save: 1,
+        flag_display: 0,
+        distance_ground_between_lines: 50,
+        length_ground_line: 80,
+        distance_air_between_lines: 100,
+        length_air_line: 50,
+      },
+    },
+  
+    solidball: {
+      names: [
+        "Btl",
+        "Btr",
+        "Bbr",
+        "Bbl",
+        "Ftl",
+        "Ftr",
+        "Fbr",
+        "Fbl",
+        "Stl",
+        "Str",
+        "Sbr",
+        "Sbl",
+        "Ctl",
+        "Ctr",
+        "Cbr",
+        "Cbl",
+        "l0s",
+        "l0e",
+        "l1s",
+        "l1e",
+        "l2s",
+        "l2e",
+        "l3s",
+        "l3e",
+        "l4s",
+        "l4e",
+        "l5s",
+        "l5e",
+        "l6s",
+        "l6e",
+        "l7s",
+        "l7e",
+        "l8s",
+        "l8e",
+        "l9s",
+        "l9e",
+        "l10s",
+        "l10e",
+        "l11s",
+        "l11e",
+        "l12s",
+        "l12e",
+        "l13s",
+        "l13e",
+        "l14s",
+        "l14e",
+        "l15s",
+        "l15e",
+        "l16s",
+        "l16e",
+        "l17s",
+        "l17e",
+        "l18s",
+        "l18e",
+        "l19s",
+        "l19e",
+        "l20s",
+        "l20e",
+        "l21s",
+        "l21e",
+        "l22s",
+        "l22e",
+        "l23s",
+        "l23e",
+        "l24s",
+        "l24e",
+        "l25s",
+        "l25e",
+        "l26s",
+        "l26e",
+        "l27s",
+        "l27e",
+        "l28s",
+        "l28e",
+        "l29s",
+        "l29e",
+        "l30s",
+        "l30e",
+        "h0s",
+        "h0m",
+        "h0e",
+        "h1s",
+        "h1m",
+        "h1e",
+        "h2s",
+        "h2m",
+        "h2e",
+      ],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [4, 5],
+        [5, 6],
+        [6, 7],
+        [7, 4],
+        [8, 9],
+        [9, 10],
+        [10, 11],
+        [11, 8],
+        [12, 13],
+        [13, 14],
+        [14, 15],
+        [15, 12],
+        [16, 17],
+        [18, 19],
+        [20, 21],
+        [22, 23],
+        [24, 25],
+        [26, 27],
+        [28, 29],
+        [30, 31],
+        [32, 33],
+        [34, 35],
+        [36, 37],
+        [38, 39],
+        [40, 41],
+        [42, 43],
+        [44, 45],
+        [46, 47],
+        [48, 49],
+        [50, 51],
+        [52, 53],
+        [54, 55],
+        [56, 57],
+        [58, 59],
+        [60, 61],
+        [62, 63],
+        [64, 65],
+        [66, 67],
+        [68, 69],
+        [70, 71],
+        [72, 73],
+        [74, 75],
+        [76, 77],
+        [78, 79],
+        [79, 80],
+        [81, 82],
+        [82, 83],
+        [84, 85],
+        [85, 86],
+      ],
+      required: {
+        cam_param_path: "",
+        flag_calibrate: 1,
+        distance_ground: "",
+      },
+      advanced: {
+        flag_check_stline: 0,
+        distance_ground_between_lines: 50,
+        cam_face_path: "",
+        anchor_face_path: "",
+        cam_face_fps: 25,
+      },
+      reserved: {
+        length_ground_line: 80,
+      },
+    },
+  
+    shotput: {
+      names: [
+        "Btl",
+        "Btr",
+        "Bbr",
+        "Bbl",
+        "Ftl",
+        "Ftr",
+        "Fbr",
+        "Fbl",
+        "Stl",
+        "Str",
+        "Sbr",
+        "Sbl",
+        "Ctl",
+        "Ctr",
+        "Cbr",
+        "Cbl",
+        "l0s",
+        "l0e",
+        "l1s",
+        "l1e",
+        "l2s",
+        "l2e",
+        "l3s",
+        "l3e",
+        "l4s",
+        "l4e",
+        "l5s",
+        "l5e",
+        "l6s",
+        "l6e",
+        "l7s",
+        "l7e",
+        "l8s",
+        "l8e",
+        "l9s",
+        "l9e",
+        "l10s",
+        "l10e",
+        "l11s",
+        "l11e",
+        "l12s",
+        "l12e",
+        "l13s",
+        "l13e",
+        "l14s",
+        "l14e",
+        "l15s",
+        "l15e",
+        "l16s",
+        "l16e",
+        "l17s",
+        "l17e",
+        "l18s",
+        "l18e",
+        "l19s",
+        "l19e",
+        "l20s",
+        "l20e",
+        "l21s",
+        "l21e",
+        "l22s",
+        "l22e",
+        "l23s",
+        "l23e",
+        "l24s",
+        "l24e",
+        "l25s",
+        "l25e",
+        "l26s",
+        "l26e",
+        "l27s",
+        "l27e",
+        "l28s",
+        "l28e",
+        "l29s",
+        "l29e",
+        "l30s",
+        "l30e",
+        "h0s",
+        "h0m",
+        "h0e",
+        "h1s",
+        "h1m",
+        "h1e",
+        "h2s",
+        "h2m",
+        "h2e",
+        "O",
+        "B0",
+        "B1",
+      ],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [4, 5],
+        [5, 6],
+        [6, 7],
+        [7, 4],
+        [8, 9],
+        [9, 10],
+        [10, 11],
+        [11, 8],
+        [12, 13],
+        [13, 14],
+        [14, 15],
+        [15, 12],
+        [16, 17],
+        [18, 19],
+        [20, 21],
+        [22, 23],
+        [24, 25],
+        [26, 27],
+        [28, 29],
+        [30, 31],
+        [32, 33],
+        [34, 35],
+        [36, 37],
+        [38, 39],
+        [40, 41],
+        [42, 43],
+        [44, 45],
+        [46, 47],
+        [48, 49],
+        [50, 51],
+        [52, 53],
+        [54, 55],
+        [56, 57],
+        [58, 59],
+        [60, 61],
+        [62, 63],
+        [64, 65],
+        [66, 67],
+        [68, 69],
+        [70, 71],
+        [72, 73],
+        [74, 75],
+        [76, 77],
+        [78, 79],
+        [79, 80],
+        [81, 82],
+        [82, 83],
+        [84, 85],
+        [85, 86],
+        [87, 88],
+        [87, 89],
+      ],
+      required: {
+        cam_param_path: "",
+        flag_calibrate: 1,
+        distance_ground: "",
+      },
+      advanced: {
+        flag_check_stline: 0,
+  
+        cam_face_path: "",
+        anchor_face_path: "",
+        cam_face_fps: 25,
+        distance_ground_between_lines: 50,
+      },
+      reserved: {
+        length_ground_line: 80,
+      },
+    },
+  
+    shortrun_st: {
+      names: [
+        "Btl",
+        "Btr",
+        "Bbr",
+        "Bbl",
+        "Ftl",
+        "Ftr",
+        "Fbr",
+        "Fbl",
+        "c0",
+        "c1",
+        "c2",
+        "c3",
+        "c4",
+        "c5",
+        "c6",
+        "c7",
+        "c8",
+        "c9",
+      ],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [4, 5],
+        [5, 6],
+        [6, 7],
+        [7, 4],
+      ],
+    },
+    shortrun: {
+      names: [
+        "Btl",
+        "Btr",
+        "Bbr",
+        "Bbl",
+        "",
+        "",
+        "",
+        "",
+        "l00",
+        "l01",
+        "l02",
+        "l10",
+        "l11",
+        "l12",
+        "l20",
+        "l21",
+        "l22",
+        "l30",
+        "l31",
+        "l32",
+        "l40",
+        "l41",
+        "l42",
+        "l50",
+        "l51",
+        "l52",
+        "l60",
+        "l61",
+        "l62",
+        "l70",
+        "l71",
+        "l72",
+        "l80",
+        "l81",
+        "l82",
+        "l90",
+        "l91",
+        "l92",
+      ],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [8, 9],
+        [9, 10],
+        [11, 12],
+        [12, 13],
+        [14, 15],
+        [15, 16],
+        [17, 18],
+        [18, 19],
+        [20, 21],
+        [21, 22],
+        [23, 24],
+        [24, 25],
+        [26, 27],
+        [27, 28],
+        [29, 30],
+        [30, 31],
+        [32, 33],
+        [33, 34],
+        [35, 36],
+        [36, 37],
+      ],
+      required: {
+        cam_face_path: "",
+        anchor_face_path: "",
+        cam_face_fps: 25,
+        distance_per_round: 400,
+        num_of_tracks: "",
+        delay_cam_second: 0.45,
+        delay_cmd_second: 0.2,
+      },
+      advanced: {},
+      reserved: {},
+    },
+    longrun_st: {
+      names: ["Btl", "Btr", "Bbr", "Bbl", "Ftl", "Ftr", "Fbr", "Fbl"],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [4, 5],
+        [5, 6],
+        [6, 7],
+        [7, 4],
+      ],
+    },
+    longrun: {
+      names: [
+        "Btl",
+        "Btr",
+        "Bbr",
+        "Bbl",
+        "",
+        "",
+        "",
+        "",
+        "l00",
+        "l01",
+        "l02",
+        "l10",
+        "l11",
+        "l12",
+        "l20",
+        "l21",
+        "l22",
+        "l30",
+        "l31",
+        "l32",
+        "l40",
+        "l41",
+        "l42",
+        "l50",
+        "l51",
+        "l52",
+        "l60",
+        "l61",
+        "l62",
+        "l70",
+        "l71",
+        "l72",
+        "l80",
+        "l81",
+        "l82",
+        "l90",
+        "l91",
+        "l92",
+      ],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [8, 9],
+        [9, 10],
+        [11, 12],
+        [12, 13],
+        [14, 15],
+        [15, 16],
+        [17, 18],
+        [18, 19],
+        [20, 21],
+        [21, 22],
+        [23, 24],
+        [24, 25],
+        [26, 27],
+        [27, 28],
+        [29, 30],
+        [30, 31],
+        [32, 33],
+        [33, 34],
+        [35, 36],
+        [36, 37],
+      ],
+      required: {
+        cam_face_path: "",
+        anchor_face_path: "",
+        cam_0_path: "",
+        anchor_0_path: "",
+        cam_face_fps: 25,
+        distance_per_round: 400,
+        num_of_tracks_startline: "",
+        num_of_tracks_endline: "",
+        delay_cam_second: 0.45,
+        delay_cmd_second: 0.2,
+      },
+      advanced: {},
+      reserved: {},
+    },
+    backrun_st: {
+      names: [
+        "Btl",
+        "Btr",
+        "Bbr",
+        "Bbl",
+        "Ftl",
+        "Ftr",
+        "Fbr",
+        "Fbl",
+        "c0",
+        "c1",
+        "c2",
+        "c3",
+        "c4",
+        "c5",
+        "c6",
+        "c7",
+        "c8",
+        "c9",
+      ],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [4, 5],
+        [5, 6],
+        [6, 7],
+        [7, 4],
+      ],
+    },
+    backrun_mid: {
+      names: [
+        "Btl",
+        "Btr",
+        "Bbr",
+        "Bbl",
+        "",
+        "",
+        "",
+        "",
+        "l00",
+        "l01",
+        "l02",
+        "l10",
+        "l11",
+        "l12",
+        "l20",
+        "l21",
+        "l22",
+        "l30",
+        "l31",
+        "l32",
+        "l40",
+        "l41",
+        "l42",
+        "l50",
+        "l51",
+        "l52",
+        "l60",
+        "l61",
+        "l62",
+        "l70",
+        "l71",
+        "l72",
+        "l80",
+        "l81",
+        "l82",
+        "l90",
+        "l91",
+        "l92",
+      ],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [8, 9],
+        [9, 10],
+        [11, 12],
+        [12, 13],
+        [14, 15],
+        [15, 16],
+        [17, 18],
+        [18, 19],
+        [20, 21],
+        [21, 22],
+        [23, 24],
+        [24, 25],
+        [26, 27],
+        [27, 28],
+        [29, 30],
+        [30, 31],
+        [32, 33],
+        [33, 34],
+        [35, 36],
+        [36, 37],
+      ],
+    },
+    backrun: {
+      names: [
+        "Btl",
+        "Btr",
+        "Bbr",
+        "Bbl",
+        "",
+        "",
+        "",
+        "",
+        "l00",
+        "l01",
+        "l02",
+        "l10",
+        "l11",
+        "l12",
+        "l20",
+        "l21",
+        "l22",
+        "l30",
+        "l31",
+        "l32",
+        "l40",
+        "l41",
+        "l42",
+        "l50",
+        "l51",
+        "l52",
+        "l60",
+        "l61",
+        "l62",
+        "l70",
+        "l71",
+        "l72",
+        "l80",
+        "l81",
+        "l82",
+        "l90",
+        "l91",
+        "l92",
+      ],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [8, 9],
+        [9, 10],
+        [11, 12],
+        [12, 13],
+        [14, 15],
+        [15, 16],
+        [17, 18],
+        [18, 19],
+        [20, 21],
+        [21, 22],
+        [23, 24],
+        [24, 25],
+        [26, 27],
+        [27, 28],
+        [29, 30],
+        [30, 31],
+        [32, 33],
+        [33, 34],
+        [35, 36],
+        [36, 37],
+      ],
+      required: {
+        cam_face_path: "",
+        anchor_face_path: "",
+        cam_0_path: "",
+        anchor_0_path: "",
+        cam_face_fps: 25,
+        distance_per_round: 400,
+        num_of_tracks: "",
+        delay_cam_second: 0.45,
+        delay_cmd_second: 0.2,
+      },
+      advanced: {},
+      reserved: {},
+    },
+    footballv1: {
+      names: [
+        "Btl",
+        "Btr",
+        "Bbr",
+        "Bbl",
+        "",
+        "",
+        "",
+        "",
+        "l00",
+        "l01",
+        "l02",
+        "l10",
+        "l11",
+        "l12",
+      ],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [8, 9],
+        [9, 10],
+        [11, 12],
+        [12, 13],
+      ],
+      required: {
+        cam_face_path: "",
+        anchor_face_path: "",
+        cam_face_fps: 25,
+        delay_cam_second: 0.45,
+        delay_cmd_second: 0.2,
+      },
+    },
+    snakerun: {
+      names: [
+        "Btl",
+        "Btr",
+        "Bbr",
+        "Bbl",
+        "",
+        "",
+        "",
+        "",
+        "l00",
+        "l01",
+        "l02",
+        "l10",
+        "l11",
+        "l12",
+      ],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [8, 9],
+        [9, 10],
+        [11, 12],
+        [12, 13],
+      ],
+      required: {
+        cam_face_path: "",
+        anchor_face_path: "",
+        cam_face_fps: 25,
+        delay_cam_second: 0.45,
+        delay_cmd_second: 0.2,
+      },
+    },
+    basketballv1: {
+      names: [
+        "Btl",
+        "Btr",
+        "Bbr",
+        "Bbl",
+        "",
+        "",
+        "",
+        "",
+        "l00",
+        "l01",
+        "l02",
+        "l10",
+        "l11",
+        "l12",
+      ],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [8, 9],
+        [9, 10],
+        [11, 12],
+        [12, 13],
+      ],
+      required: {
+        cam_face_path: "",
+        anchor_face_path: "",
+        cam_face_fps: 25,
+        delay_cam_second: 0.45,
+        delay_cmd_second: 0.2,
+      },
+    },
+    basictime: {
+      required: {
+        cam_face_path: "",
+        anchor_face_path: "",
+        cam_face_fps: 25,
+        delay_cam_second: 0.45,
+        delay_cmd_second: 0.2,
+      },
+    },
+    basicface: {
+      required: {
+        cam_face_path: "",
+        anchor_face_path: "",
+        cam_face_fps: 25,
+      },
+    },
+  
+    face: {
+      names: ["Btl", "Btr", "Bbr", "Bbl", "Ftl", "Ftr", "Fbr", "Fbl"],
+      lines: [
+        [0, 1],
+        [1, 2],
+        [2, 3],
+        [3, 0],
+        [4, 5],
+        [5, 6],
+        [6, 7],
+        [7, 4],
+      ],
+    },
+  },
 };
 export default data;

+ 6 - 4
src/utils/index.ts

@@ -34,11 +34,12 @@ let utils = {
     //格式化数据
     let list = [];
     for (let key in data) {
-      let arrList = key.split('@@');
-      let newArr = arrList[0].split('_');
+      // let arrList = key.split('@@');
+      let newArr = key.split('_');
       newArr.push(data[key]);
       list.push(newArr);
     }
+    console.log("list",list)
     //重组数组
     let newList = [];
     let project: any = dataDictionary.project;
@@ -46,10 +47,11 @@ let utils = {
       let area: any = [];
       list.forEach((item) => {
         if (key == item[0]) {
+          let arrList = item[1].split('@@');
           area.push({
             key: item[1],
-            name: item[1],
-            value: item[2]
+            name: arrList[0],
+            value: item[2],
           });
         }
       });

+ 1 - 1
src/utils/trainWs.ts

@@ -299,7 +299,7 @@ export function useWs() {
       'device_starts',
       {
         data: 'start_' + examId,
-        class_id: parameter.classes,
+        class_id: 1,
         exam_type: parameter.standard,
         etime: testTime
       },

+ 88 - 90
src/views/set/config.vue

@@ -10,8 +10,7 @@
           <template #default="scope">
             <el-button @click="chooseItem(scope.row)" size="small" type="primary" plain>部署</el-button>
             <el-button @click="delExamConf(scope.row)" size="small" type="danger" plain>删除</el-button>
-            <el-button @click="closeExam(scope.row)" size="small" type="warning" plain v-show="scope.row[1] == '已开启'">下课
-            </el-button>
+            <el-button @click="closeExam(scope.row)" size="small" type="warning" plain v-show="scope.row[1] == '已开启'">下课 </el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -28,7 +27,7 @@
 </template>
 
 <script setup name="Login" lang="ts">
-import dataDictionary from "@/utils/dataDictionary"
+import dataDictionary from '@/utils/dataDictionary';
 const { proxy } = getCurrentInstance() as any;
 const router = useRouter();
 const dic: any = dataDictionary;
@@ -37,12 +36,12 @@ const workstationWindowRef = ref();
 const data = reactive<any>({
   examList: [],
   selectValue: [],
-  loginname: "",
+  loginname: '',
   workers: {},
   workerMap: [],
   oLWorkers: {},
   examMaxIdxMap: {},
-  existExamIds: {},
+  existExamIds: {}
 });
 
 const { examList, selectValue, loginname, workers, workerMap, oLWorkers, examMaxIdxMap, existExamIds } = toRefs(data);
@@ -54,46 +53,46 @@ const handleSelectionChange = (data: any) => {
 
 //获取列表
 const getExamList = async () => {
-  let myInfo: any = localStorage.getItem("userInfo");
+  let myInfo: any = localStorage.getItem('userInfo');
   let userInfo = JSON.parse(myInfo);
   let params: any = {
     school_id: userInfo.school_id
   };
-  let res = await proxy?.$http.common.allExams(params)
-  workers.value = res.workers
+  let res = await proxy?.$http.common.allExams(params);
+  workers.value = res.workers;
   Object.keys(res.workers).forEach((item) => {
-    workerMap.value[res.workers[item][0]] = item
-    let wkVal = workers.value[item]
-    if (wkVal[1] === "在线") {
-      oLWorkers.value[item] = wkVal
+    workerMap.value[res.workers[item][0]] = item;
+    let wkVal = workers.value[item];
+    if (wkVal[1] === '在线') {
+      oLWorkers.value[item] = wkVal;
     }
-  })
-  loginname.value = res.login_name
+  });
+  loginname.value = res.login_name;
   let list = [];
   for (let i in res.exams) {
-    if (i.indexOf("skiprope") != 0 && i.indexOf("heartbeat") != 0) {
-      let enIndex = i.split('_')
-      let en = enIndex[0]
-      let eindex = enIndex[1]
+    if (i.indexOf('skiprope') != 0 && i.indexOf('heartbeat') != 0) {
+      let enIndex = i.split('_');
+      let en = enIndex[0];
+      let eindex = enIndex[1];
       res.exams[i].worker.forEach((worker: any) => {
-        if (worker in examMaxIdxMap.value) { }
-        else { examMaxIdxMap.value[worker] = {} }
+        if (worker in examMaxIdxMap.value) {
+        } else {
+          examMaxIdxMap.value[worker] = {};
+        }
         if (en in examMaxIdxMap.value[worker]) {
           if (Number(eindex) > Number(examMaxIdxMap.value[worker][en])) {
-            examMaxIdxMap.value[worker][en] = Number(eindex)
+            examMaxIdxMap.value[worker][en] = Number(eindex);
           }
         } else {
-          examMaxIdxMap.value[worker][en] = Number(eindex)
+          examMaxIdxMap.value[worker][en] = Number(eindex);
         }
         if (worker in existExamIds.value) {
-          existExamIds.value[worker].push(i)
+          existExamIds.value[worker].push(i);
         } else {
-          existExamIds.value[worker] = []
+          existExamIds.value[worker] = [];
         }
-      })
-      list.push(
-        [dic.project[en] + "_" + eindex, res.exams[i].isopen,
-        res.exams[i].worker, i])
+      });
+      list.push([dic.project[en] + '_' + eindex, res.exams[i].isopen, res.exams[i].worker, i]);
     }
   }
   examList.value = list;
@@ -101,7 +100,7 @@ const getExamList = async () => {
 
 //转换
 const showWorkers = (row: any) => {
-  return row[2].join(",")
+  return row[2].join(',');
 };
 
 //操作工作站
@@ -112,16 +111,19 @@ const openWorkstation = (type: any, title: any) => {
 //部署
 const chooseItem = (item: any) => {
   // let itemName = this.exams[item]
-  let isOn = false
+  let isOn = false;
   item[2].forEach((wkId: any) => {
     if (workers.value[wkId]) {
-      if (workers.value[wkId][1] == "在线") {
-        isOn = true
+      if (workers.value[wkId][1] == '在线') {
+        isOn = true;
       }
     }
-  })
+  });
   if (isOn) {
-
+    router.push({
+      path: '/set/deploy',
+      query: { examId: item[3] }
+    });
   } else {
     proxy?.$modal.msgError(`工作站不在线暂不允许配置`);
   }
@@ -129,92 +131,88 @@ const chooseItem = (item: any) => {
 
 //删除项目
 const delExamConf = (item: any) => {
-  let conTitle = `是否移除项目-${item[0]}`
-  proxy?.$modal.confirm(conTitle).then(() => {
-    let examId = item[3]
-    let ename = examId.split("_")[0]
-    let eidx = Number(examId.split("_")[1])
+  let conTitle = `是否移除项目-${item[0]}`;
+  proxy?.$modal
+    .confirm(conTitle)
+    .then(() => {
+      let examId = item[3];
+      let ename = examId.split('_')[0];
+      let eidx = Number(examId.split('_')[1]);
 
-    let myInfo: any = localStorage.getItem("userInfo");
-    let userInfo = JSON.parse(myInfo);
-    let params: any = {
-      exam_id: examId,
-      school_id: userInfo.school_id
-    };
-    proxy?.$http.common.delExamSettings(
-      params
-    ).then((res: any) => {
-      if (res.status == 200) {
-        let selWorker: any;
-        let existIdx: any;
-        item[2].forEach((worker: any) => {
-          existIdx = existExamIds.value[worker].indexOf(examId)
-          if (examMaxIdxMap.value[worker][ename] == eidx) {
-            examMaxIdxMap.value[worker][ename] = eidx - 1
-            selWorker = worker
-          } else if (existIdx > -1) {
-            selWorker = worker
+      let myInfo: any = localStorage.getItem('userInfo');
+      let userInfo = JSON.parse(myInfo);
+      let params: any = {
+        exam_id: examId,
+        school_id: userInfo.school_id
+      };
+      proxy?.$http.common.delExamSettings(params).then((res: any) => {
+        if (res.status == 200) {
+          let selWorker: any;
+          let existIdx: any;
+          item[2].forEach((worker: any) => {
+            existIdx = existExamIds.value[worker].indexOf(examId);
+            if (examMaxIdxMap.value[worker][ename] == eidx) {
+              examMaxIdxMap.value[worker][ename] = eidx - 1;
+              selWorker = worker;
+            } else if (existIdx > -1) {
+              selWorker = worker;
+            }
+          });
+          console.log(selWorker, examMaxIdxMap.value, 'examMaxIdxMap.value');
+          // let existIdx = existExamIds.value[selWorker].indexOf(examId)
+          let index = examList.value.indexOf(item);
+          if (existIdx > -1) {
+            existExamIds.value[selWorker].splice(existIdx, 1);
           }
-        })
-        console.log(selWorker, examMaxIdxMap.value, "examMaxIdxMap.value")
-        // let existIdx = existExamIds.value[selWorker].indexOf(examId)
-        let index = examList.value.indexOf(item)
-        if (existIdx > -1) {
-          existExamIds.value[selWorker].splice(existIdx, 1)
-        }
-        if (index > -1) {
-          examList.value.splice(index, 1)
+          if (index > -1) {
+            examList.value.splice(index, 1);
+          }
+          proxy?.$modal.msgSuccess(`删除成功`);
+        } else {
+          proxy?.$modal.msgError(`${res.message}`);
         }
-        proxy?.$modal.msgSuccess(`删除成功`);
-      } else {
-        proxy?.$modal.msgError(`${res.message}`);
-      }
-
+      });
     })
-  }).catch((err: any) => {
-  });
+    .catch((err: any) => {});
 };
 
 // 下课
 const closeExam = (item: any) => {
-  let myInfo: any = localStorage.getItem("userInfo");
+  let myInfo: any = localStorage.getItem('userInfo');
   let userInfo = JSON.parse(myInfo);
   let params: any = {
     exam_id: item[3],
     school_id: userInfo.school_id
   };
-  proxy?.$http.common.closeExamForce(
-    params
-  ).then((res: any) => {
+  proxy?.$http.common.closeExamForce(params).then((res: any) => {
     if (res.status == 200) {
       proxy?.$modal.msgSuccess(`下课成功`);
     }
-    getExamList()
-  })
+    getExamList();
+  });
 };
 
-
 /**
  * 返回刷新列表
-*/
+ */
 const returnRefresh = () => {
   getExamList();
 };
 
 /**
  * 返回
-*/
+ */
 const confirmExit = () => {
   router.go(-1);
 };
 
 onMounted(() => {
   getExamList();
-})
+});
 
 onActivated(() => {
-  getExamList()
-})
+  getExamList();
+});
 </script>
 
 <style lang="scss" scoped>
@@ -237,7 +235,7 @@ $waiPadding: 6.51rem;
 
 .footerBtn {
   width: 100%;
-  padding: 0 calc(13.02rem /2);
+  padding: 0 calc(13.02rem / 2);
   box-sizing: border-box;
   position: fixed;
   bottom: 3vh;
@@ -249,16 +247,16 @@ $waiPadding: 6.51rem;
     height: 6.1vh;
     line-height: 6.1vh;
     font-size: 3vh;
-    color: #1A293A;
+    color: #1a293a;
     text-align: center;
     border-radius: 1vh;
     cursor: pointer;
-    background: radial-gradient(159% 126% at 5% 93%, #8EFFA9 0%, #07FFE7 100%);
+    background: radial-gradient(159% 126% at 5% 93%, #8effa9 0%, #07ffe7 100%);
     box-shadow: 3px 6px 4px 1px rgba(0, 0, 0, 0.1874), inset 0px 1px 0px 2px rgba(255, 255, 255, 0.3);
     margin-left: 10px;
 
     &:hover {
-      background: #8EFFA9;
+      background: #8effa9;
     }
   }
 }

+ 3146 - 0
src/views/set/deploy.vue

@@ -0,0 +1,3146 @@
+<template>
+  <div>
+    <el-dialog
+      :title="examId"
+      center
+      width="68%"
+      top="8vh"
+      height="60%"
+      destroy-on-close
+      fullscreen
+      :visible.sync="showPlayer"
+      :modal="false"
+      :before-close="handlBeforeClose"
+      custom-class="el-dialog-body"
+      @close="handlePlayerClose"
+    >
+      <p>播放源地址:{{ viewCamurl }}</p>
+      <div v-show="anchorFlag && !caliFlag" style="display: flex; margin-top: 10px;">
+        <el-button type="primary" size="mini" style="" @click="startAnchor()">开始布点</el-button>
+
+        <el-button type="primary" size="mini" style="margin-left:5px;" :disabled="btnDis" :loading="btnDis" @click="lastAnchor()"
+          >上次布点
+        </el-button>
+        <el-upload action="#" :before-upload="loadAnchorFile" accept=".json" :show-file-list="false" style="margin-left:5px;">
+          <el-button size="mini" type="primary">加载布点</el-button>
+        </el-upload>
+        <!-- <el-button @onclick="" size="mini" type="primary" style="margin-left: 5px;">重置布点</el-button> -->
+        <el-button type="primary" size="mini" style="margin-left: 5px;" @click="saveAnchor()"> 保存 </el-button>
+      </div>
+      <br />
+      <canvas id="canvas" width="0" height="0" style="background-color:black;" />
+      <div
+        v-loading="!anchorFlag"
+        element-loading-text="请稍等几秒"
+        element-loading-spinner="el-icon-loading"
+        element-loading-background="rgba(0, 0, 0, 0.8)"
+      >
+        <video id="videoflv" muted style="height:720px;background-color:black;" controls autoplay />
+      </div>
+    </el-dialog>
+
+    <el-dialog :key="snapTime" :title="picTitle" center fullscreen destroy-on-close :visible.sync="showSnapPics">
+      <p>
+        布点图最后更新:<b> {{ snapTime }}</b>
+        <!--  布点文件最后更新:<b>{{anchorUptime[camIndex+'_error'] || anchorUptime[camIndex]}}</b> -->
+        <!--  矫正文件最后更新:<b>{{camparamUptime[camIndex + '_error']|| camparamUptime[camIndex] || '未上传' }} </b> -->
+      </p>
+      <div style="display:flex; margin-top:5px;">
+        <el-image id="snapPic" :src="snapPic" style="width:100%;" class="sanPicShowDia" lazy>
+          <div slot="error" class="image-slot">
+            <i class="el-icon-picture-outline" />
+            <p>抱歉,布点文件生成失败,请稍后点击右侧刷新按钮重试。</p>
+          </div>
+        </el-image>
+        <div style="display: flex; flex-direction: column; align-items: flex-end; margin-left: 1px;">
+          <el-button type="success" size="mini" style="" @click="showSnap(0, camIndex, 'snapshot')"> 刷新布点 </el-button>
+          <el-button type="primary" size="mini" style="margin-top: 5px;" @click="upAndDownPFile(camIndex, 'json')">下载布点 </el-button>
+          <el-button v-if="camparamUptime[camIndex]" type="primary" size="mini" style="margin-top: 5px;" @click="upAndDownPFile(camIndex, 'yml')"
+            >下载矫正
+          </el-button>
+        </div>
+      </div>
+    </el-dialog>
+    <el-container style="background: #ecf3f9;">
+      <el-header class="navbar">
+        <div class="nav1">
+          <div style="font-size: 20px; bold; font-weight: bold;">
+            <div @click="$router.push('/set/config')">返回</div>
+
+          </div>
+          <h3 style="font-weight:bold;">{{ examCN }}</h3>
+          <div>
+            <el-button type="primary" size="small" @click="submitForm('ruleForm', 0)">保存</el-button>
+            <!-- <el-button type="primary" @click="submitForm('ruleForm', 1)">保存并重启</el-button> -->
+          </div>
+        </div>
+      </el-header>
+      <el-main class="container-main">
+        <div class="">
+          <el-form
+            ref="ruleForm"
+            :model="ruleForm"
+            :rules="rules"
+            label-width="122px"
+            style="font-size:18px;letter-spacing:0;word-spacing:0"
+            label-position="left"
+            size="small"
+          >
+            <el-collapse v-model="activeNames">
+              <el-collapse-item style="margin-left:-4px; color:#0067E1; font-size: 14px; " name="1">
+                <template slot="title">
+                  基础设置
+                  <!-- <i class="header-icon el-icon-info"></i> -->
+                </template>
+                <template v-for="value, item in ruleForm">
+                  <template v-if="item in examFormConf['required']">
+                    <template v-if="item === 'cam_path'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <span slot="label">
+                          {{ anchorLabelObj[item] }}
+                          <el-popover placement="right" width="width:30%" trigger="hover">
+                            <div style="height:100px; font-size:12px; margin-bottom: 20px;line-height:20px;">
+                              <p style="font-size:10px;">
+                                大华摄像头示例: <br />
+                                rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                              <p style="font-size:10px;">
+                                大华摄像头示例-h265: <br />
+                                rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0,coder=h265
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0,coder=h265', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                              <p style="font-size:10px;">
+                                海康摄像头示例: <br />
+                                rtsp://admin:tropsedu123@192.168.3.77:554/h265/ch1/main/av_stream,coder=h265
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('rtsp://admin:tropsedu123@192.168.3.77:554/h265/ch1/main/av_stream,coder=h265', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                              <p style="font-size:10px;">
+                                USB摄像头示例: <br />
+                                /dev/video0,width=1280,height=720,framerate=30
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('/dev/video0,width=1280,height=720,framerate=30', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                            </div>
+                            <i slot="reference" class="el-icon-question" />
+                          </el-popover>
+                        </span>
+
+                        <el-input v-model="ruleForm.cam_path" clearable style="width:45%; margin-right:5px;" />
+                        <span style="margin-right:4px;font-size:10px;">帧率</span>
+                        <el-input v-model.number="ruleForm.cam_fps" style="width:9.66%;margin-right:5px;" />
+                        <el-button icon="el-icon-video-camera" type="primary" size="small" @click.prevent="viewCam(1, 0, ruleForm.cam_fps, 1)"
+                          >远程布点</el-button
+                        >
+                        <el-button
+                          icon="el-icon-video-camera"
+                          type="primary"
+                          size="small"
+                          style="margin-left:5px;"
+                          :disabled="!hasLocSRS"
+                          @click.prevent="viewCam(1, 0, ruleForm.cam_fps, 3)"
+                          >本地布点</el-button
+                        >
+                      </el-form-item>
+                    </template>
+                    <template v-if="item === 'anchor_path'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-input v-model="ruleForm.anchor_path" disabled class="input45" clearable />
+                        <el-button type="primary" size="small" plain @click.prevent="showSnap(0, 1)">预览</el-button>
+                        <el-button type="primary" size="small" plain style="margin-left:5px;" @click.prevent="checkAnchorUrl('', 1)">刷新</el-button>
+                        <el-upload
+                          name="upload_file"
+                          style="display: inline-block; margin-left: 5px; margin-right:5px;"
+                          action="#"
+                          :show-file-list="false"
+                          :data="{exam_id: examId, cindex: 1, school_id: schoolId}"
+                          :action="uploadUrl"
+                          :headers="headers"
+                          :on-success="handleUpload"
+                        >
+                          <el-button type="primary" size="small" plain>上传布点</el-button>
+                        </el-upload>
+                        <el-button type="primary" size="small" plain @click.prevent="upAndDownPFile(1, 'json')">下载布点</el-button>
+                      </el-form-item>
+                    </template>
+
+                    <template v-if="item === 'flag_calibrate'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-switch v-model="ruleForm.flag_calibrate" :active-value="1" :inactive-value="0" />
+                      </el-form-item>
+                    </template>
+
+                    <template v-if="item == 'cam_param_path'">
+                      <el-collapse-transition>
+                        <div v-show="ruleForm.flag_calibrate">
+                          <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                            <el-input v-model="ruleForm.cam_param_path" disabled class="input45" />
+                            <!--  <el-button @click.prevent="viewCam(1, 1, ruleForm.cam_fps, 1)" type="primary" size="small" plain>预览</el-button> -->
+                            <el-button type="primary" size="small" style="" @click.prevent="checkAnchorUrl('', 1, 'yml')">刷新</el-button>
+                            <el-upload
+                              name="upload_file"
+                              style="display:inline-block; margin-left: 5px;margin-right:5px;"
+                              action="#"
+                              :show-file-list="false"
+                              :data="{exam_id: examId, cindex: 1, school_id: schoolId, up_type:'yml'}"
+                              :action="uploadUrl"
+                              :headers="headers"
+                              :on-success="handleUpload"
+                              ><el-button type="primary" size="small">上传矫正</el-button>
+                            </el-upload>
+                            <el-button type="primary" size="mini" @click="upAndDownPFile(1, 'yml')">下载矫正 </el-button>
+                            <el-button type="primary" size="small" @click.prevent="viewCam(1, 2, ruleForm.cam_fps, 1)">远程矫正</el-button>
+                            <el-button type="primary" size="small" :disabled="!hasLocSRS" @click.prevent="viewCam(1, 2, ruleForm.cam_fps, 4)"
+                              >本地矫正</el-button
+                            >
+                          </el-form-item>
+                        </div>
+                      </el-collapse-transition>
+                    </template>
+
+                    <template v-if="item == 'cam_0_path'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <span slot="label">
+                          {{ anchorLabelObj[item] }}
+                          <el-popover placement="right" width="width:30%" trigger="hover">
+                            <div style="height:100px; font-size:12px; margin-bottom: 20px;line-height:20px;">
+                              <p style="font-size:10px;">
+                                大华摄像头示例: <br />
+                                rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                              <p style="font-size:10px;">
+                                海康摄像头示例: <br />
+                                rtsp://admin:tropsedu123@192.168.3.77:554/h265/ch1/main/av_stream
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('rtsp://admin:tropsedu123@192.168.3.77:554/h265/ch1/main/av_stream', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                              <p style="font-size:10px;">
+                                USB摄像头示例: <br />
+                                /dev/video0,width=1280,height=720,framerate=30
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('/dev/video0,width=1280,height=720,framerate=30')"
+                                >
+                                  复制</el-link
+                                >
+                              </p>
+                            </div>
+                            <i slot="reference" class="el-icon-question" />
+                          </el-popover>
+                        </span>
+                        <el-input v-model="ruleForm.cam_0_path" style="width:45%; margin-right:5px;" />
+                        <span style="margin-right:4px; font-size:10px;">帧率</span>
+                        <el-input v-model.number="ruleForm.cam_fps" style="width:9.66%;margin-right:5px;" />
+                        <el-button icon="el-icon-video-camera" type="primary" size="small" @click.prevent="viewCam(2, 0, ruleForm.cam_fps, 1)"
+                          >远程布点</el-button
+                        >
+                        <el-button
+                          icon="el-icon-video-camera"
+                          type="primary"
+                          size="small"
+                          style="margin-left:5px;"
+                          :disabled="!hasLocSRS"
+                          @click.prevent="viewCam(2, 0, ruleForm.cam_fps, 3)"
+                          >本地布点</el-button
+                        >
+                      </el-form-item>
+                    </template>
+
+                    <template v-if="item == 'anchor_0_path'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-input v-model="ruleForm.anchor_0_path" disabled class="input45" />
+                        <el-button type="primary" size="small" plain @click.prevent="showSnap(0, 2)">预览</el-button>
+                        <!--  <el-button @click.prevent="handleDown(2)" type="primary">下载布点</el-button> -->
+                        <el-button type="primary" size="small" plain style="margin-left:5px;" @click.prevent="checkAnchorUrl('', 2)">刷新</el-button>
+                        <el-upload
+                          name="upload_file"
+                          style="display: inline; margin-left: 5px;"
+                          action="#"
+                          :data="{exam_id: examId, cindex: 2, school_id: schoolId}"
+                          :show-file-list="false"
+                          :action="uploadUrl"
+                          :headers="headers"
+                          :on-success="handleUpload"
+                        >
+                          <el-button type="primary" size="small" plain>上传布点</el-button>
+                        </el-upload>
+                      </el-form-item>
+                    </template>
+
+                    <template v-if="item == 'cam_face_path'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <span slot="label">
+                          {{ anchorLabelObj[item] }}
+                          <el-popover placement="right" width="width:30%" trigger="hover">
+                            <div style="height:100px; font-size:12px; margin-bottom: 20px;line-height:20px;">
+                              <p style="font-size:10px;">
+                                大华摄像头示例: <br />
+                                rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                              <p style="font-size:10px;">
+                                海康摄像头示例: <br />
+                                rtsp://admin:tropsedu123@192.168.3.77:554/h265/ch1/main/av_stream
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('rtsp://admin:tropsedu123@192.168.3.77:554/h265/ch1/main/av_stream', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                              <p style="font-size:10px;">
+                                USB摄像头示例: <br />
+                                /dev/video0,width=1280,height=720,framerate=30
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('/dev/video0,width=1280,height=720,framerate=30')"
+                                >
+                                  复制</el-link
+                                >
+                              </p>
+                            </div>
+                            <i slot="reference" class="el-icon-question" />
+                          </el-popover>
+                        </span>
+                        <el-input v-model="ruleForm.cam_face_path" style="width:45% ;margin-right:5px;" clearable />
+                        <span style="margin-right:4px; font-size:10px;">帧率</span>
+                        <el-input v-model.number="ruleForm.cam_face_fps" style="width:9.66%;margin-right:5px;" />
+                        <el-button icon="el-icon-video-camera" type="primary" size="small" @click.prevent="viewCam(0, 0, ruleForm.cam_face_fps, 1)"
+                          >远程布点</el-button
+                        >
+                        <el-button
+                          icon="el-icon-video-camera"
+                          type="primary"
+                          size="small"
+                          style="margin-left:5px;"
+                          :disabled="!hasLocSRS"
+                          @click.prevent="viewCam(0, 0, ruleForm.cam_face_fps, 3)"
+                          >本地布点</el-button
+                        >
+                      </el-form-item>
+                    </template>
+                    <template v-if="item == 'anchor_face_path'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-input v-model="ruleForm.anchor_face_path" disabled class="input45" />
+                        <el-button type="primary" size="small" plain @click.prevent="showSnap(0, 0)">预览</el-button>
+                        <el-button type="primary" size="small" plain style="margin-left:5px;" @click.prevent="checkAnchorUrl('', 0)">刷新</el-button>
+                        <el-upload
+                          name="upload_file"
+                          style="display: inline-block; margin-left: 5px;"
+                          action="#"
+                          :show-file-list="false"
+                          :data="{ exam_id: examId, cindex: 0, school_id: schoolId}"
+                          :action="uploadUrl"
+                          :headers="headers"
+                          :on-success="handleUpload"
+                        >
+                          <el-button type="primary" size="small" plain>上传布点</el-button>
+                        </el-upload>
+                        <el-button type="primary" size="small" plain @click.prevent="upAndDownPFile(0, 'json')">下载布点 </el-button>
+                      </el-form-item>
+                    </template>
+
+                    <template
+                      v-if="['flag_check_stline', 'flag_check_back',
+                             'flag_check_hip', 'flag_check_knee', 'flag_check_stline', 'flag_check_hand', 'flag_check_elbow', 'flag_save',
+                             'flag_display', 'flag_check_person', 'flag_check_singleleg_jump'].includes(item)"
+                    >
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-switch v-model="ruleForm[item]" :active-value="1" :inactive-value="0" />
+                      </el-form-item>
+                    </template>
+
+                    <template
+                      v-if="['distance_ground', 'distance_maximum', 'height_maximum','length_air_line',
+                             'distance_ground_between_lines', 'length_ground_line',
+                             'distance_air_between_lines', 'distance_per_round',
+                             'num_of_tracks', 'num_of_tracks_startline', 'num_of_tracks_endline', 'delay_cam_second', 'delay_cmd_second'
+                      ].includes(item) "
+                    >
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-input-number v-model="ruleForm[item]" style="width:30%" /><span
+                          style="margin-right:4px; padding-left:10px; font-size:18px;"
+                          >cm</span
+                        >
+                      </el-form-item>
+                    </template>
+
+                    <template v-if="item == 'difficulty'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-input-number v-model="ruleForm.difficulty" :min="0" :max="5" style="width:30%" />
+                      </el-form-item>
+                    </template>
+                    <template v-if="item == 'test_time'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-input-number v-model="ruleForm.test_time" :min="1" :max="1000" style="width:30%" />
+                      </el-form-item>
+                    </template>
+                  </template>
+                </template>
+              </el-collapse-item>
+              <el-collapse-item style="margin-left:-4px; color:#0067E1; font-size: 14px;">
+                <template slot="title">
+                  高级设置
+                  <!-- <i class="header-icon el-icon-info"></i> -->
+                </template>
+                <template v-for="value, item in ruleForm">
+                  <template v-if="item in examFormConf['advanced']">
+                    <template v-if="item === 'cam_path'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <span slot="label">
+                          {{ anchorLabelObj[item] }}
+                          <el-popover placement="right" width="width:30%" trigger="hover">
+                            <div style="height:100px; font-size:12px; margin-bottom: 20px;line-height:20px;">
+                              <p style="font-size:10px;">
+                                大华摄像头示例: <br />
+                                rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                              <p style="font-size:10px;">
+                                海康摄像头示例: <br />
+                                rtsp://admin:tropsedu123@192.168.3.77:554/h265/ch1/main/av_stream
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('rtsp://admin:tropsedu123@192.168.3.77:554/h265/ch1/main/av_stream', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                              <p style="font-size:10px;">
+                                USB摄像头示例: <br />
+                                /dev/video0,width=1280,height=720,framerate=30
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('/dev/video0,width=1280,height=720,framerate=30', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                            </div>
+                            <i slot="reference" class="el-icon-question" />
+                          </el-popover>
+                        </span>
+
+                        <el-input v-model="ruleForm.cam_path" clearable style="width:45%; margin-right:5px;" />
+                        <span style="margin-right:4px;font-size:10px;">帧率</span>
+                        <el-input v-model.number="ruleForm.cam_fps" style="width:9.66%;margin-right:5px;" />
+                        <el-button icon="el-icon-video-camera" type="primary" size="small" @click.prevent="viewCam(1, 0, ruleForm.cam_fps, 1)"
+                          >远程布点</el-button
+                        >
+                        <el-button
+                          icon="el-icon-video-camera"
+                          type="primary"
+                          size="small"
+                          style="margin-left:5px;"
+                          :disabled="!hasLocSRS"
+                          @click.prevent="viewCam(1, 0, ruleForm.cam_fps, 3)"
+                          >本地布点</el-button
+                        >
+                      </el-form-item>
+                    </template>
+                    <template v-if="item === 'anchor_path'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-input v-model="ruleForm.anchor_path" disabled class="input45" />
+                        <el-button type="primary" size="small" plain @click.prevent="showSnap(0, 1)">预览</el-button>
+                        <el-button type="primary" size="small" plain style="margin-left:5px;" @click.prevent="checkAnchorUrl('', 1)">刷新</el-button>
+                        <el-upload
+                          name="upload_file"
+                          style="display: inline; margin-left: 5px;"
+                          action="#"
+                          :show-file-list="false"
+                          :data="{exam_id: examId, cindex: 1, school_id: schoolId}"
+                          :action="uploadUrl"
+                          :headers="headers"
+                          :on-success="handleUpload"
+                        >
+                          <el-button type="primary" size="small" plain>上传布点</el-button>
+                        </el-upload>
+                      </el-form-item>
+                    </template>
+
+                    <template v-if="item === 'flag_calibrate'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-switch v-model="ruleForm.flag_calibrate" :active-value="1" :inactive-value="0" />
+                      </el-form-item>
+                    </template>
+
+                    <template v-if="item == 'cam_param_path'">
+                      <el-collapse-transition>
+                        <div v-show="ruleForm.flag_calibrate">
+                          <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                            <el-input v-model="ruleForm.cam_param_path" disabled class="input45" />
+                            <!--  <el-button @click.prevent="viewCam(1, 1, ruleForm.cam_fps, 1)" type="primary" size="small" plain>预览</el-button> -->
+                            <el-button type="primary" size="small" plain style="margin-left:5px;" @click.prevent="checkAnchorUrl('', 1, 'yml')"
+                              >刷新</el-button
+                            >
+
+                            <el-upload
+                              name="upload_file"
+                              style="display: inline-block; margin-left: 5px;"
+                              action="#"
+                              :show-file-list="false"
+                              :data="{exam_id: examId, cindex: 1, school_id: schoolId, up_type:'yml'}"
+                              :action="uploadUrl"
+                              :headers="headers"
+                              :on-success="handleUpload"
+                              ><el-button type="primary" size="small" plain>上传矫正</el-button>
+                            </el-upload>
+
+                            <el-button type="primary" size="small" plain style="margin-left: 5px;" @click="upAndDownPFile(1, 'yml')"
+                              >下载矫正
+                            </el-button>
+
+                            <el-button type="primary" size="small" plain style="margin-left:5px;" @click.prevent="viewCam(1, 2, ruleForm.cam_fps, 1)"
+                              >远程矫正</el-button
+                            >
+                            <el-button
+                              type="primary"
+                              size="small"
+                              plain
+                              style="margin-left:5px;"
+                              :disabled="!hasLocSRS"
+                              @click.prevent="viewCam(1, 2, ruleForm.cam_fps, 4)"
+                              >本地矫正</el-button
+                            >
+                          </el-form-item>
+                        </div>
+                      </el-collapse-transition>
+                    </template>
+
+                    <template v-if="item == 'cam_0_path'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <span slot="label">
+                          {{ anchorLabelObj[item] }}
+                          <el-popover placement="right" width="width:30%" trigger="hover">
+                            <div style="height:100px; font-size:12px; margin-bottom: 20px;line-height:20px;">
+                              <p style="font-size:10px;">
+                                大华摄像头示例: <br />
+                                rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                              <p style="font-size:10px;">
+                                海康摄像头示例: <br />
+                                rtsp://admin:tropsedu123@192.168.3.77:554/h265/ch1/main/av_stream
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('rtsp://admin:tropsedu123@192.168.3.77:554/h265/ch1/main/av_stream', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                              <p style="font-size:10px;">
+                                USB摄像头示例: <br />
+                                /dev/video0,width=1280,height=720,framerate=30
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('/dev/video0,width=1280,height=720,framerate=30')"
+                                >
+                                  复制</el-link
+                                >
+                              </p>
+                            </div>
+                            <i slot="reference" class="el-icon-question" />
+                          </el-popover>
+                        </span>
+                        <el-input v-model="ruleForm.cam_0_path" style="width:45%; margin-right:5px;" />
+                        <span style="margin-right:4px; font-size:10px;">帧率</span>
+                        <el-input v-model.number="ruleForm.cam_fps" style="width:9.66%;margin-right:5px;" />
+                        <el-button icon="el-icon-video-camera" type="primary" size="small" @click.prevent="viewCam(2, 0, ruleForm.cam_fps, 1)"
+                          >远程布点</el-button
+                        >
+                        <el-button
+                          icon="el-icon-video-camera"
+                          type="primary"
+                          size="small"
+                          style="margin-left:5px;"
+                          :disabled="!hasLocSRS"
+                          @click.prevent="viewCam(2, 0, ruleForm.cam_fps, 3)"
+                          >本地布点</el-button
+                        >
+                      </el-form-item>
+                    </template>
+
+                    <template v-if="item == 'anchor_0_path'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-input v-model="ruleForm.anchor_0_path" disabled class="input45" />
+                        <el-button type="primary" size="small" plain @click.prevent="showSnap(0, 2)">预览</el-button>
+                        <el-button type="primary" size="small" plain style="margin-left:5px;" @click.prevent="checkAnchorUrl('', 2)">刷新</el-button>
+                        <el-upload
+                          name="upload_file"
+                          style="display: inline; margin-left: 5px;"
+                          action="#"
+                          :show-file-list="false"
+                          :data="{exam_id: examId, cindex: 2, school_id: schoolId}"
+                          :action="uploadUrl"
+                          :headers="headers"
+                          :on-success="handleUpload"
+                        >
+                          <el-button type="primary" size="small" plain>上传布点</el-button>
+                        </el-upload>
+                      </el-form-item>
+                    </template>
+
+                    <template v-if="item == 'cam_face_path'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <span slot="label">
+                          {{ anchorLabelObj[item] }}
+                          <el-popover placement="right" width="width:30%" trigger="hover">
+                            <div style="height:100px; font-size:12px; margin-bottom: 20px;line-height:20px;">
+                              <p style="font-size:10px;">
+                                大华摄像头示例: <br />
+                                rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                              <p style="font-size:10px;">
+                                海康摄像头示例: <br />
+                                rtsp://admin:tropsedu123@192.168.3.77:554/h265/ch1/main/av_stream
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('rtsp://admin:tropsedu123@192.168.3.77:554/h265/ch1/main/av_stream', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                              <p style="font-size:10px;">
+                                USB摄像头示例: <br />
+                                /dev/video0,width=1280,height=720,framerate=30
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('/dev/video0,width=1280,height=720,framerate=30')"
+                                >
+                                  复制</el-link
+                                >
+                              </p>
+                            </div>
+                            <i slot="reference" class="el-icon-question" />
+                          </el-popover>
+                        </span>
+                        <el-input v-model="ruleForm.cam_face_path" style="width:45% ;margin-right:5px;" clearable />
+                        <span style="margin-right:4px; font-size:10px;">帧率</span>
+                        <el-input v-model.number="ruleForm.cam_face_fps" style="width:9.66%;margin-right:5px;" />
+                        <el-button icon="el-icon-video-camera" type="primary" size="small" @click.prevent="viewCam(0, 0, ruleForm.cam_face_fps, 1)"
+                          >远程布点</el-button
+                        >
+                        <el-button
+                          icon="el-icon-video-camera"
+                          type="primary"
+                          size="small"
+                          style="margin-left:5px;"
+                          :disabled="!hasLocSRS"
+                          @click.prevent="viewCam(0, 0, ruleForm.cam_face_fps, 3)"
+                          >本地布点</el-button
+                        >
+                      </el-form-item>
+                    </template>
+                    <template v-if="item == 'anchor_face_path'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-input v-model="ruleForm.anchor_face_path" disabled class="input45" />
+                        <el-button type="primary" size="small" plain @click.prevent="showSnap(0, 0)">预览</el-button>
+                        <el-button type="primary" size="small" plain style="margin-left:5px;" @click.prevent="checkAnchorUrl('', 0)">刷新</el-button>
+                        <el-upload
+                          name="upload_file"
+                          style="display: inline; margin-left: 5px;"
+                          action="#"
+                          :show-file-list="false"
+                          :data="{ exam_id: examId, cindex: 0, school_id: schoolId}"
+                          :action="uploadUrl"
+                          :headers="headers"
+                          :on-success="handleUpload"
+                        >
+                          <el-button type="primary" size="small" plain>上传布点</el-button>
+                        </el-upload>
+                      </el-form-item>
+                    </template>
+
+                    <template
+                      v-if="['flag_check_stline', 'flag_check_back',
+                             'flag_check_hip', 'flag_check_knee', 'flag_check_stline', 'flag_check_hand', 'flag_check_elbow', 'flag_save',
+                             'flag_display', 'flag_check_person', 'flag_check_singleleg_jump'].includes(item)"
+                    >
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-switch v-model="ruleForm[item]" :active-value="1" :inactive-value="0" />
+                      </el-form-item>
+                    </template>
+
+                    <template
+                      v-if="['distance_ground', 'length_air_line',
+                             'distance_ground_between_lines', 'length_ground_line',
+                             'distance_air_between_lines', 'distance_per_round',
+                             'num_of_tracks', 'num_of_tracks_startline', 'num_of_tracks_endline', 'delay_cam_second', 'delay_cmd_second'
+                      ].includes(item) "
+                    >
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-input-number v-model="ruleForm[item]" style="width:30%" /><span
+                          style="margin-right:4px; padding-left:10px; font-size:18px;"
+                          >cm</span
+                        >
+                      </el-form-item>
+                    </template>
+
+                    <template v-if="item == 'difficulty'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-input-number v-model="ruleForm.difficulty" :min="0" :max="5" style="width:30%" />
+                      </el-form-item>
+                    </template>
+                    <template v-if="item == 'test_time'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-input-number v-model="ruleForm.test_time" :min="1" :max="1000" style="width:30%" />
+                      </el-form-item>
+                    </template>
+                  </template>
+                </template>
+              </el-collapse-item>
+              <el-collapse-item style="margin-left:-4px; color:#0067E1; font-size: 14px; ">
+                <template slot="title">
+                  保留参数
+                  <!-- <i class="header-icon el-icon-info"></i> -->
+                </template>
+                <template v-for="value, item in ruleForm">
+                  <template v-if="item in examFormConf['reserved']">
+                    <template v-if="item === 'cam_path'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <span slot="label">
+                          {{ anchorLabelObj[item] }}
+                          <el-popover placement="right" width="width:30%" trigger="hover">
+                            <div style="height:100px; font-size:12px; margin-bottom: 20px;line-height:20px;">
+                              <p style="font-size:10px;">
+                                大华摄像头示例: <br />
+                                rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                              <p style="font-size:10px;">
+                                海康摄像头示例: <br />
+                                rtsp://admin:tropsedu123@192.168.3.77:554/h265/ch1/main/av_stream
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('rtsp://admin:tropsedu123@192.168.3.77:554/h265/ch1/main/av_stream', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                              <p style="font-size:10px;">
+                                USB摄像头示例: <br />
+                                /dev/video0,width=1280,height=720,framerate=30
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('/dev/video0,width=1280,height=720,framerate=30', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                            </div>
+                            <i slot="reference" class="el-icon-question" />
+                          </el-popover>
+                        </span>
+
+                        <el-input v-model="ruleForm.cam_path" clearable style="width:45%; margin-right:5px;" />
+                        <span style="margin-right:4px;font-size:10px;">帧率</span>
+                        <el-input v-model.number="ruleForm.cam_fps" style="width:9.66%;margin-right:5px;" />
+                        <el-button icon="el-icon-video-camera" type="primary" size="small" @click.prevent="viewCam(1, 0, ruleForm.cam_fps, 1)"
+                          >远程布点</el-button
+                        >
+                        <el-button
+                          icon="el-icon-video-camera"
+                          type="primary"
+                          size="small"
+                          style="margin-left:5px;"
+                          :disabled="!hasLocSRS"
+                          @click.prevent="viewCam(1, 0, ruleForm.cam_fps, 3)"
+                          >本地布点</el-button
+                        >
+                      </el-form-item>
+                    </template>
+                    <template v-if="item === 'anchor_path'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-input v-model="ruleForm.anchor_path" disabled class="input45" />
+                        <el-button type="primary" size="small" plain @click.prevent="showSnap(0, 1)">预览</el-button>
+                        <el-button type="primary" size="small" plain style="margin-left:5px;" @click.prevent="checkAnchorUrl('', 1)">刷新</el-button>
+                        <el-upload
+                          name="upload_file"
+                          style="display: inline; margin-left: 5px;"
+                          action="#"
+                          :show-file-list="false"
+                          :data="{exam_id: examId, cindex: 1, school_id: schoolId}"
+                          :action="uploadUrl"
+                          :headers="headers"
+                          :on-success="handleUpload"
+                        >
+                          <el-button type="primary" size="small" plain>上传布点</el-button>
+                        </el-upload>
+                      </el-form-item>
+                    </template>
+
+                    <template v-if="item === 'flag_calibrate'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-switch v-model="ruleForm.flag_calibrate" :active-value="1" :inactive-value="0" />
+                      </el-form-item>
+                    </template>
+
+                    <template v-if="item == 'cam_param_path'">
+                      <el-collapse-transition>
+                        <div v-show="ruleForm.flag_calibrate">
+                          <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                            <el-input v-model="ruleForm.cam_param_path" disabled class="input45" />
+                            <!-- <el-button @click.prevent="viewCam(1, 1, ruleForm.cam_fps, 1)" type="primary" size="small" plain>预览</el-button -->>
+                            <el-button type="primary" size="small" plain style="margin-left:5px;" @click.prevent="checkAnchorUrl('', 1, 'yml')"
+                              >刷新</el-button
+                            >
+                            <el-upload
+                              name="upload_file"
+                              style="display: inline-block; margin-left: 5px;"
+                              action="#"
+                              :show-file-list="false"
+                              :data="{exam_id: examId, cindex: 1, school_id: schoolId, up_type:'yml'}"
+                              :action="uploadUrl"
+                              :headers="headers"
+                              :on-success="handleUpload"
+                              ><el-button type="primary" size="small" plain>上传矫正</el-button>
+                              <el-button
+                                type="primary"
+                                size="small"
+                                plain
+                                style="margin-left:5px;"
+                                @click.prevent="viewCam(1, 2, ruleForm.cam_fps, 1)"
+                                >远程矫正</el-button
+                              >
+                              <el-button
+                                type="primary"
+                                size="small"
+                                plain
+                                style="margin-left:5px;"
+                                :disabled="!hasLocSRS"
+                                @click.prevent="viewCam(1, 2, ruleForm.cam_fps, 4)"
+                                >本地矫正</el-button
+                              >
+                            </el-upload>
+                          </el-form-item>
+                        </div>
+                      </el-collapse-transition>
+                    </template>
+
+                    <template v-if="item == 'cam_0_path'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <span slot="label">
+                          {{ anchorLabelObj[item] }}
+                          <el-popover placement="right" width="width:30%" trigger="hover">
+                            <div style="height:100px; font-size:12px; margin-bottom: 20px;line-height:20px;">
+                              <p style="font-size:10px;">
+                                大华摄像头示例: <br />
+                                rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                              <p style="font-size:10px;">
+                                海康摄像头示例: <br />
+                                rtsp://admin:tropsedu123@192.168.3.77:554/h265/ch1/main/av_stream
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('rtsp://admin:tropsedu123@192.168.3.77:554/h265/ch1/main/av_stream', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                              <p style="font-size:10px;">
+                                USB摄像头示例: <br />
+                                /dev/video0,width=1280,height=720,framerate=30
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('/dev/video0,width=1280,height=720,framerate=30')"
+                                >
+                                  复制</el-link
+                                >
+                              </p>
+                            </div>
+                            <i slot="reference" class="el-icon-question" />
+                          </el-popover>
+                        </span>
+                        <el-input v-model="ruleForm.cam_0_path" style="width:45%; margin-right:5px;" />
+                        <span style="margin-right:4px; font-size:10px;">帧率</span>
+                        <el-input v-model.number="ruleForm.cam_fps" style="width:9.66%;margin-right:5px;" />
+                        <el-button icon="el-icon-video-camera" type="primary" size="small" @click.prevent="viewCam(2, 0, ruleForm.cam_fps, 1)"
+                          >远程布点</el-button
+                        >
+                        <el-button
+                          icon="el-icon-video-camera"
+                          type="primary"
+                          size="small"
+                          style="margin-left:5px;"
+                          :disabled="!hasLocSRS"
+                          @click.prevent="viewCam(2, 0, ruleForm.cam_fps, 3)"
+                          >本地布点</el-button
+                        >
+                      </el-form-item>
+                    </template>
+
+                    <template v-if="item == 'anchor_0_path'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-input v-model="ruleForm.anchor_0_path" disabled class="input45" />
+                        <el-button type="primary" size="small" plain @click.prevent="showSnap(0, 2)">预览</el-button>
+                        <el-button type="primary" size="small" plain style="margin-left:5px;" @click.prevent="checkAnchorUrl('', 2)">刷新</el-button>
+                        <el-upload
+                          name="upload_file"
+                          style="display: inline; margin-left: 5px;"
+                          action="#"
+                          :show-file-list="false"
+                          :data="{exam_id: examId, cindex: 2, school_id: schoolId}"
+                          :action="uploadUrl"
+                          :headers="headers"
+                          :on-success="handleUpload"
+                        >
+                          <el-button type="primary" size="small" plain>上传布点</el-button>
+                        </el-upload>
+                      </el-form-item>
+                    </template>
+
+                    <template v-if="item == 'cam_face_path'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <span slot="label">
+                          {{ anchorLabelObj[item] }}
+                          <el-popover placement="right" width="width:30%" trigger="hover">
+                            <div style="height:100px; font-size:12px; margin-bottom: 20px;line-height:20px;">
+                              <p style="font-size:10px;">
+                                大华摄像头示例: <br />
+                                rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('rtsp://admin:tropsedu123@192.168.3.106:554/cam/realmonitor?channel=1&subtype=0', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                              <p style="font-size:10px;">
+                                海康摄像头示例: <br />
+                                rtsp://admin:tropsedu123@192.168.3.77:554/h265/ch1/main/av_stream
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('rtsp://admin:tropsedu123@192.168.3.77:554/h265/ch1/main/av_stream', item)"
+                                  >选择</el-link
+                                >
+                              </p>
+                              <p style="font-size:10px;">
+                                USB摄像头示例: <br />
+                                /dev/video0,width=1280,height=720,framerate=30
+                                <el-link
+                                  type="primary"
+                                  style="font-size:10px;"
+                                  @click.prevent="copyContent('/dev/video0,width=1280,height=720,framerate=30')"
+                                >
+                                  复制</el-link
+                                >
+                              </p>
+                            </div>
+                            <i slot="reference" class="el-icon-question" />
+                          </el-popover>
+                        </span>
+                        <el-input v-model="ruleForm.cam_face_path" style="width:45% ;margin-right:5px;" clearable />
+                        <span style="margin-right:4px; font-size:10px;">帧率</span>
+                        <el-input v-model.number="ruleForm.cam_face_fps" style="width:9.66%;margin-right:5px;" />
+                        <el-button icon="el-icon-video-camera" type="primary" size="small" @click.prevent="viewCam(0, 0, ruleForm.cam_face_fps, 1)"
+                          >远程布点</el-button
+                        >
+                        <el-button
+                          icon="el-icon-video-camera"
+                          type="primary"
+                          size="small"
+                          style="margin-left:5px;"
+                          :disabled="!hasLocSRS"
+                          @click.prevent="viewCam(0, 0, ruleForm.cam_face_fps, 3)"
+                          >本地布点</el-button
+                        >
+                      </el-form-item>
+                    </template>
+                    <template v-if="item == 'anchor_face_path'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-input v-model="ruleForm.anchor_face_path" disabled class="input45" />
+                        <el-button type="primary" size="small" plain @click.prevent="showSnap(0, 0)">预览</el-button>
+                        <el-button type="primary" size="small" plain style="margin-left:5px;" @click.prevent="checkAnchorUrl('', 0)">刷新</el-button>
+                        <el-upload
+                          name="upload_file"
+                          style="display: inline; margin-left: 5px;"
+                          action="#"
+                          :show-file-list="false"
+                          :data="{ exam_id: examId, cindex: 0, school_id: schoolId}"
+                          :action="uploadUrl"
+                          :headers="headers"
+                          :on-success="handleUpload"
+                        >
+                          <el-button type="primary" size="small" plain>上传布点</el-button>
+                        </el-upload>
+                      </el-form-item>
+                    </template>
+
+                    <template
+                      v-if="['flag_check_stline', 'flag_check_back',
+                             'flag_check_hip', 'flag_check_knee', 'flag_check_stline', 'flag_check_hand', 'flag_check_elbow', 'flag_save',
+                             'flag_display', 'flag_check_person', 'flag_check_singleleg_jump'].includes(item)"
+                    >
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-switch v-model="ruleForm[item]" :active-value="1" :inactive-value="0" />
+                      </el-form-item>
+                    </template>
+
+                    <template
+                      v-if="['distance_ground', 'length_air_line',
+                             'distance_ground_between_lines', 'length_ground_line',
+                             'distance_air_between_lines', 'distance_per_round',
+                             'num_of_tracks', 'num_of_tracks_startline', 'num_of_tracks_endline', 'delay_cam_second', 'delay_cmd_second'
+                      ].includes(item) "
+                    >
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-input-number v-model="ruleForm[item]" style="width:30%" /><span
+                          style="margin-right:4px; padding-left:10px; font-size:18px;"
+                          >cm</span
+                        >
+                      </el-form-item>
+                    </template>
+
+                    <template v-if="item == 'difficulty'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-input-number v-model="ruleForm.difficulty" :min="0" :max="5" style="width:30%" />
+                      </el-form-item>
+                    </template>
+                    <template v-if="item == 'test_time'">
+                      <el-form-item :key="item" :prop="item" :label="anchorLabelObj[item]">
+                        <el-input-number v-model="ruleForm.test_time" :min="1" :max="1000" style="width:30%" />
+                      </el-form-item>
+                    </template>
+                  </template>
+                </template>
+              </el-collapse-item>
+            </el-collapse>
+          </el-form>
+        </div>
+      </el-main>
+    </el-container>
+  </div>
+</template>
+
+<script>
+import dataDictionary from '@/utils/dataDictionary';
+import axios from 'axios';
+export default {
+  name: 'SchoolExamConf',
+  components: {},
+  data() {
+    const Authorization = 'JWT ' + localStorage.getItem('token') || '';
+    return {
+      userinfo: {},
+      project: dataDictionary.project,
+      anchorSetsObj: dataDictionary.anchorSetsObj,
+      anchorLabelObj: dataDictionary.anchorLabelObj,
+      ruleForm: {
+        cam_path: '',
+        anchor_path: '',
+
+        flag_calibrate: 0,
+        cam_param_path: '',
+        cam_fps: 25,
+
+        cam_face_path: '',
+        cam_face_fps: 25,
+        anchor_face_path: '',
+
+        cam_0_path: '',
+        anchor_0_path: '',
+
+        flag_check_person: 1,
+        flag_check_singleleg_jump: 0,
+        distance_maximum: 800,
+        height_maximum: 300,
+        flag_check_stline: 0,
+        flag_check_back: 0,
+        flag_check_hip: 0,
+        flag_check_knee: 0,
+        flag_check_hand: 0,
+        flag_check_elbow: 0,
+
+        test_time: 60,
+        difficulty: 1,
+
+        distance_per_round: 400,
+        num_of_tracks: 1,
+        num_of_tracks_startline: 1,
+        num_of_tracks_endline: 1,
+        delay_cam_second: 0.45,
+        delay_cmd_second: 0.2,
+
+        distance_ground_between_lines: 50,
+        distance_ground: 1200,
+        distance_air_between_lines: 50,
+        length_ground_line: 80,
+        length_air_line: 50,
+        flag_save: 1,
+        flag_display: 0
+      },
+      camData: [],
+
+      rawImags: {},
+      jFiles: {},
+      uploadUrl: import.meta.env.VITE_APP_BASE_API + '/exam/gpu_anchor_update',
+      headers: { Authorization: Authorization },
+      disExams: ['jump', 'solidball', 'trijump', 'shotput', 'verticaljump'],
+      countExams: ['situp', 'pullup', 'sidepullup', 'jumprope'],
+      secResExams: ['football', 'basketball', 'badminton', 'pingpang', 'volleyball'],
+      examId: '',
+      exam_name: '',
+      schoolId: '',
+      examCN: '',
+      showSnapPics: false,
+      snapPic: '',
+      playUrl: '',
+      rtmpPlayUrl: '',
+      rtcPlayUrl: '',
+      flvUrl: '',
+      showPlayer: false,
+      anchorFlag: false,
+      caliFlag: false,
+      viewCamurl: '',
+      picTitle: '测试区布点图',
+      camIndex: 1,
+      snapUptime: {},
+      anchorUptime: {},
+      camparamUptime: {},
+      stdAnchObj: null,
+      cIntervalId: null,
+      lstAnchorIvalId: null,
+      imgSize: [1280, 720],
+      lines: null,
+      points: [],
+      rawPoints: '[]',
+      names: null,
+      scaleRate: 1,
+      windowHeight: parseInt(window.innerHeight),
+      windowWidth: parseInt(window.innerWidth * 0.96),
+      xPos: 0,
+      scaledWidth: 1080,
+      yPos: 0,
+      scaledHeight: 720,
+      anchorNum: 1,
+      video: null,
+      dragStartX: 0,
+      dragStartY: 0,
+      flvPlayer: null,
+      rtcPlayer: null,
+      srsHost: 'srs.tropsx.com',
+      lsrsHost: '',
+      playHost: '',
+      isLocStream: false,
+      checkPSTO: null,
+      checkCBFD: null,
+      checkUpFID: null,
+      // showRequire: true,
+      // showAdvance: false,
+      // showReserve: false,
+      activeNames: ['1'],
+      examFormConf: {},
+      rawFormData: '{}',
+      uutoken: '',
+      achorUrlChkId: null,
+      hasLocSRS: false,
+      btnDis: false,
+      zoomRatio: 1,
+      lastZoom: 1,
+      startDistance: 0,
+      rules: {
+        // cam_path: [
+        //   { required: true, message: '请输入摄像头地址', trigger: 'blur' },
+        //   // { min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
+        // ],
+        cam_path: [{ required: true, message: '请输入摄像头地址' }],
+        anchor_path: [{ required: true, message: '请上传布点文件' }],
+        cam_fps: [{ required: true, message: '请输入摄像头帧率' }]
+      }
+    };
+  },
+  computed: {
+    isDisExams() {
+      return this.disExams.includes(this.exam_name);
+    },
+    isSitup() {
+      return this.exam_name === 'situp';
+    },
+    isJumpRope() {
+      return this.exam_name === 'jumprope';
+    },
+    isJumps() {
+      return this.exam_name === 'jump' || this.exam_name === 'trijump';
+    },
+    isRun() {
+      return this.exam_name.indexOf('run') === 0;
+    },
+    isBackRun() {
+      return this.exam_name.indexOf('run') == 0 && this.exam_name.indexOf('x') != -1;
+    },
+    isShortRun() {
+      let c1 = this.exam_name.indexOf('run') == 0;
+      let c2 = this.exam_name.indexOf('x') == -1;
+      if (c1 && c2) {
+        if (this.exam_name.replace('run', '') < 799) {
+          return true;
+        }
+      }
+      return false;
+    },
+    isLongRun() {
+      let c1 = this.exam_name.indexOf('run') == 0;
+      let c2 = this.exam_name.indexOf('x') == -1;
+      if (c1 && c2) {
+        if (this.exam_name.replace('run', '') > 799) {
+          return true;
+        }
+      }
+      return false;
+    },
+    isBasicFace() {
+      return ['volleyball', 'pingpang', 'badminton'].includes(this.exam_name);
+    },
+    snapTime() {
+      return this.snapUptime[this.camIndex + '_error'] || this.snapUptime[this.camIndex] || '';
+    }
+  },
+  watch: {},
+  created() {
+    this.examId = this.$route.query.examId;
+    console.log(this.examId, 'this.examId');
+    let myInfo = localStorage.getItem('userInfo');
+    this.userinfo = JSON.parse(myInfo);
+    this.schoolId = this.userinfo.school_id;
+    this.exam_name = this.examId.split('_')[0];
+    this.examIndex = this.examId.split('_')[1];
+    this.examCN = this.project[this.exam_name] + '_' + this.examIndex;
+    this.isSolidBall = this.exam_name === 'solidball';
+    this.isPushBall = ['solidball', 'shotput'].includes(this.exam_name);
+    this.isSidepullup = this.exam_name === 'sidepullup';
+    this.examFormConf['required'] = JSON.parse(JSON.stringify(this.anchorSetsObj['basic']['required']));
+    this.examFormConf['advanced'] = JSON.parse(JSON.stringify(this.anchorSetsObj['basic']['advanced']));
+    this.examFormConf['reserved'] = JSON.parse(JSON.stringify(this.anchorSetsObj['basic']['reserved']));
+    console.log(this.examFormConf, 'this.examFormConf111');
+    let ename;
+    if (this.isLongRun) {
+      ename = 'longrun';
+    } else if (this.isShortRun) {
+      ename = 'shortrun';
+    } else if (this.isBackRun) {
+      ename = 'backrun';
+    } else if (this.isBasicFace) {
+      ename = 'basicface';
+    } else {
+      ename = this.exam_name;
+    }
+    console.log('ename:', ename, 'examId:', this.examId, 'schoolId:', this.schoolId);
+    Object.assign(this.examFormConf['required'], this.anchorSetsObj[ename]['required']);
+    Object.assign(this.examFormConf['advanced'], this.anchorSetsObj[ename]['advanced']);
+    Object.assign(this.examFormConf['reserved'], this.anchorSetsObj[ename]['reserved']);
+    for (let i in this.examFormConf['required']) {
+      if (!this.rules[i]) {
+        let message = `${this.anchorLabelObj[i]}必填`;
+        this.rules[i] = [{ required: true, message: message }];
+      }
+      this.ruleForm[i]=this.examFormConf['required'][i];
+    }
+    for (let i in this.examFormConf['advanced']) {
+      if (i in this.examFormConf['required']) {
+        delete this.examFormConf['advanced'][i];
+      } else {
+        this.ruleForm[i] = this.examFormConf['advanced'][i];
+      }
+    }
+    for (let i in this.examFormConf['reserved']) {
+      if (i in this.examFormConf['required'] || i in this.examFormConf['advanced']) {
+        delete this.examFormConf['reserved'][i];
+      } else {
+        this.ruleForm[i] = this.examFormConf['reserved'][i];
+      }
+    }
+    this.getExamConf(); // 获取布点配置
+  },
+
+  beforeRouteLeave(to, from, next) {
+    if (this.showSnapPics) {
+      next(false);
+      this.showSnapPics = false;
+      return;
+    }
+    if (this.showPlayer) {
+      next(false);
+      if (this.rawPoints != JSON.stringify(this.points)) {
+        setTimeout(() => {
+          this.$confirm('检测到有点位移动没有保存', '确认退出?', {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning'
+          })
+            .then(() => {
+              this.handlePlayerClose();
+              return false;
+            })
+            .catch((err) => {
+              console.log(1111, err);
+              return false;
+            });
+        }, 200);
+      } else {
+        this.handlePlayerClose();
+      }
+    } else {
+      if (this.rawFormData != JSON.stringify(this.ruleForm)) {
+        setTimeout(() => {
+          this.$confirm('检测到改动未保存', '退出后编辑过的内容将丢失', {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning'
+          })
+            .then(() => {
+              next();
+            })
+            .catch((err) => {
+              next(false);
+            });
+        }, 200);
+      } else {
+        next();
+      }
+    }
+  },
+  beforeDestroy() {
+    let tmparr = [this.checkPSTO, this.cIntervalId, this.lstAnchorIvalId, this.checkCBFD, this.achorUrlChkId];
+    tmparr.forEach((i) => {
+      clearInterval(i);
+    });
+
+    this.$http.deploy.closeLocalStream({ exam_id: this.examId, school_id: this.schoolId });
+    this.$http.deploy.closeCaliStream({ exam_id: this.examId, school_id: this.schoolId });
+    if (this.flvPlayer) {
+      if (this.flvPlayer.destroy) {
+        try {
+          this.flvPlayer.destroy();
+        } catch {
+          this.flvPlayer = null;
+        }
+      } else {
+        this.flvPlayer.unsubscribe();
+        this.flvPlayer = null;
+      }
+    }
+    if (this.rtcPlayer) {
+      try {
+        this.rtcPlayer.close();
+      } catch {
+        this.rtcPlayer = null;
+      }
+    }
+    this.anchorFlag = false;
+    this.examFormConf = {};
+  },
+
+  mounted() {
+    // this.$nextTick(() => {
+    //   window.addEventListener('resize', this.onResize);
+    // })
+  },
+
+  methods: {
+    // onResize() {
+    //   this.windowHeight = window.innerHeight * 0.9
+    //   this.windowWidth = window.innerWidth * 0.9
+    // },
+    validateIP(str) {
+      const re = /^(1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|[0-9])\.((1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|\d)\.){2}(1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|\d)$/;
+      return re.test(str);
+    },
+
+    checkHTTPPageExists(url, callback) {
+      let xhttp = new XMLHttpRequest();
+      xhttp.onreadystatechange = function () {
+        if (this.readyState === 4) {
+          if (this.status === 200) {
+            callback(true);
+          } else {
+            callback(false);
+          }
+        }
+      };
+      xhttp.open('HEAD', url, true);
+      xhttp.send();
+    },
+
+    checkLocRSR() {
+      if (this.lsrsHost) {
+        let ourl = ['http:/', this.lsrsHost + ':8081', 'players/anchor/index.html'].join('/');
+        var vm = this;
+        this.checkHTTPPageExists(ourl, function (exists) {
+          console.log(exists, 'checkHTTPPageExists');
+          if (exists) {
+            console.log('HTTP page exists.');
+            vm.hasLocSRS = true;
+          } else {
+            vm.hasLocSRS = false;
+            console.log('HTTP page does not exist or is inaccessible.');
+          }
+        });
+      } else {
+        this.hasLocSRS = false;
+      }
+    },
+
+    localView(webrtc) {
+      let action = 'anchor_config';
+      let ftype = 'json';
+      if (webrtc == 4) {
+        action = 'camera_param';
+        ftype = 'yml';
+        clearInterval(this.checkCBFD);
+        this.checkCBFD = setInterval(this.checkCalibFshd, 5000, 1);
+      } else {
+        clearInterval(this.achorUrlChkId);
+        this.achorUrlChkId = setInterval(this.checkAnchorUrl, 5000, lsturl, this.camIndex, ftype, 1);
+      }
+      let ourl = ['http:/', this.lsrsHost + ':8081', 'players/anchor/index.html'].join('/');
+      let lsturl =
+        '//aiexam-data.oss-cn-shenzhen.aliyuncs.com/midexam/uploaded_files/settings/' +
+        this.uutoken +
+        '/' +
+        this.examId +
+        '_' +
+        this.camIndex +
+        '.' +
+        ftype;
+      let ename_str = this.getEName().join('_');
+      console.log(ename_str, 'eNameeNameeName');
+      let params_str = [
+        'camUrl=' + this.viewCamurl,
+        'examId=' + this.examId,
+        'camIndex=' + this.camIndex,
+        'playUrl=' + this.rtcPlayUrl,
+        'lastAchUrl=' + lsturl,
+        'eName=' + ename_str,
+        'token=' + Authorization,
+        'baseUrl=' + process.env.VUE_APP_BASE_API2,
+        'action=' + action
+      ].join('&');
+      ourl = ourl + '?' + params_str;
+      if (window && window.navigator) {
+        window.open(ourl, '_blank');
+      } else {
+        window.location.href = ourl;
+      }
+    },
+
+    checkStreamClosed() {
+      this.$http.deploy.checkStreamClosd({ dest_uri: this.rtcPlayUrl }).then((res) => {
+        console.log('checkStreamClosed', res);
+        if (res.status == 200) {
+          if (res.data) {
+            if (res.data >= '0') {
+              // if (this.ruleForm.)
+              // this.$message.error(
+              //   '布点结束,如您已完成布点,请点刷新按钮')
+              this.$message.warning('推流结束,如您已布点成功且未检测到布点文件请点击刷新按钮');
+              clearInterval(this.achorUrlChkId);
+            }
+          } else {
+            this.$message.info('正在推流,请继续布点');
+          }
+        } else {
+          clearInterval(this.achorUrlChkId);
+        }
+        return;
+      });
+    },
+
+    checkCaliClosed() {
+      this.$http.deploy.checkCaliStreamClosd({ exam_id: this.examId }).then((res) => {
+        if (res.status == 200) {
+          if (res.data) {
+            if (res.data >= '0') {
+              // if (this.ruleForm.)
+              // this.$message.error(
+              //   '布点结束,如您已完成布点,请点刷新按钮')
+              this.$message.warning('推流结束,如您矫正成功且未检测到矫正文件请点击刷新按钮');
+              clearInterval(this.checkCBFD);
+            }
+          } else {
+            this.$message.info('正在推流,请继续矫正');
+          }
+        } else {
+          clearInterval(this.checkCBFD);
+        }
+        return;
+      });
+    },
+
+    checkAnchorUrl(url = '', camIndex = this.camIndex, ftype = 'json', chType = 0) {
+      if (!url) {
+        url =
+          '//aiexam-data.oss-cn-shenzhen.aliyuncs.com/midexam/uploaded_files/settings/' +
+          this.uutoken +
+          '/' +
+          this.examId +
+          '_' +
+          camIndex +
+          '.' +
+          ftype;
+      }
+      axios
+        .get(url)
+        .then((response) => {
+          let anchorPath = 'settings/anchor_files/' + this.examId + '_' + camIndex + '.' + ftype;
+          if (camIndex == 1) {
+            if (ftype == 'json') {
+              if (!this.ruleForm.anchor_path) {
+                this.ruleForm.anchor_path = anchorPath;
+              }
+            } else {
+              if (!this.ruleForm.cam_param_path) {
+                anchorPath = 'settings/cam_param_files/' + this.examId + '_' + camIndex + '.' + ftype;
+                this.ruleForm.cam_param_path = anchorPath;
+              }
+            }
+          } else if (camIndex == 0 && !this.ruleForm.anchor_face_path) {
+            this.ruleForm.anchor_face_path = anchorPath;
+          } else if (camIndex == 2 && !this.ruleForm.anchor_0_path) {
+            this.ruleForm.anchor_0_path = anchorPath;
+          }
+          this.$message({
+            message: '检测到云文件,读取成功',
+            type: 'success'
+          });
+          if (this.achorUrlChkId) {
+            clearInterval(this.achorUrlChkId);
+          }
+          clearInterval(this.achorUrlChkId);
+        })
+        .catch((error) => {
+          if (ftype == 'json') {
+            if (chType) {
+              this.checkStreamClosed();
+            } else {
+              clearInterval(this.achorUrlChkId);
+              this.$message({
+                message: '未检测到云文件,读取失败',
+                type: 'error'
+              });
+            }
+          } else {
+            if (chType) {
+              this.checkCaliClosed();
+            } else {
+              this.$message({
+                message: '矫正未完成,读取失败',
+                type: 'error'
+              });
+            }
+          }
+        });
+    },
+
+    async viewCam(cindex, cali = 0, cam_fps = 0, webrtc = 0) {
+      if (!cam_fps) {
+        this.$message({
+          message: '请输入摄像头帧率',
+          type: 'error'
+        });
+        return;
+      }
+      if (cindex == 0) {
+        this.viewCamurl = this.ruleForm.cam_face_path;
+      } else if (cindex == 1) {
+        this.viewCamurl = this.ruleForm.cam_path;
+      } else if (cindex == 2) {
+        this.viewCamurl = this.ruleForm.cam_0_path;
+      }
+      if (!this.viewCamurl) {
+        this.$message({
+          message: '请输入摄像头地址',
+          type: 'error'
+        });
+        return;
+      }
+      if (webrtc == 2 && !this.validateIP(this.playHost)) {
+        this.$message({
+          message: '请输入正确局域网srs IP',
+          type: 'error'
+        });
+        return;
+      }
+      this.camIndex = cindex;
+      let src_uri = this.viewCamurl;
+      if (cali == 1) {
+        if (!this.ruleForm.cam_param_path) {
+          this.$message({
+            message: '请先生成矫正文件',
+            type: 'error'
+          });
+          return;
+        }
+      }
+      this.isLocStream = false;
+      var pushHost = 'alilive-push.tropsx.com';
+      var playHost;
+      let port = 443;
+      let schema = window.location.protocol;
+      if (webrtc == 1) {
+        pushHost = this.srsHost;
+        port = 8088;
+        playHost = this.srsHost;
+        schema = 'https:';
+      } else if (webrtc == 2) {
+        pushHost = this.playHost;
+        port = 8080;
+        playHost = this.playHost;
+        schema = 'http:';
+      } else if (webrtc == 3 || webrtc == 4) {
+        playHost = this.lsrsHost;
+        port = 8081;
+        pushHost = this.lsrsHost;
+        schema = 'http:';
+      } else {
+        if (schema == 'http:') {
+          port = 80;
+        }
+        playHost = 'alilive-play.tropsx.com';
+      }
+      let baseUrl = [pushHost, this.schoolId, this.examId + '_' + cindex + '_' + cali].join('/');
+      let dest_uri = ['rtmp:/', baseUrl].join('/');
+      await this.$http.deploy.closeLocalStream({ exam_id: this.examId, school_id: this.schoolId });
+      await this.$http.deploy.closeCaliStream({ exam_id: this.examId, school_id: this.schoolId });
+      if (port != 80) {
+        this.flvUrl = [schema + '/', baseUrl + '.flv'].join('/');
+        this.playUrl = ['artc:' + '/', baseUrl].join('/');
+      } else {
+        this.flvUrl = [schema + '/', baseUrl + '.flv'].join('/');
+        this.playUrl = ['artc:' + '/', baseUrl].join('/');
+      }
+
+      this.rtmpPlayUrl = ['rtmp:/', baseUrl].join('/');
+      this.rtcPlayUrl = ['webrtc:/', baseUrl].join('/'); // 'webrtc://srs.xmtel.com:443/live/3/jump_76_1_0'
+      let params = { exam_id: this.examId, src_uri: src_uri, dest_uri: dest_uri, school_id: this.schoolId, cam_fps, cali };
+      if (this.ruleForm.flag_calibrate && this.camIndex == 1 && cali != 2) {
+        if (!this.ruleForm.cam_param_path) {
+          this.$message.warning('您已开启矫正,请配置矫正文件');
+          return;
+        }
+        params['cam_param_path'] = this.ruleForm.cam_param_path;
+      }
+
+      await this.$http.deploy.pushLocalStream(params).then((res) => {
+        if (webrtc == 3 || webrtc == 4) {
+          this.localView(webrtc);
+        } else {
+          this.showPlayer = true;
+          if (webrtc) {
+            this.$nextTick(() => {
+              this.flvPlayer = null;
+              this.playRtc();
+            });
+          } else {
+          }
+          clearInterval(this.checkPSTO);
+          this.checkPSTO = setInterval(this.checkPushStream, 3500);
+          if (cali === 2) {
+            this.caliFlag = true;
+            clearInterval(this.checkCBFD);
+            this.checkCBFD = setInterval(this.checkCalibFshd, 5000);
+          }
+        }
+      });
+    },
+
+    initSuccess() {
+      let aspectRatio = this.video.videoWidth / this.video.videoHeight;
+      this.scaledWidth = this.windowWidth;
+      this.scaledHeight = this.windowWidth / aspectRatio;
+      if (this.scaledHeight > this.windowHeight) {
+        this.scaledHeight = this.windowHeight;
+        this.scaledWidth = this.windowHeight * aspectRatio;
+      }
+      this.xPos = (this.windowWidth - this.scaledWidth) / 2;
+      this.yPos = (this.windowHeight - this.scaledHeight) / 2;
+      this.anchorFlag = true;
+      this.imgSize = [this.video.videoWidth, this.video.videoHeight];
+      setTimeout(function () {
+        console.log(this.flvPlayer);
+        if (this.flvPlayer) {
+          this.flvPlayer.play();
+        }
+      }, 100);
+    },
+
+    playLocStream() {
+      this.video = document.getElementById('videoflv');
+      var vm = this;
+      let videoSelect = document.querySelector('select#videoSource');
+      console.log('playLocStream', this.video);
+      this.video.width = this.windowWidth;
+      this.video.height = this.windowHeight;
+      function getDevices() {
+        return navigator.mediaDevices.enumerateDevices();
+      }
+      function gotDevices(deviceInfos) {
+        console.log('Available input and output devices:', deviceInfos);
+        for (const deviceInfo of deviceInfos) {
+          if (deviceInfo.deviceId) {
+            const option = document.createElement('option');
+            option.value = deviceInfo.deviceId;
+            if (deviceInfo.kind === 'videoinput' && deviceInfo.deviceId !== '') {
+              option.text = deviceInfo.label || `Camera ${videoSelect.length + 1}`;
+              videoSelect.appendChild(option);
+            }
+          }
+        }
+      }
+      function getStream() {
+        if (vm.video.srcObject) {
+          vm.video.srcObject.getTracks().forEach((track) => {
+            track.stop();
+          });
+        }
+        const videoSource = videoSelect.value;
+        const constraints = {
+          video: { deviceId: videoSource ? { exact: videoSource } : undefined }
+        };
+        return navigator.mediaDevices
+          .getUserMedia(constraints)
+          .then(gotStream)
+          .catch((error) => {
+            console.error('Error: ', error);
+          });
+      }
+
+      function gotStream(stream) {
+        // window.stream = stream; // make stream available to console
+        videoSelect.selectedIndex = [...videoSelect.options].findIndex((option) => option.text === stream.getVideoTracks()[0].label);
+        vm.video.srcObject = stream;
+      }
+      videoSelect.onchange = getStream;
+      getStream().then(getDevices).then(gotDevices);
+
+      // navigator.mediaDevices.getUserMedia({video: true})
+      // .then(stream => {
+      //   // Set video source and play
+      //   this.video.srcObject = stream;
+      //   this.video.play();
+      // })
+      // .catch(err => {
+      //   console.error(err);
+      // });
+      this.video.addEventListener('loadedmetadata', this.initSuccess);
+    },
+    checkCalibFshd(isLocal = 0) {
+      this.$http.deploy.checkCaliRes({ exam_id: this.examId }).then((res) => {
+        console.log(res, res.data, 'checkCaliRes');
+        if (res.data.error_message) {
+          if (res.data.error_code == '0') {
+            this.$message({
+              message: '矫正已完成',
+              type: 'success'
+            });
+            // this.ruleForm.cam_param_path = "settings/cam_param_files/"+this.examId+"_"+this.camIndex+".yml"
+            if (!this.ruleForm.cam_param_path) {
+              this.ruleForm.cam_param_path = res.data.cam_param_path;
+            }
+            this.caliFlag = false;
+          } else {
+            this.$message({
+              message: '矫正失败,原因:' + res.data.error_message,
+              type: 'error'
+            });
+          }
+          if (isLocal >= 1) {
+            clearInterval(this.checkCBFD);
+          }
+          // if (this.checkCBFD) {
+          //   clearInterval(this.checkCBFD)
+          // }
+        } else {
+          if (isLocal == 1) {
+            this.$message.info('矫正未完成,检测中');
+          } else if (isLocal == 2) {
+            this.$message.info('未找到矫正记录');
+          } else {
+            this.$message({
+              message: '请将棋盘放到摄像头前进行矫正',
+              type: 'info'
+            });
+          }
+        }
+      });
+    },
+    checkPushStream() {
+      this.$http.deploy
+        .checkPushStream({
+          dest_uri: this.rtmpPlayUrl
+        })
+        .then((res) => {
+          console.log('ressts', res);
+          if (res == -1) {
+            this.$message({
+              message: '开启推流中,请稍等',
+              type: 'info'
+            });
+          } else if (res == '推流成功') {
+            this.$message({
+              message: '推流成功',
+              type: 'success'
+            });
+            if (this.checkPSTO) {
+              clearInterval(this.checkPSTO);
+            }
+          } else {
+            this.$message({
+              message: '推流失败,原因:' + res,
+              type: 'error'
+            });
+          }
+        });
+    },
+
+    playRtc() {
+      if (this.rtcPlayer) {
+        this.rtcPlayer.close();
+      }
+      var vm = this;
+      this.video = document.getElementById('videoflv');
+      this.video.width = this.windowWidth;
+      this.video.height = this.windowHeight;
+      this.rtcPlayer = new this.SrsRtcPlayerAsync();
+      this.video.srcObject = this.rtcPlayer.stream;
+      // $('#videoflv').prop('srcObject', this.rtcPlayer.stream);
+      this.rtcPlayer
+        .play(this.rtcPlayUrl)
+        .then(function (session) {
+          // vm.anchorFlag = true
+          vm.video.play();
+          vm.initSuccess();
+        })
+        .catch(function (reason) {
+          if (vm.rtcPlayer) {
+            vm.rtcPlayer.close();
+          }
+          console.error(reason);
+        });
+      this.video.addEventListener('loadedmetadata', this.initSuccess);
+      // this.video.addEventListener('canplay', this.initSuccess);
+    },
+
+    handlBeforeClose(done = null) {
+      if (this.rawPoints != JSON.stringify(this.points)) {
+        this.$confirm('检测到有点位移动没有保存', '确认退出?', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        })
+          .then(() => {
+            if (done) done();
+            return true;
+          })
+          .catch((err) => {
+            console.log(1111, err);
+            return false;
+          });
+      } else {
+        if (done) done();
+        return true;
+      }
+    },
+
+    handlePlayerClose() {
+      if (this.isLocStream) {
+        this.video.srcObject.getTracks().forEach((track) => {
+          track.stop();
+        });
+        // this.video.stop()
+      } else {
+        if (this.checkPSTO) {
+          this.$http.deploy.closeLocalStream({ exam_id: this.examId, school_id: this.schoolId });
+        }
+        if (this.checkCBFD) {
+          this.$http.deploy.closeCaliStream({ exam_id: this.examId, school_id: this.schoolId });
+        }
+
+        if (this.flvPlayer) {
+          if (this.flvPlayer.destroy) {
+            this.flvPlayer.destroy();
+          } else {
+            this.flvPlayer.unsubscribe();
+          }
+        }
+        if (this.rtcPlayer) {
+          this.rtcPlayer.close();
+          this.video.pause();
+        }
+      }
+      this.showPlayer = false;
+      this.anchorFlag = false;
+      this.caliFlag = false;
+      console.log('closed!!');
+      let tmparr = [this.checkPSTO, this.cIntervalId, this.lstAnchorIvalId, this.checkCBFD];
+      tmparr.forEach((item) => {
+        if (item) clearInterval(item);
+      });
+    },
+
+    upSnapShotInfos(onlyShow, up_type = 'all') {
+      let params = { exam_id: this.examId, no_req: onlyShow, qindex: this.camIndex, school_id: this.schoolId, up_type: up_type };
+      if (this.camIndex == 1) {
+        params['cam_path'] = this.ruleForm.cam_path;
+        params['anchor_path'] = this.ruleForm.anchor_path;
+        params['cam_param_path'] = this.ruleForm.cam_param_path;
+      } else if (this.camIndex == 0) {
+        params['cam_path'] = this.ruleForm.cam_face_path;
+        params['anchor_path'] = this.ruleForm.anchor_face_path;
+      } else if (this.camIndex == 2) {
+        params['cam_path'] = this.ruleForm.cam_0_path;
+        params['anchor_path'] = this.ruleForm.anchor_0_path;
+      }
+      // if (!params['cam_path'] || !params['anchor_path']) {
+      //   this.$message.error("请先配置摄像头地址和布点文件")
+      //   return
+      // }
+      this.$http.deploy.getExamSnapShot(params).then((res) => {
+        console.log(res, 'getExamSnapShot');
+        // res.data.exam_imgs.forEach(img => {
+        //   this.snapPics.push(img + "?" + Math.random())
+        // })
+        setTimeout(() => {
+          if (res.data.exam_imgs.length == 1) {
+            this.snapPic = res.data.exam_imgs[0] + '?' + Math.random();
+          } else {
+            this.snapPic = res.data.exam_imgs[this.camIndex] + '?' + Math.random();
+          }
+          // $('#snapPic').attr('src', this.snapPic)
+          this.rawImags = res.data.raw_imgs;
+          this.jFiles = res.data.j_files;
+          this.snapUptime = res.data.snappic_uptime;
+          this.anchorUptime = res.data.anchor_uptime;
+          this.camparamUptime = res.data.camparam_uptime;
+        }, 1500);
+      });
+    },
+
+    showSnap(onlyShow = 0, camIndex = 1, up_type = 'snapshot') {
+      this.camIndex = camIndex;
+      if (camIndex == 0) {
+        this.picTitle = this.examCN + '人脸区布点图';
+        if (!this.ruleForm.cam_face_path || !this.ruleForm.anchor_face_path) {
+          this.$message.error('请配置人脸摄像头和人脸布点文件');
+          return;
+        }
+      } else if (camIndex == 1) {
+        if (!this.ruleForm.cam_path || !this.ruleForm.anchor_path) {
+          this.$message.error('请配置测试摄像头和测试布点文件');
+          return;
+        }
+        this.picTitle = this.examCN + '测试区布点图';
+      } else if (camIndex == 2) {
+        if (!this.ruleForm.cam_0_path || !this.ruleForm.anchor_0_path) {
+          this.$message.error('请配置辅助摄像头和辅助布点文件');
+          return;
+        }
+        this.picTitle = this.examCN + '辅助区布点图';
+      }
+
+      // this.snapPics = []
+      this.upSnapShotInfos(onlyShow, up_type);
+      if (!this.showSnapPics) {
+        this.showSnapPics = true;
+      }
+    },
+
+    // getGExamConfs() {
+    //   getGPUExamconfs(
+    //     { exam_id: this.examId }
+    //   ).then(res => {
+    //     this.camData = res
+    //   })
+    // },
+
+    onlyDownFile(url) {
+      const iframe = document.createElement('iframe');
+      iframe.style.display = 'none';
+      iframe.style.height = 0;
+      iframe.src = url + '?response-content-type=application/octet-stream';
+      console.log(url, 'Url onlyDownFile');
+      document.body.appendChild(iframe);
+      // 不能马上将iframe进行删除,否者会出现马上取消的情况
+      setTimeout(() => {
+        iframe.remove();
+      }, 5000);
+      // this.$message.success('文件下载成功')
+    },
+
+    checkPermission(fileUrl, fname) {
+      // let permissions = cordova.plugins.permissions
+      // permissions.checkPermission(permissions.READ_EXTERNAL_STORAGE, checkPermissionCallback, null)
+
+      // Checking for permissions
+      // function checkPermissionCallback(status) {
+      //   console.log('checking permissions')
+      //   console.log(status)
+      //   if (!status.hasPermission) {
+      //     var errorCallback = function () {
+      //       console.warn('Storage permission is not turned on')
+      //     }
+      //     // Asking permission to the user
+      //     permissions.requestPermission(
+      //       permissions.READ_EXTERNAL_STORAGE,
+      //       function (status) {
+      //         if (!status.hasPermission) {
+      //           errorCallback()
+      //         } else {
+      //           // proceed with downloading
+      //           downloadFile()
+      //         }
+      //       },
+      //       errorCallback)
+      //   } else {
+      //     downloadFile() }
+      // }
+      // fileUrl = "https:" + fileUrl
+      function downloadFile(fileUrl, fname) {
+        try {
+          let filePath = cordova.file.externalApplicationStorageDirectory + fname;
+          let fileTransfer = new FileTransfer();
+          let uri = encodeURI(fileUrl);
+          // alert(uri)
+          // alert(filePath)
+          // Downloading the file
+          fileTransfer.download(
+            uri,
+            filePath,
+            function (entry) {
+              // alert('Successfully downloaded file, full path is ' + entry.fullPath)
+              this.$message.success('文件存储于:' + entry.fullPath);
+            },
+            function (error) {
+              alert('error' + error.code);
+              alert('error' + error);
+            },
+            false
+          );
+        } catch (e) {
+          alert(e);
+        }
+      }
+      downloadFile(fileUrl, fname);
+    },
+
+    getSampleFile(url, fname) {
+      var xhr = new XMLHttpRequest();
+      xhr.open('get', url, false);
+      // xhr.responseType = 'blob'
+      xhr.send(null);
+      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
+        // console.log(xhr.response);
+        alert(xhr.response);
+        let dataObj = xhr.response;
+        // let dataObj = new Blob([xhr.response], { type: 'text/plain' })
+        this.cordovaDownFile(dataObj, fname);
+      }
+    },
+    cordovaDownFile(blob, fname) {
+      function writeFile(fileEntry, dataObj) {
+        fileEntry.createWriter(function (fileWriter) {
+          fileWriter.onwriteend = function () {
+            alert('Successful file write...');
+            alert(fileEntry.fullPath);
+          };
+
+          fileWriter.onerror = function (e) {
+            alert('Failed file write: ' + e.toString());
+          };
+
+          fileWriter.write(dataObj);
+        });
+      }
+
+      const onError = function (msg) {
+        alert('error:' + msg.code);
+        alert(Object.keys(msg) + '');
+        alert('error:' + msg.toString());
+      };
+
+      let downDir = cordova.file.externalRootDirectory + 'Download/';
+      // alert(downDir)
+      window.resolveLocalFileSystemURL(
+        downDir,
+        function (fdir) {
+          alert(fdir);
+          fdir.getFile(
+            fname,
+            { create: true },
+            function (file) {
+              file.createWriter(function (fileWriter) {
+                alert('Writing content to file');
+                fileWriter.onwriteend = function () {
+                  alert('Successful file write...');
+                };
+
+                fileWriter.onerror = function (e) {
+                  alert('Failed file write: ' + e.toString());
+                };
+                fileWriter.write(blob);
+              }, onError);
+            },
+            onError
+          );
+        },
+        onError
+      );
+      // window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function (fs) {
+      //     // Make sure you add the domain name to the Content-Security-Policy <meta> element.
+      //     alert('file system open: ' + fs.name)
+      //     fs.root.getFile(fname, { create: true, exclusive: false },
+      //         function (fileEntry) {
+      //           alert("fileEntry is file?" + fileEntry.isFile.toString())
+      //           writeFile(fileEntry, blob);
+      //       }, onError);
+      // },onError);
+    },
+
+    downloadFile(url) {
+      const fileUrl = url + '?response-content-type=application/octet-stream'; // Replace with your file URL
+      let urspll = url.split('/');
+      let fname = urspll[urspll.length - 1];
+      if (window.cordova) {
+        try {
+          this.checkPermission(url, fname);
+        } catch (e) {
+          alert(e);
+        }
+      } else {
+        this.onlyDownFile(url);
+      }
+    },
+
+    handleDown(row) {
+      let rawImg = this.rawImags[row];
+      let jFile = this.jFiles[row];
+      if (!jFile) {
+        this.$message({
+          message: '链接获取失败,建议在预览页面查看矫正文件是否上传',
+          type: 'error'
+        });
+        return;
+      }
+      let files = [
+        { url: jFile, name: '布点文件' },
+        { url: rawImg, name: '原图' }
+      ];
+      files.forEach((url) => {
+        const iframe = document.createElement('iframe');
+        iframe.style.display = 'none';
+        iframe.style.height = 0;
+        iframe.src = url.url;
+        document.body.appendChild(iframe);
+        // 不能马上将iframe进行删除,否者会出现马上取消的情况
+        setTimeout(() => {
+          iframe.remove();
+        }, 3 * 1000);
+      });
+    },
+
+    downCamParamFile() {
+      let jfile = this.jFiles[1];
+      if (!jfile) {
+        this.$message({
+          message: '请先预览布点文件再下载',
+          type: 'error'
+        });
+        return;
+      } else if (!this.camparamUptime[1]) {
+        this.$message({
+          message: '该摄像头未矫正',
+          type: 'error'
+        });
+        return;
+      }
+      let cpfile = jfile.substring(0, jfile.length - 4) + 'yml';
+      const iframe = document.createElement('iframe');
+      iframe.style.display = 'none';
+      iframe.style.height = 0;
+      iframe.src = cpfile;
+      document.body.appendChild(iframe);
+      // 不能马上将iframe进行删除,否者会出现马上取消的情况
+      setTimeout(() => {
+        iframe.remove();
+      }, 3 * 1000);
+    },
+
+    checkUpFIrr(up_type, cindex, loading) {
+      this.$http.deploy
+        .getExamSnapShot({
+          exam_id: this.examId,
+          no_req: 1,
+          qindex: cindex,
+          school_id: this.schoolId,
+          cam_path: this.ruleForm.cam_path,
+          anchor_path: this.ruleForm.anchor_path,
+          cam_param_path: this.ruleForm.cam_param_path,
+          up_type: up_type
+        })
+        .then((res) => {
+          console.log(res, 'checkUpFIrr');
+          if (up_type == 'json') {
+            let ach_err = res.data.anchor_uptime[cindex + '_error'];
+            if (ach_err == '文件校验中,请稍等') {
+              this.$message.info(ach_err);
+            } else {
+              if (loading) {
+                setTimeout(() => {
+                  loading.close();
+                  loading = null;
+                }, 1000);
+              }
+              if (this.checkUpFID) clearInterval(this.checkUpFID);
+              if (ach_err) {
+                this.$message.error('布点文件校验失败,原因:' + ach_err);
+              } else {
+                this.$message.success('布点文件上传成功!');
+                this.setFormPath(cindex, up_type);
+              }
+            }
+          } else if (up_type == 'yml') {
+            let camP_err = res.data.camparam_uptime[cindex + '_error'];
+            if (camP_err == '上传文件校验中,请稍等') {
+              this.$message.info(ach_err);
+            } else {
+              if (loading) {
+                setTimeout(() => {
+                  loading.close();
+                  loading = null;
+                }, 1000);
+              }
+              if (this.checkUpFID) clearInterval(this.checkUpFID);
+              if (camP_err) {
+                this.$message.error('矫正文件校验失败,原因:' + camP_err);
+              } else {
+                clearInterval(this.checkUpFID);
+                this.$message.success('矫正文件上传成功!');
+                this.setFormPath(cindex, up_type);
+              }
+            }
+          }
+        });
+    },
+
+    setFormPath(cindex, up_type) {
+      this.showPlayer = false;
+      if (up_type == 'yml') {
+        if (cindex == 1 && !this.ruleForm.cam_param_path) {
+          this.ruleForm.cam_param_path = 'settings/cam_param_files/' + this.examId + '_' + cindex + '.yml';
+        }
+      } else {
+        if (cindex == 1 && !this.ruleForm.anchor_path) {
+          this.ruleForm.anchor_path = 'settings/anchor_files/' + this.examId + '_' + cindex + '.json';
+        } else if (cindex == 0 && !this.ruleForm.anchor_face_path) {
+          this.ruleForm.anchor_face_path = 'settings/anchor_files/' + this.examId + '_' + cindex + '.json';
+        } else if (cindex == 2 && !this.ruleForm.anchor_0_path) {
+          this.ruleForm.anchor_0_path = 'settings/anchor_files/' + this.examId + '_' + cindex + '.json';
+        }
+      }
+    },
+
+    handleUpload(res, f, flist, loading = null) {
+      if (this.checkUpFID) {
+        clearInterval(this.checkUpFID);
+      }
+      if (res.status != 200) {
+        this.message.error(res.message);
+        return;
+      }
+      let cindex = Number(res.cindex);
+      let up_type = res.up_type;
+      console.log(res, 'handleUploadhandleUpload');
+      clearInterval(this.checkUpFID);
+      this.checkUpFID = setInterval(this.checkUpFIrr, 2000, up_type, cindex, loading);
+    },
+
+    async getExamConf() {
+      await this.$http.deploy.getExamSettings({ school_id: this.schoolId, exam_id: this.examId }).then((res) => {
+        // this.ruleForm = Object.assign({}, res.data)
+        if (res.status != 200) {
+          this.$message({
+            message: res.message,
+            type: 'error'
+          });
+          return;
+        }
+        this.uutoken = res.uutoken;
+        if (res.localip) {
+          this.lsrsHost = res.localip;
+        }
+        if (res.haslocalsrs) {
+          if (res.haslocalsrs == '0') {
+            this.hasLocSRS = true;
+          }
+        }
+        let version = 0;
+        if (res.version) {
+          version = res.version;
+          console.log('version: ', version);
+        }
+
+        Object.keys(this.ruleForm).forEach((key) => {
+          // console.log(key, res.data[key], "11112222333")
+          if (key in res.data) {
+            this.ruleForm[key] = res.data[key];
+          } else if (!(key in this.examFormConf['required'] || key in this.examFormConf['advanced'] || key in this.examFormConf['reserved'])) {
+            delete this.ruleForm[key];
+          }
+          if (version < 2.5) {
+            delete this.ruleForm['flag_check_singleleg_jump'];
+            if (this.exam_name == 'situp') {
+              delete this.ruleForm['flag_check_elbow'];
+            }
+          }
+        });
+        this.rawFormData = JSON.stringify(this.ruleForm);
+        console.log(res, 'finalformres');
+        if (!this.hasLocSRS) {
+          this.checkLocRSR();
+        }
+      });
+    },
+
+    submitForm(formName, restart) {
+      this.$refs[formName].validate((valid) => {
+        if (valid) {
+          let tmpFdata = JSON.stringify(this.ruleForm);
+          this.$http.deploy.upExamSettings({ school_id: this.schoolId, exam_id: this.examId, js_data: tmpFdata, restart }).then((res) => {
+            this.rawFormData = tmpFdata;
+            if (res.message == 'ok') {
+              this.$message({
+                message: '更新成功',
+                type: 'success'
+              });
+            } else {
+              this.$message({
+                message: res.message,
+                type: 'error'
+              });
+            }
+            console.log(res);
+          });
+        } else {
+          console.log('error submit!!');
+          return false;
+        }
+      });
+    },
+    clearFormValue(field) {
+      this.ruleForm[field] = '';
+    },
+    resetForm(formName) {
+      this.$refs[formName].resetFields();
+    },
+    back() {
+      this.$router.go(-1);
+      // this.$router.back()
+    },
+    getEName() {
+      let ename = this.exam_name;
+      let posi = '';
+      if (this.isShortRun) {
+        ename = 'shortrun';
+      } else if (this.isLongRun) {
+        ename = 'longrun';
+      } else if (this.isBackRun) {
+        ename = 'backrun';
+      }
+      if (this.camIndex == 0) {
+        if (ename.indexOf('run') > -1) {
+          ename += '_st';
+        } else {
+          ename = 'face';
+        }
+      } else if (this.camIndex === 1) {
+        if (this.isDisExams) {
+          posi = 'left';
+        }
+      } else if (this.camIndex === 2) {
+        posi = '2';
+      }
+      return [ename, posi];
+    },
+    loadAnchorFile(file) {
+      let reader = new FileReader(); // 新建一个FileReader
+      reader.readAsText(file, 'UTF-8'); // 读取文件
+      reader.onload = (evt) => {
+        // 读取文件完毕执行此函数
+        console.log(evt.target.result, 'evt.target.result');
+        const dataJson = JSON.parse(evt.target.result);
+        console.log(dataJson, 'dataJson');
+        this.anchorNum = dataJson.num || 1;
+        this.runStartAnchor(dataJson.names, dataJson.connects, dataJson.keypoints, dataJson.imagesize);
+      };
+      return false;
+    },
+
+    saveAnchor() {
+      if (!this.names) {
+        this.$message({ message: '请进入布点或加载布点文件', type: 'error' });
+        return;
+      }
+      let keypoints = [];
+      const loading = this.$loading({
+        lock: true,
+        text: '正在保存中...',
+        spinner: 'el-icon-loading'
+        // background: 'rgba(0, 0, 0, 0.7)'
+      });
+      this.points.forEach((point) => {
+        keypoints.push(parseInt((point.x - this.xPos) * (this.imgSize[0] / this.scaledWidth)));
+        keypoints.push(parseInt((point.y - this.yPos) * (this.imgSize[1] / this.scaledHeight)));
+      });
+      let jsData = { names: this.names, imagesize: this.imgSize, connects: this.connects, keypoints: keypoints, num: this.anchorNum };
+      this.$http.deploy.saveAnchorPt({ exam_id: this.examId, cindex: this.camIndex, school_id: this.schoolId, js_data: jsData }).then((res) => {
+        if (res.status == 200) {
+          this.handleUpload(res, null, [], loading);
+          this.rawPoints = JSON.stringify(this.points);
+        }
+      });
+    },
+
+    lastAnchor() {
+      this.btnDis = true;
+      let url =
+        '//aiexam-data.oss-cn-shenzhen.aliyuncs.com/midexam/uploaded_files/settings/' +
+        this.uutoken +
+        '/' +
+        this.examId +
+        '_' +
+        this.camIndex +
+        '.json' +
+        '?' +
+        Math.random();
+      this.downAnchorFile(url, 'load', 'anchor_config', this.camIndex);
+    },
+
+    upAndDownPFile(cindex, ftype = 'json') {
+      let url =
+        '//aiexam-data.oss-cn-shenzhen.aliyuncs.com/midexam/uploaded_files/settings/' + this.uutoken + '/' + this.examId + '_' + cindex + '.' + ftype;
+      console.log(url, 'upAndDownPFile');
+      let upType = 'anchor_config';
+      if (ftype == 'yml') {
+        upType = 'camera_param';
+      }
+      this.downAnchorFile(url, 'down', upType, cindex);
+    },
+
+    downAnchorFile(url, _type = 'load', upType = 'anchor_config', cindex = 1) {
+      this.upSnapShotInfos(0, upType);
+      this.$message({
+        message: '正在获取文件,请稍等',
+        type: 'warning'
+      });
+      setTimeout(() => {
+        axios
+          .get(url)
+          .then((response) => {
+            console.log(response);
+            let anchorErr;
+            if (upType == 'anchor_config') {
+              anchorErr = this.anchorUptime['error'] || this.anchorUptime[cindex + '_error'];
+            } else {
+              anchorErr = this.camparamUptime[cindex + '_error'];
+            }
+            this.btnDis = false;
+            if (anchorErr) {
+              clearInterval(this.lstAnchorIvalId);
+              this.$message({
+                message: anchorErr + ',无法获取文件',
+                type: 'error'
+              });
+              return;
+            }
+            if (this.lstAnchorIvalId) {
+              clearInterval(this.lstAnchorIvalId);
+            }
+            if (_type == 'load') {
+              this.stdAnchObj = response.data;
+              this.$message({
+                message: '上次布点加载成功。',
+                type: 'success'
+              });
+              this.runStartAnchor(this.stdAnchObj.names, this.stdAnchObj.connects, this.stdAnchObj.keypoints, this.stdAnchObj.imagesize);
+            } else if (_type == 'down') {
+              this.downloadFile(url);
+            } else {
+            }
+          })
+          .catch((error) => {
+            if (upType == 'anchor_config') {
+              this.btnDis = false;
+              let anchorErr = this.anchorUptime['error'] || this.anchorUptime[this.camIndex + '_error'];
+              if (anchorErr) {
+                if (this.lstAnchorIvalId) {
+                  clearInterval(this.lstAnchorIvalId);
+                  this.btnDis = false;
+                }
+                this.$message({
+                  message: anchorErr + ',无法获取上次布点',
+                  type: 'error'
+                });
+                return;
+              }
+              // this.lstAnchorIvalId = setInterval(
+              //    this.downAnchorFile, 3000, url, 'down', upType)
+            } else {
+              let anchorErr = this.camparamUptime['error'] || this.camparamUptime[this.camIndex + '_error'];
+              if (anchorErr) {
+                this.$message({
+                  message: anchorErr + ',无法获取矫正文件',
+                  type: 'error'
+                });
+                return;
+              }
+            }
+          });
+      }, 3500);
+    },
+    startAnchor(lsturl = '') {
+      let ep = this.getEName();
+      let ename = ep[0];
+      let posi = ep[1];
+      let url = `https://aiexam-data.oss-cn-shenzhen.aliyuncs.com/midexam/uploaded_files/std_anchors/${ename}_${posi}.json`;
+      axios
+        .get(url)
+        .then((response) => {
+          console.log(response);
+          this.stdAnchObj = response.data;
+          this.runStartAnchor(this.stdAnchObj.names, this.stdAnchObj.connects, this.stdAnchObj.keypoints, this.stdAnchObj.imagesize);
+        })
+        .catch((error) => {
+          if (ename in this.anchorSetsObj) {
+            this.stdAnchObj = this.anchorSetsObj[ename];
+          } else {
+            this.stdAnchObj = this.anchorSetsObj['basic'];
+          }
+          this.runStartAnchor(this.stdAnchObj.names, this.stdAnchObj.connects);
+          console.log(this.stdAnchObj, 'this.stdAnchObj');
+          return null;
+        });
+    },
+
+    calculateDistance(touch1, touch2) {
+      const dx = touch1.clientX - touch2.clientX;
+      const dy = touch1.clientY - touch2.clientY;
+      return Math.sqrt(dx * dx + dy * dy);
+    },
+
+    runStartAnchor(names = null, connects = null, kPoints = null, imagesize = null) {
+      var vm = this;
+      let video = this.video;
+      this.video.style.display = 'none';
+      const canvas = document.getElementById('canvas');
+      const ctx = canvas.getContext('2d');
+      // const aliplayer = this.$refs['playerRef'];
+      // aliplayer.pause()
+      canvas.height = this.windowHeight;
+      canvas.width = this.windowWidth;
+      if (this.cIntervalId) {
+        clearInterval(this.cIntervalId);
+      }
+      this.lines = [];
+      let anchorObj = this.stdAnchObj;
+      if (!connects) {
+        this.connects = [];
+        this.lines = anchorObj['lines'];
+        this.lines.forEach((line) => {
+          this.connects.push(line[0]);
+          this.connects.push(line[1]);
+        });
+      } else {
+        this.connects = connects;
+        for (let l = 0; l < connects.length / 2; l++) {
+          this.lines.push([connects[2 * l], connects[2 * l + 1]]);
+        }
+      }
+      if (!names) {
+        this.names = anchorObj['names'];
+      } else this.names = names;
+      if (!imagesize) {
+        imagesize = this.imgSize;
+      }
+      this.points = [];
+
+      if (!kPoints) {
+        this.names.forEach((name, idx) => {
+          let ix = idx % 16;
+          let iy = parseInt(idx / 16);
+          let x = (10 + 40 * ix) / (imagesize[0] / this.scaledWidth) + this.xPos;
+          let y = (40 + 40 * iy) / (imagesize[1] / this.scaledHeight) + this.yPos;
+          this.points.push({ x: x, y: y, name: name });
+        });
+      } else {
+        console.log(imagesize, 'imagesize', this.scaledWidth, this.xPos);
+        this.names.forEach((name, idx) => {
+          let x = kPoints[idx * 2] / (imagesize[0] / this.scaledWidth) + this.xPos;
+          let y = kPoints[idx * 2 + 1] / (imagesize[1] / this.scaledHeight) + this.yPos;
+          this.points.push({ x: x, y: y, name: name });
+        });
+      }
+      this.rawPoints = JSON.stringify(this.points);
+      // Define dragging variables
+      var isDragging = false;
+      var dragIndex = -1;
+      var isZoom = false;
+      // var xRate = vm.scaledWidth/this.video.videoWidth
+      // var yRate = vm.scaledHeight/this.video.videoHeight
+      console.log(vm.yPos, vm.xPos, vm.scaledWidth, vm.scaledHeight);
+      ctx.clearRect(0, 0, canvas.width, canvas.height);
+      run();
+
+      function scaledX(xal) {
+        return (xal - vm.xPos) * vm.zoomRatio + vm.xPos;
+      }
+      function scaledY(yval) {
+        return (yval - vm.yPos) * vm.zoomRatio + vm.yPos;
+      }
+
+      function pointSelect(e) {
+        if (isDragging || isZoom) {
+          return;
+        }
+
+        const touchPoints = e.touches;
+        // if (!touchPoints) {
+        //   return
+        // }
+        if (touchPoints && touchPoints.length === 2) {
+          const touch1 = touchPoints[0];
+          const touch2 = touchPoints[1];
+          vm.startDistance = vm.calculateDistance(touch1, touch2);
+          this.lastZoom = vm.zoomRatio;
+          isZoom = true;
+        } else {
+          const rect = canvas.getBoundingClientRect();
+          if (e.touches) {
+            let tcl = e.touches.length;
+            let tctx = 0;
+            let tcty = 0;
+            console.log(typeof e.touches);
+            Object.keys(e.touches).forEach((key) => {
+              let touch = e.touches[key];
+              tctx += touch.clientX;
+              tcty += touch.clientY;
+            });
+            e.clientX = tctx / tcl;
+            e.clientY = tcty / tcl;
+          }
+          console.log(e, rect.left, rect.top, '!!!');
+          const mouseX = e.clientX - rect.left;
+          const mouseY = e.clientY - rect.top;
+          // Check if mouse is over a point
+
+          dragIndex = vm.points.findIndex((point) => {
+            return Math.abs(scaledX(point.x) - mouseX) <= 18 && Math.abs(scaledY(point.y) - mouseY) <= 18;
+          });
+          // Set dragging variables
+          if (dragIndex !== -1) {
+            isDragging = true;
+            this.dragStartX = scaledX(vm.points[dragIndex].x);
+            this.dragStartY = scaledY(vm.points[dragIndex].y);
+          }
+        }
+      }
+
+      function drag(e) {
+        if (isDragging) {
+          e.preventDefault();
+          const rect = canvas.getBoundingClientRect();
+          if (e.touches) {
+            let tcl = e.touches.length;
+            console.log(tcl, 'tcl');
+            let tctx = 0;
+            let tcty = 0;
+            Object.keys(e.touches).forEach((key) => {
+              let touch = e.touches[key];
+              tctx += touch.clientX;
+              tcty += touch.clientY;
+            });
+            e.clientX = tctx / tcl;
+            e.clientY = tcty / tcl;
+          }
+          let currentx;
+          let currenty;
+          console.log(vm.xPos, e.clientX, rect.left, 'Dragdrag...');
+          if (e.clientX > vm.scaledWidth * vm.zoomRatio + vm.xPos + rect.left) {
+            currentx = vm.scaledWidth * vm.zoomRatio + vm.xPos + rect.left;
+          } else if (e.clientX < vm.xPos + rect.left) {
+            currentx = vm.xPos + rect.left;
+          } else {
+            currentx = e.clientX;
+          }
+          if (e.clientY > vm.scaledHeight * vm.zoomRatio + vm.yPos + rect.top) {
+            currenty = vm.scaledHeight * vm.zoomRatio + vm.yPos + rect.top;
+            // e.clientY = currenty
+          } else if (e.clientY < vm.yPos + rect.top) {
+            currenty = vm.yPos + rect.top;
+          } else {
+            currenty = e.clientY;
+          }
+
+          // console.log("dragged111", rect.left, rect.top, e)
+          // const mouseX = e.clientX - rect.left;
+          // const mouseY = e.clientY - rect.top;
+          const mouseX = currentx - rect.left;
+          const mouseY = currenty - rect.top;
+          // Calculate distance dragged
+          const deltaX = mouseX - this.dragStartX;
+          const deltaY = mouseY - this.dragStartY;
+          this.dragStartX = mouseX;
+          this.dragStartY = mouseY;
+          // Update dragged point
+          vm.points[dragIndex].y += deltaY / vm.zoomRatio;
+          // vm.points[dragIndex].y + deltaY
+          vm.points[dragIndex].x += deltaX / vm.zoomRatio;
+          // Redraw points and lines
+          drawPointsAndLines();
+        } else if (isZoom) {
+          const touchPoints = e.touches;
+          if (touchPoints && touchPoints.length === 2) {
+            const touch1 = touchPoints[0];
+            const touch2 = touchPoints[1];
+            vm.currentDistance = vm.calculateDistance(touch1, touch2);
+            if ((vm.currentDistance * this.lastZoom) / vm.startDistance < 1) {
+              vm.zoomRatio = 1;
+            } else {
+              vm.zoomRatio = (this.lastZoom * vm.currentDistance) / vm.startDistance;
+            }
+            canvas.width = vm.windowWidth * vm.zoomRatio;
+            canvas.height = vm.windowHeight * vm.zoomRatio;
+            drawPointsAndLines();
+          }
+        }
+      }
+      function stopDrag(e) {
+        isDragging = false;
+        dragIndex = -1;
+        isZoom = false;
+      }
+
+      function drawPointsAndLines() {
+        ctx.clearRect(0, 0, canvas.width, canvas.height);
+        ctx.drawImage(vm.video, vm.xPos, vm.yPos, vm.scaledWidth * vm.zoomRatio, vm.scaledHeight * vm.zoomRatio);
+
+        // Draw lines
+        ctx.strokeStyle = 'red';
+        ctx.lineWidth = 0.6 * vm.zoomRatio;
+        vm.lines.forEach((line) => {
+          const [startIndex, endIndex] = line;
+          const startPoint = vm.points[startIndex];
+          const endPoint = vm.points[endIndex];
+          ctx.beginPath();
+          ctx.moveTo(scaledX(startPoint.x), scaledY(startPoint.y));
+          ctx.lineTo(scaledX(endPoint.x), scaledY(endPoint.y));
+          ctx.stroke();
+        });
+
+        vm.points.forEach((point, index) => {
+          ctx.beginPath();
+          // let scaledPx = scaledX(point.x)
+          // let scaledPy = scaledY(point.y)
+          ctx.arc(scaledX(point.x), scaledY(point.y), 3.5 * vm.zoomRatio, 0, 2 * Math.PI);
+          ctx.fillStyle = 'red';
+          ctx.fillText(point.name, scaledX(point.x), scaledY(point.y) - 5);
+          ctx.fill();
+          ctx.stroke();
+          // Draw label for dragged point
+          if (index === dragIndex) {
+            ctx.fillStyle = 'origin';
+            // ctx.fillText(
+            //   `(Math.f${point.x}, ${point.y})`, point.x + 5, point.y - 5);
+          }
+        });
+      }
+
+      // function wheelAction(event) {
+      //   event.preventDefault();
+      // }
+
+      function run() {
+        if (vm.cIntervalId) {
+          clearInterval(vm.cIntervalId);
+        }
+        vm.cIntervalId = setInterval(drawPointsAndLines, 50);
+        canvas.addEventListener('mousedown', pointSelect);
+        canvas.addEventListener('touchstart', pointSelect);
+        canvas.addEventListener('mousemove', drag);
+        canvas.addEventListener('touchmove', drag, { passive: false });
+        canvas.addEventListener('mouseup', stopDrag);
+        canvas.addEventListener('mouseleave', stopDrag);
+        canvas.addEventListener('touchend', stopDrag);
+        // canvas.addEventListener('wheel', wheelAction);
+      }
+    },
+
+    copyContent(content, item) {
+      this.$copyText(content).then(
+        (e) => {
+          this.ruleForm[item] = content;
+          this.$message({
+            message: '复制成功',
+            type: 'success'
+          });
+          // this.showCamUrl = false
+        },
+        function (e) {
+          this.$message({
+            message: '复制失败,请手动复制',
+            type: 'error'
+          });
+        }
+      );
+    },
+
+    SrsRtcPlayerAsync() {
+      var self = {};
+      self.play = async function (url) {
+        var conf = self.__internal.prepareUrl(url);
+        self.pc.addTransceiver('audio', { direction: 'recvonly' });
+        self.pc.addTransceiver('video', { direction: 'recvonly' });
+
+        var offer = await self.pc.createOffer();
+        await self.pc.setLocalDescription(offer);
+        var session = await new Promise(function (resolve, reject) {
+          // @see https://github.com/rtcdn/rtcdn-draft
+          var data = {
+            api: conf.apiUrl,
+            tid: conf.tid,
+            streamurl: conf.streamUrl,
+            clientip: null,
+            sdp: offer.sdp
+          };
+          console.log('Generated offer: ', data);
+
+          $.ajax({
+            type: 'POST',
+            url: conf.apiUrl,
+            data: JSON.stringify(data),
+            contentType: 'application/json',
+            dataType: 'json'
+          })
+            .done(function (data) {
+              console.log('Got answer: ', data);
+              if (data.code) {
+                reject(data);
+                return;
+              }
+              resolve(data);
+            })
+            .fail(function (reason) {
+              reject(reason);
+            });
+        });
+        await self.pc.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: session.sdp }));
+        session.simulator = conf.schema + '//' + conf.urlObject.server + ':' + conf.port + '/rtc/v1/nack/';
+        return session;
+      };
+      // Close the player.
+      self.close = function () {
+        self.pc && self.pc.close();
+        self.pc = null;
+      };
+      self.ontrack = function (event) {
+        // https://webrtc.org/getting-started/remote-streams
+        self.stream.addTrack(event.track);
+      };
+      // Internal APIs.
+      self.__internal = {
+        defaultPath: '/rtc/v1/play/',
+        prepareUrl: function (webrtcUrl) {
+          var urlObject = self.__internal.parse(webrtcUrl);
+          // If user specifies the schema, use it as API schema.
+          var schema = urlObject.user_query.schema;
+          schema = schema ? schema + ':' : window.location.protocol;
+          var port = urlObject.port || 1985;
+          if (schema === 'https:') {
+            port = urlObject.port || 1990;
+          }
+          // @see https://github.com/rtcdn/rtcdn-draft
+          var api = urlObject.user_query.play || self.__internal.defaultPath;
+          if (api.lastIndexOf('/') !== api.length - 1) {
+            api += '/';
+          }
+          apiUrl = schema + '//' + urlObject.server + ':' + port + api;
+          for (var key in urlObject.user_query) {
+            if (key !== 'api' && key !== 'play') {
+              apiUrl += '&' + key + '=' + urlObject.user_query[key];
+            }
+          }
+          // Replace /rtc/v1/play/&k=v to /rtc/v1/play/?k=v
+          var apiUrl = apiUrl.replace(api + '&', api + '?');
+          var streamUrl = urlObject.url;
+
+          return {
+            apiUrl: apiUrl,
+            streamUrl: streamUrl,
+            schema: schema,
+            urlObject: urlObject,
+            port: port,
+            tid: Number(parseInt(new Date().getTime() * Math.random() * 100))
+              .toString(16)
+              .substr(0, 7)
+          };
+        },
+        parse: function (url) {
+          // @see: http://stackoverflow.com/questions/10469575/how-to-use-location-object-to-parse-url-without-redirecting-the-page-in-javascri
+          var a = document.createElement('a');
+          a.href = url.replace('rtmp://', 'http://').replace('webrtc://', 'http://').replace('rtc://', 'http://');
+
+          var vhost = a.hostname;
+          var app = a.pathname.substr(1, a.pathname.lastIndexOf('/') - 1);
+          var stream = a.pathname.substr(a.pathname.lastIndexOf('/') + 1);
+
+          // parse the vhost in the params of app, that srs supports.
+          app = app.replace('...vhost...', '?vhost=');
+          if (app.indexOf('?') >= 0) {
+            var params = app.substr(app.indexOf('?'));
+            app = app.substr(0, app.indexOf('?'));
+
+            if (params.indexOf('vhost=') > 0) {
+              vhost = params.substr(params.indexOf('vhost=') + 'vhost='.length);
+              if (vhost.indexOf('&') > 0) {
+                vhost = vhost.substr(0, vhost.indexOf('&'));
+              }
+            }
+          }
+          if (a.hostname === vhost) {
+            var re = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
+            if (re.test(a.hostname)) {
+              vhost = '__defaultVhost__';
+            }
+          }
+          // parse the schema
+          var schema = 'rtmp';
+          if (url.indexOf('://') > 0) {
+            schema = url.substr(0, url.indexOf('://'));
+          }
+          var port = a.port;
+          if (!port) {
+            // Finger out by webrtc url, if contains http or https port, to overwrite default 1985.
+            if (schema === 'webrtc' && url.indexOf(`webrtc://${a.host}:`) === 0) {
+              port = url.indexOf(`webrtc://${a.host}:80`) === 0 ? 80 : 1990;
+            }
+            // Guess by schema.
+            if (schema === 'http') {
+              port = 80;
+            } else if (schema === 'https') {
+              port = 1990;
+            } else if (schema === 'rtmp') {
+              port = 1935;
+            }
+          }
+          var ret = {
+            url: url,
+            schema: schema,
+            server: a.hostname,
+            port: port,
+            vhost: vhost,
+            app: app,
+            stream: stream
+          };
+          self.__internal.fill_query(a.search, ret);
+
+          if (!ret.port) {
+            if (schema === 'webrtc' || schema === 'rtc') {
+              if (ret.user_query.schema === 'https') {
+                ret.port = 1990;
+              } else if (window.location.href.indexOf('https://') === 0) {
+                ret.port = 1990;
+              } else {
+                // For WebRTC, SRS use 1985 as default API port.
+                ret.port = 1985;
+              }
+            }
+          }
+          return ret;
+        },
+        fill_query: function (query_string, obj) {
+          // pure user query object.
+          obj.user_query = {};
+          if (query_string.length === 0) {
+            return;
+          }
+
+          // split again for angularjs.
+          if (query_string.indexOf('?') >= 0) {
+            query_string = query_string.split('?')[1];
+          }
+          var queries = query_string.split('&');
+          for (var i = 0; i < queries.length; i++) {
+            var elem = queries[i];
+            var query = elem.split('=');
+            obj[query[0]] = query[1];
+            obj.user_query[query[0]] = query[1];
+          }
+
+          // alias domain for vhost.
+          if (obj.domain) {
+            obj.vhost = obj.domain;
+          }
+        }
+      };
+
+      self.pc = new RTCPeerConnection(null);
+      self.stream = new MediaStream();
+
+      // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/ontrack
+      self.pc.ontrack = function (event) {
+        if (self.ontrack) {
+          self.ontrack(event);
+        }
+      };
+      return self;
+    }
+  }
+};
+</script>
+
+<style type="text/css">
+.el-dialog__body {
+  padding: 20px !important;
+  max-height: 99vh;
+}
+.el-form-item__label {
+  font-size: 12px;
+}
+
+.el-form-item--small.el-form-item {
+  margin-bottom: 12px;
+}
+.el-collapse {
+  border-top: 1px solid #ebeef5;
+  border-bottom: 0;
+}
+</style>
+<style lang="scss" scoped>
+@import '@/assets/styles/func.scss';
+
+.dashboard {
+  &-container {
+    min-height: calc(100vh - 80px);
+    padding: 24px;
+    background: #f3f6fa;
+  }
+  &-text {
+    font-size: 30px;
+    line-height: 46px;
+  }
+}
+.input45 {
+  width: 40%;
+  margin-right: 5px;
+}
+
+.column {
+  float: left;
+  width: 90%;
+  padding: 2px;
+}
+
+/* Clearfix (clear floats) */
+.row::after {
+  content: '';
+  clear: both;
+  display: table;
+}
+
+.container-main {
+  // background:#FFFFFF;
+  width: 99%;
+  padding: auto;
+  margin: 5px auto;
+  background: #fff;
+  height: calc(100vh - 50px - 30px);
+  overflow-y: scroll;
+  position: relative;
+}
+
+.el-header {
+  // height: getvh(80);
+  margin-bottom: 5px;
+}
+
+.nav1 {
+  display: flex;
+  justify-content: space-between;
+  padding: 15px 0;
+}
+
+.navbar {
+  // padding-top: getvh(25);
+  overflow: hidden;
+  position: relative;
+  background: #fff;
+  border-radius: 4px;
+  box-sizing: border-box;
+  box-shadow: 0px 2px 4px 0px rgb(0 92 220 / 10%);
+}
+</style>

+ 979 - 0
src/views/train/device.vue

@@ -0,0 +1,979 @@
+<template>
+  <div>
+    <Header @confirmExit="confirmExit"></Header>
+    <Transition :enter-active-class="proxy?.animate.dialog.enter" :leave-active-class="proxy?.animate.dialog.leave">
+      <div class="time" v-show="[42].includes(examState)">
+        {{
+        countdownNumFormat
+        }}
+      </div>
+    </Transition>
+    <div class="main">
+      <template v-if="isLongRun">
+        <!--长跑-->
+        <swiper
+          :slides-per-view="testListArr.length >= 2 ? 2 : 1"
+          :slides-per-group="testListArr.length >= 2 ? 2 : 1"
+          :space-between="20"
+          :speed="1200"
+          :modules="[Navigation]"
+          :navigation="{ prevEl: '.swiper-button-prev', nextEl: ' .swiper-button-next' }"
+        >
+          <swiper-slide v-for="(items, indexs) in testListArr " :key="indexs">
+            <div class="main-left main-left2">
+              <div class="trackItem">
+                <TransitionGroup :enter-active-class="proxy?.animate.run.enter">
+                  <div v-for="(item, index) in items" :key="indexs + '_' + index" class="li">
+                    <div class="left">
+                      <div class="track">{{ (index + 1) + (8 * indexs) }}</div>
+                      <div class="userInfo" @click="getChooseStudent(item.track)">
+                        <div class="pic pic2" v-if="item.student_id"><img :src="item.face_pic || item.logo_url" /></div>
+                        <div class="pic" v-else>
+                          <img src="@/assets/images/test/profilePicture.png" />
+                        </div>
+                        <div class="nameBox">
+                          <div class="name">{{ item.student_name || "未检录" }}</div>
+                        </div>
+                      </div>
+                    </div>
+                    <div class="scoreBox">
+                      <div v-if="item.timeStr" class="score">
+                        {{ item.timeStr || "-" }}
+                      </div>
+                      <div v-if="item.student_id && [3, 42].includes(examState)" class="turns">
+                        <span
+                          ><i>{{ item.times !=
+                        undefined ?
+                        item.times.length : 0 }}</i></span
+                        >圈
+                      </div>
+                    </div>
+                    <div class="menuBtn menuBtn2" v-if="examState == 43 && item.student_id">等待开始测试</div>
+                    <div class="menuBtn menuBtn2" v-if="examState == 42 && item.student_id && !item.isfinish">正在测试</div>
+                    <div class="menuBtn" v-if="examState == 3 && !item.timeStr && item.isfinish && item.student_id">异常</div>
+                    <div class="close" @click="close(item)" v-if="examState == 41"></div>
+                  </div>
+                </TransitionGroup>
+              </div>
+            </div>
+          </swiper-slide>
+        </swiper>
+        <!-- 如果需要导航按钮 -->
+        <div v-show="testListArr.length > 2" class="swiper-button-prev swiper-btn swiper-btn-left" slot="button-prev"></div>
+        <div v-show="testListArr.length > 2" class="swiper-button-next swiper-btn swiper-btn-right" slot="button-next"></div>
+      </template>
+      <template v-else>
+        <!--短跑-->
+        <div class="main-left">
+          <div class="trackItem">
+            <TransitionGroup :enter-active-class="proxy?.animate.run.enter">
+              <div v-for="(item, index) in faceStudentList" :key="index" class="li">
+                <div class="left">
+                  <div class="track">{{ item.track }}</div>
+                  <div class="userInfo" @click="getChooseStudent(item.track)">
+                    <div class="pic pic2" v-if="item.student_id"><img :src="item.face_pic || item.logo_url" /></div>
+                    <div class="pic" v-else>
+                      <img src="@/assets/images/test/profilePicture.png" />
+                    </div>
+                    <div class="nameBox">
+                      <div class="name">{{ item.student_name || "未检录" }}</div>
+                    </div>
+                  </div>
+                </div>
+                <div class="scoreBox">
+                  <div v-if="item.timeStr" class="score">
+                    {{ item.timeStr || "-" }}
+                  </div>
+                  <div v-if="isBackRun && item.student_id" class="turns">
+                    往返次数:<span
+                      ><i>{{ item.turns || "0"
+                      }}</i></span
+                    >
+                  </div>
+                </div>
+                <div class="menuBtn" v-if="(examState == 41 || examState == 43) && !item.student_id" @click="getChooseStudent(item.track)">检录</div>
+                <div class="menuBtn menuBtn2" v-if="examState == 43 && item.student_id">等待开始测试</div>
+                <div class="menuBtn menuBtn2" v-if="examState == 42 && item.student_id">正在测试</div>
+                <div class="menuBtn" v-if="examState == 3 && !item.timeStr && item.isfinish && item.student_id">异常</div>
+              </div>
+            </TransitionGroup>
+          </div>
+        </div>
+        <div class="main-right">
+          <ReportList ref="reportListRef" :parameter="parameter" :showQRCode="true" />
+        </div>
+      </template>
+    </div>
+    <!-- <div>当前状态:({{ examState == 3 ? "初始化完成" : examState == 40 ? "创建测试" : examState == 41 ? "正在人脸识别":examState ==43 ? "停止人脸识别" : examState == 42 ? "正在测试" : "请初始化" }})</div> -->
+    <div class="footerBtn">
+      <div class="btn startBtn" @click="getStopFaceStartOneTest" v-if="examState == 3">开始测试</div>
+      <div class="btn" @click="getConfirmEnd" v-if="examState == 42">结 束</div>
+      <div class="btn exitBtn" @click="confirmExit" v-if="examState != 0">退 出</div>
+    </div>
+    <ChooseStudent ref="chooseStudentRef" :selectType="isLongRun ? 'multiple' : 'single'" @returnData="returnStudent" />
+  </div>
+</template>
+
+<script setup name="TrainTest" lang="ts">
+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 dayjs from 'dayjs';
+import dataDictionary from '@/utils/dataDictionary';
+import { Swiper, SwiperSlide } from 'swiper/vue';
+import { Navigation } from 'swiper/modules';
+import 'swiper/css';
+import 'swiper/scss/navigation';
+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 chooseStudentRef = ref();
+const reportListRef = ref();
+const data = reactive<any>({
+  timerManager: {}, //计时器管理
+  parameter: {}, //参数
+  time: {
+    testTime: 0, //时长
+    countdownNum: 0 //计时
+  },
+  userInfo: {}, //用户信息
+  examState: 0, //当前状态
+  resultId: null, //测试ID
+  unit: '', //单位
+  needStart: false, //是否需要按钮
+  faceStudentList: [], //跑道和人信息
+  currentTrack: null, //当前跑道
+  showTestAgain: false, //再测一次按钮
+  isLongRun: false, //是否为长跑项目
+  isBackRun: false, //是否为折返跑项目
+  sid: null //WS的id
+});
+const {
+  timerManager,
+  parameter,
+  time,
+  userInfo,
+  examState,
+  resultId,
+  faceStudentList,
+  currentTrack,
+  unit,
+  needStart,
+  showTestAgain,
+  isLongRun,
+  isBackRun,
+  sid
+} = 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 (isLongRun.value) {
+    //   let num = 80;
+    //   let list: any = [];
+    //   for (let i = 1; i <= num; i++) {
+    //     list.push({ track: i });
+    //   }
+    //   faceStudentList.value = list
+    // }
+  }
+
+  //获取跑道配置
+  if (e.cmd === 'exam_config') {
+    let num = e.data.num_of_tracks;
+    let list: any = [];
+    for (let i = 1; i <= num; 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') {
+    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 == 42) {
+    }
+  }
+  //人脸识别状态
+  if (e.cmd === 'face_check_result') {
+    let list = e.data;
+    returnStudent(list);
+  }
+  //测试结束结果
+  if (e.cmd === 'oneresult') {
+    if (e?.data) {
+      let data = e.data;
+      getAchievement(data);
+    }
+  }
+  //结果生成完成(视频图片)
+  if (e.cmd === 'static_urls_finished') {
+  }
+  //选择学生或测试结束后返回的数据
+  if (e.cmd === 'result_info') {
+  }
+};
+
+/**
+ * 开始识别
+ */
+const getOpenOneTestAndStartFace = async () => {
+  await openOneTest();
+};
+
+/**
+ * 开始测试
+ */
+const getStartOneTest = () => {
+  if (examState.value != 43) {
+    return false;
+  }
+  let list = faceStudentList.value.filter((item: any) => {
+    return item.student_id;
+  });
+  if (!list.length) {
+    proxy?.$modal.msgWarning('请选择人员!');
+    return false;
+  }
+  //停止播报;
+  speckCancel();
+  //和工作站搭配时差版
+  let advanceTime = 0; //提前发送开始的时间
+  let myTime = 0; //枪声时间
+  let myText = ''; //提示框内容
+  if (isLongRun.value) {
+    //音频时长5180毫秒,4120毫秒是播枪声
+    advanceTime = 0;
+    myTime = 4120;
+    myText = '各就位!';
+  } else {
+    //音频时长7010毫秒,5260毫秒是播枪声
+    advanceTime = 1000;
+    myTime = 5260;
+    myText = '各就位,预备!';
+  }
+  let loading = ElLoading.service({ text: myText, background: 'rgba(0, 0, 0, 0.8)', customClass: `sports ${parameter.value.project}` });
+  speckText(myText);
+  setTimeout(() => {
+    startOneTest(data == null, () => {});
+    loading?.close();
+  }, advanceTime);
+  setTimeout(() => {
+    //显示再测一次按钮
+    showTestAgain.value = true;
+    //计时项目才开
+    if (needStart.value == true) {
+    }
+    //时间为0的为正计时,大于0的为倒计时
+    if (time.value.testTime == 0) {
+      getCounting('+');
+    } else {
+      getCounting('-');
+    }
+  }, myTime);
+
+  // 立即开版本
+  // startOneTest(data == null, () => {
+  //   //显示再测一次按钮
+  //   showTestAgain.value = true;
+  //   //计时项目才开
+  //   if (needStart.value == true) {
+  //     //时间为0的为正计时,大于0的为倒计时
+  //     if (time.value.testTime == 0) {
+  //       getCounting("+");
+  //     } else {
+  //       getCounting("-");
+  //     }
+  //   } else {
+  //     speckText("请开始测试");
+  //   }
+  // })
+};
+
+/**
+ * 确认退出
+ */
+const getConfirmEnd = async () => {
+  let txt = '是否结束';
+  await proxy?.$modal.confirm(txt);
+  getClearTimer(); //清除计时器
+  speckCancel(); //停止播报;
+  let loading = ElLoading.service({ text: '请稍等...', background: 'rgba(0, 0, 0, 0.8)', customClass: `sports ${parameter.value.project}` });
+  showTestAgain.value = false;
+  //测试中
+  if (examState.value == 42) {
+    await finishOneTest();
+  }
+  //其他状态
+  if (examState.value > 3) {
+    await closeOneTest();
+  }
+  if (!isLongRun.value) {
+    await openOneTest();
+    await startFace();
+  }
+  loading?.close();
+  let loadingTime = setTimeout(() => {
+    loading?.close();
+    clearTimeout(loadingTime);
+  }, 10000);
+};
+
+/**
+ * 确认退出
+ */
+const confirmExit = () => {
+  proxy?.$modal
+    .confirm('确定退出吗?')
+    .then(() => {
+      getExit();
+    })
+    .finally(() => {});
+};
+
+/**
+ * 退出
+ */
+const getExit = () => {
+  getClearTimer(); //清除计时器
+  speckCancel(); //停止播报;
+  examEnds(); //通知工作站关闭
+  window.onbeforeunload = null; //移除事件处理器
+  if (parameter.value.taskId) {
+    router.push({ path: '/test' });
+  } else {
+    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) {
+    if (needStart.value) {
+      proxy?.$modal.msgWarning('请点击开始识别');
+    } else {
+      proxy?.$modal.msgWarning('请稍等...');
+    }
+    return false;
+  }
+  if (examState.value == 43) {
+    proxy?.$modal.msgWarning('请点击重新识别');
+    return false;
+  }
+  if (examState.value == 42) {
+    proxy?.$modal.msgWarning('正在测试...');
+    return false;
+  }
+  currentTrack.value = track;
+  // stopFace();
+  chooseStudentRef.value.open();
+};
+
+/**
+ * 返回被选学生
+ */
+const returnStudent = (data: any) => {
+  let ids = faceStudentList.value.map((item: any, index: any) => {
+    return item.student_id;
+  });
+  //排除掉已经存在的
+  let newList = data.filter((item: any, index: any) => {
+    return !ids.includes(item.id);
+  });
+  let list = newList.map((item: any, index: any) => {
+    let obj = {
+      result_id: resultId.value,
+      face_pic: item.face_pic || item.logo_url,
+      student_id: item.id,
+      student_name: item.name,
+      gender: item.gender
+    };
+    return obj;
+  });
+  faceStudentList.value.push(...list);
+};
+
+/**
+ * 清除历史记录
+ */
+const cleanData = () => {
+  time.value.countdownNum = time.value.testTime;
+  showTestAgain.value = false;
+  if (isLongRun.value) {
+    faceStudentList.value = [];
+  } else {
+    //重置全部
+    let list = faceStudentList.value.map((item: any) => {
+      let obj = {
+        student_id: null,
+        track: item.track,
+        isfinish: false
+      };
+      return obj;
+    });
+    faceStudentList.value = list;
+  }
+};
+
+/**
+ * 自动初始化项目
+ */
+const initProject = () => {
+  //停止计时
+  getClearTimer('countdownTimer');
+  time.value.countdownNum = parameter.value.time * 60;
+  getOpenOneTestAndStartFace();
+};
+
+/**
+ * 倒计时
+ */
+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) => {
+  if (!data) {
+    return;
+  }
+  let dataList = data.map((item: any) => {
+    if (item.track && item.times != 0) {
+      if (typeof item.times === 'string') {
+        item.times = JSON.parse(item.times);
+      }
+      if (typeof item.times === 'object') {
+        item.timeStr = proxy?.$utils.runTime(item.times[item.times.length - 1], false, isLongRun.value);
+        item.turns = Math.floor(item.times.length / 2);
+        if (item.times.length) {
+          item.timeTotal = item.times.reduce((total: any, num: any) => {
+            return total + Number(num);
+          });
+        } else {
+          item.timeTotal = 0;
+        }
+      } else {
+        item.timeStr = proxy?.$utils.runTime(item.times, false, isLongRun.value);
+        item.turns = 0;
+        item.timeTotal = 0;
+      }
+    }
+    return item;
+  });
+  faceStudentList.value.forEach((item: any, index: any) => {
+    let obj = dataList.find((items: any) => {
+      return parseInt(item.track) == parseInt(items.track);
+    });
+    //加this.result_id == obj.result_id是避免抢跑的时候上一把成绩没返回会被覆盖的可能,要新的ID才能赋值
+    if (obj != undefined && resultId.value == obj.result_id) {
+      if (parseInt(obj.track) === parseInt(item.track) || isLongRun.value) {
+        faceStudentList.value[index].timeStr = obj.timeStr;
+        faceStudentList.value[index].times = obj.times;
+        faceStudentList.value[index].timeTotal = obj.timeTotal;
+        faceStudentList.value[index].tid_num = obj.tid_num;
+        faceStudentList.value[index].score = obj.score;
+        faceStudentList.value[index].isfinish = obj.isfinish;
+        faceStudentList.value[index].turns = obj.turns;
+        faceStudentList.value[index].result_id = obj.result_id;
+      } else {
+        faceStudentList.value[index].trackFalse = true;
+        faceStudentList.value[index].timeStr = '跑道错误';
+      }
+    }
+  });
+};
+
+/**
+ * 移除长跑待测试列表
+ */
+const close = (data: any) => {
+  faceStudentList.value = JSON.parse(JSON.stringify(faceStudentList.value)).filter((item: any) => {
+    return item.student_id != data.student_id;
+  });
+};
+
+/**
+ * 长跑分页并按圈数排序
+ */
+const testListArr: any = computed(() => {
+  // 按圈数分组排序
+  let list = faceStudentList.value.filter((item: any) => {
+    return item.student_id;
+  });
+  let myArr: any = [];
+  JSON.parse(JSON.stringify(list)).forEach((item: any, index: number) => {
+    let myIndex = 0;
+    if (item.times != undefined) {
+      myIndex = item.times.length;
+    }
+    if (myArr[myIndex] == undefined) {
+      myArr[myIndex] = [];
+    }
+    myArr[myIndex].push(item);
+    if (myArr[myIndex]) {
+      myArr[myIndex].sort((a: any, b: any) => {
+        let val1 = a.timeTotal;
+        let val2 = b.timeTotal;
+        return val1 - val2;
+      });
+    }
+  });
+  let myList = [];
+  if (myArr.length) {
+    for (let i = myArr.length - 1; i >= 0; --i) {
+      if (myArr.hasOwnProperty(i)) {
+        myList.push(...myArr[i]);
+      }
+    }
+  } else {
+    myList = list;
+  }
+  // 分页
+  let myFaceStudentList = myList;
+  let arrList: any = [];
+  let num = 8;
+  let myLength = Math.ceil(myFaceStudentList.length / num);
+  for (let i = 0; i < myLength; i++) {
+    arrList[i] = [];
+    for (let j = 0; j < myFaceStudentList.length; j++) {
+      if (j >= i * num && j < (i + 1) * num) {
+        arrList[i].push(myFaceStudentList[j]);
+      }
+    }
+  }
+  return arrList;
+});
+
+/**
+ * 时间转换
+ */
+const countdownNumFormat = computed(() => {
+  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;
+  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;
+  }
+  //是否折返跑
+  if (project.slice(0, 3) === 'run' && project.includes('x')) {
+    isBackRun.value = true;
+  }
+  //是否长跑
+  if (project.replace('run', '') > 799) {
+    isLongRun.value = true;
+  }
+  //需要开始按钮的项目
+  if (isLongRun.value) {
+    needStart.value = true;
+  }
+  //加载WS
+  initWs({ parameter: parameter.value, testTime: time.value.testTime }, (data: any) => {
+    getMessage(data);
+  });
+  //初始化语音
+  initSpeech();
+  //刷新关闭
+  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 scoped lang="scss">
+$topPadding: 5.19rem;
+$waiPadding: 6.51rem;
+
+.time {
+  width: 20vh;
+  height: 20vh;
+  line-height: 20vh;
+  border-radius: 50%;
+  color: #ff9402;
+  font-size: 7.5vh;
+  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: 50%;
+  top: -4.5vh;
+  margin-right: -10vh;
+  font-family: 'Saira-BlackItalic';
+}
+
+.main {
+  width: calc(100% - ($waiPadding * 2));
+  height: 70vh;
+  padding-top: 10rem;
+  margin: 0 auto;
+  display: flex;
+  justify-content: space-between;
+  overflow: hidden;
+
+  .main-left {
+    width: 71.5%;
+    height: 100%;
+    display: flex;
+    background: linear-gradient(58deg, #092941 -85%, #2a484b 96%);
+    box-shadow: inset 0px 1px 0px 2px rgba(255, 255, 255, 0.4);
+    border-radius: 1.6rem;
+
+    .trackItem {
+      width: 100%;
+      overflow-y: scroll;
+      padding: 0px 10px;
+
+      &::-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);
+      }
+
+      .li {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        border-bottom: 1px solid #475557;
+        height: calc(100% / 8);
+        box-sizing: border-box;
+        padding: 0px 20px 0px 50px;
+
+        .left {
+          display: flex;
+          align-items: center;
+
+          .track {
+            width: 5rem;
+            color: #13ed84;
+            font-size: 2rem;
+            font-family: 'Saira-ExtraBold';
+            margin-right: 1rem;
+          }
+
+          .pic {
+            width: 6.7vh;
+            height: 6.7vh;
+            border-radius: 50%;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            overflow: hidden;
+            margin-right: 15px;
+
+            img {
+              width: 100%;
+            }
+          }
+
+          .pic2 {
+            box-sizing: border-box;
+            border: 1px solid rgba(255, 255, 255, 0.5);
+          }
+
+          .userInfo {
+            display: flex;
+            justify-content: flex-start;
+            align-items: center;
+
+            .nameBox {
+              width: 8rem;
+
+              .name {
+                font-size: 1.5rem;
+                color: #ffffff;
+              }
+            }
+          }
+        }
+
+        .scoreBox {
+          display: flex;
+          align-items: center;
+
+          .score {
+            color: #ffffff;
+            font-size: 2rem;
+            font-family: 'Saira-ExtraBold';
+          }
+
+          .turns {
+            margin-left: 3rem;
+            color: #ffffff;
+            display: flex;
+            align-items: center;
+
+            span {
+              font: 1.5rem;
+              margin: 0 5px;
+            }
+
+            i {
+              font-style: normal;
+              font-size: 1.8rem;
+              font-family: 'Saira-ExtraBold';
+            }
+          }
+        }
+
+        .menuBtn {
+          width: auto;
+          padding: 0 1.2rem;
+          height: 2.8rem;
+          line-height: 2.8rem;
+          border-radius: 1.4rem;
+          color: #ffffff;
+          font-size: 1.5rem;
+          background: linear-gradient(180deg, #ffb200 0%, #ed7905 72%);
+          box-shadow: inset 0px 1px 0px 1px rgba(255, 255, 255, 0.5);
+          display: flex;
+          align-items: center;
+          cursor: pointer;
+        }
+
+        .menuBtn2 {
+          color: #1a293a;
+          background: radial-gradient(159% 126% at 5% 93%, #8effa9 0%, #07ffe7 100%);
+          box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.1874), inset 0px 1px 0px 2px rgba(255, 255, 255, 0.3);
+        }
+
+        .close {
+          width: 2rem;
+          height: 2rem;
+        }
+      }
+    }
+  }
+
+  .main-left2 {
+    width: 100%;
+
+    .trackItem {
+      .li {
+        .scoreBox {
+          .score {
+            font-size: 1.8rem;
+          }
+        }
+      }
+    }
+  }
+
+  .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;
+  }
+
+  .swiper-btn {
+    width: 2.5rem;
+    height: 2.5rem;
+    display: block;
+
+    &::after {
+      display: none;
+    }
+  }
+
+  .swiper-btn-left {
+    background: url('@/assets/images/ranking/btn-left.png') left center no-repeat;
+    background-size: 100% 100%;
+  }
+
+  .swiper-btn-right {
+    background: url('@/assets/images/ranking/btn-right.png') left center no-repeat;
+    background-size: 100% 100%;
+  }
+}
+
+.footerBtn {
+  width: 100%;
+  padding: 0 calc(13.02rem / 2);
+  box-sizing: border-box;
+  position: fixed;
+  bottom: 3vh;
+  display: flex;
+  justify-content: end;
+
+  .btn {
+    width: 13vw;
+    height: 6.1vh;
+    line-height: 6.1vh;
+    font-size: 3vh;
+    color: #1a293a;
+    text-align: center;
+    border-radius: 1vh;
+    margin-left: 20px;
+    cursor: pointer;
+    background: radial-gradient(159% 126% at 5% 93%, #8effa9 0%, #07ffe7 100%);
+    box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.1874), inset 1px 1px 1px 1px rgba(255, 255, 255, 0.3);
+
+    &:hover {
+      background: #8effa9;
+    }
+  }
+
+  .startBtn {
+    color: #ffffff;
+    background: radial-gradient(159% 126% at 5% 93%, #f99f02 0%, #ed7905 100%);
+    box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.1874), inset 1px 1px 1px 1px rgba(255, 255, 255, 0.3);
+
+    &:hover {
+      background: #f99f02;
+    }
+  }
+
+  .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;
+    }
+  }
+}
+</style>

+ 1 - 1
src/views/train/index.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="train">
     <Header @confirmExit="getExit" :showTool="false" closeClass="close2"></Header>
-    <div class="menu" v-if="projectList.length" :class="projectList.length <= 12 ? 'menu1' : 'menu2'">
+    <div class="menu" v-if="projectList.length" :class="projectList.length <= 12 ? 'menu1' : 'menu2'" :key="projectList.length">
       <swiper :slides-per-view="6" :modules="[Grid]" :grid="{
         fill: projectList.length <= 12 ? 'row' : 'column',
         rows: 2,