Compare commits

...

71 Commits

Author SHA1 Message Date
Ironsp
40503b4f56 feat 默认app_title 2026-08-19 14:19:30 +08:00
Ironsp
000472231c fix 服务器环境配置 2026-08-13 11:48:53 +08:00
Ironsp
aba75fdd7c fix 去掉账号密码 2026-08-13 10:13:01 +08:00
Ironsp
e19a9df909 fix 修改部署环境 2026-08-12 11:49:51 +08:00
Ironsp
7f195f3fc1 fix 直播流追赶逻辑 2026-08-05 11:05:47 +08:00
Ironsp
04a52895d7 fix 验证码显示问题 2026-08-03 17:15:19 +08:00
Ironsp
bf3c8d8228 feat 摄像头需求变更 2026-07-27 17:07:50 +08:00
Ironsp
5ff0a2e048 feat 多像摄像头切换 2026-07-27 09:19:45 +08:00
Ironsp
1b55bf6935 feat 添加新功能多像摄像头切换 2026-07-27 09:05:53 +08:00
Ironsp
8768ec3729 feat 优化直播链路提示词 2026-07-22 14:38:40 +08:00
Ironsp
747e7000cd feat 优化直播链路 2026-07-22 14:37:14 +08:00
Ironsp
ef01ef8394 feat 优化 2026-07-22 14:26:34 +08:00
Ironsp
b638e7eaa0 feat 优化 2026-07-22 14:01:32 +08:00
Ironsp
93f3a48caf feat 优化 2026-07-22 11:52:43 +08:00
Ironsp
bbd23051e7 feat 直播流优化 2026-07-22 11:50:57 +08:00
Ironsp
aaa223f41d feat 视频优化,去掉控件 2026-07-20 12:00:41 +08:00
Ironsp
7f376ce10c feat hls分片加在成功后播放 2026-07-20 10:22:52 +08:00
Ironsp
d78dd8d42e feat 优化架构设计 2026-07-18 11:04:23 +08:00
Ironsp
d193836c70 feat 优化架构 2026-07-18 10:43:00 +08:00
Ironsp
671c5b4200 feat 优化代码 2026-07-18 10:33:33 +08:00
Ironsp
13ebb3e51a feat 优化代码结构添加ignore 2026-07-18 09:57:31 +08:00
Ironsp
43bfa65108 feat 顶部图标修改 2026-07-17 15:18:36 +08:00
Ironsp
98a30948ec feat hls错误自动恢复 2026-07-17 14:57:25 +08:00
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
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
31 changed files with 798 additions and 270 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://47.104.215.165:8080"

View File

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

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
}
]
}
}

1
.gitignore vendored
View File

@@ -13,6 +13,7 @@ selenium-debug.log
# Editor directories and files # Editor directories and files
.claude .claude
.mimocode
.idea .idea
.vscode .vscode
*.suo *.suo

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
registry = https://registry.npmmirror.com

View File

