Compare commits

..

79 Commits

Author SHA1 Message Date
yangsy
9dd13f56fb docs: 用户手册 2026-01-29 15:02:40 +08:00
yangsy
b020226538 refactor: 用 useQuery 重构录像诊断卡片 2026-01-29 13:42:41 +08:00
yangsy
b79b1df57e fix: 修复服务状态和推流统计卡片的渲染条件 2026-01-29 13:41:01 +08:00
yangsy
9b21beed0f fix: 下游设备配置添加权限校验 2026-01-28 15:57:23 +08:00
yangsy
aa4684273b feat: 添加批量导出录像诊断并优化导出体验 2026-01-28 14:31:37 +08:00
yangsy
36e839142a fix: 简化设备树选中状态与路由同步的逻辑,修复选中的设备类型被异常还原的问题 2026-01-27 11:05:25 +08:00
yangsy
03006a8f06 fix: 完善设备卡片标签页切换逻辑 2026-01-26 14:05:16 +08:00
yangsy
0af52c62ce feat: 添加权限查询和管理机制
- 新增权限管理页面
- 改进轮询链,引入权限查询
- 支持订阅权限变更或轮询权限检测变更
- 应用权限到页面和组件
2026-01-26 10:54:24 +08:00
yangsy
82789c78a9 feat: 车站卡片布局列数自适应 2026-01-21 15:23:08 +08:00
yangsy
6771abec31 refactor: 移除polling-store,重构setting-store 2026-01-20 11:22:24 +08:00
yangsy
b7b6b216fb feat: 设备告警记录页面添加告警恢复状态和确认状态筛选 2026-01-16 14:36:46 +08:00
yangsy
97f05f21ae refactor: 调整API数据结构目录 2026-01-15 14:57:33 +08:00
yangsy
c4963b5b70 refactor: 优化查询链的耗时和错误日志输出 2026-01-15 14:42:38 +08:00
yangsy
86775a6eb2 feat: 优化车站状态页面的操作栏交互逻辑 2026-01-15 14:42:38 +08:00
yangsy
db3a6ddca5 refactor: 简化录像诊断卡片逻辑 2026-01-15 14:42:38 +08:00
yangsy
b59b34e68b refactor: 抽离未读告警状态,不再持久化 2026-01-15 14:42:37 +08:00
yangsy
de35075be8 refactor: 整理页面的目录结构 2026-01-15 13:41:07 +08:00
yangsy
045f7a9a81 fix: 修复设备硬件占用率卡片中showCard计算属性未获取原始值的问题 2026-01-13 13:51:25 +08:00
yangsy
bd6ad9b932 style: 更新图标库为 lucide 2026-01-13 13:36:28 +08:00
yangsy
b392328f37 fix: 修复数据表格标题错误 2026-01-08 15:59:29 +08:00
yangsy
c3692de19e fix: 修复当API接口定义中没有响应数据时会意外抛出空数据异常的问题 2026-01-08 15:55:28 +08:00
yangsy
c273ae21d9 fix: 未登录时启用离线开发模式后添加默认用户信息 2026-01-08 15:55:09 +08:00
yangsy
653981c0e7 fix: 修正告警页路由路径错误 2026-01-05 14:56:59 +08:00
yangsy
263dd5edfc refactor: 将请求封装重构为函数模式 2026-01-05 14:56:53 +08:00
yangsy
fd70f63fc9 fix: 修复请求实例选择逻辑错误 2026-01-05 14:56:47 +08:00
yangsy
8566b06429 feat: 上级调用日志添加更多数据
- 新增日志类型、用户所属类别、被调用设备名称列
- 优化在线站点选择逻辑
2026-01-04 15:31:31 +08:00
yangsy
a00c736f33 docs: 为NdmCallLog结构添加字段注释 2026-01-04 15:31:30 +08:00
yangsy
85a6b73473 fix: 将表单中的"操作类型"标签改为"日志类型" 2026-01-04 15:31:30 +08:00
yangsy
64c501f94c feat: 移除操作参数和操作结果列 2026-01-04 15:31:30 +08:00
yangsy
c81847ea0f docs: 为NdmVimpLog结构添加字段注释 2026-01-04 15:31:30 +08:00
yangsy
080759c544 fix: 修复操作类型列渲染错误的问题 2026-01-04 15:17:48 +08:00
yangsy
670054ca01 feat: 新增流媒体推流统计卡片 2026-01-04 11:27:46 +08:00
yangsy
80e879e61f refactor: 优化服务状态卡片的渲染条件 2026-01-04 11:18:33 +08:00
yangsy
c259eabc22 fix: 添加防止设备自关联的校验 2026-01-04 10:30:30 +08:00
yangsy
286449b831 feat: 添加告警画面截图相关设置
- 配置告警画面截图保留天数
- 是否自动获取告警画面截图
2025-12-31 10:50:25 +08:00
yangsy
e112b01a21 feat: 告警忽略管理页面 2025-12-30 10:57:43 +08:00
yangsy
054b2bcdac feat: 添加告警忽略管理页面和路由配置 2025-12-30 10:45:48 +08:00
yangsy
1f23588649 refactor: 调整路由结构,使告警板块支持子路由 2025-12-30 10:45:40 +08:00
yangsy
02e29eb4f3 feat: 支持查看摄像机告警画面截图 2025-12-30 10:43:05 +08:00
yangsy
118cc8be0b refactor: 明确所有 stationCode 参数的类型 2025-12-29 14:03:13 +08:00
yangsy
5170105ab8 refactor: 明确所有 stationCode 参数的类型 2025-12-29 14:03:07 +08:00
yangsy
420637352d feat: 查询页面卸载时取消未完成的请求 2025-12-28 14:34:42 +08:00
yangsy
abdbef2d05 fix: 修复跳转设备时未检查deviceId存在性的问题 2025-12-28 13:24:58 +08:00
yangsy
cdd1f2113f feat: 扩展交换机端口诊断信息 2025-12-26 18:14:55 +08:00
yangsy
73c2354a06 feat: 当下游设备不存在时自动解除关联
- 获取下游设备时,如果发现下游设备不存在,将静默解除关联
2025-12-26 18:14:54 +08:00
yangsy
01b059fb7f feat: 告警记录支持点击设备跳转到设备详情 2025-12-26 18:14:54 +08:00
yangsy
fd7f1927ff feat: 设备关联与解除关联
- 支持配置交换机端口的下游关联设备
- 支持配置安防箱电路的下游关联设备
- 支持解除关联
- 删除设备时校验是否存在上/下游设备
2025-12-26 18:14:54 +08:00
yangsy
ed2a4f78ff feat: 扩展设备树功能
- 支持控制是否同步路由参数
- 支持配置允许的事件类型 (select/manage)
- 支持自定义设备节点前缀按钮文字
- 支持向外暴露设备选择逻辑
- 不再封装跳转设备逻辑,由外部实现
- 在车站模式下也支持选择设备
2025-12-25 16:18:41 +08:00
yangsy
f99fe0f68e fix: 移除所有设备更新表单中的上游设备字段 2025-12-25 16:18:41 +08:00
yangsy
76aa654cde fix: 修复设备管理逻辑中错误处理的loading状态和取消逻辑的顺序 2025-12-25 15:53:48 +08:00
yangsy
58b3d09d5d fix: 定位设备树时保留原有的展开状态 2025-12-25 15:53:48 +08:00
yangsy
26d894ba1c feat: 添加更新设备的组合API 2025-12-25 15:53:48 +08:00
yangsy
05b94b2707 fix: 修复设备详情组合API返回undefined的问题 2025-12-25 15:53:47 +08:00
yangsy
c0d49e345b feat: 定义更多的联合设备类型 2025-12-25 13:37:35 +08:00
yangsy
426ca4947a feat: 添加依赖注入工具函数 2025-12-25 13:37:35 +08:00
yangsy
bb140433d8 fix: 修复服务器状态查询在开启离线模式时没有主动取消的问题 2025-12-25 13:37:35 +08:00
yangsy
df304b06d3 refactor: 使用vue-query替换computedAsync解析摄像机建议安装区域 2025-12-25 13:37:35 +08:00
yangsy
9748b94046 refactor: 优化全局加载状态计算 2025-12-25 13:37:34 +08:00
yangsy
4eb2a2ab3d refactor: 简化设备树的自动定位逻辑 2025-12-24 22:34:55 +08:00
yangsy
7f07725935 feat: 摄像机卡片添加摄像机类型和建议安装区域 2025-12-24 22:34:55 +08:00
yangsy
e8ac939795 feat: 设备header卡片添加 append-info 插槽以支持自定义底部内容 2025-12-24 22:34:55 +08:00
yangsy
a7c36079cf fix: 修复指向minio的请求未添加时间戳参数的问题 2025-12-24 22:34:55 +08:00
yangsy
f58ea93985 feat: 调用新的设备告警日志导出接口 2025-12-24 22:34:54 +08:00
yangsy
1e3810ba2d feat: 调整服务器运行时间 label 文案 2025-12-24 22:34:54 +08:00
yangsy
865147fdf8 feat: DeviceHardwareCard组件添加自定义标签属性 2025-12-24 22:34:54 +08:00
yangsy
f35810f907 fix: 视频平台日志页面补全遗漏的操作类型字段 2025-12-24 22:34:54 +08:00
yangsy
92f0e832ca style 2025-12-24 22:34:54 +08:00
yangsy
42b2eacef7 chore: vite代理配置 2025-12-24 22:34:54 +08:00
yangsy
a598b9c1d3 feat: 细化设备树自动定位的触发条件
- 添加 `hasFromPage` 属性,辅助区分选择设备的来源是用户操作还是路由参数
2025-12-24 22:34:54 +08:00
yangsy
2d977c5321 refactor: 路由参数 from 改为fromPage 2025-12-24 22:34:54 +08:00
yangsy
189f7a5d63 feat: 渲染全线设备树时自动定位到所选设备 2025-12-24 22:34:54 +08:00
yangsy
44f5f4d50b fix: 修复由动画属性导致设备树在特定场景下无法自行滚动及展开节点失效的问题
- 当选中的设备所属车站在设备树的视口外时,定位操作无效,且节点无法点击展开,解决方案是在定位逻辑中切换Tree组件的animated状态
2025-12-24 22:34:54 +08:00
yangsy
075f008b65 fix: 设备树仅在非车站模式下显示收起和定位按钮 2025-12-24 22:34:53 +08:00
yangsy
c8274a8c6b refactor: 抽离登录校验查询 2025-12-24 22:34:53 +08:00
yangsy
a90e788dc5 fix: 修复设备更新面板中错误的表单校验逻辑 2025-12-24 22:34:53 +08:00
yangsy
e7fdf911e7 refactor: 简化设备树节点双击和点击事件的逻辑并添加注释 2025-12-24 22:34:53 +08:00
yangsy
c408d875f4 feat: 设备树添加管理功能
- 新增设备导入、导出、删除功能及相关API
- 封装设备管理逻辑,拆分设备选择与设备管理逻辑
- 添加右键菜单支持设备管理操作
2025-12-24 22:34:53 +08:00
yangsy
6183bfd4d7 refactor: 优化登录逻辑并添加注释 2025-12-24 22:34:53 +08:00
yangsy
9af76bacbb refactor: 优化请求封装
- 优化Result接口定义
- 新增响应数据解析逻辑
- 优化错误解析逻辑
2025-12-24 22:34:53 +08:00
155 changed files with 5157 additions and 1180 deletions

2
.env
View File

