|
@@ -0,0 +1,459 @@
|
|
|
|
+<template>
|
|
|
|
+ <div class="realtime-monitoring-full">
|
|
|
|
+ <div :style="viewStyle" class="monitor-view">
|
|
|
|
+ <RealtimeMonitoringFullHeader
|
|
|
|
+ class="monitor-view-header"
|
|
|
|
+ @back="onBack"
|
|
|
|
+ @filterChange="onFilterChange"
|
|
|
|
+ />
|
|
|
|
+ <div class="monitor-view-content">
|
|
|
|
+ <div class="monitor-view-content-left">
|
|
|
|
+ <EarlyWarningList @warnClick="toDetail" :list="warningList" />
|
|
|
|
+ <RequestCallList @backCall="onBackCall" :list="communicationList" />
|
|
|
|
+ </div>
|
|
|
|
+ <div class="monitor-view-content-right">
|
|
|
|
+ <div class="card-box monitor-view-content-right-header">
|
|
|
|
+ <SummaryLineFull data-type="trouble" :examId="filter.examId" />
|
|
|
|
+ <el-button
|
|
|
|
+ class="finish-invigilation"
|
|
|
|
+ type="danger"
|
|
|
|
+ icon="icon icon-over"
|
|
|
|
+ :disabled="!filter.examId"
|
|
|
|
+ @click="finishInvigilationExam"
|
|
|
|
+ >结束监考</el-button
|
|
|
|
+ >
|
|
|
|
+ </div>
|
|
|
|
+ <div class="monitor-view-content-video-table">
|
|
|
|
+ <div
|
|
|
|
+ class="invigilation-student-item"
|
|
|
|
+ v-for="item in dataList"
|
|
|
|
+ :key="item.examRecordId"
|
|
|
|
+ >
|
|
|
|
+ <invigilation-student-full
|
|
|
|
+ ref="InvigilationStudent"
|
|
|
|
+ :data="item"
|
|
|
|
+ :talking="talking"
|
|
|
|
+ :warning="isWarning(item)"
|
|
|
|
+ :beCalled="isBeCalled(item)"
|
|
|
|
+ @operation="onOperation"
|
|
|
|
+ @muted-change="videoAllMuted"
|
|
|
|
+ @click.native="toDetail(item)"
|
|
|
|
+ ></invigilation-student-full>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="monitor-view-content-video-pagination">
|
|
|
|
+ <el-pagination
|
|
|
|
+ background
|
|
|
|
+ layout="prev, pager, next"
|
|
|
|
+ :current-page="filter.pageNumber"
|
|
|
|
+ :total="total"
|
|
|
|
+ hide-on-single-page
|
|
|
|
+ :page-size="filter.pageSize"
|
|
|
|
+ @current-change="(pageNumber) => onFilterChange({ pageNumber })"
|
|
|
|
+ >
|
|
|
|
+ <template #prev>
|
|
|
|
+ <span>1</span>
|
|
|
|
+ </template>
|
|
|
|
+ </el-pagination>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ <!-- warning-text-message-dialog -->
|
|
|
|
+ <warning-text-message-dialog
|
|
|
|
+ :record-id="operationStudent['message'].examRecordId"
|
|
|
|
+ ref="WarningTextMessageDialog"
|
|
|
|
+ ></warning-text-message-dialog>
|
|
|
|
+ <!-- audio-record-dialog -->
|
|
|
|
+ <audio-record-dialog
|
|
|
|
+ :record-id="operationStudent['voice'].examRecordId"
|
|
|
|
+ ref="AudioRecordDialog"
|
|
|
|
+ ></audio-record-dialog>
|
|
|
|
+ <InvigilationStudentTalk
|
|
|
|
+ ref="talkDialog"
|
|
|
|
+ :record-id="operationStudent['talk'].examRecordId"
|
|
|
|
+ @talking="onTalking"
|
|
|
|
+ @muted-change="videoAllMuted"
|
|
|
|
+ />
|
|
|
|
+ </div>
|
|
|
|
+</template>
|
|
|
|
+
|
|
|
|
+<script>
|
|
|
|
+import {
|
|
|
|
+ invigilateVideoList,
|
|
|
|
+ invigilateExamFinish,
|
|
|
|
+ invigilationWarningMessage,
|
|
|
|
+ communicationList,
|
|
|
|
+} from "@/api/invigilation";
|
|
|
|
+
|
|
|
|
+import { mapState, mapMutations, mapActions } from "vuex";
|
|
|
|
+import RealtimeMonitoringFullHeader from "./components/RealtimeMonitoringFullHeader";
|
|
|
|
+import EarlyWarningList from "./components/EarlyWarningList";
|
|
|
|
+import RequestCallList from "./components/RequestCallList";
|
|
|
|
+import SummaryLineFull from "./components/SummaryLineFull";
|
|
|
|
+import InvigilationStudentFull from "./components/InvigilationStudentFull";
|
|
|
|
+import InvigilationStudentTalk from "./components/InvigilationStudentTalk";
|
|
|
|
+import WarningTextMessageDialog from "./WarningTextMessageDialog";
|
|
|
|
+import AudioRecordDialog from "./audioRecord/AudioRecordDialog";
|
|
|
|
+import { debounce } from "lodash-es";
|
|
|
|
+import IntervalMixin from "./mixins/IntervalMixin";
|
|
|
|
+const DESIGN_WIDTH = 1920;
|
|
|
|
+const DESIGN_HEIGHT = 1080;
|
|
|
|
+
|
|
|
|
+export default {
|
|
|
|
+ name: "RealtimeMonitoringFull",
|
|
|
|
+ mixins: [IntervalMixin],
|
|
|
|
+ components: {
|
|
|
|
+ RealtimeMonitoringFullHeader,
|
|
|
|
+ EarlyWarningList,
|
|
|
|
+ RequestCallList,
|
|
|
|
+ SummaryLineFull,
|
|
|
|
+ InvigilationStudentFull,
|
|
|
|
+ WarningTextMessageDialog,
|
|
|
|
+ AudioRecordDialog,
|
|
|
|
+ InvigilationStudentTalk,
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ data() {
|
|
|
|
+ return {
|
|
|
|
+ filter: {
|
|
|
|
+ examId: "",
|
|
|
|
+ roomCode: "",
|
|
|
|
+ monitorVideoSource: "",
|
|
|
|
+ pageSize: 24,
|
|
|
|
+ pageNumber: 1,
|
|
|
|
+ },
|
|
|
|
+ /** 通话待办 */
|
|
|
|
+ communication: 0,
|
|
|
|
+ /** 当前考试批次 */
|
|
|
|
+ curExamBatch: {},
|
|
|
|
+ /** 监考视频列表 */
|
|
|
|
+ dataList: [],
|
|
|
|
+ /** 监考视频总数 */
|
|
|
|
+ total: 0,
|
|
|
|
+ /** 当前操作的学生 */
|
|
|
|
+ operationStudent: {
|
|
|
|
+ message: {},
|
|
|
|
+ voice: {},
|
|
|
|
+ talk: {},
|
|
|
|
+ },
|
|
|
|
+ /** 通话中 */
|
|
|
|
+ talking: {
|
|
|
|
+ examRecordId: "",
|
|
|
|
+ isVideo: false,
|
|
|
|
+ },
|
|
|
|
+ /** 预警列表 */
|
|
|
|
+ warningList: [],
|
|
|
|
+ /** 通话列表 */
|
|
|
|
+ communicationList: [],
|
|
|
|
+ /** 屏幕适配缩放 */
|
|
|
|
+ viewStyle: {
|
|
|
|
+ "transform-origin": "center top",
|
|
|
|
+ transform: "scale(1)",
|
|
|
|
+ },
|
|
|
|
+ };
|
|
|
|
+ },
|
|
|
|
+ mounted() {
|
|
|
|
+ this.observerResize();
|
|
|
|
+ },
|
|
|
|
+ computed: {
|
|
|
|
+ ...mapState("invigilation", ["liveDomains"]),
|
|
|
|
+ isFullScreen() {
|
|
|
|
+ return this.$store.state.isFullScreen;
|
|
|
|
+ },
|
|
|
|
+ },
|
|
|
|
+ watch: {
|
|
|
|
+ isFullScreen: {
|
|
|
|
+ immediate: true,
|
|
|
|
+ handler(val, oldVal) {
|
|
|
|
+ if (val !== oldVal && !val) {
|
|
|
|
+ this.$router.replace({ name: "RealtimeMonitoring" });
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ },
|
|
|
|
+ filter: {
|
|
|
|
+ handler() {
|
|
|
|
+ this.refreshList();
|
|
|
|
+ },
|
|
|
|
+ deep: true,
|
|
|
|
+ },
|
|
|
|
+ },
|
|
|
|
+ methods: {
|
|
|
|
+ ...mapActions("invigilation", ["updateDetailIds"]),
|
|
|
|
+ ...mapMutations("invigilation", ["setDetailIds"]),
|
|
|
|
+ /** esc 返回 */
|
|
|
|
+ onBack() {
|
|
|
|
+ document.exitFullscreen?.();
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ /** filter change */
|
|
|
|
+ onFilterChange(filter) {
|
|
|
|
+ this.filter = Object.assign(this.filter, filter);
|
|
|
|
+ this.getList();
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ /** 查询视频列表 */
|
|
|
|
+ async getList() {
|
|
|
|
+ const res = await invigilateVideoList(this.filter);
|
|
|
|
+ const domainLen = this.liveDomains.length;
|
|
|
|
+ this.dataList = res.data.data.records.map((item, index) => {
|
|
|
|
+ const domain = domainLen ? this.liveDomains[index % domainLen] : "";
|
|
|
|
+ item.label = `${item.identity} ${item.courseName}(${item.courseCode}) ${item.name}`;
|
|
|
|
+ item.liveUrl = item.monitorLiveUrl
|
|
|
|
+ ? `${domain}/live/${item.monitorLiveUrl.toLowerCase()}.flv`
|
|
|
|
+ : "";
|
|
|
|
+ item.progress = item.progress ? Math.round(item.progress * 100) : 0;
|
|
|
|
+ return item;
|
|
|
|
+ });
|
|
|
|
+ this.total = res.data.data.total;
|
|
|
|
+ if (this.total > this.filter.pageSize) {
|
|
|
|
+ this.updateDetailIds({
|
|
|
|
+ filterData: this.filter,
|
|
|
|
+ fetchFunc: invigilateVideoList,
|
|
|
|
+ });
|
|
|
|
+ } else {
|
|
|
|
+ const ids = this.dataList.map((item) => item.examRecordId);
|
|
|
|
+ this.setDetailIds([...new Set(ids)]);
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ /** 结束监考 */
|
|
|
|
+ async finishInvigilationExam() {
|
|
|
|
+ const result = await this.$confirm(
|
|
|
|
+ "确定要结束监考吗?",
|
|
|
|
+ "结束监考确认提醒",
|
|
|
|
+ {
|
|
|
|
+ confirmButtonText: "确定",
|
|
|
|
+ cancelButtonText: "取消",
|
|
|
|
+ iconClass: "el-icon-warning",
|
|
|
|
+ customClass: "el-message-box__error",
|
|
|
|
+ }
|
|
|
|
+ ).catch(() => {});
|
|
|
|
+
|
|
|
|
+ if (!result) return;
|
|
|
|
+
|
|
|
|
+ await invigilateExamFinish(this.filter.examId);
|
|
|
|
+ this.onBack();
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ onOperation({ type, data }) {
|
|
|
|
+ if (type === "video") {
|
|
|
|
+ this.operationStudent["talk"] = data;
|
|
|
|
+ } else {
|
|
|
|
+ this.operationStudent[type] = data;
|
|
|
|
+ }
|
|
|
|
+ const operationMap = {
|
|
|
|
+ message: this.toSendTextMsg,
|
|
|
|
+ voice: this.toSendAudioMsg,
|
|
|
|
+ talk: () => this.toTalk(),
|
|
|
|
+ video: () => this.toTalk(true),
|
|
|
|
+ };
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
+ operationMap[type]();
|
|
|
|
+ });
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ /** 全部静音 */
|
|
|
|
+ videoAllMuted() {
|
|
|
|
+ this.$refs.InvigilationStudent.forEach((refInst) => {
|
|
|
|
+ refInst.mutedPlayer(true);
|
|
|
|
+ });
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ /** 跳转详情 */
|
|
|
|
+ toDetail(item) {
|
|
|
|
+ this.$router
|
|
|
|
+ .push({
|
|
|
|
+ name: "WarningDetail",
|
|
|
|
+ params: { examRecordId: item.examRecordId },
|
|
|
|
+ })
|
|
|
|
+ .then(() => {
|
|
|
|
+ document.exitFullscreen?.();
|
|
|
|
+ });
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ /** 发送文字消息 */
|
|
|
|
+ toSendTextMsg() {
|
|
|
|
+ this.$refs.WarningTextMessageDialog.open();
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ /** 发送语音消息 */
|
|
|
|
+ toSendAudioMsg() {
|
|
|
|
+ this.$refs.AudioRecordDialog.open();
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ /** 发起视频/语音通话 */
|
|
|
|
+ toTalk(isVideo = false) {
|
|
|
|
+ this.$refs.talkDialog.open(isVideo);
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ /** 回拨 */
|
|
|
|
+ onBackCall(data) {
|
|
|
|
+ this.onOperation({ type: "talk", data });
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ /** 通话中事件... */
|
|
|
|
+ onTalking({ examRecordId, talking }) {
|
|
|
|
+ this.talking.examRecordId = talking ? examRecordId : "";
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ /** 是否预警状态 */
|
|
|
|
+ isWarning(item) {
|
|
|
|
+ return !!this.warningList.find(
|
|
|
|
+ (warn) => warn.examRecordId === item.examRecordId
|
|
|
|
+ );
|
|
|
|
+ },
|
|
|
|
+ /** 是否申请通话中 */
|
|
|
|
+ isBeCalled(item) {
|
|
|
|
+ return !!this.communicationList.find(
|
|
|
|
+ (talk) =>
|
|
|
|
+ talk.callStatus !== "CANCEL" &&
|
|
|
|
+ talk.examRecordId === item.examRecordId
|
|
|
|
+ );
|
|
|
|
+ },
|
|
|
|
+ /** 获取通话代办列表 */
|
|
|
|
+ async getCommunicationList() {
|
|
|
|
+ if (!this.filter.examId) return;
|
|
|
|
+ const res = await communicationList({
|
|
|
|
+ examId: this.filter.examId,
|
|
|
|
+ roomCode: this.filter.roomCode,
|
|
|
|
+ callStatus: "START,CANCEL",
|
|
|
|
+ pageNumber: 1,
|
|
|
|
+ pageSize: 100,
|
|
|
|
+ });
|
|
|
|
+ this.communicationList = res.data.data.records;
|
|
|
|
+ },
|
|
|
|
+ /** 获取预警列表 */
|
|
|
|
+ async fetchWarningNotice() {
|
|
|
|
+ if (!this.filter.examId) return;
|
|
|
|
+ const res = await invigilationWarningMessage(this.filter.examId);
|
|
|
|
+ this.warningList = res.data.data;
|
|
|
|
+ },
|
|
|
|
+ /** 定时刷新预警/通话待办 */
|
|
|
|
+ refreshList() {
|
|
|
|
+ if (this.stopInterval) {
|
|
|
|
+ this.stopInterval();
|
|
|
|
+ }
|
|
|
|
+ Promise.all([this.getCommunicationList(), this.fetchWarningNotice()]);
|
|
|
|
+ this.stopInterval = this.interval(() => {
|
|
|
|
+ Promise.all([this.getCommunicationList(), this.fetchWarningNotice()]);
|
|
|
|
+ }, 10 * 1000);
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ observerResize() {
|
|
|
|
+ const resizeHandle = debounce(() => {
|
|
|
|
+ const scale = this.getScale();
|
|
|
|
+ this.viewStyle.transform = `scale(${scale})`;
|
|
|
|
+ }, 60);
|
|
|
|
+ resizeHandle();
|
|
|
|
+ window.addEventListener("resize", resizeHandle);
|
|
|
|
+ this.$once("hook:beforeDestroy", () => {
|
|
|
|
+ window.removeEventListener("resize", resizeHandle);
|
|
|
|
+ });
|
|
|
|
+ },
|
|
|
|
+ getScale() {
|
|
|
|
+ const wScale = window.innerWidth / DESIGN_WIDTH;
|
|
|
|
+ const hScale = window.innerHeight / DESIGN_HEIGHT;
|
|
|
|
+ return wScale < hScale ? wScale : hScale;
|
|
|
|
+ },
|
|
|
|
+ },
|
|
|
|
+};
|
|
|
|
+</script>
|
|
|
|
+
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
+.realtime-monitoring-full {
|
|
|
|
+ position: fixed;
|
|
|
|
+ left: 0;
|
|
|
|
+ top: 0;
|
|
|
|
+ width: 100vw;
|
|
|
|
+ height: 100vh;
|
|
|
|
+ background-color: #1a1d1f;
|
|
|
|
+ z-index: 2;
|
|
|
|
+ .card-box {
|
|
|
|
+ width: 280px;
|
|
|
|
+ background: #2e3238;
|
|
|
|
+ border-radius: 10px;
|
|
|
|
+ & ~ .card-box {
|
|
|
|
+ margin-top: 10px;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ .ellipsis {
|
|
|
|
+ overflow: hidden;
|
|
|
|
+ white-space: nowrap;
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
+ }
|
|
|
|
+ .monitor-view {
|
|
|
|
+ .monitor-view-header {
|
|
|
|
+ height: 64px;
|
|
|
|
+ background: #2e3238;
|
|
|
|
+ }
|
|
|
|
+ .monitor-view-content {
|
|
|
|
+ padding: 20px;
|
|
|
|
+ display: flex;
|
|
|
|
+ .monitor-view-content-left {
|
|
|
|
+ width: 280px;
|
|
|
|
+ margin-right: 20px;
|
|
|
|
+ }
|
|
|
|
+ .monitor-view-content-right {
|
|
|
|
+ flex: 1;
|
|
|
|
+ .monitor-view-content-right-header {
|
|
|
|
+ width: 100%;
|
|
|
|
+ padding: 10px;
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+ justify-content: space-between;
|
|
|
|
+ .finish-invigilation {
|
|
|
|
+ width: 136px;
|
|
|
|
+ height: 48px;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ .monitor-view-content-video-table {
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
+ margin-top: 20px;
|
|
|
|
+ .invigilation-student-item {
|
|
|
|
+ margin-right: 10px;
|
|
|
|
+ &:nth-child(6n) {
|
|
|
|
+ margin-right: 0;
|
|
|
|
+ }
|
|
|
|
+ &:nth-child(n + 7) {
|
|
|
|
+ margin-top: 10px;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ .monitor-view-content-video-pagination {
|
|
|
|
+ margin-top: 15px;
|
|
|
|
+ ::v-deep {
|
|
|
|
+ .el-pagination {
|
|
|
|
+ text-align: right;
|
|
|
|
+ .btn-prev,
|
|
|
|
+ .btn-next,
|
|
|
|
+ .number,
|
|
|
|
+ .more {
|
|
|
|
+ width: 32px;
|
|
|
|
+ height: 32px;
|
|
|
|
+ border: none;
|
|
|
|
+ background-color: #2e3238;
|
|
|
|
+ border-radius: 10px;
|
|
|
|
+ }
|
|
|
|
+ .btn-prev,
|
|
|
|
+ .btn-next {
|
|
|
|
+ &[disabled="disabled"] {
|
|
|
|
+ background-color: #6a6e74;
|
|
|
|
+ }
|
|
|
|
+ &:not([disabled="disabled"]):hover {
|
|
|
|
+ background-color: #1886fe;
|
|
|
|
+ color: #fff;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ .number.active {
|
|
|
|
+ background-color: #1886fe;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+</style>
|