@@ -7,27 +7,20 @@
## 配套后端代码仓库地址 ## 配套后端代码仓库地址
| 介绍 | 项目名 | 项目地址 | | 介绍 | 项目名 | 项目地址 |
|------------|:-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ----------------- | :--------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 🔥 分布式集群框架 | RuoYi-Vue-Plus | - [Gitee](https://gitee.com/dromara/RuoYi-Vue-Plus)<br> - [GitHub](https://github.com/dromara/RuoYi-Vue-Plus)<br> - [GitCode](https://gitcode.com/dromara/RuoYi-Vue-Plus) | | 🔥 分布式集群框架 | RuoYi-Vue-Plus | - [Gitee](https://gitee.com/dromara/RuoYi-Vue-Plus)<br> - [GitHub](https://github.com/dromara/RuoYi-Vue-Plus)<br> - [GitCode](https://gitcode.com/dromara/RuoYi-Vue-Plus) |
| 🔥 微服务框架 | RuoYi-Cloud-Plus | - [Gitee](https://gitee.com/dromara/RuoYi-Cloud-Plus)<br>- [GitHub](https://github.com/dromara/RuoYi-Cloud-Plus)<br> - [GitCode](https://gitcode.com/dromara/RuoYi-Cloud-Plus) | | 🔥 微服务框架 | RuoYi-Cloud-Plus | - [Gitee](https://gitee.com/dromara/RuoYi-Cloud-Plus)<br>- [GitHub](https://github.com/dromara/RuoYi-Cloud-Plus)<br> - [GitCode](https://gitcode.com/dromara/RuoYi-Cloud-Plus) |
## 分支说明 ## 分支说明
- ts分支(稳定发布主分支 生产可用) - main分支(稳定发布主分支 生产可用)
- dev分支(开发分支 开发过程中使用) - dev分支(开发分支 开发过程中使用)
## 前端运行 ## 前端运行
```bash ```bash
# 安装依赖 # 启动命令
npm install --registry=https://registry.npmmirror.com ./start.sh
# 启动服务
npm run dev
# 构建生产环境
npm run build:prod
# 前端访问地址 http://localhost:80 # 前端访问地址 http://localhost:80
``` ```

View File

@@ -8,6 +8,7 @@
"type": "module", "type": "module",
"packageManager": "pnpm@10.34.1", "packageManager": "pnpm@10.34.1",
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "vite serve --mode development", "dev": "vite serve --mode development",
"build:prod": "vite build --mode production", "build:prod": "vite build --mode production",
"build:dev": "vite build --mode development", "build:dev": "vite build --mode development",

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

@@ -13,33 +13,21 @@
</template> </template>
<div class="right-menu flex align-center"> <div class="right-menu flex align-center">
<div v-if="appStore.device !== 'mobile'"> <div v-if="appStore.device !== 'mobile'">
<!-- <el-select
v-if="userId === 1 && tenantEnabled"
v-model="companyName"
class="min-w-244px mr-2"
clearable
filterable
reserve-keyword
:placeholder="proxy.$t('navbar.selectTenant')"
@change="dynamicTenantEvent"
@clear="dynamicClearEvent"
>
<el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"> </el-option>
<template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
</el-select> -->
<search-menu ref="searchMenuRef" /> <search-menu ref="searchMenuRef" />
<!-- <el-tooltip content="搜索" effect="dark" placement="bottom">
<div class="right-menu-item hover-effect" @click="openSearchMenu">
<svg-icon class-name="search-icon" icon-class="search" />
</div> </div>
</el-tooltip> --> <!-- 消息 - 移到 v-if 外面手机端也能显示 -->
<!-- 消息 -->
<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="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>
@@ -51,26 +39,6 @@
</el-popover> </el-popover>
</div> </div>
</el-tooltip> </el-tooltip>
<!-- <el-tooltip content="Github" effect="dark" placement="bottom">
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip :content="proxy.$t('navbar.document')" effect="dark" placement="bottom">
<ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip :content="proxy.$t('navbar.full')" effect="dark" placement="bottom">
<screenfull id="screenfull" class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip :content="proxy.$t('navbar.language')" effect="dark" placement="bottom">
<lang-select id="lang-select" class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip :content="proxy.$t('navbar.layoutSize')" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip> -->
</div>
<div class="avatar-container"> <div class="avatar-container">
<el-dropdown class="right-menu-item hover-effect" trigger="click" @command="handleCommand"> <el-dropdown class="right-menu-item hover-effect" trigger="click" @command="handleCommand">
<div class="avatar-wrapper"> <div class="avatar-wrapper">
@@ -123,6 +91,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 +184,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 +216,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

@@ -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

@@ -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,12 +34,12 @@
> >
<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">
<div class="monitor-video-stack"> <div class="monitor-video-stack">
<video ref="monitorVideoRef" class="monitor-video" controls preload="metadata" :src="monitorVideoUrl">您的浏览器不支持视频播放</video> <video ref="monitorVideoRef" class="monitor-video" preload="metadata">您的浏览器不支持视频播放</video>
</div> </div>
</div> </div>
</el-dialog> </el-dialog>
@@ -53,16 +53,18 @@ 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;
import { nextTick } from 'vue'; import { nextTick, onUnmounted } 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, hanldeAction } = warningListStore; const { getWarningList, handleAction } = warningListStore;
const { warningListTotal, warningList } = storeToRefs(warningListStore); const { warningListTotal, warningList } = storeToRefs(warningListStore);
// const props = defineProps<{ // const props = defineProps<{
@@ -72,6 +74,18 @@ const warningListRows = computed(() => warningList.value?.rows || []);
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,7 +110,23 @@ const parseNoticeItem = (notice: any) => {
}; };
//查看画面 //查看画面
const currentDeviceId = ref<string | number>();
/** 刷新视频(卡死时自动调用) */
const handleRefresh = async () => {
if (!currentDeviceId.value) return;
destroyHls(monitorVideoRef);
monitorVideoUrl.value = '';
await videosStore.getVideos(currentDeviceId.value);
monitorVideoUrl.value = videosStore.hisUrl || '';
if (monitorVideoUrl.value) {
await nextTick();
initHls(monitorVideoUrl, monitorVideoRef, () => dialog.visible, handleRefresh);
}
};
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) {
@@ -105,13 +135,22 @@ const handleSee = async (row: any) => {
} }
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, () => dialog.visible, handleRefresh);
await hanldeAction(row.id); await handleAction(row.id);
await getList();
}; };
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();
@@ -150,7 +189,7 @@ watch(
(newNotices) => { (newNotices) => {
const filteredNotices = [...newNotices].map(parseNoticeItem); const filteredNotices = [...newNotices].map(parseNoticeItem);
newsList.value = filteredNotices; newsList.value = filteredNotices;
console.log('监听消息队列', newsList.value); // console.log('监听消息队列', newsList.value);
}, },
{ deep: true } { deep: true }
); );
@@ -194,10 +233,16 @@ onMounted(() => {
onBeforeRouteLeave(() => { onBeforeRouteLeave(() => {
dialog.visible = false; dialog.visible = false;
destroyHls(monitorVideoRef);
monitorVideoUrl.value = ''; monitorVideoUrl.value = '';
}); });
onUnmounted(() => {
destroyHls(monitorVideoRef);
});
onMounted(() => { onMounted(() => {
getWarningList(); getList();
console.log('111111111111111111111_warningList.value', warningList.value); console.log('111111111111111111111_warningList.value', warningList.value);
}); });
</script> </script>

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

@@ -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

@@ -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

@@ -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
status: 0, // deviceId,
pageNum: 1, // status: 0,
pageSize: 300 // pageNum: 1,
// pageSize: 10
}); });
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

