zhangjie преди 4 години
родител
ревизия
0a919f6edf

+ 2 - 2
src/api/invigilation.js

@@ -141,10 +141,10 @@ export function communicationList(datas) {
   );
 }
 
-export function communicationOver(callCancelBackendMobile) {
+export function communicationOver(recordId) {
   return httpApp.post(
     "/api/admin/monitor/call/cancel",
-    { callCancelBackendMobile },
+    { recordId },
     {
       noErrorMessage: true,
     }

BIN
src/assets/icon-new-tips.png


+ 1 - 1
src/features/examwork/InvigilateManagement/InvigilateManagement.vue

@@ -71,7 +71,7 @@
         </div>
       </el-table-column>
     </el-table>
-    <div class="page float-right">
+    <div class="part-page">
       <el-pagination
         background
         @current-change="handleCurrentChange"

+ 26 - 9
src/features/invigilation/InvigilationDetail/InvigilationDetail.vue

@@ -94,7 +94,7 @@
               <el-option
                 v-for="(val, key) in STUDENT_ONLINE_STATUS"
                 :key="key"
-                :value="key * 1"
+                :value="key"
                 :label="val"
               ></el-option>
             </el-select>
@@ -245,10 +245,10 @@ import {
   STUDENT_BEHAVIOR_STATUS,
 } from "@/constant/constants";
 import SummaryLine from "../common/SummaryLine";
-import { mapActions } from "vuex";
+import { mapActions, mapMutations } from "vuex";
 
 export default {
-  name: "invigilation-detail",
+  name: "InvigilationDetail",
   components: { SummaryLine },
   data() {
     return {
@@ -293,6 +293,7 @@ export default {
   },
   methods: {
     ...mapActions("invigilation", ["updateDetailIds"]),
+    ...mapMutations("invigilation", ["setDetailIds"]),
     async initData() {
       await this.getExamBatchList();
       this.filter.examId = this.examBatchs[0] && this.examBatchs[0].id;
@@ -315,12 +316,19 @@ export default {
       this.current = page;
       this.getList();
     },
-    toSearch() {
-      this.updateDetailIds({
-        filterData: this.filter,
-        fetchFunc: invigilationHistoryList,
-      });
-      this.toPage(1);
+    async toSearch() {
+      this.current = 1;
+      await this.getList();
+
+      if (this.total > this.size) {
+        this.updateDetailIds({
+          filterData: this.filter,
+          fetchFunc: invigilationHistoryList,
+        });
+      } else {
+        const ids = this.dataList.map((item) => item.examRecordId);
+        this.setDetailIds([...new Set(ids)]);
+      }
     },
     async getExamBatchList() {
       const userId = this.IS_INVIGILATE ? this.user.id : null;
@@ -361,5 +369,14 @@ export default {
       });
     },
   },
+  beforeRouteLeave(to, from, next) {
+    if (
+      to.name !== "WarningDetail" &&
+      to.name !== "InvigilationWarningDetail"
+    ) {
+      this.$destroy();
+    }
+    next();
+  },
 };
 </script>

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

@@ -245,7 +245,7 @@ import SummaryLine from "../common/SummaryLine";
 import RightOrWrong from "../common/RightOrWrong";
 
 export default {
-  name: "online-patrol",
+  name: "OnlinePatrol",
   components: { EchartRender, SummaryLine, RightOrWrong },
   data() {
     return {
@@ -377,6 +377,12 @@ export default {
       });
     },
   },
+  beforeRouteLeave(to, from, next) {
+    if (to.name !== "PatrolExamDetail") {
+      this.$destroy();
+    }
+    next();
+  },
 };
 </script>
 

+ 4 - 1
src/features/invigilation/RealtimeMonitoring/ExamBatchDialog.vue

@@ -22,6 +22,7 @@
 
 <script>
 import { examMonitorBatchList } from "@/api/invigilation";
+import { dateFormatForAPI } from "@/utils/utils";
 
 export default {
   name: "exam-batch-dialog",
@@ -49,7 +50,9 @@ export default {
       this.examBatchs = res.data.data.records.map((item) => {
         return {
           ...item,
-          label: `${item.name}【${item.startTime} - ${item.endTime}】`,
+          label: `${item.name}【${dateFormatForAPI(
+            item.startTime
+          )} - ${dateFormatForAPI(item.endTime)}】`,
         };
       });
 

+ 108 - 62
src/features/invigilation/RealtimeMonitoring/RealtimeMonitoring.vue

@@ -49,6 +49,7 @@
           data-type="trouble"
           :exam-id="filter.examId"
           v-if="filter.examId"
+          ref="SummaryLine"
         ></summary-line>
         <div class="part-filter-info-sub" v-if="!this.IS_INSPECTION">
           <el-badge
@@ -218,10 +219,13 @@
           </span>
         </template>
       </el-table-column>
-      <el-table-column label="操作" width="100">
+      <el-table-column label="操作" width="125">
         <template slot-scope="scope">
           <el-button
-            class="btn-table-icon"
+            :class="[
+              'btn-table-icon',
+              { 'warn-new-tips': scope.row.warningNew },
+            ]"
             type="primary"
             icon="icon icon-view"
             @click="toDetail(scope.row)"
@@ -233,7 +237,7 @@
     <div class="invigilation-student-list" v-else>
       <div
         class="invigilation-student-item"
-        v-for="item in videoList"
+        v-for="item in dataList"
         :key="item.examStudentId"
       >
         <invigilation-student :data="item"></invigilation-student>
@@ -288,7 +292,7 @@ import {
   CLIENT_WEBSOCKET_STATUS,
   MONITOR_STATUS_SOURCE,
 } from "@/constant/constants";
-import { mapActions } from "vuex";
+import { mapMutations, mapActions } from "vuex";
 
 export default {
   name: "realtime-monitoring",
@@ -319,14 +323,15 @@ export default {
       MONITOR_STATUS_SOURCE,
       IS_INSPECTION: false,
       hasNewWarning: false,
+      loopRunning: false,
+      loopSetTs: [],
       communicationCount: 0,
       curExamBatch: {},
       curViewingAngle: {},
       noticeCaches: {},
-      setT: null,
       current: 1,
       total: 0,
-      size: 100,
+      size: 24,
       multipleSelection: [],
       batchId: "",
       batchs: [],
@@ -334,7 +339,6 @@ export default {
       subjects: [],
       pageType: "0",
       dataList: [],
-      videoList: [],
       viewingAngles: [
         {
           code: "1",
@@ -363,7 +367,49 @@ export default {
     };
   },
   methods: {
-    ...mapActions("invigilation", ["setDetailIds"]),
+    ...mapActions("invigilation", ["updateDetailIds"]),
+    ...mapMutations("invigilation", ["setDetailIds"]),
+    clearLoopSetTs() {
+      if (!this.loopSetTs.length) return;
+      this.loopSetTs.forEach((sett) => {
+        clearTimeout(sett);
+      });
+      this.loopSetTs = [];
+    },
+    async timerUpdatePage() {
+      this.clearLoopSetTs();
+      if (!this.loopRunning) return;
+
+      await this.getList().catch(() => {});
+      await this.$refs.SummaryLine.initData().catch(() => {});
+      if (!this.IS_INSPECTION) {
+        await this.getMonitorCallCount().catch(() => {});
+        await this.fetchWarningNotice().catch(() => {});
+      }
+
+      this.loopSetTs.push(
+        setTimeout(() => {
+          this.timerUpdatePage();
+        }, 10 * 1000)
+      );
+    },
+    examChange(examBatch) {
+      if (!examBatch) return;
+      this.filter.examId = examBatch.id;
+      this.curExamBatch = examBatch;
+      this.toSearch();
+
+      this.timerUpdatePage();
+    },
+    pageTypeChange(pageType) {
+      this.pageType = pageType;
+      this.toPage(1);
+      this.multipleSelection = [];
+    },
+    viewingAngleChange(data) {
+      this.curViewingAngle = data;
+      // TODO:视角切换
+    },
     async getList() {
       const datas = {
         ...this.filter,
@@ -380,51 +426,65 @@ export default {
         });
       } else {
         res = await invigilateVideoList(datas);
-        this.videoList = res.data.data.records.map((item) => {
+        this.dataList = res.data.data.records.map((item) => {
           item.liveUrl = `http://live.qmth.com.cn/live/${item.monitorLiveUrl.toLowerCase()}.flv`;
           return item;
         });
       }
-      const ids = res.data.data.records.map((item) => item.examRecordId);
-      this.setDetailIds([...new Set(ids)]);
-
       this.total = res.data.data.total;
     },
     toPage(page) {
       this.current = page;
       this.getList();
     },
-    toSearch() {
-      this.toPage(1);
+    async toSearch() {
+      this.current = 1;
+      await this.getList();
+
+      if (this.total > this.size) {
+        this.updateDetailIds({
+          filterData: this.filter,
+          fetchFunc: invigilateList,
+        });
+      } else {
+        const ids = this.dataList.map((item) => item.examRecordId);
+        this.setDetailIds([...new Set(ids)]);
+      }
     },
     async getMonitorCallCount() {
       if (!this.filter.examId) return;
       const res = await monitorCallCount(this.filter.examId);
       this.communicationCount = res.data.data.count || 0;
     },
-    examChange(examBatch) {
-      if (!examBatch) return;
-      this.filter.examId = examBatch.id;
-      this.curExamBatch = examBatch;
-      this.toSearch();
-      if (this.IS_INSPECTION) return;
+    async fetchWarningNotice() {
+      if (!this.filter.examId) return;
 
-      this.getMonitorCallCount();
-      this.fetchWarningNotice();
-    },
-    pageTypeChange(pageType) {
-      this.pageType = pageType;
-      this.toPage(1);
-      this.multipleSelection = [];
+      const res = await invigilationWarningMessage(this.filter.examId);
+      res.data.data.forEach((item) => {
+        const stdKey = item.examRecordId;
+        if (!this.noticeCaches[stdKey]) {
+          this.noticeCaches[stdKey] = item;
+          this.$notify({
+            duration: 5 * 1000,
+            dangerouslyUseHTMLString: true,
+            customClass: "msg-monitor-magbox",
+            position: "bottom-right",
+            offset: 50,
+            message: `
+              <div class="msg-monitor">
+                <span class="msg-monitor-icon"><i class="icon icon-warning"></i></span>
+                <span>注意:<b>${item.name}</b>发现违纪,</span>
+                <span class="msg-monitor-action" onclick="window.inviligateWarning('${item.examRecordId}')">立即处理</span>
+              </div>
+            `,
+          });
+        }
+      });
     },
     handleSelectionChange(val) {
       console.log(val);
       this.multipleSelection = val;
     },
-    viewingAngleChange(data) {
-      this.curViewingAngle = data;
-      // TODO:视角切换
-    },
     async finishInvigilation() {
       if (!this.multipleSelection.length) {
         this.$message.error("请先选择数据!");
@@ -465,36 +525,6 @@ export default {
         },
       });
     },
-    async fetchWarningNotice() {
-      if (this.setT) clearTimeout(this.setT);
-      if (!this.filter.examId) return;
-
-      const res = await invigilationWarningMessage(this.filter.examId);
-      res.data.data.forEach((item) => {
-        const stdKey = item.examRecordId;
-        if (!this.noticeCaches[stdKey]) {
-          this.noticeCaches[stdKey] = item;
-          this.$notify({
-            duration: 5 * 1000,
-            dangerouslyUseHTMLString: true,
-            customClass: "msg-monitor-magbox",
-            position: "bottom-right",
-            offset: 50,
-            message: `
-              <div class="msg-monitor">
-                <span class="msg-monitor-icon"><i class="icon icon-warning"></i></span>
-                <span>注意:<b>${item.name}</b>发现违纪,</span>
-                <span class="msg-monitor-action" onclick="window.inviligateWarning(${item.examRecordId})">立即处理</span>
-              </div>
-            `,
-          });
-        }
-      });
-
-      this.setT = setTimeout(() => {
-        this.fetchWarningNotice();
-      }, 10 * 1000);
-    },
     toDetail(row) {
       const routerName = this.IS_INSPECTION
         ? "PatrolWarningDetail"
@@ -506,7 +536,8 @@ export default {
     },
   },
   beforeDestroy() {
-    if (this.setT) clearTimeout(this.setT);
+    this.loopRunning = false;
+    this.clearLoopSetTs();
     delete window.inviligateWarning;
   },
 };
@@ -627,4 +658,19 @@ export default {
     width: 25%;
   }
 }
+.warn-new-tips {
+  position: relative;
+
+  &::after {
+    content: "";
+    display: block;
+    position: absolute;
+    width: 32px;
+    height: 16px;
+    right: -32px;
+    top: 0;
+    background-image: url(../../../assets/icon-new-tips.png);
+    background-size: 100% 100%;
+  }
+}
 </style>

+ 8 - 4
src/features/invigilation/RealtimeMonitoring/VideoCommunication.vue

@@ -71,8 +71,8 @@
           <el-button round type="danger" @click="hangup">结束通话</el-button>
         </div>
         <div class="communication-info">
-          <span>当前网络状态良好</span>
-          <span>持续时长:{{ durationTime }}</span>
+          <!-- <span>当前网络状态良好</span> -->
+          <span>持续时长:<second-timer ref="SecondTimer"></second-timer></span>
         </div>
       </div>
       <span slot="footer" class="dialog-footer"> </span>
@@ -87,9 +87,11 @@ import {
   communicationOver,
   getUserMonitorKey,
 } from "@/api/invigilation";
+import SecondTimer from "../common/SecondTimer";
 
 export default {
   name: "video-communication",
+  components: { SecondTimer },
   data() {
     return {
       examId: this.$route.params.examId,
@@ -100,7 +102,6 @@ export default {
       total: 0,
       size: 100,
       setT: null,
-      durationTime: "00:10:23",
       appId: "1400411036",
       userMonitor: {},
       client: null,
@@ -149,7 +150,7 @@ export default {
     async answer(student, isVideo) {
       await this.initClient(student.examRecordId);
       // 结束学生的通话申请
-      await communicationOver({ recordId: student.examRecordId });
+      await communicationOver(student.examRecordId);
 
       this.dialogVisible = true;
       // 添加远程用户视频发布监听
@@ -168,6 +169,7 @@ export default {
       });
       this.client.on("stream-subscribed", (event) => {
         const remoteStream = event.stream;
+        this.$refs.SecondTimer.start();
         remoteStream.play("communication-host", { objectFit: "contain" });
       });
 
@@ -219,6 +221,8 @@ export default {
       this.localStream.play("communication-guest", { muted: true });
     },
     async hangup() {
+      this.$refs.SecondTimer.end();
+
       // 取消发布本地视频
       let unpublishStreamResult = true;
       this.client.unpublish(this.localStream).catch((error) => {

+ 7 - 3
src/features/invigilation/RealtimeMonitoring/WarningDetail.vue

@@ -254,8 +254,8 @@
           <el-button round type="danger" @click="hangup">结束通话</el-button>
         </div>
         <div class="communication-info">
-          <span>当前网络状态良好</span>
-          <span>持续时长:{{ durationTime }}</span>
+          <!-- <span>当前网络状态良好</span> -->
+          <span>持续时长:<second-timer ref="SecondTimer"></second-timer></span>
         </div>
       </div>
       <div class="communication-wait" v-show="isWaiting">
@@ -287,6 +287,7 @@ import FlvMedia from "../common/FlvMedia";
 import StudentBreachDialog from "./StudentBreachDialog";
 import WarningTextMessageDialog from "./WarningTextMessageDialog";
 import AudioRecordDialog from "./audioRecord/AudioRecordDialog";
+import SecondTimer from "../common/SecondTimer";
 import { formatDate, timeNumberToText } from "@/utils/utils";
 import { mapState } from "vuex";
 
@@ -297,6 +298,7 @@ export default {
     StudentBreachDialog,
     WarningTextMessageDialog,
     AudioRecordDialog,
+    SecondTimer,
   },
   data() {
     return {
@@ -319,7 +321,6 @@ export default {
       secondViewVideoReady: false,
       holding: false,
       // communication
-      durationTime: "00:10:23",
       appId: "1400411036",
       userMonitor: {},
       client: null,
@@ -558,6 +559,7 @@ export default {
 
         this.isWaiting = false;
         this.$nextTick(() => {
+          this.$refs.SecondTimer.start();
           remoteStream.play("communication-host", { objectFit: "contain" });
         });
       });
@@ -611,6 +613,8 @@ export default {
     async hangup() {
       if (this.setT) clearTimeout(this.setT);
 
+      this.$refs.SecondTimer.end();
+
       // 取消发布本地视频
       let unpublishStreamResult = true;
       this.client.unpublish(this.localStream).catch((error) => {

+ 1 - 1
src/features/invigilation/RealtimeMonitoring/audioRecord/AudioRecordDialog.vue

@@ -65,7 +65,7 @@ import AudioRecord from "./audioRecord";
 function recordTimeToText(timeNumber) {
   const MINUTE_TIME = 60 * 1000;
   const SECOND_TIME = 1000;
-  let [minute, second] = [0, 0, 0, 0];
+  let [minute, second] = [0, 0];
   let residueTime = timeNumber;
 
   if (residueTime >= MINUTE_TIME) {

+ 24 - 8
src/features/invigilation/WarningManage/WarningManage.vue

@@ -201,11 +201,11 @@ import {
   invigilationWarningList,
   clearInvigilationUnreadWarningList,
 } from "@/api/invigilation";
-import { mapActions } from "vuex";
+import { mapActions, mapMutations } from "vuex";
 
 import { APPROVE_STATUS } from "@/constant/constants";
 export default {
-  name: "warning-manage",
+  name: "WarningManage",
   data() {
     return {
       filter: {
@@ -247,6 +247,7 @@ export default {
   },
   methods: {
     ...mapActions("invigilation", ["updateDetailIds"]),
+    ...mapMutations("invigilation", ["setDetailIds"]),
     async initData() {
       await this.getExamBatchList();
       this.filter.examId = this.examBatchs[0] && this.examBatchs[0].id;
@@ -269,12 +270,18 @@ export default {
       this.current = page;
       this.getList();
     },
-    toSearch() {
-      this.updateDetailIds({
-        filterData: this.filter,
-        fetchFunc: invigilationWarningList,
-      });
-      this.toPage(1);
+    async toSearch() {
+      this.current = 1;
+      await this.getList();
+      if (this.total > this.size) {
+        this.updateDetailIds({
+          filterData: this.filter,
+          fetchFunc: invigilationWarningList,
+        });
+      } else {
+        const ids = this.dataList.map((item) => item.examRecordId);
+        this.setDetailIds([...new Set(ids)]);
+      }
     },
     async getExamBatchList() {
       const userId = this.IS_INVIGILATE ? this.user.id : null;
@@ -333,5 +340,14 @@ export default {
       });
     },
   },
+  beforeRouteLeave(to, from, next) {
+    if (
+      to.name !== "WarningDetail" &&
+      to.name !== "InvigilationWarningDetail"
+    ) {
+      this.$destroy();
+    }
+    next();
+  },
 };
 </script>

+ 58 - 0
src/features/invigilation/common/SecondTimer.vue

@@ -0,0 +1,58 @@
+<template>
+  <span class="second-timer">
+    {{ duration }}
+  </span>
+</template>
+
+<script>
+export default {
+  name: "SecondTimer",
+  data() {
+    return {
+      duration: "00:00:00",
+      recordSecondCount: 0,
+      setT: null,
+    };
+  },
+  methods: {
+    start() {
+      this.setT = setInterval(() => {
+        this.recordSecondCount++;
+        this.duration = this.timeToText(this.recordSecondCount);
+      }, 1000);
+    },
+    end() {
+      this.pause();
+      this.duration = "00:00:00";
+      this.recordSecondCount = 0;
+    },
+    pause() {
+      clearTimeout(this.setT);
+      this.$emit("on-duration", this.recordSecondCount, this.duration);
+    },
+    timeToText(timeNumber) {
+      const HOUR_TIME = 60 * 60;
+      const MINUTE_TIME = 60;
+      let [hour, minute, second] = [0, 0, 0];
+      let residueTime = timeNumber;
+
+      if (residueTime >= HOUR_TIME) {
+        hour = Math.floor(residueTime / HOUR_TIME);
+        residueTime -= hour * HOUR_TIME;
+      }
+      if (residueTime >= MINUTE_TIME) {
+        minute = Math.floor(residueTime / MINUTE_TIME);
+        residueTime -= minute * MINUTE_TIME;
+      }
+      second = residueTime;
+
+      return [hour, minute, second]
+        .map((item) => ("00" + item).substr(("" + item).length))
+        .join(":");
+    },
+  },
+  beforeDestroy() {
+    if (this.setT) clearInterval(this.setT);
+  },
+};
+</script>

+ 1 - 1
src/styles/base.scss

@@ -725,7 +725,7 @@ body {
       padding: 0 20px;
       color: #fff;
 
-      span {
+      > span {
         display: block;
         float: right;
 

+ 11 - 1
src/views/Layout/components/AppMain.vue

@@ -2,7 +2,7 @@
   <section class="app-main">
     <transition name="fade" mode="out-in">
       <!-- <router-view :key="key"></router-view> -->
-      <keep-alive include="ExamManagement">
+      <keep-alive :include="cacheRouters">
         <router-view />
       </keep-alive>
     </transition>
@@ -12,6 +12,16 @@
 <script>
 export default {
   name: "AppMain",
+  data() {
+    return {
+      cacheRouters: [
+        "ExamManagement",
+        "InvigilationDetail",
+        "WarningManage",
+        "OnlinePatrol",
+      ],
+    };
+  },
 };
 </script>