refactor: 重构项目结构
- 优化 `车站-设备-告警` 轮询机制 - 改进设备卡片的布局 - 支持修改设备 - 告警轮询中获取完整告警数据 - 车站告警详情支持导出完整的 `今日告警列表` - 支持将状态持久化到 `IndexedDB` - 新增轮询控制 (调试模式) - 新增离线开发模式 (调试模式) - 新增 `IndexedDB` 数据控制 (调试模式)
This commit is contained in:
6
src/components/device/device-card/ndm-server/index.ts
Normal file
6
src/components/device/device-card/ndm-server/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import ServerCard from './server-card.vue';
|
||||
import ServerCurrentDiag from './server-current-diag.vue';
|
||||
import ServerHistoryDiag from './server-history-diag.vue';
|
||||
import ServerUpdate from './server-update.vue';
|
||||
|
||||
export { ServerCard, ServerCurrentDiag, ServerHistoryDiag, ServerUpdate };
|
||||
71
src/components/device/device-card/ndm-server/server-card.vue
Normal file
71
src/components/device/device-card/ndm-server/server-card.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import type { NdmServerResultVO, Station } from '@/apis';
|
||||
import { DeviceRawCard, ServerCurrentDiag, ServerHistoryDiag, ServerUpdate } from '@/components';
|
||||
import { useSettingStore } from '@/stores';
|
||||
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, ref, toRefs, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const props = defineProps<{
|
||||
ndmDevice: NdmServerResultVO;
|
||||
station: Station;
|
||||
}>();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const settingStore = useSettingStore();
|
||||
const { debugModeEnabled } = storeToRefs(settingStore);
|
||||
|
||||
const { ndmDevice, station } = toRefs(props);
|
||||
|
||||
const showPageHeader = computed(() => {
|
||||
return !!route.query['from'];
|
||||
});
|
||||
const onBack = () => {
|
||||
router.push({ path: `${route.query['from']}` });
|
||||
};
|
||||
|
||||
const activeTabName = ref('当前诊断');
|
||||
const onTabChange = (name: string) => {
|
||||
activeTabName.value = name;
|
||||
};
|
||||
watch([ndmDevice, debugModeEnabled], ([newDevice, enabled], [oldDevice]) => {
|
||||
if (newDevice.id !== oldDevice.id || !enabled) {
|
||||
activeTabName.value = '当前诊断';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard hoverable style="height: 100%" :header-style="{ padding: '12px' }" :content-style="{ height: '100%', padding: '0', overflow: 'hidden' }">
|
||||
<template #header>
|
||||
<NPageHeader v-if="showPageHeader" @back="onBack" />
|
||||
<NTabs :value="activeTabName" @update:value="onTabChange">
|
||||
<NTab name="当前诊断">当前诊断</NTab>
|
||||
<NTab name="历史诊断">历史诊断</NTab>
|
||||
<NTab name="修改设备">修改设备</NTab>
|
||||
<NTab v-if="debugModeEnabled" name="原始数据">原始数据</NTab>
|
||||
</NTabs>
|
||||
</template>
|
||||
<template #default>
|
||||
<NScrollbar x-scrollable :content-style="{ padding: '0 12px 12px 12px' }">
|
||||
<template v-if="activeTabName === '当前诊断'">
|
||||
<ServerCurrentDiag :ndm-device="ndmDevice" :station="station" />
|
||||
</template>
|
||||
<template v-if="activeTabName === '历史诊断'">
|
||||
<ServerHistoryDiag :ndm-device="ndmDevice" :station="station" />
|
||||
</template>
|
||||
<template v-if="activeTabName === '修改设备'">
|
||||
<ServerUpdate :ndm-device="ndmDevice" :station="station" />
|
||||
</template>
|
||||
<template v-if="activeTabName === '原始数据'">
|
||||
<DeviceRawCard :ndm-device="ndmDevice" />
|
||||
</template>
|
||||
</NScrollbar>
|
||||
</template>
|
||||
</NCard>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import type { NdmServerDiagInfo, NdmServerResultVO, Station } from '@/apis';
|
||||
import { DeviceHardwareCard, DeviceHeaderCard } from '@/components';
|
||||
import destr from 'destr';
|
||||
import { NFlex } from 'naive-ui';
|
||||
import { computed, toRefs } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
ndmDevice: NdmServerResultVO;
|
||||
station: Station;
|
||||
}>();
|
||||
|
||||
const { ndmDevice, station } = toRefs(props);
|
||||
|
||||
const lastDiagInfo = computed(() => {
|
||||
const result = destr<any>(ndmDevice.value.lastDiagInfo);
|
||||
if (!result) return null;
|
||||
if (typeof result !== 'object') return null;
|
||||
return result as NdmServerDiagInfo;
|
||||
});
|
||||
|
||||
const cpuUsage = computed(() => lastDiagInfo.value?.commInfo?.CPU使用率);
|
||||
const memUsage = computed(() => lastDiagInfo.value?.commInfo?.内存使用率);
|
||||
const diskUsage = computed(() => lastDiagInfo.value?.commInfo?.磁盘使用率);
|
||||
const runningTime = computed(() => lastDiagInfo.value?.commInfo?.系统运行时间);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NFlex vertical>
|
||||
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
|
||||
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" :disk-usage="diskUsage" :running-time="runningTime" />
|
||||
</NFlex>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import type { NdmServerResultVO, Station } from '@/apis';
|
||||
import {
|
||||
DeviceAlarmHistoryCard,
|
||||
DeviceIcmpHistoryCard,
|
||||
DeviceUsageHistoryCard,
|
||||
HistoryDiagFilterCard,
|
||||
type DeviceAlarmHistoryCardProps,
|
||||
type DeviceIcmpHistoryCardProps,
|
||||
type DeviceUsageHistoryCardProps,
|
||||
} from '@/components';
|
||||
import dayjs from 'dayjs';
|
||||
import { NFlex, type SelectOption } from 'naive-ui';
|
||||
import { onMounted, ref, toRefs, watch } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
ndmDevice: NdmServerResultVO;
|
||||
station: Station;
|
||||
}>();
|
||||
|
||||
const { ndmDevice, station } = toRefs(props);
|
||||
|
||||
const historyDiagOptions: SelectOption[] = [
|
||||
{ label: '设备状态', value: 'icmp' },
|
||||
{ label: '设备告警', value: 'alarm' },
|
||||
{ label: '硬件占用', value: 'usage' },
|
||||
];
|
||||
const getWeekRange = (): [number, number] => {
|
||||
const now = dayjs();
|
||||
const todayEnd = now.endOf('date');
|
||||
const weekAgo = now.subtract(1, 'week').startOf('date');
|
||||
return [weekAgo.valueOf(), todayEnd.valueOf()];
|
||||
};
|
||||
const range = ref<[number, number]>(getWeekRange());
|
||||
const selected = ref<string[]>([...historyDiagOptions.map((option) => `${option.value}`)]);
|
||||
const loading = ref<boolean>(false);
|
||||
const icmpLoading = ref<boolean>(false);
|
||||
const alarmLoading = ref<boolean>(false);
|
||||
const usageLoading = ref<boolean>(false);
|
||||
watch([icmpLoading, alarmLoading, usageLoading], (loadings) => {
|
||||
loading.value = loadings.some((loading) => loading);
|
||||
});
|
||||
const icmpHistoryQueryFn = ref<() => void>();
|
||||
const onExposeIcmpHistoryQueryFn: DeviceIcmpHistoryCardProps['onExposeQueryFn'] = (queryFn) => {
|
||||
icmpHistoryQueryFn.value = queryFn;
|
||||
};
|
||||
const alarmHistoryQueryFn = ref<() => void>();
|
||||
const onExposeAlarmHistoryQueryFn: DeviceAlarmHistoryCardProps['onExposeQueryFn'] = (queryFn) => {
|
||||
alarmHistoryQueryFn.value = queryFn;
|
||||
};
|
||||
const usageHistoryQueryFn = ref<() => void>();
|
||||
const onExposeUsageHistoryQueryFn: DeviceUsageHistoryCardProps['onExposeQueryFn'] = (queryFn) => {
|
||||
usageHistoryQueryFn.value = queryFn;
|
||||
};
|
||||
const queryData = () => {
|
||||
if (selected.value.includes('icmp')) icmpHistoryQueryFn.value?.();
|
||||
if (selected.value.includes('alarm')) alarmHistoryQueryFn.value?.();
|
||||
if (selected.value.includes('usage')) usageHistoryQueryFn.value?.();
|
||||
};
|
||||
const onQuery = () => {
|
||||
queryData();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
queryData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NFlex vertical>
|
||||
<HistoryDiagFilterCard :options="historyDiagOptions" v-model:loading="loading" v-model:range="range" v-model:selected="selected" @query="onQuery" />
|
||||
<DeviceIcmpHistoryCard
|
||||
v-if="selected.includes('icmp')"
|
||||
:ndm-device="ndmDevice"
|
||||
:station="station"
|
||||
v-model:range="range"
|
||||
v-model:loading="icmpLoading"
|
||||
@expose-query-fn="onExposeIcmpHistoryQueryFn"
|
||||
/>
|
||||
<DeviceAlarmHistoryCard
|
||||
v-if="selected.includes('alarm')"
|
||||
:ndm-device="ndmDevice"
|
||||
:station="station"
|
||||
v-model:range="range"
|
||||
v-model:loading="alarmLoading"
|
||||
@expose-query-fn="onExposeAlarmHistoryQueryFn"
|
||||
/>
|
||||
<DeviceUsageHistoryCard
|
||||
v-if="selected.includes('usage')"
|
||||
:ndm-device="ndmDevice"
|
||||
:station="station"
|
||||
:cpu-usage-field="'commInfo.CPU使用率'"
|
||||
:mem-usage-field="'commInfo.内存使用率'"
|
||||
:disk-usage-field="'commInfo.磁盘使用率'"
|
||||
v-model:range="range"
|
||||
v-model:loading="usageLoading"
|
||||
@expose-query-fn="onExposeUsageHistoryQueryFn"
|
||||
/>
|
||||
</NFlex>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
164
src/components/device/device-card/ndm-server/server-update.vue
Normal file
164
src/components/device/device-card/ndm-server/server-update.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
detailMediaServerApi,
|
||||
detailVideoServerApi,
|
||||
icmpEntityByDeviceId,
|
||||
updateMediaServerApi,
|
||||
updateVideoServerApi,
|
||||
type NdmMediaServerUpdateVO,
|
||||
type NdmServerResultVO,
|
||||
type NdmServerUpdateVO,
|
||||
type NdmVideoServerUpdateVO,
|
||||
type Station,
|
||||
} from '@/apis';
|
||||
import { DEVICE_TYPE_LITERALS, tryGetDeviceType } from '@/enums';
|
||||
import { useDeviceStore } from '@/stores';
|
||||
import { parseErrorFeedback } from '@/utils';
|
||||
import { useMutation } from '@tanstack/vue-query';
|
||||
import { isCancel } from 'axios';
|
||||
import destr from 'destr';
|
||||
import { isString } from 'es-toolkit';
|
||||
import { NButton, NCard, NFlex, NForm, NFormItem, NFormItemGi, NGrid, NInput, NSwitch, type FormInst, type FormRules } from 'naive-ui';
|
||||
import { computed, onBeforeUnmount, ref, toRefs, useTemplateRef, watch } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
ndmDevice: NdmServerResultVO;
|
||||
station: Station;
|
||||
}>();
|
||||
|
||||
const deviceStore = useDeviceStore();
|
||||
|
||||
const { ndmDevice, station } = toRefs(props);
|
||||
|
||||
const localDevice = ref<NdmServerUpdateVO>({ ...ndmDevice.value });
|
||||
watch(ndmDevice, (newDevice) => {
|
||||
localDevice.value = { ...newDevice };
|
||||
});
|
||||
|
||||
const canEditDeviceId = computed(() => {
|
||||
const { deviceId } = ndmDevice.value;
|
||||
if (!isString(deviceId)) return true;
|
||||
if (deviceId.length === 0) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
const validatorAbortController = ref<AbortController>(new AbortController());
|
||||
const abortController = ref<AbortController>(new AbortController());
|
||||
|
||||
const formInst = useTemplateRef<FormInst>('formInst');
|
||||
const formRules: FormRules = {
|
||||
deviceId: {
|
||||
trigger: ['input'],
|
||||
asyncValidator: async (rule, value: string) => {
|
||||
await validateDeviceIdDuplicated({ deviceId: value }).catch((error) => {
|
||||
if (isCancel(error)) return;
|
||||
throw error;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { mutateAsync: validateDeviceIdDuplicated } = useMutation({
|
||||
mutationFn: async (params: { deviceId?: string }) => {
|
||||
const { deviceId } = params;
|
||||
if (!deviceId) throw new Error('请输入设备ID');
|
||||
|
||||
const deviceIdPattern = /^\d{4}(09|11)\d{4}$/;
|
||||
if (!deviceIdPattern.test(deviceId)) throw new Error('设备ID不符合规范');
|
||||
|
||||
validatorAbortController.value.abort();
|
||||
validatorAbortController.value = new AbortController();
|
||||
|
||||
const icmpEntities = await icmpEntityByDeviceId(deviceId, {
|
||||
stationCode: station.value.code,
|
||||
signal: validatorAbortController.value.signal,
|
||||
});
|
||||
if (icmpEntities.length > 0) throw new Error('该设备ID已存在');
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: updateDevice, isPending } = useMutation({
|
||||
mutationFn: async () => {
|
||||
await formInst.value?.validate().catch(() => {
|
||||
window.$message.error('表单验证失败');
|
||||
return;
|
||||
});
|
||||
|
||||
abortController.value.abort();
|
||||
abortController.value = new AbortController();
|
||||
|
||||
const deviceType = tryGetDeviceType(localDevice.value.deviceType);
|
||||
const stationCode = station.value.code;
|
||||
const signal = abortController.value.signal;
|
||||
if (deviceType === DEVICE_TYPE_LITERALS.ndmMediaServer) {
|
||||
await updateMediaServerApi(localDevice.value as NdmMediaServerUpdateVO, { stationCode, signal });
|
||||
return await detailMediaServerApi(`${localDevice.value.id}`, { stationCode, signal });
|
||||
} else if (deviceType === DEVICE_TYPE_LITERALS.ndmVideoServer) {
|
||||
await updateVideoServerApi(`${localDevice.value.id}`, localDevice.value as NdmVideoServerUpdateVO, { stationCode, signal });
|
||||
return await detailVideoServerApi(`${localDevice.value.id}`, { stationCode, signal });
|
||||
} else {
|
||||
throw new Error('不是服务器设备');
|
||||
}
|
||||
},
|
||||
onSuccess: (newDevice) => {
|
||||
localDevice.value = { ...newDevice };
|
||||
deviceStore.patchDevice(station.value.code, { ...newDevice });
|
||||
},
|
||||
onError: (error) => {
|
||||
if (isCancel(error)) return;
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
validatorAbortController.value.abort();
|
||||
abortController.value.abort();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard hoverable size="small">
|
||||
<template #default>
|
||||
<NForm size="small" ref="formInst" :model="localDevice" :rules="formRules">
|
||||
<NGrid>
|
||||
<NFormItemGi span="8" label-placement="left" label="ICMP启用">
|
||||
<NSwitch :value="destr(localDevice.icmpEnabled)" @update:value="(enabled: boolean) => (localDevice.icmpEnabled = enabled)" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="8" label-placement="left" label="SNMP启用">
|
||||
<NSwitch :value="destr(localDevice.snmpEnabled)" @update:value="(enabled: boolean) => (localDevice.snmpEnabled = enabled)" />
|
||||
</NFormItemGi>
|
||||
</NGrid>
|
||||
<NFormItem label-placement="left" label="设备ID" path="deviceId">
|
||||
<NInput v-model:value="localDevice.deviceId" :disabled="!canEditDeviceId" />
|
||||
</NFormItem>
|
||||
<NFormItem label-placement="left" label="设备名称">
|
||||
<NInput v-model:value="localDevice.name" />
|
||||
</NFormItem>
|
||||
<NFormItem label-placement="left" label="设备厂商">
|
||||
<NInput v-model:value="localDevice.manufacturer" />
|
||||
</NFormItem>
|
||||
<NFormItem label-placement="left" label="型号">
|
||||
<NInput v-model:value="localDevice.model" />
|
||||
</NFormItem>
|
||||
<NFormItem label-placement="left" label="团体字符串">
|
||||
<NInput v-model:value="localDevice.community" />
|
||||
</NFormItem>
|
||||
<NFormItem label-placement="left" label="设备描述">
|
||||
<NInput v-model:value="localDevice.description" />
|
||||
</NFormItem>
|
||||
<NFormItem label-placement="left" label="上游设备">
|
||||
<NInput v-model:value="localDevice.linkDescription" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</template>
|
||||
<template #action>
|
||||
<NFlex justify="end">
|
||||
<NButton secondary size="small" :loading="isPending" @click="() => updateDevice()">更新</NButton>
|
||||
</NFlex>
|
||||
</template>
|
||||
</NCard>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
Reference in New Issue
Block a user