313 lines
8.1 KiB
Vue
313 lines
8.1 KiB
Vue
<template>
|
||
<div v-loading="state.loading" class="layout-navbars-breadcrumb-user-news">
|
||
<div class="head-box">
|
||
<div class="head-box-title"><p style="color: red; font-size: 16px; font-weight: 800">入侵警告</p></div>
|
||
<div class="head-box-btn" @click="readAll">全部已读</div>
|
||
</div>
|
||
<div v-loading="state.loading" class="content-box">
|
||
<template v-if="newsList.length > 0">
|
||
<!-- <div v-for="(v, k) in newsList" :key="k" class="content-box-item" @click="onNewsClick(k)"> -->
|
||
<div v-for="(v, k) in newsList" :key="k" class="content-box-item">
|
||
<div class="item-conten">
|
||
<div>{{ v.message }}</div>
|
||
<div class="content-box-msg"></div>
|
||
<!-- <div class="content-box-time">{{ v.time }}</div> -->
|
||
</div>
|
||
<!-- 已读/未读 -->
|
||
<!-- <span v-if="v.read" class="el-tag el-tag--success el-tag--mini read">已读</span> -->
|
||
<button v-if="!v.read" class="el-tag el-tag--danger el-tag--mini read" @click="handleSee(v)">查看画面</button>
|
||
</div>
|
||
</template>
|
||
<el-empty v-else :description="'消息为空'"></el-empty>
|
||
</div>
|
||
<!-- <div v-if="newsList.length > 0" class="foot-box" @click="onGoToGiteeClick">前往gitee</div> -->
|
||
<!-- 添加或修改测试单对话框 -->
|
||
<el-dialog
|
||
v-model="dialog.visible"
|
||
:title="dialog.title"
|
||
width="500px"
|
||
class="monitor-dialog"
|
||
:before-close="handleMonitorDialogClose"
|
||
fullscreen
|
||
destroy-on-close
|
||
>
|
||
<template #header>
|
||
<div class="flex w-full items-center justify-between gap-2 min-w-0 pr-8">
|
||
<span class="truncate">实时监控</span>
|
||
</div>
|
||
</template>
|
||
<div class="monitor-video-wrap">
|
||
<div class="monitor-video-stack">
|
||
<video ref="monitorVideoRef" class="monitor-video" controls preload="metadata" :src="monitorVideoUrl">您的浏览器不支持视频播放</video>
|
||
</div>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts" name="layoutBreadcrumbUserNews">
|
||
import { useNoticeStore } from '@/store/modules/notice';
|
||
import { listNotice } from '@/api/system/notice';
|
||
import { initHls, destroyHls } from '@/utils/video';
|
||
import { onBeforeRouteLeave } from 'vue-router';
|
||
|
||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||
|
||
import { nextTick } from 'vue';
|
||
import { useVideosStore } from '@/store/modules/video';
|
||
const videosStore = useVideosStore();
|
||
|
||
const noticeStore = useNoticeStore();
|
||
const { readAll } = useNoticeStore();
|
||
const monitorVideoRef = ref<HTMLVideoElement>();
|
||
|
||
// 定义变量内容
|
||
const state = reactive({
|
||
loading: false
|
||
});
|
||
const newsList = ref([]) as any;
|
||
|
||
const monitorVideoUrl = ref();
|
||
const dialog = reactive<DialogOption>({
|
||
visible: false,
|
||
title: ''
|
||
});
|
||
|
||
const parseNoticeItem = (notice: any) => {
|
||
const rawMessage = String(notice.message ?? '');
|
||
const [messagePart, videoIdPart] = rawMessage.split('|', 2);
|
||
return {
|
||
...notice,
|
||
rawMessage,
|
||
message: messagePart?.trim() ?? rawMessage,
|
||
videoId: videoIdPart?.trim() ?? ''
|
||
};
|
||
};
|
||
|
||
//查看画面
|
||
const handleSee = async (row: any) => {
|
||
const videoId = row?.videoId;
|
||
console.log('查看画面', row, 'videoId', videoId);
|
||
if (!videoId) {
|
||
proxy?.$modal.msgError('视频ID不存在');
|
||
return;
|
||
}
|
||
await videosStore.getVideos(videoId);
|
||
monitorVideoUrl.value = videosStore.hisUrl;
|
||
dialog.visible = true;
|
||
await nextTick();
|
||
initHls(monitorVideoUrl, monitorVideoRef);
|
||
};
|
||
|
||
const handleMonitorDialogClose = (done: () => void) => {
|
||
// 卸载 Hls 与清理 video
|
||
destroyHls(monitorVideoRef);
|
||
done();
|
||
};
|
||
/**
|
||
* 过滤消息(只保留最新的5条)
|
||
* @param messages 消息列表
|
||
* @returns 过滤后的消息列表
|
||
*/
|
||
const filterMessages = (messages: any[]) => {
|
||
// 如果消息超过5条,则只保留最新的5条
|
||
if (messages.length > 5) {
|
||
return messages.slice(-5); // 只取最后5条最新消息
|
||
}
|
||
|
||
return messages;
|
||
};
|
||
|
||
/**
|
||
* 初始化数据
|
||
* @returns
|
||
*/
|
||
const getTableData = async () => {
|
||
state.loading = true;
|
||
const allNotices = [...noticeStore.state.notices]; // 复制数组避免直接修改原数据
|
||
|
||
// 过滤消息并解析 message/videoId
|
||
const filteredNotices = filterMessages(allNotices).map(parseNoticeItem);
|
||
newsList.value = filteredNotices;
|
||
state.loading = false;
|
||
};
|
||
|
||
// 监听通知变化,自动过滤
|
||
watch(
|
||
() => noticeStore.state.notices,
|
||
(newNotices) => {
|
||
const filteredNotices = filterMessages([...newNotices]).map(parseNoticeItem);
|
||
newsList.value = filteredNotices;
|
||
console.log('监听消息队列', newsList.value);
|
||
},
|
||
{ deep: true }
|
||
);
|
||
|
||
//点击消息,写入已读
|
||
const onNewsClick = (item: any) => {
|
||
if (newsList.value[item]) {
|
||
newsList.value[item].read = true;
|
||
//并且写入pinia
|
||
noticeStore.state.notices = noticeStore.state.notices.map((notice: any, index: number) => {
|
||
if (index === findOriginalIndex(item)) {
|
||
return { ...notice, read: true };
|
||
}
|
||
return notice;
|
||
});
|
||
}
|
||
};
|
||
|
||
// 辅助函数:找到原始索引
|
||
const findOriginalIndex = (filteredIndex: number) => {
|
||
const filteredNotices = filterMessages([...noticeStore.state.notices]).map(parseNoticeItem);
|
||
const currentMessage = filteredNotices[filteredIndex];
|
||
|
||
// 在原始数组中查找对应消息的索引
|
||
return noticeStore.state.notices.findIndex(
|
||
(notice: any) => String(notice.message) === currentMessage.rawMessage && notice.time === currentMessage.time
|
||
);
|
||
};
|
||
|
||
// 前往通知中心点击
|
||
const onGoToGiteeClick = () => {
|
||
window.open('https://gitee.com/dromara/RuoYi-Vue-Plus/tree/5.X/');
|
||
};
|
||
|
||
onMounted(() => {
|
||
nextTick(() => {
|
||
getTableData();
|
||
console.log('消息列表:', newsList.value);
|
||
});
|
||
});
|
||
|
||
onBeforeRouteLeave(() => {
|
||
dialog.visible = false;
|
||
monitorVideoUrl.value = '';
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* 全屏监控弹窗:占满视口、禁止外层滚动,视频在可视区内尽可能放大 */
|
||
:deep(.monitor-dialog.el-dialog) {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100vh;
|
||
max-height: 100vh;
|
||
margin: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
:deep(.monitor-dialog .el-dialog__header) {
|
||
flex-shrink: 0;
|
||
margin-right: 0;
|
||
}
|
||
|
||
:deep(.monitor-dialog .el-dialog__body) {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.monitor-video-wrap {
|
||
flex: 1;
|
||
min-height: 0;
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: stretch;
|
||
justify-content: center;
|
||
background: #000;
|
||
}
|
||
|
||
.monitor-video-stack {
|
||
position: relative;
|
||
flex: 1;
|
||
min-height: 0;
|
||
width: 100%;
|
||
}
|
||
|
||
.monitor-video {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
object-fit: contain; //cover裁剪
|
||
background: #000;
|
||
}
|
||
|
||
.monitor-draw-overlay {
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 2;
|
||
cursor: crosshair;
|
||
background: transparent;
|
||
}
|
||
|
||
.layout-navbars-breadcrumb-user-news {
|
||
.head-box {
|
||
display: flex;
|
||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||
box-sizing: border-box;
|
||
color: var(--el-text-color-primary);
|
||
justify-content: space-between;
|
||
height: 35px;
|
||
align-items: center;
|
||
.head-box-btn {
|
||
color: var(--el-color-primary);
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
opacity: 0.8;
|
||
&:hover {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
}
|
||
.content-box {
|
||
height: 300px;
|
||
overflow: auto;
|
||
font-size: 13px;
|
||
.content-box-item {
|
||
padding-top: 12px;
|
||
display: flex;
|
||
&:last-of-type {
|
||
padding-bottom: 12px;
|
||
}
|
||
.content-box-msg {
|
||
color: var(--el-text-color-secondary);
|
||
margin-top: 5px;
|
||
margin-bottom: 5px;
|
||
}
|
||
.content-box-time {
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
.item-conten {
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
}
|
||
.foot-box {
|
||
height: 35px;
|
||
color: var(--el-color-primary);
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
opacity: 0.8;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-top: 1px solid var(--el-border-color-lighter);
|
||
&:hover {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
:deep(.el-empty__description p) {
|
||
font-size: 13px;
|
||
}
|
||
}
|
||
</style>
|