RealtimeMonitoring.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880
  1. <template>
  2. <div class="realtime-monitoring">
  3. <div class="part-box-head">
  4. <div class="part-box-head-left">
  5. <h1>实时监控台</h1>
  6. </div>
  7. <div class="part-box-head-right">
  8. <div
  9. :class="[
  10. 'realtime-switch',
  11. { 'realtime-switch-warning': hasNewWarning },
  12. ]"
  13. >
  14. <el-button class="toggle-full-button" @click="toFullScreen">
  15. 全屏监控
  16. </el-button>
  17. <div
  18. :class="[
  19. 'realtime-switch-item',
  20. { 'realtime-switch-item-act': pageType === '0' },
  21. ]"
  22. @click="pageTypeChange('0')"
  23. >
  24. <i class="el-icon-s-fold"></i><span>列表</span>
  25. </div>
  26. <div
  27. :class="[
  28. 'realtime-switch-item',
  29. { 'realtime-switch-item-act': pageType === '1' },
  30. ]"
  31. @click="pageTypeChange('1')"
  32. >
  33. <i class="el-icon-video-camera"></i><span>视频</span>
  34. </div>
  35. </div>
  36. </div>
  37. </div>
  38. <div class="part-filter part-filter-realtime">
  39. <div class="part-filter-form">
  40. <el-form inline>
  41. <el-form-item>
  42. <div @click="$refs.ExamBatchDialog.open()">
  43. <el-input
  44. v-model="curExamBatch.label"
  45. class="realtime-top-select"
  46. placeholder="请选择批次"
  47. suffix-icon="el-icon-caret-bottom"
  48. readonly
  49. ></el-input>
  50. </div>
  51. </el-form-item>
  52. <el-form-item>
  53. <el-select
  54. v-model="filter.examActivityId"
  55. placeholder="场次"
  56. clearable
  57. >
  58. <el-option
  59. v-for="item in examActivities"
  60. :key="item.id"
  61. :value="item.id"
  62. :label="item.code"
  63. ></el-option>
  64. </el-select>
  65. </el-form-item>
  66. <el-form-item>
  67. <el-select
  68. v-model="filter.roomCode"
  69. placeholder="考场"
  70. @change="examRoomChange"
  71. >
  72. <el-option
  73. v-for="item in examRooms"
  74. :key="item.roomCode"
  75. :label="item.roomName"
  76. :value="item.roomCode"
  77. >
  78. <span>{{ item.roomName }}</span>
  79. </el-option>
  80. </el-select>
  81. </el-form-item>
  82. </el-form>
  83. <div class="part-filter-form-action">
  84. <text-clock></text-clock>
  85. </div>
  86. </div>
  87. </div>
  88. <div class="part-filter">
  89. <div class="part-filter-info">
  90. <summary-line
  91. class="part-filter-info-main"
  92. data-type="trouble"
  93. :exam-id="filter.examId"
  94. :exam-activity-id="filter.examActivityId"
  95. ref="SummaryLine"
  96. ></summary-line>
  97. <div class="part-filter-info-sub">
  98. <el-dropdown
  99. @command="viewingAngleChange"
  100. trigger="click"
  101. v-if="pageType === '1'"
  102. >
  103. <el-button type="primary"
  104. >切换视频源<i class="el-icon-arrow-down el-icon--right"></i
  105. ></el-button>
  106. <el-dropdown-menu slot="dropdown">
  107. <el-dropdown-item
  108. v-for="item in viewingAngles"
  109. :key="item.code"
  110. :command="item"
  111. >
  112. <span
  113. :class="{
  114. 'color-primary': item.code === curViewingAngle.code,
  115. }"
  116. >
  117. {{ item.name }}
  118. </span>
  119. </el-dropdown-item>
  120. </el-dropdown-menu>
  121. </el-dropdown>
  122. <el-badge
  123. :value="communicationCount"
  124. :max="99"
  125. :hidden="!communicationCount"
  126. >
  127. <el-button
  128. icon="icon icon-ring"
  129. type="success"
  130. @click="toCommunication"
  131. >通话待办</el-button
  132. >
  133. </el-badge>
  134. <!-- <el-button
  135. type="primary"
  136. icon="icon icon-handle"
  137. @click="finishInvigilation"
  138. >手动收卷</el-button
  139. > -->
  140. </div>
  141. </div>
  142. <div class="part-filter-form">
  143. <el-form ref="FilterForm" label-position="left" inline>
  144. <el-form-item>
  145. <el-select
  146. v-model="filter.paperDownload"
  147. placeholder="试题下载"
  148. clearable
  149. >
  150. <el-option
  151. v-for="(val, key) in BOOLEAN_INVERSE_TYPE"
  152. :key="key"
  153. :value="key * 1"
  154. :label="val"
  155. ></el-option>
  156. </el-select>
  157. </el-form-item>
  158. <el-form-item>
  159. <el-select v-model="filter.status" placeholder="考试状态" clearable>
  160. <el-option
  161. v-for="(val, key) in STUDENT_ONLINE_STATUS"
  162. :key="key"
  163. :value="key"
  164. :label="val"
  165. ></el-option>
  166. </el-select>
  167. </el-form-item>
  168. <el-form-item v-for="source in viewingAngles" :key="source.code">
  169. <el-select
  170. v-model="monitorStatusFilter[source.filterParam]"
  171. :placeholder="`${source.name}通讯故障`"
  172. clearable
  173. >
  174. <el-option
  175. v-for="(val, key) in MONITOR_STATUS_TYPE"
  176. :key="key"
  177. :value="key"
  178. :label="val"
  179. ></el-option>
  180. </el-select>
  181. </el-form-item>
  182. <el-form-item label="预警量">
  183. <el-input-number
  184. style="width: 52px"
  185. v-model.trim="filter.minWarningCount"
  186. placeholder="下限"
  187. :controls="false"
  188. ></el-input-number>
  189. <span class="line-split">-</span>
  190. <el-input-number
  191. style="width: 52px"
  192. v-model.trim="filter.maxWarningCount"
  193. placeholder="上限"
  194. :controls="false"
  195. ></el-input-number>
  196. </el-form-item>
  197. <el-form-item>
  198. <el-input
  199. v-model.trim="filter.name"
  200. placeholder="姓名"
  201. clearable
  202. ></el-input>
  203. </el-form-item>
  204. <el-form-item>
  205. <el-input
  206. v-model.trim="filter.identity"
  207. placeholder="证件号"
  208. clearable
  209. ></el-input>
  210. </el-form-item>
  211. <el-form-item>
  212. <el-button type="primary" @click="toSearch">查询</el-button>
  213. </el-form-item>
  214. </el-form>
  215. <div class="part-filter-form-action"></div>
  216. </div>
  217. </div>
  218. <el-table
  219. ref="TableList"
  220. :data="dataList"
  221. @selection-change="handleSelectionChange"
  222. v-if="pageType === '0'"
  223. >
  224. <el-table-column type="selection" width="55" align="center">
  225. </el-table-column>
  226. <el-table-column prop="identity" label="证件号"></el-table-column>
  227. <el-table-column prop="name" label="姓名"></el-table-column>
  228. <el-table-column prop="courseName" label="科目名称"></el-table-column>
  229. <el-table-column prop="courseCode" label="科目代码"></el-table-column>
  230. <el-table-column prop="remainTime" label="剩余时间"></el-table-column>
  231. <el-table-column prop="paperDownload" label="试题下载">
  232. <span slot-scope="scope">
  233. {{ BOOLEAN_INVERSE_TYPE[scope.row.paperDownload] }}
  234. </span>
  235. </el-table-column>
  236. <el-table-column prop="status" label="考试状态"></el-table-column>
  237. <el-table-column prop="progress" label="进度">
  238. <span slot-scope="scope">{{ scope.row.progress }}%</span>
  239. </el-table-column>
  240. <el-table-column prop="clientWebsocketStatus" label="通讯">
  241. <template slot-scope="scope">
  242. <right-or-wrong
  243. :status="CLIENT_WEBSOCKET_STATUS[scope.row.clientWebsocketStatus]"
  244. ></right-or-wrong>
  245. </template>
  246. </el-table-column>
  247. <el-table-column
  248. v-for="source in viewingAngles"
  249. :key="source.param"
  250. :prop="source.param"
  251. :label="`${source.name}通讯`"
  252. >
  253. <template slot-scope="scope">
  254. <right-or-wrong
  255. :status="MONITOR_STATUS_SOURCE[scope.row[source.param]]"
  256. ></right-or-wrong>
  257. </template>
  258. </el-table-column>
  259. <el-table-column
  260. prop="clientCurrentIp"
  261. label="IP"
  262. v-if="curExamBatch.enableIpLimit"
  263. >
  264. </el-table-column>
  265. <el-table-column prop="updateTime" label="更新时间">
  266. <span slot-scope="scope">
  267. {{ scope.row.updateTime | datetimeFilter }}
  268. </span>
  269. </el-table-column>
  270. <el-table-column prop="warningCount" label="预警数"></el-table-column>
  271. <el-table-column prop="breachStatus" label="违纪">
  272. <template slot-scope="scope">
  273. <span :class="{ 'color-danger': !scope.row.breachStatus }">
  274. {{ !scope.row.breachStatus ? "违纪" : "正常" }}
  275. </span>
  276. </template>
  277. </el-table-column>
  278. <el-table-column label="操作" width="125" fixed="right">
  279. <template slot-scope="scope">
  280. <el-button
  281. :class="[
  282. 'btn-table-icon',
  283. { 'warn-new-tips': scope.row.warningNew },
  284. ]"
  285. type="primary"
  286. icon="icon icon-view"
  287. @click="toDetail(scope.row)"
  288. >详情</el-button
  289. >
  290. </template>
  291. </el-table-column>
  292. </el-table>
  293. <div class="invigilation-student-list" v-else>
  294. <div
  295. class="invigilation-student-item"
  296. v-for="item in dataList"
  297. :key="item.examRecordId"
  298. >
  299. <invigilation-student
  300. ref="InvigilationStudent"
  301. :data="item"
  302. @muted-change="videoAllMuted"
  303. @video-view-change="videoViewChange"
  304. ></invigilation-student>
  305. </div>
  306. </div>
  307. <div class="part-page">
  308. <el-pagination
  309. background
  310. hide-on-single-page
  311. layout="prev, pager, next,total,jumper"
  312. :current-page="current"
  313. :total="total"
  314. :page-size="size"
  315. @current-change="toPage"
  316. >
  317. </el-pagination>
  318. </div>
  319. <!-- 考试批次选择 -->
  320. <exam-batch-dialog
  321. :initExamid="initExamid"
  322. @confirm="examChange"
  323. ref="ExamBatchDialog"
  324. ></exam-batch-dialog>
  325. <!-- 手动收卷 -->
  326. <handle-rollup-dialog
  327. :data-list="multipleSelection"
  328. @modified="rollupOver"
  329. ref="handleRollupDialog"
  330. ></handle-rollup-dialog>
  331. </div>
  332. </template>
  333. <script>
  334. import {
  335. invigilateVideoList,
  336. examActivityRoomList,
  337. monitorCallCount,
  338. invigilationWarningMessage,
  339. } from "@/api/invigilation";
  340. import ExamBatchDialog from "./ExamBatchDialog";
  341. import RightOrWrong from "../common/RightOrWrong";
  342. import InvigilationStudent from "../common/InvigilationStudent";
  343. import SummaryLine from "../common/SummaryLine";
  344. import handleRollupDialog from "./handleRollupDialog";
  345. import TextClock from "../common/TextClock";
  346. import {
  347. VIDEO_SOURCE_TYPE,
  348. BOOLEAN_INVERSE_TYPE,
  349. STUDENT_ONLINE_STATUS,
  350. CLIENT_WEBSOCKET_STATUS,
  351. MONITOR_STATUS_SOURCE,
  352. MONITOR_STATUS_TYPE,
  353. } from "@/constant/constants";
  354. import { mapState, mapMutations, mapActions } from "vuex";
  355. import { Notification } from "element-ui";
  356. export default {
  357. name: "RealtimeMonitoring",
  358. components: {
  359. ExamBatchDialog,
  360. RightOrWrong,
  361. InvigilationStudent,
  362. SummaryLine,
  363. handleRollupDialog,
  364. TextClock,
  365. },
  366. data() {
  367. return {
  368. initExamid: this.$route.params.examId,
  369. filter: {
  370. examId: "",
  371. examActivityId: "",
  372. roomCode: "",
  373. paperDownload: null,
  374. status: null,
  375. monitorStatusSource: null,
  376. monitorVideoSource: null,
  377. name: null,
  378. identity: null,
  379. maxWarningCount: undefined,
  380. minWarningCount: undefined,
  381. },
  382. userId: this.$store.state.user.id,
  383. monitorStatusFilter: {},
  384. VIDEO_SOURCE_TYPE,
  385. BOOLEAN_INVERSE_TYPE,
  386. STUDENT_ONLINE_STATUS,
  387. CLIENT_WEBSOCKET_STATUS,
  388. MONITOR_STATUS_SOURCE,
  389. MONITOR_STATUS_TYPE,
  390. examBatchs: [],
  391. hasNewWarning: false,
  392. loopRunning: false,
  393. loopSetTs: [],
  394. noticeLoopSetTs: [],
  395. communicationCount: 0,
  396. curExamBatch: {},
  397. curViewingAngle: {},
  398. current: 1,
  399. total: 0,
  400. size: 24,
  401. multipleSelection: [],
  402. batchId: "",
  403. batchs: [],
  404. exams: [],
  405. subjects: [],
  406. pageType: "0",
  407. cachePaperType: "",
  408. dataList: [],
  409. examRooms: [],
  410. examActivities: [],
  411. videoSourceStatusParams: {
  412. CLIENT_CAMERA: "cameraMonitorStatusSource",
  413. CLIENT_SCREEN: "screenMonitorStatusSource",
  414. MOBILE_FIRST: "mobileFirstMonitorStatusSource",
  415. MOBILE_SECOND: "mobileSecondMonitorStatusSource",
  416. },
  417. viewingAngles: [],
  418. videoIsLargeView: false,
  419. };
  420. },
  421. created() {
  422. window.inviligateWarning = (id) => {
  423. this.toDetail({ examRecordId: id });
  424. };
  425. },
  426. computed: {
  427. ...mapState("invigilation", ["liveDomains"]),
  428. isFullScreen() {
  429. return this.$store.state.isFullScreen;
  430. },
  431. },
  432. watch: {
  433. isFullScreen: {
  434. immediate: true,
  435. handler(val, oldVal) {
  436. if (val !== oldVal && val) {
  437. this.$router.replace({ name: "RealtimeMonitoringFull" });
  438. }
  439. },
  440. },
  441. },
  442. methods: {
  443. ...mapActions("invigilation", ["updateDetailIds"]),
  444. ...mapMutations("invigilation", ["setDetailIds"]),
  445. clearLoopSetTs() {
  446. if (!this.loopSetTs.length) return;
  447. this.loopSetTs.forEach((sett) => {
  448. clearTimeout(sett);
  449. });
  450. this.loopSetTs = [];
  451. },
  452. async timerUpdatePage() {
  453. this.clearLoopSetTs();
  454. if (!this.loopRunning || !this.filter.examId) return;
  455. let fetchAll = [this.getList()];
  456. if (this.$refs.SummaryLine)
  457. fetchAll.push(this.$refs.SummaryLine.initData());
  458. fetchAll.push(this.getMonitorCallCount());
  459. fetchAll.push(this.fetchWarningNotice());
  460. await Promise.all(fetchAll).catch(() => {});
  461. this.loopSetTs.push(
  462. setTimeout(() => {
  463. this.timerUpdatePage();
  464. }, 10 * 1000)
  465. );
  466. },
  467. examChange(examBatch) {
  468. if (!examBatch) return;
  469. this.filter.examId = examBatch.id;
  470. this.curExamBatch = examBatch;
  471. const monitorStatusFilter = {};
  472. if (examBatch.monitorVideoSource) {
  473. this.viewingAngles = examBatch.monitorVideoSource
  474. .split(",")
  475. .map((item) => {
  476. const filterParam = this.videoSourceStatusParams[item].replace(
  477. "Source",
  478. ""
  479. );
  480. monitorStatusFilter[filterParam] = null;
  481. return {
  482. code: item,
  483. name: this.VIDEO_SOURCE_TYPE[item],
  484. param: this.videoSourceStatusParams[item],
  485. filterParam,
  486. };
  487. });
  488. this.monitorStatusFilter = monitorStatusFilter;
  489. } else {
  490. this.viewingAngles = [];
  491. }
  492. this.curViewingAngle = this.viewingAngles[0] || {};
  493. this.filter.monitorVideoSource = this.curViewingAngle.code || "";
  494. this.filter.roomCode = "";
  495. this.getExamRooms();
  496. },
  497. async getExamRooms() {
  498. this.examRooms = [];
  499. if (!this.curExamBatch.id) return;
  500. const res = await examActivityRoomList(this.curExamBatch.id);
  501. this.examRooms = res.data.data.examRooms || [];
  502. this.filter.roomCode = this.examRooms[0] && this.examRooms[0].roomCode;
  503. this.examActivities = this.getExamActivities(res.data.data.examActivitys);
  504. this.filter.examActivityId =
  505. this.examActivities[0] && this.examActivities[0].id;
  506. this.examRoomChange();
  507. },
  508. getExamActivities(examActivitys) {
  509. if (!examActivitys.length) return [];
  510. const now = Date.now();
  511. examActivitys.forEach((item) => {
  512. item.endRemainTime = item.finishTime - now;
  513. });
  514. examActivitys.sort((a, b) => {
  515. if (a.endRemainTime < 0) return 1;
  516. if (b.endRemainTime < 0) return -1;
  517. return a.endRemainTime - b.endRemainTime;
  518. });
  519. return examActivitys;
  520. },
  521. examRoomChange() {
  522. this.toSearch();
  523. this.getMonitorCallCount();
  524. this.fetchWarningNotice();
  525. // 正在考试的考试,开启定时更新;
  526. if (this.curExamBatch.isExaming) {
  527. this.loopRunning = true;
  528. this.clearLoopSetTs();
  529. this.loopSetTs.push(
  530. setTimeout(() => {
  531. this.timerUpdatePage();
  532. }, 10 * 1000)
  533. );
  534. } else {
  535. this.loopRunning = false;
  536. this.clearLoopSetTs();
  537. }
  538. },
  539. pageTypeChange(pageType) {
  540. this.pageType = pageType;
  541. this.multipleSelection = [];
  542. },
  543. viewingAngleChange(data) {
  544. if (data.code === this.curViewingAngle.code) return;
  545. this.curViewingAngle = data;
  546. this.filter.monitorVideoSource = data.code;
  547. this.dataList = [];
  548. this.getList();
  549. },
  550. async getList() {
  551. if (this.pageType === "1" && this.videoIsLargeView) return;
  552. const datas = {
  553. ...this.filter,
  554. ...this.monitorStatusFilter,
  555. pageNumber: this.current,
  556. pageSize: this.size,
  557. };
  558. const res = await invigilateVideoList(datas);
  559. const domainLen = this.liveDomains.length;
  560. this.dataList = res.data.data.records.map((item, index) => {
  561. const domain = domainLen ? this.liveDomains[index % domainLen] : "";
  562. item.label = `${item.identity} ${item.courseName}(${item.courseCode}) ${item.name}`;
  563. item.liveUrl = item.monitorLiveUrl
  564. ? `${domain}/live/${item.monitorLiveUrl.toLowerCase()}.flv`
  565. : "";
  566. item.progress = item.progress ? Math.round(item.progress * 100) : 0;
  567. return item;
  568. });
  569. this.hasNewWarning = this.dataList.some((item) => item.warningNew);
  570. this.total = res.data.data.total;
  571. },
  572. toPage(page) {
  573. this.current = page;
  574. this.getList();
  575. },
  576. async toSearch() {
  577. this.current = 1;
  578. await this.getList();
  579. if (this.total > this.size) {
  580. this.updateDetailIds({
  581. filterData: this.filter,
  582. fetchFunc: invigilateVideoList,
  583. });
  584. } else {
  585. const ids = this.dataList.map((item) => item.examRecordId);
  586. this.setDetailIds([...new Set(ids)]);
  587. }
  588. },
  589. async getMonitorCallCount() {
  590. if (!this.filter.examId) return;
  591. const res = await monitorCallCount({
  592. examId: this.filter.examId,
  593. roomCode: this.filter.roomCode,
  594. callStatus: "START,CANCEL",
  595. });
  596. this.communicationCount = res.data.data.count || 0;
  597. },
  598. async fetchWarningNotice() {
  599. if (!this.filter.examId) return;
  600. this.cleartNoticeLoopSetTs();
  601. const showAlert = async (item) => {
  602. return new Promise((resolve) => {
  603. let st = setTimeout(() => {
  604. let notifyIns = this.$notify({
  605. duration: 5 * 1000,
  606. dangerouslyUseHTMLString: true,
  607. customClass: "msg-monitor-magbox",
  608. position: "bottom-right",
  609. offset: 50,
  610. message: `
  611. <div class="msg-monitor">
  612. <span class="msg-monitor-icon"><i class="icon icon-warning"></i></span>
  613. <span>注意:<b>${item.name}</b>发现异常,</span>
  614. <span class="msg-monitor-action" onclick="window.inviligateWarning('${item.examRecordId}')">立即处理</span>
  615. </div>
  616. `,
  617. });
  618. resolve(notifyIns);
  619. }, 500);
  620. this.noticeLoopSetTs.push(st);
  621. });
  622. };
  623. Notification.closeAll();
  624. let noticeCaches = {},
  625. noticeList = [];
  626. const maxNoticeCount = 3;
  627. const res = await invigilationWarningMessage(this.filter.examId);
  628. const dataList = res.data.data.slice(-16);
  629. for (let i = 0, len = dataList.length; i < len; i++) {
  630. const item = dataList[i];
  631. const stdKey = item.examRecordId;
  632. if (!noticeCaches[stdKey]) {
  633. if (noticeList.length > maxNoticeCount) {
  634. const prevStdKey = noticeList.shift();
  635. noticeCaches[prevStdKey].close();
  636. }
  637. noticeCaches[stdKey] = await showAlert(item);
  638. noticeList.push(stdKey);
  639. }
  640. }
  641. if (noticeList.length > maxNoticeCount) {
  642. const prevStdKey = noticeList.shift();
  643. noticeCaches[prevStdKey].close();
  644. }
  645. },
  646. cleartNoticeLoopSetTs() {
  647. this.noticeLoopSetTs.forEach((t) => clearTimeout(t));
  648. this.noticeLoopSetTs = [];
  649. },
  650. handleSelectionChange(val) {
  651. console.log(val);
  652. this.multipleSelection = val;
  653. },
  654. async finishInvigilation() {
  655. if (!this.multipleSelection.length) {
  656. this.$message.error("请先选择数据!");
  657. return;
  658. }
  659. this.$refs.handleRollupDialog.open();
  660. },
  661. rollupOver() {
  662. this.multipleSelection = [];
  663. this.getList();
  664. },
  665. toCommunication() {
  666. this.$router.push({
  667. name: "VideoCommunication",
  668. params: {
  669. examId: this.filter.examId,
  670. roomCode: this.filter.roomCode,
  671. },
  672. });
  673. },
  674. toDetail(row) {
  675. this.$router.push({
  676. name: "WarningDetail",
  677. params: { examRecordId: row.examRecordId },
  678. });
  679. },
  680. videoAllMuted() {
  681. this.$refs.InvigilationStudent.forEach((refInst) => {
  682. refInst.mutedPlayer(true);
  683. });
  684. },
  685. videoViewChange(isLarge) {
  686. this.videoIsLargeView = isLarge;
  687. },
  688. async toFullScreen() {
  689. const fullscreenEnabled =
  690. document.fullscreenEnabled ||
  691. document.mozFullScreenEnabled ||
  692. document.webkitFullscreenEnabled ||
  693. document.msFullscreenEnabled;
  694. if (!fullscreenEnabled) {
  695. this.$message.error("当前浏览器不支持全屏!");
  696. return;
  697. }
  698. const de = document.documentElement;
  699. const requestFullscreen =
  700. de.requestFullscreen ||
  701. de.mozRequestFullScreen ||
  702. de.webkitRequestFullscreen;
  703. const exitFullscreen =
  704. document.exitFullscreen ||
  705. document.mozCancelFullScreen ||
  706. document.webkitCancelFullScreen;
  707. if (this.isFullscreen) {
  708. await exitFullscreen.call(document).catch(() => {});
  709. } else {
  710. await requestFullscreen.call(de).catch(() => {});
  711. }
  712. },
  713. },
  714. beforeDestroy() {
  715. delete window.inviligateWarning;
  716. },
  717. beforeRouteEnter(to, from, next) {
  718. next((vm) => {
  719. if (["WarningDetail", "VideoCommunication"].includes(from.name)) {
  720. vm.pageType = vm.cachePaperType || "0";
  721. if (vm.curExamBatch.isExaming) {
  722. vm.loopRunning = true;
  723. vm.timerUpdatePage();
  724. }
  725. }
  726. });
  727. },
  728. beforeRouteLeave(to, from, next) {
  729. this.cachePaperType = this.pageType;
  730. this.pageType = "0";
  731. this.loopRunning = false;
  732. this.clearLoopSetTs();
  733. this.cleartNoticeLoopSetTs();
  734. Notification.closeAll();
  735. next();
  736. },
  737. };
  738. </script>
  739. <style lang="scss" scoped>
  740. .text-clock {
  741. color: #1886fe;
  742. font-weight: 600;
  743. }
  744. .realtime-switch {
  745. font-size: 0;
  746. .toggle-full-button {
  747. margin-right: 20px;
  748. height: 28px;
  749. }
  750. &-warning {
  751. .realtime-switch-item {
  752. &::before {
  753. content: "";
  754. display: block;
  755. position: absolute;
  756. width: 10px;
  757. height: 10px;
  758. top: -5px;
  759. right: -5px;
  760. border-radius: 50%;
  761. border: 2px solid #fff;
  762. background: #fe5863;
  763. z-index: 9;
  764. }
  765. }
  766. }
  767. &-item {
  768. display: inline-block;
  769. vertical-align: top;
  770. font-size: 12px;
  771. color: #8c94ac;
  772. background: #fff;
  773. line-height: 18px;
  774. padding: 5px 14px;
  775. position: relative;
  776. cursor: pointer;
  777. > i {
  778. margin-right: 5px;
  779. }
  780. &:first-child {
  781. border-radius: 6px 0px 0px 6px;
  782. }
  783. &:last-child {
  784. border-radius: 0px 6px 6px 0px;
  785. }
  786. &-act {
  787. color: #fff;
  788. background: #5fc9fa;
  789. }
  790. }
  791. }
  792. .invigilation-student-list {
  793. border-radius: 6px;
  794. font-size: 0;
  795. min-height: 200px;
  796. margin: -10px;
  797. .invigilation-student-item {
  798. display: inline-block;
  799. vertical-align: top;
  800. padding: 10px;
  801. width: 25%;
  802. font-size: 0;
  803. }
  804. .invigilation-student {
  805. padding: 20px;
  806. border: none;
  807. margin: 0;
  808. background: #fff;
  809. font-size: 14px;
  810. }
  811. }
  812. .warn-new-tips {
  813. position: relative;
  814. &::after {
  815. content: "";
  816. display: block;
  817. position: absolute;
  818. width: 32px;
  819. height: 16px;
  820. right: -32px;
  821. top: 0;
  822. background-image: url(../../../assets/icon-new-tips.png);
  823. background-size: 100% 100%;
  824. }
  825. }
  826. .part-filter-info-sub {
  827. .el-badge {
  828. margin: 0 10px;
  829. vertical-align: top;
  830. }
  831. }
  832. </style>
  833. <style lang="scss">
  834. .realtime-top-select {
  835. width: 400px;
  836. .el-input__inner {
  837. cursor: pointer;
  838. &:hover {
  839. color: #1886fe;
  840. }
  841. }
  842. }
  843. .part-filter-realtime {
  844. .el-form-item {
  845. margin-bottom: 10px;
  846. }
  847. }
  848. </style>