123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712 |
- <template>
- <div class="warning-detail">
- <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
- @click="initSubscribeVideo"
- type="primary"
- size="mini"
- icon="el-icon-arrow-left"
- >开始视频</el-button
- >
- <el-button
- @click="closeSubscribeVideo"
- type="danger"
- size="mini"
- icon="el-icon-arrow-left"
- >关闭视频</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="student-views">
- <div class="student-avatar">
- <img
- :src="detailInfo.basePhotoPath"
- :alt="detailInfo.examStudentName"
- v-if="detailInfo.basePhotoPath"
- />
- <div class="avatar-default" v-else>
- <i class="el-icon-user-solid"></i>
- </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>
- <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>
- </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="发送文字提醒"
- ></el-button>
- <el-button
- class="el-icon-btn"
- type="primary"
- icon="icon icon-audio"
- @click="toSendAudioMsg"
- ></el-button>
- <el-popover
- class="warning-body-head-call"
- placement="bottom-start"
- trigger="click"
- >
- <el-button type="success" @click="answer(0)">语音通话</el-button>
- <el-button type="primary" @click="answer(1)">视频通话</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': !detailInfo.breachStatus }">
- {{ !detailInfo.breachStatus ? "违纪" : "正常" }}
- </b>
- </span>
- </p>
- <el-button
- :type="!detailInfo.breachStatus ? 'success' : 'danger'"
- icon="icon icon-stop"
- @click="toBreach"
- >{{ !detailInfo.breachStatus ? "撤销违纪" : "违纪处理" }}</el-button
- >
- <el-button type="warning" icon="icon icon-forbide" @click="toFinish"
- >强制收卷</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>
- </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" />
- </li>
- </ul>
- </div>
- </div>
- </div>
- </div>
- <!-- student-breach-dialog -->
- <student-breach-dialog
- :instance="curDetail"
- @modified="breachFinish"
- ref="StudentBreachDialog"
- ></student-breach-dialog>
- <!-- warning-text-message-dialog -->
- <warning-text-message-dialog
- :record-id="recordId"
- ref="WarningTextMessageDialog"
- ></warning-text-message-dialog>
- <!-- audio-record-dialog -->
- <audio-record-dialog
- :record-id="recordId"
- ref="AudioRecordDialog"
- ></audio-record-dialog>
- <!-- 通话弹出层 -->
- <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" v-show="!isWaiting">
- <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>
- <div class="communication-wait" v-show="isWaiting">
- <p class="communication-wait-tips">等待接听…</p>
- <div class="communication-wait-avatar">
- <img
- :src="detailInfo.basePhotoPath"
- :alt="detailInfo.examStudentName"
- />
- </div>
- <p class="communication-wait-username">
- {{ detailInfo.examStudentName }}
- </p>
- <div class="communication-wait-action">
- <el-button round type="danger" @click="hangup">取消通话</el-button>
- </div>
- </div>
- <span slot="footer" class="dialog-footer"> </span>
- </el-dialog>
- </div>
- </template>
- <script>
- import { createClient, createStream } from "@/plugins/trtc";
- import {
- invigilateDetail,
- invigilateFinish,
- warningStudentDetail,
- getUserMonitorKey,
- } from "@/api/invigilation";
- 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, objTypeOf } from "@/utils/utils";
- import { mapState } from "vuex";
- export default {
- name: "warning-detail",
- components: {
- FlvMedia,
- StudentBreachDialog,
- WarningTextMessageDialog,
- AudioRecordDialog,
- SecondTimer,
- },
- data() {
- return {
- recordId: this.$route.params.recordId,
- detailInfo: {},
- curDetail: {},
- serialIds: [],
- exceptionSummary: [],
- firstViewVideo: {
- liveUrl: "",
- },
- secondViewVideo: {
- liveUrl: "",
- },
- firstViewVideoReady: false,
- secondViewVideoReady: false,
- holding: false,
- // communication
- userMonitor: {},
- client: null,
- localStream: null,
- dialogVisible: false,
- isWaiting: true,
- subscribeSetT: null,
- loopRunning: false,
- loopSetTs: [],
- };
- },
- computed: {
- ...mapState("invigilation", ["detailIds", "liveDomains"]),
- },
- watch: {
- $route: {
- handler() {
- this.initData();
- },
- },
- },
- mounted() {
- this.initData();
- },
- methods: {
- async initData() {
- this.recordId = this.$route.params.recordId;
- await this.getInvigilateDetail().catch(() => {});
- await this.getStudentVideo().catch(() => {});
- this.holding = false;
- // 学生正在考试,开启定时更新
- if (this.detailInfo.statusCode === "ANSWERING") {
- this.loopRunning = true;
- this.clearLoopSetTs();
- this.loopSetTs.push(
- setTimeout(() => {
- this.timerUpdatePage();
- }, 10 * 1000)
- );
- } else {
- this.loopRunning = false;
- this.clearLoopSetTs();
- }
- },
- clearLoopSetTs() {
- if (!this.loopSetTs.length) return;
- this.loopSetTs.forEach((sett) => {
- clearTimeout(sett);
- });
- this.loopSetTs = [];
- },
- async timerUpdatePage() {
- this.clearLoopSetTs();
- if (!this.loopRunning) return;
- await this.getInvigilateDetail().catch(() => {});
- this.loopSetTs.push(
- setTimeout(() => {
- this.timerUpdatePage();
- }, 10 * 1000)
- );
- },
- async getStudentVideo() {
- const res = await warningStudentDetail({ recordId: this.recordId });
- const records = res.data.data.map((item, index) => {
- const domain = this.liveDomains[index] || "";
- item.liveUrl = item.liveUrl
- ? `${domain}/live/${item.liveUrl.toLowerCase()}.flv`
- : "";
- item.name = item.source;
- return item;
- });
- console.log(records.map((item) => item.liveUrl));
- this.firstViewVideo = records[0] || {};
- this.secondViewVideo = records[2] || {};
- if (records.length) this.initSubscribeVideo();
- },
- async getInvigilateDetail() {
- const res = await invigilateDetail(this.recordId);
- this.detailInfo = res.data.data;
- this.detailInfo.examStudentLogList = this.parseStudentLogs(
- this.detailInfo.examStudentLogList
- );
- this.exceptionSummary = this.detailInfo.examStudentLogList
- .filter((item) => item.viewType === "warning")
- .slice(0, 3);
- },
- parseStudentLogs(examStudentLogList) {
- const statusTypes = {
- common: [
- "FIRST_START",
- "RESUME_START",
- "IN_PROCESS",
- "PREPARE",
- "ANSWERING",
- "BREAK_OFF",
- "RESUME_PREPARE",
- "FINISHED",
- "FIRST_PREPARE",
- ],
- warning: [
- "FACE_COUNT_ERROR",
- "FACE_COMPARE_ERROR",
- "EYE_CLOSE_ERROR",
- "LIVENESS_ACTION_ERROR",
- "REALNESS",
- ],
- exception: [
- "NET_TIME_OUT",
- "MACHING_STOP",
- "NET_TIME_BREAK",
- "SOFTWARE_STOP",
- "POWER_CUT",
- ],
- };
- let statusTypeMap = {};
- Object.keys(statusTypes).map((k) => {
- statusTypes[k].map((item) => {
- statusTypeMap[item] = k;
- });
- });
- const logs = examStudentLogList.map((item) => {
- let info = { ...item };
- info.endTime = formatDate("HH:mm:ss", new Date(info.createTime));
- info.viewType = statusTypeMap[info.type] || "common";
- const content = info.info.split(/【|】/);
- if (content.length === 3) {
- info.title = content[1];
- info.desc = content[2];
- } else {
- info.title = content[0];
- }
- if (info.remark && info.remark.includes('{"')) {
- info.remark = JSON.parse(info.remark);
- if (info.remark["MIN_CREATE_TIME"]) {
- info.startTime = formatDate(
- "HH:mm:ss",
- new Date(info.remark["MIN_CREATE_TIME"])
- );
- info.durationTime = timeNumberToText(
- info.createTime - info.remark["MIN_CREATE_TIME"]
- );
- }
- let photos = [];
- Object.keys(info.remark).map((key) => {
- if (key.includes("PHOTO")) {
- const kPhotos =
- objTypeOf(info.remark[key]) === "array"
- ? info.remark[key]
- : [info.remark[key]];
- photos = [...photos, ...kPhotos];
- }
- });
- info.photos = photos;
- } else if (info.updateTime) {
- info.startTime = formatDate("HH:mm:ss", new Date(info.createTime));
- info.endTime = formatDate("HH:mm:ss", new Date(info.updateTime));
- info.durationTime = timeNumberToText(
- info.updateTime - info.createTime
- );
- }
- return info;
- });
- return logs;
- },
- changeStudent(type) {
- let index = this.detailIds.indexOf(this.recordId);
- if (type) {
- if (index >= this.detailIds.length - 1) {
- this.$message.error("当前没有下一个学生了");
- return;
- }
- index++;
- } else {
- if (index <= 0) {
- this.$message.error("当前没有上一个学生了");
- return;
- }
- index--;
- }
- if (this.holding) return;
- this.holding = true;
- this.closeSubscribeVideo();
- console.log(this.detailIds[index]);
- this.$router.replace({
- name: "WarningDetail",
- params: {
- recordId: this.detailIds[index],
- },
- });
- },
- toBreach() {
- this.curDetail = {
- examStudentName: this.detailInfo.examStudentName,
- identity: this.detailInfo.identity,
- courseNameCode: this.detailInfo.courseNameCode,
- description: "",
- examRecordId: [this.detailInfo.examRecordId],
- breachStatus: this.detailInfo.breachStatus,
- status: this.detailInfo.breachStatus ? 0 : 1,
- // 状态,0:新建,1:撤销
- // 违纪状态:正常(1)=>新建,违纪(0)=>撤销
- type: "",
- };
- this.$refs.StudentBreachDialog.open();
- },
- async toFinish() {
- const result = await this.$confirm(
- "试卷若被强制回收,考试再无法重置继续完成考试,请您慎重选择!您确定要强制回收改考试试卷吗?",
- "强制收卷确认提醒",
- {
- confirmButtonText: "确定",
- cancelButtonText: "取消",
- iconClass: "el-icon-warning",
- customClass: "el-message-box__error",
- }
- ).catch(() => {});
- if (!result) return;
- await invigilateFinish({
- examRecordId: [this.detailInfo.examRecordId],
- type: "INTERRUPT",
- });
- this.$message.success("强制收卷成功!");
- this.goBack();
- },
- toSendTextMsg() {
- this.$refs.WarningTextMessageDialog.open();
- },
- toSendAudioMsg() {
- this.$refs.AudioRecordDialog.open();
- },
- breachFinish() {
- this.getInvigilateDetail();
- },
- // video relative
- initSubscribeVideo() {
- if (this.firstViewVideo.liveUrl) this.firstViewVideoReady = true;
- if (this.secondViewVideo.liveUrl) this.secondViewVideoReady = true;
- },
- closeSubscribeVideo() {
- this.firstViewVideoReady = false;
- this.secondViewVideoReady = false;
- },
- async initClient(examRecordId) {
- const res = await getUserMonitorKey(examRecordId);
- this.userMonitor = res.data.data;
- this.client = createClient({
- mode: "live",
- sdkAppId: this.userMonitor.appId,
- userId: this.userMonitor.monitorUserId,
- userSig: this.userMonitor.monitorUserSig,
- });
- },
- async answer(isVideo) {
- this.closeSubscribeVideo();
- await this.initClient(this.recordId);
- this.dialogVisible = true;
- // 添加远程用户视频发布监听
- this.client.on("stream-added", (event) => {
- console.log(event);
- const remoteStream = event.stream;
- // 延迟订阅视频
- this.subscribeSetT = setTimeout(() => {
- this.client
- .subscribe(remoteStream, { audio: true, video: true })
- .then(() => {
- console.log("订阅视频成功!");
- })
- .catch((error) => {
- console.log("订阅视频失败!", error);
- });
- }, 5000);
- });
- this.client.on("stream-subscribed", (event) => {
- const remoteStream = event.stream;
- this.isWaiting = false;
- this.$nextTick(() => {
- this.$refs.SecondTimer.start();
- remoteStream.play("communication-host", { objectFit: "contain" });
- });
- });
- // 加入房间
- let roomJoinResult = true;
- await this.client
- .join({
- roomId: this.userMonitor.monitorKey,
- role: "audience",
- })
- .catch((error) => {
- roomJoinResult = false;
- console.log("加入房间失败!", error);
- });
- if (!roomJoinResult) return;
- // 切换角色,连麦互动
- let switchResult = true;
- await this.client.switchRole("anchor").catch((error) => {
- console.log("切换角色失败!", error);
- switchResult = false;
- });
- if (!switchResult) return;
- // 初始化stream
- this.localStream = createStream({
- userId: this.userMonitor.monitorUserId,
- audio: true,
- video: isVideo,
- });
- let initLocalStreamResult = true;
- await this.localStream.initialize().catch((error) => {
- console.log("初始化视频失败!", error);
- initLocalStreamResult = false;
- });
- if (!initLocalStreamResult) return;
- // 发布本地视频
- let publishStreamResult = true;
- this.client.publish(this.localStream).catch((error) => {
- console.log("发布本地视频失败!", error);
- publishStreamResult = false;
- });
- if (!publishStreamResult) return;
- // 播放本地视频
- this.localStream.play("communication-guest", { muted: true });
- },
- async hangup() {
- if (this.subscribeSetT) clearTimeout(this.subscribeSetT);
- this.$refs.SecondTimer.end();
- // 取消发布本地视频
- let unpublishStreamResult = true;
- this.client.unpublish(this.localStream).catch((error) => {
- console.log("取消发布本地视频失败!", error);
- unpublishStreamResult = false;
- });
- if (!unpublishStreamResult) return;
- this.localStream.close();
- this.localStream = null;
- // 离开房间
- let result = true;
- await this.client.leave().catch((error) => {
- console.log("离开房间失败!", error);
- result = false;
- });
- if (!result) return;
- this.client.off("*");
- this.client = null;
- this.userMonitor = {};
- this.dialogVisible = false;
- this.isWaiting = true;
- this.initSubscribeVideo();
- },
- goBack() {
- window.history.go(-1);
- },
- },
- beforeDestroy() {
- this.loopRunning = false;
- this.clearLoopSetTs();
- if (this.subscribeSetT) clearTimeout(this.subscribeSetT);
- if (this.client) {
- this.client.leave();
- this.client.off("*");
- }
- },
- };
- </script>
|