Compare commits
9 Commits
0af52c62ce
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db831e82ff | ||
|
|
6bf205f461 | ||
|
|
cf3d19d89d | ||
|
|
b020226538 | ||
|
|
b79b1df57e | ||
|
|
9b21beed0f | ||
|
|
aa4684273b | ||
|
|
36e839142a | ||
|
|
03006a8f06 |
143
README.md
143
README.md
@@ -39,3 +39,146 @@ pnpm build
|
|||||||
```
|
```
|
||||||
|
|
||||||
在执行 `pnpm build` 之前,你可以在 `package.json` 中修改 `version` 字段,将其设置为你期望的版本号,构建完成后,项目的根目录中除了 `dist` 目录外,还会生成三个压缩包,文件名的格式统一为 `ndm-web-platform_v<version>_<datetime>`,文件格式则分别为 `zip`、`tar`、`tar.gz`。
|
在执行 `pnpm build` 之前,你可以在 `package.json` 中修改 `version` 字段,将其设置为你期望的版本号,构建完成后,项目的根目录中除了 `dist` 目录外,还会生成三个压缩包,文件名的格式统一为 `ndm-web-platform_v<version>_<datetime>`,文件格式则分别为 `zip`、`tar`、`tar.gz`。
|
||||||
|
|
||||||
|
## 业务结构
|
||||||
|
|
||||||
|
所有业务相关的页面都在 `src/pages` 目录下,路由配置在 `src/router/index.ts` 文件,除登录页之外,其余页面都作为 `src/layouts/app-layout.vue` 的子路由。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
src/
|
||||||
|
router/
|
||||||
|
index.ts # 路由配置文件
|
||||||
|
layouts/
|
||||||
|
app-layout.vue # 布局
|
||||||
|
pages/
|
||||||
|
login/
|
||||||
|
login-page.vue # 登录页面
|
||||||
|
station/
|
||||||
|
station-page.vue # 车站状态页面(首页)
|
||||||
|
device/
|
||||||
|
device-page.vue # 设备诊断页面
|
||||||
|
alarm/
|
||||||
|
alarm-ignore-page.vue # 告警忽略管理页面
|
||||||
|
alarm-log-page.vue # 设备告警记录页面
|
||||||
|
log/
|
||||||
|
call-log-page.vue # 上级调用日志页面
|
||||||
|
vimp-log-page.vue # 视频平台日志页面
|
||||||
|
permission/
|
||||||
|
permission-page.vue # 权限管理页面
|
||||||
|
error/
|
||||||
|
not-found-page.vue # 404 页面
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据轮询
|
||||||
|
|
||||||
|
由于后端服务的架构限制,需要前端向所有车站服务依次发送请求来获取数据,需要获取的数据包含车站状态、设备数据以及告警数据,因此需要设计一套数据轮询方案,定期从所有车站服务获取数据。
|
||||||
|
|
||||||
|
在项目中,`src/composables/query/` 目录下是所有数据轮询相关的代码,其中与业务相关的代码主要包括:
|
||||||
|
|
||||||
|
- `use-line-stations-query.ts`: 查询所有车站
|
||||||
|
- `use-line-devices-query.ts`: 查询所有设备
|
||||||
|
- `use-line-alarms-query.ts`: 查询所有告警
|
||||||
|
- `use-user-permission-query.ts`: 查询用户权限
|
||||||
|
|
||||||
|
在描述整个数据轮询流程之前,我们要明确项目中必须存在的几个关键概念:
|
||||||
|
|
||||||
|
- 车站相关:车站query + 车站store
|
||||||
|
- 设备相关:设备query + 设备store
|
||||||
|
- 告警相关:告警query + 告警store
|
||||||
|
- 权限相关:权限query + 权限store
|
||||||
|
|
||||||
|
整个数据轮询流程采用“单点驱动 + 变更监听 + 级联触发”的模式,如下图所示。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1. 轮询入口:车站query
|
||||||
|
- 触发条件:以120秒的周期自动轮询车站列表
|
||||||
|
- 数据流向:车站store
|
||||||
|
2. 核心调度:权限query
|
||||||
|
- 触发条件:车站query执行后触发
|
||||||
|
- 数据流向:权限store,并计算当前用户在各车站的权限
|
||||||
|
- 数据监听:监听车站和权限变化,触发设备query和告警query
|
||||||
|
3. 设备query & 告警query
|
||||||
|
- 触发条件:被动触发,由权限query主动调用
|
||||||
|
- 数据流向:设备store & 告警store
|
||||||
|
|
||||||
|
## 调试模式
|
||||||
|
|
||||||
|
在设置面板中有一系列与调试模式有关的设置项,主要用于开发和故障排查。
|
||||||
|
|
||||||
|
### 开启方式
|
||||||
|
|
||||||
|
调试模式默认隐藏,通过以下方式开启:
|
||||||
|
|
||||||
|
1. 使用快捷键 `Ctrl + Alt + D` 唤起验证弹窗
|
||||||
|
2. 输入授权码进行验证(授权码对应环境变量 `.env` 中的 `VITE_DEBUG_CODE`)
|
||||||
|
3. 验证通过后,在“系统设置”面板中会出现 **调试** 分组
|
||||||
|
|
||||||
|
### 设置项说明
|
||||||
|
|
||||||
|
#### 数据设置
|
||||||
|
|
||||||
|
- **显示设备原始数据**
|
||||||
|
- 控制是否在设备详情页显示“原始数据”标签页
|
||||||
|
- 开启后可查看设备接口返回的原始 JSON 数据,便于排查字段缺失或格式错误
|
||||||
|
|
||||||
|
#### 网络设置
|
||||||
|
|
||||||
|
- **轮询车站**
|
||||||
|
- 控制是否定时拉取车站状态,进而触发权限、设备及告警数据的更新
|
||||||
|
- 关闭后将暂停所有业务数据的自动轮询机制
|
||||||
|
- **主动请求**
|
||||||
|
- 控制组件挂载时是否自动发起数据请求
|
||||||
|
- 涵盖设备在线状态检测、用户登录验证等逻辑,关闭后组件在初始化时将不再自动拉取数据
|
||||||
|
- **订阅消息**
|
||||||
|
- 控制是否通过 WebSocket (STOMP) 接收实时告警或状态推送
|
||||||
|
- 关闭后将不再处理后端推送的实时消息
|
||||||
|
- **模拟用户**
|
||||||
|
- 开启后使用内置的超管用户绕过登录
|
||||||
|
- 开启时会自动进入调试模式,便于开发环境快速测试
|
||||||
|
|
||||||
|
#### 数据库设置
|
||||||
|
|
||||||
|
- **直接操作本地数据库**
|
||||||
|
- 控制某些业务逻辑(如交换机端口、安防箱回路)是否直接读写本地 IndexedDB
|
||||||
|
- 用于在无后端环境或特定测试场景下验证本地数据逻辑
|
||||||
|
|
||||||
|
## 离线开发
|
||||||
|
|
||||||
|
项目支持在无后端服务的情况下正常启动,具体操作取决于你的本地环境是否已有历史数据。
|
||||||
|
|
||||||
|
### 场景一:已有本地缓存
|
||||||
|
|
||||||
|
如果你的浏览器曾接入过现场环境,IndexedDB 中已保存了车站、设备等数据,只需在设置中关闭网络请求即可进入离线模式:
|
||||||
|
|
||||||
|
1. 开启调试模式(`Ctrl + Alt + D`)。
|
||||||
|
2. 在“网络设置”中,关闭 **轮询车站**、**主动请求** 和 **订阅消息**。
|
||||||
|
3. 此时平台将停止向后端发起请求,直接展示本地缓存的历史数据。
|
||||||
|
|
||||||
|
### 场景二:全新环境启动(新人推荐)
|
||||||
|
|
||||||
|
如果你是首次拉取项目且无法连接后端,需要按以下步骤操作:
|
||||||
|
|
||||||
|
1. **模拟登录**
|
||||||
|
在登录页按 `F12` 打开控制台,输入以下命令强制进入平台:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
window.$mockUser.value = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
执行后平台将自动完成以下操作:
|
||||||
|
- 注入测试 Token 和管理员身份信息
|
||||||
|
- 关闭所有网络请求(轮询、主动请求、消息订阅)
|
||||||
|
- 开启调试模式
|
||||||
|
- 自动跳转至平台首页
|
||||||
|
|
||||||
|
2. **导入模拟数据**
|
||||||
|
进入平台后,页面默认为空。需导入预设数据以填充内容:
|
||||||
|
- 打开“系统设置”(已自动开启调试模式)。
|
||||||
|
- 在 **调试** -> **数据库设置** 中,勾选 **直接操作本地数据库**。
|
||||||
|
- 点击该选项下方的 **导入数据** 按钮。
|
||||||
|
- 依次导入项目根目录 `docs/data/` 下的三个文件:
|
||||||
|
- `ndm-station-store.json`(车站数据)
|
||||||
|
- `ndm-device-store.json`(设备数据)
|
||||||
|
- `ndm-alarm-store.json`(告警数据)
|
||||||
|
> **注意**:每次导入一个文件后,平台会自动刷新页面以应用数据。请等待刷新完成后,重新打开设置面板导入下一个文件。
|
||||||
|
|||||||
BIN
docs/assets/query-chain.png
Normal file
BIN
docs/assets/query-chain.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
527708
docs/data/ndm-alarm-store.json
Normal file
527708
docs/data/ndm-alarm-store.json
Normal file
File diff suppressed because it is too large
Load Diff
195971
docs/data/ndm-device-store.json
Normal file
195971
docs/data/ndm-device-store.json
Normal file
File diff suppressed because one or more lines are too long
203
docs/data/ndm-station-store.json
Normal file
203
docs/data/ndm-station-store.json
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
{
|
||||||
|
"stations": [
|
||||||
|
{
|
||||||
|
"code": "1075",
|
||||||
|
"name": "吴中路控制中心",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.128.10",
|
||||||
|
"occ": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1001",
|
||||||
|
"name": "虹桥火车站",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.129.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1002",
|
||||||
|
"name": "虹桥2号航站楼",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.131.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1003",
|
||||||
|
"name": "虹桥一号航站楼",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.133.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1004",
|
||||||
|
"name": "上海动物园",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.135.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1005",
|
||||||
|
"name": "龙溪路",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.137.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1006",
|
||||||
|
"name": "水城路",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.139.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1007",
|
||||||
|
"name": "伊犁路",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.141.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1008",
|
||||||
|
"name": "宋园路",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.143.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1009",
|
||||||
|
"name": "虹桥路",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.145.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1010",
|
||||||
|
"name": "交通大学",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.147.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1011",
|
||||||
|
"name": "图书馆",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.149.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1012",
|
||||||
|
"name": "陕西南路",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.151.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1013",
|
||||||
|
"name": "新天地",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.153.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1014",
|
||||||
|
"name": "老西门",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.155.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1015",
|
||||||
|
"name": "豫园",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.157.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1016",
|
||||||
|
"name": "南京东路",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.159.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1017",
|
||||||
|
"name": "天潼路",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.161.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1018",
|
||||||
|
"name": "四川北路",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.163.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1019",
|
||||||
|
"name": "海伦路",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.165.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1020",
|
||||||
|
"name": "邮电新村",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.167.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1021",
|
||||||
|
"name": "四平路",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.169.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1022",
|
||||||
|
"name": "同济大学",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.171.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1023",
|
||||||
|
"name": "国权路",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.173.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1024",
|
||||||
|
"name": "五角场",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.175.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1025",
|
||||||
|
"name": "江湾体育场",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.177.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1026",
|
||||||
|
"name": "三门路",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.179.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1027",
|
||||||
|
"name": "殷高东路",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.181.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1028",
|
||||||
|
"name": "新江湾城",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.183.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1029",
|
||||||
|
"name": "航中路",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.185.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1030",
|
||||||
|
"name": "紫藤路",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.187.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1031",
|
||||||
|
"name": "龙柏新村",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.189.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "1032",
|
||||||
|
"name": "吴中路基地",
|
||||||
|
"online": true,
|
||||||
|
"ip": "10.18.244.10"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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,29 +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 { 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 { isCancel } from 'axios';
|
import { isCancel } from 'axios';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { DownloadIcon, RotateCwIcon } from 'lucide-vue-next';
|
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 { computed, onBeforeUnmount, onMounted, ref, toRefs, watch } from 'vue';
|
import { storeToRefs } from 'pinia';
|
||||||
|
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 queryClient = useQueryClient();
|
||||||
|
|
||||||
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) {
|
||||||
@@ -35,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();
|
||||||
@@ -114,7 +123,7 @@ const { mutate: reloadRecordCheckByGbId } = useMutation({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
getRecordCheckByParentId();
|
refetchRecordChecks();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (isCancel(error)) return;
|
if (isCancel(error)) return;
|
||||||
@@ -124,20 +133,6 @@ const { mutate: reloadRecordCheckByGbId } = useMutation({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getRecordCheckByParentId();
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => ndmDevice.value.id,
|
|
||||||
(devieDbId) => {
|
|
||||||
if (devieDbId) {
|
|
||||||
recordChecks.value = [];
|
|
||||||
getRecordCheckByParentId();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
abortController.value.abort();
|
abortController.value.abort();
|
||||||
});
|
});
|
||||||
@@ -162,7 +157,7 @@ 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="RotateCwIcon" />
|
<NIcon :component="RotateCwIcon" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ 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';
|
||||||
@@ -40,6 +42,8 @@ const { lineDevices } = storeToRefs(deviceStore);
|
|||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { useLocalDB } = storeToRefs(settingStore);
|
const { useLocalDB } = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
const { ndmDevice, station, circuits } = toRefs(props);
|
const { ndmDevice, station, circuits } = toRefs(props);
|
||||||
|
|
||||||
const showCard = computed(() => !!circuits.value && circuits.value.length > 0);
|
const showCard = computed(() => !!circuits.value && circuits.value.length > 0);
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -27,6 +29,8 @@ const { lineDevices } = storeToRefs(deviceStore);
|
|||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const { useLocalDB } = storeToRefs(settingStore);
|
const { useLocalDB } = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
const { ndmDevice, station, ports } = toRefs(props);
|
const { ndmDevice, station, ports } = toRefs(props);
|
||||||
|
|
||||||
const showCard = computed(() => !!ports.value);
|
const showCard = computed(() => !!ports.value);
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ const activeTabName = ref('当前诊断');
|
|||||||
const onTabChange = (name: string) => {
|
const onTabChange = (name: string) => {
|
||||||
activeTabName.value = name;
|
activeTabName.value = name;
|
||||||
};
|
};
|
||||||
watch([ndmDevice, showDeviceRawData], ([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 = '当前诊断';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ const activeTabName = ref('当前诊断');
|
|||||||
const onTabChange = (name: string) => {
|
const onTabChange = (name: string) => {
|
||||||
activeTabName.value = name;
|
activeTabName.value = name;
|
||||||
};
|
};
|
||||||
watch([ndmDevice, showDeviceRawData], ([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 = '当前诊断';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ const activeTabName = ref('当前诊断');
|
|||||||
const onTabChange = (name: string) => {
|
const onTabChange = (name: string) => {
|
||||||
activeTabName.value = name;
|
activeTabName.value = name;
|
||||||
};
|
};
|
||||||
watch([ndmDevice, showDeviceRawData], ([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 = '当前诊断';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ const activeTabName = ref('当前诊断');
|
|||||||
const onTabChange = (name: string) => {
|
const onTabChange = (name: string) => {
|
||||||
activeTabName.value = name;
|
activeTabName.value = name;
|
||||||
};
|
};
|
||||||
watch([ndmDevice, showDeviceRawData], ([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 = '当前诊断';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ const activeTabName = ref('当前诊断');
|
|||||||
const onTabChange = (name: string) => {
|
const onTabChange = (name: string) => {
|
||||||
activeTabName.value = name;
|
activeTabName.value = name;
|
||||||
};
|
};
|
||||||
watch([ndmDevice, showDeviceRawData], ([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 = '当前诊断';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ const activeTabName = ref('当前诊断');
|
|||||||
const onTabChange = (name: string) => {
|
const onTabChange = (name: string) => {
|
||||||
activeTabName.value = name;
|
activeTabName.value = name;
|
||||||
};
|
};
|
||||||
watch([ndmDevice, showDeviceRawData], ([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 = '当前诊断';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ watch(activeRequests, (active) => {
|
|||||||
<span>服务状态</span>
|
<span>服务状态</span>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<template v-if="activeRequests">
|
<template v-if="!activeRequests">
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ const activeTabName = ref('当前诊断');
|
|||||||
const onTabChange = (name: string) => {
|
const onTabChange = (name: string) => {
|
||||||
activeTabName.value = name;
|
activeTabName.value = name;
|
||||||
};
|
};
|
||||||
watch([ndmDevice, showDeviceRawData], ([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 = '当前诊断';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const streamPushStat = computed(() => {
|
|||||||
<span>推流统计</span>
|
<span>推流统计</span>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<template v-if="activeRequests">
|
<template v-if="!activeRequests">
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ const activeTabName = ref('当前诊断');
|
|||||||
const onTabChange = (name: string) => {
|
const onTabChange = (name: string) => {
|
||||||
activeTabName.value = name;
|
activeTabName.value = name;
|
||||||
};
|
};
|
||||||
watch([ndmDevice, showDeviceRawData], ([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 = '当前诊断';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useDeviceTree, usePermission, type UseDeviceTreeReturn } from '@/compos
|
|||||||
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType, PERMISSION_TYPE_LITERALS } from '@/enums';
|
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType, PERMISSION_TYPE_LITERALS } from '@/enums';
|
||||||
import { isNvrCluster } from '@/helpers';
|
import { isNvrCluster } from '@/helpers';
|
||||||
import { useDeviceStore, usePermissionStore } 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<{
|
||||||
/**
|
/**
|
||||||
@@ -67,15 +67,15 @@ 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);
|
||||||
@@ -484,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,13 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getRecordCheckApi, type NdmNvrResultVO, type Station } from '@/apis';
|
import { batchExportRecordCheckApi, pageDefParameterApi, type Station } from '@/apis';
|
||||||
import { exportRecordDiagCsv, isNvrCluster, transformRecordChecks } from '@/helpers';
|
import { downloadByData, parseErrorFeedback } from '@/utils';
|
||||||
import { useDeviceStore } from '@/stores';
|
|
||||||
import { 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 { storeToRefs } from 'pinia';
|
import { NButton, NFlex, NGrid, NGridItem, NModal, NScrollbar, NSpin } from 'naive-ui';
|
||||||
import { computed, ref, toRefs } from 'vue';
|
import { ref, toRefs } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
stations: Station[];
|
stations: Station[];
|
||||||
@@ -19,50 +17,66 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const show = defineModel<boolean>('show');
|
const show = defineModel<boolean>('show');
|
||||||
|
|
||||||
const deviceStore = useDeviceStore();
|
|
||||||
const { lineDevices } = storeToRefs(deviceStore);
|
|
||||||
|
|
||||||
const { stations } = toRefs(props);
|
const { stations } = toRefs(props);
|
||||||
|
|
||||||
const nvrClusterRecord = computed(() => {
|
|
||||||
const clusterMap: Record<Station['code'], { stationName: Station['name']; clusters: NdmNvrResultVO[] }> = {};
|
|
||||||
stations.value.forEach((station) => {
|
|
||||||
clusterMap[station.code] = {
|
|
||||||
stationName: station.name,
|
|
||||||
clusters: [],
|
|
||||||
};
|
|
||||||
const stationDevices = lineDevices.value[station.code];
|
|
||||||
const nvrs = stationDevices?.['ndmNvr'] ?? [];
|
|
||||||
nvrs.forEach((nvr) => {
|
|
||||||
if (isNvrCluster(nvr)) {
|
|
||||||
clusterMap[station.code]?.clusters?.push(nvr);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return clusterMap;
|
|
||||||
});
|
|
||||||
|
|
||||||
const abortController = ref<AbortController>(new AbortController());
|
const abortController = ref<AbortController>(new AbortController());
|
||||||
|
|
||||||
const { mutate: exportRecordDiags, isPending: exporting } = useMutation({
|
const { mutate: batchExportRecordCheck, isPending: batchExporting } = useMutation({
|
||||||
mutationFn: async (params: { clusters: NdmNvrResultVO[]; stationCode: Station['code'] }) => {
|
mutationFn: async (params: { stations: Station[] }) => {
|
||||||
const { clusters, stationCode } = params;
|
const timer = setTimeout(() => {
|
||||||
if (clusters.length === 0) {
|
if (!batchExporting.value) return;
|
||||||
const stationName = nvrClusterRecord.value[stationCode]?.stationName ?? '';
|
window.$message.info('导出耗时较长,请耐心等待...', { duration: 0 });
|
||||||
window.$message.info(`${stationName} 没有录像诊断数据`);
|
}, 3000);
|
||||||
return;
|
|
||||||
|
try {
|
||||||
|
abortController.value.abort();
|
||||||
|
abortController.value = new AbortController();
|
||||||
|
const { records = [] } = await pageDefParameterApi(
|
||||||
|
{
|
||||||
|
model: {
|
||||||
|
key: 'NVR_GAP_SECONDS',
|
||||||
|
},
|
||||||
|
extra: {},
|
||||||
|
current: 1,
|
||||||
|
size: 1,
|
||||||
|
sort: 'id',
|
||||||
|
order: 'descending',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal: abortController.value.signal,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const gapSeconds = parseInt(records.at(0)?.value ?? '5');
|
||||||
|
|
||||||
|
abortController.value.abort();
|
||||||
|
abortController.value = new AbortController();
|
||||||
|
const data = await batchExportRecordCheckApi(
|
||||||
|
{
|
||||||
|
checkDuration: 90,
|
||||||
|
gapSeconds,
|
||||||
|
stationCode: params.stations.map((station) => station.code),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal: abortController.value.signal,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} finally {
|
||||||
|
window.$message.destroyAll();
|
||||||
|
clearTimeout(timer);
|
||||||
}
|
}
|
||||||
const cluster = clusters.at(0);
|
|
||||||
if (!cluster) return;
|
|
||||||
abortController.value.abort();
|
|
||||||
abortController.value = new AbortController();
|
|
||||||
const checks = await getRecordCheckApi(cluster, 90, [], { stationCode: stationCode, signal: abortController.value.signal });
|
|
||||||
return checks;
|
|
||||||
},
|
},
|
||||||
onSuccess: (checks, { stationCode }) => {
|
onSuccess: (data, { stations }) => {
|
||||||
if (!checks || checks.length === 0) return;
|
const time = dayjs().format('YYYY-MM-DD_HH-mm-ss');
|
||||||
const recordDiags = transformRecordChecks(checks);
|
let stationName = '';
|
||||||
exportRecordDiagCsv(recordDiags, nvrClusterRecord.value[stationCode]?.stationName ?? '');
|
if (stations.length === 1) {
|
||||||
|
const name = stations.at(0)?.name;
|
||||||
|
if (!!name) {
|
||||||
|
stationName = `${name}_`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
downloadByData(data, `${stationName}录像缺失记录_${time}.xlsx`);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (isCancel(error)) return;
|
if (isCancel(error)) return;
|
||||||
@@ -73,6 +87,7 @@ const { mutate: exportRecordDiags, isPending: exporting } = useMutation({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onAfterLeave = () => {
|
const onAfterLeave = () => {
|
||||||
|
abortController.value.abort();
|
||||||
emit('afterLeave');
|
emit('afterLeave');
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -81,17 +96,22 @@ const onAfterLeave = () => {
|
|||||||
<NModal v-model:show="show" preset="card" title="导出录像诊断" @after-leave="onAfterLeave" style="width: 800px">
|
<NModal v-model:show="show" preset="card" title="导出录像诊断" @after-leave="onAfterLeave" style="width: 800px">
|
||||||
<template #default>
|
<template #default>
|
||||||
<NScrollbar style="height: 300px">
|
<NScrollbar style="height: 300px">
|
||||||
<NSpin size="small" :show="exporting">
|
<NSpin size="small" :show="batchExporting">
|
||||||
<NGrid :cols="6">
|
<NGrid :cols="6">
|
||||||
<template v-for="({ stationName, clusters }, code) in nvrClusterRecord" :key="code">
|
<template v-for="station in stations" :key="station.code">
|
||||||
<NGridItem>
|
<NGridItem>
|
||||||
<NButton text type="info" style="height: 30px" @click="() => exportRecordDiags({ clusters, stationCode: code })">{{ stationName }}</NButton>
|
<NButton text type="info" style="height: 30px" @click="() => batchExportRecordCheck({ stations: [station] })">{{ station.name }}</NButton>
|
||||||
</NGridItem>
|
</NGridItem>
|
||||||
</template>
|
</template>
|
||||||
</NGrid>
|
</NGrid>
|
||||||
</NSpin>
|
</NSpin>
|
||||||
</NScrollbar>
|
</NScrollbar>
|
||||||
</template>
|
</template>
|
||||||
|
<template #action>
|
||||||
|
<NFlex justify="flex-end" align="center">
|
||||||
|
<NButton secondary :loading="batchExporting" @click="() => batchExportRecordCheck({ stations })">导出全部</NButton>
|
||||||
|
</NFlex>
|
||||||
|
</template>
|
||||||
</NModal>
|
</NModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ const { mutate: syncCamera, isPending: cameraSyncing } = useMutation({
|
|||||||
window.$notification.info({
|
window.$notification.info({
|
||||||
title: '摄像机同步结果',
|
title: '摄像机同步结果',
|
||||||
content: notices.join(','),
|
content: notices.join(','),
|
||||||
duration: 3000,
|
duration: 10000,
|
||||||
});
|
});
|
||||||
if (successRequests.length > 0) {
|
if (successRequests.length > 0) {
|
||||||
// 摄像机同步后,需要重新查询一次设备
|
// 摄像机同步后,需要重新查询一次设备
|
||||||
@@ -123,7 +123,7 @@ const { mutate: syncNvrChannels, isPending: nvrChannelsSyncing } = useMutation({
|
|||||||
window.$notification.info({
|
window.$notification.info({
|
||||||
title: '录像机通道同步结果',
|
title: '录像机通道同步结果',
|
||||||
content: notices.join(','),
|
content: notices.join(','),
|
||||||
duration: 3000,
|
duration: 10000,
|
||||||
});
|
});
|
||||||
cancelAction();
|
cancelAction();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export const useSettingStore = defineStore(
|
|||||||
activeRequests.value = true;
|
activeRequests.value = true;
|
||||||
subscribeMessages.value = false;
|
subscribeMessages.value = false;
|
||||||
mockUser.value = false;
|
mockUser.value = false;
|
||||||
|
useLocalDB.value = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user