Compare commits

...

11 Commits

Author SHA1 Message Date
龚皓
5c571280e3 ReadMe 2024-12-05 16:16:19 +08:00
龚皓
3e4e455dbe latest-V.1 2024-12-05 16:05:29 +08:00
龚皓
62a7971f7a 滚轮查看 2024-11-26 16:26:58 +08:00
龚皓
056c1c95bf 定时刷新5分 2024-11-26 16:25:47 +08:00
龚皓
7551409394 告警弹窗 2024-11-26 16:24:48 +08:00
龚皓
9081420148 删除告警功能 2024-11-26 16:23:18 +08:00
龚皓
220bf593aa 其他(测试,布局滚轮适应添加宽度) 2024-11-26 16:20:27 +08:00
龚皓
bcb0c77111 用户编辑限制 2024-11-26 16:17:36 +08:00
龚皓
ccf101167a 滚零 2024-11-26 16:11:50 +08:00
龚皓
207913a253 delEvnents 2024-11-26 16:09:50 +08:00
龚皓
f0b6d900ac pinina 2024-11-26 16:07:57 +08:00
22 changed files with 2015 additions and 1365 deletions

157
README.md
View File

@@ -45,6 +45,7 @@ npm run build
> - qs:静态资源请求 > - qs:静态资源请求
> - vue:框架 > - vue:框架
> - vue-router:路由 > - vue-router:路由
> -
@@ -384,72 +385,23 @@ await apiInstance.updateCamera(token, cameraId, cameraJson);
- 时间段规则设置
> 表单数据显示摄像idnamemode对应的规则显示对应的id,name,mode,schedule
>
> 其中摄像mode有ON和OFF两种状态规则中mode有On,OFF和schedule三中状态规则模式只有在schedule状态才会显示schedule中内容type
>
> schedule中type又分为每日daily和每周weekly两周状态其中dayily直接设置当日时间范围time_slotsweekly可选择周几week_day和对应当天的当日时间范围time_slots
>
> time_slots为两个数字对应当天开始时间和结束时间计算交互方式为选择时间点
>
> 几点几分然后计算这个时间总计有多少分钟例如1点10分对应数字为70
- 结果返回信息修改(用户提示信息)
```
public async updateRule(token: string | null = null, rules: RuleData[]): Promise<any[]> {
const results: any[] = [];
for (const rule of rules) {
const url = `${this.superRule}/${rule.id}`;
try {
const res = await this.axios.patch(url, rule, this._authHeader(token));
if (res.data.err.ec === 0) {
results.push({
success: true,
ruleId: rule.id,
data: res.data.ret,
});
} else {
results.push({
success: false,
ruleId: rule.id,
error: res.data.err.dm,
});
}
} catch (error: any) {
results.push({
success: false,
ruleId: rule.id,
error: error.message || '未知错误',
});
}
}
return results;
}
```
- 时间段设置
> 添加时间段schedule的设置组件显示开始时间点和结束时间数据格式化方式为将代表累计分钟的数字转换为时间点每60分钟为1小时数字范围在0到1440实例[56,65]代表0:56到1:05组件显示将数字组转换为时间段请求时将组件时间段显示为数字组。 > 添加时间段schedule的设置组件显示开始时间点和结束时间数据格式化方式为将代表累计分钟的数字转换为时间点每60分钟为1小时数字范围在0到1440实例[56,65]代表0:56到1:05组件显示将数字组转换为时间段请求时将组件时间段显示为数字组。
> >
> 时间段结束时间点不能大于开始时间点,多时间段添加不能有重合部分。 > 时间段结束时间点不能大于开始时间点,多时间段添加不能有重合部分。
> >
> 设置规则只有在rule.mode为schedule时才可设置时间段携带参数日期类型和多个时间段数字组合在未保存切换rule。mode模式时间段不会因为隐藏组件而清空数值始终显示开始设定好的默认值当提交时处于时间段模式至少得设置一个时间段数组否则无法保存若处在schedule情况下保存携带参数请求若rule.mode处于on或者off,代表没有时间段设置那么schedule下的所有参数及时原来有值也需要伴随请求清空默认值 > 设置规则只有在rule.mode为schedule时才可设置时间段携带参数日期类型和多个时间段数字组合在未保存切换rule。mode模式时间段不会因为隐藏组件而清空数值始终显示开始设定好的默认值当提交时处于时间段模式至少得设置一个时间段数组否则无法保存若处在schedule情况下保存携带参数请求若rule.mode处于on或者off,代表没有时间段设置那么schedule下的所有参数及时原来有值也需要伴随请求清空默认值,具体如下:
> 重新修改这段CameraRule.vue代码 1.在rule.mode为schedule模式添加时间段time_slots的组件添加时间段类型输入框值默认为daily不可修改 2.开始时间和结束时间的组件,时间通过分钟数转换为格式化的时间(小时:分钟有删除按钮可删除临时的时间段设置使用数字范围0-1440单个时间点代表0点到设置的时间点累计的分钟数字 3.时间段的校验,结束时间不能小于开始时间,多时间段之间不能有重叠。 4.如果 rule.mode 为 schedule则必须包含至少一个时间段如果 rule.mode 是 on 或 off清空所有时间段和时间段类型 > 1.在rule.mode为schedule模式添加时间段time_slots的组件添加时间段类型输入框值默认为daily不可修改
>
> 2.开始时间和结束时间的组件,时间通过分钟数转换为格式化的时间(小时:分钟有删除按钮可删除临时的时间段设置使用数字范围0-1440单个时间点代表0点到设置的时间点累计的分钟数字
>
> 3.时间段的校验,结束时间不能小于开始时间,多时间段之间不能有重叠。
>
> 4.如果 rule.mode 为 schedule则必须包含至少一个时间段如果 rule.mode 是 on 或 off清空所有时间段和时间段类型
@@ -512,71 +464,6 @@ public async updateRule(token: string | null = null, rules: RuleData[]): Promise
```
模拟静态数据实例,
"camera": {
"id": 1,
"name": "421枪机",
"mode": "on",
"status": "online",
"rules": [
{
"id": 1,
"camera": 1,
"name": "入侵test2",
"mode": "schedule",
"algo": "intrude",
"schedule": {
"type": "daily",
"time_slots": [
[
118,
898
],
[
0,
60
]
]
},
},
{
"id": 3,
"camera": 1,
"name": "人员逗留",
"mode": "schedule",
"algo": "personnel_staying",
"schedule": {
"type": "daily",
"time_slots": [
[
180,
420
]
]
},
}
],
"snapshot": "http://192.168.28.33:8000/media/cameras/1/snapshot.jpg",
}
读取显示rules中多条数据每条rule中rule.mode对应el-radio三种状态在rule.model为schedule时
```
## 日志 ## 日志
### 2024.11.21 ### 2024.11.21
@@ -624,6 +511,30 @@ public async updateRule(token: string | null = null, rules: RuleData[]): Promise
### 2024.11.26
- 对话框大图显示
- 删除告警按钮
- 主页定时刷新
- 布局滚动查看调整
- 用户删除限制
### 2024.12.3
- 告警设置
- 消息队列设置
- 声音设置
- 弹窗动画添加
- 弹窗队列添加错误重试机制2s*5次

240
package-lock.json generated
View File

@@ -21,6 +21,7 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"pinia": "^2.2.6",
"vue": "^3.4.29", "vue": "^3.4.29",
"vue-router": "^4.4.0" "vue-router": "^4.4.0"
}, },
@@ -63,27 +64,27 @@
} }
}, },
"node_modules/@babel/helper-string-parser": { "node_modules/@babel/helper-string-parser": {
"version": "7.24.8", "version": "7.25.9",
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
"integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/helper-validator-identifier": { "node_modules/@babel/helper-validator-identifier": {
"version": "7.24.7", "version": "7.25.9",
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
"integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.25.3", "version": "7.26.2",
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.25.3.tgz", "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.26.2.tgz",
"integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==",
"dependencies": { "dependencies": {
"@babel/types": "^7.25.2" "@babel/types": "^7.26.0"
}, },
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@@ -104,13 +105,12 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.25.2", "version": "7.26.0",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.25.2.tgz", "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.26.0.tgz",
"integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.24.8", "@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.24.7", "@babel/helper-validator-identifier": "^7.25.9"
"to-fast-properties": "^2.0.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -963,49 +963,49 @@
} }
}, },
"node_modules/@vue/compiler-core": { "node_modules/@vue/compiler-core": {
"version": "3.4.37", "version": "3.5.13",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.4.37.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
"integrity": "sha512-ZDDT/KiLKuCRXyzWecNzC5vTcubGz4LECAtfGPENpo0nrmqJHwuWtRLxk/Sb9RAKtR9iFflFycbkjkY+W/PZUQ==", "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
"dependencies": { "dependencies": {
"@babel/parser": "^7.24.7", "@babel/parser": "^7.25.3",
"@vue/shared": "3.4.37", "@vue/shared": "3.5.13",
"entities": "^5.0.0", "entities": "^4.5.0",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"source-map-js": "^1.2.0" "source-map-js": "^1.2.0"
} }
}, },
"node_modules/@vue/compiler-dom": { "node_modules/@vue/compiler-dom": {
"version": "3.4.37", "version": "3.5.13",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.4.37.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
"integrity": "sha512-rIiSmL3YrntvgYV84rekAtU/xfogMUJIclUMeIKEtVBFngOL3IeZHhsH3UaFEgB5iFGpj6IW+8YuM/2Up+vVag==", "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
"dependencies": { "dependencies": {
"@vue/compiler-core": "3.4.37", "@vue/compiler-core": "3.5.13",
"@vue/shared": "3.4.37" "@vue/shared": "3.5.13"
} }
}, },
"node_modules/@vue/compiler-sfc": { "node_modules/@vue/compiler-sfc": {
"version": "3.4.37", "version": "3.5.13",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.4.37.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
"integrity": "sha512-vCfetdas40Wk9aK/WWf8XcVESffsbNkBQwS5t13Y/PcfqKfIwJX2gF+82th6dOpnpbptNMlMjAny80li7TaCIg==", "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
"dependencies": { "dependencies": {
"@babel/parser": "^7.24.7", "@babel/parser": "^7.25.3",
"@vue/compiler-core": "3.4.37", "@vue/compiler-core": "3.5.13",
"@vue/compiler-dom": "3.4.37", "@vue/compiler-dom": "3.5.13",
"@vue/compiler-ssr": "3.4.37", "@vue/compiler-ssr": "3.5.13",
"@vue/shared": "3.4.37", "@vue/shared": "3.5.13",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.10", "magic-string": "^0.30.11",
"postcss": "^8.4.40", "postcss": "^8.4.48",
"source-map-js": "^1.2.0" "source-map-js": "^1.2.0"
} }
}, },
"node_modules/@vue/compiler-ssr": { "node_modules/@vue/compiler-ssr": {
"version": "3.4.37", "version": "3.5.13",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.4.37.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
"integrity": "sha512-TyAgYBWrHlFrt4qpdACh8e9Ms6C/AZQ6A6xLJaWrCL8GCX5DxMzxyeFAEMfU/VFr4tylHm+a2NpfJpcd7+20XA==", "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.4.37", "@vue/compiler-dom": "3.5.13",
"@vue/shared": "3.4.37" "@vue/shared": "3.5.13"
} }
}, },
"node_modules/@vue/compiler-vue2": { "node_modules/@vue/compiler-vue2": {
@@ -1048,49 +1048,49 @@
} }
}, },
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
"version": "3.4.37", "version": "3.5.13",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.4.37.tgz", "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.13.tgz",
"integrity": "sha512-UmdKXGx0BZ5kkxPqQr3PK3tElz6adTey4307NzZ3whZu19i5VavYal7u2FfOmAzlcDVgE8+X0HZ2LxLb/jgbYw==", "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
"dependencies": { "dependencies": {
"@vue/shared": "3.4.37" "@vue/shared": "3.5.13"
} }
}, },
"node_modules/@vue/runtime-core": { "node_modules/@vue/runtime-core": {
"version": "3.4.37", "version": "3.5.13",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.4.37.tgz", "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
"integrity": "sha512-MNjrVoLV/sirHZoD7QAilU1Ifs7m/KJv4/84QVbE6nyAZGQNVOa1HGxaOzp9YqCG+GpLt1hNDC4RbH+KtanV7w==", "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
"dependencies": { "dependencies": {
"@vue/reactivity": "3.4.37", "@vue/reactivity": "3.5.13",
"@vue/shared": "3.4.37" "@vue/shared": "3.5.13"
} }
}, },
"node_modules/@vue/runtime-dom": { "node_modules/@vue/runtime-dom": {
"version": "3.4.37", "version": "3.5.13",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.4.37.tgz", "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
"integrity": "sha512-Mg2EwgGZqtwKrqdL/FKMF2NEaOHuH+Ks9TQn3DHKyX//hQTYOun+7Tqp1eo0P4Ds+SjltZshOSRq6VsU0baaNg==", "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
"dependencies": { "dependencies": {
"@vue/reactivity": "3.4.37", "@vue/reactivity": "3.5.13",
"@vue/runtime-core": "3.4.37", "@vue/runtime-core": "3.5.13",
"@vue/shared": "3.4.37", "@vue/shared": "3.5.13",
"csstype": "^3.1.3" "csstype": "^3.1.3"
} }
}, },
"node_modules/@vue/server-renderer": { "node_modules/@vue/server-renderer": {
"version": "3.4.37", "version": "3.5.13",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.4.37.tgz", "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
"integrity": "sha512-jZ5FAHDR2KBq2FsRUJW6GKDOAG9lUTX8aBEGq4Vf6B/35I9fPce66BornuwmqmKgfiSlecwuOb6oeoamYMohkg==", "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
"dependencies": { "dependencies": {
"@vue/compiler-ssr": "3.4.37", "@vue/compiler-ssr": "3.5.13",
"@vue/shared": "3.4.37" "@vue/shared": "3.5.13"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "3.4.37" "vue": "3.5.13"
} }
}, },
"node_modules/@vue/shared": { "node_modules/@vue/shared": {
"version": "3.4.37", "version": "3.5.13",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.4.37.tgz", "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-nIh8P2fc3DflG8+5Uw8PT/1i17ccFn0xxN/5oE9RfV5SVnd7G0XEFRwakrnNFE/jlS95fpGXDVG5zDETS26nmg==" "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ=="
}, },
"node_modules/@vue/tsconfig": { "node_modules/@vue/tsconfig": {
"version": "0.5.1", "version": "0.5.1",
@@ -1407,9 +1407,9 @@
} }
}, },
"node_modules/entities": { "node_modules/entities": {
"version": "5.0.0", "version": "4.5.0",
"resolved": "https://registry.npmmirror.com/entities/-/entities-5.0.0.tgz", "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-BeJFvFRJddxobhvEdm5GqHzRV/X+ACeuw0/BuuxsCh1EUZcAIz8+kYmBp/LrQuloy6K1f3a0M7+IhmZ7QnkISA==", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"engines": { "engines": {
"node": ">=0.12" "node": ">=0.12"
}, },
@@ -1725,9 +1725,9 @@
} }
}, },
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.11", "version": "0.30.13",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.11.tgz", "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.13.tgz",
"integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "integrity": "sha512-8rYBO+MsWkgjDSOvLomYnzhdwEG51olQ4zL5KXnNJWV5MNmrb4rTZdrtkhxjnD/QyZUqR/Z/XDsUs/4ej2nx0g==",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0" "@jridgewell/sourcemap-codec": "^1.5.0"
} }
@@ -1936,9 +1936,9 @@
} }
}, },
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
}, },
"node_modules/pidtree": { "node_modules/pidtree": {
"version": "0.6.0", "version": "0.6.0",
@@ -1962,10 +1962,60 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/pinia": {
"version": "2.2.6",
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.2.6.tgz",
"integrity": "sha512-vIsR8JkDN5Ga2vAxqOE2cJj4VtsHnzpR1Fz30kClxlh0yCHfec6uoMeM3e/ddqmwFUejK3NlrcQa/shnpyT4hA==",
"dependencies": {
"@vue/devtools-api": "^6.6.3",
"vue-demi": "^0.14.10"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"@vue/composition-api": "^1.4.0",
"typescript": ">=4.4.4",
"vue": "^2.6.14 || ^3.5.11"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.41", "version": "8.4.49",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.41.tgz", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.49.tgz",
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -1982,8 +2032,8 @@
], ],
"dependencies": { "dependencies": {
"nanoid": "^3.3.7", "nanoid": "^3.3.7",
"picocolors": "^1.0.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.0" "source-map-js": "^1.2.1"
}, },
"engines": { "engines": {
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
@@ -2139,9 +2189,9 @@
} }
}, },
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.0", "version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.0.tgz", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -2159,14 +2209,6 @@
"node": ">=12.22" "node": ">=12.22"
} }
}, },
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"engines": {
"node": ">=4"
}
},
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
@@ -2257,15 +2299,15 @@
"dev": true "dev": true
}, },
"node_modules/vue": { "node_modules/vue": {
"version": "3.4.37", "version": "3.5.13",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.4.37.tgz", "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.13.tgz",
"integrity": "sha512-3vXvNfkKTBsSJ7JP+LyR7GBuwQuckbWvuwAid3xbqK9ppsKt/DUvfqgZ48fgOLEfpy1IacL5f8QhUVl77RaI7A==", "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.4.37", "@vue/compiler-dom": "3.5.13",
"@vue/compiler-sfc": "3.4.37", "@vue/compiler-sfc": "3.5.13",
"@vue/runtime-dom": "3.4.37", "@vue/runtime-dom": "3.5.13",
"@vue/server-renderer": "3.4.37", "@vue/server-renderer": "3.5.13",
"@vue/shared": "3.4.37" "@vue/shared": "3.5.13"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "*" "typescript": "*"

View File

@@ -24,6 +24,7 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"pinia": "^2.2.6",
"vue": "^3.4.29", "vue": "^3.4.29",
"vue-router": "^4.4.0" "vue-router": "^4.4.0"
}, },

