Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e2ad96e56 | |||
| 6c57af9e55 | |||
| ca7d6baa2e | |||
| 0b39c9c602 | |||
| ec77b28cf2 | |||
| 983b865ff7 | |||
| 161f7db147 | |||
| 01a2a5bda6 | |||
| 6437b6bf35 | |||
| 848f2a0018 |
@@ -0,0 +1,271 @@
|
|||||||
|
# 网络设备管理平台
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
这是网络设备管理平台的前端项目,用于在地铁线路运管中心检测和查看各车站网络设备的详细数据、运行情况、异常告警,并提供分析、统计、日志和权限管理能力。
|
||||||
|
|
||||||
|
主要设备类型:
|
||||||
|
|
||||||
|
- 摄像机
|
||||||
|
- 网络录像机
|
||||||
|
- 交换机
|
||||||
|
- 解码器
|
||||||
|
- 智能安防箱
|
||||||
|
- 媒体服务器
|
||||||
|
- 视频服务器
|
||||||
|
- 网络键盘
|
||||||
|
- 报警主机
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- 包管理:pnpm
|
||||||
|
- 构建工具:Vite
|
||||||
|
- 前端框架:Vue 3
|
||||||
|
- 语言:TypeScript
|
||||||
|
- 路由:Vue Router
|
||||||
|
- 组件库:Naive UI
|
||||||
|
- 状态管理:Pinia + pinia-plugin-persistedstate
|
||||||
|
- 服务端状态/轮询:TanStack Vue Query
|
||||||
|
- 本地持久化:localStorage、sessionStorage、IndexedDB/localforage
|
||||||
|
- 网络请求:axios
|
||||||
|
- 实时消息:STOMP/WebSocket(`@stomp/stompjs`)
|
||||||
|
- 图表:ECharts
|
||||||
|
- 图标:lucide-vue-next
|
||||||
|
- 样式:Sass
|
||||||
|
|
||||||
|
## 环境与脚本
|
||||||
|
|
||||||
|
### 运行环境
|
||||||
|
|
||||||
|
- Node.js:`^20.19.0 || >=22.12.0`
|
||||||
|
- pnpm:以 `package.json` 中 `packageManager` 为准
|
||||||
|
|
||||||
|
### 常用命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install # 安装依赖
|
||||||
|
pnpm dev # 启动开发服务,默认端口 9763
|
||||||
|
pnpm build # 类型检查 + Vite 构建 + 构建产物压缩
|
||||||
|
pnpm preview # 预览构建产物
|
||||||
|
pnpm build-only # 仅执行 Vite 构建
|
||||||
|
pnpm type-check # 执行 vue-tsc 类型检查
|
||||||
|
pnpm lint # 执行 ESLint,并带 --fix
|
||||||
|
pnpm format # 使用 Prettier 格式化 src/
|
||||||
|
```
|
||||||
|
|
||||||
|
当前项目未配置测试脚本,也未发现测试文件;不要声称已运行单元测试。需要验证改动时,优先运行 `pnpm type-check`、`pnpm lint`、`pnpm build` 中与改动相关的命令。
|
||||||
|
|
||||||
|
### 构建流程
|
||||||
|
|
||||||
|
`pnpm build` 的实际流程为:
|
||||||
|
|
||||||
|
1. `tsx build/pre-build.ts`:根据 `package.json` 的版本和构建时间写入 `public/manifest.json`。
|
||||||
|
2. `vue-tsc --build`:类型检查。
|
||||||
|
3. `vite build`:生成 `dist/`。
|
||||||
|
4. `tsx build/post-build.ts`:基于 `dist/` 生成 `.zip`、`.tar`、`.tar.gz` 压缩包。
|
||||||
|
|
||||||
|
压缩包命名格式为:`ndm-web-platform_v<version>_<YYMMDD-HHmmss>`。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
项目源码集中在 `src/`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/
|
||||||
|
apis/ # 接口客户端、接口模型、业务请求封装
|
||||||
|
components/ # 业务组件与全局组件
|
||||||
|
device/
|
||||||
|
global/
|
||||||
|
permission/
|
||||||
|
station/
|
||||||
|
composables/ # 组合式函数
|
||||||
|
alarm/
|
||||||
|
common/
|
||||||
|
device/
|
||||||
|
permission/
|
||||||
|
query/ # TanStack Query 轮询与请求编排
|
||||||
|
station/
|
||||||
|
stomp/ # STOMP/WebSocket 客户端
|
||||||
|
constants/ # 常量
|
||||||
|
enums/ # 枚举
|
||||||
|
helpers/ # 辅助逻辑
|
||||||
|
layouts/
|
||||||
|
app-layout.vue # 登录后主布局
|
||||||
|
pages/ # 页面
|
||||||
|
plugins/ # Pinia 持久化等插件
|
||||||
|
router/
|
||||||
|
index.ts # 路由配置与守卫
|
||||||
|
stores/ # Pinia stores
|
||||||
|
styles/ # 全局样式
|
||||||
|
types/ # 全局/工具类型
|
||||||
|
utils/ # 通用工具函数
|
||||||
|
```
|
||||||
|
|
||||||
|
路径别名:`@/*` 指向 `src/*`。
|
||||||
|
|
||||||
|
## 页面与路由
|
||||||
|
|
||||||
|
路由配置在 `src/router/index.ts`。登录页独立于主布局;除 `/login` 外,其余页面都作为 `src/layouts/app-layout.vue` 的子路由。
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/
|
||||||
|
router/
|
||||||
|
index.ts
|
||||||
|
layouts/
|
||||||
|
app-layout.vue
|
||||||
|
pages/
|
||||||
|
login/
|
||||||
|
login-page.vue # 登录页,对应 /login
|
||||||
|
station/
|
||||||
|
station-page.vue # 车站状态页/首页,对应 /station
|
||||||
|
device/
|
||||||
|
device-page.vue # 设备诊断页,对应 /device
|
||||||
|
alarm/
|
||||||
|
alarm-log-page.vue # 设备告警记录,对应 /alarm/alarm-log
|
||||||
|
alarm-ignore-page.vue # 告警忽略管理,对应 /alarm/alarm-ignore
|
||||||
|
log/
|
||||||
|
vimp-log-page.vue # 视频平台日志,对应 /log/vimp-log
|
||||||
|
call-log-page.vue # 上级调用日志,对应 /log/call-log
|
||||||
|
permission/
|
||||||
|
permission-page.vue # 权限管理,对应 /permission
|
||||||
|
system/
|
||||||
|
changelog/
|
||||||
|
changelog-page.vue # 更新记录,对应 /changelog
|
||||||
|
error/
|
||||||
|
not-found-page.vue # 404 页面,对应 catch-all
|
||||||
|
```
|
||||||
|
|
||||||
|
路由守卫规则:
|
||||||
|
|
||||||
|
- 未登录访问非 `/login` 页面会跳转到 `/login`。
|
||||||
|
- 已登录访问 `/login` 会跳转到 `/`。
|
||||||
|
- `/` 默认重定向到 `/station`。
|
||||||
|
- `/permission` 需要 `useUserStore().isLamp` 为真,否则跳转到 404。
|
||||||
|
|
||||||
|
## 数据轮询与状态管理
|
||||||
|
|
||||||
|
由于后端服务按车站分布,前端需要向各车站服务依次请求数据。项目采用“单点驱动 + 变更监听 + 级联触发”的轮询模式。
|
||||||
|
|
||||||
|
核心文件位于 `src/composables/query/`:
|
||||||
|
|
||||||
|
- `use-line-stations-query.ts`:查询所有车站,是业务轮询入口。
|
||||||
|
- `use-user-permission-query.ts`:查询并计算当前用户在各车站的权限,负责调度后续设备/告警查询。
|
||||||
|
- `use-line-devices-query.ts`:查询设备数据。
|
||||||
|
- `use-line-alarms-query.ts`:查询告警数据。
|
||||||
|
- `use-verify-user-query.ts`:用户登录/校验相关请求。
|
||||||
|
- `use-version-check-query.ts`:版本检查,依赖构建阶段生成的 `/manifest.json`。
|
||||||
|
|
||||||
|
关键 Pinia stores 位于 `src/stores/`:
|
||||||
|
|
||||||
|
- `user.ts`:用户登录态、Token、用户类型等。
|
||||||
|
- `station.ts`:车站数据。
|
||||||
|
- `device.ts`:设备数据。
|
||||||
|
- `alarm.ts`:告警数据。
|
||||||
|
- `permission.ts`:权限数据。
|
||||||
|
- `setting.ts`:系统设置、调试开关、网络开关等。
|
||||||
|
- `polling.ts`:轮询状态控制。
|
||||||
|
- `unread.ts`:未读状态。
|
||||||
|
|
||||||
|
持久化注意事项:
|
||||||
|
|
||||||
|
- 大体量业务数据会使用 IndexedDB/localforage。
|
||||||
|
- 普通设置类状态会使用 localStorage/sessionStorage。
|
||||||
|
- `src/main.ts` 会比较 `VITE_STORAGE_VERSION` 与本地 `ndm-storage-version`,不一致时清空 localStorage 并清空 localforage。
|
||||||
|
|
||||||
|
## 接口与代理
|
||||||
|
|
||||||
|
接口相关代码位于 `src/apis/`:
|
||||||
|
|
||||||
|
- `client/`:HTTP 客户端基础封装。
|
||||||
|
- `domain/`:领域相关类型/逻辑。
|
||||||
|
- `model/`:接口模型。
|
||||||
|
- `request/`:按业务拆分的请求函数。
|
||||||
|
|
||||||
|
开发代理配置在 `vite.config.ts`:
|
||||||
|
|
||||||
|
- 开发服务端口:`9763`。
|
||||||
|
- 已配置多个线路/站点前缀代理,包括 01、02、04、10、21 相关站点。
|
||||||
|
- 当前配置包含 `/api`、`/minio`、`/ws` 等代理项。
|
||||||
|
- `ProxyItem.rewrite` 用于将本地请求前缀改写为后端真实路径,例如将 `/1001/api` 改写为 `/api`。
|
||||||
|
|
||||||
|
修改代理时,应同步检查 `key`、`target`、`rewrite`、`ws` 是否匹配实际环境。
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
项目使用 Vite 环境变量,当前变量集中在 `.env`。文档或回答中只列变量名和用途,不要复述真实密钥、密码或授权值。
|
||||||
|
|
||||||
|
常见变量:
|
||||||
|
|
||||||
|
- `VITE_APP_TITLE`:页面标题。
|
||||||
|
- `VITE_REQUEST_INTERVAL`:轮询间隔,单位秒。
|
||||||
|
- `VITE_NDM_APP_KEY`:网管 appKey。
|
||||||
|
- `VITE_LAMP_CLIENT_ID`:LAMP clientId。
|
||||||
|
- `VITE_LAMP_CLIENT_SECRET`:LAMP clientSecret。
|
||||||
|
- `VITE_LAMP_USERNAME`:LAMP 登录用户名。
|
||||||
|
- `VITE_LAMP_PASSWORD`:LAMP 登录密码。
|
||||||
|
- `VITE_LAMP_AUTHORIZATION`:已有 Authorization 时直接使用,否则由 clientId/clientSecret 生成。
|
||||||
|
- `VITE_STORAGE_VERSION`:本地缓存版本,用于触发缓存清理。
|
||||||
|
- `VITE_DEBUG_CODE`:调试模式授权码。
|
||||||
|
|
||||||
|
## 调试模式与离线开发
|
||||||
|
|
||||||
|
调试模式默认隐藏,用于开发、联调和故障排查。
|
||||||
|
|
||||||
|
开启方式:
|
||||||
|
|
||||||
|
1. 使用快捷键 `Ctrl + Alt + D` 唤起验证弹窗。
|
||||||
|
2. 输入 `VITE_DEBUG_CODE` 对应授权码。
|
||||||
|
3. 验证通过后,“系统设置”中会显示调试分组。
|
||||||
|
|
||||||
|
调试相关能力:
|
||||||
|
|
||||||
|
- 显示设备原始数据。
|
||||||
|
- 控制是否轮询车站。
|
||||||
|
- 控制是否主动请求。
|
||||||
|
- 控制是否订阅 STOMP/WebSocket 消息。
|
||||||
|
- 启用模拟用户。
|
||||||
|
- 允许在特定场景下直接操作本地 IndexedDB。
|
||||||
|
|
||||||
|
离线开发:
|
||||||
|
|
||||||
|
- 如果浏览器已有现场缓存,可在调试模式中关闭轮询、主动请求和消息订阅,直接查看本地缓存。
|
||||||
|
- 全新环境可在登录页控制台设置 `window.$mockUser.value = true` 进入模拟登录,再通过调试面板导入 `docs/data/` 下的数据:
|
||||||
|
- `ndm-station-store.json`
|
||||||
|
- `ndm-device-store.json`
|
||||||
|
- `ndm-alarm-store.json`
|
||||||
|
|
||||||
|
## 代码风格与约定
|
||||||
|
|
||||||
|
- 遵循现有 Vue SFC、TypeScript、组合式函数和 Pinia 写法。
|
||||||
|
- 新增业务请求时优先放入 `src/apis/request/` 对应业务分类。
|
||||||
|
- 新增页面时同步更新 `src/router/index.ts`,并保持 `src/pages/` 目录与路由语义一致。
|
||||||
|
- 新增共享逻辑优先放入 `src/composables/`、`src/helpers/` 或 `src/utils/`,不要在页面中堆积重复逻辑。
|
||||||
|
- 新增全局/业务组件时优先放入 `src/components/` 对应分类。
|
||||||
|
- 使用 `@/` 引用 `src/` 下模块。
|
||||||
|
- 不要在代码、文档或回复中泄露真实密码、Token、Authorization 或现场地址以外的敏感信息。
|
||||||
|
|
||||||
|
格式化配置来自 `.prettierrc.json`:
|
||||||
|
|
||||||
|
- LF 换行
|
||||||
|
- 2 空格缩进
|
||||||
|
- 单引号
|
||||||
|
- 使用分号
|
||||||
|
- `trailingComma: all`
|
||||||
|
- `printWidth: 200`
|
||||||
|
|
||||||
|
ESLint 重点规则:
|
||||||
|
|
||||||
|
- `@typescript-eslint/no-unused-vars` 为 warn。
|
||||||
|
- `@typescript-eslint/no-explicit-any` 当前关闭,但新增代码仍应尽量保持类型清晰。
|
||||||
|
- `vue/multi-word-component-names` 当前关闭。
|
||||||
|
|
||||||
|
## 协作规则
|
||||||
|
|
||||||
|
1. 默认不要直接修改项目代码;当用户明确授权修改时,只修改授权范围内的文件。
|
||||||
|
2. 修改前先阅读相关文件和既有实现,避免凭空猜测目录、接口或状态结构。
|
||||||
|
3. 修改后说明改了哪些文件、为什么改、如何验证。
|
||||||
|
4. 文档类修改至少重新读取目标文件确认内容正确。
|
||||||
|
5. 代码类修改应按影响范围运行 `pnpm type-check`、`pnpm lint`、`pnpm build` 或更小范围的可用验证命令。
|
||||||
|
6. 不要提交 Git commit,除非用户明确要求。
|
||||||
|
7. 不要删除失败测试或通过压制类型错误来规避问题。
|
||||||
|
8. 不要在最终说明中声称执行了未实际执行的命令。
|
||||||
@@ -1,4 +1,20 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"version": "0.42.0",
|
||||||
|
"date": "2026-05-20",
|
||||||
|
"changes": {
|
||||||
|
"fixes": [
|
||||||
|
{ "content": "优化安防箱环境数据卡片,避免空标签渲染并改进风扇信息展示" },
|
||||||
|
{ "content": "修复设备硬件卡片进度条异常值显示问题,并完善状态判断逻辑" }
|
||||||
|
],
|
||||||
|
"feats": [
|
||||||
|
{ "content": "新增服务器网卡信息展示" },
|
||||||
|
{ "content": "新增安防箱网卡信息展示,并优化相关卡片标题" },
|
||||||
|
{ "content": "为交换机诊断信息新增温度字段" },
|
||||||
|
{ "content": "新增录像机环境状态卡片和网卡信息展示" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.41.0",
|
"version": "0.41.0",
|
||||||
"date": "2026-05-19",
|
"date": "2026-05-19",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"version": "0.41.0",
|
"version": "0.42.0",
|
||||||
"buildTime": "2026-05-19 19:40:37"
|
"buildTime": "2026-05-20 13:52:07"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,37 @@ export interface NdmNvrDiagInfo {
|
|||||||
totalSize?: number;
|
totalSize?: number;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
ethInfo: {
|
||||||
|
adminStatus?: string; // '1'
|
||||||
|
desc?: string; // 'bond1'
|
||||||
|
ifType?: string; // '6'
|
||||||
|
inDiscards?: string; // '17931688'
|
||||||
|
inErrors?: string; // '0'
|
||||||
|
inNUcastPkts?: string; // '40945433'
|
||||||
|
inOctets?: string; // '3453544614'
|
||||||
|
inUcastPkts?: string; // '3375411816'
|
||||||
|
inUnknownProtos?: string; // '0'
|
||||||
|
index?: string; // '8'
|
||||||
|
lastChange?: string; // '0:05:42.15'
|
||||||
|
mTU?: string; // '1500'
|
||||||
|
macAddress?: string; // '04:7b:cb:69:92:58'
|
||||||
|
operStatus?: string; // '1'
|
||||||
|
outDiscards?: string; // '0'
|
||||||
|
outErrors?: string; // '0'
|
||||||
|
outNUcastPkts?: string; // '0'
|
||||||
|
outOctets?: string; // '3443476717'
|
||||||
|
outQLen?: string; // '0'
|
||||||
|
outUcastPkts?: string; // '415381735'
|
||||||
|
specific?: string; // '0.0'
|
||||||
|
speed?: string; // '2000000000'
|
||||||
|
};
|
||||||
|
ipInfo: {
|
||||||
|
broadcastAddress?: string; // '1'
|
||||||
|
iPAddress?: string; // '10.14.1.22'
|
||||||
|
index?: string; // '8'
|
||||||
|
mASK?: string; // '255.255.255.0'
|
||||||
|
reasmMaxSize?: string; // '0'
|
||||||
|
};
|
||||||
stCommonInfo?: {
|
stCommonInfo?: {
|
||||||
设备ID?: string;
|
设备ID?: string;
|
||||||
软件版本?: string;
|
软件版本?: string;
|
||||||
@@ -20,12 +51,16 @@ export interface NdmNvrDiagInfo {
|
|||||||
内存使用率?: string;
|
内存使用率?: string;
|
||||||
CPU使用率?: string;
|
CPU使用率?: string;
|
||||||
};
|
};
|
||||||
cdFanInfo?: {
|
cdFanInfo?: NdmNvrFanInfo[];
|
||||||
索引号?: string;
|
cdPowerSupplyInfo?: NdmNvrPowerSupplyInfo[];
|
||||||
'风扇转速(rpm)'?: string;
|
}
|
||||||
}[];
|
|
||||||
cdPowerSupplyInfo?: {
|
export interface NdmNvrFanInfo {
|
||||||
索引号?: string;
|
索引号?: string;
|
||||||
电源状态?: string;
|
'风扇转速(rpm)'?: string;
|
||||||
}[];
|
}
|
||||||
|
|
||||||
|
export interface NdmNvrPowerSupplyInfo {
|
||||||
|
索引号?: string;
|
||||||
|
电源状态?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,36 @@
|
|||||||
export interface NdmSecurityBoxDiagInfo {
|
export interface NdmSecurityBoxDiagInfo {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
|
ethInfo?: {
|
||||||
|
adminStatus?: string; // '1'
|
||||||
|
desc?: string; // 'br-lan'
|
||||||
|
ifType?: string; // '6'
|
||||||
|
inDiscards?: string; // '84977'
|
||||||
|
inErrors?: string; // '0'
|
||||||
|
inNUcastPkts?: string; // '233473'
|
||||||
|
inOctets?: string; // '92355850'
|
||||||
|
inUcastPkts?: string; // '899970'
|
||||||
|
inUnknownProtos?: string; // '0'
|
||||||
|
index?: string; // '8'
|
||||||
|
lastChange?: string; // '0:00:00.00'
|
||||||
|
mTU?: string; // '1500'
|
||||||
|
macAddress?: string; // '56:62:bc:d3:9e:37'
|
||||||
|
operStatus?: string; // '1'
|
||||||
|
outDiscards?: string; // '0'
|
||||||
|
outErrors?: string; // '0'
|
||||||
|
outNUcastPkts?: string; // '0'
|
||||||
|
outOctets?: string; // '68175904'
|
||||||
|
outQLen?: string; // '0'
|
||||||
|
outUcastPkts?: string; // '748100'
|
||||||
|
specific?: string; // '0.0'
|
||||||
|
speed?: string; // '0'
|
||||||
|
};
|
||||||
|
ipInfo?: {
|
||||||
|
broadcastAddress?: string; // '1'
|
||||||
|
iPAddress?: string; // '10.24.18.101'
|
||||||
|
index?: string; // '8'
|
||||||
|
mASK?: string; // '255.255.255.0'
|
||||||
|
reasmMaxSize?: string; // '0'
|
||||||
|
};
|
||||||
info?: [
|
info?: [
|
||||||
{
|
{
|
||||||
addrCode?: number;
|
addrCode?: number;
|
||||||
|
|||||||
@@ -6,4 +6,35 @@ export interface NdmServerDiagInfo {
|
|||||||
磁盘使用率?: string;
|
磁盘使用率?: string;
|
||||||
系统运行时间?: string;
|
系统运行时间?: string;
|
||||||
};
|
};
|
||||||
|
ethInfo?: {
|
||||||
|
adminStatus?: string; // '1'
|
||||||
|
desc?: string; // 'Intel Corporation I350 Gigabit Network Connection'
|
||||||
|
ifType?: string; // '6'
|
||||||
|
inDiscards?: string; // '6707634'
|
||||||
|
inErrors?: string; // '0'
|
||||||
|
inNUcastPkts?: string; // '8991944'
|
||||||
|
inOctets?: string; // '4220524983'
|
||||||
|
inUcastPkts?: string; // '2342740610'
|
||||||
|
inUnknownProtos?: string; // '0'
|
||||||
|
index?: string; // '2'
|
||||||
|
lastChange?: string; // '0:03:15.26'
|
||||||
|
mTU?: string; // '1500'
|
||||||
|
macAddress?: string; // 'e8:78:ee:f6:8d:98'
|
||||||
|
operStatus?: string; // '1'
|
||||||
|
outDiscards?: string; // '0'
|
||||||
|
outErrors?: string; // '0'
|
||||||
|
outNUcastPkts?: string; // '0'
|
||||||
|
outOctets?: string; // '1415770066'
|
||||||
|
outQLen?: string; // '0'
|
||||||
|
outUcastPkts?: string; // '3335494729'
|
||||||
|
specific?: string; // '0.0'
|
||||||
|
speed?: string; // '1000000000'
|
||||||
|
};
|
||||||
|
ipInfo?: {
|
||||||
|
broadcastAddress?: string; // '1'
|
||||||
|
iPAddress?: string; // '10.14.1.8'
|
||||||
|
index?: string; // '2'
|
||||||
|
mASK?: string; // '255.255.255.0'
|
||||||
|
reasmMaxSize?: string; // '0'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export interface NdmSwitchDiagInfo {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
cpuRatio?: string; // 因环境不同可能不存在
|
cpuRatio?: string; // 因环境不同可能不存在
|
||||||
memoryRatio?: string; // 因环境不同可能不存在
|
memoryRatio?: string; // 因环境不同可能不存在
|
||||||
|
temperature?: number;
|
||||||
logTime?: string;
|
logTime?: string;
|
||||||
info?: {
|
info?: {
|
||||||
overFlowPorts?: string[];
|
overFlowPorts?: string[];
|
||||||
|
|||||||
@@ -51,8 +51,13 @@ const formattedRunningTime = computed(() => {
|
|||||||
return (runningTime?.value ?? '-').replace('days', '天');
|
return (runningTime?.value ?? '-').replace('days', '天');
|
||||||
});
|
});
|
||||||
|
|
||||||
const getProgressStatus = (percent?: number): ProgressStatus | undefined => {
|
const getProgressPercentage = (percent: number) => {
|
||||||
if (!percent) return undefined;
|
if (percent < 0) return 0;
|
||||||
|
if (percent > 100) return 100;
|
||||||
|
return percent;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProgressStatus = (percent: number): ProgressStatus => {
|
||||||
if (percent >= 90) return 'error';
|
if (percent >= 90) return 'error';
|
||||||
if (percent >= 70) return 'warning';
|
if (percent >= 70) return 'warning';
|
||||||
return 'success';
|
return 'success';
|
||||||
@@ -66,20 +71,20 @@ const getProgressStatus = (percent?: number): ProgressStatus | undefined => {
|
|||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<NFlex vertical>
|
<NFlex vertical>
|
||||||
<NFlex v-if="cpuUsage" style="width: 100%" align="center" :wrap="false">
|
<NFlex v-if="cpuPercent" style="width: 100%" align="center" :wrap="false">
|
||||||
<NIcon :component="CpuIcon" />
|
<NIcon :component="CpuIcon" />
|
||||||
<span style="word-break: keep-all">{{ cpuUsageLabel || 'CPU' }}</span>
|
<span style="word-break: keep-all">{{ cpuUsageLabel || 'CPU' }}</span>
|
||||||
<NProgress :percentage="cpuPercent" :status="getProgressStatus(cpuPercent)">{{ cpuPercent ?? '-' }}%</NProgress>
|
<NProgress :percentage="getProgressPercentage(cpuPercent)" :status="getProgressStatus(cpuPercent)">{{ cpuPercent }}%</NProgress>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
<NFlex v-if="memUsage" style="width: 100%" align="center" :wrap="false">
|
<NFlex v-if="memPercent" style="width: 100%" align="center" :wrap="false">
|
||||||
<NIcon :component="MemoryStickIcon" />
|
<NIcon :component="MemoryStickIcon" />
|
||||||
<span style="word-break: keep-all">{{ memUsageLabel || '内存' }}</span>
|
<span style="word-break: keep-all">{{ memUsageLabel || '内存' }}</span>
|
||||||
<NProgress :percentage="memPercent" :status="getProgressStatus(memPercent)">{{ memPercent ?? '-' }}%</NProgress>
|
<NProgress :percentage="getProgressPercentage(memPercent)" :status="getProgressStatus(memPercent)">{{ memPercent }}%</NProgress>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
<NFlex v-if="diskUsage" style="width: 100%" align="center" :wrap="false">
|
<NFlex v-if="diskPercent" style="width: 100%" align="center" :wrap="false">
|
||||||
<NIcon :component="HardDriveIcon" />
|
<NIcon :component="HardDriveIcon" />
|
||||||
<span style="word-break: keep-all">{{ diskUsageLabel || '磁盘' }}</span>
|
<span style="word-break: keep-all">{{ diskUsageLabel || '磁盘' }}</span>
|
||||||
<NProgress :percentage="diskPercent" :status="getProgressStatus(diskPercent)">{{ diskPercent ?? '-' }}%</NProgress>
|
<NProgress :percentage="getProgressPercentage(diskPercent)" :status="getProgressStatus(diskPercent)">{{ diskPercent }}%</NProgress>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
<NFlex v-if="runningTime" style="width: 100%" align="center" :wrap="false">
|
<NFlex v-if="runningTime" style="width: 100%" align="center" :wrap="false">
|
||||||
<NIcon :component="ClockCheckIcon" />
|
<NIcon :component="ClockCheckIcon" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { ComponentInstance } from 'vue';
|
|||||||
import DeviceCommonCard from './device-common-card.vue';
|
import DeviceCommonCard from './device-common-card.vue';
|
||||||
import DeviceHardwareCard from './device-hardware-card.vue';
|
import DeviceHardwareCard from './device-hardware-card.vue';
|
||||||
import DeviceHeaderCard from './device-header-card.vue';
|
import DeviceHeaderCard from './device-header-card.vue';
|
||||||
|
import NvrEnvCard from './nvr-env-card.vue';
|
||||||
import NvrDiskCard from './nvr-disk-card.vue';
|
import NvrDiskCard from './nvr-disk-card.vue';
|
||||||
import NvrRecordCheckCard from './nvr-record-check-card.vue';
|
import NvrRecordCheckCard from './nvr-record-check-card.vue';
|
||||||
import SecurityBoxCircuitCard from './security-box-circuit-card.vue';
|
import SecurityBoxCircuitCard from './security-box-circuit-card.vue';
|
||||||
@@ -16,6 +17,7 @@ export {
|
|||||||
DeviceCommonCard,
|
DeviceCommonCard,
|
||||||
DeviceHardwareCard,
|
DeviceHardwareCard,
|
||||||
DeviceHeaderCard,
|
DeviceHeaderCard,
|
||||||
|
NvrEnvCard,
|
||||||
NvrDiskCard,
|
NvrDiskCard,
|
||||||
NvrRecordCheckCard,
|
NvrRecordCheckCard,
|
||||||
SecurityBoxCircuitCard,
|
SecurityBoxCircuitCard,
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { NdmNvrFanInfo, NdmNvrPowerSupplyInfo } from '@/apis';
|
||||||
|
import { FanIcon, PlugIcon } from 'lucide-vue-next';
|
||||||
|
import { NCard, NFlex, NIcon, NTag } from 'naive-ui';
|
||||||
|
import { computed, toRefs } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fanInfo?: NdmNvrFanInfo[];
|
||||||
|
powerSupplyInfo?: NdmNvrPowerSupplyInfo[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { fanInfo, powerSupplyInfo } = toRefs(props);
|
||||||
|
|
||||||
|
const showCard = computed(() => {
|
||||||
|
return Object.values(props).some((value) => !!value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NCard v-if="showCard" hoverable size="small">
|
||||||
|
<template #header>
|
||||||
|
<span>录像机环境状态</span>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<NFlex vertical>
|
||||||
|
<NTag v-for="info in fanInfo ?? []" :key="info['索引号']">
|
||||||
|
<template #icon>
|
||||||
|
<NIcon :component="FanIcon" />
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<span>风扇{{ info['索引号'] }}: {{ info['风扇转速(rpm)'] }} RPM</span>
|
||||||
|
</template>
|
||||||
|
</NTag>
|
||||||
|
<NTag v-for="info in powerSupplyInfo ?? []" :key="info['索引号']">
|
||||||
|
<template #icon>
|
||||||
|
<NIcon :component="PlugIcon" />
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<span>电源{{ info['索引号'] }}: {{ info['电源状态'] }}</span>
|
||||||
|
</template>
|
||||||
|
</NTag>
|
||||||
|
</NFlex>
|
||||||
|
</template>
|
||||||
|
</NCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
+7
-12
@@ -35,21 +35,16 @@ const getStatusTagType = (status: string | null) => {
|
|||||||
if (['失效', '开门'].includes(status ?? '')) return 'error';
|
if (['失效', '开门'].includes(status ?? '')) return 'error';
|
||||||
return 'default';
|
return 'default';
|
||||||
};
|
};
|
||||||
|
|
||||||
const formattedFanSpeeds = computed(() => {
|
|
||||||
if (!fanSpeeds?.value || fanSpeeds.value.length === 0) return null;
|
|
||||||
return fanSpeeds.value.map((speed, index) => `风扇${index + 1}: ${speed} RPM`).join(', ');
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NCard v-if="showCard" hoverable size="small">
|
<NCard v-if="showCard" hoverable size="small">
|
||||||
<template #header>
|
<template #header>
|
||||||
<span>安防箱状态</span>
|
<span>安防箱环境状态</span>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<NFlex vertical>
|
<NFlex vertical>
|
||||||
<NTag>
|
<NTag v-if="!!temperature">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="ThermometerIcon" />
|
<NIcon :component="ThermometerIcon" />
|
||||||
</template>
|
</template>
|
||||||
@@ -57,7 +52,7 @@ const formattedFanSpeeds = computed(() => {
|
|||||||
<span>温度: {{ temperature }}℃</span>
|
<span>温度: {{ temperature }}℃</span>
|
||||||
</template>
|
</template>
|
||||||
</NTag>
|
</NTag>
|
||||||
<NTag>
|
<NTag v-if="!!humidity">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="DropletIcon" />
|
<NIcon :component="DropletIcon" />
|
||||||
</template>
|
</template>
|
||||||
@@ -65,15 +60,15 @@ const formattedFanSpeeds = computed(() => {
|
|||||||
<span>湿度: {{ humidity }}%</span>
|
<span>湿度: {{ humidity }}%</span>
|
||||||
</template>
|
</template>
|
||||||
</NTag>
|
</NTag>
|
||||||
<NTag>
|
<NTag v-for="(speed, index) in fanSpeeds ?? []" :key="index">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="FanIcon" />
|
<NIcon :component="FanIcon" />
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<span>风扇: {{ formattedFanSpeeds }}</span>
|
<span>风扇{{ index + 1 }}: {{ speed }} RPM</span>
|
||||||
</template>
|
</template>
|
||||||
</NTag>
|
</NTag>
|
||||||
<NTag :type="getStatusTagType(accessControlStatus)">
|
<NTag v-if="!!accessControlStatus" :type="getStatusTagType(accessControlStatus)">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="ShieldIcon" />
|
<NIcon :component="ShieldIcon" />
|
||||||
</template>
|
</template>
|
||||||
@@ -81,7 +76,7 @@ const formattedFanSpeeds = computed(() => {
|
|||||||
<span>门禁: {{ accessControlStatus }}</span>
|
<span>门禁: {{ accessControlStatus }}</span>
|
||||||
</template>
|
</template>
|
||||||
</NTag>
|
</NTag>
|
||||||
<NTag :type="getStatusTagType(lightningProtectionStatus)">
|
<NTag v-if="!!lightningProtectionStatus" :type="getStatusTagType(lightningProtectionStatus)">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="ZapIcon" />
|
<NIcon :component="ZapIcon" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { NdmNvrDiagInfo, NdmNvrResultVO, Station } from '@/apis';
|
import type { NdmNvrDiagInfo, NdmNvrResultVO, Station } from '@/apis';
|
||||||
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, NvrDiskCard, NvrRecordCheckCard, type DeviceCommonCardProps } from '@/components';
|
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, NvrDiskCard, NvrEnvCard, NvrRecordCheckCard, type DeviceCommonCardProps } from '@/components';
|
||||||
import { isNvrCluster } from '@/helpers';
|
import { isNvrCluster } from '@/helpers';
|
||||||
import destr from 'destr';
|
import destr from 'destr';
|
||||||
import { NFlex } from 'naive-ui';
|
import { NFlex } from 'naive-ui';
|
||||||
@@ -21,9 +21,14 @@ const lastDiagInfo = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const commonInfo = computed<DeviceCommonCardProps['commonInfo']>(() => {
|
const commonInfo = computed<DeviceCommonCardProps['commonInfo']>(() => {
|
||||||
const { stCommonInfo } = lastDiagInfo.value ?? {};
|
const { ipAddress } = ndmDevice.value;
|
||||||
if (!stCommonInfo) return undefined;
|
const { ethInfo, ipInfo, stCommonInfo } = lastDiagInfo.value ?? {};
|
||||||
const { 设备ID, 软件版本, 生产厂商, 设备别名, 设备型号, 硬件版本 } = stCommonInfo;
|
const { 设备ID, 软件版本, 生产厂商, 设备别名, 设备型号, 硬件版本 } = stCommonInfo ?? {};
|
||||||
|
|
||||||
|
let operStatus = '-';
|
||||||
|
if (!!ethInfo?.operStatus) {
|
||||||
|
operStatus = ethInfo?.operStatus === '1' ? '正常' : '异常';
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -37,6 +42,17 @@ const commonInfo = computed<DeviceCommonCardProps['commonInfo']>(() => {
|
|||||||
{ label: '硬件版本', value: 硬件版本 || '-' },
|
{ label: '硬件版本', value: 硬件版本 || '-' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '设备网卡信息',
|
||||||
|
items: [
|
||||||
|
{ label: 'IP地址', value: ipAddress || '-' },
|
||||||
|
{ label: '子网掩码', value: ipInfo?.mASK || '-' },
|
||||||
|
{ label: 'MAC地址', value: ethInfo?.macAddress || '-' },
|
||||||
|
{ label: '连接速率', value: ethInfo?.speed || '-' },
|
||||||
|
{ label: 'MTU', value: ethInfo?.mTU || '-' },
|
||||||
|
{ label: '运行状态', value: operStatus },
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -45,6 +61,9 @@ const memUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.内存使用
|
|||||||
|
|
||||||
const diskHealth = computed(() => lastDiagInfo.value?.info?.diskHealth);
|
const diskHealth = computed(() => lastDiagInfo.value?.info?.diskHealth);
|
||||||
const diskArray = computed(() => lastDiagInfo.value?.info?.groupInfoList);
|
const diskArray = computed(() => lastDiagInfo.value?.info?.groupInfoList);
|
||||||
|
|
||||||
|
const fanInfo = computed(() => lastDiagInfo.value?.cdFanInfo);
|
||||||
|
const powerSupplyInfo = computed(() => lastDiagInfo.value?.cdPowerSupplyInfo);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -52,6 +71,7 @@ const diskArray = computed(() => lastDiagInfo.value?.info?.groupInfoList);
|
|||||||
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
|
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
|
||||||
<DeviceCommonCard :common-info="commonInfo" />
|
<DeviceCommonCard :common-info="commonInfo" />
|
||||||
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
|
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
|
||||||
|
<NvrEnvCard :fan-info="fanInfo" :power-supply-info="powerSupplyInfo" />
|
||||||
<NvrDiskCard :disk-health="diskHealth" :disk-array="diskArray" />
|
<NvrDiskCard :disk-health="diskHealth" :disk-array="diskArray" />
|
||||||
<template v-if="isNvrCluster(ndmDevice)">
|
<template v-if="isNvrCluster(ndmDevice)">
|
||||||
<NvrRecordCheckCard :ndm-device="ndmDevice" :station="station" />
|
<NvrRecordCheckCard :ndm-device="ndmDevice" :station="station" />
|
||||||
|
|||||||
@@ -35,9 +35,15 @@ const vendor = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const commonInfo = computed<DeviceCommonCardProps['commonInfo']>(() => {
|
const commonInfo = computed<DeviceCommonCardProps['commonInfo']>(() => {
|
||||||
const { stCommonInfo } = lastDiagInfo.value ?? {};
|
const { ipAddress } = ndmDevice.value;
|
||||||
|
const { ethInfo, ipInfo, stCommonInfo } = lastDiagInfo.value ?? {};
|
||||||
const { 设备ID, 软件版本, 设备厂商, 设备别名, 设备型号, 硬件版本 } = stCommonInfo ?? {};
|
const { 设备ID, 软件版本, 设备厂商, 设备别名, 设备型号, 硬件版本 } = stCommonInfo ?? {};
|
||||||
|
|
||||||
|
let operStatus = '-';
|
||||||
|
if (!!ethInfo?.operStatus) {
|
||||||
|
operStatus = ethInfo?.operStatus === '1' ? '正常' : '异常';
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: '设备型号信息',
|
title: '设备型号信息',
|
||||||
@@ -50,6 +56,17 @@ const commonInfo = computed<DeviceCommonCardProps['commonInfo']>(() => {
|
|||||||
{ label: '硬件版本', value: 硬件版本 || '-' },
|
{ label: '硬件版本', value: 硬件版本 || '-' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '设备网卡信息',
|
||||||
|
items: [
|
||||||
|
{ label: 'IP地址', value: ipAddress || '-' },
|
||||||
|
{ label: '子网掩码', value: ipInfo?.mASK || '-' },
|
||||||
|
{ label: 'MAC地址', value: ethInfo?.macAddress || '-' },
|
||||||
|
{ label: '连接速率', value: ethInfo?.speed || '-' },
|
||||||
|
{ label: 'MTU', value: ethInfo?.mTU || '-' },
|
||||||
|
{ label: '运行状态', value: operStatus },
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type NdmServerDiagInfo, type NdmServerResultVO, type Station } from '@/apis';
|
import { type NdmServerDiagInfo, type NdmServerResultVO, type Station } from '@/apis';
|
||||||
import { DeviceHardwareCard, DeviceHeaderCard, ServerAlive, ServerHighAvailable, ServerStreamPush } from '@/components';
|
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, ServerAlive, ServerHighAvailable, ServerStreamPush, type DeviceCommonCardProps } from '@/components';
|
||||||
import destr from 'destr';
|
import destr from 'destr';
|
||||||
import { NFlex } from 'naive-ui';
|
import { NFlex } from 'naive-ui';
|
||||||
import { computed, toRefs } from 'vue';
|
import { computed, toRefs } from 'vue';
|
||||||
@@ -19,6 +19,30 @@ const lastDiagInfo = computed(() => {
|
|||||||
return result as NdmServerDiagInfo;
|
return result as NdmServerDiagInfo;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const commonInfo = computed<DeviceCommonCardProps['commonInfo']>(() => {
|
||||||
|
const { ipAddress } = ndmDevice.value;
|
||||||
|
const { ethInfo, ipInfo } = lastDiagInfo.value ?? {};
|
||||||
|
|
||||||
|
let operStatus = '-';
|
||||||
|
if (!!ethInfo?.operStatus) {
|
||||||
|
operStatus = ethInfo?.operStatus === '1' ? '正常' : '异常';
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: '设备网卡信息',
|
||||||
|
items: [
|
||||||
|
{ label: 'IP地址', value: ipAddress || '-' },
|
||||||
|
{ label: '子网掩码', value: ipInfo?.mASK || '-' },
|
||||||
|
{ label: 'MAC地址', value: ethInfo?.macAddress || '-' },
|
||||||
|
{ label: '连接速率', value: ethInfo?.speed || '-' },
|
||||||
|
{ label: 'MTU', value: ethInfo?.mTU || '-' },
|
||||||
|
{ label: '运行状态', value: operStatus },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
const cpuUsage = computed(() => lastDiagInfo.value?.commInfo?.CPU使用率);
|
const cpuUsage = computed(() => lastDiagInfo.value?.commInfo?.CPU使用率);
|
||||||
const memUsage = computed(() => lastDiagInfo.value?.commInfo?.内存使用率);
|
const memUsage = computed(() => lastDiagInfo.value?.commInfo?.内存使用率);
|
||||||
const diskUsage = computed(() => lastDiagInfo.value?.commInfo?.磁盘使用率);
|
const diskUsage = computed(() => lastDiagInfo.value?.commInfo?.磁盘使用率);
|
||||||
@@ -29,6 +53,7 @@ const runningTime = computed(() => lastDiagInfo.value?.commInfo?.系统运行时
|
|||||||
<NFlex vertical>
|
<NFlex vertical>
|
||||||
<ServerHighAvailable :ndm-device="ndmDevice" :station="station" />
|
<ServerHighAvailable :ndm-device="ndmDevice" :station="station" />
|
||||||
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
|
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
|
||||||
|
<DeviceCommonCard :common-info="commonInfo" />
|
||||||
<DeviceHardwareCard running-time-label="服务器运行时间" :cpu-usage="cpuUsage" :mem-usage="memUsage" :disk-usage="diskUsage" :running-time="runningTime" />
|
<DeviceHardwareCard running-time-label="服务器运行时间" :cpu-usage="cpuUsage" :mem-usage="memUsage" :disk-usage="diskUsage" :running-time="runningTime" />
|
||||||
<ServerAlive :ndm-device="ndmDevice" :station="station" />
|
<ServerAlive :ndm-device="ndmDevice" :station="station" />
|
||||||
<ServerStreamPush :ndm-device="ndmDevice" :station="station" />
|
<ServerStreamPush :ndm-device="ndmDevice" :station="station" />
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const createDeviceNodeKey = (stationCode?: Station['code'], device?: NdmDeviceRe
|
|||||||
import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis';
|
import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis';
|
||||||
import { useDeviceTree, usePermission, type UseDeviceTreeReturn } from '@/composables';
|
import { useDeviceTree, usePermission, type UseDeviceTreeReturn } from '@/composables';
|
||||||
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType, PERMISSION_TYPE_LITERALS } from '@/enums';
|
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType, PERMISSION_TYPE_LITERALS } from '@/enums';
|
||||||
import { isNvrCluster } from '@/helpers';
|
import { createNvrClusterRelationship, isNvrCluster, nvrInCluster } from '@/helpers';
|
||||||
import { useDeviceStore, usePermissionStore } from '@/stores';
|
import { useDeviceStore, usePermissionStore } from '@/stores';
|
||||||
import { watchDebounced, watchImmediate } from '@vueuse/core';
|
import { watchDebounced, watchImmediate } from '@vueuse/core';
|
||||||
import destr from 'destr';
|
import destr from 'destr';
|
||||||
@@ -297,8 +297,18 @@ const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: Station[
|
|||||||
return h(NFlex, { size: 'small' }, { default: () => [renderViewDeviceButton(device, stationCode), renderDeviceStatusTag(device)] });
|
return h(NFlex, { size: 'small' }, { default: () => [renderViewDeviceButton(device, stationCode), renderDeviceStatusTag(device)] });
|
||||||
};
|
};
|
||||||
// 全线设备树
|
// 全线设备树
|
||||||
const lineDeviceTreeData = computed<Record<Station['code'], TreeOption[]>>(() => {
|
const lineDeviceTreeData = computed<Record<DeviceType, TreeOption[]>>(() => {
|
||||||
const treeData: Record<string, TreeOption[]> = {};
|
const treeData: Record<DeviceType, TreeOption[]> = {
|
||||||
|
[DEVICE_TYPE_LITERALS.ndmCamera]: [],
|
||||||
|
[DEVICE_TYPE_LITERALS.ndmNvr]: [],
|
||||||
|
[DEVICE_TYPE_LITERALS.ndmSwitch]: [],
|
||||||
|
[DEVICE_TYPE_LITERALS.ndmDecoder]: [],
|
||||||
|
[DEVICE_TYPE_LITERALS.ndmSecurityBox]: [],
|
||||||
|
[DEVICE_TYPE_LITERALS.ndmMediaServer]: [],
|
||||||
|
[DEVICE_TYPE_LITERALS.ndmVideoServer]: [],
|
||||||
|
[DEVICE_TYPE_LITERALS.ndmKeyboard]: [],
|
||||||
|
[DEVICE_TYPE_LITERALS.ndmAlarmHost]: [],
|
||||||
|
};
|
||||||
deviceTabPanes.forEach(({ name: paneName /* , tab: paneTab */ }) => {
|
deviceTabPanes.forEach(({ name: paneName /* , tab: paneTab */ }) => {
|
||||||
treeData[paneName] = stations.value.map<TreeOption>((station) => {
|
treeData[paneName] = stations.value.map<TreeOption>((station) => {
|
||||||
const { name: stationName, code: stationCode } = station;
|
const { name: stationName, code: stationCode } = station;
|
||||||
@@ -307,47 +317,55 @@ const lineDeviceTreeData = computed<Record<Station['code'], TreeOption[]>>(() =>
|
|||||||
const offlineDevices = devices?.filter((device) => device.deviceStatus === '20');
|
const offlineDevices = devices?.filter((device) => device.deviceStatus === '20');
|
||||||
// 对于录像机,需要根据clusterList字段以分号分隔设备IP,进一步形成子树结构
|
// 对于录像机,需要根据clusterList字段以分号分隔设备IP,进一步形成子树结构
|
||||||
if (paneName === DEVICE_TYPE_LITERALS.ndmNvr) {
|
if (paneName === DEVICE_TYPE_LITERALS.ndmNvr) {
|
||||||
const nvrs = devices as NdmNvrResultVO[];
|
const nvrDevices = devices as NdmNvrResultVO[];
|
||||||
const nvrClusters: NdmNvrResultVO[] = [];
|
|
||||||
const nvrSingletons: NdmNvrResultVO[] = [];
|
const { nvrClusters, nvrTreeMap, nvrStandalones } = createNvrClusterRelationship(nvrDevices);
|
||||||
for (const device of nvrs) {
|
|
||||||
if (isNvrCluster(device)) {
|
|
||||||
nvrClusters.push(device);
|
|
||||||
} else {
|
|
||||||
nvrSingletons.push(device);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
label: stationName,
|
label: stationName,
|
||||||
key: stationCode,
|
key: stationCode,
|
||||||
prefix: () => renderStationNodePrefix(station),
|
prefix: () => renderStationNodePrefix(station),
|
||||||
suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0),
|
suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0),
|
||||||
children: nvrClusters.map<TreeOption>((cluster) => {
|
children: [
|
||||||
return {
|
...nvrClusters.map((cluster) => {
|
||||||
label: `${cluster.name}`,
|
return {
|
||||||
key: createDeviceNodeKey(stationCode, cluster),
|
label: `${cluster.name}`,
|
||||||
prefix: () => renderDeviceNodePrefix(cluster, stationCode),
|
key: createDeviceNodeKey(stationCode, cluster),
|
||||||
suffix: () => `${cluster.ipAddress}`,
|
prefix: () => renderDeviceNodePrefix(cluster, stationCode),
|
||||||
children: nvrSingletons.map<TreeOption>((device) => {
|
suffix: () => `${cluster.ipAddress}`,
|
||||||
return {
|
children: (nvrTreeMap.get(cluster.ipAddress ?? '') ?? []).map((clusterNode) => {
|
||||||
label: `${device.name}`,
|
return {
|
||||||
key: createDeviceNodeKey(stationCode, device),
|
label: `${clusterNode.name}`,
|
||||||
prefix: () => renderDeviceNodePrefix(device, stationCode),
|
key: createDeviceNodeKey(stationCode, clusterNode),
|
||||||
suffix: () => `${device.ipAddress}`,
|
prefix: () => renderDeviceNodePrefix(clusterNode, stationCode),
|
||||||
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
|
suffix: () => `${clusterNode.ipAddress}`,
|
||||||
stationCode,
|
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
|
||||||
device: device,
|
stationCode,
|
||||||
};
|
device: clusterNode,
|
||||||
}),
|
};
|
||||||
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
|
}),
|
||||||
stationCode,
|
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
|
||||||
device: cluster,
|
stationCode,
|
||||||
};
|
device: cluster,
|
||||||
}),
|
};
|
||||||
|
}),
|
||||||
|
...nvrStandalones.map((device) => {
|
||||||
|
return {
|
||||||
|
label: `${device.name}`,
|
||||||
|
key: createDeviceNodeKey(stationCode, device),
|
||||||
|
prefix: () => renderDeviceNodePrefix(device, stationCode),
|
||||||
|
suffix: () => `${device.ipAddress}`,
|
||||||
|
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
|
||||||
|
stationCode,
|
||||||
|
device: device,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
],
|
||||||
stationCode,
|
stationCode,
|
||||||
deviceType: activeTab.value,
|
deviceType: activeTab.value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// 非录像机设备
|
||||||
return {
|
return {
|
||||||
label: stationName,
|
label: stationName,
|
||||||
key: stationCode,
|
key: stationCode,
|
||||||
@@ -382,37 +400,51 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
|
|||||||
const onlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '10').length;
|
const onlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '10').length;
|
||||||
const offlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '20').length;
|
const offlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '20').length;
|
||||||
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
|
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
|
||||||
const nvrs = stationDevices[deviceType] as NdmNvrResultVO[];
|
const nvrDevices = stationDevices[deviceType] as NdmNvrResultVO[];
|
||||||
const clusters = nvrs.filter((nvr) => isNvrCluster(nvr));
|
|
||||||
const singletons = nvrs.filter((nvr) => !isNvrCluster(nvr));
|
const { nvrClusters, nvrTreeMap, nvrStandalones } = createNvrClusterRelationship(nvrDevices);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: `${DEVICE_TYPE_NAMES[deviceType]}`,
|
label: `${DEVICE_TYPE_NAMES[deviceType]}`,
|
||||||
key: deviceType,
|
key: deviceType,
|
||||||
suffix: () => renderIcmpStatistics(onlineCount, offlineCount, nvrs.length),
|
suffix: () => renderIcmpStatistics(onlineCount, offlineCount, nvrDevices.length),
|
||||||
children: clusters.map<TreeOption>((cluster) => {
|
children: [
|
||||||
return {
|
...nvrClusters.map((cluster) => {
|
||||||
label: `${cluster.name}`,
|
return {
|
||||||
key: createDeviceNodeKey(stationCode, cluster),
|
label: `${cluster.name}`,
|
||||||
prefix: () => renderDeviceNodePrefix(cluster, stationCode),
|
key: createDeviceNodeKey(stationCode, cluster),
|
||||||
suffix: () => `${cluster.ipAddress}`,
|
prefix: () => renderDeviceNodePrefix(cluster, stationCode),
|
||||||
children: singletons.map<TreeOption>((device) => {
|
suffix: () => `${cluster.ipAddress}`,
|
||||||
return {
|
children: (nvrTreeMap.get(cluster.ipAddress ?? '') ?? []).map((clusterNode) => {
|
||||||
label: `${device.name}`,
|
return {
|
||||||
key: createDeviceNodeKey(stationCode, device),
|
label: `${clusterNode.name}`,
|
||||||
prefix: () => renderDeviceNodePrefix(device, stationCode),
|
key: createDeviceNodeKey(stationCode, clusterNode),
|
||||||
suffix: () => `${device.ipAddress}`,
|
prefix: () => renderDeviceNodePrefix(clusterNode, stationCode),
|
||||||
stationCode,
|
suffix: () => `${clusterNode.ipAddress}`,
|
||||||
device,
|
stationCode,
|
||||||
};
|
device: clusterNode,
|
||||||
}),
|
};
|
||||||
stationCode,
|
}),
|
||||||
device: cluster,
|
stationCode,
|
||||||
};
|
device: cluster,
|
||||||
}),
|
};
|
||||||
|
}),
|
||||||
|
...nvrStandalones.map((device) => {
|
||||||
|
return {
|
||||||
|
label: `${device.name}`,
|
||||||
|
key: createDeviceNodeKey(stationCode, device),
|
||||||
|
prefix: () => renderDeviceNodePrefix(device, stationCode),
|
||||||
|
suffix: () => `${device.ipAddress}`,
|
||||||
|
stationCode,
|
||||||
|
device,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
],
|
||||||
stationCode,
|
stationCode,
|
||||||
deviceType,
|
deviceType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// 非录像机设备
|
||||||
return {
|
return {
|
||||||
label: `${DEVICE_TYPE_NAMES[deviceType]}`,
|
label: `${DEVICE_TYPE_NAMES[deviceType]}`,
|
||||||
key: deviceType,
|
key: deviceType,
|
||||||
@@ -488,9 +520,13 @@ const onLocateDeviceTree = async () => {
|
|||||||
if (stationDevices) {
|
if (stationDevices) {
|
||||||
const selectedNvr = selectedDevice.value as NdmNvrResultVO;
|
const selectedNvr = selectedDevice.value as NdmNvrResultVO;
|
||||||
if (!isNvrCluster(selectedNvr)) {
|
if (!isNvrCluster(selectedNvr)) {
|
||||||
const nvrs = stationDevices[DEVICE_TYPE_LITERALS.ndmNvr];
|
const nvrDevices = stationDevices[DEVICE_TYPE_LITERALS.ndmNvr];
|
||||||
const clusters = nvrs.filter((nvr) => isNvrCluster(nvr) && nvr.clusterList?.includes(selectedNvr.clusterList ?? ''));
|
const clusters = nvrDevices.filter((device) => {
|
||||||
expandedKeys.value.push(...clusters.map((nvr) => createDeviceNodeKey(stationCode, nvr)));
|
if (!isNvrCluster(device)) return false;
|
||||||
|
const cluster = device;
|
||||||
|
return nvrInCluster(selectedNvr, cluster);
|
||||||
|
});
|
||||||
|
expandedKeys.value.push(...clusters.map((cluster) => createDeviceNodeKey(stationCode, cluster)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,56 @@
|
|||||||
import type { NdmNvrResultVO } from '@/apis';
|
import type { NdmNvrResultVO } from '@/apis';
|
||||||
|
|
||||||
|
// 解析 clusterList 字段
|
||||||
|
export const parseIpListFromClusterList = (nvr: NdmNvrResultVO) => {
|
||||||
|
const ipList = (nvr.clusterList ?? '').split(';');
|
||||||
|
return ipList.map((ip) => ip.trim()).filter((ip) => !!ip);
|
||||||
|
};
|
||||||
|
|
||||||
export const isNvrCluster = (maybeNvrCluster: NdmNvrResultVO) => {
|
export const isNvrCluster = (maybeNvrCluster: NdmNvrResultVO) => {
|
||||||
const { ipAddress, clusterList } = maybeNvrCluster;
|
const { ipAddress } = maybeNvrCluster;
|
||||||
if (!clusterList?.trim()) return false;
|
const ipList = parseIpListFromClusterList(maybeNvrCluster);
|
||||||
if (clusterList === ipAddress) return false;
|
if (ipList.length === 0) return false;
|
||||||
|
if (ipList.length === 1 && ipList.at(0) === ipAddress) return false;
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const nvrInCluster = (nvr: NdmNvrResultVO, cluster: NdmNvrResultVO) => {
|
||||||
|
const { ipAddress } = nvr;
|
||||||
|
if (!ipAddress) return false;
|
||||||
|
const ipList = parseIpListFromClusterList(cluster);
|
||||||
|
return ipList.includes(ipAddress);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createNvrClusterRelationship = (nvrDevices: NdmNvrResultVO[]) => {
|
||||||
|
const nvrClusters = nvrDevices.filter((nvr) => isNvrCluster(nvr));
|
||||||
|
const nvrNotClusters = nvrDevices.filter((nvr) => !isNvrCluster(nvr));
|
||||||
|
const nodedNvrIpAddressSet = new Set<string | null>();
|
||||||
|
const nvrStandalones: NdmNvrResultVO[] = [];
|
||||||
|
const nvrTreeMap = new Map<string, NdmNvrResultVO[]>();
|
||||||
|
// 遍历所有非集群录像机,将它们分配到对应的录像机集群中
|
||||||
|
for (const nvr of nvrNotClusters) {
|
||||||
|
for (const cluster of nvrClusters) {
|
||||||
|
if (nvrInCluster(nvr, cluster)) {
|
||||||
|
if (!!cluster.ipAddress) {
|
||||||
|
// 写入录像机与集群的关系
|
||||||
|
nvrTreeMap.set(cluster.ipAddress, [...(nvrTreeMap.get(cluster.ipAddress) ?? []), nvr]);
|
||||||
|
// 记录已分配的录像机IP地址
|
||||||
|
nodedNvrIpAddressSet.add(nvr.ipAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 分配完成后,过滤出未分配的录像机,形成录像机单机列表
|
||||||
|
nvrNotClusters.forEach((device) => {
|
||||||
|
if (!nodedNvrIpAddressSet.has(device.ipAddress)) {
|
||||||
|
nvrStandalones.push(device);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
nvrClusters,
|
||||||
|
// nvrNotClusters,
|
||||||
|
nvrTreeMap,
|
||||||
|
nvrStandalones,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -175,6 +175,12 @@ const line10ApiProxyList: ProxyItem[] = [
|
|||||||
{ key: '/1032/api', target: 'http://10.18.244.10:18760', rewrite: ['/1032/api', '/api'] },
|
{ key: '/1032/api', target: 'http://10.18.244.10:18760', rewrite: ['/1032/api', '/api'] },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const line21YangpuTestProxyList: ProxyItem[] = [
|
||||||
|
{ key: '/2175/api', target: 'http://10.24.0.10:18760', rewrite: ['/2175/api', '/api'] },
|
||||||
|
{ key: '/2109/api', target: 'http://10.24.17.10:18760', rewrite: ['/2109/api', '/api'] },
|
||||||
|
{ key: '/2180/api', target: 'http://10.24.116.10:18760', rewrite: ['/2180/api', '/api'] },
|
||||||
|
];
|
||||||
|
|
||||||
const apiProxyList: ProxyItem[] = [
|
const apiProxyList: ProxyItem[] = [
|
||||||
// { key: '/minio', target: 'http://10.14.0.10:9000', rewrite: ['/minio', ''] },
|
// { key: '/minio', target: 'http://10.14.0.10:9000', rewrite: ['/minio', ''] },
|
||||||
// { key: '/api', target: 'http://10.14.0.10:18760' },
|
// { key: '/api', target: 'http://10.14.0.10:18760' },
|
||||||
@@ -195,6 +201,12 @@ const apiProxyList: ProxyItem[] = [
|
|||||||
{ key: '/api', target: 'http://10.18.128.10:18760' },
|
{ key: '/api', target: 'http://10.18.128.10:18760' },
|
||||||
{ key: '/ws', target: 'ws://10.18.128.10:18103', ws: true },
|
{ key: '/ws', target: 'ws://10.18.128.10:18103', ws: true },
|
||||||
...line10ApiProxyList,
|
...line10ApiProxyList,
|
||||||
|
|
||||||
|
// 杨浦厂验环境
|
||||||
|
// { key: '/minio', target: 'http://10.24.0.10:9000', rewrite: ['/minio', ''] },
|
||||||
|
// { key: '/api', target: 'http://10.24.0.10:18760' },
|
||||||
|
// { key: '/ws', target: 'ws://10.24.0.10:18103', ws: true },
|
||||||
|
...line21YangpuTestProxyList,
|
||||||
];
|
];
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
|
|||||||
Reference in New Issue
Block a user