Compare commits

..

10 Commits

Author SHA1 Message Date
yangsy
cbb51aa501 refactor(security-box-circuit-card): probe after turnStatus 2025-11-19 17:34:11 +08:00
yangsy
4ab33d2021 feat(device-header-card): probe button 2025-11-19 16:31:54 +08:00
yangsy
fa9b435a0f feat(call-log-page): add searchFields 2025-11-19 14:37:18 +08:00
yangsy
dfbdc6d828 fix(version-check): clear localStorage if need 2025-11-19 13:54:41 +08:00
yangsy
5fa668acd3 fix(version-check): check version and buildTime 2025-11-19 12:17:06 +08:00
yangsy
2f06054e66 refactor(stores): rename 2025-11-19 11:59:31 +08:00
yangsy
df3888d8b5 feat(components): station-card 2025-11-19 11:55:51 +08:00
yangsy
91a665695e feat(pages): separate station-page from dashboard-page 2025-11-19 11:55:13 +08:00
yangsy
0c3e9c24f9 feat(pages): call-log-page 2025-11-18 20:22:26 +08:00
yangsy
799a5af857 fix(vimp-log-page): resetSearchFields 2025-11-18 17:38:13 +08:00
35 changed files with 878 additions and 251 deletions

3
.env
View File

@@ -15,5 +15,8 @@ VITE_LAMP_PASSWORD = fjoc(1KHP(Ls&Bje)C
# 如果 Authorization 已存在则会直接采用, 否则会根据 clientId 和 clientSecret 生成 # 如果 Authorization 已存在则会直接采用, 否则会根据 clientId 和 clientSecret 生成
VITE_LAMP_AUTHORIZATION = Y3VlZGVzX2FkbWluOmN1ZWRlc19hZG1pbl9zZWNyZXQ= VITE_LAMP_AUTHORIZATION = Y3VlZGVzX2FkbWluOmN1ZWRlc19hZG1pbl9zZWNyZXQ=
# 当需要重置localStorage时, 修改此变量
VITE_STORAGE_VERSION = 1
# 调试授权码 # 调试授权码
VITE_DEBUG_CODE = ndm_debug VITE_DEBUG_CODE = ndm_debug

View File

@@ -7,6 +7,7 @@ import type { NdmKeyboardVO } from './video/ndm-keyboard';
import type { NdmMediaServerVO } from './video/ndm-media-server'; import type { NdmMediaServerVO } from './video/ndm-media-server';
import type { NdmVideoServerVO } from './video/ndm-video-server'; import type { NdmVideoServerVO } from './video/ndm-video-server';
export * from './log/ndm-call-log';
export * from './log/ndm-device-alarm-log'; export * from './log/ndm-device-alarm-log';
export * from './log/ndm-icmp-log'; export * from './log/ndm-icmp-log';
export * from './log/ndm-snmp-log'; export * from './log/ndm-snmp-log';

View File

@@ -0,0 +1,17 @@
import type { BaseModel, ReduceForPageQuery, ReduceForSaveVO, ReduceForUpdateVO } from '../../base';
export interface NdmCallLogVO extends BaseModel {
sourceGbId: string;
targetGbId: string;
method: string;
messageType: string;
cmdType: string;
}
export type NdmCallLogResultVO = Partial<NdmCallLogVO>;
export type NdmCallLogSaveVO = Partial<Omit<NdmCallLogVO, ReduceForSaveVO>>;
export type NdmCallLogUpdateVO = Partial<Omit<NdmCallLogVO, ReduceForUpdateVO>>;
export type NdmCallLogPageQuery = Partial<Omit<NdmCallLogVO, ReduceForPageQuery>>;

View File

@@ -1,5 +1,6 @@
export * from './export/ndm-icmp-export'; export * from './export/ndm-icmp-export';
export * from './log/ndm-call-log';
export * from './log/ndm-device-alarm-log'; export * from './log/ndm-device-alarm-log';
export * from './log/ndm-icmp-log'; export * from './log/ndm-icmp-log';
export * from './log/ndm-snmp-log'; export * from './log/ndm-snmp-log';
@@ -15,3 +16,5 @@ export * from './video/ndm-decoder';
export * from './video/ndm-keyboard'; export * from './video/ndm-keyboard';
export * from './video/ndm-media-server'; export * from './video/ndm-media-server';
export * from './video/ndm-video-server'; export * from './video/ndm-video-server';
export * from './ndm-probe';

View File

@@ -0,0 +1,25 @@
import { ndmClient } from '@/apis/client';
import type { NdmCallLogPageQuery, NdmCallLogResultVO, PageParams, PageResult } from '@/apis/models';
export const postNdmCallLogPage = async (stationCode: string, pageQuery: PageParams<NdmCallLogPageQuery>) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.post<PageResult<NdmCallLogResultVO>>(`${prefix}/api/ndm/ndmCallLog/page`, pageQuery);
const [err, callLogData] = resp;
if (err || !callLogData) {
throw err;
}
return callLogData;
};
export const ndmCallLogDefaultExportByTemplate = async (stationCode: string, pageQuery: PageParams<NdmCallLogPageQuery>) => {
const endpoint = '/api/ndm/ndmCallLog/defaultExportByTemplate';
if (!stationCode) {
throw new Error('请选择车站');
}
const resp = await ndmClient.post<Blob>(`/${stationCode}${endpoint}`, pageQuery, { responseType: 'blob', retRaw: true });
const [err, data] = resp;
if (err || !data) {
throw err;
}
return data;
};

View File

@@ -0,0 +1,121 @@
import { ndmClient } from '@/apis/client';
import type { NdmDeviceResultVO } from '@/apis/models';
import { getNdmDecoderDetail, getNdmKeyboardDetail, getNdmMediaServerDetail, getNdmNvrDetail, getNdmSecurityBoxDetail, getNdmSwitchDetail, getNdmVideoServerDetail } from '@/apis/requests';
import { DeviceType, tryGetDeviceTypeVal } from '@/enums/device-type';
export const probeNdmDecoderByIds = async (stationCode: string, ids: string[]) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.post<void>(`${prefix}/api/ndm/ndmDecoder/probeByIds`, ids);
const [err] = resp;
if (err) {
throw err;
}
};
export const probeNdmNvrByIds = async (stationCode: string, ids: string[]) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.post<void>(`${prefix}/api/ndm/ndmNvr/probeByIds`, ids);
const [err] = resp;
if (err) {
throw err;
}
};
export const probeNdmSecurityBoxByIds = async (stationCode: string, ids: string[]) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.post<void>(`${prefix}/api/ndm/ndmSecurityBox/probeByIds`, ids);
const [err] = resp;
if (err) {
throw err;
}
};
export const probeNdmMediaServerByIds = async (stationCode: string, ids: string[]) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.post<void>(`${prefix}/api/ndm/ndmMediaServer/probeByIds`, ids);
const [err] = resp;
if (err) {
throw err;
}
};
export const probeNdmSwitchByIds = async (stationCode: string, ids: string[]) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.post<void>(`${prefix}/api/ndm/ndmSwitch/probeByIds`, ids);
const [err] = resp;
if (err) {
throw err;
}
};
export const probeNdmVideoServerByIds = async (stationCode: string, ids: string[]) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.post<void>(`${prefix}/api/ndm/ndmVideoServer/probeByIds`, ids);
const [err] = resp;
if (err) {
throw err;
}
};
export const probeDeviceApi = async (stationCode: string, device: NdmDeviceResultVO) => {
const deviceType = tryGetDeviceTypeVal(device.deviceType);
const deviceDbId = device.id;
if (!deviceType || !deviceDbId) {
throw new Error('未知的设备');
}
if (deviceType === DeviceType.Decoder) {
await probeNdmDecoderByIds(stationCode, [deviceDbId]);
return;
}
if (deviceType === DeviceType.Nvr) {
await probeNdmNvrByIds(stationCode, [deviceDbId]);
return;
}
if (deviceType === DeviceType.SecurityBox) {
await probeNdmSecurityBoxByIds(stationCode, [deviceDbId]);
return;
}
if (deviceType === DeviceType.MediaServer) {
await probeNdmMediaServerByIds(stationCode, [deviceDbId]);
return;
}
if (deviceType === DeviceType.Switch) {
await probeNdmSwitchByIds(stationCode, [deviceDbId]);
return;
}
if (deviceType === DeviceType.VideoServer) {
await probeNdmVideoServerByIds(stationCode, [deviceDbId]);
return;
}
};
export const getDeviceDetailApi = async (stationCode: string, id?: string, deviceType?: string): Promise<NdmDeviceResultVO | undefined> => {
if (!id || !deviceType) {
throw new Error('未知的设备');
}
if (deviceType === DeviceType.Camera) {
return await getNdmVideoServerDetail(stationCode, id);
}
if (deviceType === DeviceType.Decoder) {
return await getNdmDecoderDetail(stationCode, id);
}
if (deviceType === DeviceType.Keyboard) {
return await getNdmKeyboardDetail(stationCode, id);
}
if (deviceType === DeviceType.MediaServer) {
return await getNdmMediaServerDetail(stationCode, id);
}
if (deviceType === DeviceType.Nvr) {
return await getNdmNvrDetail(stationCode, id);
}
if (deviceType === DeviceType.SecurityBox) {
return await getNdmSecurityBoxDetail(stationCode, id);
}
if (deviceType === DeviceType.Switch) {
return await getNdmSwitchDetail(stationCode, id);
}
if (deviceType === DeviceType.VideoServer) {
return await getNdmVideoServerDetail(stationCode, id);
}
return undefined;
};

View File