@@ -19,7 +19,7 @@ VITE_LAMP_PASSWORD = fjoc(1KHP(Ls&Bje)C
VITE_LAMP_AUTHORIZATION = Y3VlZGVzX2FkbWluOmN1ZWRlc19hZG1pbl9zZWNyZXQ=
# 当需要重置localStorage时, 修改此变量
VITE_STORAGE_VERSION = 3
VITE_STORAGE_VERSION = 5
# 调试码
VITE_DEBUG_CODE = ndm_debug

View File

@@ -0,0 +1,327 @@
# 网络设备管理平台用户手册
## 1. 平台概述与基础操作
### 1.1 平台简介
网络设备管理平台(以下简称为“平台”)是专为地铁线路运管中心设计的综合性管理系统。本平台旨在对车站内的各类网络设备进行集中监测、数据查看、运行状态监控及异常告警管理,并提供数据分析与统计功能。
**支持管理的设备类型包括:**
- 摄像机
- 网络录像机
- 交换机
- 解码器
- 智能安防箱
- 媒体服务器
- 视频服务器
- 网络键盘
- 报警主机
### 1.2 登录与退出
**登录平台:**
1. 在浏览器地址栏输入平台访问地址。
2. 进入登录页面后,输入您的 **账号****密码**
3. 点击“登录”按钮。
4. 登录成功后,平台将默认跳转至 **车站状态模块 (首页)**
**退出登录:**
1. 在平台任意页面的 **顶部区域** 右侧,找到显示您昵称的用户按钮。
2. 将鼠标悬停或点击该按钮,在下拉菜单中选择 **“退出登录”**。
3. 平台将安全退出您的账号并返回登录页面。
### 1.3 界面概览
登录后,平台主界面主要由以下四个区域组成:
**1. 顶部区域**
位于页面最上方,包含:
- **平台标题**:点击可快速返回首页。
- **用户信息**:显示当前登录用户昵称及退出入口。
- **设置按钮**:点击可打开设置面板。
**2. 侧边菜单**
位于页面左侧,提供各功能模块的导航入口:
- **车站状态**:全线车站设备运行概览 (首页)。
- **设备诊断**:深入查看具体设备的详细运行参数。
- **设备告警**
- **设备告警记录**:查询历史告警记录。
- **告警忽略管理**:管理忽略设备。
- **系统日志**
- **视频平台日志**:审计视频平台操作。
- **上级调用日志**:审计上级调用指令。
- **权限管理**:用户与权限配置(仅超级管理员可见)。
**3. 底部状态栏**
位于页面最下方,主要用于 **未读告警提示**。当有新的设备告警产生时,此处的铃铛图标会显示红色未读数量徽标,点击可快速跳转至告警记录页面。
**4. 设置面板**
点击顶部区域的“设置”图标(⚙️)即可打开,包含:
- **通用设置**
- **主题**:切换深色/浅色模式,适应不同光照环境。
- **布局**:折叠/展开左侧菜单;调整 **车站卡片矩阵** 的显示列数1-10列
- **业务设置**
- **告警策略**配置告警截图保留天数等详见第4章仅限特定权限人员可见
### 1.4 权限与界面差异说明
本平台采用严格的权限控制机制,您的账号权限将直接决定您能看到的内容和能执行的操作。
**1. 查看权限**
- **影响范围**:决定您能看到哪些车站。
- **界面表现**:如果您没有某车站的查看权限,该车站在 **车站卡片矩阵**、设备树以及所有查询筛选器中都将 **完全不可见**
**2. 操作权限**
- **影响范围**:决定您能否对设备进行配置、控制或管理。
- **界面表现**:如果您没有某车站的操作权限,相关的 **功能按钮**(如“设备配置”、“同步数据”、“告警确认”等)将 **自动隐藏****变为不可用状态**
> **提示**:如果您发现找不到某个车站或缺少某个功能按钮,请联系管理员确认您是否拥有相应的权限。
## 2. 车站状态模块 (首页)
车站状态模块是用户登录后的默认页面,以 **车站卡片矩阵** 的形式展示全线各车站的设备整体运行状态。
### 2.1 车站卡片解读
每个卡片代表一个车站,直观展示该站的关键运行指标:
**1. 基础信息**
- **车站名称**:显示车站名称。
- **在线状态**:右上角的标签显示该车站服务器当前的在线/离线状态。
**2. 统计数据**
- **设备概览**:显示“设备总数”,以及通过颜色区分的“在线设备数”(绿色)和“离线设备数”(红色)。
- **告警概览**:显示“今日告警总数”,若有告警则以醒目颜色提示。
**3. 交互操作**
- **更多菜单**(点击卡片右上角的垂直三点图标):
- **视频平台**:跳转至该车站对应的第三方视频管理平台。
- **设备配置**打开参数配置窗口。_(注:仅当拥有该车站“操作权限”且车站在线时显示)_
- **查看设备详情**:点击设备概览区域右侧的 **详情按钮**(水平三点图标),弹出 **设备详情窗口**,展示该车站的设备树结构(支持进一步跳转至诊断页面)。
- **查看告警详情**:点击告警概览区域右侧的 **详情按钮**(水平三点图标),弹出 **告警详情窗口**,展示该车站当日的告警列表。
### 2.2 操作栏
位于页面顶部的工具栏,提供对多个车站的批量管理功能。
**1. 功能按钮与权限**
- **导出设备状态 / 导出录像诊断**_(需查看权限)_ 仅可选择您拥有“查看权限”的车站进行数据导出。
- **同步摄像机 / 同步录像机通道**_(需操作权限)_ 仅可选择您拥有“操作权限”的车站进行数据同步。
**2. 操作流程**
1. 点击操作栏中的任一功能按钮(例如“同步摄像机”)。
2. 此时进入**多选模式**所有车站卡片上会出现复选框。_(注:系统会自动根据当前操作所需的权限,禁用无权操作的车站复选框)_
3. 勾选您需要操作的车站,或勾选顶部的“全选”框。
4. 点击操作栏右侧出现的 **“确定”** 按钮执行操作:
- **对于导出类操作**:平台将弹出预览窗口,您可在确认数据无误后点击下载 Excel 文件。
- **对于同步类操作**:平台将直接在后台启动同步任务,任务完成后,页面右上角会弹出通知提示成功或失败的数量。
### 2.3 单站详情与配置
除了查看概览,您还可以对单个车站进行更细致的管理。
**1. 设备列表详情**
在设备详情窗口中,您可以浏览该车站下的所有设备。点击任意设备节点,可直接跳转至 **设备诊断页面** 查看该设备的详细指标。
**2. 参数配置** _(需操作权限)_
通过右上角菜单进入“设备配置”,您可以:
- **阈值配置**:设置交换机、服务器、录像机等设备的运行指标告警阈值(如 CPU 占用率、温度上限等)。
- **计划任务**:设置显示器等设备的自动亮屏和息屏计划。
## 3. 设备诊断模块
设备诊断模块用于查看和分析具体设备的详细运行状态。您可以通过侧边菜单的 **“设备诊断”** 进入,或从 **车站状态模块** 的设备详情、告警记录页面跳转进入告警跳转详见第4章
### 3.1 设备树 (侧边栏)
位于页面左侧,以树形结构展示全线所有车站及其下属设备。
**1. 搜索与筛选**
- **搜索框**:输入设备名称、设备 ID 或 IP 地址,可实时过滤设备树节点。
- **状态筛选**:点击单选框,可快速筛选 **“全部 / 在线 / 离线”** 设备。
**2. 设备树交互**
- **展开与选择**:按 **“车站 -> 设备类型 -> 具体设备”** 的层级展开,点击设备节点即可在右侧查看详情。
- **右键菜单管理** _(需操作权限)_
- **在车站节点上右键**
- **导入设备**:批量导入设备数据。
- **导出设备**:导出该车站的设备清单。
- **在设备节点上右键**
- **删除设备**:将该设备从平台中移除。
### 3.2 诊断详情页结构
选中设备后,右侧区域将展示该设备的详细信息。
**1. 功能标签页**
- **当前诊断**:展示设备实时的运行数据(默认视图)。头部信息栏和详细诊断指标均包含在此标签页中。
- **历史诊断**:以图表形式展示设备的历史健康度或关键指标变化趋势。
- **修改设备**_(需操作权限)_ 编辑设备的基础信息(如名称、安装位置等)。
**2. 头部信息栏 (位于“当前诊断”标签页顶部)**
- **基础状态**展示设备名称、IP 地址、在线/离线状态。
- **关联操作**
- **上游设备跳转**:如果设备有上游节点(如摄像机连接的交换机),点击可直接跳转查看上游设备。
- **管理入口**:点击 **“管理”** 按钮,可直接打开该设备自身的 Web 管理后台(如摄像机的 Web 配置页面)。
### 3.3 下游设备配置 (需操作权限)
对于交换机和智能安防箱,平台支持将其端口或电路与下游设备(如摄像机)进行关联,以便建立网络拓扑关系。
- **交换机配置**:在“当前诊断”的端口列表中,**右键点击**任意端口,选择 **“关联设备”**,可将该端口与下游设备绑定;若需解绑,右键选择 **“解除关联”** 即可。
- **智能安防箱配置**:在电路列表中,**右键点击**电路卡片,选择 **“关联设备”** 或 **“解除关联”** 进行配置。
### 3.4 设备诊断指标概览
根据设备类型的不同,“当前诊断”标签页将展示不同的关键指标。以下列举几种典型设备的展示重点:
- **交换机 / 服务器 / 录像机**:侧重硬件资源监控,展示 CPU、内存、磁盘等使用率仪表盘以及端口流量或通道录像状态。
- **摄像机 / 报警主机**侧重基础配置展示如制造商、固件版本、序列号、ONVIF/GB28181 协议配置等。
- **解码器 / 智能安防箱**:兼具硬件状态(如温度、风扇转速)与业务配置(如通道状态)的展示。
## 4. 设备告警模块
设备告警模块是运维人员处理设备异常的核心区域,包含告警记录查询、确认、忽略以及策略配置等功能。
### 4.1 设备告警记录
通过侧边菜单选择 **“设备告警” -> “设备告警记录”** 进入。
**1. 筛选查询**
顶部查询栏提供多维度的组合筛选功能,帮助您快速定位特定告警:
- **车站**:选择查询范围(仅显示您拥有“查看权限”的车站)。
- **设备类型/名称**:按设备属性精确查找。
- **告警属性**:按告警类型、级别(严重/紧急/一般等)、恢复状态(已恢复/未恢复)及确认状态进行筛选。
- **时间范围**:选择告警发生的起止时间。
**2. 列表操作**
- **告警确认** _(需操作权限)_:对于“未确认”的告警,点击操作列的 **“确认”** 按钮进行处理。确认后,系统将记录确认人及时间,且该按钮变为不可点击状态。
- **忽略设备** _(需操作权限)_:若某设备频繁误报,点击 **“忽略设备”** 将其加入忽略名单。被忽略的设备将不再产生新告警,直到被手动恢复。
- **跳转诊断**:点击列表中的设备名称,可直接跳转至该设备的诊断详情页。
**3. 数据导出**
点击页面右上角的 **“导出”** 按钮,可将当前筛选条件下的所有告警记录导出为 Excel 报表,便于线下归档与分析。
### 4.2 告警忽略管理
通过侧边菜单选择 **“设备告警” -> “告警忽略管理”** 进入。
**1. 列表查看**
展示所有当前处于“忽略状态”的设备列表,包含忽略时间、所属车站及设备信息。
**2. 取消忽略 (需操作权限)**
若需恢复对某设备的监控,点击操作列的 **“取消忽略”** 按钮。恢复后,该设备产生的新异常将重新触发告警。
### 4.3 告警策略配置 (系统设置)
部分高级告警策略需在全局设置中进行配置。
**1. 入口与权限**
- **入口**:点击顶部区域的“设置”图标,在设置面板中找到“告警”板块。
- **权限要求**:此板块仅对 **OCC (控制中心) 车站** 的操作员可见。
**2. 配置项**
- **告警画面截图保留天数**设置系统自动抓取的告警截图的存储期限1-15天。过期截图将被自动清理以释放存储空间。
- **自动获取告警画面截图**:开启/关闭告警触发时的自动截图功能。
## 5. 系统日志模块
系统日志模块用于审计和追踪系统内的关键操作记录。
### 5.1 视频平台日志
通过侧边菜单选择 **“系统日志” -> “视频平台日志”** 进入。此模块主要记录平台与第三方视频管理系统之间的交互操作。
**1. 查询功能**
- **车站筛选**:仅显示您拥有“查看权限”的车站日志。
- **操作类型**:筛选具体的交互指令类型(如“获取流地址”、“云台控制”等)。
- **时间范围**:按发生时间段进行精确检索。
**2. 数据导出**
支持将查询结果导出为 Excel 文件,用于故障排查或安全审计。
### 5.2 上级调用日志
通过侧边菜单选择 **“系统日志” -> “上级调用日志”** 进入。此模块记录上级系统(如公安或市级平台)调用本平台资源的请求记录。
**1. 查询功能**
- **车站筛选**:选择日志所属的车站范围。
- **调用类型**:筛选具体的调用指令(如“视频点播”、“录像回放”等)。
- **时间范围**:选择日志记录的时间段。
**2. 数据导出**
同样支持将查询结果导出,以便统计上级单位的调用频率和资源使用情况。
## 6. 权限管理模块
**(注:本模块仅对“超级管理员”可见,普通用户无法访问)**
权限管理模块用于配置系统用户及其对各车站的访问和操作权限。
### 6.1 用户列表管理
通过侧边菜单选择 **“权限管理”** 进入。
**1. 用户查找**
- **搜索**:在顶部搜索框输入 **用户名**,可快速定位目标用户。
- **重置**:点击重置按钮清空搜索条件,显示所有用户。
**2. 列表信息**
展示系统内所有注册用户的基本信息,包括账号、姓名等。
### 6.2 权限配置
点击用户列表右侧的 **“配置权限”** 按钮,进入该用户的权限分配界面。界面以矩阵形式展示,直观易懂。
**1. 权限矩阵解读**
- **行(车站)**:每一行代表一条线路上的具体车站。
- **列(权限类型)**
- **查看**:决定用户能否看到该车站的数据。
- **操作**:决定用户能否对该车站设备进行控制或修改配置。
**2. 批量操作技巧**
- **全选/反选**:点击表头的复选框,可一键授予或取消所有车站的某项权限。
- **单站全权**:点击某一行车站名称前的复选框(如有),可一键授予该车站的所有权限。
**3. 特殊规则说明**
**重要提示**:如果未给某用户勾选任何权限(即所有复选框均为空),平台默认该用户拥有 **超级管理员权限**,可访问所有车站并执行所有操作。请务必谨慎配置。

View File

@@ -0,0 +1,185 @@
# 《网络设备管理平台用户手册》编写计划
本计划已根据权限系统的实际逻辑VIEW/OPERATION进行了调整将在各章节中明确体现权限对界面和功能的影响。
## 阶段一:准备与入门
1. **平台简介**:平台用途与设备支持范围。
2. **登录与注销**:账号登录流程、退出操作。
3. **界面概览**
* **顶部区域 (Header)**:展示平台标题、当前用户信息及设置入口。
* **侧边菜单 (Sider)**:功能模块导航。
* **底部状态栏 (Footer)**:未读告警通知提示。
* **设置面板**
* **通用设置**:主题切换(深色模式)、布局调整(菜单折叠、车站列数)。
* **业务设置**:告警策略配置(详见阶段四)。
4. **权限与界面差异说明(新增)**
* 解释“查看权限”如何决定车站列表的显示(无权限则不显示)。
* 解释“操作权限”如何决定功能按钮的显隐(无权限则隐藏配置入口)。
## 阶段二:首页 - 车站状态监控
**涉及模块**`Station Status` (首页)
1. **车站卡片解读**
* **基础信息**:车站名称、在线/离线状态。
* **统计数据**:设备总数/在线数/离线数、今日告警总数。
* **交互操作**
* **右上角更多菜单**
* **视频平台**:跳转至视频管理平台。
* **设备配置**:打开参数配置窗口 **(注:仅当拥有该车站“操作权限”且车站在线时显示)**。
* **设备详情入口**:点击“设备总数”区域,弹出设备树模态框(可进一步跳转诊断页)。
* **告警详情入口**:点击“告警总数”区域,弹出告警列表模态框。
2. **操作栏 (Batch Actions)**
* **功能按钮与权限差异**
* **导出设备状态 / 导出录像诊断****(需查看权限)** 仅可选择拥有“查看权限”的车站进行导出。
* **同步摄像机 / 同步录像机通道****(需操作权限)** 仅可选择拥有“操作权限”的车站进行同步。
\*- **操作流程**
1. 点击操作栏中的任一功能按钮(如“同步摄像机”)。
2. 此时车站卡片上会出现复选框,且仅有权限的车站可选。
3. 勾选需要操作的车站(支持“全选”)。
4. 点击操作栏右侧出现的“确定”按钮:
* **导出类操作**:系统将弹出预览窗口,您可在窗口中确认数据并下载 Excel 文件。
* **同步类操作**:系统将直接在后台启动同步任务,任务完成后右上角会弹出结果通知(成功/失败数量)。
3. **单站详情与配置**
* **设备列表详情**:展示设备树,点击可跳转诊断页。
* **告警列表详情**。
* **参数配置**:配置阈值与计划任务 **(需操作权限)**。
## 阶段三:设备诊断模块
**涉及模块**`Device Diagnosis`
1. **设备树(侧边栏)**
* **数据范围**:说明仅展示拥有“查看权限”的车站及其下属设备。
* **搜索与筛选**:按名称/IP搜索按状态筛选。
* **树形交互**
* **展开与选择**:按“车站 -> 设备类型 -> 具体设备”层级展开与选择。
* **右键菜单管理****(需操作权限)**
* **车站节点右键**:支持 **导入设备**、**导出设备**。
* **设备节点右键**:支持 **删除设备**
2. **诊断详情页结构**
* **头部信息栏**状态、IP、关联跳转、Web管理入口。
* **功能标签页**
* **当前诊断**:实时数据展示。
* **历史诊断**:历史趋势查看。
* **修改设备**:编辑设备信息 **(注:仅对拥有“操作权限”的用户可见)**。
3. **特定设备诊断指标**
- **分类描述策略**
- **性能类设备**如交换机、服务器、NVR侧重硬件监控展示CPU/内存使用率仪表盘、端口状态、磁盘健康度等。
- **(补充) 下游设备关联配置**
- **交换机**:右键点击端口 -> 关联设备(如摄像机)。
- **安防箱**:右键点击电路 -> 关联设备。
- **操作权限要求****(需操作权限)**。
- **信息类设备**(如摄像机、报警主机):侧重参数展示,展示制造商、固件版本、协议配置等静态属性。
## 阶段四:告警管理模块
**涉及模块**`Alarm`
1. **子模块:告警记录 (Alarm Log)**
* **筛选查询**
* 组合筛选:车站(受查看权限限制)、设备类型、设备名称、告警类型、级别、状态、时间。
* **列表操作**
* **告警确认**:点击“确认”按钮处理告警 **(注:需拥有该车站的操作权限,否则按钮禁用/隐藏)**。
* **忽略设备**:将设备加入忽略列表 **(需操作权限)**。
* **数据导出**导出Excel报表。
2. **子模块:告警忽略 (Alarm Ignore)**
* **列表查看**:查看被忽略的设备记录。
* **取消忽略**:点击恢复监控 **(注:需拥有操作权限)**。
3. **告警策略配置 (系统设置)**
* **配置入口**:通过顶部导航栏“设置”按钮进入。
* **配置项**:告警画面截图保留天数、自动获取截图开关。
* **权限要求****(注:仅对 OCC 车站的操作员可见)**。
## 阶段五:日志管理模块
**涉及模块**`Log`
1. **子模块:上级调用日志 (Call Log)**
* **查询**:筛选车站(受查看权限限制)、日志类型(如视频点播、云台指令)、时间范围。
* **导出**:导出查询结果。
2. **子模块:视频平台日志 (Vimp Log)**
* **查询**:筛选车站(受查看权限限制)、内部操作类型、时间范围。
* **导出**:导出查询结果。
## 阶段六:权限管理模块
**涉及模块**`Permission`
**(注:本模块仅对“超级管理员”可见,普通用户无法访问)**
1. **用户列表管理**
* **搜索**:通过真实姓名查找用户。
* **重置**:清空搜索条件。
2. **权限配置 (Permission Config)**
* **矩阵式界面**
* **行**:对应各个车站。
* **列**:对应具体权限类型(如查看、操作)。
* **批量操作**:支持整行(单站全权)或整列(全站某权)一键勾选。
* **特殊规则说明**:若未给用户勾选任何权限,平台默认该用户拥有**所有权限**(超级管理员模式)。

View File

@@ -39,19 +39,3 @@ pnpm build
```
在执行 `pnpm build` 之前,你可以在 `package.json` 中修改 `version` 字段,将其设置为你期望的版本号,构建完成后,项目的根目录中除了 `dist` 目录外,还会生成三个压缩包,文件名的格式统一为 `ndm-web-platform_v<version>_<datetime>`,文件格式则分别为 `zip``tar``tar.gz`
## 调试模式
在调试模式中,用户可以查看设备的原始诊断数据,也可以对轮询器进行控制,或者启用离线开发模式,系统不会自动调用一些主动触发的请求。
### 开启调试模式
在非登录页的任意页面中,使用键盘组合键 `Ctrl+Alt+D`,系统会弹出一个输入框,输入环境变量 `.env` 中的 `VITE_DEBUG_CODE` 对应的值即可开启调试模式,如需关闭调试模式,再次使用上述组合键并点击 `确认` 按钮即可。
注意调试模式与其内部的功能之间没有联动关系,例如在开启调试模式后可以关闭轮询或者启用离线开发模式,但是在关闭调试模式后,轮询不会重新被开启,离线开发模式也不会被关闭,因此在关闭离线开发模式前,请务必确保系统处于正确的运行状态下。
### 关于离线开发模式
由于离线开发模式涉及到登录操作,因此项目中将离线开发模式暴露到了全局变量 `window.$offlineDev` 中,允许在登录页中直接开启离线开发模式。
如果你第一次启动这个项目,系统在正常情况下会先跳转至登录页,此时如果希望开启离线模式,可以直接打开浏览器的开发者工具,在控制台输入 `window.$offlineDev.value = true` 即可,系统会直接跳转到首页。

View File

@@ -28,6 +28,7 @@
"echarts": "^6.0.0",
"es-toolkit": "^1.41.0",
"localforage": "^1.10.0",
"lucide-vue-next": "^0.562.0",
"naive-ui": "^2.43.1",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
@@ -40,8 +41,6 @@
"@tsconfig/node22": "^22.0.2",
"@types/crypto-js": "^4.2.2",
"@types/node": "^22.18.11",
"@vicons/antd": "^0.13.0",
"@vicons/ionicons5": "^0.13.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.1.1",
"@vue/eslint-config-prettier": "^10.2.0",
@@ -58,5 +57,5 @@
"vite-plugin-vue-devtools": "^8.0.3",
"vue-tsc": "^3.1.3"
},
"packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd"
"packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48"
}

28
pnpm-lock.yaml generated
View File

@@ -44,6 +44,9 @@ importers:
localforage:
specifier: ^1.10.0
version: 1.10.0
lucide-vue-next:
specifier: ^0.562.0
version: 0.562.0(vue@3.5.24(typescript@5.9.3))
naive-ui:
specifier: ^2.43.1
version: 2.43.1(vue@3.5.24(typescript@5.9.3))
@@ -75,12 +78,6 @@ importers:
'@types/node':
specifier: ^22.18.11
version: 22.19.1
'@vicons/antd':
specifier: ^0.13.0
version: 0.13.0
'@vicons/ionicons5':
specifier: ^0.13.0
version: 0.13.0
'@vitejs/plugin-vue':
specifier: ^6.0.1
version: 6.0.1(vite@7.2.2(@types/node@22.19.1)(jiti@2.6.1)(sass@1.94.0)(tsx@4.20.6))(vue@3.5.24(typescript@5.9.3))
@@ -845,12 +842,6 @@ packages:
resolution: {integrity: sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vicons/antd@0.13.0':
resolution: {integrity: sha512-yrUGoUSz2BbGupk9ghQOahc04n5H3MwUDM9pVPsLh9U1uqB47oRWZvYRiZaT1JKPqgTgSE6BXcVw4i9MOF4M+g==}
'@vicons/ionicons5@0.13.0':
resolution: {integrity: sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ==}
'@vitejs/plugin-vue-jsx@5.1.1':
resolution: {integrity: sha512-uQkfxzlF8SGHJJVH966lFTdjM/lGcwJGzwAHpVqAPDD/QcsqoUGa+q31ox1BrUfi+FLP2ChVp7uLXE3DkHyDdQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -1636,6 +1627,11 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lucide-vue-next@0.562.0:
resolution: {integrity: sha512-LN0BLGKMFulv0lnfK29r14DcngRUhIqdcaL0zXTt2o0oS9odlrjCGaU3/X9hIihOjjN8l8e+Y9G/famcNYaI7Q==}
peerDependencies:
vue: '>=3.0.1'
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -2871,10 +2867,6 @@ snapshots:
'@typescript-eslint/types': 8.46.4
eslint-visitor-keys: 4.2.1
'@vicons/antd@0.13.0': {}
'@vicons/ionicons5@0.13.0': {}
'@vitejs/plugin-vue-jsx@5.1.1(vite@7.2.2(@types/node@22.19.1)(jiti@2.6.1)(sass@1.94.0)(tsx@4.20.6))(vue@3.5.24(typescript@5.9.3))':
dependencies:
'@babel/core': 7.28.5
@@ -3701,6 +3693,10 @@ snapshots:
dependencies:
yallist: 3.1.1
lucide-vue-next@0.562.0(vue@3.5.24(typescript@5.9.3)):
dependencies:
vue: 3.5.24(typescript@5.9.3)
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5

View File

@@ -1,16 +1,16 @@
<script setup lang="ts">
import { useVersionCheckQuery } from './composables';
import { GlobalFeedback } from '@/components';
import { useVersionCheckQuery } from '@/composables';
import { useSettingStore } from '@/stores';
import { VueQueryDevtools } from '@tanstack/vue-query-devtools';
import { dateZhCN, NConfigProvider, NDialogProvider, NLoadingBarProvider, NMessageProvider, NNotificationProvider, zhCN } from 'naive-ui';
import { storeToRefs } from 'pinia';
const settingStore = useSettingStore();
const { themeMode, offlineDev } = storeToRefs(settingStore);
const { themeMode, mockUser } = storeToRefs(settingStore);
// 允许通过控制台启用离线开发模式 (登录页适用)
window.$offlineDev = offlineDev;
window.$mockUser = mockUser;
useVersionCheckQuery();
</script>

View File

@@ -1,8 +1,8 @@
import { useUserStore } from '@/stores';
import { getAppEnvConfig, RequestClient } from '@/utils';
import { createHttpClient, getAppEnvConfig } from '@/utils';
import type { AxiosError } from 'axios';
export const ndmClient = new RequestClient({
export const ndmClient = createHttpClient({
requestInterceptor: async (config) => {
const userStore = useUserStore();
const { lampAuthorization, lampClientId, lampClientSecret } = getAppEnvConfig();
@@ -25,7 +25,7 @@ export const ndmClient = new RequestClient({
const userStore = useUserStore();
await userStore.lampLogin(stationCode);
error.config.headers.token = userStore.lampLoginResultRecord?.[stationCode]?.token ?? '';
return ndmClient.requestInstance(error.config);
return ndmClient.clientInstance(error.config);
}
return Promise.reject(error);
},

View File

@@ -1,9 +1,9 @@
import router from '@/router';
import { useUserStore } from '@/stores';
import { getAppEnvConfig, RequestClient } from '@/utils';
import { createHttpClient, getAppEnvConfig } from '@/utils';
import type { AxiosError } from 'axios';
export const userClient = new RequestClient({
export const userClient = createHttpClient({
requestInterceptor: (config) => {
const userStore = useUserStore();
const { lampAuthorization, lampClientId, lampClientSecret } = getAppEnvConfig();

View File

@@ -19,4 +19,12 @@ export interface NdmSwitchPortInfo {
outFlow: number;
portName: string;
upDown: number;
lastChangeTime: string;
opticalTemperature?: number;
opticalVoltage?: number;
opticalBiasCurrent?: number;
opticalReceivePower?: number;
opticalTransmitPower?: number;
}

View File

@@ -1,2 +1,3 @@
export * from './diag';
export * from './link-description';
export * from './station';

View File

@@ -0,0 +1,10 @@
import type { NdmCameraLinkDescription } from './ndm-camera-link-description';
import type { NdmSecurityBoxLinkDescription } from './ndm-security-box-link-description';
import type { NdmSwitchLinkDescription } from './ndm-switch-link-description';
export * from './link-description';
export * from './ndm-camera-link-description';
export * from './ndm-security-box-link-description';
export * from './ndm-switch-link-description';
export type NdmDeviceLinkDescription = NdmCameraLinkDescription | NdmSecurityBoxLinkDescription | NdmSwitchLinkDescription;

View File

@@ -0,0 +1,5 @@
import type { DeviceStoreIndex } from '@/apis';
export interface LinkDescription {
upstream?: DeviceStoreIndex[];
}

View File

@@ -0,0 +1,3 @@
import type { LinkDescription } from './link-description';
export interface NdmCameraLinkDescription extends LinkDescription {}

View File

@@ -0,0 +1,8 @@
import type { DeviceStoreIndex } from '@/apis';
import type { LinkDescription } from './link-description';
export interface NdmSecurityBoxLinkDescription extends LinkDescription {
downstream?: {
[circuitIndex: number]: DeviceStoreIndex;
};
}

View File

@@ -0,0 +1,8 @@
import type { DeviceStoreIndex } from '@/apis';
import type { LinkDescription } from './link-description';
export interface NdmSwitchLinkDescription extends LinkDescription {
downstream?: {
[portName: string]: DeviceStoreIndex;
};
}

View File

@@ -10,7 +10,13 @@ import type {
NdmVideoServerResultVO,
Station,
} from '@/apis';
import { DEVICE_TYPE_LITERALS } from '@/enums';
import { DEVICE_TYPE_LITERALS, type DeviceType } from '@/enums';
export interface DeviceStoreIndex {
stationCode: Station['code'];
deviceType: DeviceType;
deviceDbId: string;
}
export interface StationDevices {
[DEVICE_TYPE_LITERALS.ndmAlarmHost]: NdmAlarmHostResultVO[];

View File

@@ -3,4 +3,5 @@ export interface Station {
name: string;
online: boolean;
ip: string;
occ?: boolean; // 是否为控制中心
}

View File

@@ -0,0 +1,21 @@
import type { BaseModel, ReduceForPageQuery, ReduceForSaveVO, ReduceForUpdateVO } from '@/apis';
import type { Nullable, Optional } from '@/types';
export interface BaseEmployee extends BaseModel {
userId: string;
realName: string;
defUser: Nullable<
{
username: string;
nickName: string;
} & BaseModel
>;
}
export type BaseEmployeeResultVO = Nullable<BaseEmployee>;
export type BaseEmployeeSaveVO = Partial<Omit<BaseEmployee, ReduceForSaveVO>>;
export type BaseEmployeeUpdateVO = Optional<Omit<BaseEmployee, ReduceForUpdateVO>>;
export type BaseEmployeePageQuery = Partial<Omit<BaseEmployee, ReduceForPageQuery>>;

View File

@@ -1,3 +1 @@
export * from './model';
export * from './page';
export * from './reduce';
export * from './base-employee';

View File

@@ -1,45 +1,21 @@
import type { Nullable } from '@/types';
import type { NdmAlarmHost, NdmAlarmHostPageQuery } from './alarm';
import type { NdmSecurityBox, NdmSecurityBoxPageQuery, NdmSwitch, NdmSwitchPageQuery } from './other';
import type { NdmNvr, NdmNvrPageQuery } from './storage';
import type {
NdmCamera,
NdmCameraPageQuery,
NdmDecoder,
NdmDecoderPageQuery,
NdmKeyboard,
NdmKeyboardPageQuery,
NdmMediaServer,
NdmMediaServerPageQuery,
NdmMediaServerResultVO,
NdmMediaServerSaveVO,
NdmMediaServerUpdateVO,
NdmVideoServer,
NdmVideoServerPageQuery,
NdmVideoServerResultVO,
NdmVideoServerSaveVO,
NdmVideoServerUpdateVO,
} from './video';
import type { Nullable, Optional } from '@/types';
import type { ReduceForPageQuery, ReduceForSaveVO, ReduceForUpdateVO } from '../../schema';
import type { NdmAlarmHost } from './alarm';
import type { NdmSecurityBox, NdmSwitch } from './other';
import type { NdmNvr } from './storage';
import type { NdmCamera, NdmDecoder, NdmKeyboard, NdmMediaServer, NdmVideoServer } from './video';
export type NdmDevice = NdmAlarmHost | NdmCamera | NdmDecoder | NdmKeyboard | NdmMediaServer | NdmNvr | NdmSecurityBox | NdmSwitch | NdmVideoServer;
export type NdmDevicePageQuery =
| NdmAlarmHostPageQuery
| NdmCameraPageQuery
| NdmDecoderPageQuery
| NdmKeyboardPageQuery
| NdmMediaServerPageQuery
| NdmNvrPageQuery
| NdmSecurityBoxPageQuery
| NdmSwitchPageQuery
| NdmVideoServerPageQuery;
export type NdmDeviceResultVO = Nullable<NdmDevice>;
export type NdmDeviceSaveVO = Partial<Omit<NdmDevice, ReduceForSaveVO>>;
export type NdmDeviceUpdateVO = Optional<Omit<NdmDevice, ReduceForUpdateVO>>;
export type NdmDevicePageQuery = Partial<Omit<NdmDevice, ReduceForPageQuery>>;
export type NdmServer = NdmMediaServer | NdmVideoServer;
export type NdmServerResultVO = NdmMediaServerResultVO | NdmVideoServerResultVO;
export type NdmServerSaveVO = NdmMediaServerSaveVO | NdmVideoServerSaveVO;
export type NdmServerUpdateVO = NdmMediaServerUpdateVO | NdmVideoServerUpdateVO;
export type NdmServerPageQuery = NdmMediaServerPageQuery | NdmVideoServerPageQuery;
export type NdmServerResultVO = Nullable<NdmServer>;
export type NdmServerSaveVO = Partial<Omit<NdmServer, ReduceForSaveVO>>;
export type NdmServerUpdateVO = Optional<Omit<NdmServer, ReduceForUpdateVO>>;
export type NdmServerPageQuery = Partial<Omit<NdmServer, ReduceForPageQuery>>;
export * from './alarm';
export * from './icmp';

View File

@@ -1,5 +1,6 @@
export * from './ndm-call-log';
export * from './ndm-device-alarm-log';
export * from './ndm-device-alarm-snap-log';
export * from './ndm-icmp-log';
export * from './ndm-record-check';
export * from './ndm-snmp-log';

View File

@@ -2,11 +2,38 @@ import type { BaseModel, ReduceForPageQuery, ReduceForSaveVO, ReduceForUpdateVO
import type { Nullable } from '@/types';
export interface NdmCallLog extends BaseModel {
/**
* 调用者国标码
*/
sourceGbId: string;
/**
* 用户所属类别
*/
sourceType: string;
/**
* 被调用设备国标码
*/
targetGbId: string;
/**
* 被调用设备名称
*/
targetName: string;
/**
* 调用方法
*/
method: string;
/**
* message类型
*/
messageType: string;
/**
* 操作类型
*/
cmdType: string;
/**
* 日志类型
*/
logType: string;
}
export type NdmCallLogResultVO = Nullable<NdmCallLog>;

View File

@@ -0,0 +1,16 @@
import type { BaseModel, ReduceForPageQuery, ReduceForSaveVO, ReduceForUpdateVO } from '@/apis';
import type { Nullable } from '@/types';
export interface NdmDeviceAlarmSnapLog extends BaseModel {
absoluteFilePath: string;
path: string;
url: string;
}
export type NdmDeviceAlarmSnapLogResultVO = Nullable<NdmDeviceAlarmSnapLog>;
export type NdmDeviceAlarmSnapLogSaveVO = Partial<Omit<NdmDeviceAlarmSnapLog, ReduceForSaveVO>>;
export type NdmDeviceAlarmSnapLogUpdateVO = Partial<Omit<NdmDeviceAlarmSnapLog, ReduceForUpdateVO>>;
export type NdmDeviceAlarmSnapLogPageQuery = Partial<Omit<NdmDeviceAlarmSnapLog, ReduceForPageQuery>>;

View File

@@ -2,18 +2,57 @@ import type { BaseModel, ReduceForSaveVO, ReduceForUpdateVO, ReduceForPageQuery
import type { Nullable } from '@/types';
export interface NdmVimpLog extends BaseModel {
/**
* 请求IP
*/
requestIp: string;
/**
* 操作内容
*/
description: string;
/**
* 类路径
*/
classPath: string;
/**
* 函数名
*/
methodName: string;
/**
* 起始时间
*/
startTime: string;
/**
* 结束时间
*/
endTime: string;
/**
* 消耗时间
*/
consumedTime: string;
/**
* 操作参数
*/
params: string;
/**
* 操作结果
*/
result: string;
/**
* 请求类型
*/
httpMethod: string;
/**
* 请求用户
*/
userId: string;
/**
* 日志类型
*/
logType: number;
/**
* 目标国标码
*/
targetCode: string;
}

View File

@@ -1,2 +1,3 @@
export * from './ndm-permission';
export * from './ndm-security-box';
export * from './ndm-switch';

View File

@@ -0,0 +1,34 @@
import type { BaseModel, ReduceForPageQuery, ReduceForSaveVO, ReduceForUpdateVO, Station } from '@/apis';
import type { PermissionType } from '@/enums';
import type { Nullable, Optional } from '@/types';
export interface NdmPermission extends BaseModel {
/**
* 员工ID
*/
employeeId: string;
/**
* 服务器IP地址
*/
ipAddress: string;
/**
* 站号
*/
stationCode: Station['code'];
/**
* 站名
*/
name: string;
/**
* 权限类型
*/
type: PermissionType;
}
export type NdmPermissionResultVO = Nullable<NdmPermission>;
export type NdmPermissionSaveVO = Partial<Omit<NdmPermission, ReduceForSaveVO>>;
export type NdmPermissionUpdateVO = Optional<Omit<NdmPermission, ReduceForUpdateVO>>;
export type NdmPermissionPageQuery = Partial<Omit<NdmPermission, ReduceForPageQuery>>;

View File

@@ -1 +1,3 @@
export * from './invite-stream-type';
export * from './send-rtp-info';
export * from './snap-result';

View File

@@ -0,0 +1 @@
export type InviteStreamType = 'PLAY' | 'PLAYBACK' | 'DOWNLOAD' | 'PUSH' | 'PROXY' | 'CLOUD_RECORD_PUSH' | 'CLOUD_RECORD_PROXY';

View File

@@ -0,0 +1,51 @@
import type { InviteStreamType } from '@/apis';
import type { Nullable } from '@/types';
export type SendRtpInfo = Nullable<{
ip: string;
port: number;
ssrc: string;
platformId: string;
deviceId: string;
channelId: string;
app: string;
streamId: string;
/**
* 推流状态
* 0 等待设备推流上来
* 1 等待上级平台回复ack
* 2 推流中
*/
status: number;
/**
* 是否为tcp
*/
tcp: boolean;
/**
* 是否为tcp主动模式
*/
tcpActive: boolean;
localPort: number;
mediaServerId: string;
serverId: string;
callId: string;
fromTag: string;
toTag: string;
/**
* 发送时rtp的ptuint8_t,不传时默认为96
*/
pt: number;
/**
* 发送时rtp的负载类型。为true时负载为ps为false时为es
*/
usePs: boolean;
/**
* 当usePs 为false时有效。为1时发送音频为0时发送视频不传时默认为0
*/
onlyAudio: boolean;
/**
* 是否开启rtcp保活
*/
rtcp: boolean;
playType: InviteStreamType;
}>;

View File

@@ -1,5 +1,9 @@
export interface SnapResult {
absoluteFilePath: string;
path: string;
url: string;
code: number;
msg: string;
data: {
absoluteFilePath: string;
path: string;
url: string;
};
}

View File

@@ -1,4 +1,5 @@
export * from './base';
export * from './biz';
export * from './common';
export * from './schema';
export * from './system';

View File

@@ -0,0 +1,3 @@
export * from './model';
export * from './page';
export * from './reduce';

View File

@@ -0,0 +1,21 @@
import type { BaseEmployeePageQuery, BaseEmployeeResultVO, PageParams, PageResult } from '@/apis';
import { userClient } from '@/apis/client';
import { unwrapResponse } from '@/utils';
export const pageBaseEmployeeApi = async (pageQuery: PageParams<BaseEmployeePageQuery>, options?: { signal?: AbortSignal }) => {
const { signal } = options ?? {};
const client = userClient;
const endpoint = '/api/base/baseEmployee/page';
const resp = await client.post<PageResult<BaseEmployeeResultVO>>(endpoint, pageQuery, { signal });
const data = unwrapResponse(resp);
return data;
};
export const detailBaseEmployeeApi = async (id: string, options?: { signal?: AbortSignal }) => {
const { signal } = options ?? {};
const client = userClient;
const endpoint = `/api/base/baseEmployee/detail`;
const resp = await client.get<BaseEmployeeResultVO>(endpoint, { params: { id }, signal });
const data = unwrapResponse(resp);
return data;
};

View File

@@ -0,0 +1 @@
export * from './base-employee';

View File

@@ -1,7 +1,7 @@
import { initStationDevices, ndmClient, userClient, type StationDevices } from '@/apis';
import { initStationDevices, ndmClient, userClient, type Station, type StationDevices } from '@/apis';
import { unwrapResponse } from '@/utils';
export const getAllDevicesApi = async (options?: { stationCode?: string; signal?: AbortSignal }) => {
export const getAllDevicesApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';

View File

@@ -20,40 +20,31 @@ export const detailDeviceApi = async (device: NdmDeviceResultVO, options?: { sta
const deviceType = tryGetDeviceType(deviceTypeCode);
if (!deviceType) throw new Error('未知的设备');
if (deviceType === DEVICE_TYPE_LITERALS.ndmAlarmHost) {
await detailAlarmHostApi(id, { stationCode, signal });
return;
return await detailAlarmHostApi(id, { stationCode, signal });
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmCamera) {
await detailCameraApi(id, { stationCode, signal });
return;
return await detailCameraApi(id, { stationCode, signal });
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmDecoder) {
await detailDecoderApi(id, { stationCode, signal });
return;
return await detailDecoderApi(id, { stationCode, signal });
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmKeyboard) {
await detailKeyboardApi(id, { stationCode, signal });
return;
return await detailKeyboardApi(id, { stationCode, signal });
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmMediaServer) {
await detailMediaServerApi(id, { stationCode, signal });
return;
return await detailMediaServerApi(id, { stationCode, signal });
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
await detailNvrApi(id, { stationCode, signal });
return;
return await detailNvrApi(id, { stationCode, signal });
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmSecurityBox) {
await detailSecurityBoxApi(id, { stationCode, signal });
return;
return await detailSecurityBoxApi(id, { stationCode, signal });
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmSwitch) {
await detailSwitchApi(id, { stationCode, signal });
return;
return await detailSwitchApi(id, { stationCode, signal });
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmVideoServer) {
await detailVideoServerApi(id, { stationCode, signal });
return;
return await detailVideoServerApi(id, { stationCode, signal });
}
return undefined;
};

View File

@@ -3,3 +3,4 @@ export * from './detail-device';
export * from './export-device';
export * from './import-device';
export * from './probe-device';
export * from './update-device';

View File

@@ -0,0 +1,59 @@
import {
updateAlarmHostApi,
updateCameraApi,
updateDecoderApi,
updateKeyboardApi,
updateMediaServerApi,
updateNvrApi,
updateSecurityBoxApi,
updateSwitchApi,
updateVideoServerApi,
type NdmDeviceResultVO,
type Station,
} from '@/apis';
import { DEVICE_TYPE_LITERALS, tryGetDeviceType } from '@/enums';
export const updateDeviceApi = async (device: NdmDeviceResultVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }): Promise<NdmDeviceResultVO | undefined> => {
const { stationCode, signal } = options ?? {};
const { id, deviceType: deviceTypeCode } = device;
if (!id || !deviceTypeCode) throw new Error('未知的设备');
const deviceType = tryGetDeviceType(deviceTypeCode);
if (!deviceType) throw new Error('未知的设备');
if (deviceType === DEVICE_TYPE_LITERALS.ndmAlarmHost) {
await updateAlarmHostApi(device, { stationCode, signal });
return;
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmCamera) {
await updateCameraApi(device, { stationCode, signal });
return;
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmDecoder) {
await updateDecoderApi(device, { stationCode, signal });
return;
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmKeyboard) {
await updateKeyboardApi(device, { stationCode, signal });
return;
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmMediaServer) {
await updateMediaServerApi(device, { stationCode, signal });
return;
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
await updateNvrApi(device, { stationCode, signal });
return;
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmSecurityBox) {
await updateSecurityBoxApi(device, { stationCode, signal });
return;
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmSwitch) {
await updateSwitchApi(device, { stationCode, signal });
return;
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmVideoServer) {
await updateVideoServerApi(device, { stationCode, signal });
return;
}
return undefined;
};

View File

@@ -1,5 +1,5 @@
import { ndmClient, userClient, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
import { unwrapVoidResponse } from '@/utils';
export const resetMonitorScheduleApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -7,5 +7,5 @@ export const resetMonitorScheduleApi = async (options?: { stationCode?: Station[
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmConstant/anyTenant/resetMonitorSchedule`;
const resp = await client.get<void>(endpoint, { signal });
unwrapResponse(resp);
unwrapVoidResponse(resp);
};

