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 生成
VITE_LAMP_AUTHORIZATION = Y3VlZGVzX2FkbWluOmN1ZWRlc19hZG1pbl9zZWNyZXQ=
# 当需要重置localStorage时, 修改此变量
VITE_STORAGE_VERSION = 1
# 调试授权码
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 { NdmVideoServerVO } from './video/ndm-video-server';
export * from './log/ndm-call-log';
export * from './log/ndm-device-alarm-log';
export * from './log/ndm-icmp-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 './log/ndm-call-log';
export * from './log/ndm-device-alarm-log';
export * from './log/ndm-icmp-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-media-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) => {
const prefix = stationCode ? `/${stationCode}` : '';
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;
if (err || !ndmSecurityBoxData) {
throw err;
@@ -18,7 +28,7 @@ export const putNdmSecurityBox = async (stationCode: string, updateVO: NdmSecuri
if (err || !ndmSecurityBox) {
throw err;
}
return ndmSecurityBox;
return await getNdmSecurityBoxDetail(stationCode, ndmSecurityBox.id ?? '');
};
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) => {
const prefix = stationCode ? `/${stationCode}` : '';
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;
if (err || !ndmSwitchData) {
throw err;
@@ -18,5 +28,5 @@ export const putNdmSwitch = async (stationCode: string, updateVO: NdmSwitchUpdat
if (err || !ndmSwitch) {
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) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.post<PageResult<NdmNvrResultVO>>(`${prefix}/api/ndm/ndmNvr/page`, pageQuery, { signal });
const [err, ndmNvrData] = resp;
if (err || !ndmNvrData) {
const [err, ndmNvr] = resp;
if (err || !ndmNvr) {
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) => {
@@ -19,7 +29,7 @@ export const putNdmNvr = async (stationCode: string, updateVO: NdmNvrUpdateVO) =
if (err || !ndmNvr) {
throw err;
}
return ndmNvr;
return await getNdmNvrDetail(stationCode, ndmNvr.id ?? '');
};
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) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.post<PageResult<NdmCameraResultVO>>(`${prefix}/api/ndm/ndmCamera/page`, pageQuery, { signal });
const [err, ndmCameraData] = resp;
if (err || !ndmCameraData) {
const [err, ndmCamera] = resp;
if (err || !ndmCamera) {
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) => {
@@ -18,5 +28,5 @@ export const putNdmCamera = async (stationCode: string, updateVO: NdmCameraUpdat
if (err || !ndmCamera) {
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) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.post<PageResult<NdmDecoderResultVO>>(`${prefix}/api/ndm/ndmDecoder/page`, pageQuery, { signal });
const [err, ndmDecoderData] = resp;
if (err || !ndmDecoderData) {
const [err, ndmDecoder] = resp;
if (err || !ndmDecoder) {
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) => {
@@ -18,5 +28,5 @@ export const putNdmDecoder = async (stationCode: string, updateVO: NdmDecoderUpd
if (err || !ndmDecoder) {
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;
};
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) => {
const prefix = stationCode ? `/${stationCode}` : '';
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) {
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) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.post<PageResult<NdmMediaServerResultVO>>(`${prefix}/api/ndm/ndmMediaServer/page`, pageQuery, { signal });
const [err, ndmMediaServerData] = resp;
if (err || !ndmMediaServerData) {
const [err, ndmMediaServer] = resp;
if (err || !ndmMediaServer) {
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) => {
@@ -18,5 +28,5 @@ export const putNdmMediaServer = async (stationCode: string, updateVO: NdmMediaS
if (err || !ndmMediaServer) {
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) => {
const prefix = stationCode ? `/${stationCode}` : '';
const resp = await ndmClient.post<PageResult<NdmVideoServerResultVO>>(`${prefix}/api/ndm/ndmVideoServer/page`, pageQuery, { signal });
const [err, ndmVideoServerData] = resp;
if (err || !ndmVideoServerData) {
const [err, ndmVideoServer] = resp;
if (err || !ndmVideoServer) {
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) => {
@@ -18,5 +28,5 @@ export const putNdmVideoServer = async (stationCode: string, updateVO: NdmVideoS
if (err || !ndmVideoServer) {
throw err;
}
return ndmVideoServer;
return await getNdmVideoServerDetail(stationCode, ndmVideoServer.id ?? '');
};

View File

@@ -2,10 +2,9 @@
import type { Station } from '@/apis/domains';
import { DeviceType } from '@/enums/device-type';
import { type StationAlarmCounts, type StationDevices } from '@/composables/query';
import { ControlOutlined } from '@vicons/antd';
import { Video as VideoIcon } from '@vicons/carbon';
import { MoreOutlined, EllipsisOutlined } from '@vicons/antd';
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';
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();
</script>
<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>
<NTooltip v-if="station.ip" trigger="click">
<template #trigger>
<span class="font-smaller">{{ station.name }}</span>
<span class="font-medium">{{ station.name }}</span>
</template>
<span>{{ station.ip }}</span>
</NTooltip>
<span v-else class="font-smaller">{{ station.name }}</span>
<span v-else class="font-medium">{{ station.name }}</span>
</template>
<template #header-extra>
<NTag :type="station.online ? 'success' : 'error'" size="small">
{{ station.online ? '在线' : '离线' }}
</NTag>
<NFlex :size="4">
<NTag :type="station.online ? 'success' : 'error'" size="small">
{{ station.online ? '在线' : '离线' }}
</NTag>
<NDropdown trigger="click" :options="dropdownOptions" @select="selectDropdownOption">
<NButton quaternary size="tiny" :focusable="false">
<NIcon :component="MoreOutlined" />
</NButton>
</NDropdown>
</NFlex>
</template>
<template #default>
<NSpace vertical :size="8">
<NFlex :justify="'flex-start'" class="actions">
<NButton quaternary size="tiny" :focusable="false" :disabled="!station.online" @click="openVideoPlatform">
<NIcon>
<VideoIcon />
</NIcon>
<span class="btn-text">视频平台</span>
</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 vertical :size="6" class="metrics" :style="{ opacity: station.online ? '1' : '0.5' }">
<NFlex vertical :size="4" class="metric-item">
<NFlex justify="end" align="center" class="metric-line">
<span class="font-small">{{ deviceCount }} 台设备</span>
<NButton quaternary size="tiny" :focusable="false" @click="openOfflineDeviceTreeModal">
<NIcon :component="EllipsisOutlined" />
</NButton>
</NFlex>
<NFlex justify="space-between" align="baseline" class="metric-item">
<NText depth="3" class="metric-label" :class="[station.online ? 'clickable' : '']" @click="station.online && openDeviceAlarmTreeModal()">告警记录</NText>
<span class="metric-value">
<span>{{ alarmCount }}</span>
<NText depth="3" class="unit"></NText>
<NFlex justify="end" align="center" class="metric-line">
<span class="font-small">
<span :style="{ color: onlineDeviceCount > 0 ? theme.successColor : '' }">在线 {{ onlineDeviceCount }} </span>
<NText depth="3" class="sep">·</NText>
<span :style="{ color: offlineDeviceCount > 0 ? theme.errorColor : '' }">离线 {{ offlineDeviceCount }} </span>
</span>
<NButton quaternary size="tiny" :focusable="false" style="visibility: hidden">
<NIcon :component="EllipsisOutlined" />
</NButton>
</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>
</NCard>
</template>
<style scoped lang="scss">
.clickable {
text-decoration: underline dashed;
cursor: pointer;
transition: color 0.2s ease;
&:hover {
color: v-bind('theme.iconColorHover');
}
.font-medium {
font-size: medium;
}
.font-smaller {
font-size: smaller;
.font-small {
font-size: small;
}
.btn-text {
margin-left: 6px;
.metrics {
padding-top: 4px;
}
.sep {
margin: 0 6px;
font-size: xx-small;
color: v-bind('theme.textColor3');
}
.metric-label {
font-size: xx-small;
}
.metric-value {
font-size: small;
}
.unit,
.slash {
margin-left: 4px;
font-size: xx-small;
.metric-line .font-small {
white-space: nowrap;
}
</style>

View File

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

View File

@@ -1,16 +1,21 @@
<script setup lang="ts">
import type { NdmDeviceResultVO } from '@/apis/models';
import { getDeviceDetailApi, probeDeviceApi } from '@/apis/requests';
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';
const props = defineProps<{
stationCode: string;
device: NdmDeviceResultVO;
}>();
// const emit = defineEmits<{}>();
const { device } = toRefs(props);
const { stationCode, device } = toRefs(props);
const type = computed(() => {
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>
<template>
@@ -51,6 +84,8 @@ const onClickOpenMgmtPage = () => {
<div>{{ name }}</div>
<NButton v-if="canOpenMgmtPage" ghost size="tiny" type="default" :focusable="false" @click="onClickOpenMgmtPage">管理</NButton>
</NFlex>
</template>
<template #default>
<div style="font-size: small; color: #666">
<div>
<span>设备类型</span>
@@ -74,6 +109,32 @@ const onClickOpenMgmtPage = () => {
</div>
</div>
</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>
</template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,21 @@
import { getAppEnvConfig } from '@/utils/env';
import { VueQueryPlugin } from '@tanstack/vue-query';
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import persist from 'pinia-plugin-persistedstate';
import { VueQueryPlugin } from '@tanstack/vue-query';
import App from './App.vue';
import router from './router';
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);
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">
import type { Station } from '@/apis/domains';
import { ndmExportDevices } from '@/apis/requests';
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 { downloadByData } from '@/utils/download';
import { useMutation } from '@tanstack/vue-query';
import { NGrid, NGi } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { ref, watch } from 'vue';
import { watch } from 'vue';
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 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 { stationLayoutGridCols } = storeToRefs(layoutStore);
watch([lineDevicesQueryError, lineAlarmsQueryError], ([newLineDevicesQueryError, newLineAlarmsQueryError]) => {
if (newLineDevicesQueryError) {
@@ -61,23 +49,6 @@ const { mutate: exportDevices, isPending: exporting } = useMutation({
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>
<template>
@@ -89,26 +60,6 @@ const openDeviceParamsConfigModal = (station: Station) => {
@export-online="() => exportDevices({ status: '10' })"
@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>
<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 = () => {
searchFields.stationCode = stationList.value.find((station) => station.online)?.code;
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 logType_in = searchFields.logType_in.length > 0 ? [...searchFields.logType_in] : undefined;

View File

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

View File

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

View File

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