preview-info.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783
  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 />
  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 Speech from '../components/speech.vue';
  330. import AlarmAll from '../components/alarm-all.vue';
  331. import PubModal from '../components/pub-modal.vue';
  332. // import useWheel from '@/assets/js/useWheel';
  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. switch (tool.type) {
  396. case 'lx':
  397. state.value = !state.value;
  398. tool.icon = state.value ? 'fad:armrecording' : 'bx:video-recording';
  399. tool.color = state.value ? 'red' : '#fff';
  400. tool.iconClass = state.value ? 'fade-open' : '';
  401. workerObj.value.qj.WebSocketWork.postMessage({
  402. type: 'lx',
  403. lx: state.value,
  404. });
  405. break;
  406. case 'jt':
  407. workerObj.value.qj.worker.postMessage({ type: 'jt' });
  408. break;
  409. default:
  410. break;
  411. }
  412. };
  413. const gridCount = computed<number>(() => {
  414. let result = 4;
  415. switch (radioActive.value) {
  416. case 2:
  417. result = 2;
  418. break;
  419. case 4:
  420. result = 2;
  421. break;
  422. case 9:
  423. result = 3;
  424. break;
  425. // case 16:
  426. // result = 4;
  427. // break;
  428. default:
  429. result = 1;
  430. break;
  431. }
  432. return result;
  433. });
  434. const activeObj = ref<
  435. {
  436. loading: boolean;
  437. isSignal: boolean;
  438. [key: string]: any;
  439. }[]
  440. >([]);
  441. const alarmColumns = [
  442. {
  443. title: t('previewList.bjlx'),
  444. dataIndex: 'type',
  445. },
  446. {
  447. title: t('previewList.status'),
  448. dataIndex: 'status',
  449. width: 150,
  450. },
  451. ];
  452. const alarmLoading = ref(false);
  453. const alarmData = ref<{ [key: string]: number | string | boolean }[]>([
  454. // {
  455. // type: '行人闯入',
  456. // status: 0,
  457. // isRead: false,
  458. // },
  459. ]);
  460. const handleClose = async (v: number): Promise<void> => {
  461. if (activeObj?.value[v]?.workerObj) {
  462. return new Promise((resolve) => {
  463. activeObj.value[v].isReset = false;
  464. activeObj.value[v].loading = false;
  465. activeObj.value[v].workerObj.close();
  466. setTimeout(() => {
  467. activeObj.value[v].isSignal = false;
  468. activeObj.value[v].workerObj = null;
  469. resolve();
  470. }, 50);
  471. });
  472. }
  473. return undefined;
  474. };
  475. const handleRadioChange = (
  476. v: string | number | Record<string, any> | undefined
  477. ) => {
  478. radioActive.value = typeof v === 'number' ? v : 2;
  479. if (typeof v === 'number') {
  480. screenActive.value = 0;
  481. // eslint-disable-next-line no-plusplus
  482. for (let i = 0; i < v; i++) {
  483. handleClose(i);
  484. }
  485. }
  486. };
  487. const handleScreen = (i: number) => {
  488. screenActive.value = i;
  489. };
  490. const handleMouseEnter = (v: number) => {
  491. activeObj.value[v].isCloseBtn = true;
  492. };
  493. const handleMouseLeave = (v: number) => {
  494. activeObj.value[v].isCloseBtn = false;
  495. };
  496. let parent: any = null;
  497. const handleDoubleClick = (i: number) => {
  498. const el = document.querySelectorAll('.video-box')[i];
  499. const fullscreenDom: HTMLElement | null = document.querySelector(
  500. '#video-box-fullscreen>div'
  501. );
  502. if (fullscreenDom) {
  503. parent?.appendChild(fullscreenDom);
  504. const box = document.querySelector('#video-box-fullscreen');
  505. if (box) {
  506. document.body.removeChild(box);
  507. }
  508. } else {
  509. parent = el?.parentElement;
  510. const div: HTMLElement = document.createElement('div');
  511. div.id = 'video-box-fullscreen';
  512. div.style.position = 'fixed';
  513. div.style.top = '0';
  514. div.style.left = '0';
  515. div.style.width = '100%';
  516. div.style.height = '100%';
  517. div.style.zIndex = '999';
  518. div.appendChild(el);
  519. document.body.appendChild(div);
  520. }
  521. // toggle();
  522. };
  523. const intercomDisabled = ref(false);
  524. const intercomClick = () => {
  525. // (app.ip as string).split('//')[1]
  526. IntercomFn(intercomDisabled, '39.107.83.190');
  527. };
  528. const speechClick = () => {};
  529. watch(
  530. radioActive,
  531. (newV) => {
  532. activeObj.value = [];
  533. // eslint-disable-next-line no-plusplus
  534. for (let i = 0; i < newV; i++) {
  535. handleClose(i);
  536. activeObj.value.push({
  537. loading: false,
  538. isSignal: false,
  539. workerObj: null,
  540. isCloseBtn: false,
  541. isReset: false,
  542. });
  543. }
  544. },
  545. { immediate: true }
  546. );
  547. watch(
  548. () => app.partObj,
  549. (newV: any) => {
  550. const partVideoUrl = `${app.ip}/VideoShow/Preview/Part/Main?GUID=${props.data.id}&X=${newV.PartCenterX}&Y=${newV.PartCenterY}`;
  551. handleClose(screenActive.value).then(() => {
  552. const ao = activeObj.value[screenActive.value];
  553. ao.loading = true;
  554. ao.isReset = true;
  555. nextTick(() => {
  556. ao.workerObj = useWorker(
  557. partVideoUrl,
  558. `#video-canvas-part${screenActive.value}`,
  559. null,
  560. () => {
  561. ao.loading = false;
  562. ao.isSignal = false;
  563. // useWheel(document.querySelectorAll(`#video-canvas-part`), {
  564. // UUID: screenActive.value,
  565. // });
  566. }
  567. );
  568. });
  569. });
  570. },
  571. { deep: true }
  572. );
  573. let eventSource: EventSource;
  574. const alarmSSE = () => {
  575. if (!props.data.id) return;
  576. alarmLoading.value = true;
  577. eventSource = new EventSource(`${app.ip}/rpc/AiSSE?Topic=${props.data.id}`);
  578. eventSource.addEventListener(
  579. 'open',
  580. () => {
  581. alarmData.value = [];
  582. alarmLoading.value = false;
  583. },
  584. false
  585. );
  586. eventSource.addEventListener(
  587. 'message',
  588. (e) => {
  589. const data = JSON.parse(e.data);
  590. alarmData.value.unshift({
  591. type: data.AlgorithmName,
  592. status: 0,
  593. isRead: false,
  594. timeN: dayjs(+`${data.time}`.padEnd(13, '0')).format(
  595. 'YYYY-MM-DD HH:mm:ss'
  596. ),
  597. ...data,
  598. });
  599. },
  600. false
  601. );
  602. eventSource.addEventListener(
  603. 'error',
  604. () => {
  605. Message.warning({
  606. content: t('previewList.tip2'),
  607. duration: 5 * 1000,
  608. });
  609. },
  610. false
  611. );
  612. };
  613. onMounted(() => {
  614. alarmSSE();
  615. getCropFullInfo({ GUID: props.data.id }).then((res) => {
  616. cropFullInfo.value = res.data;
  617. });
  618. const videoUrl = `${app.ip}/VideoShow/Preview/Full/Main?GUID=${props.data.id}`;
  619. qjLoading.value = true;
  620. workerObj.value.qj = useWorker(videoUrl, `#video-canvas-qj`, null, () => {
  621. qjLoading.value = false;
  622. });
  623. });
  624. onUnmounted(() => {
  625. workerObj.value.qj.close();
  626. workerObj.value.qj = null;
  627. eventSource.close();
  628. });
  629. </script>
  630. <style scoped lang="scss">
  631. .preview-info {
  632. height: 100%;
  633. .general-card {
  634. height: calc(100% - 60px);
  635. :deep(.arco-space) {
  636. height: 100%;
  637. .arco-space-item {
  638. height: 100%;
  639. }
  640. .arco-grid-item {
  641. aspect-ratio: 16 / 9;
  642. }
  643. }
  644. }
  645. .split-screen-main {
  646. display: flex;
  647. flex-flow: column;
  648. gap: 15px;
  649. .alarm-box {
  650. flex: 1;
  651. min-height: 100px;
  652. }
  653. }
  654. .split-screen-box {
  655. height: 0;
  656. flex: 1;
  657. display: flex;
  658. justify-content: center;
  659. align-items: center;
  660. & > div {
  661. height: 100%;
  662. // aspect-ratio: 16/9;
  663. width: 100%;
  664. }
  665. .arco-grid-item {
  666. position: relative;
  667. }
  668. }
  669. &-main {
  670. display: flex;
  671. height: 100%;
  672. .video-box-qj {
  673. height: 100%;
  674. aspect-ratio: 2688 / 6080;
  675. position: relative;
  676. &-tip {
  677. height: 100%;
  678. width: 100%;
  679. display: flex;
  680. justify-content: center;
  681. align-items: center;
  682. color: #fff;
  683. position: absolute;
  684. top: 0;
  685. left: 0;
  686. }
  687. .video-box-wrapper {
  688. height: 100%;
  689. width: 100%;
  690. position: absolute;
  691. top: 0;
  692. left: 0;
  693. display: flex;
  694. justify-content: center;
  695. align-items: center;
  696. background-color: #000;
  697. .arco-spin {
  698. position: absolute;
  699. }
  700. .video-tools {
  701. position: absolute;
  702. right: 10px;
  703. top: 10px;
  704. z-index: 99;
  705. }
  706. }
  707. }
  708. .video-right-content {
  709. flex: 1;
  710. padding: 15px;
  711. display: flex;
  712. flex-flow: column;
  713. gap: 8px;
  714. &-tools {
  715. display: flex;
  716. align-items: center;
  717. justify-content: flex-end;
  718. gap: 4px;
  719. }
  720. }
  721. }
  722. .screen-active {
  723. border: 1px solid red;
  724. }
  725. .alarm-box {
  726. flex: 1;
  727. min-height: 100px;
  728. :deep(.arco-table) {
  729. .arco-table-scroll-y > .arco-scrollbar {
  730. height: 100%;
  731. }
  732. }
  733. }
  734. }
  735. .video-box {
  736. width: 100%;
  737. height: 100%;
  738. position: absolute;
  739. top: 0;
  740. left: 0;
  741. display: flex;
  742. justify-content: center;
  743. align-items: center;
  744. background-color: #000;
  745. overflow: hidden;
  746. box-sizing: content-box;
  747. cursor: pointer;
  748. .arco-spin {
  749. position: absolute;
  750. }
  751. &-tip {
  752. height: 100%;
  753. width: 100%;
  754. display: flex;
  755. justify-content: center;
  756. align-items: center;
  757. color: #fff;
  758. position: absolute;
  759. top: 0;
  760. left: 0;
  761. }
  762. .video-tools {
  763. position: absolute;
  764. right: 10px;
  765. top: 10px;
  766. z-index: 99;
  767. display: flex;
  768. gap: 8px;
  769. }
  770. }
  771. </style>