@@ -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,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;
} }
@@ -28,11 +31,11 @@ export const initSSE = (url: any) => {
if (!data.value) return; if (!data.value) return;
// 打印接收到的 SSE 消息 // 打印接收到的 SSE 消息
console.log('=== SSE 接收到消息 ==='); // console.log('=== SSE 接收到消息 ===');
console.log('消息内容:', data.value); // console.log('消息内容:', data.value);
console.log('消息类型:', typeof data.value); // console.log('消息类型:', typeof data.value);
console.log('接收时间:', new Date().toLocaleString()); // console.log('接收时间:', new Date().toLocaleString());
console.log('====================='); // console.log('=====================');
useNoticeStore().addNotice({ useNoticeStore().addNotice({
message: data.value, message: data.value,
@@ -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

@@ -1,62 +1,255 @@
import Hls from 'hls.js'; import Hls from 'hls.js';
let hlsInstance: Hls | null = null;
export const initHls = (monitorVideoUrl, monitorVideoRef) => { /**
* 每个播放器实例维护自己的状态,避免全局单例互相踩踏
*/
interface HlsPlayerState {
hls: Hls;
video: HTMLVideoElement;
/** live edge 追赶定时器 */
syncTimer: ReturnType<typeof setInterval> | null;
/** 卡死检测定时器 */
stallTimer: ReturnType<typeof setInterval> | null;
/** 连续加载失败计数 */
consecutiveFailures: number;
/** 是否已销毁 */
destroyed: boolean;
}
const playerStates = new WeakMap<HTMLVideoElement, HlsPlayerState>();
/**
* 直播流追赶逻辑:检测播放位置与 live edge 的差距,超过阈值就 seek
*/
function startLiveSync(state: HlsPlayerState) {
const { hls, video } = state;
state.syncTimer = setInterval(() => {
if (state.destroyed) return;
try {
const buffered = video.buffered;
if (buffered.length === 0) return;
// 取最后一个 buffer range 的 end 作为 live edge
const liveEdge = buffered.end(buffered.length - 1);
const currentTime = video.currentTime;
const gap = liveEdge - currentTime;
// 超过 6 秒就 seek 到 live edge 附近
if (gap > 300) {
video.currentTime = liveEdge - 2;
}
} catch {
// ignore
}
}, 60 * 1000);
}
function stopLiveSync(state: HlsPlayerState) {
if (state.syncTimer) {
clearInterval(state.syncTimer);
state.syncTimer = null;
}
if (state.stallTimer) {
clearInterval(state.stallTimer);
state.stallTimer = null;
}
}
export const initHls = (monitorVideoUrl, monitorVideoRef, isDialogVisible?: () => boolean, onStall?: () => void) => {
console.log('isDialogVisible', isDialogVisible);
const url = monitorVideoUrl.value; const url = monitorVideoUrl.value;
const video = monitorVideoRef.value; const video = monitorVideoRef.value;
if (!url || !video) return; if (!url || !video) return;
// 销毁已有实例 // 销毁该 video 元素上已有实例(不再操作全局变量)
if (hlsInstance) { const existingState = playerStates.get(video);
if (existingState) {
try { try {
hlsInstance.destroy(); stopLiveSync(existingState);
} catch (e) { existingState.destroyed = true;
existingState.hls.destroy();
} catch {
// ignore // ignore
} }
hlsInstance = null; playerStates.delete(video);
} }
if (Hls.isSupported()) { if (Hls.isSupported()) {
const hls = new Hls(); const hls = new Hls({
// ★ ZLM 注册流需要时间,配置自动重试
manifestLoadingMaxRetry: 5,
manifestLoadingRetryDelay: 1000,
manifestLoadingTimeOut: 5000,
levelLoadingMaxRetry: 5,
levelLoadingRetryDelay: 500,
lowLatencyMode: false,
startLevel: -1,
// ★ 直播流关键配置
liveSyncDurationCount: 3,
liveDurationInfinity: true,
// 缓冲区上限,防止无限增长导致内存爆掉
maxBufferLength: 5,
maxMaxBufferLength: 10,
// manifest 加载:最多重试 5 次,每次间隔 1 秒,单次超时 10 秒
// manifestLoadingMaxRetry: 5,
// manifestLoadingRetryDelay: 1000,
// manifestLoadingTimeOut: 10000,
// // level清晰度加载最多重试 3 次,每次间隔 1 秒,单次超时 15 秒
// levelLoadingMaxRetry: 3,
// levelLoadingRetryDelay: 1000,
// levelLoadingTimeOut: 15000,
// lowLatencyMode: true,
// startLevel: -1,
// // 直播同步:滞后 2 个分片时自动追赶 live edge
// liveSyncDurationCount: 2,
// liveDurationInfinity: true,
// // 不保留已播放过的旧缓冲,减少内存占用
backBufferLength: 0
// // 缓冲区上限 5~10 秒,限制同时发出的分片请求数,防止占满连接池
// maxBufferLength: 5,
// maxMaxBufferLength: 10,
// // 分片加载:最多重试 3 次,每次间隔 2 秒,单次超时 15 秒
// fragLoadingMaxRetry: 3,
// fragLoadingRetryDelay: 2000,
// fragLoadingTimeOut: 15000
// // 允许缓冲区内最大 0.5 秒的空洞,避免小卡顿触发不必要的重载
// maxBufferHole: 0.5
});
hls.loadSource(url); hls.loadSource(url);
hls.attachMedia(video); hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => { hls.on(Hls.Events.MANIFEST_PARSED, () => {
// 自动播放(若浏览器允许)
try { try {
video.play().catch(() => {}); video.play().catch(() => {});
} catch (e) {} } catch {
// ignore
}
});
const state: HlsPlayerState = {
hls,
video,
syncTimer: null,
stallTimer: null,
consecutiveFailures: 0,
destroyed: false
};
playerStates.set(video, state);
/**
* 完善的错误处理
*/
hls.on(Hls.Events.ERROR, (event, data) => {
// 对话框已关闭则不再恢复
if (isDialogVisible && !isDialogVisible()) return;
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
if (data.details === 'manifestLoadError' || data.details === 'manifestLoadTimeOut') {
//console.error('网络错误');
ElMessage.error('[HLS]网络错误,尝试恢复...');
onStall();
} else {
ElMessage.error('[HLS] 网络错误,自动恢复...');
hls.startLoad();
}
break;
case Hls.ErrorTypes.MEDIA_ERROR:
ElMessage.error('[HLS] 媒体解码错误,尝试恢复...');
hls.recoverMediaError();
break;
default:
ElMessage.error('[HLS] 不可恢复的错误,销毁实例');
state.destroyed = true;
stopLiveSync(state);
hls.destroy();
playerStates.delete(video);
break;
}
} else {
// ★ 非致命错误也处理,防止问题累积
// 记录加载失败
state.consecutiveFailures++;
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
// 分片加载失败等 → 重新加载
if (data.details === 'fragLoadError' || data.details === 'fragLoadTimeOut') {
ElMessage.error(`[HLS] 分片加载问题 (${data.details}),第 ${state.consecutiveFailures} 次失败,尝试恢复...`);
hls.startLoad(hls.currentLevel);
}
break;
case Hls.ErrorTypes.MEDIA_ERROR:
if (data.details === 'bufferAppendError' || data.details === 'bufferFullError') {
ElMessage.error(`[HLS] 缓冲区问题 (${data.details}),尝试恢复...`);
hls.recoverMediaError();
}
break;
default:
ElMessage.error(`[HLS] 非致命错误: ${data.type} - ${data.details}`);
break;
}
}
});
/**
* 卡死检测:每 10 秒检查一次是否还在正常加载分片
* 如果连续多次加载失败,触发 onStall 回调(自动刷新)
*/
state.stallTimer = setInterval(() => {
if (state.destroyed || !onStall) return;
if (state.consecutiveFailures >= 3) {
ElMessage.error(`[HLS] 连续 ${state.consecutiveFailures} 次加载失败,触发自动刷新`);
state.consecutiveFailures = 0;
onStall();
}
}, 10000);
// 播放开始后启动 live edge 追赶
hls.on(Hls.Events.FRAG_BUFFERED, () => {
// 加载成功,重置失败计数
state.consecutiveFailures = 0;
if (!state.syncTimer) {
startLiveSync(state);
}
}); });
hlsInstance = hls;
} else if (video.canPlayType('application/vnd.apple.mpegurl')) { } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari/原生支持 HLS // Safari 原生 HLS
video.src = url; video.src = url;
video.addEventListener('loadedmetadata', () => { video.addEventListener('loadedmetadata', () => {
try { try {
video.play().catch(() => {}); video.play().catch(() => {});
} catch (e) {} } catch {
// ignore
}
}); });
} else { } else {
//proxy?.$modal.msgWarning('当前浏览器不支持 HLS 播放');
ElMessage.error('当前浏览器不支持 HLS 播放'); ElMessage.error('当前浏览器不支持 HLS 播放');
} }
}; };
export const destroyHls = (monitorVideoRef) => { export const destroyHls = (monitorVideoRef) => {
try {
if (hlsInstance) {
hlsInstance.destroy();
hlsInstance = null;
}
} catch (e) {}
const video = monitorVideoRef.value; const video = monitorVideoRef.value;
if (video) { if (video) {
// 销毁该 video 元素上的 HLS 实例
const state = playerStates.get(video);
if (state) {
try {
stopLiveSync(state);
state.destroyed = true;
state.hls.destroy();
} catch {
// ignore
}
playerStates.delete(video);
}
// 清理原生 src避免残留 // 清理原生 src避免残留
try { try {
video.pause(); video.pause();
video.removeAttribute('src'); video.removeAttribute('src');
// 对某些浏览器,需要调用 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

@@ -39,20 +39,19 @@
<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 />
<el-table-column label="报警图片" align="center" prop="alarmImage" min-width="120"> <!-- pay content -->
<!-- <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"
@@ -64,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>
@@ -136,7 +135,7 @@ import { storeToRefs } from 'pinia';
import { useWarningListStore } from '@/store/modules/warnningList'; import { useWarningListStore } from '@/store/modules/warnningList';
const warningListStore = useWarningListStore(); const warningListStore = useWarningListStore();
const { getWarningList, hanldeAction } = warningListStore; const { getWarningList, handleAction } = warningListStore;
const { warningListTotal, warningList } = storeToRefs(warningListStore); const { warningListTotal, warningList } = storeToRefs(warningListStore);
const deviceStore = useDeviceStore(); const deviceStore = useDeviceStore();
@@ -173,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: {
@@ -193,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];
@@ -221,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;
@@ -238,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 {
@@ -246,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) {
@@ -255,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 {
@@ -263,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('数据异常');
@@ -296,4 +311,11 @@ onMounted(() => {
getList(); getList();
getDeviceName(); getDeviceName();
}); });
//监听数据源变化,自动刷新列表
watch(warningListTotal, async (newVal) => {
//console.log('监听到报警列表变化,自动刷新列表', newVal);
//数据源不同, 接口复用请求多了一遍不属于bug
await getList();
});
</script> </script>

View File

@@ -65,14 +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="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-tag v-if="scope.row.batteryLevel1">{{ scope.row.batteryLevel1 + '%' }}</el-tag>
<el-tag v-else type="info"> 未获取 </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-if="!scope.row.batteryLevel1 && !scope.row.batteryLevel2"> 未获取 </el-tag>
</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> </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>
@@ -92,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>
@@ -123,8 +230,10 @@
<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 type="primary" class="shrink-0" @click="handleVideo(videosInfo)">切换摄像头</el-button> -->
<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>
@@ -133,7 +242,7 @@
</template> </template>
<div class="monitor-video-wrap"> <div class="monitor-video-wrap">
<div class="monitor-video-stack"> <div class="monitor-video-stack">
<video ref="monitorVideoRef" class="monitor-video" controls preload="metadata" :src="monitorVideoUrl">您的浏览器不支持视频播放</video> <video ref="monitorVideoRef" class="monitor-video" preload="metadata">您的浏览器不支持视频播放</video>
<div v-show="monitorDrawCollecting" class="monitor-draw-overlay" @click.stop="onMonitorDrawClick" /> <div v-show="monitorDrawCollecting" class="monitor-draw-overlay" @click.stop="onMonitorDrawClick" />
<div v-if="monitorRectPx" class="monitor-draw-rect" :style="drawRectStyle" /> <div v-if="monitorRectPx" class="monitor-draw-rect" :style="drawRectStyle" />
<div v-if="monitorRectPx" class="monitor-draw-label" :style="drawLabelStyle">当前报警区域</div> <div v-if="monitorRectPx" class="monitor-draw-label" :style="drawLabelStyle">当前报警区域</div>
@@ -146,12 +255,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 { initHls, destroyHls } from '@/utils/video';
import Hls from 'hls.js';
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';
import { AnyARecord } from 'dns';
const videosStore = useVideosStore(); const videosStore = useVideosStore();
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -165,6 +274,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>();
@@ -180,65 +290,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 visibilityHandler: (() => void) | null = null;
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);
@@ -394,19 +447,21 @@ const resetMonitorDraw = () => {
monitorRectPx.value = null; monitorRectPx.value = null;
currentDrawArea.value = null; currentDrawArea.value = null;
}; };
// const videosInfo = ref();
const handleSee = async (row?: any) => { const handleSee = async (row?: any) => {
console.log('1111111111111111', row, row.drawArea); console.log('1111111111111111', row, row.drawArea);
// videosInfo.value = row;
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);
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 || '';
@@ -419,7 +474,7 @@ const handleSee = async (row?: any) => {
// 等待 DOM 更新后初始化 Hls // 等待 DOM 更新后初始化 Hls
await nextTick(); await nextTick();
initHls(); initHls(monitorVideoUrl, monitorVideoRef, () => dialog1.visible, handleRefresh);
// 等待视频加载完成后初始化 ResizeObserver以便能正确获取视频尺寸 // 等待视频加载完成后初始化 ResizeObserver以便能正确获取视频尺寸
initVideoResizeObserver(); initVideoResizeObserver();
@@ -454,6 +509,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,13 +597,28 @@ 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) => {
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();
cleanupVisibilityChange();
done(); done();
//刷新列表 //刷新列表
getList(); getList();
@@ -580,6 +662,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('请在视频画面上连续点击两个点,生成报警区域');
}; };
@@ -676,7 +759,7 @@ const saveImage = async () => {
const handleMonitorDialogOpen = () => { const handleMonitorDialogOpen = () => {
// 初始化 Hls 与 video // 初始化 Hls 与 video
initHls(); initHls(monitorVideoUrl, monitorVideoRef, () => dialog1.visible);
initVideoResizeObserver(); initVideoResizeObserver();
monitorDrawCollecting.value = false; monitorDrawCollecting.value = false;
monitorDrawPoints.value = []; monitorDrawPoints.value = [];
@@ -706,6 +789,46 @@ const handleExport = () => {
onMounted(() => { onMounted(() => {
getList(); getList();
}); });
watch(
() => seeSee.value,
(newValue) => {
videoName.value = newValue === 1 ? '绘制区域' : '实时监控';
}
);
/** 刷新按钮操作 */
// const handleVideo = async (row?: any) => {
// console.log('11111111111', row);
// handleSee(row);
// };
/** 刷新按钮操作 */
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, () => dialog1.visible, handleRefresh);
}
};
/** 电池颜色:>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">
@@ -790,4 +913,32 @@ onMounted(() => {
padding: 2px 6px; padding: 2px 6px;
border-radius: 2px; border-radius: 2px;
} }
.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

@@ -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

@@ -91,8 +91,8 @@ const { t } = useI18n();
const loginForm = ref<LoginData>({ const loginForm = ref<LoginData>({
tenantId: '000000', tenantId: '000000',
username: 'admin', username: '',
password: 'admin123', password: '',
rememberMe: false, rememberMe: false,
code: '', code: '',
uuid: '' uuid: ''
@@ -265,8 +265,8 @@ onMounted(() => {
.login-brand { .login-brand {
flex: 0 0 auto; flex: 0 0 auto;
width: min(420px, 90vw); width: min(450px, 90vw);
max-width: 420px; max-width: 450px;
height: 450px; height: 450px;
max-height: 450px; max-height: 450px;
box-sizing: border-box; box-sizing: border-box;
@@ -333,8 +333,8 @@ onMounted(() => {
} }
.login-brand { .login-brand {
width: min(420px, 100%); width: min(450px, 100%);
max-width: 420px; max-width: 450px;
height: 450px; height: 450px;
max-height: 450px; max-height: 450px;
border-radius: var(--app-radius-lg); border-radius: var(--app-radius-lg);
@@ -378,7 +378,7 @@ onMounted(() => {
flex-direction: column; flex-direction: column;
box-sizing: border-box; box-sizing: border-box;
min-height: 0; min-height: 0;
width: min(420px, 90vw); width: min(450px, 90vw);
height: 450px; height: 450px;
max-height: 450px; max-height: 450px;
padding: 44px 48px 36px; padding: 44px 48px 36px;

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>

25
start.sh Normal file
View File

@@ -0,0 +1,25 @@
#!/bin/bash
# 启动命令 : ./start.sh
echo "📦 正在安装依赖..."
pnpm install
echo "🚀 正在启动开发服务器..."
pnpm run dev
echo "⏳ 等待服务器启动..."
sleep 5
echo "🌐 正在打开浏览器..."
if command -v start &> /dev/null; then
# Windows
start http://localhost:80
elif command -v xdg-open &> /dev/null; then
# Linux
xdg-open http://localhost:80
elif command -v open &> /dev/null; then
# macOS
open http://localhost:80
else
echo "⚠️ 请手动打开浏览器访问: http://localhost:80"
fi

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://47.104.215.165: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), '')