View File

@@ -1,5 +1,5 @@
import { ndmClient, userClient, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
import { unwrapVoidResponse } from '@/utils';
export const verifyApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -7,5 +7,5 @@ export const verifyApi = async (options?: { stationCode?: Station['code']; signa
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmKeepAlive/verify`;
const resp = await client.post<void>(endpoint, {}, { retRaw: true, timeout: 5000, signal });
unwrapResponse(resp);
unwrapVoidResponse(resp);
};

View File

@@ -6,4 +6,5 @@ export * from './icmp';
export * from './log';
export * from './storage';
export * from './other';
export * from './upper-ndm';
export * from './video';

View File

@@ -1,5 +1,6 @@
export * from './ndm-call-log';
export * from './ndm-device-alarm-log';
export * from './ndm-device-alarm-snap-log';
export * from './ndm-icmp-log';
export * from './ndm-snmp-log';
export * from './ndm-record-check';

View File

@@ -25,7 +25,7 @@ export const exportDeviceAlarmLogApi = async (pageQuery: PageParams<NdmDeviceAla
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmDeviceAlarmLog/defaultExportByTemplate`;
const endpoint = `${prefix}/api/ndm/ndmDeviceAlarmLog/exportByTemplateV2`;
const resp = await client.post<BlobPart>(endpoint, pageQuery, { responseType: 'blob', retRaw: true, signal });
const data = unwrapResponse(resp);
return data;

View File

@@ -0,0 +1,62 @@
import {
ndmClient,
userClient,
type NdmDeviceAlarmSnapLogPageQuery,
type NdmDeviceAlarmSnapLogResultVO,
type NdmDeviceAlarmSnapLogSaveVO,
type NdmDeviceAlarmSnapLogUpdateVO,
type PageParams,
type PageResult,
type Station,
} from '@/apis';
import { unwrapResponse } from '@/utils';
export const pageDeviceAlarmSnapLogApi = async (pageQuery: PageParams<NdmDeviceAlarmSnapLogPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmDeviceAlarmSnapLog/page`;
const resp = await client.post<PageResult<NdmDeviceAlarmSnapLogResultVO>>(endpoint, pageQuery, { signal });
const data = unwrapResponse(resp);
return data;
};
export const detailDeviceAlarmSnapLogApi = async (id: 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/ndmDeviceAlarmSnapLog/detail`;
const resp = await client.get<NdmDeviceAlarmSnapLogResultVO>(endpoint, { params: { id }, signal });
const data = unwrapResponse(resp);
return data;
};
export const saveDeviceAlarmSnapLogApi = async (saveVO: NdmDeviceAlarmSnapLogSaveVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmDeviceAlarmSnapLog`;
const resp = await client.post<NdmDeviceAlarmSnapLogResultVO>(endpoint, saveVO, { signal });
const data = unwrapResponse(resp);
return data;
};
export const updateDeviceAlarmSnapLogApi = async (updateVO: NdmDeviceAlarmSnapLogUpdateVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmDeviceAlarmSnapLog`;
const resp = await client.put<NdmDeviceAlarmSnapLogResultVO>(endpoint, updateVO, { signal });
const data = unwrapResponse(resp);
return data;
};
export const deleteDeviceAlarmSnapLogApi = async (ids: 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/ndmDeviceAlarmSnapLog`;
const resp = await client.delete<boolean>(endpoint, ids, { signal });
const data = unwrapResponse(resp);
return data;
};

View File

@@ -1,8 +1,8 @@
import { ndmClient, userClient, type ClientChannel, type NdmNvrResultVO, type NdmRecordCheck } from '@/apis';
import { ndmClient, userClient, type ClientChannel, type NdmNvrResultVO, type NdmRecordCheck, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
import dayjs from 'dayjs';
export const getChannelListApi = async (ndmNvr: NdmNvrResultVO, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const getChannelListApi = async (ndmNvr: NdmNvrResultVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
@@ -12,7 +12,7 @@ export const getChannelListApi = async (ndmNvr: NdmNvrResultVO, options?: { stat
return data;
};
export const getRecordCheckApi = async (ndmNvr: NdmNvrResultVO, lastDays: number, gbCodeList: string[], options?: { stationCode?: string; signal?: AbortSignal }) => {
export const getRecordCheckApi = async (ndmNvr: NdmNvrResultVO, lastDays: number, gbCodeList: string[], options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
@@ -27,7 +27,7 @@ export const getRecordCheckApi = async (ndmNvr: NdmNvrResultVO, lastDays: number
return data;
};
export const reloadRecordCheckApi = async (channel: ClientChannel, dayOffset: number, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const reloadRecordCheckApi = async (channel: ClientChannel, dayOffset: number, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
@@ -37,7 +37,7 @@ export const reloadRecordCheckApi = async (channel: ClientChannel, dayOffset: nu
return data;
};
export const reloadAllRecordCheckApi = async (dayOffset: number, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const reloadAllRecordCheckApi = async (dayOffset: number, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
@@ -48,3 +48,13 @@ export const reloadAllRecordCheckApi = async (dayOffset: number, options?: { sta
if (!data) throw new Error(`${data}`);
return data;
};
export const batchExportRecordCheckApi = async (params: { checkDuration: number; gapSeconds: number; stationCode: Station['code'][] }, options?: { signal?: AbortSignal }) => {
const { signal } = options ?? {};
const { checkDuration, gapSeconds, stationCode } = params;
const client = userClient;
const endpoint = `/api/ndm/ndmRecordCheck/batchExportByTemplate`;
const resp = await client.post<Blob>(endpoint, { checkDuration, gapSeconds, stationCode }, { responseType: 'blob', retRaw: true, signal });
const data = unwrapResponse(resp);
return data;
};

View File

@@ -1,3 +1,4 @@
export * from './ndm-permission';
export * from './ndm-security-box';
export * from './ndm-service-available';
export * from './ndm-switch';

View File

@@ -0,0 +1,83 @@
import {
ndmClient,
userClient,
type NdmPermissionPageQuery,
type NdmPermissionResultVO,
type NdmPermissionSaveVO,
type NdmPermissionUpdateVO,
type PageParams,
type PageResult,
type Station,
} from '@/apis';
import type { PermissionTypeEnum } from '@/enums';
import { unwrapResponse } from '@/utils';
export const permissionTypesApi = 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/ndmPermission/types`;
const resp = await client.get<PermissionTypeEnum>(endpoint, { signal });
const data = unwrapResponse(resp);
return data;
};
export const pagePermissionApi = async (pageQuery: PageParams<NdmPermissionPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmPermission/page`;
const resp = await client.post<PageResult<NdmPermissionResultVO>>(endpoint, pageQuery, { signal });
const data = unwrapResponse(resp);
return data;
};
export const detailPermissionApi = async (id: 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/ndmPermission/detail`;
const resp = await client.get<NdmPermissionResultVO>(endpoint, { params: { id }, signal });
const data = unwrapResponse(resp);
return data;
};
export const savePermissionApi = async (saveVO: NdmPermissionSaveVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmPermission`;
const resp = await client.post<NdmPermissionResultVO>(endpoint, saveVO, { signal });
const result = unwrapResponse(resp);
return result;
};
export const updatePermissionApi = async (updateVO: NdmPermissionUpdateVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmPermission`;
const resp = await client.put<NdmPermissionResultVO>(endpoint, updateVO, { signal });
const result = unwrapResponse(resp);
return result;
};
export const deletePermissionApi = async (ids: 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/ndmPermission`;
const resp = await client.delete<boolean>(endpoint, ids, { signal });
const result = unwrapResponse(resp);
return result;
};
export const modifyPermissionApi = async (params: { employeeId: string; saveList: NdmPermissionSaveVO[]; removeList: 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/ndmPermission/modify`;
const resp = await client.post<boolean>(endpoint, params, { signal });
const result = unwrapResponse(resp);
return result;
};

View File

@@ -10,7 +10,7 @@ import {
type PageResult,
type Station,
} from '@/apis';
import { unwrapResponse } from '@/utils';
import { unwrapResponse, unwrapVoidResponse } from '@/utils';
export const pageSecurityBoxApi = async (pageQuery: PageParams<NdmSecurityBoxPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -90,7 +90,7 @@ export const probeSecurityBoxApi = async (ids: string[], options?: { stationCode
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSecurityBox/probeByIds`;
const resp = await client.post<void>(endpoint, ids, { signal });
unwrapResponse(resp);
unwrapVoidResponse(resp);
};
export const turnCitcuitStatusApi = async (ipAddress: string, circuitIndex: number, status: number, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {

View File

@@ -1,6 +1,16 @@
import { ndmClient, userClient, type MediaServerStatus, type Station } from '@/apis';
import { ndmClient, userClient, type MediaServerStatus, type SendRtpInfo, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
export const getAllPushApi = 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/mediaServer/getAllPush`;
const resp = await client.get<SendRtpInfo[]>(endpoint, { signal });
const data = unwrapResponse(resp);
return data;
};
export const isMediaServerAliveApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;

View File

@@ -10,11 +10,11 @@ import {
type PageResult,
type Station,
} from '@/apis';
import { unwrapResponse } from '@/utils';
import { unwrapResponse, unwrapVoidResponse } from '@/utils';
export const pageSwitchApi = async (pageQuery: PageParams<NdmSwitchPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : ndmClient;
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSwitch/page`;
const resp = await client.post<PageResult<NdmSwitchResultVO>>(endpoint, pageQuery, { signal });
@@ -24,7 +24,7 @@ export const pageSwitchApi = async (pageQuery: PageParams<NdmSwitchPageQuery>, o
export const detailSwitchApi = async (id: string, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : ndmClient;
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSwitch/detail`;
const resp = await client.get<NdmSwitchResultVO>(endpoint, { params: { id }, signal });
@@ -34,7 +34,7 @@ export const detailSwitchApi = async (id: string, options?: { stationCode?: Stat
export const saveSwitchApi = async (saveVO: NdmSwitchSaveVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : ndmClient;
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSwitch`;
const resp = await client.post<NdmSwitchResultVO>(endpoint, saveVO, { signal });
@@ -44,7 +44,7 @@ export const saveSwitchApi = async (saveVO: NdmSwitchSaveVO, options?: { station
export const updateSwitchApi = async (updateVO: NdmSwitchUpdateVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : ndmClient;
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSwitch`;
const resp = await client.put<NdmSwitchResultVO>(endpoint, updateVO, { signal });
@@ -54,7 +54,7 @@ export const updateSwitchApi = async (updateVO: NdmSwitchUpdateVO, options?: { s
export const deleteSwitchApi = async (ids: string[], options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : ndmClient;
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSwitch`;
const resp = await client.delete<boolean>(endpoint, ids, { signal });
@@ -64,7 +64,7 @@ export const deleteSwitchApi = async (ids: string[], options?: { stationCode?: S
export const exportSwitchApi = async (pageQuery: PageParams<NdmSwitchPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : ndmClient;
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSwitch/defaultExportByTemplate`;
const resp = await client.post<Blob>(endpoint, pageQuery, { responseType: 'blob', retRaw: true, signal });
@@ -86,9 +86,9 @@ export const importSwitchApi = async (file: File, options?: { stationCode?: Stat
export const probeSwitchApi = async (ids: string[], options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : ndmClient;
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSwitch/probeByIds`;
const resp = await client.post<void>(endpoint, ids, { signal });
unwrapResponse(resp);
unwrapVoidResponse(resp);
};

View File

@@ -1,5 +1,5 @@
import { ndmClient, userClient, type ImportMsg, type NdmNvrPageQuery, type NdmNvrResultVO, type NdmNvrSaveVO, type NdmNvrUpdateVO, type PageParams, type PageResult, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
import { unwrapResponse, unwrapVoidResponse } from '@/utils';
export const pageNvrPageApi = async (pageQuery: PageParams<NdmNvrPageQuery>, options?: { stationCode: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -79,7 +79,7 @@ export const probeNvrApi = async (ids: string[], options?: { stationCode?: Stati
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmNvr/probeByIds`;
const resp = await client.post<void>(endpoint, ids, { signal });
unwrapResponse(resp);
unwrapVoidResponse(resp);
};
export const syncNvrChannelsApi = async (options?: { stationCode?: string; signal?: AbortSignal }) => {
@@ -88,5 +88,5 @@ export const syncNvrChannelsApi = async (options?: { stationCode?: string; signa
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmNvr/syncNvrChannels`;
const resp = await client.get<void>(endpoint, { signal });
unwrapResponse(resp);
unwrapVoidResponse(resp);
};

View File

@@ -0,0 +1 @@
export * from './upper-ndm';

View File

@@ -0,0 +1,20 @@
import { ndmClient, userClient, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
export async function snapStatusApi(method: 'get', options?: { stationCode?: Station['code']; signal?: AbortSignal }): Promise<boolean>;
export async function snapStatusApi(method: 'post', options: { doSnap: boolean; stationCode?: Station['code']; signal?: AbortSignal }): Promise<boolean>;
export async function snapStatusApi(method: 'get' | 'post', options?: { doSnap?: boolean; stationCode?: Station['code']; signal?: AbortSignal }) {
const { doSnap, stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/anyTenant/snapStatus`;
if (method === 'get') {
const resp = await client.get<boolean>(endpoint, { signal });
const data = unwrapResponse(resp);
return data;
} else {
const resp = await client.post<boolean>(endpoint, doSnap, { signal });
const data = unwrapResponse(resp);
return data;
}
}

View File

@@ -3,4 +3,5 @@ export * from './ndm-camera-ignore';
export * from './ndm-decoder';
export * from './ndm-keyboard';
export * from './ndm-media-server';
export * from './ndm-snap';
export * from './ndm-video-server';

View File

@@ -1,7 +1,17 @@
import { ndmClient, userClient, type NdmCameraIgnorePageQuery, type NdmCameraIgnoreResultVO, type NdmCameraIgnoreSaveVO, type NdmCameraIgnoreUpdateVO, type PageParams, type PageResult } from '@/apis';
import {
ndmClient,
userClient,
type NdmCameraIgnorePageQuery,
type NdmCameraIgnoreResultVO,
type NdmCameraIgnoreSaveVO,
type NdmCameraIgnoreUpdateVO,
type PageParams,
type PageResult,
type Station,
} from '@/apis';
import { unwrapResponse } from '@/utils';
export const pageCameraIgnoreApi = async (pageQuery: PageParams<NdmCameraIgnorePageQuery>, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const pageCameraIgnoreApi = async (pageQuery: PageParams<NdmCameraIgnorePageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
@@ -11,7 +21,7 @@ export const pageCameraIgnoreApi = async (pageQuery: PageParams<NdmCameraIgnoreP
return data;
};
export const detailCameraIgnoreApi = async (id: string, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const detailCameraIgnoreApi = async (id: string, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
@@ -21,7 +31,7 @@ export const detailCameraIgnoreApi = async (id: string, options?: { stationCode?
return data;
};
export const saveCameraIgnoreApi = async (saveVO: NdmCameraIgnoreSaveVO, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const saveCameraIgnoreApi = async (saveVO: NdmCameraIgnoreSaveVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
@@ -31,7 +41,7 @@ export const saveCameraIgnoreApi = async (saveVO: NdmCameraIgnoreSaveVO, options
return data;
};
export const updateCameraIgnoreApi = async (updateVO: NdmCameraIgnoreUpdateVO, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const updateCameraIgnoreApi = async (updateVO: NdmCameraIgnoreUpdateVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
@@ -41,7 +51,7 @@ export const updateCameraIgnoreApi = async (updateVO: NdmCameraIgnoreUpdateVO, o
return data;
};
export const deleteCameraIgnoreApi = async (ids: string[], options?: { stationCode?: string; signal?: AbortSignal }) => {
export const deleteCameraIgnoreApi = async (ids: string[], options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';

View File

@@ -88,12 +88,12 @@ export const importCameraApi = async (file: File, options?: { stationCode?: Stat
export const getCameraSnapApi = async (deviceId: string, options?: { signal?: AbortSignal }) => {
const { signal } = options ?? {};
const endpoint = `/api/ndm/ndmCamera/getSnapByDeviceId`;
const resp = await ndmClient.get<SnapResult>(endpoint, { params: { deviceId }, signal });
const resp = await ndmClient.get<SnapResult>(endpoint, { params: { deviceId }, retRaw: true, signal });
const data = unwrapResponse(resp);
return data;
};
export const syncCameraApi = async (options?: { stationCode?: string; signal?: AbortSignal }) => {
export const syncCameraApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';

View File

@@ -10,7 +10,7 @@ import {
type PageResult,
type Station,
} from '@/apis';
import { unwrapResponse } from '@/utils';
import { unwrapResponse, unwrapVoidResponse } from '@/utils';
export const pageDecoderApi = async (pageQuery: PageParams<NdmDecoderPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -90,5 +90,5 @@ export const probeDecoderApi = async (ids: string[], options?: { stationCode?: S
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmDecoder/probeByIds`;
const resp = await client.post<void>(endpoint, ids, { signal });
unwrapResponse(resp);
unwrapVoidResponse(resp);
};

View File

@@ -10,7 +10,7 @@ import {
type PageResult,
type Station,
} from '@/apis';
import { unwrapResponse } from '@/utils';
import { unwrapResponse, unwrapVoidResponse } from '@/utils';
export const postNdmMediaServerPage = async (pageQuery: PageParams<NdmMediaServerPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -90,5 +90,5 @@ export const probeMediaServerApi = async (ids: string[], options?: { stationCode
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmMediaServer/probeByIds`;
const resp = await client.post<void>(endpoint, ids, { signal });
unwrapResponse(resp);
unwrapVoidResponse(resp);
};

View File

@@ -0,0 +1,20 @@
import { ndmClient, userClient, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
export async function retentionDaysApi(method: 'get', options?: { stationCode?: Station['code']; signal?: AbortSignal }): Promise<number>;
export async function retentionDaysApi(method: 'post', options: { days: number; stationCode?: Station['code']; signal?: AbortSignal }): Promise<number>;
export async function retentionDaysApi(method: 'get' | 'post', options?: { days?: number; stationCode?: Station['code']; signal?: AbortSignal }) {
const { days, stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSnap/retentionDays`;
if (method === 'get') {
const resp = await client.get<number>(endpoint, { signal });
const data = unwrapResponse(resp);
return data;
} else {
const resp = await client.post<number>(endpoint, days, { signal });
const data = unwrapResponse(resp);
return data;
}
}

View File

@@ -10,7 +10,7 @@ import {
type PageResult,
type Station,
} from '@/apis';
import { unwrapResponse } from '@/utils';
import { unwrapResponse, unwrapVoidResponse } from '@/utils';
export const pageVideoServerApi = async (pageQuery: PageParams<NdmVideoServerPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -42,7 +42,7 @@ export const saveVideoServerApi = async (saveVO: NdmVideoServerSaveVO, options?:
return data;
};
export const updateVideoServerApi = async (id: string, updateVO: NdmVideoServerUpdateVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
export const updateVideoServerApi = async (updateVO: NdmVideoServerUpdateVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
@@ -90,5 +90,5 @@ export const probeVideoServerApi = async (ids: string[], options?: { stationCode
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmVideoServer/probeByIds`;
const resp = await client.post<void>(endpoint, ids, { signal });
unwrapResponse(resp);
unwrapVoidResponse(resp);
};

View File

@@ -1,2 +1,3 @@
export * from './base';
export * from './biz';
export * from './system';

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ClockCircleOutlined, CodeOutlined, FireOutlined, SaveOutlined } from '@vicons/antd';
import { ClockCheckIcon, CpuIcon, HardDriveIcon, MemoryStickIcon } from 'lucide-vue-next';
import { NCard, NFlex, NIcon, NProgress, type ProgressStatus } from 'naive-ui';
import { computed, toRefs } from 'vue';
@@ -8,12 +8,16 @@ const props = defineProps<{
memUsage?: string;
diskUsage?: string;
runningTime?: string;
cpuUsageLabel?: string;
memUsageLabel?: string;
diskUsageLabel?: string;
runningTimeLabel?: string;
}>();
const { cpuUsage, memUsage, diskUsage, runningTime } = toRefs(props);
const { cpuUsage, memUsage, diskUsage, runningTime, cpuUsageLabel, memUsageLabel, diskUsageLabel, runningTimeLabel } = toRefs(props);
const showCard = computed(() => {
return Object.values(props).some((value) => !!value);
return Object.values({ cpuUsage, memUsage, diskUsage, runningTime }).some((refValue) => !!refValue.value);
});
const cpuPercent = computed(() => {
@@ -50,23 +54,23 @@ const getProgressStatus = (percent: number): ProgressStatus => {
<template #default>
<NFlex vertical>
<NFlex v-if="cpuUsage" style="width: 100%" align="center" :wrap="false">
<NIcon :component="FireOutlined" />
<span style="word-break: keep-all">CPU</span>
<NIcon :component="CpuIcon" />
<span style="word-break: keep-all">{{ cpuUsageLabel || 'CPU' }}</span>
<NProgress :percentage="cpuPercent" :status="getProgressStatus(cpuPercent)">{{ cpuPercent }}%</NProgress>
</NFlex>
<NFlex v-if="memUsage" style="width: 100%" align="center" :wrap="false">
<NIcon :component="CodeOutlined" />
<span style="word-break: keep-all">内存</span>
<NIcon :component="MemoryStickIcon" />
<span style="word-break: keep-all">{{ memUsageLabel || '内存' }}</span>
<NProgress :percentage="memPercent" :status="getProgressStatus(memPercent)">{{ memPercent }}%</NProgress>
</NFlex>
<NFlex v-if="diskUsage" style="width: 100%" align="center" :wrap="false">
<NIcon :component="SaveOutlined" />
<span style="word-break: keep-all">磁盘</span>
<NIcon :component="HardDriveIcon" />
<span style="word-break: keep-all">{{ diskUsageLabel || '磁盘' }}</span>
<NProgress :percentage="diskPercent" :status="getProgressStatus(diskPercent)">{{ diskPercent }}%</NProgress>
</NFlex>
<NFlex v-if="runningTime" style="width: 100%" align="center" :wrap="false">
<NIcon :component="ClockCircleOutlined" />
<span>系统运行时间</span>
<NIcon :component="ClockCheckIcon" />
<span>{{ runningTimeLabel || '运行时间' }}</span>
<span>{{ formattedRunningTime }}</span>
</NFlex>
</NFlex>

View File

@@ -1,19 +1,29 @@
<script setup lang="ts">
import { detailDeviceApi, probeDeviceApi, type NdmDeviceResultVO, type Station } from '@/apis';
import { DEVICE_TYPE_NAMES, tryGetDeviceType } from '@/enums';
import { detailDeviceApi, probeDeviceApi, type LinkDescription, type NdmDeviceResultVO, type Station } from '@/apis';
import { SELECT_DEVICE_FN_INJECTION_KEY } from '@/constants';
import { DEVICE_TYPE_LITERALS, DEVICE_TYPE_NAMES, tryGetDeviceType } from '@/enums';
import { useDeviceStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { ApiOutlined, ReloadOutlined } from '@vicons/antd';
import { isCancel } from 'axios';
import destr from 'destr';
import { LinkIcon, RotateCwIcon } from 'lucide-vue-next';
import { NButton, NCard, NFlex, NIcon, NTag, NTooltip } from 'naive-ui';
import { computed, onBeforeUnmount, ref, toRefs } from 'vue';
import { storeToRefs } from 'pinia';
import { computed, inject, onBeforeUnmount, ref, toRefs } from 'vue';
const props = defineProps<{
ndmDevice: NdmDeviceResultVO;
station: Station;
}>();
defineSlots<{
'append-info': () => any;
}>();
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
const { ndmDevice, station } = toRefs(props);
const type = computed(() => {
@@ -26,6 +36,44 @@ const status = computed(() => ndmDevice.value.deviceStatus);
const ipAddr = computed(() => ndmDevice.value.ipAddress ?? '-');
const gbCode = computed(() => Reflect.get(ndmDevice.value, 'gbCode') as string | undefined);
const linkDescription = computed(() => {
const result = destr<any>(ndmDevice.value.linkDescription);
if (!result) return null;
if (typeof result !== 'object') return null;
return result as LinkDescription;
});
const upperDevices = computed(() => {
const devices: NdmDeviceResultVO[] = [];
if (!linkDescription.value) return devices;
if (!linkDescription.value.upstream) return devices;
linkDescription.value.upstream.forEach((deviceStoreIndex) => {
const { stationCode, deviceType, deviceDbId } = deviceStoreIndex;
const stationDevices = lineDevices.value[stationCode];
if (!stationDevices) return;
const classified = stationDevices[deviceType];
const device = classified.find((device) => device.id === deviceDbId);
if (device) devices.push(device);
});
return devices.sort((aDevice, bDevice) => {
// 按在DEVICE_TYPE_LITERALS中的顺序排序
const aDeviceType = tryGetDeviceType(aDevice.deviceType);
const bDeviceType = tryGetDeviceType(bDevice.deviceType);
if (!aDeviceType || !bDeviceType) return 0;
const deviceTypes = Object.values(DEVICE_TYPE_LITERALS);
return deviceTypes.indexOf(aDeviceType) - deviceTypes.indexOf(bDeviceType);
});
});
// 获取从父组件注入的 `selectDevice` 函数
const selectDeviceFn = inject(SELECT_DEVICE_FN_INJECTION_KEY);
// 跳转到上游设备
const navigateToUpperDevice = (upperDevice: NdmDeviceResultVO) => {
if (!!selectDeviceFn && !!selectDeviceFn.value) {
selectDeviceFn.value(upperDevice, station.value.code);
}
};
const canOpenMgmtPage = computed(() => {
return Object.keys(ndmDevice.value).includes('manageUrl');
});
@@ -92,40 +140,49 @@ onBeforeUnmount(() => {
<template>
<NCard hoverable size="small">
<template #header>
<NFlex align="center">
<NTag v-if="status === '10'" size="small" type="success">在线</NTag>
<NTag v-else-if="status === '20'" size="small" type="error">线</NTag>
<NTag v-else size="small" type="warning">-</NTag>
<div>{{ name }}</div>
<NButton v-if="canOpenMgmtPage" ghost size="tiny" type="default" :focusable="false" @click="onClickOpenMgmtPage">管理</NButton>
<NFlex vertical>
<NFlex align="center">
<NTag v-if="status === '10'" size="small" type="success">线</NTag>
<NTag v-else-if="status === '20'" size="small" type="error">离线</NTag>
<NTag v-else size="small" type="warning">-</NTag>
<div>{{ name }}</div>
<NButton v-if="canOpenMgmtPage" ghost size="tiny" type="default" :focusable="false" @click="onClickOpenMgmtPage">管理</NButton>
<div style="margin-left: auto">
<NTooltip v-if="canProbe" trigger="hover">
<template #trigger>
<NButton size="small" quaternary circle :loading="probing" @click="() => probeDevice()">
<template #icon>
<NIcon :component="LinkIcon" />
</template>
</NButton>
</template>
<template #default>
<span>请求最新诊断</span>
</template>
</NTooltip>
<NTooltip trigger="hover">
<template #trigger>
<NButton size="small" quaternary circle :loading="loading" @click="() => detailDevice()">
<template #icon>
<NIcon :component="RotateCwIcon" />
</template>
</NButton>
</template>
<template #default>
<span>刷新设备</span>
</template>
</NTooltip>
</div>
</NFlex>
<div v-if="upperDevices.length > 0" style="font-size: 0.85rem">
<span>上游设备</span>
<template v-for="(device, index) in upperDevices" :key="index">
<span style="text-decoration: underline; cursor: pointer" @click="() => navigateToUpperDevice(device)">{{ device.name }}</span>
<span v-if="index < upperDevices.length - 1"></span>
</template>
</div>
</NFlex>
</template>
<template #header-extra>
<NTooltip v-if="canProbe" trigger="hover">
<template #trigger>
<NButton size="small" quaternary circle :loading="probing" @click="() => probeDevice()">
<template #icon>
<NIcon :component="ApiOutlined" />
</template>
</NButton>
</template>
<template #default>
<span>请求最新诊断</span>
</template>
</NTooltip>
<NTooltip trigger="hover">
<template #trigger>
<NButton size="small" quaternary circle :loading="loading" @click="() => detailDevice()">
<template #icon>
<NIcon :component="ReloadOutlined" />
</template>
</NButton>
</template>
<template #default>
<span>刷新设备</span>
</template>
</NTooltip>
</template>
<template #default>
<div style="font-size: small; color: #666">
<div>
@@ -150,6 +207,9 @@ onBeforeUnmount(() => {
</div>
</div>
</template>
<template #footer>
<slot name="append-info"></slot>
</template>
</NCard>
</template>

View File

@@ -4,7 +4,20 @@ import DeviceHeaderCard from './device-header-card.vue';
import NvrDiskCard from './nvr-disk-card.vue';
import NvrRecordCard from './nvr-record-card.vue';
import SecurityBoxCircuitCard from './security-box-circuit-card.vue';
import SecurityBoxCircuitLinkModal from './security-box-circuit-link-modal.vue';
import SecurityBoxEnvCard from './security-box-env-card.vue';
import SwitchPortCard from './switch-port-card.vue';
import SwitchPortLinkModal from './switch-port-link-modal.vue';
export { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, NvrDiskCard, NvrRecordCard, SecurityBoxCircuitCard, SecurityBoxEnvCard, SwitchPortCard };
export {
DeviceCommonCard,
DeviceHardwareCard,
DeviceHeaderCard,
NvrDiskCard,
NvrRecordCard,
SecurityBoxCircuitCard,
SecurityBoxCircuitLinkModal,
SecurityBoxEnvCard,
SwitchPortCard,
SwitchPortLinkModal,
};

View File

@@ -1,34 +1,58 @@
<script setup lang="ts">
import { getChannelListApi, getRecordCheckApi, reloadAllRecordCheckApi, reloadRecordCheckApi, type NdmNvrResultVO, type NdmRecordCheck, type RecordItem, type Station } from '@/apis';
import { getChannelListApi, getRecordCheckApi, reloadAllRecordCheckApi, reloadRecordCheckApi, type NdmNvrResultVO, type RecordItem, type Station } from '@/apis';
import { exportRecordDiagCsv, transformRecordChecks } from '@/helpers';
import { useStationStore } from '@/stores';
import { useSettingStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { DownloadOutlined, ReloadOutlined } from '@vicons/antd';
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import dayjs from 'dayjs';
import { DownloadIcon, RotateCwIcon } from 'lucide-vue-next';
import { NButton, NCard, NFlex, NIcon, NPagination, NPopconfirm, NPopover, NRadioButton, NRadioGroup, NTooltip, useThemeVars } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, onBeforeUnmount, onMounted, ref, toRefs, watch } from 'vue';
import { computed, onBeforeUnmount, ref, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmNvrResultVO;
station: Station;
}>();
const settingStore = useSettingStore();
const { activeRequests } = storeToRefs(settingStore);
const themeVars = useThemeVars();
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const queryClient = useQueryClient();
const { ndmDevice, station } = toRefs(props);
const recordChecks = ref<NdmRecordCheck[]>([]);
const lossInput = ref<number>(0);
const abortController = ref<AbortController>(new AbortController());
const NVR_RECORD_CHECK_KEY = 'nvr_record_check_query';
const {
data: recordChecks,
isFetching: loading,
refetch: refetchRecordChecks,
} = useQuery({
queryKey: computed(() => [NVR_RECORD_CHECK_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value),
refetchInterval: 30 * 1000,
gcTime: 0,
queryFn: async ({ signal }) => {
const checks = await getRecordCheckApi(ndmDevice.value, 90, [], { stationCode: station.value.code, signal });
return checks;
},
});
watch(activeRequests, (active) => {
if (!active) {
queryClient.cancelQueries({ queryKey: [NVR_RECORD_CHECK_KEY] });
}
});
const recordDiags = computed(() => {
return transformRecordChecks(recordChecks.value).filter((recordDiag) => {
return transformRecordChecks(recordChecks.value ?? []).filter((recordDiag) => {
if (lossInput.value === 0) {
return true;
} else if (lossInput.value === 1) {
@@ -40,26 +64,6 @@ const recordDiags = computed(() => {
});
});
const abortController = ref<AbortController>(new AbortController());
const { mutate: getRecordCheckByParentId, isPending: loading } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
const checks = await getRecordCheckApi(ndmDevice.value, 90, [], { stationCode: station.value.code, signal: abortController.value.signal });
return checks;
},
onSuccess: (checks) => {
recordChecks.value = checks;
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const { mutate: reloadAllRecordCheck, isPending: reloading } = useMutation({
mutationFn: async () => {
abortController.value.abort();
@@ -78,9 +82,7 @@ const { mutate: reloadAllRecordCheck, isPending: reloading } = useMutation({
});
const onExportRecordCheck = () => {
const code = station.value.code;
const stationName = stations.value.find((station) => station.code === code)?.name ?? '';
exportRecordDiagCsv(recordDiags.value, stationName);
exportRecordDiagCsv(recordDiags.value, station.value.name);
};
const page = ref(1);
@@ -121,7 +123,7 @@ const { mutate: reloadRecordCheckByGbId } = useMutation({
}
},
onSuccess: () => {
getRecordCheckByParentId();
refetchRecordChecks();
},
onError: (error) => {
if (isCancel(error)) return;
@@ -131,19 +133,6 @@ const { mutate: reloadRecordCheckByGbId } = useMutation({
},
});
onMounted(() => {
getRecordCheckByParentId();
});
watch(
() => ndmDevice.value.id,
(devieDbId) => {
if (devieDbId) {
getRecordCheckByParentId();
}
},
);
onBeforeUnmount(() => {
abortController.value.abort();
});
@@ -168,9 +157,9 @@ onBeforeUnmount(() => {
<NFlex>
<NTooltip trigger="hover">
<template #trigger>
<NButton size="small" quaternary circle :loading="loading" @click="() => getRecordCheckByParentId()">
<NButton size="small" quaternary circle :loading="loading" @click="() => refetchRecordChecks()">
<template #icon>
<NIcon :component="ReloadOutlined" />
<NIcon :component="RotateCwIcon" />
</template>
</NButton>
</template>
@@ -182,7 +171,7 @@ onBeforeUnmount(() => {
<template #trigger>
<NButton size="small" quaternary circle @click="onExportRecordCheck">
<template #icon>
<NIcon :component="DownloadOutlined" />
<NIcon :component="DownloadIcon" />
</template>
</NButton>
</template>

View File

@@ -1,28 +1,57 @@
<script setup lang="ts">
import { probeDeviceApi, rebootSecurityBoxApi, turnCitcuitStatusApi, type NdmSecurityBoxCircuit, type NdmSecurityBoxResultVO, type Station } from '@/apis';
import {
detailDeviceApi,
probeDeviceApi,
rebootSecurityBoxApi,
turnCitcuitStatusApi,
updateDeviceApi,
type LinkDescription,
type NdmDeviceResultVO,
type NdmSecurityBoxCircuit,
type NdmSecurityBoxLinkDescription,
type NdmSecurityBoxResultVO,
type Station,
} from '@/apis';
import { SecurityBoxCircuitLinkModal } from '@/components';
import { usePermission } from '@/composables';
import { SELECT_DEVICE_FN_INJECTION_KEY } from '@/constants';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useDeviceStore, useSettingStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { PoweroffOutlined } from '@vicons/antd';
import { watchImmediate } from '@vueuse/core';
import { NButton, NCard, NDescriptions, NDescriptionsItem, NFlex, NIcon, NPopconfirm, NPopover, NSwitch, NTag, useThemeVars, type TagProps } from 'naive-ui';
import { computed, ref, toRefs } from 'vue';
import { isCancel } from 'axios';
import destr from 'destr';
import { cloneDeep, isFunction } from 'es-toolkit';
import { PowerIcon } from 'lucide-vue-next';
import { NButton, NCard, NDescriptions, NDescriptionsItem, NDropdown, NFlex, NIcon, NPopconfirm, NPopover, NSwitch, NTag, useThemeVars, type DropdownOption, type TagProps } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, inject, ref, toRefs } from 'vue';
const props = defineProps<{
circuits?: NdmSecurityBoxCircuit[];
ndmDevice: NdmSecurityBoxResultVO;
station: Station;
circuits?: NdmSecurityBoxCircuit[];
}>();
const themeVars = useThemeVars();
const { circuits, ndmDevice, station } = toRefs(props);
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
const settingStore = useSettingStore();
const { useLocalDB } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station, circuits } = toRefs(props);
const showCard = computed(() => !!circuits.value && circuits.value.length > 0);
const boxCircuits = ref<NdmSecurityBoxCircuit[]>([]);
const localCircuits = ref<NdmSecurityBoxCircuit[]>([]);
watchImmediate(circuits, (newCircuits) => {
boxCircuits.value = newCircuits?.map((circuit) => ({ ...circuit })) ?? [];
localCircuits.value = newCircuits?.map((circuit) => ({ ...circuit })) ?? [];
});
const getCircuitStatusTagType = (circuit: NdmSecurityBoxCircuit): TagProps['type'] => {
@@ -40,22 +69,34 @@ const getCircuitStatusClassName = (circuit: NdmSecurityBoxCircuit) => {
return status === 0 ? 'circuit-off' : status === 1 ? 'circuit-on' : 'circuit-unknown';
};
const abortController = ref<AbortController>(new AbortController());
const { mutate: turnStatus, isPending: turning } = useMutation({
mutationFn: async (params: { circuitIndex: number; newStatus: boolean }) => {
abortController.value.abort();
abortController.value = new AbortController();
window.$loadingBar.start();
const { circuitIndex, newStatus } = params;
if (!ndmDevice.value.ipAddress) {
throw new Error('设备IP地址不存在');
}
const status = newStatus ? 1 : 0;
await turnCitcuitStatusApi(ndmDevice.value.ipAddress, circuitIndex, status, { stationCode: station.value.code });
await probeDeviceApi(ndmDevice.value, { stationCode: station.value.code });
const stationCode = station.value.code;
const signal = abortController.value.signal;
await turnCitcuitStatusApi(ndmDevice.value.ipAddress, circuitIndex, status, { stationCode, signal });
await probeDeviceApi(ndmDevice.value, { stationCode, signal });
return status;
},
onSuccess: (status, { circuitIndex }) => {
const circuit = boxCircuits.value.at(circuitIndex);
window.$loadingBar.finish();
const circuit = localCircuits.value.at(circuitIndex);
if (circuit) circuit.status = status;
},
onError: (error) => {
window.$loadingBar.error();
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
@@ -64,15 +105,189 @@ const { mutate: turnStatus, isPending: turning } = useMutation({
const { mutate: reboot, isPending: rebooting } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
window.$loadingBar.start();
if (!ndmDevice.value.ipAddress) {
throw new Error('设备IP地址不存在');
}
await rebootSecurityBoxApi(ndmDevice.value.ipAddress, { stationCode: station.value.code });
const stationCode = station.value.code;
const signal = abortController.value.signal;
await rebootSecurityBoxApi(ndmDevice.value.ipAddress, { stationCode, signal });
},
onSuccess: () => {
window.$loadingBar.finish();
window.$message.success('设备重启成功');
},
onError: (error) => {
window.$loadingBar.error();
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const upperDeviceLinkDescription = computed(() => {
const result = destr<any>(ndmDevice.value.linkDescription);
if (!result) return null;
if (typeof result !== 'object') return null;
return result as NdmSecurityBoxLinkDescription;
});
const getLowerDeviceByCircuitIndex = (circuitIndex: number) => {
if (!upperDeviceLinkDescription.value) return null;
const downstream = upperDeviceLinkDescription.value.downstream;
if (!downstream) return null;
const deviceStoreIndex = downstream[circuitIndex];
if (!deviceStoreIndex) return null;
const { stationCode, deviceType, deviceDbId } = deviceStoreIndex;
const stationDevices = lineDevices.value[stationCode];
if (!stationDevices) return null;
const devices = stationDevices[deviceType];
const lowerDevice = devices.find((device) => device.id === deviceDbId);
if (!lowerDevice) {
// 下游设备不存在时解除关联
const modifiedUpperDevice = cloneDeep(ndmDevice.value);
const modifiedUpperLinkDescription = cloneDeep(upperDeviceLinkDescription.value);
delete modifiedUpperLinkDescription.downstream?.[circuitIndex];
modifiedUpperDevice.linkDescription = JSON.stringify(modifiedUpperLinkDescription);
// 不需要等待异步
const stationCode = station.value.code;
updateDeviceApi(modifiedUpperDevice, { stationCode }).then(() => {
detailDeviceApi(modifiedUpperDevice, { stationCode }).then((upperDevice) => {
if (!upperDevice) return;
deviceStore.patchDevice(stationCode, { ...upperDevice });
});
});
return null;
}
return lowerDevice;
};
// 获取从父组件注入的selectDevice函数
const selectDeviceFn = inject(SELECT_DEVICE_FN_INJECTION_KEY);
// 跳转到下游设备
const navigateToLowerDevice = (circuitIndex: number) => {
const lowerDevice = getLowerDeviceByCircuitIndex(circuitIndex);
if (!lowerDevice) return;
if (!!selectDeviceFn && !!selectDeviceFn.value) {
selectDeviceFn.value(lowerDevice, station.value.code);
}
};
const showModal = ref(false);
const contextmenu = ref<{ x: number; y: number; circuitIndex?: number }>({ x: 0, y: 0 });
const showContextmenu = ref(false);
const contextmenuOptions = computed<DropdownOption[]>(() => [
{
label: '关联设备',
key: 'link-device',
onSelect: () => {
showContextmenu.value = false;
showModal.value = true;
},
},
{
label: '解除关联',
key: 'unlink-device',
onSelect: () => {
showContextmenu.value = false;
const circuitIndex = contextmenu.value.circuitIndex;
if (circuitIndex === undefined) return;
const lowerDevice = getLowerDeviceByCircuitIndex(circuitIndex);
if (!lowerDevice) return;
window.$dialog.warning({
title: '确认解除关联吗?',
content: `将解除【电路${circuitIndex + 1}】与【${lowerDevice.name}】的关联关系。`,
style: { width: '600px' },
contentStyle: { height: '60px' },
negativeText: '取消',
positiveText: '确认',
onNegativeClick: () => {
window.$dialog.destroyAll();
},
onPositiveClick: () => {
window.$dialog.destroyAll();
unlinkDevice({ circuitIndex, lowerDevice });
},
});
},
},
]);
const onSelectDropdownOption = (key: string, option: DropdownOption) => {
const onSelect = option['onSelect'];
if (isFunction(onSelect)) {
onSelect();
}
};
const onContextmenu = (payload: PointerEvent, circuitIndex: number) => {
payload.stopPropagation();
payload.preventDefault();
if (!hasPermission(station.value.code, PERMISSION_TYPE_LITERALS.OPERATION)) return;
const { clientX, clientY } = payload;
contextmenu.value = { x: clientX, y: clientY, circuitIndex };
showContextmenu.value = true;
};
const { mutate: unlinkDevice } = useMutation({
mutationFn: async (params: { circuitIndex: number; lowerDevice: NdmDeviceResultVO }) => {
abortController.value.abort();
abortController.value = new AbortController();
window.$loadingBar.start();
const { circuitIndex, lowerDevice } = params;
if (!upperDeviceLinkDescription.value) return;
// 1.从下游设备的linkDescription的upstream字段中删除当前上游设备
const modifiedLowerDevice = cloneDeep(lowerDevice);
// 解除关联时下游设备的linkDescription一定存在
const modifiedLowerDeviceLinkDescription = destr<LinkDescription>(modifiedLowerDevice.linkDescription);
// upstream字段存在时才进行删除操作
if (modifiedLowerDeviceLinkDescription.upstream) {
const index = modifiedLowerDeviceLinkDescription.upstream.findIndex((deviceStoreIndex) => deviceStoreIndex.deviceDbId === ndmDevice.value.id);
if (index !== -1) {
modifiedLowerDeviceLinkDescription.upstream.splice(index, 1);
}
}
modifiedLowerDevice.linkDescription = JSON.stringify(modifiedLowerDeviceLinkDescription);
// 2. 修改上游设备的linkDescription的downstream字段
const modifiedUpperDevice = cloneDeep(ndmDevice.value);
const modifiedUpperLinkDescription = cloneDeep(upperDeviceLinkDescription.value);
delete modifiedUpperLinkDescription.downstream?.[circuitIndex];
modifiedUpperDevice.linkDescription = JSON.stringify(modifiedUpperLinkDescription);
// 3. 发起update请求并获取最新的设备详情使用本地数据库时直接修改本地数据
if (useLocalDB.value) {
return { upperDevice: modifiedUpperDevice, lowerDevice: modifiedLowerDevice };
}
const stationCode = station.value.code;
const signal = abortController.value.signal;
await updateDeviceApi(modifiedUpperDevice, { stationCode, signal });
await updateDeviceApi(modifiedLowerDevice, { stationCode, signal });
const latestUpperDevice = await detailDeviceApi(modifiedUpperDevice, { stationCode, signal });
const latestLowerDevice = await detailDeviceApi(modifiedLowerDevice, { stationCode, signal });
return { upperDevice: latestUpperDevice, lowerDevice: latestLowerDevice };
},
onSuccess: (data) => {
window.$loadingBar.finish();
window.$message.success('解除成功');
if (!data) return;
const { upperDevice, lowerDevice } = data;
if (!!upperDevice && !!lowerDevice) {
deviceStore.patchDevice(station.value.code, { ...upperDevice });
deviceStore.patchDevice(station.value.code, { ...lowerDevice });
}
},
onError: (error) => {
window.$loadingBar.error();
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
@@ -94,40 +309,46 @@ const { mutate: reboot, isPending: rebooting } = useMutation({
</NFlex>
</template>
<template #default>
<div style="display: grid" :style="{ 'grid-template-columns': `repeat(${Math.min(boxCircuits.length, 4)}, 1fr)` }">
<template v-for="(circuit, index) in boxCircuits" :key="index">
<div style="display: grid" :style="{ 'grid-template-columns': `repeat(${Math.min(localCircuits.length, 4)}, 1fr)` }">
<template v-for="(circuit, circuitIndex) in localCircuits" :key="circuitIndex">
<NPopover :delay="300">
<template #trigger>
<NFlex justify="center" align="center" :size="0">
<div style="display: flex; justify-content: center; align-items: center" @contextmenu="(payload) => onContextmenu(payload, circuitIndex)">
<NFlex vertical class="pointer-cursor circuit" style="padding: 12px" :class="getCircuitStatusClassName(circuit)">
<NFlex align="center">
<NTag class="pointer-cursor" size="small" :type="getCircuitStatusTagType(circuit)">
<template #icon>
<NIcon :component="PoweroffOutlined" />
<NIcon :component="PowerIcon" />
</template>
<template #default>
<span>{{ getCircuitStatusText(circuit) }}</span>
</template>
</NTag>
<span>电路{{ index + 1 }}</span>
<span>电路{{ circuitIndex + 1 }}</span>
</NFlex>
<NFlex justify="end" align="center">
<NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => turnStatus({ circuitIndex: index, newStatus: circuit.status !== 1 })">
<NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => turnStatus({ circuitIndex: circuitIndex, newStatus: circuit.status !== 1 })">
<template #trigger>
<NSwitch size="small" :value="circuit.status === 1" :loading="turning" />
</template>
<template #default>
<span>确定要{{ circuit.status === 1 ? '关闭' : '开启' }}电路{{ index + 1 }}吗?</span>
<span>确定要{{ circuit.status === 1 ? '关闭' : '开启' }}电路{{ circuitIndex + 1 }}吗?</span>
</template>
</NPopconfirm>
</NFlex>
</NFlex>
</NFlex>
</div>
</template>
<template #default>
<NDescriptions bordered size="small" label-placement="left" :column="1">
<NDescriptionsItem label="电压">{{ circuit.voltage }}V</NDescriptionsItem>
<NDescriptionsItem label="电流">{{ circuit.current }}A</NDescriptionsItem>
<NDescriptionsItem label="关联设备">
<span v-if="getLowerDeviceByCircuitIndex(circuitIndex)" style="text-decoration: underline; cursor: pointer" @click="() => navigateToLowerDevice(circuitIndex)">
{{ getLowerDeviceByCircuitIndex(circuitIndex)?.name || '-' }}
</span>
<span v-else>-</span>
</NDescriptionsItem>
</NDescriptions>
</template>
</NPopover>
@@ -135,6 +356,19 @@ const { mutate: reboot, isPending: rebooting } = useMutation({
</div>
</template>
</NCard>
<SecurityBoxCircuitLinkModal v-model:show="showModal" :ndm-device="ndmDevice" :station="station" :circuit-index="contextmenu.circuitIndex" />
<NDropdown
placement="bottom-start"
trigger="manual"
:show="showContextmenu"
:x="contextmenu.x"
:y="contextmenu.y"
:options="contextmenuOptions"
@select="onSelectDropdownOption"
@clickoutside="() => (showContextmenu = false)"
/>
</template>
<style scoped lang="scss">

View File

@@ -0,0 +1,214 @@
<script setup lang="ts">
import { detailDeviceApi, updateDeviceApi, type NdmCameraLinkDescription, type NdmDeviceResultVO, type NdmSecurityBoxLinkDescription, type NdmSecurityBoxResultVO, type Station } from '@/apis';
import { DeviceTree } from '@/components';
import { tryGetDeviceType } from '@/enums';
import { useDeviceStore, useSettingStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import destr from 'destr';
import { cloneDeep } from 'es-toolkit';
import { NButton, NFlex, NModal } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, ref, toRefs } from 'vue';
const props = defineProps<{
ndmDevice: NdmSecurityBoxResultVO;
station: Station;
circuitIndex?: number;
}>();
const show = defineModel<boolean>('show', { default: false });
const deviceStore = useDeviceStore();
const settingStore = useSettingStore();
const { useLocalDB } = storeToRefs(settingStore);
const { ndmDevice, station, circuitIndex } = toRefs(props);
const upperDeviceLinkDescription = computed(() => {
const result = destr<any>(ndmDevice.value.linkDescription);
if (!result) return null;
if (typeof result !== 'object') return null;
return result as NdmSecurityBoxLinkDescription;
});
const lowerDevice = ref<NdmDeviceResultVO>();
const lowerDeviceLinkDescription = computed<NdmCameraLinkDescription | null>(() => {
if (!lowerDevice.value) return null;
const result = destr<any>(lowerDevice.value.linkDescription);
if (!result) return null;
if (typeof result !== 'object') return null;
return result;
});
const onAfterSelectDevice = (device: NdmDeviceResultVO) => {
lowerDevice.value = device;
};
const abortController = ref<AbortController>(new AbortController());
const { mutate: linkPortToDevice, isPending: linking } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
window.$loadingBar.start();
const upperDeviceType = tryGetDeviceType(ndmDevice.value.deviceType);
if (!upperDeviceType) throw new Error('本设备类型未知');
const upperDeviceDbId = ndmDevice.value.id;
if (!upperDeviceDbId) throw new Error('本设备没有ID');
if (circuitIndex.value === undefined) throw new Error('该电路不存在');
if (!lowerDevice.value) throw new Error('请选择要关联的设备');
const lowerDeviceType = tryGetDeviceType(lowerDevice.value?.deviceType);
if (!lowerDeviceType) throw new Error('该设备类型未知');
const lowerDeviceDbId = lowerDevice.value?.id;
if (!lowerDeviceDbId) throw new Error('该设备没有ID');
// 0. 检查是否会导致循环关联,
if (upperDeviceDbId === lowerDeviceDbId) throw new Error('不能关联到自身');
// 以及检查上游设备的linkDescription的downstream字段是否存在某个端口已经关联下游设备
const duplicated = Object.entries(upperDeviceLinkDescription.value?.downstream ?? {}).find(([, deviceStoreIndex]) => {
return deviceStoreIndex.deviceDbId === lowerDeviceDbId;
});
if (duplicated) {
const [portName] = duplicated;
throw new Error(`该设备已关联到端口${portName}`);
}
// 1. 修改上游设备的linkDescription的downstream字段
const modifiedUpperDevice = cloneDeep(ndmDevice.value);
let modifiedUpperDeviceLinkDescription: NdmSecurityBoxLinkDescription;
if (!upperDeviceLinkDescription.value) {
modifiedUpperDeviceLinkDescription = {
downstream: {
[circuitIndex.value]: {
stationCode: station.value.code,
deviceType: lowerDeviceType,
deviceDbId: lowerDeviceDbId,
},
},
};
} else {
modifiedUpperDeviceLinkDescription = {
...upperDeviceLinkDescription.value,
downstream: {
...upperDeviceLinkDescription.value.downstream,
[circuitIndex.value]: {
stationCode: station.value.code,
deviceType: lowerDeviceType,
deviceDbId: lowerDeviceDbId,
},
},
};
}
modifiedUpperDevice.linkDescription = JSON.stringify(modifiedUpperDeviceLinkDescription);
// 2. 修改下游设备的linkDescription的upstream字段
const modifiedLowerDevice = cloneDeep(lowerDevice.value);
let modifiedLowerDeviceLinkDescription: NdmCameraLinkDescription;
if (!lowerDeviceLinkDescription.value) {
modifiedLowerDeviceLinkDescription = {
upstream: [
{
stationCode: station.value.code,
deviceType: upperDeviceType,
deviceDbId: upperDeviceDbId,
},
],
};
} else {
const upstream = cloneDeep(lowerDeviceLinkDescription.value.upstream);
if (!upstream) {
modifiedLowerDeviceLinkDescription = {
...lowerDeviceLinkDescription.value,
upstream: [
{
stationCode: station.value.code,
deviceType: upperDeviceType,
deviceDbId: upperDeviceDbId,
},
],
};
} else {
const deviceStoreIndex = upstream.find((deviceStoreIndex) => deviceStoreIndex.deviceDbId === upperDeviceDbId);
if (!deviceStoreIndex) {
upstream.push({
stationCode: station.value.code,
deviceType: upperDeviceType,
deviceDbId: upperDeviceDbId,
});
}
modifiedLowerDeviceLinkDescription = {
...lowerDeviceLinkDescription.value,
upstream,
};
}
}
modifiedLowerDevice.linkDescription = JSON.stringify(modifiedLowerDeviceLinkDescription);
// 3. 发起update请求并获取最新的设备详情使用本地数据库时直接修改本地数据
if (useLocalDB.value) {
return { upperDevice: modifiedUpperDevice, lowerDevice: modifiedLowerDevice };
}
const stationCode = station.value.code;
const signal = abortController.value.signal;
await updateDeviceApi(modifiedUpperDevice, { stationCode, signal });
await updateDeviceApi(modifiedLowerDevice, { stationCode, signal });
const latestUpperDevice = await detailDeviceApi(modifiedUpperDevice, { stationCode, signal });
const latestLowerDevice = await detailDeviceApi(modifiedLowerDevice, { stationCode, signal });
return { upperDevice: latestUpperDevice, lowerDevice: latestLowerDevice };
},
onSuccess: ({ upperDevice, lowerDevice }) => {
show.value = false;
window.$loadingBar.finish();
window.$message.success('关联成功');
if (!!upperDevice && !!lowerDevice) {
deviceStore.patchDevice(station.value.code, { ...upperDevice });
deviceStore.patchDevice(station.value.code, { ...lowerDevice });
}
},
onError: (error) => {
window.$loadingBar.error();
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const onLink = () => {
linkPortToDevice();
};
const onCancel = () => {
abortController.value.abort();
show.value = false;
};
</script>
<template>
<NModal v-model:show="show" preset="card" style="width: 600px; height: 600px" :content-style="{ height: '100%', overflow: 'hidden' }" @close="onCancel" @esc="onCancel">
<template #header>
<span>{{ ndmDevice.name }}</span>
<span> - </span>
<span>电路{{ circuitIndex ? circuitIndex + 1 : '-' }}</span>
<span> - </span>
<span>关联设备</span>
</template>
<template #default>
<DeviceTree :station="station" :events="['select']" :device-prefix-label="'选择'" @after-select-device="onAfterSelectDevice" />
</template>
<template #action>
<NFlex justify="end">
<NButton size="small" quaternary @click="onCancel">取消</NButton>
<NButton size="small" type="primary" :disabled="!lowerDevice" :loading="linking" @click="onLink">关联</NButton>
</NFlex>
</template>
</NModal>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
import { ThunderboltOutlined } from '@vicons/antd';
import { ApertureOutline, LockOpenOutline, ThermometerOutline, WaterOutline } from '@vicons/ionicons5';
import { DropletIcon, FanIcon, ShieldIcon, ThermometerIcon, ZapIcon } from 'lucide-vue-next';
import { NCard, NFlex, NIcon, NTag } from 'naive-ui';
import { computed, toRefs } from 'vue';
@@ -52,7 +51,7 @@ const formattedFanSpeeds = computed(() => {
<NFlex vertical>
<NTag>
<template #icon>
<NIcon :component="ThermometerOutline" />
<NIcon :component="ThermometerIcon" />
</template>
<template #default>
<span>温度: {{ temperature }}</span>
@@ -60,7 +59,7 @@ const formattedFanSpeeds = computed(() => {
</NTag>
<NTag>
<template #icon>
<NIcon :component="WaterOutline" />
<NIcon :component="DropletIcon" />
</template>
<template #default>
<span>湿度: {{ humidity }}%</span>
@@ -68,7 +67,7 @@ const formattedFanSpeeds = computed(() => {
</NTag>
<NTag>
<template #icon>
<NIcon :component="ApertureOutline" />
<NIcon :component="FanIcon" />
</template>
<template #default>
<span>风扇: {{ formattedFanSpeeds }}</span>
@@ -76,7 +75,7 @@ const formattedFanSpeeds = computed(() => {
</NTag>
<NTag :type="getStatusTagType(accessControlStatus)">
<template #icon>
<NIcon :component="LockOpenOutline" />
<NIcon :component="ShieldIcon" />
</template>
<template #default>
<span>门禁: {{ accessControlStatus }}</span>
@@ -84,7 +83,7 @@ const formattedFanSpeeds = computed(() => {
</NTag>
<NTag :type="getStatusTagType(lightningProtectionStatus)">
<template #icon>
<NIcon :component="ThunderboltOutlined" />
<NIcon :component="ZapIcon" />
</template>
<template #default>
<span>防雷: {{ lightningProtectionStatus }}</span>

View File

@@ -1,16 +1,37 @@
<script setup lang="ts">
import type { NdmSwitchPortInfo } from '@/apis';
import { detailDeviceApi, updateDeviceApi, type LinkDescription, type NdmDeviceResultVO, type NdmSwitchLinkDescription, type NdmSwitchPortInfo, type NdmSwitchResultVO, type Station } from '@/apis';
import { SwitchPortLinkModal } from '@/components';
import { usePermission } from '@/composables';
import { SELECT_DEVICE_FN_INJECTION_KEY } from '@/constants';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { getPortStatusValue, transformPortSpeed } from '@/helpers';
import { NCard, NDescriptions, NDescriptionsItem, NPopover, useThemeVars } from 'naive-ui';
import { computed, toRefs } from 'vue';
import { useDeviceStore, useSettingStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import destr from 'destr';
import { cloneDeep, isFunction } from 'es-toolkit';
import { NCard, NDescriptions, NDescriptionsItem, NDropdown, NPopover, useThemeVars, type DropdownOption } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, inject, onBeforeUnmount, ref, toRefs } from 'vue';
const props = defineProps<{
ndmDevice: NdmSwitchResultVO;
station: Station;
ports?: NdmSwitchPortInfo[];
}>();
const themeVars = useThemeVars();
const { ports } = toRefs(props);
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
const settingStore = useSettingStore();
const { useLocalDB } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station, ports } = toRefs(props);
const showCard = computed(() => !!ports.value);
@@ -58,6 +79,173 @@ const getPortClassName = (port: NdmSwitchPortInfo) => {
}
return 'port-unknown';
};
const upperDeviceLinkDescription = computed(() => {
const result = destr<any>(ndmDevice.value.linkDescription);
if (!result) return null;
if (typeof result !== 'object') return null;
return result as NdmSwitchLinkDescription;
});
const getLowerDeviceByPort = (port: NdmSwitchPortInfo) => {
if (!upperDeviceLinkDescription.value) return null;
const downstream = upperDeviceLinkDescription.value.downstream;
if (!downstream) return null;
const deviceStoreIndex = downstream[port.portName];
if (!deviceStoreIndex) return null;
const { stationCode, deviceType, deviceDbId } = deviceStoreIndex;
const stationDevices = lineDevices.value[stationCode];
if (!stationDevices) return null;
const devices = stationDevices[deviceType];
const lowerDevice = devices.find((device) => device.id === deviceDbId);
if (!lowerDevice) {
// 下游设备不存在时解除关联
const modifiedUpperDevice = cloneDeep(ndmDevice.value);
const modifiedUpperLinkDescription = cloneDeep(upperDeviceLinkDescription.value);
delete modifiedUpperLinkDescription.downstream?.[port.portName];
modifiedUpperDevice.linkDescription = JSON.stringify(modifiedUpperLinkDescription);
// 不需要等待异步
const stationCode = station.value.code;
updateDeviceApi(modifiedUpperDevice, { stationCode }).then(() => {
detailDeviceApi(modifiedUpperDevice, { stationCode }).then((upperDevice) => {
if (!upperDevice) return;
deviceStore.patchDevice(stationCode, { ...upperDevice });
});
});
return null;
}
return lowerDevice;
};
// 获取从父组件注入的selectDevice函数
const selectDeviceFn = inject(SELECT_DEVICE_FN_INJECTION_KEY);
// 跳转到下游设备
const navigateToLowerDevice = (port: NdmSwitchPortInfo) => {
const lowerDevice = getLowerDeviceByPort(port);
if (!lowerDevice) return;
if (!!selectDeviceFn && !!selectDeviceFn.value) {
selectDeviceFn.value(lowerDevice, station.value.code);
}
};
const showModal = ref(false);
const contextmenu = ref<{ x: number; y: number; port?: NdmSwitchPortInfo }>({ x: 0, y: 0 });
const showContextmenu = ref(false);
const contextmenuOptions = computed<DropdownOption[]>(() => [
{
label: '关联设备',
key: 'link-device',
onSelect: () => {
showContextmenu.value = false;
showModal.value = true;
},
},
{
label: '解除关联',
key: 'unlink-device',
onSelect: () => {
showContextmenu.value = false;
const port = contextmenu.value.port;
if (!port) return;
const lowerDevice = getLowerDeviceByPort(port);
if (!lowerDevice) return;
window.$dialog.warning({
title: '确认解除关联吗?',
content: `将解除【${port.portName}】与【${lowerDevice.name}】的关联关系。`,
style: { width: '600px' },
contentStyle: { height: '60px' },
negativeText: '取消',
positiveText: '确认',
onNegativeClick: () => {
window.$dialog.destroyAll();
},
onPositiveClick: () => {
window.$dialog.destroyAll();
unlinkDevice({ port, lowerDevice });
},
});
},
},
]);
const onSelectDropdownOption = (key: string, option: DropdownOption) => {
const onSelect = option['onSelect'];
if (isFunction(onSelect)) {
onSelect();
}
};
const onContextmenu = (payload: PointerEvent, port: NdmSwitchPortInfo) => {
payload.stopPropagation();
payload.preventDefault();
if (!hasPermission(station.value.code, PERMISSION_TYPE_LITERALS.OPERATION)) return;
const { clientX, clientY } = payload;
contextmenu.value = { x: clientX, y: clientY, port };
showContextmenu.value = true;
};
const abortController = ref<AbortController>(new AbortController());
const { mutate: unlinkDevice } = useMutation({
mutationFn: async (params: { port: NdmSwitchPortInfo; lowerDevice: NdmDeviceResultVO }) => {
abortController.value.abort();
abortController.value = new AbortController();
window.$loadingBar.start();
const { port, lowerDevice } = params;
if (!upperDeviceLinkDescription.value) return;
// 1. 从下游设备的linkDescription的upstream字段中删除当前上游设备
const modifiedLowerDevice = cloneDeep(lowerDevice);
// 解除关联时下游设备的linkDescription一定存在
const modifiedLowerDeviceLinkDescription = destr<LinkDescription>(modifiedLowerDevice.linkDescription);
// upstream字段存在时才进行删除操作
if (modifiedLowerDeviceLinkDescription.upstream) {
const index = modifiedLowerDeviceLinkDescription.upstream.findIndex((deviceStoreIndex) => deviceStoreIndex.deviceDbId === ndmDevice.value.id);
if (index !== -1) {
modifiedLowerDeviceLinkDescription.upstream.splice(index, 1);
}
}
modifiedLowerDevice.linkDescription = JSON.stringify(modifiedLowerDeviceLinkDescription);
// 2. 修改上游设备的linkDescription的downstream字段
const modifiedUpperDevice = cloneDeep(ndmDevice.value);
const modifiedUpperLinkDescription = cloneDeep(upperDeviceLinkDescription.value);
delete modifiedUpperLinkDescription.downstream?.[port.portName];
modifiedUpperDevice.linkDescription = JSON.stringify(modifiedUpperLinkDescription);
// 3. 发起update请求并获取最新的设备详情使用本地数据库时直接修改本地数据
if (useLocalDB.value) {
return { upperDevice: modifiedUpperDevice, lowerDevice: modifiedLowerDevice };
}
const stationCode = station.value.code;
const signal = abortController.value.signal;
await updateDeviceApi(modifiedUpperDevice, { stationCode, signal });
await updateDeviceApi(modifiedLowerDevice, { stationCode, signal });
const latestUpperDevice = await detailDeviceApi(modifiedUpperDevice, { stationCode, signal });
const latestLowerDevice = await detailDeviceApi(modifiedLowerDevice, { stationCode, signal });
return { upperDevice: latestUpperDevice, lowerDevice: latestLowerDevice };
},
onSuccess: (data) => {
window.$loadingBar.finish();
window.$message.success('解除成功');
if (!data) return;
const { upperDevice, lowerDevice } = data;
if (!!upperDevice && !!lowerDevice) {
deviceStore.patchDevice(station.value.code, { ...upperDevice });
deviceStore.patchDevice(station.value.code, { ...lowerDevice });
}
},
onError: (error) => {
window.$loadingBar.error();
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
onBeforeUnmount(() => {
abortController.value.abort();
});
</script>
<template>
@@ -77,7 +265,12 @@ const getPortClassName = (port: NdmSwitchPortInfo) => {
<NPopover :delay="300">
<template #trigger>
<!-- 最外层div宽度100% -->
<div class="port" style="height: 40px; box-sizing: border-box; display: flex; cursor: pointer" :class="getPortClassName(port)">
<div
class="port"
style="height: 40px; box-sizing: border-box; display: flex; cursor: pointer"
:class="getPortClassName(port)"
@contextmenu="(payload) => onContextmenu(payload, port)"
>
<!-- 将端口号和状态指示器包裹起来 用于居中布局 -->
<div style="margin: auto; display: flex; flex-direction: column; align-items: center">
<div style="font-size: xx-small">{{ index }}</div>
@@ -89,9 +282,21 @@ const getPortClassName = (port: NdmSwitchPortInfo) => {
<NDescriptions bordered size="small" label-placement="left" :column="1">
<NDescriptionsItem label="端口名称">{{ port.portName }}</NDescriptionsItem>
<NDescriptionsItem label="状态">{{ getPortStatusValue(port) }}</NDescriptionsItem>
<NDescriptionsItem label="状态变更时间">{{ port.lastChangeTime.replace('days', '天') }} </NDescriptionsItem>
<NDescriptionsItem label="上行速率">{{ transformPortSpeed(port, 'in') }}</NDescriptionsItem>
<NDescriptionsItem label="下行速率">{{ transformPortSpeed(port, 'out') }}</NDescriptionsItem>
<NDescriptionsItem label="总速率">{{ transformPortSpeed(port, 'total') }}</NDescriptionsItem>
<NDescriptionsItem v-if="!!port.opticalTemperature && port.opticalTemperature >= 0" label="光模块温度">{{ port.opticalTemperature }} </NDescriptionsItem>
<NDescriptionsItem v-if="!!port.opticalVoltage && port.opticalVoltage >= 0" label="光模块电压">{{ port.opticalVoltage }} mV</NDescriptionsItem>
<NDescriptionsItem v-if="!!port.opticalBiasCurrent && port.opticalBiasCurrent >= 0" label="光模块偏置电流">{{ port.opticalBiasCurrent }} μA</NDescriptionsItem>
<NDescriptionsItem v-if="!!port.opticalReceivePower && port.opticalReceivePower >= 0" label="光模块接收功率">{{ port.opticalReceivePower / 100 }} dBm</NDescriptionsItem>
<NDescriptionsItem v-if="!!port.opticalTransmitPower && port.opticalTransmitPower >= 0" label="光模块发送功率">{{ port.opticalTransmitPower / 100 }} dBm</NDescriptionsItem>
<NDescriptionsItem label="关联设备">
<span v-if="getLowerDeviceByPort(port)" style="text-decoration: underline; cursor: pointer" @click="() => navigateToLowerDevice(port)">
{{ getLowerDeviceByPort(port)?.name || '-' }}
</span>
<span v-else>-</span>
</NDescriptionsItem>
</NDescriptions>
</template>
</NPopover>
@@ -101,6 +306,19 @@ const getPortClassName = (port: NdmSwitchPortInfo) => {
</template>
</template>
</NCard>
<SwitchPortLinkModal v-model:show="showModal" :ndm-device="ndmDevice" :station="station" :port="contextmenu.port" />
<NDropdown
placement="bottom-start"
trigger="manual"
:show="showContextmenu"
:x="contextmenu.x"
:y="contextmenu.y"
:options="contextmenuOptions"
@select="onSelectDropdownOption"
@clickoutside="() => (showContextmenu = false)"
/>
</template>
<style scoped lang="scss">

View File

@@ -0,0 +1,224 @@
<script setup lang="ts">
import {
detailDeviceApi,
updateDeviceApi,
type NdmCameraLinkDescription,
type NdmDeviceResultVO,
type NdmSwitchLinkDescription,
type NdmSwitchPortInfo,
type NdmSwitchResultVO,
type Station,
} from '@/apis';
import { DeviceTree } from '@/components';
import { tryGetDeviceType } from '@/enums';
import { useDeviceStore, useSettingStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import destr from 'destr';
import { cloneDeep } from 'es-toolkit';
import { NButton, NFlex, NModal } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, ref, toRefs } from 'vue';
const props = defineProps<{
ndmDevice: NdmSwitchResultVO;
station: Station;
port?: NdmSwitchPortInfo;
}>();
const show = defineModel<boolean>('show', { default: false });
const deviceStore = useDeviceStore();
const settingStore = useSettingStore();
const { useLocalDB } = storeToRefs(settingStore);
const { ndmDevice, station, port } = toRefs(props);
const upperDeviceLinkDescription = computed(() => {
const result = destr<any>(ndmDevice.value.linkDescription);
if (!result) return null;
if (typeof result !== 'object') return null;
return result as NdmSwitchLinkDescription;
});
const lowerDevice = ref<NdmDeviceResultVO>();
const lowerDeviceLinkDescription = computed<NdmCameraLinkDescription | NdmSwitchLinkDescription | null>(() => {
if (!lowerDevice.value) return null;
const result = destr<any>(lowerDevice.value.linkDescription);
if (!result) return null;
if (typeof result !== 'object') return null;
return result;
});
const onAfterSelectDevice = (device: NdmDeviceResultVO) => {
lowerDevice.value = device;
};
const abortController = ref<AbortController>(new AbortController());
const { mutate: linkPortToDevice, isPending: linking } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
window.$loadingBar.start();
const upperDeviceType = tryGetDeviceType(ndmDevice.value.deviceType);
if (!upperDeviceType) throw new Error('本设备类型未知');
const upperDeviceDbId = ndmDevice.value.id;
if (!upperDeviceDbId) throw new Error('本设备没有ID');
const { portName } = port.value ?? {};
if (!portName) throw new Error('该端口没有名称');
if (!lowerDevice.value) throw new Error('请选择要关联的设备');
const lowerDeviceType = tryGetDeviceType(lowerDevice.value?.deviceType);
if (!lowerDeviceType) throw new Error('该设备类型未知');
const lowerDeviceDbId = lowerDevice.value?.id;
if (!lowerDeviceDbId) throw new Error('该设备没有ID');
// 0. 检查是否会导致循环关联,
if (upperDeviceDbId === lowerDeviceDbId) throw new Error('不能关联到自身');
// 以及检查上游设备的linkDescription的downstream字段是否存在某个端口已经关联下游设备
const duplicated = Object.entries(upperDeviceLinkDescription.value?.downstream ?? {}).find(([, deviceStoreIndex]) => {
return deviceStoreIndex.deviceDbId === lowerDeviceDbId;
});
if (duplicated) {
const [portName] = duplicated;
throw new Error(`该设备已关联到端口${portName}`);
}
// 1. 修改上游设备的linkDescription的downstream字段
const modifiedUpperDevice = cloneDeep(ndmDevice.value);
let modifiedUpperDeviceLinkDescription: NdmSwitchLinkDescription;
if (!upperDeviceLinkDescription.value) {
modifiedUpperDeviceLinkDescription = {
downstream: {
[portName]: {
stationCode: station.value.code,
deviceType: lowerDeviceType,
deviceDbId: lowerDeviceDbId,
},
},
};
} else {
modifiedUpperDeviceLinkDescription = {
...upperDeviceLinkDescription.value,
downstream: {
...upperDeviceLinkDescription.value.downstream,
[portName]: {
stationCode: station.value.code,
deviceType: lowerDeviceType,
deviceDbId: lowerDeviceDbId,
},
},
};
}
modifiedUpperDevice.linkDescription = JSON.stringify(modifiedUpperDeviceLinkDescription);
// 2. 修改下游设备的linkDescription的upstream字段
const modifiedLowerDevice = cloneDeep(lowerDevice.value);
let modifiedLowerDeviceLinkDescription: NdmSwitchLinkDescription | NdmCameraLinkDescription;
if (!lowerDeviceLinkDescription.value) {
modifiedLowerDeviceLinkDescription = {
upstream: [
{
stationCode: station.value.code,
deviceType: upperDeviceType,
deviceDbId: upperDeviceDbId,
},
],
};
} else {
const upstream = cloneDeep(lowerDeviceLinkDescription.value.upstream);
if (!upstream) {
modifiedLowerDeviceLinkDescription = {
...lowerDeviceLinkDescription.value,
upstream: [
{
stationCode: station.value.code,
deviceType: upperDeviceType,
deviceDbId: upperDeviceDbId,
},
],
};
} else {
const deviceStoreIndex = upstream.find((deviceStoreIndex) => deviceStoreIndex.deviceDbId === upperDeviceDbId);
if (!deviceStoreIndex) {
upstream.push({
stationCode: station.value.code,
deviceType: upperDeviceType,
deviceDbId: upperDeviceDbId,
});
}
modifiedLowerDeviceLinkDescription = {
...lowerDeviceLinkDescription.value,
upstream,
};
}
}
modifiedLowerDevice.linkDescription = JSON.stringify(modifiedLowerDeviceLinkDescription);
// 3. 发起update请求并获取最新的设备详情使用本地数据库时直接修改本地数据
if (useLocalDB.value) {
return { upperDevice: modifiedUpperDevice, lowerDevice: modifiedLowerDevice };
}
const stationCode = station.value.code;
const signal = abortController.value.signal;
await updateDeviceApi(modifiedUpperDevice, { stationCode, signal });
await updateDeviceApi(modifiedLowerDevice, { stationCode, signal });
const latestUpperDevice = await detailDeviceApi(modifiedUpperDevice, { stationCode, signal });
const latestLowerDevice = await detailDeviceApi(modifiedLowerDevice, { stationCode, signal });
return { upperDevice: latestUpperDevice, lowerDevice: latestLowerDevice };
},
onSuccess: ({ upperDevice, lowerDevice }) => {
show.value = false;
window.$loadingBar.finish();
window.$message.success('关联成功');
if (!!upperDevice && !!lowerDevice) {
deviceStore.patchDevice(station.value.code, { ...upperDevice });
deviceStore.patchDevice(station.value.code, { ...lowerDevice });
}
},
onError: (error) => {
window.$loadingBar.error();
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const onLink = () => {
linkPortToDevice();
};
const onCancel = () => {
abortController.value.abort();
show.value = false;
};
</script>
<template>
<NModal v-model:show="show" preset="card" style="width: 600px; height: 600px" :content-style="{ height: '100%', overflow: 'hidden' }" @close="onCancel" @esc="onCancel">
<template #header>
<span>{{ ndmDevice.name }}</span>
<span> - </span>
<span>{{ port?.portName }}</span>
<span> - </span>
<span>关联设备</span>
</template>
<template #default>
<DeviceTree :station="station" :events="['select']" :device-prefix-label="'选择'" @after-select-device="onAfterSelectDevice" />
</template>
<template #action>
<NFlex justify="end">
<NButton size="small" quaternary @click="onCancel">取消</NButton>
<NButton size="small" type="primary" :disabled="!lowerDevice" :loading="linking" @click="onLink">关联</NButton>
</NFlex>
</template>
</NModal>
</template>
<style scoped lang="ts"></style>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { NdmAlarmHostResultVO, Station } from '@/apis';
import { AlarmHostCurrentDiag, AlarmHostHistoryDiag, AlarmHostUpdate, DeviceRawCard } from '@/components';
import { usePermission } from '@/composables';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useSettingStore } from '@/stores';
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
import { storeToRefs } from 'pinia';
@@ -16,7 +18,9 @@ const route = useRoute();
const router = useRouter();
const settingStore = useSettingStore();
const { debugModeEnabled } = storeToRefs(settingStore);
const { showDeviceRawData } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station } = toRefs(props);
@@ -31,8 +35,8 @@ const activeTabName = ref('当前诊断');
const onTabChange = (name: string) => {
activeTabName.value = name;
};
watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || !enabled) {
watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
activeTabName.value = '当前诊断';
}
});
@@ -45,8 +49,8 @@ watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
<NTabs :value="activeTabName" @update:value="onTabChange">
<NTab name="当前诊断">当前诊断</NTab>
<NTab name="历史诊断">历史诊断</NTab>
<NTab name="修改设备">修改设备</NTab>
<NTab v-if="debugModeEnabled" name="原始数据">原始数据</NTab>
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
</NTabs>
</template>
<template #default>

View File

@@ -139,9 +139,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { NdmCameraResultVO, Station } from '@/apis';
import { CameraCurrentDiag, CameraHistoryDiag, CameraUpdate, DeviceRawCard } from '@/components';
import { usePermission } from '@/composables';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useSettingStore } from '@/stores';
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
import { storeToRefs } from 'pinia';
@@ -16,7 +18,9 @@ const route = useRoute();
const router = useRouter();
const settingStore = useSettingStore();
const { debugModeEnabled } = storeToRefs(settingStore);
const { showDeviceRawData } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station } = toRefs(props);
@@ -31,8 +35,8 @@ const activeTabName = ref('当前诊断');
const onTabChange = (name: string) => {
activeTabName.value = name;
};
watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || !enabled) {
watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
activeTabName.value = '当前诊断';
}
});
@@ -45,8 +49,8 @@ watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
<NTabs :value="activeTabName" @update:value="onTabChange">
<NTab name="当前诊断">当前诊断</NTab>
<NTab name="历史诊断">历史诊断</NTab>
<NTab name="修改设备">修改设备</NTab>
<NTab v-if="debugModeEnabled" name="原始数据">原始数据</NTab>
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
</NTabs>
</template>
<template #default>

View File

@@ -1,16 +1,118 @@
<script lang="ts">
const CAMERA_TYPES = {
'001': '模拟彩色云台摄像机',
'002': '模拟彩色半球摄像机',
'003': '模拟彩色固定摄像机',
'004': '数字高清云台摄像机',
'005': '数字高清半球摄像机',
'006': '数字高清固定摄像机',
} as const;
type CameraType = keyof typeof CAMERA_TYPES;
const isCameraTypeCode = (code: string): code is CameraType => {
return code in CAMERA_TYPES;
};
</script>
<script setup lang="ts">
import type { NdmCameraResultVO, Station } from '@/apis';
import { DeviceCommonCard, DeviceHeaderCard } from '@/components';
import { NFlex } from 'naive-ui';
import { computed, toRefs } from 'vue';
import { useSettingStore } from '@/stores';
import { useQuery, useQueryClient } from '@tanstack/vue-query';
import axios from 'axios';
import { NDescriptions, NDescriptionsItem, NFlex } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmCameraResultVO;
station: Station;
}>();
const settingStore = useSettingStore();
const { activeRequests } = storeToRefs(settingStore);
const queryClient = useQueryClient();
const { ndmDevice, station } = toRefs(props);
const cameraType = computed(() => {
const gbCode = ndmDevice.value.gbCode;
if (!gbCode) return '-';
const cameraTypeCode = gbCode.slice(11, 14);
if (!isCameraTypeCode(cameraTypeCode)) return '-';
return CAMERA_TYPES[cameraTypeCode];
});
const QUERY_KEY = 'camera-installation-area-query';
const { data: installationArea } = useQuery({
queryKey: computed(() => [QUERY_KEY, ndmDevice.value.gbCode, station.value.code]),
enabled: computed(() => activeRequests.value),
gcTime: 0,
queryFn: async ({ signal }) => {
const UNKNOWN_NAME = '-';
const gbCode = ndmDevice.value.gbCode;
if (!gbCode) return UNKNOWN_NAME;
const MINIO_PREFIX = `/minio`;
const CDN_VIMP_CODES_PREFIX = `${MINIO_PREFIX}/cdn/vimp/codes`;
const CODE_STATIONS_JSON_PATH = `${CDN_VIMP_CODES_PREFIX}/codeStations.json`;
const CODE_STATION_AREAS_JSON_PATH = `${CDN_VIMP_CODES_PREFIX}/codeStationAreas.json`;
const CODE_PARKING_AREAS_JSON_PATH = `${CDN_VIMP_CODES_PREFIX}/codeParkingAreas.json`;
const CODE_OCC_AREAS_JSON_PATH = `${CDN_VIMP_CODES_PREFIX}/codeOccAreas.json`;
// minio中的编码表结构
type Unit = { name: string; type: 'train' | 'station' | 'parking' | 'occ' };
type Area = { code: string; name: string; subs: Array<{ code: string; name: string }> };
const { data: unitCodes } = await axios.get<Record<string, Unit>>(CODE_STATIONS_JSON_PATH, { signal });
// 根据国标编码的前6位匹配minio中的编码表
const unitCode = gbCode.slice(0, 6);
const unit = unitCodes[unitCode];
if (!unit) return UNKNOWN_NAME;
// 获取编码表中的线路/单位类型
const unitType = unit.type;
// 国标编码的第7位到第8位为1级区域编码
const tier1AreaCode = gbCode.slice(6, 8);
// 国标编码的第9位到第11位为2级区域编码
const tier2AreaCode = gbCode.slice(8, 11);
if (unitType === 'train') {
return unit.name;
}
const areaJsonPaths: Record<string, string> = {
station: CODE_STATION_AREAS_JSON_PATH,
parking: CODE_PARKING_AREAS_JSON_PATH,
occ: CODE_OCC_AREAS_JSON_PATH,
};
const jsonPath = areaJsonPaths[unitType];
if (!jsonPath) return UNKNOWN_NAME;
// 获取1级区域
const { data: areaCodes } = await axios.get<Area[]>(jsonPath, { signal });
const tier1Area = areaCodes.find((area) => area.code === tier1AreaCode);
if (!tier1Area) return UNKNOWN_NAME;
// 获取2级区域
const tier2Area = tier1Area.subs.find((area) => area.code === `${tier1AreaCode}${tier2AreaCode}`);
if (!tier2Area) return UNKNOWN_NAME;
// 拼接1级和2级区域名称
return `${tier1Area.name}-${tier2Area.name}`;
},
});
watch(activeRequests, (active) => {
if (!active) {
queryClient.cancelQueries({ queryKey: [QUERY_KEY] });
}
});
const commonInfo = computed(() => {
const {
createdTime,
@@ -44,7 +146,14 @@ const commonInfo = computed(() => {
<template>
<NFlex vertical>
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station">
<template #append-info>
<NDescriptions bordered size="small" label-placement="left" :columns="1" style="width: 60%; min-width: 400px">
<NDescriptionsItem label="摄像机类型" :span="1">{{ cameraType }}</NDescriptionsItem>
<NDescriptionsItem label="建议安装区域" :span="1">{{ installationArea ?? '-' }}</NDescriptionsItem>
</NDescriptions>
</template>
</DeviceHeaderCard>
<DeviceCommonCard :common-info="commonInfo" />
</NFlex>
</template>

View File

@@ -120,9 +120,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { NdmDecoderResultVO, Station } from '@/apis';
import { DecoderCurrentDiag, DecoderHistoryDiag, DecoderUpdate, DeviceRawCard } from '@/components';
import { usePermission } from '@/composables';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useSettingStore } from '@/stores';
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
import { storeToRefs } from 'pinia';
@@ -16,7 +18,9 @@ const route = useRoute();
const router = useRouter();
const settingStore = useSettingStore();
const { debugModeEnabled } = storeToRefs(settingStore);
const { showDeviceRawData } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station } = toRefs(props);
@@ -31,8 +35,8 @@ const activeTabName = ref('当前诊断');
const onTabChange = (name: string) => {
activeTabName.value = name;
};
watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || !enabled) {
watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
activeTabName.value = '当前诊断';
}
});
@@ -45,8 +49,8 @@ watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
<NTabs :value="activeTabName" @update:value="onTabChange">
<NTab name="当前诊断">当前诊断</NTab>
<NTab name="历史诊断">历史诊断</NTab>
<NTab name="修改设备">修改设备</NTab>
<NTab v-if="debugModeEnabled" name="原始数据">原始数据</NTab>
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
</NTabs>
</template>
<template #default>

View File

@@ -141,9 +141,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { NdmKeyboardResultVO, Station } from '@/apis';
import { DeviceRawCard, KeyboardCurrentDiag, KeyboardHistoryDiag, KeyboardUpdate } from '@/components';
import { usePermission } from '@/composables';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useSettingStore } from '@/stores';
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
import { storeToRefs } from 'pinia';
@@ -16,7 +18,9 @@ const route = useRoute();
const router = useRouter();
const settingStore = useSettingStore();
const { debugModeEnabled } = storeToRefs(settingStore);
const { showDeviceRawData } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station } = toRefs(props);
@@ -31,8 +35,8 @@ const activeTabName = ref('当前诊断');
const onTabChange = (name: string) => {
activeTabName.value = name;
};
watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || !enabled) {
watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
activeTabName.value = '当前诊断';
}
});
@@ -45,8 +49,8 @@ watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
<NTabs :value="activeTabName" @update:value="onTabChange">
<NTab name="当前诊断">当前诊断</NTab>
<NTab name="历史诊断">历史诊断</NTab>
<NTab name="修改设备">修改设备</NTab>
<NTab v-if="debugModeEnabled" name="原始数据">原始数据</NTab>
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
</NTabs>
</template>
<template #default>

View File

@@ -129,9 +129,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { NdmNvrResultVO, Station } from '@/apis';
import { DeviceRawCard, NvrCurrentDiag, NvrHistoryDiag, NvrUpdate } from '@/components';
import { usePermission } from '@/composables';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useSettingStore } from '@/stores';
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
import { storeToRefs } from 'pinia';
@@ -16,7 +18,9 @@ const route = useRoute();
const router = useRouter();
const settingStore = useSettingStore();
const { debugModeEnabled } = storeToRefs(settingStore);
const { showDeviceRawData } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station } = toRefs(props);
@@ -31,8 +35,8 @@ const activeTabName = ref('当前诊断');
const onTabChange = (name: string) => {
activeTabName.value = name;
};
watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || !enabled) {
watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
activeTabName.value = '当前诊断';
}
});
@@ -45,8 +49,8 @@ watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
<NTabs :value="activeTabName" @update:value="onTabChange">
<NTab name="当前诊断">当前诊断</NTab>
<NTab name="历史诊断">历史诊断</NTab>
<NTab name="修改设备">修改设备</NTab>
<NTab v-if="debugModeEnabled" name="原始数据">原始数据</NTab>
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
</NTabs>
</template>
<template #default>

View File

@@ -141,9 +141,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { NdmSecurityBoxResultVO, Station } from '@/apis';
import { DeviceRawCard, SecurityBoxCurrentDiag, SecurityBoxHistoryDiag, SecurityBoxUpdate } from '@/components';
import { usePermission } from '@/composables';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useSettingStore } from '@/stores';
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
import { storeToRefs } from 'pinia';
@@ -16,7 +18,9 @@ const route = useRoute();
const router = useRouter();
const settingStore = useSettingStore();
const { debugModeEnabled } = storeToRefs(settingStore);
const { showDeviceRawData } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station } = toRefs(props);
@@ -31,8 +35,8 @@ const activeTabName = ref('当前诊断');
const onTabChange = (name: string) => {
activeTabName.value = name;
};
watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || !enabled) {
watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
activeTabName.value = '当前诊断';
}
});
@@ -45,8 +49,8 @@ watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
<NTabs :value="activeTabName" @update:value="onTabChange">
<NTab name="当前诊断">当前诊断</NTab>
<NTab name="历史诊断">历史诊断</NTab>
<NTab name="修改设备">修改设备</NTab>
<NTab v-if="debugModeEnabled" name="原始数据">原始数据</NTab>
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
</NTabs>
</template>
<template #default>

View File

@@ -49,7 +49,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 :circuits="circuits" :ndm-device="ndmDevice" :station="station" />
<SecurityBoxCircuitCard :ndm-device="ndmDevice" :station="station" :circuits="circuits" />
</NFlex>
</template>

View File

@@ -132,9 +132,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -2,6 +2,7 @@ import ServerAlive from './server-alive.vue';
import ServerCard from './server-card.vue';
import ServerCurrentDiag from './server-current-diag.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 };
export { ServerAlive, ServerCard, ServerCurrentDiag, ServerHistoryDiag, ServerUpdate, ServerStreamPush };

View File

@@ -2,10 +2,10 @@
import { isMediaServerAliveApi, isSipServerAliveApi, type NdmServerResultVO, type Station } from '@/apis';
import { DEVICE_TYPE_LITERALS, tryGetDeviceType } from '@/enums';
import { useSettingStore } from '@/stores';
import { useQuery } from '@tanstack/vue-query';
import { useQuery, useQueryClient } from '@tanstack/vue-query';
import { NCard, NTag } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, toRefs } from 'vue';
import { computed, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmServerResultVO;
@@ -13,28 +13,40 @@ const props = defineProps<{
}>();
const settingStore = useSettingStore();
const { offlineDev } = storeToRefs(settingStore);
const { activeRequests } = storeToRefs(settingStore);
const queryClient = useQueryClient();
const { ndmDevice, station } = toRefs(props);
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 { data: isMediaServerAlive } = useQuery({
queryKey: computed(() => ['media-server-alive-query', ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
enabled: computed(() => !offlineDev.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmMediaServer),
queryKey: computed(() => [MEDIA_SERVER_ALIVE_QUERY_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmMediaServer),
refetchInterval: 30 * 1000,
gcTime: 0,
queryFn: async ({ signal }) => {
const alives = await isMediaServerAliveApi({ stationCode: station.value.code, signal });
return alives.find((alive) => alive.ip === ndmDevice.value.ipAddress);
},
refetchInterval: 30 * 1000,
});
const { data: isSipServerAlive } = useQuery({
queryKey: computed(() => ['video-server-alive-query', ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
enabled: computed(() => !offlineDev.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmVideoServer),
queryKey: computed(() => [VIDEO_SERVER_ALIVE_QUERY_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmVideoServer),
refetchInterval: 30 * 1000,
gcTime: 0,
queryFn: async ({ signal }) => {
return await isSipServerAliveApi({ stationCode: station.value.code, signal });
},
refetchInterval: 30 * 1000,
});
watch(activeRequests, (active) => {
if (!active) {
queryClient.cancelQueries({ queryKey: [MEDIA_SERVER_ALIVE_QUERY_KEY] });
queryClient.cancelQueries({ queryKey: [VIDEO_SERVER_ALIVE_QUERY_KEY] });
}
});
</script>
@@ -44,20 +56,29 @@ const { data: isSipServerAlive } = useQuery({
<span>服务状态</span>
</template>
<template #default>
<div v-if="deviceType === DEVICE_TYPE_LITERALS.ndmMediaServer">
<span>流媒体服务状态</span>
<template v-if="isMediaServerAlive">
<NTag size="small" :type="isMediaServerAlive.online ? 'success' : 'error'">{{ isMediaServerAlive.online ? '在线' : '离线' }}</NTag>
<template v-if="!activeRequests">
<span>-</span>
</template>
<template v-else>
<template v-if="deviceType === DEVICE_TYPE_LITERALS.ndmMediaServer">
<span>流媒体服务状态</span>
<template v-if="!!isMediaServerAlive">
<NTag size="small" :type="isMediaServerAlive.online ? 'success' : 'error'">{{ isMediaServerAlive.online ? '在线' : '离线' }}</NTag>
</template>
<template v-else>
<span>-</span>
</template>
</template>
<span v-else>-</span>
</div>
<div v-if="deviceType === DEVICE_TYPE_LITERALS.ndmVideoServer">
<span>信令服务状态</span>
<template v-if="isSipServerAlive">
<NTag size="small" :type="isSipServerAlive ? 'success' : 'error'">{{ isSipServerAlive ? '在线' : '离线' }}</NTag>
<template v-if="deviceType === DEVICE_TYPE_LITERALS.ndmVideoServer">
<span>信令服务状态</span>
<template v-if="isSipServerAlive !== undefined">
<NTag size="small" :type="isSipServerAlive ? 'success' : 'error'">{{ isSipServerAlive ? '在线' : '离线' }}</NTag>
</template>
<template v-else>
<span>-</span>
</template>
</template>
<span v-else>-</span>
</div>
</template>
</template>
</NCard>
</template>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { NdmServerResultVO, Station } from '@/apis';
import { DeviceRawCard, ServerCurrentDiag, ServerHistoryDiag, ServerUpdate } from '@/components';
import { usePermission } from '@/composables';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useSettingStore } from '@/stores';
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
import { storeToRefs } from 'pinia';
@@ -16,7 +18,9 @@ const route = useRoute();
const router = useRouter();
const settingStore = useSettingStore();
const { debugModeEnabled } = storeToRefs(settingStore);
const { showDeviceRawData } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station } = toRefs(props);
@@ -31,8 +35,8 @@ const activeTabName = ref('当前诊断');
const onTabChange = (name: string) => {
activeTabName.value = name;
};
watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || !enabled) {
watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
activeTabName.value = '当前诊断';
}
});
@@ -45,8 +49,8 @@ watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
<NTabs :value="activeTabName" @update:value="onTabChange">
<NTab name="当前诊断">当前诊断</NTab>
<NTab name="历史诊断">历史诊断</NTab>
<NTab name="修改设备">修改设备</NTab>
<NTab v-if="debugModeEnabled" name="原始数据">原始数据</NTab>
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
</NTabs>
</template>
<template #default>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { type NdmServerDiagInfo, type NdmServerResultVO, type Station } from '@/apis';
import { DeviceHardwareCard, DeviceHeaderCard, ServerAlive } from '@/components';
import { DeviceHardwareCard, DeviceHeaderCard, ServerAlive, ServerStreamPush } from '@/components';
import destr from 'destr';
import { NFlex } from 'naive-ui';
import { computed, toRefs } from 'vue';
@@ -28,8 +28,9 @@ const runningTime = computed(() => lastDiagInfo.value?.commInfo?.系统运行时
<template>
<NFlex vertical>
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
<DeviceHardwareCard :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" />
<ServerStreamPush :ndm-device="ndmDevice" :station="station" />
</NFlex>
</template>

View File

@@ -0,0 +1,100 @@
<script setup lang="ts">
import { getAllPushApi, 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 { NCard, NCollapse, NCollapseItem, NFlex, NTag, NText } 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 showCard = computed(() => deviceType.value === DEVICE_TYPE_LITERALS.ndmMediaServer);
const SERVER_STREAM_PUSH_KEY = 'server-stream-push-query';
const { data: streamPushes } = useQuery({
queryKey: computed(() => [SERVER_STREAM_PUSH_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && showCard.value),
refetchInterval: 30 * 1000,
gcTime: 0,
queryFn: async ({ signal }) => {
const streamPushes = await getAllPushApi({ stationCode: station.value.code, signal });
return streamPushes;
},
});
watch(activeRequests, (active) => {
if (!active) {
queryClient.cancelQueries({ queryKey: [SERVER_STREAM_PUSH_KEY] });
}
});
interface StreamPushStat {
ip: string;
port: number | null;
ssrc: string | null;
channelIds: string[];
}
const streamPushStat = computed(() => {
const stat: StreamPushStat[] = [];
streamPushes.value?.forEach((push) => {
if (!push.ip || !push.channelId) return;
const existIndex = stat.findIndex((item) => item.ip === push.ip);
if (existIndex === -1) {
stat.push({ ip: push.ip, port: push.port, ssrc: push.ssrc, channelIds: [push.channelId] });
} else {
const statItem = stat[existIndex];
if (!statItem) return;
statItem.channelIds.push(push.channelId);
}
});
return stat;
});
</script>
<template>
<NCard v-if="showCard" hoverable size="small">
<template #header>
<span>推流统计</span>
</template>
<template #default>
<template v-if="!activeRequests">
<span>-</span>
</template>
<template v-else>
<NFlex vertical>
<NText depth="3"> {{ streamPushStat.length }} 个推流目标</NText>
<NCollapse v-if="streamPushStat.length > 0">
<NCollapseItem v-for="{ ip, port, ssrc, channelIds } in streamPushStat" :key="`${ip}:${port}:${ssrc}`" :name="`${ip}:${port}:${ssrc}`">
<template #header>
<span>{{ ip }}</span>
</template>
<template #header-extra>
<span>{{ channelIds.length }} </span>
</template>
<template #default>
<NFlex>
<NTag v-for="channelId in channelIds" :key="channelId" size="small" type="info" :bordered="false">{{ channelId }}</NTag>
</NFlex>
</template>
</NCollapseItem>
</NCollapse>
</NFlex>
</template>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,16 +1,5 @@
<script setup lang="ts">
import {
detailMediaServerApi,
detailVideoServerApi,
icmpEntityByDeviceId,
updateMediaServerApi,
updateVideoServerApi,
type NdmMediaServerUpdateVO,
type NdmServerResultVO,
type NdmServerUpdateVO,
type NdmVideoServerUpdateVO,
type Station,
} from '@/apis';
import { detailMediaServerApi, detailVideoServerApi, icmpEntityByDeviceId, updateMediaServerApi, updateVideoServerApi, type NdmServerResultVO, type NdmServerUpdateVO, type Station } from '@/apis';
import { DEVICE_TYPE_LITERALS, tryGetDeviceType } from '@/enums';
import { useDeviceStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
@@ -93,10 +82,10 @@ const { mutate: updateDevice, isPending } = useMutation({
const stationCode = station.value.code;
const signal = abortController.value.signal;
if (deviceType === DEVICE_TYPE_LITERALS.ndmMediaServer) {
await updateMediaServerApi(localDevice.value as NdmMediaServerUpdateVO, { stationCode, signal });
await updateMediaServerApi(localDevice.value, { stationCode, signal });
return await detailMediaServerApi(`${localDevice.value.id}`, { stationCode, signal });
} else if (deviceType === DEVICE_TYPE_LITERALS.ndmVideoServer) {
await updateVideoServerApi(`${localDevice.value.id}`, localDevice.value as NdmVideoServerUpdateVO, { stationCode, signal });
await updateVideoServerApi(localDevice.value, { stationCode, signal });
return await detailVideoServerApi(`${localDevice.value.id}`, { stationCode, signal });
} else {
throw new Error('不是服务器设备');
@@ -151,9 +140,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { NdmSwitchResultVO, Station } from '@/apis';
import { DeviceRawCard, SwitchCurrentDiag, SwitchHistoryDiag, SwitchUpdate } from '@/components';
import { usePermission } from '@/composables';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useSettingStore } from '@/stores';
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
import { storeToRefs } from 'pinia';
@@ -16,7 +18,9 @@ const route = useRoute();
const router = useRouter();
const settingStore = useSettingStore();
const { debugModeEnabled } = storeToRefs(settingStore);
const { showDeviceRawData } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station } = toRefs(props);
@@ -31,8 +35,8 @@ const activeTabName = ref('当前诊断');
const onTabChange = (name: string) => {
activeTabName.value = name;
};
watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || !enabled) {
watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
activeTabName.value = '当前诊断';
}
});
@@ -45,8 +49,8 @@ watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
<NTabs :value="activeTabName" @update:value="onTabChange">
<NTab name="当前诊断">当前诊断</NTab>
<NTab name="历史诊断">历史诊断</NTab>
<NTab name="修改设备">修改设备</NTab>
<NTab v-if="debugModeEnabled" name="原始数据">原始数据</NTab>
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
</NTabs>
</template>
<template #default>

View File

@@ -29,7 +29,7 @@ const ports = computed(() => lastDiagInfo.value?.info?.portInfoList);
<NFlex vertical>
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
<SwitchPortCard :ports="ports" />
<SwitchPortCard :ndm-device="ndmDevice" :station="station" :ports="ports" />
</NFlex>
</template>

View File

@@ -141,9 +141,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -1,10 +1,10 @@
<script setup lang="ts">
import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis';
import { useDeviceTree } from '@/composables';
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType } from '@/enums';
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 { useDeviceStore, useStationStore } from '@/stores';
import { watchImmediate } from '@vueuse/core';
import { useDeviceStore, usePermissionStore } from '@/stores';
import { watchDebounced, watchImmediate } from '@vueuse/core';
import destr from 'destr';
import { isFunction } from 'es-toolkit';
import {
@@ -27,27 +27,49 @@ import {
type TreeProps,
} from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, h, nextTick, onBeforeUnmount, ref, toRefs, useTemplateRef, watch, type CSSProperties } from 'vue';
import { computed, h, nextTick, onBeforeUnmount, onMounted, ref, toRefs, useTemplateRef, watch, type CSSProperties } from 'vue';
const props = defineProps<{
station?: Station; // 支持渲染指定车站的设备树
/**
* 支持渲染指定车站的设备树
*/
station?: Station;
/**
* 允许的事件类型
*
* - `select`:允许选择设备
* - `manage`:允许右键菜单管理设备
*/
events?: ('select' | 'manage')[];
/**
* 是否同步路由参数
*/
syncRoute?: boolean;
/**
* 设备节点的前缀按钮文字
*/
devicePrefixLabel?: string;
}>();
const emit = defineEmits<{
selectDevice: [device: NdmDeviceResultVO, stationCode: Station['code']];
afterSelectDevice: [device: NdmDeviceResultVO, stationCode: Station['code']];
exposeSelectDeviceFn: [selectDeviceFn: UseDeviceTreeReturn['selectDevice']];
}>();
const { station } = toRefs(props);
const { station, events, syncRoute, devicePrefixLabel } = toRefs(props);
const themeVars = useThemeVars();
const { hasPermission } = usePermission();
const {
// 设备选择
selectedStationCode,
selectedDeviceType,
selectedDevice,
syncFromRoute,
syncToRoute,
selectDevice,
routeDevice,
// 设备管理
exportDevice,
exportDeviceTemplate,
@@ -55,18 +77,21 @@ const {
deleteDevice,
} = useDeviceTree();
// 将 `selectDevice` 函数暴露给父组件
emit('exposeSelectDeviceFn', selectDevice);
const onSelectDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
// 仅当事件列表包含 `select` 时才触发选择事件
if (!events.value) return;
if (!events.value.includes('select')) return;
selectDevice(device, stationCode);
emit('selectDevice', device, stationCode);
emit('afterSelectDevice', device, stationCode);
};
const onRouteDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
routeDevice(device, stationCode, { path: '/device' });
emit('selectDevice', device, stationCode);
};
const permissionStore = usePermissionStore();
const stations = computed(() => permissionStore.stations.VIEW ?? []);
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
@@ -187,24 +212,28 @@ const nodeProps: TreeProps['nodeProps'] = ({ option }) => {
onDblclick: (payload) => {
if (option['device']) {
payload.stopPropagation();
const device = option['device'] as NdmDeviceResultVO;
const stationCode = option['stationCode'] as Station['code'];
// 区分是否需要跳转路由
// 当 props.station 存在时,说明当前是单独渲染车站的设备树,需要跳转路由到设备诊断页面
if (!station.value) {
onSelectDevice(device, stationCode);
} else {
onRouteDevice(device, stationCode);
}
onSelectDevice(device, stationCode);
}
},
onContextmenu: (payload) => {
payload.stopPropagation();
payload.preventDefault();
const { clientX, clientY } = payload;
// 如果事件列表不包含 `manage`,则直接结束逻辑
if (!events.value?.includes('manage')) return;
const stationCode = option['stationCode'] as Station['code'];
// 仅当用户在该车站拥有操作权限时才显示右键菜单
if (!hasPermission(stationCode, PERMISSION_TYPE_LITERALS.OPERATION)) return;
const deviceType = option['deviceType'] as DeviceType | undefined;
const device = option['device'] as NdmDeviceResultVO | undefined;
const { clientX, clientY } = payload;
contextmenu.value = { x: clientX, y: clientY, stationCode, deviceType, device };
showContextmenu.value = true;
},
@@ -230,7 +259,8 @@ const renderIcmpStatistics = (onlineCount: number, offlineCount: number, count:
]);
};
const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
const renderViewDeviceButton = (device: NdmDeviceResultVO, stationCode: string) => {
const renderViewDeviceButton = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
if (!devicePrefixLabel.value) return null;
return h(
NButton,
{
@@ -242,17 +272,11 @@ const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: Station[
} as CSSProperties,
onClick: (e: MouseEvent) => {
e.stopPropagation();
// 选择设备
// 区分是否需要跳转路由
// 当 props.station 存在时,说明当前是单独渲染车站的设备树,需要跳转路由到设备诊断页面
if (!station.value) {
onSelectDevice(device, stationCode);
} else {
onRouteDevice(device, stationCode);
}
onSelectDevice(device, stationCode);
},
},
() => '查看',
() => devicePrefixLabel.value,
);
};
const renderDeviceStatusTag = (device: NdmDeviceResultVO) => {
@@ -439,7 +463,7 @@ const onLocateDeviceTree = async () => {
activeTab.value = deviceType;
// 展开选择的车站
expandedKeys.value = [selectedStationCode.value];
expandedKeys.value.push(selectedStationCode.value);
// 当选择录像机时,如果不是集群,进一步展开该录像机所在的集群节点
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
@@ -460,12 +484,37 @@ const onLocateDeviceTree = async () => {
animated.value = true;
};
// 渲染全线设备树时,若是首次选择设备,则定位设备树
const unwatch = watch(selectedDevice, async (newDevice, oldDevice) => {
// 当选择的设备发生变化时,定位设备树,并同步选中状态到路由参数
// 暂时不考虑多次执行的问题,因为当选择的设备在设备树视口内时,不会发生滚动
watch(selectedDevice, async (newDevice, oldDevice) => {
if (!!station.value) return;
if (!!newDevice && !oldDevice && !!deviceTreeInst.value) {
await onLocateDeviceTree();
unwatch();
if (newDevice?.id === oldDevice?.id) return;
// console.log('selectedDevice changed');
onLocateDeviceTree();
syncToRoute();
});
// 当全线设备发生变化时,从路由参数同步选中状态
// 但lineDevices是shallowRef因此需要深度侦听才能获取内部变化
// 而单纯的深度侦听又可能会引发性能问题,因此尝试使用防抖侦听
watchDebounced(
lineDevices,
(newLineDevices) => {
if (syncRoute.value) {
// console.log('lineDevices changed');
syncFromRoute(newLineDevices);
}
},
{
debounce: 500,
deep: true,
},
);
onMounted(() => {
if (syncRoute.value) {
syncFromRoute(lineDevices.value);
}
});
</script>
@@ -531,6 +580,7 @@ const unwatch = watch(selectedDevice, async (newDevice, oldDevice) => {
virtual-scroll
:data="stationDeviceTreeData"
:animated="animated"
:selected-keys="selectedKeys"
:show-irrelevant-nodes="false"
:pattern="searchPattern"
:filter="searchFilter"

View File

@@ -1,27 +1,33 @@
<script setup lang="ts">
import type { LineAlarms, LineDevices, Station, VersionInfo } from '@/apis';
import { retentionDaysApi, snapStatusApi, type LineAlarms, type LineDevices, type Station, type VersionInfo } from '@/apis';
import { ThemeSwitch } from '@/components';
import { usePermission } from '@/composables';
import { NDM_ALARM_STORE_ID, NDM_DEVICE_STORE_ID, NDM_STATION_STORE_ID } from '@/constants';
import { usePollingStore, useSettingStore } from '@/stores';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useSettingStore, useStationStore } from '@/stores';
import { downloadByData, getAppEnvConfig, parseErrorFeedback, sleep } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { DeleteOutlined, ExportOutlined, ImportOutlined } from '@vicons/antd';
import { useEventListener } from '@vueuse/core';
import axios from 'axios';
import axios, { isCancel } from 'axios';
import destr from 'destr';
import { isFunction } from 'es-toolkit';
import localforage from 'localforage';
import { NButton, NDivider, NDrawer, NDrawerContent, NDropdown, NFlex, NFormItem, NIcon, NInput, NInputNumber, NModal, NSwitch, NText, type DropdownOption } from 'naive-ui';
import { DownloadIcon, Trash2Icon, UploadIcon } from 'lucide-vue-next';
import { NButton, NButtonGroup, NDivider, NDrawer, NDrawerContent, NDropdown, NFlex, NFormItem, NIcon, NInput, NInputNumber, NModal, NSwitch, NText, type DropdownOption } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
import { computed, ref, watch } from 'vue';
const show = defineModel<boolean>('show', { default: false });
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const occStation = computed(() => stations.value.find((station) => !!station.occ));
const settingsStore = useSettingStore();
const { menuCollpased, stationGridCols, debugModeEnabled, offlineDev } = storeToRefs(settingsStore);
const { menuCollpased, stationGridCols, debugMode, showDeviceRawData, pollingStations, activeRequests, subscribeMessages, mockUser, useLocalDB } = storeToRefs(settingsStore);
const { hasPermission } = usePermission();
const versionInfo = ref<VersionInfo>({ version: '', buildTime: '' });
@@ -40,6 +46,83 @@ const { mutate: getVersionInfo } = useMutation({
},
});
const abortControllers = ref({
retentionDays: new AbortController(),
snapStatus: new AbortController(),
});
const retentionDays = ref(0);
const { mutate: getRetentionDays, isPending: retentionDaysLoading } = useMutation({
mutationFn: async () => {
abortControllers.value.retentionDays.abort();
abortControllers.value.retentionDays = new AbortController();
const signal = abortControllers.value.retentionDays.signal;
const days = await retentionDaysApi('get', { signal });
return days;
},
onSuccess: (days) => {
retentionDays.value = days;
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const { mutate: saveRetentionDays, isPending: retentionDaysSaving } = useMutation({
mutationFn: async () => {
abortControllers.value.retentionDays.abort();
abortControllers.value.retentionDays = new AbortController();
const signal = abortControllers.value.retentionDays.signal;
await retentionDaysApi('post', { days: retentionDays.value, signal });
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
// 修改失败,刷新 retentionDays
getRetentionDays();
},
});
const snapStatus = ref(false);
const { mutate: getSnapStatus, isPending: snapStatusLoading } = useMutation({
mutationFn: async () => {
abortControllers.value.snapStatus.abort();
abortControllers.value.snapStatus = new AbortController();
const signal = abortControllers.value.snapStatus.signal;
const status = await snapStatusApi('get', { signal });
return status;
},
onSuccess: (status) => {
snapStatus.value = status;
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const { mutate: saveSnapStatus, isPending: snapStatusSaving } = useMutation({
mutationFn: async () => {
abortControllers.value.snapStatus.abort();
abortControllers.value.snapStatus = new AbortController();
const signal = abortControllers.value.snapStatus.signal;
await snapStatusApi('post', { doSnap: snapStatus.value, signal });
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
// 修改失败,刷新 snapStatus
getSnapStatus();
},
});
const showDebugCodeModal = ref(false);
const debugCode = ref('');
const enableDebugMode = () => {
@@ -49,11 +132,11 @@ const enableDebugMode = () => {
return;
}
showDebugCodeModal.value = false;
settingsStore.enableDebugMode();
debugMode.value = true;
};
const disableDebugMode = () => {
showDebugCodeModal.value = false;
settingsStore.disableDebugMode();
debugMode.value = false;
};
useEventListener('keydown', (event) => {
const { ctrlKey, altKey, code } = event;
@@ -64,28 +147,18 @@ useEventListener('keydown', (event) => {
const expectToShowDebugCodeInput = ref(false);
const onModalAfterEnter = () => {
expectToShowDebugCodeInput.value = !debugModeEnabled.value;
expectToShowDebugCodeInput.value = !debugMode.value;
};
const onModalAfterLeave = () => {
expectToShowDebugCodeInput.value = false;
debugCode.value = '';
};
const pollingStore = usePollingStore();
const { pollingEnabled } = storeToRefs(pollingStore);
const onPollingEnabledUpdate = (enabled: boolean) => {
if (enabled) {
pollingStore.startPolling();
} else {
pollingStore.stopPolling();
}
};
type IndexedDbStoreId = typeof NDM_STATION_STORE_ID | typeof NDM_DEVICE_STORE_ID | typeof NDM_ALARM_STORE_ID;
type IndexedDbStoreStates = {
[NDM_STATION_STORE_ID]: { stations: Station[] };
[NDM_DEVICE_STORE_ID]: { lineDevices: LineDevices };
[NDM_ALARM_STORE_ID]: { lineAlarms: LineAlarms; unreadLineAlarms: LineAlarms };
[NDM_ALARM_STORE_ID]: { lineAlarms: LineAlarms };
};
const exportFromIndexedDB = async <K extends IndexedDbStoreId>(storeId: K, options?: { errorMsg?: string }) => {
const { errorMsg } = options ?? {};
@@ -98,8 +171,9 @@ const exportFromIndexedDB = async <K extends IndexedDbStoreId>(storeId: K, optio
};
const importToIndexedDB = async <K extends IndexedDbStoreId>(storeId: K, options?: { successMsg?: string; errorMsg?: string }) => {
const { successMsg, errorMsg } = options ?? {};
pollingStore.stopPolling();
offlineDev.value = true;
pollingStations.value = false;
activeRequests.value = false;
subscribeMessages.value = false;
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.json';
@@ -122,8 +196,9 @@ const importToIndexedDB = async <K extends IndexedDbStoreId>(storeId: K, options
};
};
const deleteFromIndexedDB = async (storeId: IndexedDbStoreId) => {
pollingStore.stopPolling();
offlineDev.value = true;
pollingStations.value = false;
activeRequests.value = false;
subscribeMessages.value = false;
await localforage.removeItem(storeId).catch((error) => {
window.$message.error(`${error}`);
return;
@@ -192,13 +267,27 @@ const onSelectDropdownOption = (key: string, option: DropdownOption) => {
}
};
onMounted(() => {
getVersionInfo();
watch([activeRequests, show], ([active, entered]) => {
if (!active) return;
if (entered) {
getRetentionDays();
getSnapStatus();
} else {
abortControllers.value.retentionDays.abort();
abortControllers.value.snapStatus.abort();
}
});
const onDrawerAfterEnter = () => {
getVersionInfo();
};
const onDrawerAfterLeave = () => {
abortControllers.value.retentionDays.abort();
abortControllers.value.snapStatus.abort();
};
</script>
<template>
<NDrawer v-model:show="show" :width="560" :auto-focus="false">
<NDrawer v-model:show="show" :width="560" :auto-focus="false" @after-enter="onDrawerAfterEnter" @after-leave="onDrawerAfterLeave">
<NDrawerContent closable title="系统设置" :native-scrollbar="false">
<NFlex vertical>
<NDivider>主题</NDivider>
@@ -210,26 +299,64 @@ onMounted(() => {
<NFormItem label="折叠菜单" label-placement="left">
<NSwitch size="small" v-model:value="menuCollpased" />
</NFormItem>
<template v-if="route.path === '/station'">
<NFormItem label="车站列数" label-placement="left">
<NInputNumber v-model:value="stationGridCols" :min="1" :max="10" />
<NFormItem label="车站列数" label-placement="left">
<NInputNumber v-model:value="stationGridCols" :min="1" :max="10" />
</NFormItem>
<template v-if="!!occStation && hasPermission(occStation.code, PERMISSION_TYPE_LITERALS.OPERATION)">
<NDivider>告警</NDivider>
<NFormItem label="告警画面截图保留天数" label-placement="left">
<NFlex justify="space-between" align="center" style="width: 100%">
<NInputNumber v-model:value="retentionDays" :min="1" :max="15" />
<NButtonGroup>
<NButton secondary size="small" :disabled="retentionDaysSaving" :loading="retentionDaysLoading" @click="() => getRetentionDays()">刷新</NButton>
<NButton secondary size="small" :disabled="retentionDaysLoading" :loading="retentionDaysSaving" @click="() => saveRetentionDays()">保存</NButton>
</NButtonGroup>
</NFlex>
</NFormItem>
<NFormItem label="自动获取告警画面截图" label-placement="left">
<NFlex justify="space-between" align="center" style="width: 100%">
<NSwitch size="small" v-model:value="snapStatus" />
<NButtonGroup>
<NButton secondary size="small" :disabled="snapStatusSaving" :loading="snapStatusLoading" @click="() => getSnapStatus()">刷新</NButton>
<NButton secondary size="small" :disabled="snapStatusLoading" :loading="snapStatusSaving" @click="() => saveSnapStatus()">保存</NButton>
</NButtonGroup>
</NFlex>
</NFormItem>
</template>
<template v-if="debugModeEnabled">
<template v-if="debugMode">
<NDivider title-placement="center">调试</NDivider>
<NFormItem label="启用轮询" label-placement="left">
<NSwitch size="small" :value="pollingEnabled" @update:value="onPollingEnabledUpdate" />
<NFormItem label="调试模式" label-placement="left">
<NSwitch size="small" v-model:value="debugMode" />
</NFormItem>
<NFormItem label="离线开发" label-placement="left">
<NSwitch size="small" v-model:value="offlineDev" />
<NDivider title-placement="left" dashed>数据设置</NDivider>
<NFormItem label="显示设备原始数据" label-placement="left">
<NSwitch size="small" v-model:value="showDeviceRawData" />
</NFormItem>
<NFormItem label="本地数据库" label-placement="left">
<NDivider title-placement="left" dashed>网络设置</NDivider>
<NFormItem label="轮询车站" label-placement="left">
<NSwitch size="small" v-model:value="pollingStations" />
</NFormItem>
<NFormItem label="主动请求" label-placement="left">
<NSwitch size="small" v-model:value="activeRequests" />
</NFormItem>
<NFormItem label="订阅消息" label-placement="left">
<NSwitch size="small" v-model:value="subscribeMessages" />
</NFormItem>
<NFormItem label="模拟用户" label-placement="left">
<NSwitch size="small" v-model:value="mockUser" />
</NFormItem>
<NDivider title-placement="left" dashed>数据库设置</NDivider>
<NFormItem label="直接操作本地数据库" label-placement="left">
<NSwitch size="small" v-model:value="useLocalDB" />
</NFormItem>
<NFormItem label="数据操作" label-placement="left">
<NFlex>
<NDropdown trigger="click" :options="exportDropdownOptions" @select="onSelectDropdownOption">
<NButton secondary size="small">
<template #icon>
<NIcon :component="ExportOutlined" />
<NIcon :component="DownloadIcon" />
</template>
<template #default>导出</template>
</NButton>
@@ -237,7 +364,7 @@ onMounted(() => {
<NDropdown trigger="click" :options="importDropdownOptions" @select="onSelectDropdownOption">
<NButton secondary size="small">
<template #icon>
<NIcon :component="ImportOutlined" />
<NIcon :component="UploadIcon" />
</template>
<template #default>导入</template>
</NButton>
@@ -245,7 +372,7 @@ onMounted(() => {
<NDropdown trigger="click" :options="deleteDropdownOptions" @select="onSelectDropdownOption">
<NButton secondary size="small">
<template #icon>
<NIcon :component="DeleteOutlined" />
<NIcon :component="Trash2Icon" />
</template>
<template #default>删除</template>
</NButton>
@@ -264,7 +391,7 @@ onMounted(() => {
<NModal v-model:show="showDebugCodeModal" preset="dialog" type="info" @after-enter="onModalAfterEnter" @after-leave="onModalAfterLeave">
<template #header>
<NText v-if="!debugModeEnabled">请输入调试码</NText>
<NText v-if="!debugMode">请输入调试码</NText>
<NText v-else>确认关闭调试模式</NText>
</template>
<template #default>
@@ -272,7 +399,7 @@ onMounted(() => {
</template>
<template #action>
<NButton @click="showDebugCodeModal = false">取消</NButton>
<NButton v-if="!debugModeEnabled" type="primary" @click="enableDebugMode">启用</NButton>
<NButton v-if="!debugMode" type="primary" @click="enableDebugMode">启用</NButton>
<NButton v-else type="primary" @click="disableDebugMode">确认</NButton>
</template>
</NModal>

View File

@@ -5,14 +5,14 @@ import { storeToRefs } from 'pinia';
import type { ComponentInstance } from 'vue';
const settingsStore = useSettingStore();
const { darkThemeEnabled } = storeToRefs(settingsStore);
const { darkMode } = storeToRefs(settingsStore);
// 使外部能够获取NSwitch的类型提示
defineExpose({} as ComponentInstance<typeof NSwitch>);
</script>
<template>
<NSwitch v-model:value="darkThemeEnabled">
<NSwitch v-model:value="darkMode">
<template #unchecked-icon>
<NIcon>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">

Some files were not shown because too many files have changed in this diff Show More