Compare commits

...

56 Commits

Author SHA1 Message Date
yangsy
670054ca01 feat: 新增流媒体推流统计卡片 2026-01-04 11:27:46 +08:00
yangsy
80e879e61f refactor: 优化服务状态卡片的渲染条件 2026-01-04 11:18:33 +08:00
yangsy
c259eabc22 fix: 添加防止设备自关联的校验 2026-01-04 10:30:30 +08:00
yangsy
286449b831 feat: 添加告警画面截图相关设置
- 配置告警画面截图保留天数
- 是否自动获取告警画面截图
2025-12-31 10:50:25 +08:00
yangsy
e112b01a21 feat: 告警忽略管理页面 2025-12-30 10:57:43 +08:00
yangsy
054b2bcdac feat: 添加告警忽略管理页面和路由配置 2025-12-30 10:45:48 +08:00
yangsy
1f23588649 refactor: 调整路由结构,使告警板块支持子路由 2025-12-30 10:45:40 +08:00
yangsy
02e29eb4f3 feat: 支持查看摄像机告警画面截图 2025-12-30 10:43:05 +08:00
yangsy
118cc8be0b refactor: 明确所有 stationCode 参数的类型 2025-12-29 14:03:13 +08:00
yangsy
5170105ab8 refactor: 明确所有 stationCode 参数的类型 2025-12-29 14:03:07 +08:00
yangsy
420637352d feat: 查询页面卸载时取消未完成的请求 2025-12-28 14:34:42 +08:00
yangsy
abdbef2d05 fix: 修复跳转设备时未检查deviceId存在性的问题 2025-12-28 13:24:58 +08:00
yangsy
cdd1f2113f feat: 扩展交换机端口诊断信息 2025-12-26 18:14:55 +08:00
yangsy
73c2354a06 feat: 当下游设备不存在时自动解除关联
- 获取下游设备时,如果发现下游设备不存在,将静默解除关联
2025-12-26 18:14:54 +08:00
yangsy
01b059fb7f feat: 告警记录支持点击设备跳转到设备详情 2025-12-26 18:14:54 +08:00
yangsy
fd7f1927ff feat: 设备关联与解除关联
- 支持配置交换机端口的下游关联设备
- 支持配置安防箱电路的下游关联设备
- 支持解除关联
- 删除设备时校验是否存在上/下游设备
2025-12-26 18:14:54 +08:00
yangsy
ed2a4f78ff feat: 扩展设备树功能
- 支持控制是否同步路由参数
- 支持配置允许的事件类型 (select/manage)
- 支持自定义设备节点前缀按钮文字
- 支持向外暴露设备选择逻辑
- 不再封装跳转设备逻辑,由外部实现
- 在车站模式下也支持选择设备
2025-12-25 16:18:41 +08:00
yangsy
f99fe0f68e fix: 移除所有设备更新表单中的上游设备字段 2025-12-25 16:18:41 +08:00
yangsy
76aa654cde fix: 修复设备管理逻辑中错误处理的loading状态和取消逻辑的顺序 2025-12-25 15:53:48 +08:00
yangsy
58b3d09d5d fix: 定位设备树时保留原有的展开状态 2025-12-25 15:53:48 +08:00
yangsy
26d894ba1c feat: 添加更新设备的组合API 2025-12-25 15:53:48 +08:00
yangsy
05b94b2707 fix: 修复设备详情组合API返回undefined的问题 2025-12-25 15:53:47 +08:00
yangsy
c0d49e345b feat: 定义更多的联合设备类型 2025-12-25 13:37:35 +08:00
yangsy
426ca4947a feat: 添加依赖注入工具函数 2025-12-25 13:37:35 +08:00
yangsy
bb140433d8 fix: 修复服务器状态查询在开启离线模式时没有主动取消的问题 2025-12-25 13:37:35 +08:00
yangsy
df304b06d3 refactor: 使用vue-query替换computedAsync解析摄像机建议安装区域 2025-12-25 13:37:35 +08:00
yangsy
9748b94046 refactor: 优化全局加载状态计算 2025-12-25 13:37:34 +08:00
yangsy
4eb2a2ab3d refactor: 简化设备树的自动定位逻辑 2025-12-24 22:34:55 +08:00
yangsy
7f07725935 feat: 摄像机卡片添加摄像机类型和建议安装区域 2025-12-24 22:34:55 +08:00
yangsy
e8ac939795 feat: 设备header卡片添加 append-info 插槽以支持自定义底部内容 2025-12-24 22:34:55 +08:00
yangsy
a7c36079cf fix: 修复指向minio的请求未添加时间戳参数的问题 2025-12-24 22:34:55 +08:00
yangsy
f58ea93985 feat: 调用新的设备告警日志导出接口 2025-12-24 22:34:54 +08:00
yangsy
1e3810ba2d feat: 调整服务器运行时间 label 文案 2025-12-24 22:34:54 +08:00
yangsy
865147fdf8 feat: DeviceHardwareCard组件添加自定义标签属性 2025-12-24 22:34:54 +08:00
yangsy
f35810f907 fix: 视频平台日志页面补全遗漏的操作类型字段 2025-12-24 22:34:54 +08:00
yangsy
92f0e832ca style 2025-12-24 22:34:54 +08:00
yangsy
42b2eacef7 chore: vite代理配置 2025-12-24 22:34:54 +08:00
yangsy
a598b9c1d3 feat: 细化设备树自动定位的触发条件
- 添加 `hasFromPage` 属性,辅助区分选择设备的来源是用户操作还是路由参数
2025-12-24 22:34:54 +08:00
yangsy
2d977c5321 refactor: 路由参数 from 改为fromPage 2025-12-24 22:34:54 +08:00
yangsy
189f7a5d63 feat: 渲染全线设备树时自动定位到所选设备 2025-12-24 22:34:54 +08:00
yangsy
44f5f4d50b fix: 修复由动画属性导致设备树在特定场景下无法自行滚动及展开节点失效的问题
- 当选中的设备所属车站在设备树的视口外时,定位操作无效,且节点无法点击展开,解决方案是在定位逻辑中切换Tree组件的animated状态
2025-12-24 22:34:54 +08:00
yangsy
075f008b65 fix: 设备树仅在非车站模式下显示收起和定位按钮 2025-12-24 22:34:53 +08:00
yangsy
c8274a8c6b refactor: 抽离登录校验查询 2025-12-24 22:34:53 +08:00
yangsy
a90e788dc5 fix: 修复设备更新面板中错误的表单校验逻辑 2025-12-24 22:34:53 +08:00
yangsy
e7fdf911e7 refactor: 简化设备树节点双击和点击事件的逻辑并添加注释 2025-12-24 22:34:53 +08:00
yangsy
c408d875f4 feat: 设备树添加管理功能
- 新增设备导入、导出、删除功能及相关API
- 封装设备管理逻辑,拆分设备选择与设备管理逻辑
- 添加右键菜单支持设备管理操作
2025-12-24 22:34:53 +08:00
yangsy
6183bfd4d7 refactor: 优化登录逻辑并添加注释 2025-12-24 22:34:53 +08:00
yangsy
9af76bacbb refactor: 优化请求封装
- 优化Result接口定义
- 新增响应数据解析逻辑
- 优化错误解析逻辑
2025-12-24 22:34:53 +08:00
yangsy
52ba3add3f feat: 新增设备导入导出API 2025-12-15 21:23:03 +08:00
yangsy
426d92a5f9 fix: 在导入和删除IndexedDB数据时停止轮询并启用离线开发模式以保证数据一致性 2025-12-15 12:49:21 +08:00
yangsy
91a2fcb743 fix: 兼容后端返回不完整的设备类型 2025-12-15 12:46:02 +08:00
yangsy
9ca5630c87 refactor: 移除未使用的测试组件 2025-12-12 20:15:16 +08:00
yangsy
d73861442c refactor: 移除设置面板中未使用的导入 2025-12-12 14:55:57 +08:00
yangsy
3bfef2b14a fix: 修复报警主机API未导出的问题 2025-12-12 13:38:18 +08:00
yangsy
5bfda437a6 feat: 新增流媒体/信令服务状态卡片 2025-12-12 10:44:00 +08:00
yangsy
77406f1932 style: 车站卡片文字排版 2025-12-11 15:55:27 +08:00
120 changed files with 3677 additions and 1223 deletions

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { useVersionCheckQuery } from './composables';
import { GlobalFeedback } from '@/components';
import { useVersionCheckQuery } from '@/composables';
import { useSettingStore } from '@/stores';
import { VueQueryDevtools } from '@tanstack/vue-query-devtools';
import { dateZhCN, NConfigProvider, NDialogProvider, NLoadingBarProvider, NMessageProvider, NNotificationProvider, zhCN } from 'naive-ui';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
export * from './client-channel';
export * from './media-server-status';
export * from './record-info';
export * from './record-item';

View File

