32 Commits

Author SHA1 Message Date
yangsy 1e2ad96e56 docs(AGENTS.md): 新增AGENTS.md项目说明文档 2026-05-25 10:14:54 +08:00
yangsy 6c57af9e55 feat(vite配置): 添加杨浦21号线测试环境代理配置
新增杨浦21号线测试环境专用代理配置列表,添加注释的默认测试环境代理条目,并将新代理列表加入主代理配置中
2026-05-25 10:09:43 +08:00
yangsy ca7d6baa2e refactor: 重构NVR集群处理逻辑,优化设备树展示
抽离NVR集群相关的公共工具函数,重构isNvrCluster的实现逻辑。修复原有设备树中NVR分组的逻辑错误,原本会将所有单机NVR添加到每个集群的子节点中,现在会正确将单机NVR分配到对应集群,未归属的单机NVR作为独立节点。简化设备树组件的代码,统一使用封装后的工具方法处理集群分组。修复设备定位时的集群匹配逻辑,使用更准确的IP包含判断。初始化设备树数据的默认空值,修正类型定义错误。
2026-05-20 21:18:54 +08:00
yangsy 0b39c9c602 chore: 版本信息和更新日志 2026-05-20 13:55:14 +08:00
yangsy ec77b28cf2 fix: 修复设备硬件卡片进度条异常值并完善状态判断逻辑
新增进度百分比限制函数,修正0值进度的状态判断,调整模板变量判断条件
2026-05-20 13:35:41 +08:00
yangsy 983b865ff7 feat: 新增录像机环境状态卡片和网卡信息 2026-05-20 12:52:24 +08:00
yangsy 161f7db147 feat: 为交换机诊断信息添加temperature字段 2026-05-20 12:50:27 +08:00
yangsy 01a2a5bda6 feat: 新增安防箱网卡信息展示并更新卡片标题
- 修改安防箱环境卡片标题为“安防箱环境状态”
- 新增网卡信息展示模块,包含IP、子网掩码、MAC地址等参数
2026-05-20 12:48:18 +08:00
yangsy 6437b6bf35 feat: 添加服务器网卡信息展示
扩展NdmServerDiagInfo数据类型,新增网卡和IP信息字段,并在服务器诊断面板展示相关网络状态详情
2026-05-20 12:45:54 +08:00
yangsy 848f2a0018 fix: 优化安防箱环境数据卡片空标签渲染并重构风扇展示
为温度、湿度、门禁和防雷标签添加数据存在性判断,避免渲染空标签;移除冗余计算属性,改为循环直接展示每个风扇转速
2026-05-20 12:04:04 +08:00
yangsy eaa855a09e chore: 版本信息和更新日志 2026-05-19 19:42:23 +08:00
yangsy a014bbfd13 feat: 支持配置摄像机的告警阈值 2026-05-19 16:40:18 +08:00
yangsy 87962d188c feat: 新增摄像机硬件占用率历史诊断记录卡片 2026-05-19 16:38:49 +08:00
yangsy d334e55551 fix: 修复录像机历史诊断记录loading载状态同步问题 2026-05-19 16:37:56 +08:00
yangsy b8927e064b fix: 修复解码器历史诊断记录loading载状态同步问题 2026-05-19 16:34:29 +08:00
yangsy 11b673550b 修复: 优化摄像机网卡信息的运行状态显示并移除管理状态
将运行状态的原始值转换为中文展示,值为'1'时显示“正常”,其他非空值显示“异常”,无值时显示'-'
2026-05-19 15:08:43 +08:00
yangsy c3f3844cd5 feat: 新增多厂商安防箱支持并重构相关逻辑
- 新增安防箱适配器模块,统一封装不同厂商的设备操作逻辑
- 拆分原有安防箱API为beidian、ningtech两个厂商的专属实现
- 扩展安防箱诊断信息接口,补充设备ID、版本等元数据字段
- 更新安防箱设备卡片组件,支持自动识别厂商并适配空开控制逻辑
2026-05-19 15:01:27 +08:00
yangsy a43a8b24e3 fix 修正安防箱门禁和防雷状态显示错误 2026-05-19 14:58:15 +08:00
yangsy f29ab9f768 feat: 安防箱修改面板新增开关数量和团体字符串(写) 2026-05-19 14:56:30 +08:00
yangsy 2a6f049938 feat: 为安防箱新增circuitCount字段 2026-05-19 14:55:04 +08:00
yangsy 18378b79a6 refactor: 重构helpers目录导出入口,整合原有分散的模块导出 2026-05-18 15:01:53 +08:00
yangsy 3fae0b841b feat: 为各设备实体模型新增writeCommunity字段
在各设备实体接口文件中添加writeCommunity字符串字段
2026-05-18 14:12:00 +08:00
yangsy b8ef57e417 feat: 统一并优化设备诊断页面的信息展示
重构报警主机、解码器、录像机、安防箱的设备诊断页通用信息展示逻辑,适配组件新的props格式。完善摄像头诊断相关的接口类型定义,为摄像头诊断页新增硬件使用率展示卡片,补充完整的设备基础信息和网络信息内容
2026-05-18 14:11:59 +08:00
yangsy c03667b312 fix: 修正交换机端口卡片显示条件,仅在端口非空时展示 2026-05-18 14:11:58 +08:00
yangsy 2b7b4e7bd9 refactor: 重构设备通用卡片组件,调整props格式并优化布局
- 导出DeviceCommonCardProps类型以提供类型支持
- 将commonInfo属性从键值对对象调整为分组数组格式
- 使用NFlex和NText优化卡片内部的布局展示
2026-05-18 14:11:58 +08:00
yangsy dc8184d5dd refactor: 提取设备硬件卡片中通用百分比解析函数并优化空值展示
提取统一的百分比字符串解析工具函数,替换原有重复的解析逻辑,新增空值或无效值时显示 `-` 的处理
2026-05-18 14:11:58 +08:00
yangsy f36e5c3d3c refactor: 移除未使用的 NCard 导入 2026-04-13 11:02:36 +08:00
yangsy de241334a9 chore: 版本信息和更新日志 2026-04-10 15:43:12 +08:00
yangsy c75338cb70 feat: 添加视频服务器双机热备状态 2026-04-10 15:11:58 +08:00
yangsy 86e3e1726d fix: 修复设备查询缓存键冲突问题
- 将查询键中的设备ID改为由车站编码和设备ID组成的唯一键,防止不同车站下相同设备ID导致的数据混淆。
- 同时更新相关监控逻辑,当车站或设备IP变化时重置页面状态。
2026-04-10 10:15:32 +08:00
yangsy 943aa27de1 fix: 将“电路”统一更正为“空开” 2026-04-02 10:38:48 +08:00
yangsy b4442fe6c4 fix: 修复设备树节点key值生成逻辑,最大限度避免key值碰撞导致设备树渲染异常 2026-03-30 20:30:26 +08:00
60 changed files with 1293 additions and 235 deletions
+271
View File
@@ -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. 不要在最终说明中声称执行了未实际执行的命令。
+44
View File
@@ -1,4 +1,48 @@
[
{
"version": "0.42.0",
"date": "2026-05-20",
"changes": {
"fixes": [
{ "content": "优化安防箱环境数据卡片,避免空标签渲染并改进风扇信息展示" },
{ "content": "修复设备硬件卡片进度条异常值显示问题,并完善状态判断逻辑" }
],
"feats": [
{ "content": "新增服务器网卡信息展示" },
{ "content": "新增安防箱网卡信息展示,并优化相关卡片标题" },
{ "content": "为交换机诊断信息新增温度字段" },
{ "content": "新增录像机环境状态卡片和网卡信息展示" }
]
}
},
{
"version": "0.41.0",
"date": "2026-05-19",
"changes": {
"fixes": [
{ "content": "修复交换机端口卡片在端口数据为空时仍展示的问题" },
{ "content": "修复安防箱门禁状态和防雷状态显示错误的问题" },
{ "content": "修复解码器和录像机历史诊断中硬件占用卡片的加载状态同步问题" }
],
"feats": [
{ "content": "新增摄像机阈值配置功能" },
{ "content": "新增摄像机硬件占用率历史诊断记录卡片" },
{ "content": "优化设备诊断页的信息展示结构,支持按设备型号、网卡信息、接入信息等分组展示" },
{ "content": "为摄像机诊断信息新增网卡信息、IP信息和设备通用信息展示" },
{ "content": "新增多厂商安防箱支持,支持BeiDian和NingTech安防箱的空开控制与设备重启" },
{ "content": "安防箱配置新增开关数量和团体字符串(写)" },
{ "content": "为各类设备实体新增团体字符串(写)字段" }
]
}
},
{
"version": "0.40.0",
"date": "2026-04-10",
"changes": {
"fixes": [{ "content": "修复设备树搜索时节点错乱的问题" }, { "content": "将安防箱面板的“电路”统一更正为“空开”" }, { "content": "修复设备查询缓存键冲突问题" }],
"feats": [{ "content": "添加视频服务器双机热备状态" }]
}
},
{
"version": "0.39.0",
"date": "2026-03-02",
+2 -2
View File
@@ -1,4 +1,4 @@
{
"version": "0.39.0",
"buildTime": "2026-03-11 14:35:45"
"version": "0.42.0",
"buildTime": "2026-05-20 13:52:07"
}
@@ -1,5 +1,45 @@
export interface NdmCameraDiagInfo {
[key: string]: any;
ethInfo?: {
adminStatus?: string; // '1'
desc?: string; // 'IPcamera'
ifType?: string; // '5'
inDiscards?: string; // '0'
inErrors?: string; // '0'
inNUcastPkts?: string; // '0'
inOctets?: string; // '0'
inUcastPkts?: string; // '0'
inUnknownProtos?: string; // '0'
index?: string; // '1'
lastChange?: string; // '0:00:00.00'
mTU?: string; // '1500'
macAddress?: string; // '04:ee:cd:52:3a:a5'
operStatus?: string; // '1'
outDiscards?: string; // '0'
outErrors?: string; // '0'
outNUcastPkts?: string; // '0'
outOctets?: string; // '0'
outQLen?: string; // '0'
outUcastPkts?: string; // '0'
specific?: string; // '0.0'
speed?: string; // '10000000'
};
ipInfo?: {
broadcastAddress?: string; // '0'
iPAddress?: string; // '0'
index?: string; // '1'
mASK?: string; // '255.255.255.0'
reasmMaxSize?: string; // '0'
};
logTime?: string;
info?: string;
stCommonInfo?: {
设备ID?: string;
软件版本?: string;
设备厂商?: string;
设备别名?: string;
设备型号?: string;
硬件版本?: string;
内存使用率?: string;
CPU使用率?: string;
};
}
+43 -8
View File
@@ -10,6 +10,37 @@ export interface NdmNvrDiagInfo {
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?: {
设备ID?: string;
软件版本?: string;
@@ -20,12 +51,16 @@ export interface NdmNvrDiagInfo {
内存使用率?: string;
CPU使用率?: string;
};
cdFanInfo?: {
索引号?: string;
'风扇转速(rpm)'?: string;
}[];
cdPowerSupplyInfo?: {
索引号?: string;
电源状态?: string;
}[];
cdFanInfo?: NdmNvrFanInfo[];
cdPowerSupplyInfo?: NdmNvrPowerSupplyInfo[];
}
export interface NdmNvrFanInfo {
索引号?: string;
'风扇转速(rpm)'?: string;
}
export interface NdmNvrPowerSupplyInfo {
索引号?: string;
电源状态?: string;
}
@@ -1,5 +1,36 @@
export interface NdmSecurityBoxDiagInfo {
[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?: [
{
addrCode?: number;
@@ -12,8 +43,14 @@ export interface NdmSecurityBoxDiagInfo {
];
stCommonInfo?: {
[key: string]: any;
内存使用率?: string;
CPU使用率?: string;
设备ID?: string; // 'NTBoxMetro'
软件版本?: string; // 'V0101'
设备厂商?: string; // 'NingTech'
设备别名?: string; // 'SUN-IBOX'
设备型号?: string; // 'SUN-IBOX'
硬件版本?: string; // 'V0101'
内存使用率?: string; // '18'
CPU使用率?: string; // '1'
};
}
@@ -6,4 +6,35 @@ export interface NdmServerDiagInfo {
磁盘使用率?: 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;
cpuRatio?: string; // 因环境不同可能不存在
memoryRatio?: string; // 因环境不同可能不存在
temperature?: number;
logTime?: string;
info?: {
overFlowPorts?: string[];
@@ -0,0 +1,5 @@
export interface HighAvailable {
pyip: string;
vip: string;
changeDate: string;
}
+1
View File
@@ -1,3 +1,4 @@
export * from './diag';
export * from './high-available';
export * from './link-description';
export * from './station';
@@ -20,7 +20,9 @@ export interface NdmSecurityBox extends BaseModel {
description: string;
deviceStatus: string;
deviceType: string;
circuitCount: number;
community: string;
writeCommunity: string;
frontendConfig: string;
linkDescription: string;
snmpEnabled: boolean;
@@ -21,6 +21,7 @@ export interface NdmSwitch extends BaseModel {
deviceStatus: string;
deviceType: string;
community: string;
writeCommunity: string;
frontendConfig: string;
linkDescription: string;
snmpEnabled: boolean;
@@ -30,6 +30,7 @@ export interface NdmNvr extends BaseModel {
deviceStatus: string;
deviceType: string;
community: string;
writeCommunity: string;
frontendConfig: string;
linkDescription: string;
snmpEnabled: boolean;
@@ -31,6 +31,7 @@ export interface NdmCamera extends BaseModel {
deviceType: string;
cameraType: string;
community: string;
writeCommunity: string;
frontendConfig: string;
linkDescription: string;
snmpEnabled: boolean;
@@ -28,6 +28,7 @@ export interface NdmDecoder extends BaseModel {
deviceStatus: string;
deviceType: string;
community: string;
writeCommunity: string;
frontendConfig: string;
linkDescription: string;
snmpEnabled: boolean;
@@ -21,6 +21,7 @@ export interface NdmKeyboard extends BaseModel {
deviceStatus: string;
deviceType: string;
community: string;
writeCommunity: string;
frontendConfig: string;
linkDescription: string;
snmpEnabled: boolean;
@@ -25,6 +25,7 @@ export interface NdmMediaServer extends BaseModel {
deviceStatus: string;
deviceType: string;
community: string;
writeCommunity: string;
frontendConfig: string;
linkDescription: string;
snmpEnabled: boolean;
@@ -25,6 +25,7 @@ export interface NdmVideoServer extends BaseModel {
deviceStatus: string;
deviceType: string;
community: string;
writeCommunity: string;
frontendConfig: string;
linkDescription: string;
snmpEnabled: boolean;
+28 -4
View File
@@ -93,22 +93,46 @@ export const probeSecurityBoxApi = async (ids: string[], options?: { stationCode
unwrapVoidResponse(resp);
};
export const turnCitcuitStatusApi = async (ipAddress: string, circuitIndex: number, status: number, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
// beidian安防箱切换空开状态
export const turnCircuitStatusBeidianApi = async (community: string, ipAddress: string, circuitIndex: number, status: number, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSecurityBox/turnStatus`;
const resp = await client.post<boolean>(endpoint, { community: 'public', ipAddress, circuit: `${circuitIndex}`, status }, { signal });
const resp = await client.post<boolean>(endpoint, { community, ipAddress, circuit: `${circuitIndex}`, status }, { signal });
const data = unwrapResponse(resp);
return data;
};
export const rebootSecurityBoxApi = async (ipAddress: string, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
// beidian安防箱重启
export const rebootSecurityBoxBeidianApi = async (community: string, ipAddress: string, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSecurityBox/reboot`;
const resp = await client.post<boolean>(endpoint, { community: 'public', ipAddress }, { signal });
const resp = await client.post<boolean>(endpoint, { community, ipAddress }, { signal });
const data = unwrapResponse(resp);
return data;
};
// ningtech安防箱切换空开状态
export const turnCircuitStatusNingTechApi = async (community: string, ipAddress: string, circuitIndex: number, status: number, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSecurityBox/turnStatusNingTech`;
const resp = await client.post<boolean>(endpoint, { community, ipAddress, circuit: `${circuitIndex}`, status }, { signal });
const data = unwrapResponse(resp);
return data;
};
// ningtech安防箱重启
export const rebootSecurityBoxNingTechApi = async (community: string, ipAddress: string, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSecurityBox/rebootNingTech`;
const resp = await client.post<boolean>(endpoint, { community, ipAddress }, { signal });
const data = unwrapResponse(resp);
return data;
};
@@ -1,5 +1,26 @@
import { ndmClient, userClient, type MediaServerStatus, type SendRtpInfo, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
import { ndmClient, userClient, type HighAvailable, type MediaServerStatus, type SendRtpInfo, type Station } from '@/apis';
import { unwrapNullableResponse, unwrapResponse } from '@/utils';
import destr from 'destr';
// {
// "code": 0,
// "data": "{pyip:\"10.18.128.14\",vip:\"10.18.128.6\",changeDate:\"2026-03-23 15:55:00\"}",
// "msg": "ok",
// "path": null,
// "extra": null,
// "timestamp": "1774421387908",
// "errorMsg": "",
// "isSuccess": true
// }
export const getHighAvailableApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmServiceAvailable/highAvailable/get`;
const resp = await client.get<string | null>(endpoint, { signal });
const data = unwrapNullableResponse(resp);
return destr<HighAvailable | null>(data);
};
export const getAllPushApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -1,16 +1,20 @@
<script setup lang="ts">
import { NCard, NDescriptions, NDescriptionsItem } from 'naive-ui';
import { NCard, NDescriptions, NDescriptionsItem, NFlex, NText } from 'naive-ui';
import { computed, toRefs } from 'vue';
const props = defineProps<{
commonInfo?: Record<string, string>;
commonInfo?: Array<{
title: string;
items: Array<{
label: string;
value: string;
}>;
}>;
}>();
const { commonInfo } = toRefs(props);
const showCard = computed(() => !!commonInfo.value);
const commonInfoEntries = computed(() => Object.entries(commonInfo.value ?? {}));
const showCard = computed(() => (commonInfo.value?.length ?? 0) > 0);
</script>
<template>
@@ -18,11 +22,20 @@ const commonInfoEntries = computed(() => Object.entries(commonInfo.value ?? {}))
<template #header>
<div>设备信息</div>
</template>
<NDescriptions bordered label-placement="left" :columns="2">
<template v-for="item in commonInfoEntries" :key="item[0]">
<NDescriptionsItem :label="item[0]">{{ item[1] }}</NDescriptionsItem>
</template>
</NDescriptions>
<template v-if="!!commonInfo && commonInfo.length > 0">
<NFlex vertical :size="16">
<template v-for="({ title, items }, index) in commonInfo" :key="`${title}-${index}`">
<NFlex vertical>
<NText strong :depth="3">{{ title }}</NText>
<NDescriptions bordered :label-placement="'left'" :columns="2">
<template v-for="({ label, value }, index) in items" :key="`${label}-${index}`">
<NDescriptionsItem :label="label">{{ value }}</NDescriptionsItem>
</template>
</NDescriptions>
</NFlex>
</template>
</NFlex>
</template>
</NCard>
</template>
@@ -1,3 +1,18 @@
<script lang="ts">
const parsePercentMetric = (raw?: string) => {
const trimmed = raw?.trim();
if (!!trimmed) {
const value = Number(trimmed);
if (Number.isFinite(value)) {
return value;
}
}
return undefined;
};
</script>
<script setup lang="ts">
import { ClockCheckIcon, CpuIcon, HardDriveIcon, MemoryStickIcon } from 'lucide-vue-next';
import { NCard, NFlex, NIcon, NProgress, type ProgressStatus } from 'naive-ui';
@@ -21,24 +36,27 @@ const showCard = computed(() => {
});
const cpuPercent = computed(() => {
if (!cpuUsage?.value) return 0;
return parseFloat(cpuUsage.value.replace('%', ''));
return parsePercentMetric(cpuUsage.value);
});
const memPercent = computed(() => {
if (!memUsage?.value) return 0;
return parseFloat(memUsage.value.replace('%', ''));
return parsePercentMetric(memUsage.value);
});
const diskPercent = computed(() => {
if (!diskUsage?.value) return 0;
return parseFloat(diskUsage.value.replace('%', ''));
return parsePercentMetric(diskUsage.value);
});
const formattedRunningTime = computed(() => {
return (runningTime?.value ?? '-').replace('days', '天');
});
const getProgressPercentage = (percent: number) => {
if (percent < 0) return 0;
if (percent > 100) return 100;
return percent;
};
const getProgressStatus = (percent: number): ProgressStatus => {
if (percent >= 90) return 'error';
if (percent >= 70) return 'warning';
@@ -53,20 +71,20 @@ const getProgressStatus = (percent: number): ProgressStatus => {
</template>
<template #default>
<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" />
<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 v-if="memUsage" style="width: 100%" align="center" :wrap="false">
<NFlex v-if="memPercent" style="width: 100%" align="center" :wrap="false">
<NIcon :component="MemoryStickIcon" />
<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 v-if="diskUsage" style="width: 100%" align="center" :wrap="false">
<NFlex v-if="diskPercent" style="width: 100%" align="center" :wrap="false">
<NIcon :component="HardDriveIcon" />
<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 v-if="runningTime" style="width: 100%" align="center" :wrap="false">
<NIcon :component="ClockCheckIcon" />
@@ -1,6 +1,8 @@
import type { ComponentInstance } from 'vue';
import DeviceCommonCard from './device-common-card.vue';
import DeviceHardwareCard from './device-hardware-card.vue';
import DeviceHeaderCard from './device-header-card.vue';
import NvrEnvCard from './nvr-env-card.vue';
import NvrDiskCard from './nvr-disk-card.vue';
import NvrRecordCheckCard from './nvr-record-check-card.vue';
import SecurityBoxCircuitCard from './security-box-circuit-card.vue';
@@ -9,10 +11,13 @@ import SecurityBoxEnvCard from './security-box-env-card.vue';
import SwitchPortCard from './switch-port-card.vue';
import SwitchPortLinkModal from './switch-port-link-modal.vue';
export type DeviceCommonCardProps = ComponentInstance<typeof DeviceCommonCard>['$props'];
export {
DeviceCommonCard,
DeviceHardwareCard,
DeviceHeaderCard,
NvrEnvCard,
NvrDiskCard,
NvrRecordCheckCard,
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>
@@ -92,6 +92,8 @@ const abortController = ref<AbortController>(new AbortController());
const NVR_RECORD_CHECK_KEY = 'nvr-record-check-query';
const deviceUniqueKey = computed(() => [station.value.code, ndmDevice.value.id]);
const DAY_OFFSET = 90;
const {
@@ -99,7 +101,7 @@ const {
isFetching: loading,
refetch: refetchRecordChecks,
} = useQuery({
queryKey: computed(() => [NVR_RECORD_CHECK_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
queryKey: computed(() => [NVR_RECORD_CHECK_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value),
refetchInterval: 30 * 1000,
gcTime: 0,
@@ -408,13 +410,13 @@ const ndmRecordChecksPaged = computed(() => {
return ndmRecordChecksFiltered.value.slice(startIndex, endIndex);
});
// 当设备ID、最后诊断时间或筛选类型变化时,重置分页为第一页
watch([() => ndmDevice.value.id, () => ndmDevice.value.lastDiagTime, filterType, searchInputDebounced], () => {
// 当车站号、设备IP、最后诊断时间或筛选类型变化时,重置分页为第一页
watch([() => station.value.code, () => ndmDevice.value.ipAddress, () => ndmDevice.value.lastDiagTime, filterType, searchInputDebounced], () => {
page.value = 1;
});
// 当设备ID变化时,重置搜索内容,并将筛选类型重置为「全部」
watch([() => ndmDevice.value.id], () => {
// 当车站号、设备IP变化时,重置搜索内容,并将筛选类型重置为「全部」
watch([() => station.value.code, () => ndmDevice.value.ipAddress], () => {
searchInput.value = '';
filterType.value = 'all';
});
@@ -2,8 +2,6 @@
import {
detailDeviceApi,
probeDeviceApi,
rebootSecurityBoxApi,
turnCitcuitStatusApi,
updateDeviceApi,
type LinkDescription,
type NdmDeviceResultVO,
@@ -16,6 +14,7 @@ import { SecurityBoxCircuitLinkModal } from '@/components';
import { usePermission } from '@/composables';
import { SELECT_DEVICE_FN_INJECTION_KEY } from '@/constants';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { normalizeSecurityBoxWriteCommunity, dispatchRebootSecurityBoxApi, dispatchTurnCircuitStatusApi, normalizeSecurityBoxCircuitIndex } from '@/helpers';
import { useDeviceStore, useSettingStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
@@ -31,6 +30,7 @@ import { computed, inject, ref, toRefs } from 'vue';
const props = defineProps<{
ndmDevice: NdmSecurityBoxResultVO;
station: Station;
vendor?: string;
circuits?: NdmSecurityBoxCircuit[];
}>();
@@ -44,7 +44,7 @@ const { useLocalDB } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station, circuits } = toRefs(props);
const { ndmDevice, station, vendor, circuits } = toRefs(props);
const showCard = computed(() => !!circuits.value && circuits.value.length > 0);
@@ -52,6 +52,10 @@ const localCircuits = ref<NdmSecurityBoxCircuit[]>([]);
watchImmediate(circuits, (newCircuits) => {
localCircuits.value = newCircuits?.map((circuit) => ({ ...circuit })) ?? [];
const circuitCount = ndmDevice.value.circuitCount;
if (!!circuitCount) {
localCircuits.value = localCircuits.value.slice(0, circuitCount);
}
});
const getCircuitStatusTagType = (circuit: NdmSecurityBoxCircuit): TagProps['type'] => {
@@ -79,13 +83,19 @@ const { mutate: turnStatus, isPending: turning } = useMutation({
window.$loadingBar.start();
const { circuitIndex, newStatus } = params;
if (!ndmDevice.value.ipAddress) {
const community = normalizeSecurityBoxWriteCommunity(ndmDevice.value, vendor.value);
const ipAddress = ndmDevice.value.ipAddress;
if (!ipAddress) {
throw new Error('设备IP地址不存在');
}
const status = newStatus ? 1 : 0;
const stationCode = station.value.code;
const signal = abortController.value.signal;
await turnCitcuitStatusApi(ndmDevice.value.ipAddress, circuitIndex, status, { stationCode, signal });
const turnCircuitStatusApi = dispatchTurnCircuitStatusApi(vendor.value);
const normalizedCircuitIndex = normalizeSecurityBoxCircuitIndex(circuitIndex, vendor.value);
await turnCircuitStatusApi(community, ipAddress, normalizedCircuitIndex, status, { stationCode, signal });
await probeDeviceApi(ndmDevice.value, { stationCode, signal });
return status;
},
@@ -110,13 +120,17 @@ const { mutate: reboot, isPending: rebooting } = useMutation({
window.$loadingBar.start();
if (!ndmDevice.value.ipAddress) {
const community = normalizeSecurityBoxWriteCommunity(ndmDevice.value, vendor.value);
const ipAddress = ndmDevice.value.ipAddress;
if (!ipAddress) {
throw new Error('设备IP地址不存在');
}
const stationCode = station.value.code;
const signal = abortController.value.signal;
await rebootSecurityBoxApi(ndmDevice.value.ipAddress, { stationCode, signal });
const rebootSecurityBoxApi = dispatchRebootSecurityBoxApi(vendor.value);
await rebootSecurityBoxApi(community, ipAddress, { stationCode, signal });
},
onSuccess: () => {
window.$loadingBar.finish();
@@ -202,7 +216,7 @@ const contextmenuOptions = computed<DropdownOption[]>(() => [
if (!lowerDevice) return;
window.$dialog.warning({
title: '确认解除关联吗?',
content: `将解除【电路${circuitIndex + 1}】与【${lowerDevice.name}】的关联关系。`,
content: `将解除【空开${circuitIndex + 1}】与【${lowerDevice.name}】的关联关系。`,
style: { width: '600px' },
contentStyle: { height: '60px' },
negativeText: '取消',
@@ -299,7 +313,7 @@ const { mutate: unlinkDevice } = useMutation({
<NCard v-if="showCard" hoverable size="small">
<template #header>
<NFlex align="center">
<span>电路状态</span>
<span>空开状态</span>
<NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => reboot()">
<template #trigger>
<NButton secondary size="small" :loading="rebooting">重合闸</NButton>
@@ -324,7 +338,7 @@ const { mutate: unlinkDevice } = useMutation({
<span>{{ getCircuitStatusText(circuit) }}</span>
</template>
</NTag>
<span>电路{{ circuitIndex + 1 }}</span>
<span>空开{{ circuitIndex + 1 }}</span>
</NFlex>
<NFlex justify="end" align="center">
<NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => turnStatus({ circuitIndex: circuitIndex, newStatus: circuit.status !== 1 })">
@@ -332,7 +346,7 @@ const { mutate: unlinkDevice } = useMutation({
<NSwitch size="small" :value="circuit.status === 1" :loading="turning" />
</template>
<template #default>
<span>确定要{{ circuit.status === 1 ? '关闭' : '开启' }}电路{{ circuitIndex + 1 }}吗?</span>
<span>确定要{{ circuit.status === 1 ? '关闭' : '开启' }}空开{{ circuitIndex + 1 }}吗?</span>
</template>
</NPopconfirm>
</NFlex>
@@ -60,7 +60,7 @@ const { mutate: linkPortToDevice, isPending: linking } = useMutation({
const upperDeviceDbId = ndmDevice.value.id;
if (!upperDeviceDbId) throw new Error('本设备没有ID');
if (circuitIndex.value === undefined) throw new Error('该电路不存在');
if (circuitIndex.value === undefined) throw new Error('该空开不存在');
if (!lowerDevice.value) throw new Error('请选择要关联的设备');
const lowerDeviceType = tryGetDeviceType(lowerDevice.value?.deviceType);
@@ -195,7 +195,7 @@ const onCancel = () => {
<template #header>
<span>{{ ndmDevice.name }}</span>
<span> - </span>
<span>电路{{ circuitIndex ? circuitIndex + 1 : '-' }}</span>
<span>空开{{ circuitIndex ? circuitIndex + 1 : '-' }}</span>
<span> - </span>
<span>关联设备</span>
</template>
@@ -16,40 +16,35 @@ const showCard = computed(() => {
return Object.values(props).some((value) => !!value);
});
// 门禁状态
const accessControlStatus = computed(() => {
if (!switches?.value || switches.value.length < 1) return null;
const status = switches.value.at(0)!;
return status === 0 ? '开门' : status === 1 ? '关门' : '-';
});
// 防雷状态
const lightningProtectionStatus = computed(() => {
if (!switches?.value || switches.value.length < 2) return null;
const status = switches.value.at(0)!;
return status === 0 ? '失效' : status === 1 ? '生效' : '-';
});
// 门禁状态
const accessControlStatus = computed(() => {
if (!switches?.value || switches.value.length < 1) return null;
const status = switches.value.at(1)!;
return status === 0 ? '正常' : status === 1 ? '失效' : '-';
return status === 0 ? '开门' : status === 1 ? '关门' : '-';
});
const getStatusTagType = (status: string | null) => {
if (['正常'].includes(status ?? '')) return 'success';
if (['失效'].includes(status ?? '')) return 'error';
if (['生效', '关门'].includes(status ?? '')) return 'success';
if (['失效', '开门'].includes(status ?? '')) return 'error';
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>
<template>
<NCard v-if="showCard" hoverable size="small">
<template #header>
<span>安防箱状态</span>
<span>安防箱环境状态</span>
</template>
<template #default>
<NFlex vertical>
<NTag>
<NTag v-if="!!temperature">
<template #icon>
<NIcon :component="ThermometerIcon" />
</template>
@@ -57,7 +52,7 @@ const formattedFanSpeeds = computed(() => {
<span>温度: {{ temperature }}</span>
</template>
</NTag>
<NTag>
<NTag v-if="!!humidity">
<template #icon>
<NIcon :component="DropletIcon" />
</template>
@@ -65,15 +60,15 @@ const formattedFanSpeeds = computed(() => {
<span>湿度: {{ humidity }}%</span>
</template>
</NTag>
<NTag>
<NTag v-for="(speed, index) in fanSpeeds ?? []" :key="index">
<template #icon>
<NIcon :component="FanIcon" />
</template>
<template #default>
<span>风扇: {{ formattedFanSpeeds }}</span>
<span>风扇{{ index + 1 }}: {{ speed }} RPM</span>
</template>
</NTag>
<NTag :type="getStatusTagType(accessControlStatus)">
<NTag v-if="!!accessControlStatus" :type="getStatusTagType(accessControlStatus)">
<template #icon>
<NIcon :component="ShieldIcon" />
</template>
@@ -81,7 +76,7 @@ const formattedFanSpeeds = computed(() => {
<span>门禁: {{ accessControlStatus }}</span>
</template>
</NTag>
<NTag :type="getStatusTagType(lightningProtectionStatus)">
<NTag v-if="!!lightningProtectionStatus" :type="getStatusTagType(lightningProtectionStatus)">
<template #icon>
<NIcon :component="ZapIcon" />
</template>
@@ -33,7 +33,7 @@ const { hasPermission } = usePermission();
const { ndmDevice, station, ports } = toRefs(props);
const showCard = computed(() => !!ports.value);
const showCard = computed(() => !!ports.value && ports.value.length > 0);
const switchSlots = computed(() => {
// 解析端口名称,将端口按槽位进行分组
@@ -31,7 +31,7 @@ const { ndmDevice, station } = toRefs(props);
const showDetailModal = ref(false);
const detailTableColumns: DataTableColumns<SecurityBoxCircuitRowData> = [
{ title: '电路序号', key: 'number' },
{ title: '空开序号', key: 'number' },
{
title: '状态',
key: 'status',
@@ -98,7 +98,7 @@ const tableColumns: DataTableColumns<SecurityBoxRuntimeRowData> = [
},
// { title: '开关状态', key: 'switches' },
{
title: '电路状态',
title: '空开状态',
key: 'circuits',
render(rowData) {
const { info } = rowData.diagInfo;
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { NFlex } from 'naive-ui';
import { DeviceCommonCard, DeviceHeaderCard } from '@/components';
import { DeviceCommonCard, DeviceHeaderCard, type DeviceCommonCardProps } from '@/components';
import type { NdmAlarmHostResultVO, Station } from '@/apis';
import { computed, toRefs } from 'vue';
@@ -11,13 +11,18 @@ const props = defineProps<{
const { ndmDevice, station } = toRefs(props);
const commonInfo = computed(() => {
const commonInfo = computed<DeviceCommonCardProps['commonInfo']>(() => {
const { createdTime, updatedTime, manufacturer } = ndmDevice.value;
return {
创建时间: createdTime ?? '-',
更新时间: updatedTime ?? '-',
制造商: manufacturer ?? '-',
};
return [
{
title: '设备接入信息',
items: [
{ label: '创建时间', value: createdTime || '-' },
{ label: '更新时间', value: updatedTime || '-' },
{ label: '制造商', value: manufacturer || '-' },
],
},
];
});
</script>
@@ -16,11 +16,12 @@ const isCameraTypeCode = (code: string): code is CameraType => {
</script>
<script setup lang="ts">
import type { NdmCameraResultVO, Station } from '@/apis';
import { DeviceCommonCard, DeviceHeaderCard } from '@/components';
import type { NdmCameraDiagInfo, NdmCameraResultVO, Station } from '@/apis';
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, type DeviceCommonCardProps } from '@/components';
import { useSettingStore } from '@/stores';
import { useQuery, useQueryClient } from '@tanstack/vue-query';
import axios from 'axios';
import destr from 'destr';
import { NDescriptions, NDescriptionsItem, NFlex } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, toRefs, watch } from 'vue';
@@ -113,7 +114,17 @@ watch(activeRequests, (active) => {
}
});
const commonInfo = computed(() => {
const lastDiagInfo = computed(() => {
const result = destr<any>(ndmDevice.value.lastDiagInfo);
if (!result) return null;
if (typeof result !== 'object') return null;
return result as NdmCameraDiagInfo;
});
const commonInfo = computed<DeviceCommonCardProps['commonInfo']>(() => {
const { stCommonInfo } = lastDiagInfo.value ?? {};
const { 设备ID, 软件版本, 设备厂商, 设备别名, 设备型号, 硬件版本 } = stCommonInfo ?? {};
const { ethInfo, ipInfo } = lastDiagInfo.value ?? {};
const {
createdTime,
updatedTime,
@@ -126,22 +137,58 @@ const commonInfo = computed(() => {
onvifMinorIndex,
icmpEnabled,
community,
ipAddress,
//
} = ndmDevice.value;
return {
创建时间: createdTime ?? '-',
更新时间: updatedTime ?? '-',
制造商: manufacturer ?? '-',
GB28181启用: `${!!gb28181Enabled ? '是' : '否'}`,
ONVIF端口: `${onvifPort ?? '-'}`,
ONVIF用户名: onvifUsername ?? '-',
ONVIF密码: onvifPassword ?? '-',
ONVIF主流索引: `${onvifMajorIndex ?? '-'}`,
ONVIF辅流索引: `${onvifMinorIndex ?? '-'}`,
ICMP启用: `${!!icmpEnabled ? '是' : ''}`,
团体字符串: community ?? '-',
};
let operStatus = '-';
if (!!ethInfo?.operStatus) {
operStatus = ethInfo?.operStatus === '1' ? '正常' : '异常';
}
return [
{
title: '设备型号信息',
items: [
{ label: '设备ID', value: 设备ID || '-' },
{ label: '软件版本', value: 软件版本 || '-' },
{ label: '设备厂商', value: 设备厂商 || manufacturer || '-' },
{ label: '设备别名', value: 设备别名 || '-' },
{ 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 },
],
},
{
title: '设备接入信息',
items: [
{ label: '创建时间', value: createdTime || '-' },
{ label: '更新时间', value: updatedTime || '-' },
{ label: 'GB28181启用', value: `${!!gb28181Enabled ? '是' : '否'}` },
{ label: 'ICMP启用', value: `${!!icmpEnabled ? '是' : '否'}` },
{ label: 'ONVIF用户名', value: onvifUsername || '-' },
{ label: 'ONVIF密码', value: onvifPassword || '-' },
{ label: 'ONVIF主流索引', value: `${onvifMajorIndex ?? '-'}` },
{ label: 'ONVIF辅流索引', value: `${onvifMinorIndex ?? '-'}` },
{ label: 'ONVIF端口', value: `${onvifPort ?? '-'}` },
{ label: '团体字符串', value: community || '-' },
],
},
];
});
const cpuUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.CPU使用率);
const memUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.内存使用率);
</script>
<template>
@@ -155,6 +202,7 @@ const commonInfo = computed(() => {
</template>
</DeviceHeaderCard>
<DeviceCommonCard :common-info="commonInfo" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
</NFlex>
</template>
@@ -1,6 +1,14 @@
<script setup lang="ts">
import type { NdmCameraResultVO, Station } from '@/apis';
import { DeviceAlarmHistoryCard, DeviceIcmpHistoryCard, HistoryDiagFilterCard, type DeviceAlarmHistoryCardProps, type DeviceIcmpHistoryCardProps } from '@/components';
import {
DeviceAlarmHistoryCard,
DeviceIcmpHistoryCard,
DeviceUsageHistoryCard,
HistoryDiagFilterCard,
type DeviceAlarmHistoryCardProps,
type DeviceIcmpHistoryCardProps,
type DeviceUsageHistoryCardProps,
} from '@/components';
import dayjs from 'dayjs';
import { NFlex, type SelectOption } from 'naive-ui';
import { onMounted, ref, toRefs, watch } from 'vue';
@@ -15,6 +23,7 @@ const { ndmDevice, station } = toRefs(props);
const historyDiagOptions: SelectOption[] = [
{ label: '设备状态', value: 'icmp' },
{ label: '设备告警', value: 'alarm' },
{ label: '硬件占用', value: 'usage' },
];
const getWeekRange = (): [number, number] => {
const now = dayjs();
@@ -27,7 +36,8 @@ const selected = ref<string[]>([...historyDiagOptions.map((option) => `${option.
const loading = ref<boolean>(false);
const icmpLoading = ref<boolean>(false);
const alarmLoading = ref<boolean>(false);
watch([icmpLoading, alarmLoading], (loadings) => {
const usageLoading = ref<boolean>(false);
watch([icmpLoading, alarmLoading, usageLoading], (loadings) => {
loading.value = loadings.some((loading) => loading);
});
const icmpHistoryQueryFn = ref<() => void>();
@@ -38,9 +48,14 @@ const alarmHistoryQueryFn = ref<() => void>();
const onExposeAlarmHistoryQueryFn: DeviceAlarmHistoryCardProps['onExposeQueryFn'] = (queryFn) => {
alarmHistoryQueryFn.value = queryFn;
};
const usageHistoryQueryFn = ref<() => void>();
const onExposeUsageHistoryQueryFn: DeviceUsageHistoryCardProps['onExposeQueryFn'] = (queryFn) => {
usageHistoryQueryFn.value = queryFn;
};
const queryData = () => {
if (selected.value.includes('icmp')) icmpHistoryQueryFn.value?.();
if (selected.value.includes('alarm')) alarmHistoryQueryFn.value?.();
if (selected.value.includes('usage')) usageHistoryQueryFn.value?.();
};
const onQuery = () => {
queryData();
@@ -70,6 +85,16 @@ onMounted(() => {
v-model:loading="alarmLoading"
@expose-query-fn="onExposeAlarmHistoryQueryFn"
/>
<DeviceUsageHistoryCard
v-if="selected.includes('usage')"
:ndm-device="ndmDevice"
:station="station"
:cpu-usage-field="'stCommonInfo.CPU使用率'"
:mem-usage-field="'stCommonInfo.内存使用率'"
v-model:range="range"
v-model:loading="usageLoading"
@expose-query-fn="onExposeUsageHistoryQueryFn"
/>
</NFlex>
</template>
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { NdmDecoderDiagInfo, NdmDecoderResultVO, Station } from '@/apis';
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard } from '@/components';
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, type DeviceCommonCardProps } from '@/components';
import destr from 'destr';
import { NFlex } from 'naive-ui';
import { computed, toRefs } from 'vue';
@@ -19,17 +19,23 @@ const lastDiagInfo = computed(() => {
return result as NdmDecoderDiagInfo;
});
const commonInfo = computed(() => {
const commonInfo = computed<DeviceCommonCardProps['commonInfo']>(() => {
const { stCommonInfo } = lastDiagInfo.value ?? {};
const { 设备ID, 软件版本, 设备厂商, 设备别名, 设备型号, 硬件版本 } = stCommonInfo ?? {};
return {
设备ID: 设备ID ?? '-',
软件版本: 软件版本 ?? '-',
设备厂商: 设备厂商 ?? '-',
设备别名: 设备别名 ?? '-',
设备型号: 设备型号 ?? '-',
硬件版本: 件版本 ?? '-',
};
return [
{
title: '设备型号信息',
items: [
{ label: '设备ID', value: 设备ID || '-' },
{ label: '软件版本', value: 件版本 || '-' },
{ label: '设备厂商', value: 设备厂商 || '-' },
{ label: '设备别名', value: 设备别名 || '-' },
{ label: '设备型号', value: 设备型号 || '-' },
{ label: '硬件版本', value: 硬件版本 || '-' },
],
},
];
});
const cpuUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.CPU使用率);
@@ -36,7 +36,8 @@ const selected = ref([...historyDiagOptions.map((option) => `${option.value}`)])
const loading = ref<boolean>(false);
const icmpLoading = ref<boolean>(false);
const alarmLoading = ref<boolean>(false);
watch([icmpLoading, alarmLoading], (loadings) => {
const usageLoading = ref<boolean>(false);
watch([icmpLoading, alarmLoading, usageLoading], (loadings) => {
loading.value = loadings.some((loading) => loading);
});
const icmpHistoryQueryFn = ref<() => void>();
@@ -91,7 +92,7 @@ onMounted(() => {
:cpu-usage-field="'stCommonInfo.CPU使用率'"
:mem-usage-field="'stCommonInfo.内存使用率'"
v-model:range="range"
v-model:loading="alarmLoading"
v-model:loading="usageLoading"
@expose-query-fn="onExposeUsageHistoryQueryFn"
/>
</NFlex>
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { NdmNvrDiagInfo, NdmNvrResultVO, Station } from '@/apis';
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, NvrDiskCard, NvrRecordCheckCard } from '@/components';
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, NvrDiskCard, NvrEnvCard, NvrRecordCheckCard, type DeviceCommonCardProps } from '@/components';
import { isNvrCluster } from '@/helpers';
import destr from 'destr';
import { NFlex } from 'naive-ui';
@@ -20,18 +20,40 @@ const lastDiagInfo = computed(() => {
return result as NdmNvrDiagInfo;
});
const commonInfo = computed(() => {
const { stCommonInfo } = lastDiagInfo.value ?? {};
if (!stCommonInfo) return undefined;
const { 设备ID, 软件版本, 生产厂商, 设备别名, 设备型号, 硬件版本 } = stCommonInfo;
return {
设备ID: 设备ID ?? '-',
软件版本: 软件版本 ?? '-',
生产厂商: 生产厂商 ?? '-',
设备别名: 设备别名 ?? '-',
设备型号: 设备型号 ?? '-',
硬件版本: 硬件版本 ?? '-',
};
const commonInfo = computed<DeviceCommonCardProps['commonInfo']>(() => {
const { ipAddress } = ndmDevice.value;
const { ethInfo, ipInfo, stCommonInfo } = lastDiagInfo.value ?? {};
const { 设备ID, 软件版本, 生产厂商, 设备别名, 设备型号, 硬件版本 } = stCommonInfo ?? {};
let operStatus = '-';
if (!!ethInfo?.operStatus) {
operStatus = ethInfo?.operStatus === '1' ? '正常' : '异常';
}
return [
{
title: '设备型号信息',
items: [
{ label: '设备ID', value: 设备ID || '-' },
{ label: '软件版本', value: 软件版本 || '-' },
{ label: '生产厂商', value: 生产厂商 || '-' },
{ label: '设备别名', value: 设备别名 || '-' },
{ 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 },
],
},
];
});
const cpuUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.CPU使用率);
@@ -39,6 +61,9 @@ const memUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.内存使用
const diskHealth = computed(() => lastDiagInfo.value?.info?.diskHealth);
const diskArray = computed(() => lastDiagInfo.value?.info?.groupInfoList);
const fanInfo = computed(() => lastDiagInfo.value?.cdFanInfo);
const powerSupplyInfo = computed(() => lastDiagInfo.value?.cdPowerSupplyInfo);
</script>
<template>
@@ -46,6 +71,7 @@ const diskArray = computed(() => lastDiagInfo.value?.info?.groupInfoList);
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
<DeviceCommonCard :common-info="commonInfo" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
<NvrEnvCard :fan-info="fanInfo" :power-supply-info="powerSupplyInfo" />
<NvrDiskCard :disk-health="diskHealth" :disk-array="diskArray" />
<template v-if="isNvrCluster(ndmDevice)">
<NvrRecordCheckCard :ndm-device="ndmDevice" :station="station" />
@@ -42,8 +42,9 @@ const selected = ref([...historyDiagOptions.value.map((option) => `${option.valu
const loading = ref<boolean>(false);
const icmpLoading = ref<boolean>(false);
const alarmLoading = ref<boolean>(false);
const usageLoading = ref<boolean>(false);
const diskLoading = ref<boolean>(false);
watch([icmpLoading, alarmLoading, diskLoading], (loadings) => {
watch([icmpLoading, alarmLoading, usageLoading, diskLoading], (loadings) => {
loading.value = loadings.some((loading) => loading);
});
const icmpHistoryQueryFn = ref<() => void>();
@@ -103,7 +104,7 @@ onMounted(() => {
:cpu-usage-field="'stCommonInfo.CPU使用率'"
:mem-usage-field="'stCommonInfo.内存使用率'"
v-model:range="range"
v-model:loading="alarmLoading"
v-model:loading="usageLoading"
@expose-query-fn="onExposeUsageHistoryQueryFn"
/>
<NvrDiskHistoryCard v-if="selected.includes('disk')" :ndm-device="ndmDevice" :station="station" v-model:range="range" v-model:loading="diskLoading" @expose-query-fn="onExposeDiskHistoryQueryFn" />
@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { NdmSecurityBoxDiagInfo, NdmSecurityBoxResultVO, Station } from '@/apis';
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, SecurityBoxCircuitCard, SecurityBoxEnvCard } from '@/components';
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, SecurityBoxCircuitCard, SecurityBoxEnvCard, type DeviceCommonCardProps } from '@/components';
import { SECURITY_BOX_VENDOR_LITERALS } from '@/helpers';
import destr from 'destr';
import { NFlex } from 'naive-ui';
import { computed, toRefs } from 'vue';
@@ -19,17 +20,54 @@ const lastDiagInfo = computed(() => {
return result as NdmSecurityBoxDiagInfo;
});
const commonInfo = computed(() => {
const { stCommonInfo } = lastDiagInfo.value ?? {};
// 解析安防箱厂商
const vendor = computed(() => {
// 先读取诊断信息中的 stCommonInfo 中的设备厂商
if (!!lastDiagInfo.value?.stCommonInfo?.设备厂商) {
return lastDiagInfo.value.stCommonInfo.设备厂商.trim().toLocaleLowerCase();
}
// 如果 stCommonInfo 中没有设备厂商,再读取 ndmDevice 中的 manufacturer
if (!!ndmDevice.value.manufacturer) {
return ndmDevice.value.manufacturer.trim().toLocaleLowerCase();
}
// 如果 ndmDevice 中也没有 manufacturer,返回 beidian 作为兜底
return SECURITY_BOX_VENDOR_LITERALS.beidian;
});
const commonInfo = computed<DeviceCommonCardProps['commonInfo']>(() => {
const { ipAddress } = ndmDevice.value;
const { ethInfo, ipInfo, stCommonInfo } = lastDiagInfo.value ?? {};
const { 设备ID, 软件版本, 设备厂商, 设备别名, 设备型号, 硬件版本 } = stCommonInfo ?? {};
return {
设备ID: 设备ID ?? '',
软件版本: 软件版本 ?? '',
设备厂商: 设备厂商 ?? '',
设备别名: 设备别名 ?? '',
设备型号: 设备型号 ?? '',
硬件版本: 硬件版本 ?? '',
};
let operStatus = '-';
if (!!ethInfo?.operStatus) {
operStatus = ethInfo?.operStatus === '1' ? '正常' : '异常';
}
return [
{
title: '设备型号信息',
items: [
{ label: '设备ID', value: 设备ID || '-' },
{ label: '软件版本', value: 软件版本 || '-' },
{ label: '设备厂商', value: 设备厂商 || '-' },
{ label: '设备别名', value: 设备别名 || '-' },
{ 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 },
],
},
];
});
const cpuUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.CPU使用率);
@@ -49,7 +87,7 @@ const circuits = computed(() => lastDiagInfo.value?.info?.at(0)?.circuits);
<DeviceCommonCard :common-info="commonInfo" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
<SecurityBoxEnvCard :fan-speeds="fanSpeeds" :temperature="temperature" :humidity="humidity" :switches="switches" />
<SecurityBoxCircuitCard :ndm-device="ndmDevice" :station="station" :circuits="circuits" />
<SecurityBoxCircuitCard :ndm-device="ndmDevice" :station="station" :vendor="vendor" :circuits="circuits" />
</NFlex>
</template>
@@ -6,7 +6,7 @@ import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import destr from 'destr';
import { isString } from 'es-toolkit';
import { NButton, NCard, NFlex, NForm, NFormItem, NFormItemGi, NGrid, NInput, NSwitch, type FormInst, type FormRules } from 'naive-ui';
import { NButton, NCard, NFlex, NForm, NFormItem, NFormItemGi, NGrid, NInput, NInputNumber, NSwitch, type FormInst, type FormRules } from 'naive-ui';
import { computed, onBeforeUnmount, ref, toRefs, useTemplateRef, watch } from 'vue';
const props = defineProps<{
@@ -126,9 +126,15 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="型号">
<NInput v-model:value="localDevice.model" />
</NFormItem>
<NFormItem label-placement="left" label="开关数量">
<NInputNumber clearable v-model:value="localDevice.circuitCount" />
</NFormItem>
<NFormItem label-placement="left" label="团体字符串">
<NInput v-model:value="localDevice.community" />
</NFormItem>
<NFormItem label-placement="left" label="团体字符串(写)">
<NInput v-model:value="localDevice.writeCommunity" />
</NFormItem>
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
@@ -1,8 +1,9 @@
import ServerAlive from './server-alive.vue';
import ServerCard from './server-card.vue';
import ServerCurrentDiag from './server-current-diag.vue';
import ServerHighAvailable from './server-high-available.vue';
import ServerHistoryDiag from './server-history-diag.vue';
import ServerStreamPush from './server-stream-push.vue';
import ServerUpdate from './server-update.vue';
export { ServerAlive, ServerCard, ServerCurrentDiag, ServerHistoryDiag, ServerUpdate, ServerStreamPush };
export { ServerAlive, ServerCard, ServerCurrentDiag, ServerHighAvailable, ServerHistoryDiag, ServerUpdate, ServerStreamPush };
@@ -23,8 +23,11 @@ const deviceType = computed(() => tryGetDeviceType(ndmDevice.value.deviceType));
const MEDIA_SERVER_ALIVE_QUERY_KEY = 'media-server-alive-query';
const VIDEO_SERVER_ALIVE_QUERY_KEY = 'video-server-alive-query';
const deviceUniqueKey = computed(() => [station.value.code, ndmDevice.value.id]);
const { data: isMediaServerAlive } = useQuery({
queryKey: computed(() => [MEDIA_SERVER_ALIVE_QUERY_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
queryKey: computed(() => [MEDIA_SERVER_ALIVE_QUERY_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmMediaServer),
refetchInterval: 30 * 1000,
gcTime: 0,
@@ -34,7 +37,7 @@ const { data: isMediaServerAlive } = useQuery({
},
});
const { data: isSipServerAlive } = useQuery({
queryKey: computed(() => [VIDEO_SERVER_ALIVE_QUERY_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
queryKey: computed(() => [VIDEO_SERVER_ALIVE_QUERY_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmVideoServer),
refetchInterval: 30 * 1000,
gcTime: 0,
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { type NdmServerDiagInfo, type NdmServerResultVO, type Station } from '@/apis';
import { DeviceHardwareCard, DeviceHeaderCard, ServerAlive, ServerStreamPush } from '@/components';
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, ServerAlive, ServerHighAvailable, ServerStreamPush, type DeviceCommonCardProps } from '@/components';
import destr from 'destr';
import { NFlex } from 'naive-ui';
import { computed, toRefs } from 'vue';
@@ -19,6 +19,30 @@ const lastDiagInfo = computed(() => {
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 memUsage = computed(() => lastDiagInfo.value?.commInfo?.内存使用率);
const diskUsage = computed(() => lastDiagInfo.value?.commInfo?.磁盘使用率);
@@ -27,7 +51,9 @@ const runningTime = computed(() => lastDiagInfo.value?.commInfo?.系统运行时
<template>
<NFlex vertical>
<ServerHighAvailable :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" />
<ServerAlive :ndm-device="ndmDevice" :station="station" />
<ServerStreamPush :ndm-device="ndmDevice" :station="station" />
@@ -0,0 +1,67 @@
<script setup lang="ts">
import { getHighAvailableApi, type NdmServerResultVO, type Station } from '@/apis';
import { DEVICE_TYPE_LITERALS, tryGetDeviceType } from '@/enums';
import { useSettingStore } from '@/stores';
import { useQuery, useQueryClient } from '@tanstack/vue-query';
import { NAlert, NFlex } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmServerResultVO;
station: Station;
}>();
const settingStore = useSettingStore();
const { activeRequests } = storeToRefs(settingStore);
const queryClient = useQueryClient();
const { ndmDevice, station } = toRefs(props);
const deviceType = computed(() => tryGetDeviceType(ndmDevice.value.deviceType));
const isVideoServer = computed(() => deviceType.value === DEVICE_TYPE_LITERALS.ndmVideoServer);
const SERVER_HIGH_AVAILABLE_QUERY_KEY = 'server-high-available-query';
const deviceUniqueKey = computed(() => [station.value.code, ndmDevice.value.id]);
const { data: highAvailable } = useQuery({
queryKey: computed(() => [SERVER_HIGH_AVAILABLE_QUERY_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && isVideoServer.value),
refetchInterval: 30 * 1000,
gcTime: 0,
queryFn: async ({ signal }) => {
const highAvailable = await getHighAvailableApi({ stationCode: station.value.code, signal });
return highAvailable;
},
});
watch(activeRequests, (active) => {
if (!active) {
queryClient.cancelQueries({ queryKey: [SERVER_HIGH_AVAILABLE_QUERY_KEY] });
}
});
const showCard = computed(() => {
const { pyip: physicalIp } = highAvailable.value ?? {};
const ipAddressMatched = physicalIp === ndmDevice.value.ipAddress;
return isVideoServer.value && ipAddressMatched;
});
</script>
<template>
<NAlert v-if="showCard && !!highAvailable" :bordered="false" type="success">
<template #header>
<NFlex :align="'center'">
<div>正在提供服务</div>
<NFlex :align="'center'" style="font-size: smaller">
<div>虚拟IP: {{ highAvailable.vip }}</div>
<div>启用时间: {{ highAvailable.changeDate }}</div>
</NFlex>
</NFlex>
</template>
</NAlert>
</template>
<style scoped></style>
@@ -25,8 +25,10 @@ const showCard = computed(() => deviceType.value === DEVICE_TYPE_LITERALS.ndmMed
const SERVER_STREAM_PUSH_KEY = 'server-stream-push-query';
const deviceUniqueKey = computed(() => [station.value.code, ndmDevice.value.id]);
const { data: streamPushes } = useQuery({
queryKey: computed(() => [SERVER_STREAM_PUSH_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
queryKey: computed(() => [SERVER_STREAM_PUSH_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && showCard.value),
refetchInterval: 30 * 1000,
gcTime: 0,
+111 -68
View File
@@ -1,8 +1,14 @@
<script lang="ts">
const createDeviceNodeKey = (stationCode?: Station['code'], device?: NdmDeviceResultVO) => {
return `${stationCode ?? ''}-${device?.id ?? ''}`;
};
</script>
<script setup lang="ts">
import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis';
import { useDeviceTree, usePermission, type UseDeviceTreeReturn } from '@/composables';
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 { watchDebounced, watchImmediate } from '@vueuse/core';
import destr from 'destr';
@@ -110,7 +116,7 @@ watchImmediate(selectedDeviceType, (newDeviceType) => {
}
});
const selectedKeys = computed(() => (selectedDevice.value?.id ? [selectedDevice.value.id] : undefined));
const selectedKeys = computed(() => (selectedDevice.value?.id ? [createDeviceNodeKey(selectedStationCode.value, selectedDevice.value)] : undefined));
watch([selectedKeys, selectedDevice, selectedStationCode], ([, device, code]) => {
if (device && code) {
onSelectDevice(device, code);
@@ -291,8 +297,18 @@ const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: Station[
return h(NFlex, { size: 'small' }, { default: () => [renderViewDeviceButton(device, stationCode), renderDeviceStatusTag(device)] });
};
// 全线设备树
const lineDeviceTreeData = computed<Record<Station['code'], TreeOption[]>>(() => {
const treeData: Record<string, TreeOption[]> = {};
const lineDeviceTreeData = computed<Record<DeviceType, 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 */ }) => {
treeData[paneName] = stations.value.map<TreeOption>((station) => {
const { name: stationName, code: stationCode } = station;
@@ -301,47 +317,55 @@ const lineDeviceTreeData = computed<Record<Station['code'], TreeOption[]>>(() =>
const offlineDevices = devices?.filter((device) => device.deviceStatus === '20');
// 对于录像机,需要根据clusterList字段以分号分隔设备IP,进一步形成子树结构
if (paneName === DEVICE_TYPE_LITERALS.ndmNvr) {
const nvrs = devices as NdmNvrResultVO[];
const nvrClusters: NdmNvrResultVO[] = [];
const nvrSingletons: NdmNvrResultVO[] = [];
for (const device of nvrs) {
if (isNvrCluster(device)) {
nvrClusters.push(device);
} else {
nvrSingletons.push(device);
}
}
const nvrDevices = devices as NdmNvrResultVO[];
const { nvrClusters, nvrTreeMap, nvrStandalones } = createNvrClusterRelationship(nvrDevices);
return {
label: stationName,
key: stationCode,
prefix: () => renderStationNodePrefix(station),
suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0),
children: nvrClusters.map<TreeOption>((nvrCluster) => {
return {
label: `${nvrCluster.name}`,
key: nvrCluster.id ?? `${nvrCluster.name}`,
prefix: () => renderDeviceNodePrefix(nvrCluster, stationCode),
suffix: () => `${nvrCluster.ipAddress}`,
children: nvrSingletons.map<TreeOption>((nvr) => {
return {
label: `${nvr.name}`,
key: nvr.id ?? `${nvr.name}`,
prefix: () => renderDeviceNodePrefix(nvr, stationCode),
suffix: () => `${nvr.ipAddress}`,
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
stationCode,
device: nvr,
};
}),
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
stationCode,
device: nvrCluster,
};
}),
children: [
...nvrClusters.map((cluster) => {
return {
label: `${cluster.name}`,
key: createDeviceNodeKey(stationCode, cluster),
prefix: () => renderDeviceNodePrefix(cluster, stationCode),
suffix: () => `${cluster.ipAddress}`,
children: (nvrTreeMap.get(cluster.ipAddress ?? '') ?? []).map((clusterNode) => {
return {
label: `${clusterNode.name}`,
key: createDeviceNodeKey(stationCode, clusterNode),
prefix: () => renderDeviceNodePrefix(clusterNode, stationCode),
suffix: () => `${clusterNode.ipAddress}`,
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
stationCode,
device: clusterNode,
};
}),
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
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,
deviceType: activeTab.value,
};
}
// 非录像机设备
return {
label: stationName,
key: stationCode,
@@ -352,7 +376,7 @@ const lineDeviceTreeData = computed<Record<Station['code'], TreeOption[]>>(() =>
const device = dev as NdmDeviceResultVO;
return {
label: `${device.name}`,
key: `${device.name}${device.ipAddress}`,
key: createDeviceNodeKey(stationCode, device),
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
@@ -376,37 +400,51 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
const onlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '10').length;
const offlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '20').length;
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
const nvrs = stationDevices[deviceType] as NdmNvrResultVO[];
const clusters = nvrs.filter((nvr) => isNvrCluster(nvr));
const singletons = nvrs.filter((nvr) => !isNvrCluster(nvr));
const nvrDevices = stationDevices[deviceType] as NdmNvrResultVO[];
const { nvrClusters, nvrTreeMap, nvrStandalones } = createNvrClusterRelationship(nvrDevices);
return {
label: `${DEVICE_TYPE_NAMES[deviceType]}`,
key: deviceType,
suffix: () => renderIcmpStatistics(onlineCount, offlineCount, nvrs.length),
children: clusters.map<TreeOption>((device) => {
return {
label: `${device.name}`,
key: `${device.name}${device.ipAddress}`,
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
children: singletons.map<TreeOption>((device) => {
return {
label: `${device.name}`,
key: `${device.name}${device.ipAddress}`,
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
stationCode,
device,
};
}),
stationCode,
device,
};
}),
suffix: () => renderIcmpStatistics(onlineCount, offlineCount, nvrDevices.length),
children: [
...nvrClusters.map((cluster) => {
return {
label: `${cluster.name}`,
key: createDeviceNodeKey(stationCode, cluster),
prefix: () => renderDeviceNodePrefix(cluster, stationCode),
suffix: () => `${cluster.ipAddress}`,
children: (nvrTreeMap.get(cluster.ipAddress ?? '') ?? []).map((clusterNode) => {
return {
label: `${clusterNode.name}`,
key: createDeviceNodeKey(stationCode, clusterNode),
prefix: () => renderDeviceNodePrefix(clusterNode, stationCode),
suffix: () => `${clusterNode.ipAddress}`,
stationCode,
device: clusterNode,
};
}),
stationCode,
device: cluster,
};
}),
...nvrStandalones.map((device) => {
return {
label: `${device.name}`,
key: createDeviceNodeKey(stationCode, device),
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
stationCode,
device,
};
}),
],
stationCode,
deviceType,
};
}
// 非录像机设备
return {
label: `${DEVICE_TYPE_NAMES[deviceType]}`,
key: deviceType,
@@ -414,7 +452,7 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
children: stationDevices[deviceType].map<TreeOption>((device) => {
return {
label: `${device.name}`,
key: `${device.name}${device.ipAddress}`,
key: createDeviceNodeKey(stationCode, device),
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
stationCode,
@@ -462,6 +500,7 @@ const onFoldDeviceTree = () => {
};
const onLocateDeviceTree = async () => {
if (!selectedStationCode.value) return;
const stationCode = selectedStationCode.value;
if (!selectedDevice.value) return;
const deviceType = tryGetDeviceType(selectedDevice.value.deviceType);
if (!deviceType) return;
@@ -473,24 +512,28 @@ const onLocateDeviceTree = async () => {
activeTab.value = deviceType;
// 展开选择的车站
expandedKeys.value.push(selectedStationCode.value);
expandedKeys.value.push(stationCode);
// 当选择录像机时,如果不是集群,进一步展开该录像机所在的集群节点
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
const stationDevices = lineDevices.value[selectedStationCode.value];
const stationDevices = lineDevices.value[stationCode];
if (stationDevices) {
const selectedNvr = selectedDevice.value as NdmNvrResultVO;
if (!isNvrCluster(selectedNvr)) {
const nvrs = stationDevices[DEVICE_TYPE_LITERALS.ndmNvr];
const clusters = nvrs.filter((nvr) => isNvrCluster(nvr) && nvr.clusterList?.includes(selectedNvr.clusterList ?? ''));
expandedKeys.value.push(...clusters.map((nvr) => `${nvr.id}`));
const nvrDevices = stationDevices[DEVICE_TYPE_LITERALS.ndmNvr];
const clusters = nvrDevices.filter((device) => {
if (!isNvrCluster(device)) return false;
const cluster = device;
return nvrInCluster(selectedNvr, cluster);
});
expandedKeys.value.push(...clusters.map((cluster) => createDeviceNodeKey(stationCode, cluster)));
}
}
}
// 等待设备树展开完成,滚动到选择的设备
await nextTick();
deviceTreeInst.value.scrollTo({ key: `${selectedDevice.value.id}`, behavior: 'smooth' });
deviceTreeInst.value.scrollTo({ key: createDeviceNodeKey(stationCode, selectedDevice.value), behavior: 'smooth' });
animated.value = true;
};
@@ -1,6 +1,7 @@
<script lang="ts">
// 设备参数配置在系统中的key前缀
const DEVICE_PARAM_PREFIXES = {
Camera: 'CAMERA_',
Switch: 'SWITCH_',
Server: 'SERVER_',
Decoder: 'DECODER_',
@@ -67,6 +68,10 @@ const getItemSuffix = (name: string) => {
};
const tabPanes = [
{
tab: '摄像机阈值',
name: DEVICE_PARAM_PREFIXES.Camera,
},
{
tab: '交换机阈值',
name: DEVICE_PARAM_PREFIXES.Switch,
@@ -109,7 +114,7 @@ const show = defineModel<boolean>('show', { required: true });
const { station } = toRefs(props);
const activeTabName = ref<DeviceParamPrefix>(DEVICE_PARAM_PREFIXES.Switch);
const activeTabName = ref<DeviceParamPrefix>(DEVICE_PARAM_PREFIXES.Camera);
const deviceParams = ref<DeviceParamItem[]>([]);
@@ -211,7 +216,7 @@ const onAfterModalEnter = () => {
const onBeforeModalLeave = () => {
saveDeviceParams({ tabName: activeTabName.value, items: deviceParams.value });
activeTabName.value = DEVICE_PARAM_PREFIXES.Switch;
activeTabName.value = DEVICE_PARAM_PREFIXES.Camera;
deviceParams.value = [];
};
</script>
+3
View File
@@ -0,0 +1,3 @@
export * from './nvr';
export * from './security-box';
export * from './switch';
+1
View File
@@ -0,0 +1 @@
export * from './util';
+56
View File
@@ -0,0 +1,56 @@
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) => {
const { ipAddress } = maybeNvrCluster;
const ipList = parseIpListFromClusterList(maybeNvrCluster);
if (ipList.length === 0) return false;
if (ipList.length === 1 && ipList.at(0) === ipAddress) return false;
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,
};
};
@@ -0,0 +1,64 @@
import { rebootSecurityBoxBeidianApi, rebootSecurityBoxNingTechApi, turnCircuitStatusBeidianApi, turnCircuitStatusNingTechApi, type NdmSecurityBoxResultVO } from '@/apis';
import { objectEntries } from '@vueuse/core';
const UNSUPPORTED_SECURITY_BOX_VENDOR = '不支持的安防箱厂商';
export const SECURITY_BOX_VENDOR_LITERALS = {
beidian: 'beidian',
ningtech: 'ningtech',
} as const;
export type SecurityBoxVendor = keyof typeof SECURITY_BOX_VENDOR_LITERALS;
export const resolveSecurityBoxVendor = (vendor?: string) => {
const entry = objectEntries(SECURITY_BOX_VENDOR_LITERALS).find(([, value]) => value === vendor);
return entry?.at(0);
};
export const normalizeSecurityBoxWriteCommunity = (ndmDevice: NdmSecurityBoxResultVO, vendor?: string) => {
const resolved = resolveSecurityBoxVendor(vendor);
if (resolved === SECURITY_BOX_VENDOR_LITERALS.beidian) {
const community = ndmDevice.community;
if (!community) throw new Error('团体字符串不存在');
return community;
}
if (resolved === SECURITY_BOX_VENDOR_LITERALS.ningtech) {
const community = ndmDevice.writeCommunity;
if (!community) throw new Error('团体字符串(写)不存在');
return community;
}
throw new Error(UNSUPPORTED_SECURITY_BOX_VENDOR);
};
export const normalizeSecurityBoxCircuitIndex = (index: number, vendor?: string) => {
const resolved = resolveSecurityBoxVendor(vendor);
if (resolved === SECURITY_BOX_VENDOR_LITERALS.beidian) {
return index;
}
if (resolved === SECURITY_BOX_VENDOR_LITERALS.ningtech) {
return index + 1;
}
throw new Error(UNSUPPORTED_SECURITY_BOX_VENDOR);
};
export const dispatchTurnCircuitStatusApi = (vendor?: string) => {
const resolved = resolveSecurityBoxVendor(vendor);
if (resolved === SECURITY_BOX_VENDOR_LITERALS.beidian) {
return turnCircuitStatusBeidianApi;
}
if (resolved === SECURITY_BOX_VENDOR_LITERALS.ningtech) {
return turnCircuitStatusNingTechApi;
}
throw new Error(UNSUPPORTED_SECURITY_BOX_VENDOR);
};
export const dispatchRebootSecurityBoxApi = (vendor?: string) => {
const resolved = resolveSecurityBoxVendor(vendor);
if (resolved === SECURITY_BOX_VENDOR_LITERALS.beidian) {
return rebootSecurityBoxBeidianApi;
}
if (resolved === SECURITY_BOX_VENDOR_LITERALS.ningtech) {
return rebootSecurityBoxNingTechApi;
}
throw new Error(UNSUPPORTED_SECURITY_BOX_VENDOR);
};
+1
View File
@@ -0,0 +1 @@
export * from './adapter';
+1
View File
@@ -0,0 +1 @@
export * from './util';
+2 -3
View File
@@ -1,3 +1,2 @@
export * from './device-alarm';
export * from './nvr-cluster';
export * from './switch-port';
export * from './device';
export * from './log';
+1
View File
@@ -0,0 +1 @@
export * from './render';
+1
View File
@@ -0,0 +1 @@
export * from './alarm-log';
-8
View File
@@ -1,8 +0,0 @@
import type { NdmNvrResultVO } from '@/apis';
export const isNvrCluster = (maybeNvrCluster: NdmNvrResultVO) => {
const { ipAddress, clusterList } = maybeNvrCluster;
if (!clusterList?.trim()) return false;
if (clusterList === ipAddress) return false;
return true;
};
+11
View File
@@ -136,6 +136,17 @@ export const unwrapResponse = <T>(resp: HttpResponse<T>) => {
return data;
};
export const unwrapNullableResponse = <T>(resp: HttpResponse<T>) => {
const [err, data, result] = resp;
if (err) throw err;
if (result) {
const { isSuccess, path, msg, errorMsg } = result;
if (!isSuccess) throw new Error(`${path ? `${path}: ` : ''}${msg || errorMsg || '请求失败'}`);
}
if (data === undefined) throw new Error('响应数据不存在');
return data;
};
// 针对没有数据的响应,直接判断是否存在错误
export const unwrapVoidResponse = (resp: HttpResponse<void>) => {
const [err, , result] = resp;
+12
View File
@@ -175,6 +175,12 @@ const line10ApiProxyList: ProxyItem[] = [
{ 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[] = [
// { key: '/minio', target: 'http://10.14.0.10:9000', rewrite: ['/minio', ''] },
// { 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: '/ws', target: 'ws://10.18.128.10:18103', ws: true },
...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/