@@ -4,6 +4,16 @@ import type { PageParams, NdmSecurityBoxPageQuery, PageResult, NdmSecurityBoxRes
export const postNdmSecurityBoxPage = async (stationCode: string, pageQuery: PageParams<NdmSecurityBoxPageQuery>, signal?: AbortSignal) => { export const postNdmSecurityBoxPage = async (stationCode: string, pageQuery: PageParams<NdmSecurityBoxPageQuery>, signal?: AbortSignal) => {
const prefix = stationCode ? `/${stationCode}` : ''; const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.post<PageResult<NdmSecurityBoxResultVO>>(`${prefix}/api/ndm/ndmSecurityBox/page`, pageQuery, { signal }); const resp = await ndmClient.post<PageResult<NdmSecurityBoxResultVO>>(`${prefix}/api/ndm/ndmSecurityBox/page`, pageQuery, { signal });
const [err, ndmSecurityBox] = resp;
if (err || !ndmSecurityBox) {
throw err;
}
return ndmSecurityBox;
};
export const getNdmSecurityBoxDetail = async (stationCode: string, id: string) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.get<NdmSecurityBoxResultVO>(`${prefix}/api/ndm/ndmSecurityBox/detail`, { params: { id } });
const [err, ndmSecurityBoxData] = resp; const [err, ndmSecurityBoxData] = resp;
if (err || !ndmSecurityBoxData) { if (err || !ndmSecurityBoxData) {
throw err; throw err;
@@ -18,7 +28,7 @@ export const putNdmSecurityBox = async (stationCode: string, updateVO: NdmSecuri
if (err || !ndmSecurityBox) { if (err || !ndmSecurityBox) {
throw err; throw err;
} }
return ndmSecurityBox; return await getNdmSecurityBoxDetail(stationCode, ndmSecurityBox.id ?? '');
}; };
export const turnStatus = async (stationCode: string, ipAddress: string, circuitIndex: number, status: number) => { export const turnStatus = async (stationCode: string, ipAddress: string, circuitIndex: number, status: number) => {

View File

@@ -4,6 +4,16 @@ import type { PageParams, NdmSwitchPageQuery, PageResult, NdmSwitchResultVO, Ndm
export const postNdmSwitchPage = async (stationCode: string, pageQuery: PageParams<NdmSwitchPageQuery>, signal?: AbortSignal) => { export const postNdmSwitchPage = async (stationCode: string, pageQuery: PageParams<NdmSwitchPageQuery>, signal?: AbortSignal) => {
const prefix = stationCode ? `/${stationCode}` : ''; const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.post<PageResult<NdmSwitchResultVO>>(`${prefix}/api/ndm/ndmSwitch/page`, pageQuery, { signal }); const resp = await ndmClient.post<PageResult<NdmSwitchResultVO>>(`${prefix}/api/ndm/ndmSwitch/page`, pageQuery, { signal });
const [err, ndmSwitch] = resp;
if (err || !ndmSwitch) {
throw err;
}
return ndmSwitch;
};
export const getNdmSwitchDetail = async (stationCode: string, id: string) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.get<NdmSwitchResultVO>(`${prefix}/api/ndm/ndmSwitch/detail`, { params: { id } });
const [err, ndmSwitchData] = resp; const [err, ndmSwitchData] = resp;
if (err || !ndmSwitchData) { if (err || !ndmSwitchData) {
throw err; throw err;
@@ -18,5 +28,5 @@ export const putNdmSwitch = async (stationCode: string, updateVO: NdmSwitchUpdat
if (err || !ndmSwitch) { if (err || !ndmSwitch) {
throw err; throw err;
} }
return ndmSwitch; return await getNdmSwitchDetail(stationCode, ndmSwitch.id ?? '');
}; };

View File

@@ -5,11 +5,21 @@ import dayjs from 'dayjs';
export const postNdmNvrPage = async (stationCode: string, pageQuery: PageParams<NdmNvrPageQuery>, signal?: AbortSignal) => { export const postNdmNvrPage = async (stationCode: string, pageQuery: PageParams<NdmNvrPageQuery>, signal?: AbortSignal) => {
const prefix = stationCode ? `/${stationCode}` : ''; const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.post<PageResult<NdmNvrResultVO>>(`${prefix}/api/ndm/ndmNvr/page`, pageQuery, { signal }); const resp = await ndmClient.post<PageResult<NdmNvrResultVO>>(`${prefix}/api/ndm/ndmNvr/page`, pageQuery, { signal });
const [err, ndmNvrData] = resp; const [err, ndmNvr] = resp;
if (err || !ndmNvrData) { if (err || !ndmNvr) {
throw err; throw err;
} }
return ndmNvrData; return ndmNvr;
};
export const getNdmNvrDetail = async (stationCode: string, id: string) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.get<NdmNvrResultVO>(`${prefix}/api/ndm/ndmNvr/detail`, { params: { id } });
const [err, ndmNvr] = resp;
if (err || !ndmNvr) {
throw err;
}
return ndmNvr;
}; };
export const putNdmNvr = async (stationCode: string, updateVO: NdmNvrUpdateVO) => { export const putNdmNvr = async (stationCode: string, updateVO: NdmNvrUpdateVO) => {
@@ -19,7 +29,7 @@ export const putNdmNvr = async (stationCode: string, updateVO: NdmNvrUpdateVO) =
if (err || !ndmNvr) { if (err || !ndmNvr) {
throw err; throw err;
} }
return ndmNvr; return await getNdmNvrDetail(stationCode, ndmNvr.id ?? '');
}; };
export const getChannelList = async (stationCode: string, ndmNvr: NdmNvrResultVO) => { export const getChannelList = async (stationCode: string, ndmNvr: NdmNvrResultVO) => {

View File

@@ -4,11 +4,21 @@ import type { PageParams, NdmCameraPageQuery, PageResult, NdmCameraResultVO, Ndm
export const postNdmCameraPage = async (stationCode: string, pageQuery: PageParams<NdmCameraPageQuery>, signal?: AbortSignal) => { export const postNdmCameraPage = async (stationCode: string, pageQuery: PageParams<NdmCameraPageQuery>, signal?: AbortSignal) => {
const prefix = stationCode ? `/${stationCode}` : ''; const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.post<PageResult<NdmCameraResultVO>>(`${prefix}/api/ndm/ndmCamera/page`, pageQuery, { signal }); const resp = await ndmClient.post<PageResult<NdmCameraResultVO>>(`${prefix}/api/ndm/ndmCamera/page`, pageQuery, { signal });
const [err, ndmCameraData] = resp; const [err, ndmCamera] = resp;
if (err || !ndmCameraData) { if (err || !ndmCamera) {
throw err; throw err;
} }
return ndmCameraData; return ndmCamera;
};
export const getNdmCameraDetail = async (stationCode: string, id: string) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.get<NdmCameraResultVO>(`${prefix}/api/ndm/ndmCamera/detail`, { params: { id } });
const [err, ndmCamera] = resp;
if (err || !ndmCamera) {
throw err;
}
return ndmCamera;
}; };
export const putNdmCamera = async (stationCode: string, updateVO: NdmCameraUpdateVO) => { export const putNdmCamera = async (stationCode: string, updateVO: NdmCameraUpdateVO) => {
@@ -18,5 +28,5 @@ export const putNdmCamera = async (stationCode: string, updateVO: NdmCameraUpdat
if (err || !ndmCamera) { if (err || !ndmCamera) {
throw err; throw err;
} }
return ndmCamera; return await getNdmCameraDetail(stationCode, ndmCamera.id ?? '');
}; };

View File

@@ -4,11 +4,21 @@ import type { PageParams, NdmDecoderPageQuery, PageResult, NdmDecoderResultVO, N
export const postNdmDecoderPage = async (stationCode: string, pageQuery: PageParams<NdmDecoderPageQuery>, signal?: AbortSignal) => { export const postNdmDecoderPage = async (stationCode: string, pageQuery: PageParams<NdmDecoderPageQuery>, signal?: AbortSignal) => {
const prefix = stationCode ? `/${stationCode}` : ''; const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.post<PageResult<NdmDecoderResultVO>>(`${prefix}/api/ndm/ndmDecoder/page`, pageQuery, { signal }); const resp = await ndmClient.post<PageResult<NdmDecoderResultVO>>(`${prefix}/api/ndm/ndmDecoder/page`, pageQuery, { signal });
const [err, ndmDecoderData] = resp; const [err, ndmDecoder] = resp;
if (err || !ndmDecoderData) { if (err || !ndmDecoder) {
throw err; throw err;
} }
return ndmDecoderData; return ndmDecoder;
};
export const getNdmDecoderDetail = async (stationCode: string, id: string) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.get<NdmDecoderResultVO>(`${prefix}/api/ndm/ndmDecoder/detail`, { params: { id } });
const [err, ndmDecoder] = resp;
if (err || !ndmDecoder) {
throw err;
}
return ndmDecoder;
}; };
export const putNdmDecoder = async (stationCode: string, updateVO: NdmDecoderUpdateVO) => { export const putNdmDecoder = async (stationCode: string, updateVO: NdmDecoderUpdateVO) => {
@@ -18,5 +28,5 @@ export const putNdmDecoder = async (stationCode: string, updateVO: NdmDecoderUpd
if (err || !ndmDecoder) { if (err || !ndmDecoder) {
throw err; throw err;
} }
return ndmDecoder; return await getNdmDecoderDetail(stationCode, ndmDecoder.id ?? '');
}; };

View File

@@ -11,6 +11,16 @@ export const postNdmKeyboardPage = async (stationCode: string, pageQuery: PagePa
return ndmKeyboardData; return ndmKeyboardData;
}; };
export const getNdmKeyboardDetail = async (stationCode: string, id: string) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.get<NdmKeyboardResultVO>(`${prefix}/api/ndm/ndmKeyboard/detail`, { params: { id } });
const [err, ndmKeyboardData] = resp;
if (err || !ndmKeyboardData) {
throw err;
}
return ndmKeyboardData;
};
export const putNdmKeyboard = async (stationCode: string, updateVO: NdmKeyboardUpdateVO) => { export const putNdmKeyboard = async (stationCode: string, updateVO: NdmKeyboardUpdateVO) => {
const prefix = stationCode ? `/${stationCode}` : ''; const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.put<NdmKeyboardResultVO>(`${prefix}/api/ndm/ndmKeyboard`, updateVO); const resp = await ndmClient.put<NdmKeyboardResultVO>(`${prefix}/api/ndm/ndmKeyboard`, updateVO);
@@ -18,5 +28,5 @@ export const putNdmKeyboard = async (stationCode: string, updateVO: NdmKeyboardU
if (err || !ndmKeyboard) { if (err || !ndmKeyboard) {
throw err; throw err;
} }
return ndmKeyboard; return await getNdmKeyboardDetail(stationCode, ndmKeyboard.id ?? '');
}; };

View File

@@ -4,11 +4,21 @@ import type { PageParams, NdmMediaServerPageQuery, PageResult, NdmMediaServerRes
export const postNdmMediaServerPage = async (stationCode: string, pageQuery: PageParams<NdmMediaServerPageQuery>, signal?: AbortSignal) => { export const postNdmMediaServerPage = async (stationCode: string, pageQuery: PageParams<NdmMediaServerPageQuery>, signal?: AbortSignal) => {
const prefix = stationCode ? `/${stationCode}` : ''; const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.post<PageResult<NdmMediaServerResultVO>>(`${prefix}/api/ndm/ndmMediaServer/page`, pageQuery, { signal }); const resp = await ndmClient.post<PageResult<NdmMediaServerResultVO>>(`${prefix}/api/ndm/ndmMediaServer/page`, pageQuery, { signal });
const [err, ndmMediaServerData] = resp; const [err, ndmMediaServer] = resp;
if (err || !ndmMediaServerData) { if (err || !ndmMediaServer) {
throw err; throw err;
} }
return ndmMediaServerData; return ndmMediaServer;
};
export const getNdmMediaServerDetail = async (stationCode: string, id: string) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.get<NdmMediaServerResultVO>(`${prefix}/api/ndm/ndmMediaServer/detail`, { params: { id } });
const [err, ndmMediaServer] = resp;
if (err || !ndmMediaServer) {
throw err;
}
return ndmMediaServer;
}; };
export const putNdmMediaServer = async (stationCode: string, updateVO: NdmMediaServerUpdateVO) => { export const putNdmMediaServer = async (stationCode: string, updateVO: NdmMediaServerUpdateVO) => {
@@ -18,5 +28,5 @@ export const putNdmMediaServer = async (stationCode: string, updateVO: NdmMediaS
if (err || !ndmMediaServer) { if (err || !ndmMediaServer) {
throw err; throw err;
} }
return ndmMediaServer; return await getNdmMediaServerDetail(stationCode, ndmMediaServer.id ?? '');
}; };

View File

@@ -4,11 +4,21 @@ import type { PageParams, NdmVideoServerPageQuery, PageResult, NdmVideoServerRes
export const postNdmVideoServerPage = async (stationCode: string, pageQuery: PageParams<NdmVideoServerPageQuery>, signal?: AbortSignal) => { export const postNdmVideoServerPage = async (stationCode: string, pageQuery: PageParams<NdmVideoServerPageQuery>, signal?: AbortSignal) => {
const prefix = stationCode ? `/${stationCode}` : ''; const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.post<PageResult<NdmVideoServerResultVO>>(`${prefix}/api/ndm/ndmVideoServer/page`, pageQuery, { signal }); const resp = await ndmClient.post<PageResult<NdmVideoServerResultVO>>(`${prefix}/api/ndm/ndmVideoServer/page`, pageQuery, { signal });
const [err, ndmVideoServerData] = resp; const [err, ndmVideoServer] = resp;
if (err || !ndmVideoServerData) { if (err || !ndmVideoServer) {
throw err; throw err;
} }
return ndmVideoServerData; return ndmVideoServer;
};
export const getNdmVideoServerDetail = async (stationCode: string, id: string) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.get<NdmVideoServerResultVO>(`${prefix}/api/ndm/ndmVideoServer/detail`, { params: { id } });
const [err, ndmVideoServer] = resp;
if (err || !ndmVideoServer) {
throw err;
}
return ndmVideoServer;
}; };
export const putNdmVideoServer = async (stationCode: string, updateVO: NdmVideoServerUpdateVO) => { export const putNdmVideoServer = async (stationCode: string, updateVO: NdmVideoServerUpdateVO) => {
@@ -18,5 +28,5 @@ export const putNdmVideoServer = async (stationCode: string, updateVO: NdmVideoS
if (err || !ndmVideoServer) { if (err || !ndmVideoServer) {
throw err; throw err;
} }
return ndmVideoServer; return await getNdmVideoServerDetail(stationCode, ndmVideoServer.id ?? '');
}; };

View File

@@ -2,10 +2,9 @@
import type { Station } from '@/apis/domains'; import type { Station } from '@/apis/domains';
import { DeviceType } from '@/enums/device-type'; import { DeviceType } from '@/enums/device-type';
import { type StationAlarmCounts, type StationDevices } from '@/composables/query'; import { type StationAlarmCounts, type StationDevices } from '@/composables/query';
import { ControlOutlined } from '@vicons/antd'; import { MoreOutlined, EllipsisOutlined } from '@vicons/antd';
import { Video as VideoIcon } from '@vicons/carbon';
import axios from 'axios'; import axios from 'axios';
import { NCard, NTag, NButton, NIcon, useThemeVars, NSpace, NFlex, NText, NTooltip } from 'naive-ui'; import { NCard, NTag, NButton, NIcon, useThemeVars, NFlex, NText, NTooltip, NDropdown, type DropdownOption } from 'naive-ui';
import { toRefs, computed } from 'vue'; import { toRefs, computed } from 'vue';
const props = defineProps<{ const props = defineProps<{
@@ -89,102 +88,106 @@ const openVideoPlatform = async () => {
} }
}; };
const dropdownOptions: DropdownOption[] = [
{
label: '视频平台',
key: 'video-platform',
onClick: openVideoPlatform,
},
{
label: '设备配置',
key: 'device-config',
onClick: openDeviceConfigModal,
},
];
const selectDropdownOption = (key: string, option: DropdownOption) => {
if (typeof option['onClick'] === 'function') {
option['onClick']();
}
};
const theme = useThemeVars(); const theme = useThemeVars();
</script> </script>
<template> <template>
<NCard bordered hoverable size="small" class="station-card" :header-style="{ padding: `6px` }" :content-style="{ padding: `0px 6px 6px 6px` }"> <NCard bordered hoverable size="medium" class="station-card" :header-style="{ padding: `6px` }" :content-style="{ padding: `0px 6px 6px 6px` }">
<template #header> <template #header>
<NTooltip v-if="station.ip" trigger="click"> <NTooltip v-if="station.ip" trigger="click">
<template #trigger> <template #trigger>
<span class="font-smaller">{{ station.name }}</span> <span class="font-medium">{{ station.name }}</span>
</template> </template>
<span>{{ station.ip }}</span> <span>{{ station.ip }}</span>
</NTooltip> </NTooltip>
<span v-else class="font-smaller">{{ station.name }}</span> <span v-else class="font-medium">{{ station.name }}</span>
</template> </template>
<template #header-extra> <template #header-extra>
<NTag :type="station.online ? 'success' : 'error'" size="small"> <NFlex :size="4">
{{ station.online ? '在线' : '离线' }} <NTag :type="station.online ? 'success' : 'error'" size="small">
</NTag> {{ station.online ? '在线' : '离线' }}
</NTag>
<NDropdown trigger="click" :options="dropdownOptions" @select="selectDropdownOption">
<NButton quaternary size="tiny" :focusable="false">
<NIcon :component="MoreOutlined" />
</NButton>
</NDropdown>
</NFlex>
</template> </template>
<template #default> <template #default>
<NSpace vertical :size="8"> <NFlex vertical :size="6" class="metrics" :style="{ opacity: station.online ? '1' : '0.5' }">
<NFlex :justify="'flex-start'" class="actions"> <NFlex vertical :size="4" class="metric-item">
<NButton quaternary size="tiny" :focusable="false" :disabled="!station.online" @click="openVideoPlatform"> <NFlex justify="end" align="center" class="metric-line">
<NIcon> <span class="font-small">{{ deviceCount }} 台设备</span>
<VideoIcon /> <NButton quaternary size="tiny" :focusable="false" @click="openOfflineDeviceTreeModal">
</NIcon> <NIcon :component="EllipsisOutlined" />
<span class="btn-text">视频平台</span> </NButton>
</NButton>
<NButton quaternary size="tiny" :focusable="false" :disabled="!station.online" @click="openDeviceConfigModal">
<NIcon>
<ControlOutlined />
</NIcon>
<span class="btn-text">设备配置</span>
</NButton>
</NFlex>
<NFlex vertical :size="0" class="metrics" :style="{ opacity: station.online ? '1' : '0.5' }">
<NFlex justify="space-between" align="baseline" class="metric-item">
<NText depth="3" class="metric-label" :class="[station.online ? 'clickable' : '']" @click="station.online && openOfflineDeviceTreeModal()">设备统计</NText>
<span class="metric-value">
<span :style="{ color: onlineDeviceCount > 0 ? theme.successColor : '' }">{{ onlineDeviceCount }}</span>
<NText depth="3" class="slash">/</NText>
<span :style="{ color: offlineDeviceCount > 0 ? theme.errorColor : '' }">{{ offlineDeviceCount }}</span>
<NText depth="3" class="slash">/</NText>
<span>{{ deviceCount }}</span>
<NText depth="3" class="unit"></NText>
</span>
</NFlex> </NFlex>
<NFlex justify="end" align="center" class="metric-line">
<NFlex justify="space-between" align="baseline" class="metric-item"> <span class="font-small">
<NText depth="3" class="metric-label" :class="[station.online ? 'clickable' : '']" @click="station.online && openDeviceAlarmTreeModal()">告警记录</NText> <span :style="{ color: onlineDeviceCount > 0 ? theme.successColor : '' }">在线 {{ onlineDeviceCount }} </span>
<span class="metric-value"> <NText depth="3" class="sep">·</NText>
<span>{{ alarmCount }}</span> <span :style="{ color: offlineDeviceCount > 0 ? theme.errorColor : '' }">离线 {{ offlineDeviceCount }} </span>
<NText depth="3" class="unit"></NText>
</span> </span>
<NButton quaternary size="tiny" :focusable="false" style="visibility: hidden">
<NIcon :component="EllipsisOutlined" />
</NButton>
</NFlex> </NFlex>
</NFlex> </NFlex>
</NSpace>
<NFlex justify="end" align="center" class="metric-item">
<NFlex align="center" :size="8">
<span class="font-small" :style="{ color: alarmCount > 0 ? theme.warningColor : '' }">今日 {{ alarmCount }} 条告警</span>
<NButton quaternary size="tiny" :focusable="false" @click="openDeviceAlarmTreeModal">
<NIcon :component="EllipsisOutlined" />
</NButton>
</NFlex>
</NFlex>
</NFlex>
</template> </template>
</NCard> </NCard>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.clickable { .font-medium {
text-decoration: underline dashed; font-size: medium;
cursor: pointer;
transition: color 0.2s ease;
&:hover {
color: v-bind('theme.iconColorHover');
}
} }
.font-smaller { .font-small {
font-size: smaller; font-size: small;
} }
.btn-text { .metrics {
margin-left: 6px; padding-top: 4px;
}
.sep {
margin: 0 6px;
font-size: xx-small; font-size: xx-small;
color: v-bind('theme.textColor3'); color: v-bind('theme.textColor3');
} }
.metric-label { .metric-line .font-small {
font-size: xx-small; white-space: nowrap;
}
.metric-value {
font-size: small;
}
.unit,
.slash {
margin-left: 4px;
font-size: xx-small;
} }
</style> </style>

View File

@@ -68,7 +68,7 @@ const selectedTab = ref('设备状态');
<NTabs v-model:value="selectedTab" size="small"> <NTabs v-model:value="selectedTab" size="small">
<NTabPane name="设备状态" tab="设备状态"> <NTabPane name="设备状态" tab="设备状态">
<NFlex vertical> <NFlex vertical>
<DeviceHeaderCard :device="ndmCamera" /> <DeviceHeaderCard :station-code="stationCode" :device="ndmCamera" />
<DeviceCommonCard :common-info="commonInfo" /> <DeviceCommonCard :common-info="commonInfo" />
</NFlex> </NFlex>
</NTabPane> </NTabPane>

View File

@@ -1,16 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NdmDeviceResultVO } from '@/apis/models'; import type { NdmDeviceResultVO } from '@/apis/models';
import { getDeviceDetailApi, probeDeviceApi } from '@/apis/requests';
import { DeviceType, DeviceTypeName, tryGetDeviceTypeVal, type DeviceTypeVal } from '@/enums/device-type'; import { DeviceType, DeviceTypeName, tryGetDeviceTypeVal, type DeviceTypeVal } from '@/enums/device-type';
import { NButton, NCard, NFlex, NTag } from 'naive-ui'; import { useLineDevicesStore } from '@/stores/line-devices';
import { useMutation } from '@tanstack/vue-query';
import { ApiOutlined, ReloadOutlined } from '@vicons/antd';
import { NButton, NCard, NFlex, NIcon, NTag, NTooltip } from 'naive-ui';
import { computed, toRefs } from 'vue'; import { computed, toRefs } from 'vue';
const props = defineProps<{ const props = defineProps<{
stationCode: string;
device: NdmDeviceResultVO; device: NdmDeviceResultVO;
}>(); }>();
// const emit = defineEmits<{}>(); // const emit = defineEmits<{}>();
const { device } = toRefs(props); const { stationCode, device } = toRefs(props);
const type = computed(() => { const type = computed(() => {
const deviceTypeVal = tryGetDeviceTypeVal(device.value.deviceType); const deviceTypeVal = tryGetDeviceTypeVal(device.value.deviceType);
@@ -39,6 +44,34 @@ const onClickOpenMgmtPage = () => {
} }
} }
}; };
const canProbe = computed(() => device.value.snmpEnabled);
const { mutate: probeDevice, isPending: probing } = useMutation({
mutationFn: async () => {
await probeDeviceApi(stationCode.value, device.value);
},
onError: (error) => {
console.error(error);
window.$message.error(error.message);
},
});
const { mutate: getDeviceDetail, isPending: loading } = useMutation({
mutationFn: async () => {
const { id, deviceType } = device.value;
return await getDeviceDetailApi(stationCode.value, id, tryGetDeviceTypeVal(deviceType));
},
onSuccess: (device) => {
if (device) {
useLineDevicesStore().patch(stationCode.value, device);
}
},
onError: (error) => {
console.error(error);
window.$message.error(error.message);
},
});
</script> </script>
<template> <template>
@@ -51,6 +84,8 @@ const onClickOpenMgmtPage = () => {
<div>{{ name }}</div> <div>{{ name }}</div>
<NButton v-if="canOpenMgmtPage" ghost size="tiny" type="default" :focusable="false" @click="onClickOpenMgmtPage">管理</NButton> <NButton v-if="canOpenMgmtPage" ghost size="tiny" type="default" :focusable="false" @click="onClickOpenMgmtPage">管理</NButton>
</NFlex> </NFlex>
</template>
<template #default>
<div style="font-size: small; color: #666"> <div style="font-size: small; color: #666">
<div> <div>
<span>设备类型</span> <span>设备类型</span>
@@ -74,6 +109,32 @@ const onClickOpenMgmtPage = () => {
</div> </div>
</div> </div>
</template> </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="() => getDeviceDetail()">
<template #icon>
<NIcon :component="ReloadOutlined" />
</template>
</NButton>
</template>
<template #default>
<span>刷新设备</span>
</template>
</NTooltip>
</template>
</NCard> </NCard>
</template> </template>

View File

@@ -1,10 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NdmSecurityBoxCircuit } from '@/apis/domains'; import type { NdmSecurityBoxCircuit } from '@/apis/domains';
import type { NdmSecurityBoxResultVO } from '@/apis/models'; import type { NdmSecurityBoxResultVO } from '@/apis/models';
import { rebootSecurityBox, turnStatus } from '@/apis/requests'; import { probeDeviceApi, rebootSecurityBox as rebootSecurityBoxApi, turnStatus as turnStatusApi } from '@/apis/requests';
import { useMutation } from '@tanstack/vue-query';
import { PowerOutline, FlashOutline } from '@vicons/ionicons5'; import { PowerOutline, FlashOutline } from '@vicons/ionicons5';
import { NCard, NGrid, NGridItem, NPopover, NSwitch, NIcon, NFlex, NPopconfirm, NButton } from 'naive-ui'; import { NCard, NGrid, NGridItem, NPopover, NSwitch, NIcon, NFlex, NPopconfirm, NButton } from 'naive-ui';
import { computed, toRefs } from 'vue'; import { ref, toRefs, watch } from 'vue';
/** /**
* 安防箱电路状态卡片组件 * 安防箱电路状态卡片组件
@@ -24,18 +25,18 @@ const props = defineProps<{
const { stationCode, ndmSecurityBox, circuits } = toRefs(props); const { stationCode, ndmSecurityBox, circuits } = toRefs(props);
const validCircuits = computed(() => { const localCircuits = ref<NdmSecurityBoxCircuit[]>([]);
if (!circuits.value || circuits.value.length === 0) {
return []; watch(
} circuits,
return circuits.value; (newValue) => {
}); localCircuits.value = newValue?.map((circuit) => ({ ...circuit })) ?? [];
},
{
immediate: true,
},
);
/**
* 获取电路状态样式类
* @param circuit 电路信息
* @returns CSS类名
*/
const getCircuitStatusClass = (circuit: NdmSecurityBoxCircuit) => { const getCircuitStatusClass = (circuit: NdmSecurityBoxCircuit) => {
if (circuit.status === 1) { if (circuit.status === 1) {
return 'circuit-on'; return 'circuit-on';
@@ -44,12 +45,6 @@ const getCircuitStatusClass = (circuit: NdmSecurityBoxCircuit) => {
} }
return 'circuit-unknown'; return 'circuit-unknown';
}; };
/**
* 获取电路状态文本
* @param circuit 电路信息
* @returns 状态文本
*/
const getCircuitStatusText = (circuit: NdmSecurityBoxCircuit) => { const getCircuitStatusText = (circuit: NdmSecurityBoxCircuit) => {
if (circuit.status === 1) { if (circuit.status === 1) {
return '开启'; return '开启';
@@ -59,58 +54,65 @@ const getCircuitStatusText = (circuit: NdmSecurityBoxCircuit) => {
return '未知'; return '未知';
}; };
/** const { mutate: turnStatus, isPending: turning } = useMutation({
* 处理电路开关切换 mutationFn: async (params: { circuitIndex: number; newStatus: boolean }) => {
* @param circuitIndex 电路索引 const { circuitIndex, newStatus } = params;
* @param newStatus 新状态 if (!ndmSecurityBox.value.ipAddress) {
*/ throw new Error('设备IP地址不存在');
const handleCircuitToggle = async (circuitIndex: number, newStatus: boolean) => { }
if (!ndmSecurityBox.value.ipAddress) {
window.$message.error('设备IP地址不存在');
return;
}
try {
const status = newStatus ? 1 : 0; const status = newStatus ? 1 : 0;
await turnStatus(stationCode.value, ndmSecurityBox.value.ipAddress, circuitIndex, status); await turnStatusApi(stationCode.value, ndmSecurityBox.value.ipAddress, circuitIndex, status);
window.$message.success(`电路${circuitIndex + 1}${newStatus ? '开启' : '关闭'}成功 下次更新诊断数据时将刷新状态`); await probeDeviceApi(stationCode.value, ndmSecurityBox.value);
} catch (error) { return status;
window.$message.error(`电路${circuitIndex + 1}操作失败`); },
console.error('电路开关操作失败:', error); onSuccess: (status, params) => {
} if (localCircuits.value) {
}; const circuit = localCircuits.value.at(params.circuitIndex);
if (circuit) {
circuit.status = status;
localCircuits.value.splice(params.circuitIndex, 1, circuit);
}
}
},
onError: (error) => {
console.error(error);
window.$message.error(error.message);
},
});
const onClickReboot = async () => { const { mutate: rebootSecurityBox, isPending: rebooting } = useMutation({
if (!ndmSecurityBox.value.ipAddress) { mutationFn: async () => {
window.$message.error('设备IP地址不存在'); if (!ndmSecurityBox.value.ipAddress) {
return; throw new Error('设备IP地址不存在');
} }
try { await rebootSecurityBoxApi(stationCode.value, ndmSecurityBox.value.ipAddress);
await rebootSecurityBox(stationCode.value, ndmSecurityBox.value.ipAddress); },
onSuccess: () => {
window.$message.success('设备重启成功'); window.$message.success('设备重启成功');
} catch (error) { },
window.$message.error('设备重启失败'); onError: (error) => {
console.error('设备重启失败:', error); console.error(error);
} window.$message.error(error.message);
}; },
});
</script> </script>
<template> <template>
<NCard v-if="validCircuits.length > 0" size="small" hoverable> <NCard v-if="localCircuits.length > 0" size="small" hoverable>
<template #header> <template #header>
<NFlex :align="'center'"> <NFlex :align="'center'">
<div>电路状态</div> <div>电路状态</div>
<NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="onClickReboot"> <NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => rebootSecurityBox()">
<template #trigger> <template #trigger>
<NButton secondary size="small">重合闸</NButton> <NButton secondary size="small" :loading="rebooting">重合闸</NButton>
</template> </template>
确定要执行重合闸操作吗?此操作将重启安防箱设备。 确定要执行重合闸操作吗?此操作将重启安防箱设备。
</NPopconfirm> </NPopconfirm>
</NFlex> </NFlex>
</template> </template>
<div class="circuit-layout"> <div class="circuit-layout">
<NGrid :cols="Math.min(validCircuits.length, 4)" :x-gap="12" :y-gap="12" class="circuit-grid"> <NGrid :cols="Math.min(localCircuits.length, 4)" :x-gap="12" :y-gap="12" class="circuit-grid">
<NGridItem v-for="(circuit, index) in validCircuits" :key="index"> <NGridItem v-for="(circuit, index) in localCircuits" :key="index">
<!-- 电路信息弹窗 --> <!-- 电路信息弹窗 -->
<NPopover trigger="hover" placement="top"> <NPopover trigger="hover" placement="top">
<template #trigger> <template #trigger>
@@ -126,9 +128,9 @@ const onClickReboot = async () => {
<span class="status-text">{{ getCircuitStatusText(circuit) }}</span> <span class="status-text">{{ getCircuitStatusText(circuit) }}</span>
</div> </div>
<div class="circuit-control"> <div class="circuit-control">
<NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => handleCircuitToggle(index, circuit.status !== 1)"> <NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => turnStatus({ circuitIndex: index, newStatus: circuit.status !== 1 })">
<template #trigger> <template #trigger>
<NSwitch :value="circuit.status === 1" size="small" /> <NSwitch :value="circuit.status === 1" size="small" :loading="turning" />
</template> </template>
确定要{{ circuit.status === 1 ? '关闭' : '开启' }}电路{{ index + 1 }}吗? 确定要{{ circuit.status === 1 ? '关闭' : '开启' }}电路{{ index + 1 }}吗?
</NPopconfirm> </NPopconfirm>

View File

@@ -48,7 +48,7 @@ const selectedTab = ref('设备状态');
<NTabs v-model:value="selectedTab" size="small"> <NTabs v-model:value="selectedTab" size="small">
<NTabPane name="设备状态" tab="设备状态"> <NTabPane name="设备状态" tab="设备状态">
<NFlex vertical> <NFlex vertical>
<DeviceHeaderCard :device="ndmDecoder" /> <DeviceHeaderCard :station-code="stationCode" :device="ndmDecoder" />
<DeviceCommonCard :common-info="commonInfo" /> <DeviceCommonCard :common-info="commonInfo" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" /> <DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
</NFlex> </NFlex>

View File

@@ -24,7 +24,7 @@ const selectedTab = ref('设备状态');
<NTabs v-model:value="selectedTab" size="small"> <NTabs v-model:value="selectedTab" size="small">
<NTabPane name="设备状态" tab="设备状态"> <NTabPane name="设备状态" tab="设备状态">
<NFlex vertical> <NFlex vertical>
<DeviceHeaderCard :device="ndmKeyboard" /> <DeviceHeaderCard :station-code="stationCode" :device="ndmKeyboard" />
</NFlex> </NFlex>
</NTabPane> </NTabPane>
<NTabPane name="历史诊断" tab="历史诊断"> <NTabPane name="历史诊断" tab="历史诊断">

View File

@@ -58,7 +58,7 @@ const selectedTab = ref('设备状态');
<NTabs v-model:value="selectedTab" size="small"> <NTabs v-model:value="selectedTab" size="small">
<NTabPane name="设备状态" tab="设备状态"> <NTabPane name="设备状态" tab="设备状态">
<NFlex vertical> <NFlex vertical>
<DeviceHeaderCard :device="ndmNvr" /> <DeviceHeaderCard :station-code="stationCode" :device="ndmNvr" />
<DeviceCommonCard :common-info="commonInfo" /> <DeviceCommonCard :common-info="commonInfo" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" /> <DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
<NvrDiskCard :disk-health="diskHealth" :group-info-list="groupInfoList" /> <NvrDiskCard :disk-health="diskHealth" :group-info-list="groupInfoList" />

View File

@@ -57,7 +57,7 @@ const selectedTab = ref('设备状态');
<NTabs v-model:value="selectedTab" size="small"> <NTabs v-model:value="selectedTab" size="small">
<NTabPane name="设备状态" tab="设备状态"> <NTabPane name="设备状态" tab="设备状态">
<NFlex vertical> <NFlex vertical>
<DeviceHeaderCard :device="ndmSecurityBox" /> <DeviceHeaderCard :station-code="stationCode" :device="ndmSecurityBox" />
<DeviceCommonCard :common-info="commonInfo" /> <DeviceCommonCard :common-info="commonInfo" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" /> <DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
<SecurityBoxInfoCard :fan-speeds="fanSpeeds" :temperature="temperature" :humidity="humidity" :switches="switches" /> <SecurityBoxInfoCard :fan-speeds="fanSpeeds" :temperature="temperature" :humidity="humidity" :switches="switches" />

View File

@@ -39,7 +39,7 @@ const selectedTab = ref('设备状态');
<NTabs v-model:value="selectedTab" size="small"> <NTabs v-model:value="selectedTab" size="small">
<NTabPane name="设备状态"> <NTabPane name="设备状态">
<NFlex vertical> <NFlex vertical>
<DeviceHeaderCard :device="ndmServer" /> <DeviceHeaderCard :station-code="stationCode" :device="ndmServer" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" :disk-usage="diskUsage" :running-time="runningTime" /> <DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" :disk-usage="diskUsage" :running-time="runningTime" />
</NFlex> </NFlex>
</NTabPane> </NTabPane>

View File

@@ -40,7 +40,7 @@ const selectedTab = ref('设备状态');
<NTabs v-model:value="selectedTab" size="small"> <NTabs v-model:value="selectedTab" size="small">
<NTabPane name="设备状态"> <NTabPane name="设备状态">
<NFlex vertical> <NFlex vertical>
<DeviceHeaderCard :device="ndmSwitch" /> <DeviceHeaderCard :station-code="stationCode" :device="ndmSwitch" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" /> <DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
<SwitchPortCard :port-info-list="portInfoList" /> <SwitchPortCard :port-info-list="portInfoList" />
</NFlex> </NFlex>

View File

@@ -20,7 +20,7 @@ const route = useRoute();
const show = defineModel<boolean>('show'); const show = defineModel<boolean>('show');
const layoutStore = useLayoutStore(); const layoutStore = useLayoutStore();
const { stationLayoutGridCols } = storeToRefs(layoutStore); const { stationGridColumns } = storeToRefs(layoutStore);
const queryControlStore = useQueryControlStore(); const queryControlStore = useQueryControlStore();
const { stationVerifyMode } = storeToRefs(queryControlStore); const { stationVerifyMode } = storeToRefs(queryControlStore);
@@ -73,10 +73,10 @@ useEventListener('keydown', (event) => {
<NFormItem label="深色模式" label-placement="left"> <NFormItem label="深色模式" label-placement="left">
<ThemeSwitch /> <ThemeSwitch />
</NFormItem> </NFormItem>
<template v-if="route.path === '/dashboard'"> <template v-if="route.path === '/station'">
<NDivider>布局</NDivider> <NDivider>布局</NDivider>
<NFormItem label="车站列数" label-placement="left"> <NFormItem label="车站列数" label-placement="left">
<NInputNumber v-model:value="stationLayoutGridCols" :min="1" :max="10" /> <NInputNumber v-model:value="stationGridColumns" :min="1" :max="10" />
</NFormItem> </NFormItem>
</template> </template>
<template v-if="debugEnabled"> <template v-if="debugEnabled">

View File

@@ -1,11 +1,13 @@
import type { VersionInfo } from '@/apis/domains/version-info'; import type { VersionInfo } from '@/apis/domains/version-info';
import { useQuery } from '@tanstack/vue-query'; import { useQuery } from '@tanstack/vue-query';
import axios from 'axios'; import axios from 'axios';
import { useThemeVars } from 'naive-ui';
import { h, ref, watch } from 'vue'; import { h, ref, watch } from 'vue';
export function useVersionCheckQuery() { export function useVersionCheckQuery() {
const localVersionInfo = ref<VersionInfo>(); const localVersionInfo = ref<VersionInfo>();
const dialogShow = ref<boolean>(false); const dialogShow = ref<boolean>(false);
const themeVars = useThemeVars();
const { data: remoteVersionInfo, dataUpdatedAt } = useQuery({ const { data: remoteVersionInfo, dataUpdatedAt } = useQuery({
queryKey: ['version-check'], queryKey: ['version-check'],
@@ -25,7 +27,10 @@ export function useVersionCheckQuery() {
return; return;
} }
if (localVersionInfo.value.version !== newVersionInfo.version && !dialogShow.value) { const { version: localVersion, buildTime: localBuildTime } = localVersionInfo.value;
const { version: remoteVersion, buildTime: remoteBuildTime } = newVersionInfo;
if ((localVersion !== remoteVersion || localBuildTime !== remoteBuildTime) && !dialogShow.value) {
dialogShow.value = true; dialogShow.value = true;
window.$dialog.info({ window.$dialog.info({
title: '发现新版本', title: '发现新版本',
@@ -34,6 +39,7 @@ export function useVersionCheckQuery() {
h('div', {}, { default: () => `当前版本:${localVersionInfo.value?.version}` }), h('div', {}, { default: () => `当前版本:${localVersionInfo.value?.version}` }),
h('div', {}, { default: () => `最新版本:${newVersionInfo.version}` }), h('div', {}, { default: () => `最新版本:${newVersionInfo.version}` }),
h('div', {}, { default: () => '请点击刷新页面以更新' }), h('div', {}, { default: () => '请点击刷新页面以更新' }),
h('div', { style: { marginTop: '8px', fontWeight: '700', color: themeVars.value.warningColor } }, { default: () => '⚠️ 注意,更新后可能需要重新登录!' }),
]), ]),
positiveText: '刷新页面', positiveText: '刷新页面',
maskClosable: false, maskClosable: false,

View File

@@ -15,8 +15,7 @@ import { useCurrentAlarmsStore } from '@/stores/current-alarms';
import { useUserStore } from '@/stores/user'; import { useUserStore } from '@/stores/user';
import { Client as StompClient } from '@stomp/stompjs'; import { Client as StompClient } from '@stomp/stompjs';
import { useIsFetching } from '@tanstack/vue-query'; import { useIsFetching } from '@tanstack/vue-query';
import { AlertFilled, /* AreaChartOutlined, */ FileTextFilled, HomeFilled, LogoutOutlined, SettingOutlined, VideoCameraFilled } from '@vicons/antd'; import { AlertFilled, BugFilled, CaretDownFilled, EnvironmentFilled, /* AreaChartOutlined, */ FileTextFilled, FundFilled, HddFilled, LogoutOutlined, SettingOutlined } from '@vicons/antd';
import { ChevronDown, Debug } from '@vicons/carbon';
import type { AxiosError } from 'axios'; import type { AxiosError } from 'axios';
import { destr } from 'destr'; import { destr } from 'destr';
import { NBadge, NButton, NDropdown, NFlex, NIcon, NLayout, NLayoutContent, NLayoutFooter, NLayoutHeader, NLayoutSider, NMenu, NScrollbar, type DropdownOption, type MenuOption } from 'naive-ui'; import { NBadge, NButton, NDropdown, NFlex, NIcon, NLayout, NLayoutContent, NLayoutFooter, NLayoutHeader, NLayoutSider, NMenu, NScrollbar, type DropdownOption, type MenuOption } from 'naive-ui';
@@ -98,17 +97,22 @@ const router = useRouter();
const menuOptions = ref<MenuOption[]>([ const menuOptions = ref<MenuOption[]>([
{ {
label: () => h(RouterLink, { to: '/dashboard' }, { default: () => '今日数据看板' }), label: () => h(RouterLink, { to: '/dashboard' }, { default: () => '全线总概览' }),
key: '/dashboard', key: '/dashboard',
icon: renderIcon(HomeFilled), icon: renderIcon(FundFilled),
}, },
{ {
label: () => h(RouterLink, { to: '/device' }, { default: () => '设备诊断数据' }), label: () => h(RouterLink, { to: '/station' }, { default: () => '车站状态' }),
key: '/station',
icon: renderIcon(EnvironmentFilled),
},
{
label: () => h(RouterLink, { to: '/device' }, { default: () => '设备诊断' }),
key: '/device', key: '/device',
icon: renderIcon(VideoCameraFilled), icon: renderIcon(HddFilled),
}, },
{ {
label: () => h(RouterLink, { to: '/alarm' }, { default: () => '设备告警记录' }), label: () => h(RouterLink, { to: '/alarm' }, { default: () => '设备告警' }),
key: '/alarm', key: '/alarm',
icon: renderIcon(AlertFilled), icon: renderIcon(AlertFilled),
}, },
@@ -118,20 +122,24 @@ const menuOptions = ref<MenuOption[]>([
// icon: renderIcon(AreaChartOutlined), // icon: renderIcon(AreaChartOutlined),
// }, // },
{ {
label: () => h(RouterLink, { to: '/log/vimp-log' }, { default: () => '视频平台日志' }), // '系统日志记录' label: '系统日志',
key: '/log/vimp-log', key: '/log',
icon: renderIcon(FileTextFilled), icon: renderIcon(FileTextFilled),
// children: [ children: [
// { {
// label: () => h(RouterLink, { to: '/log/vimp-log' }, { default: () => '视频平台日志' }), label: () => h(RouterLink, { to: '/log/vimp-log' }, { default: () => '视频平台日志' }),
// key: '/log/vimp-log', key: '/log/vimp-log',
// }, },
// ], {
label: () => h(RouterLink, { to: '/log/call-log' }, { default: () => '上级调用日志' }),
key: '/log/call-log',
},
],
}, },
{ {
label: () => h(RouterLink, { to: '/debug' }, { default: () => '调试' }), label: () => h(RouterLink, { to: '/debug' }, { default: () => '调试' }),
key: '/debug', key: '/debug',
icon: renderIcon(Debug), icon: renderIcon(BugFilled),
show: import.meta.env.DEV, show: import.meta.env.DEV,
}, },
]); ]);
@@ -195,15 +203,13 @@ const openSettingsDrawer = () => {
<span>{{ userInfo?.nickName ?? '' }}</span> <span>{{ userInfo?.nickName ?? '' }}</span>
</template> </template>
<template #icon> <template #icon>
<NIcon :component="ChevronDown" /> <NIcon :component="CaretDownFilled" />
</template> </template>
</NButton> </NButton>
</NDropdown> </NDropdown>
<NButton :focusable="false" quaternary @click="openSettingsDrawer" style="height: 100%"> <NButton :focusable="false" quaternary @click="openSettingsDrawer" style="height: 100%">
<template #icon> <template #icon>
<NIcon> <NIcon :component="SettingOutlined" />
<SettingOutlined />
</NIcon>
</template> </template>
</NButton> </NButton>
</NFlex> </NFlex>
@@ -217,9 +223,7 @@ const openSettingsDrawer = () => {
<NBadge :value="currentAlarmCount"> <NBadge :value="currentAlarmCount">
<NButton secondary strong @click="toAlarmPage"> <NButton secondary strong @click="toAlarmPage">
<template #icon> <template #icon>
<NIcon> <NIcon :component="AlertFilled" />
<AlertFilled />
</NIcon>
</template> </template>
</NButton> </NButton>
</NBadge> </NBadge>

View File

@@ -1,13 +1,21 @@
import { getAppEnvConfig } from '@/utils/env';
import { VueQueryPlugin } from '@tanstack/vue-query';
import { createApp } from 'vue'; import { createApp } from 'vue';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import persist from 'pinia-plugin-persistedstate'; import persist from 'pinia-plugin-persistedstate';
import { VueQueryPlugin } from '@tanstack/vue-query';
import App from './App.vue'; import App from './App.vue';
import router from './router'; import router from './router';
import '@/styles/reset.scss'; import '@/styles/reset.scss';
const { storageVersion } = getAppEnvConfig();
const localStorageVersion = window.localStorage.getItem('ndm-storage-version');
if (localStorageVersion !== storageVersion) {
window.localStorage.clear();
window.localStorage.setItem('ndm-storage-version', storageVersion);
}
const app = createApp(App); const app = createApp(App);
app.use(createPinia().use(persist)); app.use(createPinia().use(persist));

256
src/pages/call-log-page.vue Normal file
View File

@@ -0,0 +1,256 @@
<script setup lang="ts">
import type { NdmCallLogResultVO, NdmCallLogVO, PageQueryExtra } from '@/apis/models';
import { ndmCallLogDefaultExportByTemplate, postNdmCallLogPage } from '@/apis/requests';
import { useStationStore } from '@/stores/station';
import { downloadByData } from '@/utils/download';
import { useMutation } from '@tanstack/vue-query';
import dayjs from 'dayjs';
import {
NButton,
NDataTable,
NDatePicker,
NForm,
NFormItemGi,
NGrid,
NGridItem,
NInput,
NSelect,
NSpace,
NTag,
type DataTableColumns,
type DataTableRowData,
type PaginationProps,
type SelectOption,
} from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, h, reactive, ref, watch, watchEffect } from 'vue';
const stationStore = useStationStore();
const { stationList, onlineStationList } = storeToRefs(stationStore);
const stationSelectOptions = computed(() => {
return stationList.value.map<SelectOption>((station) => ({
label: station.name,
value: station.code,
disabled: !station.online,
}));
});
type SearchFields = PageQueryExtra<NdmCallLogVO> & { stationCode?: string; createdTime: [string, string] };
const searchFields = reactive<SearchFields>({
stationCode: undefined as string | undefined,
sourceGbId_like: undefined,
targetGbId_like: undefined,
method_like: undefined,
messageType_like: undefined,
cmdType_like: undefined,
createdTime: [dayjs().startOf('date').subtract(1, 'week').format('YYYY-MM-DD HH:mm:ss'), dayjs().endOf('date').format('YYYY-MM-DD HH:mm:ss')] as [string, string],
});
const resetSearchFields = () => {
searchFields.stationCode = stationList.value.find((station) => station.online)?.code;
searchFields.sourceGbId_like = undefined;
searchFields.targetGbId_like = undefined;
searchFields.method_like = undefined;
searchFields.messageType_like = undefined;
searchFields.cmdType_like = undefined;
searchFields.createdTime = [dayjs().startOf('date').subtract(1, 'week').format('YYYY-MM-DD HH:mm:ss'), dayjs().endOf('date').format('YYYY-MM-DD HH:mm:ss')];
};
const getExtraFields = () => {
const createdTime_precisest = searchFields.createdTime[0];
const createdTime_preciseed = searchFields.createdTime[1];
const sourceGbId_like = searchFields.sourceGbId_like;
const targetGbId_like = searchFields.targetGbId_like;
const method_like = searchFields.method_like;
const messageType_like = searchFields.messageType_like;
const cmdType_like = searchFields.cmdType_like;
return {
createdTime_precisest,
createdTime_preciseed,
sourceGbId_like,
targetGbId_like,
method_like,
messageType_like,
cmdType_like,
};
};
const searchFieldsChanged = ref(false);
watch(searchFields, () => {
searchFieldsChanged.value = true;
});
const tableColumns: DataTableColumns<NdmCallLogResultVO> = [
{ title: '时间', key: 'createdTime' },
{ title: '调用者国标码', key: 'sourceGbId' },
{ title: '被调用设备国标码', key: 'targetGbId' },
{ title: '调用方法', key: 'method' },
{ title: '消息类型', key: 'messageType' },
{ title: '操作类型', key: 'cmdType' },
];
const tableData = ref<DataTableRowData[]>([]);
const tablePagination = reactive<PaginationProps>({
showSizePicker: true,
page: 1,
pageSize: 10,
pageSizes: [5, 10, 20, 50, 80, 100],
itemCount: 0,
prefix: ({ itemCount }) => {
return h('div', {}, { default: () => `${itemCount}` });
},
onUpdatePage: (page: number) => {
tablePagination.page = page;
getCallLogList();
},
onUpdatePageSize: (pageSize: number) => {
tablePagination.pageSize = pageSize;
tablePagination.page = 1;
getCallLogList();
},
});
const { mutate: getCallLogList, isPending: tableLoading } = useMutation({
mutationFn: async () => {
if (!searchFields.stationCode) throw Error('请选择车站');
const res = await postNdmCallLogPage(searchFields.stationCode, {
model: {},
extra: getExtraFields(),
current: tablePagination.page ?? 1,
size: tablePagination.pageSize ?? 10,
order: 'descending',
sort: 'id',
});
return res;
},
onSuccess: (res) => {
const { records, size, total } = res;
tablePagination.pageSize = parseInt(size);
tablePagination.itemCount = parseInt(total);
tableData.value = records;
},
onError: (error) => {
console.error(error);
window.$message.error(error.message);
},
});
const onClickReset = () => {
resetSearchFields();
tablePagination.page = 1;
tablePagination.pageSize = 10;
tablePagination.itemCount = 0;
getCallLogList();
};
const onClickQuery = () => {
if (searchFieldsChanged.value) {
tablePagination.page = 1;
tablePagination.pageSize = 10;
searchFieldsChanged.value = false;
}
getCallLogList();
};
const { mutate: exportTableData, isPending: exporting } = useMutation({
mutationFn: async () => {
if (!searchFields.stationCode) throw Error('请选择车站');
const data = await ndmCallLogDefaultExportByTemplate(searchFields.stationCode, {
model: {},
extra: getExtraFields(),
current: tablePagination.page ?? 1,
size: tablePagination.pageSize ?? 10,
order: 'descending',
sort: 'id',
});
return data;
},
onSuccess: (data) => {
const time = dayjs().format('YYYY-MM-DD_HH-mm-ss');
downloadByData(data, `上级调用日志_${time}.xlsx`);
},
onError: (error) => {
console.error(error);
window.$message.error(error.message);
},
});
const defaultStation = computed(() => onlineStationList.value.at(0));
watchEffect(() => {
if (defaultStation.value?.code && !searchFields.stationCode) {
searchFields.stationCode = defaultStation.value.code;
getCallLogList();
}
});
</script>
<template>
<!-- 容器上下布局表格自适应剩余高度 -->
<div style="height: 100%; display: flex; flex-direction: column">
<!-- 查询面板 -->
<div style="flex: 0 0 auto; padding: 8px">
<NForm>
<NGrid :cols="3" :x-gap="24">
<NFormItemGi :span="1" label="车站" label-placement="left">
<NSelect
v-model:value="searchFields.stationCode"
:options="stationSelectOptions"
:render-label="
(option: SelectOption) => {
return [
h(NTag, { type: option.disabled ? 'error' : 'success', size: 'tiny' }, { default: () => (option.disabled ? '离线' : '在线') }),
h('span', {}, { default: () => `${option.label}` }),
];
}
"
:multiple="false"
clearable
/>
</NFormItemGi>
<NFormItemGi :span="1" label="调用者国标码" label-placement="left">
<NInput v-model:value="searchFields.sourceGbId_like" placeholder="请输入调用者国标码" clearable />
</NFormItemGi>
<NFormItemGi :span="1" label="被调用设备国标码" label-placement="left">
<NInput v-model:value="searchFields.targetGbId_like" placeholder="请输入被调用设备国标码" clearable />
</NFormItemGi>
<NFormItemGi :span="1" label="调用方法" label-placement="left">
<NInput v-model:value="searchFields.method_like" placeholder="请输入调用方法" clearable />
</NFormItemGi>
<NFormItemGi :span="1" label="消息类型" label-placement="left">
<NInput v-model:value="searchFields.messageType_like" placeholder="请输入消息类型" clearable />
</NFormItemGi>
<NFormItemGi :span="1" label="操作类型" label-placement="left">
<NInput v-model:value="searchFields.cmdType_like" placeholder="请输入操作类型" clearable />
</NFormItemGi>
<NFormItemGi :span="1" label="时间" label-placement="left">
<NDatePicker v-model:formatted-value="searchFields.createdTime" type="datetimerange" @update:value="undefined" />
</NFormItemGi>
</NGrid>
<!-- 按钮 -->
<NGrid :cols="1">
<NGridItem>
<NSpace>
<NButton @click="onClickReset">重置</NButton>
<NButton type="primary" :loading="tableLoading" @click="onClickQuery">查询</NButton>
</NSpace>
</NGridItem>
</NGrid>
</NForm>
</div>
<!-- 工具栏:横向、右对齐按钮) -->
<div style="flex: 0 0 auto; display: flex; align-items: center; padding: 8px">
<div style="font-size: medium">视频平台日志</div>
<NSpace style="margin-left: auto">
<NButton type="primary" :loading="exporting" @click="() => exportTableData()">导出</NButton>
</NSpace>
</div>
<!-- 表格区域:填满剩余空间 -->
<div style="flex: 1 1 auto; min-height: 0; padding: 8px">
<NDataTable remote :columns="tableColumns" :data="tableData" :pagination="tablePagination" :loading="tableLoading" :single-line="false" flex-height style="height: 100%" />
</div>
</div>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,35 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Station } from '@/apis/domains';
import { ndmExportDevices } from '@/apis/requests'; import { ndmExportDevices } from '@/apis/requests';
import { useLineAlarmsQuery, useLineDevicesQuery } from '@/composables/query'; import { useLineAlarmsQuery, useLineDevicesQuery } from '@/composables/query';
import { useLayoutStore } from '@/stores/layout';
import { useLineAlarmsStore } from '@/stores/line-alarms';
import { useLineDevicesStore } from '@/stores/line-devices'; import { useLineDevicesStore } from '@/stores/line-devices';
import { useStationStore } from '@/stores/station'; import { useStationStore } from '@/stores/station';
import { downloadByData } from '@/utils/download'; import { downloadByData } from '@/utils/download';
import { useMutation } from '@tanstack/vue-query'; import { useMutation } from '@tanstack/vue-query';
import { NGrid, NGi } from 'naive-ui';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { ref, watch } from 'vue'; import { watch } from 'vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import DeviceAlarmDetailModal from '@/components/dashboard-page/device-alarm-detail-modal.vue';
import DeviceParamsConfigModal from '@/components/dashboard-page/device-params-config-modal.vue';
import DeviceStatisticCard from '@/components/dashboard-page/device-statistic-card.vue'; import DeviceStatisticCard from '@/components/dashboard-page/device-statistic-card.vue';
import OfflineDeviceDetailModal from '@/components/dashboard-page/offline-device-detail-modal.vue';
import StationCard from '@/components/dashboard-page/station-card.vue';
const stationStore = useStationStore(); const stationStore = useStationStore();
const { stationList } = storeToRefs(stationStore); const { stationList } = storeToRefs(stationStore);
const lineDevicesStore = useLineDevicesStore(); const lineDevicesStore = useLineDevicesStore();
const { lineDevices } = storeToRefs(lineDevicesStore); const { lineDevices } = storeToRefs(lineDevicesStore);
const lineAlarmsStore = useLineAlarmsStore();
const { lineAlarmCounts } = storeToRefs(lineAlarmsStore);
const { error: lineDevicesQueryError } = useLineDevicesQuery(); const { error: lineDevicesQueryError } = useLineDevicesQuery();
const { error: lineAlarmsQueryError } = useLineAlarmsQuery(); const { error: lineAlarmsQueryError } = useLineAlarmsQuery();
const layoutStore = useLayoutStore();
const { stationLayoutGridCols } = storeToRefs(layoutStore);
watch([lineDevicesQueryError, lineAlarmsQueryError], ([newLineDevicesQueryError, newLineAlarmsQueryError]) => { watch([lineDevicesQueryError, lineAlarmsQueryError], ([newLineDevicesQueryError, newLineAlarmsQueryError]) => {
if (newLineDevicesQueryError) { if (newLineDevicesQueryError) {
@@ -61,23 +49,6 @@ const { mutate: exportDevices, isPending: exporting } = useMutation({
window.$message.error(error.message); window.$message.error(error.message);
}, },
}); });
const selectedStation = ref<Station>();
const offlineDeviceTreeModalShow = ref(false);
const deviceAlarmTreeModalShow = ref(false);
const deviceParamsConfigModalShow = ref(false);
const openOfflineDeviceDetailModal = (station: Station) => {
selectedStation.value = station;
offlineDeviceTreeModalShow.value = true;
};
const openDeviceAlarmDetailModal = (station: Station) => {
selectedStation.value = station;
deviceAlarmTreeModalShow.value = true;
};
const openDeviceParamsConfigModal = (station: Station) => {
selectedStation.value = station;
deviceParamsConfigModalShow.value = true;
};
</script> </script>
<template> <template>
@@ -89,26 +60,6 @@ const openDeviceParamsConfigModal = (station: Station) => {
@export-online="() => exportDevices({ status: '10' })" @export-online="() => exportDevices({ status: '10' })"
@export-offline="() => exportDevices({ status: '20' })" @export-offline="() => exportDevices({ status: '20' })"
/> />
<NGrid :cols="stationLayoutGridCols" :x-gap="6" :y-gap="6" style="padding: 8px">
<NGi v-for="station in stationList" :key="station.code">
<StationCard
:station="station"
:station-devices="lineDevices[station.code]"
:station-alarm-counts="lineAlarmCounts[station.code]"
@open-offline-device-detail-modal="openOfflineDeviceDetailModal"
@open-device-alarm-detail-modal="openDeviceAlarmDetailModal"
@open-device-params-config-modal="openDeviceParamsConfigModal"
/>
</NGi>
</NGrid>
<!-- 离线设备详情对话框 -->
<OfflineDeviceDetailModal v-model:show="offlineDeviceTreeModalShow" :station="selectedStation" :station-devices="selectedStation?.code ? lineDevices[selectedStation.code] : undefined" />
<!-- 设备告警详情对话框 -->
<DeviceAlarmDetailModal v-model:show="deviceAlarmTreeModalShow" :station="selectedStation" :station-alarm-counts="selectedStation?.code ? lineAlarmCounts[selectedStation.code] : undefined" />
<!-- 设备配置面板对话框 -->
<DeviceParamsConfigModal v-model:show="deviceParamsConfigModalShow" :station="selectedStation" />
</template> </template>
<style scoped></style> <style scoped lang="scss"></style>

View File

@@ -0,0 +1,78 @@
<script setup lang="ts">
import type { Station } from '@/apis/domains';
import { useLineAlarmsQuery, useLineDevicesQuery } from '@/composables/query';
import { useLayoutStore } from '@/stores/layout';
import { useLineAlarmsStore } from '@/stores/line-alarms';
import { useLineDevicesStore } from '@/stores/line-devices';
import { useStationStore } from '@/stores/station';
import { NGrid, NGi } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { ref, watch } from 'vue';
import DeviceAlarmDetailModal from '@/components/dashboard-page/device-alarm-detail-modal.vue';
import DeviceParamsConfigModal from '@/components/dashboard-page/device-params-config-modal.vue';
import OfflineDeviceDetailModal from '@/components/dashboard-page/offline-device-detail-modal.vue';
import StationCard from '@/components/dashboard-page/station-card.vue';
const stationStore = useStationStore();
const { stationList } = storeToRefs(stationStore);
const lineDevicesStore = useLineDevicesStore();
const { lineDevices } = storeToRefs(lineDevicesStore);
const lineAlarmsStore = useLineAlarmsStore();
const { lineAlarmCounts } = storeToRefs(lineAlarmsStore);
const { error: lineDevicesQueryError } = useLineDevicesQuery();
const { error: lineAlarmsQueryError } = useLineAlarmsQuery();
const layoutStore = useLayoutStore();
const { stationGridColumns } = storeToRefs(layoutStore);
watch([lineDevicesQueryError, lineAlarmsQueryError], ([newLineDevicesQueryError, newLineAlarmsQueryError]) => {
if (newLineDevicesQueryError) {
window.$message.error(newLineDevicesQueryError.message);
}
if (newLineAlarmsQueryError) {
window.$message.error(newLineAlarmsQueryError.message);
}
});
const selectedStation = ref<Station>();
const offlineDeviceTreeModalShow = ref(false);
const deviceAlarmTreeModalShow = ref(false);
const deviceParamsConfigModalShow = ref(false);
const openOfflineDeviceDetailModal = (station: Station) => {
selectedStation.value = station;
offlineDeviceTreeModalShow.value = true;
};
const openDeviceAlarmDetailModal = (station: Station) => {
selectedStation.value = station;
deviceAlarmTreeModalShow.value = true;
};
const openDeviceParamsConfigModal = (station: Station) => {
selectedStation.value = station;
deviceParamsConfigModalShow.value = true;
};
</script>
<template>
<NGrid :cols="stationGridColumns" :x-gap="6" :y-gap="6" style="padding: 8px">
<NGi v-for="station in stationList" :key="station.code">
<StationCard
:station="station"
:station-devices="lineDevices[station.code]"
:station-alarm-counts="lineAlarmCounts[station.code]"
@open-offline-device-detail-modal="openOfflineDeviceDetailModal"
@open-device-alarm-detail-modal="openDeviceAlarmDetailModal"
@open-device-params-config-modal="openDeviceParamsConfigModal"
/>
</NGi>
</NGrid>
<!-- 离线设备详情对话框 -->
<OfflineDeviceDetailModal v-model:show="offlineDeviceTreeModalShow" :station="selectedStation" :station-devices="selectedStation?.code ? lineDevices[selectedStation.code] : undefined" />
<!-- 设备告警详情对话框 -->
<DeviceAlarmDetailModal v-model:show="deviceAlarmTreeModalShow" :station="selectedStation" :station-alarm-counts="selectedStation?.code ? lineAlarmCounts[selectedStation.code] : undefined" />
<!-- 设备配置面板对话框 -->
<DeviceParamsConfigModal v-model:show="deviceParamsConfigModalShow" :station="selectedStation" />
</template>
<style scoped lang="scss"></style>

View File

@@ -76,6 +76,7 @@ const searchFields = reactive({
const resetSearchFields = () => { const resetSearchFields = () => {
searchFields.stationCode = stationList.value.find((station) => station.online)?.code; searchFields.stationCode = stationList.value.find((station) => station.online)?.code;
searchFields.logType_in = []; searchFields.logType_in = [];
searchFields.createdTime = [dayjs().startOf('date').subtract(1, 'week').format('YYYY-MM-DD HH:mm:ss'), dayjs().endOf('date').format('YYYY-MM-DD HH:mm:ss')];
}; };
const getExtraFields = () => { const getExtraFields = () => {
const logType_in = searchFields.logType_in.length > 0 ? [...searchFields.logType_in] : undefined; const logType_in = searchFields.logType_in.length > 0 ? [...searchFields.logType_in] : undefined;

View File

@@ -17,6 +17,10 @@ const router = createRouter({
path: 'dashboard', path: 'dashboard',
component: () => import('@/pages/dashboard-page.vue'), component: () => import('@/pages/dashboard-page.vue'),
}, },
{
path: 'station',
component: () => import('@/pages/station-page.vue'),
},
{ {
path: 'device', path: 'device',
component: () => import('@/pages/device-page.vue'), component: () => import('@/pages/device-page.vue'),
@@ -31,12 +35,15 @@ const router = createRouter({
}, },
{ {
path: 'log', path: 'log',
redirect: '/log/vimp-log',
children: [ children: [
{ {
path: 'vimp-log', path: 'vimp-log',
component: () => import('@/pages/vimp-log-page.vue'), component: () => import('@/pages/vimp-log-page.vue'),
}, },
{
path: 'call-log',
component: () => import('@/pages/call-log-page.vue'),
},
], ],
}, },
{ {
@@ -62,32 +69,30 @@ const router = createRouter({
const whiteList = ['/debug']; const whiteList = ['/debug'];
router.beforeEach((to, from, next) => { router.beforeEach((to) => {
const userStore = useUserStore(); const userStore = useUserStore();
const isAuthed = !!userStore.userLoginResult?.token; const isAuthed = !!userStore.userLoginResult?.token;
// 放行白名单 // 放行白名单
const inWhiteList = whiteList.some((path) => to.path.startsWith(path)); const inWhiteList = whiteList.some((path) => to.path.startsWith(path));
if (inWhiteList) { if (inWhiteList) {
next(); return true;
return;
} }
// 已登录用户不允许进入登录页(手动访问 /login 会重定向到首页) // 已登录用户不允许进入登录页(手动访问 /login 会重定向到首页)
if (to.path === '/login') { if (to.path === '/login') {
if (isAuthed) { if (isAuthed) {
next({ path: '/' }); return { path: '/' };
} else { } else {
next(); return true;
} }
return;
}
// 其它路由按登录态控制
if (!isAuthed) {
next('/login');
} else { } else {
next(); // 其它路由按登录态控制
if (!isAuthed) {
return { path: '/login' };
} else {
return true;
}
} }
}); });

View File

@@ -4,10 +4,10 @@ import { ref } from 'vue';
export const useLayoutStore = defineStore( export const useLayoutStore = defineStore(
'ndm-layout-store', 'ndm-layout-store',
() => { () => {
const stationLayoutGridCols = ref(8); const stationGridColumns = ref(6);
return { return {
stationLayoutGridCols, stationGridColumns,
}; };
}, },
{ {

View File

@@ -10,6 +10,7 @@ export const getAppEnvConfig = () => {
VITE_LAMP_PASSWORD, VITE_LAMP_PASSWORD,
VITE_LAMP_AUTHORIZATION, VITE_LAMP_AUTHORIZATION,
VITE_DEBUG_CODE, VITE_DEBUG_CODE,
VITE_STORAGE_VERSION,
} = env; } = env;
return { return {
requestInterval: Number.parseInt(VITE_REQUEST_INTERVAL as string), requestInterval: Number.parseInt(VITE_REQUEST_INTERVAL as string),
@@ -20,5 +21,6 @@ export const getAppEnvConfig = () => {
lampPassword: VITE_LAMP_PASSWORD as string, lampPassword: VITE_LAMP_PASSWORD as string,
lampAuthorization: VITE_LAMP_AUTHORIZATION as string, lampAuthorization: VITE_LAMP_AUTHORIZATION as string,
debugCode: VITE_DEBUG_CODE as string, debugCode: VITE_DEBUG_CODE as string,
storageVersion: VITE_STORAGE_VERSION as string,
}; };
}; };