Compare commits

...

23 Commits

Author SHA1 Message Date
Ironsp
3976b854d1 feat 用户配置 2026-07-16 09:59:11 +08:00
Ironsp
3ec33d520d feat 用户配置 2026-07-16 09:49:34 +08:00
Ironsp
93a5feabdc feat 页面标题和页面的favicon.ico动态修复 2026-07-07 09:23:30 +08:00
Ironsp
88186b8dfc feat 优化 2026-07-06 17:52:40 +08:00
Ironsp
55ff3d7777 feat 杀死报警后直播进程 2026-07-02 14:44:25 +08:00
Ironsp
603c82939b feat 杀死直播进程 2026-07-02 14:34:23 +08:00
Ironsp
da108742cc feat 优化 2026-07-02 10:02:43 +08:00
Ironsp
72716e0fa9 feat 引入 2026-07-02 08:13:21 +08:00
Ironsp
374e24035c feat 上服务器测试 2026-06-30 16:56:30 +08:00
Ironsp
04ca12b873 feat bug修复 2026-06-30 16:54:39 +08:00
Ironsp
c76102bf96 feat 电池样式修改 2026-06-30 11:55:46 +08:00
Ironsp
5ef955f1fc feat 用户和角色权限配置 2026-06-29 16:38:54 +08:00
Ironsp
0859fc76cd feat 用户权限bug修改 2026-06-29 16:38:10 +08:00
Ironsp
67fe72411c feat 固件版本更换, 删除逻辑修正 2026-06-29 11:56:02 +08:00
Ironsp
e05d3a49da feat 优化报警图片 2026-06-26 15:04:51 +08:00
Ironsp
a860697ba2 feat 优化ts分片加载最多重试 2026-06-26 14:59:03 +08:00
Ironsp
6e15e28f38 feat 添加刷新功能 2026-06-26 14:54:55 +08:00
wangzzpeng
92c5cf293b Merge remote-tracking branch 'origin/main' 2026-06-26 14:14:00 +08:00
wangzzpeng
ec47c164b0 删除 错误自动恢复 2026-06-26 14:13:39 +08:00
Ironsp
8bd41d6758 feat 关闭src, 切断视频流 2026-06-26 14:04:41 +08:00
Ironsp
f92baab6f7 feat hls加载,断网多次加载m3u8 2026-06-26 11:56:48 +08:00
Ironsp
a27b0ac7f2 feat hls错误自动修复 2026-06-26 11:07:29 +08:00
Ironsp
4083da42d8 feat 优化Badge 2026-06-25 08:29:00 +08:00
18 changed files with 308 additions and 120 deletions

View File

@@ -35,4 +35,6 @@ VITE_APP_WEBSOCKET = false
# sse 开关 # sse 开关
VITE_APP_SSE = true VITE_APP_SSE = true
VITE_BASE_API = "http://47.104.208.94:8089" # http://192.168.0.214:8089
# VITE_BASE_API = "http://47.104.208.94:8089"
VITE_BASE_API = "http://192.168.0.214:8089"

View File

@@ -1,5 +1,5 @@
# 页面标题 # 页面标题
VITE_APP_TITLE = 可视化电子围栏管理系统 VITE_APP_TITLE = 可视化电子围栏管理系统11111
VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus
# 生产环境配置 # 生产环境配置

View File

@@ -5,10 +5,6 @@
"ComputedRef": true, "ComputedRef": true,
"DirectiveBinding": true, "DirectiveBinding": true,
"EffectScope": true, "EffectScope": true,
"ElLoading": true,
"ElMessage": true,
"ElMessageBox": true,
"ElNotification": true,
"ExtractDefaultPropTypes": true, "ExtractDefaultPropTypes": true,
"ExtractPropTypes": true, "ExtractPropTypes": true,
"ExtractPublicPropTypes": true, "ExtractPublicPropTypes": true,

