preview-info.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784
  1. <template>
  2. <div class="preview-info">
  3. <a-button
  4. type="primary"
  5. style="margin-right: 15px"
  6. size="mini"
  7. @click="emits('handleBack')"
  8. >
  9. <template #icon>
  10. <icon-arrow-left />
  11. </template>
  12. </a-button>
  13. <Breadcrumb :items="['menu.previewList.basic', 'menu.previewInfo.basic']" />
  14. <a-card class="general-card" :body-style="{ height: '100%', padding: '0' }">
  15. <a-space direction="vertical" fill>
  16. <div class="preview-info-main">
  17. <div class="video-box-qj">
  18. <div class="video-box-wrapper">
  19. <div v-if="!workerObj.qj" class="video-box-tip"
  20. ><span>{{ $t('previewList.wxh') }}</span></div
  21. >
  22. <a-spin v-if="qjLoading" dot />
  23. <canvas
  24. id="video-canvas-qj"
  25. v-doubleClick="cropFullInfo"
  26. :style="{
  27. width: '100%',
  28. transform: `rotateZ(${cropFullInfo.Rotate}deg)`,
  29. }"
  30. ></canvas>
  31. </div>
  32. </div>
  33. <div class="video-right-content">
  34. <div class="video-right-content-tools">
  35. <!-- 截图 录像 -->
  36. <a-tooltip
  37. v-for="tool in tools"
  38. :key="tool.type"
  39. :content="tool.title"
  40. >
  41. <a-button @click="toolsClick(tool)">
  42. <template #icon>
  43. <Icon
  44. :icon="tool.icon"
  45. :color="tool.color"
  46. :class="[tool.iconClass]"
  47. width="24"
  48. height="24"
  49. />
  50. </template>
  51. </a-button>
  52. </a-tooltip>
  53. <!-- 喊话 -->
  54. <a-popover
  55. title=""
  56. trigger="click"
  57. position="top"
  58. :content-style="{ padding: '0 15px' }"
  59. >
  60. <a-button @click="speechClick">
  61. <template #icon>
  62. <Icon icon="ri:speak-line" width="22" height="22" />
  63. </template>
  64. </a-button>
  65. <template #content>
  66. <Speech :data="data" />
  67. </template>
  68. </a-popover>
  69. <!-- 对讲 -->
  70. <a-tooltip :content="$t('previewList.dj')">
  71. <a-button @click="intercomClick">
  72. <template #icon>
  73. <Icon
  74. v-if="!intercomDisabled"
  75. icon="material-symbols:record-voice-over-outline"
  76. width="22"
  77. height="22"
  78. />
  79. <Icon
  80. v-else
  81. class="fade-open"
  82. icon="icon-park-outline:voice"
  83. width="22"
  84. height="22"
  85. color="red"
  86. />
  87. </template>
  88. </a-button>
  89. </a-tooltip>
  90. <!-- 分屏 -->
  91. <a-dropdown @select="handleRadioChange">
  92. <a-tooltip :content="$t('previewList.fp')">
  93. <a-button>
  94. <template #icon>
  95. <Icon icon="carbon:split-screen" width="20" height="20" />
  96. </template>
  97. </a-button>
  98. </a-tooltip>
  99. <template #content>
  100. <a-doption
  101. v-for="item in btnList"
  102. :key="item.label"
  103. :value="item.value"
  104. >
  105. <template #default>{{ item.label }}</template>
  106. </a-doption>
  107. </template>
  108. </a-dropdown>
  109. </div>
  110. <div class="split-screen-main">
  111. <div class="split-screen-box">
  112. <a-grid
  113. class="grid-demo-grid"
  114. :cols="gridCount"
  115. :col-gap="2"
  116. :row-gap="2"
  117. >
  118. <a-grid-item
  119. v-for="(item, i) in radioActive"
  120. :key="item + '' + i"
  121. @click="handleScreen(i)"
  122. >
  123. <div
  124. class="video-box"
  125. :class="[
  126. 'demo-item',
  127. screenActive === i ? 'screen-active' : '',
  128. ]"
  129. @mouseleave="handleMouseLeave(i)"
  130. @mouseenter="handleMouseEnter(i)"
  131. @dblclick="handleDoubleClick(i)"
  132. >
  133. <a-spin v-if="activeObj[i].loading" dot />
  134. <div v-if="!activeObj[i].workerObj" class="video-box-tip"
  135. ><span>{{ t('previewList.wxh') }}</span></div
  136. >
  137. <div class="video-tools">
  138. <!-- 关闭 -->
  139. <a-tooltip :content="$t('previewList.close')">
  140. <a-button
  141. v-if="activeObj[i].isCloseBtn"
  142. type="text"
  143. size="mini"
  144. @click="handleClose(i)"
  145. >
  146. <template #icon>
  147. <Icon
  148. icon="mdi:shutdown"
  149. width="24"
  150. height="24"
  151. />
  152. </template>
  153. </a-button>
  154. </a-tooltip>
  155. <!-- 对讲 -->
  156. <!-- <a-tooltip :content="$t('previewList.close')">
  157. <a-button
  158. v-if="activeObj[i].isCloseBtn"
  159. type="text"
  160. size="mini"
  161. @click="handleClose(i)"
  162. >
  163. <Icon
  164. icon="material-symbols:record-voice-over-outline"
  165. width="24"
  166. height="24"
  167. />
  168. </a-button>
  169. </a-tooltip> -->
  170. </div>
  171. <canvas
  172. v-if="activeObj[i].isReset"
  173. :id="'video-canvas-part' + i"
  174. :style="{
  175. width: '100%',
  176. transform: `rotateZ(${cropFullInfo.Rotate}deg)`,
  177. }"
  178. ></canvas>
  179. </div>
  180. </a-grid-item>
  181. </a-grid>
  182. </div>
  183. </div>
  184. <div class="alarm-box">
  185. <a-table
  186. :columns="alarmColumns"
  187. :data="alarmData"
  188. :scroll="{ y: '100%' }"
  189. :pagination="false"
  190. :bordered="{ cell: true }"
  191. :loading="alarmLoading"
  192. size="small"
  193. style="height: 100%"
  194. >
  195. <template #columns>
  196. <a-table-column
  197. :title="t('previewList.bjlx')"
  198. data-index="type"
  199. >
  200. </a-table-column>
  201. <a-table-column
  202. :title="t('previewList.bjsj')"
  203. data-index="timeN"
  204. :width="180"
  205. >
  206. </a-table-column>
  207. <a-table-column
  208. :title="t('previewList.status')"
  209. data-index="status"
  210. :width="150"
  211. :body-cell-style="
  212. (record) => ({
  213. background: record.status ? '' : '#7e2d2d',
  214. })
  215. "
  216. >
  217. <template #cell="{ record }">
  218. <span>{{
  219. record.status
  220. ? t('previewList.ycl')
  221. : t('previewList.wcl')
  222. }}</span>
  223. </template>
  224. </a-table-column>
  225. <a-table-column
  226. :title="t('previewList.dqstatus')"
  227. data-index="isRead"
  228. :width="150"
  229. :body-cell-style="
  230. (record) => ({
  231. background: record.isRead ? '' : '#2d4f7e',
  232. })
  233. "
  234. >
  235. <template #cell="{ record }">
  236. <span>{{
  237. record.isRead
  238. ? t('previewList.yd')
  239. : t('previewList.wd')
  240. }}</span>
  241. </template>
  242. </a-table-column>
  243. <a-table-column :title="t('previewList.cz')" :width="150">
  244. <template #cell="{ record }">
  245. <a-button
  246. type="text"
  247. size="mini"
  248. @click="
  249. () => (
  250. (record.isRead = true),
  251. $modal.info({
  252. title: '提示',
  253. draggable: true,
  254. content: record.type,
  255. })
  256. )
  257. "
  258. >{{ t('previewList.ck') }}</a-button
  259. >
  260. </template>
  261. <template #title>
  262. <span>{{ t('previewList.cz') }}</span>
  263. <a-tooltip :content="$t('previewList.ckqb')">
  264. <a-button
  265. type="text"
  266. size="mini"
  267. style="position: absolute; right: 40px"
  268. @click="modalRef.open()"
  269. >
  270. <template #icon>
  271. <Icon icon="gg:view-list" width="22" height="22" />
  272. </template>
  273. </a-button>
  274. </a-tooltip>
  275. <a-tooltip :content="$t('previewList.tip1')">
  276. <a-button
  277. type="text"
  278. size="mini"
  279. style="position: absolute; right: 8px"
  280. @click="
  281. () =>
  282. alarmData.forEach((item) => (item.isRead = true))
  283. "
  284. >
  285. <template #icon>
  286. <a-badge
  287. dot
  288. color="#FFB400"
  289. :count="alarmData.filter((v) => !v.isRead).length"
  290. >
  291. <Icon
  292. icon="ant-design:clear-outlined"
  293. width="20"
  294. height="20"
  295. />
  296. </a-badge>
  297. </template>
  298. </a-button>
  299. </a-tooltip>
  300. </template>
  301. </a-table-column>
  302. </template>
  303. </a-table>
  304. </div>
  305. </div>
  306. </div>
  307. </a-space>
  308. </a-card>
  309. <PubModal
  310. ref="modalRef"
  311. title="报警列表"
  312. :fullscreen="true"
  313. :closable="true"
  314. >
  315. <AlarmAll :guid="data.id" />
  316. </PubModal>
  317. </div>
  318. </template>
  319. <script setup lang="ts">
  320. import useWorker from '@/assets/js/video-lib/omnimatrix-video-player';
  321. import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
  322. import { IntercomFn } from '@/utils/Intercom';
  323. import { useI18n } from 'vue-i18n';
  324. import { Icon } from '@iconify/vue';
  325. import { getCropFullInfo } from '@/api/system';
  326. import { useAppStore } from '@/store';
  327. import dayjs from 'dayjs';
  328. import { Message } from '@arco-design/web-vue';
  329. import useWheel from '@/assets/js/useWheel';
  330. import Speech from '../components/speech.vue';
  331. import AlarmAll from '../components/alarm-all.vue';
  332. import PubModal from '../components/pub-modal.vue';
  333. const { t } = useI18n();
  334. interface IBtn {
  335. label: string;
  336. value: number;
  337. }
  338. const app = useAppStore();
  339. const modalRef = ref();
  340. const workerObj: any = ref({ qj: null });
  341. const cropFullInfo = ref<{
  342. H: number;
  343. Rotate: number;
  344. W: number;
  345. X: number;
  346. Y: number;
  347. }>({ H: 0, Rotate: 0, W: 0, X: 0, Y: 0 });
  348. const emits = defineEmits(['handleBack']);
  349. const props = defineProps<{
  350. data: any;
  351. }>();
  352. const qjLoading = ref(false);
  353. const radioActive = ref<number>(2);
  354. const screenActive = ref<number>(0);
  355. const btnList = computed<IBtn[]>(() => [
  356. {
  357. label: t('previewList.dp'),
  358. value: 1,
  359. },
  360. {
  361. label: t('previewList.efp'),
  362. value: 2,
  363. },
  364. {
  365. label: t('previewList.sfp'),
  366. value: 4,
  367. },
  368. {
  369. label: t('previewList.jfp'),
  370. value: 9,
  371. },
  372. // {
  373. // label: t('previewList.slfp'),
  374. // value: 16,
  375. // },
  376. ]);
  377. const tools = ref([
  378. {
  379. icon: 'ri:screenshot-2-line',
  380. title: t('previewList.jt'),
  381. type: 'jt',
  382. color: '#fff',
  383. iconClass: '',
  384. },
  385. {
  386. icon: 'bx:video-recording',
  387. title: t('previewList.lx'),
  388. type: 'lx',
  389. color: '#fff',
  390. iconClass: '',
  391. },
  392. ]);
  393. const state = ref(false);
  394. const toolsClick = (tool: any) => {
  395. if (!workerObj.value.qj) return;
  396. switch (tool.type) {
  397. case 'lx':
  398. state.value = !state.value;
  399. tool.icon = state.value ? 'fad:armrecording' : 'bx:video-recording';
  400. tool.color = state.value ? 'red' : '#fff';
  401. tool.iconClass = state.value ? 'fade-open' : '';
  402. workerObj.value.qj.WebSocketWork.postMessage({
  403. type: 'lx',
  404. lx: state.value,
  405. });
  406. break;
  407. case 'jt':
  408. workerObj.value.qj.worker.postMessage({ type: 'jt' });
  409. break;
  410. default:
  411. break;
  412. }
  413. };
  414. const gridCount = computed<number>(() => {
  415. let result = 4;
  416. switch (radioActive.value) {
  417. case 2:
  418. result = 2;
  419. break;
  420. case 4:
  421. result = 2;
  422. break;
  423. case 9:
  424. result = 3;
  425. break;
  426. // case 16:
  427. // result = 4;
  428. // break;
  429. default:
  430. result = 1;
  431. break;
  432. }
  433. return result;
  434. });
  435. const activeObj = ref<
  436. {
  437. loading: boolean;
  438. isSignal: boolean;
  439. [key: string]: any;
  440. }[]
  441. >([]);
  442. const alarmColumns = [
  443. {
  444. title: t('previewList.bjlx'),
  445. dataIndex: 'type',
  446. },
  447. {
  448. title: t('previewList.status'),
  449. dataIndex: 'status',
  450. width: 150,
  451. },
  452. ];
  453. const alarmLoading = ref(false);
  454. const alarmData = ref<{ [key: string]: number | string | boolean }[]>([
  455. // {
  456. // type: '行人闯入',
  457. // status: 0,
  458. // isRead: false,
  459. // },
  460. ]);
  461. const handleClose = async (v: number): Promise<void> => {
  462. if (activeObj?.value[v]?.workerObj) {
  463. return new Promise((resolve) => {
  464. activeObj.value[v].isReset = false;
  465. activeObj.value[v].loading = false;
  466. activeObj.value[v].workerObj.close();
  467. setTimeout(() => {
  468. activeObj.value[v].isSignal = false;
  469. activeObj.value[v].workerObj = null;
  470. resolve();
  471. }, 50);
  472. });
  473. }
  474. return undefined;
  475. };
  476. const handleRadioChange = (
  477. v: string | number | Record<string, any> | undefined
  478. ) => {
  479. radioActive.value = typeof v === 'number' ? v : 2;
  480. if (typeof v === 'number') {
  481. screenActive.value = 0;
  482. // eslint-disable-next-line no-plusplus
  483. for (let i = 0; i < v; i++) {
  484. handleClose(i);
  485. }
  486. }
  487. };
  488. const handleScreen = (i: number) => {
  489. screenActive.value = i;
  490. };
  491. const handleMouseEnter = (v: number) => {
  492. activeObj.value[v].isCloseBtn = true;
  493. };
  494. const handleMouseLeave = (v: number) => {
  495. activeObj.value[v].isCloseBtn = false;
  496. };
  497. let parent: any = null;
  498. const handleDoubleClick = (i: number) => {
  499. const el = document.querySelectorAll('.video-box')[i];
  500. const fullscreenDom: HTMLElement | null = document.querySelector(
  501. '#video-box-fullscreen>div'
  502. );
  503. if (fullscreenDom) {
  504. parent?.appendChild(fullscreenDom);
  505. const box = document.querySelector('#video-box-fullscreen');
  506. if (box) {
  507. document.body.removeChild(box);
  508. }
  509. } else {
  510. parent = el?.parentElement;
  511. const div: HTMLElement = document.createElement('div');
  512. div.id = 'video-box-fullscreen';
  513. div.style.position = 'fixed';
  514. div.style.top = '0';
  515. div.style.left = '0';
  516. div.style.width = '100%';
  517. div.style.height = '100%';
  518. div.style.zIndex = '999';
  519. div.appendChild(el);
  520. document.body.appendChild(div);
  521. }
  522. // toggle();
  523. };
  524. const intercomDisabled = ref(false);
  525. const intercomClick = () => {
  526. // (app.ip as string).split('//')[1]
  527. IntercomFn(intercomDisabled, '39.107.83.190');
  528. };
  529. const speechClick = () => {};
  530. watch(
  531. radioActive,
  532. (newV) => {
  533. activeObj.value = [];
  534. // eslint-disable-next-line no-plusplus
  535. for (let i = 0; i < newV; i++) {
  536. handleClose(i);
  537. activeObj.value.push({
  538. loading: false,
  539. isSignal: false,
  540. workerObj: null,
  541. isCloseBtn: false,
  542. isReset: false,
  543. });
  544. }
  545. },
  546. { immediate: true }
  547. );
  548. watch(
  549. () => app.partObj,
  550. (newV: any) => {
  551. const partVideoUrl = `${app.ip}/VideoShow/Preview/Part/Main?GUID=${props.data.id}&X=${newV.PartCenterX}&Y=${newV.PartCenterY}`;
  552. handleClose(screenActive.value).then(() => {
  553. const ao = activeObj.value[screenActive.value];
  554. ao.loading = true;
  555. ao.isReset = true;
  556. nextTick(() => {
  557. ao.workerObj = useWorker(
  558. partVideoUrl,
  559. `#video-canvas-part${screenActive.value}`,
  560. null,
  561. () => {
  562. ao.loading = false;
  563. ao.isSignal = false;
  564. useWheel(
  565. document.querySelectorAll('.video-box')[screenActive.value],
  566. `#video-canvas-part${screenActive.value}`
  567. );
  568. }
  569. );
  570. });
  571. });
  572. },
  573. { deep: true }
  574. );
  575. let eventSource: EventSource;
  576. const alarmSSE = () => {
  577. if (!props.data.id) return;
  578. alarmLoading.value = true;
  579. eventSource = new EventSource(`${app.ip}/rpc/AiSSE?Topic=${props.data.id}`);
  580. eventSource.addEventListener(
  581. 'open',
  582. () => {
  583. alarmData.value = [];
  584. alarmLoading.value = false;
  585. },
  586. false
  587. );
  588. eventSource.addEventListener(
  589. 'message',
  590. (e) => {
  591. const data = JSON.parse(e.data);
  592. alarmData.value.unshift({
  593. type: data.AlgorithmName,
  594. status: 0,
  595. isRead: false,
  596. timeN: dayjs(+`${data.time}`.padEnd(13, '0')).format(
  597. 'YYYY-MM-DD HH:mm:ss'
  598. ),
  599. ...data,
  600. });
  601. },
  602. false
  603. );
  604. eventSource.addEventListener(
  605. 'error',
  606. () => {
  607. Message.warning({
  608. content: t('previewList.tip2'),
  609. duration: 5 * 1000,
  610. });
  611. },
  612. false
  613. );
  614. };
  615. onMounted(() => {
  616. alarmSSE();
  617. getCropFullInfo({ GUID: props.data.id }).then((res) => {
  618. cropFullInfo.value = res.data;
  619. });
  620. const videoUrl = `${app.ip}/VideoShow/Preview/Full/Main?GUID=${props.data.id}`;
  621. qjLoading.value = true;
  622. workerObj.value.qj = useWorker(videoUrl, `#video-canvas-qj`, null, () => {
  623. qjLoading.value = false;
  624. });
  625. });
  626. onUnmounted(() => {
  627. workerObj.value.qj.close();
  628. workerObj.value.qj = null;
  629. eventSource.close();
  630. });
  631. </script>
  632. <style scoped lang="scss">
  633. .preview-info {
  634. height: 100%;
  635. .general-card {
  636. height: calc(100% - 60px);
  637. :deep(.arco-space) {
  638. height: 100%;
  639. .arco-space-item {
  640. height: 100%;
  641. }
  642. .arco-grid-item {
  643. aspect-ratio: 16 / 9;
  644. }
  645. }
  646. }
  647. .split-screen-main {
  648. display: flex;
  649. flex-flow: column;
  650. gap: 15px;
  651. .alarm-box {
  652. flex: 1;
  653. min-height: 100px;
  654. }
  655. }
  656. .split-screen-box {
  657. height: 0;
  658. flex: 1;
  659. display: flex;
  660. justify-content: center;
  661. align-items: center;
  662. & > div {
  663. height: 100%;
  664. // aspect-ratio: 16/9;
  665. width: 100%;
  666. }
  667. .arco-grid-item {
  668. position: relative;
  669. }
  670. }
  671. &-main {
  672. display: flex;
  673. height: 100%;
  674. .video-box-qj {
  675. height: 100%;
  676. aspect-ratio: 2688 / 6080;
  677. position: relative;
  678. &-tip {
  679. height: 100%;
  680. width: 100%;
  681. display: flex;
  682. justify-content: center;
  683. align-items: center;
  684. color: #fff;
  685. position: absolute;
  686. top: 0;
  687. left: 0;
  688. }
  689. .video-box-wrapper {
  690. height: 100%;
  691. width: 100%;
  692. position: absolute;
  693. top: 0;
  694. left: 0;
  695. display: flex;
  696. justify-content: center;
  697. align-items: center;
  698. background-color: #000;
  699. .arco-spin {
  700. position: absolute;
  701. }
  702. .video-tools {
  703. position: absolute;
  704. right: 10px;
  705. top: 10px;
  706. z-index: 99;
  707. }
  708. }
  709. }
  710. .video-right-content {
  711. flex: 1;
  712. padding: 15px;
  713. display: flex;
  714. flex-flow: column;
  715. gap: 8px;
  716. &-tools {
  717. display: flex;
  718. align-items: center;
  719. justify-content: flex-end;
  720. gap: 4px;
  721. }
  722. }
  723. }
  724. .screen-active {
  725. border: 1px solid red;
  726. }
  727. .alarm-box {
  728. flex: 1;
  729. min-height: 100px;
  730. :deep(.arco-table) {
  731. .arco-table-scroll-y > .arco-scrollbar {
  732. height: 100%;
  733. }
  734. }
  735. }
  736. }
  737. .video-box {
  738. width: 100%;
  739. height: 100%;
  740. position: absolute;
  741. top: 0;
  742. left: 0;
  743. display: flex;
  744. justify-content: center;
  745. align-items: center;
  746. background-color: #000;
  747. overflow: hidden;
  748. box-sizing: content-box;
  749. cursor: pointer;
  750. .arco-spin {
  751. position: absolute;
  752. }
  753. &-tip {
  754. height: 100%;
  755. width: 100%;
  756. display: flex;
  757. justify-content: center;
  758. align-items: center;
  759. color: #fff;
  760. position: absolute;
  761. top: 0;
  762. left: 0;
  763. }
  764. .video-tools {
  765. position: absolute;
  766. right: 10px;
  767. top: 10px;
  768. z-index: 99;
  769. display: flex;
  770. gap: 8px;
  771. }
  772. }
  773. </style>