WarningDetail.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. <template>
  2. <div class="warning-detail">
  3. <div class="warning-detail-head">
  4. <div class="warning-detail-title">
  5. <h2>预警详情</h2>
  6. <el-button size="mini" icon="el-icon-arrow-left" @click="goBack"
  7. >返回列表</el-button
  8. >
  9. <!-- <el-button
  10. @click="initSubscribeVideo"
  11. type="primary"
  12. size="mini"
  13. icon="el-icon-arrow-left"
  14. >开始视频</el-button
  15. >
  16. <el-button
  17. @click="closeSubscribeVideo"
  18. type="danger"
  19. size="mini"
  20. icon="el-icon-arrow-left"
  21. >关闭视频</el-button
  22. > -->
  23. </div>
  24. <div class="warning-detail-student">
  25. <div class="student-head">
  26. <div class="student-head-left">
  27. <p><i class="icon icon-user-act"></i></p>
  28. <p>
  29. <span>姓名:</span><span>{{ detailInfo.examStudentName }}</span>
  30. </p>
  31. <p>
  32. <span>证件号:</span><span>{{ detailInfo.identity }}</span>
  33. </p>
  34. <p>
  35. <span>科目(代码):</span
  36. ><span>{{ detailInfo.courseNameCode }}</span>
  37. </p>
  38. </div>
  39. <div class="student-head-right">
  40. <el-button
  41. class="el-icon-btn"
  42. size="mini"
  43. type="primary"
  44. icon="el-icon-arrow-left"
  45. title="查看上一个"
  46. @click="changeStudent(0)"
  47. :disabled="holding"
  48. ></el-button>
  49. <el-button
  50. class="el-icon-btn"
  51. size="mini"
  52. type="primary"
  53. icon="el-icon-arrow-right"
  54. title="查看下一个"
  55. @click="changeStudent(1)"
  56. :disabled="holding"
  57. ></el-button>
  58. </div>
  59. </div>
  60. <div class="student-views">
  61. <div class="student-avatar">
  62. <img
  63. :src="detailInfo.basePhotoPath"
  64. :alt="detailInfo.examStudentName"
  65. v-if="detailInfo.basePhotoPath"
  66. />
  67. <div class="avatar-default" v-else>
  68. <i class="el-icon-user-solid"></i>
  69. </div>
  70. </div>
  71. <div class="student-video">
  72. <div class="student-video-item">
  73. <flv-media
  74. ref="FirstViewVideo"
  75. :live-url="firstViewVideo.liveUrl"
  76. v-if="firstViewVideoReady"
  77. ></flv-media>
  78. <div class="student-video-none" v-else>
  79. <i class="el-icon-video-camera-solid"></i>
  80. </div>
  81. </div>
  82. <div class="student-video-item">
  83. <flv-media
  84. ref="SecondViewVideo"
  85. :live-url="secondViewVideo.liveUrl"
  86. v-if="secondViewVideoReady"
  87. ></flv-media>
  88. <div class="student-video-none" v-else>
  89. <i class="el-icon-video-camera-solid"></i>
  90. </div>
  91. </div>
  92. </div>
  93. </div>
  94. <div class="student-exception">
  95. <ul>
  96. <li v-for="(log, index) in exceptionSummary" :key="index">
  97. <i>{{ index + 1 }}</i>
  98. <h4>{{ log.title }}</h4>
  99. <p v-if="log.desc">{{ log.desc }}</p>
  100. <!-- <p>
  101. 时间段:
  102. <span v-if="log.startTime">{{ log.startTime }} ~ </span>
  103. <span>{{ log.endTime }}</span>
  104. </p>
  105. <p v-if="log.durationTime">持续时长约:{{ log.durationTime }}</p> -->
  106. </li>
  107. </ul>
  108. </div>
  109. </div>
  110. </div>
  111. <div class="warning-detail-body">
  112. <div class="warning-body-head clear-float">
  113. <div class="warning-body-head-action">
  114. <h3>考试轨迹</h3>
  115. <!-- <el-button
  116. class="el-icon-btn"
  117. type="primary"
  118. icon="icon icon-view"
  119. ></el-button> -->
  120. <el-button
  121. class="el-icon-btn"
  122. type="primary"
  123. icon="icon icon-text"
  124. @click="toSendTextMsg"
  125. title="发送文字提醒"
  126. ></el-button>
  127. <el-button
  128. class="el-icon-btn"
  129. type="primary"
  130. icon="icon icon-audio"
  131. @click="toSendAudioMsg"
  132. ></el-button>
  133. <el-popover
  134. class="warning-body-head-call"
  135. placement="bottom-start"
  136. trigger="click"
  137. >
  138. <el-button type="success" @click="answer(0)">语音通话</el-button>
  139. <el-button type="primary" @click="answer(1)">视频通话</el-button>
  140. <el-button type="primary" slot="reference">实时通话</el-button>
  141. </el-popover>
  142. </div>
  143. <div class="warning-body-head-info summary-line">
  144. <p class="summary-line-item">
  145. <i class="line-point line-point-danger"></i>
  146. <span class="line-name">系统预警</span>
  147. <span>{{ detailInfo.warningCount }}次</span>
  148. </p>
  149. <p class="summary-line-item">
  150. <i class="line-point line-point-danger"></i>
  151. <span class="line-name">陌生人脸</span>
  152. <span>{{ detailInfo.multipleFaceCount }}次</span>
  153. </p>
  154. <p class="summary-line-item">
  155. <i class="line-point line-point-danger"></i>
  156. <span class="line-name">异常处理</span>
  157. <span>{{ detailInfo.exceptionCount }}次</span>
  158. </p>
  159. <p class="summary-line-item">
  160. <span></span>
  161. <span>
  162. <b>违纪状态:</b>
  163. <b :class="{ 'color-danger': !detailInfo.breachStatus }">
  164. {{ !detailInfo.breachStatus ? "违纪" : "正常" }}
  165. </b>
  166. </span>
  167. </p>
  168. <el-button
  169. :type="!detailInfo.breachStatus ? 'success' : 'danger'"
  170. icon="icon icon-stop"
  171. @click="toBreach"
  172. >{{ !detailInfo.breachStatus ? "撤销违纪" : "违纪处理" }}</el-button
  173. >
  174. <el-button type="warning" icon="icon icon-forbide" @click="toFinish"
  175. >强制收卷</el-button
  176. >
  177. </div>
  178. </div>
  179. <div class="warning-body-main">
  180. <div
  181. class="warning-history"
  182. v-for="log in detailInfo.examStudentLogList"
  183. :key="log.id"
  184. >
  185. <div class="warning-history-info">
  186. <h3>{{ log.title }}</h3>
  187. <p v-if="log.desc">{{ log.desc }}</p>
  188. <p>
  189. 时间段:
  190. <span v-if="log.startTime">{{ log.startTime }} ~ </span>
  191. <span>{{ log.endTime }}</span>
  192. </p>
  193. <p v-if="log.durationTime">持续时长约:{{ log.durationTime }}</p>
  194. </div>
  195. <div
  196. :class="[
  197. 'warning-history-type',
  198. log.viewType === 'common' ? 'type-common' : 'type-exception',
  199. ]"
  200. >
  201. <i
  202. :class="[
  203. 'icon',
  204. {
  205. 'icon-current-step': log.viewType === 'common',
  206. 'icon-warning-act': log.viewType === 'warning',
  207. 'icon-net-break': log.viewType === 'exception',
  208. },
  209. ]"
  210. ></i>
  211. </div>
  212. <div class="warning-history-media">
  213. <ul class="media-list" v-if="log.photos">
  214. <li v-for="(photo, pindex) in log.photos" :key="pindex">
  215. <img :src="photo" />
  216. </li>
  217. </ul>
  218. </div>
  219. </div>
  220. </div>
  221. </div>
  222. <!-- student-breach-dialog -->
  223. <student-breach-dialog
  224. :instance="curDetail"
  225. @modified="breachFinish"
  226. ref="StudentBreachDialog"
  227. ></student-breach-dialog>
  228. <!-- warning-text-message-dialog -->
  229. <warning-text-message-dialog
  230. :record-id="recordId"
  231. ref="WarningTextMessageDialog"
  232. ></warning-text-message-dialog>
  233. <!-- audio-record-dialog -->
  234. <audio-record-dialog
  235. :record-id="recordId"
  236. ref="AudioRecordDialog"
  237. ></audio-record-dialog>
  238. <!-- 通话弹出层 -->
  239. <el-dialog
  240. custom-class="communication-dialog"
  241. :visible.sync="dialogVisible"
  242. width="600px"
  243. :show-close="false"
  244. :close-on-press-escape="false"
  245. :close-on-click-modal="false"
  246. append-to-body
  247. fullscreen
  248. >
  249. <div class="communication-box" v-show="!isWaiting">
  250. <div class="communication-host" id="communication-host"></div>
  251. <div class="communication-guest" id="communication-guest"></div>
  252. <div class="communication-action">
  253. <el-button round type="danger" @click="hangup">结束通话</el-button>
  254. </div>
  255. <div class="communication-info">
  256. <!-- <span>当前网络状态良好</span> -->
  257. <span>持续时长:<second-timer ref="SecondTimer"></second-timer></span>
  258. </div>
  259. </div>
  260. <div class="communication-wait" v-show="isWaiting">
  261. <p class="communication-wait-tips">等待接听…</p>
  262. <div class="communication-wait-avatar">
  263. <img
  264. :src="detailInfo.basePhotoPath"
  265. :alt="detailInfo.examStudentName"
  266. />
  267. </div>
  268. <p class="communication-wait-username">
  269. {{ detailInfo.examStudentName }}
  270. </p>
  271. <div class="communication-wait-action">
  272. <el-button round type="danger" @click="hangup">取消通话</el-button>
  273. </div>
  274. </div>
  275. <span slot="footer" class="dialog-footer"> </span>
  276. </el-dialog>
  277. </div>
  278. </template>
  279. <script>
  280. import { createClient, createStream } from "@/plugins/trtc";
  281. import {
  282. invigilateDetail,
  283. invigilateFinish,
  284. warningStudentDetail,
  285. getUserMonitorKey,
  286. } from "@/api/invigilation";
  287. import FlvMedia from "../common/FlvMedia";
  288. import StudentBreachDialog from "./StudentBreachDialog";
  289. import WarningTextMessageDialog from "./WarningTextMessageDialog";
  290. import AudioRecordDialog from "./audioRecord/AudioRecordDialog";
  291. import SecondTimer from "../common/SecondTimer";
  292. import { formatDate, timeNumberToText, objTypeOf } from "@/utils/utils";
  293. import { mapState } from "vuex";
  294. export default {
  295. name: "warning-detail",
  296. components: {
  297. FlvMedia,
  298. StudentBreachDialog,
  299. WarningTextMessageDialog,
  300. AudioRecordDialog,
  301. SecondTimer,
  302. },
  303. data() {
  304. return {
  305. recordId: this.$route.params.recordId,
  306. detailInfo: {},
  307. curDetail: {},
  308. serialIds: [],
  309. exceptionSummary: [],
  310. firstViewVideo: {
  311. liveUrl: "",
  312. },
  313. secondViewVideo: {
  314. liveUrl: "",
  315. },
  316. firstViewVideoReady: false,
  317. secondViewVideoReady: false,
  318. holding: false,
  319. // communication
  320. userMonitor: {},
  321. client: null,
  322. localStream: null,
  323. dialogVisible: false,
  324. isWaiting: true,
  325. subscribeSetT: null,
  326. loopRunning: false,
  327. loopSetTs: [],
  328. };
  329. },
  330. computed: {
  331. ...mapState("invigilation", ["detailIds", "liveDomains"]),
  332. },
  333. watch: {
  334. $route: {
  335. handler() {
  336. this.initData();
  337. },
  338. },
  339. },
  340. mounted() {
  341. this.initData();
  342. },
  343. methods: {
  344. async initData() {
  345. this.recordId = this.$route.params.recordId;
  346. await this.getInvigilateDetail().catch(() => {});
  347. await this.getStudentVideo().catch(() => {});
  348. this.holding = false;
  349. // 学生正在考试,开启定时更新
  350. if (this.detailInfo.statusCode === "ANSWERING") {
  351. this.loopRunning = true;
  352. this.clearLoopSetTs();
  353. this.loopSetTs.push(
  354. setTimeout(() => {
  355. this.timerUpdatePage();
  356. }, 10 * 1000)
  357. );
  358. } else {
  359. this.loopRunning = false;
  360. this.clearLoopSetTs();
  361. }
  362. },
  363. clearLoopSetTs() {
  364. if (!this.loopSetTs.length) return;
  365. this.loopSetTs.forEach((sett) => {
  366. clearTimeout(sett);
  367. });
  368. this.loopSetTs = [];
  369. },
  370. async timerUpdatePage() {
  371. this.clearLoopSetTs();
  372. if (!this.loopRunning) return;
  373. await this.getInvigilateDetail().catch(() => {});
  374. this.loopSetTs.push(
  375. setTimeout(() => {
  376. this.timerUpdatePage();
  377. }, 10 * 1000)
  378. );
  379. },
  380. async getStudentVideo() {
  381. const res = await warningStudentDetail({ recordId: this.recordId });
  382. const records = res.data.data.map((item, index) => {
  383. const domain = this.liveDomains[index] || "";
  384. item.liveUrl = item.liveUrl
  385. ? `${domain}/live/${item.liveUrl.toLowerCase()}.flv`
  386. : "";
  387. item.name = item.source;
  388. return item;
  389. });
  390. console.log(records.map((item) => item.liveUrl));
  391. this.firstViewVideo = records[0] || {};
  392. this.secondViewVideo = records[2] || {};
  393. if (records.length) this.initSubscribeVideo();
  394. },
  395. async getInvigilateDetail() {
  396. const res = await invigilateDetail(this.recordId);
  397. this.detailInfo = res.data.data;
  398. this.detailInfo.examStudentLogList = this.parseStudentLogs(
  399. this.detailInfo.examStudentLogList
  400. );
  401. this.exceptionSummary = this.detailInfo.examStudentLogList
  402. .filter((item) => item.viewType === "warning")
  403. .slice(0, 3);
  404. },
  405. parseStudentLogs(examStudentLogList) {
  406. const statusTypes = {
  407. common: [
  408. "FIRST_START",
  409. "RESUME_START",
  410. "IN_PROCESS",
  411. "PREPARE",
  412. "ANSWERING",
  413. "BREAK_OFF",
  414. "RESUME_PREPARE",
  415. "FINISHED",
  416. "FIRST_PREPARE",
  417. ],
  418. warning: [
  419. "FACE_COUNT_ERROR",
  420. "FACE_COMPARE_ERROR",
  421. "EYE_CLOSE_ERROR",
  422. "LIVENESS_ACTION_ERROR",
  423. "REALNESS",
  424. ],
  425. exception: [
  426. "NET_TIME_OUT",
  427. "MACHING_STOP",
  428. "NET_TIME_BREAK",
  429. "SOFTWARE_STOP",
  430. "POWER_CUT",
  431. ],
  432. };
  433. let statusTypeMap = {};
  434. Object.keys(statusTypes).map((k) => {
  435. statusTypes[k].map((item) => {
  436. statusTypeMap[item] = k;
  437. });
  438. });
  439. const logs = examStudentLogList.map((item) => {
  440. let info = { ...item };
  441. info.endTime = formatDate("HH:mm:ss", new Date(info.createTime));
  442. info.viewType = statusTypeMap[info.type] || "common";
  443. const content = info.info.split(/【|】/);
  444. if (content.length === 3) {
  445. info.title = content[1];
  446. info.desc = content[2];
  447. } else {
  448. info.title = content[0];
  449. }
  450. if (info.remark && info.remark.includes('{"')) {
  451. info.remark = JSON.parse(info.remark);
  452. if (info.remark["MIN_CREATE_TIME"]) {
  453. info.startTime = formatDate(
  454. "HH:mm:ss",
  455. new Date(info.remark["MIN_CREATE_TIME"])
  456. );
  457. info.durationTime = timeNumberToText(
  458. info.createTime - info.remark["MIN_CREATE_TIME"]
  459. );
  460. }
  461. let photos = [];
  462. Object.keys(info.remark).map((key) => {
  463. if (key.includes("PHOTO")) {
  464. const kPhotos =
  465. objTypeOf(info.remark[key]) === "array"
  466. ? info.remark[key]
  467. : [info.remark[key]];
  468. photos = [...photos, ...kPhotos];
  469. }
  470. });
  471. info.photos = photos;
  472. } else if (info.updateTime) {
  473. info.startTime = formatDate("HH:mm:ss", new Date(info.createTime));
  474. info.endTime = formatDate("HH:mm:ss", new Date(info.updateTime));
  475. info.durationTime = timeNumberToText(
  476. info.updateTime - info.createTime
  477. );
  478. }
  479. return info;
  480. });
  481. return logs;
  482. },
  483. changeStudent(type) {
  484. let index = this.detailIds.indexOf(this.recordId);
  485. if (type) {
  486. if (index >= this.detailIds.length - 1) {
  487. this.$message.error("当前没有下一个学生了");
  488. return;
  489. }
  490. index++;
  491. } else {
  492. if (index <= 0) {
  493. this.$message.error("当前没有上一个学生了");
  494. return;
  495. }
  496. index--;
  497. }
  498. if (this.holding) return;
  499. this.holding = true;
  500. this.closeSubscribeVideo();
  501. console.log(this.detailIds[index]);
  502. this.$router.replace({
  503. name: "WarningDetail",
  504. params: {
  505. recordId: this.detailIds[index],
  506. },
  507. });
  508. },
  509. toBreach() {
  510. this.curDetail = {
  511. examStudentName: this.detailInfo.examStudentName,
  512. identity: this.detailInfo.identity,
  513. courseNameCode: this.detailInfo.courseNameCode,
  514. description: "",
  515. examRecordId: [this.detailInfo.examRecordId],
  516. breachStatus: this.detailInfo.breachStatus,
  517. status: this.detailInfo.breachStatus ? 0 : 1,
  518. // 状态,0:新建,1:撤销
  519. // 违纪状态:正常(1)=>新建,违纪(0)=>撤销
  520. type: "",
  521. };
  522. this.$refs.StudentBreachDialog.open();
  523. },
  524. async toFinish() {
  525. const result = await this.$confirm(
  526. "试卷若被强制回收,考试再无法重置继续完成考试,请您慎重选择!您确定要强制回收改考试试卷吗?",
  527. "强制收卷确认提醒",
  528. {
  529. confirmButtonText: "确定",
  530. cancelButtonText: "取消",
  531. iconClass: "el-icon-warning",
  532. customClass: "el-message-box__error",
  533. }
  534. ).catch(() => {});
  535. if (!result) return;
  536. await invigilateFinish({
  537. examRecordId: [this.detailInfo.examRecordId],
  538. type: "INTERRUPT",
  539. });
  540. this.$message.success("强制收卷成功!");
  541. this.goBack();
  542. },
  543. toSendTextMsg() {
  544. this.$refs.WarningTextMessageDialog.open();
  545. },
  546. toSendAudioMsg() {
  547. this.$refs.AudioRecordDialog.open();
  548. },
  549. breachFinish() {
  550. this.getInvigilateDetail();
  551. },
  552. // video relative
  553. initSubscribeVideo() {
  554. if (this.firstViewVideo.liveUrl) this.firstViewVideoReady = true;
  555. if (this.secondViewVideo.liveUrl) this.secondViewVideoReady = true;
  556. },
  557. closeSubscribeVideo() {
  558. this.firstViewVideoReady = false;
  559. this.secondViewVideoReady = false;
  560. },
  561. async initClient(examRecordId) {
  562. const res = await getUserMonitorKey(examRecordId);
  563. this.userMonitor = res.data.data;
  564. this.client = createClient({
  565. mode: "live",
  566. sdkAppId: this.userMonitor.appId,
  567. userId: this.userMonitor.monitorUserId,
  568. userSig: this.userMonitor.monitorUserSig,
  569. });
  570. },
  571. async answer(isVideo) {
  572. this.closeSubscribeVideo();
  573. await this.initClient(this.recordId);
  574. this.dialogVisible = true;
  575. // 添加远程用户视频发布监听
  576. this.client.on("stream-added", (event) => {
  577. console.log(event);
  578. const remoteStream = event.stream;
  579. // 延迟订阅视频
  580. this.subscribeSetT = setTimeout(() => {
  581. this.client
  582. .subscribe(remoteStream, { audio: true, video: true })
  583. .then(() => {
  584. console.log("订阅视频成功!");
  585. })
  586. .catch((error) => {
  587. console.log("订阅视频失败!", error);
  588. });
  589. }, 5000);
  590. });
  591. this.client.on("stream-subscribed", (event) => {
  592. const remoteStream = event.stream;
  593. this.isWaiting = false;
  594. this.$nextTick(() => {
  595. this.$refs.SecondTimer.start();
  596. remoteStream.play("communication-host", { objectFit: "contain" });
  597. });
  598. });
  599. // 加入房间
  600. let roomJoinResult = true;
  601. await this.client
  602. .join({
  603. roomId: this.userMonitor.monitorKey,
  604. role: "audience",
  605. })
  606. .catch((error) => {
  607. roomJoinResult = false;
  608. console.log("加入房间失败!", error);
  609. });
  610. if (!roomJoinResult) return;
  611. // 切换角色,连麦互动
  612. let switchResult = true;
  613. await this.client.switchRole("anchor").catch((error) => {
  614. console.log("切换角色失败!", error);
  615. switchResult = false;
  616. });
  617. if (!switchResult) return;
  618. // 初始化stream
  619. this.localStream = createStream({
  620. userId: this.userMonitor.monitorUserId,
  621. audio: true,
  622. video: isVideo,
  623. });
  624. let initLocalStreamResult = true;
  625. await this.localStream.initialize().catch((error) => {
  626. console.log("初始化视频失败!", error);
  627. initLocalStreamResult = false;
  628. });
  629. if (!initLocalStreamResult) return;
  630. // 发布本地视频
  631. let publishStreamResult = true;
  632. this.client.publish(this.localStream).catch((error) => {
  633. console.log("发布本地视频失败!", error);
  634. publishStreamResult = false;
  635. });
  636. if (!publishStreamResult) return;
  637. // 播放本地视频
  638. this.localStream.play("communication-guest", { muted: true });
  639. },
  640. async hangup() {
  641. if (this.subscribeSetT) clearTimeout(this.subscribeSetT);
  642. this.$refs.SecondTimer.end();
  643. // 取消发布本地视频
  644. let unpublishStreamResult = true;
  645. this.client.unpublish(this.localStream).catch((error) => {
  646. console.log("取消发布本地视频失败!", error);
  647. unpublishStreamResult = false;
  648. });
  649. if (!unpublishStreamResult) return;
  650. this.localStream.close();
  651. this.localStream = null;
  652. // 离开房间
  653. let result = true;
  654. await this.client.leave().catch((error) => {
  655. console.log("离开房间失败!", error);
  656. result = false;
  657. });
  658. if (!result) return;
  659. this.client.off("*");
  660. this.client = null;
  661. this.userMonitor = {};
  662. this.dialogVisible = false;
  663. this.isWaiting = true;
  664. this.initSubscribeVideo();
  665. },
  666. goBack() {
  667. window.history.go(-1);
  668. },
  669. },
  670. beforeDestroy() {
  671. this.loopRunning = false;
  672. this.clearLoopSetTs();
  673. if (this.subscribeSetT) clearTimeout(this.subscribeSetT);
  674. if (this.client) {
  675. this.client.leave();
  676. this.client.off("*");
  677. }
  678. },
  679. };
  680. </script>