Compare commits

...

28 Commits

Author SHA1 Message Date
Ironsp
5144bc9a7d feat 优化标签 2026-06-24 16:05:42 +08:00
wangzzpeng
2d2f46b6f1 改为本地ip 2026-06-24 11:55:51 +08:00
Ironsp
e4d014cf8c feat 优化标签 2026-06-23 16:10:05 +08:00
Ironsp
80a360fa0e feat 优化代码 2026-06-23 13:54:58 +08:00
Ironsp
7d2c70a19f feat 优化代码 2026-06-23 11:30:07 +08:00
Ironsp
a248e3b234 feat 增加双电池电量 2026-06-23 11:18:31 +08:00
Ironsp
70ab453d94 feat 增加双电池电量 2026-06-23 11:10:16 +08:00
Ironsp
36f0cedc62 feat 直播框区域名称动态变化 2026-06-23 09:11:06 +08:00
Ironsp
df52a3e343 feat 优化直播对话框 2026-06-23 08:59:09 +08:00
Ironsp
8d27df05d6 feat 优化消息弹窗查看画面 2026-06-23 08:53:30 +08:00
Ironsp
e47b8d354e feat 优化播放视频中videoname 2026-06-23 08:44:43 +08:00
Ironsp
ced12d5805 feat 优化处理后的副作用 2026-06-23 08:32:05 +08:00
Ironsp
dd54ce489e feat 弹窗优化, 更换标的 2026-06-23 08:28:35 +08:00
Ironsp
00efe08873 feat 引入优化 2026-06-22 17:31:58 +08:00
Ironsp
c3420640a7 feat 弹窗功能修改 2026-06-18 15:32:26 +08:00
Ironsp
4c882ac309 feat 弹窗功能修改, 去掉sse消息弹窗, 当增加消息的时候, 弹出警告弹窗 2026-06-18 15:24:33 +08:00
Ironsp
334787f2cf feat 引入 2026-06-18 09:59:59 +08:00
Ironsp
de48277991 feat 数据源问题 2026-06-15 14:26:25 +08:00
Ironsp
753f84ec26 feat 批量处理选择 2026-06-15 11:06:30 +08:00
Ironsp
3d58c73a88 feat 直播进度条 2026-06-12 17:45:37 +08:00
Ironsp
457d0c3888 feat 直播后台中中断问题优化 2026-06-12 15:28:41 +08:00
Ironsp
a8ea3746f4 feat 报警图片 2026-06-12 10:12:39 +08:00
Ironsp
55f0cd2192 feat 优化报警消息与列表联通, 报警消息与数字同步, 报警数字与列表同步刷新, 报警列表与消息列表同步刷新
Co-authored-by: Copilot <copilot@github.com>
2026-06-11 10:32:13 +08:00
Ironsp
2492b8ff5a feat 优化报警数量联动功能
Co-authored-by: Copilot <copilot@github.com>
2026-06-11 10:01:49 +08:00
Ironsp
13b00ac8de feat 优化报警数量联动功能
Co-authored-by: Copilot <copilot@github.com>
2026-06-11 09:54:03 +08:00
Ironsp
89d0dd2a1d feat 优化报警消息暂存
Co-authored-by: Copilot <copilot@github.com>
2026-06-11 09:31:18 +08:00
Ironsp
47b272441c feat 优化报警消息暂存 2026-06-11 09:29:11 +08:00
Ironsp
159d53080a feat 优化报警消息暂存 2026-06-11 09:28:46 +08:00
12 changed files with 244 additions and 61 deletions

View File

@@ -24,3 +24,9 @@ export interface DemoQuery extends PageQuery {
beginTime?: string; beginTime?: string;
endTime?: string; endTime?: string;
} }
/** 告警列表响应结构 */
export interface WarningListResult {
rows: DemoVO[];
total: number;
}

View File

