浏览代码

ui修改与监考应答调整

zhangjie 3 年之前
父节点
当前提交
605cf72dc1
共有 43 个文件被更改,包括 1360 次插入1441 次删除
  1. 4 4
      src/api/invigilation.js
  2. 二进制
      src/assets/icon-arrow-left.png
  3. 二进制
      src/assets/icon-arrow-right.png
  4. 二进制
      src/assets/icon-au1dio.png
  5. 二进制
      src/assets/icon-audio-act.png
  6. 二进制
      src/assets/icon-audio.png
  7. 二进制
      src/assets/icon-bell.png
  8. 二进制
      src/assets/icon-calendar-act.png
  9. 二进制
      src/assets/icon-call.png
  10. 二进制
      src/assets/icon-clock-danger.png
  11. 二进制
      src/assets/icon-clock.png
  12. 二进制
      src/assets/icon-face.png
  13. 二进制
      src/assets/icon-info-danger.png
  14. 二进制
      src/assets/icon-info.png
  15. 二进制
      src/assets/icon-media.png
  16. 二进制
      src/assets/icon-paper-danger.png
  17. 二进制
      src/assets/icon-person.png
  18. 二进制
      src/assets/icon-record.png
  19. 二进制
      src/assets/icon-success.png
  20. 二进制
      src/assets/icon-text-message.png
  21. 二进制
      src/assets/icon-track-common.png
  22. 二进制
      src/assets/icon-track-warning.png
  23. 二进制
      src/assets/icon-track.png
  24. 二进制
      src/assets/icon-users.png
  25. 二进制
      src/assets/icon-users1.png
  26. 1 1
      src/features/invigilation/InvigilationDetail/InvigilationDetail.vue
  27. 1 1
      src/features/invigilation/OnlinePatrol/OnlinePatrol.vue
  28. 1 1
      src/features/invigilation/OnlinePatrol/PatrolExamDetail.vue
  29. 199 226
      src/features/invigilation/OnlinePatrol/PatrolWarningDetail.vue
  30. 103 112
      src/features/invigilation/RealtimeMonitoring/RealtimeMonitoring.vue
  31. 34 251
      src/features/invigilation/RealtimeMonitoring/VideoCommunication.vue
  32. 301 298
      src/features/invigilation/RealtimeMonitoring/WarningDetail.vue
  33. 1 1
      src/features/invigilation/WarningManage/WarningManage.vue
  34. 30 8
      src/features/invigilation/common/FlvMedia.vue
  35. 44 57
      src/features/invigilation/common/InvigilationStudent.vue
  36. 4 14
      src/features/invigilation/common/SummaryLine.vue
  37. 17 1
      src/features/invigilation/common/TextClock.vue
  38. 3 3
      src/router/invigilation.js
  39. 369 316
      src/styles/base.scss
  40. 2 4
      src/styles/element-ui-custom.scss
  41. 1 1
      src/styles/element-variables.scss
  42. 240 142
      src/styles/icons.scss
  43. 5 0
      src/utils/utils.js

+ 4 - 4
src/api/invigilation.js

@@ -221,9 +221,9 @@ export function clearInvigilationFilterUnreadWarningList(datas) {
 
 
 // warning-detail
 // warning-detail
 // 获取当前学生直播视频流
 // 获取当前学生直播视频流
-export function warningStudentDetail({ recordId }) {
+export function warningStudentDetail({ examRecordId }) {
   const data = {
   const data = {
-    recordId,
+    recordId: examRecordId,
   };
   };
   return httpApp.post(
   return httpApp.post(
     "/api/admin/monitor/call/query?" + object2QueryString(data),
     "/api/admin/monitor/call/query?" + object2QueryString(data),
@@ -231,9 +231,9 @@ export function warningStudentDetail({ recordId }) {
   );
   );
 }
 }
 // 获取预警详情信息
 // 获取预警详情信息
-export function invigilateDetail(recordId) {
+export function invigilateDetail(examRecordId) {
   return httpApp.post(
   return httpApp.post(
-    "/api/admin/invigilate/list/detail?examRecordId=" + recordId,
+    "/api/admin/invigilate/list/detail?examRecordId=" + examRecordId,
     {}
     {}
   );
   );
 }
 }

二进制
src/assets/icon-arrow-left.png


二进制
src/assets/icon-arrow-right.png


二进制
src/assets/icon-au1dio.png


二进制
src/assets/icon-audio-act.png


二进制
src/assets/icon-audio.png


二进制
src/assets/icon-bell.png


二进制
src/assets/icon-calendar-act.png


二进制
src/assets/icon-call.png


二进制
src/assets/icon-clock-danger.png


二进制
src/assets/icon-clock.png


二进制
src/assets/icon-face.png


二进制
src/assets/icon-info-danger.png


二进制
src/assets/icon-info.png


二进制
src/assets/icon-media.png


二进制
src/assets/icon-paper-danger.png


二进制
src/assets/icon-person.png


二进制
src/assets/icon-record.png


二进制
src/assets/icon-success.png


二进制
src/assets/icon-text-message.png


二进制
src/assets/icon-track-common.png


二进制
src/assets/icon-track-warning.png


二进制
src/assets/icon-track.png


二进制
src/assets/icon-users.png


二进制
src/assets/icon-users1.png


+ 1 - 1
src/features/invigilation/InvigilationDetail/InvigilationDetail.vue

@@ -400,7 +400,7 @@ export default {
         : "InvigilationWarningDetail";
         : "InvigilationWarningDetail";
       this.$router.push({
       this.$router.push({
         name: router,
         name: router,
-        params: { recordId: row.examRecordId },
+        params: { examRecordId: row.examRecordId },
       });
       });
     },
     },
   },
   },

+ 1 - 1
src/features/invigilation/OnlinePatrol/OnlinePatrol.vue

@@ -372,7 +372,7 @@ export default {
     toDetail(row) {
     toDetail(row) {
       this.$router.push({
       this.$router.push({
         name: "PatrolWarningDetail",
         name: "PatrolWarningDetail",
-        params: { recordId: row.examRecordId },
+        params: { examRecordId: row.examRecordId },
       });
       });
     },
     },
     async getPatrolReportList() {
     async getPatrolReportList() {

+ 1 - 1
src/features/invigilation/OnlinePatrol/PatrolExamDetail.vue

@@ -451,7 +451,7 @@ export default {
     toDetail(row) {
     toDetail(row) {
       this.$router.push({
       this.$router.push({
         name: "PatrolWarningDetail",
         name: "PatrolWarningDetail",
-        params: { recordId: row.examRecordId },
+        params: { examRecordId: row.examRecordId },
       });
       });
     },
     },
     videoMuted(muted) {
     videoMuted(muted) {

+ 199 - 226
src/features/invigilation/OnlinePatrol/PatrolWarningDetail.vue

@@ -1,49 +1,48 @@
 <template>
 <template>
   <div class="patrol-warning-detail warning-detail">
   <div class="patrol-warning-detail warning-detail">
     <div class="warning-detail-head">
     <div class="warning-detail-head">
-      <div class="warning-detail-title">
-        <h2>预警详情</h2>
-        <el-button size="mini" icon="el-icon-arrow-left" @click="goBack">
-          返回列表
-        </el-button>
-      </div>
-      <div class="warning-detail-student">
-        <div class="student-head">
-          <div class="student-head-left">
-            <p><i class="icon icon-user-act"></i></p>
-            <p>
-              <span>姓名:</span><span>{{ detailInfo.examStudentName }}</span>
-            </p>
-            <p>
-              <span>证件号:</span><span>{{ detailInfo.identity }}</span>
-            </p>
-            <p>
-              <span>科目(代码):</span
-              ><span>{{ detailInfo.courseNameCode }}</span>
-            </p>
-          </div>
-          <div class="student-head-right">
-            <el-button
-              class="el-icon-btn"
-              size="mini"
-              type="primary"
-              icon="el-icon-arrow-left"
-              title="查看上一个"
-              @click="changeStudent(0)"
-              :disabled="holding"
-            ></el-button>
-            <el-button
-              class="el-icon-btn"
-              size="mini"
-              type="primary"
-              icon="el-icon-arrow-right"
-              title="查看下一个"
-              @click="changeStudent(1)"
-              :disabled="holding"
-            ></el-button>
-          </div>
+      <h2>预警详情</h2>
+      <el-button size="mini" icon="el-icon-arrow-left" @click="goBack">
+        返回列表
+      </el-button>
+    </div>
+
+    <div class="warning-detail-body">
+      <div class="detail-body-head">
+        <div class="detail-body-head-left">
+          <p>
+            <i class="icon icon-person"></i>
+            <span>{{ detailInfo.examStudentName }}</span>
+          </p>
+          <p>
+            <span>证件号:</span><span>{{ detailInfo.identity }}</span>
+          </p>
+          <p>
+            <span>科目(代码):</span
+            ><span>{{ detailInfo.courseNameCode }}</span>
+          </p>
         </div>
         </div>
-        <div class="student-views">
+        <div class="detail-body-head-right">
+          <el-button
+            type="primary"
+            title="查看上一个"
+            :disabled="holding"
+            @click="changeStudent(0)"
+          >
+            <i class="icon icon-arrow-left"></i>
+          </el-button>
+          <el-button
+            type="primary"
+            title="查看下一个"
+            :disabled="holding"
+            @click="changeStudent(1)"
+          >
+            <i class="icon icon-arrow-right"></i>
+          </el-button>
+        </div>
+      </div>
+      <div class="warning-detail-main">
+        <div class="warning-action">
           <div class="student-avatar">
           <div class="student-avatar">
             <img
             <img
               :src="detailInfo.basePhotoPath"
               :src="detailInfo.basePhotoPath"
@@ -53,141 +52,120 @@
             <div class="avatar-default" v-else>
             <div class="avatar-default" v-else>
               <i class="el-icon-user-solid"></i>
               <i class="el-icon-user-solid"></i>
             </div>
             </div>
+            <div class="avatar-title">学生底照</div>
           </div>
           </div>
-          <div class="student-video">
-            <div class="student-video-item">
-              <flv-media
-                ref="FirstViewVideo"
-                :live-url="firstViewVideo.liveUrl"
-                v-if="firstViewVideoReady"
-              ></flv-media>
-              <div class="student-video-none" v-else>
-                <i class="el-icon-video-camera-solid"></i>
-              </div>
-              <div
-                v-if="firstViewVideoReady"
-                :class="[
-                  'student-video-muted',
-                  { 'is-active': !firstViewVideo.muted },
-                ]"
-                @click="videoMute('first')"
-              >
-                <i class="icon icon-audio"></i>
-              </div>
+          <div class="warning-summary">
+            <div class="warning-summary-row">
+              <p class="warning-summary-col">
+                <i class="icon icon-bell"></i>
+                <span class="line-name"
+                  >系统预警
+                  <em :class="{ 'color-danger': detailInfo.warningCount > 0 }"
+                    >{{ detailInfo.warningCount }}次</em
+                  ></span
+                >
+              </p>
+              <p class="warning-summary-col">
+                <i class="icon icon-face"></i>
+                <span class="line-name"
+                  >陌生人脸
+                  <em>{{ detailInfo.multipleFaceCount }}次</em>
+                </span>
+              </p>
             </div>
             </div>
-            <div class="student-video-item">
-              <flv-media
-                ref="SecondViewVideo"
-                :live-url="secondViewVideo.liveUrl"
-                v-if="secondViewVideoReady"
-              ></flv-media>
-              <div class="student-video-none" v-else>
-                <i class="el-icon-video-camera-solid"></i>
+            <div class="warning-summary-row">
+              <p class="warning-summary-col">
+                <i class="icon icon-info"></i>
+                <span class="line-name"
+                  >异常处理
+                  <em>{{ detailInfo.exceptionCount }}次</em>
+                </span>
+              </p>
+              <p class="warning-summary-col">
+                <i class="icon icon-success"></i>
+                <span class="line-name"
+                  >违纪状态
+                  <em :class="{ 'color-danger': isBreach }">
+                    {{ isBreach ? "违纪" : "正常" }}</em
+                  >
+                </span>
+              </p>
+            </div>
+            <div class="summary-bg">
+              <div class="summary-bg-line"></div>
+              <div class="summary-bg-line"></div>
+              <div class="summary-bg-spin"></div>
+              <div class="summary-bg-spin"></div>
+              <div class="summary-bg-spin"></div>
+              <div class="summary-bg-spin"></div>
+            </div>
+          </div>
+        </div>
+        <div class="warning-content">
+          <div class="warning-videos">
+            <div
+              v-for="item in viewVideos"
+              :key="item.source"
+              class="student-video-item"
+            >
+              <div class="student-video-container">
+                <div class="student-video-tips">{{ item.name }}</div>
+                <flv-media :ref="item.ref" :live-url="item.liveUrl"></flv-media>
+                <!-- <div
+                  v-if="item.liveUrl"
+                  class="student-video-muted"
+                  @click="videoMute(item)"
+                >
+                  <i
+                    :class="[
+                      'icon',
+                      item.muted ? 'icon-audio' : 'icon-audio-act',
+                    ]"
+                  ></i>
+                </div> -->
               </div>
               </div>
+            </div>
+          </div>
+          <!-- track -->
+          <div class="warning-track">
+            <h3 class="warning-track-title">
+              <i class="icon icon-track"></i>考试轨迹
+            </h3>
+            <div
+              class="warning-track-item"
+              v-for="log in detailInfo.examStudentLogList"
+              :key="log.id"
+            >
               <div
               <div
-                v-if="secondViewVideoReady"
                 :class="[
                 :class="[
-                  'student-video-muted',
-                  { 'is-active': !secondViewVideo.muted },
+                  'warning-track-type',
+                  log.viewType === 'common' ? 'type-common' : 'type-exception',
                 ]"
                 ]"
-                @click="videoMute('second')"
               >
               >
-                <i class="icon icon-audio"></i>
+                <i :class="['icon', `icon-track-${log.viewType}`]"></i>
+              </div>
+              <div class="warning-track-body">
+                <div class="warning-track-info">
+                  <h3>{{ log.title }}</h3>
+                  <p v-if="log.desc">{{ log.desc }}</p>
+                  <p>
+                    时间段:
+                    <span v-if="log.startTime">{{ log.startTime }} ~ </span>
+                    <span>{{ log.endTime }}</span>
+                  </p>
+                  <p v-if="log.durationTime">
+                    持续时长约:{{ log.durationTime }}
+                  </p>
+                </div>
+                <ul class="warning-track-media" v-if="log.photos">
+                  <li v-for="(photo, pindex) in log.photos" :key="pindex">
+                    <img :src="photo" @click="toViewImg(photo)" />
+                  </li>
+                </ul>
               </div>
               </div>
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
-        <div class="student-exception">
-          <ul>
-            <li v-for="(log, index) in exceptionSummary" :key="index">
-              <i>{{ index + 1 }}</i>
-              <h4>{{ log.title }}</h4>
-              <p v-if="log.desc">{{ log.desc }}</p>
-              <!-- <p>
-                时间段:
-                <span v-if="log.startTime">{{ log.startTime }} ~ </span>
-                <span>{{ log.endTime }}</span>
-              </p>
-              <p v-if="log.durationTime">持续时长约:{{ log.durationTime }}</p> -->
-            </li>
-          </ul>
-        </div>
-      </div>
-    </div>
-
-    <div class="warning-detail-body">
-      <div class="warning-body-head clear-float">
-        <div class="warning-body-head-action">
-          <h3>考试轨迹</h3>
-        </div>
-        <div class="warning-body-head-info summary-line">
-          <p class="summary-line-item">
-            <i class="line-point line-point-danger"></i>
-            <span class="line-name">系统预警</span>
-            <span>{{ detailInfo.warningCount }}次</span>
-          </p>
-          <p class="summary-line-item">
-            <i class="line-point line-point-danger"></i>
-            <span class="line-name">陌生人脸</span>
-            <span>{{ detailInfo.multipleFaceCount }}次</span>
-          </p>
-          <p class="summary-line-item">
-            <i class="line-point line-point-danger"></i>
-            <span class="line-name">异常处理</span>
-            <span>{{ detailInfo.exceptionCount }}次</span>
-          </p>
-          <p class="summary-line-item">
-            <span></span>
-            <span>
-              <b>违纪状态:</b>
-              <b :class="{ 'color-danger': isBreach }">
-                {{ isBreach ? "违纪" : "正常" }}
-              </b>
-            </span>
-          </p>
-        </div>
-      </div>
-      <div class="warning-body-main">
-        <div
-          class="warning-history"
-          v-for="log in detailInfo.examStudentLogList"
-          :key="log.id"
-        >
-          <div class="warning-history-info">
-            <h3>{{ log.title }}</h3>
-            <p v-if="log.desc">{{ log.desc }}</p>
-            <p>
-              时间段:
-              <span v-if="log.startTime">{{ log.startTime }} ~ </span>
-              <span>{{ log.endTime }}</span>
-            </p>
-            <p v-if="log.durationTime">持续时长约:{{ log.durationTime }}</p>
-          </div>
-          <div
-            :class="[
-              'warning-history-type',
-              log.viewType === 'common' ? 'type-common' : 'type-exception',
-            ]"
-          >
-            <i
-              :class="[
-                'icon',
-                {
-                  'icon-current-step': log.viewType === 'common',
-                  'icon-warning-act': log.viewType === 'warning',
-                  'icon-net-break': log.viewType === 'exception',
-                },
-              ]"
-            ></i>
-          </div>
-          <div class="warning-history-media">
-            <ul class="media-list" v-if="log.photos">
-              <li v-for="(photo, pindex) in log.photos" :key="pindex">
-                <img :src="photo" @click="toViewImg(photo)" />
-              </li>
-            </ul>
-          </div>
-        </div>
       </div>
       </div>
     </div>
     </div>
 
 