View File

@@ -1,119 +1,14 @@
<template> <template>
<div id="app" class="app-container"> <div id="app" class="app-container">
<router-view></router-view> <router-view></router-view>
<el-dialog title="告警提示" v-model="globalDialogVisible" width="50%" @close="handleDialogClose"> <GlobalDialog />
<el-row style="margin-bottom: 2vh;">
<el-col :span="17" style="text-align: center;">
<img :src="globalDialogContent.snapshotUrl" alt="告警图片" v-if="globalDialogContent.snapshotUrl"
style="max-width: 100%;" />
<!-- 可选的视频展示 -->
<!-- <video v-if="globalDialogContent.videoUrl" :src="globalDialogContent.videoUrl" controls style="max-width: 100%;"></video> -->
</el-col>
<el-col :span="7" >
<el-row class="dialog-event-col">
<el-col :span="24"><strong >告警编号</strong>{{ globalDialogContent.id }}</el-col>
<el-col :span="24"><strong >告警点位</strong>{{ globalDialogContent.camera?.name }}</el-col>
<el-col :span="24"><strong >告警类型</strong>{{ globalDialogContent.types }}</el-col>
<el-col :span="24"><strong >告警时间</strong>{{ globalDialogContent.started_at }}</el-col>
</el-row>
</el-col>
</el-row>
</el-dialog>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted } from 'vue'; import GlobalDialog from '@/components/GlobalDialog.vue';
import eventBus from '@/utils/eventBus';
import { BoxApi } from '@/utils/boxApi';
import dayjs from 'dayjs';
interface GlobalDialogContent {
id: number | null;
camera_id: number | null;
camera: { name: string };
types: string | null;
started_at: string | null; // 支持 null 或字符串类型
snapshotUrl: string;
videoUrl: string;
}
const apiInstance = new BoxApi();
const globalDialogVisible = ref(false); // 控制全局对话框的可见性
const globalDialogContent = ref<GlobalDialogContent>({
id: null,
camera_id: null,
camera: { name: '' },
types: null,
started_at: null,
snapshotUrl: '',
videoUrl: ''
});
const algorithmMap = ref(new Map()); // 算法类型映射表
// 加载算法映射表
const loadAlgorithms = async () => {
const token = localStorage.getItem('alertToken');
if (token) {
const algorithms = await apiInstance.getAlgorithms(token);
algorithmMap.value = new Map(
algorithms.map((algo: { code_name: string; name: string }) => [algo.code_name, algo.name])
);
} else {
console.error('Token 未找到,请登录');
}
};
// 显示告警详情对话框
const showDialog = async (data: any) => {
const token = localStorage.getItem('alertToken');
if (!token) {
console.error('Token 未找到,请登录');
return;
}
console.log('showDialog>>>>>>>>>>>>>', data);
// 获取告警事件的详细信息
const eventDetails = await apiInstance.getEventById(data.id, token);
const snapshot = eventDetails.mediums.find((item: any) => item.name === 'snapshot');
const video = eventDetails.mediums.find((item: any) => item.name === 'video');
// 更新对话框内容
globalDialogContent.value = {
id: eventDetails.id,
camera_id: eventDetails.camera_id,
camera: eventDetails.camera,
types: algorithmMap.value.get(eventDetails.types),
started_at: formatDateTime(eventDetails.started_at) ,
snapshotUrl: snapshot?.file || '',
videoUrl: video?.file || ''
};
globalDialogVisible.value = true; // 显示对话框
};
const formatDateTime = (isoString: string): string => dayjs(isoString).format('YYYY-MM-DD HH:mm:ss');
// 关闭对话框并重置内容
const handleDialogClose = () => {
globalDialogVisible.value = false;
globalDialogContent.value = {
id: null,
camera_id: null,
camera: { name: '' },
types: null,
started_at: null,
snapshotUrl: '',
videoUrl: ''
};
};
// 生命周期钩子
onMounted(() => {
loadAlgorithms(); // 加载算法数据
eventBus.on('showDialog', showDialog); // 监听事件总线的 showDialog 事件
});
</script> </script>
<style scoped> <style scoped>
@@ -123,9 +18,4 @@ onMounted(() => {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.dialog-event-col{
font-size: 14px;
gap: 30px;
padding: 20px;
}
</style> </style>

View File

@@ -152,6 +152,21 @@ const filterCameras = () => {
// console.log("filteredCameras>>:", filteredCameras.value); // console.log("filteredCameras>>:", filteredCameras.value);
}; };
// 名称+ID查询
// const filterCameras = () => {
// const keyword = searchKeyword.value.trim().toLowerCase();
// if (!keyword) {
// filteredCameras.value = [...cameras.value];
// } else {
// filteredCameras.value = cameras.value.filter(camera => {
// const idMatch = camera.id.toString().toLowerCase().includes(keyword);
// const nameMatch = camera.name.toLowerCase().includes(keyword);
// return idMatch || nameMatch;
// });
// }
// };
const selectCameraById = (cameraId) => { const selectCameraById = (cameraId) => {
const camera = cameras.value.find(c => c.id === cameraId); const camera = cameras.value.find(c => c.id === cameraId);
if (camera && !selectedCameras.value.some(c => c.id === camera.id)) { if (camera && !selectedCameras.value.some(c => c.id === camera.id)) {
@@ -175,7 +190,7 @@ const handleSettings = async (cameraId) => {
}; };
const handleSaveResult = ({ success, message }) => { const handleSaveResult = ({ success, message }) => {
console.log('收到子组件保存结果事件:', { success, message }); // console.log('收到子组件保存结果事件:', { success, message });
ElMessage({ ElMessage({
message, message,
@@ -517,16 +532,17 @@ onBeforeUnmount(() => {
width: 68vw; width: 68vw;
height: 55vh; height: 55vh;
max-height: 58vh; max-height: 58vh;
overflow-y: scroll; /* overflow-y: scroll; */
scrollbar-width: none; overflow-y: auto;
scrollbar-width:thin;
/* background-color: #ffffff; */ /* background-color: #ffffff; */
background-color: #F1F1F1; background-color: #F1F1F1;
/* background-color: #c71515; */ /* background-color: #c71515; */
} }
.camera-grid::-webkit-scrollbar { /* .camera-grid::-webkit-scrollbar {
display: none; display: none;
} } */
.grid-item { .grid-item {
margin: 1vh; margin: 1vh;

View File

@@ -0,0 +1,220 @@
<template>
<el-dialog title="告警提示" v-model="globalDialogVisible" width="50%" @close="handleDialogClose">
<el-row style="margin-bottom: 2vh;">
<el-col :span="17" style="text-align: center;">
<img :src="globalDialogContent.snapshotUrl" alt="告警图片" v-if="globalDialogContent.snapshotUrl"
style="max-width: 100%;" />
<!-- 可选的视频展示 -->
<!-- <video v-if="globalDialogContent.videoUrl" :src="globalDialogContent.videoUrl" controls style="max-width: 100%;"></video> -->
</el-col>
<el-col :span="7">
<el-row class="dialog-event-col">
<el-col :span="24"><strong>告警编号</strong>{{ globalDialogContent.id }}</el-col>
<el-col :span="24"><strong>告警点位</strong>{{ globalDialogContent.camera?.name }}</el-col>
<el-col :span="24"><strong>告警类型</strong>{{ globalDialogContent.types }}</el-col>
<el-col :span="24"><strong>告警时间</strong>{{ globalDialogContent.started_at }}</el-col>
</el-row>
</el-col>
</el-row>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted,watch } from 'vue';
import eventBus from '@/utils/eventBus';
import { BoxApi } from '@/utils/boxApi';
import dayjs from 'dayjs';
interface GlobalDialogContent {
id: number | null;
camera_id: number | null;
camera: { name: string };
types: string | null;
started_at: string | null; // 支持 null 或字符串类型
snapshotUrl: string;
videoUrl: string;
}
const apiInstance = new BoxApi();
const globalDialogVisible = ref(false); // 控制全局对话框的可见性
const globalDialogContent = ref<GlobalDialogContent>({
id: null,
camera_id: null,
camera: { name: '' },
types: null,
started_at: null,
snapshotUrl: '',
videoUrl: ''
});
const algorithmMap = ref(new Map()); // 算法类型映射表
const requestQueue: any[] = []; // 消息队列
let isProcessing = false; // 标志是否正在处理队列
// 加载算法映射表
const loadAlgorithms = async () => {
const token = localStorage.getItem('alertToken');
if (token) {
const algorithms = await apiInstance.getAlgorithms(token);
algorithmMap.value = new Map(
algorithms.map((algo: { code_name: string; name: string }) => [algo.code_name, algo.name])
);
} else {
console.error('Token 未找到,请登录');
}
};
const fetchWithRetry = async (fetchFn: () => Promise<any>, retries = 5, delay = 2000): Promise<any> => {
for (let i = 0; i < retries; i++) {
try {
return await fetchFn();
} catch (error) {
console.error(`Retry ${i + 1} failed:`, error);
if (i < retries - 1) await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error('All retries failed');
};
// const showDialog = async (data: any) => {
// const token = localStorage.getItem('alertToken');
// if (!token) {
// console.error('Token 未找到,请登录');
// return;
// }
// console.log('弹窗接收>>>>>>>>>>>>>', data);
// const eventDetails = await apiInstance.getEventById(data.id, token);
// console.log('showDialog>>>>>>>>>>>>>', eventDetails);
// const snapshot = eventDetails.mediums.find((item: any) => item.name === 'snapshot');
// const video = eventDetails.mediums.find((item: any) => item.name === 'video');
// globalDialogContent.value = {
// id: eventDetails.id,
// camera_id: eventDetails.camera_id,
// camera: eventDetails.camera,
// types: algorithmMap.value.get(eventDetails.types),
// started_at: formatDateTime(eventDetails.started_at),
// snapshotUrl: snapshot?.file || '',
// videoUrl: video?.file || ''
// };
// globalDialogVisible.value = true;
// };
const processQueue = async () => {
if (isProcessing || requestQueue.length === 0) return; // 正在处理或队列为空时不执行
isProcessing = true; // 标记正在处理
const data = requestQueue.shift(); // 从队列中取出数据
try {
const token = localStorage.getItem('alertToken');
if (!token) throw new Error('Token 未找到,请登录');
// 使用延时重试获取数据
const eventDetails = await fetchWithRetry(() => apiInstance.getEventById(data.id, token));
// console.log('processQueue>>>>>>>>>>>>>', eventDetails);
if (!eventDetails || !eventDetails.mediums) {
console.error('Event details or mediums not found:', eventDetails);
return;
}
const snapshot = eventDetails.mediums.find((item: any) => item.name === 'snapshot');
const video = eventDetails.mediums.find((item: any) => item.name === 'video');
globalDialogContent.value = {
id: eventDetails.id,
camera_id: eventDetails.camera_id,
camera: eventDetails.camera,
types: algorithmMap.value.get(eventDetails.types) || '未知类型',
started_at: formatDateTime(eventDetails.started_at),
snapshotUrl: snapshot?.file || '',
videoUrl: video?.file || ''
};
globalDialogVisible.value = true; // 显示对话框
} catch (error) {
console.error('Error processing data in queue:', error);
} finally {
isProcessing = false;
processQueue(); // 处理队列中的下一个请求
}
};
const enqueueRequest = (data: any) => {
requestQueue.push(data);
processQueue();
};
const formatDateTime = (isoString: string): string => dayjs(isoString).format('YYYY-MM-DD HH:mm:ss');
// 关闭对话框并重置内容
const handleDialogClose = () => {
globalDialogVisible.value = false;
globalDialogContent.value = {
id: null,
camera_id: null,
camera: { name: '' },
types: null,
started_at: null,
snapshotUrl: '',
videoUrl: ''
};
};
watch(globalDialogContent, (newValue, oldValue) => {
if (newValue !== oldValue) {
const dialogElement = document.querySelector('.dialog-event-col');
if (dialogElement) {
dialogElement.classList.add('border-blink');
setTimeout(() => {
dialogElement.classList.remove('border-blink');
}, 3000);
}
}
});
onMounted(async () => {
await loadAlgorithms();
// eventBus.on('showDialog', showDialog);
eventBus.on('showDialog', enqueueRequest);
});
onUnmounted(() => {
// eventBus.off('showDialog', showDialog);
eventBus.off('showDialog', enqueueRequest);
});
</script>
<style scoped>
.dialog-event-col {
font-size: 14px;
gap: 30px;
padding: 20px;
/* transition: border 0.3s ease-in-out; */
}
.border-blink {
animation: borderBlink 1s infinite;
}
@keyframes borderBlink {
0% {
border: 2px solid red;
}
50% {
border: 2px solid transparent;
}
100% {
border: 2px solid red;
}
}
</style>

View File

@@ -29,11 +29,13 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, nextTick } from 'vue'; import { ref, onMounted, nextTick, onBeforeUnmount } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { BoxApi } from '@/utils/boxApi.ts'; import { BoxApi } from '@/utils/boxApi.ts';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { useGlobalTimerStore } from '@/stores/globalTimerStore';
const activeName = ref('first'); const activeName = ref('first');
@@ -51,6 +53,16 @@
const monthDates = ref([]); const monthDates = ref([]);
const apiInstance = new BoxApi(); const apiInstance = new BoxApi();
const globalTimerStore = useGlobalTimerStore();
const updateChartData = async () => {
const range =
activeName.value === 'first' ? 'day' :
activeName.value === 'second' ? 'week' : 'month';
await updateCounts(range);
};
// 设置时间范围获取计数 // 设置时间范围获取计数
const getEventCount = async (timeBefore, timeAfter) => { const getEventCount = async (timeBefore, timeAfter) => {
@@ -98,7 +110,7 @@
}, },
grid: { grid: {
top: '10%', top: '10%',
left: '5%', left: '7%',
right: '5%', right: '5%',
bottom: '20%', bottom: '20%',
}, },
@@ -189,10 +201,26 @@
calculateWeekDates(); calculateWeekDates();
calculateMonthDates(); calculateMonthDates();
await handleClick({ props: { name: 'first' } }); await handleClick({ props: { name: 'first' } });
// 注册定时器回调
globalTimerStore.registerCallback(updateChartData);
// 启动全局定时器
globalTimerStore.startTimer();
window.addEventListener('resize', debounce(() => { window.addEventListener('resize', debounce(() => {
if (chartInstance.value) chartInstance.value.resize(); if (chartInstance.value) chartInstance.value.resize();
}, 300)); }, 300));
}); });
onBeforeUnmount(() => {
globalTimerStore.unregisterCallback(updateChartData);
globalTimerStore.stopTimer();
window.removeEventListener('resize', debounce(() => {
if (chartInstance.value) chartInstance.value.resize();
}));
});
</script> </script>
@@ -218,6 +246,7 @@
border-bottom: 1px solid #3a4b5c; border-bottom: 1px solid #3a4b5c;
} */ } */
.chart-container { .chart-container {
/* min-height: 350px; */ /* min-height: 350px; */
width: 100%; width: 100%;
@@ -245,8 +274,6 @@
box-sizing: border-box !important; box-sizing: border-box !important;
background-clip: content-box !important; background-clip: content-box !important;
} */ } */
.el-tabs__active-bar { .el-tabs__active-bar {
background-color: transparent !important; background-color: transparent !important;
} }

View File

@@ -19,16 +19,14 @@
<div class="table-container"> <div class="table-container">
<el-table :data="tableData" @row-click="handleRowClick" class="table-part"> <el-table :data="tableData" @row-click="handleRowClick" class="table-part">
<el-table-column v-show="false" prop="id" label="告警编号" v-if="false"></el-table-column> <el-table-column v-show="false" prop="id" label="告警编号" v-if="false"></el-table-column>
<el-table-column label="告警类型" :width="adjustedWidths[0]" align="center" <el-table-column label="告警类型" :width="adjustedWidths[0]" align="center" :show-overflow-tooltip="true">
:show-overflow-tooltip="true">
<template v-slot="scope"> <template v-slot="scope">
{{ typeMapping[scope.row.types] }} {{ typeMapping[scope.row.types] }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="camera.name" label="告警位置" :width="adjustedWidths[1]" align="center" <el-table-column prop="camera.name" label="告警位置" :width="adjustedWidths[1]" align="center"
:show-overflow-tooltip="true"></el-table-column> :show-overflow-tooltip="true"></el-table-column>
<el-table-column label="告警时间" :width="adjustedWidths[2]" align="center" <el-table-column label="告警时间" :width="adjustedWidths[2]" align="center" :show-overflow-tooltip="true">
:show-overflow-tooltip="true">
<template v-slot="scope"> <template v-slot="scope">
{{ formatDateTime(scope.row.ended_at) }} {{ formatDateTime(scope.row.ended_at) }}
</template> </template>
@@ -62,7 +60,7 @@
<el-col :span="12" class="dialog-left"> <el-col :span="12" class="dialog-left">
<el-row gutter class="dialog-image-container"> <el-row gutter class="dialog-image-container">
<template v-if="hasSnapshot"> <template v-if="hasSnapshot">
<el-image :src="snapshotFile"></el-image> <el-image :src="snapshotFile" @click="handleImageClick(snapshotFile)" style="cursor: pointer;"></el-image>
</template> </template>
<!-- <template v-if="hasVideo"> <!-- <template v-if="hasVideo">
<video :src="videoFile" controls></video> <video :src="videoFile" controls></video>
@@ -109,12 +107,17 @@
</el-row> </el-row>
</div> </div>
</el-dialog> </el-dialog>
<el-dialog v-model="previewVisible" width="60%" custom-class="image-preview-dialog" :close-on-click-modal="true">
<img :src="previewImage" alt="预览图片" style="width: 100%; height: auto; display: block; margin: auto;" />
</el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'; import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
import { BoxApi } from '@/utils/boxApi.ts'; import { BoxApi } from '@/utils/boxApi.ts';
import { useGlobalTimerStore } from '@/stores/globalTimerStore';
const boxApi = new BoxApi(); const boxApi = new BoxApi();
const tableData = ref([]); const tableData = ref([]);
@@ -142,6 +145,13 @@
const originalWidths = [97, 150, 160]; // 默认宽度 const originalWidths = [97, 150, 160]; // 默认宽度
const adjustedWidths = ref([...originalWidths]); const adjustedWidths = ref([...originalWidths]);
const baseWidth = 2150; const baseWidth = 2150;
const previewVisible = ref(false); // 控制预览弹窗显示
const previewImage = ref(''); // 预览的图片路径
const handleImageClick = (imagePath) => {
previewImage.value = imagePath; // 设置预览图片路径
previewVisible.value = true; // 显示预览弹窗
};
const formatDateTimeToISO = (datetime) => { const formatDateTimeToISO = (datetime) => {
return new Date(datetime).toISOString().replace('.000', ''); return new Date(datetime).toISOString().replace('.000', '');
@@ -289,15 +299,21 @@
onMounted(async () => { onMounted(async () => {
token.value = localStorage.getItem('alertToken'); token.value = localStorage.getItem('alertToken');
await fetchTypeMapping(token.value); await fetchTypeMapping(token.value);
await fetchEvents(); await fetchEvents();
await adjustColumnWidths(); await adjustColumnWidths();
window.addEventListener('resize', adjustColumnWidths); window.addEventListener('resize', adjustColumnWidths);
const globalTimerStore = useGlobalTimerStore();
globalTimerStore.registerCallback(fetchEvents);
globalTimerStore.startTimer();
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('resize', adjustColumnWidths); window.removeEventListener('resize', adjustColumnWidths);
const globalTimerStore = useGlobalTimerStore();
globalTimerStore.unregisterCallback(fetchEvents);
}); });
</script> </script>
@@ -404,9 +420,11 @@
.table-container>>>.el-table__row>td { .table-container>>>.el-table__row>td {
border: none; border: none;
} }
.table-container>>>.el-table th.is-leaf { .table-container>>>.el-table th.is-leaf {
border: none; border: none;
} }
::v-deep .el-table__inner-wrapper::before { ::v-deep .el-table__inner-wrapper::before {
height: 0; height: 0;
} }

View File

@@ -13,7 +13,9 @@ import { ref, onMounted, onBeforeUnmount } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { BoxApi } from '@/utils/boxApi.ts'; import { BoxApi } from '@/utils/boxApi.ts';
import { DCaret } from '@element-plus/icons-vue'; import { DCaret } from '@element-plus/icons-vue';
import { useGlobalTimerStore } from '@/stores/globalTimerStore';
const globalTimerStore = useGlobalTimerStore();
const chartContainer = ref(null); const chartContainer = ref(null);
let myChart = null; let myChart = null;
const cameras = ref([]); const cameras = ref([]);
@@ -236,18 +238,14 @@ const fetchCameras = async () => {
} }
}; };
// 清理轮播和事件监听 const resizeChart = () => {
onBeforeUnmount(() => { if (myChart && !myChart.isDisposed()) {
if (dataZoomMoveTimer) { myChart.resize();
clearInterval(dataZoomMoveTimer); } else {
dataZoomMoveTimer = null; // 确保计时器被清空 console.warn('Attempted to resize a disposed ECharts instance.');
} }
if (myChart) { };
window.removeEventListener('resize', resizeChart); // 确保事件监听器被移除
myChart.dispose();
myChart = null;
}
});
@@ -259,16 +257,26 @@ onMounted(async () => {
// 监听窗口变化事件,调整图表大小 // 监听窗口变化事件,调整图表大小
window.addEventListener('resize', resizeChart); window.addEventListener('resize', resizeChart);
globalTimerStore.registerCallback(fetchCameras);
globalTimerStore.startTimer();
}); });
// 清理轮播和事件监听
const resizeChart = () => { onBeforeUnmount(() => {
if (myChart && !myChart.isDisposed()) { if (dataZoomMoveTimer) {
myChart.resize(); clearInterval(dataZoomMoveTimer);
} else { dataZoomMoveTimer = null; // 确保计时器被清空
console.warn('Attempted to resize a disposed ECharts instance.');
} }
}; if (myChart) {
window.removeEventListener('resize', resizeChart); // 确保事件监听器被移除
myChart.dispose();
myChart = null;
}
globalTimerStore.unregisterCallback(fetchCameras);
globalTimerStore.stopTimer();
});
// const resizeChart = () => { // const resizeChart = () => {
@@ -289,17 +297,19 @@ const resizeChart = () => {
height: 41vh; height: 41vh;
margin: 20px; margin: 20px;
} }
.show-bt { .show-bt {
background-color: #001529; background-color: #001529;
border: #001529; border: #001529;
width: 6vw; width: 79px;
height: 4vh; height: 30px;
border-radius: 6px; border-radius: 6px;
color: white; color: white;
font-weight: bolder; font-weight: bolder;
font-size: 15px; font-size: 0.9rem;
padding: 0; padding: 0;
} }
.chart-container { .chart-container {
/* margin: 0 1vw 0 1vw; */ /* margin: 0 1vw 0 1vw; */
padding-top: 0; padding-top: 0;

View File

@@ -18,7 +18,7 @@
<!-- 右侧分为两行 --> <!-- 右侧分为两行 -->
<el-col :sm="24" :md="16"> <el-col :sm="24" :md="16">
<el-row> <el-row>
<el-col :sm="24" :md="24"> <el-col :sm="24" :md="24" class="inner-title-text">
通道总数 通道总数
</el-col> </el-col>
</el-row> </el-row>
@@ -41,7 +41,7 @@
<!-- 右侧分为两行 --> <!-- 右侧分为两行 -->
<el-col :sm="24" :md="16"> <el-col :sm="24" :md="16">
<el-row> <el-row>
<el-col :sm="24" :md="24"> <el-col :sm="24" :md="24" class="inner-title-text">
在线 在线
</el-col> </el-col>
</el-row> </el-row>
@@ -64,7 +64,7 @@
<!-- 右侧分为两行 --> <!-- 右侧分为两行 -->
<el-col :sm="24" :md="16"> <el-col :sm="24" :md="16">
<el-row> <el-row>
<el-col :sm="24" :md="24"> <el-col :sm="24" :md="24" class="inner-title-text">
离线: 离线:
</el-col> </el-col>
</el-row> </el-row>
@@ -89,7 +89,7 @@
<!-- 右侧分为两行 --> <!-- 右侧分为两行 -->
<el-col :sm="24" :md="16"> <el-col :sm="24" :md="16">
<el-row> <el-row>
<el-col :sm="24" :md="24"> <el-col :sm="24" :md="24" class="inner-title-text">
事件总数 事件总数
</el-col> </el-col>
</el-row> </el-row>
@@ -112,7 +112,7 @@
<!-- 右侧分为两行 --> <!-- 右侧分为两行 -->
<el-col :sm="24" :md="16"> <el-col :sm="24" :md="16">
<el-row> <el-row>
<el-col :sm="24" :md="24"> <el-col :sm="24" :md="24" class="inner-title-text">
已处理: 已处理:
</el-col> </el-col>
</el-row> </el-row>
@@ -135,7 +135,7 @@
<!-- 右侧分为两行 --> <!-- 右侧分为两行 -->
<el-col :sm="24" :md="16"> <el-col :sm="24" :md="16">
<el-row> <el-row>
<el-col :sm="24" :md="24"> <el-col :sm="24" :md="24" class="inner-title-text">
未处理: 未处理:
</el-col> </el-col>
</el-row> </el-row>
@@ -163,6 +163,7 @@ import CameraOffline from '@/icons/CameraOffline.vue';
import EventAll from '@/icons/EventAll.vue'; import EventAll from '@/icons/EventAll.vue';
import EventClosed from '@/icons/EventClosed.vue'; import EventClosed from '@/icons/EventClosed.vue';
import EventPending from '@/icons/EventPending.vue'; import EventPending from '@/icons/EventPending.vue';
import { useGlobalTimerStore } from '@/stores/globalTimerStore';
const apiInstance = new BoxApi(); const apiInstance = new BoxApi();
const cameraCount = ref(0); const cameraCount = ref(0);
@@ -320,8 +321,19 @@ onMounted(() => {
// getMonthData(); // getMonthData();
fetchCameras(); fetchCameras();
fetchEvents(); fetchEvents();
const globalTimerStore = useGlobalTimerStore();
globalTimerStore.registerCallback(fetchCameras);
globalTimerStore.registerCallback(fetchEvents);
globalTimerStore.startTimer();
});
onBeforeUnmount(() => {
const globalTimerStore = useGlobalTimerStore();
// 注销回调
globalTimerStore.unregisterCallback(fetchCameras);
globalTimerStore.unregisterCallback(fetchEvents);
}); });
@@ -332,8 +344,12 @@ onMounted(() => {
width: 100%; width: 100%;
height: 100%; height: 100%;
margin: 1vh; margin: 1vh;
padding: 2vh 0; padding: 1vh 1vw;
overflow-y: scroll;
scrollbar-width:none;
}
.count-container::-webkit-scrollbar {
display: none;
} }
@@ -341,14 +357,19 @@ onMounted(() => {
.bottom-row { .bottom-row {
background-color: #001529; background-color: #001529;
color: aliceblue; color: aliceblue;
padding: 0; padding: 0.5vh;
margin: 0; margin: 0;
} }
.inner-title-text{
font-size: 0.9rem;
}
.inner-count-text { .inner-count-text {
color: rgb(91, 224, 241); color: rgb(91, 224, 241);
font-size: 1rem;
} }
.tab-div { .tab-div {
@@ -357,7 +378,7 @@ onMounted(() => {
::v-deep .el-tabs__item { ::v-deep .el-tabs__item {
color: #fff; color: #fff;
font-size: 13px; font-size: 0.8rem;
padding: 0; padding: 0;
margin-left: 1vh; margin-left: 1vh;
height: 20px; height: 20px;

View File

@@ -3,16 +3,16 @@
<div class="search-row"> <div class="search-row">
<div class="bt-search"> <div class="bt-search">
<el-button type="primary" @click="handleFilter" class="alert-bt">点击查询</el-button> <el-button type="primary" @click="handleFilter" class="alert-bt"><el-icon><Search /></el-icon>点击查询</el-button>
</div> </div>
<div class="start-col"> <div class="start-col">
<el-date-picker v-model="filterParams.timeAfter" :teleported="false" type="datetime" placeholder="请选择开始时间" prefix-icon="CaretBottom" popper-class="popperClass" ></el-date-picker> <el-date-picker v-model="filterParams.timeAfter" :teleported="false" type="datetime" placeholder="请选择开始时间"
prefix-icon="CaretBottom" popper-class="popperClass"></el-date-picker>
</div> </div>
<div class="end-col"> <div class="end-col">
<el-date-picker v-model="filterParams.timeBefore" :teleported="false" type="datetime" placeholder="请选择结束时间" prefix-icon="CaretBottom" popper-class="popperClass" ></el-date-picker> <el-date-picker v-model="filterParams.timeBefore" :teleported="false" type="datetime" placeholder="请选择结束时间"
prefix-icon="CaretBottom" popper-class="popperClass"></el-date-picker>
</div> </div>
</div> </div>
<div id="3d-bar-chart" class="myPanChart"></div> <div id="3d-bar-chart" class="myPanChart"></div>
@@ -34,6 +34,10 @@
import { ref, reactive, onMounted, onBeforeUnmount, computed } from 'vue'; import { ref, reactive, onMounted, onBeforeUnmount, computed } from 'vue';
import { BoxApi } from '@/utils/boxApi.ts'; import { BoxApi } from '@/utils/boxApi.ts';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { Search } from '@element-plus/icons-vue';
import { useGlobalTimerStore } from '@/stores/globalTimerStore';
const globalTimerStore = useGlobalTimerStore();
const apiInstance = new BoxApi(); const apiInstance = new BoxApi();
const typeMapping = reactive({}); const typeMapping = reactive({});
@@ -60,22 +64,118 @@ const renderChart = () => {
const chartInstance = echarts.init(chartDom); const chartInstance = echarts.init(chartDom);
const colorList = [ const colorList = [
// new echarts.graphic.LinearGradient(0, 0, 1, 0, [
// { offset: 0, color: "rgba(69,233,254,1)" },
// { offset: 1, color: "rgba(69,233,254,0.3)" }
// ]),
new echarts.graphic.LinearGradient(1, 0, 0, 0, [ new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ offset: 0, color: "rgba(255,181,111,1)" }, { offset: 0, color: "rgba(135, 206, 250,0.7)" },
{ offset: 1, color: "rgba(255,181,111,0.3)" } { offset: 1, color: "rgba(135, 206, 250,0.3)" }
]), ]),
new echarts.graphic.LinearGradient(0, 1, 0, 0, [ new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{ offset: 0, color: "rgba(101,122,250,1)" }, { offset: 0, color: "rgba(95, 158, 160,0.8)" },
{ offset: 1, color: "rgba(101,122,250,0.3)" } { offset: 1, color: "rgba(95, 158, 160,0.3)" }
]),
new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ offset: 0, color: "rgba(255, 99, 71, 1)" },
{ offset: 1, color: "rgba(255, 99, 71, 0.3)" }
]),
new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ offset: 0, color: "rgba(218, 112, 214,0.6)" },
{ offset: 1, color: "rgba(218, 112, 214,0.3)" }
]),
new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{ offset: 0, color: "rgba(138, 43, 226,0.7)" },
{ offset: 1, color: "rgba(138, 43, 226,0.3)" }
]), ]),
new echarts.graphic.LinearGradient(0, 0, 1, 0, [ new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: "rgba(45,190,146,1)" }, { offset: 0, color: "rgba(75, 0, 130,0.8)" },
{ offset: 1, color: "rgba(45,190,146,0.3)" } { offset: 1, color: "rgba(75, 0, 130,0.3)" }
]), ]),
new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ offset: 0, color: "rgba(173, 216, 230,0.7)" },
{ offset: 1, color: "rgba(173, 216, 230,0.3)" }
]),
new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{ offset: 0, color: "rgba(100, 149, 237,0.8)" },
{ offset: 1, color: "rgba(100, 149, 237,0.3)" }
]),
new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: "rgba(25, 25, 112,0.9)" },
{ offset: 1, color: "rgba(25, 25, 112,0.3)" }
]),
new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ offset: 0, color: "rgba(64, 224, 208,0.7)" },
{ offset: 1, color: "rgba(64, 224, 208,0.3)" }
]),
new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{ offset: 0, color: "rgba(32, 178, 170,0.8)" },
{ offset: 1, color: "rgba(32, 178, 170,0.3)" }
]),
new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: "rgba(47, 79, 79,0.9)" },
{ offset: 1, color: "rgba(47, 79, 79,0.3)" }
]),
//深海绿 //海浪青 //薄荷青 //幽夜蓝 //极地蓝 //浅冰蓝 //深紫罗兰 //静夜紫 //薰衣草紫 //薄暮紫 //冰川青 //清晨蓝
new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: "rgba(123, 104, 238,0.7)" },
{ offset: 1, color: "rgba(123, 104, 238,0.3)" }
]),
new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: "rgba(255, 165, 0, 1)" },
{ offset: 1, color: "rgba(255, 165, 0, 0.3)" }
]), // 橙色
new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ offset: 0, color: "rgba(255, 182, 193, 1)" },
{ offset: 1, color: "rgba(255, 182, 193, 0.3)" }
]), // 浅粉红
new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{ offset: 0, color: "rgba(250, 128, 114, 1)" },
{ offset: 1, color: "rgba(250, 128, 114, 0.3)" }
]), // 鲑鱼橙
new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{ offset: 0, color: "rgba(240, 128, 128, 1)" },
{ offset: 1, color: "rgba(240, 128, 128, 0.3)" }
]), // 浅珊瑚红
new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: "rgba(255, 140, 0, 1)" },
{ offset: 1, color: "rgba(255, 140, 0, 0.3)" }
]), // 深橙色
new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ offset: 0, color: "rgba(255, 215, 0, 1)" },
{ offset: 1, color: "rgba(255, 215, 0, 0.3)" }
]), // 金色
new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{ offset: 0, color: "rgba(255, 228, 181, 1)" },
{ offset: 1, color: "rgba(255, 228, 181, 0.3)" }
]), // 小麦色
new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: "rgba(255, 160, 122, 1)" },
{ offset: 1, color: "rgba(255, 160, 122, 0.3)" }
]), // 浅鲑鱼色
new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ offset: 0, color: "rgba(250, 250, 210, 1)" },
{ offset: 1, color: "rgba(250, 250, 210, 0.3)" }
]), // 浅金黄
new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{ offset: 0, color: "rgba(255, 223, 186, 1)" },
{ offset: 1, color: "rgba(255, 223, 186, 0.3)" }
]), // 暖杏色
new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: "rgba(245, 222, 179, 1)" },
{ offset: 1, color: "rgba(245, 222, 179, 0.3)" }
]), // 小麦暖黄
]; ];
@@ -202,9 +302,9 @@ const handleFilter = () => {
const timeAfter = filterParams.timeAfter ? formatDateTimeToISO(new Date(filterParams.timeAfter)) : null; const timeAfter = filterParams.timeAfter ? formatDateTimeToISO(new Date(filterParams.timeAfter)) : null;
const timeBefore = filterParams.timeBefore ? formatDateTimeToISO(new Date(filterParams.timeBefore)) : null; const timeBefore = filterParams.timeBefore ? formatDateTimeToISO(new Date(filterParams.timeBefore)) : null;
fetchTypeCounts(timeAfter, timeBefore); // 重新统计数量,添加时间条件 fetchTypeCounts(timeAfter, timeBefore); // 重新统计数量,添加时间条件
}; };
const formatDateTime = (datetime) => { const formatDateTime = (datetime) => {
const date = new Date(datetime); const date = new Date(datetime);
const year = date.getFullYear(); const year = date.getFullYear();
@@ -238,10 +338,14 @@ onMounted(async () => {
await fetchTypeCounts(); // 初次加载时不加时间条件 await fetchTypeCounts(); // 初次加载时不加时间条件
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
await renderChart(); await renderChart();
globalTimerStore.registerCallback(handleFilter);
globalTimerStore.startTimer();
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
globalTimerStore.unregisterCallback(handleFilter);
globalTimerStore.stopTimer();
}); });
</script> </script>
@@ -275,7 +379,9 @@ onBeforeUnmount(() => {
} }
.start-col,.end-col,.bt-search{ .start-col,
.end-col,
.bt-search {
display: flex; display: flex;
justify-content: left; justify-content: left;
align-items: center; align-items: center;

View File

@@ -1,18 +1,74 @@
<template> <template>
<div class="settings-container"> <div class="settings-container">
<el-row class="popup-row"> <el-row class="tip-row">
<el-col :sm="24" :md="24">弹窗设置</el-col>
<div class="model-row">
<el-col :sm="24" :md="8">
<el-card style="max-width: 480px" >
<template #header>
弹窗模式设置
</template>
<el-row>
<el-col :sm="24" :md="24"> <el-col :sm="24" :md="24">
<el-checkbox v-model="isInteractivePopupEnabled" @change="handleInteractiveChange" style="color: aliceblue;"> <el-checkbox v-model="isInteractivePopupEnabled" @change="handleInteractiveChange">
开启交互式弹窗 开启交互式弹窗
</el-checkbox> </el-checkbox>
</el-col> </el-col>
<el-col :sm="24" :md="24"> <el-col :sm="24" :md="24">
<el-checkbox v-model="isResponsivePopupEnabled" @change="handleResponsiveChange" style="color: aliceblue;"> <el-checkbox v-model="isResponsivePopupEnabled" @change="handleResponsiveChange">
开启响应式弹窗 开启响应式弹窗
</el-checkbox> </el-checkbox>
</el-col> </el-col>
</el-row> </el-row>
<template #footer>(Tips: 不支持同时生效,关闭状态无声音)</template>
</el-card>
</el-col>
<el-col :sm="24" :md="8">
<el-card style="max-width: 480px" >
<template #header>
<span>弹窗队列设置</span>
</template>
<el-col :sm="24" :md="24" style="margin:1vh 0;">
<el-button @click="loadNextNotification" size="small" type="primary">加载更多</el-button>
</el-col>
<el-col :sm="24" :md="24">
<el-button @click="clearAllNotifications" size="small" type="danger">清空所有</el-button>
</el-col>
<template #footer>(Tips: 交互模式下加载/清空累积弹窗提示)</template>
</el-card>
</el-col>
<el-col :sm="24" :md="8" class="audio-card">
<el-card style="max-width: 480px" >
<template #header>
<span>弹窗声音设置</span>
</template>
<el-col :sm="24" :md="24">
开启提示声音:&nbsp;&nbsp;&nbsp;
<el-switch v-model="isSoundEnabled" @change="handleSoundSwitch">开启振荡器发声</el-switch>
</el-col>
<el-row :gutter="20">
<el-col :sm="24" :md="5" >
<span>音量设置: </span>
</el-col>
<el-col :sm="24" :md="16">
<el-slider v-model="volume" :min="0" :max="1" :step="0.01" :disabled="isSoundEnabled"
@change="previewSound" size="small" />
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :sm="24" :md="5" >
<span>音调设置: </span>
</el-col>
<el-col :sm="24" :md="16">
<el-slider v-model="frequency" :min="300" :max="1200" :step="10" :disabled="isSoundEnabled"
@change="previewSound" size="small" />
</el-col>
</el-row>
<template #footer>(Tips: 开启后锁定声音设置)</template>
</el-card>
</el-col>
</div>
</el-row>
<el-row class="channel-row"> <el-row class="channel-row">
<Channel /> <Channel />
</el-row> </el-row>
@@ -20,26 +76,90 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, inject, onMounted } from 'vue'; import { ref, inject, onMounted, onUnmounted, watch } from 'vue';
import { ElMessageBox, ElMessage } from 'element-plus'; import { ElMessageBox, ElMessage } from 'element-plus';
import type { GlobalWebSocket } from '@/utils/useGlobalWebSocket'; import type { GlobalWebSocket } from '@/utils/useGlobalWebSocket';
import Channel from '@/components/Channel.vue'; import Channel from '@/components/Channel.vue';
const isInteractivePopupEnabled = ref(false); // 交互式弹窗状态 const isInteractivePopupEnabled = ref(false); // 交互式弹窗状态
const isResponsivePopupEnabled = ref(false); // 响应式弹窗状态 const isResponsivePopupEnabled = ref(false); // 响应式弹窗状态
const globalWebSocket = inject<GlobalWebSocket>('globalWebSocket');
if (!globalWebSocket) { const globalWebSocket = inject<GlobalWebSocket>('globalWebSocket');
throw new Error('globalWebSocket 注入失败'); if (!globalWebSocket) throw new Error('globalWebSocket 注入失败');
}
const { notificationSoundParams, setNotificationSoundParams } = globalWebSocket;
const isSoundEnabled = ref(notificationSoundParams.isSoundEnabled);
const volume = ref(notificationSoundParams.volume);
const frequency = ref(notificationSoundParams.frequency);
let audioContext: AudioContext | null = null;
const loadNextNotification = () => {
if (globalWebSocket) globalWebSocket.loadNextNotification();
};
const clearAllNotifications = () => {
if (globalWebSocket) globalWebSocket.clearAllNotifications();
};
// 初始化时加载弹窗模式状态 // 初始化时加载弹窗模式状态
onMounted(() => { onMounted(() => {
isInteractivePopupEnabled.value = localStorage.getItem('isInteractivePopupEnabled') === 'true'; isInteractivePopupEnabled.value = localStorage.getItem('isInteractivePopupEnabled') === 'true';
isResponsivePopupEnabled.value = localStorage.getItem('isResponsivePopupEnabled') === 'true'; isResponsivePopupEnabled.value = localStorage.getItem('isResponsivePopupEnabled') === 'true';
isSoundEnabled.value = notificationSoundParams.isSoundEnabled;
volume.value = notificationSoundParams.volume;
frequency.value = notificationSoundParams.frequency;
updateWebSocketConnection(); updateWebSocketConnection();
}); });
const handleSoundSwitch = () => {
setNotificationSoundParams({
isSoundEnabled: isSoundEnabled.value,
volume: volume.value,
frequency: frequency.value,
});
ElMessage.success(isSoundEnabled.value ? '提示音已开启,当前参数已锁定' : '提示音已关闭');
};
// 预览提示音
const previewSound = () => {
if (audioContext) {
audioContext.close();
}
audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.type = 'triangle';
oscillator.frequency.setValueAtTime(frequency.value, audioContext.currentTime);
gainNode.gain.setValueAtTime(volume.value, audioContext.currentTime);
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.start();
setTimeout(() => {
oscillator.stop();
audioContext?.close();
audioContext = null;
}, 300); // 持续播放 300ms
};
watch([isSoundEnabled, volume, frequency], () => {
setNotificationSoundParams({
isSoundEnabled: isSoundEnabled.value,
volume: volume.value,
frequency: frequency.value,
});
});
// const handleInteractiveChange = () => { // const handleInteractiveChange = () => {
// if (isInteractivePopupEnabled.value) { // if (isInteractivePopupEnabled.value) {
// isResponsivePopupEnabled.value = false; // isResponsivePopupEnabled.value = false;
@@ -132,6 +252,10 @@ const updateWebSocketConnection = () => {
globalWebSocket.closeWebSocket(); globalWebSocket.closeWebSocket();
} }
}; };
onUnmounted(() => {
audioContext?.close();
});
</script> </script>
<style scoped> <style scoped>
@@ -143,7 +267,9 @@ const updateWebSocketConnection = () => {
height: 100%; height: 100%;
} }
.popup-row { .tip-row {
display: flex;
flex-direction: column;
margin-bottom: 20px; margin-bottom: 20px;
height: 20vh; height: 20vh;
width: 80vw; width: 80vw;
@@ -154,6 +280,62 @@ const updateWebSocketConnection = () => {
color: white; color: white;
} }
.tip-row .el-card{
border: none !important;
color: white;
}
.tip-row .el-card .el-checkbox{
color: white;
}
::v-deep .tip-row .el-card__header {
background-color: #001529;
/* height: 3vh; */
padding: 1vh 0 1vh 1vw;
display: flex;
font-size: 18px;
font-weight: bold;
border-bottom: none !important;
}
::v-deep .tip-row .el-card__body {
background-color: #001529;
height: 12vh;
font-size: 16px;
/* border: 0px ; */
}
::v-deep .tip-row .el-card .is-always-shadow{
color: aqua;
}
::v-deep .tip-row .el-card__footer {
background-color: #001529;
/* height: 3vh; */
padding: 0 1vw 0 0;
display: flex;
justify-content: end;
font-size: 14px;
font-weight: bold;
border-top: none !important;
color: #a8a3a3;
}
.model-row {
display: flex;
flex-direction: row;
}
::v-deep .audio-card .el-card__body {
padding: 1vh 1vw;
/* background-color: aqua; */
}
.channel-row { .channel-row {
margin: 1vh 2vw; margin: 1vh 2vw;
width: 80vw; width: 80vw;

View File

@@ -48,34 +48,38 @@
<el-date-picker v-model="filterParams.timeBefore" type="datetime" placeholder="请选择结束时间"></el-date-picker> <el-date-picker v-model="filterParams.timeBefore" type="datetime" placeholder="请选择结束时间"></el-date-picker>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="4" class="filter-buttons" :offset="15"> <el-col :span="8" class="filter-buttons" :offset="0">
<el-button type="primary" @click="handleFilter">查询</el-button> <el-button type="primary" @click="handleFilter">查询</el-button>
<el-button @click="handleReset">重置</el-button> <el-button @click="handleReset">重置</el-button>
<el-button :disabled="isExporting" @click="exportData"> <el-button :disabled="isExporting" @click="exportData">
{{ isExporting ? '正在导出请勿重复点击' : '导出' }} {{ isExporting ? '正在导出请勿重复点击' : '导出' }}
</el-button> </el-button>
</el-col>
<el-col :span="1" :offset="0">
<el-button type="primary" :disabled="!selectedAlerts.length" @click="batchMarkAsProcessed"> <el-button type="primary" :disabled="!selectedAlerts.length" @click="batchMarkAsProcessed">
批量标记为已处理 批量标记为已处理
</el-button> </el-button>
<!-- <el-button type="danger" :disabled="!selectedAlerts.length" @click="handleDelete">删除选中</el-button> -->
</el-col>
<el-col :span="15" :offset="0">
</el-col> </el-col>
</el-row> </el-row>
<el-row class="table-row"> <el-row class="table-row">
<el-col :span="24" class="table-col"> <el-col :span="24" class="table-col">
<div class="table-container"> <div class="table-container">
<el-table :data="tableData" header-row-class-name="table-header" :fit="true" height="580" <el-table :data="tableData" class="events-table" :fit="true"
@selection-change="handleSelectionChange"> @selection-change="handleSelectionChange">
<el-table-column type="selection" min-width="55"></el-table-column> <el-table-column type="selection" min-width="55" align="center"></el-table-column>
<el-table-column prop="id" label="告警编号" min-width="100"></el-table-column> <el-table-column type="index" label="号" min-width="30"
<el-table-column label="告警类型" min-width="150"> :index="(index) => index + 1 + (currentPage - 1) * pageSize" align="center">
</el-table-column>
<el-table-column prop="id" label="告警编号" min-width="100" align="center" v-if="showColumn"></el-table-column>
<el-table-column label="告警类型" min-width="100" align="center">
<template v-slot="scope"> <template v-slot="scope">
{{ typeMapping[scope.row.types] }} {{ typeMapping[scope.row.types] }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="camera.name" label="告警位置" min-width="150"></el-table-column> <el-table-column prop="camera.name" label="告警位置" min-width="100" align="center"></el-table-column>
<el-table-column label="告警时间" min-width="200"> <el-table-column label="告警时间" min-width="200" align="center">
<template v-slot="scope"> <template v-slot="scope">
{{ formatDateTime(scope.row.ended_at) }} {{ formatDateTime(scope.row.ended_at) }}
</template> </template>
@@ -200,9 +204,9 @@ import { ref, reactive, onMounted, computed } from 'vue';
// import Statistics from '@/components/Statistics.vue'; // import Statistics from '@/components/Statistics.vue';
// import AlertChart from '@/components/AlertChart.vue'; // import AlertChart from '@/components/AlertChart.vue';
import { BoxApi } from '@/utils/boxApi.ts'; // 导入 BoxApi import { BoxApi } from '@/utils/boxApi.ts'; // 导入 BoxApi
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
import Papa from 'papaparse'; import Papa from 'papaparse';
import { ElMessageBox, ElMessage } from 'element-plus';
// 创建 BoxApi 实例 // 创建 BoxApi 实例
const boxApi = new BoxApi(); const boxApi = new BoxApi();
@@ -232,6 +236,7 @@ const displayTotalItems = ref(0); // 用于展示的数字
const cameras = ref([]); const cameras = ref([]);
const selectedAlerts = ref([]); const selectedAlerts = ref([]);
const showColumn = ref(true);
const handleSelectionChange = (selectedRows) => { const handleSelectionChange = (selectedRows) => {
selectedAlerts.value = selectedRows.map(row => row.id); selectedAlerts.value = selectedRows.map(row => row.id);
@@ -250,6 +255,49 @@ const batchMarkAsProcessed = async () => {
} }
}; };
const handleDelete = () => {
// 显示确认框
ElMessageBox.confirm(
`确认删除选中的 ${selectedAlerts.value.length} 条告警吗?`,
'删除确认',
{
confirmButtonText: '删除',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(async () => {
try {
// 调用 API 删除选中的告警
const results = await boxApi.delEvents(token.value, selectedAlerts.value);
// 检查结果,显示操作成功或失败
const failedDeletions = results.filter(item => !item.success);
if (failedDeletions.length === 0) {
ElMessage.success('删除成功');
} else {
ElMessage.error(
`部分删除失败:${failedDeletions
.map(item => `ID: ${item.id}`)
.join(', ')}`
);
}
// 刷新表格数据
await fetchEvents();
selectedAlerts.value = []; // 清空选中项
} catch (error) {
console.error('删除操作失败:', error);
ElMessage.error('删除失败,请稍后再试');
}
})
.catch(() => {
// 用户取消删除操作
ElMessage.info('删除已取消');
});
};
const formatDateTimeToISO = (datetime) => { const formatDateTimeToISO = (datetime) => {
return new Date(datetime).toISOString().replace('.000', ''); return new Date(datetime).toISOString().replace('.000', '');
@@ -447,9 +495,10 @@ const fetchTypeMapping = async (token) => {
const fetchEvents = async () => { const fetchEvents = async () => {
try { try {
const { tableData: data, totalItems: total } = await boxApi.getEvents(token.value, pageSize.value, currentPage.value); // 使用 BoxApi 的 getEvents 方法 const { tableData: data, totalItems: total } = await boxApi.getEvents(token.value, pageSize.value, currentPage.value); // 使用 BoxApi 的 getEvents 方法
tableData.value = data; // tableData.value = data;
tableData.value = data.sort((a, b) => new Date(b.ended_at) - new Date(a.ended_at));
totalItems.value = total; totalItems.value = total;
animateNumberChange(total); // animateNumberChange(total);
} catch (error) { } catch (error) {
console.error("Error fetching events data:", error); console.error("Error fetching events data:", error);
} }
@@ -588,7 +637,8 @@ onMounted(async () => {
.table-container { .table-container {
max-width: 100%; max-width: 100%;
height: 80%; height: 100%;
height: 56vh;
/* min-height: 50vh; */ /* min-height: 50vh; */
/* max-height: 70vh; */ /* max-height: 70vh; */
overflow-x: auto; overflow-x: auto;
@@ -600,6 +650,11 @@ onMounted(async () => {
} }
::v-deep .events-table .el-table__inner-wrapper{
height: 55vh;
max-height: 56vh;
}
.pagination-container { .pagination-container {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
@@ -608,10 +663,10 @@ onMounted(async () => {
margin: 0vh 6vw 5vh 1vw; margin: 0vh 6vw 5vh 1vw;
} }
.table-header { /* .table-header {
background-color: #f7f8fa; background-color: #f7f8fa;
font-weight: bold; font-weight: bold;
} } */
::v-deep .el-table th.el-table__cell { ::v-deep .el-table th.el-table__cell {
background-color: #e9eefca4; background-color: #e9eefca4;

View File

@@ -114,7 +114,7 @@
if (newVal) { if (newVal) {
// cameraDataZ.value = JSON.parse(JSON.stringify(newVal)); // cameraDataZ.value = JSON.parse(JSON.stringify(newVal));
Object.assign(cameraDataZ.value, JSON.parse(JSON.stringify(props.cameraData))); Object.assign(cameraDataZ.value, JSON.parse(JSON.stringify(props.cameraData)));
console.log("cameraDataZ:", cameraDataZ.value); // console.log("cameraDataZ:", cameraDataZ.value);
formatCameraData(cameraDataZ.value); formatCameraData(cameraDataZ.value);
} }
} }
@@ -299,7 +299,7 @@
// mode: rule.mode, // mode: rule.mode,
// })), // })),
}; };
console.log("页面的cameraUpdate》》》》》》》》》》》:", cameraUpdate); // console.log("页面的cameraUpdate》》》》》》》》》》》:", cameraUpdate);
// 构造 ruleUpdate 数据 // 构造 ruleUpdate 数据
const ruleUpdate = CameraDialog.value.rules.map((rule) => ({ const ruleUpdate = CameraDialog.value.rules.map((rule) => ({

View File

@@ -54,10 +54,10 @@
<div class="section bottom-left corner-style">左下 <div class="section bottom-left corner-style">左下
<div class="section hiden"></div> <div class="section hiden"></div>
</div> --> </div> -->
<dv-border-box-13 title="告警数据概览(数据计算数字)" class="section top-left"> <dv-border-box-13 title="告警数据概览(数据计算数字)" class="section top-left"><strong>告警数据统计</strong>
<LeftTop /> <LeftTop />
</dv-border-box-13> </dv-border-box-13>
<dv-border-box-13 title="点位告警数量(不同点位的数量)" class="section middle-left">不同点位告警的数量 <dv-border-box-13 title="点位告警数量(不同点位的数量)" class="section middle-left"><strong>告警点位数据</strong>
<LeftMiddle /> <LeftMiddle />
</dv-border-box-13> </dv-border-box-13>
<!-- <dv-border-box-13 title="今日告警列表(告警详情)" class="section bottom-left ">今日告警列表告警详情 <!-- <dv-border-box-13 title="今日告警列表(告警详情)" class="section bottom-left ">今日告警列表告警详情
@@ -79,7 +79,7 @@
</dv-border-box8> </dv-border-box8>
<div class="center-bottom"> <div class="center-bottom">
<!-- <dv-border-box-13 class="center-bottom-left">中下左</dv-border-box-13> --> <!-- <dv-border-box-13 class="center-bottom-left">中下左</dv-border-box-13> -->
<dv-border-box-13 class="center-bottom-right">告警数量分布情况 <dv-border-box-13 class="center-bottom-right"><strong>告警数量分布</strong>
<CenterBottom /> <CenterBottom />
</dv-border-box-13> </dv-border-box-13>
</div> </div>
@@ -87,11 +87,11 @@
<!-- 右侧区域 --> <!-- 右侧区域 -->
<div class="right-section"> <div class="right-section">
<dv-border-box-13 class="section top-right corner-style">时间段告警总数分布 <dv-border-box-13 class="section top-right corner-style"><strong>告警种类数据</strong>
<RightTop /> <RightTop />
</dv-border-box-13> </dv-border-box-13>
<!-- <dv-border-box-13 class="section middle-right corner-style">告警数量分布</dv-border-box-13> --> <!-- <dv-border-box-13 class="section middle-right corner-style">告警数量分布</dv-border-box-13> -->
<dv-border-box-13 class="section bottom-right corner-style">告警种类划分 <dv-border-box-13 class="section bottom-right corner-style"><strong>今日告警数据</strong>
<LeftBottom /> <LeftBottom />
</dv-border-box-13> </dv-border-box-13>
</div> </div>
@@ -122,7 +122,7 @@ import CenterTop from '@/components/Max/CenterTop.vue';
margin: 0; margin: 0;
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
padding: 4vh 10vw 10vh 7vw; padding: 4vh 10vw 10vh 9vw;
/* background-color: rgb(121, 184, 243); */ /* background-color: rgb(121, 184, 243); */
background-color: #001529; background-color: #001529;
/* background-image: url('/bg05.png'); */ /* background-image: url('/bg05.png'); */
@@ -133,10 +133,12 @@ import CenterTop from '@/components/Max/CenterTop.vue';
position: relative; position: relative;
color: black; color: black;
box-sizing: border-box; box-sizing: border-box;
} }
.custom-decoration { .custom-decoration {
color: #70e5fa;; color: #70e5fa;
;
font-weight: bold; font-weight: bold;
font-size: 25px; font-size: 25px;
} }
@@ -161,12 +163,13 @@ import CenterTop from '@/components/Max/CenterTop.vue';
.container { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 80vw; width: 82vw;
height: 100%; height: 100%;
gap: 1vh; gap: 1vh;
position: relative; position: relative;
/* 为了在背景下显示 */ /* 为了在背景下显示 */
z-index: 1; z-index: 1;
} }
.header { .header {
@@ -194,25 +197,31 @@ import CenterTop from '@/components/Max/CenterTop.vue';
.title-right { .title-right {
display: flex; display: flex;
flex-direction: column; /* 设置为垂直布局 */ flex-direction: column;
/* 设置为垂直布局 */
} }
.first-row { .first-row {
display: flex; display: flex;
justify-content: flex-start; /* 第一行内容靠左 */ justify-content: flex-start;
/* 第一行内容靠左 */
} }
.second-row { .second-row {
display: flex; display: flex;
justify-content: flex-end; /* 第二行内容靠右 */ justify-content: flex-end;
/* 第二行内容靠右 */
} }
.main-section { .main-section {
width: 84vw;
height: 72vh; height: 72vh;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 1vw; gap: 1vw;
overflow-y: scroll;
scrollbar-width: none;
} }
.left-section, .left-section,
@@ -241,13 +250,15 @@ import CenterTop from '@/components/Max/CenterTop.vue';
} }
.top-left { .top-left {
position: relative; /* position: relative; */
padding: 1vh 1vw; color: white;
text-align: center;
/* padding: 1vh 1vw; */
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-grow: 1; flex-grow: 1;
width: 20vw; width: 22vw;
height: 20vh; height: 20vh;
} }

View File

@@ -1,164 +1,62 @@
<template> <template>
<div class="settings-container"> <div class="glowing-border">
<el-row class="popup-row"> <div class="content">
<el-col :sm="24" :md="24">弹窗设置</el-col> <!-- 在这里插入你想要的内容 -->
<el-col :sm="24" :md="24"> </div>
<el-checkbox v-model="isInteractivePopupEnabled" @change="handleInteractiveChange" style="color: aliceblue;">
开启交互式弹窗
</el-checkbox>
</el-col>
<el-col :sm="24" :md="24">
<el-checkbox v-model="isResponsivePopupEnabled" @change="handleResponsiveChange" style="color: aliceblue;">
开启响应式弹窗
</el-checkbox>
</el-col>
</el-row>
<el-row class="channel-row">
<Channel />
</el-row>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, inject, onMounted } from 'vue'; // 这里可以留空或者添加其他逻辑
import { ElMessageBox, ElMessage } from 'element-plus';
import type { GlobalWebSocket } from '@/utils/useGlobalWebSocket';
import Channel from '@/components/Channel.vue';
const isInteractivePopupEnabled = ref(false); // 交互式弹窗状态
const isResponsivePopupEnabled = ref(false); // 响应式弹窗状态
const globalWebSocket = inject<GlobalWebSocket>('globalWebSocket');
if (!globalWebSocket) {
throw new Error('globalWebSocket 注入失败');
}
// 初始化时加载弹窗模式状态
onMounted(() => {
isInteractivePopupEnabled.value = localStorage.getItem('isInteractivePopupEnabled') === 'true';
isResponsivePopupEnabled.value = localStorage.getItem('isResponsivePopupEnabled') === 'true';
updateWebSocketConnection();
});
// const handleInteractiveChange = () => {
// if (isInteractivePopupEnabled.value) {
// isResponsivePopupEnabled.value = false;
// localStorage.setItem('isResponsivePopupEnabled', 'false');
// }
// localStorage.setItem('isInteractivePopupEnabled', String(isInteractivePopupEnabled.value));
// updateWebSocketConnection();
// };
// const handleResponsiveChange = () => {
// if (isResponsivePopupEnabled.value) {
// isInteractivePopupEnabled.value = false;
// localStorage.setItem('isInteractivePopupEnabled', 'false');
// }
// localStorage.setItem('isResponsivePopupEnabled', String(isResponsivePopupEnabled.value));
// updateWebSocketConnection();
// };
const handleInteractiveChange = () => {
if (isInteractivePopupEnabled.value) {
ElMessageBox.confirm('是否开启交互式弹窗提示?', '确认提示', {
confirmButtonText: '是',
cancelButtonText: '否',
type: 'warning',
})
.then(() => {
isResponsivePopupEnabled.value = false;
localStorage.setItem('isResponsivePopupEnabled', 'false');
localStorage.setItem('isInteractivePopupEnabled', 'true');
updateWebSocketConnection();
})
.catch(() => {
isInteractivePopupEnabled.value = false;
});
} else {
ElMessageBox.confirm('是否关闭交互式弹窗?', '确认提示', {
confirmButtonText: '是',
cancelButtonText: '否',
type: 'warning',
})
.then(() => {
localStorage.setItem('isInteractivePopupEnabled', 'false');
updateWebSocketConnection();
})
.catch(() => {
isInteractivePopupEnabled.value = true;
});
}
};
const handleResponsiveChange = () => {
if (isResponsivePopupEnabled.value) {
ElMessageBox.confirm('是否开启响应式弹窗提示?', '确认提示', {
confirmButtonText: '是',
cancelButtonText: '否',
type: 'warning',
})
.then(() => {
isInteractivePopupEnabled.value = false;
localStorage.setItem('isInteractivePopupEnabled', 'false');
localStorage.setItem('isResponsivePopupEnabled', 'true');
updateWebSocketConnection();
})
.catch(() => {
isResponsivePopupEnabled.value = false;
});
} else {
ElMessageBox.confirm('是否关闭响应式弹窗?', '确认提示', {
confirmButtonText: '是',
cancelButtonText: '否',
type: 'warning',
})
.then(() => {
localStorage.setItem('isResponsivePopupEnabled', 'false');
updateWebSocketConnection();
})
.catch(() => {
isResponsivePopupEnabled.value = true;
});
}
};
// 根据开关状态更新 WebSocket 连接
const updateWebSocketConnection = () => {
if (isInteractivePopupEnabled.value || isResponsivePopupEnabled.value) {
globalWebSocket.connectWebSocket();
} else {
globalWebSocket.closeWebSocket();
}
};
</script> </script>
<style scoped> <style scoped>
.settings-container { /* 外层容器 */
display: flex; .glowing-border {
flex-direction: column; width: 400px; /* 设置容器宽度 */
background-color: #F1F1F1; height: 200px; /* 设置容器高度 */
background-color: #1a1a1a; /* 背景颜色 */
border-radius: 15px; /* 圆角 */
position: relative;
box-shadow: 0 0 20px 5px rgba(138, 43, 226, 0.5);
overflow: hidden;
}
/* .glowing-border::before {
content: '';
position: absolute;
top: -20px;
left: -20px;
width: calc(100% + 40px);
height: calc(100% + 40px);
border-radius: 20px;
background: linear-gradient(45deg, rgba(138, 43, 226, 0.8), rgba(75, 0, 130, 0.8));
filter: blur(15px);
z-index: -1;
animation: glow 3s infinite alternate;
}
.content {
width: 100%; width: 100%;
height: 100%; height: 100%;
} display: flex;
align-items: center;
.popup-row { justify-content: center;
margin-bottom: 20px;
height: 20vh;
width: 80vw;
padding: 1vh 1vw;
margin: 1vh 2vw;
background-color: #001529;
border-radius: 8px;
color: white; color: white;
font-size: 20px;
font-family: Arial, sans-serif;
} }
.channel-row {
margin: 1vh 2vw; @keyframes glow {
width: 80vw; 0% {
height: 70vh; opacity: 0.7;
background-color: #001529; transform: scale(1);
border-radius: 8px;
} }
100% {
opacity: 1;
transform: scale(1.1);
}
} */
</style> </style>

View File

@@ -35,8 +35,7 @@
<el-row> <el-row>
<el-col :span="24"> <el-col :span="24">
<div class="table-part"> <div class="table-part">
<el-table v-loading="loading" :data="tableData" <el-table v-loading="loading" :data="tableData" class="user-table" border max-height="700px">
class="user-table" border max-height="700px">
<el-table-column v-if="getColumnVisibility('selection')" type="selection" <el-table-column v-if="getColumnVisibility('selection')" type="selection"
min-width="55"></el-table-column> min-width="55"></el-table-column>
<el-table-column v-if="getColumnVisibility('id')" prop="id" label="序号" <el-table-column v-if="getColumnVisibility('id')" prop="id" label="序号"
@@ -45,11 +44,21 @@
min-width="180"></el-table-column> min-width="180"></el-table-column>
<el-table-column v-if="getColumnVisibility('email')" prop="email" label="邮箱" <el-table-column v-if="getColumnVisibility('email')" prop="email" label="邮箱"
min-width="200"></el-table-column> min-width="200"></el-table-column>
<el-table-column v-if="getColumnVisibility('actions')" label="操作" min-width="200" align="center"> <el-table-column v-if="getColumnVisibility('actions')" label="操作" min-width="200"
align="center">
<!-- 用户编辑限制 -->
<template #default="scope"> <template #default="scope">
<el-button type="text" size="small" <el-tooltip v-if="scope.row.isLocked" content="此用户无法被编辑或删除">
@click="showEditUserDialog(scope.row)">编辑</el-button> <el-button type="text" size="small" disabled>
<el-button type="text" size="small" @click="confirmDelete(scope.row)">删除</el-button> 编辑
</el-button>
</el-tooltip>
<el-button v-else type="text" size="small" @click="showEditUserDialog(scope.row)">
编辑
</el-button>
<el-button type="text" size="small" :disabled="scope.row.isLocked"
@click="confirmDelete(scope.row)">删除
</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -138,9 +147,10 @@ const refreshTable = async () => {
try { try {
const users = await apiInstance.getAllUsers(); // 调用getAllUsers方法获取用户 const users = await apiInstance.getAllUsers(); // 调用getAllUsers方法获取用户
tableData.value = users.map((user: any, index: number) => ({ tableData.value = users.map((user: any, index: number) => ({
id: index + 1, // 增加id索引 id: index + 1,
username: user.username, username: user.username,
email: user.email email: user.email,
isLocked: ['csmixc','admin'].includes(user.username)
})); }));
} catch (error) { } catch (error) {
ElMessage.error('刷新用户列表失败'); ElMessage.error('刷新用户列表失败');

View File

@@ -13,16 +13,20 @@ import '@kjgl77/datav-vue3/dist/style.css';
import mitt from 'mitt'; import mitt from 'mitt';
import Antd from 'ant-design-vue'; import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/reset.css'; import 'ant-design-vue/dist/reset.css';
import { createPinia } from 'pinia';
const app = createApp(App) const app = createApp(App)
const globalWebSocket = useGlobalWebSocket(); const globalWebSocket = useGlobalWebSocket();
const pinia = createPinia();
app.provide('globalWebSocket', globalWebSocket); app.provide('globalWebSocket', globalWebSocket);
// app.provide('axios', axiosInstance); // app.provide('axios', axiosInstance);
app.use(ElementPlus, { locale: zhCn }); app.use(ElementPlus, { locale: zhCn });
app.use(router); app.use(router);
app.use(DataVVue3); app.use(DataVVue3);
app.use(Antd); app.use(Antd);
app.use(pinia)
// 导航守卫,检查登录状态 // 导航守卫,检查登录状态
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {

View File

@@ -0,0 +1,59 @@
import {ref} from 'vue'
import { defineStore } from 'pinia';
// 定义回调类型
type TimerCallback = () => void;
export const useGlobalTimerStore = defineStore('globalTimer', () => {
const intervalId = ref<number | null>(null); // 定时器 ID
const refreshIntervalMs = ref(300000); // 默认刷新间隔5 分钟)
const registeredCallbacks = ref<TimerCallback[]>([]); // 注册的回调函数列表
// 注册回调
const registerCallback = (callback: TimerCallback) => {
if (typeof callback === 'function' && !registeredCallbacks.value.includes(callback)) {
registeredCallbacks.value.push(callback);
}
};
// 注销回调
const unregisterCallback = (callback: TimerCallback) => {
const index = registeredCallbacks.value.indexOf(callback);
if (index !== -1) {
registeredCallbacks.value.splice(index, 1);
}
};
// 启动定时器
const startTimer = () => {
if (!intervalId.value) {
intervalId.value = window.setInterval(() => {
registeredCallbacks.value.forEach((callback) => callback());
}, refreshIntervalMs.value);
}
};
// 停止定时器
const stopTimer = () => {
if (intervalId.value) {
clearInterval(intervalId.value);
intervalId.value = null;
}
};
// 设置定时器间隔
const setRefreshInterval = (interval: number) => {
refreshIntervalMs.value = interval;
stopTimer();
startTimer();
};
return {
refreshIntervalMs,
registerCallback,
unregisterCallback,
startTimer,
stopTimer,
setRefreshInterval,
};
});

View File

@@ -19,10 +19,7 @@ class BoxApi {
private readonly apiAllCameras: string = "/camera/cameras/get_all"; private readonly apiAllCameras: string = "/camera/cameras/get_all";
private readonly superCamera: string = "/camera/cameras"; private readonly superCamera: string = "/camera/cameras";
private readonly superRule: string = "/rules"; private readonly superRule: string = "/rules";
private readonly getEventByIdUrl: string = "/event/events/retrieves"; private readonly superEvents: string = "/event/events";
private readonly loginConfig: object = { private readonly loginConfig: object = {
headers: { headers: {
@@ -85,7 +82,7 @@ class BoxApi {
try { try {
const res = await this.axios.post(this.apiLogin, loginData, this.loginConfig) const res = await this.axios.post(this.apiLogin, loginData, this.loginConfig)
console.log(res) // console.log(res)
if (res.data.err.ec === 0) { if (res.data.err.ec === 0) {
this.token = res.data.ret.token; this.token = res.data.ret.token;
await this.updateCodemap(this.token); await this.updateCodemap(this.token);
@@ -253,7 +250,7 @@ class BoxApi {
public async updateCamera(token: string | null = null, cameraId: number, jsonData: CameraData): Promise<any> { public async updateCamera(token: string | null = null, cameraId: number, jsonData: CameraData): Promise<any> {
const url = `${this.superCamera}/${cameraId}`; const url = `${this.superCamera}/${cameraId}`;
// const { rules, ...cameraData } = jsonData; // const { rules, ...cameraData } = jsonData;
console.log("接口接收的摄像设置>>>>>>>>>>>>>>", jsonData); // console.log("接口接收的摄像设置>>>>>>>>>>>>>>", jsonData);
try { try {
const newCamera = { const newCamera = {
@@ -300,6 +297,26 @@ class BoxApi {
// throw error; // throw error;
// } // }
// } // }
public async delEvents(token: string | null = null, eventIds: number[]): Promise<{ id: number, success: boolean, message?: string }[]> {
const results: { id: number, success: boolean, message?: string }[] = [];
for (const eventId of eventIds) {
const url = `${this.superEvents}/${eventId}`;
try {
await this.axios.delete(url, this._authHeader(token));
results.push({ id: eventId, success: true });
} catch (error: any) {
results.push({
id: eventId,
success: false,
message: error.response?.data?.err?.dm || error.message
});
}
}
return results;
}
public async updateRule(token: string | null = null, rules: RuleData[]): Promise<any[]> { public async updateRule(token: string | null = null, rules: RuleData[]): Promise<any[]> {
@@ -312,7 +329,7 @@ class BoxApi {
...rule, ...rule,
schedule: rule.schedule || {}, schedule: rule.schedule || {},
}; };
console.log("接口接收的规则设置>>>>>>>>>>>>>>", cleanedRule); // console.log("接口接收的规则设置>>>>>>>>>>>>>>", cleanedRule);
const res = await this.axios.patch(url, cleanedRule, this._authHeader(token)); const res = await this.axios.patch(url, cleanedRule, this._authHeader(token));
if (res.data.err.ec === 0) { if (res.data.err.ec === 0) {
results.push({ id: rule.id, success: true, data: res.data.ret }); results.push({ id: rule.id, success: true, data: res.data.ret });
@@ -476,23 +493,25 @@ class BoxApi {
public async getEventById(id: number, token: string | null = null): Promise<any> { public async getEventById(id: number, token: string | null = null): Promise<any> {
try { try {
const url = `${this.getEventByIdUrl}`; const url = `${this.superEvents}/retrieves`;
const params = { id }; const params = { id };
const res = await this.axios.get(url, { const res = await this.axios.get(url, {
...this._authHeader(token), ...this._authHeader(token),
params: params params: params
}); });
if (res.data.err.ec === 0) { if (res.data.err.ec === 0 && res.data.ret.objects.length > 0) {
// return res.data.ret.objects[0];
return res.data.ret.objects[0]; return res.data.ret.objects[0];
} else { } else {
throw new Error(res.data.err.dm); throw new Error(res.data.err.dm || '未获取到有效数据');
} }
} catch (error) { } catch (error) {
console.error(`getEventById error for ID ${id}:`, error);
throw error; throw error;
} }
} }
// public async getOneEvent(token: string | null = null): Promise<any> { // public async getOneEvent(token: string | null = null): Promise<any> {
// try { // try {
// return await this.getEvents(1, 0, token); // return await this.getEvents(1, 0, token);

View File

@@ -1,12 +1,53 @@
import { ref } from 'vue'; import { ref, reactive } from 'vue';
import { ElMessage, ElNotification } from 'element-plus'; import { ElMessage, ElNotification } from 'element-plus';
import eventBus from '@/utils/eventBus'; import eventBus from '@/utils/eventBus';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { BoxApi } from '@/utils/boxApi'; import { BoxApi } from '@/utils/boxApi';
const apiInstance = new BoxApi(); const websocket = ref<WebSocket | null>(null); // WebSocket 实例
const isWebSocketConnected = ref(false); // WebSocket 连接状态
let heartbeatInterval: number | null = null; // 心跳定时器
const formatDateTime = (isoString: string): string => dayjs(isoString).format('YYYY-MM-DD HH:mm:ss');
const apiInstance = new BoxApi();
const algorithmMap = ref(new Map()); const algorithmMap = ref(new Map());
const maxVisibleNotifications = 2; // 最多同时显示的通知数量
const visibleNotifications: Map<string, any> = new Map(); // 当前显示的通知
const notificationQueue: any[] = []; // 等待显示的通知队列
declare global {
interface Window {
webkitAudioContext?: typeof AudioContext;
}
}
// let notificationSoundParams = { volume: 0.5, frequency: 440 };
// const setNotificationSoundParams = (params: { volume: number; frequency: number }) => {
// notificationSoundParams = params;
// };
const notificationSoundParams = reactive({
volume: 0.5,
frequency: 440,
isSoundEnabled: false,
});
const setNotificationSoundParams = (params: { volume?: number; frequency?: number; isSoundEnabled?: boolean }) => {
Object.assign(notificationSoundParams, params);
// 更新 localStorage
if (params.volume !== undefined) localStorage.setItem('volume', String(notificationSoundParams.volume));
if (params.frequency !== undefined) localStorage.setItem('frequency', String(notificationSoundParams.frequency));
if (params.isSoundEnabled !== undefined) localStorage.setItem('isSoundEnabled', String(notificationSoundParams.isSoundEnabled));
};
// 初始化:从 localStorage 加载参数
const initializeNotificationSoundParams = () => {
notificationSoundParams.volume = parseFloat(localStorage.getItem('volume') || '0.5');
notificationSoundParams.frequency = parseFloat(localStorage.getItem('frequency') || '440');
notificationSoundParams.isSoundEnabled = localStorage.getItem('isSoundEnabled') === 'true';
};
initializeNotificationSoundParams();
// 加载算法映射表 // 加载算法映射表
const loadAlgorithms = async () => { const loadAlgorithms = async () => {
const token = localStorage.getItem('alertToken'); const token = localStorage.getItem('alertToken');
@@ -20,17 +61,14 @@ const loadAlgorithms = async () => {
} }
}; };
const websocket = ref<WebSocket | null>(null); // WebSocket 实例
const isWebSocketConnected = ref(false); // WebSocket 连接状态
let heartbeatInterval: number | null = null; // 心跳定时器
const formatDateTime = (isoString: string): string => dayjs(isoString).format('YYYY-MM-DD HH:mm:ss');
// 连接 WebSocket // 连接 WebSocket
const connectWebSocket = () => { const connectWebSocket = async () => {
const rememberedAddress = localStorage.getItem('rememberedAddress'); // 获取存储的主机地址 const rememberedAddress = localStorage.getItem('rememberedAddress'); // 获取存储的主机地址
if (rememberedAddress) { if (rememberedAddress) {
websocket.value = new WebSocket(`ws://${rememberedAddress}:8080/ws/event`); // websocket.value = new WebSocket(`ws://${rememberedAddress}:8080/ws/event`);
// websocket.value = new WebSocket(`ws://192.168.28.33:8080/ws/event`); websocket.value = new WebSocket(`ws://172.19.7.9:8080/ws/event`);
loadAlgorithms(); await loadAlgorithms();
websocket.value.onopen = () => { websocket.value.onopen = () => {
ElMessage.success('全局 WebSocket 连接成功'); ElMessage.success('全局 WebSocket 连接成功');
isWebSocketConnected.value = true; isWebSocketConnected.value = true;
@@ -38,6 +76,17 @@ const connectWebSocket = () => {
}; };
websocket.value.onmessage = (event) => { websocket.value.onmessage = (event) => {
const data = JSON.parse(event.data); // 解析收到的数据 const data = JSON.parse(event.data); // 解析收到的数据
const isInteractive = localStorage.getItem('isInteractivePopupEnabled') === 'true';
const isResponsive = localStorage.getItem('isResponsivePopupEnabled') === 'true';
if (!isInteractive && !isResponsive) {
return; // 弹窗关闭,直接返回
}
let num = 0;
num += 1;
// console.log(`${new Date().toISOString()}收到新的第${num}弹窗信息:`, data);
playNotificationSound();
handlePopupNotification(data); // 根据模式处理弹窗通知 handlePopupNotification(data); // 根据模式处理弹窗通知
}; };
websocket.value.onclose = handleClose; // 处理连接关闭 websocket.value.onclose = handleClose; // 处理连接关闭
@@ -47,6 +96,33 @@ const connectWebSocket = () => {
} }
}; };
const playNotificationSound = () => {
// if (!isWebSocketConnected.value) return;
if (!notificationSoundParams.isSoundEnabled) return;
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext!)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.type = 'triangle';
oscillator.frequency.setValueAtTime(notificationSoundParams.frequency, audioContext.currentTime);
gainNode.gain.setValueAtTime(notificationSoundParams.volume, audioContext.currentTime);
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.start();
setTimeout(() => {
oscillator.stop();
audioContext.close();
}, 350); // 持续播放 200 毫秒
} catch (error) {
console.error('播放提示音时出错:', error);
}
};
// 关闭 WebSocket // 关闭 WebSocket
const closeWebSocket = () => { const closeWebSocket = () => {
if (websocket.value) { if (websocket.value) {
@@ -90,18 +166,78 @@ const handleError = () => {
stopHeartbeat(); // 停止心跳 stopHeartbeat(); // 停止心跳
}; };
// 显示通知
const showNotification = (notification: any) => {
const id = `${Date.now()}-${Math.random()}`; // 唯一 ID
notification.id = id;
if (visibleNotifications.size < maxVisibleNotifications) {
// 如果未达到最大显示数量,直接显示
const elNotification = ElNotification({
...notification,
onClose: () => {
// 从显示列表中移除
visibleNotifications.delete(id);
// 显示下一条队列中的通知
if (notificationQueue.length > 0) {
showNotification(notificationQueue.shift());
}
// 调用用户定义的关闭回调(如果有)
notification.onClose?.();
},
});
visibleNotifications.set(id, elNotification);
} else {
// 如果达到最大显示数量,存入队列
notificationQueue.push(notification);
}
};
const loadNextNotification = () => {
// 清空当前所有显示的通知
visibleNotifications.forEach((notif) => notif.close());
visibleNotifications.clear();
// 从队列中加载下一条通知
if (notificationQueue.length > 0) {
const nextNotification = notificationQueue.shift();
if (nextNotification) {
showNotification(nextNotification);
}
} else {
// console.log('通知队列为空');
}
};
// 手动清空所有通知
const clearAllNotifications = () => {
visibleNotifications.forEach((notif) => notif.close());
visibleNotifications.clear();
notificationQueue.length = 0;
};
const seenNotificationIds = new Set();
// 根据弹窗模式处理通知 // 根据弹窗模式处理通知
const handlePopupNotification = (data: any) => { const handlePopupNotification = (data: any) => {
const isInteractive = localStorage.getItem('isInteractivePopupEnabled') === 'true'; // 是否为交互式弹窗 const isInteractive = localStorage.getItem('isInteractivePopupEnabled') === 'true'; // 是否为交互式弹窗
const isResponsive = localStorage.getItem('isResponsivePopupEnabled') === 'true'; // 是否为响应式弹窗 const isResponsive = localStorage.getItem('isResponsivePopupEnabled') === 'true'; // 是否为响应式弹窗
if (seenNotificationIds.has(data.id)) return;
seenNotificationIds.add(data.id);
setTimeout(() => seenNotificationIds.delete(data.id), 30000);
if (isResponsive) { if (isResponsive) {
// 响应式模式:直接显示对话框 // 响应式模式:直接显示对话框
// console.log('handlePopupNotification triggered:', data);
eventBus.emit('showDialog', data); eventBus.emit('showDialog', data);
} else if (isInteractive) { } else if (isInteractive) {
const formattedTime = formatDateTime(data.started_at); const formattedTime = formatDateTime(data.started_at);
ElNotification({ showNotification({
title: '新告警', title: '新告警',
message: ` message: `
<div style="max-height: 200px; overflow-y: auto;"> <div style="max-height: 200px; overflow-y: auto;">
@@ -112,8 +248,10 @@ const handlePopupNotification = (data: any) => {
</div> </div>
`, `,
dangerouslyUseHTMLString: true, dangerouslyUseHTMLString: true,
duration: 5000, duration: 10000,
customClass: 'custom-notification', customClass: 'custom-notification',
position: 'bottom-right',
type: 'info',
onClick: () => { onClick: () => {
eventBus.emit('showDialog', data); // 点击通知触发对话框 eventBus.emit('showDialog', data); // 点击通知触发对话框
} }
@@ -126,10 +264,22 @@ export interface GlobalWebSocket {
connectWebSocket: () => void; // 连接 WebSocket connectWebSocket: () => void; // 连接 WebSocket
closeWebSocket: () => void; // 关闭 WebSocket closeWebSocket: () => void; // 关闭 WebSocket
isWebSocketConnected: typeof isWebSocketConnected; // WebSocket 连接状态 isWebSocketConnected: typeof isWebSocketConnected; // WebSocket 连接状态
loadNextNotification: () => void; // 加载下一条通知
clearAllNotifications: () => void; // 清空所有通知
setNotificationSoundParams: (params: { volume?: number; frequency?: number; isSoundEnabled?: boolean }) => void;
notificationSoundParams: {
volume: number;
frequency: number;
isSoundEnabled: boolean;
};
} }
export const useGlobalWebSocket = (): GlobalWebSocket => ({ export const useGlobalWebSocket = (): GlobalWebSocket => ({
connectWebSocket, connectWebSocket,
closeWebSocket, closeWebSocket,
isWebSocketConnected, isWebSocketConnected,
loadNextNotification, // 导出加载下一条通知的方法
clearAllNotifications, // 导出清空所有通知的方法
notificationSoundParams,
setNotificationSoundParams,
}); });