@@ -37,9 +37,16 @@
<!-- 消息 --> <!-- 消息 -->
<el-tooltip content="警报" effect="dark" placement="bottom"> <el-tooltip content="警报" effect="dark" placement="bottom">
<div style="display: flex; align-items: center" class="mr-[30px]"> <div style="display: flex; align-items: center" class="mr-[30px]">
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="400" :persistent="false"> <el-popover
placement="bottom"
transition="el-zoom-in-top"
:width="400"
:persistent="false"
trigger="click"
v-model:visible="popoverVisible"
>
<template #reference> <template #reference>
<el-badge :value="warningListStore.warningListTotal > 0 ? warningListStore.warningListTotal : ''" :max="999"> <el-badge :value="infoData" :max="999">
<div class="right-menu-item hover-effect" @click="goToWarningList"> <div class="right-menu-item hover-effect" @click="goToWarningList">
<el-icon><BellFilled /></el-icon> <el-icon><BellFilled /></el-icon>
</div> </div>
@@ -123,6 +130,9 @@ const noticeStore = storeToRefs(useNoticeStore());
const newNotice = ref(<number>0); const newNotice = ref(<number>0);
const { warningList, warningListTotal } = storeToRefs(warningListStore); const { warningList, warningListTotal } = storeToRefs(warningListStore);
const infoData = ref(warningListStore.warningListTotal ?? 0);
const popoverVisible = ref(false);
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const userId = ref(userStore.userId); const userId = ref(userStore.userId);
@@ -213,8 +223,14 @@ const handleCommand = (command: string) => {
//用深度监听 消息 //用深度监听 消息
watch( watch(
() => noticeStore.state.value.notices, () => noticeStore.state.value.notices,
(newVal) => { async (newVal) => {
newNotice.value = newVal.filter((item: any) => !item.read).length; newNotice.value = newVal.filter((item: any) => !item.read).length;
//监听到消息变化,刷新告警列表
await warningListStore.getWarningList({
status: 0,
pageNum: 1,
pageSize: 300
});
}, },
{ deep: true } { deep: true }
); );
@@ -239,8 +255,38 @@ onMounted(() => {
}); });
}); });
onMounted(() => { onMounted(() => {
warningListStore.getWarningList(); warningListStore.getWarningList({
status: 0,
pageNum: 1,
pageSize: 300
}); });
});
//监听数据变化
watch(
() => warningListStore.warningListTotal,
(newVal) => {
infoData.value = newVal;
},
{ deep: true }
);
// 告警数据变化时自动弹出弹窗
// 当 isNotice 变为 true 时弹出弹窗,并重置为 false
// let noticeFirstLoad = true; //解决初始化时弹出弹窗问题
watch(
() => noticeStore.state.value.isNotice,
(newVal) => {
// if (noticeFirstLoad) {
// noticeFirstLoad = false;
// return;
// }
if (newVal) {
noticeStore.state.value.isNotice = false;
popoverVisible.value = true;
}
},
{ deep: true }
);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -95,10 +95,13 @@ const isLastView = () => {
}; };
const filterAffixTags = (routes: RouteRecordRaw[], basePath = '') => { const filterAffixTags = (routes: RouteRecordRaw[], basePath = '') => {
let tags: RouteLocationNormalized[] = []; let tags: RouteLocationNormalized[] = [];
const seen = new Set<string>();
routes.forEach((route) => { routes.forEach((route) => {
if (route.meta && route.meta.affix) { if (route.meta && route.meta.affix) {
const tagPath = getNormalPath(basePath + '/' + route.path); const tagPath = getNormalPath(basePath + '/' + route.path);
if (!seen.has(tagPath)) {
seen.add(tagPath);
tags.push({ tags.push({
hash: '', hash: '',
matched: [], matched: [],
@@ -111,6 +114,7 @@ const filterAffixTags = (routes: RouteRecordRaw[], basePath = '') => {
meta: { ...route.meta } meta: { ...route.meta }
}); });
} }
}
if (route.children) { if (route.children) {
const tempTags = filterAffixTags(route.children, route.path); const tempTags = filterAffixTags(route.children, route.path);
if (tempTags.length >= 1) { if (tempTags.length >= 1) {
@@ -124,7 +128,6 @@ const initTags = () => {
const res = filterAffixTags(routes.value); const res = filterAffixTags(routes.value);
affixTags.value = res; affixTags.value = res;
for (const tag of res) { for (const tag of res) {
// Must have tag name
if (tag.name) { if (tag.name) {
useTagsViewStore().addVisitedView(tag); useTagsViewStore().addVisitedView(tag);
} }
@@ -136,6 +139,10 @@ const addTags = () => {
route.meta.title = route.query.title as string; route.meta.title = route.query.title as string;
} }
if (name) { if (name) {
// 如果 visitedViews 中已有同 title 的固定标签affix不再添加普通标签
if (visitedViews.value.some((v) => v.meta?.affix && v.title === route.meta?.title)) return;
// 常规去重:已有同 path 或 fullPath 的标签则跳过
if (visitedViews.value.some((v) => v.path === route.path || v.fullPath === route.fullPath)) return;
useTagsViewStore().addView(route as any); useTagsViewStore().addView(route as any);
} }
}; };

View File

@@ -34,7 +34,7 @@
> >
<template #header> <template #header>
<div class="flex w-full items-center justify-between gap-2 min-w-0 pr-8"> <div class="flex w-full items-center justify-between gap-2 min-w-0 pr-8">
<span class="truncate">实时监控</span> <span class="truncate">实时监控-{{ monitoringArea }}监控</span>
</div> </div>
</template> </template>
<div class="monitor-video-wrap"> <div class="monitor-video-wrap">
@@ -52,25 +52,39 @@ import { listNotice } from '@/api/system/notice';
import { initHls, destroyHls } from '@/utils/video'; import { initHls, destroyHls } from '@/utils/video';
import { onBeforeRouteLeave } from 'vue-router'; import { onBeforeRouteLeave } from 'vue-router';
import { useWarningListStore } from '@/store/modules/warnningList'; import { useWarningListStore } from '@/store/modules/warnningList';
import type { WarningListResult } from '@/api/WarningList/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { useVideosStore } from '@/store/modules/video'; import { useVideosStore } from '@/store/modules/video';
const videosStore = useVideosStore(); const videosStore = useVideosStore();
const monitoringArea = ref<string>('');
const warningListStore = useWarningListStore(); const warningListStore = useWarningListStore();
const noticeStore = useNoticeStore(); const noticeStore = useNoticeStore();
const { getWarningList, handleAction } = warningListStore;
const { warningListTotal, warningList } = storeToRefs(warningListStore);
const props = defineProps<{ // const props = defineProps<{
warningList: any; // warningList?: WarningListResult;
}>(); // }>();
const { warningList } = toRefs(props);
const warningListRows = computed(() => warningList.value?.rows || []); const warningListRows = computed(() => warningList.value?.rows || []);
// console.log('666666666666666666_warningListRows', warningListRows.value);
const { readAll } = useNoticeStore(); const { readAll } = useNoticeStore();
const monitorVideoRef = ref<HTMLVideoElement>(); const monitorVideoRef = ref<HTMLVideoElement>();
const getList = async () => {
try {
await getWarningList({
status: 0,
pageNum: 1,
pageSize: 300
});
} catch (error) {
console.error('获取消息列表失败', error);
}
};
// 定义变量内容 // 定义变量内容
const state = reactive({ const state = reactive({
loading: false loading: false
@@ -96,20 +110,23 @@ const parseNoticeItem = (notice: any) => {
//查看画面 //查看画面
const handleSee = async (row: any) => { const handleSee = async (row: any) => {
const videoId = row?.videoId; const videoId = row?.deviceId;
console.log('查看画面', row, 'videoId', videoId); console.log('查看画面', row, 'deviceId', videoId);
if (!videoId) { if (!videoId) {
proxy?.$modal.msgError('视频ID不存在'); proxy?.$modal.msgError('视频ID不存在');
return; return;
} }
await videosStore.getVideos(videoId); await videosStore.getVideos(videoId);
monitorVideoUrl.value = videosStore.hisUrl; monitorVideoUrl.value = videosStore.hisUrl;
monitoringArea.value = row.monitoringArea;
dialog.visible = true; dialog.visible = true;
await nextTick(); await nextTick();
initHls(monitorVideoUrl, monitorVideoRef); initHls(monitorVideoUrl, monitorVideoRef);
await handleAction(row.id);
await getList();
}; };
const handleMonitorDialogClose = (done: () => void) => { const handleMonitorDialogClose = async (done: () => void) => {
// 卸载 Hls 与清理 video // 卸载 Hls 与清理 video
destroyHls(monitorVideoRef); destroyHls(monitorVideoRef);
done(); done();
@@ -194,9 +211,11 @@ onBeforeRouteLeave(() => {
dialog.visible = false; dialog.visible = false;
monitorVideoUrl.value = ''; monitorVideoUrl.value = '';
}); });
// onMounted(() => {
// warningListStore.getWarningList(); onMounted(() => {
// }); getList();
console.log('111111111111111111111_warningList.value', warningList.value);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -10,7 +10,8 @@ interface NoticeItem {
export const useNoticeStore = defineStore('notice', () => { export const useNoticeStore = defineStore('notice', () => {
const state = reactive({ const state = reactive({
notices: [] as NoticeItem[] notices: [] as NoticeItem[],
isNotice: false as boolean
}); });
const addNotice = (notice: NoticeItem) => { const addNotice = (notice: NoticeItem) => {

View File

@@ -37,7 +37,12 @@ export const useTagsViewStore = defineStore('tagsView', () => {
}); });
}; };
const addVisitedView = (view: RouteLocationNormalized): void => { const addVisitedView = (view: RouteLocationNormalized): void => {
if (visitedViews.value.some((v: RouteLocationNormalized) => v.path === view.path)) return; // 同时通过 path、fullPath、name 去重
if (visitedViews.value.some((v: RouteLocationNormalized) =>
v.path === view.path ||
v.fullPath === view.fullPath ||
(v.name && view.name && v.name === view.name)
)) return;
visitedViews.value.push( visitedViews.value.push(
Object.assign({}, view, { Object.assign({}, view, {
title: view.meta?.title || 'no-name' title: view.meta?.title || 'no-name'

View File

@@ -1,36 +1,45 @@
/** 此数据源跟表列不一致,(后端接口重复使用因逻辑复杂, 经商定不动后端接口, 设置此数据源) 适用于消息 */
/** 抽取公用代码更加复杂模糊-暂存数据源 */
import { listDemo, updateDemo } from '@/api/WarningList/index'; import { listDemo, updateDemo } from '@/api/WarningList/index';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { any } from 'vue-types';
export const useWarningListStore = defineStore('warningListStore', () => { export const useWarningListStore = defineStore('warningListStore', () => {
// --- State (状态) --- // --- State (状态) ---
// 相当于 Vuex 中的 state用 ref 或 reactive 定义 const warningList = ref();
const warningList = ref([]);
const warningListTotal = ref(0); const warningListTotal = ref(0);
// --- Getters (计算属性) --- // --- Getters (计算属性) ---
// 相当于 Vuex 中的 getters直接用 computed 定义
// --- Actions (动作) --- // --- Actions (动作) ---
// 相当于 Vuex 中的 actions/mutations直接写同步或异步函数 async function getWarningList(query?: any) {
async function getWarningList(deviceId?: string | number) {
try { try {
const response = await listDemo({ const response = await listDemo({
deviceId, ...query
pageNum: 1, // deviceId,
pageSize: 10 // status: 0,
// pageNum: 1,
// pageSize: 10
}); });
// console.log('6666666666666666666', response); warningList.value = response.data;
warningList.value = response.data || [];
warningListTotal.value = response.data.total || 0; warningListTotal.value = response.data.total || 0;
} catch (error) { } catch (error) {
console.error('获取消息列表失败', error); console.error('获取消息列表失败', error);
} }
} }
async function handleAction(ids: string | number | Array<string | number>) {
try {
await updateDemo(ids);
} catch (error) {
console.error('处理告警失败', error);
}
}
// 核心点:必须把需要暴露出去的变量和方法 return 出去! // 核心点:必须把需要暴露出去的变量和方法 return 出去!
return { return {
warningList, warningList,
warningListTotal, warningListTotal,
getWarningList getWarningList,
handleAction
}; };
}); });

View File

@@ -1,9 +1,12 @@
import { getToken } from '@/utils/auth'; import { getToken } from '@/utils/auth';
import { ElNotification } from 'element-plus'; import { ElNotification } from 'element-plus';
import { useNoticeStore } from '@/store/modules/notice'; import { useNoticeStore } from '@/store/modules/notice';
import { toRefs } from 'vue';
// 初始化 // 初始化
export const initSSE = (url: any) => { export const initSSE = (url: any) => {
const noticeStore = useNoticeStore();
const { isNotice, notices } = toRefs(noticeStore.state);
if (import.meta.env.VITE_APP_SSE === 'false') { if (import.meta.env.VITE_APP_SSE === 'false') {
return; return;
} }
@@ -40,12 +43,13 @@ export const initSSE = (url: any) => {
time: new Date().toLocaleString() time: new Date().toLocaleString()
}); });
const displayMessage = data.value.split('|')[0].trim(); const displayMessage = data.value.split('|')[0].trim();
ElNotification({ // ElNotification({
title: '消息', // title: '消息',
message: displayMessage, // message: displayMessage,
type: 'success', // type: 'success',
duration: 3000 // duration: 3000
}); // });
noticeStore.state.isNotice = true;
data.value = null; data.value = null;
}); });
}; };

View File

@@ -39,19 +39,18 @@
<template #header> <template #header>
<el-row :gutter="10" class="flex justify-end mb8"> <el-row :gutter="10" class="flex justify-end mb8">
<el-col :span="1.5"> <el-col :span="1.5">
<el-button v-hasPermi="['demo:demo:edit']" type="success" plain icon="Edit" :disabled="multiple" @click="handleBatchProcess" <el-button type="success" plain icon="Edit" :disabled="multiple" @click="handleBatchProcess">批量处理</el-button>
>批量处理</el-button
>
</el-col> </el-col>
</el-row> </el-row>
</template> </template>
<el-table v-loading="loading" border :data="demoList" @selection-change="handleSelectionChange"> <el-table v-loading="loading" border :data="demoList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" /> <el-table-column type="selection" width="55" align="center" :selectable="checkSelectable" />
<el-table-column label="报警编号" align="center" prop="alarmCode" width="180" /> <el-table-column label="报警编号" align="center" prop="alarmCode" width="180" />
<el-table-column label="报警时间" align="center" prop="alarmTime" min-width="170" /> <el-table-column label="报警时间" align="center" prop="alarmTime" min-width="170" />
<el-table-column label="所属设备" align="center" prop="deviceName" min-width="120" show-overflow-tooltip /> <el-table-column label="所属设备" align="center" prop="deviceName" min-width="120" show-overflow-tooltip />
<el-table-column label="报警区域" align="center" prop="monitoringArea" min-width="120" show-overflow-tooltip /> <el-table-column label="报警区域" align="center" prop="monitoringArea" min-width="120" show-overflow-tooltip />
<!-- pay content -->
<el-table-column label="报警图片" align="center" prop="alarmImage" min-width="120"> <el-table-column label="报警图片" align="center" prop="alarmImage" min-width="120">
<template #default="scope"> <template #default="scope">
<el-image <el-image
@@ -133,6 +132,11 @@ import { listDemo, updateDemo } from '@/api/WarningList/index';
//import { DemoVO, DemoQuery, DemoForm } from '@/api/WarningList/types'; //import { DemoVO, DemoQuery, DemoForm } from '@/api/WarningList/types';
import { useDeviceStore } from '@/store/modules/equipmentList'; import { useDeviceStore } from '@/store/modules/equipmentList';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useWarningListStore } from '@/store/modules/warnningList';
const warningListStore = useWarningListStore();
const { getWarningList, handleAction } = warningListStore;
const { warningListTotal, warningList } = storeToRefs(warningListStore);
const deviceStore = useDeviceStore(); const deviceStore = useDeviceStore();
const { deviceList } = storeToRefs(deviceStore); const { deviceList } = storeToRefs(deviceStore);
@@ -168,6 +172,7 @@ const initFormData: any = {
processingTime: undefined processingTime: undefined
}; };
/* ?id=&deviceId=&pageSize=1&pageNum=1&beginTime=&endTime= */ /* ?id=&deviceId=&pageSize=1&pageNum=1&beginTime=&endTime= */
/** 查询参数 */
const data = reactive({ const data = reactive({
form: { ...initFormData }, form: { ...initFormData },
queryParams: { queryParams: {
@@ -188,27 +193,35 @@ const data = reactive({
const { queryParams, form, rules, queryTime } = toRefs(data); const { queryParams, form, rules, queryTime } = toRefs(data);
/** 获取列表 */
const getList = async () => { const getList = async () => {
loading.value = true; loading.value = true;
try { try {
const res = await listDemo(queryParams.value); const res = await listDemo(queryParams.value);
demoList.value = res.data.rows; demoList.value = res.data.rows;
total.value = res.data.total; total.value = res.data.total;
await getWarningList({
status: 0,
pageNum: 1,
pageSize: 300
});
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; };
/** 取消按钮 */
const cancel = () => { const cancel = () => {
reset(); reset();
dialog.visible = false; dialog.visible = false;
}; };
/** 重置按钮 */
const reset = () => { const reset = () => {
form.value = { ...initFormData }; form.value = { ...initFormData };
demoFormRef.value?.resetFields(); demoFormRef.value?.resetFields();
}; };
/** 查询按钮 */
const handleQuery = () => { const handleQuery = () => {
queryParams.value.pageNum = 1; queryParams.value.pageNum = 1;
queryParams.value.beginTime = queryTime.value[0]; queryParams.value.beginTime = queryTime.value[0];
@@ -216,12 +229,13 @@ const handleQuery = () => {
getList(); getList();
}; };
/** 重置按钮 */
const resetQuery = () => { const resetQuery = () => {
queryFormRef.value?.resetFields(); queryFormRef.value?.resetFields();
queryTime.value = []; queryTime.value = [];
handleQuery(); handleQuery();
}; };
/** 多选框选中数据 */
const handleSelectionChange = (selection: any) => { const handleSelectionChange = (selection: any) => {
ids.value = selection.map((item) => item.id as string | number); ids.value = selection.map((item) => item.id as string | number);
multiple.value = !selection.length; multiple.value = !selection.length;
@@ -233,7 +247,7 @@ const handleUpdate = async (row: any) => {
loading.value = true; loading.value = true;
try { try {
await updateDemo(row.id); await handleAction(row.id);
proxy?.$modal.msgSuccess('处理成功'); proxy?.$modal.msgSuccess('处理成功');
await getList(); await getList();
} finally { } finally {
@@ -241,6 +255,11 @@ const handleUpdate = async (row: any) => {
} }
}; };
/** 检查行是否可选(已处理不可选) */
const checkSelectable = (row: any) => {
return row.status !== 1;
};
/** 批量:确认后直接提交 */ /** 批量:确认后直接提交 */
const handleBatchProcess = async () => { const handleBatchProcess = async () => {
if (!ids.value.length) { if (!ids.value.length) {
@@ -250,7 +269,7 @@ const handleBatchProcess = async () => {
await proxy?.$modal.confirm('是否确认批量处理已选中的报警?'); await proxy?.$modal.confirm('是否确认批量处理已选中的报警?');
loading.value = true; loading.value = true;
try { try {
await updateDemo(ids.value); await handleAction(ids.value);
proxy?.$modal.msgSuccess('处理成功'); proxy?.$modal.msgSuccess('处理成功');
await getList(); await getList();
} finally { } finally {
@@ -258,6 +277,7 @@ const handleBatchProcess = async () => {
} }
}; };
/** 提交按钮 */
const submitForm = async () => { const submitForm = async () => {
if (form.value.id == null) { if (form.value.id == null) {
proxy?.$modal.msgWarning('数据异常'); proxy?.$modal.msgWarning('数据异常');
@@ -291,4 +311,11 @@ onMounted(() => {
getList(); getList();
getDeviceName(); getDeviceName();
}); });
//监听数据源变化,自动刷新列表
watch(warningListTotal, async (newVal) => {
//console.log('监听到报警列表变化,自动刷新列表', newVal);
//数据源不同, 接口复用请求多了一遍不属于bug
await getList();
});
</script> </script>

View File

@@ -65,10 +65,19 @@
<el-tag v-else>{{ scope.row.status ?? '-' }}</el-tag> <el-tag v-else>{{ scope.row.status ?? '-' }}</el-tag>
</template> --> </template> -->
</el-table-column> </el-table-column>
<el-table-column label="电池电量" align="center" prop="batteryLevel"> <el-table-column label="电池电量" align="center" prop="batteryLevel1" width="150">
<template #default="scope"> <template #default="scope">
<el-tag v-if="scope.row.batteryLevel">{{ scope.row.batteryLevel + '%' }}</el-tag> <!-- <el-button type="primary" v-if="scope.row.batteryLevel1" plain size="small">{{ scope.row.batteryLevel1 + '%' }}</el-button>
<el-tag v-else type="info"> 未获取 </el-tag> <el-button type="primary" v-if="scope.row.batteryLevel2" plain size="small">
{{ scope.row.batteryLevel2 + '%' }}
</el-button> -->
<!-- <el-button type="primary" v-else plain size="small"> 未获取 </el-button> -->
<el-tag v-if="scope.row.batteryLevel1">{{ scope.row.batteryLevel1 + '%' }}</el-tag>
<el-tag v-if="scope.row.batteryLevel2" :style="scope.row.batteryLevel1 && scope.row.batteryLevel2 ? 'margin-left: 10px' : ''">
{{ scope.row.batteryLevel2 + '%' }}
</el-tag>
<!-- <el-tag v-else type="info"> 未获取 </el-tag> -->
<el-tag v-if="!scope.row.batteryLevel1 && !scope.row.batteryLevel2"> 未获取 </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="设备型号" align="center" prop="model" /> <el-table-column label="设备型号" align="center" prop="model" />
@@ -123,7 +132,7 @@
<el-dialog v-model="dialog1.visible" class="monitor-dialog" :before-close="handleMonitorDialogClose" fullscreen destroy-on-close> <el-dialog v-model="dialog1.visible" class="monitor-dialog" :before-close="handleMonitorDialogClose" fullscreen destroy-on-close>
<template #header> <template #header>
<div class="flex w-full items-center justify-between gap-2 min-w-0 pr-8"> <div class="flex w-full items-center justify-between gap-2 min-w-0 pr-8">
<span class="truncate">实时监控</span> <span class="truncate">{{ videoName }}-{{ monitoringArea }}监控</span>
<div> <div>
<el-button v-if="!seeSee" type="primary" class="shrink-0" @click="drawImage()">绘制区域</el-button> <el-button v-if="!seeSee" type="primary" class="shrink-0" @click="drawImage()">绘制区域</el-button>
<el-button v-if="seeSee" type="primary" class="shrink-0" @click="clearImage()">清空区域</el-button> <el-button v-if="seeSee" type="primary" class="shrink-0" @click="clearImage()">清空区域</el-button>
@@ -165,6 +174,7 @@ const multiple = ref(true);
const total = ref(0); const total = ref(0);
const seeSee = ref(0); const seeSee = ref(0);
const haveCode = ref(false); const haveCode = ref(false);
const videoName = ref<string>('实时监控');
const queryFormRef = ref<ElFormInstance>(); const queryFormRef = ref<ElFormInstance>();
const demoFormRef = ref<ElFormInstance>(); const demoFormRef = ref<ElFormInstance>();
@@ -181,6 +191,8 @@ const dialog1 = reactive<DialogOption>({
const monitorVideoUrl = ref(''); const monitorVideoUrl = ref('');
const monitorVideoRef = ref<HTMLVideoElement>(); const monitorVideoRef = ref<HTMLVideoElement>();
let hlsInstance: Hls | null = null; let hlsInstance: Hls | null = null;
let visibilityHandler: (() => void) | null = null;
const monitoringArea = ref<string>('');
const initHls = () => { const initHls = () => {
const url = monitorVideoUrl.value; const url = monitorVideoUrl.value;
@@ -400,6 +412,7 @@ const handleSee = async (row?: any) => {
reset(); reset();
resetMonitorDraw(); resetMonitorDraw();
seeSee.value = row?.isDrawArea === 1 ? 1 : 0; seeSee.value = row?.isDrawArea === 1 ? 1 : 0;
monitoringArea.value = row?.monitoringArea;
const _id = row?.id || ids.value[0]; const _id = row?.id || ids.value[0];
const res = await getDeviceInfo(_id); const res = await getDeviceInfo(_id);
Object.assign(form.value, res.data); Object.assign(form.value, res.data);
@@ -454,6 +467,18 @@ const handleSee = async (row?: any) => {
console.error('[查看监控] 解析 drawArea 失败:', error); console.error('[查看监控] 解析 drawArea 失败:', error);
} }
} }
// 后台运行的时候恢复播放
cleanupVisibilityChange();
const handleVisibilityChange = () => {
if (!document.hidden) {
const video = monitorVideoRef.value;
if (video && video.paused) {
video.play().catch((e) => console.warn('恢复播放失败:', e));
}
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
visibilityHandler = handleVisibilityChange;
}; };
// 提取显示报警区域的逻辑为独立函数 // 提取显示报警区域的逻辑为独立函数
@@ -530,6 +555,13 @@ const destroyVideoResizeObserver = () => {
videoResizeObserver.value = null; videoResizeObserver.value = null;
} }
}; };
// 清理 visibilitychange 事件监听
const cleanupVisibilityChange = () => {
if (visibilityHandler) {
document.removeEventListener('visibilitychange', visibilityHandler);
visibilityHandler = null;
}
};
const handleMonitorDialogClose = (done: () => void) => { const handleMonitorDialogClose = (done: () => void) => {
// 卸载 Hls 与清理 video // 卸载 Hls 与清理 video
@@ -537,6 +569,7 @@ const handleMonitorDialogClose = (done: () => void) => {
monitorVideoUrl.value = ''; monitorVideoUrl.value = '';
destroyVideoResizeObserver(); destroyVideoResizeObserver();
resetMonitorDraw(); resetMonitorDraw();
cleanupVisibilityChange();
done(); done();
//刷新列表 //刷新列表
getList(); getList();
@@ -580,6 +613,7 @@ const payload = ref<AlarmAreaData[]>([]);
const drawImage = () => { const drawImage = () => {
resetMonitorDraw(); resetMonitorDraw();
monitorDrawCollecting.value = true; monitorDrawCollecting.value = true;
videoName.value = '绘制区域';
proxy?.$modal.msgSuccess('请在视频画面上连续点击两个点,生成报警区域'); proxy?.$modal.msgSuccess('请在视频画面上连续点击两个点,生成报警区域');
}; };
@@ -706,6 +740,13 @@ const handleExport = () => {
onMounted(() => { onMounted(() => {
getList(); getList();
}); });
watch(
() => seeSee.value,
(newValue) => {
videoName.value = newValue === 1 ? '绘制区域' : '实时监控';
}
);
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -790,4 +831,19 @@ onMounted(() => {
padding: 2px 6px; padding: 2px 6px;
border-radius: 2px; border-radius: 2px;
} }
/* 隐藏视频进度条,保留其他控制按钮 */
.monitor-video::-webkit-media-controls-timeline {
display: none !important;
}
.monitor-video::-webkit-media-controls-current-time-display,
.monitor-video::-webkit-media-controls-time-remaining-display {
display: none !important;
}
.monitor-video::-moz-range-track {
display: none !important;
}
.monitor-video::-moz-progress-bar {
display: none !important;
}
</style> </style>

View File

@@ -74,7 +74,7 @@
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" class="monitor-dialog" fullscreen destroy-on-close> <el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" class="monitor-dialog" fullscreen destroy-on-close>
<template #header> <template #header>
<div class="flex w-full items-center justify-between gap-2 min-w-0 pr-8"> <div class="flex w-full items-center justify-between gap-2 min-w-0 pr-8">
<span class="truncate">实时监控-北门出入门监控</span> <span class="truncate">播放-{{ videoName }}</span>
</div> </div>
</template> </template>
<div class="monitor-video-wrap"> <div class="monitor-video-wrap">
@@ -109,6 +109,7 @@ const multiple = ref(true);
const total = ref(0); const total = ref(0);
const monitorVideoUrl = ref(); const monitorVideoUrl = ref();
const downloadUrl = ref(); const downloadUrl = ref();
const videoName = ref<string>('录像文件');
const queryFormRef = ref<ElFormInstance>(); const queryFormRef = ref<ElFormInstance>();
const demoFormRef = ref<ElFormInstance>(); const demoFormRef = ref<ElFormInstance>();
@@ -218,6 +219,7 @@ const handlePlay = async (row?: any) => {
return; return;
} }
monitorVideoUrl.value = row.videoUrl; monitorVideoUrl.value = row.videoUrl;
videoName.value = row.videoName;
dialog.visible = true; dialog.visible = true;
}; };
const handleDown = async (row?: any) => { const handleDown = async (row?: any) => {

View File

@@ -24,7 +24,8 @@ export default defineConfig(({ mode, command }) => {
open: true, open: true,
proxy: { proxy: {
[env.VITE_APP_BASE_API]: { [env.VITE_APP_BASE_API]: {
target: 'http://47.104.208.94:8089', // target: 'http://47.104.208.94:8089',
target: 'http://192.168.0.214:8089',
changeOrigin: true, changeOrigin: true,
ws: true, ws: true,
rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '') rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')