Compare commits

..

9 Commits

Author SHA1 Message Date
yangsy
b7ae983eb1 feat: 添加告警画面截图相关设置
- 配置告警画面截图保留天数
- 是否自动获取告警画面截图
2025-12-30 15:14:48 +08:00
yangsy
e112b01a21 feat: 告警忽略管理页面 2025-12-30 10:57:43 +08:00
yangsy
054b2bcdac feat: 添加告警忽略管理页面和路由配置 2025-12-30 10:45:48 +08:00
yangsy
1f23588649 refactor: 调整路由结构,使告警板块支持子路由 2025-12-30 10:45:40 +08:00
yangsy
02e29eb4f3 feat: 支持查看摄像机告警画面截图 2025-12-30 10:43:05 +08:00
yangsy
118cc8be0b refactor: 明确所有 stationCode 参数的类型 2025-12-29 14:03:13 +08:00
yangsy
5170105ab8 refactor: 明确所有 stationCode 参数的类型 2025-12-29 14:03:07 +08:00
yangsy
420637352d feat: 查询页面卸载时取消未完成的请求 2025-12-28 14:34:42 +08:00
yangsy
abdbef2d05 fix: 修复跳转设备时未检查deviceId存在性的问题 2025-12-28 13:24:58 +08:00
23 changed files with 717 additions and 69 deletions

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { initStationDevices, ndmClient, userClient, type StationDevices } from '@/apis';
import { initStationDevices, ndmClient, userClient, type Station, type StationDevices } from '@/apis';
import { unwrapResponse } from '@/utils';
export const getAllDevicesApi = async (options?: { stationCode?: string; signal?: AbortSignal }) => {
export const getAllDevicesApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import { ndmClient, userClient, type ClientChannel, type NdmNvrResultVO, type NdmRecordCheck } from '@/apis';
import { ndmClient, userClient, type ClientChannel, type NdmNvrResultVO, type NdmRecordCheck, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
import dayjs from 'dayjs';
export const getChannelListApi = async (ndmNvr: NdmNvrResultVO, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const getChannelListApi = async (ndmNvr: NdmNvrResultVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
@@ -12,7 +12,7 @@ export const getChannelListApi = async (ndmNvr: NdmNvrResultVO, options?: { stat
return data;
};
export const getRecordCheckApi = async (ndmNvr: NdmNvrResultVO, lastDays: number, gbCodeList: string[], options?: { stationCode?: string; signal?: AbortSignal }) => {
export const getRecordCheckApi = async (ndmNvr: NdmNvrResultVO, lastDays: number, gbCodeList: string[], options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
@@ -27,7 +27,7 @@ export const getRecordCheckApi = async (ndmNvr: NdmNvrResultVO, lastDays: number
return data;
};
export const reloadRecordCheckApi = async (channel: ClientChannel, dayOffset: number, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const reloadRecordCheckApi = async (channel: ClientChannel, dayOffset: number, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
@@ -37,7 +37,7 @@ export const reloadRecordCheckApi = async (channel: ClientChannel, dayOffset: nu
return data;
};
export const reloadAllRecordCheckApi = async (dayOffset: number, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const reloadAllRecordCheckApi = async (dayOffset: number, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,17 @@
import { ndmClient, userClient, type NdmCameraIgnorePageQuery, type NdmCameraIgnoreResultVO, type NdmCameraIgnoreSaveVO, type NdmCameraIgnoreUpdateVO, type PageParams, type PageResult } from '@/apis';
import {
ndmClient,
userClient,
type NdmCameraIgnorePageQuery,
type NdmCameraIgnoreResultVO,
type NdmCameraIgnoreSaveVO,
type NdmCameraIgnoreUpdateVO,
type PageParams,
type PageResult,
type Station,
} from '@/apis';
import { unwrapResponse } from '@/utils';
export const pageCameraIgnoreApi = async (pageQuery: PageParams<NdmCameraIgnorePageQuery>, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const pageCameraIgnoreApi = async (pageQuery: PageParams<NdmCameraIgnorePageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
@@ -11,7 +21,7 @@ export const pageCameraIgnoreApi = async (pageQuery: PageParams<NdmCameraIgnoreP
return data;
};
export const detailCameraIgnoreApi = async (id: string, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const detailCameraIgnoreApi = async (id: string, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
@@ -21,7 +31,7 @@ export const detailCameraIgnoreApi = async (id: string, options?: { stationCode?
return data;
};
export const saveCameraIgnoreApi = async (saveVO: NdmCameraIgnoreSaveVO, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const saveCameraIgnoreApi = async (saveVO: NdmCameraIgnoreSaveVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
@@ -31,7 +41,7 @@ export const saveCameraIgnoreApi = async (saveVO: NdmCameraIgnoreSaveVO, options
return data;
};
export const updateCameraIgnoreApi = async (updateVO: NdmCameraIgnoreUpdateVO, options?: { stationCode?: string; signal?: AbortSignal }) => {
export const updateCameraIgnoreApi = async (updateVO: NdmCameraIgnoreUpdateVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
@@ -41,7 +51,7 @@ export const updateCameraIgnoreApi = async (updateVO: NdmCameraIgnoreUpdateVO, o
return data;
};
export const deleteCameraIgnoreApi = async (ids: string[], options?: { stationCode?: string; signal?: AbortSignal }) => {
export const deleteCameraIgnoreApi = async (ids: string[], options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';

View File

@@ -93,7 +93,7 @@ export const getCameraSnapApi = async (deviceId: string, options?: { signal?: Ab
return data;
};
export const syncCameraApi = async (options?: { stationCode?: string; signal?: AbortSignal }) => {
export const syncCameraApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';

View File

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

View File

@@ -252,7 +252,7 @@ const renderIcmpStatistics = (onlineCount: number, offlineCount: number, count:
]);
};
const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
const renderViewDeviceButton = (device: NdmDeviceResultVO, stationCode: string) => {
const renderViewDeviceButton = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
if (!devicePrefixLabel.value) return null;
return h(
NButton,

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { LineAlarms, LineDevices, Station, VersionInfo } from '@/apis';
import { retentionDaysApi, snapStatusApi, type LineAlarms, type LineDevices, type Station, type VersionInfo } from '@/apis';
import { ThemeSwitch } from '@/components';
import { NDM_ALARM_STORE_ID, NDM_DEVICE_STORE_ID, NDM_STATION_STORE_ID } from '@/constants';
import { usePollingStore, useSettingStore } from '@/stores';
@@ -7,16 +7,13 @@ import { downloadByData, getAppEnvConfig, parseErrorFeedback, sleep } from '@/ut
import { useMutation } from '@tanstack/vue-query';
import { DeleteOutlined, ExportOutlined, ImportOutlined } from '@vicons/antd';
import { useEventListener } from '@vueuse/core';
import axios from 'axios';
import axios, { isCancel } from 'axios';
import destr from 'destr';
import { isFunction } from 'es-toolkit';
import localforage from 'localforage';
import { NButton, NDivider, NDrawer, NDrawerContent, NDropdown, NFlex, NFormItem, NIcon, NInput, NInputNumber, NModal, NSwitch, NText, type DropdownOption } from 'naive-ui';
import { NButton, NButtonGroup, NDivider, NDrawer, NDrawerContent, NDropdown, NFlex, NFormItem, NIcon, NInput, NInputNumber, NModal, NSwitch, NText, type DropdownOption } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
import { ref, watch } from 'vue';
const show = defineModel<boolean>('show', { default: false });
@@ -40,6 +37,83 @@ const { mutate: getVersionInfo } = useMutation({
},
});
const abortControllers = ref({
retentionDays: new AbortController(),
snapStatus: new AbortController(),
});
const retentionDays = ref(0);
const { mutate: getRetentionDays, isPending: retentionDaysLoading } = useMutation({
mutationFn: async () => {
abortControllers.value.retentionDays.abort();
abortControllers.value.retentionDays = new AbortController();
const signal = abortControllers.value.retentionDays.signal;
const days = await retentionDaysApi('get', { signal });
return days;
},
onSuccess: (days) => {
retentionDays.value = days;
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const { mutate: saveRetentionDays, isPending: retentionDaysSaving } = useMutation({
mutationFn: async () => {
abortControllers.value.retentionDays.abort();
abortControllers.value.retentionDays = new AbortController();
const signal = abortControllers.value.retentionDays.signal;
await retentionDaysApi('post', { days: retentionDays.value, signal });
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
// 修改失败,刷新 retentionDays
getRetentionDays();
},
});
const snapStatus = ref(false);
const { mutate: getSnapStatus, isPending: snapStatusLoading } = useMutation({
mutationFn: async () => {
abortControllers.value.snapStatus.abort();
abortControllers.value.snapStatus = new AbortController();
const signal = abortControllers.value.snapStatus.signal;
const status = await snapStatusApi('get', { signal });
return status;
},
onSuccess: (status) => {
snapStatus.value = status;
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const { mutate: saveSnapStatus, isPending: snapStatusSaving } = useMutation({
mutationFn: async () => {
abortControllers.value.snapStatus.abort();
abortControllers.value.snapStatus = new AbortController();
const signal = abortControllers.value.snapStatus.signal;
await snapStatusApi('post', { doSnap: snapStatus.value, signal });
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
// 修改失败,刷新 snapStatus
getSnapStatus();
},
});
const showDebugCodeModal = ref(false);
const debugCode = ref('');
const enableDebugMode = () => {
@@ -192,13 +266,24 @@ const onSelectDropdownOption = (key: string, option: DropdownOption) => {
}
};
onMounted(() => {
getVersionInfo();
watch([offlineDev, show], ([offline, entered]) => {
if (!offline) {
if (entered) {
getRetentionDays();
getSnapStatus();
} else {
abortControllers.value.retentionDays.abort();
abortControllers.value.snapStatus.abort();
}
}
});
const onDrawerAfterEnter = () => {
getVersionInfo();
};
</script>
<template>
<NDrawer v-model:show="show" :width="560" :auto-focus="false">
<NDrawer v-model:show="show" :width="560" :auto-focus="false" @after-enter="onDrawerAfterEnter">
<NDrawerContent closable title="系统设置" :native-scrollbar="false">
<NFlex vertical>
<NDivider>主题</NDivider>
@@ -210,11 +295,29 @@ onMounted(() => {
<NFormItem label="折叠菜单" label-placement="left">
<NSwitch size="small" v-model:value="menuCollpased" />
</NFormItem>
<template v-if="route.path === '/station'">
<NFormItem label="车站列数" label-placement="left">
<NInputNumber v-model:value="stationGridCols" :min="1" :max="10" />
</NFormItem>
</template>
<NFormItem label="车站列数" label-placement="left">
<NInputNumber v-model:value="stationGridCols" :min="1" :max="10" />
</NFormItem>
<NDivider>告警</NDivider>
<NFormItem label="告警画面截图保留天数" label-placement="left">
<NFlex justify="space-between" align="center" style="width: 100%">
<NInputNumber v-model:value="retentionDays" :min="1" :max="15" />
<NButtonGroup>
<NButton secondary size="small" :disabled="retentionDaysSaving" :loading="retentionDaysLoading" @click="() => getRetentionDays()">刷新</NButton>
<NButton secondary size="small" :disabled="retentionDaysLoading" :loading="retentionDaysSaving" @click="() => saveRetentionDays()">保存</NButton>
</NButtonGroup>
</NFlex>
</NFormItem>
<NFormItem label="自动获取告警画面截图" label-placement="left">
<NFlex justify="space-between" align="center" style="width: 100%">
<NSwitch size="small" v-model:value="snapStatus" />
<NButtonGroup>
<NButton secondary size="small" :disabled="snapStatusSaving" :loading="snapStatusLoading" @click="() => getSnapStatus()">刷新</NButton>
<NButton secondary size="small" :disabled="snapStatusLoading" :loading="snapStatusSaving" @click="() => saveSnapStatus()">保存</NButton>
</NButtonGroup>
</NFlex>
</NFormItem>
<template v-if="debugModeEnabled">
<NDivider title-placement="center">调试</NDivider>

View File

@@ -59,7 +59,7 @@ const tableColumns = ref<DataTableBaseColumn<NdmDeviceAlarmLogResultVO>[]>([
const stationDevices = lineDevices.value[stationCode];
if (!stationDevices) return;
const classified = stationDevices[deviceType];
const device = classified.find((device) => device.deviceId === rowData.deviceId);
const device = classified.find((device) => !!device.deviceId && device.deviceId === rowData.deviceId);
if (!device) return;
const deviceDbId = device.id;
router.push({

View File

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

View File

@@ -77,9 +77,19 @@ const menuOptions: MenuOption[] = [
icon: renderIcon(HddFilled),
},
{
label: () => h(RouterLink, { to: '/alarm' }, { default: () => '设备告警' }),
label: '设备告警',
key: '/alarm',
icon: renderIcon(AlertFilled),
children: [
{
label: () => h(RouterLink, { to: '/alarm/alarm-log' }, { default: () => '设备告警记录' }),
key: '/alarm/alarm-log',
},
{
label: () => h(RouterLink, { to: '/alarm/alarm-ignore' }, { default: () => '告警忽略管理' }),
key: '/alarm/alarm-ignore',
},
],
},
{
label: '系统日志',

View File

@@ -0,0 +1,314 @@
<script lang="ts">
const NDM_TYPES: Record<string, DeviceType> = {
'01': DEVICE_TYPE_LITERALS.ndmAlarmHost,
// '02': '报警源',
'03': DEVICE_TYPE_LITERALS.ndmSecurityBox,
'04': DEVICE_TYPE_LITERALS.ndmSwitch,
'05': DEVICE_TYPE_LITERALS.ndmNvr,
'06': DEVICE_TYPE_LITERALS.ndmCamera,
'07': DEVICE_TYPE_LITERALS.ndmDecoder,
'08': DEVICE_TYPE_LITERALS.ndmKeyboard,
'09': DEVICE_TYPE_LITERALS.ndmMediaServer,
// '10': '显示器',
'11': DEVICE_TYPE_LITERALS.ndmVideoServer,
};
</script>
<script setup lang="ts">
import { deleteCameraIgnoreApi, pageCameraIgnoreApi, type NdmCameraIgnore, type NdmCameraIgnoreResultVO, type PageQueryExtra, type Station } from '@/apis';
import { DEVICE_TYPE_LITERALS, DEVICE_TYPE_NAMES, type DeviceType } from '@/enums';
import { useDeviceStore, useStationStore } from '@/stores';
import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import {
NButton,
NDataTable,
NDatePicker,
NFlex,
NForm,
NFormItemGi,
NGrid,
NGridItem,
NPopconfirm,
NSelect,
type DataTableColumns,
type DataTableRowData,
type PaginationProps,
type SelectOption,
} from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, h, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
interface SearchFields extends PageQueryExtra<NdmCameraIgnore> {
createdTime?: [string, string];
updatedTime?: [string, string];
stationCode?: Station['code'];
// deviceId_like?: string;
}
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
const stationSelectOptions = computed<SelectOption[]>(() => {
return stations.value.map((station) => ({
label: station.name,
value: station.code,
}));
});
// const deviceTypeSelectOptions = computed<SelectOption[]>(() => {
// return Object.values(DEVICE_TYPE_LITERALS).map<SelectOption>((deviceType) => ({
// label: DEVICE_TYPE_NAMES[deviceType],
// value: deviceType,
// }));
// });
const searchFields = ref<SearchFields>({});
const resetSearchFields = () => {
searchFields.value = {};
};
const getExtraFields = (): PageQueryExtra<NdmCameraIgnore> => {
const [createdTime_precisest, createdTime_preciseed] = searchFields.value.createdTime ?? [];
const stationCode = searchFields.value.stationCode;
return {
createdTime_precisest,
createdTime_preciseed,
deviceId_like: stationCode?.slice(0, 4),
};
};
const searchFieldsChanged = ref(false);
watch(searchFields, () => {
searchFieldsChanged.value = true;
});
const tableColumns: DataTableColumns<NdmCameraIgnoreResultVO> = [
{ title: '忽略时间', key: 'createdTime', align: 'center' },
// { title: '更新时间', key: 'updatedTime' },
{
title: '车站',
key: 'stationCode',
align: 'center',
render: (rowData) => {
const stationCode = rowData.deviceId?.slice(0, 4);
if (!stationCode) return '';
const station = stations.value.find((station) => station.code === stationCode);
if (!station) return '';
return station.name;
},
},
// { title: '设备ID', key: 'deviceId', align: 'center' },
// { title: '忽略类型', key: 'ignoreType', align: 'center' },
{
title: '设备类型',
key: 'deviceType',
align: 'center',
render: (rowData) => {
const UNKNOWN_NAME = '-';
const ndmType = rowData.deviceId?.slice(4, 6);
if (!ndmType) return UNKNOWN_NAME;
const deviceType = NDM_TYPES[ndmType];
if (!deviceType) return UNKNOWN_NAME;
return DEVICE_TYPE_NAMES[deviceType];
},
},
{
title: '设备名称',
key: 'deviceName',
align: 'center',
render: (rowData) => {
const UNKNOWN_NAME = '-';
const { deviceId } = rowData;
if (!deviceId) return UNKNOWN_NAME;
const stationCode = deviceId.slice(0, 4);
if (!stationCode) return UNKNOWN_NAME;
const stationDevices = lineDevices.value[stationCode];
if (!stationDevices) return UNKNOWN_NAME;
const ndmType = rowData.deviceId?.slice(4, 6);
if (!ndmType) return UNKNOWN_NAME;
const deviceType = NDM_TYPES[ndmType];
if (!deviceType) return UNKNOWN_NAME;
const classified = stationDevices[deviceType];
const device = classified.find((device) => device.deviceId === deviceId);
if (!device) return UNKNOWN_NAME;
return device.name ?? UNKNOWN_NAME;
},
},
{
title: '操作',
key: 'action',
align: 'center',
width: 120,
render: (rowData) => {
return h(
NPopconfirm,
{
onPositiveClick: () => {
console.log(rowData);
const { id } = rowData;
cancelIgnore({ id });
},
},
{
trigger: () => {
return h(
NButton,
{
size: 'small',
secondary: true,
},
{ default: () => `取消忽略` },
);
},
default: () => `确认取消忽略吗?`,
},
);
},
},
];
const { mutate: cancelIgnore } = useMutation({
mutationFn: async (params: { id?: string; signal?: AbortSignal }) => {
const { id, signal } = params;
if (!id) return;
return deleteCameraIgnoreApi([id], { signal });
},
onSuccess: (data) => {
if (!data) {
window.$message.error('取消忽略失败,请再试一次');
return;
}
window.$message.success('取消忽略成功');
getTableData();
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
window.$message.error(error.message);
},
});
const tableData = ref<DataTableRowData[]>([]);
const DEFAULT_PAGE_SIZE = 10;
const pagination = reactive<PaginationProps>({
showSizePicker: true,
page: 1,
pageSize: DEFAULT_PAGE_SIZE,
pageSizes: [5, 10, 20, 50, 80, 100],
itemCount: 0,
prefix: ({ itemCount }) => {
return h('div', {}, { default: () => `${itemCount}` });
},
onUpdatePage: (page: number) => {
pagination.page = page;
getTableData();
},
onUpdatePageSize: (pageSize: number) => {
pagination.pageSize = pageSize;
pagination.page = 1;
getTableData();
},
});
const abortController = ref(new AbortController());
const { mutate: getTableData, isPending: tableLoading } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
const signal = abortController.value.signal;
const res = await pageCameraIgnoreApi(
{
model: {},
extra: getExtraFields(),
current: pagination.page ?? 1,
size: pagination.pageSize ?? DEFAULT_PAGE_SIZE,
sort: 'id',
order: 'descending',
},
{
signal,
},
);
return res;
},
onSuccess: (res) => {
const { records, size, total } = res;
pagination.pageSize = parseInt(size);
pagination.itemCount = parseInt(total);
tableData.value = records;
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
window.$message.error(error.message);
},
});
const onClickReset = () => {
resetSearchFields();
pagination.page = 1;
pagination.pageSize = DEFAULT_PAGE_SIZE;
pagination.itemCount = 0;
getTableData();
};
const onClickQuery = () => {
if (searchFieldsChanged.value) {
pagination.page = 1;
pagination.pageSize = DEFAULT_PAGE_SIZE;
searchFieldsChanged.value = false;
}
getTableData();
};
onMounted(() => {
getTableData();
});
onBeforeUnmount(() => {
abortController.value.abort();
});
</script>
<template>
<NFlex vertical :size="0" style="height: 100%">
<!-- 查询面板 -->
<NForm style="flex: 0 0 auto; padding: 8px">
<NGrid cols="3" :x-gap="24">
<NFormItemGi span="1" label="车站" label-placement="left">
<NSelect clearable placeholder="请选择车站" v-model:value="searchFields.stationCode" :options="stationSelectOptions" />
</NFormItemGi>
<NFormItemGi span="2" label="忽略时间" label-placement="left">
<NDatePicker v-model:formatted-value="searchFields.createdTime" type="datetimerange" />
</NFormItemGi>
</NGrid>
<!-- 操作按钮 -->
<NGrid :cols="1">
<NGridItem>
<NFlex>
<NButton @click="onClickReset">重置</NButton>
<NButton type="primary" :loading="tableLoading" @click="onClickQuery">查询</NButton>
</NFlex>
</NGridItem>
</NGrid>
</NForm>
<!-- 数据表格工具栏 -->
<NFlex align="center" style="padding: 8px; flex: 0 0 auto">
<div style="font-size: medium">视频平台日志</div>
<NFlex style="margin-left: auto">
<!-- <NButton type="primary" :loading="exporting" @click="() => exportTableData()">导出</NButton> -->
</NFlex>
</NFlex>
<!-- 数据表格 -->
<NDataTable remote :columns="tableColumns" :data="tableData" :pagination="pagination" :loading="tableLoading" :single-line="false" flex-height style="height: 100%; padding: 8px; flex: 1 1 auto" />
</NFlex>
</template>
<style scoped lang="scss"></style>

View File

@@ -7,6 +7,7 @@ import { useAlarmStore, useDeviceStore, useStationStore } from '@/stores';
import { downloadByData, parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { watchDebounced } from '@vueuse/core';
import { isCancel } from 'axios';
import dayjs from 'dayjs';
import {
NButton,
@@ -26,7 +27,7 @@ import {
type SelectOption,
} from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, h, onBeforeMount, reactive, ref, watch, type CSSProperties } from 'vue';
import { computed, h, onBeforeMount, onBeforeUnmount, reactive, ref, watch, type CSSProperties } from 'vue';
import { useRoute, useRouter } from 'vue-router';
interface SearchFields extends PageQueryExtra<NdmDeviceAlarmLog> {
@@ -138,7 +139,7 @@ const tableData = ref<DataTableRowData[]>([]);
const { cameraSnapColumn } = useCameraSnapColumn(tableData);
const { alarmActionColumn } = useAlarmActionColumn(tableData);
const tableColumns: DataTableColumns<NdmDeviceAlarmLogResultVO & { snapUrl?: string }> = [
const tableColumns: DataTableColumns<NdmDeviceAlarmLogResultVO> = [
// { title: 'ID', key: 'deviceId' },
// { title: '', key: 'faultCode', align: 'center' },
// { title: '', key: 'faultLocation' },
@@ -162,7 +163,7 @@ const tableColumns: DataTableColumns<NdmDeviceAlarmLogResultVO & { snapUrl?: str
const stationDevices = lineDevices.value[stationCode];
if (!stationDevices) return;
const classified = stationDevices[deviceType];
const device = classified.find((device) => device.deviceId === rowData.deviceId);
const device = classified.find((device) => !!device.deviceId && device.deviceId === rowData.deviceId);
if (!device) return;
const deviceDbId = device.id;
router.push({
@@ -211,16 +212,29 @@ const pagination = reactive<PaginationProps>({
},
});
const abortController = ref(new AbortController());
const { mutate: getTableData, isPending: tableLoading } = useMutation({
mutationFn: async () => {
const res = await pageDeviceAlarmLogApi({
model: {},
extra: getExtraFields(),
current: pagination.page ?? 1,
size: pagination.pageSize ?? DEFAULT_PAGE_SIZE,
sort: 'id',
order: 'descending',
});
abortController.value.abort();
abortController.value = new AbortController();
const signal = abortController.value.signal;
const res = await pageDeviceAlarmLogApi(
{
model: {},
extra: getExtraFields(),
current: pagination.page ?? 1,
size: pagination.pageSize ?? DEFAULT_PAGE_SIZE,
sort: 'id',
order: 'descending',
},
{
signal,
},
);
return res;
},
onSuccess: (res) => {
@@ -230,6 +244,7 @@ const { mutate: getTableData, isPending: tableLoading } = useMutation({
tableData.value = records;
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
@@ -256,14 +271,24 @@ const onClickQuery = () => {
const { mutate: exportTableData, isPending: exporting } = useMutation({
mutationFn: async () => {
const data = await exportDeviceAlarmLogApi({
model: {},
extra: getExtraFields(),
current: pagination.page ?? 1,
size: pagination.pageSize ?? 10,
order: 'descending',
sort: 'id',
});
abortController.value.abort();
abortController.value = new AbortController();
const signal = abortController.value.signal;
const data = await exportDeviceAlarmLogApi(
{
model: {},
extra: getExtraFields(),
current: pagination.page ?? 1,
size: pagination.pageSize ?? 10,
order: 'descending',
sort: 'id',
},
{
signal,
},
);
return data;
},
onSuccess: (data) => {
@@ -271,6 +296,7 @@ const { mutate: exportTableData, isPending: exporting } = useMutation({
downloadByData(data, `设备告警记录_${time}.xlsx`);
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
@@ -280,6 +306,9 @@ const { mutate: exportTableData, isPending: exporting } = useMutation({
onBeforeMount(() => {
getTableData();
});
onBeforeUnmount(() => {
abortController.value.abort();
});
</script>
<template>

View File

@@ -3,6 +3,7 @@ import { exportCallLogApi, pageCallLogApi, type NdmCallLog, type NdmCallLogResul
import { useStationStore } from '@/stores';
import { downloadByData, parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import dayjs from 'dayjs';
import {
NButton,
@@ -21,7 +22,7 @@ import {
type SelectOption,
} from 'naive-ui';
import { storeToRefs } from 'pinia';
import { h } from 'vue';
import { h, onBeforeUnmount } from 'vue';
import { computed, reactive, ref, watch, watchEffect } from 'vue';
interface SearchFields extends PageQueryExtra<NdmCallLog> {
@@ -105,9 +106,18 @@ const pagination = reactive<PaginationProps>({
},
});
const abortController = ref(new AbortController());
const { mutate: getTableData, isPending: tableLoading } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
if (!searchFields.value.stationCode) throw Error('请选择车站');
const stationCode = searchFields.value.stationCode;
const signal = abortController.value.signal;
const res = await pageCallLogApi(
{
model: {},
@@ -118,7 +128,8 @@ const { mutate: getTableData, isPending: tableLoading } = useMutation({
sort: 'id',
},
{
stationCode: searchFields.value.stationCode,
stationCode,
signal,
},
);
return res;
@@ -130,6 +141,7 @@ const { mutate: getTableData, isPending: tableLoading } = useMutation({
tableData.value = records;
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
@@ -154,7 +166,14 @@ const onClickQuery = () => {
const { mutate: exportTableData, isPending: exporting } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
if (!searchFields.value.stationCode) throw Error('请选择车站');
const stationCode = searchFields.value.stationCode;
const signal = abortController.value.signal;
const data = await exportCallLogApi(
{
model: {},
@@ -165,7 +184,8 @@ const { mutate: exportTableData, isPending: exporting } = useMutation({
sort: 'id',
},
{
stationCode: searchFields.value.stationCode,
stationCode,
signal,
},
);
return data;
@@ -175,6 +195,7 @@ const { mutate: exportTableData, isPending: exporting } = useMutation({
downloadByData(data, `上级调用日志_${time}.xlsx`);
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
@@ -204,6 +225,10 @@ watch(
immediate: true,
},
);
onBeforeUnmount(() => {
abortController.value.abort();
});
</script>
<template>

View File

@@ -32,6 +32,7 @@ import { exportVimpLogApi, pageVimpLogApi, type NdmVimpLog, type NdmVimpLogResul
import { useStationStore } from '@/stores';
import { downloadByData, parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import dayjs from 'dayjs';
import destr from 'destr';
import {
@@ -53,7 +54,7 @@ import {
type SelectOption,
} from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, h, reactive, ref, watch, watchEffect } from 'vue';
import { computed, h, onBeforeUnmount, reactive, ref, watch, watchEffect } from 'vue';
interface SearchFields extends PageQueryExtra<NdmVimpLog> {
stationCode?: Station['code'];
@@ -176,9 +177,18 @@ const pagination = reactive<PaginationProps>({
},
});
const abortController = ref(new AbortController());
const { mutate: getTableData, isPending: tableLoading } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
if (!searchFields.value.stationCode) throw Error('请选择车站');
const stationCode = searchFields.value.stationCode;
const signal = abortController.value.signal;
const res = await pageVimpLogApi(
{
model: {},
@@ -189,7 +199,8 @@ const { mutate: getTableData, isPending: tableLoading } = useMutation({
sort: 'id',
},
{
stationCode: searchFields.value.stationCode,
stationCode,
signal,
},
);
return res;
@@ -201,6 +212,7 @@ const { mutate: getTableData, isPending: tableLoading } = useMutation({
tableData.value = records;
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
@@ -225,7 +237,14 @@ const onClickQuery = () => {
const { mutate: exportTableData, isPending: exporting } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
if (!searchFields.value.stationCode) throw Error('请选择车站');
const stationCode = searchFields.value.stationCode;
const signal = abortController.value.signal;
const data = await exportVimpLogApi(
{
model: {},
@@ -236,7 +255,8 @@ const { mutate: exportTableData, isPending: exporting } = useMutation({
sort: 'id',
},
{
stationCode: searchFields.value.stationCode,
stationCode,
signal,
},
);
return data;
@@ -246,6 +266,7 @@ const { mutate: exportTableData, isPending: exporting } = useMutation({
downloadByData(data, `视频操作日志_${time}.xlsx`);
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
@@ -275,6 +296,10 @@ watch(
immediate: true,
},
);
onBeforeUnmount(() => {
abortController.value.abort();
});
</script>
<template>

View File

@@ -23,7 +23,16 @@ const router = createRouter({
},
{
path: 'alarm',
component: () => import('@/pages/alarm-page.vue'),
children: [
{
path: 'alarm-log',
component: () => import('@/pages/alarm-log-page.vue'),
},
{
path: 'alarm-ignore',
component: () => import('@/pages/alarm-ignore-page.vue'),
},
],
},
{
path: 'log',