RealtimeMonitoring.vue 26 KB

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