|
|
@@ -0,0 +1,411 @@
|
|
|
+<template>
|
|
|
+ <div class="mask-bg">
|
|
|
+ <div class="abnormal-wrapper">
|
|
|
+ <div class="title-pane">
|
|
|
+ <div class="title"><span>异常告警</span></div>
|
|
|
+ <img src="@/assets/images/video-grid/title-decoration.png" alt="" />
|
|
|
+ <el-icon class="close-icon" @click="handleClose"><Close /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="content-container">
|
|
|
+ <div class="list-pane">
|
|
|
+ <el-scrollbar class="list-scroll">
|
|
|
+ <div
|
|
|
+ v-for="(it, idx) in items"
|
|
|
+ :key="it.id"
|
|
|
+ class="list-item"
|
|
|
+ :class="{ active: idx === selectedIndex }"
|
|
|
+ @click="onSelect(idx)"
|
|
|
+ >
|
|
|
+ <div class="thumb">
|
|
|
+ <img :src="it.pictures[0]" alt="" />
|
|
|
+ </div>
|
|
|
+ <div class="meta">
|
|
|
+ <div class="row">
|
|
|
+ <span class="label">类型:</span>
|
|
|
+ <span class="value ellipsis" :title="it.title">{{ it.title }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="row">
|
|
|
+ <span class="label">地点:</span>
|
|
|
+ <span class="value ellipsis" :title="it.workspaceName">{{ it.workspaceName }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="row">
|
|
|
+ <span class="label">时间:</span>
|
|
|
+ <span class="value ellipsis" :title="it.createdAt">{{ it.createdAt }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="!items.length" class="empty-tip">暂无数据</div>
|
|
|
+ </el-scrollbar>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="content-pane">
|
|
|
+ <div class="tabs">
|
|
|
+ <div class="tab" :class="{ on: activeTab === 'image' }" @click="activeTab = 'image'">异常画面</div>
|
|
|
+ <div
|
|
|
+ v-if="current?.videos.length"
|
|
|
+ class="tab"
|
|
|
+ :class="{ on: activeTab === 'video' }"
|
|
|
+ @click="activeTab = 'video'"
|
|
|
+ >视频回放</div
|
|
|
+ >
|
|
|
+ <div class="tab" :class="{ on: activeTab === 'live' }" @click="activeTab = 'live'">实时画面</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="stage">
|
|
|
+ <div v-show="activeTab === 'image'" class="stage-inner">
|
|
|
+ <ImageViewer v-if="current?.pictures.length" :images="current?.pictures" :initial-index="0" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-show="activeTab === 'video'" class="stage-inner">
|
|
|
+ <div class="media video-stage">
|
|
|
+ <video
|
|
|
+ v-if="current?.videos.length"
|
|
|
+ ref="videoRef"
|
|
|
+ preload="metadata"
|
|
|
+ :src="current?.videos[0]"
|
|
|
+ controls
|
|
|
+ playsinline
|
|
|
+ ></video>
|
|
|
+ <div v-else class="media-placeholder">无视频</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-show="activeTab === 'live'" class="stage-inner">
|
|
|
+ <div class="media">
|
|
|
+ <div class="media-placeholder" id="live-stage">
|
|
|
+ <LiveVideo
|
|
|
+ :url="getWsUrl(currentStreamIp)"
|
|
|
+ :poster="currentImageUrl"
|
|
|
+ @dblclick="isFullScreen ? exitFullscreen() : fullScreen('live-stage', 'single')"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+ import { computed, onMounted, ref, watch } from 'vue';
|
|
|
+ import { ElIcon, ElScrollbar } from 'element-plus';
|
|
|
+ import { Close } from '@element-plus/icons-vue';
|
|
|
+ import { storeToRefs } from 'pinia';
|
|
|
+ import urlJoin from 'url-join';
|
|
|
+ import { userSplitScreenFullScreen } from '@/store/modules/userSplitScreenFullScreen';
|
|
|
+ import ImageViewer from './ImageViewer.vue';
|
|
|
+ import LiveVideo from '@/components/live/LiveVideoFlv.vue';
|
|
|
+ import { QueryTodayIssueListByWorkspaceRes, getTodayQuestionListApi } from '@/apis/splitScreenRetrieval';
|
|
|
+ import { getCameraInfoDetail } from '@/apis/camera/camera';
|
|
|
+
|
|
|
+ const { isFullScreen } = storeToRefs(userSplitScreenFullScreen());
|
|
|
+ const { fullScreen, exitFullscreen } = userSplitScreenFullScreen();
|
|
|
+
|
|
|
+ const props = defineProps<{
|
|
|
+ id: number;
|
|
|
+ }>();
|
|
|
+
|
|
|
+ const emits = defineEmits<{
|
|
|
+ (e: 'close'): void;
|
|
|
+ }>();
|
|
|
+
|
|
|
+ const items = ref<QueryTodayIssueListByWorkspaceRes[]>([]);
|
|
|
+ const selectedIndex = ref<number>(0);
|
|
|
+ const current = computed<QueryTodayIssueListByWorkspaceRes | undefined>(() => items.value[selectedIndex.value]);
|
|
|
+ const currentImageUrl = ref('');
|
|
|
+ const currentStreamIp = ref('');
|
|
|
+
|
|
|
+ const activeTab = ref<'image' | 'video' | 'live'>('image');
|
|
|
+
|
|
|
+ const videoRef = ref<HTMLVideoElement | null>(null);
|
|
|
+
|
|
|
+ watch([activeTab, () => selectedIndex.value], () => {
|
|
|
+ if (videoRef.value) {
|
|
|
+ videoRef.value.pause();
|
|
|
+ videoRef.value.currentTime = Math.min(videoRef.value.currentTime, videoRef.value.duration || 0);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const isHttps = () => {
|
|
|
+ return window.location.protocol.startsWith('https');
|
|
|
+ };
|
|
|
+
|
|
|
+ const getWsUrl = (videoUrl: string) => {
|
|
|
+ if (!videoUrl) return '';
|
|
|
+ const protocol = isHttps() ? 'wss' : 'ws';
|
|
|
+ // 如果是绝对地址
|
|
|
+ if (videoUrl.startsWith('http')) {
|
|
|
+ // 如果是https的话,websocket要用wss
|
|
|
+ return videoUrl.replace('http', protocol);
|
|
|
+ }
|
|
|
+ const u = urlJoin(
|
|
|
+ `${protocol}://`,
|
|
|
+ window.location.host,
|
|
|
+ window.location.pathname === '/' ? '' : window.location.pathname,
|
|
|
+ videoUrl,
|
|
|
+ );
|
|
|
+ return u;
|
|
|
+ };
|
|
|
+
|
|
|
+ const onSelect = (idx: number): void => {
|
|
|
+ selectedIndex.value = idx;
|
|
|
+ activeTab.value = 'image';
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleClose = () => {
|
|
|
+ emits('close');
|
|
|
+ };
|
|
|
+
|
|
|
+ const load = async (): Promise<void> => {
|
|
|
+ items.value = await getTodayQuestionListApi(props.id);
|
|
|
+ selectedIndex.value = 0;
|
|
|
+ activeTab.value = 'image';
|
|
|
+ };
|
|
|
+
|
|
|
+ watch(
|
|
|
+ () => current.value,
|
|
|
+ (newVal) => {
|
|
|
+ getCameraInfoDetail(newVal?.cameraCode || '').then((res) => {
|
|
|
+ currentImageUrl.value = res?.pushStreamDTO?.imageUrl || '';
|
|
|
+ currentStreamIp.value = res?.pushStreamDTO?.videoUrls?.pushstreamIp || '';
|
|
|
+ });
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ watch(
|
|
|
+ () => props.id,
|
|
|
+ () => {
|
|
|
+ load();
|
|
|
+ },
|
|
|
+ { immediate: false },
|
|
|
+ );
|
|
|
+
|
|
|
+ onMounted(() => {
|
|
|
+ load();
|
|
|
+ });
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+ .mask-bg {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background-color: rgba(0, 0, 0, 0.8);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .abnormal-wrapper {
|
|
|
+ width: 78.5%;
|
|
|
+ height: 71.5%;
|
|
|
+ padding: 15px 12px 12px 12px;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ }
|
|
|
+
|
|
|
+ .title-pane {
|
|
|
+ width: 100%;
|
|
|
+ height: 47px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ .title {
|
|
|
+ width: 370px;
|
|
|
+ height: 100%;
|
|
|
+ background: url(@/assets/icons/violation-title-bg.png) no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ span {
|
|
|
+ position: absolute;
|
|
|
+ top: 8px;
|
|
|
+ left: 68px;
|
|
|
+ font-size: 20px;
|
|
|
+ color: #d8f0ff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ img {
|
|
|
+ height: 21px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .close-icon {
|
|
|
+ font-size: 24px;
|
|
|
+ margin-left: auto;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+
|
|
|
+ .close-icon:hover {
|
|
|
+ color: #177dff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .content-container {
|
|
|
+ width: 100%;
|
|
|
+ height: calc(100% - 57px);
|
|
|
+ border: 1px solid;
|
|
|
+ border-image: linear-gradient(to right, #1890ff80, #1890ff00, #1890ff80) 1;
|
|
|
+ padding: 12px;
|
|
|
+ display: flex;
|
|
|
+ }
|
|
|
+
|
|
|
+ .list-pane {
|
|
|
+ width: 25%;
|
|
|
+ height: 100%;
|
|
|
+
|
|
|
+ .list-scroll {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ padding-right: 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .list-item {
|
|
|
+ width: 100%;
|
|
|
+ height: 100px;
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 137px 1fr;
|
|
|
+ align-items: center;
|
|
|
+ column-gap: 12px;
|
|
|
+ padding: 12px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ transition: transform 0.1s ease, background 0.2s ease;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+ .list-item:hover {
|
|
|
+ transform: translateY(-1px);
|
|
|
+ background-image: url('@/assets/images/video-grid/item-hover.png');
|
|
|
+ background-size: 100% 100%;
|
|
|
+ background-position: center;
|
|
|
+ }
|
|
|
+ .list-item.active {
|
|
|
+ background-image: url('@/assets/images/video-grid/item-active.png');
|
|
|
+ background-size: 100% 100%;
|
|
|
+ background-position: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .thumb {
|
|
|
+ width: 137px;
|
|
|
+ height: 76px;
|
|
|
+ background: #000;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ img {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .meta {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ row-gap: 6px;
|
|
|
+ min-width: 0;
|
|
|
+
|
|
|
+ .row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
+ .label {
|
|
|
+ color: #9bb2c9;
|
|
|
+ font-size: 12px;
|
|
|
+ flex: none;
|
|
|
+ }
|
|
|
+ .value {
|
|
|
+ color: #e9f0f7;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .content-pane {
|
|
|
+ width: calc(75% - 20px);
|
|
|
+ height: 100%;
|
|
|
+ margin-left: 20px;
|
|
|
+
|
|
|
+ .tabs {
|
|
|
+ width: fit-content;
|
|
|
+ height: 48px;
|
|
|
+ background: rgba(255, 255, 255, 0.1);
|
|
|
+ border-radius: 6px;
|
|
|
+ padding: 4px;
|
|
|
+ display: flex;
|
|
|
+
|
|
|
+ .tab {
|
|
|
+ width: 177px;
|
|
|
+ height: 40px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 16px;
|
|
|
+ color: rgba(255, 255, 255, 0.8);
|
|
|
+ letter-spacing: 1px;
|
|
|
+ text-shadow: inset 0px 1px 0px #dcdfe6, inset 0px -1px 0px #dcdfe6, inset -1px 0px 0px #dcdfe6;
|
|
|
+ line-height: 40px;
|
|
|
+ cursor: pointer;
|
|
|
+ margin-right: 2px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tab:last-child {
|
|
|
+ margin-right: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tab:hover {
|
|
|
+ background: rgba(255, 255, 255, 0.2);
|
|
|
+ }
|
|
|
+
|
|
|
+ .tab.on {
|
|
|
+ background: rgba(19, 147, 255, 0.3);
|
|
|
+ font-weight: 500;
|
|
|
+ color: #ffffff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .stage {
|
|
|
+ width: 100%;
|
|
|
+ height: calc(100% - 48px - 24px - 14px);
|
|
|
+ margin: 24px 0 14px 0;
|
|
|
+
|
|
|
+ .stage-inner {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .media {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background: #000;
|
|
|
+ color: #cbd5e1;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .video-stage video {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: contain;
|
|
|
+ background: #000;
|
|
|
+ }
|
|
|
+
|
|
|
+ .media-placeholder {
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .ellipsis {
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+</style>
|