Compare commits
19 Commits
b392328f37
...
manual
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dd13f56fb | ||
|
|
b020226538 | ||
|
|
b79b1df57e | ||
|
|
9b21beed0f | ||
|
|
aa4684273b | ||
|
|
36e839142a | ||
|
|
03006a8f06 | ||
|
|
0af52c62ce | ||
|
|
82789c78a9 | ||
|
|
6771abec31 | ||
|
|
b7b6b216fb | ||
|
|
97f05f21ae | ||
|
|
c4963b5b70 | ||
|
|
86775a6eb2 | ||
|
|
db3a6ddca5 | ||
|
|
b59b34e68b | ||
|
|
de35075be8 | ||
|
|
045f7a9a81 | ||
|
|
bd6ad9b932 |
2
.env
2
.env
@@ -19,7 +19,7 @@ VITE_LAMP_PASSWORD = fjoc(1KHP(Ls&Bje)C
|
|||||||
VITE_LAMP_AUTHORIZATION = Y3VlZGVzX2FkbWluOmN1ZWRlc19hZG1pbl9zZWNyZXQ=
|
VITE_LAMP_AUTHORIZATION = Y3VlZGVzX2FkbWluOmN1ZWRlc19hZG1pbl9zZWNyZXQ=
|
||||||
|
|
||||||
# 当需要重置localStorage时, 修改此变量
|
# 当需要重置localStorage时, 修改此变量
|
||||||
VITE_STORAGE_VERSION = 3
|
VITE_STORAGE_VERSION = 5
|
||||||
|
|
||||||
# 调试码
|
# 调试码
|
||||||
VITE_DEBUG_CODE = ndm_debug
|
VITE_DEBUG_CODE = ndm_debug
|
||||||
|
|||||||
327
.trae/documents/网络设备管理平台用户手册.md
Normal file
327
.trae/documents/网络设备管理平台用户手册.md
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
# 网络设备管理平台用户手册
|
||||||
|
|
||||||
|
## 1. 平台概述与基础操作
|
||||||
|
|
||||||
|
### 1.1 平台简介
|
||||||
|
|
||||||
|
网络设备管理平台(以下简称为“平台”)是专为地铁线路运管中心设计的综合性管理系统。本平台旨在对车站内的各类网络设备进行集中监测、数据查看、运行状态监控及异常告警管理,并提供数据分析与统计功能。
|
||||||
|
|
||||||
|
**支持管理的设备类型包括:**
|
||||||
|
|
||||||
|
- 摄像机
|
||||||
|
- 网络录像机
|
||||||
|
- 交换机
|
||||||
|
- 解码器
|
||||||
|
- 智能安防箱
|
||||||
|
- 媒体服务器
|
||||||
|
- 视频服务器
|
||||||
|
- 网络键盘
|
||||||
|
- 报警主机
|
||||||
|
|
||||||
|
### 1.2 登录与退出
|
||||||
|
|
||||||
|
**登录平台:**
|
||||||
|
|
||||||
|
1. 在浏览器地址栏输入平台访问地址。
|
||||||
|
2. 进入登录页面后,输入您的 **账号** 和 **密码**。
|
||||||
|
3. 点击“登录”按钮。
|
||||||
|
4. 登录成功后,平台将默认跳转至 **车站状态模块 (首页)**。
|
||||||
|
|
||||||
|
**退出登录:**
|
||||||
|
|
||||||
|
1. 在平台任意页面的 **顶部区域** 右侧,找到显示您昵称的用户按钮。
|
||||||
|
2. 将鼠标悬停或点击该按钮,在下拉菜单中选择 **“退出登录”**。
|
||||||
|
3. 平台将安全退出您的账号并返回登录页面。
|
||||||
|
|
||||||
|
### 1.3 界面概览
|
||||||
|
|
||||||
|
登录后,平台主界面主要由以下四个区域组成:
|
||||||
|
|
||||||
|
**1. 顶部区域**
|
||||||
|
|
||||||
|
位于页面最上方,包含:
|
||||||
|
|
||||||
|
- **平台标题**:点击可快速返回首页。
|
||||||
|
- **用户信息**:显示当前登录用户昵称及退出入口。
|
||||||
|
- **设置按钮**:点击可打开设置面板。
|
||||||
|
|
||||||
|
**2. 侧边菜单**
|
||||||
|
|
||||||
|
位于页面左侧,提供各功能模块的导航入口:
|
||||||
|
|
||||||
|
- **车站状态**:全线车站设备运行概览 (首页)。
|
||||||
|
- **设备诊断**:深入查看具体设备的详细运行参数。
|
||||||
|
- **设备告警**:
|
||||||
|
- **设备告警记录**:查询历史告警记录。
|
||||||
|
- **告警忽略管理**:管理忽略设备。
|
||||||
|
- **系统日志**:
|
||||||
|
- **视频平台日志**:审计视频平台操作。
|
||||||
|
- **上级调用日志**:审计上级调用指令。
|
||||||
|
- **权限管理**:用户与权限配置(仅超级管理员可见)。
|
||||||
|
|
||||||
|
**3. 底部状态栏**
|
||||||
|
|
||||||
|
位于页面最下方,主要用于 **未读告警提示**。当有新的设备告警产生时,此处的铃铛图标会显示红色未读数量徽标,点击可快速跳转至告警记录页面。
|
||||||
|
|
||||||
|
**4. 设置面板**
|
||||||
|
|
||||||
|
点击顶部区域的“设置”图标(⚙️)即可打开,包含:
|
||||||
|
|
||||||
|
- **通用设置**:
|
||||||
|
- **主题**:切换深色/浅色模式,适应不同光照环境。
|
||||||
|
- **布局**:折叠/展开左侧菜单;调整 **车站卡片矩阵** 的显示列数(1-10列)。
|
||||||
|
- **业务设置**:
|
||||||
|
- **告警策略**:配置告警截图保留天数等(详见第4章,仅限特定权限人员可见)。
|
||||||
|
|
||||||
|
### 1.4 权限与界面差异说明
|
||||||
|
|
||||||
|
本平台采用严格的权限控制机制,您的账号权限将直接决定您能看到的内容和能执行的操作。
|
||||||
|
|
||||||
|
**1. 查看权限**
|
||||||
|
|
||||||
|
- **影响范围**:决定您能看到哪些车站。
|
||||||
|
- **界面表现**:如果您没有某车站的查看权限,该车站在 **车站卡片矩阵**、设备树以及所有查询筛选器中都将 **完全不可见**。
|
||||||
|
|
||||||
|
**2. 操作权限**
|
||||||
|
|
||||||
|
- **影响范围**:决定您能否对设备进行配置、控制或管理。
|
||||||
|
- **界面表现**:如果您没有某车站的操作权限,相关的 **功能按钮**(如“设备配置”、“同步数据”、“告警确认”等)将 **自动隐藏** 或 **变为不可用状态**。
|
||||||
|
|
||||||
|
> **提示**:如果您发现找不到某个车站或缺少某个功能按钮,请联系管理员确认您是否拥有相应的权限。
|
||||||
|
|
||||||
|
## 2. 车站状态模块 (首页)
|
||||||
|
|
||||||
|
车站状态模块是用户登录后的默认页面,以 **车站卡片矩阵** 的形式展示全线各车站的设备整体运行状态。
|
||||||
|
|
||||||
|
### 2.1 车站卡片解读
|
||||||
|
|
||||||
|
每个卡片代表一个车站,直观展示该站的关键运行指标:
|
||||||
|
|
||||||
|
**1. 基础信息**
|
||||||
|
|
||||||
|
- **车站名称**:显示车站名称。
|
||||||
|
- **在线状态**:右上角的标签显示该车站服务器当前的在线/离线状态。
|
||||||
|
|
||||||
|
**2. 统计数据**
|
||||||
|
|
||||||
|
- **设备概览**:显示“设备总数”,以及通过颜色区分的“在线设备数”(绿色)和“离线设备数”(红色)。
|
||||||
|
- **告警概览**:显示“今日告警总数”,若有告警则以醒目颜色提示。
|
||||||
|
|
||||||
|
**3. 交互操作**
|
||||||
|
|
||||||
|
- **更多菜单**(点击卡片右上角的垂直三点图标):
|
||||||
|
- **视频平台**:跳转至该车站对应的第三方视频管理平台。
|
||||||
|
- **设备配置**:打开参数配置窗口。_(注:仅当拥有该车站“操作权限”且车站在线时显示)_
|
||||||
|
- **查看设备详情**:点击设备概览区域右侧的 **详情按钮**(水平三点图标),弹出 **设备详情窗口**,展示该车站的设备树结构(支持进一步跳转至诊断页面)。
|
||||||
|
- **查看告警详情**:点击告警概览区域右侧的 **详情按钮**(水平三点图标),弹出 **告警详情窗口**,展示该车站当日的告警列表。
|
||||||
|
|
||||||
|
### 2.2 操作栏
|
||||||
|
|
||||||
|
位于页面顶部的工具栏,提供对多个车站的批量管理功能。
|
||||||
|
|
||||||
|
**1. 功能按钮与权限**
|
||||||
|
|
||||||
|
- **导出设备状态 / 导出录像诊断**:_(需查看权限)_ 仅可选择您拥有“查看权限”的车站进行数据导出。
|
||||||
|
- **同步摄像机 / 同步录像机通道**:_(需操作权限)_ 仅可选择您拥有“操作权限”的车站进行数据同步。
|
||||||
|
|
||||||
|
**2. 操作流程**
|
||||||
|
|
||||||
|
1. 点击操作栏中的任一功能按钮(例如“同步摄像机”)。
|
||||||
|
2. 此时进入**多选模式**,所有车站卡片上会出现复选框。_(注:系统会自动根据当前操作所需的权限,禁用无权操作的车站复选框)_
|
||||||
|
3. 勾选您需要操作的车站,或勾选顶部的“全选”框。
|
||||||
|
4. 点击操作栏右侧出现的 **“确定”** 按钮执行操作:
|
||||||
|
- **对于导出类操作**:平台将弹出预览窗口,您可在确认数据无误后点击下载 Excel 文件。
|
||||||
|
- **对于同步类操作**:平台将直接在后台启动同步任务,任务完成后,页面右上角会弹出通知提示成功或失败的数量。
|
||||||
|
|
||||||
|
### 2.3 单站详情与配置
|
||||||
|
|
||||||
|
除了查看概览,您还可以对单个车站进行更细致的管理。
|
||||||
|
|
||||||
|
**1. 设备列表详情**
|
||||||
|
|
||||||
|
在设备详情窗口中,您可以浏览该车站下的所有设备。点击任意设备节点,可直接跳转至 **设备诊断页面** 查看该设备的详细指标。
|
||||||
|
|
||||||
|
**2. 参数配置** _(需操作权限)_
|
||||||
|
|
||||||
|
通过右上角菜单进入“设备配置”,您可以:
|
||||||
|
|
||||||
|
- **阈值配置**:设置交换机、服务器、录像机等设备的运行指标告警阈值(如 CPU 占用率、温度上限等)。
|
||||||
|
- **计划任务**:设置显示器等设备的自动亮屏和息屏计划。
|
||||||
|
|
||||||
|
## 3. 设备诊断模块
|
||||||
|
|
||||||
|
设备诊断模块用于查看和分析具体设备的详细运行状态。您可以通过侧边菜单的 **“设备诊断”** 进入,或从 **车站状态模块** 的设备详情、告警记录页面跳转进入(告警跳转详见第4章)。
|
||||||
|
|
||||||
|
### 3.1 设备树 (侧边栏)
|
||||||
|
|
||||||
|
位于页面左侧,以树形结构展示全线所有车站及其下属设备。
|
||||||
|
|
||||||
|
**1. 搜索与筛选**
|
||||||
|
|
||||||
|
- **搜索框**:输入设备名称、设备 ID 或 IP 地址,可实时过滤设备树节点。
|
||||||
|
- **状态筛选**:点击单选框,可快速筛选 **“全部 / 在线 / 离线”** 设备。
|
||||||
|
|
||||||
|
**2. 设备树交互**
|
||||||
|
|
||||||
|
- **展开与选择**:按 **“车站 -> 设备类型 -> 具体设备”** 的层级展开,点击设备节点即可在右侧查看详情。
|
||||||
|
- **右键菜单管理** _(需操作权限)_:
|
||||||
|
- **在车站节点上右键**:
|
||||||
|
- **导入设备**:批量导入设备数据。
|
||||||
|
- **导出设备**:导出该车站的设备清单。
|
||||||
|
- **在设备节点上右键**:
|
||||||
|
- **删除设备**:将该设备从平台中移除。
|
||||||
|
|
||||||
|
### 3.2 诊断详情页结构
|
||||||
|
|
||||||
|
选中设备后,右侧区域将展示该设备的详细信息。
|
||||||
|
|
||||||
|
**1. 功能标签页**
|
||||||
|
|
||||||
|
- **当前诊断**:展示设备实时的运行数据(默认视图)。头部信息栏和详细诊断指标均包含在此标签页中。
|
||||||
|
- **历史诊断**:以图表形式展示设备的历史健康度或关键指标变化趋势。
|
||||||
|
- **修改设备**:_(需操作权限)_ 编辑设备的基础信息(如名称、安装位置等)。
|
||||||
|
|
||||||
|
**2. 头部信息栏 (位于“当前诊断”标签页顶部)**
|
||||||
|
|
||||||
|
- **基础状态**:展示设备名称、IP 地址、在线/离线状态。
|
||||||
|
- **关联操作**:
|
||||||
|
- **上游设备跳转**:如果设备有上游节点(如摄像机连接的交换机),点击可直接跳转查看上游设备。
|
||||||
|
- **管理入口**:点击 **“管理”** 按钮,可直接打开该设备自身的 Web 管理后台(如摄像机的 Web 配置页面)。
|
||||||
|
|
||||||
|
### 3.3 下游设备配置 (需操作权限)
|
||||||
|
|
||||||
|
对于交换机和智能安防箱,平台支持将其端口或电路与下游设备(如摄像机)进行关联,以便建立网络拓扑关系。
|
||||||
|
|
||||||
|
- **交换机配置**:在“当前诊断”的端口列表中,**右键点击**任意端口,选择 **“关联设备”**,可将该端口与下游设备绑定;若需解绑,右键选择 **“解除关联”** 即可。
|
||||||
|
- **智能安防箱配置**:在电路列表中,**右键点击**电路卡片,选择 **“关联设备”** 或 **“解除关联”** 进行配置。
|
||||||
|
|
||||||
|
### 3.4 设备诊断指标概览
|
||||||
|
|
||||||
|
根据设备类型的不同,“当前诊断”标签页将展示不同的关键指标。以下列举几种典型设备的展示重点:
|
||||||
|
|
||||||
|
- **交换机 / 服务器 / 录像机**:侧重硬件资源监控,展示 CPU、内存、磁盘等使用率仪表盘,以及端口流量或通道录像状态。
|
||||||
|
- **摄像机 / 报警主机**:侧重基础配置展示,如制造商、固件版本、序列号、ONVIF/GB28181 协议配置等。
|
||||||
|
- **解码器 / 智能安防箱**:兼具硬件状态(如温度、风扇转速)与业务配置(如通道状态)的展示。
|
||||||
|
|
||||||
|
## 4. 设备告警模块
|
||||||
|
|
||||||
|
设备告警模块是运维人员处理设备异常的核心区域,包含告警记录查询、确认、忽略以及策略配置等功能。
|
||||||
|
|
||||||
|
### 4.1 设备告警记录
|
||||||
|
|
||||||
|
通过侧边菜单选择 **“设备告警” -> “设备告警记录”** 进入。
|
||||||
|
|
||||||
|
**1. 筛选查询**
|
||||||
|
顶部查询栏提供多维度的组合筛选功能,帮助您快速定位特定告警:
|
||||||
|
|
||||||
|
- **车站**:选择查询范围(仅显示您拥有“查看权限”的车站)。
|
||||||
|
- **设备类型/名称**:按设备属性精确查找。
|
||||||
|
- **告警属性**:按告警类型、级别(严重/紧急/一般等)、恢复状态(已恢复/未恢复)及确认状态进行筛选。
|
||||||
|
- **时间范围**:选择告警发生的起止时间。
|
||||||
|
|
||||||
|
**2. 列表操作**
|
||||||
|
|
||||||
|
- **告警确认** _(需操作权限)_:对于“未确认”的告警,点击操作列的 **“确认”** 按钮进行处理。确认后,系统将记录确认人及时间,且该按钮变为不可点击状态。
|
||||||
|
- **忽略设备** _(需操作权限)_:若某设备频繁误报,点击 **“忽略设备”** 将其加入忽略名单。被忽略的设备将不再产生新告警,直到被手动恢复。
|
||||||
|
- **跳转诊断**:点击列表中的设备名称,可直接跳转至该设备的诊断详情页。
|
||||||
|
|
||||||
|
**3. 数据导出**
|
||||||
|
|
||||||
|
点击页面右上角的 **“导出”** 按钮,可将当前筛选条件下的所有告警记录导出为 Excel 报表,便于线下归档与分析。
|
||||||
|
|
||||||
|
### 4.2 告警忽略管理
|
||||||
|
|
||||||
|
通过侧边菜单选择 **“设备告警” -> “告警忽略管理”** 进入。
|
||||||
|
|
||||||
|
**1. 列表查看**
|
||||||
|
|
||||||
|
展示所有当前处于“忽略状态”的设备列表,包含忽略时间、所属车站及设备信息。
|
||||||
|
|
||||||
|
**2. 取消忽略 (需操作权限)**
|
||||||
|
|
||||||
|
若需恢复对某设备的监控,点击操作列的 **“取消忽略”** 按钮。恢复后,该设备产生的新异常将重新触发告警。
|
||||||
|
|
||||||
|
### 4.3 告警策略配置 (系统设置)
|
||||||
|
|
||||||
|
部分高级告警策略需在全局设置中进行配置。
|
||||||
|
|
||||||
|
**1. 入口与权限**
|
||||||
|
|
||||||
|
- **入口**:点击顶部区域的“设置”图标,在设置面板中找到“告警”板块。
|
||||||
|
- **权限要求**:此板块仅对 **OCC (控制中心) 车站** 的操作员可见。
|
||||||
|
|
||||||
|
**2. 配置项**
|
||||||
|
|
||||||
|
- **告警画面截图保留天数**:设置系统自动抓取的告警截图的存储期限(1-15天)。过期截图将被自动清理以释放存储空间。
|
||||||
|
- **自动获取告警画面截图**:开启/关闭告警触发时的自动截图功能。
|
||||||
|
|
||||||
|
## 5. 系统日志模块
|
||||||
|
|
||||||
|
系统日志模块用于审计和追踪系统内的关键操作记录。
|
||||||
|
|
||||||
|
### 5.1 视频平台日志
|
||||||
|
|
||||||
|
通过侧边菜单选择 **“系统日志” -> “视频平台日志”** 进入。此模块主要记录平台与第三方视频管理系统之间的交互操作。
|
||||||
|
|
||||||
|
**1. 查询功能**
|
||||||
|
|
||||||
|
- **车站筛选**:仅显示您拥有“查看权限”的车站日志。
|
||||||
|
- **操作类型**:筛选具体的交互指令类型(如“获取流地址”、“云台控制”等)。
|
||||||
|
- **时间范围**:按发生时间段进行精确检索。
|
||||||
|
|
||||||
|
**2. 数据导出**
|
||||||
|
|
||||||
|
支持将查询结果导出为 Excel 文件,用于故障排查或安全审计。
|
||||||
|
|
||||||
|
### 5.2 上级调用日志
|
||||||
|
|
||||||
|
通过侧边菜单选择 **“系统日志” -> “上级调用日志”** 进入。此模块记录上级系统(如公安或市级平台)调用本平台资源的请求记录。
|
||||||
|
|
||||||
|
**1. 查询功能**
|
||||||
|
|
||||||
|
- **车站筛选**:选择日志所属的车站范围。
|
||||||
|
- **调用类型**:筛选具体的调用指令(如“视频点播”、“录像回放”等)。
|
||||||
|
- **时间范围**:选择日志记录的时间段。
|
||||||
|
|
||||||
|
**2. 数据导出**
|
||||||
|
|
||||||
|
同样支持将查询结果导出,以便统计上级单位的调用频率和资源使用情况。
|
||||||
|
|
||||||
|
## 6. 权限管理模块
|
||||||
|
|
||||||
|
**(注:本模块仅对“超级管理员”可见,普通用户无法访问)**
|
||||||
|
|
||||||
|
权限管理模块用于配置系统用户及其对各车站的访问和操作权限。
|
||||||
|
|
||||||
|
### 6.1 用户列表管理
|
||||||
|
|
||||||
|
通过侧边菜单选择 **“权限管理”** 进入。
|
||||||
|
|
||||||
|
**1. 用户查找**
|
||||||
|
|
||||||
|
- **搜索**:在顶部搜索框输入 **用户名**,可快速定位目标用户。
|
||||||
|
- **重置**:点击重置按钮清空搜索条件,显示所有用户。
|
||||||
|
|
||||||
|
**2. 列表信息**
|
||||||
|
|
||||||
|
展示系统内所有注册用户的基本信息,包括账号、姓名等。
|
||||||
|
|
||||||
|
### 6.2 权限配置
|
||||||
|
|
||||||
|
点击用户列表右侧的 **“配置权限”** 按钮,进入该用户的权限分配界面。界面以矩阵形式展示,直观易懂。
|
||||||
|
|
||||||
|
**1. 权限矩阵解读**
|
||||||
|
|
||||||
|
- **行(车站)**:每一行代表一条线路上的具体车站。
|
||||||
|
- **列(权限类型)**:
|
||||||
|
- **查看**:决定用户能否看到该车站的数据。
|
||||||
|
- **操作**:决定用户能否对该车站设备进行控制或修改配置。
|
||||||
|
|
||||||
|
**2. 批量操作技巧**
|
||||||
|
|
||||||
|
- **全选/反选**:点击表头的复选框,可一键授予或取消所有车站的某项权限。
|
||||||
|
- **单站全权**:点击某一行车站名称前的复选框(如有),可一键授予该车站的所有权限。
|
||||||
|
|
||||||
|
**3. 特殊规则说明**
|
||||||
|
|
||||||
|
**重要提示**:如果未给某用户勾选任何权限(即所有复选框均为空),平台默认该用户拥有 **超级管理员权限**,可访问所有车站并执行所有操作。请务必谨慎配置。
|
||||||
185
.trae/documents/网络设备管理平台用户手册编写计划.md
Normal file
185
.trae/documents/网络设备管理平台用户手册编写计划.md
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
# 《网络设备管理平台用户手册》编写计划
|
||||||
|
|
||||||
|
本计划已根据权限系统的实际逻辑(VIEW/OPERATION)进行了调整,将在各章节中明确体现权限对界面和功能的影响。
|
||||||
|
|
||||||
|
## 阶段一:准备与入门
|
||||||
|
|
||||||
|
1. **平台简介**:平台用途与设备支持范围。
|
||||||
|
2. **登录与注销**:账号登录流程、退出操作。
|
||||||
|
3. **界面概览**:
|
||||||
|
|
||||||
|
* **顶部区域 (Header)**:展示平台标题、当前用户信息及设置入口。
|
||||||
|
|
||||||
|
* **侧边菜单 (Sider)**:功能模块导航。
|
||||||
|
|
||||||
|
* **底部状态栏 (Footer)**:未读告警通知提示。
|
||||||
|
|
||||||
|
* **设置面板**:
|
||||||
|
|
||||||
|
* **通用设置**:主题切换(深色模式)、布局调整(菜单折叠、车站列数)。
|
||||||
|
|
||||||
|
* **业务设置**:告警策略配置(详见阶段四)。
|
||||||
|
4. **权限与界面差异说明(新增)**:
|
||||||
|
|
||||||
|
* 解释“查看权限”如何决定车站列表的显示(无权限则不显示)。
|
||||||
|
|
||||||
|
* 解释“操作权限”如何决定功能按钮的显隐(无权限则隐藏配置入口)。
|
||||||
|
|
||||||
|
## 阶段二:首页 - 车站状态监控
|
||||||
|
|
||||||
|
**涉及模块**:`Station Status` (首页)
|
||||||
|
|
||||||
|
1. **车站卡片解读**
|
||||||
|
|
||||||
|
* **基础信息**:车站名称、在线/离线状态。
|
||||||
|
|
||||||
|
* **统计数据**:设备总数/在线数/离线数、今日告警总数。
|
||||||
|
|
||||||
|
* **交互操作**:
|
||||||
|
|
||||||
|
* **右上角更多菜单**:
|
||||||
|
|
||||||
|
* **视频平台**:跳转至视频管理平台。
|
||||||
|
|
||||||
|
* **设备配置**:打开参数配置窗口 **(注:仅当拥有该车站“操作权限”且车站在线时显示)**。
|
||||||
|
|
||||||
|
* **设备详情入口**:点击“设备总数”区域,弹出设备树模态框(可进一步跳转诊断页)。
|
||||||
|
|
||||||
|
* **告警详情入口**:点击“告警总数”区域,弹出告警列表模态框。
|
||||||
|
2. **操作栏 (Batch Actions)**
|
||||||
|
|
||||||
|
* **功能按钮与权限差异**:
|
||||||
|
|
||||||
|
* **导出设备状态 / 导出录像诊断**:**(需查看权限)** 仅可选择拥有“查看权限”的车站进行导出。
|
||||||
|
|
||||||
|
* **同步摄像机 / 同步录像机通道**:**(需操作权限)** 仅可选择拥有“操作权限”的车站进行同步。
|
||||||
|
|
||||||
|
\*- **操作流程**:
|
||||||
|
|
||||||
|
1. 点击操作栏中的任一功能按钮(如“同步摄像机”)。
|
||||||
|
2. 此时车站卡片上会出现复选框,且仅有权限的车站可选。
|
||||||
|
3. 勾选需要操作的车站(支持“全选”)。
|
||||||
|
4. 点击操作栏右侧出现的“确定”按钮:
|
||||||
|
|
||||||
|
* **导出类操作**:系统将弹出预览窗口,您可在窗口中确认数据并下载 Excel 文件。
|
||||||
|
|
||||||
|
* **同步类操作**:系统将直接在后台启动同步任务,任务完成后右上角会弹出结果通知(成功/失败数量)。
|
||||||
|
3. **单站详情与配置**
|
||||||
|
|
||||||
|
* **设备列表详情**:展示设备树,点击可跳转诊断页。
|
||||||
|
|
||||||
|
* **告警列表详情**。
|
||||||
|
|
||||||
|
* **参数配置**:配置阈值与计划任务 **(需操作权限)**。
|
||||||
|
|
||||||
|
## 阶段三:设备诊断模块
|
||||||
|
|
||||||
|
**涉及模块**:`Device Diagnosis`
|
||||||
|
|
||||||
|
1. **设备树(侧边栏)**
|
||||||
|
|
||||||
|
* **数据范围**:说明仅展示拥有“查看权限”的车站及其下属设备。
|
||||||
|
|
||||||
|
* **搜索与筛选**:按名称/IP搜索,按状态筛选。
|
||||||
|
|
||||||
|
* **树形交互**:
|
||||||
|
|
||||||
|
* **展开与选择**:按“车站 -> 设备类型 -> 具体设备”层级展开与选择。
|
||||||
|
|
||||||
|
* **右键菜单管理**:**(需操作权限)**
|
||||||
|
|
||||||
|
* **车站节点右键**:支持 **导入设备**、**导出设备**。
|
||||||
|
|
||||||
|
* **设备节点右键**:支持 **删除设备**。
|
||||||
|
2. **诊断详情页结构**
|
||||||
|
|
||||||
|
* **头部信息栏**:状态、IP、关联跳转、Web管理入口。
|
||||||
|
|
||||||
|
* **功能标签页**:
|
||||||
|
|
||||||
|
* **当前诊断**:实时数据展示。
|
||||||
|
|
||||||
|
* **历史诊断**:历史趋势查看。
|
||||||
|
|
||||||
|
* **修改设备**:编辑设备信息 **(注:仅对拥有“操作权限”的用户可见)**。
|
||||||
|
3. **特定设备诊断指标**
|
||||||
|
- **分类描述策略**:
|
||||||
|
- **性能类设备**(如交换机、服务器、NVR):侧重硬件监控,展示CPU/内存使用率仪表盘、端口状态、磁盘健康度等。
|
||||||
|
- **(补充) 下游设备关联配置**:
|
||||||
|
- **交换机**:右键点击端口 -> 关联设备(如摄像机)。
|
||||||
|
- **安防箱**:右键点击电路 -> 关联设备。
|
||||||
|
- **操作权限要求**:**(需操作权限)**。
|
||||||
|
- **信息类设备**(如摄像机、报警主机):侧重参数展示,展示制造商、固件版本、协议配置等静态属性。
|
||||||
|
|
||||||
|
## 阶段四:告警管理模块
|
||||||
|
|
||||||
|
**涉及模块**:`Alarm`
|
||||||
|
|
||||||
|
1. **子模块:告警记录 (Alarm Log)**
|
||||||
|
|
||||||
|
* **筛选查询**:
|
||||||
|
|
||||||
|
* 组合筛选:车站(受查看权限限制)、设备类型、设备名称、告警类型、级别、状态、时间。
|
||||||
|
|
||||||
|
* **列表操作**:
|
||||||
|
|
||||||
|
* **告警确认**:点击“确认”按钮处理告警 **(注:需拥有该车站的操作权限,否则按钮禁用/隐藏)**。
|
||||||
|
|
||||||
|
* **忽略设备**:将设备加入忽略列表 **(需操作权限)**。
|
||||||
|
|
||||||
|
* **数据导出**:导出Excel报表。
|
||||||
|
|
||||||
|
2. **子模块:告警忽略 (Alarm Ignore)**
|
||||||
|
|
||||||
|
* **列表查看**:查看被忽略的设备记录。
|
||||||
|
|
||||||
|
* **取消忽略**:点击恢复监控 **(注:需拥有操作权限)**。
|
||||||
|
|
||||||
|
3. **告警策略配置 (系统设置)**
|
||||||
|
|
||||||
|
* **配置入口**:通过顶部导航栏“设置”按钮进入。
|
||||||
|
|
||||||
|
* **配置项**:告警画面截图保留天数、自动获取截图开关。
|
||||||
|
|
||||||
|
* **权限要求**:**(注:仅对 OCC 车站的操作员可见)**。
|
||||||
|
|
||||||
|
## 阶段五:日志管理模块
|
||||||
|
|
||||||
|
**涉及模块**:`Log`
|
||||||
|
|
||||||
|
1. **子模块:上级调用日志 (Call Log)**
|
||||||
|
|
||||||
|
* **查询**:筛选车站(受查看权限限制)、日志类型(如视频点播、云台指令)、时间范围。
|
||||||
|
|
||||||
|
* **导出**:导出查询结果。
|
||||||
|
|
||||||
|
2. **子模块:视频平台日志 (Vimp Log)**
|
||||||
|
|
||||||
|
* **查询**:筛选车站(受查看权限限制)、内部操作类型、时间范围。
|
||||||
|
|
||||||
|
* **导出**:导出查询结果。
|
||||||
|
|
||||||
|
## 阶段六:权限管理模块
|
||||||
|
|
||||||
|
**涉及模块**:`Permission`
|
||||||
|
|
||||||
|
**(注:本模块仅对“超级管理员”可见,普通用户无法访问)**
|
||||||
|
|
||||||
|
1. **用户列表管理**
|
||||||
|
|
||||||
|
* **搜索**:通过真实姓名查找用户。
|
||||||
|
|
||||||
|
* **重置**:清空搜索条件。
|
||||||
|
|
||||||
|
2. **权限配置 (Permission Config)**
|
||||||
|
|
||||||
|
* **矩阵式界面**:
|
||||||
|
|
||||||
|
* **行**:对应各个车站。
|
||||||
|
|
||||||
|
* **列**:对应具体权限类型(如查看、操作)。
|
||||||
|
|
||||||
|
* **批量操作**:支持整行(单站全权)或整列(全站某权)一键勾选。
|
||||||
|
|
||||||
|
* **特殊规则说明**:若未给用户勾选任何权限,平台默认该用户拥有**所有权限**(超级管理员模式)。
|
||||||
|
|
||||||
16
README.md
16
README.md
@@ -39,19 +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`。
|
||||||
|
|
||||||
## 调试模式
|
|
||||||
|
|
||||||
在调试模式中,用户可以查看设备的原始诊断数据,也可以对轮询器进行控制,或者启用离线开发模式,系统不会自动调用一些主动触发的请求。
|
|
||||||
|
|
||||||
### 开启调试模式
|
|
||||||
|
|
||||||
在非登录页的任意页面中,使用键盘组合键 `Ctrl+Alt+D`,系统会弹出一个输入框,输入环境变量 `.env` 中的 `VITE_DEBUG_CODE` 对应的值即可开启调试模式,如需关闭调试模式,再次使用上述组合键并点击 `确认` 按钮即可。
|
|
||||||
|
|
||||||
注意调试模式与其内部的功能之间没有联动关系,例如在开启调试模式后可以关闭轮询或者启用离线开发模式,但是在关闭调试模式后,轮询不会重新被开启,离线开发模式也不会被关闭,因此在关闭离线开发模式前,请务必确保系统处于正确的运行状态下。
|
|
||||||
|
|
||||||
### 关于离线开发模式
|
|
||||||
|
|
||||||
由于离线开发模式涉及到登录操作,因此项目中将离线开发模式暴露到了全局变量 `window.$offlineDev` 中,允许在登录页中直接开启离线开发模式。
|
|
||||||
|
|
||||||
如果你第一次启动这个项目,系统在正常情况下会先跳转至登录页,此时如果希望开启离线模式,可以直接打开浏览器的开发者工具,在控制台输入 `window.$offlineDev.value = true` 即可,系统会直接跳转到首页。
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"es-toolkit": "^1.41.0",
|
"es-toolkit": "^1.41.0",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
|
"lucide-vue-next": "^0.562.0",
|
||||||
"naive-ui": "^2.43.1",
|
"naive-ui": "^2.43.1",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"pinia-plugin-persistedstate": "^4.7.1",
|
"pinia-plugin-persistedstate": "^4.7.1",
|
||||||
@@ -40,8 +41,6 @@
|
|||||||
"@tsconfig/node22": "^22.0.2",
|
"@tsconfig/node22": "^22.0.2",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/node": "^22.18.11",
|
"@types/node": "^22.18.11",
|
||||||
"@vicons/antd": "^0.13.0",
|
|
||||||
"@vicons/ionicons5": "^0.13.0",
|
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vitejs/plugin-vue-jsx": "^5.1.1",
|
"@vitejs/plugin-vue-jsx": "^5.1.1",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
@@ -58,5 +57,5 @@
|
|||||||
"vite-plugin-vue-devtools": "^8.0.3",
|
"vite-plugin-vue-devtools": "^8.0.3",
|
||||||
"vue-tsc": "^3.1.3"
|
"vue-tsc": "^3.1.3"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd"
|
"packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48"
|
||||||
}
|
}
|
||||||
|
|||||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -44,6 +44,9 @@ importers:
|
|||||||
localforage:
|
localforage:
|
||||||
specifier: ^1.10.0
|
specifier: ^1.10.0
|
||||||
version: 1.10.0
|
version: 1.10.0
|
||||||
|
lucide-vue-next:
|
||||||
|
specifier: ^0.562.0
|
||||||
|
version: 0.562.0(vue@3.5.24(typescript@5.9.3))
|
||||||
naive-ui:
|
naive-ui:
|
||||||
specifier: ^2.43.1
|
specifier: ^2.43.1
|
||||||
version: 2.43.1(vue@3.5.24(typescript@5.9.3))
|
version: 2.43.1(vue@3.5.24(typescript@5.9.3))
|
||||||
@@ -75,12 +78,6 @@ importers:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.18.11
|
specifier: ^22.18.11
|
||||||
version: 22.19.1
|
version: 22.19.1
|
||||||
'@vicons/antd':
|
|
||||||
specifier: ^0.13.0
|
|
||||||
version: 0.13.0
|
|
||||||
'@vicons/ionicons5':
|
|
||||||
specifier: ^0.13.0
|
|
||||||
version: 0.13.0
|
|
||||||
'@vitejs/plugin-vue':
|
'@vitejs/plugin-vue':
|
||||||
specifier: ^6.0.1
|
specifier: ^6.0.1
|
||||||
version: 6.0.1(vite@7.2.2(@types/node@22.19.1)(jiti@2.6.1)(sass@1.94.0)(tsx@4.20.6))(vue@3.5.24(typescript@5.9.3))
|
version: 6.0.1(vite@7.2.2(@types/node@22.19.1)(jiti@2.6.1)(sass@1.94.0)(tsx@4.20.6))(vue@3.5.24(typescript@5.9.3))
|
||||||
@@ -845,12 +842,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==}
|
resolution: {integrity: sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@vicons/antd@0.13.0':
|
|
||||||
resolution: {integrity: sha512-yrUGoUSz2BbGupk9ghQOahc04n5H3MwUDM9pVPsLh9U1uqB47oRWZvYRiZaT1JKPqgTgSE6BXcVw4i9MOF4M+g==}
|
|
||||||
|
|
||||||
'@vicons/ionicons5@0.13.0':
|
|
||||||
resolution: {integrity: sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ==}
|
|
||||||
|
|
||||||
'@vitejs/plugin-vue-jsx@5.1.1':
|
'@vitejs/plugin-vue-jsx@5.1.1':
|
||||||
resolution: {integrity: sha512-uQkfxzlF8SGHJJVH966lFTdjM/lGcwJGzwAHpVqAPDD/QcsqoUGa+q31ox1BrUfi+FLP2ChVp7uLXE3DkHyDdQ==}
|
resolution: {integrity: sha512-uQkfxzlF8SGHJJVH966lFTdjM/lGcwJGzwAHpVqAPDD/QcsqoUGa+q31ox1BrUfi+FLP2ChVp7uLXE3DkHyDdQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -1636,6 +1627,11 @@ packages:
|
|||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
|
|
||||||
|
lucide-vue-next@0.562.0:
|
||||||
|
resolution: {integrity: sha512-LN0BLGKMFulv0lnfK29r14DcngRUhIqdcaL0zXTt2o0oS9odlrjCGaU3/X9hIihOjjN8l8e+Y9G/famcNYaI7Q==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: '>=3.0.1'
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
@@ -2871,10 +2867,6 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.46.4
|
'@typescript-eslint/types': 8.46.4
|
||||||
eslint-visitor-keys: 4.2.1
|
eslint-visitor-keys: 4.2.1
|
||||||
|
|
||||||
'@vicons/antd@0.13.0': {}
|
|
||||||
|
|
||||||
'@vicons/ionicons5@0.13.0': {}
|
|
||||||
|
|
||||||
'@vitejs/plugin-vue-jsx@5.1.1(vite@7.2.2(@types/node@22.19.1)(jiti@2.6.1)(sass@1.94.0)(tsx@4.20.6))(vue@3.5.24(typescript@5.9.3))':
|
'@vitejs/plugin-vue-jsx@5.1.1(vite@7.2.2(@types/node@22.19.1)(jiti@2.6.1)(sass@1.94.0)(tsx@4.20.6))(vue@3.5.24(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
@@ -3701,6 +3693,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
yallist: 3.1.1
|
||||||
|
|
||||||
|
lucide-vue-next@0.562.0(vue@3.5.24(typescript@5.9.3)):
|
||||||
|
dependencies:
|
||||||
|
vue: 3.5.24(typescript@5.9.3)
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import { dateZhCN, NConfigProvider, NDialogProvider, NLoadingBarProvider, NMessa
|
|||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { themeMode, offlineDev } = storeToRefs(settingStore);
|
const { themeMode, mockUser } = storeToRefs(settingStore);
|
||||||
|
|
||||||
// 允许通过控制台启用离线开发模式 (登录页适用)
|
// 允许通过控制台启用离线开发模式 (登录页适用)
|
||||||
window.$offlineDev = offlineDev;
|
window.$mockUser = mockUser;
|
||||||
|
|
||||||
useVersionCheckQuery();
|
useVersionCheckQuery();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ export interface Station {
|
|||||||
name: string;
|
name: string;
|
||||||
online: boolean;
|
online: boolean;
|
||||||
ip: string;
|
ip: string;
|
||||||
|
occ?: boolean; // 是否为控制中心
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/apis/model/base/base-employee.ts
Normal file
21
src/apis/model/base/base-employee.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { BaseModel, ReduceForPageQuery, ReduceForSaveVO, ReduceForUpdateVO } from '@/apis';
|
||||||
|
import type { Nullable, Optional } from '@/types';
|
||||||
|
|
||||||
|
export interface BaseEmployee extends BaseModel {
|
||||||
|
userId: string;
|
||||||
|
realName: string;
|
||||||
|
defUser: Nullable<
|
||||||
|
{
|
||||||
|
username: string;
|
||||||
|
nickName: string;
|
||||||
|
} & BaseModel
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BaseEmployeeResultVO = Nullable<BaseEmployee>;
|
||||||
|
|
||||||
|
export type BaseEmployeeSaveVO = Partial<Omit<BaseEmployee, ReduceForSaveVO>>;
|
||||||
|
|
||||||
|
export type BaseEmployeeUpdateVO = Optional<Omit<BaseEmployee, ReduceForUpdateVO>>;
|
||||||
|
|
||||||
|
export type BaseEmployeePageQuery = Partial<Omit<BaseEmployee, ReduceForPageQuery>>;
|
||||||
@@ -1,3 +1 @@
|
|||||||
export * from './model';
|
export * from './base-employee';
|
||||||
export * from './page';
|
|
||||||
export * from './reduce';
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Nullable, Optional } from '@/types';
|
import type { Nullable, Optional } from '@/types';
|
||||||
import type { ReduceForPageQuery, ReduceForSaveVO, ReduceForUpdateVO } from '../../base';
|
import type { ReduceForPageQuery, ReduceForSaveVO, ReduceForUpdateVO } from '../../schema';
|
||||||
import type { NdmAlarmHost } from './alarm';
|
import type { NdmAlarmHost } from './alarm';
|
||||||
import type { NdmSecurityBox, NdmSwitch } from './other';
|
import type { NdmSecurityBox, NdmSwitch } from './other';
|
||||||
import type { NdmNvr } from './storage';
|
import type { NdmNvr } from './storage';
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
|
export * from './ndm-permission';
|
||||||
export * from './ndm-security-box';
|
export * from './ndm-security-box';
|
||||||
export * from './ndm-switch';
|
export * from './ndm-switch';
|
||||||
|
|||||||
34
src/apis/model/biz/entity/other/ndm-permission.ts
Normal file
34
src/apis/model/biz/entity/other/ndm-permission.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { BaseModel, ReduceForPageQuery, ReduceForSaveVO, ReduceForUpdateVO, Station } from '@/apis';
|
||||||
|
import type { PermissionType } from '@/enums';
|
||||||
|
import type { Nullable, Optional } from '@/types';
|
||||||
|
|
||||||
|
export interface NdmPermission extends BaseModel {
|
||||||
|
/**
|
||||||
|
* 员工ID
|
||||||
|
*/
|
||||||
|
employeeId: string;
|
||||||
|
/**
|
||||||
|
* 服务器IP地址
|
||||||
|
*/
|
||||||
|
ipAddress: string;
|
||||||
|
/**
|
||||||
|
* 站号
|
||||||
|
*/
|
||||||
|
stationCode: Station['code'];
|
||||||
|
/**
|
||||||
|
* 站名
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* 权限类型
|
||||||
|
*/
|
||||||
|
type: PermissionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NdmPermissionResultVO = Nullable<NdmPermission>;
|
||||||
|
|
||||||
|
export type NdmPermissionSaveVO = Partial<Omit<NdmPermission, ReduceForSaveVO>>;
|
||||||
|
|
||||||
|
export type NdmPermissionUpdateVO = Optional<Omit<NdmPermission, ReduceForUpdateVO>>;
|
||||||
|
|
||||||
|
export type NdmPermissionPageQuery = Partial<Omit<NdmPermission, ReduceForPageQuery>>;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './base';
|
export * from './base';
|
||||||
export * from './biz';
|
export * from './biz';
|
||||||
export * from './common';
|
export * from './common';
|
||||||
|
export * from './schema';
|
||||||
export * from './system';
|
export * from './system';
|
||||||
|
|||||||
3
src/apis/model/schema/index.ts
Normal file
3
src/apis/model/schema/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './model';
|
||||||
|
export * from './page';
|
||||||
|
export * from './reduce';
|
||||||
21
src/apis/request/base/base-employee.ts
Normal file
21
src/apis/request/base/base-employee.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { BaseEmployeePageQuery, BaseEmployeeResultVO, PageParams, PageResult } from '@/apis';
|
||||||
|
import { userClient } from '@/apis/client';
|
||||||
|
import { unwrapResponse } from '@/utils';
|
||||||
|
|
||||||
|
export const pageBaseEmployeeApi = async (pageQuery: PageParams<BaseEmployeePageQuery>, options?: { signal?: AbortSignal }) => {
|
||||||
|
const { signal } = options ?? {};
|
||||||
|
const client = userClient;
|
||||||
|
const endpoint = '/api/base/baseEmployee/page';
|
||||||
|
const resp = await client.post<PageResult<BaseEmployeeResultVO>>(endpoint, pageQuery, { signal });
|
||||||
|
const data = unwrapResponse(resp);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const detailBaseEmployeeApi = async (id: string, options?: { signal?: AbortSignal }) => {
|
||||||
|
const { signal } = options ?? {};
|
||||||
|
const client = userClient;
|
||||||
|
const endpoint = `/api/base/baseEmployee/detail`;
|
||||||
|
const resp = await client.get<BaseEmployeeResultVO>(endpoint, { params: { id }, signal });
|
||||||
|
const data = unwrapResponse(resp);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
1
src/apis/request/base/index.ts
Normal file
1
src/apis/request/base/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './base-employee';
|
||||||
@@ -48,3 +48,13 @@ export const reloadAllRecordCheckApi = async (dayOffset: number, options?: { sta
|
|||||||
if (!data) throw new Error(`${data}`);
|
if (!data) throw new Error(`${data}`);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const batchExportRecordCheckApi = async (params: { checkDuration: number; gapSeconds: number; stationCode: Station['code'][] }, options?: { signal?: AbortSignal }) => {
|
||||||
|
const { signal } = options ?? {};
|
||||||
|
const { checkDuration, gapSeconds, stationCode } = params;
|
||||||
|
const client = userClient;
|
||||||
|
const endpoint = `/api/ndm/ndmRecordCheck/batchExportByTemplate`;
|
||||||
|
const resp = await client.post<Blob>(endpoint, { checkDuration, gapSeconds, stationCode }, { responseType: 'blob', retRaw: true, signal });
|
||||||
|
const data = unwrapResponse(resp);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from './ndm-permission';
|
||||||
export * from './ndm-security-box';
|
export * from './ndm-security-box';
|
||||||
export * from './ndm-service-available';
|
export * from './ndm-service-available';
|
||||||
export * from './ndm-switch';
|
export * from './ndm-switch';
|
||||||
|
|||||||
83
src/apis/request/biz/other/ndm-permission.ts
Normal file
83
src/apis/request/biz/other/ndm-permission.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import {
|
||||||
|
ndmClient,
|
||||||
|
userClient,
|
||||||
|
type NdmPermissionPageQuery,
|
||||||
|
type NdmPermissionResultVO,
|
||||||
|
type NdmPermissionSaveVO,
|
||||||
|
type NdmPermissionUpdateVO,
|
||||||
|
type PageParams,
|
||||||
|
type PageResult,
|
||||||
|
type Station,
|
||||||
|
} from '@/apis';
|
||||||
|
import type { PermissionTypeEnum } from '@/enums';
|
||||||
|
import { unwrapResponse } from '@/utils';
|
||||||
|
|
||||||
|
export const permissionTypesApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
|
||||||
|
const { stationCode, signal } = options ?? {};
|
||||||
|
const client = stationCode ? ndmClient : userClient;
|
||||||
|
const prefix = stationCode ? `/${stationCode}` : '';
|
||||||
|
const endpoint = `${prefix}/api/ndm/ndmPermission/types`;
|
||||||
|
const resp = await client.get<PermissionTypeEnum>(endpoint, { signal });
|
||||||
|
const data = unwrapResponse(resp);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pagePermissionApi = async (pageQuery: PageParams<NdmPermissionPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
|
||||||
|
const { stationCode, signal } = options ?? {};
|
||||||
|
const client = stationCode ? ndmClient : userClient;
|
||||||
|
const prefix = stationCode ? `/${stationCode}` : '';
|
||||||
|
const endpoint = `${prefix}/api/ndm/ndmPermission/page`;
|
||||||
|
const resp = await client.post<PageResult<NdmPermissionResultVO>>(endpoint, pageQuery, { signal });
|
||||||
|
const data = unwrapResponse(resp);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const detailPermissionApi = async (id: string, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
|
||||||
|
const { stationCode, signal } = options ?? {};
|
||||||
|
const client = stationCode ? ndmClient : userClient;
|
||||||
|
const prefix = stationCode ? `/${stationCode}` : '';
|
||||||
|
const endpoint = `${prefix}/api/ndm/ndmPermission/detail`;
|
||||||
|
const resp = await client.get<NdmPermissionResultVO>(endpoint, { params: { id }, signal });
|
||||||
|
const data = unwrapResponse(resp);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const savePermissionApi = async (saveVO: NdmPermissionSaveVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
|
||||||
|
const { stationCode, signal } = options ?? {};
|
||||||
|
const client = stationCode ? ndmClient : userClient;
|
||||||
|
const prefix = stationCode ? `/${stationCode}` : '';
|
||||||
|
const endpoint = `${prefix}/api/ndm/ndmPermission`;
|
||||||
|
const resp = await client.post<NdmPermissionResultVO>(endpoint, saveVO, { signal });
|
||||||
|
const result = unwrapResponse(resp);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updatePermissionApi = async (updateVO: NdmPermissionUpdateVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
|
||||||
|
const { stationCode, signal } = options ?? {};
|
||||||
|
const client = stationCode ? ndmClient : userClient;
|
||||||
|
const prefix = stationCode ? `/${stationCode}` : '';
|
||||||
|
const endpoint = `${prefix}/api/ndm/ndmPermission`;
|
||||||
|
const resp = await client.put<NdmPermissionResultVO>(endpoint, updateVO, { signal });
|
||||||
|
const result = unwrapResponse(resp);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deletePermissionApi = async (ids: string[], options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
|
||||||
|
const { stationCode, signal } = options ?? {};
|
||||||
|
const client = stationCode ? ndmClient : userClient;
|
||||||
|
const prefix = stationCode ? `/${stationCode}` : '';
|
||||||
|
const endpoint = `${prefix}/api/ndm/ndmPermission`;
|
||||||
|
const resp = await client.delete<boolean>(endpoint, ids, { signal });
|
||||||
|
const result = unwrapResponse(resp);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const modifyPermissionApi = async (params: { employeeId: string; saveList: NdmPermissionSaveVO[]; removeList: string[] }, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
|
||||||
|
const { stationCode, signal } = options ?? {};
|
||||||
|
const client = stationCode ? ndmClient : userClient;
|
||||||
|
const prefix = stationCode ? `/${stationCode}` : '';
|
||||||
|
const endpoint = `${prefix}/api/ndm/ndmPermission/modify`;
|
||||||
|
const resp = await client.post<boolean>(endpoint, params, { signal });
|
||||||
|
const result = unwrapResponse(resp);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
|
export * from './base';
|
||||||
export * from './biz';
|
export * from './biz';
|
||||||
export * from './system';
|
export * from './system';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ClockCircleOutlined, CodeOutlined, FireOutlined, SaveOutlined } from '@vicons/antd';
|
import { ClockCheckIcon, CpuIcon, HardDriveIcon, MemoryStickIcon } from 'lucide-vue-next';
|
||||||
import { NCard, NFlex, NIcon, NProgress, type ProgressStatus } from 'naive-ui';
|
import { NCard, NFlex, NIcon, NProgress, type ProgressStatus } from 'naive-ui';
|
||||||
import { computed, toRefs } from 'vue';
|
import { computed, toRefs } from 'vue';
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ const props = defineProps<{
|
|||||||
const { cpuUsage, memUsage, diskUsage, runningTime, cpuUsageLabel, memUsageLabel, diskUsageLabel, runningTimeLabel } = toRefs(props);
|
const { cpuUsage, memUsage, diskUsage, runningTime, cpuUsageLabel, memUsageLabel, diskUsageLabel, runningTimeLabel } = toRefs(props);
|
||||||
|
|
||||||
const showCard = computed(() => {
|
const showCard = computed(() => {
|
||||||
return Object.values({ cpuUsage, memUsage, diskUsage, runningTime }).some((value) => !!value);
|
return Object.values({ cpuUsage, memUsage, diskUsage, runningTime }).some((refValue) => !!refValue.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const cpuPercent = computed(() => {
|
const cpuPercent = computed(() => {
|
||||||
@@ -54,22 +54,22 @@ const getProgressStatus = (percent: number): ProgressStatus => {
|
|||||||
<template #default>
|
<template #default>
|
||||||
<NFlex vertical>
|
<NFlex vertical>
|
||||||
<NFlex v-if="cpuUsage" style="width: 100%" align="center" :wrap="false">
|
<NFlex v-if="cpuUsage" style="width: 100%" align="center" :wrap="false">
|
||||||
<NIcon :component="FireOutlined" />
|
<NIcon :component="CpuIcon" />
|
||||||
<span style="word-break: keep-all">{{ cpuUsageLabel || 'CPU' }}</span>
|
<span style="word-break: keep-all">{{ cpuUsageLabel || 'CPU' }}</span>
|
||||||
<NProgress :percentage="cpuPercent" :status="getProgressStatus(cpuPercent)">{{ cpuPercent }}%</NProgress>
|
<NProgress :percentage="cpuPercent" :status="getProgressStatus(cpuPercent)">{{ cpuPercent }}%</NProgress>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
<NFlex v-if="memUsage" style="width: 100%" align="center" :wrap="false">
|
<NFlex v-if="memUsage" style="width: 100%" align="center" :wrap="false">
|
||||||
<NIcon :component="CodeOutlined" />
|
<NIcon :component="MemoryStickIcon" />
|
||||||
<span style="word-break: keep-all">{{ memUsageLabel || '内存' }}</span>
|
<span style="word-break: keep-all">{{ memUsageLabel || '内存' }}</span>
|
||||||
<NProgress :percentage="memPercent" :status="getProgressStatus(memPercent)">{{ memPercent }}%</NProgress>
|
<NProgress :percentage="memPercent" :status="getProgressStatus(memPercent)">{{ memPercent }}%</NProgress>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
<NFlex v-if="diskUsage" style="width: 100%" align="center" :wrap="false">
|
<NFlex v-if="diskUsage" style="width: 100%" align="center" :wrap="false">
|
||||||
<NIcon :component="SaveOutlined" />
|
<NIcon :component="HardDriveIcon" />
|
||||||
<span style="word-break: keep-all">{{ diskUsageLabel || '磁盘' }}</span>
|
<span style="word-break: keep-all">{{ diskUsageLabel || '磁盘' }}</span>
|
||||||
<NProgress :percentage="diskPercent" :status="getProgressStatus(diskPercent)">{{ diskPercent }}%</NProgress>
|
<NProgress :percentage="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="ClockCircleOutlined" />
|
<NIcon :component="ClockCheckIcon" />
|
||||||
<span>{{ runningTimeLabel || '运行时间' }}</span>
|
<span>{{ runningTimeLabel || '运行时间' }}</span>
|
||||||
<span>{{ formattedRunningTime }}</span>
|
<span>{{ formattedRunningTime }}</span>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { DEVICE_TYPE_LITERALS, DEVICE_TYPE_NAMES, tryGetDeviceType } from '@/enu
|
|||||||
import { useDeviceStore } from '@/stores';
|
import { useDeviceStore } from '@/stores';
|
||||||
import { parseErrorFeedback } from '@/utils';
|
import { parseErrorFeedback } from '@/utils';
|
||||||
import { useMutation } from '@tanstack/vue-query';
|
import { useMutation } from '@tanstack/vue-query';
|
||||||
import { ApiOutlined, ReloadOutlined } from '@vicons/antd';
|
|
||||||
import { isCancel } from 'axios';
|
import { isCancel } from 'axios';
|
||||||
import destr from 'destr';
|
import destr from 'destr';
|
||||||
|
import { LinkIcon, RotateCwIcon } from 'lucide-vue-next';
|
||||||
import { NButton, NCard, NFlex, NIcon, NTag, NTooltip } from 'naive-ui';
|
import { NButton, NCard, NFlex, NIcon, NTag, NTooltip } from 'naive-ui';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { computed, inject, onBeforeUnmount, ref, toRefs } from 'vue';
|
import { computed, inject, onBeforeUnmount, ref, toRefs } from 'vue';
|
||||||
@@ -152,7 +152,7 @@ onBeforeUnmount(() => {
|
|||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NButton size="small" quaternary circle :loading="probing" @click="() => probeDevice()">
|
<NButton size="small" quaternary circle :loading="probing" @click="() => probeDevice()">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="ApiOutlined" />
|
<NIcon :component="LinkIcon" />
|
||||||
</template>
|
</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
@@ -164,7 +164,7 @@ onBeforeUnmount(() => {
|
|||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NButton size="small" quaternary circle :loading="loading" @click="() => detailDevice()">
|
<NButton size="small" quaternary circle :loading="loading" @click="() => detailDevice()">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="ReloadOutlined" />
|
<NIcon :component="RotateCwIcon" />
|
||||||
</template>
|
</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,34 +1,58 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getChannelListApi, getRecordCheckApi, reloadAllRecordCheckApi, reloadRecordCheckApi, type NdmNvrResultVO, type NdmRecordCheck, type RecordItem, type Station } from '@/apis';
|
import { getChannelListApi, getRecordCheckApi, reloadAllRecordCheckApi, reloadRecordCheckApi, type NdmNvrResultVO, type RecordItem, type Station } from '@/apis';
|
||||||
import { exportRecordDiagCsv, transformRecordChecks } from '@/helpers';
|
import { exportRecordDiagCsv, transformRecordChecks } from '@/helpers';
|
||||||
import { useStationStore } from '@/stores';
|
import { useSettingStore } from '@/stores';
|
||||||
import { parseErrorFeedback } from '@/utils';
|
import { parseErrorFeedback } from '@/utils';
|
||||||
import { useMutation } from '@tanstack/vue-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query';
|
||||||
import { DownloadOutlined, ReloadOutlined } from '@vicons/antd';
|
|
||||||
import { isCancel } from 'axios';
|
import { isCancel } from 'axios';
|
||||||
import dayjs from 'dayjs';
|
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 { NButton, NCard, NFlex, NIcon, NPagination, NPopconfirm, NPopover, NRadioButton, NRadioGroup, NTooltip, useThemeVars } from 'naive-ui';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, toRefs, watch } from 'vue';
|
import { computed, onBeforeUnmount, ref, toRefs, watch } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
ndmDevice: NdmNvrResultVO;
|
ndmDevice: NdmNvrResultVO;
|
||||||
station: Station;
|
station: Station;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const { activeRequests } = storeToRefs(settingStore);
|
||||||
|
|
||||||
const themeVars = useThemeVars();
|
const themeVars = useThemeVars();
|
||||||
|
|
||||||
const stationStore = useStationStore();
|
const queryClient = useQueryClient();
|
||||||
const { stations } = storeToRefs(stationStore);
|
|
||||||
|
|
||||||
const { ndmDevice, station } = toRefs(props);
|
const { ndmDevice, station } = toRefs(props);
|
||||||
|
|
||||||
const recordChecks = ref<NdmRecordCheck[]>([]);
|
|
||||||
|
|
||||||
const lossInput = ref<number>(0);
|
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(() => {
|
const recordDiags = computed(() => {
|
||||||
return transformRecordChecks(recordChecks.value).filter((recordDiag) => {
|
return transformRecordChecks(recordChecks.value ?? []).filter((recordDiag) => {
|
||||||
if (lossInput.value === 0) {
|
if (lossInput.value === 0) {
|
||||||
return true;
|
return true;
|
||||||
} else if (lossInput.value === 1) {
|
} else if (lossInput.value === 1) {
|
||||||
@@ -40,26 +64,6 @@ const recordDiags = computed(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const abortController = ref<AbortController>(new AbortController());
|
|
||||||
|
|
||||||
const { mutate: getRecordCheckByParentId, isPending: loading } = useMutation({
|
|
||||||
mutationFn: async () => {
|
|
||||||
abortController.value.abort();
|
|
||||||
abortController.value = new AbortController();
|
|
||||||
const checks = await getRecordCheckApi(ndmDevice.value, 90, [], { stationCode: station.value.code, signal: abortController.value.signal });
|
|
||||||
return checks;
|
|
||||||
},
|
|
||||||
onSuccess: (checks) => {
|
|
||||||
recordChecks.value = checks;
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
if (isCancel(error)) return;
|
|
||||||
console.error(error);
|
|
||||||
const errorFeedback = parseErrorFeedback(error);
|
|
||||||
window.$message.error(errorFeedback);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutate: reloadAllRecordCheck, isPending: reloading } = useMutation({
|
const { mutate: reloadAllRecordCheck, isPending: reloading } = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
abortController.value.abort();
|
abortController.value.abort();
|
||||||
@@ -78,9 +82,7 @@ const { mutate: reloadAllRecordCheck, isPending: reloading } = useMutation({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onExportRecordCheck = () => {
|
const onExportRecordCheck = () => {
|
||||||
const code = station.value.code;
|
exportRecordDiagCsv(recordDiags.value, station.value.name);
|
||||||
const stationName = stations.value.find((station) => station.code === code)?.name ?? '';
|
|
||||||
exportRecordDiagCsv(recordDiags.value, stationName);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
@@ -121,7 +123,7 @@ const { mutate: reloadRecordCheckByGbId } = useMutation({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
getRecordCheckByParentId();
|
refetchRecordChecks();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (isCancel(error)) return;
|
if (isCancel(error)) return;
|
||||||
@@ -131,19 +133,6 @@ const { mutate: reloadRecordCheckByGbId } = useMutation({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getRecordCheckByParentId();
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => ndmDevice.value.id,
|
|
||||||
(devieDbId) => {
|
|
||||||
if (devieDbId) {
|
|
||||||
getRecordCheckByParentId();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
abortController.value.abort();
|
abortController.value.abort();
|
||||||
});
|
});
|
||||||
@@ -168,9 +157,9 @@ onBeforeUnmount(() => {
|
|||||||
<NFlex>
|
<NFlex>
|
||||||
<NTooltip trigger="hover">
|
<NTooltip trigger="hover">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NButton size="small" quaternary circle :loading="loading" @click="() => getRecordCheckByParentId()">
|
<NButton size="small" quaternary circle :loading="loading" @click="() => refetchRecordChecks()">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="ReloadOutlined" />
|
<NIcon :component="RotateCwIcon" />
|
||||||
</template>
|
</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
@@ -182,7 +171,7 @@ onBeforeUnmount(() => {
|
|||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NButton size="small" quaternary circle @click="onExportRecordCheck">
|
<NButton size="small" quaternary circle @click="onExportRecordCheck">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="DownloadOutlined" />
|
<NIcon :component="DownloadIcon" />
|
||||||
</template>
|
</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -13,15 +13,17 @@ import {
|
|||||||
type Station,
|
type Station,
|
||||||
} from '@/apis';
|
} from '@/apis';
|
||||||
import { SecurityBoxCircuitLinkModal } from '@/components';
|
import { SecurityBoxCircuitLinkModal } from '@/components';
|
||||||
|
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 { 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';
|
||||||
import { PoweroffOutlined } from '@vicons/antd';
|
|
||||||
import { watchImmediate } from '@vueuse/core';
|
import { watchImmediate } from '@vueuse/core';
|
||||||
import { isCancel } from 'axios';
|
import { isCancel } from 'axios';
|
||||||
import destr from 'destr';
|
import destr from 'destr';
|
||||||
import { cloneDeep, isFunction } from 'es-toolkit';
|
import { cloneDeep, isFunction } from 'es-toolkit';
|
||||||
|
import { PowerIcon } from 'lucide-vue-next';
|
||||||
import { NButton, NCard, NDescriptions, NDescriptionsItem, NDropdown, NFlex, NIcon, NPopconfirm, NPopover, NSwitch, NTag, useThemeVars, type DropdownOption, type TagProps } from 'naive-ui';
|
import { NButton, NCard, NDescriptions, NDescriptionsItem, NDropdown, NFlex, NIcon, NPopconfirm, NPopover, NSwitch, NTag, useThemeVars, type DropdownOption, type TagProps } from 'naive-ui';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { computed, inject, ref, toRefs } from 'vue';
|
import { computed, inject, ref, toRefs } from 'vue';
|
||||||
@@ -38,7 +40,9 @@ const deviceStore = useDeviceStore();
|
|||||||
const { lineDevices } = storeToRefs(deviceStore);
|
const { lineDevices } = storeToRefs(deviceStore);
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { offlineDev } = storeToRefs(settingStore);
|
const { useLocalDB } = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
const { ndmDevice, station, circuits } = toRefs(props);
|
const { ndmDevice, station, circuits } = toRefs(props);
|
||||||
|
|
||||||
@@ -223,6 +227,7 @@ const onSelectDropdownOption = (key: string, option: DropdownOption) => {
|
|||||||
const onContextmenu = (payload: PointerEvent, circuitIndex: number) => {
|
const onContextmenu = (payload: PointerEvent, circuitIndex: number) => {
|
||||||
payload.stopPropagation();
|
payload.stopPropagation();
|
||||||
payload.preventDefault();
|
payload.preventDefault();
|
||||||
|
if (!hasPermission(station.value.code, PERMISSION_TYPE_LITERALS.OPERATION)) return;
|
||||||
const { clientX, clientY } = payload;
|
const { clientX, clientY } = payload;
|
||||||
contextmenu.value = { x: clientX, y: clientY, circuitIndex };
|
contextmenu.value = { x: clientX, y: clientY, circuitIndex };
|
||||||
showContextmenu.value = true;
|
showContextmenu.value = true;
|
||||||
@@ -258,8 +263,8 @@ const { mutate: unlinkDevice } = useMutation({
|
|||||||
delete modifiedUpperLinkDescription.downstream?.[circuitIndex];
|
delete modifiedUpperLinkDescription.downstream?.[circuitIndex];
|
||||||
modifiedUpperDevice.linkDescription = JSON.stringify(modifiedUpperLinkDescription);
|
modifiedUpperDevice.linkDescription = JSON.stringify(modifiedUpperLinkDescription);
|
||||||
|
|
||||||
// 3. 发起update请求并获取最新的设备详情(离线模式下直接修改本地数据)
|
// 3. 发起update请求并获取最新的设备详情(使用本地数据库时直接修改本地数据)
|
||||||
if (offlineDev.value) {
|
if (useLocalDB.value) {
|
||||||
return { upperDevice: modifiedUpperDevice, lowerDevice: modifiedLowerDevice };
|
return { upperDevice: modifiedUpperDevice, lowerDevice: modifiedLowerDevice };
|
||||||
}
|
}
|
||||||
const stationCode = station.value.code;
|
const stationCode = station.value.code;
|
||||||
@@ -313,7 +318,7 @@ const { mutate: unlinkDevice } = useMutation({
|
|||||||
<NFlex align="center">
|
<NFlex align="center">
|
||||||
<NTag class="pointer-cursor" size="small" :type="getCircuitStatusTagType(circuit)">
|
<NTag class="pointer-cursor" size="small" :type="getCircuitStatusTagType(circuit)">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="PoweroffOutlined" />
|
<NIcon :component="PowerIcon" />
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<span>{{ getCircuitStatusText(circuit) }}</span>
|
<span>{{ getCircuitStatusText(circuit) }}</span>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const show = defineModel<boolean>('show', { default: false });
|
|||||||
const deviceStore = useDeviceStore();
|
const deviceStore = useDeviceStore();
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { offlineDev } = storeToRefs(settingStore);
|
const { useLocalDB } = storeToRefs(settingStore);
|
||||||
|
|
||||||
const { ndmDevice, station, circuitIndex } = toRefs(props);
|
const { ndmDevice, station, circuitIndex } = toRefs(props);
|
||||||
|
|
||||||
@@ -150,8 +150,8 @@ const { mutate: linkPortToDevice, isPending: linking } = useMutation({
|
|||||||
}
|
}
|
||||||
modifiedLowerDevice.linkDescription = JSON.stringify(modifiedLowerDeviceLinkDescription);
|
modifiedLowerDevice.linkDescription = JSON.stringify(modifiedLowerDeviceLinkDescription);
|
||||||
|
|
||||||
// 3. 发起update请求并获取最新的设备详情(离线模式下直接修改本地数据)
|
// 3. 发起update请求并获取最新的设备详情(使用本地数据库时直接修改本地数据)
|
||||||
if (offlineDev.value) {
|
if (useLocalDB.value) {
|
||||||
return { upperDevice: modifiedUpperDevice, lowerDevice: modifiedLowerDevice };
|
return { upperDevice: modifiedUpperDevice, lowerDevice: modifiedLowerDevice };
|
||||||
}
|
}
|
||||||
const stationCode = station.value.code;
|
const stationCode = station.value.code;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ThunderboltOutlined } from '@vicons/antd';
|
import { DropletIcon, FanIcon, ShieldIcon, ThermometerIcon, ZapIcon } from 'lucide-vue-next';
|
||||||
import { ApertureOutline, LockOpenOutline, ThermometerOutline, WaterOutline } from '@vicons/ionicons5';
|
|
||||||
import { NCard, NFlex, NIcon, NTag } from 'naive-ui';
|
import { NCard, NFlex, NIcon, NTag } from 'naive-ui';
|
||||||
import { computed, toRefs } from 'vue';
|
import { computed, toRefs } from 'vue';
|
||||||
|
|
||||||
@@ -52,7 +51,7 @@ const formattedFanSpeeds = computed(() => {
|
|||||||
<NFlex vertical>
|
<NFlex vertical>
|
||||||
<NTag>
|
<NTag>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="ThermometerOutline" />
|
<NIcon :component="ThermometerIcon" />
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<span>温度: {{ temperature }}℃</span>
|
<span>温度: {{ temperature }}℃</span>
|
||||||
@@ -60,7 +59,7 @@ const formattedFanSpeeds = computed(() => {
|
|||||||
</NTag>
|
</NTag>
|
||||||
<NTag>
|
<NTag>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="WaterOutline" />
|
<NIcon :component="DropletIcon" />
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<span>湿度: {{ humidity }}%</span>
|
<span>湿度: {{ humidity }}%</span>
|
||||||
@@ -68,7 +67,7 @@ const formattedFanSpeeds = computed(() => {
|
|||||||
</NTag>
|
</NTag>
|
||||||
<NTag>
|
<NTag>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="ApertureOutline" />
|
<NIcon :component="FanIcon" />
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<span>风扇: {{ formattedFanSpeeds }}</span>
|
<span>风扇: {{ formattedFanSpeeds }}</span>
|
||||||
@@ -76,7 +75,7 @@ const formattedFanSpeeds = computed(() => {
|
|||||||
</NTag>
|
</NTag>
|
||||||
<NTag :type="getStatusTagType(accessControlStatus)">
|
<NTag :type="getStatusTagType(accessControlStatus)">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="LockOpenOutline" />
|
<NIcon :component="ShieldIcon" />
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<span>门禁: {{ accessControlStatus }}</span>
|
<span>门禁: {{ accessControlStatus }}</span>
|
||||||
@@ -84,7 +83,7 @@ const formattedFanSpeeds = computed(() => {
|
|||||||
</NTag>
|
</NTag>
|
||||||
<NTag :type="getStatusTagType(lightningProtectionStatus)">
|
<NTag :type="getStatusTagType(lightningProtectionStatus)">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="ThunderboltOutlined" />
|
<NIcon :component="ZapIcon" />
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<span>防雷: {{ lightningProtectionStatus }}</span>
|
<span>防雷: {{ lightningProtectionStatus }}</span>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { detailDeviceApi, updateDeviceApi, type LinkDescription, type NdmDeviceResultVO, type NdmSwitchLinkDescription, type NdmSwitchPortInfo, type NdmSwitchResultVO, type Station } from '@/apis';
|
import { detailDeviceApi, updateDeviceApi, type LinkDescription, type NdmDeviceResultVO, type NdmSwitchLinkDescription, type NdmSwitchPortInfo, type NdmSwitchResultVO, type Station } from '@/apis';
|
||||||
import { SwitchPortLinkModal } from '@/components';
|
import { SwitchPortLinkModal } from '@/components';
|
||||||
|
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 { getPortStatusValue, transformPortSpeed } from '@/helpers';
|
import { getPortStatusValue, transformPortSpeed } from '@/helpers';
|
||||||
import { useDeviceStore, useSettingStore } from '@/stores';
|
import { useDeviceStore, useSettingStore } from '@/stores';
|
||||||
import { parseErrorFeedback } from '@/utils';
|
import { parseErrorFeedback } from '@/utils';
|
||||||
@@ -25,7 +27,9 @@ const deviceStore = useDeviceStore();
|
|||||||
const { lineDevices } = storeToRefs(deviceStore);
|
const { lineDevices } = storeToRefs(deviceStore);
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { offlineDev } = storeToRefs(settingStore);
|
const { useLocalDB } = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
const { ndmDevice, station, ports } = toRefs(props);
|
const { ndmDevice, station, ports } = toRefs(props);
|
||||||
|
|
||||||
@@ -172,6 +176,7 @@ const onSelectDropdownOption = (key: string, option: DropdownOption) => {
|
|||||||
const onContextmenu = (payload: PointerEvent, port: NdmSwitchPortInfo) => {
|
const onContextmenu = (payload: PointerEvent, port: NdmSwitchPortInfo) => {
|
||||||
payload.stopPropagation();
|
payload.stopPropagation();
|
||||||
payload.preventDefault();
|
payload.preventDefault();
|
||||||
|
if (!hasPermission(station.value.code, PERMISSION_TYPE_LITERALS.OPERATION)) return;
|
||||||
const { clientX, clientY } = payload;
|
const { clientX, clientY } = payload;
|
||||||
contextmenu.value = { x: clientX, y: clientY, port };
|
contextmenu.value = { x: clientX, y: clientY, port };
|
||||||
showContextmenu.value = true;
|
showContextmenu.value = true;
|
||||||
@@ -208,8 +213,8 @@ const { mutate: unlinkDevice } = useMutation({
|
|||||||
delete modifiedUpperLinkDescription.downstream?.[port.portName];
|
delete modifiedUpperLinkDescription.downstream?.[port.portName];
|
||||||
modifiedUpperDevice.linkDescription = JSON.stringify(modifiedUpperLinkDescription);
|
modifiedUpperDevice.linkDescription = JSON.stringify(modifiedUpperLinkDescription);
|
||||||
|
|
||||||
// 3. 发起update请求并获取最新的设备详情(离线模式下直接修改本地数据)
|
// 3. 发起update请求并获取最新的设备详情(使用本地数据库时直接修改本地数据)
|
||||||
if (offlineDev.value) {
|
if (useLocalDB.value) {
|
||||||
return { upperDevice: modifiedUpperDevice, lowerDevice: modifiedLowerDevice };
|
return { upperDevice: modifiedUpperDevice, lowerDevice: modifiedLowerDevice };
|
||||||
}
|
}
|
||||||
const stationCode = station.value.code;
|
const stationCode = station.value.code;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const show = defineModel<boolean>('show', { default: false });
|
|||||||
const deviceStore = useDeviceStore();
|
const deviceStore = useDeviceStore();
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { offlineDev } = storeToRefs(settingStore);
|
const { useLocalDB } = storeToRefs(settingStore);
|
||||||
|
|
||||||
const { ndmDevice, station, port } = toRefs(props);
|
const { ndmDevice, station, port } = toRefs(props);
|
||||||
|
|
||||||
@@ -160,8 +160,8 @@ const { mutate: linkPortToDevice, isPending: linking } = useMutation({
|
|||||||
}
|
}
|
||||||
modifiedLowerDevice.linkDescription = JSON.stringify(modifiedLowerDeviceLinkDescription);
|
modifiedLowerDevice.linkDescription = JSON.stringify(modifiedLowerDeviceLinkDescription);
|
||||||
|
|
||||||
// 3. 发起update请求并获取最新的设备详情(离线模式下直接修改本地数据)
|
// 3. 发起update请求并获取最新的设备详情(使用本地数据库时直接修改本地数据)
|
||||||
if (offlineDev.value) {
|
if (useLocalDB.value) {
|
||||||
return { upperDevice: modifiedUpperDevice, lowerDevice: modifiedLowerDevice };
|
return { upperDevice: modifiedUpperDevice, lowerDevice: modifiedLowerDevice };
|
||||||
}
|
}
|
||||||
const stationCode = station.value.code;
|
const stationCode = station.value.code;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { NdmAlarmHostResultVO, Station } from '@/apis';
|
import type { NdmAlarmHostResultVO, Station } from '@/apis';
|
||||||
import { AlarmHostCurrentDiag, AlarmHostHistoryDiag, AlarmHostUpdate, DeviceRawCard } from '@/components';
|
import { AlarmHostCurrentDiag, AlarmHostHistoryDiag, AlarmHostUpdate, DeviceRawCard } from '@/components';
|
||||||
|
import { usePermission } from '@/composables';
|
||||||
|
import { PERMISSION_TYPE_LITERALS } from '@/enums';
|
||||||
import { useSettingStore } from '@/stores';
|
import { useSettingStore } from '@/stores';
|
||||||
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
|
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
@@ -16,7 +18,9 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { debugModeEnabled } = storeToRefs(settingStore);
|
const { showDeviceRawData } = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
const { ndmDevice, station } = toRefs(props);
|
const { ndmDevice, station } = toRefs(props);
|
||||||
|
|
||||||
@@ -31,8 +35,8 @@ const activeTabName = ref('当前诊断');
|
|||||||
const onTabChange = (name: string) => {
|
const onTabChange = (name: string) => {
|
||||||
activeTabName.value = name;
|
activeTabName.value = name;
|
||||||
};
|
};
|
||||||
watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
|
watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
|
||||||
if (newDevice.id !== oldDevice.id || !enabled) {
|
if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
|
||||||
activeTabName.value = '当前诊断';
|
activeTabName.value = '当前诊断';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -45,8 +49,8 @@ watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
|
|||||||
<NTabs :value="activeTabName" @update:value="onTabChange">
|
<NTabs :value="activeTabName" @update:value="onTabChange">
|
||||||
<NTab name="当前诊断">当前诊断</NTab>
|
<NTab name="当前诊断">当前诊断</NTab>
|
||||||
<NTab name="历史诊断">历史诊断</NTab>
|
<NTab name="历史诊断">历史诊断</NTab>
|
||||||
<NTab name="修改设备">修改设备</NTab>
|
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
|
||||||
<NTab v-if="debugModeEnabled" name="原始数据">原始数据</NTab>
|
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
|
||||||
</NTabs>
|
</NTabs>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { NdmCameraResultVO, Station } from '@/apis';
|
import type { NdmCameraResultVO, Station } from '@/apis';
|
||||||
import { CameraCurrentDiag, CameraHistoryDiag, CameraUpdate, DeviceRawCard } from '@/components';
|
import { CameraCurrentDiag, CameraHistoryDiag, CameraUpdate, DeviceRawCard } from '@/components';
|
||||||
|
import { usePermission } from '@/composables';
|
||||||
|
import { PERMISSION_TYPE_LITERALS } from '@/enums';
|
||||||
import { useSettingStore } from '@/stores';
|
import { useSettingStore } from '@/stores';
|
||||||
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
|
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
@@ -16,7 +18,9 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { debugModeEnabled } = storeToRefs(settingStore);
|
const { showDeviceRawData } = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
const { ndmDevice, station } = toRefs(props);
|
const { ndmDevice, station } = toRefs(props);
|
||||||
|
|
||||||
@@ -31,8 +35,8 @@ const activeTabName = ref('当前诊断');
|
|||||||
const onTabChange = (name: string) => {
|
const onTabChange = (name: string) => {
|
||||||
activeTabName.value = name;
|
activeTabName.value = name;
|
||||||
};
|
};
|
||||||
watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
|
watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
|
||||||
if (newDevice.id !== oldDevice.id || !enabled) {
|
if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
|
||||||
activeTabName.value = '当前诊断';
|
activeTabName.value = '当前诊断';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -45,8 +49,8 @@ watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
|
|||||||
<NTabs :value="activeTabName" @update:value="onTabChange">
|
<NTabs :value="activeTabName" @update:value="onTabChange">
|
||||||
<NTab name="当前诊断">当前诊断</NTab>
|
<NTab name="当前诊断">当前诊断</NTab>
|
||||||
<NTab name="历史诊断">历史诊断</NTab>
|
<NTab name="历史诊断">历史诊断</NTab>
|
||||||
<NTab name="修改设备">修改设备</NTab>
|
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
|
||||||
<NTab v-if="debugModeEnabled" name="原始数据">原始数据</NTab>
|
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
|
||||||
</NTabs>
|
</NTabs>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { offlineDev } = storeToRefs(settingStore);
|
const { activeRequests } = storeToRefs(settingStore);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ const QUERY_KEY = 'camera-installation-area-query';
|
|||||||
|
|
||||||
const { data: installationArea } = useQuery({
|
const { data: installationArea } = useQuery({
|
||||||
queryKey: computed(() => [QUERY_KEY, ndmDevice.value.gbCode, station.value.code]),
|
queryKey: computed(() => [QUERY_KEY, ndmDevice.value.gbCode, station.value.code]),
|
||||||
enabled: computed(() => !offlineDev.value),
|
enabled: computed(() => activeRequests.value),
|
||||||
gcTime: 0,
|
gcTime: 0,
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
const UNKNOWN_NAME = '-';
|
const UNKNOWN_NAME = '-';
|
||||||
@@ -107,8 +107,8 @@ const { data: installationArea } = useQuery({
|
|||||||
return `${tier1Area.name}-${tier2Area.name}`;
|
return `${tier1Area.name}-${tier2Area.name}`;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
watch(offlineDev, (offline) => {
|
watch(activeRequests, (active) => {
|
||||||
if (offline) {
|
if (!active) {
|
||||||
queryClient.cancelQueries({ queryKey: [QUERY_KEY] });
|
queryClient.cancelQueries({ queryKey: [QUERY_KEY] });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { NdmDecoderResultVO, Station } from '@/apis';
|
import type { NdmDecoderResultVO, Station } from '@/apis';
|
||||||
import { DecoderCurrentDiag, DecoderHistoryDiag, DecoderUpdate, DeviceRawCard } from '@/components';
|
import { DecoderCurrentDiag, DecoderHistoryDiag, DecoderUpdate, DeviceRawCard } from '@/components';
|
||||||
|
import { usePermission } from '@/composables';
|
||||||
|
import { PERMISSION_TYPE_LITERALS } from '@/enums';
|
||||||
import { useSettingStore } from '@/stores';
|
import { useSettingStore } from '@/stores';
|
||||||
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
|
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
@@ -16,7 +18,9 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { debugModeEnabled } = storeToRefs(settingStore);
|
const { showDeviceRawData } = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
const { ndmDevice, station } = toRefs(props);
|
const { ndmDevice, station } = toRefs(props);
|
||||||
|
|
||||||
@@ -31,8 +35,8 @@ const activeTabName = ref('当前诊断');
|
|||||||
const onTabChange = (name: string) => {
|
const onTabChange = (name: string) => {
|
||||||
activeTabName.value = name;
|
activeTabName.value = name;
|
||||||
};
|
};
|
||||||
watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
|
watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
|
||||||
if (newDevice.id !== oldDevice.id || !enabled) {
|
if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
|
||||||
activeTabName.value = '当前诊断';
|
activeTabName.value = '当前诊断';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -45,8 +49,8 @@ watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
|
|||||||
<NTabs :value="activeTabName" @update:value="onTabChange">
|
<NTabs :value="activeTabName" @update:value="onTabChange">
|
||||||
<NTab name="当前诊断">当前诊断</NTab>
|
<NTab name="当前诊断">当前诊断</NTab>
|
||||||
<NTab name="历史诊断">历史诊断</NTab>
|
<NTab name="历史诊断">历史诊断</NTab>
|
||||||
<NTab name="修改设备">修改设备</NTab>
|
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
|
||||||
<NTab v-if="debugModeEnabled" name="原始数据">原始数据</NTab>
|
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
|
||||||
</NTabs>
|
</NTabs>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { NdmKeyboardResultVO, Station } from '@/apis';
|
import type { NdmKeyboardResultVO, Station } from '@/apis';
|
||||||
import { DeviceRawCard, KeyboardCurrentDiag, KeyboardHistoryDiag, KeyboardUpdate } from '@/components';
|
import { DeviceRawCard, KeyboardCurrentDiag, KeyboardHistoryDiag, KeyboardUpdate } from '@/components';
|
||||||
|
import { usePermission } from '@/composables';
|
||||||
|
import { PERMISSION_TYPE_LITERALS } from '@/enums';
|
||||||
import { useSettingStore } from '@/stores';
|
import { useSettingStore } from '@/stores';
|
||||||
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
|
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
@@ -16,7 +18,9 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { debugModeEnabled } = storeToRefs(settingStore);
|
const { showDeviceRawData } = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
const { ndmDevice, station } = toRefs(props);
|
const { ndmDevice, station } = toRefs(props);
|
||||||
|
|
||||||
@@ -31,8 +35,8 @@ const activeTabName = ref('当前诊断');
|
|||||||
const onTabChange = (name: string) => {
|
const onTabChange = (name: string) => {
|
||||||
activeTabName.value = name;
|
activeTabName.value = name;
|
||||||
};
|
};
|
||||||
watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
|
watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
|
||||||
if (newDevice.id !== oldDevice.id || !enabled) {
|
if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
|
||||||
activeTabName.value = '当前诊断';
|
activeTabName.value = '当前诊断';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -45,8 +49,8 @@ watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
|
|||||||
<NTabs :value="activeTabName" @update:value="onTabChange">
|
<NTabs :value="activeTabName" @update:value="onTabChange">
|
||||||
<NTab name="当前诊断">当前诊断</NTab>
|
<NTab name="当前诊断">当前诊断</NTab>
|
||||||
<NTab name="历史诊断">历史诊断</NTab>
|
<NTab name="历史诊断">历史诊断</NTab>
|
||||||
<NTab name="修改设备">修改设备</NTab>
|
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
|
||||||
<NTab v-if="debugModeEnabled" name="原始数据">原始数据</NTab>
|
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
|
||||||
</NTabs>
|
</NTabs>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { NdmNvrResultVO, Station } from '@/apis';
|
import type { NdmNvrResultVO, Station } from '@/apis';
|
||||||
import { DeviceRawCard, NvrCurrentDiag, NvrHistoryDiag, NvrUpdate } from '@/components';
|
import { DeviceRawCard, NvrCurrentDiag, NvrHistoryDiag, NvrUpdate } from '@/components';
|
||||||
|
import { usePermission } from '@/composables';
|
||||||
|
import { PERMISSION_TYPE_LITERALS } from '@/enums';
|
||||||
import { useSettingStore } from '@/stores';
|
import { useSettingStore } from '@/stores';
|
||||||
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
|
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
@@ -16,7 +18,9 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { debugModeEnabled } = storeToRefs(settingStore);
|
const { showDeviceRawData } = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
const { ndmDevice, station } = toRefs(props);
|
const { ndmDevice, station } = toRefs(props);
|
||||||
|
|
||||||
@@ -31,8 +35,8 @@ const activeTabName = ref('当前诊断');
|
|||||||
const onTabChange = (name: string) => {
|
const onTabChange = (name: string) => {
|
||||||
activeTabName.value = name;
|
activeTabName.value = name;
|
||||||
};
|
};
|
||||||
watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
|
watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
|
||||||
if (newDevice.id !== oldDevice.id || !enabled) {
|
if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
|
||||||
activeTabName.value = '当前诊断';
|
activeTabName.value = '当前诊断';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -45,8 +49,8 @@ watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
|
|||||||
<NTabs :value="activeTabName" @update:value="onTabChange">
|
<NTabs :value="activeTabName" @update:value="onTabChange">
|
||||||
<NTab name="当前诊断">当前诊断</NTab>
|
<NTab name="当前诊断">当前诊断</NTab>
|
||||||
<NTab name="历史诊断">历史诊断</NTab>
|
<NTab name="历史诊断">历史诊断</NTab>
|
||||||
<NTab name="修改设备">修改设备</NTab>
|
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
|
||||||
<NTab v-if="debugModeEnabled" name="原始数据">原始数据</NTab>
|
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
|
||||||
</NTabs>
|
</NTabs>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { NdmSecurityBoxResultVO, Station } from '@/apis';
|
import type { NdmSecurityBoxResultVO, Station } from '@/apis';
|
||||||
import { DeviceRawCard, SecurityBoxCurrentDiag, SecurityBoxHistoryDiag, SecurityBoxUpdate } from '@/components';
|
import { DeviceRawCard, SecurityBoxCurrentDiag, SecurityBoxHistoryDiag, SecurityBoxUpdate } from '@/components';
|
||||||
|
import { usePermission } from '@/composables';
|
||||||
|
import { PERMISSION_TYPE_LITERALS } from '@/enums';
|
||||||
import { useSettingStore } from '@/stores';
|
import { useSettingStore } from '@/stores';
|
||||||
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
|
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
@@ -16,7 +18,9 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { debugModeEnabled } = storeToRefs(settingStore);
|
const { showDeviceRawData } = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
const { ndmDevice, station } = toRefs(props);
|
const { ndmDevice, station } = toRefs(props);
|
||||||
|
|
||||||
@@ -31,8 +35,8 @@ const activeTabName = ref('当前诊断');
|
|||||||
const onTabChange = (name: string) => {
|
const onTabChange = (name: string) => {
|
||||||
activeTabName.value = name;
|
activeTabName.value = name;
|
||||||
};
|
};
|
||||||
watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
|
watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
|
||||||
if (newDevice.id !== oldDevice.id || !enabled) {
|
if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
|
||||||
activeTabName.value = '当前诊断';
|
activeTabName.value = '当前诊断';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -45,8 +49,8 @@ watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
|
|||||||
<NTabs :value="activeTabName" @update:value="onTabChange">
|
<NTabs :value="activeTabName" @update:value="onTabChange">
|
||||||
<NTab name="当前诊断">当前诊断</NTab>
|
<NTab name="当前诊断">当前诊断</NTab>
|
||||||
<NTab name="历史诊断">历史诊断</NTab>
|
<NTab name="历史诊断">历史诊断</NTab>
|
||||||
<NTab name="修改设备">修改设备</NTab>
|
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
|
||||||
<NTab v-if="debugModeEnabled" name="原始数据">原始数据</NTab>
|
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
|
||||||
</NTabs>
|
</NTabs>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { offlineDev } = storeToRefs(settingStore);
|
const { activeRequests } = storeToRefs(settingStore);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ 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 { data: isMediaServerAlive } = useQuery({
|
const { data: isMediaServerAlive } = useQuery({
|
||||||
queryKey: computed(() => [MEDIA_SERVER_ALIVE_QUERY_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
|
queryKey: computed(() => [MEDIA_SERVER_ALIVE_QUERY_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
|
||||||
enabled: computed(() => !offlineDev.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,
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
@@ -35,15 +35,15 @@ const { data: isMediaServerAlive } = useQuery({
|
|||||||
});
|
});
|
||||||
const { data: isSipServerAlive } = useQuery({
|
const { data: isSipServerAlive } = useQuery({
|
||||||
queryKey: computed(() => [VIDEO_SERVER_ALIVE_QUERY_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
|
queryKey: computed(() => [VIDEO_SERVER_ALIVE_QUERY_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
|
||||||
enabled: computed(() => !offlineDev.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,
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
return await isSipServerAliveApi({ stationCode: station.value.code, signal });
|
return await isSipServerAliveApi({ stationCode: station.value.code, signal });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
watch(offlineDev, (offline) => {
|
watch(activeRequests, (active) => {
|
||||||
if (offline) {
|
if (!active) {
|
||||||
queryClient.cancelQueries({ queryKey: [MEDIA_SERVER_ALIVE_QUERY_KEY] });
|
queryClient.cancelQueries({ queryKey: [MEDIA_SERVER_ALIVE_QUERY_KEY] });
|
||||||
queryClient.cancelQueries({ queryKey: [VIDEO_SERVER_ALIVE_QUERY_KEY] });
|
queryClient.cancelQueries({ queryKey: [VIDEO_SERVER_ALIVE_QUERY_KEY] });
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ watch(offlineDev, (offline) => {
|
|||||||
<span>服务状态</span>
|
<span>服务状态</span>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<template v-if="offlineDev">
|
<template v-if="!activeRequests">
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { NdmServerResultVO, Station } from '@/apis';
|
import type { NdmServerResultVO, Station } from '@/apis';
|
||||||
import { DeviceRawCard, ServerCurrentDiag, ServerHistoryDiag, ServerUpdate } from '@/components';
|
import { DeviceRawCard, ServerCurrentDiag, ServerHistoryDiag, ServerUpdate } from '@/components';
|
||||||
|
import { usePermission } from '@/composables';
|
||||||
|
import { PERMISSION_TYPE_LITERALS } from '@/enums';
|
||||||
import { useSettingStore } from '@/stores';
|
import { useSettingStore } from '@/stores';
|
||||||
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
|
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
@@ -16,7 +18,9 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { debugModeEnabled } = storeToRefs(settingStore);
|
const { showDeviceRawData } = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
const { ndmDevice, station } = toRefs(props);
|
const { ndmDevice, station } = toRefs(props);
|
||||||
|
|
||||||
@@ -31,8 +35,8 @@ const activeTabName = ref('当前诊断');
|
|||||||
const onTabChange = (name: string) => {
|
const onTabChange = (name: string) => {
|
||||||
activeTabName.value = name;
|
activeTabName.value = name;
|
||||||
};
|
};
|
||||||
watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
|
watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
|
||||||
if (newDevice.id !== oldDevice.id || !enabled) {
|
if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
|
||||||
activeTabName.value = '当前诊断';
|
activeTabName.value = '当前诊断';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -45,8 +49,8 @@ watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
|
|||||||
<NTabs :value="activeTabName" @update:value="onTabChange">
|
<NTabs :value="activeTabName" @update:value="onTabChange">
|
||||||
<NTab name="当前诊断">当前诊断</NTab>
|
<NTab name="当前诊断">当前诊断</NTab>
|
||||||
<NTab name="历史诊断">历史诊断</NTab>
|
<NTab name="历史诊断">历史诊断</NTab>
|
||||||
<NTab name="修改设备">修改设备</NTab>
|
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
|
||||||
<NTab v-if="debugModeEnabled" name="原始数据">原始数据</NTab>
|
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
|
||||||
</NTabs>
|
</NTabs>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { offlineDev } = storeToRefs(settingStore);
|
const { activeRequests } = storeToRefs(settingStore);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ const SERVER_STREAM_PUSH_KEY = 'server-stream-push-query';
|
|||||||
|
|
||||||
const { data: streamPushes } = useQuery({
|
const { data: streamPushes } = useQuery({
|
||||||
queryKey: computed(() => [SERVER_STREAM_PUSH_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
|
queryKey: computed(() => [SERVER_STREAM_PUSH_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
|
||||||
enabled: computed(() => !offlineDev.value && showCard.value),
|
enabled: computed(() => activeRequests.value && showCard.value),
|
||||||
refetchInterval: 30 * 1000,
|
refetchInterval: 30 * 1000,
|
||||||
gcTime: 0,
|
gcTime: 0,
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
@@ -35,8 +35,8 @@ const { data: streamPushes } = useQuery({
|
|||||||
return streamPushes;
|
return streamPushes;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
watch(offlineDev, (offline) => {
|
watch(activeRequests, (active) => {
|
||||||
if (offline) {
|
if (!active) {
|
||||||
queryClient.cancelQueries({ queryKey: [SERVER_STREAM_PUSH_KEY] });
|
queryClient.cancelQueries({ queryKey: [SERVER_STREAM_PUSH_KEY] });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -70,7 +70,7 @@ const streamPushStat = computed(() => {
|
|||||||
<span>推流统计</span>
|
<span>推流统计</span>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<template v-if="offlineDev">
|
<template v-if="!activeRequests">
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { NdmSwitchResultVO, Station } from '@/apis';
|
import type { NdmSwitchResultVO, Station } from '@/apis';
|
||||||
import { DeviceRawCard, SwitchCurrentDiag, SwitchHistoryDiag, SwitchUpdate } from '@/components';
|
import { DeviceRawCard, SwitchCurrentDiag, SwitchHistoryDiag, SwitchUpdate } from '@/components';
|
||||||
|
import { usePermission } from '@/composables';
|
||||||
|
import { PERMISSION_TYPE_LITERALS } from '@/enums';
|
||||||
import { useSettingStore } from '@/stores';
|
import { useSettingStore } from '@/stores';
|
||||||
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
|
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
@@ -16,7 +18,9 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { debugModeEnabled } = storeToRefs(settingStore);
|
const { showDeviceRawData } = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
const { ndmDevice, station } = toRefs(props);
|
const { ndmDevice, station } = toRefs(props);
|
||||||
|
|
||||||
@@ -31,8 +35,8 @@ const activeTabName = ref('当前诊断');
|
|||||||
const onTabChange = (name: string) => {
|
const onTabChange = (name: string) => {
|
||||||
activeTabName.value = name;
|
activeTabName.value = name;
|
||||||
};
|
};
|
||||||
watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
|
watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
|
||||||
if (newDevice.id !== oldDevice.id || !enabled) {
|
if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
|
||||||
activeTabName.value = '当前诊断';
|
activeTabName.value = '当前诊断';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -45,8 +49,8 @@ watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
|
|||||||
<NTabs :value="activeTabName" @update:value="onTabChange">
|
<NTabs :value="activeTabName" @update:value="onTabChange">
|
||||||
<NTab name="当前诊断">当前诊断</NTab>
|
<NTab name="当前诊断">当前诊断</NTab>
|
||||||
<NTab name="历史诊断">历史诊断</NTab>
|
<NTab name="历史诊断">历史诊断</NTab>
|
||||||
<NTab name="修改设备">修改设备</NTab>
|
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
|
||||||
<NTab v-if="debugModeEnabled" name="原始数据">原始数据</NTab>
|
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
|
||||||
</NTabs>
|
</NTabs>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<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, type UseDeviceTreeReturn } from '@/composables';
|
import { useDeviceTree, usePermission, type UseDeviceTreeReturn } from '@/composables';
|
||||||
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType } from '@/enums';
|
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType, PERMISSION_TYPE_LITERALS } from '@/enums';
|
||||||
import { isNvrCluster } from '@/helpers';
|
import { isNvrCluster } from '@/helpers';
|
||||||
import { useDeviceStore, useStationStore } from '@/stores';
|
import { useDeviceStore, usePermissionStore } from '@/stores';
|
||||||
import { watchImmediate } from '@vueuse/core';
|
import { watchDebounced, watchImmediate } from '@vueuse/core';
|
||||||
import destr from 'destr';
|
import destr from 'destr';
|
||||||
import { isFunction } from 'es-toolkit';
|
import { isFunction } from 'es-toolkit';
|
||||||
import {
|
import {
|
||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
type TreeProps,
|
type TreeProps,
|
||||||
} from 'naive-ui';
|
} from 'naive-ui';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { computed, h, nextTick, onBeforeUnmount, ref, toRefs, useTemplateRef, watch, type CSSProperties } from 'vue';
|
import { computed, h, nextTick, onBeforeUnmount, onMounted, ref, toRefs, useTemplateRef, watch, type CSSProperties } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
/**
|
/**
|
||||||
@@ -60,20 +60,22 @@ const { station, events, syncRoute, devicePrefixLabel } = toRefs(props);
|
|||||||
|
|
||||||
const themeVars = useThemeVars();
|
const themeVars = useThemeVars();
|
||||||
|
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
// 设备选择
|
// 设备选择
|
||||||
selectedStationCode,
|
selectedStationCode,
|
||||||
selectedDeviceType,
|
selectedDeviceType,
|
||||||
selectedDevice,
|
selectedDevice,
|
||||||
|
syncFromRoute,
|
||||||
|
syncToRoute,
|
||||||
selectDevice,
|
selectDevice,
|
||||||
// 设备管理
|
// 设备管理
|
||||||
exportDevice,
|
exportDevice,
|
||||||
exportDeviceTemplate,
|
exportDeviceTemplate,
|
||||||
importDevice,
|
importDevice,
|
||||||
deleteDevice,
|
deleteDevice,
|
||||||
} = useDeviceTree({
|
} = useDeviceTree();
|
||||||
syncRoute: computed(() => !!syncRoute.value),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 将 `selectDevice` 函数暴露给父组件
|
// 将 `selectDevice` 函数暴露给父组件
|
||||||
emit('exposeSelectDeviceFn', selectDevice);
|
emit('exposeSelectDeviceFn', selectDevice);
|
||||||
@@ -87,8 +89,9 @@ const onSelectDevice = (device: NdmDeviceResultVO, stationCode: Station['code'])
|
|||||||
emit('afterSelectDevice', device, stationCode);
|
emit('afterSelectDevice', device, stationCode);
|
||||||
};
|
};
|
||||||
|
|
||||||
const stationStore = useStationStore();
|
const permissionStore = usePermissionStore();
|
||||||
const { stations } = storeToRefs(stationStore);
|
const stations = computed(() => permissionStore.stations.VIEW ?? []);
|
||||||
|
|
||||||
const deviceStore = useDeviceStore();
|
const deviceStore = useDeviceStore();
|
||||||
const { lineDevices } = storeToRefs(deviceStore);
|
const { lineDevices } = storeToRefs(deviceStore);
|
||||||
|
|
||||||
@@ -220,13 +223,17 @@ const nodeProps: TreeProps['nodeProps'] = ({ option }) => {
|
|||||||
payload.stopPropagation();
|
payload.stopPropagation();
|
||||||
payload.preventDefault();
|
payload.preventDefault();
|
||||||
|
|
||||||
// 仅当事件列表包含 `manage` 时才显示右键菜单
|
// 如果事件列表不包含 `manage`,则直接结束逻辑
|
||||||
if (!events.value?.includes('manage')) return;
|
if (!events.value?.includes('manage')) return;
|
||||||
|
|
||||||
const { clientX, clientY } = payload;
|
|
||||||
const stationCode = option['stationCode'] as Station['code'];
|
const stationCode = option['stationCode'] as Station['code'];
|
||||||
|
|
||||||
|
// 仅当用户在该车站拥有操作权限时才显示右键菜单
|
||||||
|
if (!hasPermission(stationCode, PERMISSION_TYPE_LITERALS.OPERATION)) return;
|
||||||
|
|
||||||
const deviceType = option['deviceType'] as DeviceType | undefined;
|
const deviceType = option['deviceType'] as DeviceType | undefined;
|
||||||
const device = option['device'] as NdmDeviceResultVO | undefined;
|
const device = option['device'] as NdmDeviceResultVO | undefined;
|
||||||
|
const { clientX, clientY } = payload;
|
||||||
contextmenu.value = { x: clientX, y: clientY, stationCode, deviceType, device };
|
contextmenu.value = { x: clientX, y: clientY, stationCode, deviceType, device };
|
||||||
showContextmenu.value = true;
|
showContextmenu.value = true;
|
||||||
},
|
},
|
||||||
@@ -477,11 +484,38 @@ const onLocateDeviceTree = async () => {
|
|||||||
|
|
||||||
animated.value = true;
|
animated.value = true;
|
||||||
};
|
};
|
||||||
// 渲染全线设备树时,当选择的设备发生变化,则定位设备树
|
|
||||||
|
// 当选择的设备发生变化时,定位设备树,并同步选中状态到路由参数
|
||||||
// 暂时不考虑多次执行的问题,因为当选择的设备在设备树视口内时,不会发生滚动
|
// 暂时不考虑多次执行的问题,因为当选择的设备在设备树视口内时,不会发生滚动
|
||||||
watch(selectedDevice, async () => {
|
watch(selectedDevice, async (newDevice, oldDevice) => {
|
||||||
if (!!station.value) return;
|
if (!!station.value) return;
|
||||||
await onLocateDeviceTree();
|
if (newDevice?.id === oldDevice?.id) return;
|
||||||
|
// console.log('selectedDevice changed');
|
||||||
|
onLocateDeviceTree();
|
||||||
|
syncToRoute();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当全线设备发生变化时,从路由参数同步选中状态
|
||||||
|
// 但lineDevices是shallowRef,因此需要深度侦听才能获取内部变化,
|
||||||
|
// 而单纯的深度侦听又可能会引发性能问题,因此尝试使用防抖侦听
|
||||||
|
watchDebounced(
|
||||||
|
lineDevices,
|
||||||
|
(newLineDevices) => {
|
||||||
|
if (syncRoute.value) {
|
||||||
|
// console.log('lineDevices changed');
|
||||||
|
syncFromRoute(newLineDevices);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
debounce: 500,
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (syncRoute.value) {
|
||||||
|
syncFromRoute(lineDevices.value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,33 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { retentionDaysApi, snapStatusApi, type LineAlarms, type LineDevices, type Station, type VersionInfo } from '@/apis';
|
import { retentionDaysApi, snapStatusApi, type LineAlarms, type LineDevices, type Station, type VersionInfo } from '@/apis';
|
||||||
import { ThemeSwitch } from '@/components';
|
import { ThemeSwitch } from '@/components';
|
||||||
|
import { usePermission } from '@/composables';
|
||||||
import { NDM_ALARM_STORE_ID, NDM_DEVICE_STORE_ID, NDM_STATION_STORE_ID } from '@/constants';
|
import { NDM_ALARM_STORE_ID, NDM_DEVICE_STORE_ID, NDM_STATION_STORE_ID } from '@/constants';
|
||||||
import { usePollingStore, useSettingStore } from '@/stores';
|
import { PERMISSION_TYPE_LITERALS } from '@/enums';
|
||||||
|
import { useSettingStore, useStationStore } from '@/stores';
|
||||||
import { downloadByData, getAppEnvConfig, parseErrorFeedback, sleep } from '@/utils';
|
import { downloadByData, getAppEnvConfig, parseErrorFeedback, sleep } from '@/utils';
|
||||||
import { useMutation } from '@tanstack/vue-query';
|
import { useMutation } from '@tanstack/vue-query';
|
||||||
import { DeleteOutlined, ExportOutlined, ImportOutlined } from '@vicons/antd';
|
|
||||||
import { useEventListener } from '@vueuse/core';
|
import { useEventListener } from '@vueuse/core';
|
||||||
import axios, { isCancel } from 'axios';
|
import axios, { isCancel } from 'axios';
|
||||||
import destr from 'destr';
|
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 { NButton, NButtonGroup, NDivider, NDrawer, NDrawerContent, NDropdown, NFlex, NFormItem, NIcon, NInput, NInputNumber, NModal, NSwitch, NText, 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 { ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
const show = defineModel<boolean>('show', { default: false });
|
const show = defineModel<boolean>('show', { default: false });
|
||||||
|
|
||||||
|
const stationStore = useStationStore();
|
||||||
|
const { stations } = storeToRefs(stationStore);
|
||||||
|
|
||||||
|
const occStation = computed(() => stations.value.find((station) => !!station.occ));
|
||||||
|
|
||||||
const settingsStore = useSettingStore();
|
const settingsStore = useSettingStore();
|
||||||
const { menuCollpased, stationGridCols, debugModeEnabled, offlineDev } = storeToRefs(settingsStore);
|
const { menuCollpased, stationGridCols, debugMode, showDeviceRawData, pollingStations, activeRequests, subscribeMessages, mockUser, useLocalDB } = storeToRefs(settingsStore);
|
||||||
|
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
const versionInfo = ref<VersionInfo>({ version: '', buildTime: '' });
|
const versionInfo = ref<VersionInfo>({ version: '', buildTime: '' });
|
||||||
|
|
||||||
@@ -123,11 +132,11 @@ const enableDebugMode = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showDebugCodeModal.value = false;
|
showDebugCodeModal.value = false;
|
||||||
settingsStore.enableDebugMode();
|
debugMode.value = true;
|
||||||
};
|
};
|
||||||
const disableDebugMode = () => {
|
const disableDebugMode = () => {
|
||||||
showDebugCodeModal.value = false;
|
showDebugCodeModal.value = false;
|
||||||
settingsStore.disableDebugMode();
|
debugMode.value = false;
|
||||||
};
|
};
|
||||||
useEventListener('keydown', (event) => {
|
useEventListener('keydown', (event) => {
|
||||||
const { ctrlKey, altKey, code } = event;
|
const { ctrlKey, altKey, code } = event;
|
||||||
@@ -138,28 +147,18 @@ useEventListener('keydown', (event) => {
|
|||||||
|
|
||||||
const expectToShowDebugCodeInput = ref(false);
|
const expectToShowDebugCodeInput = ref(false);
|
||||||
const onModalAfterEnter = () => {
|
const onModalAfterEnter = () => {
|
||||||
expectToShowDebugCodeInput.value = !debugModeEnabled.value;
|
expectToShowDebugCodeInput.value = !debugMode.value;
|
||||||
};
|
};
|
||||||
const onModalAfterLeave = () => {
|
const onModalAfterLeave = () => {
|
||||||
expectToShowDebugCodeInput.value = false;
|
expectToShowDebugCodeInput.value = false;
|
||||||
debugCode.value = '';
|
debugCode.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const pollingStore = usePollingStore();
|
|
||||||
const { pollingEnabled } = storeToRefs(pollingStore);
|
|
||||||
const onPollingEnabledUpdate = (enabled: boolean) => {
|
|
||||||
if (enabled) {
|
|
||||||
pollingStore.startPolling();
|
|
||||||
} else {
|
|
||||||
pollingStore.stopPolling();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
type IndexedDbStoreId = typeof NDM_STATION_STORE_ID | typeof NDM_DEVICE_STORE_ID | typeof NDM_ALARM_STORE_ID;
|
type IndexedDbStoreId = typeof NDM_STATION_STORE_ID | typeof NDM_DEVICE_STORE_ID | typeof NDM_ALARM_STORE_ID;
|
||||||
type IndexedDbStoreStates = {
|
type IndexedDbStoreStates = {
|
||||||
[NDM_STATION_STORE_ID]: { stations: Station[] };
|
[NDM_STATION_STORE_ID]: { stations: Station[] };
|
||||||
[NDM_DEVICE_STORE_ID]: { lineDevices: LineDevices };
|
[NDM_DEVICE_STORE_ID]: { lineDevices: LineDevices };
|
||||||
[NDM_ALARM_STORE_ID]: { lineAlarms: LineAlarms; unreadLineAlarms: LineAlarms };
|
[NDM_ALARM_STORE_ID]: { lineAlarms: LineAlarms };
|
||||||
};
|
};
|
||||||
const exportFromIndexedDB = async <K extends IndexedDbStoreId>(storeId: K, options?: { errorMsg?: string }) => {
|
const exportFromIndexedDB = async <K extends IndexedDbStoreId>(storeId: K, options?: { errorMsg?: string }) => {
|
||||||
const { errorMsg } = options ?? {};
|
const { errorMsg } = options ?? {};
|
||||||
@@ -172,8 +171,9 @@ const exportFromIndexedDB = async <K extends IndexedDbStoreId>(storeId: K, optio
|
|||||||
};
|
};
|
||||||
const importToIndexedDB = async <K extends IndexedDbStoreId>(storeId: K, options?: { successMsg?: string; errorMsg?: string }) => {
|
const importToIndexedDB = async <K extends IndexedDbStoreId>(storeId: K, options?: { successMsg?: string; errorMsg?: string }) => {
|
||||||
const { successMsg, errorMsg } = options ?? {};
|
const { successMsg, errorMsg } = options ?? {};
|
||||||
pollingStore.stopPolling();
|
pollingStations.value = false;
|
||||||
offlineDev.value = true;
|
activeRequests.value = false;
|
||||||
|
subscribeMessages.value = false;
|
||||||
const fileInput = document.createElement('input');
|
const fileInput = document.createElement('input');
|
||||||
fileInput.type = 'file';
|
fileInput.type = 'file';
|
||||||
fileInput.accept = '.json';
|
fileInput.accept = '.json';
|
||||||
@@ -196,8 +196,9 @@ const importToIndexedDB = async <K extends IndexedDbStoreId>(storeId: K, options
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
const deleteFromIndexedDB = async (storeId: IndexedDbStoreId) => {
|
const deleteFromIndexedDB = async (storeId: IndexedDbStoreId) => {
|
||||||
pollingStore.stopPolling();
|
pollingStations.value = false;
|
||||||
offlineDev.value = true;
|
activeRequests.value = false;
|
||||||
|
subscribeMessages.value = false;
|
||||||
await localforage.removeItem(storeId).catch((error) => {
|
await localforage.removeItem(storeId).catch((error) => {
|
||||||
window.$message.error(`${error}`);
|
window.$message.error(`${error}`);
|
||||||
return;
|
return;
|
||||||
@@ -266,8 +267,8 @@ const onSelectDropdownOption = (key: string, option: DropdownOption) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
watch([offlineDev, show], ([offline, entered]) => {
|
watch([activeRequests, show], ([active, entered]) => {
|
||||||
if (!offline) {
|
if (!active) return;
|
||||||
if (entered) {
|
if (entered) {
|
||||||
getRetentionDays();
|
getRetentionDays();
|
||||||
getSnapStatus();
|
getSnapStatus();
|
||||||
@@ -275,7 +276,6 @@ watch([offlineDev, show], ([offline, entered]) => {
|
|||||||
abortControllers.value.retentionDays.abort();
|
abortControllers.value.retentionDays.abort();
|
||||||
abortControllers.value.snapStatus.abort();
|
abortControllers.value.snapStatus.abort();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
const onDrawerAfterEnter = () => {
|
const onDrawerAfterEnter = () => {
|
||||||
getVersionInfo();
|
getVersionInfo();
|
||||||
@@ -303,6 +303,7 @@ const onDrawerAfterLeave = () => {
|
|||||||
<NInputNumber v-model:value="stationGridCols" :min="1" :max="10" />
|
<NInputNumber v-model:value="stationGridCols" :min="1" :max="10" />
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
|
|
||||||
|
<template v-if="!!occStation && hasPermission(occStation.code, PERMISSION_TYPE_LITERALS.OPERATION)">
|
||||||
<NDivider>告警</NDivider>
|
<NDivider>告警</NDivider>
|
||||||
<NFormItem label="告警画面截图保留天数" label-placement="left">
|
<NFormItem label="告警画面截图保留天数" label-placement="left">
|
||||||
<NFlex justify="space-between" align="center" style="width: 100%">
|
<NFlex justify="space-between" align="center" style="width: 100%">
|
||||||
@@ -322,21 +323,40 @@ const onDrawerAfterLeave = () => {
|
|||||||
</NButtonGroup>
|
</NButtonGroup>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-if="debugModeEnabled">
|
<template v-if="debugMode">
|
||||||
<NDivider title-placement="center">调试</NDivider>
|
<NDivider title-placement="center">调试</NDivider>
|
||||||
<NFormItem label="启用轮询" label-placement="left">
|
<NFormItem label="调试模式" label-placement="left">
|
||||||
<NSwitch size="small" :value="pollingEnabled" @update:value="onPollingEnabledUpdate" />
|
<NSwitch size="small" v-model:value="debugMode" />
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NFormItem label="离线开发" label-placement="left">
|
<NDivider title-placement="left" dashed>数据设置</NDivider>
|
||||||
<NSwitch size="small" v-model:value="offlineDev" />
|
<NFormItem label="显示设备原始数据" label-placement="left">
|
||||||
|
<NSwitch size="small" v-model:value="showDeviceRawData" />
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NFormItem label="本地数据库" label-placement="left">
|
<NDivider title-placement="left" dashed>网络设置</NDivider>
|
||||||
|
<NFormItem label="轮询车站" label-placement="left">
|
||||||
|
<NSwitch size="small" v-model:value="pollingStations" />
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="主动请求" label-placement="left">
|
||||||
|
<NSwitch size="small" v-model:value="activeRequests" />
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="订阅消息" label-placement="left">
|
||||||
|
<NSwitch size="small" v-model:value="subscribeMessages" />
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="模拟用户" label-placement="left">
|
||||||
|
<NSwitch size="small" v-model:value="mockUser" />
|
||||||
|
</NFormItem>
|
||||||
|
<NDivider title-placement="left" dashed>数据库设置</NDivider>
|
||||||
|
<NFormItem label="直接操作本地数据库" label-placement="left">
|
||||||
|
<NSwitch size="small" v-model:value="useLocalDB" />
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="数据操作" label-placement="left">
|
||||||
<NFlex>
|
<NFlex>
|
||||||
<NDropdown trigger="click" :options="exportDropdownOptions" @select="onSelectDropdownOption">
|
<NDropdown trigger="click" :options="exportDropdownOptions" @select="onSelectDropdownOption">
|
||||||
<NButton secondary size="small">
|
<NButton secondary size="small">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="ExportOutlined" />
|
<NIcon :component="DownloadIcon" />
|
||||||
</template>
|
</template>
|
||||||
<template #default>导出</template>
|
<template #default>导出</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
@@ -344,7 +364,7 @@ const onDrawerAfterLeave = () => {
|
|||||||
<NDropdown trigger="click" :options="importDropdownOptions" @select="onSelectDropdownOption">
|
<NDropdown trigger="click" :options="importDropdownOptions" @select="onSelectDropdownOption">
|
||||||
<NButton secondary size="small">
|
<NButton secondary size="small">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="ImportOutlined" />
|
<NIcon :component="UploadIcon" />
|
||||||
</template>
|
</template>
|
||||||
<template #default>导入</template>
|
<template #default>导入</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
@@ -352,7 +372,7 @@ const onDrawerAfterLeave = () => {
|
|||||||
<NDropdown trigger="click" :options="deleteDropdownOptions" @select="onSelectDropdownOption">
|
<NDropdown trigger="click" :options="deleteDropdownOptions" @select="onSelectDropdownOption">
|
||||||
<NButton secondary size="small">
|
<NButton secondary size="small">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="DeleteOutlined" />
|
<NIcon :component="Trash2Icon" />
|
||||||
</template>
|
</template>
|
||||||
<template #default>删除</template>
|
<template #default>删除</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
@@ -371,7 +391,7 @@ const onDrawerAfterLeave = () => {
|
|||||||
|
|
||||||
<NModal v-model:show="showDebugCodeModal" preset="dialog" type="info" @after-enter="onModalAfterEnter" @after-leave="onModalAfterLeave">
|
<NModal v-model:show="showDebugCodeModal" preset="dialog" type="info" @after-enter="onModalAfterEnter" @after-leave="onModalAfterLeave">
|
||||||
<template #header>
|
<template #header>
|
||||||
<NText v-if="!debugModeEnabled">请输入调试码</NText>
|
<NText v-if="!debugMode">请输入调试码</NText>
|
||||||
<NText v-else>确认关闭调试模式</NText>
|
<NText v-else>确认关闭调试模式</NText>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
@@ -379,7 +399,7 @@ const onDrawerAfterLeave = () => {
|
|||||||
</template>
|
</template>
|
||||||
<template #action>
|
<template #action>
|
||||||
<NButton @click="showDebugCodeModal = false">取消</NButton>
|
<NButton @click="showDebugCodeModal = false">取消</NButton>
|
||||||
<NButton v-if="!debugModeEnabled" type="primary" @click="enableDebugMode">启用</NButton>
|
<NButton v-if="!debugMode" type="primary" @click="enableDebugMode">启用</NButton>
|
||||||
<NButton v-else type="primary" @click="disableDebugMode">确认</NButton>
|
<NButton v-else type="primary" @click="disableDebugMode">确认</NButton>
|
||||||
</template>
|
</template>
|
||||||
</NModal>
|
</NModal>
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { storeToRefs } from 'pinia';
|
|||||||
import type { ComponentInstance } from 'vue';
|
import type { ComponentInstance } from 'vue';
|
||||||
|
|
||||||
const settingsStore = useSettingStore();
|
const settingsStore = useSettingStore();
|
||||||
const { darkThemeEnabled } = storeToRefs(settingsStore);
|
const { darkMode } = storeToRefs(settingsStore);
|
||||||
|
|
||||||
// 使外部能够获取NSwitch的类型提示
|
// 使外部能够获取NSwitch的类型提示
|
||||||
defineExpose({} as ComponentInstance<typeof NSwitch>);
|
defineExpose({} as ComponentInstance<typeof NSwitch>);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NSwitch v-model:value="darkThemeEnabled">
|
<NSwitch v-model:value="darkMode">
|
||||||
<template #unchecked-icon>
|
<template #unchecked-icon>
|
||||||
<NIcon>
|
<NIcon>
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './device';
|
export * from './device';
|
||||||
export * from './global';
|
export * from './global';
|
||||||
|
export * from './permission';
|
||||||
export * from './station';
|
export * from './station';
|
||||||
|
|||||||
6
src/components/permission/index.ts
Normal file
6
src/components/permission/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type { ComponentInstance } from 'vue';
|
||||||
|
import PermissionConfigModal from './permission-config-modal.vue';
|
||||||
|
|
||||||
|
export type PermissionConfigModalProps = ComponentInstance<typeof PermissionConfigModal>['$props'];
|
||||||
|
|
||||||
|
export { PermissionConfigModal };
|
||||||
302
src/components/permission/permission-config-modal.vue
Normal file
302
src/components/permission/permission-config-modal.vue
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { detailBaseEmployeeApi, modifyPermissionApi, pagePermissionApi, type BaseEmployeeResultVO, type NdmPermissionResultVO, type NdmPermissionSaveVO, type Station } from '@/apis';
|
||||||
|
import { PERMISSION_TYPE_LITERALS, PERMISSION_TYPE_NAMES, type PermissionType } from '@/enums';
|
||||||
|
import { useStationStore } from '@/stores';
|
||||||
|
import { parseErrorFeedback } from '@/utils';
|
||||||
|
import { useMutation } from '@tanstack/vue-query';
|
||||||
|
import { objectEntries } from '@vueuse/core';
|
||||||
|
import { isCancel } from 'axios';
|
||||||
|
import { cloneDeep } from 'es-toolkit';
|
||||||
|
import { NButton, NCheckbox, NDataTable, NFlex, NModal, NText, type DataTableColumn, type DataTableColumns } from 'naive-ui';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { computed, h, ref, toRefs } from 'vue';
|
||||||
|
|
||||||
|
type NdmPermissionSaveOrResultVO = NdmPermissionSaveVO | NdmPermissionResultVO;
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
employeeId?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const stationStore = useStationStore();
|
||||||
|
const { stations } = storeToRefs(stationStore);
|
||||||
|
|
||||||
|
const show = defineModel<boolean>('show', { default: false });
|
||||||
|
|
||||||
|
const { employeeId } = toRefs(props);
|
||||||
|
|
||||||
|
const abortController = ref<AbortController>(new AbortController());
|
||||||
|
|
||||||
|
const employee = ref<BaseEmployeeResultVO>();
|
||||||
|
const { mutateAsync: getEmployeeAsync } = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
abortController.value.abort();
|
||||||
|
abortController.value = new AbortController();
|
||||||
|
|
||||||
|
if (!employeeId.value) return;
|
||||||
|
|
||||||
|
const signal = abortController.value.signal;
|
||||||
|
|
||||||
|
const data = await detailBaseEmployeeApi(employeeId.value, { signal });
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (!data) return;
|
||||||
|
employee.value = data;
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
if (isCancel(error)) return;
|
||||||
|
console.error(error);
|
||||||
|
const errorFeedback = parseErrorFeedback(error);
|
||||||
|
window.$message.error(errorFeedback);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 从后端获取的原始权限列表
|
||||||
|
const originalList = ref<NdmPermissionResultVO[]>([]);
|
||||||
|
// 当前用户配置的权限列表
|
||||||
|
const currentList = ref<NdmPermissionSaveOrResultVO[]>([]);
|
||||||
|
|
||||||
|
const { mutate: getPermissions, isPending: permissionsLoading } = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (!employeeId.value) throw new Error('员工ID不能为空');
|
||||||
|
|
||||||
|
abortController.value.abort();
|
||||||
|
abortController.value = new AbortController();
|
||||||
|
|
||||||
|
const signal = abortController.value.signal;
|
||||||
|
|
||||||
|
const data = await pagePermissionApi(
|
||||||
|
{
|
||||||
|
model: {
|
||||||
|
employeeId: employeeId.value,
|
||||||
|
},
|
||||||
|
current: 1,
|
||||||
|
size: Object.keys(PERMISSION_TYPE_LITERALS).length * stations.value.length,
|
||||||
|
},
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (!data) return;
|
||||||
|
const { records } = data;
|
||||||
|
originalList.value = cloneDeep(records);
|
||||||
|
currentList.value = cloneDeep(records);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
if (isCancel(error)) return;
|
||||||
|
console.error(error);
|
||||||
|
const errorFeedback = parseErrorFeedback(error);
|
||||||
|
window.$message.error(errorFeedback);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onUpdatePermissionChecked = (checked: boolean, stationCode: Station['code'], permissionType: PermissionType) => {
|
||||||
|
if (!employeeId.value) return;
|
||||||
|
if (checked) {
|
||||||
|
const existed = currentList.value.some((permission) => permission.stationCode === stationCode && permission.type === permissionType);
|
||||||
|
if (!existed) {
|
||||||
|
const saveVO: NdmPermissionSaveVO = {
|
||||||
|
employeeId: employeeId.value,
|
||||||
|
stationCode,
|
||||||
|
type: permissionType,
|
||||||
|
};
|
||||||
|
currentList.value.push(saveVO);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const index = currentList.value.findIndex((permission) => permission.stationCode === stationCode && permission.type === permissionType);
|
||||||
|
if (index !== -1) {
|
||||||
|
currentList.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableColumns = computed<DataTableColumns<Station>>(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: () => {
|
||||||
|
const permissionCount = currentList.value.length;
|
||||||
|
const permissionTypeCount = objectEntries(PERMISSION_TYPE_LITERALS).length;
|
||||||
|
const checked = permissionCount === stations.value.length * permissionTypeCount;
|
||||||
|
const indeterminate = permissionCount > 0 && permissionCount < stations.value.length * permissionTypeCount;
|
||||||
|
return h(NCheckbox, {
|
||||||
|
checked,
|
||||||
|
indeterminate,
|
||||||
|
onUpdateChecked: (checked) => {
|
||||||
|
objectEntries(PERMISSION_TYPE_LITERALS).forEach(([permissionType]) => {
|
||||||
|
stations.value.forEach((station) => {
|
||||||
|
onUpdatePermissionChecked(checked, station.code, permissionType);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
key: 'row-check',
|
||||||
|
align: 'center',
|
||||||
|
width: 60,
|
||||||
|
fixed: 'left',
|
||||||
|
render: (rowData) => {
|
||||||
|
const { code: stationCode } = rowData;
|
||||||
|
const permissionTypeCount = objectEntries(PERMISSION_TYPE_LITERALS).length;
|
||||||
|
const stationCheckedPermissions = currentList.value.filter((permission) => permission.stationCode === stationCode);
|
||||||
|
const checked = stationCheckedPermissions.length === permissionTypeCount;
|
||||||
|
const indeterminate = stationCheckedPermissions.length > 0 && stationCheckedPermissions.length < permissionTypeCount;
|
||||||
|
return h(NCheckbox, {
|
||||||
|
checked,
|
||||||
|
indeterminate,
|
||||||
|
onUpdateChecked: (checked) => {
|
||||||
|
objectEntries(PERMISSION_TYPE_LITERALS).forEach(([permissionType]) => {
|
||||||
|
onUpdatePermissionChecked(checked, stationCode, permissionType);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ title: '车站编号', key: 'code', align: 'center', width: 120 },
|
||||||
|
{ title: '车站名称', key: 'name', align: 'center', width: 360 },
|
||||||
|
// 「权限」列
|
||||||
|
...objectEntries(PERMISSION_TYPE_NAMES).map<DataTableColumn<Station>>(([permissionType, title]) => ({
|
||||||
|
title: () => {
|
||||||
|
const permissionCount = currentList.value.filter((permission) => permission.type === permissionType).length;
|
||||||
|
const checked = permissionCount === stations.value.length;
|
||||||
|
const indeterminate = permissionCount > 0 && permissionCount < stations.value.length;
|
||||||
|
return h(
|
||||||
|
NFlex,
|
||||||
|
{
|
||||||
|
justify: 'center',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () => [
|
||||||
|
h(NCheckbox, {
|
||||||
|
checked,
|
||||||
|
indeterminate,
|
||||||
|
onUpdateChecked: (checked) => {
|
||||||
|
stations.value.forEach((station) => {
|
||||||
|
onUpdatePermissionChecked(checked, station.code, permissionType);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
h('span', title),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
key: permissionType,
|
||||||
|
align: 'center',
|
||||||
|
render: (rowData) => {
|
||||||
|
const { code: stationCode } = rowData;
|
||||||
|
return h(NCheckbox, {
|
||||||
|
checked: currentList.value.some((permission) => permission.stationCode === stationCode && permission.type === permissionType),
|
||||||
|
onUpdateChecked: (checked) => onUpdatePermissionChecked(checked, stationCode, permissionType),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: savePermissions, isPending: permissionsSaving } = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (!employeeId.value) throw new Error('员工ID不能为空');
|
||||||
|
|
||||||
|
abortController.value.abort();
|
||||||
|
abortController.value = new AbortController();
|
||||||
|
|
||||||
|
const signal = abortController.value.signal;
|
||||||
|
|
||||||
|
// 执行diff计算,生成需要保存的权限列表和需要删除的权限ID列表
|
||||||
|
const saveList: NdmPermissionSaveVO[] = [];
|
||||||
|
const removeList: string[] = [];
|
||||||
|
// 遍历当前状态,如果权限不在原始权限列表中,说明是需要新增的权限
|
||||||
|
currentList.value.forEach((permission) => {
|
||||||
|
const { stationCode, type } = permission;
|
||||||
|
if (!stationCode || !type) return;
|
||||||
|
if (!originalList.value.some((permission) => permission.stationCode === stationCode && permission.type === type)) {
|
||||||
|
saveList.push({
|
||||||
|
employeeId: employeeId.value,
|
||||||
|
stationCode,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 遍历原始状态,如果权限不在当前状态中,说明是需要删除的权限
|
||||||
|
originalList.value.forEach((permission) => {
|
||||||
|
const { id, stationCode, type } = permission;
|
||||||
|
if (!id) return;
|
||||||
|
if (!currentList.value.some((permission) => permission.stationCode === stationCode && permission.type === type)) {
|
||||||
|
removeList.push(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await modifyPermissionApi(
|
||||||
|
{
|
||||||
|
employeeId: employeeId.value,
|
||||||
|
saveList,
|
||||||
|
removeList,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
window.$message.success('权限配置保存成功');
|
||||||
|
getPermissions();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
if (isCancel(error)) return;
|
||||||
|
console.error(error);
|
||||||
|
const errorFeedback = parseErrorFeedback(error);
|
||||||
|
window.$message.error(errorFeedback);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onAfterEnter = () => {
|
||||||
|
getEmployeeAsync().then(() => getPermissions());
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAfterLeave = () => {
|
||||||
|
employee.value = undefined;
|
||||||
|
originalList.value = [];
|
||||||
|
currentList.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
abortController.value.abort();
|
||||||
|
show.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NModal
|
||||||
|
v-model:show="show"
|
||||||
|
preset="card"
|
||||||
|
style="width: 100vw; height: 100vh"
|
||||||
|
:content-style="{ height: '100%', overflow: 'hidden' }"
|
||||||
|
:close-on-esc="false"
|
||||||
|
:mask-closable="false"
|
||||||
|
:auto-focus="false"
|
||||||
|
@after-enter="onAfterEnter"
|
||||||
|
@after-leave="onAfterLeave"
|
||||||
|
@close="onClose"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span>{{ `配置权限 - ${employee?.realName ?? ''}` }}</span>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<NDataTable flex-height style="height: 100%" :columns="tableColumns" :data="stations" :loading="permissionsLoading" :single-line="false" />
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<NText depth="3" style="font-size: smaller">*未勾选任何权限的用户将被认为拥有所有权限</NText>
|
||||||
|
</template>
|
||||||
|
<template #action>
|
||||||
|
<NFlex justify="end">
|
||||||
|
<NButton size="small" @click="onClose">取消</NButton>
|
||||||
|
<NButton type="primary" size="small" :loading="permissionsSaving" @click="() => savePermissions()">保存</NButton>
|
||||||
|
</NFlex>
|
||||||
|
</template>
|
||||||
|
</NModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getRecordCheckApi, type NdmNvrResultVO, type Station } from '@/apis';
|
import { batchExportRecordCheckApi, getRecordCheckApi, pageDefParameterApi, type NdmNvrResultVO, type Station } from '@/apis';
|
||||||
import { exportRecordDiagCsv, isNvrCluster, transformRecordChecks } from '@/helpers';
|
import { exportRecordDiagCsv, isNvrCluster, transformRecordChecks } from '@/helpers';
|
||||||
import { useDeviceStore } from '@/stores';
|
import { useDeviceStore } from '@/stores';
|
||||||
import { 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 { NButton, NGrid, NGridItem, NModal, NScrollbar, NSpin } from 'naive-ui';
|
import dayjs from 'dayjs';
|
||||||
|
import { NButton, NFlex, NGrid, NGridItem, NModal, NScrollbar, NSpin } from 'naive-ui';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { computed, ref, toRefs } from 'vue';
|
import { computed, ref, toRefs } from 'vue';
|
||||||
|
|
||||||
@@ -60,7 +61,10 @@ const { mutate: exportRecordDiags, isPending: exporting } = useMutation({
|
|||||||
return checks;
|
return checks;
|
||||||
},
|
},
|
||||||
onSuccess: (checks, { stationCode }) => {
|
onSuccess: (checks, { stationCode }) => {
|
||||||
if (!checks || checks.length === 0) return;
|
if (!checks || checks.length === 0) {
|
||||||
|
window.$message.info(`没有录像诊断数据`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const recordDiags = transformRecordChecks(checks);
|
const recordDiags = transformRecordChecks(checks);
|
||||||
exportRecordDiagCsv(recordDiags, nvrClusterRecord.value[stationCode]?.stationName ?? '');
|
exportRecordDiagCsv(recordDiags, nvrClusterRecord.value[stationCode]?.stationName ?? '');
|
||||||
},
|
},
|
||||||
@@ -72,7 +76,48 @@ const { mutate: exportRecordDiags, isPending: exporting } = useMutation({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
if (isCancel(error)) return;
|
||||||
|
console.error(error);
|
||||||
|
const errorFeedback = parseErrorFeedback(error);
|
||||||
|
window.$message.error(errorFeedback);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const onAfterLeave = () => {
|
const onAfterLeave = () => {
|
||||||
|
abortController.value.abort();
|
||||||
emit('afterLeave');
|
emit('afterLeave');
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -92,6 +137,11 @@ const onAfterLeave = () => {
|
|||||||
</NSpin>
|
</NSpin>
|
||||||
</NScrollbar>
|
</NScrollbar>
|
||||||
</template>
|
</template>
|
||||||
|
<template #action>
|
||||||
|
<NFlex justify="flex-end" align="center">
|
||||||
|
<NButton secondary :loading="batchExporting" @click="() => batchExportRecordCheck()">导出全部</NButton>
|
||||||
|
</NFlex>
|
||||||
|
</template>
|
||||||
</NModal>
|
</NModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Station, StationAlarms, StationDevices } from '@/apis';
|
import type { Station, StationAlarms, StationDevices } from '@/apis';
|
||||||
import { DEVICE_TYPE_LITERALS } from '@/enums';
|
import { usePermission } from '@/composables';
|
||||||
import { EllipsisOutlined, MoreOutlined } from '@vicons/antd';
|
import { DEVICE_TYPE_LITERALS, PERMISSION_TYPE_LITERALS } from '@/enums';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { isFunction } from 'es-toolkit';
|
import { isFunction } from 'es-toolkit';
|
||||||
|
import { MoreHorizontalIcon, MoreVerticalIcon } from 'lucide-vue-next';
|
||||||
import { NButton, NCard, NCheckbox, NDropdown, NFlex, NIcon, NTag, NTooltip, useThemeVars, type DropdownOption } from 'naive-ui';
|
import { NButton, NCard, NCheckbox, NDropdown, NFlex, NIcon, NTag, NTooltip, useThemeVars, type DropdownOption } from 'naive-ui';
|
||||||
import { computed, toRefs } from 'vue';
|
import { computed, toRefs } from 'vue';
|
||||||
|
|
||||||
@@ -24,6 +25,8 @@ const emit = defineEmits<{
|
|||||||
clickConfig: [station: Station];
|
clickConfig: [station: Station];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
const { station, devices, alarms, selectable } = toRefs(props);
|
const { station, devices, alarms, selectable } = toRefs(props);
|
||||||
|
|
||||||
const onlineDeviceCount = computed(() => {
|
const onlineDeviceCount = computed(() => {
|
||||||
@@ -71,7 +74,7 @@ const openDeviceConfigModal = () => {
|
|||||||
emit('clickConfig', station.value);
|
emit('clickConfig', station.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const dropdownOptions: DropdownOption[] = [
|
const dropdownOptions = computed<DropdownOption[]>(() => [
|
||||||
{
|
{
|
||||||
label: '视频平台',
|
label: '视频平台',
|
||||||
key: 'video-platform',
|
key: 'video-platform',
|
||||||
@@ -80,9 +83,10 @@ const dropdownOptions: DropdownOption[] = [
|
|||||||
{
|
{
|
||||||
label: '设备配置',
|
label: '设备配置',
|
||||||
key: 'device-config',
|
key: 'device-config',
|
||||||
|
show: hasPermission(station.value.code, PERMISSION_TYPE_LITERALS.OPERATION),
|
||||||
onSelect: openDeviceConfigModal,
|
onSelect: openDeviceConfigModal,
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
|
|
||||||
const onSelectDropdownOption = (key: string, option: DropdownOption) => {
|
const onSelectDropdownOption = (key: string, option: DropdownOption) => {
|
||||||
const onSelect = option['onSelect'];
|
const onSelect = option['onSelect'];
|
||||||
@@ -116,7 +120,7 @@ const onSelectDropdownOption = (key: string, option: DropdownOption) => {
|
|||||||
<NDropdown trigger="click" :options="dropdownOptions" @select="onSelectDropdownOption">
|
<NDropdown trigger="click" :options="dropdownOptions" @select="onSelectDropdownOption">
|
||||||
<NButton quaternary size="tiny" :focusable="false">
|
<NButton quaternary size="tiny" :focusable="false">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="MoreOutlined" />
|
<NIcon :component="MoreVerticalIcon" />
|
||||||
</template>
|
</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
</NDropdown>
|
</NDropdown>
|
||||||
@@ -129,7 +133,7 @@ const onSelectDropdownOption = (key: string, option: DropdownOption) => {
|
|||||||
<span>{{ deviceCount }} 台设备</span>
|
<span>{{ deviceCount }} 台设备</span>
|
||||||
<NButton quaternary size="tiny" :focusable="false" @click="() => emit('clickDetail', 'device', station)">
|
<NButton quaternary size="tiny" :focusable="false" @click="() => emit('clickDetail', 'device', station)">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="EllipsisOutlined" />
|
<NIcon :component="MoreHorizontalIcon" />
|
||||||
</template>
|
</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
@@ -143,7 +147,7 @@ const onSelectDropdownOption = (key: string, option: DropdownOption) => {
|
|||||||
<!-- 占位按钮,对齐布局 -->
|
<!-- 占位按钮,对齐布局 -->
|
||||||
<NButton quaternary size="tiny" :focusable="false" style="visibility: hidden">
|
<NButton quaternary size="tiny" :focusable="false" style="visibility: hidden">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="EllipsisOutlined" />
|
<NIcon :component="MoreHorizontalIcon" />
|
||||||
</template>
|
</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
@@ -153,7 +157,7 @@ const onSelectDropdownOption = (key: string, option: DropdownOption) => {
|
|||||||
<span :style="{ color: alarmCount > 0 ? themeVars.warningColor : '' }">今日 {{ alarmCount }} 条告警</span>
|
<span :style="{ color: alarmCount > 0 ? themeVars.warningColor : '' }">今日 {{ alarmCount }} 条告警</span>
|
||||||
<NButton quaternary size="tiny" :focusable="false" @click="() => emit('clickDetail', 'alarm', station)">
|
<NButton quaternary size="tiny" :focusable="false" @click="() => emit('clickDetail', 'alarm', station)">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="EllipsisOutlined" />
|
<NIcon :component="MoreHorizontalIcon" />
|
||||||
</template>
|
</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { Station, SyncCameraResult } from '@/apis';
|
||||||
|
import { usePermissionStore } from '@/stores';
|
||||||
import { watchDebounced } from '@vueuse/core';
|
import { watchDebounced } from '@vueuse/core';
|
||||||
|
import { EditIcon, PlusCircleIcon, Trash2Icon } from 'lucide-vue-next';
|
||||||
import { NFlex, NIcon, NList, NListItem, NModal, NScrollbar, NStatistic, NText, NThing } from 'naive-ui';
|
import { NFlex, NIcon, NList, NListItem, NModal, NScrollbar, NStatistic, NText, NThing } from 'naive-ui';
|
||||||
import { computed, ref, toRefs } from 'vue';
|
import { computed, ref, toRefs } from 'vue';
|
||||||
import { useStationStore } from '@/stores';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import type { Station, SyncCameraResult } from '@/apis';
|
|
||||||
import { DeleteFilled, EditFilled, PlusCircleFilled } from '@vicons/antd';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
syncCameraResult: Record<Station['code'], SyncCameraResult>;
|
syncCameraResult: Record<Station['code'], SyncCameraResult>;
|
||||||
@@ -15,8 +14,8 @@ const emit = defineEmits<{
|
|||||||
afterLeave: [];
|
afterLeave: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const stationStore = useStationStore();
|
const permissionStore = usePermissionStore();
|
||||||
const { stations } = storeToRefs(stationStore);
|
const stations = computed(() => permissionStore.stations.VIEW ?? []);
|
||||||
|
|
||||||
const { syncCameraResult } = toRefs(props);
|
const { syncCameraResult } = toRefs(props);
|
||||||
|
|
||||||
@@ -61,19 +60,19 @@ const syncList = computed(() => {
|
|||||||
<NFlex justify="space-around" :size="24" style="margin-top: 8px">
|
<NFlex justify="space-around" :size="24" style="margin-top: 8px">
|
||||||
<NStatistic label="新增">
|
<NStatistic label="新增">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<NIcon :component="PlusCircleFilled" />
|
<NIcon :component="PlusCircleIcon" />
|
||||||
</template>
|
</template>
|
||||||
{{ insertList.length }}
|
{{ insertList.length }}
|
||||||
</NStatistic>
|
</NStatistic>
|
||||||
<NStatistic label="更新">
|
<NStatistic label="更新">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<NIcon :component="EditFilled" />
|
<NIcon :component="EditIcon" />
|
||||||
</template>
|
</template>
|
||||||
{{ updateList.length }}
|
{{ updateList.length }}
|
||||||
</NStatistic>
|
</NStatistic>
|
||||||
<NStatistic label="删除">
|
<NStatistic label="删除">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<NIcon :component="DeleteFilled" />
|
<NIcon :component="Trash2Icon" />
|
||||||
</template>
|
</template>
|
||||||
{{ deleteList.length }}
|
{{ deleteList.length }}
|
||||||
</NStatistic>
|
</NStatistic>
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
|
import { usePermission } from '../permission';
|
||||||
import { deleteCameraIgnoreApi, pageCameraIgnoreApi, saveCameraIgnoreApi, updateDeviceAlarmLogApi, type NdmDeviceAlarmLogResultVO } from '@/apis';
|
import { deleteCameraIgnoreApi, pageCameraIgnoreApi, saveCameraIgnoreApi, updateDeviceAlarmLogApi, type NdmDeviceAlarmLogResultVO } from '@/apis';
|
||||||
import { DEVICE_TYPE_LITERALS, tryGetDeviceType } from '@/enums';
|
import { DEVICE_TYPE_LITERALS, PERMISSION_TYPE_LITERALS, tryGetDeviceType } from '@/enums';
|
||||||
import { parseErrorFeedback } from '@/utils';
|
import { parseErrorFeedback } from '@/utils';
|
||||||
import { useMutation } from '@tanstack/vue-query';
|
import { useMutation } from '@tanstack/vue-query';
|
||||||
import { NButton, NFlex, NPopconfirm, type DataTableColumn, type DataTableRowData } from 'naive-ui';
|
import { NButton, NFlex, NPopconfirm, type DataTableColumn, type DataTableRowData } from 'naive-ui';
|
||||||
import { h, type Ref } from 'vue';
|
import { h, type Ref } from 'vue';
|
||||||
|
|
||||||
export const useAlarmActionColumn = (tableData: Ref<DataTableRowData[]>) => {
|
export const useAlarmActionColumn = (tableData: Ref<DataTableRowData[]>) => {
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
const { mutate: confirmAlarm } = useMutation({
|
const { mutate: confirmAlarm } = useMutation({
|
||||||
mutationFn: async (params: { id: string | null }) => {
|
mutationFn: async (params: { id: string | null }) => {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
@@ -115,7 +118,9 @@ export const useAlarmActionColumn = (tableData: Ref<DataTableRowData[]>) => {
|
|||||||
default: () => '确认告警?',
|
default: () => '确认告警?',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
tryGetDeviceType(rowData.deviceType) === DEVICE_TYPE_LITERALS.ndmCamera && [
|
tryGetDeviceType(rowData.deviceType) === DEVICE_TYPE_LITERALS.ndmCamera &&
|
||||||
|
rowData.stationCode &&
|
||||||
|
hasPermission(rowData.stationCode, PERMISSION_TYPE_LITERALS.OPERATION) && [
|
||||||
h(
|
h(
|
||||||
NPopconfirm,
|
NPopconfirm,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,39 +1,33 @@
|
|||||||
import type { LineDevices, NdmDeviceResultVO, Station } from '@/apis';
|
import type { LineDevices, NdmDeviceResultVO, Station } from '@/apis';
|
||||||
import { tryGetDeviceType, type DeviceType } from '@/enums';
|
import { tryGetDeviceType, type DeviceType } from '@/enums';
|
||||||
import { useDeviceStore } from '@/stores';
|
import { ref } from 'vue';
|
||||||
import { watchDebounced } from '@vueuse/core';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { onMounted, ref, toValue, watch, type MaybeRefOrGetter } from 'vue';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
export const useDeviceSelection = (options?: { syncRoute?: MaybeRefOrGetter<boolean> }) => {
|
export const useDeviceSelection = () => {
|
||||||
const { syncRoute } = options ?? {};
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const deviceStore = useDeviceStore();
|
|
||||||
const { lineDevices } = storeToRefs(deviceStore);
|
|
||||||
|
|
||||||
const selectedStationCode = ref<Station['code']>();
|
const selectedStationCode = ref<Station['code']>();
|
||||||
const selectedDeviceType = ref<DeviceType>();
|
const selectedDeviceType = ref<DeviceType>();
|
||||||
const selectedDevice = ref<NdmDeviceResultVO>();
|
const selectedDevice = ref<NdmDeviceResultVO>();
|
||||||
|
|
||||||
const initFromRoute = (lineDevices: LineDevices) => {
|
// 从路由参数同步选中的车站、设备类型以及设备
|
||||||
const { stationCode, deviceType, deviceDbId } = route.query;
|
const syncFromRoute = (lineDevices: LineDevices) => {
|
||||||
if (stationCode) {
|
// console.log('sync from route');
|
||||||
selectedStationCode.value = stationCode as Station['code'];
|
const { stationCode: routeStationCode, deviceType: routeDeviceType, deviceDbId: routeDeviceDbId } = route.query;
|
||||||
|
if (routeStationCode) {
|
||||||
|
selectedStationCode.value = routeStationCode as Station['code'];
|
||||||
}
|
}
|
||||||
if (deviceType) {
|
if (routeDeviceType) {
|
||||||
selectedDeviceType.value = deviceType as DeviceType;
|
selectedDeviceType.value = routeDeviceType as DeviceType;
|
||||||
}
|
}
|
||||||
if (deviceDbId && selectedStationCode.value && selectedDeviceType.value) {
|
if (routeDeviceDbId && selectedStationCode.value && selectedDeviceType.value) {
|
||||||
const selectedDeviceDbId = deviceDbId as string;
|
const selectedDeviceDbId = routeDeviceDbId as string;
|
||||||
const stationDevices = lineDevices[selectedStationCode.value];
|
const stationDevices = lineDevices[selectedStationCode.value];
|
||||||
if (stationDevices) {
|
if (stationDevices) {
|
||||||
const devices = stationDevices[selectedDeviceType.value];
|
const classifiedDevices = stationDevices[selectedDeviceType.value];
|
||||||
if (devices) {
|
if (classifiedDevices) {
|
||||||
const device = devices.find((device) => device.id === selectedDeviceDbId);
|
const device = classifiedDevices.find((device) => device.id === selectedDeviceDbId);
|
||||||
if (device) {
|
if (device) {
|
||||||
selectedDevice.value = device;
|
selectedDevice.value = device;
|
||||||
}
|
}
|
||||||
@@ -51,7 +45,9 @@ export const useDeviceSelection = (options?: { syncRoute?: MaybeRefOrGetter<bool
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 将选中的车站、设备类型以及设备ID同步到路由参数
|
||||||
const syncToRoute = () => {
|
const syncToRoute = () => {
|
||||||
|
// console.log('sync to route');
|
||||||
const query = { ...route.query };
|
const query = { ...route.query };
|
||||||
// 当选中的设备发生变化时,删除fromPage参数
|
// 当选中的设备发生变化时,删除fromPage参数
|
||||||
if (selectedDevice.value?.id && route.query.deviceDbId !== selectedDevice.value.id) {
|
if (selectedDevice.value?.id && route.query.deviceDbId !== selectedDevice.value.id) {
|
||||||
@@ -69,39 +65,13 @@ export const useDeviceSelection = (options?: { syncRoute?: MaybeRefOrGetter<bool
|
|||||||
router.replace({ query });
|
router.replace({ query });
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(selectedDevice, () => {
|
|
||||||
if (toValue(syncRoute)) {
|
|
||||||
syncToRoute();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// lineDevices是shallowRef,因此需要深度侦听才能获取内部变化,
|
|
||||||
// 而单纯的深度侦听又可能会引发性能问题,因此尝试使用防抖侦听
|
|
||||||
watchDebounced(
|
|
||||||
lineDevices,
|
|
||||||
(newLineDevices) => {
|
|
||||||
if (toValue(syncRoute)) {
|
|
||||||
initFromRoute(newLineDevices);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
debounce: 500,
|
|
||||||
deep: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (toValue(syncRoute)) {
|
|
||||||
initFromRoute(lineDevices.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedStationCode,
|
selectedStationCode,
|
||||||
selectedDeviceType,
|
selectedDeviceType,
|
||||||
selectedDevice,
|
selectedDevice,
|
||||||
|
|
||||||
initFromRoute,
|
syncFromRoute,
|
||||||
|
syncToRoute,
|
||||||
selectDevice,
|
selectDevice,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import type { MaybeRefOrGetter } from 'vue';
|
|
||||||
import { useDeviceManagement } from './use-device-management';
|
import { useDeviceManagement } from './use-device-management';
|
||||||
import { useDeviceSelection } from './use-device-selection';
|
import { useDeviceSelection } from './use-device-selection';
|
||||||
|
|
||||||
export const useDeviceTree = (options?: { syncRoute?: MaybeRefOrGetter<boolean> }) => {
|
export const useDeviceTree = () => {
|
||||||
const { syncRoute } = options ?? {};
|
const deviceSelection = useDeviceSelection();
|
||||||
|
|
||||||
const deviceSelection = useDeviceSelection({ syncRoute });
|
|
||||||
const deviceManagement = useDeviceManagement();
|
const deviceManagement = useDeviceManagement();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export * from './alarm';
|
export * from './alarm';
|
||||||
export * from './device';
|
export * from './device';
|
||||||
|
export * from './permission';
|
||||||
export * from './query';
|
export * from './query';
|
||||||
|
export * from './station';
|
||||||
export * from './stomp';
|
export * from './stomp';
|
||||||
|
|||||||
1
src/composables/permission/index.ts
Normal file
1
src/composables/permission/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './use-permission';
|
||||||
14
src/composables/permission/use-permission.ts
Normal file
14
src/composables/permission/use-permission.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { PermissionType } from '@/enums';
|
||||||
|
import { usePermissionStore } from '@/stores';
|
||||||
|
|
||||||
|
export const usePermission = () => {
|
||||||
|
const permissionStore = usePermissionStore();
|
||||||
|
|
||||||
|
const hasPermission = (stationCode: string, permissionType: PermissionType) => {
|
||||||
|
return !!permissionStore.permissions[stationCode]?.includes(permissionType);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasPermission,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from './use-line-alarms-query';
|
export * from './use-line-alarms-query';
|
||||||
export * from './use-line-devices-query';
|
export * from './use-line-devices-query';
|
||||||
export * from './use-line-stations-query';
|
export * from './use-line-stations-query';
|
||||||
|
export * from './use-user-permission-query';
|
||||||
export * from './use-verify-user-query';
|
export * from './use-verify-user-query';
|
||||||
export * from './use-version-check-query';
|
export * from './use-version-check-query';
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { initStationAlarms, pageDeviceAlarmLogApi, type Station } from '@/apis';
|
import { initStationAlarms, pageDeviceAlarmLogApi, type Station } from '@/apis';
|
||||||
import { LINE_ALARMS_QUERY_KEY, STATION_ALARMS_MUTATION_KEY } from '@/constants';
|
import { LINE_ALARMS_QUERY_KEY, STATION_ALARMS_MUTATION_KEY } from '@/constants';
|
||||||
import { tryGetDeviceType } from '@/enums';
|
import { tryGetDeviceType } from '@/enums';
|
||||||
import { useAlarmStore, useStationStore } from '@/stores';
|
import { useAlarmStore, usePermissionStore } from '@/stores';
|
||||||
import { parseErrorFeedback } from '@/utils';
|
import { parseErrorFeedback } from '@/utils';
|
||||||
import { CancelledError, useMutation, useQuery } from '@tanstack/vue-query';
|
import { CancelledError, useMutation, useQuery } from '@tanstack/vue-query';
|
||||||
import { isCancel } from 'axios';
|
import { isCancel } from 'axios';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
export const useStationAlarmsMutation = () => {
|
export const useStationAlarmsMutation = () => {
|
||||||
@@ -56,8 +55,8 @@ export const useStationAlarmsMutation = () => {
|
|||||||
alarmStore.setStationAlarms(station.code, stationAlarms);
|
alarmStore.setStationAlarms(station.code, stationAlarms);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error(error);
|
|
||||||
if (isCancel(error) || error instanceof CancelledError) return;
|
if (isCancel(error) || error instanceof CancelledError) return;
|
||||||
|
console.error(error);
|
||||||
const errorFeedback = parseErrorFeedback(error);
|
const errorFeedback = parseErrorFeedback(error);
|
||||||
window.$message.error(errorFeedback);
|
window.$message.error(errorFeedback);
|
||||||
},
|
},
|
||||||
@@ -69,19 +68,22 @@ export const useStationAlarmsMutation = () => {
|
|||||||
* @see [use-line-stations-query.ts](./use-line-stations-query.ts)
|
* @see [use-line-stations-query.ts](./use-line-stations-query.ts)
|
||||||
*/
|
*/
|
||||||
export const useLineAlarmsQuery = () => {
|
export const useLineAlarmsQuery = () => {
|
||||||
const stationStore = useStationStore();
|
const permissionStore = usePermissionStore();
|
||||||
const { stations } = storeToRefs(stationStore);
|
const stations = computed(() => permissionStore.stations.VIEW ?? []);
|
||||||
|
|
||||||
const { mutateAsync: getStationAlarms } = useStationAlarmsMutation();
|
const { mutateAsync: getStationAlarms } = useStationAlarmsMutation();
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: computed(() => [LINE_ALARMS_QUERY_KEY]),
|
queryKey: computed(() => [LINE_ALARMS_QUERY_KEY]),
|
||||||
enabled: false,
|
enabled: false,
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
console.time(LINE_ALARMS_QUERY_KEY);
|
const startTime = performance.now();
|
||||||
for (const station of stations.value) {
|
for (const station of stations.value) {
|
||||||
await getStationAlarms({ station, signal }).catch(() => {});
|
await getStationAlarms({ station, signal }).catch(() => {});
|
||||||
}
|
}
|
||||||
console.timeEnd(LINE_ALARMS_QUERY_KEY);
|
const endTime = performance.now();
|
||||||
|
console.log(`${LINE_ALARMS_QUERY_KEY}: ${endTime - startTime} ms`);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { getAllDevicesApi, initStationDevices, type Station } from '@/apis';
|
import { getAllDevicesApi, initStationDevices, type Station } from '@/apis';
|
||||||
import { LINE_DEVICES_QUERY_KEY, STATION_DEVICES_MUTATION_KEY } from '@/constants';
|
import { LINE_DEVICES_QUERY_KEY, STATION_DEVICES_MUTATION_KEY } from '@/constants';
|
||||||
import { useDeviceStore, useStationStore } from '@/stores';
|
import { useDeviceStore, usePermissionStore } from '@/stores';
|
||||||
import { parseErrorFeedback } from '@/utils';
|
import { parseErrorFeedback } from '@/utils';
|
||||||
import { CancelledError, useMutation, useQuery } from '@tanstack/vue-query';
|
import { CancelledError, useMutation, useQuery } from '@tanstack/vue-query';
|
||||||
import { isCancel } from 'axios';
|
import { isCancel } from 'axios';
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
export const useStationDevicesMutation = () => {
|
export const useStationDevicesMutation = () => {
|
||||||
@@ -23,8 +22,8 @@ export const useStationDevicesMutation = () => {
|
|||||||
deviceStore.setStationDevices(station.code, devices);
|
deviceStore.setStationDevices(station.code, devices);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error(error);
|
|
||||||
if (isCancel(error) || error instanceof CancelledError) return;
|
if (isCancel(error) || error instanceof CancelledError) return;
|
||||||
|
console.error(error);
|
||||||
const errorFeedback = parseErrorFeedback(error);
|
const errorFeedback = parseErrorFeedback(error);
|
||||||
window.$message.error(errorFeedback);
|
window.$message.error(errorFeedback);
|
||||||
},
|
},
|
||||||
@@ -36,19 +35,22 @@ export const useStationDevicesMutation = () => {
|
|||||||
* @see [use-line-stations-query.ts](./use-line-stations-query.ts)
|
* @see [use-line-stations-query.ts](./use-line-stations-query.ts)
|
||||||
*/
|
*/
|
||||||
export const useLineDevicesQuery = () => {
|
export const useLineDevicesQuery = () => {
|
||||||
const stationStore = useStationStore();
|
const permissionStore = usePermissionStore();
|
||||||
const { stations } = storeToRefs(stationStore);
|
const stations = computed(() => permissionStore.stations.VIEW ?? []);
|
||||||
|
|
||||||
const { mutateAsync: getStationDevices } = useStationDevicesMutation();
|
const { mutateAsync: getStationDevices } = useStationDevicesMutation();
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: computed(() => [LINE_DEVICES_QUERY_KEY]),
|
queryKey: computed(() => [LINE_DEVICES_QUERY_KEY]),
|
||||||
enabled: false,
|
enabled: false,
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
console.time(LINE_DEVICES_QUERY_KEY);
|
const startTime = performance.now();
|
||||||
for (const station of stations.value) {
|
for (const station of stations.value) {
|
||||||
await getStationDevices({ station, signal }).catch(() => {});
|
await getStationDevices({ station, signal }).catch(() => {});
|
||||||
}
|
}
|
||||||
console.timeEnd(LINE_DEVICES_QUERY_KEY);
|
const endTime = performance.now();
|
||||||
|
console.log(`${LINE_DEVICES_QUERY_KEY}: ${endTime - startTime} ms`);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { batchVerifyApi, type Station } from '@/apis';
|
import { batchVerifyApi, type Station } from '@/apis';
|
||||||
import { LINE_STATIONS_MUTATION_KEY, LINE_STATIONS_QUERY_KEY } from '@/constants';
|
import { LINE_STATIONS_MUTATION_KEY, LINE_STATIONS_QUERY_KEY } from '@/constants';
|
||||||
import { usePollingStore, useStationStore } from '@/stores';
|
import { useSettingStore, useStationStore } from '@/stores';
|
||||||
import { getAppEnvConfig, parseErrorFeedback } from '@/utils';
|
import { getAppEnvConfig, parseErrorFeedback } from '@/utils';
|
||||||
import { CancelledError, useMutation, useQuery } from '@tanstack/vue-query';
|
import { CancelledError, useMutation, useQuery } from '@tanstack/vue-query';
|
||||||
import axios, { isCancel } from 'axios';
|
import axios, { isCancel } from 'axios';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useLineDevicesQuery } from './use-line-devices-query';
|
|
||||||
import { useLineAlarmsQuery } from './use-line-alarms-query';
|
|
||||||
|
|
||||||
export const useLineStationsMutation = () => {
|
export const useLineStationsMutation = () => {
|
||||||
const stationStore = useStationStore();
|
const stationStore = useStationStore();
|
||||||
@@ -17,12 +15,13 @@ export const useLineStationsMutation = () => {
|
|||||||
mutationKey: [LINE_STATIONS_MUTATION_KEY],
|
mutationKey: [LINE_STATIONS_MUTATION_KEY],
|
||||||
mutationFn: async (params: { signal?: AbortSignal }) => {
|
mutationFn: async (params: { signal?: AbortSignal }) => {
|
||||||
const { signal } = params;
|
const { signal } = params;
|
||||||
const { data: ndmStationList } = await axios.get<{ code: string; name: string }[]>(`/minio/ndm/ndm-stations.json?_t=${dayjs().unix()}`, { signal });
|
const { data: ndmStationList } = await axios.get<Omit<Station, 'online' | 'ip'>[]>(`/minio/ndm/ndm-stations.json?_t=${dayjs().unix()}`, { signal });
|
||||||
const stations = ndmStationList.map<Station>((station) => ({
|
const stations = ndmStationList.map<Station>((station) => ({
|
||||||
code: station.code ?? '',
|
code: station.code ?? '',
|
||||||
name: station.name ?? '',
|
name: station.name ?? '',
|
||||||
online: false,
|
online: false,
|
||||||
ip: '',
|
ip: '',
|
||||||
|
occ: station.occ,
|
||||||
}));
|
}));
|
||||||
const verifyList = await batchVerifyApi({ signal });
|
const verifyList = await batchVerifyApi({ signal });
|
||||||
return stations.map((station) => ({
|
return stations.map((station) => ({
|
||||||
@@ -35,8 +34,8 @@ export const useLineStationsMutation = () => {
|
|||||||
stationStore.setStations(stations);
|
stationStore.setStations(stations);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error(error);
|
|
||||||
if (isCancel(error) || error instanceof CancelledError) return;
|
if (isCancel(error) || error instanceof CancelledError) return;
|
||||||
|
console.error(error);
|
||||||
const errorFeedback = parseErrorFeedback(error);
|
const errorFeedback = parseErrorFeedback(error);
|
||||||
window.$message.error(errorFeedback);
|
window.$message.error(errorFeedback);
|
||||||
},
|
},
|
||||||
@@ -44,28 +43,21 @@ export const useLineStationsMutation = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useLineStationsQuery = () => {
|
export const useLineStationsQuery = () => {
|
||||||
const pollingStore = usePollingStore();
|
const settingStore = useSettingStore();
|
||||||
const { pollingEnabled } = storeToRefs(pollingStore);
|
const { pollingStations } = storeToRefs(settingStore);
|
||||||
const { requestInterval } = getAppEnvConfig();
|
const { requestInterval } = getAppEnvConfig();
|
||||||
const { mutateAsync: getLineStations } = useLineStationsMutation();
|
const { mutateAsync: getLineStations } = useLineStationsMutation();
|
||||||
const { refetch: refetchLineDevicesQuery } = useLineDevicesQuery();
|
|
||||||
const { refetch: refetchLineAlarmsQuery } = useLineAlarmsQuery();
|
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: computed(() => [LINE_STATIONS_QUERY_KEY]),
|
queryKey: computed(() => [LINE_STATIONS_QUERY_KEY]),
|
||||||
enabled: computed(() => pollingEnabled.value),
|
enabled: computed(() => pollingStations.value),
|
||||||
refetchInterval: requestInterval * 1000,
|
refetchInterval: requestInterval * 1000,
|
||||||
staleTime: (requestInterval * 1000) / 2,
|
staleTime: (requestInterval * 1000) / 2,
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
console.time(LINE_STATIONS_QUERY_KEY);
|
const startTime = performance.now();
|
||||||
await getLineStations({ signal }).catch(() => {});
|
await getLineStations({ signal }).catch(() => {});
|
||||||
console.timeEnd(LINE_STATIONS_QUERY_KEY);
|
const endTime = performance.now();
|
||||||
|
console.log(`${LINE_STATIONS_QUERY_KEY}: ${endTime - startTime} ms`);
|
||||||
if (!pollingEnabled.value) return null;
|
|
||||||
await refetchLineDevicesQuery();
|
|
||||||
|
|
||||||
if (!pollingEnabled.value) return null;
|
|
||||||
await refetchLineAlarmsQuery();
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|||||||
67
src/composables/query/use-user-permission-query.ts
Normal file
67
src/composables/query/use-user-permission-query.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { useLineDevicesQuery } from './use-line-devices-query';
|
||||||
|
import { useLineAlarmsQuery } from './use-line-alarms-query';
|
||||||
|
import { pagePermissionApi } from '@/apis';
|
||||||
|
import { USER_PERMISSION_QUERY_KEY } from '@/constants';
|
||||||
|
import { PERMISSION_TYPE_LITERALS } from '@/enums';
|
||||||
|
import { usePermissionStore, useSettingStore, useStationStore, useUserStore } from '@/stores';
|
||||||
|
import { useQuery } from '@tanstack/vue-query';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { computed, watch } from 'vue';
|
||||||
|
import { useLineStationsQuery } from './use-line-stations-query';
|
||||||
|
|
||||||
|
export const useUserPermissionQuery = () => {
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const { pollingStations, activeRequests } = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const { userInfo } = storeToRefs(userStore);
|
||||||
|
|
||||||
|
const stationStore = useStationStore();
|
||||||
|
const { stations } = storeToRefs(stationStore);
|
||||||
|
|
||||||
|
const permissionStore = usePermissionStore();
|
||||||
|
const { permissions } = storeToRefs(permissionStore);
|
||||||
|
|
||||||
|
const { dataUpdatedAt: stationsUpdatedTime } = useLineStationsQuery();
|
||||||
|
const { refetch: refetchLineDevicesQuery } = useLineDevicesQuery();
|
||||||
|
const { refetch: refetchLineAlarmsQuery } = useLineAlarmsQuery();
|
||||||
|
|
||||||
|
watch([permissions, stationsUpdatedTime], async ([newPermissions, newUpdatedTime], [oldPermissions, oldUpdatedTime]) => {
|
||||||
|
const newPermissionsJson = JSON.stringify(newPermissions);
|
||||||
|
const oldPermissionsJson = JSON.stringify(oldPermissions);
|
||||||
|
if (newPermissionsJson === oldPermissionsJson && newUpdatedTime === oldUpdatedTime) return;
|
||||||
|
// 设备查询和告警查询依赖pollingEnabdled
|
||||||
|
// 当关闭轮询时,只会取消当前正在执行的查询,
|
||||||
|
// 所以如果在关闭轮询时refetch还未执行,那么这一次取消就是无效的,refetch依然会执行,
|
||||||
|
// 所以在每个refetch被调用前都需要检查pollingEnabled,否则就可能会取消失败
|
||||||
|
if (!pollingStations.value) return;
|
||||||
|
await refetchLineDevicesQuery();
|
||||||
|
if (!pollingStations.value) return;
|
||||||
|
await refetchLineAlarmsQuery();
|
||||||
|
});
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: computed(() => [USER_PERMISSION_QUERY_KEY]),
|
||||||
|
// 启用【车站轮询】或【主动请求】时,都认为查询被启用
|
||||||
|
enabled: computed(() => (pollingStations.value || activeRequests.value) && userInfo.value?.['employeeId'] && stations.value.length > 0),
|
||||||
|
// 当启用【车站轮询】时,刷新间隔为10秒,缓存时间为5秒
|
||||||
|
refetchInterval: computed(() => (pollingStations.value ? 10 * 1000 : undefined)),
|
||||||
|
staleTime: computed(() => (pollingStations.value ? 5 * 1000 : undefined)),
|
||||||
|
queryFn: async ({ signal }) => {
|
||||||
|
const { records } = await pagePermissionApi(
|
||||||
|
{
|
||||||
|
model: {
|
||||||
|
employeeId: userInfo.value['employeeId'],
|
||||||
|
},
|
||||||
|
current: 1,
|
||||||
|
size: Object.keys(PERMISSION_TYPE_LITERALS).length * stations.value.length,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
permissionStore.setPermRecords(records);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -8,17 +8,17 @@ import { computed, watch } from 'vue';
|
|||||||
export const useVerifyUserQuery = () => {
|
export const useVerifyUserQuery = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { offlineDev } = storeToRefs(settingStore);
|
const { activeRequests } = storeToRefs(settingStore);
|
||||||
|
|
||||||
watch(offlineDev, (offline) => {
|
watch(activeRequests, (active) => {
|
||||||
if (offline) {
|
if (!active) {
|
||||||
queryClient.cancelQueries({ queryKey: [VERIFY_USER_QUERY_KEY] });
|
queryClient.cancelQueries({ queryKey: [VERIFY_USER_QUERY_KEY] });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [VERIFY_USER_QUERY_KEY],
|
queryKey: [VERIFY_USER_QUERY_KEY],
|
||||||
enabled: computed(() => !offlineDev.value),
|
enabled: computed(() => activeRequests.value),
|
||||||
refetchInterval: 10 * 1000,
|
refetchInterval: 10 * 1000,
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
await verifyApi({ signal });
|
await verifyApi({ signal });
|
||||||
|
|||||||
1
src/composables/station/index.ts
Normal file
1
src/composables/station/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './use-batch-actions';
|
||||||
139
src/composables/station/use-batch-actions.ts
Normal file
139
src/composables/station/use-batch-actions.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { usePermission } from '../permission';
|
||||||
|
import { type Station } from '@/apis';
|
||||||
|
import { PERMISSION_TYPE_LITERALS, type PermissionType } from '@/enums';
|
||||||
|
import { objectEntries } from '@vueuse/core';
|
||||||
|
import type { CheckboxProps } from 'naive-ui';
|
||||||
|
import { computed, ref, watch, type Ref } from 'vue';
|
||||||
|
|
||||||
|
type BatchActionKey = 'export-icmp' | 'export-record' | 'sync-camera' | 'sync-nvr';
|
||||||
|
|
||||||
|
type BatchAction = {
|
||||||
|
label: string;
|
||||||
|
key: BatchActionKey;
|
||||||
|
permission: PermissionType;
|
||||||
|
active: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBatchActions = (stations: Ref<Station[]>, abortController?: Ref<AbortController | undefined>) => {
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
|
const batchActions = ref<BatchAction[]>([
|
||||||
|
{
|
||||||
|
label: '导出设备状态',
|
||||||
|
key: 'export-icmp',
|
||||||
|
permission: PERMISSION_TYPE_LITERALS.VIEW,
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '导出录像诊断',
|
||||||
|
key: 'export-record',
|
||||||
|
permission: PERMISSION_TYPE_LITERALS.VIEW,
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '同步摄像机',
|
||||||
|
key: 'sync-camera',
|
||||||
|
permission: PERMISSION_TYPE_LITERALS.OPERATION,
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '同步录像机通道',
|
||||||
|
key: 'sync-nvr',
|
||||||
|
permission: PERMISSION_TYPE_LITERALS.OPERATION,
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const selectedAction = computed(() => {
|
||||||
|
return batchActions.value.find((action) => action.active);
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectableStations = computed(() => {
|
||||||
|
if (!selectedAction.value) return [];
|
||||||
|
const result: Station[] = [];
|
||||||
|
if (selectedAction.value.permission === PERMISSION_TYPE_LITERALS.VIEW) {
|
||||||
|
result.push(...stations.value.filter((station) => hasPermission(station.code, PERMISSION_TYPE_LITERALS.VIEW)));
|
||||||
|
}
|
||||||
|
if (selectedAction.value.permission === PERMISSION_TYPE_LITERALS.OPERATION) {
|
||||||
|
result.push(...stations.value.filter((station) => hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const stationSelection = ref<Record<Station['code'], boolean>>({});
|
||||||
|
|
||||||
|
const selectionProps = computed<CheckboxProps>(() => {
|
||||||
|
const selectableStationsLength = selectableStations.value.length;
|
||||||
|
const selectedStationsLength = objectEntries(stationSelection.value).filter(([, selected]) => selected).length;
|
||||||
|
|
||||||
|
const disabled = selectableStationsLength === 0;
|
||||||
|
const checked = selectableStationsLength > 0 && selectedStationsLength === selectableStationsLength;
|
||||||
|
const indeterminate = selectableStationsLength > 0 && selectedStationsLength > 0 && selectedStationsLength < selectableStationsLength;
|
||||||
|
|
||||||
|
return {
|
||||||
|
disabled,
|
||||||
|
checked,
|
||||||
|
indeterminate,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleSelectAction = (action: BatchAction) => {
|
||||||
|
batchActions.value.forEach((batchAction) => {
|
||||||
|
if (batchAction.key === action.key) {
|
||||||
|
if (batchAction.active) stationSelection.value = {};
|
||||||
|
batchAction.active = !batchAction.active;
|
||||||
|
} else {
|
||||||
|
batchAction.active = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSelectAllStations = (checked: boolean) => {
|
||||||
|
if (!checked) {
|
||||||
|
stationSelection.value = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectableStations.value.forEach((station) => {
|
||||||
|
stationSelection.value[station.code] = checked;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(selectedAction, () => {
|
||||||
|
toggleSelectAllStations(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const confirmAction = (callbacks: Record<BatchActionKey, () => void>) => {
|
||||||
|
const { key } = selectedAction.value ?? {};
|
||||||
|
if (!key) return;
|
||||||
|
const noStationSeleted = !Object.values(stationSelection.value).some((selected) => selected);
|
||||||
|
if (noStationSeleted) {
|
||||||
|
window.$message.warning('请选择车站');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callbacks[key]();
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelAction = () => {
|
||||||
|
abortController?.value?.abort();
|
||||||
|
stationSelection.value = {};
|
||||||
|
batchActions.value.forEach((batchAction) => {
|
||||||
|
batchAction.active = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
batchActions,
|
||||||
|
selectedAction,
|
||||||
|
|
||||||
|
selectableStations,
|
||||||
|
stationSelection,
|
||||||
|
|
||||||
|
selectionProps,
|
||||||
|
|
||||||
|
toggleSelectAction,
|
||||||
|
toggleSelectAllStations,
|
||||||
|
|
||||||
|
confirmAction,
|
||||||
|
cancelAction,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { NdmDeviceAlarmLogResultVO, Station, SyncCameraResult } from '@/apis';
|
import type { NdmDeviceAlarmLogResultVO, Station, SyncCameraResult } from '@/apis';
|
||||||
import { ALARM_TOPIC, SYNC_CAMERA_STATUS_TOPIC } from '@/constants';
|
import { ALARM_TOPIC, PERMISSION_TOPIC, SYNC_CAMERA_STATUS_TOPIC } from '@/constants';
|
||||||
import { useAlarmStore, useSettingStore, useStationStore } from '@/stores';
|
import { useSettingStore, useStationStore, useUnreadStore, useUserStore } from '@/stores';
|
||||||
import { Client } from '@stomp/stompjs';
|
import { Client } from '@stomp/stompjs';
|
||||||
import { watchDebounced } from '@vueuse/core';
|
import { watchDebounced } from '@vueuse/core';
|
||||||
import destr from 'destr';
|
import destr from 'destr';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { useStationAlarmsMutation } from '../query';
|
import { useStationAlarmsMutation, useUserPermissionQuery } from '../query';
|
||||||
|
|
||||||
const getBrokerUrl = () => {
|
const getBrokerUrl = () => {
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
@@ -19,11 +19,17 @@ const getBrokerUrl = () => {
|
|||||||
export const useStompClient = () => {
|
export const useStompClient = () => {
|
||||||
const stationStore = useStationStore();
|
const stationStore = useStationStore();
|
||||||
const { stations } = storeToRefs(stationStore);
|
const { stations } = storeToRefs(stationStore);
|
||||||
const alarmStore = useAlarmStore();
|
|
||||||
const { unreadLineAlarms } = storeToRefs(alarmStore);
|
|
||||||
const settingStore = useSettingStore();
|
|
||||||
const { offlineDev } = storeToRefs(settingStore);
|
|
||||||
|
|
||||||
|
const unreadStore = useUnreadStore();
|
||||||
|
const { unreadLineAlarms } = storeToRefs(unreadStore);
|
||||||
|
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const { subscribeMessages } = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const { userInfo } = storeToRefs(userStore);
|
||||||
|
|
||||||
|
const { refetch: refetchUserPermissionQuery } = useUserPermissionQuery();
|
||||||
const { mutate: refreshStationAlarms } = useStationAlarmsMutation();
|
const { mutate: refreshStationAlarms } = useStationAlarmsMutation();
|
||||||
|
|
||||||
const stompClient = ref<Client | null>(null);
|
const stompClient = ref<Client | null>(null);
|
||||||
@@ -40,10 +46,16 @@ export const useStompClient = () => {
|
|||||||
console.log('Stomp连接成功');
|
console.log('Stomp连接成功');
|
||||||
stompClient.value?.subscribe(ALARM_TOPIC, (message) => {
|
stompClient.value?.subscribe(ALARM_TOPIC, (message) => {
|
||||||
const alarm = destr<NdmDeviceAlarmLogResultVO>(message.body);
|
const alarm = destr<NdmDeviceAlarmLogResultVO>(message.body);
|
||||||
if (alarm.alarmCategory === '1') {
|
const { alarmCategory, stationCode } = alarm;
|
||||||
alarmStore.pushUnreadAlarm(alarm);
|
if (alarmCategory === '1' && !!stations.value.find((station) => station.code === stationCode)) {
|
||||||
|
unreadStore.pushUnreadAlarm(alarm);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
stompClient.value?.subscribe(PERMISSION_TOPIC, (message) => {
|
||||||
|
const employeeId = destr<string>(message.body);
|
||||||
|
if (userInfo.value?.['employeeId'] !== employeeId) return;
|
||||||
|
refetchUserPermissionQuery();
|
||||||
|
});
|
||||||
stompClient.value?.subscribe(SYNC_CAMERA_STATUS_TOPIC, (message) => {
|
stompClient.value?.subscribe(SYNC_CAMERA_STATUS_TOPIC, (message) => {
|
||||||
const { stationCode, startTime, endTime, insertList, updateList, deleteList } = destr<SyncCameraResult>(message.body);
|
const { stationCode, startTime, endTime, insertList, updateList, deleteList } = destr<SyncCameraResult>(message.body);
|
||||||
syncCameraResult.value[stationCode] = { stationCode, startTime, endTime, insertList, updateList, deleteList };
|
syncCameraResult.value[stationCode] = { stationCode, startTime, endTime, insertList, updateList, deleteList };
|
||||||
@@ -52,6 +64,7 @@ export const useStompClient = () => {
|
|||||||
onDisconnect: () => {
|
onDisconnect: () => {
|
||||||
console.log('Stomp连接断开');
|
console.log('Stomp连接断开');
|
||||||
stompClient.value?.unsubscribe(ALARM_TOPIC);
|
stompClient.value?.unsubscribe(ALARM_TOPIC);
|
||||||
|
stompClient.value?.unsubscribe(PERMISSION_TOPIC);
|
||||||
stompClient.value?.unsubscribe(SYNC_CAMERA_STATUS_TOPIC);
|
stompClient.value?.unsubscribe(SYNC_CAMERA_STATUS_TOPIC);
|
||||||
},
|
},
|
||||||
onStompError: (frame) => {
|
onStompError: (frame) => {
|
||||||
@@ -63,7 +76,7 @@ export const useStompClient = () => {
|
|||||||
window.$message.error('WebSocket错误');
|
window.$message.error('WebSocket错误');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!offlineDev.value) {
|
if (subscribeMessages.value) {
|
||||||
stompClient.value.activate();
|
stompClient.value.activate();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -73,11 +86,11 @@ export const useStompClient = () => {
|
|||||||
stompClient.value = null;
|
stompClient.value = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(offlineDev, (offline) => {
|
watch(subscribeMessages, (subscribe) => {
|
||||||
if (offline) {
|
if (subscribe) {
|
||||||
stompClient.value?.deactivate();
|
|
||||||
} else {
|
|
||||||
stompClient.value?.activate();
|
stompClient.value?.activate();
|
||||||
|
} else {
|
||||||
|
stompClient.value?.deactivate();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,8 +100,8 @@ export const useStompClient = () => {
|
|||||||
watchDebounced(
|
watchDebounced(
|
||||||
() => Object.entries(unreadLineAlarms.value).map(([stationCode, stationAlarms]) => ({ stationCode, count: stationAlarms['unclassified'].length })),
|
() => Object.entries(unreadLineAlarms.value).map(([stationCode, stationAlarms]) => ({ stationCode, count: stationAlarms['unclassified'].length })),
|
||||||
(newValue, oldValue) => {
|
(newValue, oldValue) => {
|
||||||
// 启用离线模式时,跳过处理
|
// 关闭消息订阅时,跳过处理
|
||||||
if (offlineDev.value) return;
|
if (!subscribeMessages.value) return;
|
||||||
if (newValue.length === 0) return;
|
if (newValue.length === 0) return;
|
||||||
const codes: Station['code'][] = [];
|
const codes: Station['code'][] = [];
|
||||||
newValue.forEach(({ stationCode, count }) => {
|
newValue.forEach(({ stationCode, count }) => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export const LINE_ALARMS_QUERY_KEY = 'line-alarms';
|
export const LINE_ALARMS_QUERY_KEY = 'line-alarms';
|
||||||
export const LINE_DEVICES_QUERY_KEY = 'line-devices';
|
export const LINE_DEVICES_QUERY_KEY = 'line-devices';
|
||||||
export const LINE_STATIONS_QUERY_KEY = 'line-stations';
|
export const LINE_STATIONS_QUERY_KEY = 'line-stations';
|
||||||
|
export const USER_PERMISSION_QUERY_KEY = 'user-permission';
|
||||||
export const VERIFY_USER_QUERY_KEY = 'verify-user';
|
export const VERIFY_USER_QUERY_KEY = 'verify-user';
|
||||||
export const VERSION_CHECK_QUERY_KEY = 'version-check';
|
export const VERSION_CHECK_QUERY_KEY = 'version-check';
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export const ALARM_TOPIC = '/topic/deviceAlarm';
|
export const ALARM_TOPIC = '/topic/deviceAlarm';
|
||||||
|
export const PERMISSION_TOPIC = '/topic/permission';
|
||||||
export const SYNC_CAMERA_STATUS_TOPIC = '/topic/syncCameraStatus';
|
export const SYNC_CAMERA_STATUS_TOPIC = '/topic/syncCameraStatus';
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
export const NDM_ALARM_STORE_ID = 'ndm-alarm-store';
|
export const NDM_ALARM_STORE_ID = 'ndm-alarm-store';
|
||||||
export const NDM_DEVICE_STORE_ID = 'ndm-device-store';
|
export const NDM_DEVICE_STORE_ID = 'ndm-device-store';
|
||||||
|
export const NDM_PERMISSION_STORE_ID = 'ndm-permission-store';
|
||||||
export const NDM_POLLIING_STORE_ID = 'ndm-polling-store';
|
export const NDM_POLLIING_STORE_ID = 'ndm-polling-store';
|
||||||
export const NDM_SETTING_STORE_ID = 'ndm-setting-store';
|
export const NDM_SETTING_STORE_ID = 'ndm-setting-store';
|
||||||
export const NDM_STATION_STORE_ID = 'ndm-station-store';
|
export const NDM_STATION_STORE_ID = 'ndm-station-store';
|
||||||
|
export const NDM_UNREAD_STORE_ID = 'ndm-unread-store';
|
||||||
export const NDM_USER_STORE_ID = 'ndm-user-store';
|
export const NDM_USER_STORE_ID = 'ndm-user-store';
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './alarm-type';
|
export * from './alarm-type';
|
||||||
export * from './device-type';
|
export * from './device-type';
|
||||||
export * from './fault-level';
|
export * from './fault-level';
|
||||||
|
export * from './permission-type';
|
||||||
|
|||||||
13
src/enums/permission-type.ts
Normal file
13
src/enums/permission-type.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export const PERMISSION_TYPE_LITERALS = {
|
||||||
|
VIEW: 'VIEW',
|
||||||
|
OPERATION: 'OPERATION',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type PermissionType = keyof typeof PERMISSION_TYPE_LITERALS;
|
||||||
|
|
||||||
|
export const PERMISSION_TYPE_NAMES = {
|
||||||
|
[PERMISSION_TYPE_LITERALS.VIEW]: '查看',
|
||||||
|
[PERMISSION_TYPE_LITERALS.OPERATION]: '操作',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type PermissionTypeEnum = typeof PERMISSION_TYPE_NAMES;
|
||||||
2
src/global.d.ts
vendored
2
src/global.d.ts
vendored
@@ -7,6 +7,6 @@ declare global {
|
|||||||
$loadingBar: ReturnType<typeof useLoadingBar>;
|
$loadingBar: ReturnType<typeof useLoadingBar>;
|
||||||
$message: ReturnType<typeof useMessage>;
|
$message: ReturnType<typeof useMessage>;
|
||||||
$notification: ReturnType<typeof useNotification>;
|
$notification: ReturnType<typeof useNotification>;
|
||||||
$offlineDev: Ref<boolean>;
|
$mockUser: Ref<boolean>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SettingsDrawer, SyncCameraResultModal } from '@/components';
|
import { SettingsDrawer, SyncCameraResultModal } from '@/components';
|
||||||
import { useLineStationsQuery, useStompClient, useVerifyUserQuery } from '@/composables';
|
import { useLineStationsQuery, useStompClient, useUserPermissionQuery, useVerifyUserQuery } from '@/composables';
|
||||||
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 { useAlarmStore, useSettingStore, useUserStore } from '@/stores';
|
import { useSettingStore, useUnreadStore, useUserStore } from '@/stores';
|
||||||
import { parseErrorFeedback } from '@/utils';
|
import { useIsFetching, useIsMutating } from '@tanstack/vue-query';
|
||||||
import { useIsFetching, useIsMutating, useMutation } from '@tanstack/vue-query';
|
import { ChevronDownIcon, ChevronsLeftIcon, ChevronsRightIcon, ComputerIcon, KeyRoundIcon, LogOutIcon, LogsIcon, MapPinIcon, SettingsIcon, SirenIcon } from 'lucide-vue-next';
|
||||||
import { AlertFilled, CaretDownFilled, DoubleLeftOutlined, DoubleRightOutlined, EnvironmentFilled, FileTextFilled, HddFilled, LogoutOutlined, SettingOutlined } from '@vicons/antd';
|
|
||||||
import { isCancel } from 'axios';
|
|
||||||
import {
|
import {
|
||||||
NBadge,
|
NBadge,
|
||||||
NButton,
|
NButton,
|
||||||
@@ -24,25 +22,26 @@ import {
|
|||||||
type MenuOption,
|
type MenuOption,
|
||||||
} from 'naive-ui';
|
} from 'naive-ui';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { computed, h, ref, watchEffect, type Component, type VNode } from 'vue';
|
import { computed, h, ref, type Component, type VNode } from 'vue';
|
||||||
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const { userInfo } = storeToRefs(userStore);
|
const { userInfo, isLamp } = storeToRefs(userStore);
|
||||||
|
|
||||||
const alarmStore = useAlarmStore();
|
const unreadStore = useUnreadStore();
|
||||||
const { unreadAlarmCount } = storeToRefs(alarmStore);
|
const { unreadAlarmCount } = storeToRefs(unreadStore);
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { menuCollpased, offlineDev } = storeToRefs(settingStore);
|
const { menuCollpased } = storeToRefs(settingStore);
|
||||||
|
|
||||||
const { syncCameraResult, afterCheckSyncCameraResult } = useStompClient();
|
const { syncCameraResult, afterCheckSyncCameraResult } = useStompClient();
|
||||||
|
|
||||||
useVerifyUserQuery();
|
useVerifyUserQuery();
|
||||||
useLineStationsQuery();
|
useLineStationsQuery();
|
||||||
|
useUserPermissionQuery();
|
||||||
|
|
||||||
// 全局loading状态依赖于轮询query的queryKey以及相关的mutationKey
|
// 全局loading状态依赖于轮询query的queryKey以及相关的mutationKey
|
||||||
const queryingCount = useIsFetching({
|
const queryingCount = useIsFetching({
|
||||||
@@ -65,21 +64,21 @@ const onToggleMenuCollapsed = () => {
|
|||||||
menuCollpased.value = !menuCollpased.value;
|
menuCollpased.value = !menuCollpased.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuOptions: MenuOption[] = [
|
const menuOptions = computed<MenuOption[]>(() => [
|
||||||
{
|
{
|
||||||
label: () => h(RouterLink, { to: '/station' }, { default: () => '车站状态' }),
|
label: () => h(RouterLink, { to: '/station' }, { default: () => '车站状态' }),
|
||||||
key: '/station',
|
key: '/station',
|
||||||
icon: renderIcon(EnvironmentFilled),
|
icon: renderIcon(MapPinIcon),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: () => h(RouterLink, { to: '/device' }, { default: () => '设备诊断' }),
|
label: () => h(RouterLink, { to: '/device' }, { default: () => '设备诊断' }),
|
||||||
key: '/device',
|
key: '/device',
|
||||||
icon: renderIcon(HddFilled),
|
icon: renderIcon(ComputerIcon),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '设备告警',
|
label: '设备告警',
|
||||||
key: '/alarm',
|
key: '/alarm',
|
||||||
icon: renderIcon(AlertFilled),
|
icon: renderIcon(SirenIcon),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
label: () => h(RouterLink, { to: '/alarm/alarm-log' }, { default: () => '设备告警记录' }),
|
label: () => h(RouterLink, { to: '/alarm/alarm-log' }, { default: () => '设备告警记录' }),
|
||||||
@@ -94,7 +93,7 @@ const menuOptions: MenuOption[] = [
|
|||||||
{
|
{
|
||||||
label: '系统日志',
|
label: '系统日志',
|
||||||
key: '/log',
|
key: '/log',
|
||||||
icon: renderIcon(FileTextFilled),
|
icon: renderIcon(LogsIcon),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
label: () => h(RouterLink, { to: '/log/vimp-log' }, { default: () => '视频平台日志' }),
|
label: () => h(RouterLink, { to: '/log/vimp-log' }, { default: () => '视频平台日志' }),
|
||||||
@@ -106,13 +105,19 @@ const menuOptions: MenuOption[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
label: () => h(RouterLink, { to: '/permission' }, { default: () => '权限管理' }),
|
||||||
|
key: '/permission',
|
||||||
|
show: isLamp.value,
|
||||||
|
icon: renderIcon(KeyRoundIcon),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
const dropdownOptions: DropdownOption[] = [
|
const dropdownOptions: DropdownOption[] = [
|
||||||
{
|
{
|
||||||
label: '退出登录',
|
label: '退出登录',
|
||||||
key: 'logout',
|
key: 'logout',
|
||||||
icon: renderIcon(LogoutOutlined),
|
icon: renderIcon(LogOutIcon),
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
try {
|
try {
|
||||||
await userStore.userLogout();
|
await userStore.userLogout();
|
||||||
@@ -141,33 +146,12 @@ const routeToRoot = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const routeToAlarmPage = () => {
|
const routeToAlarmPage = () => {
|
||||||
alarmStore.clearUnreadAlarms();
|
unreadStore.clearUnreadAlarms();
|
||||||
if (route.path !== '/alarm/alarm-log') {
|
if (route.path !== '/alarm/alarm-log') {
|
||||||
router.push({ path: '/alarm/alarm-log' });
|
router.push({ path: '/alarm/alarm-log' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutate: getUserInfo } = useMutation({
|
|
||||||
mutationFn: async (params?: { signal?: AbortSignal }) => {
|
|
||||||
const { signal } = params ?? {};
|
|
||||||
await userStore.userGetInfo({ signal });
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
if (isCancel(error)) return;
|
|
||||||
console.error(error);
|
|
||||||
const errorFeedback = parseErrorFeedback(error);
|
|
||||||
window.$message.error(errorFeedback);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 判断是否为离线开发模式 决定是否自动发送获取用户信息请求
|
|
||||||
watchEffect((onCleanup) => {
|
|
||||||
if (offlineDev.value) return;
|
|
||||||
const abortController = new AbortController();
|
|
||||||
getUserInfo({ signal: abortController.signal });
|
|
||||||
onCleanup(() => abortController.abort());
|
|
||||||
});
|
|
||||||
|
|
||||||
function renderIcon(icon: Component): () => VNode {
|
function renderIcon(icon: Component): () => VNode {
|
||||||
return () => h(NIcon, null, { default: () => h(icon) });
|
return () => h(NIcon, null, { default: () => h(icon) });
|
||||||
}
|
}
|
||||||
@@ -181,7 +165,7 @@ function renderIcon(icon: Component): () => VNode {
|
|||||||
<NMenu :collapsed="menuCollpased" :collapsed-width="64" :collapsed-icon-size="20" :value="route.path" :options="menuOptions" />
|
<NMenu :collapsed="menuCollpased" :collapsed-width="64" :collapsed-icon-size="20" :value="route.path" :options="menuOptions" />
|
||||||
<NButton block quaternary :focusable="false" @click="onToggleMenuCollapsed">
|
<NButton block quaternary :focusable="false" @click="onToggleMenuCollapsed">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="menuCollpased ? DoubleRightOutlined : DoubleLeftOutlined" />
|
<NIcon :component="menuCollpased ? ChevronsRightIcon : ChevronsLeftIcon" />
|
||||||
</template>
|
</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
@@ -201,13 +185,13 @@ function renderIcon(icon: Component): () => VNode {
|
|||||||
<span>{{ userInfo?.nickName ?? '' }}</span>
|
<span>{{ userInfo?.nickName ?? '' }}</span>
|
||||||
</template>
|
</template>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="CaretDownFilled" />
|
<NIcon :component="ChevronDownIcon" />
|
||||||
</template>
|
</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
</NDropdown>
|
</NDropdown>
|
||||||
<NButton quaternary :focusable="false" style="height: 100%" @click="openSettingsDrawer">
|
<NButton quaternary :focusable="false" style="height: 100%" @click="openSettingsDrawer">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="SettingOutlined" />
|
<NIcon :component="SettingsIcon" />
|
||||||
</template>
|
</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
@@ -221,7 +205,7 @@ function renderIcon(icon: Component): () => VNode {
|
|||||||
<NBadge :value="unreadAlarmCount">
|
<NBadge :value="unreadAlarmCount">
|
||||||
<NButton secondary strong @click="routeToAlarmPage">
|
<NButton secondary strong @click="routeToAlarmPage">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="AlertFilled" />
|
<NIcon :component="SirenIcon" />
|
||||||
</template>
|
</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
</NBadge>
|
</NBadge>
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ const NDM_TYPES: Record<string, DeviceType> = {
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { deleteCameraIgnoreApi, pageCameraIgnoreApi, type NdmCameraIgnore, type NdmCameraIgnoreResultVO, type PageQueryExtra, type Station } from '@/apis';
|
import { deleteCameraIgnoreApi, pageCameraIgnoreApi, type NdmCameraIgnore, type NdmCameraIgnoreResultVO, type PageQueryExtra, type Station } from '@/apis';
|
||||||
import { DEVICE_TYPE_LITERALS, DEVICE_TYPE_NAMES, type DeviceType } from '@/enums';
|
import { usePermission } from '@/composables';
|
||||||
import { useDeviceStore, useStationStore } from '@/stores';
|
import { DEVICE_TYPE_LITERALS, DEVICE_TYPE_NAMES, PERMISSION_TYPE_LITERALS, type DeviceType } from '@/enums';
|
||||||
|
import { useDeviceStore, usePermissionStore } from '@/stores';
|
||||||
import { useMutation } from '@tanstack/vue-query';
|
import { useMutation } from '@tanstack/vue-query';
|
||||||
import { isCancel } from 'axios';
|
import { isCancel } from 'axios';
|
||||||
import {
|
import {
|
||||||
@@ -46,8 +47,13 @@ interface SearchFields extends PageQueryExtra<NdmCameraIgnore> {
|
|||||||
// deviceId_like?: string;
|
// deviceId_like?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stationStore = useStationStore();
|
const permissionStore = usePermissionStore();
|
||||||
const { stations } = storeToRefs(stationStore);
|
const { permissions } = storeToRefs(permissionStore);
|
||||||
|
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
|
const stations = computed(() => permissionStore.stations.VIEW ?? []);
|
||||||
|
|
||||||
const deviceStore = useDeviceStore();
|
const deviceStore = useDeviceStore();
|
||||||
const { lineDevices } = storeToRefs(deviceStore);
|
const { lineDevices } = storeToRefs(deviceStore);
|
||||||
|
|
||||||
@@ -64,6 +70,14 @@ const stationSelectOptions = computed<SelectOption[]>(() => {
|
|||||||
// }));
|
// }));
|
||||||
// });
|
// });
|
||||||
|
|
||||||
|
// 权限变化时,需要刷新表格数据
|
||||||
|
watch(permissions, (newPermissions, oldPermissions) => {
|
||||||
|
const oldPermissionsJson = JSON.stringify(oldPermissions);
|
||||||
|
const newPermissionsJson = JSON.stringify(newPermissions);
|
||||||
|
if (oldPermissionsJson === newPermissionsJson) return;
|
||||||
|
onClickReset();
|
||||||
|
});
|
||||||
|
|
||||||
const searchFields = ref<SearchFields>({});
|
const searchFields = ref<SearchFields>({});
|
||||||
const resetSearchFields = () => {
|
const resetSearchFields = () => {
|
||||||
searchFields.value = {};
|
searchFields.value = {};
|
||||||
@@ -84,7 +98,7 @@ watch(searchFields, () => {
|
|||||||
searchFieldsChanged.value = true;
|
searchFieldsChanged.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const tableColumns: DataTableColumns<NdmCameraIgnoreResultVO> = [
|
const tableColumns = computed<DataTableColumns<NdmCameraIgnoreResultVO>>(() => [
|
||||||
{ title: '忽略时间', key: 'createdTime', align: 'center' },
|
{ title: '忽略时间', key: 'createdTime', align: 'center' },
|
||||||
// { title: '更新时间', key: 'updatedTime' },
|
// { title: '更新时间', key: 'updatedTime' },
|
||||||
{
|
{
|
||||||
@@ -142,6 +156,11 @@ const tableColumns: DataTableColumns<NdmCameraIgnoreResultVO> = [
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
width: 120,
|
width: 120,
|
||||||
render: (rowData) => {
|
render: (rowData) => {
|
||||||
|
const { deviceId } = rowData;
|
||||||
|
if (!deviceId) return null;
|
||||||
|
const stationCode = deviceId.slice(0, 4);
|
||||||
|
if (!stationCode) return null;
|
||||||
|
if (!hasPermission(stationCode, PERMISSION_TYPE_LITERALS.OPERATION)) return null;
|
||||||
return h(
|
return h(
|
||||||
NPopconfirm,
|
NPopconfirm,
|
||||||
{
|
{
|
||||||
@@ -167,7 +186,7 @@ const tableColumns: DataTableColumns<NdmCameraIgnoreResultVO> = [
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
|
|
||||||
const { mutate: cancelIgnore } = useMutation({
|
const { mutate: cancelIgnore } = useMutation({
|
||||||
mutationFn: async (params: { id?: string; signal?: AbortSignal }) => {
|
mutationFn: async (params: { id?: string; signal?: AbortSignal }) => {
|
||||||
@@ -3,7 +3,7 @@ import { exportDeviceAlarmLogApi, pageDeviceAlarmLogApi, type NdmDeviceAlarmLog,
|
|||||||
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';
|
||||||
import { useAlarmStore, useDeviceStore, useStationStore } from '@/stores';
|
import { useDeviceStore, usePermissionStore, useUnreadStore } 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 { watchDebounced } from '@vueuse/core';
|
import { watchDebounced } from '@vueuse/core';
|
||||||
@@ -37,17 +37,23 @@ interface SearchFields extends PageQueryExtra<NdmDeviceAlarmLog> {
|
|||||||
alarmType_in: string[];
|
alarmType_in: string[];
|
||||||
faultLevel_in: string[];
|
faultLevel_in: string[];
|
||||||
alarmDate: [number, number];
|
alarmDate: [number, number];
|
||||||
|
alarmCategory: string;
|
||||||
|
alarmConfirm: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const stationStore = useStationStore();
|
const permissionStore = usePermissionStore();
|
||||||
const { stations } = storeToRefs(stationStore);
|
const { permissions } = storeToRefs(permissionStore);
|
||||||
|
|
||||||
|
const stations = computed(() => permissionStore.stations.VIEW ?? []);
|
||||||
|
|
||||||
const deviceStore = useDeviceStore();
|
const deviceStore = useDeviceStore();
|
||||||
const { lineDevices } = storeToRefs(deviceStore);
|
const { lineDevices } = storeToRefs(deviceStore);
|
||||||
const alarmStore = useAlarmStore();
|
|
||||||
const { unreadAlarmCount } = storeToRefs(alarmStore);
|
const unreadStore = useUnreadStore();
|
||||||
|
const { unreadAlarmCount } = storeToRefs(unreadStore);
|
||||||
|
|
||||||
const stationSelectOptions = computed<SelectOption[]>(() => {
|
const stationSelectOptions = computed<SelectOption[]>(() => {
|
||||||
return stations.value.map((station) => ({
|
return stations.value.map((station) => ({
|
||||||
@@ -74,6 +80,14 @@ const faultLevelSelectOptions = computed<SelectOption[]>(() => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 权限变化时,需要刷新表格数据
|
||||||
|
watch(permissions, (newPermissions, oldPermissions) => {
|
||||||
|
const oldPermissionsJson = JSON.stringify(oldPermissions);
|
||||||
|
const newPermissionsJson = JSON.stringify(newPermissions);
|
||||||
|
if (oldPermissionsJson === newPermissionsJson) return;
|
||||||
|
onClickReset();
|
||||||
|
});
|
||||||
|
|
||||||
// 未读告警数量被清零时,代表从别的页面跳转过来,需要刷新告警表格数据
|
// 未读告警数量被清零时,代表从别的页面跳转过来,需要刷新告警表格数据
|
||||||
const unreadCountCleared = computed(() => unreadAlarmCount.value === 0);
|
const unreadCountCleared = computed(() => unreadAlarmCount.value === 0);
|
||||||
watch(unreadCountCleared, (newValue, oldValue) => {
|
watch(unreadCountCleared, (newValue, oldValue) => {
|
||||||
@@ -103,6 +117,8 @@ const searchFields = ref<SearchFields>({
|
|||||||
alarmType_in: [],
|
alarmType_in: [],
|
||||||
faultLevel_in: [],
|
faultLevel_in: [],
|
||||||
alarmDate: [dayjs().startOf('date').valueOf(), dayjs().endOf('date').valueOf()],
|
alarmDate: [dayjs().startOf('date').valueOf(), dayjs().endOf('date').valueOf()],
|
||||||
|
alarmCategory: '',
|
||||||
|
alarmConfirm: '',
|
||||||
});
|
});
|
||||||
const resetSearchFields = () => {
|
const resetSearchFields = () => {
|
||||||
searchFields.value = {
|
searchFields.value = {
|
||||||
@@ -112,19 +128,24 @@ const resetSearchFields = () => {
|
|||||||
alarmType_in: [],
|
alarmType_in: [],
|
||||||
faultLevel_in: [],
|
faultLevel_in: [],
|
||||||
alarmDate: [dayjs().startOf('date').valueOf(), dayjs().endOf('date').valueOf()],
|
alarmDate: [dayjs().startOf('date').valueOf(), dayjs().endOf('date').valueOf()],
|
||||||
|
alarmCategory: '',
|
||||||
|
alarmConfirm: '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
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]);
|
||||||
|
const deviceNameLike = searchFields.value.deviceName_like;
|
||||||
|
const alarmTypeIn = searchFields.value.alarmType_in;
|
||||||
|
const faultLevelIn = searchFields.value.faultLevel_in;
|
||||||
const alarmDateGe = searchFields.value.alarmDate[0];
|
const alarmDateGe = searchFields.value.alarmDate[0];
|
||||||
const alarmDateLe = searchFields.value.alarmDate[1];
|
const alarmDateLe = searchFields.value.alarmDate[1];
|
||||||
return {
|
return {
|
||||||
stationCode_in: stationCodeIn ? (stationCodeIn.length > 0 ? [...stationCodeIn] : undefined) : undefined,
|
stationCode_in: stationCodeIn.length > 0 ? [...stationCodeIn] : stations.value.map((station) => station.code),
|
||||||
deviceType_in: deviceTypeIn ? (deviceTypeIn.length > 0 ? [...deviceTypeIn] : undefined) : undefined,
|
deviceType_in: deviceTypeIn.length > 0 ? [...deviceTypeIn] : undefined,
|
||||||
deviceName_like: !!searchFields.value.deviceName_like ? searchFields.value.deviceName_like : undefined,
|
deviceName_like: deviceNameLike.length > 0 ? deviceNameLike : undefined,
|
||||||
alarmType_in: searchFields.value.alarmType_in.length > 0 ? [...searchFields.value.alarmType_in] : undefined,
|
alarmType_in: alarmTypeIn.length > 0 ? [...alarmTypeIn] : undefined,
|
||||||
faultLevel_in: searchFields.value.faultLevel_in.length > 0 ? [...searchFields.value.faultLevel_in] : undefined,
|
faultLevel_in: faultLevelIn.length > 0 ? [...faultLevelIn] : undefined,
|
||||||
alarmDate_ge: alarmDateGe,
|
alarmDate_ge: alarmDateGe,
|
||||||
alarmDate_le: alarmDateLe,
|
alarmDate_le: alarmDateLe,
|
||||||
};
|
};
|
||||||
@@ -223,7 +244,10 @@ const { mutate: getTableData, isPending: tableLoading } = useMutation({
|
|||||||
|
|
||||||
const res = await pageDeviceAlarmLogApi(
|
const res = await pageDeviceAlarmLogApi(
|
||||||
{
|
{
|
||||||
model: {},
|
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,
|
||||||
@@ -331,6 +355,28 @@ onBeforeUnmount(() => {
|
|||||||
<NFormItemGi span="1" label="告警级别" label-placement="left">
|
<NFormItemGi span="1" label="告警级别" label-placement="left">
|
||||||
<NSelect multiple clearable placeholder="请选择告警级别" v-model:value="searchFields.faultLevel_in" :options="faultLevelSelectOptions" />
|
<NSelect multiple clearable placeholder="请选择告警级别" v-model:value="searchFields.faultLevel_in" :options="faultLevelSelectOptions" />
|
||||||
</NFormItemGi>
|
</NFormItemGi>
|
||||||
|
<NFormItemGi span="1" label="恢复状态" label-placement="left">
|
||||||
|
<NSelect
|
||||||
|
clearable
|
||||||
|
placeholder="请选择恢复状态"
|
||||||
|
v-model:value="searchFields.alarmCategory"
|
||||||
|
:options="[
|
||||||
|
{ label: '未恢复', value: '1' },
|
||||||
|
{ label: '已恢复', value: '2' },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</NFormItemGi>
|
||||||
|
<NFormItemGi span="1" label="确认状态" label-placement="left">
|
||||||
|
<NSelect
|
||||||
|
clearable
|
||||||
|
placeholder="请选择确认状态"
|
||||||
|
v-model:value="searchFields.alarmConfirm"
|
||||||
|
:options="[
|
||||||
|
{ label: '未确认', value: '2' },
|
||||||
|
{ label: '已确认', value: '1' },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</NFormItemGi>
|
||||||
<NFormItemGi span="1" label="告警时间" label-placement="left">
|
<NFormItemGi span="1" label="告警时间" label-placement="left">
|
||||||
<NDatePicker v-model:value="searchFields.alarmDate" type="datetimerange" />
|
<NDatePicker v-model:value="searchFields.alarmDate" type="datetimerange" />
|
||||||
</NFormItemGi>
|
</NFormItemGi>
|
||||||
@@ -3,13 +3,12 @@ import type { NdmDeviceResultVO, Station } from '@/apis';
|
|||||||
import { DeviceRenderer, DeviceTree, type DeviceTreeProps } from '@/components';
|
import { DeviceRenderer, DeviceTree, type DeviceTreeProps } from '@/components';
|
||||||
import type { UseDeviceSelectionReturn } from '@/composables';
|
import type { UseDeviceSelectionReturn } from '@/composables';
|
||||||
import { SELECT_DEVICE_FN_INJECTION_KEY } from '@/constants';
|
import { SELECT_DEVICE_FN_INJECTION_KEY } from '@/constants';
|
||||||
import { useStationStore } from '@/stores';
|
import { usePermissionStore } from '@/stores';
|
||||||
import { NLayout, NLayoutContent, NLayoutSider } from 'naive-ui';
|
import { NLayout, NLayoutContent, NLayoutSider } from 'naive-ui';
|
||||||
import { storeToRefs } from 'pinia';
|
import { computed, provide, ref } from 'vue';
|
||||||
import { provide, ref } from 'vue';
|
|
||||||
|
|
||||||
const stationStore = useStationStore();
|
const permissionStore = usePermissionStore();
|
||||||
const { stations } = storeToRefs(stationStore);
|
const stations = computed(() => permissionStore.stations.VIEW ?? []);
|
||||||
|
|
||||||
const selectedStation = ref<Station>();
|
const selectedStation = ref<Station>();
|
||||||
const selectedDevice = ref<NdmDeviceResultVO>();
|
const selectedDevice = ref<NdmDeviceResultVO>();
|
||||||
@@ -32,7 +32,7 @@ const callLogTypeOptions: SelectOption[] = [
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { exportCallLogApi, pageCallLogApi, type NdmCallLog, type NdmCallLogResultVO, type PageQueryExtra, type Station } from '@/apis';
|
import { exportCallLogApi, pageCallLogApi, type NdmCallLog, type NdmCallLogResultVO, type PageQueryExtra, type Station } from '@/apis';
|
||||||
import { useStationStore } from '@/stores';
|
import { usePermissionStore } 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';
|
||||||
@@ -63,8 +63,11 @@ interface SearchFields extends PageQueryExtra<NdmCallLog> {
|
|||||||
createdTime: [string, string];
|
createdTime: [string, string];
|
||||||
}
|
}
|
||||||
|
|
||||||
const stationStore = useStationStore();
|
const permissionStore = usePermissionStore();
|
||||||
const { stations, onlineStations } = storeToRefs(stationStore);
|
const { permissions } = storeToRefs(permissionStore);
|
||||||
|
|
||||||
|
const stations = computed(() => permissionStore.stations.VIEW ?? []);
|
||||||
|
const onlineStations = computed(() => stations.value.filter((station) => station.online));
|
||||||
|
|
||||||
const stationSelectOptions = computed(() => {
|
const stationSelectOptions = computed(() => {
|
||||||
return stations.value.map<SelectOption>((station) => ({
|
return stations.value.map<SelectOption>((station) => ({
|
||||||
@@ -74,6 +77,14 @@ const stationSelectOptions = computed(() => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 权限变化时,需要刷新表格数据
|
||||||
|
watch(permissions, (newPermissions, oldPermissions) => {
|
||||||
|
const oldPermissionsJson = JSON.stringify(oldPermissions);
|
||||||
|
const newPermissionsJson = JSON.stringify(newPermissions);
|
||||||
|
if (oldPermissionsJson === newPermissionsJson) return;
|
||||||
|
onClickReset();
|
||||||
|
});
|
||||||
|
|
||||||
const searchFields = ref<SearchFields>({
|
const searchFields = ref<SearchFields>({
|
||||||
logType_in: [],
|
logType_in: [],
|
||||||
createdTime: [dayjs().startOf('date').subtract(1, 'week').format('YYYY-MM-DD HH:mm:ss'), dayjs().endOf('date').format('YYYY-MM-DD HH:mm:ss')],
|
createdTime: [dayjs().startOf('date').subtract(1, 'week').format('YYYY-MM-DD HH:mm:ss'), dayjs().endOf('date').format('YYYY-MM-DD HH:mm:ss')],
|
||||||
@@ -29,7 +29,7 @@ const vimpLogTypeOptions: SelectOption[] = [
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { exportVimpLogApi, pageVimpLogApi, type NdmVimpLog, type NdmVimpLogResultVO, type PageQueryExtra, type Station } from '@/apis';
|
import { exportVimpLogApi, pageVimpLogApi, type NdmVimpLog, type NdmVimpLogResultVO, type PageQueryExtra, type Station } from '@/apis';
|
||||||
import { useStationStore } from '@/stores';
|
import { usePermissionStore } 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';
|
||||||
@@ -59,8 +59,11 @@ interface SearchFields extends PageQueryExtra<NdmVimpLog> {
|
|||||||
createdTime: [string, string];
|
createdTime: [string, string];
|
||||||
}
|
}
|
||||||
|
|
||||||
const stationStore = useStationStore();
|
const permissionStore = usePermissionStore();
|
||||||
const { stations, onlineStations } = storeToRefs(stationStore);
|
const { permissions } = storeToRefs(permissionStore);
|
||||||
|
|
||||||
|
const stations = computed(() => permissionStore.stations.VIEW ?? []);
|
||||||
|
const onlineStations = computed(() => stations.value.filter((station) => station.online));
|
||||||
|
|
||||||
const stationSelectOptions = computed(() => {
|
const stationSelectOptions = computed(() => {
|
||||||
return stations.value.map<SelectOption>((station) => ({
|
return stations.value.map<SelectOption>((station) => ({
|
||||||
@@ -70,6 +73,14 @@ const stationSelectOptions = computed(() => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 权限变化时,需要刷新表格数据
|
||||||
|
watch(permissions, (newPermissions, oldPermissions) => {
|
||||||
|
const oldPermissionsJson = JSON.stringify(oldPermissions);
|
||||||
|
const newPermissionsJson = JSON.stringify(newPermissions);
|
||||||
|
if (oldPermissionsJson === newPermissionsJson) return;
|
||||||
|
onClickReset();
|
||||||
|
});
|
||||||
|
|
||||||
const searchFields = ref<SearchFields>({
|
const searchFields = ref<SearchFields>({
|
||||||
logType_in: [],
|
logType_in: [],
|
||||||
createdTime: [dayjs().startOf('date').subtract(1, 'week').format('YYYY-MM-DD HH:mm:ss'), dayjs().endOf('date').format('YYYY-MM-DD HH:mm:ss')] as [string, string],
|
createdTime: [dayjs().startOf('date').subtract(1, 'week').format('YYYY-MM-DD HH:mm:ss'), dayjs().endOf('date').format('YYYY-MM-DD HH:mm:ss')] as [string, string],
|
||||||
@@ -22,8 +22,7 @@ const { mutate: login, isPending: loading } = useMutation({
|
|||||||
mutationFn: async (params: LoginParams) => {
|
mutationFn: async (params: LoginParams) => {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
await userStore.userLogin(params);
|
await userStore.userLogin(params);
|
||||||
const [err] = await userClient.post<void>(`/api/ndm/ndmKeepAlive/verify`, {}, { timeout: 5000 });
|
await userStore.userGetInfo();
|
||||||
if (err) throw err;
|
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
window.$message.success('登录成功');
|
window.$message.success('登录成功');
|
||||||
184
src/pages/permission/permission-page.vue
Normal file
184
src/pages/permission/permission-page.vue
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { pageBaseEmployeeApi, type BaseEmployeePageQuery, type BaseEmployeeResultVO } from '@/apis';
|
||||||
|
import { PermissionConfigModal } from '@/components';
|
||||||
|
import { parseErrorFeedback } from '@/utils';
|
||||||
|
import { useMutation } from '@tanstack/vue-query';
|
||||||
|
import { isCancel } from 'axios';
|
||||||
|
import { KeyIcon } from 'lucide-vue-next';
|
||||||
|
import { NButton, NDataTable, NFlex, NForm, NFormItemGi, NGrid, NGridItem, NInput, type DataTableColumns, type DataTableRowData, type PaginationProps } from 'naive-ui';
|
||||||
|
import { h, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
interface SearchFields extends BaseEmployeePageQuery {}
|
||||||
|
|
||||||
|
const searchFields = ref<SearchFields>({});
|
||||||
|
const resetSearchFields = () => {
|
||||||
|
searchFields.value = {
|
||||||
|
realName: '',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const getModelFields = (): BaseEmployeePageQuery => {
|
||||||
|
return {
|
||||||
|
realName: searchFields.value.realName,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchFieldsChanged = ref(false);
|
||||||
|
watch(searchFields, () => {
|
||||||
|
searchFieldsChanged.value = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const showPermissionConfigModal = ref(false);
|
||||||
|
const selectedEmployeeId = ref('');
|
||||||
|
|
||||||
|
const tableColumns: DataTableColumns<BaseEmployeeResultVO> = [
|
||||||
|
{ title: '姓名', key: 'realName', align: 'center' },
|
||||||
|
{ title: '创建时间', key: 'createdTime', align: 'center' },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
align: 'center',
|
||||||
|
width: 150,
|
||||||
|
render: (rowData) => {
|
||||||
|
return h(
|
||||||
|
NButton,
|
||||||
|
{
|
||||||
|
secondary: true,
|
||||||
|
type: 'info',
|
||||||
|
size: 'small',
|
||||||
|
onClick: () => {
|
||||||
|
const { id } = rowData;
|
||||||
|
if (!id) return;
|
||||||
|
selectedEmployeeId.value = id;
|
||||||
|
showPermissionConfigModal.value = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: () => h(KeyIcon),
|
||||||
|
default: () => '配置权限',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tableData = ref<DataTableRowData[]>([]);
|
||||||
|
|
||||||
|
const DEFAULT_PAGE_SIZE = 10;
|
||||||
|
const pagination = reactive<PaginationProps>({
|
||||||
|
showSizePicker: true,
|
||||||
|
page: 1,
|
||||||
|
pageSize: DEFAULT_PAGE_SIZE,
|
||||||
|
pageSizes: [5, 10, 20, 50, 80, 100],
|
||||||
|
itemCount: 0,
|
||||||
|
prefix: ({ itemCount }) => {
|
||||||
|
return h('div', {}, { default: () => `共${itemCount}条` });
|
||||||
|
},
|
||||||
|
onUpdatePage: (page: number) => {
|
||||||
|
pagination.page = page;
|
||||||
|
getTableData();
|
||||||
|
},
|
||||||
|
onUpdatePageSize: (pageSize: number) => {
|
||||||
|
pagination.pageSize = pageSize;
|
||||||
|
pagination.page = 1;
|
||||||
|
getTableData();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const abortController = ref(new AbortController());
|
||||||
|
|
||||||
|
const { mutate: getTableData, isPending: tableLoading } = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
abortController.value.abort();
|
||||||
|
abortController.value = new AbortController();
|
||||||
|
|
||||||
|
const signal = abortController.value.signal;
|
||||||
|
|
||||||
|
const res = await pageBaseEmployeeApi(
|
||||||
|
{
|
||||||
|
model: getModelFields(),
|
||||||
|
extra: {},
|
||||||
|
current: pagination.page ?? 1,
|
||||||
|
size: pagination.pageSize ?? DEFAULT_PAGE_SIZE,
|
||||||
|
order: 'descending',
|
||||||
|
sort: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
onSuccess: (res) => {
|
||||||
|
const { records, size, total } = res;
|
||||||
|
pagination.pageSize = parseInt(size);
|
||||||
|
pagination.itemCount = parseInt(total);
|
||||||
|
tableData.value = records;
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
if (isCancel(error)) return;
|
||||||
|
console.error(error);
|
||||||
|
const errorFeedback = parseErrorFeedback(error);
|
||||||
|
window.$message.error(errorFeedback);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onClickReset = () => {
|
||||||
|
resetSearchFields();
|
||||||
|
pagination.page = 1;
|
||||||
|
pagination.pageSize = DEFAULT_PAGE_SIZE;
|
||||||
|
pagination.itemCount = 0;
|
||||||
|
getTableData();
|
||||||
|
};
|
||||||
|
const onClickQuery = () => {
|
||||||
|
if (searchFieldsChanged.value) {
|
||||||
|
pagination.page = 1;
|
||||||
|
pagination.pageSize = DEFAULT_PAGE_SIZE;
|
||||||
|
searchFieldsChanged.value = false;
|
||||||
|
}
|
||||||
|
getTableData();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getTableData();
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
abortController.value.abort();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NFlex vertical :size="0" style="height: 100%">
|
||||||
|
<!-- 查询面板 -->
|
||||||
|
<NForm style="flex: 0 0 auto; padding: 8px">
|
||||||
|
<NGrid cols="3" :x-gap="24">
|
||||||
|
<NFormItemGi span="1" label="姓名" label-placement="left">
|
||||||
|
<NInput v-model:value="searchFields.realName" />
|
||||||
|
</NFormItemGi>
|
||||||
|
</NGrid>
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<NGrid :cols="1">
|
||||||
|
<NGridItem>
|
||||||
|
<NFlex>
|
||||||
|
<NButton @click="onClickReset">重置</NButton>
|
||||||
|
<NButton type="primary" :loading="tableLoading" @click="onClickQuery">查询</NButton>
|
||||||
|
</NFlex>
|
||||||
|
</NGridItem>
|
||||||
|
</NGrid>
|
||||||
|
</NForm>
|
||||||
|
|
||||||
|
<!-- 数据表格工具栏 -->
|
||||||
|
<NFlex align="center" style="padding: 8px; flex: 0 0 auto">
|
||||||
|
<div style="font-size: medium">用户权限列表</div>
|
||||||
|
<NFlex style="margin-left: auto">
|
||||||
|
<!-- <NButton type="primary" :loading="exporting" @click="() => exportTableData()">导出</NButton> -->
|
||||||
|
</NFlex>
|
||||||
|
</NFlex>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<NDataTable remote :columns="tableColumns" :data="tableData" :pagination="pagination" :loading="tableLoading" :single-line="false" flex-height style="height: 100%; padding: 8px; flex: 1 1 auto" />
|
||||||
|
</NFlex>
|
||||||
|
|
||||||
|
<PermissionConfigModal v-model:show="showPermissionConfigModal" :employee-id="selectedEmployeeId" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { initStationAlarms, initStationDevices, syncCameraApi, syncNvrChannelsApi, type Station } from '@/apis';
|
|
||||||
import { AlarmDetailModal, DeviceDetailModal, DeviceParamConfigModal, IcmpExportModal, RecordCheckExportModal, StationCard, type StationCardProps } from '@/components';
|
|
||||||
import { useLineDevicesQuery } from '@/composables';
|
|
||||||
import { useAlarmStore, useDeviceStore, useSettingStore, useStationStore } from '@/stores';
|
|
||||||
import { parseErrorFeedback } from '@/utils';
|
|
||||||
import { useMutation } from '@tanstack/vue-query';
|
|
||||||
import { NButton, NButtonGroup, NCheckbox, NFlex, NGrid, NGridItem, NScrollbar } from 'naive-ui';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
|
||||||
const { stationGridCols: stationGridColumns } = storeToRefs(settingStore);
|
|
||||||
const stationStore = useStationStore();
|
|
||||||
const { stations } = storeToRefs(stationStore);
|
|
||||||
const deviceStore = useDeviceStore();
|
|
||||||
const { lineDevices } = storeToRefs(deviceStore);
|
|
||||||
const alarmStore = useAlarmStore();
|
|
||||||
const { lineAlarms } = storeToRefs(alarmStore);
|
|
||||||
|
|
||||||
const { refetch: refetchLineDevicesQuery } = useLineDevicesQuery();
|
|
||||||
|
|
||||||
// 操作栏
|
|
||||||
// 当点击操作栏中的一个按钮时,其他按钮会被禁用
|
|
||||||
type Action = 'export-icmp' | 'export-record' | 'sync-camera' | 'sync-nvr' | null;
|
|
||||||
const selectedAction = ref<Action>(null);
|
|
||||||
const showOperation = ref(false);
|
|
||||||
const stationSelectable = ref(false);
|
|
||||||
const stationSelection = ref<Record<Station['code'], boolean>>({});
|
|
||||||
const showIcmpExportModal = ref(false);
|
|
||||||
const showRecordCheckExportModal = ref(false);
|
|
||||||
|
|
||||||
const onToggleSelectAll = (checked: boolean) => {
|
|
||||||
if (!checked) {
|
|
||||||
stationSelection.value = {};
|
|
||||||
} else {
|
|
||||||
stationSelection.value = Object.fromEntries(stations.value.map((station) => [station.code, true]));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAction = (action: Action) => {
|
|
||||||
selectedAction.value = action;
|
|
||||||
if (action === null) return;
|
|
||||||
showOperation.value = true;
|
|
||||||
stationSelectable.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCancel = () => {
|
|
||||||
selectedAction.value = null;
|
|
||||||
showOperation.value = false;
|
|
||||||
stationSelectable.value = false;
|
|
||||||
stationSelection.value = {};
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFinish = onCancel;
|
|
||||||
|
|
||||||
const { mutate: syncCamera, isPending: cameraSyncing } = useMutation({
|
|
||||||
mutationFn: async () => {
|
|
||||||
const stationCodes = Object.entries(stationSelection.value)
|
|
||||||
.filter(([, selected]) => selected)
|
|
||||||
.map(([code]) => code);
|
|
||||||
const results = await Promise.allSettled(stationCodes.map((stationCode) => syncCameraApi({ stationCode })));
|
|
||||||
return results.map((result, index) => ({ ...result, stationCode: stationCodes[index] }));
|
|
||||||
},
|
|
||||||
onSuccess: (results) => {
|
|
||||||
const successCount = results.filter((result) => result.status === 'fulfilled').length;
|
|
||||||
const failures = results.filter((result) => result.status === 'rejected');
|
|
||||||
const failureCount = failures.length;
|
|
||||||
if (failureCount > 0) {
|
|
||||||
const failedStations = failures.map((f) => stations.value.find((s) => s.code === f.stationCode)?.name).join('、');
|
|
||||||
if (successCount === 0) {
|
|
||||||
window.$message.error('摄像机同步全部失败');
|
|
||||||
window.$message.error(`${failedStations}`);
|
|
||||||
} else {
|
|
||||||
window.$message.warning(`摄像机同步完成:成功${successCount}个车站,失败${failureCount}个车站`);
|
|
||||||
window.$message.warning(`${failedStations}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
window.$message.success('摄像机同步成功');
|
|
||||||
}
|
|
||||||
if (successCount > 0) {
|
|
||||||
// 摄像机同步后,需要重新查询一次设备,待测试
|
|
||||||
refetchLineDevicesQuery();
|
|
||||||
}
|
|
||||||
onFinish();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error(error);
|
|
||||||
const errorFeedback = parseErrorFeedback(error);
|
|
||||||
window.$message.error(errorFeedback);
|
|
||||||
onCancel();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutate: syncNvrChannels, isPending: nvrChannelsSyncing } = useMutation({
|
|
||||||
mutationFn: async () => {
|
|
||||||
const stationCodes = Object.entries(stationSelection.value)
|
|
||||||
.filter(([, selected]) => selected)
|
|
||||||
.map(([code]) => code);
|
|
||||||
const results = await Promise.allSettled(stationCodes.map((stationCode) => syncNvrChannelsApi({ stationCode })));
|
|
||||||
return results.map((result, index) => ({ ...result, stationCode: stationCodes[index] }));
|
|
||||||
},
|
|
||||||
onSuccess: (results) => {
|
|
||||||
const successCount = results.filter((result) => result.status === 'fulfilled').length;
|
|
||||||
const failures = results.filter((result) => result.status === 'rejected');
|
|
||||||
const failureCount = failures.length;
|
|
||||||
if (failureCount > 0) {
|
|
||||||
const failedStations = failures.map((failure) => stations.value.find((station) => station.code === failure.stationCode)?.name).join('、');
|
|
||||||
if (successCount === 0) {
|
|
||||||
window.$message.error('录像机通道同步全部失败');
|
|
||||||
window.$message.error(`${failedStations}`);
|
|
||||||
} else {
|
|
||||||
window.$message.warning(`录像机通道同步完成:成功${successCount}个车站,失败${failureCount}个车站`);
|
|
||||||
window.$message.warning(`${failedStations}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
window.$message.success('录像机通道同步成功');
|
|
||||||
}
|
|
||||||
onFinish();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error(error);
|
|
||||||
const errorFeedback = parseErrorFeedback(error);
|
|
||||||
window.$message.error(errorFeedback);
|
|
||||||
onCancel();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const confirming = computed(() => cameraSyncing.value || nvrChannelsSyncing.value);
|
|
||||||
|
|
||||||
const onConfirm = async () => {
|
|
||||||
const noStationSelected = !Object.values(stationSelection.value).some((selected) => selected);
|
|
||||||
if (selectedAction.value === 'export-icmp') {
|
|
||||||
if (noStationSelected) {
|
|
||||||
window.$message.warning('请选择要导出设备状态的车站');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showIcmpExportModal.value = true;
|
|
||||||
} else if (selectedAction.value === 'export-record') {
|
|
||||||
if (noStationSelected) {
|
|
||||||
window.$message.warning('请选择要导出录像诊断的车站');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showRecordCheckExportModal.value = true;
|
|
||||||
} else if (selectedAction.value === 'sync-camera') {
|
|
||||||
if (noStationSelected) {
|
|
||||||
window.$message.warning('请选择要同步摄像机的车站');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
syncCamera();
|
|
||||||
} else if (selectedAction.value === 'sync-nvr') {
|
|
||||||
if (noStationSelected) {
|
|
||||||
window.$message.warning('请选择要同步录像机通道的车站');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
syncNvrChannels();
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 车站卡片的事件
|
|
||||||
const selectedStation = ref<Station>();
|
|
||||||
const showDeviceParamConfigModal = ref(false);
|
|
||||||
const showDeviceDetailModal = ref(false);
|
|
||||||
const showAlarmDetailModal = ref(false);
|
|
||||||
|
|
||||||
const onClickConfig: StationCardProps['onClickConfig'] = (station) => {
|
|
||||||
selectedStation.value = station;
|
|
||||||
showDeviceParamConfigModal.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClickDetail: StationCardProps['onClickDetail'] = (type, station) => {
|
|
||||||
selectedStation.value = station;
|
|
||||||
if (type === 'device') {
|
|
||||||
showDeviceDetailModal.value = true;
|
|
||||||
}
|
|
||||||
if (type === 'alarm') {
|
|
||||||
showAlarmDetailModal.value = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<NScrollbar content-style="padding-right: 8px" style="width: 100%; height: 100%">
|
|
||||||
<!-- 工具栏 -->
|
|
||||||
<NFlex align="center" style="padding: 8px 8px 0 8px">
|
|
||||||
<NButtonGroup>
|
|
||||||
<NButton secondary :focusable="false" :disabled="!!selectedAction && selectedAction !== 'export-icmp'" @click="() => onAction('export-icmp')">导出设备状态</NButton>
|
|
||||||
<NButton secondary :focusable="false" :disabled="!!selectedAction && selectedAction !== 'export-record'" @click="() => onAction('export-record')">导出录像诊断</NButton>
|
|
||||||
<NButton secondary :focusable="false" :disabled="!!selectedAction && selectedAction !== 'sync-camera'" @click="() => onAction('sync-camera')">同步摄像机</NButton>
|
|
||||||
<NButton secondary :focusable="false" :disabled="!!selectedAction && selectedAction !== 'sync-nvr'" @click="() => onAction('sync-nvr')">同步录像机通道</NButton>
|
|
||||||
</NButtonGroup>
|
|
||||||
<template v-if="showOperation">
|
|
||||||
<NCheckbox label="全选" @update:checked="onToggleSelectAll" />
|
|
||||||
<NButton tertiary size="small" type="primary" :focusable="false" :loading="confirming" @click="onConfirm">确定</NButton>
|
|
||||||
<NButton tertiary size="small" type="tertiary" :focusable="false" :disabled="confirming" @click="onCancel">取消</NButton>
|
|
||||||
</template>
|
|
||||||
</NFlex>
|
|
||||||
|
|
||||||
<!-- 车站 -->
|
|
||||||
<NGrid :cols="stationGridColumns" :x-gap="6" :y-gap="6" style="padding: 8px">
|
|
||||||
<NGridItem v-for="station in stations" :key="station.code">
|
|
||||||
<StationCard
|
|
||||||
:station="station"
|
|
||||||
:devices="lineDevices[station.code] ?? initStationDevices()"
|
|
||||||
:alarms="lineAlarms[station.code] ?? initStationAlarms()"
|
|
||||||
:selectable="stationSelectable"
|
|
||||||
v-model:selected="stationSelection[station.code]"
|
|
||||||
@click-detail="onClickDetail"
|
|
||||||
@click-config="onClickConfig"
|
|
||||||
/>
|
|
||||||
</NGridItem>
|
|
||||||
</NGrid>
|
|
||||||
</NScrollbar>
|
|
||||||
|
|
||||||
<IcmpExportModal v-model:show="showIcmpExportModal" :stations="stations.filter((station) => stationSelection[station.code])" @after-leave="onFinish" />
|
|
||||||
<RecordCheckExportModal v-model:show="showRecordCheckExportModal" :stations="stations.filter((station) => stationSelection[station.code])" @after-leave="onFinish" />
|
|
||||||
|
|
||||||
<DeviceParamConfigModal v-model:show="showDeviceParamConfigModal" :station="selectedStation" />
|
|
||||||
<DeviceDetailModal v-model:show="showDeviceDetailModal" :station="selectedStation" />
|
|
||||||
<AlarmDetailModal v-model:show="showAlarmDetailModal" :station="selectedStation" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
|
||||||
215
src/pages/station/station-page.vue
Normal file
215
src/pages/station/station-page.vue
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { initStationAlarms, initStationDevices, syncCameraApi, syncNvrChannelsApi, type Station } from '@/apis';
|
||||||
|
import { AlarmDetailModal, DeviceDetailModal, DeviceParamConfigModal, IcmpExportModal, RecordCheckExportModal, StationCard, type StationCardProps } from '@/components';
|
||||||
|
import { useBatchActions, useLineDevicesQuery } from '@/composables';
|
||||||
|
import { useAlarmStore, useDeviceStore, usePermissionStore, useSettingStore } from '@/stores';
|
||||||
|
import { useMutation } from '@tanstack/vue-query';
|
||||||
|
import { useElementSize } from '@vueuse/core';
|
||||||
|
import { isCancel } from 'axios';
|
||||||
|
import { NButton, NButtonGroup, NCheckbox, NFlex, NGrid, NGridItem, NScrollbar } from 'naive-ui';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { computed, ref, useTemplateRef } from 'vue';
|
||||||
|
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const { stationGridCols } = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const permissionStore = usePermissionStore();
|
||||||
|
const stations = computed(() => permissionStore.stations.VIEW ?? []);
|
||||||
|
|
||||||
|
const deviceStore = useDeviceStore();
|
||||||
|
const { lineDevices } = storeToRefs(deviceStore);
|
||||||
|
|
||||||
|
const alarmStore = useAlarmStore();
|
||||||
|
const { lineAlarms } = storeToRefs(alarmStore);
|
||||||
|
|
||||||
|
const STATION_CARD_MIN_WIDTH = 230;
|
||||||
|
const STATION_GRID_PADDING = 8;
|
||||||
|
const STATION_GRID_GAP = 6;
|
||||||
|
const STATION_GRID_REF_NAME = 'stationGridRef';
|
||||||
|
const stationGridRef = useTemplateRef<HTMLDivElement>(STATION_GRID_REF_NAME);
|
||||||
|
const { width: stationGridWidth } = useElementSize(stationGridRef);
|
||||||
|
// 计算合适的车站布局列数
|
||||||
|
const actualStationGridColumns = computed(() => {
|
||||||
|
const currentStationCardWidth = (stationGridWidth.value - STATION_GRID_PADDING * 2 - (stationGridCols.value - 1) * STATION_GRID_GAP) / stationGridCols.value;
|
||||||
|
// 当卡片宽度大于最小宽度时,说明用户的设置没有问题,直接返回列数
|
||||||
|
if (currentStationCardWidth > STATION_CARD_MIN_WIDTH) return stationGridCols.value;
|
||||||
|
// 否则,说明用户的设置不合适,需要根据当前布局宽度重新计算列数
|
||||||
|
return Math.floor((stationGridWidth.value - STATION_GRID_PADDING * 2 + STATION_GRID_GAP) / STATION_CARD_MIN_WIDTH);
|
||||||
|
});
|
||||||
|
|
||||||
|
const showIcmpExportModal = ref(false);
|
||||||
|
const showRecordCheckExportModal = ref(false);
|
||||||
|
|
||||||
|
const abortController = ref(new AbortController());
|
||||||
|
|
||||||
|
const { batchActions, selectedAction, selectableStations, stationSelection, selectionProps, toggleSelectAction, toggleSelectAllStations, confirmAction, cancelAction } = useBatchActions(
|
||||||
|
stations,
|
||||||
|
abortController,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { refetch: refetchLineDevicesQuery } = useLineDevicesQuery();
|
||||||
|
|
||||||
|
const { mutate: syncCamera, isPending: cameraSyncing } = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
abortController.value.abort();
|
||||||
|
abortController.value = new AbortController();
|
||||||
|
|
||||||
|
const signal = abortController.value.signal;
|
||||||
|
|
||||||
|
const stationCodes = Object.entries(stationSelection.value)
|
||||||
|
.filter(([, selected]) => selected)
|
||||||
|
.map(([code]) => code);
|
||||||
|
const requests = await Promise.allSettled(stationCodes.map((stationCode) => syncCameraApi({ stationCode, signal })));
|
||||||
|
return requests.map((result, index) => ({ ...result, stationCode: stationCodes[index] }));
|
||||||
|
},
|
||||||
|
onSuccess: (requests) => {
|
||||||
|
type PromiseRequest = (typeof requests)[number];
|
||||||
|
const successRequests: PromiseRequest[] = [];
|
||||||
|
const failedRequests: PromiseRequest[] = [];
|
||||||
|
const canceledRequests: PromiseRequest[] = [];
|
||||||
|
for (const request of requests) {
|
||||||
|
if (request.status === 'fulfilled') {
|
||||||
|
successRequests.push(request);
|
||||||
|
} else if (isCancel(request.reason)) {
|
||||||
|
canceledRequests.push(request);
|
||||||
|
} else {
|
||||||
|
failedRequests.push(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const notices: string[] = [`成功 ${successRequests.length} 个车站`, `失败 ${failedRequests.length} 个车站`];
|
||||||
|
if (canceledRequests.length > 0) notices.push(`取消 ${canceledRequests.length} 个车站`);
|
||||||
|
window.$notification.info({
|
||||||
|
title: '摄像机同步结果',
|
||||||
|
content: notices.join(','),
|
||||||
|
duration: 10000,
|
||||||
|
});
|
||||||
|
if (successRequests.length > 0) {
|
||||||
|
// 摄像机同步后,需要重新查询一次设备
|
||||||
|
refetchLineDevicesQuery();
|
||||||
|
}
|
||||||
|
cancelAction();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: syncNvrChannels, isPending: nvrChannelsSyncing } = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
abortController.value.abort();
|
||||||
|
abortController.value = new AbortController();
|
||||||
|
|
||||||
|
const signal = abortController.value.signal;
|
||||||
|
|
||||||
|
const stationCodes = Object.entries(stationSelection.value)
|
||||||
|
.filter(([, selected]) => selected)
|
||||||
|
.map(([code]) => code);
|
||||||
|
const requests = await Promise.allSettled(stationCodes.map((stationCode) => syncNvrChannelsApi({ stationCode, signal })));
|
||||||
|
return requests.map((result, index) => ({ ...result, stationCode: stationCodes[index] }));
|
||||||
|
},
|
||||||
|
onSuccess: (requests) => {
|
||||||
|
type PromiseRequest = (typeof requests)[number];
|
||||||
|
const successRequests: PromiseRequest[] = [];
|
||||||
|
const failedRequests: PromiseRequest[] = [];
|
||||||
|
const canceledRequests: PromiseRequest[] = [];
|
||||||
|
for (const request of requests) {
|
||||||
|
if (request.status === 'fulfilled') {
|
||||||
|
successRequests.push(request);
|
||||||
|
} else if (isCancel(request.reason)) {
|
||||||
|
canceledRequests.push(request);
|
||||||
|
} else {
|
||||||
|
failedRequests.push(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const notices: string[] = [`成功 ${successRequests.length} 个车站`, `失败 ${failedRequests.length} 个车站`];
|
||||||
|
if (canceledRequests.length > 0) notices.push(`取消 ${canceledRequests.length} 个车站`);
|
||||||
|
window.$notification.info({
|
||||||
|
title: '录像机通道同步结果',
|
||||||
|
content: notices.join(','),
|
||||||
|
duration: 10000,
|
||||||
|
});
|
||||||
|
cancelAction();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const confirming = computed(() => cameraSyncing.value || nvrChannelsSyncing.value);
|
||||||
|
|
||||||
|
const onClickConfirmAction = () => {
|
||||||
|
confirmAction({
|
||||||
|
'export-icmp': () => {
|
||||||
|
showIcmpExportModal.value = true;
|
||||||
|
},
|
||||||
|
'export-record': () => {
|
||||||
|
showRecordCheckExportModal.value = true;
|
||||||
|
},
|
||||||
|
'sync-camera': () => {
|
||||||
|
syncCamera();
|
||||||
|
},
|
||||||
|
'sync-nvr': () => {
|
||||||
|
syncNvrChannels();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 车站卡片的事件
|
||||||
|
const modalStation = ref<Station>();
|
||||||
|
const showDeviceParamConfigModal = ref(false);
|
||||||
|
const showDeviceDetailModal = ref(false);
|
||||||
|
const showAlarmDetailModal = ref(false);
|
||||||
|
|
||||||
|
const onClickConfig: StationCardProps['onClickConfig'] = (station) => {
|
||||||
|
modalStation.value = station;
|
||||||
|
showDeviceParamConfigModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickDetail: StationCardProps['onClickDetail'] = (type, station) => {
|
||||||
|
modalStation.value = station;
|
||||||
|
if (type === 'device') {
|
||||||
|
showDeviceDetailModal.value = true;
|
||||||
|
}
|
||||||
|
if (type === 'alarm') {
|
||||||
|
showAlarmDetailModal.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NScrollbar content-style="padding-right: 8px" style="width: 100%; height: 100%">
|
||||||
|
<!-- 工具栏 -->
|
||||||
|
<NFlex align="center" :style="{ padding: `${STATION_GRID_PADDING}px ${STATION_GRID_PADDING}px 0 ${STATION_GRID_PADDING}px` }">
|
||||||
|
<NButtonGroup>
|
||||||
|
<template v-for="batchAction in batchActions" :key="batchAction.key">
|
||||||
|
<NButton :secondary="!batchAction.active" :focusable="false" @click="() => toggleSelectAction(batchAction)">{{ batchAction.label }}</NButton>
|
||||||
|
</template>
|
||||||
|
</NButtonGroup>
|
||||||
|
<template v-if="selectedAction">
|
||||||
|
<NCheckbox label="全选" :disabled="selectionProps.disabled" :checked="selectionProps.checked" :indeterminate="selectionProps.indeterminate" @update:checked="toggleSelectAllStations" />
|
||||||
|
<NButton tertiary size="small" type="primary" :focusable="false" :loading="confirming" @click="onClickConfirmAction">确定</NButton>
|
||||||
|
<NButton tertiary size="small" type="tertiary" :focusable="false" @click="cancelAction">取消</NButton>
|
||||||
|
</template>
|
||||||
|
</NFlex>
|
||||||
|
|
||||||
|
<!-- 车站 -->
|
||||||
|
<div :ref="STATION_GRID_REF_NAME">
|
||||||
|
<NGrid :cols="actualStationGridColumns" :x-gap="STATION_GRID_GAP" :y-gap="STATION_GRID_GAP" :style="{ padding: `${STATION_GRID_PADDING}px` }">
|
||||||
|
<NGridItem v-for="station in stations" :key="station.code">
|
||||||
|
<StationCard
|
||||||
|
:station="station"
|
||||||
|
:devices="lineDevices[station.code] ?? initStationDevices()"
|
||||||
|
:alarms="lineAlarms[station.code] ?? initStationAlarms()"
|
||||||
|
:selectable="!!selectableStations.find((selectable) => selectable.code === station.code)"
|
||||||
|
v-model:selected="stationSelection[station.code]"
|
||||||
|
@click-detail="onClickDetail"
|
||||||
|
@click-config="onClickConfig"
|
||||||
|
/>
|
||||||
|
</NGridItem>
|
||||||
|
</NGrid>
|
||||||
|
</div>
|
||||||
|
</NScrollbar>
|
||||||
|
|
||||||
|
<IcmpExportModal v-model:show="showIcmpExportModal" :stations="stations.filter((station) => stationSelection[station.code])" @after-leave="cancelAction" />
|
||||||
|
<RecordCheckExportModal v-model:show="showRecordCheckExportModal" :stations="stations.filter((station) => stationSelection[station.code])" @after-leave="cancelAction" />
|
||||||
|
|
||||||
|
<DeviceParamConfigModal v-model:show="showDeviceParamConfigModal" :station="modalStation" />
|
||||||
|
<DeviceDetailModal v-model:show="showDeviceDetailModal" :station="modalStation" />
|
||||||
|
<AlarmDetailModal v-model:show="showAlarmDetailModal" :station="modalStation" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
@@ -6,7 +6,7 @@ const router = createRouter({
|
|||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
component: () => import('@/pages/login-page.vue'),
|
component: () => import('@/pages/login/login-page.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -15,22 +15,22 @@ const router = createRouter({
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'station',
|
path: 'station',
|
||||||
component: () => import('@/pages/station-page.vue'),
|
component: () => import('@/pages/station/station-page.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'device',
|
path: 'device',
|
||||||
component: () => import('@/pages/device-page.vue'),
|
component: () => import('@/pages/device/device-page.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'alarm',
|
path: 'alarm',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'alarm-log',
|
path: 'alarm-log',
|
||||||
component: () => import('@/pages/alarm-log-page.vue'),
|
component: () => import('@/pages/alarm/alarm-log-page.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'alarm-ignore',
|
path: 'alarm-ignore',
|
||||||
component: () => import('@/pages/alarm-ignore-page.vue'),
|
component: () => import('@/pages/alarm/alarm-ignore-page.vue'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -39,17 +39,26 @@ const router = createRouter({
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'vimp-log',
|
path: 'vimp-log',
|
||||||
component: () => import('@/pages/vimp-log-page.vue'),
|
component: () => import('@/pages/log/vimp-log-page.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'call-log',
|
path: 'call-log',
|
||||||
component: () => import('@/pages/call-log-page.vue'),
|
component: () => import('@/pages/log/call-log-page.vue'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'permission',
|
||||||
|
component: () => import('@/pages/permission/permission-page.vue'),
|
||||||
|
beforeEnter: () => {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
if (userStore.isLamp) return true;
|
||||||
|
return { path: '/404' };
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
component: () => import('@/pages/not-found-page.vue'),
|
component: () => import('@/pages/error/not-found-page.vue'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { initStationAlarms, type LineAlarms, type NdmDeviceAlarmLogResultVO, type Station, type StationAlarms } from '@/apis';
|
import { type LineAlarms, type Station, type StationAlarms } from '@/apis';
|
||||||
import { NDM_ALARM_STORE_ID } from '@/constants';
|
import { NDM_ALARM_STORE_ID } from '@/constants';
|
||||||
import { tryGetDeviceType } from '@/enums';
|
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { computed, shallowRef, triggerRef } from 'vue';
|
import { shallowRef, triggerRef } from 'vue';
|
||||||
|
|
||||||
export const useAlarmStore = defineStore(
|
export const useAlarmStore = defineStore(
|
||||||
NDM_ALARM_STORE_ID,
|
NDM_ALARM_STORE_ID,
|
||||||
@@ -18,42 +17,10 @@ export const useAlarmStore = defineStore(
|
|||||||
triggerRef(lineAlarms);
|
triggerRef(lineAlarms);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 全线所有车站的未读告警 (来自stomp订阅)
|
|
||||||
const unreadLineAlarms = shallowRef<LineAlarms>({});
|
|
||||||
const unreadAlarmCount = computed(() => {
|
|
||||||
let count = 0;
|
|
||||||
Object.values(unreadLineAlarms.value).forEach((stationAlarms) => {
|
|
||||||
count += stationAlarms['unclassified'].length;
|
|
||||||
});
|
|
||||||
return count;
|
|
||||||
});
|
|
||||||
const pushUnreadAlarm = (alarm: NdmDeviceAlarmLogResultVO) => {
|
|
||||||
const stationCode = alarm.stationCode;
|
|
||||||
if (!stationCode) return;
|
|
||||||
if (!unreadLineAlarms.value[stationCode]) {
|
|
||||||
unreadLineAlarms.value[stationCode] = initStationAlarms();
|
|
||||||
}
|
|
||||||
const deviceType = tryGetDeviceType(alarm.deviceType);
|
|
||||||
if (!deviceType) return;
|
|
||||||
const stationAlarms = unreadLineAlarms.value[stationCode];
|
|
||||||
stationAlarms[deviceType].push(alarm);
|
|
||||||
stationAlarms['unclassified'].push(alarm);
|
|
||||||
triggerRef(unreadLineAlarms);
|
|
||||||
};
|
|
||||||
const clearUnreadAlarms = () => {
|
|
||||||
unreadLineAlarms.value = {};
|
|
||||||
triggerRef(unreadLineAlarms);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lineAlarms,
|
lineAlarms,
|
||||||
setLineAlarms,
|
setLineAlarms,
|
||||||
setStationAlarms,
|
setStationAlarms,
|
||||||
|
|
||||||
unreadLineAlarms,
|
|
||||||
unreadAlarmCount,
|
|
||||||
pushUnreadAlarm,
|
|
||||||
clearUnreadAlarms,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from './alarm';
|
export * from './alarm';
|
||||||
export * from './device';
|
export * from './device';
|
||||||
export * from './polling';
|
export * from './permission';
|
||||||
export * from './setting';
|
export * from './setting';
|
||||||
export * from './station';
|
export * from './station';
|
||||||
|
export * from './unread';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
|
|||||||
77
src/stores/permission.ts
Normal file
77
src/stores/permission.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import type { NdmPermissionResultVO, Station } from '@/apis';
|
||||||
|
import { NDM_PERMISSION_STORE_ID } from '@/constants';
|
||||||
|
import { PERMISSION_TYPE_NAMES, type PermissionType } from '@/enums';
|
||||||
|
import { useStationStore } from '@/stores';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { objectEntries } from '@vueuse/core';
|
||||||
|
|
||||||
|
type Permissions = Record<Station['code'], PermissionType[]>;
|
||||||
|
|
||||||
|
export const usePermissionStore = defineStore(
|
||||||
|
NDM_PERMISSION_STORE_ID,
|
||||||
|
() => {
|
||||||
|
const stationStore = useStationStore();
|
||||||
|
|
||||||
|
const permissionRecords = ref<NdmPermissionResultVO[] | null>(null);
|
||||||
|
|
||||||
|
const permissions = computed<Permissions>(() => {
|
||||||
|
const result: Permissions = {};
|
||||||
|
|
||||||
|
const records = permissionRecords.value;
|
||||||
|
|
||||||
|
// 如果权限记录不存在,则不做权限配置
|
||||||
|
if (!records) return result;
|
||||||
|
|
||||||
|
// 如果该用户没有任何权限记录,则开放所有权限,否则根据记录配置权限
|
||||||
|
if (records.length === 0) {
|
||||||
|
stationStore.stations.forEach((station) => {
|
||||||
|
result[station.code] = [...objectEntries(PERMISSION_TYPE_NAMES).map(([permType]) => permType)];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
stationStore.stations.forEach((station) => {
|
||||||
|
result[station.code] = [];
|
||||||
|
const stationPermRecords = records.filter((record) => record.stationCode === station.code);
|
||||||
|
if (stationPermRecords.length === 0) return;
|
||||||
|
stationPermRecords.forEach(({ type: permType }) => {
|
||||||
|
if (!permType) return;
|
||||||
|
result[station.code]?.push(permType);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按权限对车站进行分类
|
||||||
|
const stations = computed(() => {
|
||||||
|
const result: Partial<Record<PermissionType, Station[]>> = {};
|
||||||
|
// 按原始的车站顺序进行遍历,保持显示顺序不变
|
||||||
|
stationStore.stations.forEach((station) => {
|
||||||
|
const permissionTypes = permissions.value[station.code];
|
||||||
|
if (!permissionTypes) return;
|
||||||
|
permissionTypes.forEach((permissionType) => {
|
||||||
|
if (!result[permissionType]) result[permissionType] = [];
|
||||||
|
result[permissionType].push(station);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const setPermRecords = (records: NdmPermissionResultVO[]) => {
|
||||||
|
permissionRecords.value = records;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
permissionRecords,
|
||||||
|
|
||||||
|
permissions,
|
||||||
|
stations,
|
||||||
|
|
||||||
|
setPermRecords,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
persist: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -1,36 +1,71 @@
|
|||||||
import { NDM_SETTING_STORE_ID } from '@/constants';
|
import { useUserStore } from './user';
|
||||||
|
import { LINE_ALARMS_QUERY_KEY, LINE_DEVICES_QUERY_KEY, LINE_STATIONS_QUERY_KEY, NDM_SETTING_STORE_ID } from '@/constants';
|
||||||
|
import router from '@/router';
|
||||||
|
import { useQueryClient } from '@tanstack/vue-query';
|
||||||
import { darkTheme, lightTheme } from 'naive-ui';
|
import { darkTheme, lightTheme } from 'naive-ui';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { useUserStore } from './user';
|
|
||||||
import router from '@/router';
|
|
||||||
|
|
||||||
export const useSettingStore = defineStore(
|
export const useSettingStore = defineStore(
|
||||||
NDM_SETTING_STORE_ID,
|
NDM_SETTING_STORE_ID,
|
||||||
() => {
|
() => {
|
||||||
const darkThemeEnabled = ref(true);
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
// 主题设置
|
||||||
|
const darkMode = ref(true);
|
||||||
const themeMode = computed(() => {
|
const themeMode = computed(() => {
|
||||||
return darkThemeEnabled.value ? darkTheme : lightTheme;
|
return darkMode.value ? darkTheme : lightTheme;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 布局设置
|
||||||
const menuCollpased = ref(false);
|
const menuCollpased = ref(false);
|
||||||
|
|
||||||
const stationGridCols = ref(6);
|
const stationGridCols = ref(6);
|
||||||
|
|
||||||
const debugModeEnabled = ref(false);
|
// 调试模式
|
||||||
const enableDebugMode = () => {
|
const debugMode = ref(false);
|
||||||
debugModeEnabled.value = true;
|
/* 数据设置 */
|
||||||
};
|
// 显示设备原始数据
|
||||||
const disableDebugMode = () => {
|
const showDeviceRawData = ref(false);
|
||||||
debugModeEnabled.value = false;
|
/* 网络设置 */
|
||||||
};
|
// 轮询车站
|
||||||
|
const pollingStations = ref(true);
|
||||||
|
// 主动请求
|
||||||
|
const activeRequests = ref(true);
|
||||||
|
// 订阅消息
|
||||||
|
const subscribeMessages = ref(true);
|
||||||
|
// 模拟用户
|
||||||
|
const mockUser = ref(false);
|
||||||
|
/* 数据库设置 */
|
||||||
|
// 使用本地数据库
|
||||||
|
const useLocalDB = ref(false);
|
||||||
|
|
||||||
// 离线开发模式
|
watch(debugMode, (newValue, oldValue) => {
|
||||||
// 控制 版本轮询 stomp连接 app-layout中的自动getUserInfo
|
// 监听关闭调试模式
|
||||||
const offlineDev = ref(false);
|
if (oldValue && !newValue) {
|
||||||
watch(offlineDev, (newValue, oldValue) => {
|
showDeviceRawData.value = false;
|
||||||
// 如果启用离线开发模式且当前未登录 自动填写token以绕过路由守卫并跳过登录页
|
pollingStations.value = true;
|
||||||
|
activeRequests.value = true;
|
||||||
|
subscribeMessages.value = false;
|
||||||
|
mockUser.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(pollingStations, (newValue, oldValue) => {
|
||||||
|
// 监听关闭车站轮询
|
||||||
|
if (oldValue && !newValue) {
|
||||||
|
queryClient.cancelQueries({ queryKey: [LINE_STATIONS_QUERY_KEY] });
|
||||||
|
queryClient.cancelQueries({ queryKey: [LINE_DEVICES_QUERY_KEY] });
|
||||||
|
queryClient.cancelQueries({ queryKey: [LINE_ALARMS_QUERY_KEY] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: [LINE_STATIONS_QUERY_KEY] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: [LINE_DEVICES_QUERY_KEY] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: [LINE_ALARMS_QUERY_KEY] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(mockUser, (newValue, oldValue) => {
|
||||||
|
// 监听启用模拟用户
|
||||||
if (!oldValue && newValue) {
|
if (!oldValue && newValue) {
|
||||||
|
// 如果启当前未登录,填写token以绕过路由守卫
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
if (!userStore.userLoginResult) {
|
if (!userStore.userLoginResult) {
|
||||||
userStore.userLoginResult = {
|
userStore.userLoginResult = {
|
||||||
@@ -42,9 +77,11 @@ export const useSettingStore = defineStore(
|
|||||||
expiration: '',
|
expiration: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// 如果token为空,填写token
|
||||||
if (!userStore.userLoginResult.token) {
|
if (!userStore.userLoginResult.token) {
|
||||||
userStore.userLoginResult.token = 'test';
|
userStore.userLoginResult.token = 'test';
|
||||||
}
|
}
|
||||||
|
// 如果用户信息为空,填写用户信息
|
||||||
if (!userStore.userInfo) {
|
if (!userStore.userInfo) {
|
||||||
userStore.userInfo = {
|
userStore.userInfo = {
|
||||||
id: '2',
|
id: '2',
|
||||||
@@ -55,35 +92,42 @@ export const useSettingStore = defineStore(
|
|||||||
tenantId: '1',
|
tenantId: '1',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// 如果当前路由为登录页,跳转到首页
|
||||||
if (router.currentRoute.value.path === '/login') {
|
if (router.currentRoute.value.path === '/login') {
|
||||||
router.push({ path: '/' });
|
router.push({ path: '/' });
|
||||||
}
|
}
|
||||||
|
// 开启模拟用户时,也开启调试模式,但关闭其他的网络设置
|
||||||
|
debugMode.value = true;
|
||||||
|
pollingStations.value = false;
|
||||||
|
activeRequests.value = false;
|
||||||
|
subscribeMessages.value = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
darkThemeEnabled,
|
darkMode,
|
||||||
themeMode,
|
themeMode,
|
||||||
|
|
||||||
menuCollpased,
|
menuCollpased,
|
||||||
|
|
||||||
stationGridCols,
|
stationGridCols,
|
||||||
|
|
||||||
debugModeEnabled,
|
debugMode,
|
||||||
enableDebugMode,
|
showDeviceRawData,
|
||||||
disableDebugMode,
|
pollingStations,
|
||||||
|
activeRequests,
|
||||||
offlineDev,
|
subscribeMessages,
|
||||||
|
mockUser,
|
||||||
|
useLocalDB,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
persist: [
|
persist: [
|
||||||
{
|
{
|
||||||
omit: ['debugModeEnabled'],
|
omit: ['showDeviceRawData'],
|
||||||
storage: window.localStorage,
|
storage: window.localStorage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pick: ['debugModeEnabled'],
|
pick: ['showDeviceRawData'],
|
||||||
storage: window.sessionStorage,
|
storage: window.sessionStorage,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
44
src/stores/unread.ts
Normal file
44
src/stores/unread.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { initStationAlarms, type LineAlarms, type NdmDeviceAlarmLogResultVO } from '@/apis';
|
||||||
|
import { NDM_UNREAD_STORE_ID } from '@/constants';
|
||||||
|
import { tryGetDeviceType } from '@/enums';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { computed, shallowRef, triggerRef } from 'vue';
|
||||||
|
|
||||||
|
export const useUnreadStore = defineStore(NDM_UNREAD_STORE_ID, () => {
|
||||||
|
// 全线所有车站的未读告警 (来自stomp订阅)
|
||||||
|
const unreadLineAlarms = shallowRef<LineAlarms>({});
|
||||||
|
const unreadAlarmCount = computed(() => {
|
||||||
|
let count = 0;
|
||||||
|
Object.values(unreadLineAlarms.value).forEach((stationAlarms) => {
|
||||||
|
count += stationAlarms['unclassified'].length;
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
});
|
||||||
|
|
||||||
|
const pushUnreadAlarm = (alarm: NdmDeviceAlarmLogResultVO) => {
|
||||||
|
const stationCode = alarm.stationCode;
|
||||||
|
if (!stationCode) return;
|
||||||
|
if (!unreadLineAlarms.value[stationCode]) {
|
||||||
|
unreadLineAlarms.value[stationCode] = initStationAlarms();
|
||||||
|
}
|
||||||
|
const deviceType = tryGetDeviceType(alarm.deviceType);
|
||||||
|
if (!deviceType) return;
|
||||||
|
const stationAlarms = unreadLineAlarms.value[stationCode];
|
||||||
|
stationAlarms[deviceType].push(alarm);
|
||||||
|
stationAlarms['unclassified'].push(alarm);
|
||||||
|
triggerRef(unreadLineAlarms);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearUnreadAlarms = () => {
|
||||||
|
unreadLineAlarms.value = {};
|
||||||
|
triggerRef(unreadLineAlarms);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
unreadLineAlarms,
|
||||||
|
unreadAlarmCount,
|
||||||
|
|
||||||
|
pushUnreadAlarm,
|
||||||
|
clearUnreadAlarms,
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user