25
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,25 @@
# RTK — Token-Optimized CLI
**rtk** is a CLI proxy that filters and compresses command outputs, saving 60-90% tokens.
## Rule
Always prefix shell commands with `rtk`:
```bash
# Instead of: Use:
git status rtk git status
git log -10 rtk git log -10
cargo test rtk cargo test
docker ps rtk docker ps
kubectl get pods rtk kubectl pods
```
## Meta commands (use directly)
```bash
rtk gain # Token savings dashboard
rtk gain --history # Per-command savings history
rtk discover # Find missed rtk opportunities
rtk proxy <cmd> # Run raw (no filtering) but track usage
```

12
.github/hooks/rtk-rewrite.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"hooks": {
"PreToolUse": [
{
"type": "command",
"command": "rtk hook copilot",
"cwd": ".",
"timeout": 5
}
]
}
}

View File

@@ -22,8 +22,8 @@ export const listDemo = (deviceId?: string | number) => {
*/ */
export const addDemo = (data: any) => { export const addDemo = (data: any) => {
return request({ return request({
url: '/haikang/isup/stream/live/stop', url: `/haikang/isup/stream/live/stop?id=${data}`,
method: 'get', method: 'get'
params: data // params: data
}); });
}; };

View File

@@ -46,7 +46,7 @@
v-model:visible="popoverVisible" v-model:visible="popoverVisible"
> >
<template #reference> <template #reference>
<el-badge :value="infoData" :max="999"> <el-badge :value="infoData" :max="9999">
<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>

View File

@@ -2,11 +2,11 @@
<div class="sidebar-logo-container" :class="{ collapse: collapse }"> <div class="sidebar-logo-container" :class="{ collapse: collapse }">
<transition :enter-active-class="proxy?.animate.logoAnimate.enter" mode="out-in"> <transition :enter-active-class="proxy?.animate.logoAnimate.enter" mode="out-in">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/"> <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logoUrl" class="sidebar-logo" /> <img v-if="showLogoImage" :src="logoUrl" class="sidebar-logo" />
<h1 v-else class="sidebar-title">{{ logoName }}</h1> <h1 v-else class="sidebar-title">{{ logoName }}</h1>
</router-link> </router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/"> <router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logoUrl" class="sidebar-logo" /> <img v-if="showLogoImage" :src="logoUrl" class="sidebar-logo" />
<h1 class="sidebar-title">{{ logoName }}</h1> <h1 class="sidebar-title">{{ logoName }}</h1>
</router-link> </router-link>
</transition> </transition>
@@ -15,11 +15,9 @@
<script setup lang="ts"> <script setup lang="ts">
import variables from '@/assets/styles/variables.module.scss'; import variables from '@/assets/styles/variables.module.scss';
import logo from '@/assets/logo/logo.png';
import { useSettingsStore } from '@/store/modules/settings'; import { useSettingsStore } from '@/store/modules/settings';
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { NavTypeEnum } from '@/enums/NavTypeEnum'; import { NavTypeEnum } from '@/enums/NavTypeEnum';
import { getDemo } from '@/api/systemList/index';
defineProps({ defineProps({
collapse: { collapse: {
@@ -28,13 +26,15 @@ defineProps({
} }
}); });
const title = import.meta.env.VITE_APP_LOGO_TITLE;
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const sideTheme = computed(() => settingsStore.sideTheme); const sideTheme = computed(() => settingsStore.sideTheme);
const logoUrl = computed(() => settingsStore.appLogoUrl || '');
const logoName = computed(() => settingsStore.appTitle || import.meta.env.VITE_APP_LOGO_TITLE || 'RuoYi-Vue-Plus');
const showLogoImage = computed(() => Boolean(logoUrl.value));
// 获取Logo背景色 // 获取Logo背景色
const getLogoBackground = computed(() => { const getLogoBackground = computed(() => {
if (settingsStore.isDark) { if (settingsStore.dark) {
return 'var(--sidebar-bg)'; return 'var(--sidebar-bg)';
} }
if (settingsStore.navType == NavTypeEnum.TOP) { if (settingsStore.navType == NavTypeEnum.TOP) {
@@ -45,7 +45,7 @@ const getLogoBackground = computed(() => {
// 获取Logo文字颜色 // 获取Logo文字颜色
const getLogoTextColor = computed(() => { const getLogoTextColor = computed(() => {
if (settingsStore.isDark) { if (settingsStore.dark) {
return 'var(--sidebar-text)'; return 'var(--sidebar-text)';
} }
if (settingsStore.navType == NavTypeEnum.TOP) { if (settingsStore.navType == NavTypeEnum.TOP) {
@@ -53,18 +53,6 @@ const getLogoTextColor = computed(() => {
} }
return sideTheme.value === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor; return sideTheme.value === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor;
}); });
// 获取Logo图片
const logoUrl = ref();
const logoName = ref();
onMounted(() => {
getDemo().then((res) => {
console.log('系统11111111111', res);
logoName.value = res.data.name;
logoUrl.value = res.data.url;
});
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -53,6 +53,7 @@ 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'; import type { WarningListResult } from '@/api/WarningList/types';
import { addDemo } from '@/api/video/index';
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -109,7 +110,9 @@ const parseNoticeItem = (notice: any) => {
}; };
//查看画面 //查看画面
const currentDeviceId = ref<string | number>();
const handleSee = async (row: any) => { const handleSee = async (row: any) => {
currentDeviceId.value = row?.deviceId;
const videoId = row?.deviceId; const videoId = row?.deviceId;
console.log('查看画面', row, 'deviceId', videoId); console.log('查看画面', row, 'deviceId', videoId);
if (!videoId) { if (!videoId) {
@@ -127,6 +130,13 @@ const handleSee = async (row: any) => {
}; };
const handleMonitorDialogClose = async (done: () => void) => { const handleMonitorDialogClose = async (done: () => void) => {
try {
addDemo(currentDeviceId.value as string).then(() => {
console.log('关闭视频111111');
});
} catch (error) {
console.log('关闭视频22222222', error);
}
// 卸载 Hls 与清理 video // 卸载 Hls 与清理 video
destroyHls(monitorVideoRef); destroyHls(monitorVideoRef);
done(); done();

View File

@@ -30,6 +30,8 @@ import './permission';
// 国际化 // 国际化
import i18n from '@/lang/index'; import i18n from '@/lang/index';
import { useSettingsStore } from '@/store/modules/settings';
import { getDemo } from '@/api/systemList/index';
// vxeTable // vxeTable
import VXETable from 'vxe-table'; import VXETable from 'vxe-table';
@@ -54,4 +56,24 @@ app.use(plugins);
// 自定义指令 // 自定义指令
directive(app); directive(app);
const settingsStore = useSettingsStore();
getDemo()
.then((res) => {
const name = res?.data?.name;
const url = res?.data?.url;
if (name || url) {
settingsStore.setSystemInfo(name || '', url || '');
if (url) {
const link = document.querySelector("link[rel='icon']") as HTMLLinkElement | null;
if (link) {
link.href = url;
}
}
}
})
.catch(() => {
// ignore
});
app.mount('#app'); app.mount('#app');

View File

@@ -19,6 +19,8 @@ export const useSettingsStore = defineStore('setting', () => {
radiusBase: defaultSettings.radiusBase radiusBase: defaultSettings.radiusBase
}); });
const title = ref<string>(defaultSettings.title); const title = ref<string>(defaultSettings.title);
const appTitle = ref<string>(defaultSettings.title);
const appLogoUrl = ref<string>('');
const theme = ref<string>(storageSetting.value.theme); const theme = ref<string>(storageSetting.value.theme);
const sideTheme = ref<string>(storageSetting.value.sideTheme); const sideTheme = ref<string>(storageSetting.value.sideTheme);
const showSettings = ref<boolean>(defaultSettings.showSettings); const showSettings = ref<boolean>(defaultSettings.showSettings);
@@ -36,8 +38,17 @@ export const useSettingsStore = defineStore('setting', () => {
title.value = value; title.value = value;
useDynamicTitle(); useDynamicTitle();
}; };
const setSystemInfo = (name: string, url: string) => {
appTitle.value = name || defaultSettings.title;
appLogoUrl.value = url || '';
useDynamicTitle();
};
return { return {
title, title,
appTitle,
appLogoUrl,
theme, theme,
sideTheme, sideTheme,
showSettings, showSettings,
@@ -50,6 +61,7 @@ export const useSettingsStore = defineStore('setting', () => {
dark, dark,
navType, navType,
radiusBase, radiusBase,
setTitle setTitle,
setSystemInfo
}; };
}); });

View File

@@ -6,9 +6,12 @@ import { useSettingsStore } from '@/store/modules/settings';
*/ */
export const useDynamicTitle = () => { export const useDynamicTitle = () => {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const pageTitle = settingsStore.title || (defaultSettings.title as string);
const appTitle = settingsStore.appTitle || (defaultSettings.title as string);
if (settingsStore.dynamicTitle) { if (settingsStore.dynamicTitle) {
document.title = settingsStore.title + ' - ' + import.meta.env.VITE_APP_TITLE; document.title = `${pageTitle} - ${appTitle}`;
} else { } else {
document.title = defaultSettings.title as string; document.title = appTitle;
} }
}; };

View File

@@ -1,5 +1,7 @@
import Hls from 'hls.js'; import Hls from 'hls.js';
import { log } from 'node:console';
let hlsInstance: Hls | null = null; let hlsInstance: Hls | null = null;
// let hlsRetryCount = 0;
export const initHls = (monitorVideoUrl, monitorVideoRef) => { export const initHls = (monitorVideoUrl, monitorVideoRef) => {
const url = monitorVideoUrl.value; const url = monitorVideoUrl.value;
@@ -17,7 +19,16 @@ export const initHls = (monitorVideoUrl, monitorVideoRef) => {
} }
if (Hls.isSupported()) { if (Hls.isSupported()) {
const hls = new Hls(); const hls = new Hls({
// ★ ZLM 注册流需要时间,配置自动重试
manifestLoadingMaxRetry: 5, // m3u8 加载最多重试20次
manifestLoadingRetryDelay: 500, // 每次重试间隔500ms
manifestLoadingTimeOut: 5000, // m3u8 请求超时5秒
levelLoadingMaxRetry: 2, // ts 分片加载最多重试20次
levelLoadingRetryDelay: 500,
lowLatencyMode: false, // 关掉低延迟模式,更稳定
startLevel: -1 // 自动选择码率
});
hls.loadSource(url); hls.loadSource(url);
hls.attachMedia(video); hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => { hls.on(Hls.Events.MANIFEST_PARSED, () => {
@@ -25,6 +36,8 @@ export const initHls = (monitorVideoUrl, monitorVideoRef) => {
try { try {
video.play().catch(() => {}); video.play().catch(() => {});
} catch (e) {} } catch (e) {}
// 加载成功,重置重试计数
// hlsRetryCount = 0;
}); });
hlsInstance = hls; hlsInstance = hls;
} else if (video.canPlayType('application/vnd.apple.mpegurl')) { } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
@@ -56,7 +69,9 @@ export const destroyHls = (monitorVideoRef) => {
video.removeAttribute('src'); video.removeAttribute('src');
// 对某些浏览器,需要调用 load() 来重置 media element // 对某些浏览器,需要调用 load() 来重置 media element
if (typeof video.load === 'function') video.load(); if (typeof video.load === 'function') video.load();
} catch (e) {} } catch (e) {
console.log('Error in destroyHls:', e);
}
} }
}; };

View File

@@ -51,7 +51,7 @@
<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 --> <!-- 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
v-if="scope.row.alarmImage" v-if="scope.row.alarmImage"
@@ -63,7 +63,7 @@
/> />
<span v-else>暂无图片</span> <span v-else>暂无图片</span>
</template> </template>
</el-table-column> </el-table-column> -->
<el-table-column label="处理状态" align="center" prop="status" width="100"> <el-table-column label="处理状态" align="center" prop="status" width="100">
<template #default="scope"> <template #default="scope">
<el-tag v-if="scope.row.status === 0" type="warning">未处理</el-tag> <el-tag v-if="scope.row.status === 0" type="warning">未处理</el-tag>

View File

@@ -65,23 +65,121 @@
<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="batteryLevel1" width="150"> <!-- <el-table-column label="电池电量" align="center" prop="batteryLevel1" width="150">
<template #default="scope"> <template #default="scope">
<!-- <el-button type="primary" v-if="scope.row.batteryLevel1" plain size="small">{{ scope.row.batteryLevel1 + '%' }}</el-button> <el-tag v-if="scope.row.batteryLevel1">{{ scope.row.batteryLevel1 + '%' }}</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' : ''"> <el-tag v-if="scope.row.batteryLevel2" :style="scope.row.batteryLevel1 && scope.row.batteryLevel2 ? 'margin-left: 10px' : ''">
{{ scope.row.batteryLevel2 + '%' }} {{ scope.row.batteryLevel2 + '%' }}
</el-tag> </el-tag>
<!-- <el-tag v-else type="info"> 未获取 </el-tag> -->
<el-tag v-if="!scope.row.batteryLevel1 && !scope.row.batteryLevel2"> 未获取 </el-tag> <el-tag v-if="!scope.row.batteryLevel1 && !scope.row.batteryLevel2"> 未获取 </el-tag>
</template> </template>
</el-table-column> -->
<el-table-column label="电池电量" align="center" width="160">
<template #default="scope">
<div v-if="scope.row.batteryLevel1 != null || scope.row.batteryLevel2 != null" class="battery-group">
<!-- 左电池 -->
<div v-if="scope.row.batteryLevel1 != null" class="battery-item">
<svg viewBox="0 0 56 20" width="56" height="20">
<rect
x="0"
y="1"
width="44"
height="18"
rx="4"
ry="4"
fill="none"
:stroke="getBatteryColor(scope.row.batteryLevel1)"
stroke-width="1.5"
/>
<rect x="44" y="6" width="4" height="8" rx="2" ry="2" :fill="getBatteryColor(scope.row.batteryLevel1)" />
<rect
x="2"
y="3"
:width="Math.max((40 * scope.row.batteryLevel1) / 100, 0)"
height="14"
rx="2"
ry="2"
:fill="getBatteryColor(scope.row.batteryLevel1)"
opacity="0.25"
/>
<text
x="24"
y="11.5"
text-anchor="middle"
dominant-baseline="middle"
font-size="2"
:fill="getBatteryColor(scope.row.batteryLevel1)"
>
{{ scope.row.batteryLevel1 }}%
</text>
</svg>
</div>
<!-- 右电池 -->
<div v-if="scope.row.batteryLevel2 != null" class="battery-item">
<svg viewBox="0 0 56 20" width="56" height="20">
<rect
x="0"
y="1"
width="44"
height="18"
rx="4"
ry="4"
fill="none"
:stroke="getBatteryColor(scope.row.batteryLevel2)"
stroke-width="1.5"
/>
<rect x="44" y="6" width="4" height="8" rx="2" ry="2" :fill="getBatteryColor(scope.row.batteryLevel2)" />
<rect
x="2"
y="3"
:width="Math.max((40 * scope.row.batteryLevel2) / 100, 0)"
height="14"
rx="2"
ry="2"
:fill="getBatteryColor(scope.row.batteryLevel2)"
opacity="0.25"
/>
<text
x="24"
y="11.5"
text-anchor="middle"
dominant-baseline="middle"
font-size="2"
:fill="getBatteryColor(scope.row.batteryLevel2)"
>
{{ scope.row.batteryLevel2 }}%
</text>
</svg>
</div>
</div>
<span v-else>未获取</span>
</template>
</el-table-column> </el-table-column>
<!-- <el-table-column align="center" label="电量" prop="powerLevel" width="160">
<template #default="scope">
<div v-if="scope.row.powerLevel !== undefined && scope.row.powerLevel !== null && scope.row.powerLevel !== ''" class="power-level">
<svg class="power-level__icon" :class="getPowerLevelClass(scope.row.powerLevel)" viewBox="0 0 48 24" width="48" height="24">
<rect x="0" y="2" width="42" height="20" rx="4" ry="4" fill="none" stroke="currentColor" stroke-width="2" />
<rect x="42" y="8" width="4" height="8" rx="2" ry="2" fill="currentColor" />
<rect
x="3"
y="5"
:width="Math.max((36 * formatPowerLevel(scope.row.powerLevel)) / 100, 0)"
height="14"
rx="2"
ry="2"
fill="currentColor"
/>
</svg>
<span class="power-level__text">{{ formatPowerLevel(scope.row.powerLevel) }}%</span>
</div>
<span v-else>-</span>
</template>
</el-table-column> -->
<el-table-column label="设备型号" align="center" prop="model" /> <el-table-column label="设备型号" align="center" prop="model" />
<el-table-column label="固件版本" align="center" prop="id" /> <el-table-column label="固件版本" align="center" prop="version" />
<el-table-column label="是否绘制区域" align="center" prop="isDrawArea"> <el-table-column label="是否绘制区域" align="center" prop="isDrawArea">
<template #default="scope"> <template #default="scope">
<el-tag v-if="scope.row.isDrawArea === 1 || scope.row.isDrawArea === '1'"></el-tag> <el-tag v-if="scope.row.isDrawArea === 1 || scope.row.isDrawArea === '1'"></el-tag>
@@ -101,7 +199,7 @@
@click="handleActivate(scope.row)" @click="handleActivate(scope.row)"
>唤醒设备</el-button >唤醒设备</el-button
> >
<el-button type="primary" @click="handleDelete(scope.row)">删除</el-button> <el-button :disabled="scope.row.status !== 2 && scope.row.status !== '2'" type="primary" @click="handleDelete(scope.row)">删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -134,6 +232,7 @@
<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">{{ videoName }}-{{ monitoringArea }}监控</span> <span class="truncate">{{ videoName }}-{{ monitoringArea }}监控</span>
<div> <div>
<el-button type="primary" class="shrink-0" @click="handleRefresh">刷新</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="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>
<el-button v-if="seeSee" type="primary" class="shrink-0" @click="saveImage()">保存区域</el-button> <el-button v-if="seeSee" type="primary" class="shrink-0" @click="saveImage()">保存区域</el-button>
@@ -155,12 +254,12 @@
<script setup name="Demo" lang="ts"> <script setup name="Demo" lang="ts">
import { addEquipment, getDevicetList, getDeviceInfo, delDevice, actDevice, updateDeviceInfo, drawInfo } from '@/api/equipmentList/index'; import { addEquipment, getDevicetList, getDeviceInfo, delDevice, actDevice, updateDeviceInfo, drawInfo } from '@/api/equipmentList/index';
import { EquipmentList, DeviceInfoForm, AlarmAreaData } from '@/api/equipmentList/types'; import { EquipmentList, DeviceInfoForm, AlarmAreaData } from '@/api/equipmentList/types';
import { listDemo, getDemo, delDemo, addDemo, updateDemo } from '@/api/demo/demo';
import { DemoVO, DemoQuery, DemoForm } from '@/api/demo/demo/types'; import { DemoVO, DemoQuery, DemoForm } from '@/api/demo/demo/types';
import { log } from 'node:console'; import { log } from 'node:console';
import Hls from 'hls.js'; import { initHls, destroyHls } from '@/utils/video';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { useVideosStore } from '@/store/modules/video'; import { useVideosStore } from '@/store/modules/video';
import { addDemo } from '@/api/video/index';
const videosStore = useVideosStore(); const videosStore = useVideosStore();
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -190,68 +289,9 @@ const dialog1 = reactive<DialogOption>({
const monitorVideoUrl = ref(''); const monitorVideoUrl = ref('');
const monitorVideoRef = ref<HTMLVideoElement>(); const monitorVideoRef = ref<HTMLVideoElement>();
let hlsInstance: Hls | null = null;
let visibilityHandler: (() => void) | null = null; let visibilityHandler: (() => void) | null = null;
const monitoringArea = ref<string>(''); const monitoringArea = ref<string>('');
const initHls = () => {
const url = monitorVideoUrl.value;
const video = monitorVideoRef.value;
if (!url || !video) return;
// 销毁已有实例
if (hlsInstance) {
try {
hlsInstance.destroy();
} catch (e) {
// ignore
}
hlsInstance = null;
}
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(url);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
// 自动播放(若浏览器允许)
try {
video.play().catch(() => {});
} catch (e) {}
});
hlsInstance = hls;
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari/原生支持 HLS
video.src = url;
video.addEventListener('loadedmetadata', () => {
try {
video.play().catch(() => {});
} catch (e) {}
});
} else {
proxy?.$modal.msgWarning('当前浏览器不支持 HLS 播放');
}
};
const destroyHls = () => {
try {
if (hlsInstance) {
hlsInstance.destroy();
hlsInstance = null;
}
} catch (e) {}
const video = monitorVideoRef.value;
if (video) {
// 清理原生 src避免残留
try {
video.pause();
video.removeAttribute('src');
// 对某些浏览器,需要调用 load() 来重置 media element
if (typeof video.load === 'function') video.load();
} catch (e) {}
}
};
type MonitorPoint = { x: number; y: number }; type MonitorPoint = { x: number; y: number };
const monitorDrawCollecting = ref(false); const monitorDrawCollecting = ref(false);
const monitorDrawPoints = ref<MonitorPoint[]>([]); const monitorDrawPoints = ref<MonitorPoint[]>([]);
@@ -419,7 +459,7 @@ const handleSee = async (row?: any) => {
dialog1.visible = true; dialog1.visible = true;
dialog1.title = '修改设备'; dialog1.title = '修改设备';
destroyHls(); destroyHls(monitorVideoRef);
monitorVideoUrl.value = ''; monitorVideoUrl.value = '';
await videosStore.getVideos(row.id); await videosStore.getVideos(row.id);
monitorVideoUrl.value = videosStore.hisUrl || ''; monitorVideoUrl.value = videosStore.hisUrl || '';
@@ -432,7 +472,7 @@ const handleSee = async (row?: any) => {
// 等待 DOM 更新后初始化 Hls // 等待 DOM 更新后初始化 Hls
await nextTick(); await nextTick();
initHls(); initHls(monitorVideoUrl, monitorVideoRef);
// 等待视频加载完成后初始化 ResizeObserver以便能正确获取视频尺寸 // 等待视频加载完成后初始化 ResizeObserver以便能正确获取视频尺寸
initVideoResizeObserver(); initVideoResizeObserver();
@@ -564,8 +604,15 @@ const cleanupVisibilityChange = () => {
}; };
const handleMonitorDialogClose = (done: () => void) => { const handleMonitorDialogClose = (done: () => void) => {
try {
addDemo(form.value.id).then(() => {
console.log('关闭视频111111');
});
} catch (error) {
console.log('关闭视频22222222', error);
}
// 卸载 Hls 与清理 video // 卸载 Hls 与清理 video
destroyHls(); destroyHls(monitorVideoRef);
monitorVideoUrl.value = ''; monitorVideoUrl.value = '';
destroyVideoResizeObserver(); destroyVideoResizeObserver();
resetMonitorDraw(); resetMonitorDraw();
@@ -710,7 +757,7 @@ const saveImage = async () => {
const handleMonitorDialogOpen = () => { const handleMonitorDialogOpen = () => {
// 初始化 Hls 与 video // 初始化 Hls 与 video
initHls(); initHls(monitorVideoUrl, monitorVideoRef);
initVideoResizeObserver(); initVideoResizeObserver();
monitorDrawCollecting.value = false; monitorDrawCollecting.value = false;
monitorDrawPoints.value = []; monitorDrawPoints.value = [];
@@ -747,6 +794,34 @@ watch(
videoName.value = newValue === 1 ? '绘制区域' : '实时监控'; videoName.value = newValue === 1 ? '绘制区域' : '实时监控';
} }
); );
/** 刷新按钮操作 */
const handleRefresh = async () => {
if (!form.value.id) return;
destroyHls(monitorVideoRef);
monitorVideoUrl.value = '';
await videosStore.getVideos(form.value.id);
monitorVideoUrl.value = videosStore.hisUrl || '';
if (monitorVideoUrl.value) {
await nextTick();
initHls(monitorVideoUrl, monitorVideoRef);
}
};
/** 电池颜色:>50% 绿30-50% 黄,<30% 红 */
const getBatteryColor = (val: number) => {
if (val > 50) return '#52c41a';
if (val >= 30) return '#faad14';
return '#ff4d4f';
};
const formatPowerLevel = (val: any) => Number(val) || 0;
const getPowerLevelClass = (val: any) => {
const v = Number(val);
if (v > 50) return 'power--high';
if (v >= 30) return 'power--mid';
return 'power--low';
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -846,4 +921,32 @@ watch(
.monitor-video::-moz-progress-bar { .monitor-video::-moz-progress-bar {
display: none !important; display: none !important;
} }
.battery-group {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.battery-item {
display: flex;
align-items: center;
gap: 2px;
}
.battery-label {
font-size: 12px;
color: #909399;
}
/* 电量电池颜色 */
.power--high {
color: #52c41a;
}
.power--mid {
color: #faad14;
}
.power--low {
color: #ff4d4f;
}
</style> </style>

View File

@@ -91,7 +91,7 @@ const save = async () => {
}) })
.catch((err) => { .catch((err) => {
console.error('保存失败', err); console.error('保存失败', err);
ElMessage.error('保存失败'); // ElMessage.error('保存失败');
}); });
}; };

View File

@@ -20,7 +20,7 @@
<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="['system:role:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button> <el-button type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
</el-col> </el-col>
<!-- <el-col :span="1.5"> <!-- <el-col :span="1.5">
<el-button v-hasPermi="['system:role:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button> <el-button v-hasPermi="['system:role:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>

View File

@@ -23,7 +23,7 @@
<div v-show="showSearch" class="mb-[10px]"> <div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover"> <el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true"> <el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="用户名称" prop="userName"> <el-form-item label="用户账号" prop="userName">
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" /> <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
<!-- <el-form-item label="用户账号" prop="nickName"> <!-- <el-form-item label="用户账号" prop="nickName">
@@ -62,7 +62,7 @@
<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-has-permi="['system:user:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button> <el-button type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
</el-col> </el-col>
<!-- <el-col :span="1.5"> <!-- <el-col :span="1.5">
<el-button v-has-permi="['system:user:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()"> <el-button v-has-permi="['system:user:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">
@@ -123,10 +123,10 @@
<el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width"> <el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
<template #default="scope"> <template #default="scope">
<!-- <el-tooltip v-if="scope.row.userId !== 1" content="修改" placement="top"> --> <!-- <el-tooltip v-if="scope.row.userId !== 1" content="修改" placement="top"> -->
<el-button v-hasPermi="['system:user:edit']" type="primary" @click="handleUpdate(scope.row)">编辑</el-button> <el-button type="primary" @click="handleUpdate(scope.row)">编辑</el-button>
<!-- </el-tooltip> <!-- </el-tooltip>
<el-tooltip v-if="scope.row.userId !== 1" content="删除" placement="top"> --> <el-tooltip v-if="scope.row.userId !== 1" content="删除" placement="top"> -->
<el-button v-hasPermi="['system:user:remove']" type="primary" @click="handleDelete(scope.row)">删除</el-button> <el-button type="primary" @click="handleDelete(scope.row)">删除</el-button>
<!-- </el-tooltip> --> <!-- </el-tooltip> -->
<!-- <el-tooltip v-if="scope.row.userId !== 1" content="重置密码" placement="top"> <!-- <el-tooltip v-if="scope.row.userId !== 1" content="重置密码" placement="top">
@@ -156,7 +156,7 @@
<el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px"> <el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px">
<el-row> <el-row>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="用户账号" prop="nickName"> <el-form-item label="用户名称" prop="nickName">
<el-input v-model="form.nickName" placeholder="请输入用户账号" maxlength="30" /> <el-input v-model="form.nickName" placeholder="请输入用户账号" maxlength="30" />
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -188,7 +188,7 @@
<!-- </el-row> <!-- </el-row>
<el-row> --> <el-row> -->
<el-col :span="12" v-if="form.userId == undefined"> <el-col :span="12" v-if="form.userId == undefined">
<el-form-item label="用户名称" prop="userName"> <el-form-item label="用户账号" prop="userName">
<el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" /> <el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" />
</el-form-item> </el-form-item>
</el-col> </el-col>