@@ -0,0 +1,5 @@
export interface MediaServerStatus {
id: string;
ip: string;
online: boolean;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
export interface ImportMsg {
wrongLines: WrongLine[];
wrongNum: number;
updateNum: number;
insertNum: number;
unchangedNum: number;
total: number;
}
export interface WrongLine {
rowNum: number;
msg: string;
}

View File

@@ -0,0 +1 @@
export * from './import-msg';

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import {
ndmClient,
userClient,
type ImportMsg,
type NdmAlarmHostPageQuery,
type NdmAlarmHostResultVO,
type NdmAlarmHostSaveVO,
@@ -9,6 +10,7 @@ import {
type PageResult,
type Station,
} from '@/apis';
import { unwrapResponse } from '@/utils';
export const pageAlarmHostApi = async (pageQuery: PageParams<NdmAlarmHostPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -16,9 +18,7 @@ export const pageAlarmHostApi = async (pageQuery: PageParams<NdmAlarmHostPageQue
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmAlarmHost/page`;
const resp = await client.post<PageResult<NdmAlarmHostResultVO>>(endpoint, pageQuery, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -28,9 +28,7 @@ export const detailAlarmHostApi = async (id: string, options?: { stationCode?: S
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmAlarmHost/detail`;
const resp = await client.get<NdmAlarmHostResultVO>(endpoint, { params: { id }, signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -40,9 +38,7 @@ export const saveAlarmHostApi = async (saveVO: NdmAlarmHostSaveVO, options?: { s
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmAlarmHost`;
const resp = await client.post<NdmAlarmHostResultVO>(endpoint, saveVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -52,9 +48,7 @@ export const updateAlarmHostApi = async (updateVO: NdmAlarmHostUpdateVO, options
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmAlarmHost`;
const resp = await client.put<NdmAlarmHostResultVO>(endpoint, updateVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -64,8 +58,28 @@ export const deleteAlarmHostApi = async (ids: string[], options?: { stationCode?
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmAlarmHost`;
const resp = await client.delete<boolean>(endpoint, ids, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
export const exportAlarmHostApi = async (pageQuery: PageParams<NdmAlarmHostPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmAlarmHost/defaultExportByTemplate`;
const resp = await client.post<Blob>(endpoint, pageQuery, { responseType: 'blob', retRaw: true, signal });
const data = unwrapResponse(resp);
return data;
};
export const importAlarmHostApi = async (file: File, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmAlarmHost/importReturnMsg`;
const formData = new FormData();
formData.append('file', file);
const resp = await client.post<ImportMsg>(endpoint, formData, { signal, upload: true });
const data = unwrapResponse(resp);
return data;
};

View File

@@ -1,13 +1,16 @@
import { ndmClient, userClient, type StationDevices } from '@/apis';
import { initStationDevices, ndmClient, userClient, type Station, type StationDevices } from '@/apis';
import { unwrapResponse } from '@/utils';
export const getAllDevicesApi = async (options?: { stationCode?: string; signal?: AbortSignal }) => {
export const getAllDevicesApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmDevices/all`;
const resp = await client.get<StationDevices>(endpoint, { retRaw: true, signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
return data;
const data = unwrapResponse(resp);
// 由于各线路后端版本不一致,接口返回的设备类型可能不够完整,需要做一次合并
return {
...initStationDevices(),
...data,
};
};

View File

@@ -0,0 +1,32 @@
import {
deleteAlarmHostApi,
deleteCameraApi,
deleteDecoderApi,
deleteKeyboardApi,
deleteMediaServerApi,
deleteNvrApi,
deleteSecurityBoxApi,
deleteSwitchApi,
deleteVideoServerApi,
type Station,
} from '@/apis';
import { DEVICE_TYPE_LITERALS, type DeviceType } from '@/enums';
export const deleteDeviceApi = async (deviceType: DeviceType, id: string, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const apiRecord = {
[DEVICE_TYPE_LITERALS.ndmAlarmHost]: deleteAlarmHostApi,
[DEVICE_TYPE_LITERALS.ndmCamera]: deleteCameraApi,
[DEVICE_TYPE_LITERALS.ndmDecoder]: deleteDecoderApi,
[DEVICE_TYPE_LITERALS.ndmKeyboard]: deleteKeyboardApi,
[DEVICE_TYPE_LITERALS.ndmMediaServer]: deleteMediaServerApi,
[DEVICE_TYPE_LITERALS.ndmNvr]: deleteNvrApi,
[DEVICE_TYPE_LITERALS.ndmSecurityBox]: deleteSecurityBoxApi,
[DEVICE_TYPE_LITERALS.ndmSwitch]: deleteSwitchApi,
[DEVICE_TYPE_LITERALS.ndmVideoServer]: deleteVideoServerApi,
};
const deleteApi = apiRecord[deviceType];
if (!deleteApi) throw new Error('接口不存在');
return deleteApi([id], options);
};

View File

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

View File

@@ -0,0 +1,34 @@
import {
exportAlarmHostApi,
exportCameraApi,
exportDecoderApi,
exportKeyboardApi,
exportMediaServerApi,
exportNvrApi,
exportSecurityBoxApi,
exportSwitchApi,
exportVideoServerApi,
type NdmDevicePageQuery,
type PageParams,
type Station,
} from '@/apis';
import { DEVICE_TYPE_LITERALS, type DeviceType } from '@/enums';
export const exportDeviceApi = async (deviceType: DeviceType, pageQuery: PageParams<NdmDevicePageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const apiRecord = {
[DEVICE_TYPE_LITERALS.ndmAlarmHost]: exportAlarmHostApi,
[DEVICE_TYPE_LITERALS.ndmCamera]: exportCameraApi,
[DEVICE_TYPE_LITERALS.ndmDecoder]: exportDecoderApi,
[DEVICE_TYPE_LITERALS.ndmKeyboard]: exportKeyboardApi,
[DEVICE_TYPE_LITERALS.ndmMediaServer]: exportMediaServerApi,
[DEVICE_TYPE_LITERALS.ndmNvr]: exportNvrApi,
[DEVICE_TYPE_LITERALS.ndmSecurityBox]: exportSecurityBoxApi,
[DEVICE_TYPE_LITERALS.ndmSwitch]: exportSwitchApi,
[DEVICE_TYPE_LITERALS.ndmVideoServer]: exportVideoServerApi,
};
const exportApi = apiRecord[deviceType];
if (!exportApi) throw new Error('接口不存在');
return exportApi(pageQuery, options);
};

View File

@@ -0,0 +1,32 @@
import {
importAlarmHostApi,
importCameraApi,
importDecoderApi,
importKeyboardApi,
importMediaServerApi,
importNvrApi,
importSecurityBoxApi,
importSwitchApi,
importVideoServerApi,
type Station,
} from '@/apis';
import { DEVICE_TYPE_LITERALS, type DeviceType } from '@/enums';
export const importDeviceApi = async (deviceType: DeviceType, file: File, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const apiRecord = {
[DEVICE_TYPE_LITERALS.ndmAlarmHost]: importAlarmHostApi,
[DEVICE_TYPE_LITERALS.ndmCamera]: importCameraApi,
[DEVICE_TYPE_LITERALS.ndmDecoder]: importDecoderApi,
[DEVICE_TYPE_LITERALS.ndmKeyboard]: importKeyboardApi,
[DEVICE_TYPE_LITERALS.ndmMediaServer]: importMediaServerApi,
[DEVICE_TYPE_LITERALS.ndmNvr]: importNvrApi,
[DEVICE_TYPE_LITERALS.ndmSecurityBox]: importSecurityBoxApi,
[DEVICE_TYPE_LITERALS.ndmSwitch]: importSwitchApi,
[DEVICE_TYPE_LITERALS.ndmVideoServer]: importVideoServerApi,
};
const importApi = apiRecord[deviceType];
if (!importApi) throw new Error('接口不存在');
return importApi(file, options);
};

View File

@@ -1,2 +1,6 @@
export * from './delete-device';
export * from './detail-device';
export * from './export-device';
export * from './import-device';
export * from './probe-device';
export * from './update-device';

View File

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

View File

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

View File

@@ -1,11 +1,10 @@
import { userClient, type VerifyServer } from '@/apis';
import { unwrapResponse } from '@/utils';
export const batchVerifyApi = async (options?: { signal?: AbortSignal }) => {
const { signal } = options ?? {};
const endpoint = `/api/ndm/ndmKeepAlive/batchVerify`;
const resp = await userClient.post<VerifyServer[]>(endpoint, {}, { retRaw: true, timeout: 5000, signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};

View File

@@ -1,4 +1,5 @@
import { ndmClient, userClient, type IcmpEntity, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
export const exportIcmpApi = async (status?: string, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -13,9 +14,7 @@ export const exportIcmpApi = async (status?: string, options?: { stationCode?: S
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
signal,
});
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -36,9 +35,7 @@ export const exportIcmpByStationApi = async (stationCodes: Station['code'][], st
signal,
},
);
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -48,8 +45,6 @@ export const icmpEntityByDeviceId = async (deviceId: string, options?: { station
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmIcmpExport/icmpEntityByDeviceId`;
const resp = await client.get<IcmpEntity[]>(endpoint, { params: { deviceId }, signal, retRaw: true });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};

View File

@@ -1,11 +1,11 @@
import { ndmClient, userClient, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
export const verifyApi = 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/ndmKeepAlive/verify`;
const resp = await client.post<void>(endpoint, {}, { timeout: 5000, signal });
const [err] = resp;
if (err) throw err;
const resp = await client.post<void>(endpoint, {}, { retRaw: true, timeout: 5000, signal });
unwrapResponse(resp);
};

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import { ndmClient, userClient, type NdmCallLogPageQuery, type NdmCallLogResultVO, type PageParams, type PageResult, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
export const pageCallLogApi = async (pageQuery: PageParams<NdmCallLogPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -6,9 +7,7 @@ export const pageCallLogApi = async (pageQuery: PageParams<NdmCallLogPageQuery>,
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmCallLog/page`;
const resp = await client.post<PageResult<NdmCallLogResultVO>>(endpoint, pageQuery, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -18,8 +17,6 @@ export const exportCallLogApi = async (pageQuery: PageParams<NdmCallLogPageQuery
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmCallLog/defaultExportByTemplate`;
const resp = await client.post<Blob>(endpoint, pageQuery, { responseType: 'blob', retRaw: true, signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};

View File

@@ -1,4 +1,5 @@
import { ndmClient, userClient, type NdmDeviceAlarmLogPageQuery, type NdmDeviceAlarmLogResultVO, type NdmDeviceAlarmLogUpdateVO, type PageParams, type PageResult, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
export const pageDeviceAlarmLogApi = async (pageQuery: PageParams<NdmDeviceAlarmLogPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -6,9 +7,7 @@ export const pageDeviceAlarmLogApi = async (pageQuery: PageParams<NdmDeviceAlarm
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmDeviceAlarmLog/page`;
const resp = await client.post<PageResult<NdmDeviceAlarmLogResultVO>>(endpoint, pageQuery, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -18,9 +17,7 @@ export const updateDeviceAlarmLogApi = async (updateVO: NdmDeviceAlarmLogUpdateV
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmDeviceAlarmLog`;
const resp = await client.put<NdmDeviceAlarmLogResultVO>(endpoint, updateVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -28,10 +25,8 @@ export const exportDeviceAlarmLogApi = async (pageQuery: PageParams<NdmDeviceAla
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmDeviceAlarmLog/defaultExportByTemplate`;
const endpoint = `${prefix}/api/ndm/ndmDeviceAlarmLog/exportByTemplateV2`;
const resp = await client.post<BlobPart>(endpoint, pageQuery, { responseType: 'blob', retRaw: true, signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};

View File

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

View File

@@ -1,4 +1,5 @@
import { ndmClient, userClient, type NdmIcmpLogPageQuery, type NdmIcmpLogResultVO, type PageParams, type PageResult, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
export const pageIcmpLogApi = async (pageQuery: PageParams<NdmIcmpLogPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -6,8 +7,6 @@ export const pageIcmpLogApi = async (pageQuery: PageParams<NdmIcmpLogPageQuery>,
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmIcmpLog/page`;
const resp = await client.post<PageResult<NdmIcmpLogResultVO>>(endpoint, pageQuery, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};

View File

@@ -1,19 +1,18 @@
import { ndmClient, userClient, type ClientChannel, type NdmNvrResultVO, type NdmRecordCheck } from '@/apis';
import { ndmClient, userClient, type ClientChannel, type NdmNvrResultVO, type NdmRecordCheck, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
import dayjs from 'dayjs';
export const getChannelListApi = async (ndmNvr: NdmNvrResultVO, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const getChannelListApi = async (ndmNvr: NdmNvrResultVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmRecordCheck/getChannelList`;
const resp = await client.post<ClientChannel[]>(endpoint, { code: ndmNvr.gbCode, time: '' }, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
export const getRecordCheckApi = async (ndmNvr: NdmNvrResultVO, lastDays: number, gbCodeList: string[], options?: { stationCode?: string; signal?: AbortSignal }) => {
export const getRecordCheckApi = async (ndmNvr: NdmNvrResultVO, lastDays: number, gbCodeList: string[], options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
@@ -24,25 +23,21 @@ export const getRecordCheckApi = async (ndmNvr: NdmNvrResultVO, lastDays: number
const end = endDateTime.format('YYYY-MM-DD');
const parentId = ndmNvr.gbCode;
const resp = await client.post<NdmRecordCheck[]>(endpoint, { start, end, parentId, gbCodeList }, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
export const reloadRecordCheckApi = async (channel: ClientChannel, dayOffset: number, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const reloadRecordCheckApi = async (channel: ClientChannel, dayOffset: number, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmRecordCheck/reloadRecordCheckByGbId`;
const resp = await client.post<boolean>(endpoint, { ...channel, dayOffset }, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
export const reloadAllRecordCheckApi = async (dayOffset: number, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const reloadAllRecordCheckApi = async (dayOffset: number, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';

View File

@@ -1,4 +1,5 @@
import { ndmClient, userClient, type NdmSnmpLogPageQuery, type NdmSnmpLogResultVO, type PageParams, type PageResult, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
export const pageSnmpLogApi = async (pageQuery: PageParams<NdmSnmpLogPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -6,8 +7,6 @@ export const pageSnmpLogApi = async (pageQuery: PageParams<NdmSnmpLogPageQuery>,
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSnmpLog/page`;
const resp = await client.post<PageResult<NdmSnmpLogResultVO>>(endpoint, pageQuery, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};

View File

@@ -1,4 +1,5 @@
import { ndmClient, userClient, type NdmVimpLogPageQuery, type NdmVimpLogResultVO, type PageParams, type PageResult, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
export const pageVimpLogApi = async (pageQuery: PageParams<NdmVimpLogPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -6,9 +7,7 @@ export const pageVimpLogApi = async (pageQuery: PageParams<NdmVimpLogPageQuery>,
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmVimpLog/page`;
const resp = await client.post<PageResult<NdmVimpLogResultVO>>(endpoint, pageQuery, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -18,8 +17,6 @@ export const exportVimpLogApi = async (pageQuery: PageParams<NdmVimpLogPageQuery
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmVimpLog/defaultExportByTemplate`;
const resp = await client.post<Blob>(endpoint, pageQuery, { responseType: 'blob', retRaw: true, signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};

View File

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

View File

@@ -1,6 +1,7 @@
import {
ndmClient,
userClient,
type ImportMsg,
type NdmSecurityBoxPageQuery,
type NdmSecurityBoxResultVO,
type NdmSecurityBoxSaveVO,
@@ -9,6 +10,7 @@ import {
type PageResult,
type Station,
} from '@/apis';
import { unwrapResponse } from '@/utils';
export const pageSecurityBoxApi = async (pageQuery: PageParams<NdmSecurityBoxPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -16,9 +18,7 @@ export const pageSecurityBoxApi = async (pageQuery: PageParams<NdmSecurityBoxPag
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSecurityBox/page`;
const resp = await client.post<PageResult<NdmSecurityBoxResultVO>>(endpoint, pageQuery, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -28,9 +28,7 @@ export const detailSecurityBoxApi = async (id: string, options?: { stationCode?:
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSecurityBox/detail`;
const resp = await client.get<NdmSecurityBoxResultVO>(endpoint, { params: { id }, signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -40,9 +38,7 @@ export const saveSecurityBoxApi = async (saveVO: NdmSecurityBoxSaveVO, options?:
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSecurityBox`;
const resp = await client.post<NdmSecurityBoxResultVO>(endpoint, saveVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -52,9 +48,7 @@ export const updateSecurityBoxApi = async (updateVO: NdmSecurityBoxUpdateVO, opt
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSecurityBox`;
const resp = await client.put<NdmSecurityBoxResultVO>(endpoint, updateVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -64,9 +58,29 @@ export const deleteSecurityBoxApi = async (ids: string[], options?: { stationCod
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSecurityBox`;
const resp = await client.delete<boolean>(endpoint, ids, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
export const exportSecurityBoxApi = async (pageQuery: PageParams<NdmSecurityBoxPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSecurityBox/defaultExportByTemplate`;
const resp = await client.post<Blob>(endpoint, pageQuery, { responseType: 'blob', retRaw: true, signal });
const data = unwrapResponse(resp);
return data;
};
export const importSecurityBoxApi = async (file: File, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSecurityBox/importReturnMsg`;
const formData = new FormData();
formData.append('file', file);
const resp = await client.post<ImportMsg>(endpoint, formData, { signal, upload: true });
const data = unwrapResponse(resp);
return data;
};
@@ -76,8 +90,7 @@ export const probeSecurityBoxApi = async (ids: string[], options?: { stationCode
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSecurityBox/probeByIds`;
const resp = await client.post<void>(endpoint, ids, { signal });
const [err] = resp;
if (err) throw err;
unwrapResponse(resp);
};
export const turnCitcuitStatusApi = async (ipAddress: string, circuitIndex: number, status: number, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
@@ -86,9 +99,7 @@ export const turnCitcuitStatusApi = async (ipAddress: string, circuitIndex: numb
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSecurityBox/turnStatus`;
const resp = await client.post<boolean>(endpoint, { community: 'public', ipAddress, circuit: `${circuitIndex}`, status }, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -98,8 +109,6 @@ export const rebootSecurityBoxApi = async (ipAddress: string, options?: { statio
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSecurityBox/reboot`;
const resp = await client.post<boolean>(endpoint, { community: 'public', ipAddress }, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};

View File

@@ -0,0 +1,32 @@
import { ndmClient, userClient, type MediaServerStatus, type SendRtpInfo, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
export const getAllPushApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmServiceAvailable/mediaServer/getAllPush`;
const resp = await client.get<SendRtpInfo[]>(endpoint, { signal });
const data = unwrapResponse(resp);
return data;
};
export const isMediaServerAliveApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmServiceAvailable/mediaServer/isAlive`;
const resp = await client.get<MediaServerStatus[]>(endpoint, { signal });
const data = unwrapResponse(resp);
return data;
};
export const isSipServerAliveApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmServiceAvailable/sipServer/isAlive`;
const resp = await client.get<boolean>(endpoint, { signal });
const data = unwrapResponse(resp);
return data;
};

View File

@@ -1,4 +1,16 @@
import { ndmClient, type NdmSwitchPageQuery, type NdmSwitchResultVO, type NdmSwitchSaveVO, type NdmSwitchUpdateVO, type PageParams, type PageResult, type Station } from '@/apis';
import {
ndmClient,
userClient,
type ImportMsg,
type NdmSwitchPageQuery,
type NdmSwitchResultVO,
type NdmSwitchSaveVO,
type NdmSwitchUpdateVO,
type PageParams,
type PageResult,
type Station,
} from '@/apis';
import { unwrapResponse } from '@/utils';
export const pageSwitchApi = async (pageQuery: PageParams<NdmSwitchPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -6,9 +18,7 @@ export const pageSwitchApi = async (pageQuery: PageParams<NdmSwitchPageQuery>, o
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSwitch/page`;
const resp = await client.post<PageResult<NdmSwitchResultVO>>(endpoint, pageQuery, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -18,9 +28,7 @@ export const detailSwitchApi = async (id: string, options?: { stationCode?: Stat
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSwitch/detail`;
const resp = await client.get<NdmSwitchResultVO>(endpoint, { params: { id }, signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -30,9 +38,7 @@ export const saveSwitchApi = async (saveVO: NdmSwitchSaveVO, options?: { station
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSwitch`;
const resp = await client.post<NdmSwitchResultVO>(endpoint, saveVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -42,9 +48,7 @@ export const updateSwitchApi = async (updateVO: NdmSwitchUpdateVO, options?: { s
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSwitch`;
const resp = await client.put<NdmSwitchResultVO>(endpoint, updateVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -54,9 +58,29 @@ export const deleteSwitchApi = async (ids: string[], options?: { stationCode?: S
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSwitch`;
const resp = await client.delete<boolean>(endpoint, ids, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
export const exportSwitchApi = async (pageQuery: PageParams<NdmSwitchPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : ndmClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSwitch/defaultExportByTemplate`;
const resp = await client.post<Blob>(endpoint, pageQuery, { responseType: 'blob', retRaw: true, signal });
const data = unwrapResponse(resp);
return data;
};
export const importSwitchApi = async (file: File, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSwitch/importReturnMsg`;
const formData = new FormData();
formData.append('file', file);
const resp = await client.post<ImportMsg>(endpoint, formData, { signal, upload: true });
const data = unwrapResponse(resp);
return data;
};
@@ -66,6 +90,5 @@ export const probeSwitchApi = async (ids: string[], options?: { stationCode?: St
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmSwitch/probeByIds`;
const resp = await client.post<void>(endpoint, ids, { signal });
const [err] = resp;
if (err) throw err;
unwrapResponse(resp);
};

View File

@@ -1,4 +1,5 @@
import { ndmClient, userClient, type NdmNvrPageQuery, type NdmNvrResultVO, type NdmNvrSaveVO, type NdmNvrUpdateVO, type PageParams, type PageResult, type Station } from '@/apis';
import { ndmClient, userClient, type ImportMsg, type NdmNvrPageQuery, type NdmNvrResultVO, type NdmNvrSaveVO, type NdmNvrUpdateVO, type PageParams, type PageResult, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
export const pageNvrPageApi = async (pageQuery: PageParams<NdmNvrPageQuery>, options?: { stationCode: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -6,9 +7,7 @@ export const pageNvrPageApi = async (pageQuery: PageParams<NdmNvrPageQuery>, opt
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmNvr/page`;
const resp = await client.post<PageResult<NdmNvrResultVO>>(endpoint, pageQuery, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -18,9 +17,7 @@ export const detailNvrApi = async (id: string, options?: { stationCode?: Station
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmNvr/detail`;
const resp = await client.get<NdmNvrResultVO>(endpoint, { params: { id }, signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -30,9 +27,7 @@ export const saveNvrApi = async (saveVO: NdmNvrSaveVO, options?: { stationCode?:
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmNvr`;
const resp = await client.post<NdmNvrResultVO>(endpoint, saveVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -42,9 +37,7 @@ export const updateNvrApi = async (updateVO: NdmNvrUpdateVO, options?: { station
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmNvr`;
const resp = await client.put<NdmNvrResultVO>(endpoint, updateVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -54,9 +47,29 @@ export const deleteNvrApi = async (ids: string[], options?: { stationCode?: Stat
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmNvr`;
const resp = await client.delete<boolean>(endpoint, ids, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
export const exportNvrApi = async (pageQuery: PageParams<NdmNvrPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmNvr/defaultExportByTemplate`;
const resp = await client.post<Blob>(endpoint, pageQuery, { responseType: 'blob', retRaw: true, signal });
const data = unwrapResponse(resp);
return data;
};
export const importNvrApi = async (file: File, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmNvr/importReturnMsg`;
const formData = new FormData();
formData.append('file', file);
const resp = await client.post<ImportMsg>(endpoint, formData, { signal, upload: true });
const data = unwrapResponse(resp);
return data;
};
@@ -66,8 +79,7 @@ export const probeNvrApi = async (ids: string[], options?: { stationCode?: Stati
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmNvr/probeByIds`;
const resp = await client.post<void>(endpoint, ids, { signal });
const [err] = resp;
if (err) throw err;
unwrapResponse(resp);
};
export const syncNvrChannelsApi = async (options?: { stationCode?: string; signal?: AbortSignal }) => {
@@ -76,6 +88,5 @@ export const syncNvrChannelsApi = async (options?: { stationCode?: string; signa
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmNvr/syncNvrChannels`;
const resp = await client.get<void>(endpoint, { signal });
const [err] = resp;
if (err) throw err;
unwrapResponse(resp);
};

View File

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

View File

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

View File

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

View File

@@ -1,61 +1,62 @@
import { ndmClient, userClient, type NdmCameraIgnorePageQuery, type NdmCameraIgnoreResultVO, type NdmCameraIgnoreSaveVO, type NdmCameraIgnoreUpdateVO, type PageParams, type PageResult } from '@/apis';
import {
ndmClient,
userClient,
type NdmCameraIgnorePageQuery,
type NdmCameraIgnoreResultVO,
type NdmCameraIgnoreSaveVO,
type NdmCameraIgnoreUpdateVO,
type PageParams,
type PageResult,
type Station,
} from '@/apis';
import { unwrapResponse } from '@/utils';
export const pageCameraIgnoreApi = async (pageQuery: PageParams<NdmCameraIgnorePageQuery>, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const pageCameraIgnoreApi = async (pageQuery: PageParams<NdmCameraIgnorePageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmCameraIgnore/page`;
const resp = await client.post<PageResult<NdmCameraIgnoreResultVO>>(endpoint, pageQuery, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
export const detailCameraIgnoreApi = async (id: string, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const detailCameraIgnoreApi = async (id: string, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmCameraIgnore/detail`;
const resp = await client.get<NdmCameraIgnoreResultVO>(endpoint, { params: { id }, signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
export const saveCameraIgnoreApi = async (saveVO: NdmCameraIgnoreSaveVO, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const saveCameraIgnoreApi = async (saveVO: NdmCameraIgnoreSaveVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmCameraIgnore`;
const resp = await client.post<NdmCameraIgnoreResultVO>(endpoint, saveVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
export const updateCameraIgnoreApi = async (updateVO: NdmCameraIgnoreUpdateVO, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const updateCameraIgnoreApi = async (updateVO: NdmCameraIgnoreUpdateVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmCameraIgnore`;
const resp = await client.put<NdmCameraIgnoreResultVO>(endpoint, updateVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
export const deleteCameraIgnoreApi = async (ids: string[], options?: { stationCode?: string; signal?: AbortSignal }) => {
export const deleteCameraIgnoreApi = async (ids: string[], options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmCameraIgnore`;
const resp = await client.delete<boolean>(endpoint, ids, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};

View File

@@ -1,6 +1,7 @@
import {
ndmClient,
userClient,
type ImportMsg,
type NdmCameraPageQuery,
type NdmCameraResultVO,
type NdmCameraSaveVO,
@@ -10,6 +11,7 @@ import {
type SnapResult,
type Station,
} from '@/apis';
import { unwrapResponse } from '@/utils';
export const pageCameraApi = async (pageQuery: PageParams<NdmCameraPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -17,9 +19,7 @@ export const pageCameraApi = async (pageQuery: PageParams<NdmCameraPageQuery>, o
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmCamera/page`;
const resp = await client.post<PageResult<NdmCameraResultVO>>(endpoint, pageQuery, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -29,9 +29,7 @@ export const detailCameraApi = async (id: string, options?: { stationCode?: Stat
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmCamera/detail`;
const resp = await client.get<NdmCameraResultVO>(endpoint, { params: { id }, signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -41,9 +39,7 @@ export const saveCameraApi = async (saveVO: NdmCameraSaveVO, options?: { station
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmCamera`;
const resp = await client.post<NdmCameraResultVO>(endpoint, saveVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -53,9 +49,7 @@ export const updateCameraApi = async (updateVO: NdmCameraUpdateVO, options?: { s
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmCamera`;
const resp = await client.put<NdmCameraResultVO>(endpoint, updateVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -65,30 +59,46 @@ export const deleteCameraApi = async (ids: string[], options?: { stationCode?: S
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmCamera`;
const resp = await client.delete<boolean>(endpoint, ids, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
export const exportCameraApi = async (pageQuery: PageParams<NdmCameraPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmCamera/defaultExportByTemplate`;
const resp = await client.post<Blob>(endpoint, pageQuery, { responseType: 'blob', retRaw: true, signal });
const data = unwrapResponse(resp);
return data;
};
export const importCameraApi = async (file: File, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmCamera/importReturnMsg`;
const formData = new FormData();
formData.append('file', file);
const resp = await client.post<ImportMsg>(endpoint, formData, { signal, upload: true });
const data = unwrapResponse(resp);
return data;
};
export const getCameraSnapApi = async (deviceId: string, options?: { signal?: AbortSignal }) => {
const { signal } = options ?? {};
const endpoint = `/api/ndm/ndmCamera/getSnapByDeviceId`;
const resp = await ndmClient.get<SnapResult>(endpoint, { params: { deviceId }, signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const resp = await ndmClient.get<SnapResult>(endpoint, { params: { deviceId }, retRaw: true, signal });
const data = unwrapResponse(resp);
return data;
};
export const syncCameraApi = async (options?: { stationCode?: string; signal?: AbortSignal }) => {
export const syncCameraApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmCamera/syncCamera`;
const resp = await client.get<boolean>(endpoint, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};

View File

@@ -1,4 +1,16 @@
import { ndmClient, userClient, type NdmDecoderPageQuery, type NdmDecoderResultVO, type NdmDecoderSaveVO, type NdmDecoderUpdateVO, type PageParams, type PageResult, type Station } from '@/apis';
import {
ndmClient,
userClient,
type ImportMsg,
type NdmDecoderPageQuery,
type NdmDecoderResultVO,
type NdmDecoderSaveVO,
type NdmDecoderUpdateVO,
type PageParams,
type PageResult,
type Station,
} from '@/apis';
import { unwrapResponse } from '@/utils';
export const pageDecoderApi = async (pageQuery: PageParams<NdmDecoderPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -6,9 +18,7 @@ export const pageDecoderApi = async (pageQuery: PageParams<NdmDecoderPageQuery>,
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmDecoder/page`;
const resp = await client.post<PageResult<NdmDecoderResultVO>>(endpoint, pageQuery, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -18,9 +28,7 @@ export const detailDecoderApi = async (id: string, options?: { stationCode?: Sta
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmDecoder/detail`;
const resp = await client.get<NdmDecoderResultVO>(endpoint, { params: { id }, signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -30,9 +38,7 @@ export const saveDecoderApi = async (saveVO: NdmDecoderSaveVO, options?: { stati
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmDecoder`;
const resp = await client.post<NdmDecoderResultVO>(endpoint, saveVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -42,9 +48,7 @@ export const updateDecoderApi = async (updateVO: NdmDecoderUpdateVO, options?: {
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmDecoder`;
const resp = await client.put<NdmDecoderResultVO>(endpoint, updateVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -54,9 +58,29 @@ export const deleteDecoderApi = async (ids: string[], options?: { stationCode?:
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmDecoder`;
const resp = await client.delete<boolean>(endpoint, ids, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
export const exportDecoderApi = async (pageQuery: PageParams<NdmDecoderPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmDecoder/defaultExportByTemplate`;
const resp = await client.post<Blob>(endpoint, pageQuery, { responseType: 'blob', retRaw: true, signal });
const data = unwrapResponse(resp);
return data;
};
export const importDecoderApi = async (file: File, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmDecoder/importReturnMsg`;
const formData = new FormData();
formData.append('file', file);
const resp = await client.post<ImportMsg>(endpoint, formData, { signal, upload: true });
const data = unwrapResponse(resp);
return data;
};
@@ -66,6 +90,5 @@ export const probeDecoderApi = async (ids: string[], options?: { stationCode?: S
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmDecoder/probeByIds`;
const resp = await client.post<void>(endpoint, ids, { signal });
const [err] = resp;
if (err) throw err;
unwrapResponse(resp);
};

View File

@@ -1,4 +1,16 @@
import { ndmClient, userClient, type NdmKeyboardPageQuery, type NdmKeyboardResultVO, type NdmKeyboardSaveVO, type NdmKeyboardUpdateVO, type PageParams, type PageResult, type Station } from '@/apis';
import {
ndmClient,
userClient,
type ImportMsg,
type NdmKeyboardPageQuery,
type NdmKeyboardResultVO,
type NdmKeyboardSaveVO,
type NdmKeyboardUpdateVO,
type PageParams,
type PageResult,
type Station,
} from '@/apis';
import { unwrapResponse } from '@/utils';
export const pageKeyboardApi = async (pageQuery: PageParams<NdmKeyboardPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -6,9 +18,7 @@ export const pageKeyboardApi = async (pageQuery: PageParams<NdmKeyboardPageQuery
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmKeyboard/page`;
const resp = await client.post<PageResult<NdmKeyboardResultVO>>(endpoint, pageQuery, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -18,9 +28,7 @@ export const detailKeyboardApi = async (id: string, options?: { stationCode?: St
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmKeyboard/detail`;
const resp = await client.get<NdmKeyboardResultVO>(endpoint, { params: { id }, signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -30,9 +38,7 @@ export const saveKeyboardApi = async (saveVO: NdmKeyboardSaveVO, options?: { sta
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmKeyboard`;
const resp = await client.post<NdmKeyboardResultVO>(endpoint, saveVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -42,9 +48,7 @@ export const updateKeyboardApi = async (updateVO: NdmKeyboardUpdateVO, options?:
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmKeyboard`;
const resp = await client.put<NdmKeyboardResultVO>(endpoint, updateVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -54,8 +58,28 @@ export const deleteKeyboardApi = async (ids: string[], options?: { stationCode?:
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmKeyboard`;
const resp = await client.delete<boolean>(endpoint, ids, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
export const exportKeyboardApi = async (pageQuery: PageParams<NdmKeyboardPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmKeyboard/defaultExportByTemplate`;
const resp = await client.post<Blob>(endpoint, pageQuery, { responseType: 'blob', retRaw: true, signal });
const data = unwrapResponse(resp);
return data;
};
export const importKeyboardApi = async (file: File, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmKeyboard/importReturnMsg`;
const formData = new FormData();
formData.append('file', file);
const resp = await client.post<ImportMsg>(endpoint, formData, { signal, upload: true });
const data = unwrapResponse(resp);
return data;
};

View File

@@ -1,6 +1,7 @@
import {
ndmClient,
userClient,
type ImportMsg,
type NdmMediaServerPageQuery,
type NdmMediaServerResultVO,
type NdmMediaServerSaveVO,
@@ -9,6 +10,7 @@ import {
type PageResult,
type Station,
} from '@/apis';
import { unwrapResponse } from '@/utils';
export const postNdmMediaServerPage = async (pageQuery: PageParams<NdmMediaServerPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -16,9 +18,7 @@ export const postNdmMediaServerPage = async (pageQuery: PageParams<NdmMediaServe
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmMediaServer/page`;
const resp = await client.post<PageResult<NdmMediaServerResultVO>>(endpoint, pageQuery, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -28,9 +28,7 @@ export const detailMediaServerApi = async (id: string, options?: { stationCode?:
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmMediaServer/detail`;
const resp = await client.get<NdmMediaServerResultVO>(endpoint, { params: { id }, signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -40,9 +38,7 @@ export const saveMediaServerApi = async (saveVO: NdmMediaServerSaveVO, options?:
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmMediaServer`;
const resp = await client.post<NdmMediaServerResultVO>(endpoint, saveVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -52,9 +48,7 @@ export const updateMediaServerApi = async (updateVO: NdmMediaServerUpdateVO, opt
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmMediaServer`;
const resp = await client.put<NdmMediaServerResultVO>(endpoint, updateVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -64,9 +58,29 @@ export const deleteMediaServerApi = async (ids: string[], options?: { stationCod
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmMediaServer`;
const resp = await client.delete<boolean>(endpoint, ids, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
export const exportMediaServerApi = async (pageQuery: PageParams<NdmMediaServerPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmMediaServer/defaultExportByTemplate`;
const resp = await client.post<Blob>(endpoint, pageQuery, { responseType: 'blob', retRaw: true, signal });
const data = unwrapResponse(resp);
return data;
};
export const importMediaServerApi = async (file: File, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmMediaServer/importReturnMsg`;
const formData = new FormData();
formData.append('file', file);
const resp = await client.post<ImportMsg>(endpoint, formData, { signal, upload: true });
const data = unwrapResponse(resp);
return data;
};
@@ -76,6 +90,5 @@ export const probeMediaServerApi = async (ids: string[], options?: { stationCode
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmMediaServer/probeByIds`;
const resp = await client.post<void>(endpoint, ids, { signal });
const [err] = resp;
if (err) throw err;
unwrapResponse(resp);
};

View File

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

View File

@@ -1,6 +1,7 @@
import {
ndmClient,
userClient,
type ImportMsg,
type NdmVideoServerPageQuery,
type NdmVideoServerResultVO,
type NdmVideoServerSaveVO,
@@ -9,6 +10,7 @@ import {
type PageResult,
type Station,
} from '@/apis';
import { unwrapResponse } from '@/utils';
export const pageVideoServerApi = async (pageQuery: PageParams<NdmVideoServerPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -16,9 +18,7 @@ export const pageVideoServerApi = async (pageQuery: PageParams<NdmVideoServerPag
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmVideoServer/page`;
const resp = await client.post<PageResult<NdmVideoServerResultVO>>(endpoint, pageQuery, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -28,9 +28,7 @@ export const detailVideoServerApi = async (id: string, options?: { stationCode?:
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmVideoServer/detail`;
const resp = await client.get<NdmVideoServerResultVO>(endpoint, { params: { id }, signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -40,21 +38,17 @@ export const saveVideoServerApi = async (saveVO: NdmVideoServerSaveVO, options?:
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmVideoServer`;
const resp = await client.post<NdmVideoServerResultVO>(endpoint, saveVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
export const updateVideoServerApi = async (id: string, updateVO: NdmVideoServerUpdateVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
export const updateVideoServerApi = async (updateVO: NdmVideoServerUpdateVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmVideoServer`;
const resp = await client.put<NdmVideoServerResultVO>(endpoint, updateVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -64,9 +58,29 @@ export const deleteVideoServerApi = async (ids: string[], options?: { stationCod
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmVideoServer`;
const resp = await client.delete<boolean>(endpoint, ids, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
export const exportVideoServerApi = async (pageQuery: PageParams<NdmVideoServerPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmVideoServer/defaultExportByTemplate`;
const resp = await client.post<Blob>(endpoint, pageQuery, { responseType: 'blob', retRaw: true, signal });
const data = unwrapResponse(resp);
return data;
};
export const importVideoServerApi = async (file: File, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmVideoServer/importReturnMsg`;
const formData = new FormData();
formData.append('file', file);
const resp = await client.post<ImportMsg>(endpoint, formData, { signal, upload: true });
const data = unwrapResponse(resp);
return data;
};
@@ -76,6 +90,5 @@ export const probeVideoServerApi = async (ids: string[], options?: { stationCode
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmVideoServer/probeByIds`;
const resp = await client.post<void>(endpoint, ids, { signal });
const [err] = resp;
if (err) throw err;
unwrapResponse(resp);
};

View File

@@ -1,5 +1,6 @@
import { ndmClient, userClient, type DefParameterPageQuery, type DefParameterResultVO, type DefParameterUpdateVO, type PageParams, type PageResult, type Station } from '@/apis';
import type { Result } from '@/types';
import { unwrapResponse } from '@/utils';
export const pageDefParameterApi = async (pageQuery: PageParams<DefParameterPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
@@ -7,9 +8,7 @@ export const pageDefParameterApi = async (pageQuery: PageParams<DefParameterPage
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/system/defParameter/page`;
const resp = await client.post<PageResult<DefParameterResultVO>>(endpoint, pageQuery, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};
@@ -19,8 +18,6 @@ export const updateDefParameterApi = async (updateVO: DefParameterUpdateVO, opti
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/system/defParameter`;
const resp = await client.put<Result<DefParameterResultVO>>(endpoint, updateVO, { signal });
const [err, data] = resp;
if (err) throw err;
if (!data) throw new Error(`${data}`);
const data = unwrapResponse(resp);
return data;
};

View File

@@ -8,12 +8,16 @@ const props = defineProps<{
memUsage?: string;
diskUsage?: string;
runningTime?: string;
cpuUsageLabel?: string;
memUsageLabel?: string;
diskUsageLabel?: string;
runningTimeLabel?: string;
}>();
const { cpuUsage, memUsage, diskUsage, runningTime } = toRefs(props);
const { cpuUsage, memUsage, diskUsage, runningTime, cpuUsageLabel, memUsageLabel, diskUsageLabel, runningTimeLabel } = toRefs(props);
const showCard = computed(() => {
return Object.values(props).some((value) => !!value);
return Object.values({ cpuUsage, memUsage, diskUsage, runningTime }).some((value) => !!value);
});
const cpuPercent = computed(() => {
@@ -51,22 +55,22 @@ const getProgressStatus = (percent: number): ProgressStatus => {
<NFlex vertical>
<NFlex v-if="cpuUsage" style="width: 100%" align="center" :wrap="false">
<NIcon :component="FireOutlined" />
<span style="word-break: keep-all">CPU</span>
<span style="word-break: keep-all">{{ cpuUsageLabel || 'CPU' }}</span>
<NProgress :percentage="cpuPercent" :status="getProgressStatus(cpuPercent)">{{ cpuPercent }}%</NProgress>
</NFlex>
<NFlex v-if="memUsage" style="width: 100%" align="center" :wrap="false">
<NIcon :component="CodeOutlined" />
<span style="word-break: keep-all">内存</span>
<span style="word-break: keep-all">{{ memUsageLabel || '内存' }}</span>
<NProgress :percentage="memPercent" :status="getProgressStatus(memPercent)">{{ memPercent }}%</NProgress>
</NFlex>
<NFlex v-if="diskUsage" style="width: 100%" align="center" :wrap="false">
<NIcon :component="SaveOutlined" />
<span style="word-break: keep-all">磁盘</span>
<span style="word-break: keep-all">{{ diskUsageLabel || '磁盘' }}</span>
<NProgress :percentage="diskPercent" :status="getProgressStatus(diskPercent)">{{ diskPercent }}%</NProgress>
</NFlex>
<NFlex v-if="runningTime" style="width: 100%" align="center" :wrap="false">
<NIcon :component="ClockCircleOutlined" />
<span>系统运行时间</span>
<span>{{ runningTimeLabel || '运行时间' }}</span>
<span>{{ formattedRunningTime }}</span>
</NFlex>
</NFlex>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,10 +21,10 @@ const { debugModeEnabled } = storeToRefs(settingStore);
const { ndmDevice, station } = toRefs(props);
const showPageHeader = computed(() => {
return !!route.query['from'];
return !!route.query['fromPage'];
});
const onBack = () => {
router.push({ path: `${route.query['from']}` });
router.push({ path: `${route.query['fromPage']}` });
};
const activeTabName = ref('当前诊断');

View File

@@ -55,6 +55,9 @@ const { mutateAsync: validateDeviceIdDuplicated } = useMutation({
const deviceIdPattern = /^\d{4}01\d{4}$/;
if (!deviceIdPattern.test(deviceId)) throw new Error('设备ID不符合规范');
// 如果没有修改设备ID则不做ID唯一性校验
if (deviceId === ndmDevice.value.deviceId) return;
validatorAbortController.value.abort();
validatorAbortController.value = new AbortController();
@@ -69,8 +72,7 @@ const { mutateAsync: validateDeviceIdDuplicated } = useMutation({
const { mutate: updateDevice, isPending: loading } = useMutation({
mutationFn: async () => {
await formInst.value?.validate().catch(() => {
window.$message.error('表单验失败');
return;
throw new Error('表单验失败');
});
abortController.value.abort();
@@ -85,6 +87,7 @@ const { mutate: updateDevice, isPending: loading } = useMutation({
onSuccess: (newDevice) => {
localDevice.value = { ...newDevice };
deviceStore.patchDevice(station.value.code, { ...newDevice });
window.$message.success('更新成功');
},
onError: (error) => {
if (isCancel(error)) return;
@@ -136,9 +139,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -21,10 +21,10 @@ const { debugModeEnabled } = storeToRefs(settingStore);
const { ndmDevice, station } = toRefs(props);
const showPageHeader = computed(() => {
return !!route.query['from'];
return !!route.query['fromPage'];
});
const onBack = () => {
router.push({ path: `${route.query['from']}` });
router.push({ path: `${route.query['fromPage']}` });
};
const activeTabName = ref('当前诊断');

View File

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

View File

@@ -54,6 +54,9 @@ const { mutateAsync: validateDeviceIdDuplicated } = useMutation({
const deviceIdPattern = /^\d{4}06\d{4}$/;
if (!deviceIdPattern.test(deviceId)) throw new Error('设备ID不符合规范');
// 如果没有修改设备ID则不做ID唯一性校验
if (deviceId === ndmDevice.value.deviceId) return;
validatorAbortController.value.abort();
validatorAbortController.value = new AbortController();
@@ -68,8 +71,7 @@ const { mutateAsync: validateDeviceIdDuplicated } = useMutation({
const { mutate: updateDevice, isPending: loading } = useMutation({
mutationFn: async () => {
await formInst.value?.validate().catch(() => {
window.$message.error('表单验失败');
return;
throw new Error('表单验失败');
});
abortController.value.abort();
@@ -84,6 +86,7 @@ const { mutate: updateDevice, isPending: loading } = useMutation({
onSuccess: (newDevice) => {
localDevice.value = { ...newDevice };
deviceStore.patchDevice(station.value.code, { ...newDevice });
window.$message.success('更新成功');
},
onError: (error) => {
if (isCancel(error)) return;
@@ -117,9 +120,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -21,10 +21,10 @@ const { debugModeEnabled } = storeToRefs(settingStore);
const { ndmDevice, station } = toRefs(props);
const showPageHeader = computed(() => {
return !!route.query['from'];
return !!route.query['fromPage'];
});
const onBack = () => {
router.push({ path: `${route.query['from']}` });
router.push({ path: `${route.query['fromPage']}` });
};
const activeTabName = ref('当前诊断');

View File

@@ -54,6 +54,9 @@ const { mutateAsync: validateDeviceIdDuplicated } = useMutation({
const deviceIdPattern = /^\d{4}07\d{4}$/;
if (!deviceIdPattern.test(deviceId)) throw new Error('设备ID不符合规范');
// 如果没有修改设备ID则不做ID唯一性校验
if (deviceId === ndmDevice.value.deviceId) return;
validatorAbortController.value.abort();
validatorAbortController.value = new AbortController();
@@ -68,8 +71,7 @@ const { mutateAsync: validateDeviceIdDuplicated } = useMutation({
const { mutate: updateDevice, isPending: loading } = useMutation({
mutationFn: async () => {
await formInst.value?.validate().catch(() => {
window.$message.error('表单验失败');
return;
throw new Error('表单验失败');
});
abortController.value.abort();
@@ -84,6 +86,7 @@ const { mutate: updateDevice, isPending: loading } = useMutation({
onSuccess: (newDevice) => {
localDevice.value = { ...newDevice };
deviceStore.patchDevice(station.value.code, { ...newDevice });
window.$message.success('更新成功');
},
onError: (error) => {
if (isCancel(error)) return;
@@ -138,9 +141,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -21,10 +21,10 @@ const { debugModeEnabled } = storeToRefs(settingStore);
const { ndmDevice, station } = toRefs(props);
const showPageHeader = computed(() => {
return !!route.query['from'];
return !!route.query['fromPage'];
});
const onBack = () => {
router.push({ path: `${route.query['from']}` });
router.push({ path: `${route.query['fromPage']}` });
};
const activeTabName = ref('当前诊断');

View File

@@ -54,6 +54,9 @@ const { mutateAsync: validateDeviceIdDuplicated } = useMutation({
const deviceIdPattern = /^\d{4}08\d{4}$/;
if (!deviceIdPattern.test(deviceId)) throw new Error('设备ID不符合规范');
// 如果没有修改设备ID则不做ID唯一性校验
if (deviceId === ndmDevice.value.deviceId) return;
validatorAbortController.value.abort();
validatorAbortController.value = new AbortController();
@@ -68,8 +71,7 @@ const { mutateAsync: validateDeviceIdDuplicated } = useMutation({
const { mutate: updateDevice, isPending: loading } = useMutation({
mutationFn: async () => {
await formInst.value?.validate().catch(() => {
window.$message.error('表单验失败');
return;
throw new Error('表单验失败');
});
abortController.value.abort();
@@ -84,6 +86,7 @@ const { mutate: updateDevice, isPending: loading } = useMutation({
onSuccess: (newDevice) => {
localDevice.value = { ...newDevice };
deviceStore.patchDevice(station.value.code, { ...newDevice });
window.$message.success('更新成功');
},
onError: (error) => {
if (isCancel(error)) return;
@@ -126,9 +129,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -21,10 +21,10 @@ const { debugModeEnabled } = storeToRefs(settingStore);
const { ndmDevice, station } = toRefs(props);
const showPageHeader = computed(() => {
return !!route.query['from'];
return !!route.query['fromPage'];
});
const onBack = () => {
router.push({ path: `${route.query['from']}` });
router.push({ path: `${route.query['fromPage']}` });
};
const activeTabName = ref('当前诊断');

View File

@@ -54,6 +54,9 @@ const { mutateAsync: validateDeviceIdDuplicated } = useMutation({
const deviceIdPattern = /^\d{4}05\d{4}$/;
if (!deviceIdPattern.test(deviceId)) throw new Error('设备ID不符合规范');
// 如果没有修改设备ID则不做ID唯一性校验
if (deviceId === ndmDevice.value.deviceId) return;
validatorAbortController.value.abort();
validatorAbortController.value = new AbortController();
@@ -68,8 +71,7 @@ const { mutateAsync: validateDeviceIdDuplicated } = useMutation({
const { mutate: updateDevice, isPending } = useMutation({
mutationFn: async () => {
await formInst.value?.validate().catch(() => {
window.$message.error('表单验失败');
return;
throw new Error('表单验失败');
});
abortController.value.abort();
@@ -84,6 +86,7 @@ const { mutate: updateDevice, isPending } = useMutation({
onSuccess: (newDevice) => {
localDevice.value = { ...newDevice };
deviceStore.patchDevice(station.value.code, { ...newDevice });
window.$message.success('更新成功');
},
onError: (error) => {
if (isCancel(error)) return;
@@ -138,9 +141,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -21,10 +21,10 @@ const { debugModeEnabled } = storeToRefs(settingStore);
const { ndmDevice, station } = toRefs(props);
const showPageHeader = computed(() => {
return !!route.query['from'];
return !!route.query['fromPage'];
});
const onBack = () => {
router.push({ path: `${route.query['from']}` });
router.push({ path: `${route.query['fromPage']}` });
};
const activeTabName = ref('当前诊断');

View File

@@ -49,7 +49,7 @@ const circuits = computed(() => lastDiagInfo.value?.info?.at(0)?.circuits);
<DeviceCommonCard :common-info="commonInfo" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
<SecurityBoxEnvCard :fan-speeds="fanSpeeds" :temperature="temperature" :humidity="humidity" :switches="switches" />
<SecurityBoxCircuitCard :circuits="circuits" :ndm-device="ndmDevice" :station="station" />
<SecurityBoxCircuitCard :ndm-device="ndmDevice" :station="station" :circuits="circuits" />
</NFlex>
</template>

View File

@@ -54,6 +54,9 @@ const { mutateAsync: validateDeviceIdDuplicated } = useMutation({
const deviceIdPattern = /^\d{4}03\d{4}$/;
if (!deviceIdPattern.test(deviceId)) throw new Error('设备ID不符合规范');
// 如果没有修改设备ID则不做ID唯一性校验
if (deviceId === ndmDevice.value.deviceId) return;
validatorAbortController.value.abort();
validatorAbortController.value = new AbortController();
@@ -68,8 +71,7 @@ const { mutateAsync: validateDeviceIdDuplicated } = useMutation({
const { mutate: updateDevice, isPending } = useMutation({
mutationFn: async () => {
await formInst.value?.validate().catch(() => {
window.$message.error('表单验失败');
return;
throw new Error('表单验失败');
});
abortController.value.abort();
@@ -84,6 +86,7 @@ const { mutate: updateDevice, isPending } = useMutation({
onSuccess: (newDevice) => {
localDevice.value = { ...newDevice };
deviceStore.patchDevice(station.value.code, { ...newDevice });
window.$message.success('更新成功');
},
onError: (error) => {
if (isCancel(error)) return;
@@ -129,9 +132,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -1,6 +1,8 @@
import ServerAlive from './server-alive.vue';
import ServerCard from './server-card.vue';
import ServerCurrentDiag from './server-current-diag.vue';
import ServerHistoryDiag from './server-history-diag.vue';
import ServerStreamPush from './server-stream-push.vue';
import ServerUpdate from './server-update.vue';
export { ServerCard, ServerCurrentDiag, ServerHistoryDiag, ServerUpdate };
export { ServerAlive, ServerCard, ServerCurrentDiag, ServerHistoryDiag, ServerUpdate, ServerStreamPush };

View File

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

View File

@@ -21,10 +21,10 @@ const { debugModeEnabled } = storeToRefs(settingStore);
const { ndmDevice, station } = toRefs(props);
const showPageHeader = computed(() => {
return !!route.query['from'];
return !!route.query['fromPage'];
});
const onBack = () => {
router.push({ path: `${route.query['from']}` });
router.push({ path: `${route.query['fromPage']}` });
};
const activeTabName = ref('当前诊断');

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { NdmServerDiagInfo, NdmServerResultVO, Station } from '@/apis';
import { DeviceHardwareCard, DeviceHeaderCard } from '@/components';
import { type NdmServerDiagInfo, type NdmServerResultVO, type Station } from '@/apis';
import { DeviceHardwareCard, DeviceHeaderCard, ServerAlive, ServerStreamPush } from '@/components';
import destr from 'destr';
import { NFlex } from 'naive-ui';
import { computed, toRefs } from 'vue';
@@ -28,7 +28,9 @@ const runningTime = computed(() => lastDiagInfo.value?.commInfo?.系统运行时
<template>
<NFlex vertical>
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" :disk-usage="diskUsage" :running-time="runningTime" />
<DeviceHardwareCard running-time-label="服务器运行时间" :cpu-usage="cpuUsage" :mem-usage="memUsage" :disk-usage="diskUsage" :running-time="runningTime" />
<ServerAlive :ndm-device="ndmDevice" :station="station" />
<ServerStreamPush :ndm-device="ndmDevice" :station="station" />
</NFlex>
</template>

View File

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

View File

@@ -1,16 +1,5 @@
<script setup lang="ts">
import {
detailMediaServerApi,
detailVideoServerApi,
icmpEntityByDeviceId,
updateMediaServerApi,
updateVideoServerApi,
type NdmMediaServerUpdateVO,
type NdmServerResultVO,
type NdmServerUpdateVO,
type NdmVideoServerUpdateVO,
type Station,
} from '@/apis';
import { detailMediaServerApi, detailVideoServerApi, icmpEntityByDeviceId, updateMediaServerApi, updateVideoServerApi, type NdmServerResultVO, type NdmServerUpdateVO, type Station } from '@/apis';
import { DEVICE_TYPE_LITERALS, tryGetDeviceType } from '@/enums';
import { useDeviceStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
@@ -66,6 +55,9 @@ const { mutateAsync: validateDeviceIdDuplicated } = useMutation({
const deviceIdPattern = /^\d{4}(09|11)\d{4}$/;
if (!deviceIdPattern.test(deviceId)) throw new Error('设备ID不符合规范');
// 如果没有修改设备ID则不做ID唯一性校验
if (deviceId === ndmDevice.value.deviceId) return;
validatorAbortController.value.abort();
validatorAbortController.value = new AbortController();
@@ -80,8 +72,7 @@ const { mutateAsync: validateDeviceIdDuplicated } = useMutation({
const { mutate: updateDevice, isPending } = useMutation({
mutationFn: async () => {
await formInst.value?.validate().catch(() => {
window.$message.error('表单验失败');
return;
throw new Error('表单验失败');
});
abortController.value.abort();
@@ -91,10 +82,10 @@ const { mutate: updateDevice, isPending } = useMutation({
const stationCode = station.value.code;
const signal = abortController.value.signal;
if (deviceType === DEVICE_TYPE_LITERALS.ndmMediaServer) {
await updateMediaServerApi(localDevice.value as NdmMediaServerUpdateVO, { stationCode, signal });
await updateMediaServerApi(localDevice.value, { stationCode, signal });
return await detailMediaServerApi(`${localDevice.value.id}`, { stationCode, signal });
} else if (deviceType === DEVICE_TYPE_LITERALS.ndmVideoServer) {
await updateVideoServerApi(`${localDevice.value.id}`, localDevice.value as NdmVideoServerUpdateVO, { stationCode, signal });
await updateVideoServerApi(localDevice.value, { stationCode, signal });
return await detailVideoServerApi(`${localDevice.value.id}`, { stationCode, signal });
} else {
throw new Error('不是服务器设备');
@@ -103,6 +94,7 @@ const { mutate: updateDevice, isPending } = useMutation({
onSuccess: (newDevice) => {
localDevice.value = { ...newDevice };
deviceStore.patchDevice(station.value.code, { ...newDevice });
window.$message.success('更新成功');
},
onError: (error) => {
if (isCancel(error)) return;
@@ -148,9 +140,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -21,10 +21,10 @@ const { debugModeEnabled } = storeToRefs(settingStore);
const { ndmDevice, station } = toRefs(props);
const showPageHeader = computed(() => {
return !!route.query['from'];
return !!route.query['fromPage'];
});
const onBack = () => {
router.push({ path: `${route.query['from']}` });
router.push({ path: `${route.query['fromPage']}` });
};
const activeTabName = ref('当前诊断');

View File

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

View File

@@ -54,6 +54,9 @@ const { mutateAsync: validateDeviceIdDuplicated } = useMutation({
const deviceIdPattern = /^\d{4}04\d{4}$/;
if (!deviceIdPattern.test(deviceId)) throw new Error('设备ID不符合规范');
// 如果没有修改设备ID则不做ID唯一性校验
if (deviceId === ndmDevice.value.deviceId) return;
validatorAbortController.value.abort();
validatorAbortController.value = new AbortController();
@@ -68,8 +71,7 @@ const { mutateAsync: validateDeviceIdDuplicated } = useMutation({
const { mutate: updateDevice, isPending: loading } = useMutation({
mutationFn: async () => {
await formInst.value?.validate().catch(() => {
window.$message.error('表单验失败');
return;
throw new Error('表单验失败');
});
abortController.value.abort();
@@ -84,6 +86,7 @@ const { mutate: updateDevice, isPending: loading } = useMutation({
onSuccess: (newDevice) => {
localDevice.value = { ...newDevice };
deviceStore.patchDevice(station.value.code, { ...newDevice });
window.$message.success('更新成功');
},
onError: (error) => {
if (isCancel(error)) return;
@@ -138,9 +141,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -1,475 +0,0 @@
<script setup lang="ts">
import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis';
import { useDeviceTree } from '@/composables';
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType } from '@/enums';
import { isNvrCluster } from '@/helpers';
import { useDeviceStore, useStationStore } from '@/stores';
import { sleep } from '@/utils';
import { watchDebounced, watchImmediate } from '@vueuse/core';
import destr from 'destr';
import { isFunction } from 'es-toolkit';
import {
NButton,
NDropdown,
NFlex,
NInput,
NRadio,
NRadioGroup,
NTab,
NTabs,
NTag,
NTree,
useThemeVars,
type DropdownOption,
type TagProps,
type TreeInst,
type TreeOption,
type TreeOverrideNodeClickBehavior,
type TreeProps,
} from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, h, onMounted, ref, toRefs, useTemplateRef, watch, type CSSProperties } from 'vue';
const props = defineProps<{
station?: Station; // 支持渲染指定车站的设备树
}>();
const emit = defineEmits<{
selectDevice: [device: NdmDeviceResultVO, stationCode: Station['code']];
}>();
const { station } = toRefs(props);
const themeVars = useThemeVars();
const { selectedStationCode, selectedDeviceType, selectedDevice, initFromRoute, selectDevice, routeDevice } = useDeviceTree();
const onSelectDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
selectDevice(device, stationCode);
emit('selectDevice', device, stationCode);
};
const onRouteDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
routeDevice(device, stationCode, { path: '/device' });
emit('selectDevice', device, stationCode);
};
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
onMounted(() => {
initFromRoute(lineDevices.value);
});
// lineDevices是shallowRef因此需要深度侦听才能获取内部变化
// 而单纯的深度侦听又可能会引发性能问题,因此尝试使用防抖侦听
watchDebounced(
lineDevices,
(newLineDevices) => {
initFromRoute(newLineDevices);
},
{
debounce: 500,
deep: true,
},
);
const deviceTabPanes = Object.values(DEVICE_TYPE_LITERALS).map((deviceType) => ({
name: deviceType,
tab: DEVICE_TYPE_NAMES[deviceType],
}));
const activeTab = ref<DeviceType>(deviceTabPanes.at(0)!.name);
watchImmediate(selectedDeviceType, (newDeviceType) => {
if (newDeviceType) {
activeTab.value = newDeviceType;
}
});
const selectedKeys = computed(() => (selectedDevice.value?.id ? [selectedDevice.value.id] : undefined));
watch([selectedKeys, selectedDevice, selectedStationCode], ([, device, code]) => {
if (device && code) {
onSelectDevice(device, code);
}
});
const contextmenu = ref<{ x: number; y: number; stationCode?: Station['code']; deviceType: DeviceType | null }>({ x: 0, y: 0, deviceType: null });
const showContextmenu = ref(false);
const contextmenuOptions: DropdownOption[] = [
{
label: '导出设备',
key: 'export-device',
onSelect: () => {
// 需要拿到当前选中的设备类型和车站编号
const { stationCode, deviceType } = contextmenu.value;
console.log(stationCode, deviceType);
showContextmenu.value = false;
},
},
];
const onSelectDropdownOption = (key: string, option: DropdownOption) => {
const onSelect = option['onSelect'];
if (isFunction(onSelect)) {
onSelect();
}
};
// ========== 设备树节点交互 ==========
const override: TreeOverrideNodeClickBehavior = ({ option }) => {
const hasChildren = (option.children?.length ?? 0) > 0;
const isDeviceNode = !!option['device'];
if (hasChildren || !isDeviceNode) {
return 'toggleExpand';
} else {
return 'none';
}
};
const nodeProps: TreeProps['nodeProps'] = ({ option }) => {
return {
onDblclick: (payload) => {
if (option['device']) {
payload.stopPropagation();
const device = option['device'] as NdmDeviceResultVO;
const stationCode = option['stationCode'] as string;
// 区分是否需要跳转路由
if (!station.value) {
onSelectDevice(device, stationCode);
} else {
onRouteDevice(device, station.value.code);
}
}
},
// TODO: 支持右键点击车站导出设备列表
onContextmenu: (payload) => {
payload.stopPropagation();
payload.preventDefault();
if (!option['device']) {
const { clientX, clientY } = payload;
const stationCode = option['stationCode'] as string;
const deviceType = option['deviceType'] as DeviceType;
contextmenu.value = { x: clientX, y: clientY, stationCode, deviceType };
showContextmenu.value = true;
}
},
};
};
// ========== 设备树数据 ==========
const renderStationNodePrefix = (station: Station) => {
const { online } = station;
const tagType: TagProps['type'] = online ? 'success' : 'error';
const tagText = online ? '在线' : '离线';
return h(NTag, { type: tagType, size: 'tiny' }, () => tagText);
};
const renderIcmpStatistics = (onlineCount: number, offlineCount: number, count: number) => {
return h('span', null, [
'(',
h('span', { style: { color: themeVars.value.successColor } }, `${onlineCount}`),
'/',
h('span', { style: { color: themeVars.value.errorColor } }, `${offlineCount}`),
'/',
`${count}`,
')',
]);
};
const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: string) => {
const renderViewDeviceButton = (device: NdmDeviceResultVO, stationCode: string) => {
return h(
NButton,
{
text: true,
size: 'tiny',
type: 'info',
style: {
marginRight: 8,
} as CSSProperties,
onClick: (e: MouseEvent) => {
e.stopPropagation();
// 选择设备
// 区分是否需要跳转路由
if (!station.value) {
onSelectDevice(device, stationCode);
} else {
onRouteDevice(device, station.value.code);
}
},
},
() => '查看',
);
};
const renderDeviceStatusTag = (device: NdmDeviceResultVO) => {
const { deviceStatus } = device;
const color = deviceStatus === '10' ? themeVars.value.successColor : deviceStatus === '20' ? themeVars.value.errorColor : themeVars.value.warningColor;
return h('div', { style: { color } }, { default: () => '◉' });
};
return h(NFlex, { size: 'small' }, { default: () => [renderViewDeviceButton(device, stationCode), renderDeviceStatusTag(device)] });
};
// 全线设备树
const lineDeviceTreeData = computed<Record<string, TreeOption[]>>(() => {
const treeData: Record<string, TreeOption[]> = {};
deviceTabPanes.forEach(({ name: paneName /* , tab: paneTab */ }) => {
treeData[paneName] = stations.value.map<TreeOption>((station) => {
const { name: stationName, code: stationCode } = station;
const devices = lineDevices.value[stationCode]?.[paneName] ?? ([] as NdmDeviceResultVO[]);
const onlineDevices = devices?.filter((device) => device.deviceStatus === '10');
const offlineDevices = devices?.filter((device) => device.deviceStatus === '20');
// 对于录像机需要根据clusterList字段以分号分隔设备IP进一步形成子树结构
if (paneName === DEVICE_TYPE_LITERALS.ndmNvr) {
const nvrs = devices as NdmNvrResultVO[];
const nvrClusters: NdmNvrResultVO[] = [];
const nvrSingletons: NdmNvrResultVO[] = [];
for (const device of nvrs) {
if (isNvrCluster(device)) {
nvrClusters.push(device);
} else {
nvrSingletons.push(device);
}
}
return {
label: stationName,
key: stationCode,
prefix: () => renderStationNodePrefix(station),
suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0),
children: nvrClusters.map<TreeOption>((nvrCluster) => {
return {
label: `${nvrCluster.name}`,
key: nvrCluster.id ?? `${nvrCluster.name}`,
prefix: () => renderDeviceNodePrefix(nvrCluster, stationCode),
suffix: () => `${nvrCluster.ipAddress}`,
children: nvrSingletons.map<TreeOption>((nvr) => {
return {
label: `${nvr.name}`,
key: nvr.id ?? `${nvr.name}`,
prefix: () => renderDeviceNodePrefix(nvr, stationCode),
suffix: () => `${nvr.ipAddress}`,
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
stationCode,
device: nvr,
};
}),
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
stationCode,
device: nvrCluster,
};
}),
stationCode,
deviceType: activeTab.value,
};
}
return {
label: stationName,
key: stationCode,
prefix: () => renderStationNodePrefix(station),
suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0),
children:
lineDevices.value[stationCode]?.[paneName]?.map<TreeOption>((dev) => {
const device = dev as NdmDeviceResultVO;
return {
label: `${device.name}`,
key: device.id ?? `${device.name}`,
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
stationCode,
device,
};
}) ?? [],
stationCode,
deviceType: activeTab.value,
};
});
});
return treeData;
});
// 车站设备树
const stationDeviceTreeData = computed<TreeOption[]>(() => {
const stationCode = station.value?.code;
if (!stationCode) return [];
return Object.values(DEVICE_TYPE_LITERALS).map<TreeOption>((deviceType) => {
const stationDevices = lineDevices.value[stationCode] ?? initStationDevices();
const onlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '10').length;
const offlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '20').length;
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
const nvrs = stationDevices[deviceType] as NdmNvrResultVO[];
const clusters = nvrs.filter((nvr) => isNvrCluster(nvr));
const singletons = nvrs.filter((nvr) => !isNvrCluster(nvr));
return {
label: `${DEVICE_TYPE_NAMES[deviceType]}`,
key: deviceType,
suffix: () => renderIcmpStatistics(onlineCount, offlineCount, nvrs.length),
children: clusters.map<TreeOption>((device) => {
return {
label: `${device.name}`,
key: device.id ?? `${device.name}`,
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
children: singletons.map<TreeOption>((device) => {
return {
label: `${device.name}`,
key: device.id ?? `${device.name}`,
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
stationCode,
device,
};
}),
stationCode,
device,
};
}),
stationCode,
deviceType,
};
}
return {
label: `${DEVICE_TYPE_NAMES[deviceType]}`,
key: deviceType,
suffix: () => renderIcmpStatistics(onlineCount, offlineCount, stationDevices[deviceType].length),
children: stationDevices[deviceType].map<TreeOption>((device) => {
return {
label: `${device.name}`,
key: device.id ?? `${device.name}`,
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
stationCode,
device,
};
}),
stationCode,
deviceType,
};
});
});
// ========== 设备树搜索 ==========
const searchInput = ref('');
const statusInput = ref('');
// 设备树将搜索框和单选框的值都交给NTree的pattern属性
// 但是如果一个车站下没有匹配的设备,那么这个车站节点也不会显示
const searchPattern = computed(() => {
const search = searchInput.value;
const status = statusInput.value;
if (!search && !status) return ''; // 如果pattern非空会导致NTree组件认为筛选完成UI上发生全量匹配
return JSON.stringify({ search: searchInput.value, status: statusInput.value });
});
const searchFilter = (pattern: string, node: TreeOption): boolean => {
const { search, status } = destr<{ search: string; status: string }>(pattern);
const device = node['device'] as NdmDeviceResultVO | undefined;
const { name, ipAddress, deviceId, deviceStatus } = device ?? {};
const searchMatched = (name ?? '').includes(search) || (ipAddress ?? '').includes(search) || (deviceId ?? '').includes(search);
const statusMatched = status === '' || status === deviceStatus;
return searchMatched && statusMatched;
};
// ========== 设备树交互 ==========
const expandedKeys = ref<string[]>();
const deviceTreeInst = useTemplateRef<TreeInst>('deviceTreeInst');
const onFoldDeviceTree = () => {
expandedKeys.value = [];
};
const onLocateDeviceTree = () => {
const stationCode = selectedStationCode.value;
const device = selectedDevice.value;
if (!stationCode || !device?.id) return;
const deviceTypeVal = tryGetDeviceType(device.deviceType);
if (!!deviceTypeVal) {
activeTab.value = deviceTypeVal;
}
const expanded = [stationCode];
if (activeTab.value === DEVICE_TYPE_LITERALS.ndmNvr) {
const nvrs = lineDevices.value[stationCode]?.[DEVICE_TYPE_LITERALS.ndmNvr];
if (nvrs) {
const clusterKeys = nvrs.filter((nvr) => !!nvr.clusterList?.trim() && nvr.clusterList !== nvr.ipAddress).map((nvr) => String(nvr.id));
expanded.push(...clusterKeys);
}
}
expandedKeys.value = expanded;
// 由于数据量大所以开启虚拟滚动,
// 但是无法知晓NTree内部的虚拟列表容器何时创建完成所以使用setTimeout延迟固定时间后执行滚动
scrollDeviceTreeToSelectedDevice();
};
async function scrollDeviceTreeToSelectedDevice() {
await sleep(350);
const inst = deviceTreeInst.value;
inst?.scrollTo({ key: selectedDevice?.value?.id ?? `${selectedDevice.value?.name}`, behavior: 'smooth' });
}
</script>
<template>
<div style="height: 100%; display: flex; flex-direction: column">
<!-- 搜索和筛选 -->
<div style="padding: 12px; flex: 0 0 auto">
<NInput v-model:value="searchInput" placeholder="搜索设备名称、设备ID或IP地址" clearable />
<NFlex align="center">
<NRadioGroup v-model:value="statusInput">
<NRadio value="">全部</NRadio>
<NRadio value="10">在线</NRadio>
<NRadio value="20">离线</NRadio>
</NRadioGroup>
<NButton text size="tiny" type="info" @click="onFoldDeviceTree" style="margin-left: auto">收起</NButton>
<NButton text size="tiny" type="info" @click="onLocateDeviceTree">定位</NButton>
</NFlex>
</div>
<!-- 设备树 -->
<div style="overflow: hidden; flex: 1 1 auto; display: flex">
<template v-if="!station">
<div style="height: 100%; flex: 0 0 auto">
<NTabs v-model:value="activeTab" animated type="line" placement="left" style="height: 100%">
<NTab v-for="pane in deviceTabPanes" :key="pane.name" :name="pane.name" :tab="pane.tab"></NTab>
</NTabs>
</div>
<div style="min-width: 0; flex: 1 1 auto">
<NTree
style="height: 100%"
v-model:expanded-keys="expandedKeys"
block-line
block-node
show-line
virtual-scroll
:ref="'deviceTreeInst'"
:selected-keys="selectedKeys"
:data="lineDeviceTreeData[activeTab]"
:show-irrelevant-nodes="false"
:pattern="searchPattern"
:filter="searchFilter"
:override-default-node-click-behavior="override"
:node-props="nodeProps"
:default-expand-all="false"
/>
</div>
</template>
<template v-else>
<NTree
style="height: 100%"
block-line
block-node
show-line
virtual-scroll
:data="stationDeviceTreeData"
:show-irrelevant-nodes="false"
:pattern="searchPattern"
:filter="searchFilter"
:override-default-node-click-behavior="override"
:node-props="nodeProps"
:default-expand-all="false"
/>
</template>
</div>
</div>
<NDropdown
placement="bottom-start"
trigger="manual"
:show="showContextmenu"
:x="contextmenu.x"
:y="contextmenu.y"
:options="contextmenuOptions"
@select="onSelectDropdownOption"
@clickoutside="() => (showContextmenu = false)"
/>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,14 +1,15 @@
<script setup lang="ts">
import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis';
import { useDeviceTree } from '@/composables';
import { useDeviceTree, type UseDeviceTreeReturn } from '@/composables';
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType } from '@/enums';
import { isNvrCluster } from '@/helpers';
import { useDeviceStore, useStationStore } from '@/stores';
import { sleep } from '@/utils';
import { watchDebounced, watchImmediate } from '@vueuse/core';
import { watchImmediate } from '@vueuse/core';
import destr from 'destr';
import { isFunction } from 'es-toolkit';
import {
NButton,
NDropdown,
NFlex,
NInput,
NRadio,
@@ -18,6 +19,7 @@ import {
NTag,
NTree,
useThemeVars,
type DropdownOption,
type TagProps,
type TreeInst,
type TreeOption,
@@ -25,30 +27,64 @@ import {
type TreeProps,
} from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, h, onMounted, ref, toRefs, useTemplateRef, watch, type CSSProperties } from 'vue';
import { computed, h, nextTick, onBeforeUnmount, ref, toRefs, useTemplateRef, watch, type CSSProperties } from 'vue';
const props = defineProps<{
station?: Station; // 支持渲染指定车站的设备树
/**
* 支持渲染指定车站的设备树
*/
station?: Station;
/**
* 允许的事件类型
*
* - `select`:允许选择设备
* - `manage`:允许右键菜单管理设备
*/
events?: ('select' | 'manage')[];
/**
* 是否同步路由参数
*/
syncRoute?: boolean;
/**
* 设备节点的前缀按钮文字
*/
devicePrefixLabel?: string;
}>();
const emit = defineEmits<{
selectDevice: [device: NdmDeviceResultVO, stationCode: Station['code']];
afterSelectDevice: [device: NdmDeviceResultVO, stationCode: Station['code']];
exposeSelectDeviceFn: [selectDeviceFn: UseDeviceTreeReturn['selectDevice']];
}>();
const { station } = toRefs(props);
const { station, events, syncRoute, devicePrefixLabel } = toRefs(props);
const themeVars = useThemeVars();
const { selectedStationCode, selectedDeviceType, selectedDevice, initFromRoute, selectDevice, routeDevice } = useDeviceTree();
const {
// 设备选择
selectedStationCode,
selectedDeviceType,
selectedDevice,
selectDevice,
// 设备管理
exportDevice,
exportDeviceTemplate,
importDevice,
deleteDevice,
} = useDeviceTree({
syncRoute: computed(() => !!syncRoute.value),
});
// 将 `selectDevice` 函数暴露给父组件
emit('exposeSelectDeviceFn', selectDevice);
const onSelectDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
selectDevice(device, stationCode);
emit('selectDevice', device, stationCode);
};
// 仅当事件列表包含 `select` 时才触发选择事件
if (!events.value) return;
if (!events.value.includes('select')) return;
const onRouteDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
routeDevice(device, stationCode, { path: '/device' });
emit('selectDevice', device, stationCode);
selectDevice(device, stationCode);
emit('afterSelectDevice', device, stationCode);
};
const stationStore = useStationStore();
@@ -56,23 +92,6 @@ const { stations } = storeToRefs(stationStore);
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
onMounted(() => {
initFromRoute(lineDevices.value);
});
// lineDevices是shallowRef因此需要深度侦听才能获取内部变化
// 而单纯的深度侦听又可能会引发性能问题,因此尝试使用防抖侦听
watchDebounced(
lineDevices,
(newLineDevices) => {
initFromRoute(newLineDevices);
},
{
debounce: 500,
deep: true,
},
);
const deviceTabPanes = Object.values(DEVICE_TYPE_LITERALS).map((deviceType) => ({
name: deviceType,
tab: DEVICE_TYPE_NAMES[deviceType],
@@ -91,6 +110,90 @@ watch([selectedKeys, selectedDevice, selectedStationCode], ([, device, code]) =>
}
});
const abortController = ref(new AbortController());
const contextmenu = ref<{ x: number; y: number; stationCode?: Station['code']; deviceType?: DeviceType | null; device?: NdmDeviceResultVO }>({ x: 0, y: 0, deviceType: null });
const showContextmenu = ref(false);
const contextmenuOptions = computed<DropdownOption[]>(() => [
{
label: '导出设备',
key: 'export-device',
show: !!contextmenu.value.deviceType,
onSelect: () => {
const { stationCode, deviceType } = contextmenu.value;
// console.log(stationCode, deviceType);
showContextmenu.value = false;
if (!stationCode || !deviceType) return;
abortController.value.abort();
abortController.value = new AbortController();
exportDevice({ deviceType, stationCode, signal: abortController.value.signal });
},
},
{
label: '导入设备',
key: 'import-device',
show: !!contextmenu.value.deviceType,
onSelect: () => {
const { stationCode, deviceType } = contextmenu.value;
// console.log(stationCode, deviceType);
showContextmenu.value = false;
if (!stationCode || !deviceType) return;
abortController.value.abort();
abortController.value = new AbortController();
importDevice({ deviceType, stationCode, signal: abortController.value.signal });
},
},
{
label: '下载导入模板',
key: 'export-template',
// 导出模板功能有缺陷,暂时不展示
show: false,
onSelect: () => {
const { stationCode, deviceType } = contextmenu.value;
// console.log(stationCode, deviceType);
showContextmenu.value = false;
if (!stationCode || !deviceType) return;
abortController.value.abort();
abortController.value = new AbortController();
exportDeviceTemplate({ deviceType, stationCode, signal: abortController.value.signal });
},
},
{
label: '删除设备',
key: 'delete-device',
show: !!contextmenu.value.device,
onSelect: () => {
const { stationCode, device } = contextmenu.value;
// console.log(stationCode, device);
showContextmenu.value = false;
if (!stationCode || !device) return;
const id = device.id;
const deviceType = tryGetDeviceType(device.deviceType);
if (!id || !deviceType) return;
window.$dialog.destroyAll();
window.$dialog.warning({
title: '删除设备',
content: `确认删除设备 ${device.name || device.deviceId || device.id} 吗?`,
positiveText: '确认',
negativeText: '取消',
onPositiveClick: () => {
abortController.value.abort();
abortController.value = new AbortController();
deleteDevice({ id, deviceType, stationCode, signal: abortController.value.signal });
},
});
},
},
]);
const onSelectDropdownOption = (key: string, option: DropdownOption) => {
const onSelect = option['onSelect'];
if (isFunction(onSelect)) {
onSelect();
}
};
onBeforeUnmount(() => {
abortController.value.abort();
});
// ========== 设备树节点交互 ==========
const override: TreeOverrideNodeClickBehavior = ({ option }) => {
const hasChildren = (option.children?.length ?? 0) > 0;
@@ -106,16 +209,27 @@ const nodeProps: TreeProps['nodeProps'] = ({ option }) => {
onDblclick: (payload) => {
if (option['device']) {
payload.stopPropagation();
const device = option['device'] as NdmDeviceResultVO;
const stationCode = option['stationCode'] as string;
// 区分是否需要跳转路由
if (!station.value) {
onSelectDevice(device, stationCode);
} else {
onRouteDevice(device, station.value.code);
}
const stationCode = option['stationCode'] as Station['code'];
onSelectDevice(device, stationCode);
}
},
onContextmenu: (payload) => {
payload.stopPropagation();
payload.preventDefault();
// 仅当事件列表包含 `manage` 时才显示右键菜单
if (!events.value?.includes('manage')) return;
const { clientX, clientY } = payload;
const stationCode = option['stationCode'] as Station['code'];
const deviceType = option['deviceType'] as DeviceType | undefined;
const device = option['device'] as NdmDeviceResultVO | undefined;
contextmenu.value = { x: clientX, y: clientY, stationCode, deviceType, device };
showContextmenu.value = true;
},
};
};
@@ -137,8 +251,9 @@ const renderIcmpStatistics = (onlineCount: number, offlineCount: number, count:
')',
]);
};
const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: string) => {
const renderViewDeviceButton = (device: NdmDeviceResultVO, stationCode: string) => {
const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
const renderViewDeviceButton = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
if (!devicePrefixLabel.value) return null;
return h(
NButton,
{
@@ -150,16 +265,11 @@ const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: string)
} as CSSProperties,
onClick: (e: MouseEvent) => {
e.stopPropagation();
// 选择设备
// 区分是否需要跳转路由
if (!station.value) {
onSelectDevice(device, stationCode);
} else {
onRouteDevice(device, station.value.code);
}
onSelectDevice(device, stationCode);
},
},
() => '查看',
() => devicePrefixLabel.value,
);
};
const renderDeviceStatusTag = (device: NdmDeviceResultVO) => {
@@ -170,7 +280,7 @@ const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: string)
return h(NFlex, { size: 'small' }, { default: () => [renderViewDeviceButton(device, stationCode), renderDeviceStatusTag(device)] });
};
// 全线设备树
const lineDeviceTreeData = computed<Record<string, TreeOption[]>>(() => {
const lineDeviceTreeData = computed<Record<Station['code'], TreeOption[]>>(() => {
const treeData: Record<string, TreeOption[]> = {};
deviceTabPanes.forEach(({ name: paneName /* , tab: paneTab */ }) => {
treeData[paneName] = stations.value.map<TreeOption>((station) => {
@@ -217,6 +327,8 @@ const lineDeviceTreeData = computed<Record<string, TreeOption[]>>(() => {
device: nvrCluster,
};
}),
stationCode,
deviceType: activeTab.value,
};
}
return {
@@ -229,7 +341,7 @@ const lineDeviceTreeData = computed<Record<string, TreeOption[]>>(() => {
const device = dev as NdmDeviceResultVO;
return {
label: `${device.name}`,
key: device.id ?? `${device.name}`,
key: `${device.id}`,
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
@@ -237,6 +349,8 @@ const lineDeviceTreeData = computed<Record<string, TreeOption[]>>(() => {
device,
};
}) ?? [],
stationCode,
deviceType: activeTab.value,
};
});
});
@@ -261,13 +375,13 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
children: clusters.map<TreeOption>((device) => {
return {
label: `${device.name}`,
key: device.id ?? `${device.name}`,
key: `${device.id}`,
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
children: singletons.map<TreeOption>((device) => {
return {
label: `${device.name}`,
key: device.id ?? `${device.name}`,
key: `${device.id}`,
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
stationCode,
@@ -278,6 +392,8 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
device,
};
}),
stationCode,
deviceType,
};
}
return {
@@ -287,13 +403,15 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
children: stationDevices[deviceType].map<TreeOption>((device) => {
return {
label: `${device.name}`,
key: device.id ?? `${device.name}`,
key: `${device.id}`,
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
stationCode,
device,
};
}),
stationCode,
deviceType,
};
});
});
@@ -319,39 +437,52 @@ const searchFilter = (pattern: string, node: TreeOption): boolean => {
};
// ========== 设备树交互 ==========
const expandedKeys = ref<string[]>();
const animated = ref(true);
const expandedKeys = ref<string[]>([]);
const deviceTreeInst = useTemplateRef<TreeInst>('deviceTreeInst');
const onFoldDeviceTree = () => {
expandedKeys.value = [];
};
const onLocateDeviceTree = () => {
const stationCode = selectedStationCode.value;
const device = selectedDevice.value;
if (!stationCode || !device?.id) return;
const deviceTypeVal = tryGetDeviceType(device.deviceType);
if (!!deviceTypeVal) {
activeTab.value = deviceTypeVal;
}
const onLocateDeviceTree = async () => {
if (!selectedStationCode.value) return;
if (!selectedDevice.value) return;
const deviceType = tryGetDeviceType(selectedDevice.value.deviceType);
if (!deviceType) return;
if (!deviceTreeInst.value) return;
const expanded = [stationCode];
if (activeTab.value === DEVICE_TYPE_LITERALS.ndmNvr) {
const nvrs = lineDevices.value[stationCode]?.[DEVICE_TYPE_LITERALS.ndmNvr];
if (nvrs) {
const clusterKeys = nvrs.filter((nvr) => !!nvr.clusterList?.trim() && nvr.clusterList !== nvr.ipAddress).map((nvr) => String(nvr.id));
expanded.push(...clusterKeys);
animated.value = false;
// 定位设备类型
activeTab.value = deviceType;
// 展开选择的车站
expandedKeys.value.push(selectedStationCode.value);
// 当选择录像机时,如果不是集群,进一步展开该录像机所在的集群节点
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
const stationDevices = lineDevices.value[selectedStationCode.value];
if (stationDevices) {
const selectedNvr = selectedDevice.value as NdmNvrResultVO;
if (!isNvrCluster(selectedNvr)) {
const nvrs = stationDevices[DEVICE_TYPE_LITERALS.ndmNvr];
const clusters = nvrs.filter((nvr) => isNvrCluster(nvr) && nvr.clusterList?.includes(selectedNvr.clusterList ?? ''));
expandedKeys.value.push(...clusters.map((nvr) => `${nvr.id}`));
}
}
}
expandedKeys.value = expanded;
// 由于数据量大所以开启虚拟滚动,
// 但是无法知晓NTree内部的虚拟列表容器何时创建完成所以使用setTimeout延迟固定时间后执行滚动
scrollDeviceTreeToSelectedDevice();
// 等待设备树展开完成,滚动到选择的设备
await nextTick();
deviceTreeInst.value.scrollTo({ key: `${selectedDevice.value.id}`, behavior: 'smooth' });
animated.value = true;
};
async function scrollDeviceTreeToSelectedDevice() {
await sleep(350);
const inst = deviceTreeInst.value;
inst?.scrollTo({ key: selectedDevice?.value?.id ?? `${selectedDevice.value?.name}`, behavior: 'smooth' });
}
// 渲染全线设备树时,当选择的设备发生变化,则定位设备树
// 暂时不考虑多次执行的问题,因为当选择的设备在设备树视口内时,不会发生滚动
watch(selectedDevice, async () => {
if (!!station.value) return;
await onLocateDeviceTree();
});
</script>
<template>
@@ -365,15 +496,23 @@ async function scrollDeviceTreeToSelectedDevice() {
<NRadio value="10">在线</NRadio>
<NRadio value="20">离线</NRadio>
</NRadioGroup>
<NButton text size="tiny" type="info" @click="onFoldDeviceTree" style="margin-left: auto">收起</NButton>
<NButton text size="tiny" type="info" @click="onLocateDeviceTree">定位</NButton>
<template v-if="!station">
<NButton text size="tiny" type="info" @click="onFoldDeviceTree" style="margin-left: auto">收起</NButton>
<NButton text size="tiny" type="info" @click="onLocateDeviceTree">定位</NButton>
</template>
</NFlex>
</div>
<!-- 设备树 -->
<div style="overflow: hidden; flex: 1 1 auto; display: flex">
<div
style="overflow: hidden; flex: 1 1 auto; display: flex"
:style="{
// 当右键菜单显示时,禁用设备树的点击事件,避免在打开菜单时仍能点击设备树节点
'pointer-events': showContextmenu ? 'none' : 'auto',
}"
>
<template v-if="!station">
<div style="height: 100%; flex: 0 0 auto">
<NTabs v-model:value="activeTab" animated type="line" placement="left" style="height: 100%">
<NTabs v-model:value="activeTab" type="line" placement="left" style="height: 100%">
<NTab v-for="pane in deviceTabPanes" :key="pane.name" :name="pane.name" :tab="pane.tab"></NTab>
</NTabs>
</div>
@@ -386,6 +525,7 @@ async function scrollDeviceTreeToSelectedDevice() {
show-line
virtual-scroll
:ref="'deviceTreeInst'"
:animated="animated"
:selected-keys="selectedKeys"
:data="lineDeviceTreeData[activeTab]"
:show-irrelevant-nodes="false"
@@ -405,6 +545,8 @@ async function scrollDeviceTreeToSelectedDevice() {
show-line
virtual-scroll
:data="stationDeviceTreeData"
:animated="animated"
:selected-keys="selectedKeys"
:show-irrelevant-nodes="false"
:pattern="searchPattern"
:filter="searchFilter"
@@ -415,6 +557,17 @@ async function scrollDeviceTreeToSelectedDevice() {
</template>
</div>
</div>
<NDropdown
placement="bottom-start"
trigger="manual"
:show="showContextmenu"
:x="contextmenu.x"
:y="contextmenu.y"
:options="contextmenuOptions"
@select="onSelectDropdownOption"
@clickoutside="() => (showContextmenu = false)"
/>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { LineAlarms, LineDevices, NdmDeviceResultVO, Station, VersionInfo } from '@/apis';
import { retentionDaysApi, snapStatusApi, type LineAlarms, type LineDevices, type Station, type VersionInfo } from '@/apis';
import { ThemeSwitch } from '@/components';
import { NDM_ALARM_STORE_ID, NDM_DEVICE_STORE_ID, NDM_STATION_STORE_ID } from '@/constants';
import { usePollingStore, useSettingStore } from '@/stores';
@@ -7,16 +7,13 @@ import { downloadByData, getAppEnvConfig, parseErrorFeedback, sleep } from '@/ut
import { useMutation } from '@tanstack/vue-query';
import { DeleteOutlined, ExportOutlined, ImportOutlined } from '@vicons/antd';
import { useEventListener } from '@vueuse/core';
import axios from 'axios';
import axios, { isCancel } from 'axios';
import destr from 'destr';
import { isFunction } from 'es-toolkit';
import localforage from 'localforage';
import { NButton, NDivider, NDrawer, NDrawerContent, NDropdown, NFlex, NFormItem, NIcon, NInput, NInputNumber, NModal, NSwitch, NText, type DropdownOption } from 'naive-ui';
import { NButton, NButtonGroup, NDivider, NDrawer, NDrawerContent, NDropdown, NFlex, NFormItem, NIcon, NInput, NInputNumber, NModal, NSwitch, NText, type DropdownOption } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
import { ref, watch } from 'vue';
const show = defineModel<boolean>('show', { default: false });
@@ -40,6 +37,83 @@ const { mutate: getVersionInfo } = useMutation({
},
});
const abortControllers = ref({
retentionDays: new AbortController(),
snapStatus: new AbortController(),
});
const retentionDays = ref(0);
const { mutate: getRetentionDays, isPending: retentionDaysLoading } = useMutation({
mutationFn: async () => {
abortControllers.value.retentionDays.abort();
abortControllers.value.retentionDays = new AbortController();
const signal = abortControllers.value.retentionDays.signal;
const days = await retentionDaysApi('get', { signal });
return days;
},
onSuccess: (days) => {
retentionDays.value = days;
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const { mutate: saveRetentionDays, isPending: retentionDaysSaving } = useMutation({
mutationFn: async () => {
abortControllers.value.retentionDays.abort();
abortControllers.value.retentionDays = new AbortController();
const signal = abortControllers.value.retentionDays.signal;
await retentionDaysApi('post', { days: retentionDays.value, signal });
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
// 修改失败,刷新 retentionDays
getRetentionDays();
},
});
const snapStatus = ref(false);
const { mutate: getSnapStatus, isPending: snapStatusLoading } = useMutation({
mutationFn: async () => {
abortControllers.value.snapStatus.abort();
abortControllers.value.snapStatus = new AbortController();
const signal = abortControllers.value.snapStatus.signal;
const status = await snapStatusApi('get', { signal });
return status;
},
onSuccess: (status) => {
snapStatus.value = status;
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const { mutate: saveSnapStatus, isPending: snapStatusSaving } = useMutation({
mutationFn: async () => {
abortControllers.value.snapStatus.abort();
abortControllers.value.snapStatus = new AbortController();
const signal = abortControllers.value.snapStatus.signal;
await snapStatusApi('post', { doSnap: snapStatus.value, signal });
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
// 修改失败,刷新 snapStatus
getSnapStatus();
},
});
const showDebugCodeModal = ref(false);
const debugCode = ref('');
const enableDebugMode = () => {
@@ -98,6 +172,8 @@ const exportFromIndexedDB = async <K extends IndexedDbStoreId>(storeId: K, optio
};
const importToIndexedDB = async <K extends IndexedDbStoreId>(storeId: K, options?: { successMsg?: string; errorMsg?: string }) => {
const { successMsg, errorMsg } = options ?? {};
pollingStore.stopPolling();
offlineDev.value = true;
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.json';
@@ -113,18 +189,20 @@ const importToIndexedDB = async <K extends IndexedDbStoreId>(storeId: K, options
reader.onload = async () => {
const data = destr<IndexedDbStoreStates[K]>(reader.result as string);
await localforage.setItem(storeId, data);
window.$message.success(successMsg ?? '导入数据成功');
window.$message.success(`${successMsg ?? '导入数据成功'},即将刷新页面`);
await sleep(2000);
window.location.reload();
};
};
};
const deleteFromIndexedDB = async (storeId: IndexedDbStoreId) => {
pollingStore.stopPolling();
offlineDev.value = true;
await localforage.removeItem(storeId).catch((error) => {
window.$message.error(`${error}`);
return;
});
window.$message.success('删除成功');
window.$message.success('删除成功,即将刷新页面');
await sleep(2000);
window.location.reload();
};
@@ -188,13 +266,28 @@ const onSelectDropdownOption = (key: string, option: DropdownOption) => {
}
};
onMounted(() => {
getVersionInfo();
watch([offlineDev, show], ([offline, entered]) => {
if (!offline) {
if (entered) {
getRetentionDays();
getSnapStatus();
} else {
abortControllers.value.retentionDays.abort();
abortControllers.value.snapStatus.abort();
}
}
});
const onDrawerAfterEnter = () => {
getVersionInfo();
};
const onDrawerAfterLeave = () => {
abortControllers.value.retentionDays.abort();
abortControllers.value.snapStatus.abort();
};
</script>
<template>
<NDrawer v-model:show="show" :width="560" :auto-focus="false">
<NDrawer v-model:show="show" :width="560" :auto-focus="false" @after-enter="onDrawerAfterEnter" @after-leave="onDrawerAfterLeave">
<NDrawerContent closable title="系统设置" :native-scrollbar="false">
<NFlex vertical>
<NDivider>主题</NDivider>
@@ -206,11 +299,29 @@ onMounted(() => {
<NFormItem label="折叠菜单" label-placement="left">
<NSwitch size="small" v-model:value="menuCollpased" />
</NFormItem>
<template v-if="route.path === '/station'">
<NFormItem label="车站列数" label-placement="left">
<NInputNumber v-model:value="stationGridCols" :min="1" :max="10" />
</NFormItem>
</template>
<NFormItem label="车站列数" label-placement="left">
<NInputNumber v-model:value="stationGridCols" :min="1" :max="10" />
</NFormItem>
<NDivider>告警</NDivider>
<NFormItem label="告警画面截图保留天数" label-placement="left">
<NFlex justify="space-between" align="center" style="width: 100%">
<NInputNumber v-model:value="retentionDays" :min="1" :max="15" />
<NButtonGroup>
<NButton secondary size="small" :disabled="retentionDaysSaving" :loading="retentionDaysLoading" @click="() => getRetentionDays()">刷新</NButton>
<NButton secondary size="small" :disabled="retentionDaysLoading" :loading="retentionDaysSaving" @click="() => saveRetentionDays()">保存</NButton>
</NButtonGroup>
</NFlex>
</NFormItem>
<NFormItem label="自动获取告警画面截图" label-placement="left">
<NFlex justify="space-between" align="center" style="width: 100%">
<NSwitch size="small" v-model:value="snapStatus" />
<NButtonGroup>
<NButton secondary size="small" :disabled="snapStatusSaving" :loading="snapStatusLoading" @click="() => getSnapStatus()">刷新</NButton>
<NButton secondary size="small" :disabled="snapStatusLoading" :loading="snapStatusSaving" @click="() => saveSnapStatus()">保存</NButton>
</NButtonGroup>
</NFlex>
</NFormItem>
<template v-if="debugModeEnabled">
<NDivider title-placement="center">调试</NDivider>

View File

@@ -2,12 +2,13 @@
import type { NdmDeviceAlarmLogResultVO, Station } from '@/apis';
import { ALARM_TYPES, DEVICE_TYPE_LITERALS, DEVICE_TYPE_NAMES, FAULT_LEVELS, tryGetDeviceType } from '@/enums';
import { renderAlarmDateCell, renderAlarmTypeCell, renderDeviceTypeCell, renderFaultLevelCell } from '@/helpers';
import { useAlarmStore } from '@/stores';
import { useAlarmStore, useDeviceStore } from '@/stores';
import { downloadByData } from '@/utils';
import dayjs from 'dayjs';
import { NButton, NDataTable, NFlex, NGrid, NGridItem, NModal, NStatistic, NTag, type DataTableBaseColumn, type DataTableRowData, type PaginationProps } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, h, reactive, ref, toRefs } from 'vue';
import { computed, h, reactive, ref, toRefs, type CSSProperties } from 'vue';
import { useRoute, useRouter } from 'vue-router';
const props = defineProps<{
station?: Station;
@@ -15,8 +16,13 @@ const props = defineProps<{
const show = defineModel<boolean>('show', { default: false });
const route = useRoute();
const router = useRouter();
const { station } = toRefs(props);
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
const alarmStore = useAlarmStore();
const { lineAlarms } = storeToRefs(alarmStore);
@@ -37,7 +43,40 @@ const tableColumns = ref<DataTableBaseColumn<NdmDeviceAlarmLogResultVO>[]>([
{ title: '告警流水号', key: 'alarmNo' },
{ title: '告警时间', key: 'alarmDate', render: renderAlarmDateCell },
{ title: '设备类型', key: 'deviceType', render: renderDeviceTypeCell },
{ title: '设备名称', key: 'deviceName' },
{
title: '设备名称',
key: 'deviceName',
render: (rowData) => {
return h(
'div',
{
style: { textDecoration: 'underline', cursor: 'pointer' } as CSSProperties,
onClick: () => {
const stationCode = rowData.stationCode;
if (!stationCode) return;
const deviceType = tryGetDeviceType(rowData.deviceType);
if (!deviceType) return;
const stationDevices = lineDevices.value[stationCode];
if (!stationDevices) return;
const classified = stationDevices[deviceType];
const device = classified.find((device) => !!device.deviceId && device.deviceId === rowData.deviceId);
if (!device) return;
const deviceDbId = device.id;
router.push({
path: '/device',
query: {
stationCode,
deviceType,
deviceDbId,
fromPage: route.path,
},
});
},
},
`${rowData.deviceName}`,
);
},
},
{ title: '告警类型', key: 'alarmType', align: 'center', render: renderAlarmTypeCell },
{ title: '故障级别', key: 'faultLevel', align: 'center', render: renderFaultLevelCell },
// { title: '故障编码', key: 'faultCode', align: 'center' },

View File

@@ -1,8 +1,10 @@
<script setup lang="ts">
import type { Station } from '@/apis';
import { DeviceTree } from '@/components';
import { DeviceTree, type DeviceTreeProps } from '@/components';
import { tryGetDeviceType } from '@/enums';
import { NModal } from 'naive-ui';
import { toRefs } from 'vue';
import { useRoute, useRouter } from 'vue-router';
const props = defineProps<{
station?: Station;
@@ -10,13 +12,30 @@ const props = defineProps<{
const show = defineModel<boolean>('show', { default: false });
const route = useRoute();
const router = useRouter();
const { station } = toRefs(props);
const onAfterSelectDevice: DeviceTreeProps['onAfterSelectDevice'] = (device, stationCode) => {
const deviceDbId = device.id;
const deviceType = tryGetDeviceType(device.deviceType);
router.push({
path: '/device',
query: {
stationCode,
deviceType,
deviceDbId,
fromPage: route.path,
},
});
};
</script>
<template>
<NModal v-model:show="show" preset="card" style="width: 600px; height: 600px" :title="`${station?.name} - 设备详情`" :content-style="{ height: '100%', overflow: 'hidden' }">
<template #default>
<DeviceTree :station="station" />
<DeviceTree :station="station" :events="['select', 'manage']" :device-prefix-label="'查看'" @after-select-device="onAfterSelectDevice" />
</template>
</NModal>
</template>

View File

@@ -3,6 +3,7 @@ import type { Station, StationAlarms, StationDevices } from '@/apis';
import { DEVICE_TYPE_LITERALS } from '@/enums';
import { EllipsisOutlined, MoreOutlined } from '@vicons/antd';
import axios from 'axios';
import dayjs from 'dayjs';
import { isFunction } from 'es-toolkit';
import { NButton, NCard, NCheckbox, NDropdown, NFlex, NIcon, NTag, NTooltip, useThemeVars, type DropdownOption } from 'naive-ui';
import { computed, toRefs } from 'vue';
@@ -49,7 +50,7 @@ const alarmCount = computed(() => {
const openVideoPlatform = async () => {
try {
const response = await axios.get<Record<string, string>>('/minio/ndm/ndm-vimps.json');
const response = await axios.get<Record<string, string>>(`/minio/ndm/ndm-vimps.json?_t=${dayjs().unix()}`);
const url = response.data[station.value.code];
if (!url) {
window.$message.warning(`未找到车站编码 ${station.value.code} 对应的视频平台URL`);
@@ -135,9 +136,9 @@ const onSelectDropdownOption = (key: string, option: DropdownOption) => {
<NFlex justify="flex-end" align="center" :size="2">
<div>
<span :style="{ color: onlineDeviceCount > 0 ? themeVars.successColor : '' }">在线{{ onlineDeviceCount }}</span>
<span :style="{ color: onlineDeviceCount > 0 ? themeVars.successColor : '' }">在线 {{ onlineDeviceCount }} </span>
<span> · </span>
<span :style="{ color: offlineDeviceCount > 0 ? themeVars.errorColor : '' }">离线 {{ offlineDeviceCount }}</span>
<span :style="{ color: offlineDeviceCount > 0 ? themeVars.errorColor : '' }">离线 {{ offlineDeviceCount }} </span>
</div>
<!-- 占位按钮对齐布局 -->
<NButton quaternary size="tiny" :focusable="false" style="visibility: hidden">

View File

@@ -1,4 +1,4 @@
import { getCameraSnapApi, type NdmDeviceAlarmLogResultVO } from '@/apis';
import { detailDeviceAlarmSnapLogApi, type NdmDeviceAlarmLogResultVO } from '@/apis';
import { tryGetDeviceType, DEVICE_TYPE_LITERALS } from '@/enums';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
@@ -7,11 +7,9 @@ import { h, ref, watch, type Ref } from 'vue';
export const useCameraSnapColumn = (tableData: Ref<DataTableRowData[]>) => {
const { mutateAsync: getSnapByDeviceId } = useMutation({
mutationFn: async (params: { deviceAlarmLog: NdmDeviceAlarmLogResultVO }) => {
const { deviceAlarmLog } = params;
const { deviceId } = deviceAlarmLog;
if (!deviceId) throw new Error('设备ID不能为空');
const snap = await getCameraSnapApi(deviceId);
mutationFn: async (params: { id: string }) => {
const { id } = params;
const snap = await detailDeviceAlarmSnapLogApi(id);
return snap;
},
onError: (error) => {
@@ -28,15 +26,15 @@ export const useCameraSnapColumn = (tableData: Ref<DataTableRowData[]>) => {
});
const cameraSnapColumn: DataTableColumn<NdmDeviceAlarmLogResultVO & { snapUrl?: string }> = {
title: '实时画面截图',
title: '告警画面截图',
key: 'snapUrl',
align: 'center',
render: (rowData) => {
const { deviceType: deviceTypeCode, snapUrl } = rowData;
const { id, deviceType: deviceTypeCode, snapUrl } = rowData;
if (!id) return null;
const deviceType = tryGetDeviceType(deviceTypeCode);
if (deviceType !== DEVICE_TYPE_LITERALS.ndmCamera) return null;
if (!snapUrl) {
const id = rowData.id ?? '';
return h(
NButton,
{
@@ -46,7 +44,8 @@ export const useCameraSnapColumn = (tableData: Ref<DataTableRowData[]>) => {
onClick: async () => {
loadingMap.value[id] = true;
try {
const snap = await getSnapByDeviceId({ deviceAlarmLog: rowData });
const snap = await getSnapByDeviceId({ id });
if (!snap.url) return;
rowData.snapUrl = snap.url;
} finally {
loadingMap.value[id] = false;
@@ -60,6 +59,7 @@ export const useCameraSnapColumn = (tableData: Ref<DataTableRowData[]>) => {
}
},
};
return {
cameraSnapColumn,
};

View File

@@ -1 +1,3 @@
export * from './use-device-management';
export * from './use-device-selection';
export * from './use-device-tree';

View File

@@ -0,0 +1,213 @@
import { deleteDeviceApi, exportDeviceApi, importDeviceApi, type ImportMsg, type NdmDeviceLinkDescription, type NdmDevicePageQuery, type PageParams, type Station } from '@/apis';
import { useStationDevicesMutation } from '@/composables';
import { DEVICE_TYPE_NAMES, type DeviceType } from '@/enums';
import { useDeviceStore, useStationStore } from '@/stores';
import { downloadByData, parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import dayjs from 'dayjs';
import destr from 'destr';
import { storeToRefs } from 'pinia';
import { h, onBeforeUnmount } from 'vue';
export const useDeviceManagement = () => {
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
const { mutate: refreshStationDevices } = useStationDevicesMutation();
// 导出设备
const { mutate: exportDevice } = useMutation({
mutationFn: async (params: { deviceType: DeviceType; stationCode: Station['code']; signal?: AbortSignal }) => {
const { deviceType, stationCode, signal } = params;
const deviceTypeName = DEVICE_TYPE_NAMES[deviceType];
const stationDevices = lineDevices.value[stationCode];
if (!stationDevices) throw new Error(`该车站没有${deviceTypeName}`);
const devices = stationDevices[deviceType];
const pageQuery: PageParams<NdmDevicePageQuery> = {
model: {},
extra: {},
current: 1,
size: devices.length,
sort: 'id',
order: 'descending',
};
window.$loadingBar.start();
const data = await exportDeviceApi(deviceType, pageQuery, { stationCode, signal });
return data;
},
onSuccess: (data, { deviceType, stationCode }) => {
window.$loadingBar.finish();
const time = dayjs().format('YYYY-MM-DD_HH-mm-ss');
const stationName = stations.value.find((station) => station.code === stationCode)?.name ?? '';
const deviceTypeName = DEVICE_TYPE_NAMES[deviceType];
downloadByData(data, `${stationName}_${deviceTypeName}列表_${time}.xlsx`);
},
onError: (error) => {
window.$loadingBar.error();
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
// 下载设备导入模板
// FIXME: 采用导出空列表的方案但是后端生成的xlsx中会多一行空行如果直接再导入该文件就会多导入一个空设备
const { mutate: exportDeviceTemplate } = useMutation({
mutationFn: async (params: { deviceType: DeviceType; stationCode: Station['code']; signal?: AbortSignal }) => {
const { deviceType, stationCode, signal } = params;
const pageQuery: PageParams<NdmDevicePageQuery> = {
model: {},
extra: {},
current: 1,
size: 0,
sort: 'id',
order: 'descending',
};
window.$loadingBar.start();
const data = await exportDeviceApi(deviceType, pageQuery, { stationCode, signal });
return data;
},
onSuccess: (data, { deviceType, stationCode }) => {
window.$loadingBar.finish();
const time = dayjs().format('YYYY-MM-DD_HH-mm-ss');
const stationName = stations.value.find((station) => station.code === stationCode)?.name ?? '';
const deviceTypeName = DEVICE_TYPE_NAMES[deviceType];
downloadByData(data, `${stationName}_${deviceTypeName}导入模板_${time}.xlsx`);
},
onError: (error) => {
window.$loadingBar.error();
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
// 导入设备
const { mutate: importDevice } = useMutation({
mutationFn: async (params: { deviceType: DeviceType; stationCode: Station['code']; signal?: AbortSignal }) => {
const { deviceType, stationCode, signal } = params;
const data = await new Promise<ImportMsg>((resolve) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.xlsx';
fileInput.click();
fileInput.onchange = async () => {
const file = fileInput.files?.[0];
// console.log(file);
if (!file) {
window.$message.error('导入失败');
return;
}
window.$loadingBar.start();
const data = await importDeviceApi(deviceType, file, { stationCode, signal });
resolve(data);
};
});
return data;
},
onSuccess: (data, { stationCode, signal }) => {
window.$loadingBar.finish();
window.$dialog.success({
title: '导入成功',
content: () => {
return h('div', {}, [
h('p', {}, `新增数据:${data.insertNum}`),
h('p', {}, `更新数据:${data.updateNum}`),
h('p', {}, `不变数据:${data.unchangedNum}`),
h('p', {}, `错误数据:${data.wrongNum}`),
data.wrongLines.map((line) => h('p', { style: { 'margin-left': '8px' } }, `${line.rowNum}行:${line.msg}`)),
]);
},
});
const station = stations.value.find((station) => station.code === stationCode);
if (station) {
refreshStationDevices({ station, signal });
}
},
onError: (error) => {
window.$loadingBar.error();
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
// 删除设备
const { mutate: deleteDevice } = useMutation({
mutationFn: async (params: { id: string; deviceType: DeviceType; stationCode: Station['code']; signal?: AbortSignal }) => {
const { id, deviceType, stationCode, signal } = params;
window.$loadingBar.start();
// 检查要删除的设备是否存在关联设备
const stationDevices = lineDevices.value[stationCode];
if (!!stationDevices) {
const classified = stationDevices[deviceType];
if (!!classified) {
const device = classified.find((device) => device.id === id);
if (!!device) {
const maybeLinkDescription = destr<any>(device.linkDescription);
if (!!maybeLinkDescription && typeof maybeLinkDescription === 'object') {
const linkDescription = maybeLinkDescription as NdmDeviceLinkDescription;
// 只要有上游或下游设备,就不能删除
const { upstream } = linkDescription;
if (!!upstream && upstream.length > 0) {
throw new Error('该设备存在关联的上游设备,无法删除');
}
if ('downstream' in linkDescription) {
const { downstream } = linkDescription;
if (!!downstream && Object.keys(downstream).length > 0) {
throw new Error('该设备存在关联的下游设备,无法删除');
}
}
}
}
}
}
return await deleteDeviceApi(deviceType, id, { stationCode, signal });
},
onSuccess: (_, { stationCode, signal }) => {
window.$loadingBar.finish();
window.$message.success('删除成功');
const station = stations.value.find((station) => station.code === stationCode);
if (station) {
refreshStationDevices({ station, signal });
}
},
onError: (error) => {
window.$loadingBar.error();
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
onBeforeUnmount(() => {
window.$loadingBar.finish();
});
return {
exportDevice,
exportDeviceTemplate,
importDevice,
deleteDevice,
};
};
export type UseDeviceManagementReturn = ReturnType<typeof useDeviceManagement>;

View File

@@ -0,0 +1,109 @@
import type { LineDevices, NdmDeviceResultVO, Station } from '@/apis';
import { tryGetDeviceType, type DeviceType } from '@/enums';
import { useDeviceStore } from '@/stores';
import { watchDebounced } from '@vueuse/core';
import { storeToRefs } from 'pinia';
import { onMounted, ref, toValue, watch, type MaybeRefOrGetter } from 'vue';
import { useRoute, useRouter } from 'vue-router';
export const useDeviceSelection = (options?: { syncRoute?: MaybeRefOrGetter<boolean> }) => {
const { syncRoute } = options ?? {};
const route = useRoute();
const router = useRouter();
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
const selectedStationCode = ref<Station['code']>();
const selectedDeviceType = ref<DeviceType>();
const selectedDevice = ref<NdmDeviceResultVO>();
const initFromRoute = (lineDevices: LineDevices) => {
const { stationCode, deviceType, deviceDbId } = route.query;
if (stationCode) {
selectedStationCode.value = stationCode as Station['code'];
}
if (deviceType) {
selectedDeviceType.value = deviceType as DeviceType;
}
if (deviceDbId && selectedStationCode.value && selectedDeviceType.value) {
const selectedDeviceDbId = deviceDbId as string;
const stationDevices = lineDevices[selectedStationCode.value];
if (stationDevices) {
const devices = stationDevices[selectedDeviceType.value];
if (devices) {
const device = devices.find((device) => device.id === selectedDeviceDbId);
if (device) {
selectedDevice.value = device;
}
}
}
}
};
const selectDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
selectedDevice.value = device;
selectedStationCode.value = stationCode;
const deviceType = tryGetDeviceType(device.deviceType);
if (deviceType) {
selectedDeviceType.value = deviceType;
}
};
const syncToRoute = () => {
const query = { ...route.query };
// 当选中的设备发生变化时删除fromPage参数
if (selectedDevice.value?.id && route.query.deviceDbId !== selectedDevice.value.id) {
delete query['fromPage'];
}
if (selectedStationCode.value) {
query['stationCode'] = selectedStationCode.value;
}
if (selectedDeviceType.value) {
query['deviceType'] = selectedDeviceType.value;
}
if (selectedDevice.value?.id) {
query['deviceDbId'] = selectedDevice.value.id;
}
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 {
selectedStationCode,
selectedDeviceType,
selectedDevice,
initFromRoute,
selectDevice,
};
};
export type UseDeviceSelectionReturn = ReturnType<typeof useDeviceSelection>;

View File

@@ -1,89 +1,17 @@
import type { LineDevices, NdmDeviceResultVO } from '@/apis';
import { tryGetDeviceType, type DeviceType } from '@/enums';
import { ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import type { MaybeRefOrGetter } from 'vue';
import { useDeviceManagement } from './use-device-management';
import { useDeviceSelection } from './use-device-selection';
export const useDeviceTree = () => {
const route = useRoute();
const router = useRouter();
export const useDeviceTree = (options?: { syncRoute?: MaybeRefOrGetter<boolean> }) => {
const { syncRoute } = options ?? {};
const selectedStationCode = ref<string>();
const selectedDeviceType = ref<DeviceType>();
const selectedDevice = ref<NdmDeviceResultVO>();
const initFromRoute = (lineDevices: LineDevices) => {
const { stationCode, deviceType, deviceDbId } = route.query;
if (stationCode) {
selectedStationCode.value = stationCode as string;
}
if (deviceType) {
selectedDeviceType.value = deviceType as DeviceType;
}
if (deviceDbId && selectedStationCode.value && selectedDeviceType.value) {
const selectedDeviceDbId = deviceDbId as string;
const stationDevices = lineDevices[selectedStationCode.value];
if (stationDevices) {
const devices = stationDevices[selectedDeviceType.value];
if (devices) {
const device = devices.find((device) => device.id === selectedDeviceDbId);
if (device) {
selectedDevice.value = device;
}
}
}
}
};
const selectDevice = (device: NdmDeviceResultVO, stationCode: string) => {
selectedDevice.value = device;
selectedStationCode.value = stationCode;
const deviceType = tryGetDeviceType(device.deviceType);
if (deviceType) {
selectedDeviceType.value = deviceType;
}
};
const routeDevice = (device: NdmDeviceResultVO, stationCode: string, to: { path: string }) => {
const deviceDbId = device.id;
const deviceType = tryGetDeviceType(device.deviceType);
router.push({
path: to.path,
query: {
stationCode,
deviceType,
deviceDbId,
from: route.path,
},
});
};
const syncToRoute = () => {
const query = { ...route.query };
// 当选中的设备发生变化时删除from参数
if (selectedDevice.value?.id && route.query.deviceDbId !== selectedDevice.value.id) {
delete query['from'];
}
if (selectedStationCode.value) {
query['stationCode'] = selectedStationCode.value;
}
if (selectedDeviceType.value) {
query['deviceType'] = selectedDeviceType.value;
}
if (selectedDevice.value?.id) {
query['deviceDbId'] = selectedDevice.value.id;
}
router.replace({ query });
};
watch(selectedDevice, syncToRoute);
const deviceSelection = useDeviceSelection({ syncRoute });
const deviceManagement = useDeviceManagement();
return {
selectedStationCode,
selectedDeviceType,
selectedDevice,
initFromRoute,
selectDevice,
routeDevice,
...deviceSelection,
...deviceManagement,
};
};
export type UseDeviceTreeReturn = ReturnType<typeof useDeviceTree>;

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