@@ -203,7 +181,12 @@
 import SimpleImagePreview from "@/components/imagePreview/SimpleImagePreview";
 import SimpleImagePreview from "@/components/imagePreview/SimpleImagePreview";
 import FlvMedia from "../common/FlvMedia";
 import FlvMedia from "../common/FlvMedia";
 import { invigilateDetail, warningStudentDetail } from "@/api/invigilation";
 import { invigilateDetail, warningStudentDetail } from "@/api/invigilation";
-import { formatDate, timeNumberToText, objTypeOf } from "@/utils/utils";
+import {
+  formatDate,
+  timeNumberToText,
+  objTypeOf,
+  snakeToHump,
+} from "@/utils/utils";
 import { mapState } from "vuex";
 import { mapState } from "vuex";
 
 
 export default {
 export default {
@@ -211,21 +194,13 @@ export default {
   components: { FlvMedia, SimpleImagePreview },
   components: { FlvMedia, SimpleImagePreview },
   data() {
   data() {
     return {
     return {
-      recordId: this.$route.params.recordId,
+      examRecordId: this.$route.params.examRecordId,
       detailInfo: {},
       detailInfo: {},
       curDetail: {},
       curDetail: {},
       serialIds: [],
       serialIds: [],
       exceptionSummary: [],
       exceptionSummary: [],
-      firstViewVideo: {
-        liveUrl: "",
-        muted: true,
-      },
-      secondViewVideo: {
-        liveUrl: "",
-        muted: true,
-      },
-      firstViewVideoReady: false,
-      secondViewVideoReady: false,
+      viewVideos: [],
+      viewVideoReady: false,
       holding: false,
       holding: false,
       curImage: { imgSrc: "" },
       curImage: { imgSrc: "" },
     };
     };
@@ -250,40 +225,55 @@ export default {
   },
   },
   methods: {
   methods: {
     async initData() {
     async initData() {
-      this.recordId = this.$route.params.recordId;
+      this.examRecordId = this.$route.params.examRecordId;
       await this.getInvigilateDetail().catch(() => {});
       await this.getInvigilateDetail().catch(() => {});
       await this.getStudentVideo().catch(() => {});
       await this.getStudentVideo().catch(() => {});
       this.holding = false;
       this.holding = false;
     },
     },
     async getStudentVideo() {
     async getStudentVideo() {
-      const res = await warningStudentDetail({ recordId: this.recordId });
-      const records = res.data.data.map((item, index) => {
-        const domain = this.liveDomains[index] || "";
+      const res = await warningStudentDetail({
+        examRecordId: this.examRecordId,
+      });
+      const orderSources = [
+        "CLIENT_CAMERA",
+        "CLIENT_SCREEN",
+        "MOBILE_FIRST",
+        "MOBILE_SECOND",
+      ];
+      const sourceNames = {
+        CLIENT_CAMERA: "电脑摄像头",
+        CLIENT_SCREEN: "考生屏幕",
+        MOBILE_FIRST: "手机主机位",
+        MOBILE_SECOND: "手机辅机位",
+      };
+
+      let records = {};
+      res.data.data.forEach((item, index) => {
+        const domain = this.liveDomains[index] || this.liveDomains[0];
         item.liveUrl = item.liveUrl
         item.liveUrl = item.liveUrl
           ? `${domain}/live/${item.liveUrl.toLowerCase()}.flv`
           ? `${domain}/live/${item.liveUrl.toLowerCase()}.flv`
           : "";
           : "";
-        item.name = item.source;
+        item.name = sourceNames[item.source];
         item.muted = true;
         item.muted = true;
-        return item;
+        item.ref = snakeToHump(item.source) + "Video";
+        records[item.source] = item;
       });
       });
-      const orderSources = {
-        CLIENT_CAMERA: 1,
-        MOBILE_FIRST: 2,
-        CLIENT_SCREEN: 3,
-        MOBILE_SECOND: 4,
-      };
 
 
-      records.sort((a, b) => {
-        return orderSources[a.source] - orderSources[b.source];
+      this.viewVideos = orderSources.map((source) => {
+        return (
+          records[source] || {
+            liveUrl: null,
+            muted: true,
+            source,
+            name: sourceNames[source],
+            ref: snakeToHump(source) + "Video",
+          }
+        );
       });
       });
-
-      this.firstViewVideo = records[0] || {};
-      this.secondViewVideo = records[1] || {};
-
-      if (records.length) this.initSubscribeVideo();
+      this.initSubscribeVideo();
     },
     },
     async getInvigilateDetail() {
     async getInvigilateDetail() {
-      const res = await invigilateDetail(this.recordId);
+      const res = await invigilateDetail(this.examRecordId);
       this.detailInfo = res.data.data;
       this.detailInfo = res.data.data;
       this.detailInfo.examStudentLogList = this.parseStudentLogs(
       this.detailInfo.examStudentLogList = this.parseStudentLogs(
         this.detailInfo.examStudentLogList
         this.detailInfo.examStudentLogList
@@ -379,7 +369,7 @@ export default {
       return logs;
       return logs;
     },
     },
     changeStudent(type) {
     changeStudent(type) {
-      let index = this.detailIds.indexOf(this.recordId);
+      let index = this.detailIds.indexOf(this.examRecordId);
       if (type) {
       if (type) {
         if (index >= this.detailIds.length - 1) {
         if (index >= this.detailIds.length - 1) {
           this.$message.error("当前没有下一个学生了");
           this.$message.error("当前没有下一个学生了");
@@ -403,7 +393,7 @@ export default {
       this.$router.replace({
       this.$router.replace({
         name: "PatrolWarningDetail",
         name: "PatrolWarningDetail",
         params: {
         params: {
-          recordId: this.detailIds[index],
+          examRecordId: this.detailIds[index],
         },
         },
       });
       });
     },
     },
@@ -413,35 +403,18 @@ export default {
     },
     },
     // video relative
     // video relative
     initSubscribeVideo() {
     initSubscribeVideo() {
-      if (this.firstViewVideo.liveUrl) this.firstViewVideoReady = true;
-      if (this.secondViewVideo.liveUrl) this.secondViewVideoReady = true;
+      this.viewVideoReady = true;
     },
     },
     closeSubscribeVideo() {
     closeSubscribeVideo() {
-      this.firstViewVideoReady = false;
-      this.secondViewVideoReady = false;
+      this.viewVideoReady = false;
     },
     },
-    videoMute(type) {
-      if (type === "first") {
-        if (this.secondViewVideoReady) {
-          this.secondViewVideo.muted = true;
-          this.$refs.SecondViewVideo.mutedPlayer(true);
-        }
-
-        const res = this.$refs.FirstViewVideo.mutedPlayer(
-          !this.firstViewVideo.muted
-        );
-        if (res) this.firstViewVideo.muted = !this.firstViewVideo.muted;
-      } else {
-        if (this.firstViewVideoReady) {
-          this.firstViewVideo.muted = true;
-          this.$refs.FirstViewVideo.mutedPlayer(true);
-        }
-
-        const res = this.$refs.SecondViewVideo.mutedPlayer(
-          !this.secondViewVideo.muted
-        );
-        if (res) this.secondViewVideo.muted = !this.secondViewVideo.muted;
-      }
+    videoMutedChange() {
+      this.viewVideos
+        .filter((vv) => vv.liveUrl)
+        .forEach((vv) => {
+          let res = this.$refs[vv.ref][0].mutedPlayer(true);
+          if (res) vv.muted = true;
+        });
     },
     },
     goBack() {
     goBack() {
       window.history.go(-1);
       window.history.go(-1);

+ 103 - 112
src/features/invigilation/RealtimeMonitoring/RealtimeMonitoring.vue

@@ -1,16 +1,5 @@
 <template>
 <template>
   <div class="realtime-monitoring">
   <div class="realtime-monitoring">
-    <div class="realtime-top clear-float">
-      <p :title="curExamBatch.roomName">
-        考场名称:{{ curExamBatch.roomName }}
-      </p>
-      <p class="realtime-top-select" @click="$refs.ExamBatchDialog.open()">
-        <span>{{ curExamBatch.label || "请选择批次" }}</span>
-        <i class="el-icon-caret-bottom"></i>
-      </p>
-      <text-clock></text-clock>
-    </div>
-
     <div class="part-box-head">
     <div class="part-box-head">
       <div class="part-box-head-left">
       <div class="part-box-head-left">
         <h1>实时监控台</h1>
         <h1>实时监控台</h1>
@@ -44,6 +33,32 @@
       </div>
       </div>
     </div>
     </div>
 
 
+    <div class="part-filter part-filter-realtime">
+      <div class="part-filter-form">
+        <el-form inline>
+          <el-form-item>
+            <div :title="curExamBatch.roomName">
+              <el-input v-model="curExamBatch.roomName" readonly></el-input>
+            </div>
+          </el-form-item>
+          <el-form-item>
+            <div @click="$refs.ExamBatchDialog.open()">
+              <el-input
+                v-model="curExamBatch.label"
+                class="realtime-top-select"
+                placeholder="请选择批次"
+                suffix-icon="el-icon-caret-bottom"
+                readonly
+              ></el-input>
+            </div>
+          </el-form-item>
+        </el-form>
+        <div class="part-filter-form-action">
+          <text-clock></text-clock>
+        </div>
+      </div>
+    </div>
+
     <div class="part-filter">
     <div class="part-filter">
       <div class="part-filter-info">
       <div class="part-filter-info">
         <summary-line
         <summary-line
@@ -53,6 +68,30 @@
           ref="SummaryLine"
           ref="SummaryLine"
         ></summary-line>
         ></summary-line>
         <div class="part-filter-info-sub">
         <div class="part-filter-info-sub">
+          <el-dropdown
+            @command="viewingAngleChange"
+            trigger="click"
+            v-if="pageType === '1'"
+          >
+            <el-button type="primary"
+              >切换视频源<i class="el-icon-arrow-down el-icon--right"></i
+            ></el-button>
+            <el-dropdown-menu slot="dropdown">
+              <el-dropdown-item
+                v-for="item in viewingAngles"
+                :key="item.code"
+                :command="item"
+              >
+                <span
+                  :class="{
+                    'color-primary': item.code === curViewingAngle.code,
+                  }"
+                >
+                  {{ item.name }}
+                </span>
+              </el-dropdown-item>
+            </el-dropdown-menu>
+          </el-dropdown>
           <el-badge
           <el-badge
             :value="communicationCount"
             :value="communicationCount"
             :max="99"
             :max="99"
@@ -65,6 +104,19 @@
               >通话待办</el-button
               >通话待办</el-button
             >
             >
           </el-badge>
           </el-badge>
+          <!-- <el-button
+            type="primary"
+            icon="icon icon-handle"
+            @click="finishInvigilation"
+            >手动收卷</el-button
+          > -->
+          <el-button
+            type="danger"
+            icon="icon icon-over"
+            :disabled="!filter.examId"
+            @click="finishInvigilationExam"
+            >结束监考</el-button
+          >
         </div>
         </div>
       </div>
       </div>
 
 
@@ -141,47 +193,7 @@
             <el-button type="primary" @click="toSearch">查询</el-button>
             <el-button type="primary" @click="toSearch">查询</el-button>
           </el-form-item>
           </el-form-item>
         </el-form>
         </el-form>
-
-        <div class="part-filter-form-action">
-          <el-dropdown
-            @command="viewingAngleChange"
-            style="margin-right: 10px;"
-            trigger="click"
-            v-if="pageType === '1'"
-          >
-            <el-button type="primary"
-              >切换视频源<i class="el-icon-arrow-down el-icon--right"></i
-            ></el-button>
-            <el-dropdown-menu slot="dropdown">
-              <el-dropdown-item
-                v-for="item in viewingAngles"
-                :key="item.code"
-                :command="item"
-              >
-                <span
-                  :class="{
-                    'color-primary': item.code === curViewingAngle.code,
-                  }"
-                >
-                  {{ item.name }}
-                </span>
-              </el-dropdown-item>
-            </el-dropdown-menu>
-          </el-dropdown>
-          <!-- <el-button
-            type="primary"
-            icon="icon icon-handle"
-            @click="finishInvigilation"
-            >手动收卷</el-button
-          > -->
-          <el-button
-            type="danger"
-            icon="icon icon-over"
-            :disabled="!filter.examId"
-            @click="finishInvigilationExam"
-            >结束监考</el-button
-          >
-        </div>
+        <div class="part-filter-form-action"></div>
       </div>
       </div>
     </div>
     </div>
 
 
@@ -350,6 +362,7 @@ export default {
         maxWarningCount: undefined,
         maxWarningCount: undefined,
         minWarningCount: undefined,
         minWarningCount: undefined,
       },
       },
+      userId: this.$store.state.user.id,
       monitorStatusFilter: {},
       monitorStatusFilter: {},
       VIDEO_SOURCE_TYPE,
       VIDEO_SOURCE_TYPE,
       BOOLEAN_INVERSE_TYPE,
       BOOLEAN_INVERSE_TYPE,
@@ -357,6 +370,7 @@ export default {
       CLIENT_WEBSOCKET_STATUS,
       CLIENT_WEBSOCKET_STATUS,
       MONITOR_STATUS_SOURCE,
       MONITOR_STATUS_SOURCE,
       MONITOR_STATUS_TYPE,
       MONITOR_STATUS_TYPE,
+      examBatchs: [],
       hasNewWarning: false,
       hasNewWarning: false,
       loopRunning: false,
       loopRunning: false,
       loopSetTs: [],
       loopSetTs: [],
@@ -609,7 +623,7 @@ export default {
     toDetail(row) {
     toDetail(row) {
       this.$router.push({
       this.$router.push({
         name: "WarningDetail",
         name: "WarningDetail",
-        params: { recordId: row.examRecordId },
+        params: { examRecordId: row.examRecordId },
       });
       });
     },
     },
     videoMuted(muted) {
     videoMuted(muted) {
@@ -629,62 +643,9 @@ export default {
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
-.realtime-top {
-  position: relative;
-  padding: 9px 20px 9px 73px;
-  background: rgba(24, 134, 254, 1);
-  border-radius: 6px;
-  color: #fff;
-  margin-bottom: 30px;
-  line-height: 32px;
-
-  &::before {
-    content: "";
-    display: block;
-    position: absolute;
-    width: 59px;
-    height: 49px;
-    left: 13px;
-    top: 0;
-    background-image: url(../../../assets/bg-stars.png);
-    background-size: 100% 100%;
-  }
-
-  > p {
-    float: left;
-    margin: 0;
-  }
-  > p:first-child {
-    margin-right: 40px;
-    min-width: 150px;
-    max-width: 300px;
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
-  }
-  .realtime-top-select {
-    display: inline-block;
-    position: relative;
-    height: 32px;
-    line-height: 32px;
-    border-radius: 6px;
-    min-width: 200px;
-    background-color: #fff;
-    color: #1886fe;
-    padding: 0 26px 0 12px;
-    cursor: pointer;
-
-    > i {
-      position: absolute;
-      right: 8px;
-      top: 9px;
-    }
-  }
-  .text-clock {
-    float: right;
-    font-size: 12px;
-    opacity: 0.8;
-  }
+.text-clock {
+  color: #1886fe;
+  font-weight: 600;
 }
 }
 .realtime-switch {
 .realtime-switch {
   font-size: 0;
   font-size: 0;
@@ -734,17 +695,24 @@ export default {
   }
   }
 }
 }
 .invigilation-student-list {
 .invigilation-student-list {
-  background: #ffffff;
   border-radius: 6px;
   border-radius: 6px;
-  padding: 10px 10px;
   font-size: 0;
   font-size: 0;
   min-height: 200px;
   min-height: 200px;
+  margin: -10px;
   .invigilation-student-item {
   .invigilation-student-item {
-    font-size: 14px;
     display: inline-block;
     display: inline-block;
     vertical-align: top;
     vertical-align: top;
     padding: 10px;
     padding: 10px;
     width: 25%;
     width: 25%;
+    font-size: 0;
+  }
+
+  .invigilation-student {
+    padding: 20px;
+    border: none;
+    margin: 0;
+    background: #fff;
+    font-size: 14px;
   }
   }
 }
 }
 .warn-new-tips {
 .warn-new-tips {
@@ -762,4 +730,27 @@ export default {
     background-size: 100% 100%;
     background-size: 100% 100%;
   }
   }
 }
 }
+.part-filter-info-sub {
+  .el-badge {
+    margin: 0 10px;
+    vertical-align: top;
+  }
+}
+</style>
+<style lang="scss">
+.realtime-top-select {
+  width: 400px;
+
+  .el-input__inner {
+    cursor: pointer;
+    &:hover {
+      color: #1886fe;
+    }
+  }
+}
+.part-filter-realtime {
+  .el-form-item {
+    margin-bottom: 10px;
+  }
+}
 </style>
 </style>

+ 34 - 251
src/features/invigilation/RealtimeMonitoring/VideoCommunication.vue

@@ -35,19 +35,11 @@
             </div>
             </div>
             <h4 class="student-name">{{ item.examStudentName }}</h4>
             <h4 class="student-name">{{ item.examStudentName }}</h4>
             <div v-if="callStatus === 'START'">
             <div v-if="callStatus === 'START'">
-              <el-button
-                round
-                type="success"
-                @click="answer(item, 0)"
-                :loading="holding"
+              <el-button round type="success" @click="answer(item, 0)"
                 >语音通话</el-button
                 >语音通话</el-button
               >
               >
               <br />
               <br />
-              <el-button
-                round
-                type="primary"
-                @click="answer(item, 1)"
-                :loading="holding"
+              <el-button round type="primary" @click="answer(item, 1)"
                 >视频通话</el-button
                 >视频通话</el-button
               >
               >
             </div>
             </div>
@@ -78,85 +70,46 @@
       >
       >
       </el-pagination>
       </el-pagination>
     </div>
     </div>
-
-    <!-- 通话弹出层 -->
-    <el-dialog
-      custom-class="communication-dialog"
-      :visible.sync="dialogVisible"
-      width="600px"
-      :show-close="false"
-      :close-on-press-escape="false"
-      :close-on-click-modal="false"
-      append-to-body
-      fullscreen
-    >
-      <div class="communication-box">
-        <div class="communication-host" id="communication-host"></div>
-        <div class="communication-guest" id="communication-guest"></div>
-        <div class="communication-action">
-          <el-button round type="danger" @click="hangup">结束通话</el-button>
-        </div>
-        <div class="communication-info">
-          <!-- <span>当前网络状态良好</span> -->
-          <span>持续时长:<second-timer ref="SecondTimer"></second-timer></span>
-        </div>
-      </div>
-      <span slot="footer" class="dialog-footer"> </span>
-    </el-dialog>
   </div>
   </div>
 </template>
 </template>
 
 
 <script>
 <script>
-import {
-  checkSystemRequirements,
-  createClient,
-  createStream,
-} from "@/plugins/trtc";
-import {
-  communicationList,
-  communicationCalling,
-  communicationOver,
-  getUserMonitorKey,
-} from "@/api/invigilation";
-import SecondTimer from "../common/SecondTimer";
+import { communicationList } from "@/api/invigilation";
 import { formatDate, timeNumberToText } from "@/utils/utils";
 import { formatDate, timeNumberToText } from "@/utils/utils";
-
-const domEmpty = (dom) => {
-  dom.childNodes.forEach((childNode) => {
-    dom.removeChild(childNode);
-  });
-};
+import timeMixin from "../../../mixins/timeMixin";
 
 
 export default {
 export default {
   name: "video-communication",
   name: "video-communication",
-  components: { SecondTimer },
+  mixins: [timeMixin],
   data() {
   data() {
     return {
     return {
       examId: this.$route.params.examId,
       examId: this.$route.params.examId,
       callStatus: "START",
       callStatus: "START",
-      dialogVisible: false,
-      holding: false,
       students: [],
       students: [],
-      curStudent: {},
       current: 1,
       current: 1,
       total: 0,
       total: 0,
       size: 100,
       size: 100,
-      setT: null,
       loopRunning: false,
       loopRunning: false,
-      userMonitor: {},
-      client: null,
-      localStream: null,
-      isHandup: false,
     };
     };
   },
   },
   mounted() {
   mounted() {
+    const cachePageInfo = window.sessionStorage.getItem(
+      "videoCommunicationCache"
+    );
+    if (cachePageInfo) {
+      const { pageNumber, callStatus } = JSON.parse(cachePageInfo);
+      this.pageNumber = pageNumber;
+      this.callStatus = callStatus;
+      window.sessionStorage.removeItem("videoCommunicationCache");
+    }
+
     this.loopRunning = true;
     this.loopRunning = true;
     this.getCommunicationList();
     this.getCommunicationList();
   },
   },
   methods: {
   methods: {
     async getCommunicationList() {
     async getCommunicationList() {
-      if (this.setT) clearTimeout(this.setT);
       if (!this.loopRunning) return;
       if (!this.loopRunning) return;
+      this.clearSetTs();
 
 
       const res = await communicationList({
       const res = await communicationList({
         examId: this.examId,
         examId: this.examId,
@@ -184,7 +137,7 @@ export default {
         this.current--;
         this.current--;
         this.getCommunicationList();
         this.getCommunicationList();
       } else {
       } else {
-        this.setT = setTimeout(() => {
+        this.addSetTime(() => {
           this.getCommunicationList();
           this.getCommunicationList();
         }, 5000);
         }, 5000);
       }
       }
@@ -193,190 +146,24 @@ export default {
       this.current = page;
       this.current = page;
       this.getCommunicationList();
       this.getCommunicationList();
     },
     },
-    notifyError(content) {
-      this.$notify({
-        type: "error",
-        message: content,
-      });
-    },
-    async initClient(examRecordId) {
-      const res = await getUserMonitorKey(examRecordId);
-      this.userMonitor = res.data.data;
-      this.client = createClient({
-        mode: "live",
-        sdkAppId: this.userMonitor.appId * 1,
-        userId: this.userMonitor.monitorUserId,
-        userSig: this.userMonitor.monitorUserSig,
-        useStringRoomId: true,
-      });
-    },
-    async getLocalMedia(isVideo) {
-      const localStream = createStream({
-        userId: this.userMonitor.monitorUserId,
-        audio: true,
-        video: !!isVideo,
-      });
-      const errorTips = {
-        NotFoundError: "找不到硬件设备,请确保硬件设备已经正常插入。",
-        NotAllowedError: "不授权摄像头/麦克风访问无法进行音视频通话。",
-        NotReadableError:
-          "暂时无法访问摄像头/麦克风,请确保当前没有其他应用请求访问摄像头/麦克风,并重试。",
-        OverConstrainedError: "设备异常",
-        AbortError: "设备异常",
-        RtcError: "无应答客户端",
-      };
-
-      let initLocalStreamResult = true;
-      await localStream.initialize().catch((error) => {
-        console.log(errorTips[error.name]);
-        this.notifyError(errorTips[error.name] || "未知错误");
-        initLocalStreamResult = false;
-        localStream.close();
-      });
-      return initLocalStreamResult && localStream;
-    },
-    async answer(student, isVideo) {
-      const result = await checkSystemRequirements().catch(() => {
-        this.$message.error(
-          `您的浏览器不支持当前音视频通讯版本。建议使用最新版的chrome浏览器!`
-        );
-      });
-      if (!result) return;
-
-      if (this.holding) return;
-      this.holding = true;
-
-      this.curStudent = student;
-      await this.initClient(student.examRecordId).catch(() => {});
-      if (!this.client) {
-        this.holding = false;
-        return;
-      }
-      this.localStream = await this.getLocalMedia(isVideo);
-      if (!this.localStream) {
-        this.holding = false;
-        return;
-      }
-
-      this.dialogVisible = true;
-      this.holding = false;
-      // 添加远程用户视频发布监听
-      this.client.on("stream-added", (event) => {
-        console.log(event);
-        console.log(event.stream.getUserId(), student.sourceUserId);
-        const remoteStream = event.stream;
-        if (remoteStream.getUserId() !== student.sourceUserId) return;
-        if (remoteStream.getType() !== "main") return;
-        console.log(`有效视频${remoteStream.getUserId()},准备订阅`);
-
-        this.client
-          .subscribe(remoteStream, { audio: true, video: true })
-          .catch((error) => {
-            console.log(`${remoteStream.getUserId()}视频订阅失败!`, error);
-            this.notifyError("学生视频获取失败!");
-          });
-      });
-      this.client.on("stream-subscribed", (event) => {
-        const remoteStream = event.stream;
-        console.log(event);
-        console.log(`${remoteStream.getUserId()}视频已订阅!`);
-        if (!this.$refs.SecondTimer.recoding) this.$refs.SecondTimer.start();
-        domEmpty(document.getElementById("communication-host"));
-        remoteStream.play("communication-host", { objectFit: "contain" });
-      });
-      this.client.on("stream-removed", (event) => {
-        // 监考端手动挂断时也会触发这个事件
-        const remoteStream = event.stream;
-        if (
-          remoteStream.getUserId() !== student.sourceUserId ||
-          remoteStream.getType() !== "main" ||
-          this.isHandup
-        )
-          return;
-        console.log(event);
-        console.log(`${remoteStream.getUserId()}已退出房间!`);
-        this.notifyError("对方已挂断!");
-        this.hangup();
-      });
-
-      // 加入房间
-      let roomJoinResult = true;
-      await this.client
-        .join({
-          roomId: this.userMonitor.monitorKey,
-          role: "audience",
-          // role: "anchor"
+    answer(student, isVideo) {
+      window.sessionStorage.setItem(
+        "autoAnswerInfo",
+        JSON.stringify({ ...student, isVideo })
+      );
+      window.sessionStorage.setItem(
+        "videoCommunicationCache",
+        JSON.stringify({
+          pageNumber: this.pageNumber,
+          callStatus: this.callStatus,
         })
         })
-        .catch((error) => {
-          roomJoinResult = false;
-          console.log("加入房间失败!", error);
-          this.notifyError("接通通信失败!");
-        });
-      if (!roomJoinResult) return;
-      console.log("加入房间成功!");
-
-      // 切换角色,连麦互动
-      let switchResult = true;
-      await this.client.switchRole("anchor").catch((error) => {
-        console.log("切换角色失败!", error);
-        this.notifyError("角色错误!");
-        switchResult = false;
-      });
-      if (!switchResult) return;
-
-      // 发布本地视频
-      let publishStreamResult = true;
-      this.client.publish(this.localStream).catch((error) => {
-        console.log("发布本地视频失败!", error);
-        this.notifyError("本地音视频推送失败!");
-        publishStreamResult = false;
-      });
-      if (!publishStreamResult) return;
-      console.log("发布本地音视频成功!");
-
-      // 播放本地视频
-      this.localStream.play("communication-guest", { muted: true });
-
-      // 更改学生的通话申请状态
-      await communicationCalling({
-        recordId: student.examRecordId,
-        source: student.source,
-      });
-      this.isHandup = false;
-    },
-    async hangup() {
-      if (this.isHandup) return;
-      this.isHandup = true;
-      this.$refs.SecondTimer.end();
-      // 结束学生的通话
-      await communicationOver({
-        recordId: this.curStudent.examRecordId,
-        source: this.curStudent.source,
-      }).catch(() => {
-        console.log("结束通话状态异常!");
-      });
-      this.curStudent = {};
-
-      // 取消发布本地视频
-      await this.client.unpublish(this.localStream).catch((error) => {
-        console.log("取消发布本地视频失败!", error);
+      );
+      this.$router.push({
+        name: "WarningDetail",
+        params: {
+          examRecordId: student.examRecordId,
+        },
       });
       });
-
-      this.localStream.close();
-      this.localStream = null;
-
-      let result = true;
-      await this.client.leave().catch((error) => {
-        console.log("离开房间失败!", error);
-        this.notifyError("操作异常,请重新尝试!");
-        result = false;
-      });
-      if (!result) return;
-
-      this.client.off("*");
-      this.client = null;
-      this.userMonitor = {};
-      this.dialogVisible = false;
     },
     },
     goBack() {
     goBack() {
       window.history.go(-1);
       window.history.go(-1);
@@ -384,11 +171,7 @@ export default {
   },
   },
   beforeDestroy() {
   beforeDestroy() {
     this.loopRunning = false;
     this.loopRunning = false;
-    if (this.setT) clearTimeout(this.setT);
-    if (this.client) {
-      this.client.leave();
-      this.client.off("*");
-    }
+    this.clearSetTs();
   },
   },
 };
 };
 </script>
 </script>

+ 301 - 298
src/features/invigilation/RealtimeMonitoring/WarningDetail.vue

@@ -1,12 +1,11 @@
 <template>
 <template>
   <div class="warning-detail">
   <div class="warning-detail">
     <div class="warning-detail-head">
     <div class="warning-detail-head">
-      <div class="warning-detail-title">
-        <h2>预警详情</h2>
-        <el-button size="mini" icon="el-icon-arrow-left" @click="goBack"
-          >返回列表</el-button
-        >
-        <!-- <el-button
+      <h2>预警详情</h2>
+      <el-button size="mini" icon="el-icon-arrow-left" @click="goBack"
+        >返回列表</el-button
+      >
+      <!-- <el-button
           @click="initSubscribeVideo"
           @click="initSubscribeVideo"
           type="primary"
           type="primary"
           size="mini"
           size="mini"
@@ -20,44 +19,44 @@
           icon="el-icon-arrow-left"
           icon="el-icon-arrow-left"
           >关闭视频</el-button
           >关闭视频</el-button
         > -->
         > -->
-      </div>
-      <div class="warning-detail-student">
-        <div class="student-head">
-          <div class="student-head-left">
-            <p><i class="icon icon-user-act"></i></p>
-            <p>
-              <span>姓名:</span><span>{{ detailInfo.examStudentName }}</span>
-            </p>
-            <p>
-              <span>证件号:</span><span>{{ detailInfo.identity }}</span>
-            </p>
-            <p>
-              <span>科目(代码):</span
-              ><span>{{ detailInfo.courseNameCode }}</span>
-            </p>
-          </div>
-          <div class="student-head-right">
-            <el-button
-              class="el-icon-btn"
-              size="mini"
-              type="primary"
-              icon="el-icon-arrow-left"
-              title="查看上一个"
-              @click="changeStudent(0)"
-              :disabled="holding"
-            ></el-button>
-            <el-button
-              class="el-icon-btn"
-              size="mini"
-              type="primary"
-              icon="el-icon-arrow-right"
-              title="查看下一个"
-              @click="changeStudent(1)"
-              :disabled="holding"
-            ></el-button>
-          </div>
+    </div>
+
+    <div class="warning-detail-body">
+      <div class="detail-body-head">
+        <div class="detail-body-head-left">
+          <p>
+            <i class="icon icon-person"></i>
+            <span>{{ detailInfo.examStudentName }}</span>
+          </p>
+          <p>
+            <span>证件号:</span><span>{{ detailInfo.identity }}</span>
+          </p>
+          <p>
+            <span>科目(代码):</span
+            ><span>{{ detailInfo.courseNameCode }}</span>
+          </p>
         </div>
         </div>
-        <div class="student-views">
+        <div class="detail-body-head-right">
+          <el-button
+            type="primary"
+            title="查看上一个"
+            :disabled="holding"
+            @click="changeStudent(0)"
+          >
+            <i class="icon icon-arrow-left"></i>
+          </el-button>
+          <el-button
+            type="primary"
+            title="查看下一个"
+            :disabled="holding"
+            @click="changeStudent(1)"
+          >
+            <i class="icon icon-arrow-right"></i>
+          </el-button>
+        </div>
+      </div>
+      <div class="warning-detail-main">
+        <div class="warning-action">
           <div class="student-avatar">
           <div class="student-avatar">
             <img
             <img
               :src="detailInfo.basePhotoPath"
               :src="detailInfo.basePhotoPath"
@@ -67,186 +66,155 @@
             <div class="avatar-default" v-else>
             <div class="avatar-default" v-else>
               <i class="el-icon-user-solid"></i>
               <i class="el-icon-user-solid"></i>
             </div>
             </div>
+            <div class="avatar-title">学生底照</div>
           </div>
           </div>
-          <div class="student-video">
-            <div class="student-video-item">
-              <flv-media
-                ref="FirstViewVideo"
-                :live-url="firstViewVideo.liveUrl"
-                v-if="firstViewVideoReady"
-              ></flv-media>
-              <div class="student-video-none" v-else>
-                <i class="el-icon-video-camera-solid"></i>
-              </div>
-              <div
-                v-if="firstViewVideoReady"
-                :class="[
-                  'student-video-muted',
-                  { 'is-active': !firstViewVideo.muted },
-                ]"
-                @click="videoMute('first')"
-              >
-                <i class="icon icon-audio"></i>
-              </div>
+          <div class="warning-summary">
+            <div class="warning-summary-row">
+              <p class="warning-summary-col">
+                <i class="icon icon-bell"></i>
+                <span class="line-name"
+                  >系统预警
+                  <em :class="{ 'color-danger': detailInfo.warningCount > 0 }"
+                    >{{ detailInfo.warningCount }}次</em
+                  ></span
+                >
+              </p>
+              <p class="warning-summary-col">
+                <i class="icon icon-face"></i>
+                <span class="line-name"
+                  >陌生人脸
+                  <em>{{ detailInfo.multipleFaceCount }}次</em>
+                </span>
+              </p>
             </div>
             </div>
-            <div class="student-video-item">
-              <flv-media
-                ref="SecondViewVideo"
-                :live-url="secondViewVideo.liveUrl"
-                v-if="secondViewVideoReady"
-              ></flv-media>
-              <div class="student-video-none" v-else>
-                <i class="el-icon-video-camera-solid"></i>
-              </div>
-              <div
-                v-if="secondViewVideoReady"
-                :class="[
-                  'student-video-muted',
-                  { 'is-active': !secondViewVideo.muted },
-                ]"
-                @click="videoMute('second')"
-              >
-                <i class="icon icon-audio"></i>
-              </div>
+            <div class="warning-summary-row">
+              <p class="warning-summary-col">
+                <i class="icon icon-info"></i>
+                <span class="line-name"
+                  >异常处理
+                  <em>{{ detailInfo.exceptionCount }}次</em>
+                </span>
+              </p>
+              <p class="warning-summary-col">
+                <i class="icon icon-success"></i>
+                <span class="line-name"
+                  >违纪状态
+                  <em :class="{ 'color-danger': isBreach }">
+                    {{ isBreach ? "违纪" : "正常" }}</em
+                  >
+                </span>
+              </p>
+            </div>
+            <div class="summary-bg">
+              <div class="summary-bg-line"></div>
+              <div class="summary-bg-line"></div>
+              <div class="summary-bg-spin"></div>
+              <div class="summary-bg-spin"></div>
+              <div class="summary-bg-spin"></div>
+              <div class="summary-bg-spin"></div>
             </div>
             </div>
           </div>
           </div>
-        </div>
-        <div class="student-exception">
-          <ul>
-            <li v-for="(log, index) in exceptionSummary" :key="index">
-              <i>{{ index + 1 }}</i>
-              <h4>{{ log.title }}</h4>
-              <p v-if="log.desc">{{ log.desc }}</p>
-              <!-- <p>
-                时间段:
-                <span v-if="log.startTime">{{ log.startTime }} ~ </span>
-                <span>{{ log.endTime }}</span>
-              </p>
-              <p v-if="log.durationTime">持续时长约:{{ log.durationTime }}</p> -->
-            </li>
-          </ul>
-        </div>
-      </div>
-    </div>
-
-    <div class="warning-detail-body">
-      <div class="warning-body-head clear-float">
-        <div class="warning-body-head-action">
-          <h3>考试轨迹</h3>
-          <!-- <el-button
-            class="el-icon-btn"
-            type="primary"
-            icon="icon icon-view"
-          ></el-button> -->
-          <el-button
-            class="el-icon-btn"
-            type="primary"
-            icon="icon icon-text"
-            @click="toSendTextMsg"
-            title="发送文字提醒"
-            v-if="actionValid"
-          ></el-button>
-          <el-button
-            class="el-icon-btn"
-            type="primary"
-            icon="icon icon-audio"
-            @click="toSendAudioMsg"
-            v-if="actionValid"
-          ></el-button>
-          <el-popover
-            class="warning-body-head-call"
-            placement="bottom-start"
-            v-model="popoverVisible"
-            v-if="detailInfo.monitorVideoSource && actionValid"
-          >
-            <el-button type="success" @click="answer(0)" :loading="holding"
+          <div class="action-list">
+            <el-button
+              v-if="actionValid"
+              icon="icon icon-text-message"
+              size="mideum"
+              @click="toSendTextMsg"
+              >文字提醒</el-button
+            >
+            <el-button
+              v-if="actionValid"
+              icon="icon icon-record"
+              size="mideum"
+              @click="toSendAudioMsg"
+              >录音提醒</el-button
+            >
+            <el-button
+              v-if="detailInfo.monitorVideoSource && actionValid"
+              icon="icon icon-call"
+              size="mideum"
+              :loading="holding"
+              @click="answer(0)"
               >语音通话</el-button
               >语音通话</el-button
             >
             >
-            <el-button type="primary" @click="answer(1)" :loading="holding"
+            <el-button
+              v-if="detailInfo.monitorVideoSource && actionValid"
+              icon="icon icon-media"
+              size="mideum"
+              :loading="holding"
+              @click="answer(1)"
               >视频通话</el-button
               >视频通话</el-button
             >
             >
-            <el-button type="primary" slot="reference">实时通话</el-button>
-          </el-popover>
-        </div>
-        <div class="warning-body-head-info summary-line">
-          <p class="summary-line-item">
-            <i class="line-point line-point-danger"></i>
-            <span class="line-name">系统预警</span>
-            <span>{{ detailInfo.warningCount }}次</span>
-          </p>
-          <p class="summary-line-item">
-            <i class="line-point line-point-danger"></i>
-            <span class="line-name">陌生人脸</span>
-            <span>{{ detailInfo.multipleFaceCount }}次</span>
-          </p>
-          <p class="summary-line-item">
-            <i class="line-point line-point-danger"></i>
-            <span class="line-name">异常处理</span>
-            <span>{{ detailInfo.exceptionCount }}次</span>
-          </p>
-          <p class="summary-line-item">
-            <span></span>
-            <span>
-              <b>违纪状态:</b>
-              <b :class="{ 'color-danger': isBreach }">
-                {{ isBreach ? "违纪" : "正常" }}
-              </b>
-            </span>
-          </p>
-          <el-button
-            :type="isBreach ? 'success' : 'danger'"
-            icon="icon icon-stop"
-            @click="toBreach"
-            >{{ isBreach ? "撤销违纪" : "违纪处理" }}</el-button
-          >
-          <el-button
-            type="warning"
-            icon="icon icon-forbide"
-            @click="toFinish"
-            v-if="detailInfo.statusCode === 'ANSWERING'"
-            >强制收卷</el-button
-          >
-        </div>
-      </div>
-      <div class="warning-body-main">
-        <div
-          class="warning-history"
-          v-for="log in detailInfo.examStudentLogList"
-          :key="log.id"
-        >
-          <div class="warning-history-info">
-            <h3>{{ log.title }}</h3>
-            <p v-if="log.desc">{{ log.desc }}</p>
-            <p>
-              时间段:
-              <span v-if="log.startTime">{{ log.startTime }} ~ </span>
-              <span>{{ log.endTime }}</span>
-            </p>
-            <p v-if="log.durationTime">持续时长约:{{ log.durationTime }}</p>
+            <el-button
+              icon="icon icon-info-danger"
+              size="mideum"
+              @click="toBreach"
+              >{{ isBreach ? "撤销违纪" : "违纪处理" }}</el-button
+            >
+            <el-button
+              v-if="detailInfo.statusCode !== 'ANSWERING'"
+              icon="icon icon-paper-danger"
+              size="mideum"
+              @click="toFinish"
+              >强制收卷</el-button
+            >
           </div>
           </div>
-          <div
-            :class="[
-              'warning-history-type',
-              log.viewType === 'common' ? 'type-common' : 'type-exception',
-            ]"
-          >
-            <i
-              :class="[
-                'icon',
-                {
-                  'icon-current-step': log.viewType === 'common',
-                  'icon-warning-act': log.viewType === 'warning',
-                  'icon-net-break': log.viewType === 'exception',
-                },
-              ]"
-            ></i>
+        </div>
+        <div class="warning-content">
+          <div class="warning-videos">
+            <div
+              v-for="item in viewVideos"
+              :key="item.source"
+              class="student-video-item"
+            >
+              <div class="student-video-container">
+                <div class="student-video-tips">{{ item.name }}</div>
+                <flv-media
+                  :ref="item.ref"
+                  :live-url="item.liveUrl"
+                  @muted-change="videoAllMuted"
+                ></flv-media>
+              </div>
+            </div>
           </div>
           </div>
-          <div class="warning-history-media">
-            <ul class="media-list" v-if="log.photos">
-              <li v-for="(photo, pindex) in log.photos" :key="pindex">
-                <img :src="photo" @click="toViewImg(photo)" />
-              </li>
-            </ul>
+          <!-- track -->
+          <div class="warning-track">
+            <h3 class="warning-track-title">
+              <i class="icon icon-track"></i>考试轨迹
+            </h3>
+            <div
+              class="warning-track-item"
+              v-for="log in detailInfo.examStudentLogList"
+              :key="log.id"
+            >
+              <div
+                :class="[
+                  'warning-track-type',
+                  log.viewType === 'common' ? 'type-common' : 'type-exception',
+                ]"
+              >
+                <i :class="['icon', `icon-track-${log.viewType}`]"></i>
+              </div>
+              <div class="warning-track-body">
+                <div class="warning-track-info">
+                  <h3>{{ log.title }}</h3>
+                  <p v-if="log.desc">{{ log.desc }}</p>
+                  <p>
+                    时间段:
+                    <span v-if="log.startTime">{{ log.startTime }} ~ </span>
+                    <span>{{ log.endTime }}</span>
+                  </p>
+                  <p v-if="log.durationTime">
+                    持续时长约:{{ log.durationTime }}
+                  </p>
+                </div>
+                <ul class="warning-track-media" v-if="log.photos">
+                  <li v-for="(photo, pindex) in log.photos" :key="pindex">
+                    <img :src="photo" @click="toViewImg(photo)" />
+                  </li>
+                </ul>
+              </div>
+            </div>
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
@@ -260,12 +228,12 @@
     ></student-breach-dialog>
     ></student-breach-dialog>
     <!-- warning-text-message-dialog -->
     <!-- warning-text-message-dialog -->
     <warning-text-message-dialog
     <warning-text-message-dialog
-      :record-id="recordId"
+      :record-id="examRecordId"
       ref="WarningTextMessageDialog"
       ref="WarningTextMessageDialog"
     ></warning-text-message-dialog>
     ></warning-text-message-dialog>
     <!-- audio-record-dialog -->
     <!-- audio-record-dialog -->
     <audio-record-dialog
     <audio-record-dialog
-      :record-id="recordId"
+      :record-id="examRecordId"
       ref="AudioRecordDialog"
       ref="AudioRecordDialog"
     ></audio-record-dialog>
     ></audio-record-dialog>
     <!-- image-preview -->
     <!-- image-preview -->
@@ -285,6 +253,7 @@
       append-to-body
       append-to-body
       fullscreen
       fullscreen
     >
     >
+      <!-- TODO:拖动 -->
       <div class="communication-box" v-show="!isWaiting">
       <div class="communication-box" v-show="!isWaiting">
         <div class="communication-host" id="communication-host"></div>
         <div class="communication-host" id="communication-host"></div>
         <div class="communication-guest" id="communication-guest"></div>
         <div class="communication-guest" id="communication-guest"></div>
@@ -325,6 +294,8 @@ import {
 import {
 import {
   invigilateDetail,
   invigilateDetail,
   invigilateFinish,
   invigilateFinish,
+  communicationCalling,
+  communicationOver,
   warningStudentDetail,
   warningStudentDetail,
   getUserMonitorKey,
   getUserMonitorKey,
 } from "@/api/invigilation";
 } from "@/api/invigilation";
@@ -334,7 +305,12 @@ import WarningTextMessageDialog from "./WarningTextMessageDialog";
 import AudioRecordDialog from "./audioRecord/AudioRecordDialog";
 import AudioRecordDialog from "./audioRecord/AudioRecordDialog";
 import SimpleImagePreview from "@/components/imagePreview/SimpleImagePreview";
 import SimpleImagePreview from "@/components/imagePreview/SimpleImagePreview";
 import SecondTimer from "../common/SecondTimer";
 import SecondTimer from "../common/SecondTimer";
-import { formatDate, timeNumberToText, objTypeOf } from "@/utils/utils";
+import {
+  formatDate,
+  timeNumberToText,
+  objTypeOf,
+  snakeToHump,
+} from "@/utils/utils";
 import { mapState } from "vuex";
 import { mapState } from "vuex";
 
 
 const domEmpty = (dom) => {
 const domEmpty = (dom) => {
@@ -355,21 +331,14 @@ export default {
   },
   },
   data() {
   data() {
     return {
     return {
-      recordId: this.$route.params.recordId,
+      examRecordId: this.$route.params.examRecordId,
+      autoAnswerInfo: null,
       detailInfo: {},
       detailInfo: {},
       curDetail: {},
       curDetail: {},
       serialIds: [],
       serialIds: [],
       exceptionSummary: [],
       exceptionSummary: [],
-      firstViewVideo: {
-        liveUrl: "",
-        muted: true,
-      },
-      secondViewVideo: {
-        liveUrl: "",
-        muted: true,
-      },
-      firstViewVideoReady: false,
-      secondViewVideoReady: false,
+      viewVideos: [],
+      viewVideoReady: false,
       holding: false,
       holding: false,
       // communication
       // communication
       popoverVisible: false,
       popoverVisible: false,
@@ -378,7 +347,7 @@ export default {
       localStream: null,
       localStream: null,
       dialogVisible: false,
       dialogVisible: false,
       isWaiting: true,
       isWaiting: true,
-      subscribeSetT: null,
+      subscribeSetTs: [],
       loopRunning: false,
       loopRunning: false,
       loopSetTs: [],
       loopSetTs: [],
       isHandup: false,
       isHandup: false,
@@ -407,11 +376,13 @@ export default {
     },
     },
   },
   },
   mounted() {
   mounted() {
+    const autoAnswerInfo = window.sessionStorage.getItem("autoAnswerInfo");
+    this.autoAnswerInfo = autoAnswerInfo ? JSON.parse(autoAnswerInfo) : null;
     this.initData();
     this.initData();
   },
   },
   methods: {
   methods: {
     async initData() {
     async initData() {
-      this.recordId = this.$route.params.recordId;
+      this.examRecordId = this.$route.params.examRecordId;
       await this.getInvigilateDetail().catch(() => {});
       await this.getInvigilateDetail().catch(() => {});
       await this.getStudentVideo().catch(() => {});
       await this.getStudentVideo().catch(() => {});
       this.holding = false;
       this.holding = false;
@@ -430,6 +401,18 @@ export default {
         this.loopRunning = false;
         this.loopRunning = false;
         this.clearLoopSetTs();
         this.clearLoopSetTs();
       }
       }
+
+      // 自动应答
+      if (this.autoAnswerInfo) {
+        this.autoAnswer();
+      }
+    },
+    clearSubscribeSetTs() {
+      if (!this.subscribeSetTs.length) return;
+      this.subscribeSetTs.forEach((sett) => {
+        clearTimeout(sett);
+      });
+      this.subscribeSetTs = [];
     },
     },
     clearLoopSetTs() {
     clearLoopSetTs() {
       if (!this.loopSetTs.length) return;
       if (!this.loopSetTs.length) return;
@@ -451,34 +434,49 @@ export default {
       );
       );
     },
     },
     async getStudentVideo() {
     async getStudentVideo() {
-      const res = await warningStudentDetail({ recordId: this.recordId });
-      const records = res.data.data.map((item, index) => {
-        const domain = this.liveDomains[index] || "";
+      const res = await warningStudentDetail({
+        examRecordId: this.examRecordId,
+      });
+      const orderSources = [
+        "CLIENT_CAMERA",
+        "CLIENT_SCREEN",
+        "MOBILE_FIRST",
+        "MOBILE_SECOND",
+      ];
+      const sourceNames = {
+        CLIENT_CAMERA: "电脑摄像头",
+        CLIENT_SCREEN: "考生屏幕",
+        MOBILE_FIRST: "手机主机位",
+        MOBILE_SECOND: "手机辅机位",
+      };
+
+      let records = {};
+      res.data.data.forEach((item, index) => {
+        const domain = this.liveDomains[index] || this.liveDomains[0];
         item.liveUrl = item.liveUrl
         item.liveUrl = item.liveUrl
           ? `${domain}/live/${item.liveUrl.toLowerCase()}.flv`
           ? `${domain}/live/${item.liveUrl.toLowerCase()}.flv`
           : "";
           : "";
-        item.name = item.source;
+        item.name = sourceNames[item.source];
         item.muted = true;
         item.muted = true;
-        return item;
+        item.ref = snakeToHump(item.source) + "Video";
+        records[item.source] = item;
       });
       });
-      const orderSources = {
-        CLIENT_CAMERA: 1,
-        MOBILE_FIRST: 2,
-        CLIENT_SCREEN: 3,
-        MOBILE_SECOND: 4,
-      };
 
 
-      records.sort((a, b) => {
-        return orderSources[a.source] - orderSources[b.source];
+      this.viewVideos = orderSources.map((source) => {
+        return (
+          records[source] || {
+            liveUrl: null,
+            muted: true,
+            source,
+            name: sourceNames[source],
+            ref: snakeToHump(source) + "Video",
+          }
+        );
       });
       });
-
-      this.firstViewVideo = records[0] || {};
-      this.secondViewVideo = records[1] || {};
-
-      if (records.length) this.initSubscribeVideo();
+      this.initSubscribeVideo();
     },
     },
     async getInvigilateDetail() {
     async getInvigilateDetail() {
-      const res = await invigilateDetail(this.recordId);
+      const res = await invigilateDetail(this.examRecordId);
       this.detailInfo = res.data.data;
       this.detailInfo = res.data.data;
       this.detailInfo.examStudentLogList = this.parseStudentLogs(
       this.detailInfo.examStudentLogList = this.parseStudentLogs(
         this.detailInfo.examStudentLogList
         this.detailInfo.examStudentLogList
@@ -574,7 +572,7 @@ export default {
       return logs;
       return logs;
     },
     },
     changeStudent(type) {
     changeStudent(type) {
-      let index = this.detailIds.indexOf(this.recordId);
+      let index = this.detailIds.indexOf(this.examRecordId);
       if (type) {
       if (type) {
         if (index >= this.detailIds.length - 1) {
         if (index >= this.detailIds.length - 1) {
           this.$message.error("当前没有下一个学生了");
           this.$message.error("当前没有下一个学生了");
@@ -598,7 +596,7 @@ export default {
       this.$router.replace({
       this.$router.replace({
         name: "WarningDetail",
         name: "WarningDetail",
         params: {
         params: {
-          recordId: this.detailIds[index],
+          examRecordId: this.detailIds[index],
         },
         },
       });
       });
     },
     },
@@ -648,14 +646,6 @@ export default {
       this.getInvigilateDetail();
       this.getInvigilateDetail();
     },
     },
     // video relative
     // video relative
-    initSubscribeVideo() {
-      if (this.firstViewVideo.liveUrl) this.firstViewVideoReady = true;
-      if (this.secondViewVideo.liveUrl) this.secondViewVideoReady = true;
-    },
-    closeSubscribeVideo() {
-      this.firstViewVideoReady = false;
-      this.secondViewVideoReady = false;
-    },
     notifyError(content) {
     notifyError(content) {
       this.$notify({
       this.$notify({
         type: "error",
         type: "error",
@@ -697,6 +687,14 @@ export default {
       });
       });
       return initLocalStreamResult && localStream;
       return initLocalStreamResult && localStream;
     },
     },
+    async autoAnswer() {
+      await this.answer(this.autoAnswerInfo.isVideo);
+      // 更改学生的通话申请状态
+      await communicationCalling({
+        recordId: this.examRecordId,
+        source: this.autoAnswerInfo.source,
+      });
+    },
     async answer(isVideo) {
     async answer(isVideo) {
       const result = await checkSystemRequirements().catch(() => {
       const result = await checkSystemRequirements().catch(() => {
         this.$message.error(
         this.$message.error(
@@ -711,9 +709,9 @@ export default {
       // 手机端userId各不同
       // 手机端userId各不同
       if (this.holding) return;
       if (this.holding) return;
       this.holding = true;
       this.holding = true;
-      this.videoAllMute();
+      this.videoAllMuted();
 
 
-      await this.initClient(this.recordId).catch(() => {});
+      await this.initClient(this.examRecordId).catch(() => {});
       if (!this.client) {
       if (!this.client) {
         this.holding = false;
         this.holding = false;
         return;
         return;
@@ -736,15 +734,30 @@ export default {
         if (remoteStream.getType() !== "main") return;
         if (remoteStream.getType() !== "main") return;
         console.log(`有效视频${remoteStream.getUserId()},准备订阅`);
         console.log(`有效视频${remoteStream.getUserId()},准备订阅`);
 
 
-        // 延迟订阅视频
-        this.subscribeSetT = setTimeout(() => {
+        if (this.autoAnswerInfo) {
+          // 存在自动应答信息时,不再延迟订阅学生音视频流
           this.client
           this.client
             .subscribe(remoteStream, { audio: true, video: true })
             .subscribe(remoteStream, { audio: true, video: true })
             .catch((error) => {
             .catch((error) => {
               console.log(`${remoteStream.getUserId()}视频订阅失败!`, error);
               console.log(`${remoteStream.getUserId()}视频订阅失败!`, error);
               this.notifyError("学生视频获取失败!");
               this.notifyError("学生视频获取失败!");
             });
             });
-        }, 5000);
+        } else {
+          // 延迟订阅视频
+          this.subscribeSetTs.push(
+            setTimeout(() => {
+              this.client
+                .subscribe(remoteStream, { audio: true, video: true })
+                .catch((error) => {
+                  console.log(
+                    `${remoteStream.getUserId()}视频订阅失败!`,
+                    error
+                  );
+                  this.notifyError("学生视频获取失败!");
+                });
+            }, 5000)
+          );
+        }
       });
       });
       this.client.on("stream-subscribed", (event) => {
       this.client.on("stream-subscribed", (event) => {
         const remoteStream = event.stream;
         const remoteStream = event.stream;
@@ -813,10 +826,19 @@ export default {
     async hangup() {
     async hangup() {
       if (this.isHandup) return;
       if (this.isHandup) return;
       this.isHandup = true;
       this.isHandup = true;
-      if (this.subscribeSetT) clearTimeout(this.subscribeSetT);
-
+      this.clearSubscribeSetTs();
       this.$refs.SecondTimer.end();
       this.$refs.SecondTimer.end();
 
 
+      if (this.autoAnswerInfo) {
+        // 结束学生的通话
+        await communicationOver({
+          recordId: this.examRecordId,
+          source: this.autoAnswerInfo.source,
+        }).catch(() => {
+          console.log("结束通话状态异常!");
+        });
+      }
+
       // 取消发布本地视频
       // 取消发布本地视频
       await this.client.unpublish(this.localStream).catch((error) => {
       await this.client.unpublish(this.localStream).catch((error) => {
         console.log("取消发布本地视频失败!", error);
         console.log("取消发布本地视频失败!", error);
@@ -842,39 +864,19 @@ export default {
       this.isWaiting = true;
       this.isWaiting = true;
       // this.initSubscribeVideo();
       // this.initSubscribeVideo();
     },
     },
-    videoMute(type) {
-      if (type === "first") {
-        if (this.secondViewVideoReady) {
-          let res = this.$refs.SecondViewVideo.mutedPlayer(true);
-          if (res) this.secondViewVideo.muted = true;
-        }
-
-        const res = this.$refs.FirstViewVideo.mutedPlayer(
-          !this.firstViewVideo.muted
-        );
-        if (res) this.firstViewVideo.muted = !this.firstViewVideo.muted;
-      } else {
-        if (this.firstViewVideoReady) {
-          let res = this.$refs.FirstViewVideo.mutedPlayer(true);
-          if (res) this.firstViewVideo.muted = true;
-        }
-
-        const res = this.$refs.SecondViewVideo.mutedPlayer(
-          !this.secondViewVideo.muted
-        );
-
-        if (res) this.secondViewVideo.muted = !this.secondViewVideo.muted;
-      }
+    initSubscribeVideo() {
+      this.viewVideoReady = true;
     },
     },
-    videoAllMute() {
-      if (this.firstViewVideoReady) {
-        let res = this.$refs.FirstViewVideo.mutedPlayer(true);
-        if (res) this.firstViewVideo.muted = true;
-      }
-      if (this.secondViewVideoReady) {
-        let res = this.$refs.SecondViewVideo.mutedPlayer(true);
-        if (res) this.secondViewVideo.muted = true;
-      }
+    closeSubscribeVideo() {
+      this.viewVideoReady = false;
+    },
+    videoAllMuted() {
+      this.viewVideos
+        .filter((vv) => vv.liveUrl)
+        .forEach((vv) => {
+          let res = this.$refs[vv.ref][0].mutedPlayer(true);
+          if (res) vv.muted = true;
+        });
     },
     },
     toViewImg(photo) {
     toViewImg(photo) {
       this.curImage = { imgSrc: photo };
       this.curImage = { imgSrc: photo };
@@ -885,9 +887,10 @@ export default {
     },
     },
   },
   },
   beforeDestroy() {
   beforeDestroy() {
+    window.sessionStorage.removeItem("autoAnswerInfo");
     this.loopRunning = false;
     this.loopRunning = false;
     this.clearLoopSetTs();
     this.clearLoopSetTs();
-    if (this.subscribeSetT) clearTimeout(this.subscribeSetT);
+    this.clearSubscribeSetTs();
     if (this.client) {
     if (this.client) {
       this.client.leave();
       this.client.leave();
       this.client.off("*");
       this.client.off("*");

+ 1 - 1
src/features/invigilation/WarningManage/WarningManage.vue

@@ -361,7 +361,7 @@ export default {
         : "InvigilationWarningDetail";
         : "InvigilationWarningDetail";
       this.$router.push({
       this.$router.push({
         name: router,
         name: router,
-        params: { recordId: row.examRecordId },
+        params: { examRecordId: row.examRecordId },
       });
       });
     },
     },
   },
   },

+ 30 - 8
src/features/invigilation/common/FlvMedia.vue

@@ -8,12 +8,22 @@
     <video ref="VideoMedia" muted @ended="destroyPlayer"></video>
     <video ref="VideoMedia" muted @ended="destroyPlayer"></video>
     <div v-if="result.error" class="media-error" @click.stop="() => {}">
     <div v-if="result.error" class="media-error" @click.stop="() => {}">
       <div class="media-error-content">
       <div class="media-error-content">
-        <div>{{ result.message }}</div>
-        <el-button type="text" icon="el-icon-refresh-left" @click="reloadVideo"
-          >重新播放</el-button
-        >
+        <span>{{ result.message }},</span>
+        <el-button type="text" @click="reloadVideo">点击刷新</el-button>
       </div>
       </div>
     </div>
     </div>
+    <div v-if="!liveUrl" class="media-error" @click.stop="() => {}">
+      <div class="media-error-content">
+        <span>无视频源</span>
+      </div>
+    </div>
+    <div
+      v-if="liveUrl && !result.error && !loading"
+      class="media-video-muted"
+      @click.stop="videoMuted"
+    >
+      <i :class="['icon', isLoud ? 'icon-audio-act' : 'icon-audio']"></i>
+    </div>
   </div>
   </div>
 </template>
 </template>
 
 
@@ -35,20 +45,27 @@ export default {
       flvPlayer: null,
       flvPlayer: null,
       retryCount: 0,
       retryCount: 0,
       maxRetryCount: 3,
       maxRetryCount: 3,
-      loading: true,
+      loading: false,
       result: {
       result: {
         error: false,
         error: false,
         message: "",
         message: "",
       },
       },
     };
     };
   },
   },
+  computed: {
+    isLoud() {
+      return this.flvPlayer && !this.flvPlayer.muted;
+    },
+  },
   mounted() {
   mounted() {
     this.initVideo();
     this.initVideo();
+    this.retryCount++;
   },
   },
   methods: {
   methods: {
     initVideo() {
     initVideo() {
       if (!this.liveUrl) return;
       if (!this.liveUrl) return;
       if (!flvjs.isSupported()) return;
       if (!flvjs.isSupported()) return;
+      this.loading = true;
 
 
       this.flvPlayer = flvjs.createPlayer(
       this.flvPlayer = flvjs.createPlayer(
         {
         {
@@ -84,7 +101,7 @@ export default {
           console.log("未知问题,无法播放!");
           console.log("未知问题,无法播放!");
           this.result = {
           this.result = {
             error: true,
             error: true,
-            message: "播放失败",
+            message: "播放失败",
           };
           };
           break;
           break;
       }
       }
@@ -94,7 +111,7 @@ export default {
         console.log("已尝试最大次数重新播放!");
         console.log("已尝试最大次数重新播放!");
         this.result = {
         this.result = {
           error: true,
           error: true,
-          message: "播放失败",
+          message: "播放失败",
         };
         };
         this.retryCount = 0;
         this.retryCount = 0;
         this.loading = false;
         this.loading = false;
@@ -103,7 +120,6 @@ export default {
 
 
       this.addSetTime(() => {
       this.addSetTime(() => {
         this.reloadVideo();
         this.reloadVideo();
-        this.retryCount++;
         console.log("已重新播放");
         console.log("已重新播放");
       }, 1000);
       }, 1000);
     },
     },
@@ -127,6 +143,11 @@ export default {
       this.flvPlayer.muted = muted;
       this.flvPlayer.muted = muted;
       return true;
       return true;
     },
     },
+    videoMuted() {
+      if (!this.flvPlayer) return;
+      this.$emit("muted-change");
+      this.flvPlayer.muted = !this.flvPlayer.muted;
+    },
     reloadVideo() {
     reloadVideo() {
       this.result = {
       this.result = {
         error: false,
         error: false,
@@ -136,6 +157,7 @@ export default {
       this.$nextTick(() => {
       this.$nextTick(() => {
         this.destroyPlayer();
         this.destroyPlayer();
         this.initVideo();
         this.initVideo();
+        this.retryCount++;
       });
       });
     },
     },
   },
   },

+ 44 - 57
src/features/invigilation/common/InvigilationStudent.vue

@@ -14,14 +14,13 @@
           ><i class="icon icon-net-break"></i>
           ><i class="icon icon-net-break"></i>
         </h6>
         </h6>
         <div class="student-info-tips">
         <div class="student-info-tips">
-          <div
-            :class="['student-info-audio', { 'is-active': !this.muted }]"
-            @click="toMuted"
-          >
-            <i class="icon icon-audio"></i>
+          <div class="student-info-audio" @click="toMuted">
+            <i :class="['icon', muted ? 'icon-audio' : 'icon-audio-act']"></i>
           </div>
           </div>
           <div class="student-info-time">
           <div class="student-info-time">
-            <i class="icon icon-alarm-clock"></i>
+            <i
+              :class="['icon', isNetbreak ? 'icon-clock-danger' : 'icon-clock']"
+            ></i>
             <span>{{ data.remainTime }}</span>
             <span>{{ data.remainTime }}</span>
           </div>
           </div>
         </div>
         </div>
@@ -32,9 +31,9 @@
           <p>答题进度:{{ data.progress || 0 }}%</p>
           <p>答题进度:{{ data.progress || 0 }}%</p>
         </div>
         </div>
         <div class="student-info-detail">
         <div class="student-info-detail">
-          <el-button type="primary" size="mini" round @click="toDetail"
-            >详情</el-button
-          >
+          <el-button type="text" size="small" @click="toDetail"
+            >详情 <i class="el-icon-arrow-right"></i
+          ></el-button>
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
@@ -74,12 +73,15 @@ export default {
     };
     };
   },
   },
   computed: {
   computed: {
+    isNetbreak() {
+      return this.data.statusCode === "BREAK_OFF";
+    },
     classes() {
     classes() {
       return [
       return [
         "invigilation-student",
         "invigilation-student",
         {
         {
           "invigilation-student-breach": !this.data.breachStatus,
           "invigilation-student-breach": !this.data.breachStatus,
-          "invigilation-student-netbreak": this.data.statusCode === "BREAK_OFF",
+          "invigilation-student-netbreak": this.isNetbreak,
         },
         },
       ];
       ];
     },
     },
@@ -89,12 +91,12 @@ export default {
       if (this.showType === "invigilation") {
       if (this.showType === "invigilation") {
         this.$router.push({
         this.$router.push({
           name: "WarningDetail",
           name: "WarningDetail",
-          params: { recordId: this.data.examRecordId },
+          params: { examRecordId: this.data.examRecordId },
         });
         });
       } else {
       } else {
         this.$router.push({
         this.$router.push({
           name: "PatrolWarningDetail",
           name: "PatrolWarningDetail",
-          params: { recordId: this.data.examRecordId },
+          params: { examRecordId: this.data.examRecordId },
         });
         });
       }
       }
     },
     },
@@ -142,28 +144,29 @@ export default {
       background-size: 100% 100%;
       background-size: 100% 100%;
       z-index: 99;
       z-index: 99;
     }
     }
-    &::after {
-      content: "";
-      display: block;
-      position: absolute;
-      top: 4px;
-      right: 4px;
-      width: 12px;
-      height: 12px;
+    // &::after {
+    //   content: "";
+    //   display: block;
+    //   position: absolute;
+    //   top: 4px;
+    //   right: 4px;
+    //   width: 12px;
+    //   height: 12px;
 
 
-      border: 2px solid #fff;
-      border-radius: 50%;
-      background: #fe5863;
-      z-index: 99;
-    }
+    //   border: 2px solid #fff;
+    //   border-radius: 50%;
+    //   background: #fe5863;
+    //   z-index: 99;
+    // }
   }
   }
 
 
   &-netbreak {
   &-netbreak {
     .icon-net-break {
     .icon-net-break {
       display: inline-block !important;
       display: inline-block !important;
     }
     }
-    .student-time {
-      background-color: #fe5863 !important;
+    .student-info-time {
+      background-color: rgba(254, 88, 99, 0.1) !important;
+      color: rgba(254, 88, 99, 1);
     }
     }
   }
   }
 
 
@@ -183,25 +186,12 @@ export default {
   background: #626a82;
   background: #626a82;
   margin-bottom: 15px;
   margin-bottom: 15px;
   position: relative;
   position: relative;
+  border-radius: 6px;
+  overflow: hidden;
   cursor: pointer;
   cursor: pointer;
-
-  // &::before {
-  //   content: "";
-  //   display: block;
-  //   position: absolute;
-  //   top: 50%;
-  //   left: 50%;
-  //   width: 32px;
-  //   height: 20px;
-  //   margin-left: -16px;
-  //   margin-top: -10px;
-  //   background-image: url(../../../assets/icon-video.png);
-  //   background-size: 100% 100%;
-  // }
 }
 }
 .student-info {
 .student-info {
   position: relative;
   position: relative;
-  padding: 0 10px 10px;
 
 
   &-header {
   &-header {
     display: flex;
     display: flex;
@@ -216,21 +206,13 @@ export default {
     height: 24px;
     height: 24px;
     text-align: center;
     text-align: center;
     line-height: 24px;
     line-height: 24px;
-    background: #abb8c9;
-    border-radius: 50%;
     margin-right: 5px;
     margin-right: 5px;
     cursor: pointer;
     cursor: pointer;
     &:hover {
     &:hover {
-      background: #1886fe;
-    }
-
-    &.is-active {
-      background: #1886fe;
+      transform: scale(1.05, 1.05);
     }
     }
 
 
     .icon {
     .icon {
-      width: 14px;
-      height: 12px;
       margin-top: -2px;
       margin-top: -2px;
     }
     }
   }
   }
@@ -238,12 +220,12 @@ export default {
     display: inline-block;
     display: inline-block;
     vertical-align: middle;
     vertical-align: middle;
     height: 24px;
     height: 24px;
-    padding: 4px 12px;
-    border-radius: 12px;
-    background: #abb8c9;
-    color: #fff;
-    line-height: 16px;
-    font-size: 12px;
+    padding: 0 12px;
+    border-radius: 5px;
+    background: #f0f4f9;
+    color: #626a82;
+    line-height: 24px;
+    font-size: 14px;
     > i {
     > i {
       margin-right: 5px;
       margin-right: 5px;
       width: 12px;
       width: 12px;
@@ -287,6 +269,11 @@ export default {
     margin-left: 5px;
     margin-left: 5px;
     flex-grow: 0;
     flex-grow: 0;
     flex-shrink: 0;
     flex-shrink: 0;
+
+    .el-button {
+      padding: 0;
+      font-size: 14px;
+    }
   }
   }
 }
 }
 </style>
 </style>

+ 4 - 14
src/features/invigilation/common/SummaryLine.vue

@@ -1,14 +1,9 @@
 <template>
 <template>
   <div class="summary-line">
   <div class="summary-line">
+    <p class="summary-line-item">
+      <i class="icon icon-users"></i>
+    </p>
     <p class="summary-line-item" v-for="item in paramList" :key="item.param">
     <p class="summary-line-item" v-for="item in paramList" :key="item.param">
-      <i :class="['icon', `icon-${item.icon}`]" v-if="item.icon"></i>
-      <i
-        :class="[
-          'line-point',
-          item.pointType && `line-point-${item.pointType}`,
-        ]"
-        v-else
-      ></i>
       <el-popover
       <el-popover
         placement="bottom-start"
         placement="bottom-start"
         popper-class="warning-popover"
         popper-class="warning-popover"
@@ -17,7 +12,7 @@
         :content="item.content"
         :content="item.content"
         v-if="item.desc"
         v-if="item.desc"
       >
       >
-        <span class="line-name" slot="reference">{{ item.name }}</span>
+        <span class="line-name" slot="reference">{{ item.name }}</span>
       </el-popover>
       </el-popover>
       <span class="line-name" v-else>{{ item.name }}</span>
       <span class="line-name" v-else>{{ item.name }}</span>
       <span>{{ examPropData[item.param] }}{{ item.unit }}</span>
       <span>{{ examPropData[item.param] }}{{ item.unit }}</span>
@@ -47,21 +42,18 @@ const paramInfo = {
     name: "已待考",
     name: "已待考",
     param: "prepareCount",
     param: "prepareCount",
     desc: "已进入待考界面等待开考的考生。",
     desc: "已进入待考界面等待开考的考生。",
-    pointType: "success",
     unit: "人",
     unit: "人",
   },
   },
   exam: {
   exam: {
     name: "考试中",
     name: "考试中",
     param: "examCount",
     param: "examCount",
     desc: "正在答题的考生。",
     desc: "正在答题的考生。",
-    pointType: "primary",
     unit: "人",
     unit: "人",
   },
   },
   complete: {
   complete: {
     name: "已交卷",
     name: "已交卷",
     param: "alreadyComplete",
     param: "alreadyComplete",
     desc: "某科次的某次考试已完成“交卷”。",
     desc: "某科次的某次考试已完成“交卷”。",
-    pointType: "danger",
     unit: "人",
     unit: "人",
   },
   },
   trouble: {
   trouble: {
@@ -69,14 +61,12 @@ const paramInfo = {
     param: "clientWebsocketStatusCount",
     param: "clientWebsocketStatusCount",
     desc:
     desc:
       "考生端出现断网、断电、软硬件故障等异常导致考生端与监考端无法正常连接的考生。",
       "考生端出现断网、断电、软硬件故障等异常导致考生端与监考端无法正常连接的考生。",
-    pointType: "danger",
     unit: "人",
     unit: "人",
   },
   },
   unfinish: {
   unfinish: {
     name: "未参加考试",
     name: "未参加考试",
     param: "notComplete",
     param: "notComplete",
     desc: "某科次完成“交卷”的考试次数为0。",
     desc: "某科次完成“交卷”的考试次数为0。",
-    pointType: "danger",
     unit: "人",
     unit: "人",
   },
   },
 };
 };

+ 17 - 1
src/features/invigilation/common/TextClock.vue

@@ -1,5 +1,7 @@
 <template>
 <template>
-  <p class="text-clock">现在是{{ text }}</p>
+  <p class="text-clock">
+    <i class="icon icon-calendar-act"></i><span>{{ text }}</span>
+  </p>
 </template>
 </template>
 
 
 <script>
 <script>
@@ -44,3 +46,17 @@ export default {
   },
   },
 };
 };
 </script>
 </script>
+
+<style scoped>
+.text-clock {
+  margin: 0;
+}
+.text-clock i {
+  margin-right: 10px;
+  margin-top: -1px;
+}
+.text-clock span {
+  display: inline-block;
+  vertical-align: middle;
+}
+</style>

+ 3 - 3
src/router/invigilation.js

@@ -41,7 +41,7 @@ const routes = [
     },
     },
   },
   },
   {
   {
-    path: "online-patrol/warning-detail/:recordId",
+    path: "online-patrol/warning-detail/:examRecordId",
     name: "PatrolWarningDetail",
     name: "PatrolWarningDetail",
     component: () =>
     component: () =>
       import(
       import(
@@ -71,7 +71,7 @@ const routes = [
     },
     },
   },
   },
   {
   {
-    path: "warning-detail/:recordId",
+    path: "warning-detail/:examRecordId",
     name: "WarningDetail",
     name: "WarningDetail",
     component: () =>
     component: () =>
       import(
       import(
@@ -90,7 +90,7 @@ const routes = [
       ),
       ),
   },
   },
   {
   {
-    path: "invigilation-warning-detail/:recordId",
+    path: "invigilation-warning-detail/:examRecordId",
     name: "InvigilationWarningDetail",
     name: "InvigilationWarningDetail",
     component: () =>
     component: () =>
       import(
       import(

+ 369 - 316
src/styles/base.scss

@@ -60,18 +60,10 @@ body {
 
 
   &-head {
   &-head {
     margin-bottom: 20px;
     margin-bottom: 20px;
-    line-height: 32px;
-
-    &::after {
-      content: "";
-      display: block;
-      clear: both;
-      visibility: hidden;
-    }
-
+    display: flex;
+    align-items: stretch;
+    justify-content: space-between;
     &-left {
     &-left {
-      float: left;
-
       > h1 {
       > h1 {
         color: #202b4b;
         color: #202b4b;
         font-size: 18px;
         font-size: 18px;
@@ -79,9 +71,6 @@ body {
         margin: 0;
         margin: 0;
       }
       }
     }
     }
-    &-right {
-      float: right;
-    }
   }
   }
 }
 }
 .part {
 .part {
@@ -240,9 +229,14 @@ body {
     font-size: 14px;
     font-size: 14px;
     display: inline-block;
     display: inline-block;
     vertical-align: middle;
     vertical-align: middle;
-    margin: 0 30px 0 0;
+    margin: 0 25px 0 0;
     line-height: 20px;
     line-height: 20px;
     color: #202b4b;
     color: #202b4b;
+    cursor: pointer;
+
+    &:first-child {
+      margin: 0;
+    }
 
 
     > .icon {
     > .icon {
       margin-right: 8px;
       margin-right: 8px;
@@ -251,35 +245,17 @@ body {
 
 
     span.line-name {
     span.line-name {
       color: #626a82;
       color: #626a82;
-      margin-right: 8px;
     }
     }
     > span:last-child {
     > span:last-child {
       color: #202b4b;
       color: #202b4b;
       font-weight: 600;
       font-weight: 600;
     }
     }
 
 
-    i.line-point {
-      display: inline-block;
-      vertical-align: middle;
-      width: 8px;
-      height: 8px;
-      border: 2px solid #202b4b;
-      margin-right: 6px;
-      margin-top: -3px;
-      border-radius: 50%;
-    }
-
-    i.line-point-primary {
-      border-color: #1886fe;
-    }
-    i.line-point-info {
-      border-color: #5fcafc;
-    }
-    i.line-point-success {
-      border-color: #1cd1a1;
-    }
-    i.line-point-danger {
-      border-color: #fe5863;
+    &:hover {
+      span.line-name,
+      > span:last-child {
+        color: #1886fe;
+      }
     }
     }
   }
   }
 }
 }
@@ -343,358 +319,402 @@ body {
 
 
 // warning-detail
 // warning-detail
 .warning-detail {
 .warning-detail {
+  position: absolute;
+  top: 80px;
+  left: 20px;
+  bottom: 54px;
+  right: 20px;
+  z-index: auto;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+
   &-head {
   &-head {
-    margin: -30px -30px 0;
-    padding: 30px;
-    background: #fff;
-    border: 1px solid #f0f4f9;
-  }
-  &-title {
-    overflow: hidden;
-    padding-bottom: 20px;
-    border-bottom: 1px solid #f0f4f9;
+    margin-bottom: 10px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    flex-grow: 0;
+    flex-shrink: 0;
+    position: relative;
+
     > h2 {
     > h2 {
-      float: left;
       font-weight: 600;
       font-weight: 600;
       font-size: 18px;
       font-size: 18px;
       line-height: 28px;
       line-height: 28px;
       margin: 0;
       margin: 0;
     }
     }
-    > .el-button {
-      float: right;
-    }
   }
   }
-}
-.warning-detail-student {
-  .student-head {
-    padding: 20px 0;
+
+  &-body {
+    border-radius: 6px;
+    background-color: #fff;
     overflow: hidden;
     overflow: hidden;
+    flex-grow: 2;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+  }
+  .detail-body-head {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 13px 20px;
+    border-bottom: 1px solid #f0f4f9;
+    flex-grow: 0;
+    flex-shrink: 0;
+
     &-left {
     &-left {
-      float: left;
       > p {
       > p {
         display: inline-block;
         display: inline-block;
         vertical-align: middle;
         vertical-align: middle;
-        margin: 0;
-        line-height: 28px;
-        height: 28px;
-        margin-right: 15px;
+        line-height: 32px;
+        height: 32px;
+        margin: 0 15px 0 0;
+        color: #6d768e;
 
 
         &:first-child {
         &:first-child {
-          margin-right: 10px;
-        }
-
-        > span {
-          color: #626a82;
+          font-size: 18px;
+          color: #202b4b;
 
 
-          &:last-child {
-            color: #202b4b;
-            margin-left: 5px;
-            font-weight: 600;
+          i {
+            margin-right: 10px;
           }
           }
         }
         }
       }
       }
     }
     }
+
     &-right {
     &-right {
-      float: right;
+      .el-button {
+        padding: 0;
+        width: 32px;
+        height: 32px;
+      }
     }
     }
   }
   }
-  .student-views {
-    height: 240px;
-    margin: 0 -15px;
+  &-main {
+    flex-grow: 2;
+    position: relative;
   }
   }
+}
+.warning-action {
+  position: absolute;
+  width: 292px;
+  left: 0;
+  top: 0;
+  bottom: 0;
+  overflow-x: hidden;
+  overflow-y: auto;
+  padding: 20px;
+  border-right: 1px solid #f0f4f9;
+
   .student-avatar {
   .student-avatar {
-    padding: 0 15px;
-    width: 210px;
-    height: 100%;
-    float: left;
+    width: 180px;
+    height: 240px;
+    margin: 0 auto;
+    position: relative;
+    background-color: #e8edf3;
+
     > img {
     > img {
       display: block;
       display: block;
       width: 100%;
       width: 100%;
       height: 100%;
       height: 100%;
-      border-radius: 6px;
-      background-color: #e8edf3;
+      object-fit: cover;
+    }
+
+    .avatar-title {
+      position: absolute;
+      background-color: rgba($color: #000000, $alpha: 0.3);
+      color: #fff;
+      line-height: 30px;
+      text-align: center;
+      height: 30px;
+      bottom: 0;
+      width: 100%;
+      z-index: auto;
+      font-size: 12px;
     }
     }
   }
   }
-  .student-video {
-    margin-left: 210px;
-    height: 240px;
-    font-size: 0;
+
+  .warning-summary {
+    width: 180px;
+    margin: 24px auto;
     position: relative;
     position: relative;
 
 
+    .summary-bg {
+      &-line {
+        &:nth-of-type(1) {
+          position: absolute;
+          width: 100%;
+          left: 0;
+          top: 50%;
+          z-index: 8;
+          border-bottom: 1px dashed #e8edf3;
+        }
+        &:nth-of-type(2) {
+          position: absolute;
+          height: 100%;
+          left: 50%;
+          top: 0;
+          z-index: 8;
+          border-left: 1px dashed #e8edf3;
+        }
+      }
+      &-spin {
+        &:nth-of-type(3) {
+          position: absolute;
+          height: 8px;
+          border-left: 1px solid #b3bfce;
+          left: 50%;
+          top: 50%;
+          margin-top: -12px;
+          z-index: 9;
+        }
+        &:nth-of-type(4) {
+          position: absolute;
+          height: 8px;
+          border-left: 1px solid #b3bfce;
+          left: 50%;
+          top: 50%;
+          margin-top: 4px;
+          z-index: 9;
+        }
+        &:nth-of-type(5) {
+          position: absolute;
+          width: 8px;
+          border-bottom: 1px solid #b3bfce;
+          left: 50%;
+          top: 50%;
+          margin-left: -12px;
+          z-index: 9;
+        }
+        &:nth-of-type(6) {
+          position: absolute;
+          width: 8px;
+          border-bottom: 1px solid #b3bfce;
+          left: 50%;
+          top: 50%;
+          margin-left: 4px;
+          z-index: 9;
+        }
+      }
+    }
+
+    &-row {
+      font-size: 0;
+
+      &:nth-of-type(1) {
+        padding-bottom: 16px;
+      }
+      &:nth-of-type(2) {
+        padding-top: 16px;
+      }
+    }
+    &-col {
+      font-size: 14px;
+      display: inline-block;
+      vertical-align: top;
+      width: 50%;
+      color: #626a82;
+      margin: 0;
+
+      &:last-child {
+        text-align: right;
+      }
+      .icon {
+        vertical-align: top;
+        margin-right: 8px;
+      }
+
+      span {
+        display: inline-block;
+        vertical-align: top;
+        line-height: 1;
+      }
+      em {
+        display: block;
+        font-size: 16px;
+        font-weight: 600;
+        margin-top: 10px;
+        font-style: normal;
+        text-align: left;
+      }
+    }
+  }
+
+  .action-list {
+    font-size: 0;
+    .el-button {
+      margin-bottom: 10px;
+    }
+    button:nth-of-type(odd) {
+      margin: 0;
+    }
+  }
+}
+.warning-content {
+  position: absolute;
+  right: 0;
+  left: 292px;
+  top: 0;
+  bottom: 0;
+  overflow-x: hidden;
+  overflow-y: auto;
+  padding: 20px;
+
+  .warning-videos {
+    margin: -5px;
+    font-size: 0;
+  }
+  .student-video {
     &-item {
     &-item {
-      position: relative;
       display: inline-block;
       display: inline-block;
       vertical-align: top;
       vertical-align: top;
-      font-size: 14px;
-      padding: 0 15px;
+      padding: 5px;
       width: 50%;
       width: 50%;
-      height: 100%;
     }
     }
-    video {
-      display: block;
-      width: 100%;
-      height: 100%;
+    &-container {
       border-radius: 6px;
       border-radius: 6px;
-      box-shadow: 0 0 1px #333;
-      background: #606060;
+      overflow: hidden;
+      position: relative;
+      height: 240px;
+    }
+    &-tips {
+      position: absolute;
+      bottom: 10px;
+      left: 10px;
+      z-index: 100;
+      padding: 8px 20px;
+      font-size: 12px;
+      line-height: 1;
+      background-color: rgba(215, 215, 215, 0.3);
+      border-radius: 12px;
+      color: #fff;
     }
     }
 
 
     &-muted {
     &-muted {
       position: absolute;
       position: absolute;
       width: 24px;
       width: 24px;
       height: 24px;
       height: 24px;
-      bottom: 0;
-      right: 0;
-      z-index: 99;
+      bottom: 10px;
+      right: 10px;
+      z-index: 100;
       text-align: center;
       text-align: center;
       line-height: 24px;
       line-height: 24px;
-      background: #abb8c9;
-      border-radius: 50%;
-      margin-right: 5px;
       cursor: pointer;
       cursor: pointer;
+
       &:hover {
       &:hover {
-        background: #1886fe;
+        transform: scale(1.05, 1.05);
       }
       }
 
 
-      &.is-active {
-        background: #1886fe;
-      }
       .icon {
       .icon {
-        width: 14px;
-        height: 12px;
         margin-top: -2px;
         margin-top: -2px;
       }
       }
     }
     }
-
-    &-main {
-      position: absolute;
-      top: 0;
-      left: 15px;
-      right: 15px;
-      bottom: 0;
-      z-index: 9;
-      border-radius: 6px;
-    }
-
-    &-none {
-      height: 100%;
-      background: #606060;
-      border-radius: 6px;
-      font-size: 50px;
-      text-align: center;
-      padding-top: 90px;
-      color: #202b4b;
-    }
   }
   }
-  .student-exception {
+  .warning-track {
     margin-top: 30px;
     margin-top: 30px;
-    ul,
-    li {
-      margin: 0;
-      padding: 0;
-    }
-    ul {
-      white-space: nowrap;
-      font-size: 0;
-    }
-    li {
-      display: inline-block;
-      vertical-align: top;
-      font-size: 14px;
-      position: relative;
-      padding: 0 15px 0 38px;
-      width: 33.33%;
+    padding-top: 20px;
+    border-top: 1px solid #f0f4f9;
 
 
-      > i {
-        display: block;
-        position: absolute;
-        font-size: 29px;
-        line-height: 1;
-        color: #d9dfe8;
-        top: 3px;
-        left: 5px;
-        z-index: 9;
-        font-style: normal;
+    &-title {
+      font-size: 16px;
+      margin-bottom: 30px;
+      font-weight: 600;
 
 
-        &::after {
-          content: "";
-          display: block;
-          position: absolute;
-          background: #fff;
-          width: 15px;
-          height: 15px;
-          transform: rotate(45deg);
-          bottom: -2px;
-          right: -10px;
-          z-index: 9;
-        }
+      .icon {
+        margin-top: -2px;
+        margin-right: 10px;
       }
       }
+    }
+    &-item {
+      min-height: 100px;
+      display: flex;
+      align-items: stretch;
+      justify-content: space-between;
 
 
-      &:not(:last-child):after {
-        content: "";
-        display: block;
-        position: absolute;
-        background-image: url(../assets/bg-split-line.png);
-        background-position: 100% 100%;
-        width: 30px;
-        height: 52px;
-        top: 6px;
-        right: 10px;
-        z-index: 9;
+      &:last-child {
+        .warning-track-type {
+          &::before {
+            display: none;
+          }
+        }
       }
       }
-      h4 {
+    }
+    &-body {
+      flex-grow: 2;
+      padding-bottom: 30px;
+    }
+    &-info {
+      > h3 {
         font-size: 16px;
         font-size: 16px;
         font-weight: 600;
         font-weight: 600;
         color: #202b4b;
         color: #202b4b;
-        line-height: 22px;
+        line-height: 1;
+        margin-bottom: 6px;
+      }
+
+      > h5 {
+        font-size: 13px;
+        font-weight: 600;
+        color: #626a82;
+        line-height: 18px;
         margin-bottom: 4px;
         margin-bottom: 4px;
       }
       }
-      p {
-        margin: 0;
+
+      > p {
+        font-size: 12px;
         font-weight: 400;
         font-weight: 400;
         color: #626a82;
         color: #626a82;
-        line-height: 20px;
-        white-space: normal;
+        line-height: 16px;
+        margin: 0;
       }
       }
     }
     }
-  }
-}
-.warning-detail-body {
-  margin-top: 30px;
-  border-radius: 6px;
-  background: #fff;
-}
-.warning-body-head {
-  border-bottom: 1px solid #f0f4f9;
-  padding: 20px;
-  &-action {
-    float: left;
-    > h3 {
-      line-height: 32px;
-      display: inline-block;
-      vertical-align: middle;
-      font-size: 18px;
-      font-weight: 600;
-      margin: 0 30px 0 0;
-    }
 
 
-    .el-button >>> i.icon {
-      margin: 0;
-    }
-  }
-  &-call {
-    margin: 0 10px;
-  }
-  &-info {
-    float: right;
-    padding: 0;
-    > p {
-      height: 32px;
-      line-height: 32px;
-      margin-right: 20px;
-    }
-    .el-button {
-      vertical-align: middle;
-    }
-  }
-}
-.warning-body-main {
-  min-height: 400px;
-  padding: 15px;
-}
-.warning-history {
-  display: flex;
-  min-height: 100px;
-  align-items: center;
-  &-info {
-    padding: 15px 30px 15px 0;
-    text-align: right;
-    width: 210px;
-    flex-shrink: 0;
-    > h3 {
-      font-size: 16px;
-      font-weight: 600;
-      color: #202b4b;
-      line-height: 22px;
-      margin-bottom: 4px;
-    }
-
-    > h5 {
-      font-size: 13px;
-      font-weight: 600;
-      color: #626a82;
-      line-height: 18px;
-      margin-bottom: 4px;
-    }
-
-    > p {
-      font-size: 12px;
-      font-weight: 400;
-      color: #626a82;
-      line-height: 17px;
-      margin: 0;
-    }
-  }
-
-  &-type {
-    position: relative;
-    width: 36px;
-    height: 36px;
-    padding-top: 5px;
-    text-align: center;
-    border-radius: 50%;
-    border: 2px solid #1cd1a1;
-    background-color: #fff;
-    z-index: 9;
-    flex-shrink: 0;
-    &.type-exception {
-      border-color: #fe5863;
-    }
-    > i {
-      width: 18px;
-      height: 17px;
-    }
-    i.icon-current-step {
-      width: 12px;
-    }
-  }
+    &-type {
+      position: relative;
+      text-align: center;
+      flex-grow: 0;
+      flex-shrink: 0;
+      padding: 0 20px;
 
 
-  &-media {
-    padding: 10px 0 10px 20px;
-    position: relative;
-    min-height: 100px;
-    z-index: 8;
+      .icon {
+        position: relative;
+        z-index: 9;
+      }
 
 
-    &::before {
-      content: "";
-      display: block;
-      position: absolute;
-      height: 100%;
-      border-left: 1px solid #f0f4f9;
-      left: -18px;
-      top: 0;
+      &::before {
+        content: "";
+        display: block;
+        position: absolute;
+        height: 100%;
+        border-left: 1px dashed #abb8c9;
+        left: 50%;
+        top: 0;
+        z-index: 8;
+      }
     }
     }
-  }
-  .media-list {
-    padding: 0;
-    margin: 0;
-    list-style: none;
-
-    li {
-      display: inline-block;
-      vertical-align: top;
-      width: 160px;
-      height: 100px;
-      margin: 5px 10px;
-      border-radius: 6px;
-      background-color: #e8edf3;
-      overflow: hidden;
+    &-media {
+      padding: 0;
+      margin: 10px 0 0 0;
+      list-style: none;
 
 
-      > img {
-        width: 100%;
-        height: 100%;
-        object-fit: contain;
-        cursor: pointer;
+      li {
+        display: inline-block;
+        vertical-align: top;
+        width: 200px;
+        height: 100px;
+        margin: 0 10px 5px 0;
+        border-radius: 6px;
+        background-color: #e8edf3;
+        overflow: hidden;
+
+        > img {
+          width: 100%;
+          height: 100%;
+          object-fit: contain;
+          cursor: pointer;
+        }
       }
       }
     }
     }
   }
   }
@@ -1019,12 +1039,45 @@ body {
     top: 0;
     top: 0;
     left: 0;
     left: 0;
     z-index: 99;
     z-index: 99;
-    background-color: rgba(0, 0, 0, 0.5);
-    color: #fff;
+    background-color: rgba(0, 0, 0, 1);
+    color: #d0d0d0;
     text-align: center;
     text-align: center;
     display: flex;
     display: flex;
     align-items: center;
     align-items: center;
     justify-content: space-around;
     justify-content: space-around;
+    border-radius: 6px;
+    font-size: 14px;
+
+    &-content {
+      span {
+        display: inline-block;
+        vertical-align: middle;
+      }
+
+      .el-button {
+        padding: 0;
+        font-size: 14px;
+        outline: none;
+        border: none;
+      }
+    }
+  }
+  .media-video-muted {
+    position: absolute;
+    width: 28px;
+    height: 28px;
+    bottom: 10px;
+    right: 10px;
+    z-index: 100;
+    text-align: center;
+    line-height: 28px;
+    background-color: rgba(215, 215, 215, 0.3);
+    border-radius: 50%;
+    cursor: pointer;
+
+    &:hover {
+      transform: scale(1.1, 1.1);
+    }
   }
   }
 }
 }
 
 

+ 2 - 4
src/styles/element-ui-custom.scss

@@ -48,12 +48,10 @@
 // .el-button
 // .el-button
 .el-button {
 .el-button {
   border-radius: 6px;
   border-radius: 6px;
-
-  .icon {
+  > .icon {
     margin-right: 5px;
     margin-right: 5px;
-    width: 13px;
-    height: 13px;
   }
   }
+
   .icon-view {
   .icon-view {
     height: 10px;
     height: 10px;
   }
   }

+ 1 - 1
src/styles/element-variables.scss

@@ -3,7 +3,7 @@ Write your variables here. All available variables can be
 found in element-ui/packages/theme-chalk/src/common/var.scss.
 found in element-ui/packages/theme-chalk/src/common/var.scss.
 For example, to overwrite the theme color:
 For example, to overwrite the theme color:
 */
 */
-$--color-primary: #1886fe;
+$--color-primary: #7196be;
 $--color-success: #1cd1a1;
 $--color-success: #1cd1a1;
 
 
 /* icon font path, required */
 /* icon font path, required */

+ 240 - 142
src/styles/icons.scss

@@ -1,142 +1,240 @@
-// icon
-.icon {
-  display: inline-block;
-  vertical-align: middle;
-  width: 16px;
-  height: 16px;
-  background-repeat: no-repeat;
-  background-size: 100% 100%;
-
-  &-base {
-    background-image: url(../assets/icon-base.png);
-  }
-  &-base-act {
-    background-image: url(../assets/icon-base-act.png);
-  }
-  &-user {
-    background-image: url(../assets/icon-user.png);
-  }
-  &-user-act {
-    background-image: url(../assets/icon-user-act.png);
-  }
-  &-business {
-    background-image: url(../assets/icon-business.png);
-  }
-  &-business-act {
-    background-image: url(../assets/icon-business-act.png);
-  }
-  &-invigilation {
-    background-image: url(../assets/icon-invigilation.png);
-  }
-  &-invigilation-act {
-    background-image: url(../assets/icon-invigilation-act.png);
-  }
-  &-warning {
-    background-image: url(../assets/icon-warning.png);
-  }
-  &-warning-act {
-    background-image: url(../assets/icon-warning-act.png);
-  }
-
-  &-clean {
-    background-image: url(../assets/icon-clean.png);
-  }
-  &-handle {
-    background-image: url(../assets/icon-handle.png);
-  }
-  &-over {
-    background-image: url(../assets/icon-over.png);
-  }
-  &-scan {
-    background-image: url(../assets/icon-scan.png);
-  }
-  &-stop {
-    background-image: url(../assets/icon-stop.png);
-  }
-  &-forbide {
-    background-image: url(../assets/icon-forbide.png);
-  }
-  &-view {
-    background-image: url(../assets/icon-view.png);
-  }
-  &-text {
-    background-image: url(../assets/icon-text.png);
-  }
-  &-audio {
-    background-image: url(../assets/icon-audio.png);
-  }
-  &-logout {
-    background-image: url(../assets/icon-logout.png);
-  }
-  &-error {
-    background-image: url(../assets/icon-error.png);
-  }
-
-  &-reexam {
-    background-image: url(../assets/icon-reexam.png);
-  }
-  &-password {
-    background-image: url(../assets/icon-password.png);
-  }
-  &-seal {
-    background-image: url(../assets/icon-seal.png);
-  }
-  &-upload {
-    height: 12px;
-    background-image: url(../assets/icon-upload.png);
-  }
-  &-download {
-    height: 12px;
-    background-image: url(../assets/icon-download.png);
-  }
-  &-analysis {
-    background-image: url(../assets/icon-analysis.png);
-  }
-  &-rate {
-    background-image: url(../assets/icon-rate.png);
-  }
-  &-add {
-    background-image: url(../assets/icon-add.png);
-  }
-  &-copy {
-    background-image: url(../assets/icon-copy.png);
-  }
-  &-ring {
-    background-image: url(../assets/icon-ring.png);
-  }
-  &-right {
-    width: 15px;
-    height: 12px;
-    background-image: url(../assets/icon-right.png);
-  }
-  &-wrong {
-    width: 13px;
-    height: 12px;
-    background-image: url(../assets/icon-wrong.png);
-  }
-  &-users {
-    height: 13px;
-    background-image: url(../assets/icon-users.png);
-  }
-  &-exam-detail {
-    background-image: url(../assets/icon-exam-detail.png);
-  }
-  &-alarm-clock {
-    background-image: url(../assets/icon-alarm-clock.png);
-  }
-  &-net-break {
-    background-image: url(../assets/icon-net-break.png);
-  }
-  &-full-screen {
-    background-image: url(../assets/icon-full-screen.png);
-  }
-  &-current-step {
-    background-image: url(../assets/icon-current-step.png);
-  }
-  &-arrows-up {
-    background-image: url(../assets/icon-arrows-up.png);
-  }
-  &-arrows-down {
-    background-image: url(../assets/icon-arrows-down.png);
-  }
-}
+// icon
+.icon {
+  display: inline-block;
+  vertical-align: middle;
+  width: 16px;
+  height: 16px;
+  background-repeat: no-repeat;
+  background-size: 100% 100%;
+
+  &-base {
+    background-image: url(../assets/icon-base.png);
+  }
+  &-base-act {
+    background-image: url(../assets/icon-base-act.png);
+  }
+  &-user {
+    background-image: url(../assets/icon-user.png);
+  }
+  &-user-act {
+    background-image: url(../assets/icon-user-act.png);
+  }
+  &-business {
+    background-image: url(../assets/icon-business.png);
+  }
+  &-business-act {
+    background-image: url(../assets/icon-business-act.png);
+  }
+  &-invigilation {
+    background-image: url(../assets/icon-invigilation.png);
+  }
+  &-invigilation-act {
+    background-image: url(../assets/icon-invigilation-act.png);
+  }
+  &-warning {
+    background-image: url(../assets/icon-warning.png);
+  }
+  &-warning-act {
+    background-image: url(../assets/icon-warning-act.png);
+  }
+
+  &-clean {
+    background-image: url(../assets/icon-clean.png);
+  }
+  &-handle {
+    background-image: url(../assets/icon-handle.png);
+  }
+  &-over {
+    background-image: url(../assets/icon-over.png);
+  }
+  &-scan {
+    background-image: url(../assets/icon-scan.png);
+  }
+  &-stop {
+    background-image: url(../assets/icon-stop.png);
+  }
+  &-forbide {
+    background-image: url(../assets/icon-forbide.png);
+  }
+  &-view {
+    background-image: url(../assets/icon-view.png);
+  }
+  &-text {
+    background-image: url(../assets/icon-text.png);
+  }
+  &-audio {
+    height: 12px;
+    background-image: url(../assets/icon-audio.png);
+  }
+  &-audio-act {
+    height: 12px;
+    background-image: url(../assets/icon-audio-act.png);
+  }
+  &-logout {
+    background-image: url(../assets/icon-logout.png);
+  }
+  &-error {
+    background-image: url(../assets/icon-error.png);
+  }
+
+  &-reexam {
+    background-image: url(../assets/icon-reexam.png);
+  }
+  &-password {
+    background-image: url(../assets/icon-password.png);
+  }
+  &-seal {
+    background-image: url(../assets/icon-seal.png);
+  }
+  &-upload {
+    height: 12px;
+    background-image: url(../assets/icon-upload.png);
+  }
+  &-download {
+    height: 12px;
+    background-image: url(../assets/icon-download.png);
+  }
+  &-analysis {
+    background-image: url(../assets/icon-analysis.png);
+  }
+  &-rate {
+    background-image: url(../assets/icon-rate.png);
+  }
+  &-add {
+    background-image: url(../assets/icon-add.png);
+  }
+  &-copy {
+    background-image: url(../assets/icon-copy.png);
+  }
+  &-ring {
+    background-image: url(../assets/icon-ring.png);
+  }
+  &-right {
+    width: 15px;
+    height: 12px;
+    background-image: url(../assets/icon-right.png);
+  }
+  &-wrong {
+    width: 13px;
+    height: 12px;
+    background-image: url(../assets/icon-wrong.png);
+  }
+  &-users {
+    height: 12px;
+    background-image: url(../assets/icon-users.png);
+  }
+  &-track {
+    width: 16px;
+    height: 14px;
+    background-image: url(../assets/icon-track.png);
+  }
+  &-track-common {
+    width: 36px;
+    height: 36px;
+    background-image: url(../assets/icon-track-common.png);
+  }
+  &-track-warning {
+    width: 36px;
+    height: 36px;
+    background-image: url(../assets/icon-track-warning.png);
+  }
+  &-text-message {
+    width: 12px;
+    height: 12px;
+    background-image: url(../assets/icon-text-message.png);
+  }
+  &-call {
+    width: 12px;
+    height: 12px;
+    background-image: url(../assets/icon-call.png);
+  }
+  &-media {
+    width: 12px;
+    height: 12px;
+    background-image: url(../assets/icon-media.png);
+  }
+  &-record {
+    width: 12px;
+    height: 12px;
+    background-image: url(../assets/icon-record.png);
+  }
+  &-paper-danger {
+    width: 12px;
+    height: 12px;
+    background-image: url(../assets/icon-paper-danger.png);
+  }
+  &-info-danger {
+    width: 12px;
+    height: 12px;
+    background-image: url(../assets/icon-info-danger.png);
+  }
+  &-info {
+    width: 12px;
+    height: 12px;
+    background-image: url(../assets/icon-info.png);
+  }
+  &-success {
+    width: 12px;
+    height: 12px;
+    background-image: url(../assets/icon-success.png);
+  }
+  &-face {
+    width: 12px;
+    height: 12px;
+    background-image: url(../assets/icon-face.png);
+  }
+  &-bell {
+    width: 12px;
+    height: 12px;
+    background-image: url(../assets/icon-bell.png);
+  }
+  &-clock {
+    width: 12px;
+    height: 12px;
+    background-image: url(../assets/icon-clock.png);
+  }
+  &-clock-danger {
+    width: 12px;
+    height: 12px;
+    background-image: url(../assets/icon-clock-danger.png);
+  }
+  &-calendar-act {
+    background-image: url(../assets/icon-calendar-act.png);
+  }
+  &-arrow-left {
+    width: 32px;
+    height: 32px;
+    background-image: url(../assets/icon-arrow-left.png);
+  }
+  &-arrow-right {
+    width: 32px;
+    height: 32px;
+    background-image: url(../assets/icon-arrow-right.png);
+  }
+  &-person {
+    width: 16px;
+    height: 14px;
+    background-image: url(../assets/icon-person.png);
+  }
+  &-exam-detail {
+    background-image: url(../assets/icon-exam-detail.png);
+  }
+  &-alarm-clock {
+    background-image: url(../assets/icon-alarm-clock.png);
+  }
+  &-net-break {
+    background-image: url(../assets/icon-net-break.png);
+  }
+  &-full-screen {
+    background-image: url(../assets/icon-full-screen.png);
+  }
+  &-current-step {
+    background-image: url(../assets/icon-current-step.png);
+  }
+  &-arrows-up {
+    background-image: url(../assets/icon-arrows-up.png);
+  }
+  &-arrows-down {
+    background-image: url(../assets/icon-arrows-down.png);
+  }
+}

+ 5 - 0
src/utils/utils.js

@@ -235,3 +235,8 @@ export function randomCode(len = 16) {
 
 
   return stepNums.join("");
   return stepNums.join("");
 }
 }
+
+export function snakeToHump(content) {
+  let cont = content.toLowerCase().split("_");
+  return cont.map((item) => item[0].toUpperCase() + item.substr(1)).join("");
+}