RealtimeMonitoring.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935
  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", ["updateDetailIds"]),
  448. ...mapMutations("invigilation", [
  449. "setDetailIds",
  450. "setWarningMessageTimeCaches",
  451. ]),
  452. clearLoopSetTs() {
  453. if (!this.loopSetTs.length) return;
  454. this.loopSetTs.forEach((sett) => {
  455. clearTimeout(sett);
  456. });
  457. this.loopSetTs = [];
  458. },
  459. async timerUpdatePage() {
  460. this.clearLoopSetTs();
  461. if (!this.loopRunning || !this.filter.examId) return;
  462. let fetchAll = [this.getList()];
  463. if (this.$refs.SummaryLine)
  464. fetchAll.push(this.$refs.SummaryLine.initData());
  465. fetchAll.push(this.getMonitorCallCount());
  466. fetchAll.push(this.fetchWarningNotice());
  467. await Promise.all(fetchAll).catch(() => {});
  468. this.loopSetTs.push(
  469. setTimeout(() => {
  470. this.timerUpdatePage();
  471. }, 10 * 1000)
  472. );
  473. },
  474. examChange(examBatch) {
  475. if (!examBatch) return;
  476. this.filter.examId = examBatch.id;
  477. this.curExamBatch = examBatch;
  478. const monitorStatusFilter = {};
  479. if (examBatch.monitorVideoSource) {
  480. this.viewingAngles = examBatch.monitorVideoSource
  481. .split(",")
  482. .map((item) => {
  483. const filterParam = this.videoSourceStatusParams[item].replace(
  484. "Source",
  485. ""
  486. );
  487. monitorStatusFilter[filterParam] = null;
  488. return {
  489. code: item,
  490. name: this.VIDEO_SOURCE_TYPE[item],
  491. param: this.videoSourceStatusParams[item],
  492. filterParam,
  493. };
  494. });
  495. this.monitorStatusFilter = monitorStatusFilter;
  496. } else {
  497. this.viewingAngles = [];
  498. }
  499. this.curViewingAngle = this.viewingAngles[0] || {};
  500. this.filter.monitorVideoSource = this.curViewingAngle.code || "";
  501. this.filter.roomCode = "";
  502. this.getExamRooms();
  503. },
  504. async getExamRooms() {
  505. this.examRooms = [];
  506. if (!this.curExamBatch.id) return;
  507. const res = await examActivityRoomList(this.curExamBatch.id);
  508. this.examRooms = res.data.data.examRooms || [];
  509. this.filter.roomCode = this.examRooms[0] && this.examRooms[0].roomCode;
  510. this.examActivities = this.getExamActivities(res.data.data.examActivitys);
  511. this.filter.examActivityId =
  512. this.examActivities[0] && this.examActivities[0].id;
  513. this.examRoomChange();
  514. },
  515. getExamActivities(examActivitys) {
  516. if (!examActivitys.length) return [];
  517. const now = Date.now();
  518. examActivitys.forEach((item) => {
  519. item.endRemainTime = item.finishTime - now;
  520. });
  521. examActivitys.sort((a, b) => {
  522. if (a.endRemainTime < 0) return 1;
  523. if (b.endRemainTime < 0) return -1;
  524. return a.endRemainTime - b.endRemainTime;
  525. });
  526. return examActivitys;
  527. },
  528. examRoomChange() {
  529. this.toSearch();
  530. this.getMonitorCallCount();
  531. this.fetchWarningNotice();
  532. // 正在考试的考试,开启定时更新;
  533. if (this.curExamBatch.isExaming) {
  534. this.loopRunning = true;
  535. this.clearLoopSetTs();
  536. this.loopSetTs.push(
  537. setTimeout(() => {
  538. this.timerUpdatePage();
  539. }, 10 * 1000)
  540. );
  541. } else {
  542. this.loopRunning = false;
  543. this.clearLoopSetTs();
  544. }
  545. },
  546. pageTypeChange(pageType) {
  547. this.pageType = pageType;
  548. this.multipleSelection = [];
  549. },
  550. viewingAngleChange(data) {
  551. if (data.code === this.curViewingAngle.code) return;
  552. this.curViewingAngle = data;
  553. this.filter.monitorVideoSource = data.code;
  554. this.dataList = [];
  555. this.getList();
  556. },
  557. async getList() {
  558. if (this.pageType === "1" && this.videoIsLargeView) return;
  559. const datas = {
  560. ...this.filter,
  561. ...this.monitorStatusFilter,
  562. pageNumber: this.current,
  563. pageSize: this.size,
  564. };
  565. const res = await invigilateVideoList(datas);
  566. const domainLen = this.liveDomains.length;
  567. this.dataList = res.data.data.records.map((item, index) => {
  568. const domain = domainLen ? this.liveDomains[index % domainLen] : "";
  569. item.label = `${item.identity} ${item.courseName}(${item.courseCode}) ${item.name}`;
  570. item.liveUrl = item.monitorLiveUrl
  571. ? `${domain}/live/${item.monitorLiveUrl.toLowerCase()}.flv`
  572. : "";
  573. item.progress = item.progress ? Math.round(item.progress * 100) : 0;
  574. return item;
  575. });
  576. this.hasNewWarning = this.dataList.some((item) => item.warningNew);
  577. this.total = res.data.data.total;
  578. },
  579. toPage(page) {
  580. this.current = page;
  581. this.getList();
  582. },
  583. async toSearch() {
  584. this.current = 1;
  585. await this.getList();
  586. if (this.total > this.size) {
  587. this.updateDetailIds({
  588. filterData: this.filter,
  589. fetchFunc: invigilateVideoList,
  590. });
  591. } else {
  592. const ids = this.dataList.map((item) => item.examRecordId);
  593. this.setDetailIds([...new Set(ids)]);
  594. }
  595. },
  596. async getMonitorCallCount() {
  597. if (!this.filter.examId) return;
  598. const res = await monitorCallCount({
  599. examId: this.filter.examId,
  600. roomCode: this.filter.roomCode,
  601. callStatus: "START,CANCEL",
  602. });
  603. this.communicationCount = res.data.data.count || 0;
  604. },
  605. getValidWarningList(data) {
  606. let dataList = [];
  607. const maxCount = 16;
  608. const maxCacheTime = 5 * 60 * 1000;
  609. // 每个人只取最新一条消息
  610. let warningListMap = {};
  611. data.forEach((item, index) => {
  612. item.index = index;
  613. warningListMap[item.examRecordId] = item;
  614. });
  615. const warningList = Object.values(warningListMap).sort(
  616. (a, b) => a.index - b.index
  617. );
  618. const warningMessageTimeCaches = { ...this.warningMessageTimeCaches };
  619. for (let i = 0; i < warningList.length; i++) {
  620. const item = warningList[i];
  621. const content = (item.info || "").split(/【|】/);
  622. if (content.length === 3) {
  623. item.title = content[1];
  624. } else {
  625. item.title = "";
  626. }
  627. const stdKey = `${item.examRecordId}_${item.title}`;
  628. const nowTime = Date.now();
  629. if (warningMessageTimeCaches[stdKey]) {
  630. if (nowTime - warningMessageTimeCaches[stdKey] > maxCacheTime) {
  631. warningMessageTimeCaches[stdKey] = nowTime;
  632. dataList.push(item);
  633. }
  634. } else {
  635. warningMessageTimeCaches[stdKey] = nowTime;
  636. dataList.push(item);
  637. }
  638. if (dataList.length >= maxCount) {
  639. this.setWarningMessageTimeCaches(warningMessageTimeCaches);
  640. return dataList;
  641. }
  642. }
  643. this.setWarningMessageTimeCaches(warningMessageTimeCaches);
  644. return dataList;
  645. },
  646. async fetchWarningNotice() {
  647. if (!this.filter.examId) return;
  648. this.cleartNoticeLoopSetTs();
  649. const showAlert = async (item) => {
  650. return new Promise((resolve) => {
  651. let st = setTimeout(() => {
  652. let notifyIns = this.$notify({
  653. duration: 5 * 1000,
  654. dangerouslyUseHTMLString: true,
  655. customClass: "msg-monitor-magbox",
  656. position: "bottom-right",
  657. offset: 50,
  658. message: `
  659. <div class="msg-monitor">
  660. <span class="msg-monitor-icon"><i class="icon icon-warning"></i></span>
  661. <span>注意:<b>${item.name}</b>发现异常,</span>
  662. <span class="msg-monitor-action" onclick="window.inviligateWarning('${item.examRecordId}')">立即处理</span>
  663. </div>
  664. `,
  665. });
  666. resolve(notifyIns);
  667. }, 500);
  668. this.noticeLoopSetTs.push(st);
  669. });
  670. };
  671. Notification.closeAll();
  672. let noticeCaches = {},
  673. noticeList = [];
  674. const maxNoticeCount = 3;
  675. const res = await invigilationWarningMessage(this.filter.examId);
  676. const dataList = this.getValidWarningList(res.data.data);
  677. for (let i = 0, len = dataList.length; i < len; i++) {
  678. const item = dataList[i];
  679. const stdKey = item.examRecordId;
  680. if (!noticeCaches[stdKey]) {
  681. if (noticeList.length > maxNoticeCount) {
  682. const prevStdKey = noticeList.shift();
  683. noticeCaches[prevStdKey].close();
  684. }
  685. noticeCaches[stdKey] = await showAlert(item);
  686. noticeList.push(stdKey);
  687. }
  688. }
  689. if (noticeList.length > maxNoticeCount) {
  690. const prevStdKey = noticeList.shift();
  691. noticeCaches[prevStdKey].close();
  692. }
  693. },
  694. cleartNoticeLoopSetTs() {
  695. this.noticeLoopSetTs.forEach((t) => clearTimeout(t));
  696. this.noticeLoopSetTs = [];
  697. },
  698. handleSelectionChange(val) {
  699. console.log(val);
  700. this.multipleSelection = val;
  701. },
  702. async finishInvigilation() {
  703. if (!this.multipleSelection.length) {
  704. this.$message.error("请先选择数据!");
  705. return;
  706. }
  707. this.$refs.handleRollupDialog.open();
  708. },
  709. rollupOver() {
  710. this.multipleSelection = [];
  711. this.getList();
  712. },
  713. toCommunication() {
  714. this.$router.push({
  715. name: "VideoCommunication",
  716. params: {
  717. examId: this.filter.examId,
  718. roomCode: this.filter.roomCode,
  719. },
  720. });
  721. },
  722. toDetail(row) {
  723. this.$router.push({
  724. name: "WarningDetail",
  725. params: { examRecordId: row.examRecordId },
  726. });
  727. },
  728. videoAllMuted() {
  729. this.$refs.InvigilationStudent.forEach((refInst) => {
  730. refInst.mutedPlayer(true);
  731. });
  732. },
  733. videoViewChange(isLarge) {
  734. this.videoIsLargeView = isLarge;
  735. },
  736. async toFullScreen() {
  737. const fullscreenEnabled =
  738. document.fullscreenEnabled ||
  739. document.mozFullScreenEnabled ||
  740. document.webkitFullscreenEnabled ||
  741. document.msFullscreenEnabled;
  742. if (!fullscreenEnabled) {
  743. this.$message.error("当前浏览器不支持全屏!");
  744. return;
  745. }
  746. const de = document.documentElement;
  747. const requestFullscreen =
  748. de.requestFullscreen ||
  749. de.mozRequestFullScreen ||
  750. de.webkitRequestFullscreen;
  751. const exitFullscreen =
  752. document.exitFullscreen ||
  753. document.mozCancelFullScreen ||
  754. document.webkitCancelFullScreen;
  755. if (this.isFullscreen) {
  756. await exitFullscreen.call(document).catch(() => {});
  757. } else {
  758. await requestFullscreen.call(de).catch(() => {});
  759. }
  760. },
  761. },
  762. beforeDestroy() {
  763. this.setWarningMessageTimeCaches = {};
  764. delete window.inviligateWarning;
  765. },
  766. beforeRouteEnter(to, from, next) {
  767. next((vm) => {
  768. if (["WarningDetail", "VideoCommunication"].includes(from.name)) {
  769. vm.pageType = vm.cachePaperType || "0";
  770. if (vm.curExamBatch.isExaming) {
  771. vm.loopRunning = true;
  772. vm.timerUpdatePage();
  773. }
  774. }
  775. });
  776. },
  777. beforeRouteLeave(to, from, next) {
  778. this.cachePaperType = this.pageType;
  779. this.pageType = "0";
  780. this.loopRunning = false;
  781. this.clearLoopSetTs();
  782. this.cleartNoticeLoopSetTs();
  783. Notification.closeAll();
  784. next();
  785. },
  786. };
  787. </script>
  788. <style lang="scss" scoped>
  789. .text-clock {
  790. color: #1886fe;
  791. font-weight: 600;
  792. }
  793. .realtime-switch {
  794. font-size: 0;
  795. .toggle-full-button {
  796. margin-right: 20px;
  797. height: 28px;
  798. }
  799. &-warning {
  800. .realtime-switch-item {
  801. &::before {
  802. content: "";
  803. display: block;
  804. position: absolute;
  805. width: 10px;
  806. height: 10px;
  807. top: -5px;
  808. right: -5px;
  809. border-radius: 50%;
  810. border: 2px solid #fff;
  811. background: #fe5863;
  812. z-index: 9;
  813. }
  814. }
  815. }
  816. &-item {
  817. display: inline-block;
  818. vertical-align: top;
  819. font-size: 12px;
  820. color: #8c94ac;
  821. background: #fff;
  822. line-height: 18px;
  823. padding: 5px 14px;
  824. position: relative;
  825. cursor: pointer;
  826. > i {
  827. margin-right: 5px;
  828. }
  829. &:first-child {
  830. border-radius: 6px 0px 0px 6px;
  831. }
  832. &:last-child {
  833. border-radius: 0px 6px 6px 0px;
  834. }
  835. &-act {
  836. color: #fff;
  837. background: #5fc9fa;
  838. }
  839. }
  840. }
  841. .invigilation-student-list {
  842. border-radius: 6px;
  843. font-size: 0;
  844. min-height: 200px;
  845. margin: -10px;
  846. .invigilation-student-item {
  847. display: inline-block;
  848. vertical-align: top;
  849. padding: 10px;
  850. width: 25%;
  851. font-size: 0;
  852. }
  853. .invigilation-student {
  854. padding: 20px;
  855. border: none;
  856. margin: 0;
  857. background: #fff;
  858. font-size: 14px;
  859. }
  860. }
  861. .warn-new-tips {
  862. position: relative;
  863. &::after {
  864. content: "";
  865. display: block;
  866. position: absolute;
  867. width: 32px;
  868. height: 16px;
  869. right: -32px;
  870. top: 0;
  871. background-image: url(../../../assets/icon-new-tips.png);
  872. background-size: 100% 100%;
  873. }
  874. }
  875. .part-filter-info-sub {
  876. .el-badge {
  877. margin: 0 10px;
  878. vertical-align: top;
  879. }
  880. }
  881. </style>
  882. <style lang="scss">
  883. .realtime-top-select {
  884. width: 400px;
  885. .el-input__inner {
  886. cursor: pointer;
  887. &:hover {
  888. color: #1886fe;
  889. }
  890. }
  891. }
  892. .part-filter-realtime {
  893. .el-form-item {
  894. margin-bottom: 10px;
  895. }
  896. }
  897. </style>