1 Commits

Author SHA1 Message Date
yangsy 9dd13f56fb docs: 用户手册 2026-01-29 15:02:40 +08:00
117 changed files with 1201 additions and 729540 deletions
@@ -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. 特殊规则说明**
**重要提示**:如果未给某用户勾选任何权限(即所有复选框均为空),平台默认该用户拥有 **超级管理员权限**,可访问所有车站并执行所有操作。请务必谨慎配置。
@@ -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)**
* **矩阵式界面**
* **行**:对应各个车站。
* **列**:对应具体权限类型(如查看、操作)。
* **批量操作**:支持整行(单站全权)或整列(全站某权)一键勾选。
* **特殊规则说明**:若未给用户勾选任何权限,平台默认该用户拥有**所有权限**(超级管理员模式)。
-271
View File
@@ -1,271 +0,0 @@
# 网络设备管理平台
## 项目概述
这是网络设备管理平台的前端项目,用于在地铁线路运管中心检测和查看各车站网络设备的详细数据、运行情况、异常告警,并提供分析、统计、日志和权限管理能力。
主要设备类型:
- 摄像机
- 网络录像机
- 交换机
- 解码器
- 智能安防箱
- 媒体服务器
- 视频服务器
- 网络键盘
- 报警主机
## 技术栈
- 包管理:pnpm
- 构建工具:Vite
- 前端框架:Vue 3
- 语言:TypeScript
- 路由:Vue Router
- 组件库:Naive UI
- 状态管理:Pinia + pinia-plugin-persistedstate
- 服务端状态/轮询:TanStack Vue Query
- 本地持久化:localStorage、sessionStorage、IndexedDB/localforage
- 网络请求:axios
- 实时消息:STOMP/WebSocket`@stomp/stompjs`
- 图表:ECharts
- 图标:lucide-vue-next
- 样式:Sass
## 环境与脚本
### 运行环境
- Node.js`^20.19.0 || >=22.12.0`
- pnpm:以 `package.json``packageManager` 为准
### 常用命令
```bash
pnpm install # 安装依赖
pnpm dev # 启动开发服务,默认端口 9763
pnpm build # 类型检查 + Vite 构建 + 构建产物压缩
pnpm preview # 预览构建产物
pnpm build-only # 仅执行 Vite 构建
pnpm type-check # 执行 vue-tsc 类型检查
pnpm lint # 执行 ESLint,并带 --fix
pnpm format # 使用 Prettier 格式化 src/
```
当前项目未配置测试脚本,也未发现测试文件;不要声称已运行单元测试。需要验证改动时,优先运行 `pnpm type-check``pnpm lint``pnpm build` 中与改动相关的命令。
### 构建流程
`pnpm build` 的实际流程为:
1. `tsx build/pre-build.ts`:根据 `package.json` 的版本和构建时间写入 `public/manifest.json`
2. `vue-tsc --build`:类型检查。
3. `vite build`:生成 `dist/`
4. `tsx build/post-build.ts`:基于 `dist/` 生成 `.zip``.tar``.tar.gz` 压缩包。
压缩包命名格式为:`ndm-web-platform_v<version>_<YYMMDD-HHmmss>`
## 目录结构
项目源码集中在 `src/`
```text
src/
apis/ # 接口客户端、接口模型、业务请求封装
components/ # 业务组件与全局组件
device/
global/
permission/
station/
composables/ # 组合式函数
alarm/
common/
device/
permission/
query/ # TanStack Query 轮询与请求编排
station/
stomp/ # STOMP/WebSocket 客户端
constants/ # 常量
enums/ # 枚举
helpers/ # 辅助逻辑
layouts/
app-layout.vue # 登录后主布局
pages/ # 页面
plugins/ # Pinia 持久化等插件
router/
index.ts # 路由配置与守卫
stores/ # Pinia stores
styles/ # 全局样式
types/ # 全局/工具类型
utils/ # 通用工具函数
```
路径别名:`@/*` 指向 `src/*`
## 页面与路由
路由配置在 `src/router/index.ts`。登录页独立于主布局;除 `/login` 外,其余页面都作为 `src/layouts/app-layout.vue` 的子路由。
```text
src/
router/
index.ts
layouts/
app-layout.vue
pages/
login/
login-page.vue # 登录页,对应 /login
station/
station-page.vue # 车站状态页/首页,对应 /station
device/
device-page.vue # 设备诊断页,对应 /device
alarm/
alarm-log-page.vue # 设备告警记录,对应 /alarm/alarm-log
alarm-ignore-page.vue # 告警忽略管理,对应 /alarm/alarm-ignore
log/
vimp-log-page.vue # 视频平台日志,对应 /log/vimp-log
call-log-page.vue # 上级调用日志,对应 /log/call-log
permission/
permission-page.vue # 权限管理,对应 /permission
system/
changelog/
changelog-page.vue # 更新记录,对应 /changelog
error/
not-found-page.vue # 404 页面,对应 catch-all
```
路由守卫规则:
- 未登录访问非 `/login` 页面会跳转到 `/login`
- 已登录访问 `/login` 会跳转到 `/`
- `/` 默认重定向到 `/station`
- `/permission` 需要 `useUserStore().isLamp` 为真,否则跳转到 404。
## 数据轮询与状态管理
由于后端服务按车站分布,前端需要向各车站服务依次请求数据。项目采用“单点驱动 + 变更监听 + 级联触发”的轮询模式。
核心文件位于 `src/composables/query/`
- `use-line-stations-query.ts`:查询所有车站,是业务轮询入口。
- `use-user-permission-query.ts`:查询并计算当前用户在各车站的权限,负责调度后续设备/告警查询。
- `use-line-devices-query.ts`:查询设备数据。
- `use-line-alarms-query.ts`:查询告警数据。
- `use-verify-user-query.ts`:用户登录/校验相关请求。
- `use-version-check-query.ts`:版本检查,依赖构建阶段生成的 `/manifest.json`
关键 Pinia stores 位于 `src/stores/`
- `user.ts`:用户登录态、Token、用户类型等。
- `station.ts`:车站数据。
- `device.ts`:设备数据。
- `alarm.ts`:告警数据。
- `permission.ts`:权限数据。
- `setting.ts`:系统设置、调试开关、网络开关等。
- `polling.ts`:轮询状态控制。
- `unread.ts`:未读状态。
持久化注意事项:
- 大体量业务数据会使用 IndexedDB/localforage。
- 普通设置类状态会使用 localStorage/sessionStorage。
- `src/main.ts` 会比较 `VITE_STORAGE_VERSION` 与本地 `ndm-storage-version`,不一致时清空 localStorage 并清空 localforage。
## 接口与代理
接口相关代码位于 `src/apis/`
- `client/`HTTP 客户端基础封装。
- `domain/`:领域相关类型/逻辑。
- `model/`:接口模型。
- `request/`:按业务拆分的请求函数。
开发代理配置在 `vite.config.ts`
- 开发服务端口:`9763`
- 已配置多个线路/站点前缀代理,包括 01、02、04、10、21 相关站点。
- 当前配置包含 `/api``/minio``/ws` 等代理项。
- `ProxyItem.rewrite` 用于将本地请求前缀改写为后端真实路径,例如将 `/1001/api` 改写为 `/api`
修改代理时,应同步检查 `key``target``rewrite``ws` 是否匹配实际环境。
## 环境变量
项目使用 Vite 环境变量,当前变量集中在 `.env`。文档或回答中只列变量名和用途,不要复述真实密钥、密码或授权值。
常见变量:
- `VITE_APP_TITLE`:页面标题。
- `VITE_REQUEST_INTERVAL`:轮询间隔,单位秒。
- `VITE_NDM_APP_KEY`:网管 appKey。
- `VITE_LAMP_CLIENT_ID`LAMP clientId。
- `VITE_LAMP_CLIENT_SECRET`LAMP clientSecret。
- `VITE_LAMP_USERNAME`LAMP 登录用户名。
- `VITE_LAMP_PASSWORD`LAMP 登录密码。
- `VITE_LAMP_AUTHORIZATION`:已有 Authorization 时直接使用,否则由 clientId/clientSecret 生成。
- `VITE_STORAGE_VERSION`:本地缓存版本,用于触发缓存清理。
- `VITE_DEBUG_CODE`:调试模式授权码。
## 调试模式与离线开发
调试模式默认隐藏,用于开发、联调和故障排查。
开启方式:
1. 使用快捷键 `Ctrl + Alt + D` 唤起验证弹窗。
2. 输入 `VITE_DEBUG_CODE` 对应授权码。
3. 验证通过后,“系统设置”中会显示调试分组。
调试相关能力:
- 显示设备原始数据。
- 控制是否轮询车站。
- 控制是否主动请求。
- 控制是否订阅 STOMP/WebSocket 消息。
- 启用模拟用户。
- 允许在特定场景下直接操作本地 IndexedDB。
离线开发:
- 如果浏览器已有现场缓存,可在调试模式中关闭轮询、主动请求和消息订阅,直接查看本地缓存。
- 全新环境可在登录页控制台设置 `window.$mockUser.value = true` 进入模拟登录,再通过调试面板导入 `docs/data/` 下的数据:
- `ndm-station-store.json`
- `ndm-device-store.json`
- `ndm-alarm-store.json`
## 代码风格与约定
- 遵循现有 Vue SFC、TypeScript、组合式函数和 Pinia 写法。
- 新增业务请求时优先放入 `src/apis/request/` 对应业务分类。
- 新增页面时同步更新 `src/router/index.ts`,并保持 `src/pages/` 目录与路由语义一致。
- 新增共享逻辑优先放入 `src/composables/``src/helpers/``src/utils/`,不要在页面中堆积重复逻辑。
- 新增全局/业务组件时优先放入 `src/components/` 对应分类。
- 使用 `@/` 引用 `src/` 下模块。
- 不要在代码、文档或回复中泄露真实密码、Token、Authorization 或现场地址以外的敏感信息。
格式化配置来自 `.prettierrc.json`
- LF 换行
- 2 空格缩进
- 单引号
- 使用分号
- `trailingComma: all`
- `printWidth: 200`
ESLint 重点规则:
- `@typescript-eslint/no-unused-vars` 为 warn。
- `@typescript-eslint/no-explicit-any` 当前关闭,但新增代码仍应尽量保持类型清晰。
- `vue/multi-word-component-names` 当前关闭。
## 协作规则
1. 默认不要直接修改项目代码;当用户明确授权修改时,只修改授权范围内的文件。
2. 修改前先阅读相关文件和既有实现,避免凭空猜测目录、接口或状态结构。
3. 修改后说明改了哪些文件、为什么改、如何验证。
4. 文档类修改至少重新读取目标文件确认内容正确。
5. 代码类修改应按影响范围运行 `pnpm type-check``pnpm lint``pnpm build` 或更小范围的可用验证命令。
6. 不要提交 Git commit,除非用户明确要求。
7. 不要删除失败测试或通过压制类型错误来规避问题。
8. 不要在最终说明中声称执行了未实际执行的命令。
-146
View File
@@ -39,149 +39,3 @@ pnpm build
``` ```
在执行 `pnpm build` 之前,你可以在 `package.json` 中修改 `version` 字段,将其设置为你期望的版本号,构建完成后,项目的根目录中除了 `dist` 目录外,还会生成三个压缩包,文件名的格式统一为 `ndm-web-platform_v<version>_<datetime>`,文件格式则分别为 `zip``tar``tar.gz` 在执行 `pnpm build` 之前,你可以在 `package.json` 中修改 `version` 字段,将其设置为你期望的版本号,构建完成后,项目的根目录中除了 `dist` 目录外,还会生成三个压缩包,文件名的格式统一为 `ndm-web-platform_v<version>_<datetime>`,文件格式则分别为 `zip``tar``tar.gz`
## 业务结构
所有业务相关的页面都在 `src/pages` 目录下,路由配置在 `src/router/index.ts` 文件,除登录页之外,其余页面都作为 `src/layouts/app-layout.vue` 的子路由。
```bash
src/
router/
index.ts # 路由配置文件
layouts/
app-layout.vue # 布局
pages/
login/
login-page.vue # 登录页面
station/
station-page.vue # 车站状态页面(首页)
device/
device-page.vue # 设备诊断页面
alarm/
alarm-ignore-page.vue # 告警忽略管理页面
alarm-log-page.vue # 设备告警记录页面
log/
call-log-page.vue # 上级调用日志页面
vimp-log-page.vue # 视频平台日志页面
permission/
permission-page.vue # 权限管理页面
system/
changelog/
changelog-page.vue # 更新记录页面
error/
not-found-page.vue # 404 页面
```
## 数据轮询
由于后端服务的架构限制,需要前端向所有车站服务依次发送请求来获取数据,需要获取的数据包含车站状态、设备数据以及告警数据,因此需要设计一套数据轮询方案,定期从所有车站服务获取数据。
在项目中,`src/composables/query/` 目录下是所有数据轮询相关的代码,其中与业务相关的代码主要包括:
- `use-line-stations-query.ts`: 查询所有车站
- `use-line-devices-query.ts`: 查询所有设备
- `use-line-alarms-query.ts`: 查询所有告警
- `use-user-permission-query.ts`: 查询用户权限
在描述整个数据轮询流程之前,我们要明确项目中必须存在的几个关键概念:
- 车站相关:车站query + 车站store
- 设备相关:设备query + 设备store
- 告警相关:告警query + 告警store
- 权限相关:权限query + 权限store
整个数据轮询流程采用“单点驱动 + 变更监听 + 级联触发”的模式,如下图所示。
![数据轮询流程](./docs/assets/query-chain.png)
1. 轮询入口:车站query
- 触发条件:以120秒的周期自动轮询车站列表
- 数据流向:车站store
2. 核心调度:权限query
- 触发条件:车站query执行后触发
- 数据流向:权限store,并计算当前用户在各车站的权限
- 数据监听:监听车站和权限变化,触发设备query和告警query
3. 设备query & 告警query
- 触发条件:被动触发,由权限query主动调用
- 数据流向:设备store & 告警store
## 调试模式
在设置面板中有一系列与调试模式有关的设置项,主要用于开发和故障排查。
### 开启方式
调试模式默认隐藏,通过以下方式开启:
1. 使用快捷键 `Ctrl + Alt + D` 唤起验证弹窗
2. 输入授权码进行验证(授权码对应环境变量 `.env` 中的 `VITE_DEBUG_CODE`
3. 验证通过后,在“系统设置”面板中会出现 **调试** 分组
### 设置项说明
#### 数据设置
- **显示设备原始数据**
- 控制是否在设备详情页显示“原始数据”标签页
- 开启后可查看设备接口返回的原始 JSON 数据,便于排查字段缺失或格式错误
#### 网络设置
- **轮询车站**
- 控制是否定时拉取车站状态,进而触发权限、设备及告警数据的更新
- 关闭后将暂停所有业务数据的自动轮询机制
- **主动请求**
- 控制组件挂载时是否自动发起数据请求
- 涵盖设备在线状态检测、用户登录验证等逻辑,关闭后组件在初始化时将不再自动拉取数据
- **订阅消息**
- 控制是否通过 WebSocket (STOMP) 接收实时告警或状态推送
- 关闭后将不再处理后端推送的实时消息
- **模拟用户**
- 开启后使用内置的超管用户绕过登录
- 开启时会自动进入调试模式,便于开发环境快速测试
#### 数据库设置
- **直接操作本地数据库**
- 控制某些业务逻辑(如交换机端口、安防箱回路)是否直接读写本地 IndexedDB
- 用于在无后端环境或特定测试场景下验证本地数据逻辑
## 离线开发
项目支持在无后端服务的情况下正常启动,具体操作取决于你的本地环境是否已有历史数据。
### 场景一:已有本地缓存
如果你的浏览器曾接入过现场环境,IndexedDB 中已保存了车站、设备等数据,只需在设置中关闭网络请求即可进入离线模式:
1. 开启调试模式(`Ctrl + Alt + D`)。
2. 在“网络设置”中,关闭 **轮询车站**、**主动请求** 和 **订阅消息**
3. 此时平台将停止向后端发起请求,直接展示本地缓存的历史数据。
### 场景二:全新环境启动(新人推荐)
如果你是首次拉取项目且无法连接后端,需要按以下步骤操作:
1. **模拟登录**
在登录页按 `F12` 打开控制台,输入以下命令强制进入平台:
```javascript
window.$mockUser.value = true;
```
执行后平台将自动完成以下操作:
- 注入测试 Token 和管理员身份信息
- 关闭所有网络请求(轮询、主动请求、消息订阅)
- 开启调试模式
- 自动跳转至平台首页
2. **导入模拟数据**
进入平台后,页面默认为空。需导入预设数据以填充内容:
- 打开“系统设置”(已自动开启调试模式)。
- 在 **调试** -> **数据库设置** 中,勾选 **直接操作本地数据库**。
- 点击该选项下方的 **导入数据** 按钮。
- 依次导入项目根目录 `docs/data/` 下的三个文件:
- `ndm-station-store.json`(车站数据)
- `ndm-device-store.json`(设备数据)
- `ndm-alarm-store.json`(告警数据)
> **注意**:每次导入一个文件后,平台会自动刷新页面以应用数据。请等待刷新完成后,重新打开设置面板导入下一个文件。
+1 -1
View File
@@ -11,7 +11,7 @@ const versionInfo = {
}; };
try { try {
await writeFile('./public/manifest.json', `${JSON.stringify(versionInfo, null, 2)}\n`); await writeFile('./public/manifest.json', JSON.stringify(versionInfo, null, 2));
} catch (error) { } catch (error) {
console.error('写入manifest失败:', error); console.error('写入manifest失败:', error);
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-203
View File
@@ -1,203 +0,0 @@
{
"stations": [
{
"code": "1075",
"name": "吴中路控制中心",
"online": true,
"ip": "10.18.128.10",
"occ": true
},
{
"code": "1001",
"name": "虹桥火车站",
"online": true,
"ip": "10.18.129.10"
},
{
"code": "1002",
"name": "虹桥2号航站楼",
"online": true,
"ip": "10.18.131.10"
},
{
"code": "1003",
"name": "虹桥一号航站楼",
"online": true,
"ip": "10.18.133.10"
},
{
"code": "1004",
"name": "上海动物园",
"online": true,
"ip": "10.18.135.10"
},
{
"code": "1005",
"name": "龙溪路",
"online": true,
"ip": "10.18.137.10"
},
{
"code": "1006",
"name": "水城路",
"online": true,
"ip": "10.18.139.10"
},
{
"code": "1007",
"name": "伊犁路",
"online": true,
"ip": "10.18.141.10"
},
{
"code": "1008",
"name": "宋园路",
"online": true,
"ip": "10.18.143.10"
},
{
"code": "1009",
"name": "虹桥路",
"online": true,
"ip": "10.18.145.10"
},
{
"code": "1010",
"name": "交通大学",
"online": true,
"ip": "10.18.147.10"
},
{
"code": "1011",
"name": "图书馆",
"online": true,
"ip": "10.18.149.10"
},
{
"code": "1012",
"name": "陕西南路",
"online": true,
"ip": "10.18.151.10"
},
{
"code": "1013",
"name": "新天地",
"online": true,
"ip": "10.18.153.10"
},
{
"code": "1014",
"name": "老西门",
"online": true,
"ip": "10.18.155.10"
},
{
"code": "1015",
"name": "豫园",
"online": true,
"ip": "10.18.157.10"
},
{
"code": "1016",
"name": "南京东路",
"online": true,
"ip": "10.18.159.10"
},
{
"code": "1017",
"name": "天潼路",
"online": true,
"ip": "10.18.161.10"
},
{
"code": "1018",
"name": "四川北路",
"online": true,
"ip": "10.18.163.10"
},
{
"code": "1019",
"name": "海伦路",
"online": true,
"ip": "10.18.165.10"
},
{
"code": "1020",
"name": "邮电新村",
"online": true,
"ip": "10.18.167.10"
},
{
"code": "1021",
"name": "四平路",
"online": true,
"ip": "10.18.169.10"
},
{
"code": "1022",
"name": "同济大学",
"online": true,
"ip": "10.18.171.10"
},
{
"code": "1023",
"name": "国权路",
"online": true,
"ip": "10.18.173.10"
},
{
"code": "1024",
"name": "五角场",
"online": true,
"ip": "10.18.175.10"
},
{
"code": "1025",
"name": "江湾体育场",
"online": true,
"ip": "10.18.177.10"
},
{
"code": "1026",
"name": "三门路",
"online": true,
"ip": "10.18.179.10"
},
{
"code": "1027",
"name": "殷高东路",
"online": true,
"ip": "10.18.181.10"
},
{
"code": "1028",
"name": "新江湾城",
"online": true,
"ip": "10.18.183.10"
},
{
"code": "1029",
"name": "航中路",
"online": true,
"ip": "10.18.185.10"
},
{
"code": "1030",
"name": "紫藤路",
"online": true,
"ip": "10.18.187.10"
},
{
"code": "1031",
"name": "龙柏新村",
"online": true,
"ip": "10.18.189.10"
},
{
"code": "1032",
"name": "吴中路基地",
"online": true,
"ip": "10.18.244.10"
}
]
}
-351
View File
@@ -1,351 +0,0 @@
[
{
"version": "0.42.0",
"date": "2026-05-20",
"changes": {
"fixes": [
{ "content": "优化安防箱环境数据卡片,避免空标签渲染并改进风扇信息展示" },
{ "content": "修复设备硬件卡片进度条异常值显示问题,并完善状态判断逻辑" }
],
"feats": [
{ "content": "新增服务器网卡信息展示" },
{ "content": "新增安防箱网卡信息展示,并优化相关卡片标题" },
{ "content": "为交换机诊断信息新增温度字段" },
{ "content": "新增录像机环境状态卡片和网卡信息展示" }
]
}
},
{
"version": "0.41.0",
"date": "2026-05-19",
"changes": {
"fixes": [
{ "content": "修复交换机端口卡片在端口数据为空时仍展示的问题" },
{ "content": "修复安防箱门禁状态和防雷状态显示错误的问题" },
{ "content": "修复解码器和录像机历史诊断中硬件占用卡片的加载状态同步问题" }
],
"feats": [
{ "content": "新增摄像机阈值配置功能" },
{ "content": "新增摄像机硬件占用率历史诊断记录卡片" },
{ "content": "优化设备诊断页的信息展示结构,支持按设备型号、网卡信息、接入信息等分组展示" },
{ "content": "为摄像机诊断信息新增网卡信息、IP信息和设备通用信息展示" },
{ "content": "新增多厂商安防箱支持,支持BeiDian和NingTech安防箱的空开控制与设备重启" },
{ "content": "安防箱配置新增开关数量和团体字符串(写)" },
{ "content": "为各类设备实体新增团体字符串(写)字段" }
]
}
},
{
"version": "0.40.0",
"date": "2026-04-10",
"changes": {
"fixes": [{ "content": "修复设备树搜索时节点错乱的问题" }, { "content": "将安防箱面板的“电路”统一更正为“空开”" }, { "content": "修复设备查询缓存键冲突问题" }],
"feats": [{ "content": "添加视频服务器双机热备状态" }]
}
},
{
"version": "0.39.0",
"date": "2026-03-02",
"changes": {
"fixes": [{ "content": "修复设备树搜索时节点错乱的问题" }],
"feats": [{ "content": "新版录像记录诊断卡片" }, { "content": "新增平台更新记录页面" }]
}
},
{
"version": "0.38.5",
"date": "2026-02-06",
"changes": {
"fixes": [{ "content": "修复视频平台和上级调用日志的默认查询没有携带logType参数的问题" }]
}
},
{
"version": "0.38.4",
"date": "2026-02-05",
"changes": {
"fixes": [{ "content": "修复告警记录导出未添加条件筛选" }, { "content": "将各查询页的默认分页size从10调整为20" }]
}
},
{
"version": "0.38.3",
"date": "2026-01-30",
"changes": {
"fixes": [{ "content": "修复录像诊断导出面板统一使用批量接口" }]
}
},
{
"version": "0.38.2",
"date": "2026-01-29",
"changes": {
"fixes": [{ "content": "修复服务状态和推流统计卡片的渲染条件" }, { "content": "用 useQuery 重构录像诊断卡片" }]
}
},
{
"version": "0.38.1",
"date": "2026-01-28",
"changes": {
"fixes": [{ "content": "下游设备配置添加权限校验" }]
}
},
{
"version": "0.38.0",
"date": "2026-01-28",
"changes": {
"fixes": [{ "content": "新增批量导出录像诊断功能并优化导出体验" }]
}
},
{
"version": "0.37.2",
"date": "2026-01-27",
"changes": {
"fixes": [{ "content": "修复设备树选中状态与路由同步的逻辑,修复选中的设备类型被异常还原的问题" }]
}
},
{
"version": "0.37.1",
"date": "2026-01-27",
"changes": {
"fixes": [{ "content": "完善设备卡片标签页切换逻辑" }]
}
},
{
"version": "0.37.0",
"date": "2026-01-22",
"changes": {
"feats": [{ "content": "添加权限查询和管理机制" }]
}
},
{
"version": "0.36.2",
"date": "2026-01-21",
"changes": {
"feats": [{ "content": "车站卡片布局列数自适应" }]
}
},
{
"version": "0.36.1",
"date": "2026-01-21",
"changes": {
"fixes": [{ "content": "重构内部状态管理" }]
}
},
{
"version": "0.36.0",
"date": "2026-01-16",
"changes": {
"feats": [{ "content": "设备告警记录页面添加告警恢复状态和确认状态筛选" }]
}
},
{
"version": "0.35.2",
"date": "2026-01-15",
"changes": {
"fixes": [
{ "content": "优化查询链的耗时和错误日志输出" },
{ "content": "优化车站状态页面的操作栏交互逻辑" },
{ "content": "简化录像诊断卡片逻辑" },
{ "content": "抽离未读告警状态,不再持久化" }
]
}
},
{
"version": "0.35.1",
"date": "2026-01-13",
"changes": {
"fixes": [{ "content": "修复设备硬件占用率卡片中showCard计算属性未获取原始值的问题" }]
}
},
{
"version": "0.35.0",
"date": "2026-01-13",
"changes": {
"feats": [{ "content": "更新图标" }]
}
},
{
"version": "0.34.1",
"date": "2026-01-08",
"changes": {
"fixes": [
{ "content": "修复数据表格标题错误" },
{ "content": "修复当API接口定义中没有响应数据时会意外抛出空数据异常的问题" },
{ "content": "未登录时启用离线开发模式后添加默认用户信息" },
{ "content": "修正告警页路由路径错误" },
{ "content": "将请求封装重构为函数模式" },
{ "content": "修复请求实例选择逻辑错误" }
]
}
},
{
"version": "0.34.0",
"date": "2026-01-04",
"changes": {
"fixes": [{ "content": "将表单中的“操作类型”标签改为“日志类型”" }, { "content": "移除操作参数和操作结果列" }, { "content": "修复操作类型列渲染错误的问题" }],
"feats": [{ "content": "上级调用日志添加更多数据" }]
}
},
{
"version": "0.33.0",
"date": "2026-01-04",
"changes": {
"fixes": [{ "content": "优化服务状态卡片的渲染条件" }, { "content": "添加防止设备自关联的校验" }],
"feats": [{ "content": "新增流媒体推流统计卡片" }, { "content": "新增告警画面截图相关设置" }]
}
},
{
"version": "0.32.0",
"date": "2025-12-30",
"changes": {
"feats": [{ "content": "新增告警画面截图相关设置" }]
}
},
{
"version": "0.31.0",
"date": "2025-12-30",
"changes": {
"feats": [{ "content": "新增告警忽略管理页面" }]
}
},
{
"version": "0.30.0",
"date": "2025-12-28",
"changes": {
"fixes": [{ "content": "调整路由结构,使告警板块支持子路由" }, { "content": "修复跳转设备时未检查deviceId存在性的问题" }],
"feats": [{ "content": "新增告警忽略管理页面" }, { "content": "支持查看摄像机告警画面截图" }, { "content": "查询页面卸载时取消未完成的请求" }]
}
},
{
"version": "0.29.0",
"date": "2025-12-26",
"changes": {
"feats": [{ "content": "扩展交换机端口诊断信息" }]
}
},
{
"version": "0.28.1",
"date": "2025-12-26",
"changes": {
"feats": [{ "content": "当下游设备不存在时自动解除关联" }]
}
},
{
"version": "0.28.0",
"date": "2025-12-26",
"changes": {
"feats": [{ "content": "告警记录支持点击设备跳转到设备详情" }, { "content": "设备关联与解除关联" }, { "content": "扩展设备树功能" }]
}
},
{
"version": "0.27.6",
"date": "2025-12-25",
"changes": {
"fixes": [{ "content": "移除所有设备更新表单中的上游设备字段" }]
}
},
{
"version": "0.27.5",
"date": "2025-12-25",
"changes": {
"fixes": [{ "content": "修复设备管理逻辑中错误处理的loading状态和取消逻辑的顺序" }]
}
},
{
"version": "0.27.4",
"date": "2025-12-24",
"changes": {
"fixes": [{ "content": "修复优化请求封装后获取摄像机画面截图请求异常的问题" }, { "content": "简化设备树的自动定位逻辑" }]
}
},
{
"version": "0.27.3",
"date": "2025-12-23",
"changes": {
"fixes": [{ "content": "次渲染全线设备树时不再区分是否从路由跳转而来,补全遗漏的取消监听" }]
}
},
{
"version": "0.27.2",
"date": "2025-12-23",
"changes": {
"feats": [{ "content": "摄像机卡片添加摄像机类型和建议安装区域" }]
}
},
{
"version": "0.26.3",
"date": "2025-12-19",
"changes": {
"feats": [{ "content": "调用新的设备告警日志导出接口" }]
}
},
{
"version": "0.26.2",
"date": "2025-12-19",
"changes": {
"fixes": [{ "content": "视频平台日志页面补全遗漏的操作类型字段" }]
}
},
{
"version": "0.26.1",
"date": "2025-12-19",
"changes": {
"fixes": [
{ "content": "修复由动画属性导致设备树在特定场景下无法自行滚动及展开节点失效的问题" },
{ "content": "设备树仅在非车站模式下显示收起和定位按钮" },
{ "content": "修复设备更新面板中错误的表单校验逻辑" },
{ "content": "简化设备树节点双击和点击事件的逻辑并添加注释" }
],
"feats": [{ "content": "细化设备树自动定位的触发条件" }, { "content": "渲染全线设备树时自动定位到所选设备" }]
}
},
{
"version": "0.26.0",
"date": "2025-12-17",
"changes": {
"fixes": [
{ "content": "简化设备树节点双击和点击事件的逻辑并添加注释" },
{ "content": "修复设备更新面板中错误的表单校验逻辑" },
{ "content": "426d92a - fix: 在导入和删除IndexedDB数据时停止轮询并启用离线开发模式以保证数据一致性" }
],
"feats": [{ "content": "新增设备树管理功能" }, { "content": "新增流媒体/信令服务状态卡片" }]
}
},
{
"version": "0.25.0",
"date": "2025-12-11",
"changes": {
"fixes": [{ "content": "改进设备卡片的布局" }, { "content": "改进内部状态管理" }],
"feats": [
{ "content": "全面优化平台数据轮询机制,提升平台性能" },
{ "content": "支持修改设备" },
{ "content": "告警轮询中获取完整告警数据" },
{ "content": "车站告警详情支持导出完整的今日告警列表" }
]
}
},
{
"version": "更早版本~0.25.0",
"date": "~2025-12-11",
"changes": {
"fixes": [
{ "content": "修复安防箱部分开关状态错误" },
{ "content": "优化版本更新机制" },
{ "content": "优化交互时的数据查询机制" },
{ "content": "优化获取摄像机告警时画面截图的交互体验" },
{ "content": "修复更改显示器息屏计划时的错误请求" },
{ "content": "修复开启实时告警刷新时的交互错误" },
{ "content": "修复404异常时的页面跳转错误" },
{ "content": "......." }
],
"feats": [
{ "content": "新增同步摄像机功能" },
{ "content": "支持多选车站导出设备列表" },
{ "content": "新增车站状态页面的操作栏" },
{ "content": "支持忽略摄像机告警" },
{ "content": "新增报警主机设备" },
{ "content": "设备告警页面支持实时刷新" },
{ "content": "新增支持获取摄像机告警时的画面截图" },
{ "content": "新增支持手动诊断设备" },
{ "content": "......" }
]
}
}
]
+2 -2
View File
@@ -1,4 +1,4 @@
{ {
"version": "0.42.0", "version": "",
"buildTime": "2026-05-20 13:52:07" "buildTime": ""
} }
-43
View File
@@ -1,43 +0,0 @@
{
"001": { "name": "1号线", "color": "#E81B38" },
"002": { "name": "2号线", "color": "#8AC63F" },
"003": { "name": "3号线", "color": "#FBD005" },
"004": { "name": "4号线", "color": "#4F2D8B" },
"005": { "name": "5号线", "color": "#9056A3" },
"006": { "name": "6号线", "color": "#D61670" },
"007": { "name": "7号线", "color": "#F37120" },
"008": { "name": "8号线", "color": "#009DD8" },
"009": { "name": "9号线", "color": "#7AC7EA" },
"010": { "name": "10号线", "color": "#BCA8D1" },
"011": { "name": "11号线", "color": "#7D2030" },
"012": { "name": "12号线", "color": "#007C65" },
"013": { "name": "13号线", "color": "#E795C0" },
"014": { "name": "14号线", "color": "#5E5C29" },
"015": { "name": "15号线", "color": "#759092" },
"016": { "name": "16号线", "color": "#8ED1C0" },
"017": { "name": "17号线", "color": "#B87975" },
"018": { "name": "18号线", "color": "#BB8C51" },
"019": { "name": "19号线", "color": "#BABABA" },
"020": { "name": "20号线", "color": "#BABABA" },
"021": { "name": "21号线", "color": "#BABABA" },
"022": { "name": "22号线", "color": "#BABABA" },
"023": { "name": "23号线", "color": "#BABABA" },
"024": { "name": "24号线", "color": "#BABABA" },
"025": { "name": "25号线", "color": "#BABABA" },
"051": { "name": "浦江线", "color": "#BABABA" },
"501": { "name": "COCC", "color": "#BABABA" },
"502": { "name": "BCOCC", "color": "#BABABA" },
"601": { "name": "上海火车站分控", "color": "#BABABA" },
"602": { "name": "徐家汇分控", "color": "#BABABA" },
"603": { "name": "宜山路分控", "color": "#BABABA" },
"604": { "name": "陆家嘴分控", "color": "#BABABA" },
"605": { "name": "人民广场分控", "color": "#BABABA" },
"606": { "name": "东宝兴路分控", "color": "#BABABA" },
"607": { "name": "虹桥枢纽分控", "color": "#BABABA" },
"608": { "name": "松江大学城分控", "color": "#BABABA" },
"609": { "name": "民生路分控", "color": "#BABABA" },
"610": { "name": "西藏南路分控", "color": "#BABABA" },
"611": { "name": "延吉中路分控", "color": "#BABABA" },
"612": { "name": "迪士尼分控", "color": "#BABABA" },
"900": { "name": "轨交总队", "color": "#BABABA" }
}
-102
View File
@@ -1,102 +0,0 @@
[
{
"code": "01",
"name": "地下区",
"subs": [
{ "code": "01001", "name": "地下01层" },
{ "code": "01002", "name": "地下02层" },
{ "code": "01003", "name": "地下03层" },
{ "code": "01004", "name": "地下04层" },
{ "code": "01005", "name": "地下05层" },
{ "code": "01006", "name": "地下06层" },
{ "code": "01007", "name": "地下07层" },
{ "code": "01008", "name": "地下08层" },
{ "code": "01009", "name": "地下09层" }
]
},
{
"code": "02",
"name": "调度大厅",
"subs": [{ "code": "02001", "name": "调度大厅" }]
},
{
"code": "03",
"name": "一区",
"subs": [
{ "code": "03001", "name": "01层" },
{ "code": "03002", "name": "02层" },
{ "code": "03003", "name": "03层" },
{ "code": "03004", "name": "04层" },
{ "code": "03005", "name": "05层" },
{ "code": "03006", "name": "06层" },
{ "code": "03007", "name": "07层" },
{ "code": "03008", "name": "08层" },
{ "code": "03009", "name": "09层" },
{ "code": "03010", "name": "10层" }
]
},
{
"code": "04",
"name": "二区",
"subs": [
{ "code": "04001", "name": "11层" },
{ "code": "04002", "name": "12层" },
{ "code": "04003", "name": "13层" },
{ "code": "04004", "name": "14层" },
{ "code": "04005", "name": "15层" },
{ "code": "04006", "name": "16层" },
{ "code": "04007", "name": "17层" },
{ "code": "04008", "name": "18层" },
{ "code": "04009", "name": "19层" },
{ "code": "04010", "name": "20层" }
]
},
{
"code": "05",
"name": "三区",
"subs": [
{ "code": "05001", "name": "21层" },
{ "code": "05002", "name": "22层" },
{ "code": "05003", "name": "23层" },
{ "code": "05004", "name": "24层" },
{ "code": "05005", "name": "25层" },
{ "code": "05006", "name": "26层" },
{ "code": "05007", "name": "27层" },
{ "code": "05008", "name": "28层" },
{ "code": "05009", "name": "29层" },
{ "code": "05010", "name": "30层" }
]
},
{
"code": "06",
"name": "四区",
"subs": [
{ "code": "06001", "name": "31层" },
{ "code": "06002", "name": "32层" },
{ "code": "06003", "name": "33层" },
{ "code": "06004", "name": "34层" },
{ "code": "06005", "name": "35层" },
{ "code": "06006", "name": "36层" },
{ "code": "06007", "name": "37层" },
{ "code": "06008", "name": "38层" },
{ "code": "06009", "name": "39层" },
{ "code": "06010", "name": "40层" }
]
},
{
"code": "07",
"name": "五区",
"subs": [
{ "code": "07001", "name": "41层" },
{ "code": "07002", "name": "42层" },
{ "code": "07003", "name": "43层" },
{ "code": "07004", "name": "44层" },
{ "code": "07005", "name": "45层" },
{ "code": "07006", "name": "46层" },
{ "code": "07007", "name": "47层" },
{ "code": "07008", "name": "48层" },
{ "code": "07009", "name": "49层" },
{ "code": "07010", "name": "50层" }
]
}
]
-378
View File
@@ -1,378 +0,0 @@
[
{
"code": "01",
"name": "停车库",
"subs": [
{ "code": "01001", "name": "库门01" },
{ "code": "01002", "name": "库门02" },
{ "code": "01003", "name": "库门03" },
{ "code": "01004", "name": "库门04" },
{ "code": "01005", "name": "库门05" },
{ "code": "01006", "name": "库门06" },
{ "code": "01007", "name": "库门07" },
{ "code": "01008", "name": "库门08" },
{ "code": "01009", "name": "库门09" },
{ "code": "01010", "name": "库门10" },
{ "code": "01011", "name": "库门11" },
{ "code": "01012", "name": "库门12" },
{ "code": "01013", "name": "库门13" },
{ "code": "01014", "name": "库门14" },
{ "code": "01015", "name": "库门15" },
{ "code": "01016", "name": "库门16" },
{ "code": "01017", "name": "库门17" },
{ "code": "01018", "name": "库门18" },
{ "code": "01019", "name": "库门19" },
{ "code": "01020", "name": "库门20" },
{ "code": "01031", "name": "库门31" },
{ "code": "01032", "name": "库门32" },
{ "code": "01033", "name": "库门33" },
{ "code": "01034", "name": "库门34" },
{ "code": "01035", "name": "库门35" },
{ "code": "01036", "name": "库门36" },
{ "code": "01037", "name": "库门37" },
{ "code": "01038", "name": "库门38" },
{ "code": "01039", "name": "库门39" },
{ "code": "01040", "name": "库门40" },
{ "code": "01041", "name": "库门41" },
{ "code": "01042", "name": "库门42" },
{ "code": "01043", "name": "库门43" },
{ "code": "01044", "name": "库门44" },
{ "code": "01045", "name": "库门45" },
{ "code": "01046", "name": "库门46" },
{ "code": "01047", "name": "库门47" },
{ "code": "01048", "name": "库门48" },
{ "code": "01049", "name": "库门49" },
{ "code": "01050", "name": "库门50" }
]
},
{
"code": "02",
"name": "其它车库",
"subs": [
{ "code": "02001", "name": "检修库01" },
{ "code": "02002", "name": "检修库02" },
{ "code": "02003", "name": "检修库03" },
{ "code": "02004", "name": "检修库04" },
{ "code": "02005", "name": "检修库05" },
{ "code": "02006", "name": "洗车库01" },
{ "code": "02007", "name": "洗车库02" },
{ "code": "02008", "name": "洗车库03" },
{ "code": "02009", "name": "洗车库04" },
{ "code": "02010", "name": "洗车库05" },
{ "code": "02011", "name": "镟轮库01" },
{ "code": "02012", "name": "镟轮库02" },
{ "code": "02013", "name": "镟轮库03" },
{ "code": "02014", "name": "镟轮库04" },
{ "code": "02015", "name": "镟轮库05" },
{ "code": "02016", "name": "地下机动车库01" },
{ "code": "02017", "name": "地下机动车库02" },
{ "code": "02018", "name": "地下机动车库03" },
{ "code": "02019", "name": "地下机动车库04" },
{ "code": "02020", "name": "地下机动车库05" },
{ "code": "02021", "name": "特种车库01" },
{ "code": "02022", "name": "特种车库02" },
{ "code": "02023", "name": "特种车库03" },
{ "code": "02024", "name": "特种车库04" },
{ "code": "02025", "name": "特种车库05" },
{ "code": "02026", "name": "联合车库01" },
{ "code": "02027", "name": "联合车库02" },
{ "code": "02028", "name": "联合车库03" },
{ "code": "02029", "name": "联合车库04" },
{ "code": "02030", "name": "联合车库05" },
{ "code": "02031", "name": "静调库01" },
{ "code": "02032", "name": "静调库02" },
{ "code": "02033", "name": "静调库03" },
{ "code": "02034", "name": "静调库04" },
{ "code": "02035", "name": "静调库05" }
]
},
{
"code": "03",
"name": "办公楼",
"subs": [
{ "code": "03001", "name": "信号楼01" },
{ "code": "03002", "name": "信号楼02" },
{ "code": "03003", "name": "信号楼03" },
{ "code": "03004", "name": "信号楼04" },
{ "code": "03005", "name": "信号楼05" },
{ "code": "03006", "name": "信号楼06" },
{ "code": "03007", "name": "信号楼07" },
{ "code": "03008", "name": "信号楼08" },
{ "code": "03009", "name": "信号楼09" },
{ "code": "03010", "name": "信号楼10" },
{ "code": "03011", "name": "办公楼01" },
{ "code": "03012", "name": "办公楼02" },
{ "code": "03013", "name": "办公楼03" },
{ "code": "03014", "name": "办公楼04" },
{ "code": "03015", "name": "办公楼05" },
{ "code": "03016", "name": "办公楼06" },
{ "code": "03017", "name": "办公楼07" },
{ "code": "03018", "name": "办公楼08" },
{ "code": "03019", "name": "办公楼09" },
{ "code": "03020", "name": "办公楼10" }
]
},
{
"code": "04",
"name": "出入口",
"subs": [
{ "code": "04001", "name": "出入口01" },
{ "code": "04002", "name": "出入口02" },
{ "code": "04003", "name": "出入口03" },
{ "code": "04004", "name": "出入口04" },
{ "code": "04005", "name": "出入口05" },
{ "code": "04006", "name": "喉部01" },
{ "code": "04007", "name": "喉部02" },
{ "code": "04008", "name": "喉部03" },
{ "code": "04009", "name": "喉部04" },
{ "code": "04010", "name": "喉部05" },
{ "code": "04011", "name": "峒口01" },
{ "code": "04012", "name": "峒口02" },
{ "code": "04013", "name": "峒口03" },
{ "code": "04014", "name": "峒口04" },
{ "code": "04015", "name": "峒口05" }
]
},
{
"code": "05",
"name": "周界",
"subs": [
{ "code": "05001", "name": "周界01" },
{ "code": "05002", "name": "周界02" },
{ "code": "05003", "name": "周界03" },
{ "code": "05004", "name": "周界04" },
{ "code": "05005", "name": "周界05" },
{ "code": "05006", "name": "周界06" },
{ "code": "05007", "name": "周界07" },
{ "code": "05008", "name": "周界08" },
{ "code": "05009", "name": "周界09" },
{ "code": "05010", "name": "周界10" }
]
},
{
"code": "06",
"name": "仓库",
"subs": [
{ "code": "06001", "name": "危险物品仓库01" },
{ "code": "06002", "name": "危险物品仓库02" },
{ "code": "06003", "name": "危险物品仓库03" },
{ "code": "06004", "name": "危险物品仓库04" },
{ "code": "06005", "name": "危险物品仓库05" },
{ "code": "06006", "name": "物流仓库01" },
{ "code": "06007", "name": "物流仓库02" },
{ "code": "06008", "name": "物流仓库03" },
{ "code": "06009", "name": "物流仓库04" },
{ "code": "06010", "name": "物流仓库05" }
]
},
{
"code": "07",
"name": "道路",
"subs": [
{ "code": "07001", "name": "道路01" },
{ "code": "07002", "name": "道路02" },
{ "code": "07003", "name": "道路03" },
{ "code": "07004", "name": "道路04" },
{ "code": "07005", "name": "道路05" }
]
},
{
"code": "08",
"name": "其它",
"subs": [
{ "code": "08001", "name": "变电站01" },
{ "code": "08002", "name": "变电站02" },
{ "code": "08003", "name": "变电站03" },
{ "code": "08004", "name": "变电站04" },
{ "code": "08005", "name": "变电站05" }
]
},
{
"code": "11",
"name": "停车库(录)",
"subs": [
{ "code": "11001", "name": "库门01" },
{ "code": "11002", "name": "库门02" },
{ "code": "11003", "name": "库门03" },
{ "code": "11004", "name": "库门04" },
{ "code": "11005", "name": "库门05" },
{ "code": "11006", "name": "库门06" },
{ "code": "11007", "name": "库门07" },
{ "code": "11008", "name": "库门08" },
{ "code": "11009", "name": "库门09" },
{ "code": "11010", "name": "库门10" },
{ "code": "11011", "name": "库门11" },
{ "code": "11012", "name": "库门12" },
{ "code": "11013", "name": "库门13" },
{ "code": "11014", "name": "库门14" },
{ "code": "11015", "name": "库门15" },
{ "code": "11016", "name": "库门16" },
{ "code": "11017", "name": "库门17" },
{ "code": "11018", "name": "库门18" },
{ "code": "11019", "name": "库门19" },
{ "code": "11020", "name": "库门20" },
{ "code": "11031", "name": "库门31" },
{ "code": "11032", "name": "库门32" },
{ "code": "11033", "name": "库门33" },
{ "code": "11034", "name": "库门34" },
{ "code": "11035", "name": "库门35" },
{ "code": "11036", "name": "库门36" },
{ "code": "11037", "name": "库门37" },
{ "code": "11038", "name": "库门38" },
{ "code": "11039", "name": "库门39" },
{ "code": "11040", "name": "库门40" },
{ "code": "11041", "name": "库门41" },
{ "code": "11042", "name": "库门42" },
{ "code": "11043", "name": "库门43" },
{ "code": "11044", "name": "库门44" },
{ "code": "11045", "name": "库门45" },
{ "code": "11046", "name": "库门46" },
{ "code": "11047", "name": "库门47" },
{ "code": "11048", "name": "库门48" },
{ "code": "11049", "name": "库门49" },
{ "code": "11050", "name": "库门50" }
]
},
{
"code": "12",
"name": "其它车库(录)",
"subs": [
{ "code": "12001", "name": "检修库01" },
{ "code": "12002", "name": "检修库02" },
{ "code": "12003", "name": "检修库03" },
{ "code": "12004", "name": "检修库04" },
{ "code": "12005", "name": "检修库05" },
{ "code": "12006", "name": "洗车库01" },
{ "code": "12007", "name": "洗车库02" },
{ "code": "12008", "name": "洗车库03" },
{ "code": "12009", "name": "洗车库04" },
{ "code": "12010", "name": "洗车库05" },
{ "code": "12011", "name": "镟轮库01" },
{ "code": "12012", "name": "镟轮库02" },
{ "code": "12013", "name": "镟轮库03" },
{ "code": "12014", "name": "镟轮库04" },
{ "code": "12015", "name": "镟轮库05" },
{ "code": "12016", "name": "地下机动车库01" },
{ "code": "12017", "name": "地下机动车库02" },
{ "code": "12018", "name": "地下机动车库03" },
{ "code": "12019", "name": "地下机动车库04" },
{ "code": "12020", "name": "地下机动车库05" },
{ "code": "12021", "name": "特种车库01" },
{ "code": "12022", "name": "特种车库02" },
{ "code": "12023", "name": "特种车库03" },
{ "code": "12024", "name": "特种车库04" },
{ "code": "12025", "name": "特种车库05" },
{ "code": "12026", "name": "联合车库01" },
{ "code": "12027", "name": "联合车库02" },
{ "code": "12028", "name": "联合车库03" },
{ "code": "12029", "name": "联合车库04" },
{ "code": "12030", "name": "联合车库05" },
{ "code": "12031", "name": "静调库01" },
{ "code": "12032", "name": "静调库02" },
{ "code": "12033", "name": "静调库03" },
{ "code": "12034", "name": "静调库04" },
{ "code": "12035", "name": "静调库05" }
]
},
{
"code": "13",
"name": "办公楼(录)",
"subs": [
{ "code": "13001", "name": "信号楼01" },
{ "code": "13002", "name": "信号楼02" },
{ "code": "13003", "name": "信号楼03" },
{ "code": "13004", "name": "信号楼04" },
{ "code": "13005", "name": "信号楼05" },
{ "code": "13006", "name": "信号楼06" },
{ "code": "13007", "name": "信号楼07" },
{ "code": "13008", "name": "信号楼08" },
{ "code": "13009", "name": "信号楼09" },
{ "code": "13010", "name": "信号楼10" },
{ "code": "13011", "name": "办公楼01" },
{ "code": "13012", "name": "办公楼02" },
{ "code": "13013", "name": "办公楼03" },
{ "code": "13014", "name": "办公楼04" },
{ "code": "13015", "name": "办公楼05" },
{ "code": "13016", "name": "办公楼06" },
{ "code": "13017", "name": "办公楼07" },
{ "code": "13018", "name": "办公楼08" },
{ "code": "13019", "name": "办公楼09" },
{ "code": "13020", "name": "办公楼10" }
]
},
{
"code": "14",
"name": "出入口(录)",
"subs": [
{ "code": "14001", "name": "出入口01" },
{ "code": "14002", "name": "出入口02" },
{ "code": "14003", "name": "出入口03" },
{ "code": "14004", "name": "出入口04" },
{ "code": "14005", "name": "出入口05" },
{ "code": "14006", "name": "喉部01" },
{ "code": "14007", "name": "喉部02" },
{ "code": "14008", "name": "喉部03" },
{ "code": "14009", "name": "喉部04" },
{ "code": "14010", "name": "喉部05" },
{ "code": "14011", "name": "峒口01" },
{ "code": "14012", "name": "峒口02" },
{ "code": "14013", "name": "峒口03" },
{ "code": "14014", "name": "峒口04" },
{ "code": "14015", "name": "峒口05" }
]
},
{
"code": "15",
"name": "周界(录)",
"subs": [
{ "code": "15001", "name": "周界01" },
{ "code": "15002", "name": "周界02" },
{ "code": "15003", "name": "周界03" },
{ "code": "15004", "name": "周界04" },
{ "code": "15005", "name": "周界05" },
{ "code": "15006", "name": "周界06" },
{ "code": "15007", "name": "周界07" },
{ "code": "15008", "name": "周界08" },
{ "code": "15009", "name": "周界09" },
{ "code": "15010", "name": "周界10" }
]
},
{
"code": "16",
"name": "仓库(录)",
"subs": [
{ "code": "16001", "name": "危险物品仓库01" },
{ "code": "16002", "name": "危险物品仓库02" },
{ "code": "16003", "name": "危险物品仓库03" },
{ "code": "16004", "name": "危险物品仓库04" },
{ "code": "16005", "name": "危险物品仓库05" },
{ "code": "16006", "name": "物流仓库01" },
{ "code": "16007", "name": "物流仓库02" },
{ "code": "16008", "name": "物流仓库03" },
{ "code": "16009", "name": "物流仓库04" },
{ "code": "16010", "name": "物流仓库05" }
]
},
{
"code": "17",
"name": "道路(录)",
"subs": [
{ "code": "17001", "name": "道路01" },
{ "code": "17002", "name": "道路02" },
{ "code": "17003", "name": "道路03" },
{ "code": "17004", "name": "道路04" },
{ "code": "17005", "name": "道路05" }
]
},
{
"code": "18",
"name": "其它(录)",
"subs": [
{ "code": "18001", "name": "变电站01" },
{ "code": "18002", "name": "变电站02" },
{ "code": "18003", "name": "变电站03" },
{ "code": "18004", "name": "变电站04" },
{ "code": "18005", "name": "变电站05" }
]
}
]
-470
View File
@@ -1,470 +0,0 @@
[
{
"code": "01",
"name": "站厅层",
"subs": [
{ "code": "01001", "name": "客服中心01" },
{ "code": "01002", "name": "客服中心02" },
{ "code": "01003", "name": "客服中心03" },
{ "code": "01004", "name": "客服中心04" },
{ "code": "01005", "name": "闸机组01" },
{ "code": "01006", "name": "闸机组02" },
{ "code": "01007", "name": "闸机组03" },
{ "code": "01008", "name": "闸机组04" },
{ "code": "01009", "name": "闸机组05" },
{ "code": "01010", "name": "闸机组06" },
{ "code": "01011", "name": "闸机组07" },
{ "code": "01012", "name": "闸机组08" },
{ "code": "01013", "name": "闸机组09" },
{ "code": "01014", "name": "闸机组10" },
{ "code": "01015", "name": "闸机组11" },
{ "code": "01016", "name": "闸机组12" },
{ "code": "01017", "name": "闸机组13" },
{ "code": "01018", "name": "闸机组14" },
{ "code": "01019", "name": "闸机组15" },
{ "code": "01020", "name": "闸机组16" },
{ "code": "01021", "name": "闸机组17" },
{ "code": "01022", "name": "闸机组18" },
{ "code": "01023", "name": "闸机组19" },
{ "code": "01024", "name": "闸机组20" },
{ "code": "01025", "name": "人工售票01" },
{ "code": "01026", "name": "人工售票02" },
{ "code": "01027", "name": "人工售票03" },
{ "code": "01028", "name": "人工售票04" },
{ "code": "01029", "name": "人工售票05" },
{ "code": "01030", "name": "自动售票01" },
{ "code": "01031", "name": "自动售票02" },
{ "code": "01032", "name": "自动售票03" },
{ "code": "01033", "name": "自动售票04" },
{ "code": "01034", "name": "自动售票05" },
{ "code": "01035", "name": "站厅球机" },
{ "code": "01036", "name": "直升梯01" },
{ "code": "01037", "name": "直升梯02" },
{ "code": "01038", "name": "直升梯03" },
{ "code": "01039", "name": "直升梯04" },
{ "code": "01040", "name": "公共区01" },
{ "code": "01041", "name": "公共区02" },
{ "code": "01042", "name": "公共区03" },
{ "code": "01043", "name": "公共区04" },
{ "code": "01044", "name": "公共区05" },
{ "code": "01045", "name": "站厅楼梯01" },
{ "code": "01046", "name": "站厅楼梯02" },
{ "code": "01047", "name": "站厅楼梯03" },
{ "code": "01048", "name": "站厅楼梯04" },
{ "code": "01049", "name": "站厅楼梯05" },
{ "code": "01050", "name": "站厅扶梯01" },
{ "code": "01051", "name": "站厅扶梯02" },
{ "code": "01052", "name": "站厅扶梯03" },
{ "code": "01053", "name": "站厅扶梯04" },
{ "code": "01054", "name": "站厅扶梯05" },
{ "code": "01055", "name": "01号口" },
{ "code": "01056", "name": "02号口" },
{ "code": "01057", "name": "03号口" },
{ "code": "01058", "name": "04号口" },
{ "code": "01059", "name": "05号口" },
{ "code": "01060", "name": "06号口" },
{ "code": "01061", "name": "07号口" },
{ "code": "01062", "name": "08号口" },
{ "code": "01063", "name": "09号口" },
{ "code": "01064", "name": "10号口" },
{ "code": "01065", "name": "11号口" },
{ "code": "01066", "name": "12号口" },
{ "code": "01067", "name": "13号口" },
{ "code": "01068", "name": "14号口" },
{ "code": "01069", "name": "15号口" },
{ "code": "01070", "name": "16号口" },
{ "code": "01071", "name": "17号口" },
{ "code": "01072", "name": "18号口" },
{ "code": "01073", "name": "19号口" },
{ "code": "01074", "name": "20号口" },
{ "code": "01075", "name": "21号口" },
{ "code": "01076", "name": "22号口" },
{ "code": "01077", "name": "23号口" },
{ "code": "01078", "name": "24号口" },
{ "code": "01079", "name": "25号口" },
{ "code": "01080", "name": "26号口" },
{ "code": "01081", "name": "换乘厅01" },
{ "code": "01082", "name": "换乘厅02" },
{ "code": "01083", "name": "长通道01" },
{ "code": "01084", "name": "长通道02" },
{ "code": "01085", "name": "安检区" },
{ "code": "01086", "name": "预留1" },
{ "code": "01087", "name": "预留2" },
{ "code": "01088", "name": "预留3" }
]
},
{
"code": "02",
"name": "站台层",
"subs": [
{ "code": "02001", "name": "站台球机" },
{ "code": "02002", "name": "公共区01" },
{ "code": "02003", "name": "公共区02" },
{ "code": "02004", "name": "公共区03" },
{ "code": "02005", "name": "公共区04" },
{ "code": "02006", "name": "公共区05" },
{ "code": "02007", "name": "上行01" },
{ "code": "02008", "name": "上行02" },
{ "code": "02009", "name": "上行03" },
{ "code": "02010", "name": "上行04" },
{ "code": "02011", "name": "上行05" },
{ "code": "02012", "name": "下行01" },
{ "code": "02013", "name": "下行02" },
{ "code": "02014", "name": "下行03" },
{ "code": "02015", "name": "下行04" },
{ "code": "02016", "name": "下行05" },
{ "code": "02017", "name": "站台楼梯01" },
{ "code": "02018", "name": "站台楼梯02" },
{ "code": "02019", "name": "站台楼梯03" },
{ "code": "02020", "name": "站台楼梯04" },
{ "code": "02021", "name": "站台楼梯05" },
{ "code": "02022", "name": "预留1" },
{ "code": "02023", "name": "预留2" },
{ "code": "02024", "name": "预留3" }
]
},
{
"code": "03",
"name": "设备区",
"subs": [
{ "code": "03001", "name": "站厅走道01" },
{ "code": "03002", "name": "站厅走道02" },
{ "code": "03003", "name": "站厅走道03" },
{ "code": "03004", "name": "站厅走道04" },
{ "code": "03005", "name": "站厅走道05" },
{ "code": "03006", "name": "站厅走道06" },
{ "code": "03007", "name": "站厅走道07" },
{ "code": "03008", "name": "站厅走道08" },
{ "code": "03009", "name": "站厅走道09" },
{ "code": "03010", "name": "站厅走道10" },
{ "code": "03011", "name": "站厅楼梯01" },
{ "code": "03012", "name": "站厅楼梯02" },
{ "code": "03013", "name": "站厅楼梯03" },
{ "code": "03014", "name": "站厅楼梯04" },
{ "code": "03015", "name": "站厅楼梯05" },
{ "code": "03016", "name": "站厅楼梯06" },
{ "code": "03017", "name": "站厅楼梯07" },
{ "code": "03018", "name": "站厅楼梯08" },
{ "code": "03019", "name": "站厅楼梯09" },
{ "code": "03020", "name": "站厅楼梯10" },
{ "code": "03021", "name": "站台走道01" },
{ "code": "03022", "name": "站台走道02" },
{ "code": "03023", "name": "站台走道03" },
{ "code": "03024", "name": "站台走道04" },
{ "code": "03025", "name": "站台走道05" },
{ "code": "03026", "name": "站台走道06" },
{ "code": "03027", "name": "站台走道07" },
{ "code": "03028", "name": "站台走道08" },
{ "code": "03029", "name": "站台走道09" },
{ "code": "03030", "name": "站台走道10" },
{ "code": "03031", "name": "站台楼梯01" },
{ "code": "03032", "name": "站台楼梯02" },
{ "code": "03033", "name": "站台楼梯03" },
{ "code": "03034", "name": "站台楼梯04" },
{ "code": "03035", "name": "站台楼梯05" },
{ "code": "03036", "name": "站台楼梯06" },
{ "code": "03037", "name": "站台楼梯07" },
{ "code": "03038", "name": "站台楼梯08" },
{ "code": "03039", "name": "站台楼梯09" },
{ "code": "03040", "name": "站台楼梯10" },
{ "code": "03041", "name": "编码室" },
{ "code": "03042", "name": "车控室01" },
{ "code": "03043", "name": "车控室02" },
{ "code": "03044", "name": "主变电站01" },
{ "code": "03045", "name": "主变电站02" },
{ "code": "03046", "name": "降压变电所01" },
{ "code": "03047", "name": "降压变电所02" },
{ "code": "03048", "name": "通信设备室" },
{ "code": "03049", "name": "信号设备室" },
{ "code": "03050", "name": "弱电电源室" },
{ "code": "03051", "name": "民用通信机房" },
{ "code": "03052", "name": "区间通风机房" },
{ "code": "03053", "name": "环控机房" },
{ "code": "03054", "name": "冷水机房" },
{ "code": "03055", "name": "环控电控室" },
{ "code": "03056", "name": "线间楼梯01" },
{ "code": "03057", "name": "线间楼梯02" },
{ "code": "03058", "name": "线间楼梯03" },
{ "code": "03059", "name": "线间楼梯04" },
{ "code": "03060", "name": "线间楼梯05" },
{ "code": "03061", "name": "线间通道01" },
{ "code": "03062", "name": "线间通道02" },
{ "code": "03063", "name": "线间通道03" },
{ "code": "03064", "name": "线间通道04" },
{ "code": "03065", "name": "线间通道05" },
{ "code": "03066", "name": "站长室外" },
{ "code": "03067", "name": "气瓶间" },
{ "code": "03068", "name": "消防泵房" },
{ "code": "03069", "name": "钢瓶室" },
{ "code": "03070", "name": "民用通信机房" },
{ "code": "03071", "name": "民用机房" },
{ "code": "03074", "name": "公网引入室" },
{ "code": "03075", "name": "环控电控室" },
{ "code": "03076", "name": "UPS间" }
]
},
{
"code": "04",
"name": "区间内",
"subs": [
{ "code": "04001", "name": "区间变电01" },
{ "code": "04002", "name": "区间变电02" },
{ "code": "04003", "name": "区间变电03" },
{ "code": "04004", "name": "区间变电04" },
{ "code": "04005", "name": "区间变电05" },
{ "code": "04006", "name": "上行峒口" },
{ "code": "04007", "name": "下行峒口" },
{ "code": "04008", "name": "上行轨道" },
{ "code": "04009", "name": "下行轨道" },
{ "code": "04010", "name": "上行风井" },
{ "code": "04011", "name": "下行风井" },
{ "code": "04012", "name": "旁通道" },
{ "code": "04013", "name": "道岔" },
{ "code": "04014", "name": "预留1" },
{ "code": "04015", "name": "预留2" },
{ "code": "04016", "name": "预留3" }
]
},
{
"code": "05",
"name": "派出所",
"subs": [
{ "code": "05001", "name": "监控室" },
{ "code": "05002", "name": "综合执法站" },
{ "code": "05003", "name": "预留1" },
{ "code": "05004", "name": "预留2" },
{ "code": "05005", "name": "预留3" }
]
},
{
"code": "06",
"name": "站厅层(录)",
"subs": [
{ "code": "06001", "name": "客服中心01" },
{ "code": "06002", "name": "客服中心02" },
{ "code": "06003", "name": "客服中心03" },
{ "code": "06004", "name": "客服中心04" },
{ "code": "06005", "name": "闸机组01" },
{ "code": "06006", "name": "闸机组02" },
{ "code": "06007", "name": "闸机组03" },
{ "code": "06008", "name": "闸机组04" },
{ "code": "06009", "name": "闸机组05" },
{ "code": "06010", "name": "闸机组06" },
{ "code": "06011", "name": "闸机组07" },
{ "code": "06012", "name": "闸机组08" },
{ "code": "06013", "name": "闸机组09" },
{ "code": "06014", "name": "闸机组10" },
{ "code": "06015", "name": "闸机组11" },
{ "code": "06016", "name": "闸机组12" },
{ "code": "06017", "name": "闸机组13" },
{ "code": "06018", "name": "闸机组14" },
{ "code": "06019", "name": "闸机组15" },
{ "code": "06020", "name": "闸机组16" },
{ "code": "06021", "name": "闸机组17" },
{ "code": "06022", "name": "闸机组18" },
{ "code": "06023", "name": "闸机组19" },
{ "code": "06024", "name": "闸机组20" },
{ "code": "06025", "name": "人工售票01" },
{ "code": "06026", "name": "人工售票02" },
{ "code": "06027", "name": "人工售票03" },
{ "code": "06028", "name": "人工售票04" },
{ "code": "06029", "name": "人工售票05" },
{ "code": "06030", "name": "自动售票01" },
{ "code": "06031", "name": "自动售票02" },
{ "code": "06032", "name": "自动售票03" },
{ "code": "06033", "name": "自动售票04" },
{ "code": "06034", "name": "自动售票05" },
{ "code": "06035", "name": "站厅球机" },
{ "code": "06036", "name": "直升梯01" },
{ "code": "06037", "name": "直升梯02" },
{ "code": "06038", "name": "直升梯03" },
{ "code": "06039", "name": "直升梯04" },
{ "code": "06040", "name": "公共区01" },
{ "code": "06041", "name": "公共区02" },
{ "code": "06042", "name": "公共区03" },
{ "code": "06043", "name": "公共区04" },
{ "code": "06044", "name": "公共区05" },
{ "code": "06045", "name": "站厅楼梯01" },
{ "code": "06046", "name": "站厅楼梯02" },
{ "code": "06047", "name": "站厅楼梯03" },
{ "code": "06048", "name": "站厅楼梯04" },
{ "code": "06049", "name": "站厅楼梯05" },
{ "code": "06050", "name": "站厅扶梯01" },
{ "code": "06051", "name": "站厅扶梯02" },
{ "code": "06052", "name": "站厅扶梯03" },
{ "code": "06053", "name": "站厅扶梯04" },
{ "code": "06054", "name": "站厅扶梯05" },
{ "code": "06055", "name": "01号口" },
{ "code": "06056", "name": "02号口" },
{ "code": "06057", "name": "03号口" },
{ "code": "06058", "name": "04号口" },
{ "code": "06059", "name": "05号口" },
{ "code": "06060", "name": "06号口" },
{ "code": "06061", "name": "07号口" },
{ "code": "06062", "name": "08号口" },
{ "code": "06063", "name": "09号口" },
{ "code": "06064", "name": "10号口" },
{ "code": "06065", "name": "11号口" },
{ "code": "06066", "name": "12号口" },
{ "code": "06067", "name": "13号口" },
{ "code": "06068", "name": "14号口" },
{ "code": "06069", "name": "15号口" },
{ "code": "06070", "name": "16号口" },
{ "code": "06071", "name": "17号口" },
{ "code": "06072", "name": "18号口" },
{ "code": "06073", "name": "19号口" },
{ "code": "06074", "name": "20号口" },
{ "code": "06075", "name": "21号口" },
{ "code": "06076", "name": "22号口" },
{ "code": "06077", "name": "23号口" },
{ "code": "06078", "name": "24号口" },
{ "code": "06079", "name": "25号口" },
{ "code": "06080", "name": "26号口" },
{ "code": "06081", "name": "换乘厅01" },
{ "code": "06082", "name": "换乘厅02" },
{ "code": "06083", "name": "长通道01" },
{ "code": "06084", "name": "长通道02" },
{ "code": "06085", "name": "安检区" },
{ "code": "06086", "name": "预留1" },
{ "code": "06087", "name": "预留2" },
{ "code": "06088", "name": "预留3" }
]
},
{
"code": "07",
"name": "站台层(录)",
"subs": [
{ "code": "07001", "name": "站台球机" },
{ "code": "07002", "name": "公共区01" },
{ "code": "07003", "name": "公共区02" },
{ "code": "07004", "name": "公共区03" },
{ "code": "07005", "name": "公共区04" },
{ "code": "07006", "name": "公共区05" },
{ "code": "07007", "name": "上行01" },
{ "code": "07008", "name": "上行02" },
{ "code": "07009", "name": "上行03" },
{ "code": "07010", "name": "上行04" },
{ "code": "07011", "name": "上行05" },
{ "code": "07012", "name": "下行01" },
{ "code": "07013", "name": "下行02" },
{ "code": "07014", "name": "下行03" },
{ "code": "07015", "name": "下行04" },
{ "code": "07016", "name": "下行05" },
{ "code": "07017", "name": "站台楼梯01" },
{ "code": "07018", "name": "站台楼梯02" },
{ "code": "07019", "name": "站台楼梯03" },
{ "code": "07020", "name": "站台楼梯04" },
{ "code": "07021", "name": "站台楼梯05" },
{ "code": "07022", "name": "预留1" },
{ "code": "07023", "name": "预留2" },
{ "code": "07024", "name": "预留3" }
]
},
{
"code": "08",
"name": "设备区(录)",
"subs": [
{ "code": "08001", "name": "站厅走道01" },
{ "code": "08002", "name": "站厅走道02" },
{ "code": "08003", "name": "站厅走道03" },
{ "code": "08004", "name": "站厅走道04" },
{ "code": "08005", "name": "站厅走道05" },
{ "code": "08006", "name": "站厅走道06" },
{ "code": "08007", "name": "站厅走道07" },
{ "code": "08008", "name": "站厅走道08" },
{ "code": "08009", "name": "站厅走道09" },
{ "code": "08010", "name": "站厅走道10" },
{ "code": "08011", "name": "站厅楼梯01" },
{ "code": "08012", "name": "站厅楼梯02" },
{ "code": "08013", "name": "站厅楼梯03" },
{ "code": "08014", "name": "站厅楼梯04" },
{ "code": "08015", "name": "站厅楼梯05" },
{ "code": "08016", "name": "站厅楼梯06" },
{ "code": "08017", "name": "站厅楼梯07" },
{ "code": "08018", "name": "站厅楼梯08" },
{ "code": "08019", "name": "站厅楼梯09" },
{ "code": "08020", "name": "站厅楼梯10" },
{ "code": "08021", "name": "站台走道01" },
{ "code": "08022", "name": "站台走道02" },
{ "code": "08023", "name": "站台走道03" },
{ "code": "08024", "name": "站台走道04" },
{ "code": "08025", "name": "站台走道05" },
{ "code": "08026", "name": "站台走道06" },
{ "code": "08027", "name": "站台走道07" },
{ "code": "08028", "name": "站台走道08" },
{ "code": "08029", "name": "站台走道09" },
{ "code": "08030", "name": "站台走道10" },
{ "code": "08031", "name": "站台楼梯01" },
{ "code": "08032", "name": "站台楼梯02" },
{ "code": "08033", "name": "站台楼梯03" },
{ "code": "08034", "name": "站台楼梯04" },
{ "code": "08035", "name": "站台楼梯05" },
{ "code": "08036", "name": "站台楼梯06" },
{ "code": "08037", "name": "站台楼梯07" },
{ "code": "08038", "name": "站台楼梯08" },
{ "code": "08039", "name": "站台楼梯09" },
{ "code": "08040", "name": "站台楼梯10" },
{ "code": "08041", "name": "编码室" },
{ "code": "08042", "name": "车控室01" },
{ "code": "08043", "name": "车控室02" },
{ "code": "08044", "name": "主变电站01" },
{ "code": "08045", "name": "主变电站02" },
{ "code": "08046", "name": "降压变电所01" },
{ "code": "08047", "name": "降压变电所02" },
{ "code": "08048", "name": "通信设备室" },
{ "code": "08049", "name": "信号设备室" },
{ "code": "08050", "name": "弱电电源室" },
{ "code": "08051", "name": "民用通信机房" },
{ "code": "08052", "name": "区间通风机房" },
{ "code": "08053", "name": "环控机房" },
{ "code": "08054", "name": "冷水机房" },
{ "code": "08055", "name": "环控电控室" },
{ "code": "08056", "name": "线间楼梯01" },
{ "code": "08057", "name": "线间楼梯02" },
{ "code": "08058", "name": "线间楼梯03" },
{ "code": "08059", "name": "线间楼梯04" },
{ "code": "08060", "name": "线间楼梯05" },
{ "code": "08061", "name": "线间通道01" },
{ "code": "08062", "name": "线间通道02" },
{ "code": "08063", "name": "线间通道03" },
{ "code": "08064", "name": "线间通道04" },
{ "code": "08065", "name": "线间通道05" },
{ "code": "08066", "name": "预留1" },
{ "code": "08067", "name": "预留2" },
{ "code": "08068", "name": "预留3" }
]
},
{
"code": "09",
"name": "区间内(录)",
"subs": [
{ "code": "09001", "name": "区间变电01" },
{ "code": "09002", "name": "区间变电02" },
{ "code": "09003", "name": "区间变电03" },
{ "code": "09004", "name": "区间变电04" },
{ "code": "09005", "name": "区间变电05" },
{ "code": "09006", "name": "上行峒口" },
{ "code": "09007", "name": "下行峒口" },
{ "code": "09008", "name": "上行轨道" },
{ "code": "09009", "name": "下行轨道" },
{ "code": "09010", "name": "上行风井" },
{ "code": "09011", "name": "下行风井" },
{ "code": "09012", "name": "旁通道" },
{ "code": "09013", "name": "道岔" },
{ "code": "09014", "name": "预留1" },
{ "code": "09015", "name": "预留2" },
{ "code": "09016", "name": "预留3" }
]
},
{
"code": "10",
"name": "派出所(录)",
"subs": [
{ "code": "10001", "name": "监控室" },
{ "code": "10002", "name": "综合执法站" },
{ "code": "10003", "name": "预留1" },
{ "code": "10004", "name": "预留2" },
{ "code": "10005", "name": "预留3" }
]
}
]
-753
View File
@@ -1,753 +0,0 @@
{
"901101": { "name": "虹桥枢纽", "type": "station" },
"901102": { "name": "中春路", "type": "station" },
"901103": { "name": "景洪路", "type": "station" },
"901104": { "name": "三林南", "type": "station" },
"901105": { "name": "康桥东", "type": "station" },
"901106": { "name": "度假区", "type": "station" },
"901107": { "name": "浦东机场", "type": "station" },
"901108": { "name": "T3航站楼", "type": "station" },
"901109": { "name": "上海东站", "type": "station" },
"901180": { "name": "临时检修基地", "type": "parking" },
"901181": { "name": "申昆路停车场", "type": "parking" },
"901182": { "name": "下盐路停车场", "type": "parking" },
"001601": { "name": "莘庄站", "type": "station" },
"001602": { "name": "外环路站", "type": "station" },
"001603": { "name": "莲花路站", "type": "station" },
"001604": { "name": "锦江乐园站", "type": "station" },
"001605": { "name": "上海南站站", "type": "station" },
"001606": { "name": "漕宝路站", "type": "station" },
"001607": { "name": "上海体育馆站", "type": "station" },
"001608": { "name": "徐家汇站", "type": "station" },
"001609": { "name": "衡山路站", "type": "station" },
"001610": { "name": "常熟路站", "type": "station" },
"001611": { "name": "陕西南路站", "type": "station" },
"001612": { "name": "黄陂南路站", "type": "station" },
"001613": { "name": "人民广场站", "type": "station" },
"001614": { "name": "新闸路站", "type": "station" },
"001615": { "name": "汉中路站", "type": "station" },
"001616": { "name": "上海火车站站", "type": "station" },
"001617": { "name": "中山北路站", "type": "station" },
"001618": { "name": "延长路站", "type": "station" },
"001619": { "name": "上海马戏城站", "type": "station" },
"001620": { "name": "汶水路站", "type": "station" },
"001621": { "name": "彭浦新村站", "type": "station" },
"001622": { "name": "共康路站", "type": "station" },
"001623": { "name": "通河新村站", "type": "station" },
"001624": { "name": "呼兰路站", "type": "station" },
"001625": { "name": "共富新村站", "type": "station" },
"001626": { "name": "宝安公路站", "type": "station" },
"001627": { "name": "友谊西路站", "type": "station" },
"001628": { "name": "富锦路站", "type": "station" },
"001676": { "name": "新闸路OCC", "type": "occ" },
"001680": { "name": "富锦路基地", "type": "parking" },
"001681": { "name": "梅陇基地", "type": "parking" },
"002502": { "name": "淞虹路站", "type": "station" },
"002503": { "name": "北新泾站", "type": "station" },
"002504": { "name": "威宁路站", "type": "station" },
"002505": { "name": "娄山关路站", "type": "station" },
"002506": { "name": "中山公园站", "type": "station" },
"002507": { "name": "江苏路站", "type": "station" },
"002508": { "name": "静安寺站", "type": "station" },
"002509": { "name": "南京西路站", "type": "station" },
"002510": { "name": "人民广场站", "type": "station" },
"002511": { "name": "南京东路站", "type": "station" },
"002512": { "name": "陆家嘴站", "type": "station" },
"002513": { "name": "东昌路站", "type": "station" },
"002514": { "name": "世纪大道站", "type": "station" },
"002515": { "name": "上海科技馆站", "type": "station" },
"002516": { "name": "世纪公园站", "type": "station" },
"002517": { "name": "龙阳路站", "type": "station" },
"002524": { "name": "张江高科站", "type": "station" },
"002525": { "name": "金科路站", "type": "station" },
"002526": { "name": "广兰路站", "type": "station" },
"002527": { "name": "唐镇站", "type": "station" },
"002528": { "name": "创新中路站", "type": "station" },
"002529": { "name": "华夏东路站", "type": "station" },
"002530": { "name": "川沙站", "type": "station" },
"002531": { "name": "凌空路站", "type": "station" },
"002532": { "name": "远东大道站", "type": "station" },
"002533": { "name": "海天三路站", "type": "station" },
"002534": { "name": "浦东机场站", "type": "station" },
"002535": { "name": "虹桥2号航站楼站", "type": "station" },
"002536": { "name": "虹桥火车站站", "type": "station" },
"002537": { "name": "徐泾东站", "type": "station" },
"002575": { "name": "OCC", "type": "occ" },
"002576": { "name": "备OCC", "type": "occ" },
"002580": { "name": "车辆基地0", "type": "parking" },
"002581": { "name": "车辆基地1", "type": "parking" },
"002582": { "name": "车辆基地2", "type": "parking" },
"002599": { "name": "列车", "type": "train" },
"002602": { "name": "淞虹路站", "type": "station" },
"002603": { "name": "北新泾站", "type": "station" },
"002604": { "name": "威宁路站", "type": "station" },
"002605": { "name": "娄山关路站", "type": "station" },
"002606": { "name": "中山公园站", "type": "station" },
"002607": { "name": "江苏路站", "type": "station" },
"002608": { "name": "静安寺站", "type": "station" },
"002609": { "name": "南京西路站", "type": "station" },
"002610": { "name": "人民广场站", "type": "station" },
"002611": { "name": "南京东路站", "type": "station" },
"002612": { "name": "陆家嘴站", "type": "station" },
"002613": { "name": "东昌路站", "type": "station" },
"002614": { "name": "世纪大道站", "type": "station" },
"002615": { "name": "上海科技馆站", "type": "station" },
"002616": { "name": "世纪公园站", "type": "station" },
"002617": { "name": "龙阳路站", "type": "station" },
"002624": { "name": "张江高科站", "type": "station" },
"002625": { "name": "金科路站", "type": "station" },
"002626": { "name": "广兰路站", "type": "station" },
"002627": { "name": "唐镇站", "type": "station" },
"002628": { "name": "创新中路站", "type": "station" },
"002629": { "name": "华夏东路站", "type": "station" },
"002630": { "name": "川沙站", "type": "station" },
"002631": { "name": "凌空路站", "type": "station" },
"002632": { "name": "远东大道站", "type": "station" },
"002633": { "name": "海天三路站", "type": "station" },
"002634": { "name": "浦东机场站", "type": "station" },
"002635": { "name": "虹桥2号航站楼站", "type": "station" },
"002636": { "name": "虹桥火车站站", "type": "station" },
"002637": { "name": "徐泾东站", "type": "station" },
"002675": { "name": "OCC", "type": "occ" },
"002676": { "name": "备OCC", "type": "occ" },
"002680": { "name": "车辆基地0", "type": "parking" },
"002681": { "name": "车辆基地1", "type": "parking" },
"002682": { "name": "车辆基地2", "type": "parking" },
"002699": { "name": "列车", "type": "train" },
"003601": { "name": "上海南站站", "type": "station" },
"003602": { "name": "石龙路站", "type": "station" },
"003603": { "name": "龙漕路站", "type": "station" },
"003604": { "name": "漕溪路站", "type": "station" },
"003605": { "name": "宜山路站", "type": "station" },
"003606": { "name": "虹桥路站", "type": "station" },
"003607": { "name": "延安西路站", "type": "station" },
"003608": { "name": "中山公园站", "type": "station" },
"003609": { "name": "金沙江路站", "type": "station" },
"003610": { "name": "曹杨路站", "type": "station" },
"003611": { "name": "镇坪路站", "type": "station" },
"003612": { "name": "中潭路站", "type": "station" },
"003613": { "name": "上海火车站站", "type": "station" },
"003614": { "name": "宝山路站", "type": "station" },
"003615": { "name": "东宝兴路站", "type": "station" },
"003616": { "name": "虹口足球场站", "type": "station" },
"003617": { "name": "赤峰路站", "type": "station" },
"003618": { "name": "大柏树站", "type": "station" },
"003619": { "name": "江湾镇站", "type": "station" },
"003620": { "name": "殷高西路站", "type": "station" },
"003621": { "name": "长江南路站", "type": "station" },
"003622": { "name": "淞发路站", "type": "station" },
"003623": { "name": "张华浜站", "type": "station" },
"003624": { "name": "淞滨路站", "type": "station" },
"003625": { "name": "水产路站", "type": "station" },
"003626": { "name": "宝杨路站", "type": "station" },
"003627": { "name": "友谊路站", "type": "station" },
"003628": { "name": "铁力路站", "type": "station" },
"003629": { "name": "江杨北路站", "type": "station" },
"003675": { "name": "OCC", "type": "occ" },
"003676": { "name": "备OCC", "type": "occ" },
"003680": { "name": "车辆基地0", "type": "parking" },
"003681": { "name": "车辆基地1", "type": "parking" },
"003682": { "name": "车辆基地2", "type": "parking" },
"003699": { "name": "列车", "type": "train" },
"004501": { "name": "海伦路站", "type": "station" },
"004502": { "name": "临平路站", "type": "station" },
"004503": { "name": "大连路站", "type": "station" },
"004504": { "name": "杨树浦路站", "type": "station" },
"004505": { "name": "浦东大道站", "type": "station" },
"004506": { "name": "世纪大道站", "type": "station" },
"004507": { "name": "浦电路站", "type": "station" },
"004508": { "name": "蓝村路站", "type": "station" },
"004509": { "name": "塘桥站", "type": "station" },
"004510": { "name": "南浦大桥站", "type": "station" },
"004511": { "name": "西藏南路站", "type": "station" },
"004512": { "name": "鲁班路站", "type": "station" },
"004513": { "name": "大木桥路站", "type": "station" },
"004514": { "name": "东安路站", "type": "station" },
"004515": { "name": "上海体育场站", "type": "station" },
"004516": { "name": "上海体育馆站", "type": "station" },
"004517": { "name": "宜山路站", "type": "station" },
"004575": { "name": "OCC", "type": "occ" },
"004576": { "name": "备OCC", "type": "occ" },
"004580": { "name": "车辆基地0", "type": "parking" },
"004581": { "name": "车辆基地1", "type": "parking" },
"004582": { "name": "车辆基地2", "type": "parking" },
"004599": { "name": "列车", "type": "train" },
"004601": { "name": "海伦路站", "type": "station" },
"004602": { "name": "临平路站", "type": "station" },
"004603": { "name": "大连路站", "type": "station" },
"004604": { "name": "杨树浦路站", "type": "station" },
"004605": { "name": "浦东大道站", "type": "station" },
"004606": { "name": "世纪大道站", "type": "station" },
"004607": { "name": "浦电路站", "type": "station" },
"004608": { "name": "蓝村路站", "type": "station" },
"004609": { "name": "塘桥站", "type": "station" },
"004610": { "name": "南浦大桥站", "type": "station" },
"004611": { "name": "西藏南路站", "type": "station" },
"004612": { "name": "鲁班路站", "type": "station" },
"004613": { "name": "大木桥路站", "type": "station" },
"004614": { "name": "东安路站", "type": "station" },
"004615": { "name": "上海体育场站", "type": "station" },
"004616": { "name": "上海体育馆站", "type": "station" },
"004617": { "name": "宜山路站", "type": "station" },
"004675": { "name": "OCC", "type": "occ" },
"004676": { "name": "备OCC", "type": "occ" },
"004680": { "name": "车辆基地0", "type": "parking" },
"004681": { "name": "车辆基地1", "type": "parking" },
"004682": { "name": "车辆基地2", "type": "parking" },
"004699": { "name": "列车", "type": "train" },
"005501": { "name": "莘庄站", "type": "station" },
"005502": { "name": "春申站", "type": "station" },
"005503": { "name": "银都站", "type": "station" },
"005504": { "name": "颛桥站", "type": "station" },
"005505": { "name": "北桥站", "type": "station" },
"005506": { "name": "剑川站", "type": "station" },
"005507": { "name": "东川站", "type": "station" },
"005508": { "name": "金平站", "type": "station" },
"005509": { "name": "华宁站", "type": "station" },
"005510": { "name": "文井站", "type": "station" },
"005511": { "name": "闵行开发区站", "type": "station" },
"005521": { "name": "江川路站", "type": "station" },
"005522": { "name": "西渡站", "type": "station" },
"005523": { "name": "萧塘站", "type": "station" },
"005524": { "name": "奉浦站", "type": "station" },
"005525": { "name": "环东路站", "type": "station" },
"005526": { "name": "望园路站", "type": "station" },
"005527": { "name": "金海湖站", "type": "station" },
"005528": { "name": "奉贤新城站", "type": "station" },
"005529": { "name": "平庄停车场", "type": "station" },
"005530": { "name": "莘庄停车场", "type": "station" },
"005531": { "name": "东川路站", "type": "station" },
"005575": { "name": "OCC", "type": "occ" },
"005576": { "name": "备OCC", "type": "occ" },
"005580": { "name": "车辆基地0", "type": "parking" },
"005581": { "name": "车辆基地1", "type": "parking" },
"005582": { "name": "车辆基地2", "type": "parking" },
"005599": { "name": "列车", "type": "train" },
"005601": { "name": "莘庄站", "type": "station" },
"005602": { "name": "春申站", "type": "station" },
"005603": { "name": "银都站", "type": "station" },
"005604": { "name": "颛桥站", "type": "station" },
"005605": { "name": "北桥站", "type": "station" },
"005606": { "name": "剑川站", "type": "station" },
"005607": { "name": "东川站", "type": "station" },
"005608": { "name": "金平站", "type": "station" },
"005609": { "name": "华宁站", "type": "station" },
"005610": { "name": "文井站", "type": "station" },
"005611": { "name": "闵行开发区站", "type": "station" },
"005612": { "name": "颛桥中心", "type": "station" },
"005621": { "name": "江川路站", "type": "station" },
"005622": { "name": "西渡站", "type": "station" },
"005623": { "name": "萧塘站", "type": "station" },
"005624": { "name": "奉浦站", "type": "station" },
"005625": { "name": "环东路站", "type": "station" },
"005626": { "name": "望园路站", "type": "station" },
"005627": { "name": "金海湖站", "type": "station" },
"005628": { "name": "奉贤新城站", "type": "station" },
"005629": { "name": "平庄停车场", "type": "station" },
"005630": { "name": "莘庄停车场", "type": "station" },
"005631": { "name": "东川路站", "type": "station" },
"005675": { "name": "OCC", "type": "occ" },
"005676": { "name": "备OCC", "type": "occ" },
"005680": { "name": "车辆基地0", "type": "parking" },
"005681": { "name": "车辆基地1", "type": "parking" },
"005682": { "name": "车辆基地2", "type": "parking" },
"005699": { "name": "列车", "type": "train" },
"006501": { "name": "港城路站", "type": "station" },
"006502": { "name": "外高桥保税区北站", "type": "station" },
"006503": { "name": "航津路站", "type": "station" },
"006504": { "name": "外高桥保税区南站", "type": "station" },
"006505": { "name": "洲海路站", "type": "station" },
"006506": { "name": "五洲大道站", "type": "station" },
"006507": { "name": "东靖路站", "type": "station" },
"006508": { "name": "巨峰路站", "type": "station" },
"006509": { "name": "五莲路站", "type": "station" },
"006510": { "name": "博兴路站", "type": "station" },
"006511": { "name": "金桥路站", "type": "station" },
"006512": { "name": "云山路站", "type": "station" },
"006513": { "name": "德平路站", "type": "station" },
"006514": { "name": "北洋泾路站", "type": "station" },
"006515": { "name": "民生路站", "type": "station" },
"006516": { "name": "源深体育中心站", "type": "station" },
"006517": { "name": "世纪大道站", "type": "station" },
"006518": { "name": "浦电路站", "type": "station" },
"006519": { "name": "蓝村路站", "type": "station" },
"006520": { "name": "儿童医学中心站", "type": "station" },
"006521": { "name": "临沂新村站", "type": "station" },
"006522": { "name": "高科西路站", "type": "station" },
"006523": { "name": "东明路站", "type": "station" },
"006524": { "name": "高青路站", "type": "station" },
"006525": { "name": "华夏西路站", "type": "station" },
"006526": { "name": "上南路站", "type": "station" },
"006527": { "name": "灵岩南路站", "type": "station" },
"006528": { "name": "济阳路站", "type": "station" },
"006575": { "name": "OCC", "type": "occ" },
"006576": { "name": "备OCC", "type": "occ" },
"006580": { "name": "车辆基地0", "type": "parking" },
"006581": { "name": "车辆基地1", "type": "parking" },
"006582": { "name": "车辆基地2", "type": "parking" },
"006599": { "name": "列车", "type": "train" },
"007601": { "name": "美兰湖站", "type": "station" },
"007602": { "name": "罗南新村站", "type": "station" },
"007603": { "name": "潘广站", "type": "station" },
"007604": { "name": "刘行站", "type": "station" },
"007605": { "name": "顾村公园站", "type": "station" },
"007606": { "name": "祁华站", "type": "station" },
"007607": { "name": "上海大学站", "type": "station" },
"007608": { "name": "南陈路站", "type": "station" },
"007609": { "name": "上大路站", "type": "station" },
"007610": { "name": "场中路站", "type": "station" },
"007611": { "name": "大场镇站", "type": "station" },
"007612": { "name": "行知路站", "type": "station" },
"007613": { "name": "大华三路站", "type": "station" },
"007614": { "name": "新村路站", "type": "station" },
"007615": { "name": "岚皋路站", "type": "station" },
"007616": { "name": "镇坪路站", "type": "station" },
"007617": { "name": "长寿路站", "type": "station" },
"007618": { "name": "昌平路站", "type": "station" },
"007619": { "name": "静安寺站", "type": "station" },
"007620": { "name": "常熟路站", "type": "station" },
"007621": { "name": "肇家浜路站", "type": "station" },
"007622": { "name": "东安路站", "type": "station" },
"007623": { "name": "船厂路站", "type": "station" },
"007624": { "name": "后滩路站", "type": "station" },
"007625": { "name": "长清路", "type": "station" },
"007626": { "name": "耀华路站", "type": "station" },
"007627": { "name": "云台路站", "type": "station" },
"007628": { "name": "高科西路站", "type": "station" },
"007629": { "name": "杨高南路站", "type": "station" },
"007630": { "name": "锦绣路站", "type": "station" },
"007631": { "name": "芳华路", "type": "station" },
"007632": { "name": "龙阳路站", "type": "station" },
"007633": { "name": "花木路站", "type": "station" },
"007675": { "name": "OCC", "type": "occ" },
"007676": { "name": "备OCC", "type": "occ" },
"007680": { "name": "车辆基地0", "type": "parking" },
"007681": { "name": "车辆基地1", "type": "parking" },
"007682": { "name": "车辆基地2", "type": "parking" },
"007699": { "name": "列车", "type": "train" },
"008501": { "name": "市光路站", "type": "station" },
"008502": { "name": "嫩江路站", "type": "station" },
"008503": { "name": "翔殷路站", "type": "station" },
"008504": { "name": "黄兴公园站", "type": "station" },
"008505": { "name": "延吉中路站", "type": "station" },
"008506": { "name": "黄兴路站", "type": "station" },
"008507": { "name": "江浦路站", "type": "station" },
"008508": { "name": "鞍山新村站", "type": "station" },
"008509": { "name": "四平路站", "type": "station" },
"008510": { "name": "曲阳路站", "type": "station" },
"008511": { "name": "虹口足球场站", "type": "station" },
"008512": { "name": "西藏北路站", "type": "station" },
"008513": { "name": "中兴路站", "type": "station" },
"008514": { "name": "曲阜路站", "type": "station" },
"008515": { "name": "人民广场站", "type": "station" },
"008516": { "name": "大世界站", "type": "station" },
"008517": { "name": "老西门站", "type": "station" },
"008518": { "name": "陆家浜路站", "type": "station" },
"008519": { "name": "西藏南路站", "type": "station" },
"008520": { "name": "中华艺术馆站", "type": "station" },
"008521": { "name": "耀华路站", "type": "station" },
"008522": { "name": "成山路站", "type": "station" },
"008523": { "name": "杨思站", "type": "station" },
"008524": { "name": "济阳路站", "type": "station" },
"008525": { "name": "凌兆路站", "type": "station" },
"008526": { "name": "芦恒路站", "type": "station" },
"008527": { "name": "浦江镇站", "type": "station" },
"008528": { "name": "江月路站", "type": "station" },
"008529": { "name": "联航路站", "type": "station" },
"008530": { "name": "航天博物馆站", "type": "station" },
"008575": { "name": "OCC", "type": "occ" },
"008576": { "name": "备OCC", "type": "occ" },
"008580": { "name": "车辆基地0", "type": "parking" },
"008581": { "name": "车辆基地1", "type": "parking" },
"008582": { "name": "车辆基地2", "type": "parking" },
"008599": { "name": "列车", "type": "train" },
"009326": { "name": "芳甸路站", "type": "station" },
"009327": { "name": "蓝天路站", "type": "station" },
"009328": { "name": "台儿庄路站", "type": "station" },
"009329": { "name": "金桥路站", "type": "station" },
"009330": { "name": "金吉路站", "type": "station" },
"009331": { "name": "金海路站", "type": "station" },
"009332": { "name": "顾唐路站", "type": "station" },
"009333": { "name": "民雷路站", "type": "station" },
"009334": { "name": "曹路路站", "type": "station" },
"009335": { "name": "停车场站", "type": "station" },
"009503": { "name": "松江新城站", "type": "station" },
"009504": { "name": "松江新大学城站", "type": "station" },
"009505": { "name": "洞泾站", "type": "station" },
"009506": { "name": "佘山站", "type": "station" },
"009507": { "name": "泗泾站", "type": "station" },
"009508": { "name": "九亭站", "type": "station" },
"009509": { "name": "中春路站", "type": "station" },
"009510": { "name": "七宝站", "type": "station" },
"009511": { "name": "星中路站", "type": "station" },
"009512": { "name": "合川路站", "type": "station" },
"009513": { "name": "漕河泾开发区站", "type": "station" },
"009514": { "name": "桂林路站", "type": "station" },
"009515": { "name": "宜山路站", "type": "station" },
"009516": { "name": "徐家汇站", "type": "station" },
"009517": { "name": "肇家浜路", "type": "station" },
"009518": { "name": "嘉善路站", "type": "station" },
"009519": { "name": "打浦桥站", "type": "station" },
"009520": { "name": "马当路", "type": "station" },
"009521": { "name": "陆家浜路站", "type": "station" },
"009522": { "name": "小南门站", "type": "station" },
"009523": { "name": "商城路站", "type": "station" },
"009524": { "name": "世纪大道站", "type": "station" },
"009525": { "name": "杨高中路站", "type": "station" },
"009526": { "name": "芳甸路", "type": "station" },
"009527": { "name": "蓝天路", "type": "station" },
"009528": { "name": "台儿庄", "type": "station" },
"009529": { "name": "金桥站", "type": "station" },
"009530": { "name": "金吉路", "type": "station" },
"009531": { "name": "金海路", "type": "station" },
"009532": { "name": "顾唐路", "type": "station" },
"009533": { "name": "民雷路", "type": "station" },
"009534": { "name": "曹路", "type": "station" },
"009535": { "name": "金桥停车场", "type": "station" },
"009540": { "name": "松江南站站", "type": "station" },
"009541": { "name": "醉白池站", "type": "station" },
"009542": { "name": "松江体育中心站", "type": "station" },
"009575": { "name": "OCC", "type": "occ" },
"009576": { "name": "备OCC", "type": "occ" },
"009580": { "name": "车辆基地0", "type": "parking" },
"009581": { "name": "车辆基地1", "type": "parking" },
"009582": { "name": "车辆基地2", "type": "parking" },
"009599": { "name": "列车", "type": "train" },
"010501": { "name": "虹桥火车站站", "type": "station" },
"010502": { "name": "虹桥机场2号航站", "type": "station" },
"010503": { "name": "虹桥机场1号航站", "type": "station" },
"010504": { "name": "上海动物园站", "type": "station" },
"010505": { "name": "龙溪路站", "type": "station" },
"010506": { "name": "水城路站", "type": "station" },
"010507": { "name": "伊犁路站", "type": "station" },
"010508": { "name": "宋园路站", "type": "station" },
"010509": { "name": "虹桥路站", "type": "station" },
"010510": { "name": "交通大学站", "type": "station" },
"010511": { "name": "上海图书馆站", "type": "station" },
"010512": { "name": "陕西南路站", "type": "station" },
"010513": { "name": "新天地站", "type": "station" },
"010514": { "name": "老西门站", "type": "station" },
"010515": { "name": "豫园站", "type": "station" },
"010516": { "name": "南京东路站", "type": "station" },
"010517": { "name": "天潼路站", "type": "station" },
"010518": { "name": "四川北路站", "type": "station" },
"010519": { "name": "海伦路站", "type": "station" },
"010520": { "name": "邮电新村站", "type": "station" },
"010521": { "name": "四平路站", "type": "station" },
"010522": { "name": "同济大学站", "type": "station" },
"010523": { "name": "国权路站", "type": "station" },
"010524": { "name": "五角场站", "type": "station" },
"010525": { "name": "江湾体育场站", "type": "station" },
"010526": { "name": "三门路", "type": "station" },
"010527": { "name": "殷高东路站", "type": "station" },
"010528": { "name": "新江湾城站", "type": "station" },
"010529": { "name": "航中路站", "type": "station" },
"010530": { "name": "紫藤路站", "type": "station" },
"010531": { "name": "龙柏新村站", "type": "station" },
"010232": { "name": "国帆路站", "type": "station" },
"010233": { "name": "双江路站", "type": "station" },
"010234": { "name": "高桥西路站", "type": "station" },
"010235": { "name": "高桥路站", "type": "station" },
"010236": { "name": "港城路站", "type": "station" },
"010237": { "name": "基隆路站", "type": "station" },
"010275": { "name": "OCC", "type": "occ" },
"010576": { "name": "备OCC", "type": "occ" },
"010580": { "name": "港城基地", "type": "parking" },
"010581": { "name": "车辆基地1", "type": "parking" },
"010582": { "name": "车辆基地2", "type": "parking" },
"010199": { "name": "一期列车", "type": "train" },
"010599": { "name": "二期列车", "type": "train" },
"010601": { "name": "虹桥火车站站", "type": "station" },
"010602": { "name": "虹桥机场2号航站", "type": "station" },
"010603": { "name": "虹桥机场1号航站", "type": "station" },
"010604": { "name": "上海动物园站", "type": "station" },
"010605": { "name": "龙溪路站", "type": "station" },
"010606": { "name": "水城路站", "type": "station" },
"010607": { "name": "伊犁路站", "type": "station" },
"010608": { "name": "宋园路站", "type": "station" },
"010609": { "name": "虹桥路站", "type": "station" },
"010610": { "name": "交通大学站", "type": "station" },
"010611": { "name": "上海图书馆站", "type": "station" },
"010612": { "name": "陕西南路站", "type": "station" },
"010613": { "name": "新天地站", "type": "station" },
"010614": { "name": "老西门站", "type": "station" },
"010615": { "name": "豫园站", "type": "station" },
"010616": { "name": "南京东路站", "type": "station" },
"010617": { "name": "天潼路站", "type": "station" },
"010618": { "name": "四川北路站", "type": "station" },
"010619": { "name": "海伦路站", "type": "station" },
"010620": { "name": "邮电新村站", "type": "station" },
"010621": { "name": "四平路站", "type": "station" },
"010622": { "name": "同济大学站", "type": "station" },
"010623": { "name": "国权路站", "type": "station" },
"010624": { "name": "五角场站", "type": "station" },
"010625": { "name": "江湾体育场站", "type": "station" },
"010626": { "name": "三门路", "type": "station" },
"010627": { "name": "殷高东路站", "type": "station" },
"010628": { "name": "新江湾城站", "type": "station" },
"010629": { "name": "航中路站", "type": "station" },
"010630": { "name": "紫藤路站", "type": "station" },
"010631": { "name": "龙柏新村站", "type": "station" },
"010632": { "name": "国帆路站", "type": "station" },
"010633": { "name": "双江路站", "type": "station" },
"010634": { "name": "高桥西路站", "type": "station" },
"010635": { "name": "高桥路站", "type": "station" },
"010636": { "name": "港城路站", "type": "station" },
"010637": { "name": "基隆路站", "type": "station" },
"010680": { "name": "吴中路基地", "type": "parking" },
"011501": { "name": "嘉定北站", "type": "station" },
"011502": { "name": "嘉定西站", "type": "station" },
"011503": { "name": "白银路站", "type": "station" },
"011504": { "name": "嘉定新城站", "type": "station" },
"011505": { "name": "马陆站", "type": "station" },
"011506": { "name": "南翔站", "type": "station" },
"011507": { "name": "桃浦新村站", "type": "station" },
"011508": { "name": "威武路站", "type": "station" },
"011509": { "name": "祁连山路站", "type": "station" },
"011510": { "name": "李子园站", "type": "station" },
"011511": { "name": "上海西站站", "type": "station" },
"011512": { "name": "真如站", "type": "station" },
"011513": { "name": "枫桥路站", "type": "station" },
"011514": { "name": "曹杨路站", "type": "station" },
"011515": { "name": "隆德路站", "type": "station" },
"011516": { "name": "江苏路站", "type": "station" },
"011517": { "name": "上海赛车场站", "type": "station" },
"011518": { "name": "昌吉东路站", "type": "station" },
"011519": { "name": "上海汽车城站", "type": "station" },
"011520": { "name": "安亭站", "type": "station" },
"011531": { "name": "交通大学站", "type": "station" },
"011532": { "name": "徐家汇站", "type": "station" },
"011533": { "name": "游泳馆站", "type": "station" },
"011534": { "name": "龙华站", "type": "station" },
"011535": { "name": "云锦路站", "type": "station" },
"011536": { "name": "龙耀路站", "type": "station" },
"011537": { "name": "东体站", "type": "station" },
"011538": { "name": "三林站", "type": "station" },
"011539": { "name": "三林东站", "type": "station" },
"011540": { "name": "浦三路站", "type": "station" },
"011541": { "name": "御桥路站", "type": "station" },
"011542": { "name": "罗山路站", "type": "station" },
"011543": { "name": "兆丰路站", "type": "station" },
"011544": { "name": "光明路站", "type": "station" },
"011545": { "name": "花桥路站", "type": "station" },
"011546": { "name": "秀沿路站", "type": "station" },
"011547": { "name": "康新公路站", "type": "station" },
"011548": { "name": "迪斯尼站", "type": "station" },
"011551": { "name": "陈翔路车站", "type": "station" },
"011575": { "name": "OCC", "type": "occ" },
"011576": { "name": "备OCC", "type": "occ" },
"011580": { "name": "车辆基地0", "type": "parking" },
"011581": { "name": "车辆基地1", "type": "parking" },
"011582": { "name": "车辆基地2", "type": "parking" },
"011599": { "name": "列车", "type": "train" },
"011628": { "name": "新站", "type": "station" },
"012501": { "name": "七莘路站", "type": "station" },
"012502": { "name": "虹莘路站", "type": "station" },
"012503": { "name": "顾戴路站", "type": "station" },
"012504": { "name": "东兰路站", "type": "station" },
"012505": { "name": "虹梅路站", "type": "station" },
"012506": { "name": "虹漕路站", "type": "station" },
"012507": { "name": "桂林公园站", "type": "station" },
"012508": { "name": "漕宝路站", "type": "station" },
"012509": { "name": "龙漕路站", "type": "station" },
"012510": { "name": "龙华站", "type": "station" },
"012511": { "name": "龙华中路站", "type": "station" },
"012512": { "name": "大木桥路站", "type": "station" },
"012513": { "name": "嘉善路站", "type": "station" },
"012514": { "name": "陕西南路站", "type": "station" },
"012515": { "name": "南京西路站", "type": "station" },
"012516": { "name": "汉中路站", "type": "station" },
"012517": { "name": "曲阜路站", "type": "station" },
"012518": { "name": "天潼路站", "type": "station" },
"012519": { "name": "国际客运中心站", "type": "station" },
"012520": { "name": "提篮桥站", "type": "station" },
"012521": { "name": "大连路站", "type": "station" },
"012522": { "name": "浦江公园站", "type": "station" },
"012523": { "name": "宁国路站", "type": "station" },
"012524": { "name": "隆昌路站", "type": "station" },
"012525": { "name": "爱国路站", "type": "station" },
"012526": { "name": "复兴岛站", "type": "station" },
"012527": { "name": "东陆路站", "type": "station" },
"012528": { "name": "巨峰路站", "type": "station" },
"012529": { "name": "杨高北路站", "type": "station" },
"012530": { "name": "金京路站", "type": "station" },
"012531": { "name": "申江路站", "type": "station" },
"012532": { "name": "金海路站", "type": "station" },
"012575": { "name": "OCC", "type": "occ" },
"012576": { "name": "备OCC", "type": "occ" },
"012580": { "name": "车辆基地0", "type": "parking" },
"012581": { "name": "车辆基地1", "type": "parking" },
"012582": { "name": "车辆基地2", "type": "parking" },
"012599": { "name": "列车", "type": "train" },
"013501": { "name": "金运路站", "type": "station" },
"013502": { "name": "金沙江西路", "type": "station" },
"013503": { "name": "丰庄站", "type": "station" },
"013504": { "name": "祁连山南路站", "type": "station" },
"013505": { "name": "真北路站", "type": "station" },
"013506": { "name": "大渡河路站", "type": "station" },
"013507": { "name": "金沙江路站", "type": "station" },
"013508": { "name": "隆德路站", "type": "station" },
"013509": { "name": "武宁路站", "type": "station" },
"013510": { "name": "长寿路站", "type": "station" },
"013511": { "name": "江宁路站", "type": "station" },
"013512": { "name": "汉中路站", "type": "station" },
"013513": { "name": "自然博物馆站", "type": "station" },
"013514": { "name": "南京西路站", "type": "station" },
"013515": { "name": "淮海中路站", "type": "station" },
"013516": { "name": "新天地站", "type": "station" },
"013517": { "name": "马当路站", "type": "station" },
"013518": { "name": "世博会博物馆站", "type": "station" },
"013519": { "name": "世博大道站", "type": "station" },
"013520": { "name": "北翟路停车场", "type": "station" },
"013521": { "name": "长清站", "type": "station" },
"013522": { "name": "成山站", "type": "station" },
"013523": { "name": "东明站", "type": "station" },
"013524": { "name": "华鹏站", "type": "station" },
"013525": { "name": "下南站", "type": "station" },
"013526": { "name": "北蔡站", "type": "station" },
"013527": { "name": "陈春站", "type": "station" },
"013528": { "name": "莲溪站", "type": "station" },
"013529": { "name": "华夏中路站", "type": "station" },
"013530": { "name": "中科站", "type": "station" },
"013531": { "name": "学林站", "type": "station" },
"013532": { "name": "张江站", "type": "station" },
"013575": { "name": "OCC", "type": "occ" },
"013576": { "name": "备OCC", "type": "occ" },
"0136580": { "name": "车辆基地0", "type": "parking" },
"0136581": { "name": "车辆基地1", "type": "parking" },
"0136582": { "name": "车辆基地2", "type": "parking" },
"013599": { "name": "列车", "type": "train" },
"015501": { "name": "紫竹高新区站", "type": "station" },
"015502": { "name": "永德路站", "type": "station" },
"015503": { "name": "元江路站", "type": "station" },
"015504": { "name": "双柏路站", "type": "station" },
"015505": { "name": "曙建路站", "type": "station" },
"015506": { "name": "景西路站", "type": "station" },
"015507": { "name": "虹梅南路站", "type": "station" },
"015508": { "name": "华泾西路站", "type": "station" },
"015509": { "name": "朱梅路站", "type": "station" },
"015510": { "name": "罗秀路站", "type": "station" },
"015511": { "name": "华东理工站", "type": "station" },
"015512": { "name": "上海南站站", "type": "station" },
"015513": { "name": "桂林公园站", "type": "station" },
"015514": { "name": "桂林路站", "type": "station" },
"015515": { "name": "吴中路站", "type": "station" },
"015516": { "name": "姚虹路站", "type": "station" },
"015517": { "name": "红宝石路站", "type": "station" },
"015518": { "name": "娄山关路站", "type": "station" },
"015519": { "name": "长风公园站", "type": "station" },
"015520": { "name": "大渡河站", "type": "station" },
"015521": { "name": "梅岭北路站", "type": "station" },
"015522": { "name": "铜川路站", "type": "station" },
"015523": { "name": "上海西站站", "type": "station" },
"015524": { "name": "武威东路站", "type": "station" },
"015525": { "name": "古浪路站", "type": "station" },
"015526": { "name": "祁安路站", "type": "station" },
"015527": { "name": "南大路站", "type": "station" },
"015528": { "name": "丰翔路站", "type": "station" },
"015529": { "name": "锦秋路站", "type": "station" },
"015530": { "name": "顾村公园站", "type": "station" },
"015575": { "name": "控制中心", "type": "occ" },
"015580": { "name": "陈太路停车场", "type": "parking" },
"015581": { "name": "元江路车辆段", "type": "parking" },
"015599": { "name": "列车", "type": "train" },
"016501": { "name": "龙阳路站", "type": "station" },
"016502": { "name": "华夏中路站", "type": "station" },
"016503": { "name": "罗山路站", "type": "station" },
"016504": { "name": "周浦东站", "type": "station" },
"016505": { "name": "鹤沙航城站", "type": "station" },
"016506": { "name": "航头东站", "type": "station" },
"016507": { "name": "新场站", "type": "station" },
"016508": { "name": "野生动物园站", "type": "station" },
"016509": { "name": "惠南站", "type": "station" },
"016510": { "name": "惠南东站", "type": "station" },
"016511": { "name": "书院站", "type": "station" },
"016512": { "name": "临港大道站", "type": "station" },
"016513": { "name": "滴水湖", "type": "station" },
"016575": { "name": "OCC", "type": "occ" },
"016576": { "name": "备OCC", "type": "occ" },
"016580": { "name": "车辆基地0", "type": "parking" },
"016581": { "name": "车辆基地1", "type": "parking" },
"016582": { "name": "车辆基地2", "type": "parking" },
"016599": { "name": "列车", "type": "train" },
"017501": { "name": "东方绿舟", "type": "station" },
"017502": { "name": "朱家角", "type": "station" },
"017503": { "name": "淀山湖", "type": "station" },
"017504": { "name": "漕盈路", "type": "station" },
"017505": { "name": "青浦", "type": "station" },
"017506": { "name": "汇金路站", "type": "station" },
"017507": { "name": "赵巷", "type": "station" },
"017508": { "name": "嘉松中路", "type": "station" },
"017509": { "name": "徐泾北城", "type": "station" },
"017510": { "name": "徐盈路", "type": "station" },
"017511": { "name": "蟠龙路", "type": "station" },
"017512": { "name": "诸光路", "type": "station" },
"017513": { "name": "虹桥火车站", "type": "station" },
"017576": { "name": "朱家角备控", "type": "occ" },
"017580": { "name": "朱家角停车场", "type": "parking" },
"017581": { "name": "徐泾停车场", "type": "parking" },
"018501": { "name": "航头站", "type": "station" },
"018502": { "name": "下沙站", "type": "station" },
"018503": { "name": "鹤涛路站", "type": "station" },
"018504": { "name": "沈梅路站", "type": "station" },
"018505": { "name": "繁荣路站", "type": "station" },
"018506": { "name": "周浦站", "type": "station" },
"018507": { "name": "康桥站", "type": "station" },
"018508": { "name": "御桥站", "type": "station" },
"021509": { "name": "申江南路", "type": "station" },
"021575": { "name": "OCC", "type": "occ" },
"021609": { "name": "申江南路", "type": "station" },
"021675": { "name": "OCC", "type": "occ" },
"021680": { "name": "六陈路车辆段", "type": "parking" },
"051501": { "name": "沈杜公路", "type": "station" },
"051502": { "name": "三鲁公路", "type": "station" },
"051503": { "name": "闵瑞路", "type": "station" },
"051504": { "name": "浦航路", "type": "station" },
"051505": { "name": "东城一路", "type": "station" },
"051506": { "name": "汇臻路", "type": "station" },
"051575": { "name": "OCC", "type": "occ" },
"051576": { "name": "备OCC", "type": "occ" },
"051580": { "name": "车辆基地0", "type": "parking" },
"051581": { "name": "车辆基地1", "type": "parking" },
"051582": { "name": "车辆基地2", "type": "parking" },
"051599": { "name": "列车", "type": "train" },
"501500": { "name": "COCC", "type": "manage" },
"502500": { "name": "BCOCC", "type": "manage" },
"601500": { "name": "上海火车站分控", "type": "manage" },
"602500": { "name": "徐家汇分控", "type": "manage" },
"603500": { "name": "宜山路分控", "type": "manage" },
"604500": { "name": "陆家嘴分控", "type": "manage" },
"605500": { "name": "人民广场分控", "type": "manage" },
"606500": { "name": "东宝兴路分控", "type": "manage" },
"607500": { "name": "虹桥枢纽分控", "type": "manage" },
"608500": { "name": "松江大学城分控", "type": "manage" },
"609500": { "name": "民生路分控", "type": "manage" },
"610500": { "name": "西藏南路分控", "type": "manage" },
"611500": { "name": "延吉中路分控", "type": "manage" },
"612500": { "name": "迪士尼分控", "type": "manage" },
"900500": { "name": "轨交总队", "type": "manage" }
}
@@ -1,45 +1,5 @@
export interface NdmCameraDiagInfo { export interface NdmCameraDiagInfo {
[key: string]: any; [key: string]: any;
ethInfo?: {
adminStatus?: string; // '1'
desc?: string; // 'IPcamera'
ifType?: string; // '5'
inDiscards?: string; // '0'
inErrors?: string; // '0'
inNUcastPkts?: string; // '0'
inOctets?: string; // '0'
inUcastPkts?: string; // '0'
inUnknownProtos?: string; // '0'
index?: string; // '1'
lastChange?: string; // '0:00:00.00'
mTU?: string; // '1500'
macAddress?: string; // '04:ee:cd:52:3a:a5'
operStatus?: string; // '1'
outDiscards?: string; // '0'
outErrors?: string; // '0'
outNUcastPkts?: string; // '0'
outOctets?: string; // '0'
outQLen?: string; // '0'
outUcastPkts?: string; // '0'
specific?: string; // '0.0'
speed?: string; // '10000000'
};
ipInfo?: {
broadcastAddress?: string; // '0'
iPAddress?: string; // '0'
index?: string; // '1'
mASK?: string; // '255.255.255.0'
reasmMaxSize?: string; // '0'
};
logTime?: string; logTime?: string;
stCommonInfo?: { info?: string;
设备ID?: string;
软件版本?: string;
设备厂商?: string;
设备别名?: string;
设备型号?: string;
硬件版本?: string;
内存使用率?: string;
CPU使用率?: string;
};
} }
+8 -43
View File
@@ -10,37 +10,6 @@ export interface NdmNvrDiagInfo {
totalSize?: number; totalSize?: number;
}[]; }[];
}; };
ethInfo: {
adminStatus?: string; // '1'
desc?: string; // 'bond1'
ifType?: string; // '6'
inDiscards?: string; // '17931688'
inErrors?: string; // '0'
inNUcastPkts?: string; // '40945433'
inOctets?: string; // '3453544614'
inUcastPkts?: string; // '3375411816'
inUnknownProtos?: string; // '0'
index?: string; // '8'
lastChange?: string; // '0:05:42.15'
mTU?: string; // '1500'
macAddress?: string; // '04:7b:cb:69:92:58'
operStatus?: string; // '1'
outDiscards?: string; // '0'
outErrors?: string; // '0'
outNUcastPkts?: string; // '0'
outOctets?: string; // '3443476717'
outQLen?: string; // '0'
outUcastPkts?: string; // '415381735'
specific?: string; // '0.0'
speed?: string; // '2000000000'
};
ipInfo: {
broadcastAddress?: string; // '1'
iPAddress?: string; // '10.14.1.22'
index?: string; // '8'
mASK?: string; // '255.255.255.0'
reasmMaxSize?: string; // '0'
};
stCommonInfo?: { stCommonInfo?: {
设备ID?: string; 设备ID?: string;
软件版本?: string; 软件版本?: string;
@@ -51,16 +20,12 @@ export interface NdmNvrDiagInfo {
内存使用率?: string; 内存使用率?: string;
CPU使用率?: string; CPU使用率?: string;
}; };
cdFanInfo?: NdmNvrFanInfo[]; cdFanInfo?: {
cdPowerSupplyInfo?: NdmNvrPowerSupplyInfo[]; 索引号?: string;
} '风扇转速(rpm)'?: string;
}[];
export interface NdmNvrFanInfo { cdPowerSupplyInfo?: {
索引号?: string; 索引号?: string;
'风扇转速(rpm)'?: string; 电源状态?: string;
} }[];
export interface NdmNvrPowerSupplyInfo {
索引号?: string;
电源状态?: string;
} }
@@ -1,36 +1,5 @@
export interface NdmSecurityBoxDiagInfo { export interface NdmSecurityBoxDiagInfo {
[key: string]: any; [key: string]: any;
ethInfo?: {
adminStatus?: string; // '1'
desc?: string; // 'br-lan'
ifType?: string; // '6'
inDiscards?: string; // '84977'
inErrors?: string; // '0'
inNUcastPkts?: string; // '233473'
inOctets?: string; // '92355850'
inUcastPkts?: string; // '899970'
inUnknownProtos?: string; // '0'
index?: string; // '8'
lastChange?: string; // '0:00:00.00'
mTU?: string; // '1500'
macAddress?: string; // '56:62:bc:d3:9e:37'
operStatus?: string; // '1'
outDiscards?: string; // '0'
outErrors?: string; // '0'
outNUcastPkts?: string; // '0'
outOctets?: string; // '68175904'
outQLen?: string; // '0'
outUcastPkts?: string; // '748100'
specific?: string; // '0.0'
speed?: string; // '0'
};
ipInfo?: {
broadcastAddress?: string; // '1'
iPAddress?: string; // '10.24.18.101'
index?: string; // '8'
mASK?: string; // '255.255.255.0'
reasmMaxSize?: string; // '0'
};
info?: [ info?: [
{ {
addrCode?: number; addrCode?: number;
@@ -43,14 +12,8 @@ export interface NdmSecurityBoxDiagInfo {
]; ];
stCommonInfo?: { stCommonInfo?: {
[key: string]: any; [key: string]: any;
设备ID?: string; // 'NTBoxMetro' 内存使用率?: string;
软件版本?: string; // 'V0101' CPU使用率?: string;
设备厂商?: string; // 'NingTech'
设备别名?: string; // 'SUN-IBOX'
设备型号?: string; // 'SUN-IBOX'
硬件版本?: string; // 'V0101'
内存使用率?: string; // '18'
CPU使用率?: string; // '1'
}; };
} }
@@ -6,35 +6,4 @@ export interface NdmServerDiagInfo {
磁盘使用率?: string; 磁盘使用率?: string;
系统运行时间?: string; 系统运行时间?: string;
}; };
ethInfo?: {
adminStatus?: string; // '1'
desc?: string; // 'Intel Corporation I350 Gigabit Network Connection'
ifType?: string; // '6'
inDiscards?: string; // '6707634'
inErrors?: string; // '0'
inNUcastPkts?: string; // '8991944'
inOctets?: string; // '4220524983'
inUcastPkts?: string; // '2342740610'
inUnknownProtos?: string; // '0'
index?: string; // '2'
lastChange?: string; // '0:03:15.26'
mTU?: string; // '1500'
macAddress?: string; // 'e8:78:ee:f6:8d:98'
operStatus?: string; // '1'
outDiscards?: string; // '0'
outErrors?: string; // '0'
outNUcastPkts?: string; // '0'
outOctets?: string; // '1415770066'
outQLen?: string; // '0'
outUcastPkts?: string; // '3335494729'
specific?: string; // '0.0'
speed?: string; // '1000000000'
};
ipInfo?: {
broadcastAddress?: string; // '1'
iPAddress?: string; // '10.14.1.8'
index?: string; // '2'
mASK?: string; // '255.255.255.0'
reasmMaxSize?: string; // '0'
};
} }
@@ -2,7 +2,6 @@ export interface NdmSwitchDiagInfo {
[key: string]: any; [key: string]: any;
cpuRatio?: string; // 因环境不同可能不存在 cpuRatio?: string; // 因环境不同可能不存在
memoryRatio?: string; // 因环境不同可能不存在 memoryRatio?: string; // 因环境不同可能不存在
temperature?: number;
logTime?: string; logTime?: string;
info?: { info?: {
overFlowPorts?: string[]; overFlowPorts?: string[];
@@ -1,5 +0,0 @@
export interface HighAvailable {
pyip: string;
vip: string;
changeDate: string;
}
-1
View File
@@ -1,4 +1,3 @@
export * from './diag'; export * from './diag';
export * from './high-available';
export * from './link-description'; export * from './link-description';
export * from './station'; export * from './station';
-13
View File
@@ -1,13 +0,0 @@
export interface ChangeLogDescription {
content: string;
}
export interface Changelog {
version: string;
date: string;
changes: {
breaks?: ChangeLogDescription[];
fixes?: ChangeLogDescription[];
feats?: ChangeLogDescription[];
};
}
-1
View File
@@ -1,2 +1 @@
export * from './changelog';
export * from './version-info'; export * from './version-info';
@@ -20,9 +20,7 @@ export interface NdmSecurityBox extends BaseModel {
description: string; description: string;
deviceStatus: string; deviceStatus: string;
deviceType: string; deviceType: string;
circuitCount: number;
community: string; community: string;
writeCommunity: string;
frontendConfig: string; frontendConfig: string;
linkDescription: string; linkDescription: string;
snmpEnabled: boolean; snmpEnabled: boolean;
@@ -21,7 +21,6 @@ export interface NdmSwitch extends BaseModel {
deviceStatus: string; deviceStatus: string;
deviceType: string; deviceType: string;
community: string; community: string;
writeCommunity: string;
frontendConfig: string; frontendConfig: string;
linkDescription: string; linkDescription: string;
snmpEnabled: boolean; snmpEnabled: boolean;
@@ -30,7 +30,6 @@ export interface NdmNvr extends BaseModel {
deviceStatus: string; deviceStatus: string;
deviceType: string; deviceType: string;
community: string; community: string;
writeCommunity: string;
frontendConfig: string; frontendConfig: string;
linkDescription: string; linkDescription: string;
snmpEnabled: boolean; snmpEnabled: boolean;
@@ -31,7 +31,6 @@ export interface NdmCamera extends BaseModel {
deviceType: string; deviceType: string;
cameraType: string; cameraType: string;
community: string; community: string;
writeCommunity: string;
frontendConfig: string; frontendConfig: string;
linkDescription: string; linkDescription: string;
snmpEnabled: boolean; snmpEnabled: boolean;
@@ -28,7 +28,6 @@ export interface NdmDecoder extends BaseModel {
deviceStatus: string; deviceStatus: string;
deviceType: string; deviceType: string;
community: string; community: string;
writeCommunity: string;
frontendConfig: string; frontendConfig: string;
linkDescription: string; linkDescription: string;
snmpEnabled: boolean; snmpEnabled: boolean;
@@ -21,7 +21,6 @@ export interface NdmKeyboard extends BaseModel {
deviceStatus: string; deviceStatus: string;
deviceType: string; deviceType: string;
community: string; community: string;
writeCommunity: string;
frontendConfig: string; frontendConfig: string;
linkDescription: string; linkDescription: string;
snmpEnabled: boolean; snmpEnabled: boolean;
@@ -25,7 +25,6 @@ export interface NdmMediaServer extends BaseModel {
deviceStatus: string; deviceStatus: string;
deviceType: string; deviceType: string;
community: string; community: string;
writeCommunity: string;
frontendConfig: string; frontendConfig: string;
linkDescription: string; linkDescription: string;
snmpEnabled: boolean; snmpEnabled: boolean;
@@ -25,7 +25,6 @@ export interface NdmVideoServer extends BaseModel {
deviceStatus: string; deviceStatus: string;
deviceType: string; deviceType: string;
community: string; community: string;
writeCommunity: string;
frontendConfig: string; frontendConfig: string;
linkDescription: string; linkDescription: string;
snmpEnabled: boolean; snmpEnabled: boolean;
+4 -28
View File
@@ -93,46 +93,22 @@ export const probeSecurityBoxApi = async (ids: string[], options?: { stationCode
unwrapVoidResponse(resp); unwrapVoidResponse(resp);
}; };
// beidian安防箱切换空开状态 export const turnCitcuitStatusApi = async (ipAddress: string, circuitIndex: number, status: number, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
export const turnCircuitStatusBeidianApi = async (community: string, ipAddress: string, circuitIndex: number, status: number, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {}; const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient; const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : ''; const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSecurityBox/turnStatus`; const endpoint = `${prefix}/api/ndm/ndmSecurityBox/turnStatus`;
const resp = await client.post<boolean>(endpoint, { community, ipAddress, circuit: `${circuitIndex}`, status }, { signal }); const resp = await client.post<boolean>(endpoint, { community: 'public', ipAddress, circuit: `${circuitIndex}`, status }, { signal });
const data = unwrapResponse(resp); const data = unwrapResponse(resp);
return data; return data;
}; };
// beidian安防箱重启 export const rebootSecurityBoxApi = async (ipAddress: string, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
export const rebootSecurityBoxBeidianApi = async (community: string, ipAddress: string, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {}; const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient; const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : ''; const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSecurityBox/reboot`; const endpoint = `${prefix}/api/ndm/ndmSecurityBox/reboot`;
const resp = await client.post<boolean>(endpoint, { community, ipAddress }, { signal }); const resp = await client.post<boolean>(endpoint, { community: 'public', ipAddress }, { signal });
const data = unwrapResponse(resp);
return data;
};
// ningtech安防箱切换空开状态
export const turnCircuitStatusNingTechApi = async (community: string, ipAddress: string, circuitIndex: number, status: number, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSecurityBox/turnStatusNingTech`;
const resp = await client.post<boolean>(endpoint, { community, ipAddress, circuit: `${circuitIndex}`, status }, { signal });
const data = unwrapResponse(resp);
return data;
};
// ningtech安防箱重启
export const rebootSecurityBoxNingTechApi = async (community: string, ipAddress: string, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSecurityBox/rebootNingTech`;
const resp = await client.post<boolean>(endpoint, { community, ipAddress }, { signal });
const data = unwrapResponse(resp); const data = unwrapResponse(resp);
return data; return data;
}; };
@@ -1,26 +1,5 @@
import { ndmClient, userClient, type HighAvailable, type MediaServerStatus, type SendRtpInfo, type Station } from '@/apis'; import { ndmClient, userClient, type MediaServerStatus, type SendRtpInfo, type Station } from '@/apis';
import { unwrapNullableResponse, unwrapResponse } from '@/utils'; import { unwrapResponse } from '@/utils';
import destr from 'destr';
// {
// "code": 0,
// "data": "{pyip:\"10.18.128.14\",vip:\"10.18.128.6\",changeDate:\"2026-03-23 15:55:00\"}",
// "msg": "ok",
// "path": null,
// "extra": null,
// "timestamp": "1774421387908",
// "errorMsg": "",
// "isSuccess": true
// }
export const getHighAvailableApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmServiceAvailable/highAvailable/get`;
const resp = await client.get<string | null>(endpoint, { signal });
const data = unwrapNullableResponse(resp);
return destr<HighAvailable | null>(data);
};
export const getAllPushApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => { export const getAllPushApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {}; const { stationCode, signal } = options ?? {};
@@ -1,20 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { NCard, NDescriptions, NDescriptionsItem, NFlex, NText } from 'naive-ui'; import { NCard, NDescriptions, NDescriptionsItem } from 'naive-ui';
import { computed, toRefs } from 'vue'; import { computed, toRefs } from 'vue';
const props = defineProps<{ const props = defineProps<{
commonInfo?: Array<{ commonInfo?: Record<string, string>;
title: string;
items: Array<{
label: string;
value: string;
}>;
}>;
}>(); }>();
const { commonInfo } = toRefs(props); const { commonInfo } = toRefs(props);
const showCard = computed(() => (commonInfo.value?.length ?? 0) > 0); const showCard = computed(() => !!commonInfo.value);
const commonInfoEntries = computed(() => Object.entries(commonInfo.value ?? {}));
</script> </script>
<template> <template>
@@ -22,20 +18,11 @@ const showCard = computed(() => (commonInfo.value?.length ?? 0) > 0);
<template #header> <template #header>
<div>设备信息</div> <div>设备信息</div>
</template> </template>
<template v-if="!!commonInfo && commonInfo.length > 0"> <NDescriptions bordered label-placement="left" :columns="2">
<NFlex vertical :size="16"> <template v-for="item in commonInfoEntries" :key="item[0]">
<template v-for="({ title, items }, index) in commonInfo" :key="`${title}-${index}`"> <NDescriptionsItem :label="item[0]">{{ item[1] }}</NDescriptionsItem>
<NFlex vertical> </template>
<NText strong :depth="3">{{ title }}</NText> </NDescriptions>
<NDescriptions bordered :label-placement="'left'" :columns="2">
<template v-for="({ label, value }, index) in items" :key="`${label}-${index}`">
<NDescriptionsItem :label="label">{{ value }}</NDescriptionsItem>
</template>
</NDescriptions>
</NFlex>
</template>
</NFlex>
</template>
</NCard> </NCard>
</template> </template>
@@ -1,18 +1,3 @@
<script lang="ts">
const parsePercentMetric = (raw?: string) => {
const trimmed = raw?.trim();
if (!!trimmed) {
const value = Number(trimmed);
if (Number.isFinite(value)) {
return value;
}
}
return undefined;
};
</script>
<script setup lang="ts"> <script setup lang="ts">
import { ClockCheckIcon, CpuIcon, HardDriveIcon, MemoryStickIcon } from 'lucide-vue-next'; import { ClockCheckIcon, CpuIcon, HardDriveIcon, MemoryStickIcon } from 'lucide-vue-next';
import { NCard, NFlex, NIcon, NProgress, type ProgressStatus } from 'naive-ui'; import { NCard, NFlex, NIcon, NProgress, type ProgressStatus } from 'naive-ui';
@@ -36,27 +21,24 @@ const showCard = computed(() => {
}); });
const cpuPercent = computed(() => { const cpuPercent = computed(() => {
return parsePercentMetric(cpuUsage.value); if (!cpuUsage?.value) return 0;
return parseFloat(cpuUsage.value.replace('%', ''));
}); });
const memPercent = computed(() => { const memPercent = computed(() => {
return parsePercentMetric(memUsage.value); if (!memUsage?.value) return 0;
return parseFloat(memUsage.value.replace('%', ''));
}); });
const diskPercent = computed(() => { const diskPercent = computed(() => {
return parsePercentMetric(diskUsage.value); if (!diskUsage?.value) return 0;
return parseFloat(diskUsage.value.replace('%', ''));
}); });
const formattedRunningTime = computed(() => { const formattedRunningTime = computed(() => {
return (runningTime?.value ?? '-').replace('days', '天'); return (runningTime?.value ?? '-').replace('days', '天');
}); });
const getProgressPercentage = (percent: number) => {
if (percent < 0) return 0;
if (percent > 100) return 100;
return percent;
};
const getProgressStatus = (percent: number): ProgressStatus => { const getProgressStatus = (percent: number): ProgressStatus => {
if (percent >= 90) return 'error'; if (percent >= 90) return 'error';
if (percent >= 70) return 'warning'; if (percent >= 70) return 'warning';
@@ -71,20 +53,20 @@ const getProgressStatus = (percent: number): ProgressStatus => {
</template> </template>
<template #default> <template #default>
<NFlex vertical> <NFlex vertical>
<NFlex v-if="cpuPercent" style="width: 100%" align="center" :wrap="false"> <NFlex v-if="cpuUsage" style="width: 100%" align="center" :wrap="false">
<NIcon :component="CpuIcon" /> <NIcon :component="CpuIcon" />
<span style="word-break: keep-all">{{ cpuUsageLabel || 'CPU' }}</span> <span style="word-break: keep-all">{{ cpuUsageLabel || 'CPU' }}</span>
<NProgress :percentage="getProgressPercentage(cpuPercent)" :status="getProgressStatus(cpuPercent)">{{ cpuPercent }}%</NProgress> <NProgress :percentage="cpuPercent" :status="getProgressStatus(cpuPercent)">{{ cpuPercent }}%</NProgress>
</NFlex> </NFlex>
<NFlex v-if="memPercent" style="width: 100%" align="center" :wrap="false"> <NFlex v-if="memUsage" style="width: 100%" align="center" :wrap="false">
<NIcon :component="MemoryStickIcon" /> <NIcon :component="MemoryStickIcon" />
<span style="word-break: keep-all">{{ memUsageLabel || '内存' }}</span> <span style="word-break: keep-all">{{ memUsageLabel || '内存' }}</span>
<NProgress :percentage="getProgressPercentage(memPercent)" :status="getProgressStatus(memPercent)">{{ memPercent }}%</NProgress> <NProgress :percentage="memPercent" :status="getProgressStatus(memPercent)">{{ memPercent }}%</NProgress>
</NFlex> </NFlex>
<NFlex v-if="diskPercent" style="width: 100%" align="center" :wrap="false"> <NFlex v-if="diskUsage" style="width: 100%" align="center" :wrap="false">
<NIcon :component="HardDriveIcon" /> <NIcon :component="HardDriveIcon" />
<span style="word-break: keep-all">{{ diskUsageLabel || '磁盘' }}</span> <span style="word-break: keep-all">{{ diskUsageLabel || '磁盘' }}</span>
<NProgress :percentage="getProgressPercentage(diskPercent)" :status="getProgressStatus(diskPercent)">{{ diskPercent }}%</NProgress> <NProgress :percentage="diskPercent" :status="getProgressStatus(diskPercent)">{{ diskPercent }}%</NProgress>
</NFlex> </NFlex>
<NFlex v-if="runningTime" style="width: 100%" align="center" :wrap="false"> <NFlex v-if="runningTime" style="width: 100%" align="center" :wrap="false">
<NIcon :component="ClockCheckIcon" /> <NIcon :component="ClockCheckIcon" />
@@ -1,25 +1,20 @@
import type { ComponentInstance } from 'vue';
import DeviceCommonCard from './device-common-card.vue'; import DeviceCommonCard from './device-common-card.vue';
import DeviceHardwareCard from './device-hardware-card.vue'; import DeviceHardwareCard from './device-hardware-card.vue';
import DeviceHeaderCard from './device-header-card.vue'; import DeviceHeaderCard from './device-header-card.vue';
import NvrEnvCard from './nvr-env-card.vue';
import NvrDiskCard from './nvr-disk-card.vue'; import NvrDiskCard from './nvr-disk-card.vue';
import NvrRecordCheckCard from './nvr-record-check-card.vue'; import NvrRecordCard from './nvr-record-card.vue';
import SecurityBoxCircuitCard from './security-box-circuit-card.vue'; import SecurityBoxCircuitCard from './security-box-circuit-card.vue';
import SecurityBoxCircuitLinkModal from './security-box-circuit-link-modal.vue'; import SecurityBoxCircuitLinkModal from './security-box-circuit-link-modal.vue';
import SecurityBoxEnvCard from './security-box-env-card.vue'; import SecurityBoxEnvCard from './security-box-env-card.vue';
import SwitchPortCard from './switch-port-card.vue'; import SwitchPortCard from './switch-port-card.vue';
import SwitchPortLinkModal from './switch-port-link-modal.vue'; import SwitchPortLinkModal from './switch-port-link-modal.vue';
export type DeviceCommonCardProps = ComponentInstance<typeof DeviceCommonCard>['$props'];
export { export {
DeviceCommonCard, DeviceCommonCard,
DeviceHardwareCard, DeviceHardwareCard,
DeviceHeaderCard, DeviceHeaderCard,
NvrEnvCard,
NvrDiskCard, NvrDiskCard,
NvrRecordCheckCard, NvrRecordCard,
SecurityBoxCircuitCard, SecurityBoxCircuitCard,
SecurityBoxCircuitLinkModal, SecurityBoxCircuitLinkModal,
SecurityBoxEnvCard, SecurityBoxEnvCard,
@@ -1,47 +0,0 @@
<script setup lang="ts">
import type { NdmNvrFanInfo, NdmNvrPowerSupplyInfo } from '@/apis';
import { FanIcon, PlugIcon } from 'lucide-vue-next';
import { NCard, NFlex, NIcon, NTag } from 'naive-ui';
import { computed, toRefs } from 'vue';
const props = defineProps<{
fanInfo?: NdmNvrFanInfo[];
powerSupplyInfo?: NdmNvrPowerSupplyInfo[];
}>();
const { fanInfo, powerSupplyInfo } = toRefs(props);
const showCard = computed(() => {
return Object.values(props).some((value) => !!value);
});
</script>
<template>
<NCard v-if="showCard" hoverable size="small">
<template #header>
<span>录像机环境状态</span>
</template>
<template #default>
<NFlex vertical>
<NTag v-for="info in fanInfo ?? []" :key="info['索引号']">
<template #icon>
<NIcon :component="FanIcon" />
</template>
<template #default>
<span>风扇{{ info['索引号'] }}: {{ info['风扇转速(rpm)'] }} RPM</span>
</template>
</NTag>
<NTag v-for="info in powerSupplyInfo ?? []" :key="info['索引号']">
<template #icon>
<NIcon :component="PlugIcon" />
</template>
<template #default>
<span>电源{{ info['索引号'] }}: {{ info['电源状态'] }}</span>
</template>
</NTag>
</NFlex>
</template>
</NCard>
</template>
<style scoped></style>
@@ -0,0 +1,235 @@
<script setup lang="ts">
import { getChannelListApi, getRecordCheckApi, reloadAllRecordCheckApi, reloadRecordCheckApi, type NdmNvrResultVO, type RecordItem, type Station } from '@/apis';
import { exportRecordDiagCsv, transformRecordChecks } from '@/helpers';
import { useSettingStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
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, ref, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmNvrResultVO;
station: Station;
}>();
const settingStore = useSettingStore();
const { activeRequests } = storeToRefs(settingStore);
const themeVars = useThemeVars();
const queryClient = useQueryClient();
const { ndmDevice, station } = toRefs(props);
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) => {
if (lossInput.value === 0) {
return true;
} else if (lossInput.value === 1) {
return recordDiag.lostChunks.length > 0;
} else if (lossInput.value === 2) {
return recordDiag.lostChunks.length === 0;
}
return false;
});
});
const { mutate: reloadAllRecordCheck, isPending: reloading } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
await reloadAllRecordCheckApi(90, { stationCode: station.value.code, signal: abortController.value.signal });
},
onSuccess: () => {
window.$message.success('正在逐步刷新中,请稍后点击刷新按钮查看');
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const onExportRecordCheck = () => {
exportRecordDiagCsv(recordDiags.value, station.value.name);
};
const page = ref(1);
const pageSize = ref(10);
const pagedRecordDiags = computed(() => {
const startIndex = (page.value - 1) * pageSize.value;
const endIndex = page.value * pageSize.value;
return recordDiags.value.slice(startIndex, endIndex);
});
const getLostChunkDOMStyle = (lostChunk: RecordItem, duration: RecordItem) => {
const chunk = dayjs(lostChunk.endTime).diff(dayjs(lostChunk.startTime));
const offset = dayjs(lostChunk.startTime).diff(dayjs(duration.startTime));
const total = dayjs(duration.endTime).diff(dayjs(duration.startTime));
return {
left: `${(offset / total) * 100}%`,
width: `${(chunk / total) * 100}%`,
};
};
const { mutate: reloadRecordCheckByGbId } = useMutation({
mutationFn: async (params: { gbCode: string }) => {
abortController.value.abort();
abortController.value = new AbortController();
const channelList = await getChannelListApi(ndmDevice.value, { stationCode: station.value.code, signal: abortController.value.signal });
const channel = channelList.find((channel) => channel.code === params.gbCode);
if (!channel) throw new Error('通道不存在');
window.$message.loading('刷新耗时较长, 请不要多次刷新, 并耐心等待...', {
duration: 1000 * 60 * 60 * 24 * 300,
});
const isSuccess = await reloadRecordCheckApi(channel, 90, { stationCode: station.value.code, signal: abortController.value.signal });
window.$message.destroyAll();
if (isSuccess) {
window.$message.success('刷新成功');
} else {
window.$message.error('刷新失败');
}
},
onSuccess: () => {
refetchRecordChecks();
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
onBeforeUnmount(() => {
abortController.value.abort();
});
</script>
<template>
<NCard hoverable size="small">
<template #header>
<NFlex align="center" :size="24">
<div>录像诊断</div>
<NPopconfirm @positive-click="() => reloadAllRecordCheck()">
<template #trigger>
<NButton secondary size="small" :loading="reloading">更新所有通道录像诊断</NButton>
</template>
<template #default>
<span>确认更新所有通道录像诊断吗?</span>
</template>
</NPopconfirm>
</NFlex>
</template>
<template #header-extra>
<NFlex>
<NTooltip trigger="hover">
<template #trigger>
<NButton size="small" quaternary circle :loading="loading" @click="() => refetchRecordChecks()">
<template #icon>
<NIcon :component="RotateCwIcon" />
</template>
</NButton>
</template>
<template #default>
<span>刷新数据</span>
</template>
</NTooltip>
<NTooltip trigger="hover">
<template #trigger>
<NButton size="small" quaternary circle @click="onExportRecordCheck">
<template #icon>
<NIcon :component="DownloadIcon" />
</template>
</NButton>
</template>
<template #default>
<span>导出录像诊断</span>
</template>
</NTooltip>
</NFlex>
</template>
<template #default>
<NFlex justify="flex-end" style="margin-bottom: 6px">
<NRadioGroup size="small" v-model:value="lossInput">
<NRadioButton label="全部" :value="0" />
<NRadioButton label="有缺失" :value="1" />
<NRadioButton label="无缺失" :value="2" />
</NRadioGroup>
</NFlex>
<template v-for="{ gbCode, channelName, recordDuration, lostChunks } in pagedRecordDiags" :key="gbCode">
<div style="display: flex; justify-content: space-between">
<div>
<span>{{ channelName }}</span>
<span>{{ '\u3000' }}</span>
<span>{{ recordDuration.startTime }} - {{ recordDuration.endTime }}</span>
</div>
<NPopconfirm trigger="click" @positive-click="() => reloadRecordCheckByGbId({ gbCode })">
<template #trigger>
<NButton ghost size="tiny" type="info">刷新</NButton>
</template>
<template #default>
<span>是否确认刷新?</span>
</template>
</NPopconfirm>
</div>
<div style="position: relative; height: 24px; margin: 2px 0" :style="{ backgroundColor: lostChunks.length > 0 ? themeVars.infoColor : themeVars.successColor }">
<template v-for="{ startTime, endTime } in lostChunks" :key="`${startTime}-${endTime}`">
<NPopover trigger="hover">
<template #trigger>
<div style="position: absolute; height: 100%; cursor: pointer; background-color: #eee" :style="getLostChunkDOMStyle({ startTime, endTime }, recordDuration)" />
</template>
<template #default>
<div>开始时间:{{ dayjs(startTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
<div>结束时间:{{ dayjs(endTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
</template>
</NPopover>
</template>
</div>
</template>
</template>
<template #action>
<NFlex justify="flex-end">
<NPagination size="small" :page="page" :page-size="pageSize" :page-count="Math.ceil(recordDiags.length / pageSize)" @update:page="(p) => (page = p)">
<template #prefix>
<span>{{ `共 ${recordDiags.length} 个通道` }}</span>
</template>
</NPagination>
</NFlex>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>
@@ -1,653 +0,0 @@
<script lang="ts">
const DAY_RANGE_VALUE = 24 * 60 * 60 * 1000;
const formatDuration = (ms: number, options?: { withinDay?: boolean }) => {
const { withinDay = false } = options ?? {};
const duration = dayjs.duration(ms);
if (withinDay) {
if (duration.asDays() > 1) {
throw new Error('时长不能超过24小时');
}
}
const days = duration.days();
const hours = duration.hours();
const minutes = duration.minutes();
const seconds = duration.seconds();
let result = '';
if (days > 0) {
result += `${days}`;
}
if (hours > 0) {
result += `${hours}小时`;
}
if (minutes > 0) {
result += `${minutes}分钟`;
}
if (seconds > 0) {
result += `${seconds}`;
}
if (result === '') {
result = '0秒';
}
return result;
};
</script>
<script setup lang="ts">
import {
batchExportRecordCheckApi,
getChannelListApi,
getRecordCheckApi,
pageDefParameterApi,
reloadAllRecordCheckApi,
reloadRecordCheckApi,
type NdmNvrResultVO,
type RecordInfo,
type RecordItem,
type Station,
} from '@/apis';
import { useSettingStore } from '@/stores';
import { downloadByData, parseErrorFeedback } from '@/utils';
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query';
import { refDebounced } from '@vueuse/core';
import { isCancel } from 'axios';
import dayjs from 'dayjs';
import destr from 'destr';
import { DownloadIcon, RotateCwIcon } from 'lucide-vue-next';
import { NButton, NCard, NDataTable, NFlex, NIcon, NInput, NModal, NPagination, NPopconfirm, NPopover, NRadioButton, NRadioGroup, NTooltip, useThemeVars, type DataTableColumns } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, onBeforeUnmount, ref, toRefs, watch } from 'vue';
type DailyLossItem = {
date: string;
total: number; // 缺失时长,单位:ms
percent: number; // 缺失比例,范围:0-100
chunks: (RecordItem & { startValue: number; endValue: number })[];
};
type NdmRecordCheckAggregated = {
gbCode: string;
channelName: string;
range: RecordItem;
dailyLoss: DailyLossItem[];
};
const props = defineProps<{
ndmDevice: NdmNvrResultVO;
station: Station;
}>();
const { ndmDevice, station } = toRefs(props);
const settingStore = useSettingStore();
const { activeRequests } = storeToRefs(settingStore);
const themeVars = useThemeVars();
const queryClient = useQueryClient();
const filterType = ref<'all' | 'some' | 'none'>('all');
const abortController = ref<AbortController>(new AbortController());
const NVR_RECORD_CHECK_KEY = 'nvr-record-check-query';
const deviceUniqueKey = computed(() => [station.value.code, ndmDevice.value.id]);
const DAY_OFFSET = 90;
const {
data: recordChecks,
isFetching: loading,
refetch: refetchRecordChecks,
} = useQuery({
queryKey: computed(() => [NVR_RECORD_CHECK_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value),
refetchInterval: 30 * 1000,
gcTime: 0,
queryFn: async ({ signal }) => {
const checks = await getRecordCheckApi(ndmDevice.value, DAY_OFFSET, [], { stationCode: station.value.code, signal });
return checks;
},
});
watch(activeRequests, (active) => {
if (!active) queryClient.cancelQueries({ queryKey: [NVR_RECORD_CHECK_KEY] });
});
const { mutate: reloadAllRecordCheck, isPending: reloading } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
await reloadAllRecordCheckApi(DAY_OFFSET, { stationCode: station.value.code, signal: abortController.value.signal });
},
onSuccess: () => {
window.$message.success('正在逐步刷新中,请稍后点击刷新按钮查看');
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const { mutate: reloadRecordCheckByGbId } = useMutation({
mutationFn: async (params: { gbCode: string }) => {
abortController.value.abort();
abortController.value = new AbortController();
const channelList = await getChannelListApi(ndmDevice.value, { stationCode: station.value.code, signal: abortController.value.signal });
const channel = channelList.find((channel) => channel.code === params.gbCode);
if (!channel) throw new Error('通道不存在');
window.$message.loading('刷新耗时较长, 请不要多次刷新, 并耐心等待...', {
duration: 1000 * 60 * 60 * 24 * 300,
});
const isSuccess = await reloadRecordCheckApi(channel, DAY_OFFSET, { stationCode: station.value.code, signal: abortController.value.signal });
window.$message.destroyAll();
if (isSuccess) {
window.$message.success('刷新成功');
} else {
window.$message.error('刷新失败');
}
},
onSuccess: () => {
refetchRecordChecks();
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const { mutate: exportRecordCheck, isPending: exporting } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
const { records = [] } = await pageDefParameterApi(
{
model: {
key: 'NVR_GAP_SECONDS',
},
extra: {},
current: 1,
size: 1,
sort: 'id',
order: 'descending',
},
{
signal: abortController.value.signal,
},
);
const gapSeconds = parseInt(records.at(0)?.value ?? '5');
abortController.value.abort();
abortController.value = new AbortController();
const data = await batchExportRecordCheckApi(
{
checkDuration: DAY_OFFSET,
gapSeconds,
stationCode: [station.value.code],
},
{
signal: abortController.value.signal,
},
);
return data;
},
onSuccess: (data) => {
const time = dayjs().format('YYYY-MM-DD_HH-mm-ss');
downloadByData(data, `${station.value.name}_录像缺失记录_${time}.xlsx`);
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
onBeforeUnmount(() => {
abortController.value.abort();
});
// 按天聚合录像缺失片段
const ndmRecordChecksAggregated = computed(() => {
// 1. 解析diagInfo字段
const parsedChecks = (recordChecks.value ?? []).map((check) => {
return { ...check, diagInfo: destr<RecordInfo>(check.diagInfo) };
});
// 2. 按gbCode分组
// 原始数据的基本单元是一个通道在一天内的录像诊断,
// 所以我们要将相同通道的诊断数据组织到一起,于是形成一个Map结构
const recordChecksMap = new Map<string, typeof parsedChecks>();
parsedChecks.forEach((check) => {
const { gbCode } = check;
if (!recordChecksMap.has(gbCode)) {
recordChecksMap.set(gbCode, []);
}
recordChecksMap.get(gbCode)?.push(check);
});
// 3. 按天进行聚合
// 我们的最终目标是从每个通道的录像记录中解析出缺失的录像片段,
// 并按天来组织这些片段,形成NdmRecordCheckAggregated结构
const aggregated = Array.from(recordChecksMap.entries()).map<NdmRecordCheckAggregated>(([gbCode, checks]) => {
// 首先,将该通道的所有录像记录合并到一个数组中,
// 并对这些记录进行排序,确保按时间顺序排列
const records = checks
.flatMap((check) => {
return check.diagInfo.recordList.map((record) => {
const startValue = dayjs(record.startTime).valueOf();
const endValue = dayjs(record.endTime).valueOf();
const startTime = dayjs(record.startTime).format('YYYY-MM-DD HH:mm:ss');
const endTime = dayjs(record.endTime).format('YYYY-MM-DD HH:mm:ss');
return { startValue, endValue, startTime, endTime };
});
})
.sort(({ startValue: startValue1 }, { startValue: startValue2 }) => {
return startValue1 - startValue2;
});
const tomorrow = dayjs().add(1, 'day');
// 由于DAY_OFFSET实际上不包含今天,而获取的数据又是包含今天的,
// 所以实际的时间范围是 DAY_OFFSET + 1 天
const dateLength = DAY_OFFSET + 1;
// 初始化每日缺失记录,
// 在处理完成后,如果有一天的数据没有变化,就说明这一天没有缺失录像
const dailyLoss = Array.from({ length: dateLength }).map<NdmRecordCheckAggregated['dailyLoss'][number]>((_, index) => {
return {
date: tomorrow.subtract(dateLength - index, 'day').format('YYYY-MM-DD'),
total: 0,
percent: 0,
chunks: [],
};
});
// 开始解析按天组织的缺失录像片段,
// 缺失片段的持续时间很可能是跨天甚至是跨越多天的,所以为了将缺失片段分配到每一天,我们采用「游标 + 切片」的设计
// 首先,确定时间范围的开始和结束点
const rangeStart = dayjs(dailyLoss.at(0)?.date).startOf('day').valueOf();
const rangeEnd = dayjs(dailyLoss.at(-1)?.date).add(1, 'day').startOf('day').valueOf();
// 初始化时间游标,从第一天的开始时间开始
let timeCursor = rangeStart;
records.forEach((record) => {
const recordStart = record.startValue;
const recordEnd = record.endValue;
// 如果timeCursor < recordStart,说明 [timeCursor, recordStart] 这段时间的录像是缺失的,
// 而这一段缺失有可能是跨天的,我们需要进行处理
while (timeCursor < recordStart) {
// 当前游标所属的日期
const cursorDate = dayjs(timeCursor).format('YYYY-MM-DD');
// 当前游标所属日期的末尾(下一天的开始时间)
const cursorDateEnd = dayjs(cursorDate).add(1, 'day').startOf('day').valueOf();
// 确定这一段缺失的终点,
// 要么是 [timeCursor, recordStart](没跨天),
// 要么是 [timeCursor, cursorDateEnd](跨天),
// 我们取较小的那个
const sliceEnd = Math.min(recordStart, cursorDateEnd);
// 只要这段缺失有效,就记下它
if (timeCursor < sliceEnd) {
const loss = dailyLoss.find((loss) => loss.date === cursorDate);
if (!!loss) {
const startValue = timeCursor;
const endValue = sliceEnd;
const startTime = dayjs(startValue).format('YYYY-MM-DD HH:mm:ss');
const endTime = dayjs(endValue).format('YYYY-MM-DD HH:mm:ss');
loss.chunks.push({ startValue, endValue, startTime, endTime });
loss.total += endValue - startValue;
loss.percent = (loss.total / DAY_RANGE_VALUE) * 100;
}
// 推进游标
timeCursor = sliceEnd;
} else {
// 假设这段缺失无效,说明这一天的数据有错乱,
// 我们推进游标到下一天的开始时间
timeCursor = cursorDateEnd;
}
}
// 上面我们处理了 [timeCursor, recordStart] 这段时间的缺失,
// 而 [recordStart, recordEnd] 这段时间的录像是完整的,
// 所以我们可以直接推进游标到 recordEnd
// 使用 Math.max 是为了防止两段录像记录交叉从而导致游标又发生回退
timeCursor = Math.max(timeCursor, recordEnd);
});
// 现在我们处理完了所有的录像记录,但如果游标还没有到rangeEnd,
// 说明还有一段缺失的录像记录没有被处理到,
// 我们需要将这一段缺失记录分配到最后一天
while (timeCursor < rangeEnd) {
const cursorDate = dayjs(timeCursor).format('YYYY-MM-DD');
const cursorDateEnd = dayjs(cursorDate).add(1, 'day').startOf('day').valueOf();
const sliceEnd = Math.min(rangeEnd, cursorDateEnd);
if (timeCursor < sliceEnd) {
const loss = dailyLoss.find((loss) => loss.date === cursorDate);
if (!!loss) {
const startValue = timeCursor;
const endValue = sliceEnd;
const startTime = dayjs(startValue).format('YYYY-MM-DD HH:mm:ss');
const endTime = dayjs(endValue).format('YYYY-MM-DD HH:mm:ss');
loss.chunks.push({ startValue, endValue, startTime, endTime });
loss.total += endValue - startValue;
loss.percent = (loss.total / DAY_RANGE_VALUE) * 100;
}
timeCursor = sliceEnd;
} else {
timeCursor = cursorDateEnd;
}
}
return {
gbCode: gbCode,
channelName: checks.at(-1)?.name ?? '',
range: {
startTime: records.at(0)?.startTime ?? '',
endTime: records.at(-1)?.endTime ?? '',
},
dailyLoss: dailyLoss,
};
});
// 最后我们把所有的gbCode按照字典序进行排序
return aggregated.sort((check1, check2) => {
return check1.gbCode.localeCompare(check2.gbCode);
});
});
const searchInput = ref<string>('');
const searchInputDebounced = refDebounced(searchInput, 100);
const ndmRecordChecksSearched = computed(() => {
if (!searchInputDebounced.value.trim()) {
return ndmRecordChecksAggregated.value;
}
return ndmRecordChecksAggregated.value.filter(({ channelName }) => {
return channelName.includes(searchInputDebounced.value);
});
});
const ndmRecordChecksFiltered = computed(() => {
// 最后一天就是「今天」,录像不可能完整,slice的时候别算进去
return ndmRecordChecksSearched.value.filter(({ dailyLoss }) => {
if (filterType.value === 'all') {
return true;
} else if (filterType.value === 'some') {
// return dailyLoss.slice(0, -1).some(({ percent }) => percent > 0);
for (let i = 0; i < dailyLoss.length - 1; i++) {
if ((dailyLoss[i]?.percent ?? 0) > 0) {
return true;
}
}
return false;
} else if (filterType.value === 'none') {
// return dailyLoss.slice(0, -1).every(({ percent }) => percent === 0);
for (let i = 0; i < dailyLoss.length - 1; i++) {
if ((dailyLoss[i]?.percent ?? 0) !== 0) {
return false;
}
}
return true;
}
return false;
});
});
const page = ref(1);
const pageSize = ref(10);
const ndmRecordChecksPaged = computed(() => {
const startIndex = (page.value - 1) * pageSize.value;
const endIndex = page.value * pageSize.value;
return ndmRecordChecksFiltered.value.slice(startIndex, endIndex);
});
// 当车站号、设备IP、最后诊断时间或筛选类型变化时,重置分页为第一页
watch([() => station.value.code, () => ndmDevice.value.ipAddress, () => ndmDevice.value.lastDiagTime, filterType, searchInputDebounced], () => {
page.value = 1;
});
// 当车站号、设备IP变化时,重置搜索内容,并将筛选类型重置为「全部」
watch([() => station.value.code, () => ndmDevice.value.ipAddress], () => {
searchInput.value = '';
filterType.value = 'all';
});
// 录像诊断块的交互
const dailyCheckContext = ref<{
show: boolean;
x: number;
y: number;
info?: DailyLossItem;
}>({
show: false,
x: 0,
y: 0,
});
// 为了提升性能,不循环渲染Popover,而改为manual模式,
// 但是当鼠标移动到Popover上时,将触发录像诊断div块的mouseleave事件,从而导致Popover隐藏。
// 为了解决这个问题,当鼠标移出录像诊断块,延迟100ms后再隐藏Popover
// 在延时期间,如果鼠标再次移入录像诊断块或移入Popover,则取消隐藏Popover的延迟操作,
// 当鼠标离开Popover,再次延时隐藏Popover。
const popoverTimer = ref<ReturnType<typeof setTimeout> | null>(null);
const showDailyCheckPopover = (event: MouseEvent, dailyLossItem: DailyLossItem) => {
if (!!popoverTimer.value) {
clearTimeout(popoverTimer.value);
popoverTimer.value = null;
}
const { target } = event;
if (!target) return;
const { width, left, top } = (target as HTMLDivElement).getBoundingClientRect();
dailyCheckContext.value = {
show: true,
x: left + width / 2,
y: top,
info: dailyLossItem,
};
};
const hideDailyCheckPopover = () => {
popoverTimer.value = setTimeout(() => {
dailyCheckContext.value.show = false;
}, 100);
};
const onMouseEnterDailyCheckPopover = () => {
if (!!popoverTimer.value) {
clearTimeout(popoverTimer.value);
popoverTimer.value = null;
}
};
const onMouseLeaveDailyCheckPopover = () => {
hideDailyCheckPopover();
};
// 录像缺失详情弹窗
const showDailyLossModal = ref(false);
const onClickDailyCheck = () => {
const { info } = dailyCheckContext.value;
if (!info) return;
const { total } = info;
if (total === 0) return;
showDailyLossModal.value = true;
};
const columns: DataTableColumns<DailyLossItem['chunks'][number]> = [
{ title: '开始时间', key: 'startTime' },
{ title: '结束时间', key: 'endTime' },
{
title: '持续时间',
key: 'duration',
render: ({ startValue, endValue }) => {
return formatDuration(endValue - startValue, { withinDay: true });
},
},
];
</script>
<template>
<NCard hoverable size="small">
<template #header>
<NFlex align="center" :size="24">
<div>录像诊断</div>
<NPopconfirm @positive-click="() => reloadAllRecordCheck()">
<template #trigger>
<NButton secondary size="small" :loading="reloading">更新所有通道录像诊断</NButton>
</template>
<template #default>
<span>确认更新所有通道录像诊断吗?</span>
</template>
</NPopconfirm>
</NFlex>
</template>
<template #header-extra>
<NFlex>
<NTooltip trigger="hover">
<template #trigger>
<NButton size="small" quaternary circle :loading="loading" @click="() => refetchRecordChecks()">
<template #icon>
<NIcon :component="RotateCwIcon" />
</template>
</NButton>
</template>
<template #default>
<span>刷新数据</span>
</template>
</NTooltip>
<NTooltip trigger="hover">
<template #trigger>
<NButton size="small" quaternary circle :loading="exporting" @click="() => exportRecordCheck()">
<template #icon>
<NIcon :component="DownloadIcon" />
</template>
</NButton>
</template>
<template #default>
<span>导出录像诊断</span>
</template>
</NTooltip>
</NFlex>
</template>
<template #default>
<NFlex justify="flex-end" align="center" :wrap="false" style="width: 100%; margin-bottom: 6px">
<NInput v-model:value="searchInput" placeholder="搜索通道名称" clearable />
<NRadioGroup size="small" v-model:value="filterType">
<NRadioButton label="全部" :value="'all'" />
<NRadioButton label="有缺失" :value="'some'" />
<NRadioButton label="无缺失" :value="'none'" />
</NRadioGroup>
</NFlex>
<template v-for="{ gbCode, channelName, range, dailyLoss } in ndmRecordChecksPaged" :key="gbCode">
<div style="display: flex; justify-content: space-between">
<div>
<span>{{ channelName }}</span>
<span>{{ '\u3000' }}</span>
<span>{{ range.startTime }} - {{ range.endTime }}</span>
</div>
<NPopconfirm trigger="click" @positive-click="() => reloadRecordCheckByGbId({ gbCode })">
<template #trigger>
<NButton ghost size="tiny" type="info">刷新</NButton>
</template>
<template #default>
<span>是否确认刷新?</span>
</template>
</NPopconfirm>
</div>
<div
style="position: relative; height: 24px; margin: 2px 0; background-color: #ccc; display: grid"
:style="{
gridTemplateRows: `1fr`,
gridTemplateColumns: `repeat(${dailyLoss.length}, 1fr)`,
}"
>
<template v-for="({ date, total, percent, chunks }, index) in dailyLoss" :key="date">
<div
style="border-width: 0 1px; border-style: solid"
:style="{
cursor: percent > 0 ? 'pointer' : 'default',
borderColor: themeVars.baseColor,
backgroundColor: (() => {
// 如果是最后一天(今天),且录像的确持续到了最后一天,则不设置背景颜色
if (index === dailyLoss.length - 1) {
if (dayjs(dailyLoss.at(-1)?.date).startOf('day').diff(dayjs(range.endTime)) < 0) {
return 'transparent';
}
}
// 不缺失,设置为绿色
if (percent === 0) {
return `rgb(24, 160, 88)`;
}
// 将缺失占比映射到范围为 [0.2, 1] 的红色透明度通道
const opacity = 0.2 + (1 - 0.2) * (percent / 100);
return `rgba(208, 48, 80, ${opacity})`;
})(),
}"
@mouseenter="(event) => showDailyCheckPopover(event, { date, total, percent, chunks })"
@mouseleave="hideDailyCheckPopover"
@click="onClickDailyCheck"
></div>
</template>
</div>
</template>
</template>
<template #action>
<NFlex justify="flex-end">
<NPagination size="small" :page="page" :page-size="pageSize" :page-count="Math.ceil(ndmRecordChecksFiltered.length / pageSize)" @update:page="(p) => (page = p)">
<template #prefix>
<span>{{ `共 ${ndmRecordChecksFiltered.length} 个通道` }}</span>
</template>
</NPagination>
</NFlex>
</template>
</NCard>
<NPopover
trigger="manual"
:show="dailyCheckContext.show"
:x="dailyCheckContext.x"
:y="dailyCheckContext.y"
:show-arrow="false"
@mouseenter="onMouseEnterDailyCheckPopover"
@mouseleave="onMouseLeaveDailyCheckPopover"
>
<template #default>
<template v-if="!!dailyCheckContext.info">
<div>日期:{{ dailyCheckContext.info.date }}</div>
<template v-if="dailyCheckContext.info.percent > 0">
<div>缺失时长:{{ formatDuration(dailyCheckContext.info.total, { withinDay: true }) }}</div>
<div>缺失比例:{{ dailyCheckContext.info.percent.toFixed(2) }}%</div>
<div style="font-size: xx-small; opacity: 0.5; cursor: pointer" @click="onClickDailyCheck">点击查看详情</div>
</template>
<template v-else>
<div>录像完整</div>
</template>
</template>
</template>
</NPopover>
<NModal v-model:show="showDailyLossModal" preset="card" title="录像缺失详情" style="width: 600px">
<template #default>
<template v-if="!!dailyCheckContext.info">
<div style="margin-bottom: 16px; font-weight: bold">{{ dailyCheckContext.info.date }} 共缺失 {{ dailyCheckContext.info.chunks.length }} 个录像片段</div>
<NDataTable :columns="columns" :data="dailyCheckContext.info.chunks" :pagination="{ pageSize: 10 }" size="small" :min-height="400" :max-height="400" />
</template>
</template>
</NModal>
</template>
<style scoped></style>
@@ -2,6 +2,8 @@
import { import {
detailDeviceApi, detailDeviceApi,
probeDeviceApi, probeDeviceApi,
rebootSecurityBoxApi,
turnCitcuitStatusApi,
updateDeviceApi, updateDeviceApi,
type LinkDescription, type LinkDescription,
type NdmDeviceResultVO, type NdmDeviceResultVO,
@@ -14,7 +16,6 @@ import { SecurityBoxCircuitLinkModal } from '@/components';
import { usePermission } from '@/composables'; import { usePermission } from '@/composables';
import { SELECT_DEVICE_FN_INJECTION_KEY } from '@/constants'; import { SELECT_DEVICE_FN_INJECTION_KEY } from '@/constants';
import { PERMISSION_TYPE_LITERALS } from '@/enums'; import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { normalizeSecurityBoxWriteCommunity, dispatchRebootSecurityBoxApi, dispatchTurnCircuitStatusApi, normalizeSecurityBoxCircuitIndex } from '@/helpers';
import { useDeviceStore, useSettingStore } from '@/stores'; import { useDeviceStore, useSettingStore } from '@/stores';
import { parseErrorFeedback } from '@/utils'; import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query'; import { useMutation } from '@tanstack/vue-query';
@@ -30,7 +31,6 @@ import { computed, inject, ref, toRefs } from 'vue';
const props = defineProps<{ const props = defineProps<{
ndmDevice: NdmSecurityBoxResultVO; ndmDevice: NdmSecurityBoxResultVO;
station: Station; station: Station;
vendor?: string;
circuits?: NdmSecurityBoxCircuit[]; circuits?: NdmSecurityBoxCircuit[];
}>(); }>();
@@ -44,7 +44,7 @@ const { useLocalDB } = storeToRefs(settingStore);
const { hasPermission } = usePermission(); const { hasPermission } = usePermission();
const { ndmDevice, station, vendor, circuits } = toRefs(props); const { ndmDevice, station, circuits } = toRefs(props);
const showCard = computed(() => !!circuits.value && circuits.value.length > 0); const showCard = computed(() => !!circuits.value && circuits.value.length > 0);
@@ -52,10 +52,6 @@ const localCircuits = ref<NdmSecurityBoxCircuit[]>([]);
watchImmediate(circuits, (newCircuits) => { watchImmediate(circuits, (newCircuits) => {
localCircuits.value = newCircuits?.map((circuit) => ({ ...circuit })) ?? []; localCircuits.value = newCircuits?.map((circuit) => ({ ...circuit })) ?? [];
const circuitCount = ndmDevice.value.circuitCount;
if (!!circuitCount) {
localCircuits.value = localCircuits.value.slice(0, circuitCount);
}
}); });
const getCircuitStatusTagType = (circuit: NdmSecurityBoxCircuit): TagProps['type'] => { const getCircuitStatusTagType = (circuit: NdmSecurityBoxCircuit): TagProps['type'] => {
@@ -83,19 +79,13 @@ const { mutate: turnStatus, isPending: turning } = useMutation({
window.$loadingBar.start(); window.$loadingBar.start();
const { circuitIndex, newStatus } = params; const { circuitIndex, newStatus } = params;
const community = normalizeSecurityBoxWriteCommunity(ndmDevice.value, vendor.value); if (!ndmDevice.value.ipAddress) {
const ipAddress = ndmDevice.value.ipAddress;
if (!ipAddress) {
throw new Error('设备IP地址不存在'); throw new Error('设备IP地址不存在');
} }
const status = newStatus ? 1 : 0; const status = newStatus ? 1 : 0;
const stationCode = station.value.code; const stationCode = station.value.code;
const signal = abortController.value.signal; const signal = abortController.value.signal;
await turnCitcuitStatusApi(ndmDevice.value.ipAddress, circuitIndex, status, { stationCode, signal });
const turnCircuitStatusApi = dispatchTurnCircuitStatusApi(vendor.value);
const normalizedCircuitIndex = normalizeSecurityBoxCircuitIndex(circuitIndex, vendor.value);
await turnCircuitStatusApi(community, ipAddress, normalizedCircuitIndex, status, { stationCode, signal });
await probeDeviceApi(ndmDevice.value, { stationCode, signal }); await probeDeviceApi(ndmDevice.value, { stationCode, signal });
return status; return status;
}, },
@@ -120,17 +110,13 @@ const { mutate: reboot, isPending: rebooting } = useMutation({
window.$loadingBar.start(); window.$loadingBar.start();
const community = normalizeSecurityBoxWriteCommunity(ndmDevice.value, vendor.value); if (!ndmDevice.value.ipAddress) {
const ipAddress = ndmDevice.value.ipAddress;
if (!ipAddress) {
throw new Error('设备IP地址不存在'); throw new Error('设备IP地址不存在');
} }
const stationCode = station.value.code; const stationCode = station.value.code;
const signal = abortController.value.signal; const signal = abortController.value.signal;
await rebootSecurityBoxApi(ndmDevice.value.ipAddress, { stationCode, signal });
const rebootSecurityBoxApi = dispatchRebootSecurityBoxApi(vendor.value);
await rebootSecurityBoxApi(community, ipAddress, { stationCode, signal });
}, },
onSuccess: () => { onSuccess: () => {
window.$loadingBar.finish(); window.$loadingBar.finish();
@@ -216,7 +202,7 @@ const contextmenuOptions = computed<DropdownOption[]>(() => [
if (!lowerDevice) return; if (!lowerDevice) return;
window.$dialog.warning({ window.$dialog.warning({
title: '确认解除关联吗?', title: '确认解除关联吗?',
content: `将解除【空开${circuitIndex + 1}】与【${lowerDevice.name}】的关联关系。`, content: `将解除【电路${circuitIndex + 1}】与【${lowerDevice.name}】的关联关系。`,
style: { width: '600px' }, style: { width: '600px' },
contentStyle: { height: '60px' }, contentStyle: { height: '60px' },
negativeText: '取消', negativeText: '取消',
@@ -313,7 +299,7 @@ const { mutate: unlinkDevice } = useMutation({
<NCard v-if="showCard" hoverable size="small"> <NCard v-if="showCard" hoverable size="small">
<template #header> <template #header>
<NFlex align="center"> <NFlex align="center">
<span>空开状态</span> <span>电路状态</span>
<NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => reboot()"> <NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => reboot()">
<template #trigger> <template #trigger>
<NButton secondary size="small" :loading="rebooting">重合闸</NButton> <NButton secondary size="small" :loading="rebooting">重合闸</NButton>
@@ -338,7 +324,7 @@ const { mutate: unlinkDevice } = useMutation({
<span>{{ getCircuitStatusText(circuit) }}</span> <span>{{ getCircuitStatusText(circuit) }}</span>
</template> </template>
</NTag> </NTag>
<span>空开{{ circuitIndex + 1 }}</span> <span>电路{{ circuitIndex + 1 }}</span>
</NFlex> </NFlex>
<NFlex justify="end" align="center"> <NFlex justify="end" align="center">
<NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => turnStatus({ circuitIndex: circuitIndex, newStatus: circuit.status !== 1 })"> <NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => turnStatus({ circuitIndex: circuitIndex, newStatus: circuit.status !== 1 })">
@@ -346,7 +332,7 @@ const { mutate: unlinkDevice } = useMutation({
<NSwitch size="small" :value="circuit.status === 1" :loading="turning" /> <NSwitch size="small" :value="circuit.status === 1" :loading="turning" />
</template> </template>
<template #default> <template #default>
<span>确定要{{ circuit.status === 1 ? '关闭' : '开启' }}空开{{ circuitIndex + 1 }}吗?</span> <span>确定要{{ circuit.status === 1 ? '关闭' : '开启' }}电路{{ circuitIndex + 1 }}吗?</span>
</template> </template>
</NPopconfirm> </NPopconfirm>
</NFlex> </NFlex>
@@ -60,7 +60,7 @@ const { mutate: linkPortToDevice, isPending: linking } = useMutation({
const upperDeviceDbId = ndmDevice.value.id; const upperDeviceDbId = ndmDevice.value.id;
if (!upperDeviceDbId) throw new Error('本设备没有ID'); if (!upperDeviceDbId) throw new Error('本设备没有ID');
if (circuitIndex.value === undefined) throw new Error('该空开不存在'); if (circuitIndex.value === undefined) throw new Error('该电路不存在');
if (!lowerDevice.value) throw new Error('请选择要关联的设备'); if (!lowerDevice.value) throw new Error('请选择要关联的设备');
const lowerDeviceType = tryGetDeviceType(lowerDevice.value?.deviceType); const lowerDeviceType = tryGetDeviceType(lowerDevice.value?.deviceType);
@@ -195,7 +195,7 @@ const onCancel = () => {
<template #header> <template #header>
<span>{{ ndmDevice.name }}</span> <span>{{ ndmDevice.name }}</span>
<span> - </span> <span> - </span>
<span>空开{{ circuitIndex ? circuitIndex + 1 : '-' }}</span> <span>电路{{ circuitIndex ? circuitIndex + 1 : '-' }}</span>
<span> - </span> <span> - </span>
<span>关联设备</span> <span>关联设备</span>
</template> </template>
@@ -16,35 +16,40 @@ const showCard = computed(() => {
return Object.values(props).some((value) => !!value); return Object.values(props).some((value) => !!value);
}); });
// 防雷状态
const lightningProtectionStatus = computed(() => {
if (!switches?.value || switches.value.length < 2) return null;
const status = switches.value.at(0)!;
return status === 0 ? '失效' : status === 1 ? '生效' : '-';
});
// 门禁状态 // 门禁状态
const accessControlStatus = computed(() => { const accessControlStatus = computed(() => {
if (!switches?.value || switches.value.length < 1) return null; if (!switches?.value || switches.value.length < 1) return null;
const status = switches.value.at(1)!; const status = switches.value.at(0)!;
return status === 0 ? '开门' : status === 1 ? '关门' : '-'; return status === 0 ? '开门' : status === 1 ? '关门' : '-';
}); });
// 防雷状态
const lightningProtectionStatus = computed(() => {
if (!switches?.value || switches.value.length < 2) return null;
const status = switches.value.at(1)!;
return status === 0 ? '正常' : status === 1 ? '失效' : '-';
});
const getStatusTagType = (status: string | null) => { const getStatusTagType = (status: string | null) => {
if (['生效', '关门'].includes(status ?? '')) return 'success'; if (['正常'].includes(status ?? '')) return 'success';
if (['失效', '开门'].includes(status ?? '')) return 'error'; if (['失效'].includes(status ?? '')) return 'error';
return 'default'; return 'default';
}; };
const formattedFanSpeeds = computed(() => {
if (!fanSpeeds?.value || fanSpeeds.value.length === 0) return null;
return fanSpeeds.value.map((speed, index) => `风扇${index + 1}: ${speed} RPM`).join(', ');
});
</script> </script>
<template> <template>
<NCard v-if="showCard" hoverable size="small"> <NCard v-if="showCard" hoverable size="small">
<template #header> <template #header>
<span>安防箱环境状态</span> <span>安防箱状态</span>
</template> </template>
<template #default> <template #default>
<NFlex vertical> <NFlex vertical>
<NTag v-if="!!temperature"> <NTag>
<template #icon> <template #icon>
<NIcon :component="ThermometerIcon" /> <NIcon :component="ThermometerIcon" />
</template> </template>
@@ -52,7 +57,7 @@ const getStatusTagType = (status: string | null) => {
<span>温度: {{ temperature }}</span> <span>温度: {{ temperature }}</span>
</template> </template>
</NTag> </NTag>
<NTag v-if="!!humidity"> <NTag>
<template #icon> <template #icon>
<NIcon :component="DropletIcon" /> <NIcon :component="DropletIcon" />
</template> </template>
@@ -60,15 +65,15 @@ const getStatusTagType = (status: string | null) => {
<span>湿度: {{ humidity }}%</span> <span>湿度: {{ humidity }}%</span>
</template> </template>
</NTag> </NTag>
<NTag v-for="(speed, index) in fanSpeeds ?? []" :key="index"> <NTag>
<template #icon> <template #icon>
<NIcon :component="FanIcon" /> <NIcon :component="FanIcon" />
</template> </template>
<template #default> <template #default>
<span>风扇{{ index + 1 }}: {{ speed }} RPM</span> <span>风扇: {{ formattedFanSpeeds }}</span>
</template> </template>
</NTag> </NTag>
<NTag v-if="!!accessControlStatus" :type="getStatusTagType(accessControlStatus)"> <NTag :type="getStatusTagType(accessControlStatus)">
<template #icon> <template #icon>
<NIcon :component="ShieldIcon" /> <NIcon :component="ShieldIcon" />
</template> </template>
@@ -76,7 +81,7 @@ const getStatusTagType = (status: string | null) => {
<span>门禁: {{ accessControlStatus }}</span> <span>门禁: {{ accessControlStatus }}</span>
</template> </template>
</NTag> </NTag>
<NTag v-if="!!lightningProtectionStatus" :type="getStatusTagType(lightningProtectionStatus)"> <NTag :type="getStatusTagType(lightningProtectionStatus)">
<template #icon> <template #icon>
<NIcon :component="ZapIcon" /> <NIcon :component="ZapIcon" />
</template> </template>
@@ -33,7 +33,7 @@ const { hasPermission } = usePermission();
const { ndmDevice, station, ports } = toRefs(props); const { ndmDevice, station, ports } = toRefs(props);
const showCard = computed(() => !!ports.value && ports.value.length > 0); const showCard = computed(() => !!ports.value);
const switchSlots = computed(() => { const switchSlots = computed(() => {
// 解析端口名称,将端口按槽位进行分组 // 解析端口名称,将端口按槽位进行分组
@@ -31,7 +31,7 @@ const { ndmDevice, station } = toRefs(props);
const showDetailModal = ref(false); const showDetailModal = ref(false);
const detailTableColumns: DataTableColumns<SecurityBoxCircuitRowData> = [ const detailTableColumns: DataTableColumns<SecurityBoxCircuitRowData> = [
{ title: '空开序号', key: 'number' }, { title: '电路序号', key: 'number' },
{ {
title: '状态', title: '状态',
key: 'status', key: 'status',
@@ -98,7 +98,7 @@ const tableColumns: DataTableColumns<SecurityBoxRuntimeRowData> = [
}, },
// { title: '开关状态', key: 'switches' }, // { title: '开关状态', key: 'switches' },
{ {
title: '空开状态', title: '电路状态',
key: 'circuits', key: 'circuits',
render(rowData) { render(rowData) {
const { info } = rowData.diagInfo; const { info } = rowData.diagInfo;
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { NFlex } from 'naive-ui'; import { NFlex } from 'naive-ui';
import { DeviceCommonCard, DeviceHeaderCard, type DeviceCommonCardProps } from '@/components'; import { DeviceCommonCard, DeviceHeaderCard } from '@/components';
import type { NdmAlarmHostResultVO, Station } from '@/apis'; import type { NdmAlarmHostResultVO, Station } from '@/apis';
import { computed, toRefs } from 'vue'; import { computed, toRefs } from 'vue';
@@ -11,18 +11,13 @@ const props = defineProps<{
const { ndmDevice, station } = toRefs(props); const { ndmDevice, station } = toRefs(props);
const commonInfo = computed<DeviceCommonCardProps['commonInfo']>(() => { const commonInfo = computed(() => {
const { createdTime, updatedTime, manufacturer } = ndmDevice.value; const { createdTime, updatedTime, manufacturer } = ndmDevice.value;
return [ return {
{ 创建时间: createdTime ?? '-',
title: '设备接入信息', 更新时间: updatedTime ?? '-',
items: [ 制造商: manufacturer ?? '-',
{ label: '创建时间', value: createdTime || '-' }, };
{ label: '更新时间', value: updatedTime || '-' },
{ label: '制造商', value: manufacturer || '-' },
],
},
];
}); });
</script> </script>
@@ -16,12 +16,11 @@ const isCameraTypeCode = (code: string): code is CameraType => {
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import type { NdmCameraDiagInfo, NdmCameraResultVO, Station } from '@/apis'; import type { NdmCameraResultVO, Station } from '@/apis';
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, type DeviceCommonCardProps } from '@/components'; import { DeviceCommonCard, DeviceHeaderCard } from '@/components';
import { useSettingStore } from '@/stores'; import { useSettingStore } from '@/stores';
import { useQuery, useQueryClient } from '@tanstack/vue-query'; import { useQuery, useQueryClient } from '@tanstack/vue-query';
import axios from 'axios'; import axios from 'axios';
import destr from 'destr';
import { NDescriptions, NDescriptionsItem, NFlex } from 'naive-ui'; import { NDescriptions, NDescriptionsItem, NFlex } from 'naive-ui';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { computed, toRefs, watch } from 'vue'; import { computed, toRefs, watch } from 'vue';
@@ -114,17 +113,7 @@ watch(activeRequests, (active) => {
} }
}); });
const lastDiagInfo = computed(() => { const commonInfo = computed(() => {
const result = destr<any>(ndmDevice.value.lastDiagInfo);
if (!result) return null;
if (typeof result !== 'object') return null;
return result as NdmCameraDiagInfo;
});
const commonInfo = computed<DeviceCommonCardProps['commonInfo']>(() => {
const { stCommonInfo } = lastDiagInfo.value ?? {};
const { 设备ID, 软件版本, 设备厂商, 设备别名, 设备型号, 硬件版本 } = stCommonInfo ?? {};
const { ethInfo, ipInfo } = lastDiagInfo.value ?? {};
const { const {
createdTime, createdTime,
updatedTime, updatedTime,
@@ -137,58 +126,22 @@ const commonInfo = computed<DeviceCommonCardProps['commonInfo']>(() => {
onvifMinorIndex, onvifMinorIndex,
icmpEnabled, icmpEnabled,
community, community,
ipAddress,
// //
} = ndmDevice.value; } = ndmDevice.value;
return {
let operStatus = '-'; 创建时间: createdTime ?? '-',
if (!!ethInfo?.operStatus) { 更新时间: updatedTime ?? '-',
operStatus = ethInfo?.operStatus === '1' ? '正常' : '异常'; 制造商: manufacturer ?? '-',
} GB28181启用: `${!!gb28181Enabled ? '是' : '否'}`,
ONVIF端口: `${onvifPort ?? '-'}`,
return [ ONVIF用户名: onvifUsername ?? '-',
{ ONVIF密码: onvifPassword ?? '-',
title: '设备型号信息', ONVIF主流索引: `${onvifMajorIndex ?? '-'}`,
items: [ ONVIF辅流索引: `${onvifMinorIndex ?? '-'}`,
{ label: '设备ID', value: 设备ID || '-' }, ICMP启用: `${!!icmpEnabled ? '是' : ''}`,
{ label: '软件版本', value: 软件版本 || '-' }, 团体字符串: community ?? '-',
{ label: '设备厂商', value: 设备厂商 || manufacturer || '-' }, };
{ label: '设备别名', value: 设备别名 || '-' },
{ label: '设备型号', value: 设备型号 || '-' },
{ label: '硬件版本', value: 硬件版本 || '-' },
],
},
{
title: '设备网卡信息',
items: [
{ label: 'IP地址', value: ipAddress || '-' },
{ label: '子网掩码', value: ipInfo?.mASK || '-' },
{ label: 'MAC地址', value: ethInfo?.macAddress || '-' },
{ label: '连接速率', value: ethInfo?.speed || '-' },
{ label: 'MTU', value: ethInfo?.mTU || '-' },
{ label: '运行状态', value: operStatus },
],
},
{
title: '设备接入信息',
items: [
{ label: '创建时间', value: createdTime || '-' },
{ label: '更新时间', value: updatedTime || '-' },
{ label: 'GB28181启用', value: `${!!gb28181Enabled ? '是' : '否'}` },
{ label: 'ICMP启用', value: `${!!icmpEnabled ? '是' : '否'}` },
{ label: 'ONVIF用户名', value: onvifUsername || '-' },
{ label: 'ONVIF密码', value: onvifPassword || '-' },
{ label: 'ONVIF主流索引', value: `${onvifMajorIndex ?? '-'}` },
{ label: 'ONVIF辅流索引', value: `${onvifMinorIndex ?? '-'}` },
{ label: 'ONVIF端口', value: `${onvifPort ?? '-'}` },
{ label: '团体字符串', value: community || '-' },
],
},
];
}); });
const cpuUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.CPU使用率);
const memUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.内存使用率);
</script> </script>
<template> <template>
@@ -202,7 +155,6 @@ const memUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.内存使用
</template> </template>
</DeviceHeaderCard> </DeviceHeaderCard>
<DeviceCommonCard :common-info="commonInfo" /> <DeviceCommonCard :common-info="commonInfo" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
</NFlex> </NFlex>
</template> </template>
@@ -1,14 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NdmCameraResultVO, Station } from '@/apis'; import type { NdmCameraResultVO, Station } from '@/apis';
import { import { DeviceAlarmHistoryCard, DeviceIcmpHistoryCard, HistoryDiagFilterCard, type DeviceAlarmHistoryCardProps, type DeviceIcmpHistoryCardProps } from '@/components';
DeviceAlarmHistoryCard,
DeviceIcmpHistoryCard,
DeviceUsageHistoryCard,
HistoryDiagFilterCard,
type DeviceAlarmHistoryCardProps,
type DeviceIcmpHistoryCardProps,
type DeviceUsageHistoryCardProps,
} from '@/components';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { NFlex, type SelectOption } from 'naive-ui'; import { NFlex, type SelectOption } from 'naive-ui';
import { onMounted, ref, toRefs, watch } from 'vue'; import { onMounted, ref, toRefs, watch } from 'vue';
@@ -23,7 +15,6 @@ const { ndmDevice, station } = toRefs(props);
const historyDiagOptions: SelectOption[] = [ const historyDiagOptions: SelectOption[] = [
{ label: '设备状态', value: 'icmp' }, { label: '设备状态', value: 'icmp' },
{ label: '设备告警', value: 'alarm' }, { label: '设备告警', value: 'alarm' },
{ label: '硬件占用', value: 'usage' },
]; ];
const getWeekRange = (): [number, number] => { const getWeekRange = (): [number, number] => {
const now = dayjs(); const now = dayjs();
@@ -36,8 +27,7 @@ const selected = ref<string[]>([...historyDiagOptions.map((option) => `${option.
const loading = ref<boolean>(false); const loading = ref<boolean>(false);
const icmpLoading = ref<boolean>(false); const icmpLoading = ref<boolean>(false);
const alarmLoading = ref<boolean>(false); const alarmLoading = ref<boolean>(false);
const usageLoading = ref<boolean>(false); watch([icmpLoading, alarmLoading], (loadings) => {
watch([icmpLoading, alarmLoading, usageLoading], (loadings) => {
loading.value = loadings.some((loading) => loading); loading.value = loadings.some((loading) => loading);
}); });
const icmpHistoryQueryFn = ref<() => void>(); const icmpHistoryQueryFn = ref<() => void>();
@@ -48,14 +38,9 @@ const alarmHistoryQueryFn = ref<() => void>();
const onExposeAlarmHistoryQueryFn: DeviceAlarmHistoryCardProps['onExposeQueryFn'] = (queryFn) => { const onExposeAlarmHistoryQueryFn: DeviceAlarmHistoryCardProps['onExposeQueryFn'] = (queryFn) => {
alarmHistoryQueryFn.value = queryFn; alarmHistoryQueryFn.value = queryFn;
}; };
const usageHistoryQueryFn = ref<() => void>();
const onExposeUsageHistoryQueryFn: DeviceUsageHistoryCardProps['onExposeQueryFn'] = (queryFn) => {
usageHistoryQueryFn.value = queryFn;
};
const queryData = () => { const queryData = () => {
if (selected.value.includes('icmp')) icmpHistoryQueryFn.value?.(); if (selected.value.includes('icmp')) icmpHistoryQueryFn.value?.();
if (selected.value.includes('alarm')) alarmHistoryQueryFn.value?.(); if (selected.value.includes('alarm')) alarmHistoryQueryFn.value?.();
if (selected.value.includes('usage')) usageHistoryQueryFn.value?.();
}; };
const onQuery = () => { const onQuery = () => {
queryData(); queryData();
@@ -85,16 +70,6 @@ onMounted(() => {
v-model:loading="alarmLoading" v-model:loading="alarmLoading"
@expose-query-fn="onExposeAlarmHistoryQueryFn" @expose-query-fn="onExposeAlarmHistoryQueryFn"
/> />
<DeviceUsageHistoryCard
v-if="selected.includes('usage')"
:ndm-device="ndmDevice"
:station="station"
:cpu-usage-field="'stCommonInfo.CPU使用率'"
:mem-usage-field="'stCommonInfo.内存使用率'"
v-model:range="range"
v-model:loading="usageLoading"
@expose-query-fn="onExposeUsageHistoryQueryFn"
/>
</NFlex> </NFlex>
</template> </template>
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NdmDecoderDiagInfo, NdmDecoderResultVO, Station } from '@/apis'; import type { NdmDecoderDiagInfo, NdmDecoderResultVO, Station } from '@/apis';
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, type DeviceCommonCardProps } from '@/components'; import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard } from '@/components';
import destr from 'destr'; import destr from 'destr';
import { NFlex } from 'naive-ui'; import { NFlex } from 'naive-ui';
import { computed, toRefs } from 'vue'; import { computed, toRefs } from 'vue';
@@ -19,23 +19,17 @@ const lastDiagInfo = computed(() => {
return result as NdmDecoderDiagInfo; return result as NdmDecoderDiagInfo;
}); });
const commonInfo = computed<DeviceCommonCardProps['commonInfo']>(() => { const commonInfo = computed(() => {
const { stCommonInfo } = lastDiagInfo.value ?? {}; const { stCommonInfo } = lastDiagInfo.value ?? {};
const { 设备ID, 软件版本, 设备厂商, 设备别名, 设备型号, 硬件版本 } = stCommonInfo ?? {}; const { 设备ID, 软件版本, 设备厂商, 设备别名, 设备型号, 硬件版本 } = stCommonInfo ?? {};
return {
return [ 设备ID: 设备ID ?? '-',
{ 软件版本: 软件版本 ?? '-',
title: '设备型号信息', 设备厂商: 设备厂商 ?? '-',
items: [ 设备别名: 设备别名 ?? '-',
{ label: '设备ID', value: 设备ID || '-' }, 设备型号: 设备型号 ?? '-',
{ label: '软件版本', value: 件版本 || '-' }, 硬件版本: 件版本 ?? '-',
{ label: '设备厂商', value: 设备厂商 || '-' }, };
{ label: '设备别名', value: 设备别名 || '-' },
{ label: '设备型号', value: 设备型号 || '-' },
{ label: '硬件版本', value: 硬件版本 || '-' },
],
},
];
}); });
const cpuUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.CPU使用率); const cpuUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.CPU使用率);
@@ -36,8 +36,7 @@ const selected = ref([...historyDiagOptions.map((option) => `${option.value}`)])
const loading = ref<boolean>(false); const loading = ref<boolean>(false);
const icmpLoading = ref<boolean>(false); const icmpLoading = ref<boolean>(false);
const alarmLoading = ref<boolean>(false); const alarmLoading = ref<boolean>(false);
const usageLoading = ref<boolean>(false); watch([icmpLoading, alarmLoading], (loadings) => {
watch([icmpLoading, alarmLoading, usageLoading], (loadings) => {
loading.value = loadings.some((loading) => loading); loading.value = loadings.some((loading) => loading);
}); });
const icmpHistoryQueryFn = ref<() => void>(); const icmpHistoryQueryFn = ref<() => void>();
@@ -92,7 +91,7 @@ onMounted(() => {
:cpu-usage-field="'stCommonInfo.CPU使用率'" :cpu-usage-field="'stCommonInfo.CPU使用率'"
:mem-usage-field="'stCommonInfo.内存使用率'" :mem-usage-field="'stCommonInfo.内存使用率'"
v-model:range="range" v-model:range="range"
v-model:loading="usageLoading" v-model:loading="alarmLoading"
@expose-query-fn="onExposeUsageHistoryQueryFn" @expose-query-fn="onExposeUsageHistoryQueryFn"
/> />
</NFlex> </NFlex>
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NdmNvrDiagInfo, NdmNvrResultVO, Station } from '@/apis'; import type { NdmNvrDiagInfo, NdmNvrResultVO, Station } from '@/apis';
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, NvrDiskCard, NvrEnvCard, NvrRecordCheckCard, type DeviceCommonCardProps } from '@/components'; import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, NvrDiskCard, NvrRecordCard } from '@/components';
import { isNvrCluster } from '@/helpers'; import { isNvrCluster } from '@/helpers';
import destr from 'destr'; import destr from 'destr';
import { NFlex } from 'naive-ui'; import { NFlex } from 'naive-ui';
@@ -20,40 +20,18 @@ const lastDiagInfo = computed(() => {
return result as NdmNvrDiagInfo; return result as NdmNvrDiagInfo;
}); });
const commonInfo = computed<DeviceCommonCardProps['commonInfo']>(() => { const commonInfo = computed(() => {
const { ipAddress } = ndmDevice.value; const { stCommonInfo } = lastDiagInfo.value ?? {};
const { ethInfo, ipInfo, stCommonInfo } = lastDiagInfo.value ?? {}; if (!stCommonInfo) return undefined;
const { 设备ID, 软件版本, 生产厂商, 设备别名, 设备型号, 硬件版本 } = stCommonInfo ?? {}; const { 设备ID, 软件版本, 生产厂商, 设备别名, 设备型号, 硬件版本 } = stCommonInfo;
return {
let operStatus = '-'; 设备ID: 设备ID ?? '-',
if (!!ethInfo?.operStatus) { 软件版本: 软件版本 ?? '-',
operStatus = ethInfo?.operStatus === '1' ? '正常' : '异常'; 生产厂商: 生产厂商 ?? '-',
} 设备别名: 设备别名 ?? '-',
设备型号: 设备型号 ?? '-',
return [ 硬件版本: 硬件版本 ?? '-',
{ };
title: '设备型号信息',
items: [
{ label: '设备ID', value: 设备ID || '-' },
{ label: '软件版本', value: 软件版本 || '-' },
{ label: '生产厂商', value: 生产厂商 || '-' },
{ label: '设备别名', value: 设备别名 || '-' },
{ label: '设备型号', value: 设备型号 || '-' },
{ label: '硬件版本', value: 硬件版本 || '-' },
],
},
{
title: '设备网卡信息',
items: [
{ label: 'IP地址', value: ipAddress || '-' },
{ label: '子网掩码', value: ipInfo?.mASK || '-' },
{ label: 'MAC地址', value: ethInfo?.macAddress || '-' },
{ label: '连接速率', value: ethInfo?.speed || '-' },
{ label: 'MTU', value: ethInfo?.mTU || '-' },
{ label: '运行状态', value: operStatus },
],
},
];
}); });
const cpuUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.CPU使用率); const cpuUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.CPU使用率);
@@ -61,9 +39,6 @@ const memUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.内存使用
const diskHealth = computed(() => lastDiagInfo.value?.info?.diskHealth); const diskHealth = computed(() => lastDiagInfo.value?.info?.diskHealth);
const diskArray = computed(() => lastDiagInfo.value?.info?.groupInfoList); const diskArray = computed(() => lastDiagInfo.value?.info?.groupInfoList);
const fanInfo = computed(() => lastDiagInfo.value?.cdFanInfo);
const powerSupplyInfo = computed(() => lastDiagInfo.value?.cdPowerSupplyInfo);
</script> </script>
<template> <template>
@@ -71,11 +46,8 @@ const powerSupplyInfo = computed(() => lastDiagInfo.value?.cdPowerSupplyInfo);
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" /> <DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
<DeviceCommonCard :common-info="commonInfo" /> <DeviceCommonCard :common-info="commonInfo" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" /> <DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
<NvrEnvCard :fan-info="fanInfo" :power-supply-info="powerSupplyInfo" />
<NvrDiskCard :disk-health="diskHealth" :disk-array="diskArray" /> <NvrDiskCard :disk-health="diskHealth" :disk-array="diskArray" />
<template v-if="isNvrCluster(ndmDevice)"> <NvrRecordCard v-if="isNvrCluster(ndmDevice)" :ndm-device="ndmDevice" :station="station" />
<NvrRecordCheckCard :ndm-device="ndmDevice" :station="station" />
</template>
</NFlex> </NFlex>
</template> </template>
@@ -42,9 +42,8 @@ const selected = ref([...historyDiagOptions.value.map((option) => `${option.valu
const loading = ref<boolean>(false); const loading = ref<boolean>(false);
const icmpLoading = ref<boolean>(false); const icmpLoading = ref<boolean>(false);
const alarmLoading = ref<boolean>(false); const alarmLoading = ref<boolean>(false);
const usageLoading = ref<boolean>(false);
const diskLoading = ref<boolean>(false); const diskLoading = ref<boolean>(false);
watch([icmpLoading, alarmLoading, usageLoading, diskLoading], (loadings) => { watch([icmpLoading, alarmLoading, diskLoading], (loadings) => {
loading.value = loadings.some((loading) => loading); loading.value = loadings.some((loading) => loading);
}); });
const icmpHistoryQueryFn = ref<() => void>(); const icmpHistoryQueryFn = ref<() => void>();
@@ -104,7 +103,7 @@ onMounted(() => {
:cpu-usage-field="'stCommonInfo.CPU使用率'" :cpu-usage-field="'stCommonInfo.CPU使用率'"
:mem-usage-field="'stCommonInfo.内存使用率'" :mem-usage-field="'stCommonInfo.内存使用率'"
v-model:range="range" v-model:range="range"
v-model:loading="usageLoading" v-model:loading="alarmLoading"
@expose-query-fn="onExposeUsageHistoryQueryFn" @expose-query-fn="onExposeUsageHistoryQueryFn"
/> />
<NvrDiskHistoryCard v-if="selected.includes('disk')" :ndm-device="ndmDevice" :station="station" v-model:range="range" v-model:loading="diskLoading" @expose-query-fn="onExposeDiskHistoryQueryFn" /> <NvrDiskHistoryCard v-if="selected.includes('disk')" :ndm-device="ndmDevice" :station="station" v-model:range="range" v-model:loading="diskLoading" @expose-query-fn="onExposeDiskHistoryQueryFn" />
@@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NdmSecurityBoxDiagInfo, NdmSecurityBoxResultVO, Station } from '@/apis'; import type { NdmSecurityBoxDiagInfo, NdmSecurityBoxResultVO, Station } from '@/apis';
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, SecurityBoxCircuitCard, SecurityBoxEnvCard, type DeviceCommonCardProps } from '@/components'; import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, SecurityBoxCircuitCard, SecurityBoxEnvCard } from '@/components';
import { SECURITY_BOX_VENDOR_LITERALS } from '@/helpers';
import destr from 'destr'; import destr from 'destr';
import { NFlex } from 'naive-ui'; import { NFlex } from 'naive-ui';
import { computed, toRefs } from 'vue'; import { computed, toRefs } from 'vue';
@@ -20,54 +19,17 @@ const lastDiagInfo = computed(() => {
return result as NdmSecurityBoxDiagInfo; return result as NdmSecurityBoxDiagInfo;
}); });
// 解析安防箱厂商 const commonInfo = computed(() => {
const vendor = computed(() => { const { stCommonInfo } = lastDiagInfo.value ?? {};
// 先读取诊断信息中的 stCommonInfo 中的设备厂商
if (!!lastDiagInfo.value?.stCommonInfo?.设备厂商) {
return lastDiagInfo.value.stCommonInfo.设备厂商.trim().toLocaleLowerCase();
}
// 如果 stCommonInfo 中没有设备厂商,再读取 ndmDevice 中的 manufacturer
if (!!ndmDevice.value.manufacturer) {
return ndmDevice.value.manufacturer.trim().toLocaleLowerCase();
}
// 如果 ndmDevice 中也没有 manufacturer,返回 beidian 作为兜底
return SECURITY_BOX_VENDOR_LITERALS.beidian;
});
const commonInfo = computed<DeviceCommonCardProps['commonInfo']>(() => {
const { ipAddress } = ndmDevice.value;
const { ethInfo, ipInfo, stCommonInfo } = lastDiagInfo.value ?? {};
const { 设备ID, 软件版本, 设备厂商, 设备别名, 设备型号, 硬件版本 } = stCommonInfo ?? {}; const { 设备ID, 软件版本, 设备厂商, 设备别名, 设备型号, 硬件版本 } = stCommonInfo ?? {};
return {
let operStatus = '-'; 设备ID: 设备ID ?? '',
if (!!ethInfo?.operStatus) { 软件版本: 软件版本 ?? '',
operStatus = ethInfo?.operStatus === '1' ? '正常' : '异常'; 设备厂商: 设备厂商 ?? '',
} 设备别名: 设备别名 ?? '',
设备型号: 设备型号 ?? '',
return [ 硬件版本: 硬件版本 ?? '',
{ };
title: '设备型号信息',
items: [
{ label: '设备ID', value: 设备ID || '-' },
{ label: '软件版本', value: 软件版本 || '-' },
{ label: '设备厂商', value: 设备厂商 || '-' },
{ label: '设备别名', value: 设备别名 || '-' },
{ label: '设备型号', value: 设备型号 || '-' },
{ label: '硬件版本', value: 硬件版本 || '-' },
],
},
{
title: '设备网卡信息',
items: [
{ label: 'IP地址', value: ipAddress || '-' },
{ label: '子网掩码', value: ipInfo?.mASK || '-' },
{ label: 'MAC地址', value: ethInfo?.macAddress || '-' },
{ label: '连接速率', value: ethInfo?.speed || '-' },
{ label: 'MTU', value: ethInfo?.mTU || '-' },
{ label: '运行状态', value: operStatus },
],
},
];
}); });
const cpuUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.CPU使用率); const cpuUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.CPU使用率);
@@ -87,7 +49,7 @@ const circuits = computed(() => lastDiagInfo.value?.info?.at(0)?.circuits);
<DeviceCommonCard :common-info="commonInfo" /> <DeviceCommonCard :common-info="commonInfo" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" /> <DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
<SecurityBoxEnvCard :fan-speeds="fanSpeeds" :temperature="temperature" :humidity="humidity" :switches="switches" /> <SecurityBoxEnvCard :fan-speeds="fanSpeeds" :temperature="temperature" :humidity="humidity" :switches="switches" />
<SecurityBoxCircuitCard :ndm-device="ndmDevice" :station="station" :vendor="vendor" :circuits="circuits" /> <SecurityBoxCircuitCard :ndm-device="ndmDevice" :station="station" :circuits="circuits" />
</NFlex> </NFlex>
</template> </template>
@@ -6,7 +6,7 @@ import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios'; import { isCancel } from 'axios';
import destr from 'destr'; import destr from 'destr';
import { isString } from 'es-toolkit'; import { isString } from 'es-toolkit';
import { NButton, NCard, NFlex, NForm, NFormItem, NFormItemGi, NGrid, NInput, NInputNumber, NSwitch, type FormInst, type FormRules } from 'naive-ui'; import { NButton, NCard, NFlex, NForm, NFormItem, NFormItemGi, NGrid, NInput, NSwitch, type FormInst, type FormRules } from 'naive-ui';
import { computed, onBeforeUnmount, ref, toRefs, useTemplateRef, watch } from 'vue'; import { computed, onBeforeUnmount, ref, toRefs, useTemplateRef, watch } from 'vue';
const props = defineProps<{ const props = defineProps<{
@@ -126,15 +126,9 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="型号"> <NFormItem label-placement="left" label="型号">
<NInput v-model:value="localDevice.model" /> <NInput v-model:value="localDevice.model" />
</NFormItem> </NFormItem>
<NFormItem label-placement="left" label="开关数量">
<NInputNumber clearable v-model:value="localDevice.circuitCount" />
</NFormItem>
<NFormItem label-placement="left" label="团体字符串"> <NFormItem label-placement="left" label="团体字符串">
<NInput v-model:value="localDevice.community" /> <NInput v-model:value="localDevice.community" />
</NFormItem> </NFormItem>
<NFormItem label-placement="left" label="团体字符串(写)">
<NInput v-model:value="localDevice.writeCommunity" />
</NFormItem>
<NFormItem label-placement="left" label="设备描述"> <NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" /> <NInput v-model:value="localDevice.description" />
</NFormItem> </NFormItem>
@@ -1,9 +1,8 @@
import ServerAlive from './server-alive.vue'; import ServerAlive from './server-alive.vue';
import ServerCard from './server-card.vue'; import ServerCard from './server-card.vue';
import ServerCurrentDiag from './server-current-diag.vue'; import ServerCurrentDiag from './server-current-diag.vue';
import ServerHighAvailable from './server-high-available.vue';
import ServerHistoryDiag from './server-history-diag.vue'; import ServerHistoryDiag from './server-history-diag.vue';
import ServerStreamPush from './server-stream-push.vue'; import ServerStreamPush from './server-stream-push.vue';
import ServerUpdate from './server-update.vue'; import ServerUpdate from './server-update.vue';
export { ServerAlive, ServerCard, ServerCurrentDiag, ServerHighAvailable, ServerHistoryDiag, ServerUpdate, ServerStreamPush }; export { ServerAlive, ServerCard, ServerCurrentDiag, ServerHistoryDiag, ServerUpdate, ServerStreamPush };
@@ -23,11 +23,8 @@ const deviceType = computed(() => tryGetDeviceType(ndmDevice.value.deviceType));
const MEDIA_SERVER_ALIVE_QUERY_KEY = 'media-server-alive-query'; const MEDIA_SERVER_ALIVE_QUERY_KEY = 'media-server-alive-query';
const VIDEO_SERVER_ALIVE_QUERY_KEY = 'video-server-alive-query'; const VIDEO_SERVER_ALIVE_QUERY_KEY = 'video-server-alive-query';
const deviceUniqueKey = computed(() => [station.value.code, ndmDevice.value.id]);
const { data: isMediaServerAlive } = useQuery({ const { data: isMediaServerAlive } = useQuery({
queryKey: computed(() => [MEDIA_SERVER_ALIVE_QUERY_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]), queryKey: computed(() => [MEDIA_SERVER_ALIVE_QUERY_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmMediaServer), enabled: computed(() => activeRequests.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmMediaServer),
refetchInterval: 30 * 1000, refetchInterval: 30 * 1000,
gcTime: 0, gcTime: 0,
@@ -37,7 +34,7 @@ const { data: isMediaServerAlive } = useQuery({
}, },
}); });
const { data: isSipServerAlive } = useQuery({ const { data: isSipServerAlive } = useQuery({
queryKey: computed(() => [VIDEO_SERVER_ALIVE_QUERY_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]), queryKey: computed(() => [VIDEO_SERVER_ALIVE_QUERY_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmVideoServer), enabled: computed(() => activeRequests.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmVideoServer),
refetchInterval: 30 * 1000, refetchInterval: 30 * 1000,
gcTime: 0, gcTime: 0,
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { type NdmServerDiagInfo, type NdmServerResultVO, type Station } from '@/apis'; import { type NdmServerDiagInfo, type NdmServerResultVO, type Station } from '@/apis';
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, ServerAlive, ServerHighAvailable, ServerStreamPush, type DeviceCommonCardProps } from '@/components'; import { DeviceHardwareCard, DeviceHeaderCard, ServerAlive, ServerStreamPush } from '@/components';
import destr from 'destr'; import destr from 'destr';
import { NFlex } from 'naive-ui'; import { NFlex } from 'naive-ui';
import { computed, toRefs } from 'vue'; import { computed, toRefs } from 'vue';
@@ -19,30 +19,6 @@ const lastDiagInfo = computed(() => {
return result as NdmServerDiagInfo; return result as NdmServerDiagInfo;
}); });
const commonInfo = computed<DeviceCommonCardProps['commonInfo']>(() => {
const { ipAddress } = ndmDevice.value;
const { ethInfo, ipInfo } = lastDiagInfo.value ?? {};
let operStatus = '-';
if (!!ethInfo?.operStatus) {
operStatus = ethInfo?.operStatus === '1' ? '正常' : '异常';
}
return [
{
title: '设备网卡信息',
items: [
{ label: 'IP地址', value: ipAddress || '-' },
{ label: '子网掩码', value: ipInfo?.mASK || '-' },
{ label: 'MAC地址', value: ethInfo?.macAddress || '-' },
{ label: '连接速率', value: ethInfo?.speed || '-' },
{ label: 'MTU', value: ethInfo?.mTU || '-' },
{ label: '运行状态', value: operStatus },
],
},
];
});
const cpuUsage = computed(() => lastDiagInfo.value?.commInfo?.CPU使用率); const cpuUsage = computed(() => lastDiagInfo.value?.commInfo?.CPU使用率);
const memUsage = computed(() => lastDiagInfo.value?.commInfo?.内存使用率); const memUsage = computed(() => lastDiagInfo.value?.commInfo?.内存使用率);
const diskUsage = computed(() => lastDiagInfo.value?.commInfo?.磁盘使用率); const diskUsage = computed(() => lastDiagInfo.value?.commInfo?.磁盘使用率);
@@ -51,9 +27,7 @@ const runningTime = computed(() => lastDiagInfo.value?.commInfo?.系统运行时
<template> <template>
<NFlex vertical> <NFlex vertical>
<ServerHighAvailable :ndm-device="ndmDevice" :station="station" />
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" /> <DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
<DeviceCommonCard :common-info="commonInfo" />
<DeviceHardwareCard running-time-label="服务器运行时间" :cpu-usage="cpuUsage" :mem-usage="memUsage" :disk-usage="diskUsage" :running-time="runningTime" /> <DeviceHardwareCard running-time-label="服务器运行时间" :cpu-usage="cpuUsage" :mem-usage="memUsage" :disk-usage="diskUsage" :running-time="runningTime" />
<ServerAlive :ndm-device="ndmDevice" :station="station" /> <ServerAlive :ndm-device="ndmDevice" :station="station" />
<ServerStreamPush :ndm-device="ndmDevice" :station="station" /> <ServerStreamPush :ndm-device="ndmDevice" :station="station" />
@@ -1,67 +0,0 @@
<script setup lang="ts">
import { getHighAvailableApi, type NdmServerResultVO, type Station } from '@/apis';
import { DEVICE_TYPE_LITERALS, tryGetDeviceType } from '@/enums';
import { useSettingStore } from '@/stores';
import { useQuery, useQueryClient } from '@tanstack/vue-query';
import { NAlert, NFlex } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmServerResultVO;
station: Station;
}>();
const settingStore = useSettingStore();
const { activeRequests } = storeToRefs(settingStore);
const queryClient = useQueryClient();
const { ndmDevice, station } = toRefs(props);
const deviceType = computed(() => tryGetDeviceType(ndmDevice.value.deviceType));
const isVideoServer = computed(() => deviceType.value === DEVICE_TYPE_LITERALS.ndmVideoServer);
const SERVER_HIGH_AVAILABLE_QUERY_KEY = 'server-high-available-query';
const deviceUniqueKey = computed(() => [station.value.code, ndmDevice.value.id]);
const { data: highAvailable } = useQuery({
queryKey: computed(() => [SERVER_HIGH_AVAILABLE_QUERY_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && isVideoServer.value),
refetchInterval: 30 * 1000,
gcTime: 0,
queryFn: async ({ signal }) => {
const highAvailable = await getHighAvailableApi({ stationCode: station.value.code, signal });
return highAvailable;
},
});
watch(activeRequests, (active) => {
if (!active) {
queryClient.cancelQueries({ queryKey: [SERVER_HIGH_AVAILABLE_QUERY_KEY] });
}
});
const showCard = computed(() => {
const { pyip: physicalIp } = highAvailable.value ?? {};
const ipAddressMatched = physicalIp === ndmDevice.value.ipAddress;
return isVideoServer.value && ipAddressMatched;
});
</script>
<template>
<NAlert v-if="showCard && !!highAvailable" :bordered="false" type="success">
<template #header>
<NFlex :align="'center'">
<div>正在提供服务</div>
<NFlex :align="'center'" style="font-size: smaller">
<div>虚拟IP: {{ highAvailable.vip }}</div>
<div>启用时间: {{ highAvailable.changeDate }}</div>
</NFlex>
</NFlex>
</template>
</NAlert>
</template>
<style scoped></style>
@@ -25,10 +25,8 @@ const showCard = computed(() => deviceType.value === DEVICE_TYPE_LITERALS.ndmMed
const SERVER_STREAM_PUSH_KEY = 'server-stream-push-query'; const SERVER_STREAM_PUSH_KEY = 'server-stream-push-query';
const deviceUniqueKey = computed(() => [station.value.code, ndmDevice.value.id]);
const { data: streamPushes } = useQuery({ const { data: streamPushes } = useQuery({
queryKey: computed(() => [SERVER_STREAM_PUSH_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]), queryKey: computed(() => [SERVER_STREAM_PUSH_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && showCard.value), enabled: computed(() => activeRequests.value && showCard.value),
refetchInterval: 30 * 1000, refetchInterval: 30 * 1000,
gcTime: 0, gcTime: 0,
+74 -134
View File
@@ -1,14 +1,8 @@
<script lang="ts">
const createDeviceNodeKey = (stationCode?: Station['code'], device?: NdmDeviceResultVO) => {
return `${stationCode ?? ''}-${device?.id ?? ''}`;
};
</script>
<script setup lang="ts"> <script setup lang="ts">
import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis'; import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis';
import { useDeviceTree, usePermission, type UseDeviceTreeReturn } from '@/composables'; import { useDeviceTree, usePermission, type UseDeviceTreeReturn } from '@/composables';
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType, PERMISSION_TYPE_LITERALS } from '@/enums'; import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType, PERMISSION_TYPE_LITERALS } from '@/enums';
import { createNvrClusterRelationship, isNvrCluster, nvrInCluster } from '@/helpers'; import { isNvrCluster } from '@/helpers';
import { useDeviceStore, usePermissionStore } from '@/stores'; import { useDeviceStore, usePermissionStore } from '@/stores';
import { watchDebounced, watchImmediate } from '@vueuse/core'; import { watchDebounced, watchImmediate } from '@vueuse/core';
import destr from 'destr'; import destr from 'destr';
@@ -17,19 +11,15 @@ import {
NButton, NButton,
NDropdown, NDropdown,
NFlex, NFlex,
NGrid,
NGridItem,
NInput, NInput,
NRadio, NRadio,
NRadioGroup, NRadioGroup,
NSelect,
NTab, NTab,
NTabs, NTabs,
NTag, NTag,
NTree, NTree,
useThemeVars, useThemeVars,
type DropdownOption, type DropdownOption,
type SelectOption,
type TagProps, type TagProps,
type TreeInst, type TreeInst,
type TreeOption, type TreeOption,
@@ -116,7 +106,7 @@ watchImmediate(selectedDeviceType, (newDeviceType) => {
} }
}); });
const selectedKeys = computed(() => (selectedDevice.value?.id ? [createDeviceNodeKey(selectedStationCode.value, selectedDevice.value)] : undefined)); const selectedKeys = computed(() => (selectedDevice.value?.id ? [selectedDevice.value.id] : undefined));
watch([selectedKeys, selectedDevice, selectedStationCode], ([, device, code]) => { watch([selectedKeys, selectedDevice, selectedStationCode], ([, device, code]) => {
if (device && code) { if (device && code) {
onSelectDevice(device, code); onSelectDevice(device, code);
@@ -297,18 +287,8 @@ const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: Station[
return h(NFlex, { size: 'small' }, { default: () => [renderViewDeviceButton(device, stationCode), renderDeviceStatusTag(device)] }); return h(NFlex, { size: 'small' }, { default: () => [renderViewDeviceButton(device, stationCode), renderDeviceStatusTag(device)] });
}; };
// 全线设备树 // 全线设备树
const lineDeviceTreeData = computed<Record<DeviceType, TreeOption[]>>(() => { const lineDeviceTreeData = computed<Record<Station['code'], TreeOption[]>>(() => {
const treeData: Record<DeviceType, TreeOption[]> = { const treeData: Record<string, TreeOption[]> = {};
[DEVICE_TYPE_LITERALS.ndmCamera]: [],
[DEVICE_TYPE_LITERALS.ndmNvr]: [],
[DEVICE_TYPE_LITERALS.ndmSwitch]: [],
[DEVICE_TYPE_LITERALS.ndmDecoder]: [],
[DEVICE_TYPE_LITERALS.ndmSecurityBox]: [],
[DEVICE_TYPE_LITERALS.ndmMediaServer]: [],
[DEVICE_TYPE_LITERALS.ndmVideoServer]: [],
[DEVICE_TYPE_LITERALS.ndmKeyboard]: [],
[DEVICE_TYPE_LITERALS.ndmAlarmHost]: [],
};
deviceTabPanes.forEach(({ name: paneName /* , tab: paneTab */ }) => { deviceTabPanes.forEach(({ name: paneName /* , tab: paneTab */ }) => {
treeData[paneName] = stations.value.map<TreeOption>((station) => { treeData[paneName] = stations.value.map<TreeOption>((station) => {
const { name: stationName, code: stationCode } = station; const { name: stationName, code: stationCode } = station;
@@ -317,55 +297,47 @@ const lineDeviceTreeData = computed<Record<DeviceType, TreeOption[]>>(() => {
const offlineDevices = devices?.filter((device) => device.deviceStatus === '20'); const offlineDevices = devices?.filter((device) => device.deviceStatus === '20');
// 对于录像机,需要根据clusterList字段以分号分隔设备IP,进一步形成子树结构 // 对于录像机,需要根据clusterList字段以分号分隔设备IP,进一步形成子树结构
if (paneName === DEVICE_TYPE_LITERALS.ndmNvr) { if (paneName === DEVICE_TYPE_LITERALS.ndmNvr) {
const nvrDevices = devices as NdmNvrResultVO[]; const nvrs = devices as NdmNvrResultVO[];
const nvrClusters: NdmNvrResultVO[] = [];
const { nvrClusters, nvrTreeMap, nvrStandalones } = createNvrClusterRelationship(nvrDevices); const nvrSingletons: NdmNvrResultVO[] = [];
for (const device of nvrs) {
if (isNvrCluster(device)) {
nvrClusters.push(device);
} else {
nvrSingletons.push(device);
}
}
return { return {
label: stationName, label: stationName,
key: stationCode, key: stationCode,
prefix: () => renderStationNodePrefix(station), prefix: () => renderStationNodePrefix(station),
suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0), suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0),
children: [ children: nvrClusters.map<TreeOption>((nvrCluster) => {
...nvrClusters.map((cluster) => { return {
return { label: `${nvrCluster.name}`,
label: `${cluster.name}`, key: nvrCluster.id ?? `${nvrCluster.name}`,
key: createDeviceNodeKey(stationCode, cluster), prefix: () => renderDeviceNodePrefix(nvrCluster, stationCode),
prefix: () => renderDeviceNodePrefix(cluster, stationCode), suffix: () => `${nvrCluster.ipAddress}`,
suffix: () => `${cluster.ipAddress}`, children: nvrSingletons.map<TreeOption>((nvr) => {
children: (nvrTreeMap.get(cluster.ipAddress ?? '') ?? []).map((clusterNode) => { return {
return { label: `${nvr.name}`,
label: `${clusterNode.name}`, key: nvr.id ?? `${nvr.name}`,
key: createDeviceNodeKey(stationCode, clusterNode), prefix: () => renderDeviceNodePrefix(nvr, stationCode),
prefix: () => renderDeviceNodePrefix(clusterNode, stationCode), suffix: () => `${nvr.ipAddress}`,
suffix: () => `${clusterNode.ipAddress}`, // 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站 stationCode,
stationCode, device: nvr,
device: clusterNode, };
}; }),
}), // 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站 stationCode,
stationCode, device: nvrCluster,
device: cluster, };
}; }),
}),
...nvrStandalones.map((device) => {
return {
label: `${device.name}`,
key: createDeviceNodeKey(stationCode, device),
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
stationCode,
device: device,
};
}),
],
stationCode, stationCode,
deviceType: activeTab.value, deviceType: activeTab.value,
}; };
} }
// 非录像机设备
return { return {
label: stationName, label: stationName,
key: stationCode, key: stationCode,
@@ -376,7 +348,7 @@ const lineDeviceTreeData = computed<Record<DeviceType, TreeOption[]>>(() => {
const device = dev as NdmDeviceResultVO; const device = dev as NdmDeviceResultVO;
return { return {
label: `${device.name}`, label: `${device.name}`,
key: createDeviceNodeKey(stationCode, device), key: `${device.id}`,
prefix: () => renderDeviceNodePrefix(device, stationCode), prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`, suffix: () => `${device.ipAddress}`,
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站 // 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
@@ -400,51 +372,37 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
const onlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '10').length; const onlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '10').length;
const offlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '20').length; const offlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '20').length;
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) { if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
const nvrDevices = stationDevices[deviceType] as NdmNvrResultVO[]; const nvrs = stationDevices[deviceType] as NdmNvrResultVO[];
const clusters = nvrs.filter((nvr) => isNvrCluster(nvr));
const { nvrClusters, nvrTreeMap, nvrStandalones } = createNvrClusterRelationship(nvrDevices); const singletons = nvrs.filter((nvr) => !isNvrCluster(nvr));
return { return {
label: `${DEVICE_TYPE_NAMES[deviceType]}`, label: `${DEVICE_TYPE_NAMES[deviceType]}`,
key: deviceType, key: deviceType,
suffix: () => renderIcmpStatistics(onlineCount, offlineCount, nvrDevices.length), suffix: () => renderIcmpStatistics(onlineCount, offlineCount, nvrs.length),
children: [ children: clusters.map<TreeOption>((device) => {
...nvrClusters.map((cluster) => { return {
return { label: `${device.name}`,
label: `${cluster.name}`, key: `${device.id}`,
key: createDeviceNodeKey(stationCode, cluster), prefix: () => renderDeviceNodePrefix(device, stationCode),
prefix: () => renderDeviceNodePrefix(cluster, stationCode), suffix: () => `${device.ipAddress}`,
suffix: () => `${cluster.ipAddress}`, children: singletons.map<TreeOption>((device) => {
children: (nvrTreeMap.get(cluster.ipAddress ?? '') ?? []).map((clusterNode) => { return {
return { label: `${device.name}`,
label: `${clusterNode.name}`, key: `${device.id}`,
key: createDeviceNodeKey(stationCode, clusterNode), prefix: () => renderDeviceNodePrefix(device, stationCode),
prefix: () => renderDeviceNodePrefix(clusterNode, stationCode), suffix: () => `${device.ipAddress}`,
suffix: () => `${clusterNode.ipAddress}`, stationCode,
stationCode, device,
device: clusterNode, };
}; }),
}), stationCode,
stationCode, device,
device: cluster, };
}; }),
}),
...nvrStandalones.map((device) => {
return {
label: `${device.name}`,
key: createDeviceNodeKey(stationCode, device),
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
stationCode,
device,
};
}),
],
stationCode, stationCode,
deviceType, deviceType,
}; };
} }
// 非录像机设备
return { return {
label: `${DEVICE_TYPE_NAMES[deviceType]}`, label: `${DEVICE_TYPE_NAMES[deviceType]}`,
key: deviceType, key: deviceType,
@@ -452,7 +410,7 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
children: stationDevices[deviceType].map<TreeOption>((device) => { children: stationDevices[deviceType].map<TreeOption>((device) => {
return { return {
label: `${device.name}`, label: `${device.name}`,
key: createDeviceNodeKey(stationCode, device), key: `${device.id}`,
prefix: () => renderDeviceNodePrefix(device, stationCode), prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`, suffix: () => `${device.ipAddress}`,
stationCode, stationCode,
@@ -467,26 +425,20 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
// ========== 设备树搜索 ========== // ========== 设备树搜索 ==========
const searchInput = ref(''); const searchInput = ref('');
const searchTypeOptions: SelectOption[] = [
{ label: '设备名称', value: 'name' },
{ label: 'IP地址', value: 'ipAddress' },
];
type SearchType = 'name' | 'ipAddress';
const typeInput = ref<SearchType>('name');
const statusInput = ref(''); const statusInput = ref('');
// 设备树将搜索框、选择器以及单选框的值都交给NTree的pattern属性 // 设备树将搜索框单选框的值都交给NTree的pattern属性
// 但是如果一个车站下没有匹配的设备,那么这个车站节点也不会显示 // 但是如果一个车站下没有匹配的设备,那么这个车站节点也不会显示
const searchPattern = computed(() => { const searchPattern = computed(() => {
const search = searchInput.value; const search = searchInput.value;
const status = statusInput.value; const status = statusInput.value;
if (!search && !status) return ''; // 如果pattern非空会导致NTree组件认为筛选完成,UI上发生全量匹配 if (!search && !status) return ''; // 如果pattern非空会导致NTree组件认为筛选完成,UI上发生全量匹配
return JSON.stringify({ search: searchInput.value, type: typeInput.value, status: statusInput.value }); return JSON.stringify({ search: searchInput.value, status: statusInput.value });
}); });
const searchFilter = (pattern: string, node: TreeOption): boolean => { const searchFilter = (pattern: string, node: TreeOption): boolean => {
const { search, type, status } = destr<{ search: string; type: SearchType; status: string }>(pattern); const { search, status } = destr<{ search: string; status: string }>(pattern);
const device = node['device'] as NdmDeviceResultVO | undefined; const device = node['device'] as NdmDeviceResultVO | undefined;
const { deviceStatus } = device ?? {}; const { name, ipAddress, deviceId, deviceStatus } = device ?? {};
const searchMatched = !!device?.[type]?.includes(search); const searchMatched = (name ?? '').includes(search) || (ipAddress ?? '').includes(search) || (deviceId ?? '').includes(search);
const statusMatched = status === '' || status === deviceStatus; const statusMatched = status === '' || status === deviceStatus;
return searchMatched && statusMatched; return searchMatched && statusMatched;
}; };
@@ -500,7 +452,6 @@ const onFoldDeviceTree = () => {
}; };
const onLocateDeviceTree = async () => { const onLocateDeviceTree = async () => {
if (!selectedStationCode.value) return; if (!selectedStationCode.value) return;
const stationCode = selectedStationCode.value;
if (!selectedDevice.value) return; if (!selectedDevice.value) return;
const deviceType = tryGetDeviceType(selectedDevice.value.deviceType); const deviceType = tryGetDeviceType(selectedDevice.value.deviceType);
if (!deviceType) return; if (!deviceType) return;
@@ -512,28 +463,24 @@ const onLocateDeviceTree = async () => {
activeTab.value = deviceType; activeTab.value = deviceType;
// 展开选择的车站 // 展开选择的车站
expandedKeys.value.push(stationCode); expandedKeys.value.push(selectedStationCode.value);
// 当选择录像机时,如果不是集群,进一步展开该录像机所在的集群节点 // 当选择录像机时,如果不是集群,进一步展开该录像机所在的集群节点
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) { if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
const stationDevices = lineDevices.value[stationCode]; const stationDevices = lineDevices.value[selectedStationCode.value];
if (stationDevices) { if (stationDevices) {
const selectedNvr = selectedDevice.value as NdmNvrResultVO; const selectedNvr = selectedDevice.value as NdmNvrResultVO;
if (!isNvrCluster(selectedNvr)) { if (!isNvrCluster(selectedNvr)) {
const nvrDevices = stationDevices[DEVICE_TYPE_LITERALS.ndmNvr]; const nvrs = stationDevices[DEVICE_TYPE_LITERALS.ndmNvr];
const clusters = nvrDevices.filter((device) => { const clusters = nvrs.filter((nvr) => isNvrCluster(nvr) && nvr.clusterList?.includes(selectedNvr.clusterList ?? ''));
if (!isNvrCluster(device)) return false; expandedKeys.value.push(...clusters.map((nvr) => `${nvr.id}`));
const cluster = device;
return nvrInCluster(selectedNvr, cluster);
});
expandedKeys.value.push(...clusters.map((cluster) => createDeviceNodeKey(stationCode, cluster)));
} }
} }
} }
// 等待设备树展开完成,滚动到选择的设备 // 等待设备树展开完成,滚动到选择的设备
await nextTick(); await nextTick();
deviceTreeInst.value.scrollTo({ key: createDeviceNodeKey(stationCode, selectedDevice.value), behavior: 'smooth' }); deviceTreeInst.value.scrollTo({ key: `${selectedDevice.value.id}`, behavior: 'smooth' });
animated.value = true; animated.value = true;
}; };
@@ -576,14 +523,7 @@ onMounted(() => {
<div style="height: 100%; display: flex; flex-direction: column"> <div style="height: 100%; display: flex; flex-direction: column">
<!-- 搜索和筛选 --> <!-- 搜索和筛选 -->
<div style="padding: 12px; flex: 0 0 auto"> <div style="padding: 12px; flex: 0 0 auto">
<NGrid :cols="10" :x-gap="8"> <NInput v-model:value="searchInput" placeholder="搜索设备名称、设备ID或IP地址" clearable />
<NGridItem :span="7">
<NInput v-model:value="searchInput" placeholder="搜索设备名称或IP地址" clearable />
</NGridItem>
<NGridItem :span="3">
<NSelect v-model:value="typeInput" :options="searchTypeOptions" placeholder="搜索类型" />
</NGridItem>
</NGrid>
<NFlex align="center"> <NFlex align="center">
<NRadioGroup v-model:value="statusInput"> <NRadioGroup v-model:value="statusInput">
<NRadio value="">全部</NRadio> <NRadio value="">全部</NRadio>
@@ -13,12 +13,9 @@ import destr from 'destr';
import { isFunction } from 'es-toolkit'; import { isFunction } from 'es-toolkit';
import localforage from 'localforage'; import localforage from 'localforage';
import { DownloadIcon, Trash2Icon, UploadIcon } from 'lucide-vue-next'; import { DownloadIcon, Trash2Icon, UploadIcon } from 'lucide-vue-next';
import { NButton, NButtonGroup, NDivider, NDrawer, NDrawerContent, NDropdown, NFlex, NFormItem, NIcon, NInput, NInputNumber, NModal, NSwitch, NText, NTooltip, type DropdownOption } from 'naive-ui'; 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 { storeToRefs } from 'pinia';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const show = defineModel<boolean>('show', { default: false }); const show = defineModel<boolean>('show', { default: false });
@@ -287,11 +284,6 @@ const onDrawerAfterLeave = () => {
abortControllers.value.retentionDays.abort(); abortControllers.value.retentionDays.abort();
abortControllers.value.snapStatus.abort(); abortControllers.value.snapStatus.abort();
}; };
const onClickVersion = () => {
show.value = false;
router.push({ path: '/changelog' });
};
</script> </script>
<template> <template>
@@ -391,16 +383,7 @@ const onClickVersion = () => {
</NFlex> </NFlex>
<template #footer> <template #footer>
<NFlex vertical justify="flex-end" align="center" style="width: 100%; font-size: 12px; gap: 4px"> <NFlex vertical justify="flex-end" align="center" style="width: 100%; font-size: 12px; gap: 4px">
<NTooltip> <NText :depth="3">平台版本: {{ versionInfo.version }} ({{ versionInfo.buildTime }})</NText>
<template #trigger>
<div @click="onClickVersion">
<NText :depth="3" style="cursor: pointer">平台版本: {{ versionInfo.version }} ({{ versionInfo.buildTime }})</NText>
</div>
</template>
<template #default>
<NText :depth="3">点击可查看平台更新记录</NText>
</template>
</NTooltip>
</NFlex> </NFlex>
</template> </template>
</NDrawerContent> </NDrawerContent>
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
// 设备参数配置在系统中的key前缀 // 设备参数配置在系统中的key前缀
const DEVICE_PARAM_PREFIXES = { const DEVICE_PARAM_PREFIXES = {
Camera: 'CAMERA_',
Switch: 'SWITCH_', Switch: 'SWITCH_',
Server: 'SERVER_', Server: 'SERVER_',
Decoder: 'DECODER_', Decoder: 'DECODER_',
@@ -68,10 +67,6 @@ const getItemSuffix = (name: string) => {
}; };
const tabPanes = [ const tabPanes = [
{
tab: '摄像机阈值',
name: DEVICE_PARAM_PREFIXES.Camera,
},
{ {
tab: '交换机阈值', tab: '交换机阈值',
name: DEVICE_PARAM_PREFIXES.Switch, name: DEVICE_PARAM_PREFIXES.Switch,
@@ -114,7 +109,7 @@ const show = defineModel<boolean>('show', { required: true });
const { station } = toRefs(props); const { station } = toRefs(props);
const activeTabName = ref<DeviceParamPrefix>(DEVICE_PARAM_PREFIXES.Camera); const activeTabName = ref<DeviceParamPrefix>(DEVICE_PARAM_PREFIXES.Switch);
const deviceParams = ref<DeviceParamItem[]>([]); const deviceParams = ref<DeviceParamItem[]>([]);
@@ -216,7 +211,7 @@ const onAfterModalEnter = () => {
const onBeforeModalLeave = () => { const onBeforeModalLeave = () => {
saveDeviceParams({ tabName: activeTabName.value, items: deviceParams.value }); saveDeviceParams({ tabName: activeTabName.value, items: deviceParams.value });
activeTabName.value = DEVICE_PARAM_PREFIXES.Camera; activeTabName.value = DEVICE_PARAM_PREFIXES.Switch;
deviceParams.value = []; deviceParams.value = [];
}; };
</script> </script>
@@ -1,11 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { batchExportRecordCheckApi, pageDefParameterApi, type Station } from '@/apis'; import { batchExportRecordCheckApi, getRecordCheckApi, pageDefParameterApi, type NdmNvrResultVO, type Station } from '@/apis';
import { exportRecordDiagCsv, isNvrCluster, transformRecordChecks } from '@/helpers';
import { useDeviceStore } from '@/stores';
import { downloadByData, parseErrorFeedback } from '@/utils'; import { downloadByData, parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query'; import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios'; import { isCancel } from 'axios';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { NButton, NFlex, NGrid, NGridItem, NModal, NScrollbar, NSpin } from 'naive-ui'; import { NButton, NFlex, NGrid, NGridItem, NModal, NScrollbar, NSpin } from 'naive-ui';
import { ref, toRefs } from 'vue'; import { storeToRefs } from 'pinia';
import { computed, ref, toRefs } from 'vue';
const props = defineProps<{ const props = defineProps<{
stations: Station[]; stations: Station[];
@@ -17,66 +20,93 @@ const emit = defineEmits<{
const show = defineModel<boolean>('show'); const show = defineModel<boolean>('show');
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
const { stations } = toRefs(props); const { stations } = toRefs(props);
const nvrClusterRecord = computed(() => {
const clusterMap: Record<Station['code'], { stationName: Station['name']; clusters: NdmNvrResultVO[] }> = {};
stations.value.forEach((station) => {
clusterMap[station.code] = {
stationName: station.name,
clusters: [],
};
const stationDevices = lineDevices.value[station.code];
const nvrs = stationDevices?.['ndmNvr'] ?? [];
nvrs.forEach((nvr) => {
if (isNvrCluster(nvr)) {
clusterMap[station.code]?.clusters?.push(nvr);
}
});
});
return clusterMap;
});
const abortController = ref<AbortController>(new AbortController()); const abortController = ref<AbortController>(new AbortController());
const { mutate: batchExportRecordCheck, isPending: batchExporting } = useMutation({ const { mutate: exportRecordDiags, isPending: exporting } = useMutation({
mutationFn: async (params: { stations: Station[] }) => { mutationFn: async (params: { clusters: NdmNvrResultVO[]; stationCode: Station['code'] }) => {
const timer = setTimeout(() => { const { clusters, stationCode } = params;
if (!batchExporting.value) return; if (clusters.length === 0) {
window.$message.info('导出耗时较长,请耐心等待...', { duration: 0 }); const stationName = nvrClusterRecord.value[stationCode]?.stationName ?? '';
}, 3000); window.$message.info(`${stationName} 没有录像诊断数据`);
return;
try {
abortController.value.abort();
abortController.value = new AbortController();
const { records = [] } = await pageDefParameterApi(
{
model: {
key: 'NVR_GAP_SECONDS',
},
extra: {},
current: 1,
size: 1,
sort: 'id',
order: 'descending',
},
{
signal: abortController.value.signal,
},
);
const gapSeconds = parseInt(records.at(0)?.value ?? '5');
abortController.value.abort();
abortController.value = new AbortController();
const data = await batchExportRecordCheckApi(
{
checkDuration: 90,
gapSeconds,
stationCode: params.stations.map((station) => station.code),
},
{
signal: abortController.value.signal,
},
);
return data;
} finally {
window.$message.destroyAll();
clearTimeout(timer);
} }
const cluster = clusters.at(0);
if (!cluster) return;
abortController.value.abort();
abortController.value = new AbortController();
const checks = await getRecordCheckApi(cluster, 90, [], { stationCode: stationCode, signal: abortController.value.signal });
return checks;
}, },
onSuccess: (data, { stations }) => { onSuccess: (checks, { stationCode }) => {
const time = dayjs().format('YYYY-MM-DD_HH-mm-ss'); if (!checks || checks.length === 0) {
let stationName = ''; window.$message.info(`没有录像诊断数据`);
if (stations.length === 1) { return;
const name = stations.at(0)?.name;
if (!!name) {
stationName = `${name}_`;
}
} }
downloadByData(data, `${stationName}录像缺失记录_${time}.xlsx`); const recordDiags = transformRecordChecks(checks);
exportRecordDiagCsv(recordDiags, nvrClusterRecord.value[stationCode]?.stationName ?? '');
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const { mutate: batchExportRecordCheck, isPending: batchExporting } = useMutation({
mutationFn: async () => {
const { records = [] } = await pageDefParameterApi({
model: {
key: 'NVR_GAP_SECONDS',
},
extra: {},
current: 1,
size: 1,
sort: 'id',
order: 'descending',
});
const gapSeconds = parseInt(records.at(0)?.value ?? '5');
window.$message.info('导出耗时较长,请耐心等待...');
const data = await batchExportRecordCheckApi(
{
checkDuration: 90,
gapSeconds,
stationCode: stations.value.map((station) => station.code),
},
{
signal: abortController.value.signal,
},
);
return data;
},
onSuccess: (data) => {
const time = dayjs().format('YYYY-MM-DD_HH-mm-ss');
downloadByData(data, `录像缺失记录_${time}.xlsx`);
}, },
onError: (error) => { onError: (error) => {
if (isCancel(error)) return; if (isCancel(error)) return;
@@ -96,11 +126,11 @@ const onAfterLeave = () => {
<NModal v-model:show="show" preset="card" title="导出录像诊断" @after-leave="onAfterLeave" style="width: 800px"> <NModal v-model:show="show" preset="card" title="导出录像诊断" @after-leave="onAfterLeave" style="width: 800px">
<template #default> <template #default>
<NScrollbar style="height: 300px"> <NScrollbar style="height: 300px">
<NSpin size="small" :show="batchExporting"> <NSpin size="small" :show="exporting">
<NGrid :cols="6"> <NGrid :cols="6">
<template v-for="station in stations" :key="station.code"> <template v-for="({ stationName, clusters }, code) in nvrClusterRecord" :key="code">
<NGridItem> <NGridItem>
<NButton text type="info" style="height: 30px" @click="() => batchExportRecordCheck({ stations: [station] })">{{ station.name }}</NButton> <NButton text type="info" style="height: 30px" @click="() => exportRecordDiags({ clusters, stationCode: code })">{{ stationName }}</NButton>
</NGridItem> </NGridItem>
</template> </template>
</NGrid> </NGrid>
@@ -109,7 +139,7 @@ const onAfterLeave = () => {
</template> </template>
<template #action> <template #action>
<NFlex justify="flex-end" align="center"> <NFlex justify="flex-end" align="center">
<NButton secondary :loading="batchExporting" @click="() => batchExportRecordCheck({ stations })">导出全部</NButton> <NButton secondary :loading="batchExporting" @click="() => batchExportRecordCheck()">导出全部</NButton>
</NFlex> </NFlex>
</template> </template>
</NModal> </NModal>
-3
View File
@@ -1,3 +0,0 @@
export * from './nvr';
export * from './security-box';
export * from './switch';
-1
View File
@@ -1 +0,0 @@
export * from './util';
-56
View File
@@ -1,56 +0,0 @@
import type { NdmNvrResultVO } from '@/apis';
// 解析 clusterList 字段
export const parseIpListFromClusterList = (nvr: NdmNvrResultVO) => {
const ipList = (nvr.clusterList ?? '').split(';');
return ipList.map((ip) => ip.trim()).filter((ip) => !!ip);
};
export const isNvrCluster = (maybeNvrCluster: NdmNvrResultVO) => {
const { ipAddress } = maybeNvrCluster;
const ipList = parseIpListFromClusterList(maybeNvrCluster);
if (ipList.length === 0) return false;
if (ipList.length === 1 && ipList.at(0) === ipAddress) return false;
return true;
};
export const nvrInCluster = (nvr: NdmNvrResultVO, cluster: NdmNvrResultVO) => {
const { ipAddress } = nvr;
if (!ipAddress) return false;
const ipList = parseIpListFromClusterList(cluster);
return ipList.includes(ipAddress);
};
export const createNvrClusterRelationship = (nvrDevices: NdmNvrResultVO[]) => {
const nvrClusters = nvrDevices.filter((nvr) => isNvrCluster(nvr));
const nvrNotClusters = nvrDevices.filter((nvr) => !isNvrCluster(nvr));
const nodedNvrIpAddressSet = new Set<string | null>();
const nvrStandalones: NdmNvrResultVO[] = [];
const nvrTreeMap = new Map<string, NdmNvrResultVO[]>();
// 遍历所有非集群录像机,将它们分配到对应的录像机集群中
for (const nvr of nvrNotClusters) {
for (const cluster of nvrClusters) {
if (nvrInCluster(nvr, cluster)) {
if (!!cluster.ipAddress) {
// 写入录像机与集群的关系
nvrTreeMap.set(cluster.ipAddress, [...(nvrTreeMap.get(cluster.ipAddress) ?? []), nvr]);
// 记录已分配的录像机IP地址
nodedNvrIpAddressSet.add(nvr.ipAddress);
}
}
}
}
// 分配完成后,过滤出未分配的录像机,形成录像机单机列表
nvrNotClusters.forEach((device) => {
if (!nodedNvrIpAddressSet.has(device.ipAddress)) {
nvrStandalones.push(device);
}
});
return {
nvrClusters,
// nvrNotClusters,
nvrTreeMap,
nvrStandalones,
};
};
@@ -1,64 +0,0 @@
import { rebootSecurityBoxBeidianApi, rebootSecurityBoxNingTechApi, turnCircuitStatusBeidianApi, turnCircuitStatusNingTechApi, type NdmSecurityBoxResultVO } from '@/apis';
import { objectEntries } from '@vueuse/core';
const UNSUPPORTED_SECURITY_BOX_VENDOR = '不支持的安防箱厂商';
export const SECURITY_BOX_VENDOR_LITERALS = {
beidian: 'beidian',
ningtech: 'ningtech',
} as const;
export type SecurityBoxVendor = keyof typeof SECURITY_BOX_VENDOR_LITERALS;
export const resolveSecurityBoxVendor = (vendor?: string) => {
const entry = objectEntries(SECURITY_BOX_VENDOR_LITERALS).find(([, value]) => value === vendor);
return entry?.at(0);
};
export const normalizeSecurityBoxWriteCommunity = (ndmDevice: NdmSecurityBoxResultVO, vendor?: string) => {
const resolved = resolveSecurityBoxVendor(vendor);
if (resolved === SECURITY_BOX_VENDOR_LITERALS.beidian) {
const community = ndmDevice.community;
if (!community) throw new Error('团体字符串不存在');
return community;
}
if (resolved === SECURITY_BOX_VENDOR_LITERALS.ningtech) {
const community = ndmDevice.writeCommunity;
if (!community) throw new Error('团体字符串(写)不存在');
return community;
}
throw new Error(UNSUPPORTED_SECURITY_BOX_VENDOR);
};
export const normalizeSecurityBoxCircuitIndex = (index: number, vendor?: string) => {
const resolved = resolveSecurityBoxVendor(vendor);
if (resolved === SECURITY_BOX_VENDOR_LITERALS.beidian) {
return index;
}
if (resolved === SECURITY_BOX_VENDOR_LITERALS.ningtech) {
return index + 1;
}
throw new Error(UNSUPPORTED_SECURITY_BOX_VENDOR);
};
export const dispatchTurnCircuitStatusApi = (vendor?: string) => {
const resolved = resolveSecurityBoxVendor(vendor);
if (resolved === SECURITY_BOX_VENDOR_LITERALS.beidian) {
return turnCircuitStatusBeidianApi;
}
if (resolved === SECURITY_BOX_VENDOR_LITERALS.ningtech) {
return turnCircuitStatusNingTechApi;
}
throw new Error(UNSUPPORTED_SECURITY_BOX_VENDOR);
};
export const dispatchRebootSecurityBoxApi = (vendor?: string) => {
const resolved = resolveSecurityBoxVendor(vendor);
if (resolved === SECURITY_BOX_VENDOR_LITERALS.beidian) {
return rebootSecurityBoxBeidianApi;
}
if (resolved === SECURITY_BOX_VENDOR_LITERALS.ningtech) {
return rebootSecurityBoxNingTechApi;
}
throw new Error(UNSUPPORTED_SECURITY_BOX_VENDOR);
};
-1
View File
@@ -1 +0,0 @@
export * from './adapter';
-1
View File
@@ -1 +0,0 @@
export * from './util';
+26
View File
@@ -0,0 +1,26 @@
import type { Station } from '@/apis';
import type { NvrRecordDiag } from './record-check';
import { downloadByData, formatDuration } from '@/utils';
import dayjs from 'dayjs';
export const exportRecordDiagCsv = (recordDiags: NvrRecordDiag[], stationName: Station['name']) => {
const csvHeader = '通道名称,开始时间,结束时间,持续时长\n';
const csvRows = recordDiags
.map((channel) => {
if (channel.lostChunks.length === 0) {
return `${channel.channelName},,,`;
}
return channel.lostChunks
.map((loss) => {
const duration = formatDuration(loss.startTime, loss.endTime);
const startTime = dayjs(loss.startTime).format('YYYY-MM-DD HH:mm:ss');
const endTime = dayjs(loss.endTime).format('YYYY-MM-DD HH:mm:ss');
return `${channel.channelName},${startTime},${endTime},${duration}`;
})
.join('\n');
})
.join('\n');
const csvContent = csvHeader.concat(csvRows);
const time = dayjs().format('YYYY-MM-DD_HH-mm-ss');
downloadByData(csvContent, `${stationName}_录像缺失记录_${time}.csv`, 'text/csv;charset=utf-8', '\ufeff');
};
+5 -2
View File
@@ -1,2 +1,5 @@
export * from './device'; export * from './device-alarm';
export * from './log'; export * from './export-record-diag-csv';
export * from './nvr-cluster';
export * from './record-check';
export * from './switch-port';
-1
View File
@@ -1 +0,0 @@
export * from './render';
-1
View File
@@ -1 +0,0 @@
export * from './alarm-log';
+8
View File
@@ -0,0 +1,8 @@
import type { NdmNvrResultVO } from '@/apis';
export const isNvrCluster = (maybeNvrCluster: NdmNvrResultVO) => {
const { ipAddress, clusterList } = maybeNvrCluster;
if (!clusterList?.trim()) return false;
if (clusterList === ipAddress) return false;
return true;
};
+69
View File
@@ -0,0 +1,69 @@
import type { NdmRecordCheck, RecordInfo, RecordItem } from '@/apis';
import dayjs from 'dayjs';
import destr from 'destr';
import { groupBy } from 'es-toolkit';
export type NvrRecordDiag = {
gbCode: string;
channelName: string;
recordDuration: RecordItem;
lostChunks: RecordItem[];
};
// 解析出丢失的录像时间段
export const transformRecordChecks = (rawRecordChecks: NdmRecordCheck[]): NvrRecordDiag[] => {
// 解析diagInfo
const parsedRecordChecks = rawRecordChecks.map((recordCheck) => ({
...recordCheck,
diagInfo: destr<RecordInfo>(recordCheck.diagInfo),
}));
// 按国标码分组
const recordChecksByGbCode = groupBy(parsedRecordChecks, (recordCheck) => recordCheck.gbCode);
// 提取分组后的国标码和录像诊断记录
const channelGbCodes = Object.keys(recordChecksByGbCode);
const recordChecksList = Object.values(recordChecksByGbCode);
// 初始化每个通道的录像诊断数据结构
const recordDiags = channelGbCodes.map((gbCode, index) => ({
gbCode,
channelName: recordChecksList.at(index)?.at(-1)?.name ?? '',
records: [] as RecordItem[],
lostChunks: [] as RecordItem[],
}));
// 写入同一gbCode的录像片段
recordChecksList.forEach((recordChecks, index) => {
recordChecks.forEach((recordCheck) => {
recordDiags.at(index)?.records.push(...recordCheck.diagInfo.recordList);
});
});
// 过滤掉没有录像记录的通道
const filteredRecordDiags = recordDiags.filter((recordDiag) => recordDiag.records.length > 0);
// 计算每个通道丢失的录像时间片段
filteredRecordDiags.forEach((recordDiag) => {
recordDiag.records.forEach((record, index, records) => {
const nextRecordItem = records.at(index + 1);
if (!!nextRecordItem) {
// 如果下一段录像的开始时间不等于当前录像的结束时间,则判定为丢失
const nextStartTime = nextRecordItem.startTime;
const currEndTime = record.endTime;
if (nextStartTime !== currEndTime) {
recordDiag.lostChunks.push({
startTime: currEndTime,
endTime: nextStartTime,
});
}
}
});
});
return recordDiags.map((recordDiag) => {
const firstRecord = recordDiag.records.at(0);
const startTime = firstRecord ? dayjs(firstRecord.startTime).format('YYYY-MM-DD HH:mm:ss') : '';
const lastRecord = recordDiag.records.at(-1);
const endTime = lastRecord ? dayjs(lastRecord.endTime).format('YYYY-MM-DD HH:mm:ss') : '';
return {
gbCode: recordDiag.gbCode,
channelName: recordDiag.channelName,
recordDuration: { startTime, endTime },
lostChunks: recordDiag.lostChunks,
};
});
};
+1 -6
View File
@@ -4,7 +4,7 @@ import { useLineStationsQuery, useStompClient, useUserPermissionQuery, useVerify
import { LINE_ALARMS_QUERY_KEY, LINE_DEVICES_QUERY_KEY, LINE_STATIONS_MUTATION_KEY, LINE_STATIONS_QUERY_KEY, STATION_ALARMS_MUTATION_KEY, STATION_DEVICES_MUTATION_KEY } from '@/constants'; import { LINE_ALARMS_QUERY_KEY, LINE_DEVICES_QUERY_KEY, LINE_STATIONS_MUTATION_KEY, LINE_STATIONS_QUERY_KEY, STATION_ALARMS_MUTATION_KEY, STATION_DEVICES_MUTATION_KEY } from '@/constants';
import { useSettingStore, useUnreadStore, useUserStore } from '@/stores'; import { useSettingStore, useUnreadStore, useUserStore } from '@/stores';
import { useIsFetching, useIsMutating } from '@tanstack/vue-query'; import { useIsFetching, useIsMutating } from '@tanstack/vue-query';
import { ChevronDownIcon, ChevronsLeftIcon, ChevronsRightIcon, ComputerIcon, KeyRoundIcon, LogOutIcon, LogsIcon, MapPinIcon, MonitorPlayIcon, SettingsIcon, SirenIcon } from 'lucide-vue-next'; import { ChevronDownIcon, ChevronsLeftIcon, ChevronsRightIcon, ComputerIcon, KeyRoundIcon, LogOutIcon, LogsIcon, MapPinIcon, SettingsIcon, SirenIcon } from 'lucide-vue-next';
import { import {
NBadge, NBadge,
NButton, NButton,
@@ -111,11 +111,6 @@ const menuOptions = computed<MenuOption[]>(() => [
show: isLamp.value, show: isLamp.value,
icon: renderIcon(KeyRoundIcon), icon: renderIcon(KeyRoundIcon),
}, },
{
label: () => h(RouterLink, { to: '/vimp' }, { default: () => '视频综合管理平台' }),
key: '/vimp',
icon: renderIcon(MonitorPlayIcon),
},
]); ]);
const dropdownOptions: DropdownOption[] = [ const dropdownOptions: DropdownOption[] = [
+1 -1
View File
@@ -211,7 +211,7 @@ const { mutate: cancelIgnore } = useMutation({
const tableData = ref<DataTableRowData[]>([]); const tableData = ref<DataTableRowData[]>([]);
const DEFAULT_PAGE_SIZE = 20; const DEFAULT_PAGE_SIZE = 10;
const pagination = reactive<PaginationProps>({ const pagination = reactive<PaginationProps>({
showSizePicker: true, showSizePicker: true,
page: 1, page: 1,
+7 -10
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { exportDeviceAlarmLogApi, pageDeviceAlarmLogApi, type NdmDeviceAlarmLog, type NdmDeviceAlarmLogPageQuery, type NdmDeviceAlarmLogResultVO, type PageQueryExtra, type Station } from '@/apis'; import { exportDeviceAlarmLogApi, pageDeviceAlarmLogApi, type NdmDeviceAlarmLog, type NdmDeviceAlarmLogResultVO, type PageQueryExtra, type Station } from '@/apis';
import { useAlarmActionColumn, useCameraSnapColumn } from '@/composables'; import { useAlarmActionColumn, useCameraSnapColumn } from '@/composables';
import { ALARM_TYPES, DEVICE_TYPE_CODES, DEVICE_TYPE_LITERALS, DEVICE_TYPE_NAMES, FAULT_LEVELS, tryGetDeviceType, type DeviceType } from '@/enums'; import { ALARM_TYPES, DEVICE_TYPE_CODES, DEVICE_TYPE_LITERALS, DEVICE_TYPE_NAMES, FAULT_LEVELS, tryGetDeviceType, type DeviceType } from '@/enums';
import { renderAlarmDateCell, renderAlarmTypeCell, renderDeviceTypeCell, renderFaultLevelCell } from '@/helpers'; import { renderAlarmDateCell, renderAlarmTypeCell, renderDeviceTypeCell, renderFaultLevelCell } from '@/helpers';
@@ -132,12 +132,6 @@ const resetSearchFields = () => {
alarmConfirm: '', alarmConfirm: '',
}; };
}; };
const getModelFields = (): NdmDeviceAlarmLogPageQuery => {
return {
alarmCategory: searchFields.value.alarmCategory || undefined,
alarmConfirm: searchFields.value.alarmConfirm || undefined,
};
};
const getExtraFields = (): PageQueryExtra<NdmDeviceAlarmLog> => { const getExtraFields = (): PageQueryExtra<NdmDeviceAlarmLog> => {
const stationCodeIn = searchFields.value.stationCode_in; const stationCodeIn = searchFields.value.stationCode_in;
const deviceTypeIn = searchFields.value.deviceType_in.flatMap((deviceType) => DEVICE_TYPE_CODES[deviceType as DeviceType]); const deviceTypeIn = searchFields.value.deviceType_in.flatMap((deviceType) => DEVICE_TYPE_CODES[deviceType as DeviceType]);
@@ -218,7 +212,7 @@ const tableColumns: DataTableColumns<NdmDeviceAlarmLogResultVO> = [
alarmActionColumn, alarmActionColumn,
]; ];
const DEFAULT_PAGE_SIZE = 20; const DEFAULT_PAGE_SIZE = 10;
const pagination = reactive<PaginationProps>({ const pagination = reactive<PaginationProps>({
showSizePicker: true, showSizePicker: true,
page: 1, page: 1,
@@ -250,7 +244,10 @@ const { mutate: getTableData, isPending: tableLoading } = useMutation({
const res = await pageDeviceAlarmLogApi( const res = await pageDeviceAlarmLogApi(
{ {
model: getModelFields(), model: {
alarmCategory: searchFields.value.alarmCategory || undefined,
alarmConfirm: searchFields.value.alarmConfirm || undefined,
},
extra: getExtraFields(), extra: getExtraFields(),
current: pagination.page ?? 1, current: pagination.page ?? 1,
size: pagination.pageSize ?? DEFAULT_PAGE_SIZE, size: pagination.pageSize ?? DEFAULT_PAGE_SIZE,
@@ -305,7 +302,7 @@ const { mutate: exportTableData, isPending: exporting } = useMutation({
const data = await exportDeviceAlarmLogApi( const data = await exportDeviceAlarmLogApi(
{ {
model: getModelFields(), model: {},
extra: getExtraFields(), extra: getExtraFields(),
current: pagination.page ?? 1, current: pagination.page ?? 1,
size: pagination.pageSize ?? 10, size: pagination.pageSize ?? 10,
+2 -2
View File
@@ -104,7 +104,7 @@ const getExtraFields = (): PageQueryExtra<NdmCallLog> => {
const method_like = searchFields.value.method_like; const method_like = searchFields.value.method_like;
const messageType_like = searchFields.value.messageType_like; const messageType_like = searchFields.value.messageType_like;
const cmdType_like = searchFields.value.cmdType_like; const cmdType_like = searchFields.value.cmdType_like;
const logType_in = searchFields.value.logType_in.length > 0 ? [...searchFields.value.logType_in] : [...callLogTypeOptions.map((option) => option.value)]; const logType_in = searchFields.value.logType_in;
return { return {
createdTime_precisest, createdTime_precisest,
createdTime_preciseed, createdTime_preciseed,
@@ -143,7 +143,7 @@ const tableColumns: DataTableColumns<NdmCallLogResultVO> = [
const tableData = ref<DataTableRowData[]>([]); const tableData = ref<DataTableRowData[]>([]);
const DEFAULT_PAGE_SIZE = 20; const DEFAULT_PAGE_SIZE = 10;
const pagination = reactive<PaginationProps>({ const pagination = reactive<PaginationProps>({
showSizePicker: true, showSizePicker: true,
page: 1, page: 1,
+10 -3
View File
@@ -95,7 +95,7 @@ const resetSearchFields = () => {
const getExtraFields = (): PageQueryExtra<NdmVimpLog> => { const getExtraFields = (): PageQueryExtra<NdmVimpLog> => {
const createdTime_precisest = searchFields.value.createdTime[0]; const createdTime_precisest = searchFields.value.createdTime[0];
const createdTime_preciseed = searchFields.value.createdTime[1]; const createdTime_preciseed = searchFields.value.createdTime[1];
const logType_in = searchFields.value.logType_in.length > 0 ? [...searchFields.value.logType_in] : [...vimpLogTypeOptions.map((option) => option.value)]; const logType_in = (searchFields.value.logType_in ?? []).length > 0 ? [...searchFields.value.logType_in] : undefined;
return { return {
createdTime_precisest, createdTime_precisest,
createdTime_preciseed, createdTime_preciseed,
@@ -110,7 +110,14 @@ watch(searchFields, () => {
const tableColumns: DataTableColumns<NdmVimpLogResultVO> = [ const tableColumns: DataTableColumns<NdmVimpLogResultVO> = [
{ title: '时间', key: 'createdTime' }, { title: '时间', key: 'createdTime' },
{ title: '操作类型', key: 'description' }, {
title: '操作类型',
key: 'logType',
render: (rowData) => {
const option = vimpLogTypeOptions.find((option) => option.value === rowData.logType);
return `${option?.label ?? ''}`;
},
},
{ title: '请求IP', key: 'requestIp' }, { title: '请求IP', key: 'requestIp' },
{ title: '耗时(ms)', key: 'consumedTime' }, { title: '耗时(ms)', key: 'consumedTime' },
{ title: '被调用设备', key: 'targetCode' }, { title: '被调用设备', key: 'targetCode' },
@@ -118,7 +125,7 @@ const tableColumns: DataTableColumns<NdmVimpLogResultVO> = [
const tableData = ref<DataTableRowData[]>([]); const tableData = ref<DataTableRowData[]>([]);
const DEFAULT_PAGE_SIZE = 20; const DEFAULT_PAGE_SIZE = 10;
const pagination = reactive<PaginationProps>({ const pagination = reactive<PaginationProps>({
showSizePicker: true, showSizePicker: true,
page: 1, page: 1,
+1 -1
View File
@@ -63,7 +63,7 @@ const tableColumns: DataTableColumns<BaseEmployeeResultVO> = [
const tableData = ref<DataTableRowData[]>([]); const tableData = ref<DataTableRowData[]>([]);
const DEFAULT_PAGE_SIZE = 20; const DEFAULT_PAGE_SIZE = 10;
const pagination = reactive<PaginationProps>({ const pagination = reactive<PaginationProps>({
showSizePicker: true, showSizePicker: true,
page: 1, page: 1,
@@ -1,58 +0,0 @@
<script setup lang="ts">
import type { Changelog } from '@/apis';
import { useQuery } from '@tanstack/vue-query';
import axios from 'axios';
import { NH1, NH2, NH3, NLi, NP, NScrollbar, NText, NUl } from 'naive-ui';
import { computed } from 'vue';
const CHENGELOGS_QUERY_KEY = 'changelogs-query';
const { data: changelogs = [] } = useQuery({
queryKey: computed(() => [CHENGELOGS_QUERY_KEY]),
queryFn: async ({ signal }) => {
const response = await axios.get<Changelog[]>(`changelogs.json?t=${Date.now()}`, { signal });
return response.data;
},
});
</script>
<template>
<NScrollbar content-style="padding: 32px 24px 56px 56px" style="width: 100%; height: 100%">
<NH1>平台更新记录</NH1>
<template v-for="{ version, date, changes } in changelogs" :key="version">
<NH2>{{ version }}</NH2>
<NP>
<NText code>{{ date }}</NText>
</NP>
<template v-if="(changes.breaks?.length ?? 0) > 0">
<NH3>重大变更</NH3>
<template v-for="({ content }, index) in changes.breaks" :key="index">
<NUl>
<NLi>{{ content }}</NLi>
</NUl>
</template>
</template>
<template v-if="(changes.fixes?.length ?? 0) > 0">
<NH3>修复</NH3>
<template v-for="({ content }, index) in changes.fixes" :key="index">
<NUl>
<NLi>{{ content }}</NLi>
</NUl>
</template>
</template>
<template v-if="(changes.feats?.length ?? 0) > 0">
<NH3>新增</NH3>
<template v-for="({ content }, index) in changes.feats" :key="index">
<NUl>
<NLi>{{ content }}</NLi>
</NUl>
</template>
</template>
</template>
</NScrollbar>
</template>
<style scoped></style>
-1
View File
@@ -1 +0,0 @@
export * from './vimp-client';
-45
View File
@@ -1,45 +0,0 @@
import type { AxiosError, AxiosRequestConfig, CreateAxiosDefaults } from 'axios';
import axios from 'axios';
import type { VimpResponse, VimpResult } from '../../types';
export const createVimpClient = (config?: CreateAxiosDefaults) => {
const instance = axios.create(config);
const vimpPost = <T>(url: string, data?: AxiosRequestConfig['data'], options?: Partial<Omit<AxiosRequestConfig, 'data'>> & { retRaw?: boolean; upload?: boolean }): Promise<VimpResponse<T>> => {
const { retRaw, upload, ...reqConfig } = options ?? {};
return new Promise((resolve) => {
instance
.post(url, data, { headers: { 'content-type': upload ? 'multipart/form-data' : 'application/json' }, ...reqConfig })
.then((res) => {
const resData = res.data;
if (retRaw) {
resolve([null, resData as T, null]);
} else {
resolve([null, resData.data as T, resData as VimpResult<T>]);
}
})
.catch((err) => {
resolve([err as AxiosError, null, null]);
});
});
};
return {
instance,
post: vimpPost,
};
};
export const unwrapVimpResponse = <T>(resp: VimpResponse<T>) => {
const [err, data, result] = resp;
if (err) throw err;
if (result) {
const { code, msg } = result;
if (code !== 0 && code !== 200) throw new Error(`${msg || '请求失败'}`);
}
return data;
};
export const vimpClient = createVimpClient({
baseURL: `/vimp/api/client`,
});
-3
View File
@@ -1,3 +0,0 @@
export * from './client';
export * from './model';
export * from './request';
-2
View File
@@ -1,2 +0,0 @@
export * from './vimp-channel';
export * from './vimp-station';
-15
View File
@@ -1,15 +0,0 @@
export interface VimpChannel {
address: string;
block: string;
civilCode: string;
code: string;
latitude: number;
longitude: number;
manufacture: string;
model: string;
name: string;
owner: string;
parentId: string;
parental: number;
status: number;
}
@@ -1,5 +0,0 @@
export interface VimpStation {
code: string;
name: string;
online: boolean;
}
@@ -1,11 +0,0 @@
import { unwrapVimpResponse, vimpClient } from '../client';
import type { VimpStation } from '../model';
export const catalogAllDeviceApi = async (options?: { signal?: AbortSignal }) => {
const { signal } = options ?? {};
const client = vimpClient;
const endpoint = `/catalog/allDevice`;
const resp = await client.post<VimpStation[]>(endpoint, {}, { signal });
const data = unwrapVimpResponse(resp);
return data;
};
@@ -1,11 +0,0 @@
import { unwrapVimpResponse, vimpClient } from '../client';
import type { VimpChannel } from '../model';
export const catalogChannelApi = async (code: string, options?: { signal?: AbortSignal }) => {
const { signal } = options ?? {};
const client = vimpClient;
const endpoint = `/catalog/channel`;
const resp = await client.post<VimpChannel[]>(endpoint, { code, time: '' }, { signal });
const data = unwrapVimpResponse(resp);
return data;
};
-2
View File
@@ -1,2 +0,0 @@
export * from './catalog.channel';
export * from './catalog.all-device';
-131
View File
@@ -1,131 +0,0 @@
<script setup lang="ts">
import { NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type TreeProps } from 'naive-ui';
import { h, type CSSProperties } from 'vue';
import { useAlarmStore, useResourcePanelStore } from '../stores';
import { storeToRefs } from 'pinia';
import { useDeviceCenterQuery } from '../composables';
import { isAlarmNode, isAlarmSiteNode, isAlarmAreaNode } from '../types';
const { isLoading } = useDeviceCenterQuery();
const alarmStore = useAlarmStore();
const { lineTabPanes } = storeToRefs(alarmStore);
const overrideNodeClickBehavior: TreeOverrideNodeClickBehavior = ({ option }) => {
const hasChildren = (option.children?.length ?? 0) > 0;
if (hasChildren) {
return 'toggleExpand';
} else {
return 'none';
}
};
const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
// 是车站节点
if (isAlarmSiteNode(option)) {
const siteOnline = option.online;
const siteNodeStyle: CSSProperties = {
opacity: siteOnline ? 1 : 0.5,
};
return h('div', { style: siteNodeStyle }, option.label);
}
// 是中间节点(一级/二级区域)
if (isAlarmAreaNode(option)) {
const site = option.site;
const nodeStyle: CSSProperties = {
opacity: site.online ? 1 : 0.5,
};
return h('div', { style: nodeStyle }, option.label);
}
// 是警报器节点
if (isAlarmNode(option)) {
const alarm = option.alarm;
const site = option.site;
const alarmOnline = () => {
return alarm.status === 1 && site.online;
};
const alarmNodeStyle: CSSProperties = {
opacity: alarmOnline() ? 1 : 0.5,
cursor: alarmOnline() ? 'pointer' : 'not-allowed',
};
return h(
'div',
{
style: alarmNodeStyle,
draggable: alarmOnline(),
onDblclick() {
if (!alarmOnline()) return;
window.$message.info(`查看警报器:${JSON.stringify({ code: alarm.code, name: alarm.name })}`);
},
onDragstart(event) {
if (!alarmOnline()) return;
console.log(event);
event.dataTransfer?.setData('type', 'alarm');
event.dataTransfer?.setData('code', alarm.code);
event.dataTransfer?.setData('name', alarm.name);
},
},
alarm.name,
);
}
// 其他节点(兜底,理论上不会走到这里)
return option.label;
};
const resourcePanelStore = useResourcePanelStore();
const { searchPattern } = storeToRefs(resourcePanelStore);
const searchFilter: TreeProps['filter'] = (pattern, node) => {
if (!isAlarmNode(node)) return false;
return node.alarm.name.includes(pattern);
};
</script>
<template>
<template v-if="isLoading">
<div>loading...</div>
</template>
<template v-else-if="lineTabPanes.length === 1">
<NTree
block-line
block-node
show-line
virtual-scroll
style="height: 100%"
:render-label="renderNodeLabel"
:override-default-node-click-behavior="overrideNodeClickBehavior"
:default-expand-all="searchPattern.trim().length > 0"
:show-irrelevant-nodes="false"
:data="lineTabPanes.at(0)?.alarmTree"
:pattern="searchPattern"
:filter="searchFilter"
/>
</template>
<template v-if="lineTabPanes.length > 1">
<NTabs :type="'card'" :placement="'left'" style="height: 100%">
<NTabPane v-for="{ lineCode, lineName, alarmTree } in lineTabPanes" :key="lineCode" :name="lineName" :tab="lineName">
<NTree
block-line
block-node
show-line
virtual-scroll
style="height: 100%"
:render-label="renderNodeLabel"
:override-default-node-click-behavior="overrideNodeClickBehavior"
:default-expand-all="false"
:show-irrelevant-nodes="false"
:data="alarmTree"
:pattern="searchPattern"
:filter="searchFilter"
/>
</NTabPane>
</NTabs>
</template>
</template>
<style scoped></style>
-131
View File
@@ -1,131 +0,0 @@
<script setup lang="ts">
import { NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type TreeProps } from 'naive-ui';
import { h, type CSSProperties } from 'vue';
import { useCameraStore, useResourcePanelStore } from '../stores';
import { storeToRefs } from 'pinia';
import { useDeviceCenterQuery } from '../composables';
import { isCameraNode, isCameraSiteNode, isCameraAreaNode } from '../types';
const { isLoading } = useDeviceCenterQuery();
const cameraStore = useCameraStore();
const { lineTabPanes } = storeToRefs(cameraStore);
const overrideNodeClickBehavior: TreeOverrideNodeClickBehavior = ({ option }) => {
const hasChildren = (option.children?.length ?? 0) > 0;
if (hasChildren) {
return 'toggleExpand';
} else {
return 'none';
}
};
const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
// 是车站节点
if (isCameraSiteNode(option)) {
const siteOnline = option.online;
const siteNodeStyle: CSSProperties = {
opacity: siteOnline ? 1 : 0.5,
};
return h('div', { style: siteNodeStyle }, option.label);
}
// 是中间节点(一级/二级区域)
if (isCameraAreaNode(option)) {
const site = option.site;
const nodeStyle: CSSProperties = {
opacity: site.online ? 1 : 0.5,
};
return h('div', { style: nodeStyle }, option.label);
}
// 是摄像机节点
if (isCameraNode(option)) {
const camera = option.camera;
const site = option.site;
const cameraOnline = () => {
return camera.status === 1 && site.online;
};
const cameraNodeStyle: CSSProperties = {
opacity: cameraOnline() ? 1 : 0.5,
cursor: cameraOnline() ? 'pointer' : 'not-allowed',
};
return h(
'div',
{
style: cameraNodeStyle,
draggable: cameraOnline(),
onDblclick() {
if (!cameraOnline()) return;
window.$message.info(`播放:${JSON.stringify({ code: camera.code, name: camera.name })}`);
},
onDragstart(event) {
if (!cameraOnline()) return;
console.log(event);
event.dataTransfer?.setData('type', 'camera');
event.dataTransfer?.setData('code', camera.code);
event.dataTransfer?.setData('name', camera.name);
},
},
camera.name,
);
}
// 其他节点(兜底,理论上不会走到这里)
return option.label;
};
const resourcePanelStore = useResourcePanelStore();
const { searchPattern } = storeToRefs(resourcePanelStore);
const searchFilter: TreeProps['filter'] = (pattern, node) => {
if (!isCameraNode(node)) return false;
return node.camera.name.includes(pattern);
};
</script>
<template>
<template v-if="isLoading">
<div>loading...</div>
</template>
<template v-else-if="lineTabPanes.length === 1">
<NTree
block-line
block-node
show-line
virtual-scroll
style="height: 100%"
:render-label="renderNodeLabel"
:override-default-node-click-behavior="overrideNodeClickBehavior"
:default-expand-all="searchPattern.trim().length > 0"
:show-irrelevant-nodes="false"
:data="lineTabPanes.at(0)?.cameraTree"
:pattern="searchPattern"
:filter="searchFilter"
/>
</template>
<template v-if="lineTabPanes.length > 1">
<NTabs :type="'card'" :placement="'left'" style="height: 100%">
<NTabPane v-for="{ lineCode, lineName, cameraTree } in lineTabPanes" :key="lineCode" :name="lineName" :tab="lineName">
<NTree
block-line
block-node
show-line
virtual-scroll
style="height: 100%"
:render-label="renderNodeLabel"
:override-default-node-click-behavior="overrideNodeClickBehavior"
:default-expand-all="false"
:show-irrelevant-nodes="false"
:data="cameraTree"
:pattern="searchPattern"
:filter="searchFilter"
/>
</NTabPane>
</NTabs>
</template>
</template>
<style scoped></style>
@@ -1,7 +0,0 @@
<template>
<svg class="icon" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path
d="m6.03 12.03l2 3.47l-2.53 3.18L2 12.62l4.03-.59M17 18v-2.71c.88-.39 1.5-1.26 1.5-2.29c0-.57-.2-1.1-.53-1.5l1.97-1.15c1.01-.59 1.36-1.88.77-2.89l-1.38-2.4a2.125 2.125 0 0 0-2.89-.78L8.31 9c-.95.53-1.28 1.75-.73 2.71l1.5 2.6c.55.95 1.78 1.28 2.73.73l1.88-1.08c.25.59.72 1.07 1.31 1.33V18c0 1.1.9 2 2 2h5v-2h-5Z"
/>
</svg>
</template>
@@ -1,9 +0,0 @@
<template>
<svg class="icon" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-width="4">
<path d="M8 10v14c0 8.837 7.163 16 16 16s16-7.163 16-16V10" />
<path stroke-linecap="round" stroke-linejoin="round" d="M4 10h40" />
<path stroke-linejoin="round" d="M24 30a6 6 0 1 0 0-12a6 6 0 0 0 0 12Z" />
</g>
</svg>
</template>
@@ -1,7 +0,0 @@
<template>
<svg class="icon" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path
d="M2 4.5A1.5 1.5 0 0 1 3.5 3h13a1.5 1.5 0 0 1 0 3h-13A1.5 1.5 0 0 1 2 4.5ZM10 9a3 3 0 1 0 0 6a3 3 0 0 0 0-6Zm-2 3a2 2 0 1 1 4 0a2 2 0 0 1-4 0ZM3 7h14v4a7 7 0 1 1-14 0V7Zm7 1a4 4 0 1 0 0 8a4 4 0 0 0 0-8Z"
/>
</svg>
</template>

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