refactor: 重构项目结构

- 优化 `车站-设备-告警`  轮询机制
- 改进设备卡片的布局
- 支持修改设备
- 告警轮询中获取完整告警数据
- 车站告警详情支持导出完整的 `今日告警列表`
- 支持将状态持久化到 `IndexedDB`
- 新增轮询控制 (调试模式)
- 新增离线开发模式 (调试模式)
- 新增 `IndexedDB` 数据控制 (调试模式)
This commit is contained in:
yangsy
2025-12-11 13:42:22 +08:00
commit 37781216b2
278 changed files with 17988 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import { NCard, NDescriptions, NDescriptionsItem } from 'naive-ui';
import { computed, toRefs } from 'vue';
const props = defineProps<{
commonInfo?: Record<string, string>;
}>();
const { commonInfo } = toRefs(props);
const showCard = computed(() => !!commonInfo.value);
const commonInfoEntries = computed(() => Object.entries(commonInfo.value ?? {}));
</script>
<template>
<NCard v-if="showCard" hoverable size="small">
<template #header>
<div>设备信息</div>
</template>
<NDescriptions bordered label-placement="left" :columns="2">
<template v-for="item in commonInfoEntries" :key="item[0]">
<NDescriptionsItem :label="item[0]">{{ item[1] }}</NDescriptionsItem>
</template>
</NDescriptions>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,77 @@
<script setup lang="ts">
import { ClockCircleOutlined, CodeOutlined, FireOutlined, SaveOutlined } from '@vicons/antd';
import { NCard, NFlex, NIcon, NProgress, type ProgressStatus } from 'naive-ui';
import { computed, toRefs } from 'vue';
const props = defineProps<{
cpuUsage?: string;
memUsage?: string;
diskUsage?: string;
runningTime?: string;
}>();
const { cpuUsage, memUsage, diskUsage, runningTime } = toRefs(props);
const showCard = computed(() => {
return Object.values(props).some((value) => !!value);
});
const cpuPercent = computed(() => {
if (!cpuUsage?.value) return 0;
return parseFloat(cpuUsage.value.replace('%', ''));
});
const memPercent = computed(() => {
if (!memUsage?.value) return 0;
return parseFloat(memUsage.value.replace('%', ''));
});
const diskPercent = computed(() => {
if (!diskUsage?.value) return 0;
return parseFloat(diskUsage.value.replace('%', ''));
});
const formattedRunningTime = computed(() => {
return (runningTime?.value ?? '-').replace('days', '天');
});
const getProgressStatus = (percent: number): ProgressStatus => {
if (percent >= 90) return 'error';
if (percent >= 70) return 'warning';
return 'success';
};
</script>
<template>
<NCard v-if="showCard" hoverable size="small">
<template #header>
<span>硬件占用率</span>
</template>
<template #default>
<NFlex vertical>
<NFlex v-if="cpuUsage" style="width: 100%" align="center" :wrap="false">
<NIcon :component="FireOutlined" />
<span style="word-break: keep-all">CPU</span>
<NProgress :percentage="cpuPercent" :status="getProgressStatus(cpuPercent)">{{ cpuPercent }}%</NProgress>
</NFlex>
<NFlex v-if="memUsage" style="width: 100%" align="center" :wrap="false">
<NIcon :component="CodeOutlined" />
<span style="word-break: keep-all">内存</span>
<NProgress :percentage="memPercent" :status="getProgressStatus(memPercent)">{{ memPercent }}%</NProgress>
</NFlex>
<NFlex v-if="diskUsage" style="width: 100%" align="center" :wrap="false">
<NIcon :component="SaveOutlined" />
<span style="word-break: keep-all">磁盘</span>
<NProgress :percentage="diskPercent" :status="getProgressStatus(diskPercent)">{{ diskPercent }}%</NProgress>
</NFlex>
<NFlex v-if="runningTime" style="width: 100%" align="center" :wrap="false">
<NIcon :component="ClockCircleOutlined" />
<span>系统运行时间</span>
<span>{{ formattedRunningTime }}</span>
</NFlex>
</NFlex>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,156 @@
<script setup lang="ts">
import { detailDeviceApi, probeDeviceApi, type NdmDeviceResultVO, type Station } from '@/apis';
import { DEVICE_TYPE_NAMES, tryGetDeviceType } from '@/enums';
import { useDeviceStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { ApiOutlined, ReloadOutlined } from '@vicons/antd';
import { isCancel } from 'axios';
import { NButton, NCard, NFlex, NIcon, NTag, NTooltip } from 'naive-ui';
import { computed, onBeforeUnmount, ref, toRefs } from 'vue';
const props = defineProps<{
ndmDevice: NdmDeviceResultVO;
station: Station;
}>();
const { ndmDevice, station } = toRefs(props);
const type = computed(() => {
const deviceType = tryGetDeviceType(ndmDevice.value.deviceType);
if (!deviceType) return '-';
return DEVICE_TYPE_NAMES[deviceType];
});
const name = computed(() => ndmDevice.value.name ?? '-');
const status = computed(() => ndmDevice.value.deviceStatus);
const ipAddr = computed(() => ndmDevice.value.ipAddress ?? '-');
const gbCode = computed(() => Reflect.get(ndmDevice.value, 'gbCode') as string | undefined);
const canOpenMgmtPage = computed(() => {
return Object.keys(ndmDevice.value).includes('manageUrl');
});
const canProbe = computed(() => !!ndmDevice.value.snmpEnabled);
const onClickOpenMgmtPage = () => {
if (canOpenMgmtPage.value) {
let target = '';
const manageUrl = ndmDevice.value.manageUrl;
const ipAddress = ndmDevice.value.ipAddress;
if (manageUrl) {
target = manageUrl;
} else if (ipAddress) {
target = `http://${ipAddress}`;
}
if (target) {
window.open(target, '_blank');
return;
}
window.$message.warning('未找到设备IP地址');
}
};
const abortController = ref<AbortController>(new AbortController());
const { mutate: probeDevice, isPending: probing } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
await probeDeviceApi(ndmDevice.value, { stationCode: station.value.code, signal: abortController.value.signal });
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const { mutate: detailDevice, isPending: loading } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
return await detailDeviceApi(ndmDevice.value, { stationCode: station.value.code, signal: abortController.value.signal });
},
onSuccess: (device) => {
if (device) {
useDeviceStore().patchDevice(station.value.code, device);
}
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
onBeforeUnmount(() => {
abortController.value.abort();
});
</script>
<template>
<NCard hoverable size="small">
<template #header>
<NFlex align="center">
<NTag v-if="status === '10'" size="small" type="success">在线</NTag>
<NTag v-else-if="status === '20'" size="small" type="error">离线</NTag>
<NTag v-else size="small" type="warning">-</NTag>
<div>{{ name }}</div>
<NButton v-if="canOpenMgmtPage" ghost size="tiny" type="default" :focusable="false" @click="onClickOpenMgmtPage">管理</NButton>
</NFlex>
</template>
<template #header-extra>
<NTooltip v-if="canProbe" trigger="hover">
<template #trigger>
<NButton size="small" quaternary circle :loading="probing" @click="() => probeDevice()">
<template #icon>
<NIcon :component="ApiOutlined" />
</template>
</NButton>
</template>
<template #default>
<span>请求最新诊断</span>
</template>
</NTooltip>
<NTooltip trigger="hover">
<template #trigger>
<NButton size="small" quaternary circle :loading="loading" @click="() => detailDevice()">
<template #icon>
<NIcon :component="ReloadOutlined" />
</template>
</NButton>
</template>
<template #default>
<span>刷新设备</span>
</template>
</NTooltip>
</template>
<template #default>
<div style="font-size: small; color: #666">
<div>
<span>设备类型</span>
<span>{{ type }}</span>
</div>
<div>
<span>IP地址</span>
<span>{{ ipAddr }}</span>
</div>
<div v-if="gbCode">
<span>国标编码</span>
<span>{{ gbCode }}</span>
</div>
<div v-if="!!ndmDevice.description">
<span>设备描述</span>
<span>{{ ndmDevice.description }}</span>
</div>
<div v-if="ndmDevice.snmpEnabled">
<span>上次诊断时间</span>
<span>{{ ndmDevice.lastDiagTime ?? '-' }}</span>
</div>
</div>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,10 @@
import DeviceCommonCard from './device-common-card.vue';
import DeviceHardwareCard from './device-hardware-card.vue';
import DeviceHeaderCard from './device-header-card.vue';
import NvrDiskCard from './nvr-disk-card.vue';
import NvrRecordCard from './nvr-record-card.vue';
import SecurityBoxCircuitCard from './security-box-circuit-card.vue';
import SecurityBoxEnvCard from './security-box-env-card.vue';
import SwitchPortCard from './switch-port-card.vue';
export { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, NvrDiskCard, NvrRecordCard, SecurityBoxCircuitCard, SecurityBoxEnvCard, SwitchPortCard };

View File

@@ -0,0 +1,168 @@
<script setup lang="ts">
import type { NdmNvrDiagInfo } from '@/apis';
import { NCard, NDescriptions, NDescriptionsItem, NFlex, NPopover, NProgress, NTag, useThemeVars, type ProgressProps, type TagProps } from 'naive-ui';
import { computed, toRefs } from 'vue';
const props = defineProps<{
diskHealth: NonNullable<NdmNvrDiagInfo['info']>['diskHealth'];
diskArray: NonNullable<NdmNvrDiagInfo['info']>['groupInfoList'];
}>();
const themeVars = useThemeVars();
const { diskHealth, diskArray } = toRefs(props);
const hasDiskHealth = computed(() => !!diskHealth.value && diskHealth.value.length > 0);
const hasDiskArray = computed(() => !!diskArray.value && diskArray.value.length > 0);
const showCard = computed(() => hasDiskHealth.value || hasDiskArray.value);
const healthList = computed(() => diskHealth.value ?? []);
const arrayList = computed(() => diskArray.value ?? []);
const getDiskHealthClassName = (health: number) => {
if (health === 0) return 'disk-healthy';
if (health === 1) return 'disk-warning';
return 'disk-error';
};
const getDiskHealthText = (health: number) => {
if (health === 0) return '正常';
if (health === 1) return '警告';
return '故障';
};
const getArrayTagType = (state?: number): TagProps['type'] => {
if (state === 1) return 'success';
return 'error';
};
const formatArraySize = (megabytes: number): string => {
const units = ['MB', 'GB', 'TB'];
let index = 0;
while (megabytes >= 1024 && index < units.length - 1) {
megabytes /= 1024;
index++;
}
return `${megabytes.toFixed(2)} ${units[index]}`;
};
const getArrayUsagePercentage = (freeSize: number, totalSize: number) => {
if (totalSize === 0) return 0;
return Math.round(((totalSize - freeSize) / totalSize) * 100);
};
const getArrayProgressStatus = (percentage: number): ProgressProps['status'] => {
if (percentage >= 90) return 'error';
if (percentage >= 70) return 'warning';
return 'success';
};
</script>
<template>
<NCard v-if="showCard" hoverable size="small">
<template #header>
<span>磁盘状态</span>
</template>
<template #default>
<NFlex vertical>
<NCard v-if="hasDiskHealth" size="small">
<template #header>
<div style="font-size: small">磁盘健康状态{{ healthList.length }}个硬盘槽位</div>
</template>
<template #default>
<div style="display: grid; grid-template-rows: repeat(3, auto)" :style="{ 'grid-template-columns': `repeat(${Math.max(healthList.length / 3, 12)}, 1fr)` }">
<template v-for="(health, index) in healthList" :key="index">
<NPopover :delay="300">
<template #trigger>
<!-- 最外层div宽度100% -->
<div class="disk-health" style="height: 40px; box-sizing: border-box; display: flex; cursor: pointer" :class="getDiskHealthClassName(health)">
<!-- 将磁盘号和状态指示器包裹起来 用于居中布局 -->
<div style="margin: auto; display: flex; flex-direction: column; align-items: center">
<div style="font-size: xx-small">{{ index + 1 }}</div>
<div class="indicator" style="width: 8px; height: 8px; border-radius: 50%" :class="getDiskHealthClassName(health)"></div>
</div>
</div>
</template>
<template #default>
<NDescriptions bordered size="small" label-placement="left" :column="1">
<NDescriptionsItem label="硬盘槽位">{{ index + 1 }}</NDescriptionsItem>
<NDescriptionsItem label="状态">{{ getDiskHealthText(health) }}</NDescriptionsItem>
</NDescriptions>
</template>
</NPopover>
</template>
</div>
</template>
</NCard>
<NCard v-if="hasDiskArray" size="small">
<template #header>
<div style="font-size: small">磁盘阵列状态{{ arrayList.length }}个阵列</div>
</template>
<template #default>
<template v-for="({ state, stateValue, freeSize, totalSize }, index) in arrayList" :key="index">
<NFlex vertical>
<NFlex align="center">
<div>磁盘阵列{{ index + 1 }}</div>
<NTag round size="small" :bordered="false" :type="getArrayTagType(state)">{{ stateValue }}</NTag>
</NFlex>
<NFlex align="center" :wrap="false">
<div style="word-break: keep-all">存储使用率</div>
<NProgress :percentage="getArrayUsagePercentage(freeSize ?? 0, totalSize ?? 0)" :status="getArrayProgressStatus(getArrayUsagePercentage(freeSize ?? 0, totalSize ?? 0))">
{{ getArrayUsagePercentage(freeSize ?? 0, totalSize ?? 0) }}%
</NProgress>
</NFlex>
<NFlex>
<span>总容量{{ formatArraySize(totalSize ?? 0) }}</span>
<span>已用{{ formatArraySize((totalSize ?? 0) - (freeSize ?? 0)) }}</span>
<span>可用{{ formatArraySize(freeSize ?? 0) }}</span>
</NFlex>
</NFlex>
</template>
</template>
</NCard>
</NFlex>
</template>
</NCard>
</template>
<style scoped lang="scss">
.disk-health {
&.disk-healthy {
&:hover {
background-color: #18a05816;
}
}
&.disk-warning {
&:hover {
background-color: #d0305016;
}
}
&.disk-error {
&:hover {
background-color: #f0a02016;
}
}
&:hover {
transform: translateY(-2px);
transition: transform 0.2s ease-in-out;
box-shadow: v-bind('themeVars.boxShadow1');
}
.indicator {
&.disk-healthy {
background-color: #18a058;
}
&.disk-warning {
background-color: #d03050;
}
&.disk-error {
background-color: #f0a020;
}
}
}
</style>

View File

@@ -0,0 +1,246 @@
<script setup lang="ts">
import { getChannelListApi, getRecordCheckApi, reloadAllRecordCheckApi, reloadRecordCheckApi, type NdmNvrResultVO, type NdmRecordCheck, type RecordItem, type Station } from '@/apis';
import { exportRecordDiagCsv, transformRecordChecks } from '@/helpers';
import { useStationStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { DownloadOutlined, ReloadOutlined } from '@vicons/antd';
import { isCancel } from 'axios';
import dayjs from 'dayjs';
import { NButton, NCard, NFlex, NIcon, NPagination, NPopconfirm, NPopover, NRadioButton, NRadioGroup, NTooltip, useThemeVars } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, onBeforeUnmount, onMounted, ref, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmNvrResultVO;
station: Station;
}>();
const themeVars = useThemeVars();
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const { ndmDevice, station } = toRefs(props);
const recordChecks = ref<NdmRecordCheck[]>([]);
const lossInput = ref<number>(0);
const recordDiags = computed(() => {
return transformRecordChecks(recordChecks.value).filter((recordDiag) => {
if (lossInput.value === 0) {
return true;
} else if (lossInput.value === 1) {
return recordDiag.lostChunks.length > 0;
} else if (lossInput.value === 2) {
return recordDiag.lostChunks.length === 0;
}
return false;
});
});
const abortController = ref<AbortController>(new AbortController());
const { mutate: getRecordCheckByParentId, isPending: loading } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
const checks = await getRecordCheckApi(ndmDevice.value, 90, [], { stationCode: station.value.code, signal: abortController.value.signal });
return checks;
},
onSuccess: (checks) => {
recordChecks.value = checks;
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const { mutate: reloadAllRecordCheck, isPending: reloading } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
await reloadAllRecordCheckApi(90, { stationCode: station.value.code, signal: abortController.value.signal });
},
onSuccess: () => {
window.$message.success('正在逐步刷新中,请稍后点击刷新按钮查看');
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const onExportRecordCheck = () => {
const code = station.value.code;
const stationName = stations.value.find((station) => station.code === code)?.name ?? '';
exportRecordDiagCsv(recordDiags.value, stationName);
};
const page = ref(1);
const pageSize = ref(10);
const pagedRecordDiags = computed(() => {
const startIndex = (page.value - 1) * pageSize.value;
const endIndex = page.value * pageSize.value;
return recordDiags.value.slice(startIndex, endIndex);
});
const getLostChunkDOMStyle = (lostChunk: RecordItem, duration: RecordItem) => {
const chunk = dayjs(lostChunk.endTime).diff(dayjs(lostChunk.startTime));
const offset = dayjs(lostChunk.startTime).diff(dayjs(duration.startTime));
const total = dayjs(duration.endTime).diff(dayjs(duration.startTime));
return {
left: `${(offset / total) * 100}%`,
width: `${(chunk / total) * 100}%`,
};
};
const { mutate: reloadRecordCheckByGbId } = useMutation({
mutationFn: async (params: { gbCode: string }) => {
abortController.value.abort();
abortController.value = new AbortController();
const channelList = await getChannelListApi(ndmDevice.value, { stationCode: station.value.code, signal: abortController.value.signal });
const channel = channelList.find((channel) => channel.code === params.gbCode);
if (!channel) throw new Error('通道不存在');
window.$message.loading('刷新耗时较长, 请不要多次刷新, 并耐心等待...', {
duration: 1000 * 60 * 60 * 24 * 300,
});
const isSuccess = await reloadRecordCheckApi(channel, 90, { stationCode: station.value.code, signal: abortController.value.signal });
window.$message.destroyAll();
if (isSuccess) {
window.$message.success('刷新成功');
} else {
window.$message.error('刷新失败');
}
},
onSuccess: () => {
getRecordCheckByParentId();
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
onMounted(() => {
getRecordCheckByParentId();
});
watch(
() => ndmDevice.value.id,
(devieDbId) => {
if (devieDbId) {
getRecordCheckByParentId();
}
},
);
onBeforeUnmount(() => {
abortController.value.abort();
});
</script>
<template>
<NCard hoverable size="small">
<template #header>
<NFlex align="center" :size="24">
<div>录像诊断</div>
<NPopconfirm @positive-click="() => reloadAllRecordCheck()">
<template #trigger>
<NButton secondary size="small" :loading="reloading">更新所有通道录像诊断</NButton>
</template>
<template #default>
<span>确认更新所有通道录像诊断吗?</span>
</template>
</NPopconfirm>
</NFlex>
</template>
<template #header-extra>
<NFlex>
<NTooltip trigger="hover">
<template #trigger>
<NButton size="small" quaternary circle :loading="loading" @click="() => getRecordCheckByParentId()">
<template #icon>
<NIcon :component="ReloadOutlined" />
</template>
</NButton>
</template>
<template #default>
<span>刷新数据</span>
</template>
</NTooltip>
<NTooltip trigger="hover">
<template #trigger>
<NButton size="small" quaternary circle @click="onExportRecordCheck">
<template #icon>
<NIcon :component="DownloadOutlined" />
</template>
</NButton>
</template>
<template #default>
<span>导出录像诊断</span>
</template>
</NTooltip>
</NFlex>
</template>
<template #default>
<NFlex justify="flex-end" style="margin-bottom: 6px">
<NRadioGroup size="small" v-model:value="lossInput">
<NRadioButton label="全部" :value="0" />
<NRadioButton label="有缺失" :value="1" />
<NRadioButton label="无缺失" :value="2" />
</NRadioGroup>
</NFlex>
<template v-for="{ gbCode, channelName, recordDuration, lostChunks } in pagedRecordDiags" :key="gbCode">
<div style="display: flex; justify-content: space-between">
<div>
<span>{{ channelName }}</span>
<span>{{ '\u3000' }}</span>
<span>{{ recordDuration.startTime }} - {{ recordDuration.endTime }}</span>
</div>
<NPopconfirm trigger="click" @positive-click="() => reloadRecordCheckByGbId({ gbCode })">
<template #trigger>
<NButton ghost size="tiny" type="info">刷新</NButton>
</template>
<template #default>
<span>是否确认刷新?</span>
</template>
</NPopconfirm>
</div>
<div style="position: relative; height: 24px; margin: 2px 0" :style="{ backgroundColor: lostChunks.length > 0 ? themeVars.infoColor : themeVars.successColor }">
<template v-for="{ startTime, endTime } in lostChunks" :key="`${startTime}-${endTime}`">
<NPopover trigger="hover">
<template #trigger>
<div style="position: absolute; height: 100%; cursor: pointer; background-color: #eee" :style="getLostChunkDOMStyle({ startTime, endTime }, recordDuration)" />
</template>
<template #default>
<div>开始时间:{{ dayjs(startTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
<div>结束时间:{{ dayjs(endTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
</template>
</NPopover>
</template>
</div>
</template>
</template>
<template #action>
<NFlex justify="flex-end">
<NPagination size="small" :page="page" :page-size="pageSize" :page-count="Math.ceil(recordDiags.length / pageSize)" @update:page="(p) => (page = p)">
<template #prefix>
<span>{{ `共 ${recordDiags.length} 个通道` }}</span>
</template>
</NPagination>
</NFlex>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,170 @@
<script setup lang="ts">
import { probeDeviceApi, rebootSecurityBoxApi, turnCitcuitStatusApi, type NdmSecurityBoxCircuit, type NdmSecurityBoxResultVO, type Station } from '@/apis';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { PoweroffOutlined } from '@vicons/antd';
import { watchImmediate } from '@vueuse/core';
import { NButton, NCard, NDescriptions, NDescriptionsItem, NFlex, NIcon, NPopconfirm, NPopover, NSwitch, NTag, useThemeVars, type TagProps } from 'naive-ui';
import { computed, ref, toRefs } from 'vue';
const props = defineProps<{
circuits?: NdmSecurityBoxCircuit[];
ndmDevice: NdmSecurityBoxResultVO;
station: Station;
}>();
const themeVars = useThemeVars();
const { circuits, ndmDevice, station } = toRefs(props);
const showCard = computed(() => !!circuits.value && circuits.value.length > 0);
const boxCircuits = ref<NdmSecurityBoxCircuit[]>([]);
watchImmediate(circuits, (newCircuits) => {
boxCircuits.value = newCircuits?.map((circuit) => ({ ...circuit })) ?? [];
});
const getCircuitStatusTagType = (circuit: NdmSecurityBoxCircuit): TagProps['type'] => {
const { status } = circuit;
return status === 0 ? 'error' : status === 1 ? 'success' : 'warning';
};
const getCircuitStatusText = (circuit: NdmSecurityBoxCircuit) => {
const { status } = circuit;
return status === 0 ? '关闭' : status === 1 ? '开启' : '-';
};
const getCircuitStatusClassName = (circuit: NdmSecurityBoxCircuit) => {
const { status } = circuit;
return status === 0 ? 'circuit-off' : status === 1 ? 'circuit-on' : 'circuit-unknown';
};
const { mutate: turnStatus, isPending: turning } = useMutation({
mutationFn: async (params: { circuitIndex: number; newStatus: boolean }) => {
const { circuitIndex, newStatus } = params;
if (!ndmDevice.value.ipAddress) {
throw new Error('设备IP地址不存在');
}
const status = newStatus ? 1 : 0;
await turnCitcuitStatusApi(ndmDevice.value.ipAddress, circuitIndex, status, { stationCode: station.value.code });
await probeDeviceApi(ndmDevice.value, { stationCode: station.value.code });
return status;
},
onSuccess: (status, { circuitIndex }) => {
const circuit = boxCircuits.value.at(circuitIndex);
if (circuit) circuit.status = status;
},
onError: (error) => {
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const { mutate: reboot, isPending: rebooting } = useMutation({
mutationFn: async () => {
if (!ndmDevice.value.ipAddress) {
throw new Error('设备IP地址不存在');
}
await rebootSecurityBoxApi(ndmDevice.value.ipAddress, { stationCode: station.value.code });
},
onSuccess: () => {
window.$message.success('设备重启成功');
},
onError: (error) => {
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
</script>
<template>
<NCard v-if="showCard" hoverable size="small">
<template #header>
<NFlex align="center">
<span>电路状态</span>
<NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => reboot()">
<template #trigger>
<NButton secondary size="small" :loading="rebooting">重合闸</NButton>
</template>
<span>确定要执行重合闸操作吗?</span>
</NPopconfirm>
</NFlex>
</template>
<template #default>
<div style="display: grid" :style="{ 'grid-template-columns': `repeat(${Math.min(boxCircuits.length, 4)}, 1fr)` }">
<template v-for="(circuit, index) in boxCircuits" :key="index">
<NPopover :delay="300">
<template #trigger>
<NFlex justify="center" align="center" :size="0">
<NFlex vertical class="pointer-cursor circuit" style="padding: 12px" :class="getCircuitStatusClassName(circuit)">
<NFlex align="center">
<NTag class="pointer-cursor" size="small" :type="getCircuitStatusTagType(circuit)">
<template #icon>
<NIcon :component="PoweroffOutlined" />
</template>
<template #default>
<span>{{ getCircuitStatusText(circuit) }}</span>
</template>
</NTag>
<span>电路{{ index + 1 }}</span>
</NFlex>
<NFlex justify="end" align="center">
<NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => turnStatus({ circuitIndex: index, newStatus: circuit.status !== 1 })">
<template #trigger>
<NSwitch size="small" :value="circuit.status === 1" :loading="turning" />
</template>
<template #default>
<span>确定要{{ circuit.status === 1 ? '关闭' : '开启' }}电路{{ index + 1 }}吗?</span>
</template>
</NPopconfirm>
</NFlex>
</NFlex>
</NFlex>
</template>
<template #default>
<NDescriptions bordered size="small" label-placement="left" :column="1">
<NDescriptionsItem label="电压">{{ circuit.voltage }}V</NDescriptionsItem>
<NDescriptionsItem label="电流">{{ circuit.current }}A</NDescriptionsItem>
</NDescriptions>
</template>
</NPopover>
</template>
</div>
</template>
</NCard>
</template>
<style scoped lang="scss">
.pointer-cursor {
cursor: pointer;
}
.circuit {
&.circuit-on {
&:hover {
background-color: #18a05816;
}
}
&.circuit-off {
&:hover {
background-color: #d0305016;
}
}
&.circuit-unknown {
&:hover {
background-color: #f0a02016;
}
}
&:hover {
transform: translateY(-2px);
transition: transform 0.2s ease-in-out;
box-shadow: v-bind('themeVars.boxShadow1');
}
}
</style>

View File

@@ -0,0 +1,98 @@
<script setup lang="ts">
import { ThunderboltOutlined } from '@vicons/antd';
import { ApertureOutline, LockOpenOutline, ThermometerOutline, WaterOutline } from '@vicons/ionicons5';
import { NCard, NFlex, NIcon, NTag } from 'naive-ui';
import { computed, toRefs } from 'vue';
const props = defineProps<{
fanSpeeds?: number[];
temperature?: number;
humidity?: number;
switches?: number[];
}>();
const { fanSpeeds, temperature, humidity, switches } = toRefs(props);
const showCard = computed(() => {
return Object.values(props).some((value) => !!value);
});
// 门禁状态
const accessControlStatus = computed(() => {
if (!switches?.value || switches.value.length < 1) return null;
const status = switches.value.at(0)!;
return status === 0 ? '开门' : status === 1 ? '关门' : '-';
});
// 防雷状态
const lightningProtectionStatus = computed(() => {
if (!switches?.value || switches.value.length < 2) return null;
const status = switches.value.at(1)!;
return status === 0 ? '正常' : status === 1 ? '失效' : '-';
});
const getStatusTagType = (status: string | null) => {
if (['正常'].includes(status ?? '')) return 'success';
if (['失效'].includes(status ?? '')) return 'error';
return 'default';
};
const formattedFanSpeeds = computed(() => {
if (!fanSpeeds?.value || fanSpeeds.value.length === 0) return null;
return fanSpeeds.value.map((speed, index) => `风扇${index + 1}: ${speed} RPM`).join(', ');
});
</script>
<template>
<NCard v-if="showCard" hoverable size="small">
<template #header>
<span>安防箱状态</span>
</template>
<template #default>
<NFlex vertical>
<NTag>
<template #icon>
<NIcon :component="ThermometerOutline" />
</template>
<template #default>
<span>温度: {{ temperature }}</span>
</template>
</NTag>
<NTag>
<template #icon>
<NIcon :component="WaterOutline" />
</template>
<template #default>
<span>湿度: {{ humidity }}%</span>
</template>
</NTag>
<NTag>
<template #icon>
<NIcon :component="ApertureOutline" />
</template>
<template #default>
<span>风扇: {{ formattedFanSpeeds }}</span>
</template>
</NTag>
<NTag :type="getStatusTagType(accessControlStatus)">
<template #icon>
<NIcon :component="LockOpenOutline" />
</template>
<template #default>
<span>门禁: {{ accessControlStatus }}</span>
</template>
</NTag>
<NTag :type="getStatusTagType(lightningProtectionStatus)">
<template #icon>
<NIcon :component="ThunderboltOutlined" />
</template>
<template #default>
<span>防雷: {{ lightningProtectionStatus }}</span>
</template>
</NTag>
</NFlex>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,146 @@
<script setup lang="ts">
import type { NdmSwitchPortInfo } from '@/apis';
import { getPortStatusValue, transformPortSpeed } from '@/helpers';
import { NCard, NDescriptions, NDescriptionsItem, NPopover, useThemeVars } from 'naive-ui';
import { computed, toRefs } from 'vue';
const props = defineProps<{
ports?: NdmSwitchPortInfo[];
}>();
const themeVars = useThemeVars();
const { ports } = toRefs(props);
const showCard = computed(() => !!ports.value);
const switchSlots = computed(() => {
// 解析端口名称,将端口按槽位进行分组
const groupMap = new Map<string, NdmSwitchPortInfo[]>();
ports.value?.forEach((port) => {
const parts = port.portName.split('/');
if (parts.length >= 3) {
const slotName = `${parts[0]}/${parts[1]}`;
if (!groupMap.has(slotName)) {
groupMap.set(slotName, []);
}
groupMap.get(slotName)!.push(port);
}
});
// 将Map转换为entries
const entries = Array.from(groupMap.entries());
// 按槽位进行排序
const sorted = entries.sort(([aSlotName], [bSlotName]) => {
const [mainA = 0, subA = 0] = aSlotName.split('/').map((value) => parseInt(value));
const [mainB = 0, subB = 0] = bSlotName.split('/').map((value) => parseInt(value));
return mainA - mainB || subA - subB;
});
// 按端口号进行排序
return sorted.map(([slotName, ports]) => ({
slotName,
ports: ports.sort((portA, portB) => {
const portNumA = parseInt(portA.portName.split('/').at(2) ?? '0');
const portNumB = parseInt(portB.portName.split('/').at(2) ?? '0');
return portNumA - portNumB;
}),
}));
});
const getPortClassName = (port: NdmSwitchPortInfo) => {
if (port.upDown === 1) {
return 'port-up';
}
if (port.upDown === 2) {
return 'port-down';
}
return 'port-unknown';
};
</script>
<template>
<NCard v-if="showCard" hoverable size="small">
<template #header>
<span>端口数据</span>
</template>
<template #default>
<!-- 遍历所有槽位 -->
<template v-for="{ slotName, ports } in switchSlots" :key="slotName">
<div style="padding: 8px 0">
<span>{{ slotName }}{{ ports.length }}个端口</span>
<!-- 端口布局 2 至少12列 纵向排序 -->
<div style="display: grid; grid-template-rows: repeat(2, auto); grid-auto-flow: column" :style="{ 'grid-template-columns': `repeat(${Math.max(ports.length / 2, 12)}, 1fr)` }">
<template v-for="(port, index) in ports" :key="port.portName">
<!-- 端口 -->
<NPopover :delay="300">
<template #trigger>
<!-- 最外层div宽度100% -->
<div class="port" style="height: 40px; box-sizing: border-box; display: flex; cursor: pointer" :class="getPortClassName(port)">
<!-- 将端口号和状态指示器包裹起来 用于居中布局 -->
<div style="margin: auto; display: flex; flex-direction: column; align-items: center">
<div style="font-size: xx-small">{{ index }}</div>
<div class="indicator" style="width: 8px; height: 8px; border-radius: 50%" :class="getPortClassName(port)"></div>
</div>
</div>
</template>
<template #default>
<NDescriptions bordered size="small" label-placement="left" :column="1">
<NDescriptionsItem label="端口名称">{{ port.portName }}</NDescriptionsItem>
<NDescriptionsItem label="状态">{{ getPortStatusValue(port) }}</NDescriptionsItem>
<NDescriptionsItem label="上行速率">{{ transformPortSpeed(port, 'in') }}</NDescriptionsItem>
<NDescriptionsItem label="下行速率">{{ transformPortSpeed(port, 'out') }}</NDescriptionsItem>
<NDescriptionsItem label="总速率">{{ transformPortSpeed(port, 'total') }}</NDescriptionsItem>
</NDescriptions>
</template>
</NPopover>
</template>
</div>
</div>
</template>
</template>
</NCard>
</template>
<style scoped lang="scss">
.port {
&.port-up {
&:hover {
background-color: #18a05816;
}
}
&.port-down {
&:hover {
background-color: #d0305016;
}
}
&.port-unknown {
&:hover {
background-color: #f0a02016;
}
}
&:hover {
transform: translateY(-2px);
transition: transform 0.2s ease-in-out;
box-shadow: v-bind('themeVars.boxShadow1');
}
.indicator {
&.port-up {
background-color: #18a058;
}
&.port-down {
background-color: #d03050;
}
&.port-unknown {
background-color: #f0a020;
}
}
}
</style>

View File

@@ -0,0 +1,152 @@
<script setup lang="ts">
import { pageDeviceAlarmLogApi, type NdmDeviceAlarmLogResultVO, type NdmDeviceResultVO, type PageParams, type Station } from '@/apis';
import { renderAlarmDateCell, renderAlarmTypeCell, renderFaultDescriptionCell, renderFaultLevelCell } from '@/helpers';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import { NCard, NDataTable, type DataTableColumns, type DataTableRowData, type PaginationProps } from 'naive-ui';
import { h, onBeforeUnmount, reactive, ref, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmDeviceResultVO;
station: Station;
}>();
const range = defineModel<[number, number]>('range');
const loading = defineModel<boolean>('loading', { default: false });
const emit = defineEmits<{
exposeQueryFn: [queryFn: () => void];
}>();
const { ndmDevice, station } = toRefs(props);
const tableColumns: DataTableColumns<NdmDeviceAlarmLogResultVO> = [
{ title: '告警流水号', key: 'alarmNo' },
{
title: '告警时间',
key: 'alarmDate',
render: renderAlarmDateCell,
},
{
title: '告警类型',
key: 'alarmType',
align: 'center',
render: renderAlarmTypeCell,
},
{
title: '故障级别',
key: 'faultLevel',
align: 'center',
render: renderFaultLevelCell,
},
// { title: '故障编码', key: 'faultCode', align: 'center' },
{
title: '故障描述',
key: 'faultDescription',
render: (rowData) => renderFaultDescriptionCell(rowData, ndmDevice.value),
},
{
title: '是否恢复',
key: 'alarmCategory',
align: 'center',
render: (rowData) => {
return rowData.alarmCategory === '2' ? '是' : '否';
},
},
{ title: '恢复时间', key: 'updatedTime' },
];
const tableData = ref<DataTableRowData[]>([]);
const DEFAULT_PAGE_SIZE = 10;
const pagination = reactive<PaginationProps>({
size: 'small',
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<AbortController>(new AbortController());
const { mutate: getTableData, isPending } = useMutation({
mutationFn: async () => {
if (!range.value) throw new Error('请选择时间范围');
const deviceId = ndmDevice.value.deviceId;
if (!deviceId) throw new Error('该设备未配置设备ID无法查询告警记录');
abortController.value?.abort();
abortController.value = new AbortController();
const alarmDate_ge = range.value[0];
const alarmDate_le = range.value[1];
const restParams: Omit<PageParams<{ id: string }>, 'model' | 'extra'> = {
current: pagination.page ?? 1,
size: pagination.pageSize ?? DEFAULT_PAGE_SIZE,
sort: 'id',
order: 'descending',
};
const pageResult = await pageDeviceAlarmLogApi(
{
model: { deviceId },
extra: { alarmDate_ge, alarmDate_le },
...restParams,
},
{
stationCode: station.value.code,
signal: abortController.value.signal,
},
);
return pageResult;
},
onSuccess: ({ records, size, total }) => {
pagination.pageSize = parseInt(size);
pagination.itemCount = parseInt(total);
tableData.value = records;
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
emit('exposeQueryFn', () => {
pagination.page = 1;
pagination.pageSize = DEFAULT_PAGE_SIZE;
getTableData();
});
watch(isPending, (pending) => {
loading.value = pending;
});
onBeforeUnmount(() => {
abortController.value.abort();
});
</script>
<template>
<NCard hoverable size="small">
<template #header>
<span>设备告警记录</span>
</template>
<template #default>
<NDataTable size="small" :loading="isPending" :columns="tableColumns" :data="tableData" :pagination="pagination" :single-line="false" remote flex-height style="height: 500px" />
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,183 @@
<script setup lang="ts">
import { pageIcmpLogApi, type NdmDeviceResultVO, type NdmIcmpLogResultVO, type PageParams, type PageResult, type Station } from '@/apis';
import { formatDuration, parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import dayjs from 'dayjs';
import { NCard, NFlex, NPagination, NScrollbar, NSpin, NTimeline, NTimelineItem, type PaginationProps, type TimelineItemProps } from 'naive-ui';
import { computed, h, onBeforeUnmount, reactive, ref, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmDeviceResultVO;
station: Station;
}>();
const range = defineModel<[number, number]>('range');
const loading = defineModel<boolean>('loading', { default: false });
const emit = defineEmits<{
exposeQueryFn: [queryFn: () => void];
}>();
const { ndmDevice, station } = toRefs(props);
const icmpLogs = ref<NdmIcmpLogResultVO[]>([]);
const lastItem = ref<NdmIcmpLogResultVO>();
const DEFAULT_PAGE_SIZE = 6;
const pagination = reactive<PaginationProps>({
size: 'small',
showSizePicker: true,
page: 1,
pageSize: DEFAULT_PAGE_SIZE,
pageSizes: [6, 10, 20, 50, 80, 100],
itemCount: 0,
prefix: ({ itemCount }) => {
return h('div', {}, { default: () => `${itemCount}` });
},
onUpdatePage: (page: number) => {
pagination.page = page;
getIcmpLogs();
},
onUpdatePageSize: (pageSize: number) => {
pagination.pageSize = pageSize;
pagination.page = 1;
getIcmpLogs();
},
});
const abortController = ref<AbortController>(new AbortController());
const { mutate: getIcmpLogs, isPending } = useMutation({
mutationFn: async () => {
if (!range.value) throw new Error('请选择时间范围');
abortController.value?.abort();
abortController.value = new AbortController();
const deviceId = ndmDevice.value.id;
if (!deviceId) throw new Error('该设备没有ID');
const createdTime_precisest = dayjs(range.value[0]).format('YYYY-MM-DD HH:mm:ss');
const createdTime_preciseed = dayjs(range.value[1]).format('YYYY-MM-DD HH:mm:ss');
const restParams: Omit<PageParams<{ id: string }>, 'model' | 'extra'> = {
current: pagination.page ?? 1,
size: pagination.pageSize ?? DEFAULT_PAGE_SIZE,
sort: 'id',
order: 'descending',
};
const currPageResult = await pageIcmpLogApi(
{
model: { deviceId: deviceId },
extra: { createdTime_precisest, createdTime_preciseed },
...restParams,
},
{
stationCode: station.value.code,
signal: abortController.value.signal,
},
);
let lastPageResult: PageResult<NdmIcmpLogResultVO> | undefined = undefined;
if ((pagination.page ?? 1) > 1) {
lastPageResult = await pageIcmpLogApi(
{
model: { deviceId: deviceId },
extra: { createdTime_precisest, createdTime_preciseed },
...restParams,
current: (pagination.page ?? 1) - 1,
},
{
stationCode: station.value.code,
signal: abortController.value.signal,
},
);
}
return { currPageResult, lastPageResult };
},
onSuccess: ({ currPageResult: { records, size, total }, lastPageResult }) => {
pagination.pageSize = parseInt(size);
pagination.itemCount = parseInt(total);
icmpLogs.value = records;
lastItem.value = lastPageResult?.records.at(-1);
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
emit('exposeQueryFn', () => {
pagination.page = 1;
pagination.pageSize = DEFAULT_PAGE_SIZE;
getIcmpLogs();
});
watch(isPending, (pending) => {
loading.value = pending;
});
const timelineItems = computed<TimelineItemProps[]>(() => {
const items: TimelineItemProps[] = [];
if (icmpLogs.value.length === 0 && !loading.value) {
const { deviceStatus } = ndmDevice.value;
const type: TimelineItemProps['type'] = deviceStatus === '10' ? 'success' : deviceStatus === '20' ? 'error' : 'warning';
const title = deviceStatus === '10' ? '在线' : deviceStatus === '20' ? '离线' : '未知';
const content = '至今' + (deviceStatus === '10' ? '在线' : deviceStatus === '20' ? '离线' : '未知');
items.push({ type, title, content });
return items;
}
let lastIcmpLog = lastItem.value;
for (const icmpLog of icmpLogs.value) {
const { deviceStatus, createdTime } = icmpLog;
if (!createdTime) continue;
const type: TimelineItemProps['type'] = deviceStatus === '10' ? 'success' : deviceStatus === '20' ? 'error' : 'warning';
const title = deviceStatus === '10' ? '在线' : deviceStatus === '20' ? '离线' : '未知';
const content = `持续时长:${lastIcmpLog ? formatDuration(createdTime, lastIcmpLog.createdTime) : '至今'}`;
const time = createdTime;
items.push({ type, title, content, time });
lastIcmpLog = icmpLog;
}
return items;
});
onBeforeUnmount(() => {
abortController.value.abort();
});
</script>
<template>
<NCard hoverable size="small" :content-style="{ 'max-height': '500px' }">
<template #header>
<span>设备状态记录</span>
</template>
<template #default>
<NSpin v-if="loading" style="width: 100%; height: 100%; display: flex"></NSpin>
<div v-else-if="timelineItems.length === 0" style="width: 100%; display: flex; color: #666">
<div style="margin: auto">暂无记录</div>
</div>
<NTimeline v-else-if="timelineItems.length <= DEFAULT_PAGE_SIZE">
<NTimelineItem v-for="{ type, title, content, time } in timelineItems" :key="time" :type="type" :title="title" :content="content" :time="time"></NTimelineItem>
</NTimeline>
<NScrollbar v-else x-scrollable>
<NTimeline>
<NTimelineItem v-for="{ type, title, content, time } in timelineItems" :key="time" :type="type" :title="title" :content="content" :time="time"></NTimelineItem>
</NTimeline>
</NScrollbar>
</template>
<template #footer>
<NFlex justify="end">
<NPagination
v-model:page="pagination.page"
v-model:page-size="pagination.pageSize"
:page-sizes="pagination.pageSizes"
:item-count="pagination.itemCount"
size="small"
show-size-picker
@update-page="pagination.onUpdatePage"
@update:page-size="pagination.onUpdatePageSize"
/>
</NFlex>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,153 @@
<script lang="ts">
const getValueByFieldPath = (record: Record<string, any>, fieldPath?: string) => {
if (!fieldPath) return undefined;
return fieldPath.split('.').reduce((acc, cur) => acc?.[cur], record);
};
</script>
<script setup lang="ts">
import { pageSnmpLogApi, type NdmDeviceResultVO, type NdmSnmpLogResultVO, type PageParams, type Station } from '@/apis';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import dayjs from 'dayjs';
import destr from 'destr';
import { NCard, NDataTable, type DataTableColumns, type DataTableRowData, type PaginationProps } from 'naive-ui';
import { computed, h, onBeforeUnmount, reactive, ref, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmDeviceResultVO;
station: Station;
cpuUsageField?: string;
memUsageField?: string;
diskUsageField?: string;
}>();
const range = defineModel<[number, number]>('range');
const loading = defineModel<boolean>('loading', { default: false });
const emit = defineEmits<{
exposeQueryFn: [queryFn: () => void];
}>();
const { ndmDevice, station, cpuUsageField, memUsageField, diskUsageField } = toRefs(props);
const tableColumns = computed(() => {
const columns: DataTableColumns<NdmSnmpLogResultVO> = [{ title: '诊断时间', key: 'createdTime' }];
if (cpuUsageField.value) {
columns.push({ title: 'CPU使用率(%)', key: 'cpuUsage' });
}
if (memUsageField.value) {
columns.push({ title: '内存使用率(%)', key: 'memUsage' });
}
if (diskUsageField.value) {
columns.push({ title: '磁盘使用率(%)', key: 'diskUsage' });
}
return columns;
});
const tableData = ref<DataTableRowData[]>([]);
const DEFAULT_PAGE_SIZE = 10;
const pagination = reactive<PaginationProps>({
size: 'small',
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) => {
pagination.page = page;
getTableData();
},
onUpdatePageSize: (pageSize) => {
pagination.pageSize = pageSize;
pagination.page = 1;
getTableData();
},
});
const abortController = ref<AbortController>(new AbortController());
const { mutate: getTableData, isPending } = useMutation({
mutationFn: async () => {
if (!range.value) throw new Error('请选择时间范围');
abortController.value?.abort();
abortController.value = new AbortController();
const deviceId = ndmDevice.value.id;
if (!deviceId) throw new Error('该设备没有ID');
const createdTime_precisest = dayjs(range.value[0]).format('YYYY-MM-DD HH:mm:ss');
const createdTime_preciseed = dayjs(range.value[1]).format('YYYY-MM-DD HH:mm:ss');
const restParams: Omit<PageParams<{ id: string }>, 'model' | 'extra'> = {
current: pagination.page ?? 1,
size: pagination.pageSize ?? DEFAULT_PAGE_SIZE,
sort: 'id',
order: 'descending',
};
const pageResult = await pageSnmpLogApi(
{
model: { deviceId },
extra: { createdTime_precisest, createdTime_preciseed },
...restParams,
},
{
stationCode: station.value.code,
signal: abortController.value.signal,
},
);
return pageResult;
},
onSuccess: ({ records, size, total }) => {
pagination.pageSize = parseInt(size);
pagination.itemCount = parseInt(total);
tableData.value = records.map((record) => {
const diagInfoJsonString = record.diagInfo;
const diagInfo = destr(diagInfoJsonString);
if (!diagInfo) return {};
if (typeof diagInfo !== 'object') return {};
return {
createdTime: record.createdTime,
cpuUsage: getValueByFieldPath(diagInfo, cpuUsageField.value),
memUsage: getValueByFieldPath(diagInfo, memUsageField.value),
diskUsage: getValueByFieldPath(diagInfo, diskUsageField.value),
diagInfo,
};
});
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
emit('exposeQueryFn', () => {
pagination.page = 1;
pagination.pageSize = DEFAULT_PAGE_SIZE;
getTableData();
});
watch(isPending, (pending) => {
loading.value = pending;
});
onBeforeUnmount(() => {
abortController.value.abort();
});
</script>
<template>
<NCard hoverable size="small">
<template #header>
<span>设备硬件占用率记录</span>
</template>
<template #default>
<NDataTable size="small" :loading="isPending" :columns="tableColumns" :data="tableData" :pagination="pagination" :single-line="false" remote flex-height style="height: 500px" />
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import { NButton, NCard, NDatePicker, NFlex, NGrid, NGridItem, NSelect, type SelectOption } from 'naive-ui';
import { toRefs } from 'vue';
const props = defineProps<{
options: SelectOption[];
}>();
const range = defineModel<[number, number]>('range');
const selected = defineModel<string[]>('selected', { default: [] });
const loading = defineModel<boolean>('loading', { default: false });
const emit = defineEmits<{
query: [params?: { range: [number, number]; selected: string[] }];
}>();
const { options } = toRefs(props);
const onQuery = () => {
emit('query');
};
</script>
<template>
<NCard size="small">
<template #default>
<NFlex justify="space-between" :wrap="false">
<NGrid :x-gap="8" :y-gap="8">
<NGridItem span="22">
<NDatePicker type="datetimerange" v-model:value="range" />
</NGridItem>
<NGridItem span="22">
<NSelect multiple v-model:value="selected" :options="options" />
</NGridItem>
</NGrid>
<NButton secondary :loading="loading" @click="onQuery">查询数据</NButton>
</NFlex>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,27 @@
import DeviceAlarmHistoryCard from './device-alarm-history-card.vue';
import DeviceIcmpHistoryCard from './device-icmp-history-card.vue';
import DeviceUsageHistoryCard from './device-usage-history-card.vue';
import HistoryDiagFilterCard from './history-diag-filter-card.vue';
import NvrDiskHistoryCard from './nvr-disk-history-card.vue';
import SecurityBoxRuntimeHistoryCard from './security-box-runtime-history-card.vue';
import SwitchPortHistoryCard from './switch-port-history-card.vue';
import type { ComponentInstance } from 'vue';
export type HistoryDiagFilterCardProps = ComponentInstance<typeof HistoryDiagFilterCard>['$props'];
export type DeviceIcmpHistoryCardProps = ComponentInstance<typeof DeviceIcmpHistoryCard>['$props'];
export type DeviceAlarmHistoryCardProps = ComponentInstance<typeof DeviceAlarmHistoryCard>['$props'];
export type DeviceUsageHistoryCardProps = ComponentInstance<typeof DeviceUsageHistoryCard>['$props'];
export type NvrDiskHistoryCardProps = ComponentInstance<typeof NvrDiskHistoryCard>['$props'];
export type SecurityBoxRuntimeHistoryCardProps = ComponentInstance<typeof SecurityBoxRuntimeHistoryCard>['$props'];
export type SwitchPortHistoryCardProps = ComponentInstance<typeof SwitchPortHistoryCard>['$props'];
export {
//
DeviceAlarmHistoryCard,
DeviceIcmpHistoryCard,
DeviceUsageHistoryCard,
HistoryDiagFilterCard,
NvrDiskHistoryCard,
SecurityBoxRuntimeHistoryCard,
SwitchPortHistoryCard,
};

View File

@@ -0,0 +1,155 @@
<script lang="ts">
type NvrDiskHealthRowData = {
createdTime: string;
diskHealthRatio: string;
diskHealth: number[];
};
</script>
<script setup lang="ts">
import { pageSnmpLogApi, type NdmNvrDiagInfo, type NdmNvrResultVO, type PageParams, type Station } from '@/apis';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import dayjs from 'dayjs';
import destr from 'destr';
import { NCard, NDataTable, type DataTableColumns, type DataTableRowData, type PaginationProps } from 'naive-ui';
import { h, onBeforeUnmount, reactive, ref, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmNvrResultVO;
station: Station;
}>();
const range = defineModel<[number, number]>('range');
const loading = defineModel<boolean>('loading', { default: false });
const emit = defineEmits<{
exposeQueryFn: [queryFn: () => void];
}>();
const { ndmDevice, station } = toRefs(props);
const tableColumns: DataTableColumns<NvrDiskHealthRowData> = [
{ title: '诊断时间', key: 'createdTime' },
{ title: '磁盘健康度', key: 'diskHealthRatio' },
{
title: '描述',
key: 'desc',
render(rowData) {
const { diskHealth } = rowData;
const unhealthyDiskIndexes = diskHealth
.map((health, index) => ({ index, health }))
.filter(({ health }) => health !== 0)
.map(({ index }) => index);
if (unhealthyDiskIndexes.length === 0) return '正常';
return `磁盘${unhealthyDiskIndexes.join(', ')}异常`;
},
},
];
const tableData = ref<DataTableRowData[]>([]);
const DEFAULT_PAGE_SIZE = 10;
const pagination = reactive<PaginationProps>({
size: 'small',
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<AbortController>(new AbortController());
const { mutate: getTableData, isPending } = useMutation({
mutationFn: async () => {
if (!range.value) throw new Error('请选择时间范围');
const deviceId = ndmDevice.value.id;
if (!deviceId) throw new Error('该设备没有ID');
abortController.value?.abort();
abortController.value = new AbortController();
const createdTime_precisest = dayjs(range.value[0]).format('YYYY-MM-DD HH:mm:ss');
const createdTime_preciseed = dayjs(range.value[1]).format('YYYY-MM-DD HH:mm:ss');
const restParams: Omit<PageParams<{ id: string }>, 'model' | 'extra'> = {
current: pagination.page ?? 1,
size: pagination.pageSize ?? DEFAULT_PAGE_SIZE,
sort: 'id',
order: 'descending',
};
const pageResult = await pageSnmpLogApi(
{
model: { deviceId },
extra: { createdTime_precisest, createdTime_preciseed },
...restParams,
},
{
stationCode: station.value.code,
signal: abortController.value.signal,
},
);
return pageResult;
},
onSuccess: ({ records, size, total }) => {
pagination.pageSize = parseInt(size);
pagination.itemCount = parseInt(total);
tableData.value = records.map((record) => {
const diagInfoJsonString = record.diagInfo;
const diagInfo = destr<NdmNvrDiagInfo>(diagInfoJsonString);
if (!diagInfo) return {};
if (typeof diagInfo !== 'object') return {};
const healthDiskCount = diagInfo.info?.diskHealth?.filter((health) => health === 0).length ?? 0;
const totalDiskCount = diagInfo.info?.diskHealth?.length ?? 0;
return {
createdTime: record.createdTime,
diskHealthRatio: `${healthDiskCount}/${totalDiskCount}`,
diskHealth: diagInfo.info?.diskHealth ?? [],
};
});
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
emit('exposeQueryFn', () => {
pagination.page = 1;
pagination.pageSize = DEFAULT_PAGE_SIZE;
getTableData();
});
watch(isPending, (pending) => {
loading.value = pending;
});
onBeforeUnmount(() => {
abortController.value.abort();
});
</script>
<template>
<NCard hoverable size="small">
<template #header>
<span>磁盘健康度记录</span>
</template>
<template #default>
<NDataTable size="small" :loading="isPending" :columns="tableColumns" :data="tableData" :pagination="pagination" :single-line="false" remote flex-height style="height: 500px" />
</template>
</NCard>
</template>
<style scoped></style>

View File

@@ -0,0 +1,230 @@
<script lang="ts">
type SecurityBoxCircuitRowData = { number: number } & NdmSecurityBoxCircuit;
type SecurityBoxRuntimeRowData = { createdTime: string; diagInfo: NdmSecurityBoxDiagInfo };
</script>
<script setup lang="ts">
import { pageSnmpLogApi, type NdmSecurityBoxCircuit, type NdmSecurityBoxDiagInfo, type NdmSecurityBoxResultVO, type PageParams, type Station } from '@/apis';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import dayjs from 'dayjs';
import destr from 'destr';
import { NButton, NCard, NDataTable, NModal, type DataTableColumns, type DataTableRowData, type PaginationProps } from 'naive-ui';
import { h, onBeforeUnmount, reactive, ref, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmSecurityBoxResultVO;
station: Station;
}>();
const range = defineModel<[number, number]>('range');
const loading = defineModel<boolean>('loading', { default: false });
const emit = defineEmits<{
exposeQueryFn: [queryFn: () => void];
}>();
const { ndmDevice, station } = toRefs(props);
const showDetailModal = ref(false);
const detailTableColumns: DataTableColumns<SecurityBoxCircuitRowData> = [
{ title: '电路序号', key: 'number' },
{
title: '状态',
key: 'status',
render(rowData) {
const { status } = rowData;
if (status === 1) {
return '开启';
} else if (status === 0) {
return '关闭';
} else {
return '未知';
}
},
},
{
title: '电流(A)',
key: 'current',
render(rowData) {
return `${rowData.current.toFixed(3)}`;
},
},
{
title: '电压(V)',
key: 'voltage',
render(rowData) {
return `${rowData.voltage.toFixed(3)}`;
},
},
];
const detailTableData = ref<DataTableRowData[]>([]);
const tableColumns: DataTableColumns<SecurityBoxRuntimeRowData> = [
{ title: '诊断时间', key: 'createdTime' },
{
title: '温度(℃)',
key: 'temperature',
render(rowData) {
const { info } = rowData.diagInfo;
const boxInfo = info?.at(0);
if (!boxInfo) return '';
return boxInfo.temperature;
},
},
{
title: '湿度(%)',
key: 'humidity',
render(rowData) {
const { info } = rowData.diagInfo;
const boxInfo = info?.at(0);
if (!boxInfo) return '';
return boxInfo.humidity;
},
},
{
title: '风扇转速(rpm)',
key: 'fanSpeeds',
render(rowData) {
const { info } = rowData.diagInfo;
const boxInfo = info?.at(0);
if (!boxInfo) return '';
return h('pre', {}, { default: () => boxInfo.fanSpeeds?.join('\n') ?? '' });
},
},
// { title: '开关状态', key: 'switches' },
{
title: '电路状态',
key: 'circuits',
render(rowData) {
const { info } = rowData.diagInfo;
const boxInfo = info?.at(0);
if (!boxInfo) return '';
return h(
NButton,
{
text: true,
type: 'info',
size: 'small',
onClick: () => {
detailTableData.value = boxInfo.circuits?.map((circuit, index) => ({ ...circuit, number: index + 1 })) ?? [];
showDetailModal.value = true;
},
},
{
default: () => '查看',
},
);
},
},
];
const tableData = ref<DataTableRowData[]>([]);
const DEFAULT_PAGE_SIZE = 10;
const pagination = reactive<PaginationProps>({
size: 'small',
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) => {
pagination.page = page;
getTableData();
},
onUpdatePageSize: (pageSize) => {
pagination.pageSize = pageSize;
pagination.page = 1;
getTableData();
},
});
const abortController = ref<AbortController>(new AbortController());
const { mutate: getTableData, isPending } = useMutation({
mutationFn: async () => {
if (!range.value) throw new Error('请选择时间范围');
abortController.value?.abort();
abortController.value = new AbortController();
const deviceId = ndmDevice.value.id;
if (!deviceId) throw new Error('该设备没有ID');
const createdTime_precisest = dayjs(range.value[0]).format('YYYY-MM-DD HH:mm:ss');
const createdTime_preciseed = dayjs(range.value[1]).format('YYYY-MM-DD HH:mm:ss');
const restParams: Omit<PageParams<{ id: string }>, 'model' | 'extra'> = {
current: pagination.page ?? 1,
size: pagination.pageSize ?? DEFAULT_PAGE_SIZE,
sort: 'id',
order: 'descending',
};
const pageResult = await pageSnmpLogApi(
{
model: { deviceId },
extra: { createdTime_precisest, createdTime_preciseed },
...restParams,
},
{
stationCode: station.value.code,
signal: abortController.value.signal,
},
);
return pageResult;
},
onSuccess: ({ records, size, total }) => {
pagination.pageSize = parseInt(size);
pagination.itemCount = parseInt(total);
tableData.value = records.map((record) => {
const diagInfoJsonString = record.diagInfo;
const diagInfo = destr<NdmSecurityBoxDiagInfo>(diagInfoJsonString);
if (!diagInfo) return {};
if (typeof diagInfo !== 'object') return {};
return {
createdTime: record.createdTime,
diagInfo,
};
});
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
emit('exposeQueryFn', () => {
pagination.page = 1;
pagination.pageSize = DEFAULT_PAGE_SIZE;
getTableData();
});
watch(isPending, (pending) => {
loading.value = pending;
});
onBeforeUnmount(() => {
abortController.value.abort();
});
</script>
<template>
<NCard hoverable size="small">
<template #header>
<span>运行情况记录</span>
</template>
<template #default>
<NDataTable size="small" :loading="isPending" :columns="tableColumns" :data="tableData" :pagination="pagination" :single-line="false" remote flex-height style="height: 500px" />
</template>
</NCard>
<NModal v-model:show="showDetailModal" :preset="'card'" :auto-focus="false" :transform-origin="'center'" :content-style="{ height: '100%' }" style="min-width: 1440px; height: 100vh">
<NDataTable size="small" :columns="detailTableColumns" :data="detailTableData" :single-line="false" :remote="false" flex-height style="height: 100%" />
</NModal>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,206 @@
<script lang="ts">
type SwitchPortRowData = {
createdTime: string;
diagInfo: NdmSwitchDiagInfo;
};
</script>
<script setup lang="ts">
import { pageSnmpLogApi, type NdmSwitchDiagInfo, type NdmSwitchPortInfo, type NdmSwitchResultVO, type PageParams, type Station } from '@/apis';
import { getPortStatusValue, transformPortSpeed } from '@/helpers';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import dayjs from 'dayjs';
import destr from 'destr';
import { NButton, NCard, NDataTable, NModal, type DataTableColumns, type DataTableRowData, type PaginationProps } from 'naive-ui';
import { h, onBeforeUnmount, reactive, ref, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmSwitchResultVO;
station: Station;
}>();
const range = defineModel<[number, number]>('range');
const loading = defineModel<boolean>('loading', { default: false });
const emit = defineEmits<{
exposeQueryFn: [queryFn: () => void];
}>();
const { ndmDevice, station } = toRefs(props);
const showDetailModal = ref(false);
const detailTableColumns: DataTableColumns<NdmSwitchPortInfo> = [
{ title: '端口名称', key: 'portName' },
{
title: '状态',
key: 'upDown',
render(rowData) {
return getPortStatusValue(rowData);
},
},
{
title: '上行速率',
key: 'inFlow',
render(rowData) {
return transformPortSpeed(rowData, 'in');
},
},
{
title: '下行速率',
key: 'outFlow',
render(rowData) {
return transformPortSpeed(rowData, 'out');
},
},
{
title: '总速率',
key: 'totalFlow',
render(rowData) {
return transformPortSpeed(rowData, 'total');
},
},
];
const detailTableData = ref<DataTableRowData[]>([]);
const tableColumns: DataTableColumns<SwitchPortRowData> = [
{ title: '诊断时间', key: 'createdTime' },
{
title: '超载端口',
key: 'overFlowPorts',
render(rowData) {
const { diagInfo } = rowData;
const { info } = diagInfo;
const { overFlowPorts } = info ?? {};
return h('pre', {}, { default: () => overFlowPorts?.join('\n') ?? '' });
},
},
{
title: '操作',
key: 'action',
render(rowData) {
return h(
NButton,
{
text: true,
type: 'info',
onClick: () => {
const { diagInfo } = rowData;
if (!diagInfo) return;
showDetailModal.value = true;
detailTableData.value = diagInfo.info?.portInfoList ?? [];
},
},
{ default: () => '查看' },
);
},
},
];
const tableData = ref<DataTableRowData[]>([]);
const DEFAULT_PAGE_SIZE = 10;
const pagination = reactive<PaginationProps>({
size: 'small',
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) => {
pagination.page = page;
getTableData();
},
onUpdatePageSize: (pageSize) => {
pagination.pageSize = pageSize;
pagination.page = 1;
getTableData();
},
});
const abortController = ref<AbortController>(new AbortController());
const { mutate: getTableData, isPending } = useMutation({
mutationFn: async () => {
if (!range.value) throw new Error('请选择时间范围');
abortController.value?.abort();
abortController.value = new AbortController();
const deviceId = ndmDevice.value.id;
if (!deviceId) throw new Error('该设备没有ID');
const createdTime_precisest = dayjs(range.value[0]).format('YYYY-MM-DD HH:mm:ss');
const createdTime_preciseed = dayjs(range.value[1]).format('YYYY-MM-DD HH:mm:ss');
const restParams: Omit<PageParams<{ id: string }>, 'model' | 'extra'> = {
current: pagination.page ?? 1,
size: pagination.pageSize ?? DEFAULT_PAGE_SIZE,
sort: 'id',
order: 'descending',
};
const pageResult = await pageSnmpLogApi(
{
model: { deviceId },
extra: { createdTime_precisest, createdTime_preciseed },
...restParams,
},
{
stationCode: station.value.code,
},
);
return pageResult;
},
onSuccess: ({ records, size, total }) => {
pagination.pageSize = parseInt(size);
pagination.itemCount = parseInt(total);
tableData.value = records.map((record) => {
const diagInfoJsonString = record.diagInfo;
const diagInfo = destr<NdmSwitchDiagInfo>(diagInfoJsonString);
if (!diagInfo) return {};
if (typeof diagInfo !== 'object') return {};
return {
createdTime: record.createdTime,
diagInfo,
};
});
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
emit('exposeQueryFn', () => {
pagination.page = 1;
pagination.pageSize = DEFAULT_PAGE_SIZE;
getTableData();
});
watch(isPending, (pending) => {
loading.value = pending;
});
onBeforeUnmount(() => {
abortController.value.abort();
});
</script>
<template>
<NCard hoverable size="small">
<template #header>
<span>端口速率记录</span>
</template>
<template #default>
<NDataTable size="small" :loading="isPending" :columns="tableColumns" :data="tableData" :pagination="pagination" :single-line="false" remote flex-height style="height: 500px" />
</template>
</NCard>
<NModal v-model:show="showDetailModal" :preset="'card'" :auto-focus="false" :transform-origin="'center'" :content-style="{ height: '100%' }" style="min-width: 1440px; height: 100vh">
<NDataTable size="small" :columns="detailTableColumns" :data="detailTableData" :single-line="false" :remote="false" flex-height style="height: 100%" />
</NModal>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,3 @@
export * from './current-diag';
export * from './history-diag';
export * from './raw-diag';

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import type { NdmDeviceResultVO } from '@/apis';
import destr from 'destr';
import { NCard } from 'naive-ui';
import { computed, toRefs } from 'vue';
const props = defineProps<{
ndmDevice: NdmDeviceResultVO;
}>();
const { ndmDevice } = toRefs(props);
const lastDiagInfo = computed(() => {
return destr(ndmDevice.value.lastDiagInfo);
});
</script>
<template>
<NCard hoverable size="small">
<template #default>
<pre>{{ { ...ndmDevice, lastDiagInfo } }}</pre>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,3 @@
import DeviceRawCard from './device-raw-card.vue';
export { DeviceRawCard };

View File

@@ -0,0 +1,9 @@
export * from './components';
export * from './ndm-alarm-host';
export * from './ndm-camera';
export * from './ndm-decoder';
export * from './ndm-keyboard';
export * from './ndm-nvr';
export * from './ndm-security-box';
export * from './ndm-server';
export * from './ndm-switch';

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import type { NdmAlarmHostResultVO, Station } from '@/apis';
import { AlarmHostCurrentDiag, AlarmHostHistoryDiag, AlarmHostUpdate, DeviceRawCard } 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: NdmAlarmHostResultVO;
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 === '当前诊断'">
<AlarmHostCurrentDiag :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '历史诊断'">
<AlarmHostHistoryDiag :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '修改设备'">
<AlarmHostUpdate :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '原始数据'">
<DeviceRawCard :ndm-device="ndmDevice" />
</template>
</NScrollbar>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import { NFlex } from 'naive-ui';
import { DeviceCommonCard, DeviceHeaderCard } from '@/components';
import type { NdmAlarmHostResultVO, Station } from '@/apis';
import { computed, toRefs } from 'vue';
const props = defineProps<{
ndmDevice: NdmAlarmHostResultVO;
station: Station;
}>();
const { ndmDevice, station } = toRefs(props);
const commonInfo = computed(() => {
const { createdTime, updatedTime, manufacturer } = ndmDevice.value;
return {
创建时间: createdTime ?? '-',
更新时间: updatedTime ?? '-',
制造商: manufacturer ?? '-',
};
});
</script>
<template>
<NFlex vertical>
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
<DeviceCommonCard :common-info="commonInfo" />
</NFlex>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,76 @@
<script setup lang="ts">
import type { NdmAlarmHostResultVO, Station } from '@/apis';
import { DeviceAlarmHistoryCard, DeviceIcmpHistoryCard, HistoryDiagFilterCard, type DeviceAlarmHistoryCardProps, type DeviceIcmpHistoryCardProps } from '@/components';
import dayjs from 'dayjs';
import { NFlex, type SelectOption } from 'naive-ui';
import { onMounted, ref, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmAlarmHostResultVO;
station: Station;
}>();
const { ndmDevice, station } = toRefs(props);
const historyDiagOptions: SelectOption[] = [
{ label: '设备状态', value: 'icmp' },
{ label: '设备告警', value: 'alarm' },
];
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);
watch([icmpLoading, alarmLoading], (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 queryData = () => {
if (selected.value.includes('icmp')) icmpHistoryQueryFn.value?.();
if (selected.value.includes('alarm')) alarmHistoryQueryFn.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"
/>
</NFlex>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,152 @@
<script setup lang="ts">
import { icmpEntityByDeviceId, type NdmAlarmHostResultVO, type NdmAlarmHostUpdateVO, type Station } from '@/apis';
import { detailAlarmHostApi, updateAlarmHostApi } from '@/apis/request/biz/alarm/ndm-alarm-host';
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: NdmAlarmHostResultVO;
station: Station;
}>();
const deviceStore = useDeviceStore();
const { ndmDevice, station } = toRefs(props);
const localDevice = ref<NdmAlarmHostUpdateVO>({ ...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}01\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: loading } = useMutation({
mutationFn: async () => {
await formInst.value?.validate().catch(() => {
window.$message.error('表单验证失败');
return;
});
abortController.value.abort();
abortController.value = new AbortController();
const stationCode = station.value.code;
const signal = abortController.value.signal;
await updateAlarmHostApi(localDevice.value, { stationCode, signal });
const result = await detailAlarmHostApi(`${localDevice.value.id}`, { stationCode, signal });
return result;
},
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="管理URL">
<NInput v-model:value="localDevice.manageUrl" />
</NFormItem>
<NFormItem label-placement="left" label="管理用户名">
<NInput v-model:value="localDevice.manageUsername" />
</NFormItem>
<NFormItem label-placement="left" label="管理密码">
<NInput v-model:value="localDevice.managePassword" />
</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="loading" @click="() => updateDevice()">更新</NButton>
</NFlex>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,6 @@
import AlarmHostCard from './alarm-host-card.vue';
import AlarmHostCurrentDiag from './alarm-host-current-diag.vue';
import AlarmHostHistoryDiag from './alarm-host-history-diag.vue';
import AlarmHostUpdate from './alarm-host-update.vue';
export { AlarmHostCard, AlarmHostCurrentDiag, AlarmHostHistoryDiag, AlarmHostUpdate };

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import type { NdmCameraResultVO, Station } from '@/apis';
import { CameraCurrentDiag, CameraHistoryDiag, CameraUpdate, DeviceRawCard } 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: NdmCameraResultVO;
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 === '当前诊断'">
<CameraCurrentDiag :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '历史诊断'">
<CameraHistoryDiag :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '修改设备'">
<CameraUpdate :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '原始数据'">
<DeviceRawCard :ndm-device="ndmDevice" />
</template>
</NScrollbar>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,52 @@
<script setup lang="ts">
import type { NdmCameraResultVO, Station } from '@/apis';
import { DeviceCommonCard, DeviceHeaderCard } from '@/components';
import { NFlex } from 'naive-ui';
import { computed, toRefs } from 'vue';
const props = defineProps<{
ndmDevice: NdmCameraResultVO;
station: Station;
}>();
const { ndmDevice, station } = toRefs(props);
const commonInfo = computed(() => {
const {
createdTime,
updatedTime,
manufacturer,
gb28181Enabled,
onvifPort,
onvifUsername,
onvifPassword,
onvifMajorIndex,
onvifMinorIndex,
icmpEnabled,
community,
//
} = ndmDevice.value;
return {
创建时间: createdTime ?? '-',
更新时间: updatedTime ?? '-',
制造商: manufacturer ?? '-',
GB28181启用: `${!!gb28181Enabled ? '是' : '否'}`,
ONVIF端口: `${onvifPort ?? '-'}`,
ONVIF用户名: onvifUsername ?? '-',
ONVIF密码: onvifPassword ?? '-',
ONVIF主流索引: `${onvifMajorIndex ?? '-'}`,
ONVIF辅流索引: `${onvifMinorIndex ?? '-'}`,
ICMP启用: `${!!icmpEnabled ? '是' : '否'}`,
团体字符串: community ?? '-',
};
});
</script>
<template>
<NFlex vertical>
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
<DeviceCommonCard :common-info="commonInfo" />
</NFlex>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,76 @@
<script setup lang="ts">
import type { NdmCameraResultVO, Station } from '@/apis';
import { DeviceAlarmHistoryCard, DeviceIcmpHistoryCard, HistoryDiagFilterCard, type DeviceAlarmHistoryCardProps, type DeviceIcmpHistoryCardProps } from '@/components';
import dayjs from 'dayjs';
import { NFlex, type SelectOption } from 'naive-ui';
import { onMounted, ref, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmCameraResultVO;
station: Station;
}>();
const { ndmDevice, station } = toRefs(props);
const historyDiagOptions: SelectOption[] = [
{ label: '设备状态', value: 'icmp' },
{ label: '设备告警', value: 'alarm' },
];
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);
watch([icmpLoading, alarmLoading], (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 queryData = () => {
if (selected.value.includes('icmp')) icmpHistoryQueryFn.value?.();
if (selected.value.includes('alarm')) alarmHistoryQueryFn.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"
/>
</NFlex>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,133 @@
<script setup lang="ts">
import { detailCameraApi, icmpEntityByDeviceId, updateCameraApi, type NdmCameraResultVO, type NdmCameraUpdateVO, type Station } from '@/apis';
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: NdmCameraResultVO;
station: Station;
}>();
const deviceStore = useDeviceStore();
const { ndmDevice, station } = toRefs(props);
const localDevice = ref<NdmCameraUpdateVO>({ ...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}06\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: loading } = useMutation({
mutationFn: async () => {
await formInst.value?.validate().catch(() => {
window.$message.error('表单验证失败');
return;
});
abortController.value.abort();
abortController.value = new AbortController();
const stationCode = station.value.code;
const signal = abortController.value.signal;
await updateCameraApi(localDevice.value, { stationCode, signal });
const result = await detailCameraApi(`${localDevice.value.id}`, { stationCode, signal });
return result;
},
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.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="loading" @click="() => updateDevice()">更新</NButton>
</NFlex>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,6 @@
import CameraCard from './camera-card.vue';
import CameraCurrentDiag from './camera-current-diag.vue';
import CameraHistoryDiag from './camera-history-diag.vue';
import CameraUpdate from './camera-update.vue';
export { CameraCard, CameraCurrentDiag, CameraHistoryDiag, CameraUpdate };

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import type { NdmDecoderResultVO, Station } from '@/apis';
import { DecoderCurrentDiag, DecoderHistoryDiag, DecoderUpdate, DeviceRawCard } 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: NdmDecoderResultVO;
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 === '当前诊断'">
<DecoderCurrentDiag :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '历史诊断'">
<DecoderHistoryDiag :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '修改设备'">
<DecoderUpdate :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '原始数据'">
<DeviceRawCard :ndm-device="ndmDevice" />
</template>
</NScrollbar>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
import type { NdmDecoderDiagInfo, NdmDecoderResultVO, Station } from '@/apis';
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard } from '@/components';
import destr from 'destr';
import { NFlex } from 'naive-ui';
import { computed, toRefs } from 'vue';
const props = defineProps<{
ndmDevice: NdmDecoderResultVO;
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 NdmDecoderDiagInfo;
});
const commonInfo = computed(() => {
const { stCommonInfo } = lastDiagInfo.value ?? {};
const { 设备ID, 软件版本, 设备厂商, 设备别名, 设备型号, 硬件版本 } = stCommonInfo ?? {};
return {
设备ID: 设备ID ?? '-',
软件版本: 软件版本 ?? '-',
设备厂商: 设备厂商 ?? '-',
设备别名: 设备别名 ?? '-',
设备型号: 设备型号 ?? '-',
硬件版本: 硬件版本 ?? '-',
};
});
const cpuUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.CPU使用率);
const memUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.内存使用率);
</script>
<template>
<NFlex vertical>
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
<DeviceCommonCard :common-info="commonInfo" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
</NFlex>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,100 @@
<script setup lang="ts">
import type { NdmDecoderResultVO, 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: NdmDecoderResultVO;
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([...historyDiagOptions.map((option) => `${option.value}`)]);
const loading = ref<boolean>(false);
const icmpLoading = ref<boolean>(false);
const alarmLoading = ref<boolean>(false);
watch([icmpLoading, alarmLoading], (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="'stCommonInfo.CPU使用率'"
:mem-usage-field="'stCommonInfo.内存使用率'"
v-model:range="range"
v-model:loading="alarmLoading"
@expose-query-fn="onExposeUsageHistoryQueryFn"
/>
</NFlex>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,154 @@
<script setup lang="ts">
import { detailDecoderApi, icmpEntityByDeviceId, updateDecoderApi, type NdmDecoderResultVO, type NdmDecoderUpdateVO, type Station } from '@/apis';
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: NdmDecoderResultVO;
station: Station;
}>();
const deviceStore = useDeviceStore();
const { ndmDevice, station } = toRefs(props);
const localDevice = ref<NdmDecoderUpdateVO>({ ...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}07\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: loading } = useMutation({
mutationFn: async () => {
await formInst.value?.validate().catch(() => {
window.$message.error('表单验证失败');
return;
});
abortController.value.abort();
abortController.value = new AbortController();
const stationCode = station.value.code;
const signal = abortController.value.signal;
await updateDecoderApi(localDevice.value, { stationCode, signal });
const result = await detailDecoderApi(`${localDevice.value.id}`, { stationCode, signal });
return result;
},
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="device">
<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="管理URL">
<NInput v-model:value="localDevice.manageUrl" />
</NFormItem>
<NFormItem label-placement="left" label="管理用户名">
<NInput v-model:value="localDevice.manageUsername" />
</NFormItem>
<NFormItem label-placement="left" label="管理密码">
<NInput v-model:value="localDevice.managePassword" />
</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="loading" @click="() => updateDevice()">更新</NButton>
</NFlex>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,6 @@
import DecoderCard from './decoder-card.vue';
import DecoderCurrentDiag from './decoder-current-diag.vue';
import DecoderHistoryDiag from './decoder-history-diag.vue';
import DecoderUpdate from './decoder-update.vue';
export { DecoderCard, DecoderCurrentDiag, DecoderHistoryDiag, DecoderUpdate };

View File

@@ -0,0 +1,6 @@
import KeyboardCard from './keyboard-card.vue';
import KeyboardCurrentDiag from './keyboard-current-diag.vue';
import KeyboardHistoryDiag from './keyboard-history-diag.vue';
import KeyboardUpdate from './keyboard-update.vue';
export { KeyboardCard, KeyboardCurrentDiag, KeyboardHistoryDiag, KeyboardUpdate };

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import type { NdmKeyboardResultVO, Station } from '@/apis';
import { DeviceRawCard, KeyboardCurrentDiag, KeyboardHistoryDiag, KeyboardUpdate } from '@/components';
import { useSettingStore } from '@/stores';
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { ref, toRefs, computed, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
const props = defineProps<{
ndmDevice: NdmKeyboardResultVO;
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 === '当前诊断'">
<KeyboardCurrentDiag :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '历史诊断'">
<KeyboardHistoryDiag :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '修改设备'">
<KeyboardUpdate :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '原始数据'">
<DeviceRawCard :ndm-device="ndmDevice" />
</template>
</NScrollbar>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { NdmKeyboardResultVO, Station } from '@/apis';
import { DeviceHeaderCard } from '@/components';
import { NFlex } from 'naive-ui';
import { toRefs } from 'vue';
const props = defineProps<{
ndmDevice: NdmKeyboardResultVO;
station: Station;
}>();
const { ndmDevice, station } = toRefs(props);
</script>
<template>
<NFlex vertical>
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
</NFlex>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,76 @@
<script setup lang="ts">
import type { NdmKeyboardResultVO, Station } from '@/apis';
import { DeviceAlarmHistoryCard, DeviceIcmpHistoryCard, HistoryDiagFilterCard, type DeviceAlarmHistoryCardProps, type DeviceIcmpHistoryCardProps } from '@/components';
import dayjs from 'dayjs';
import { NFlex, type SelectOption } from 'naive-ui';
import { onMounted, ref, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmKeyboardResultVO;
station: Station;
}>();
const { ndmDevice, station } = toRefs(props);
const historyDiagOptions: SelectOption[] = [
{ label: '设备状态', value: 'icmp' },
{ label: '设备告警', value: 'alarm' },
];
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);
watch([icmpLoading, alarmLoading], (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 queryData = () => {
if (selected.value.includes('icmp')) icmpHistoryQueryFn.value?.();
if (selected.value.includes('alarm')) alarmHistoryQueryFn.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"
/>
</NFlex>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,142 @@
<script setup lang="ts">
import { detailKeyboardApi, icmpEntityByDeviceId, updateKeyboardApi, type NdmKeyboardResultVO, type NdmKeyboardUpdateVO, type Station } from '@/apis';
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: NdmKeyboardResultVO;
station: Station;
}>();
const deviceStore = useDeviceStore();
const { ndmDevice, station } = toRefs(props);
const localDevice = ref<NdmKeyboardUpdateVO>({ ...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}08\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: loading } = useMutation({
mutationFn: async () => {
await formInst.value?.validate().catch(() => {
window.$message.error('表单验证失败');
return;
});
abortController.value.abort();
abortController.value = new AbortController();
const stationCode = station.value.code;
const signal = abortController.value.signal;
await updateKeyboardApi(localDevice.value, { stationCode, signal });
const result = await detailKeyboardApi(`${localDevice.value.id}`, { stationCode, signal });
return result;
},
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.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="loading" @click="() => updateDevice()">更新</NButton>
</NFlex>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,6 @@
import NvrCard from './nvr-card.vue';
import NvrCurrentDiag from './nvr-current-diag.vue';
import NvrHistoryDiag from './nvr-history-diag.vue';
import NvrUpdate from './nvr-update.vue';
export { NvrCard, NvrCurrentDiag, NvrHistoryDiag, NvrUpdate };

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import type { NdmNvrResultVO, Station } from '@/apis';
import { DeviceRawCard, NvrCurrentDiag, NvrHistoryDiag, NvrUpdate } 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: NdmNvrResultVO;
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 === '当前诊断'">
<NvrCurrentDiag :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '历史诊断'">
<NvrHistoryDiag :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '修改设备'">
<NvrUpdate :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '原始数据'">
<DeviceRawCard :ndm-device="ndmDevice" />
</template>
</NScrollbar>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,54 @@
<script setup lang="ts">
import type { NdmNvrDiagInfo, NdmNvrResultVO, Station } from '@/apis';
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, NvrDiskCard, NvrRecordCard } from '@/components';
import { isNvrCluster } from '@/helpers';
import destr from 'destr';
import { NFlex } from 'naive-ui';
import { computed, toRefs } from 'vue';
const props = defineProps<{
ndmDevice: NdmNvrResultVO;
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 NdmNvrDiagInfo;
});
const commonInfo = computed(() => {
const { stCommonInfo } = lastDiagInfo.value ?? {};
if (!stCommonInfo) return undefined;
const { 设备ID, 软件版本, 生产厂商, 设备别名, 设备型号, 硬件版本 } = stCommonInfo;
return {
设备ID: 设备ID ?? '-',
软件版本: 软件版本 ?? '-',
生产厂商: 生产厂商 ?? '-',
设备别名: 设备别名 ?? '-',
设备型号: 设备型号 ?? '-',
硬件版本: 硬件版本 ?? '-',
};
});
const cpuUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.CPU使用率);
const memUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.内存使用率);
const diskHealth = computed(() => lastDiagInfo.value?.info?.diskHealth);
const diskArray = computed(() => lastDiagInfo.value?.info?.groupInfoList);
</script>
<template>
<NFlex vertical>
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
<DeviceCommonCard :common-info="commonInfo" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
<NvrDiskCard :disk-health="diskHealth" :disk-array="diskArray" />
<NvrRecordCard v-if="isNvrCluster(ndmDevice)" :ndm-device="ndmDevice" :station="station" />
</NFlex>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,113 @@
<script setup lang="ts">
import type { NdmNvrResultVO, Station } from '@/apis';
import {
DeviceAlarmHistoryCard,
DeviceIcmpHistoryCard,
DeviceUsageHistoryCard,
HistoryDiagFilterCard,
NvrDiskHistoryCard,
type DeviceAlarmHistoryCardProps,
type DeviceIcmpHistoryCardProps,
type DeviceUsageHistoryCardProps,
type NvrDiskHistoryCardProps,
} from '@/components';
import { isNvrCluster } from '@/helpers';
import dayjs from 'dayjs';
import { NFlex, type SelectOption } from 'naive-ui';
import { computed, onMounted, ref, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmNvrResultVO;
station: Station;
}>();
const { ndmDevice, station } = toRefs(props);
const historyDiagOptions = computed<SelectOption[]>(() => {
const options: SelectOption[] = [
{ label: '设备状态', value: 'icmp' },
{ label: '设备告警', value: 'alarm' },
];
if (isNvrCluster(ndmDevice.value)) return options;
return [...options, { label: '硬件占用', value: 'usage' }, { label: '磁盘健康', value: 'disk' }];
});
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([...historyDiagOptions.value.map((option) => `${option.value}`)]);
const loading = ref<boolean>(false);
const icmpLoading = ref<boolean>(false);
const alarmLoading = ref<boolean>(false);
const diskLoading = ref<boolean>(false);
watch([icmpLoading, alarmLoading, diskLoading], (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 diskHistoryQueryFn = ref<() => void>();
const onExposeDiskHistoryQueryFn: NvrDiskHistoryCardProps['onExposeQueryFn'] = (queryFn) => {
diskHistoryQueryFn.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?.();
if (selected.value.includes('disk')) diskHistoryQueryFn.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="'stCommonInfo.CPU使用率'"
:mem-usage-field="'stCommonInfo.内存使用率'"
v-model:range="range"
v-model:loading="alarmLoading"
@expose-query-fn="onExposeUsageHistoryQueryFn"
/>
<NvrDiskHistoryCard v-if="selected.includes('disk')" :ndm-device="ndmDevice" :station="station" v-model:range="range" v-model:loading="diskLoading" @expose-query-fn="onExposeDiskHistoryQueryFn" />
</NFlex>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,154 @@
<script setup lang="ts">
import { detailNvrApi, icmpEntityByDeviceId, updateNvrApi, type NdmNvrResultVO, type NdmNvrUpdateVO, type Station } from '@/apis';
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: NdmNvrResultVO;
station: Station;
}>();
const deviceStore = useDeviceStore();
const { ndmDevice, station } = toRefs(props);
const localDevice = ref<NdmNvrUpdateVO>({ ...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}05\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 stationCode = station.value.code;
const signal = abortController.value.signal;
await updateNvrApi(localDevice.value, { stationCode, signal });
const result = await detailNvrApi(`${localDevice.value.id}`, { stationCode, signal });
return result;
},
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="管理URL">
<NInput v-model:value="localDevice.manageUrl" />
</NFormItem>
<NFormItem label-placement="left" label="管理用户名">
<NInput v-model:value="localDevice.manageUsername" />
</NFormItem>
<NFormItem label-placement="left" label="管理密码">
<NInput v-model:value="localDevice.managePassword" />
</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>

View File

@@ -0,0 +1,6 @@
import SecurityBoxCard from './security-box-card.vue';
import SecurityBoxCurrentDiag from './security-box-current-diag.vue';
import SecurityBoxHistoryDiag from './security-box-history-diag.vue';
import SecurityBoxUpdate from './security-box-update.vue';
export { SecurityBoxCard, SecurityBoxCurrentDiag, SecurityBoxHistoryDiag, SecurityBoxUpdate };

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import type { NdmSecurityBoxResultVO, Station } from '@/apis';
import { DeviceRawCard, SecurityBoxCurrentDiag, SecurityBoxHistoryDiag, SecurityBoxUpdate } 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: NdmSecurityBoxResultVO;
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 === '当前诊断'">
<SecurityBoxCurrentDiag :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '历史诊断'">
<SecurityBoxHistoryDiag :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '修改设备'">
<SecurityBoxUpdate :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '原始数据'">
<DeviceRawCard :ndm-device="ndmDevice" />
</template>
</NScrollbar>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,56 @@
<script setup lang="ts">
import type { NdmSecurityBoxDiagInfo, NdmSecurityBoxResultVO, Station } from '@/apis';
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, SecurityBoxCircuitCard, SecurityBoxEnvCard } from '@/components';
import destr from 'destr';
import { NFlex } from 'naive-ui';
import { computed, toRefs } from 'vue';
const props = defineProps<{
ndmDevice: NdmSecurityBoxResultVO;
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 NdmSecurityBoxDiagInfo;
});
const commonInfo = computed(() => {
const { stCommonInfo } = lastDiagInfo.value ?? {};
const { 设备ID, 软件版本, 设备厂商, 设备别名, 设备型号, 硬件版本 } = stCommonInfo ?? {};
return {
设备ID: 设备ID ?? '',
软件版本: 软件版本 ?? '',
设备厂商: 设备厂商 ?? '',
设备别名: 设备别名 ?? '',
设备型号: 设备型号 ?? '',
硬件版本: 硬件版本 ?? '',
};
});
const cpuUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.CPU使用率);
const memUsage = computed(() => lastDiagInfo.value?.stCommonInfo?.内存使用率);
const fanSpeeds = computed(() => lastDiagInfo.value?.info?.at(0)?.fanSpeeds);
const temperature = computed(() => lastDiagInfo.value?.info?.at(0)?.temperature);
const humidity = computed(() => lastDiagInfo.value?.info?.at(0)?.humidity);
const switches = computed(() => lastDiagInfo.value?.info?.at(0)?.switches);
const circuits = computed(() => lastDiagInfo.value?.info?.at(0)?.circuits);
</script>
<template>
<NFlex vertical>
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
<DeviceCommonCard :common-info="commonInfo" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
<SecurityBoxEnvCard :fan-speeds="fanSpeeds" :temperature="temperature" :humidity="humidity" :switches="switches" />
<SecurityBoxCircuitCard :circuits="circuits" :ndm-device="ndmDevice" :station="station" />
</NFlex>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,118 @@
<script setup lang="ts">
import type { NdmSecurityBoxResultVO, Station } from '@/apis';
import {
DeviceAlarmHistoryCard,
DeviceIcmpHistoryCard,
DeviceUsageHistoryCard,
HistoryDiagFilterCard,
SecurityBoxRuntimeHistoryCard,
type DeviceAlarmHistoryCardProps,
type DeviceIcmpHistoryCardProps,
type DeviceUsageHistoryCardProps,
type SecurityBoxRuntimeHistoryCardProps,
} from '@/components';
import dayjs from 'dayjs';
import { NFlex, type SelectOption } from 'naive-ui';
import { onMounted, ref, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmSecurityBoxResultVO;
station: Station;
}>();
const { ndmDevice, station } = toRefs(props);
const historyDiagOptions: SelectOption[] = [
{ label: '设备状态', value: 'icmp' },
{ label: '设备告警', value: 'alarm' },
{ label: '硬件占用', value: 'usage' },
{ label: '运行情况', value: 'runtime' },
];
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);
const runtimeLoading = ref<boolean>(false);
watch([icmpLoading, alarmLoading, usageLoading, runtimeLoading], (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 runtimeHistoryQueryFn = ref<() => void>();
const onExposeRuntimeHistoryQueryFn: SecurityBoxRuntimeHistoryCardProps['onExposeQueryFn'] = (queryFn) => {
runtimeHistoryQueryFn.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?.();
if (selected.value.includes('runtime')) runtimeHistoryQueryFn.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="'stCommonInfo.CPU使用率'"
:mem-usage-field="'stCommonInfo.内存使用率'"
v-model:range="range"
v-model:loading="usageLoading"
@expose-query-fn="onExposeUsageHistoryQueryFn"
/>
<SecurityBoxRuntimeHistoryCard
v-if="selected.includes('runtime')"
:ndm-device="ndmDevice"
:station="station"
v-model:range="range"
v-model:loading="runtimeLoading"
@expose-query-fn="onExposeRuntimeHistoryQueryFn"
/>
</NFlex>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,145 @@
<script setup lang="ts">
import { detailSecurityBoxApi, icmpEntityByDeviceId, updateSecurityBoxApi, type NdmSecurityBoxResultVO, type NdmSecurityBoxUpdateVO, type Station } from '@/apis';
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: NdmSecurityBoxResultVO;
station: Station;
}>();
const deviceStore = useDeviceStore();
const { ndmDevice, station } = toRefs(props);
const localDevice = ref<NdmSecurityBoxUpdateVO>({ ...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}03\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 stationCode = station.value.code;
const signal = abortController.value.signal;
await updateSecurityBoxApi(localDevice.value, { stationCode, signal });
const result = await detailSecurityBoxApi(`${localDevice.value.id}`, { stationCode, signal });
return result;
},
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>

View 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 };

View 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>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -0,0 +1,6 @@
import SwitchCard from './switch-card.vue';
import SwitchCurrentDiag from './switch-current-diag.vue';
import SwitchHistoryDiag from './switch-history-diag.vue';
import SwitchUpdate from './switch-update.vue';
export { SwitchCard, SwitchCurrentDiag, SwitchHistoryDiag, SwitchUpdate };

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import type { NdmSwitchResultVO, Station } from '@/apis';
import { DeviceRawCard, SwitchCurrentDiag, SwitchHistoryDiag, SwitchUpdate } 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: NdmSwitchResultVO;
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 === '当前诊断'">
<SwitchCurrentDiag :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '历史诊断'">
<SwitchHistoryDiag :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '修改设备'">
<SwitchUpdate :ndm-device="ndmDevice" :station="station" />
</template>
<template v-if="activeTabName === '原始数据'">
<DeviceRawCard :ndm-device="ndmDevice" />
</template>
</NScrollbar>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import type { NdmSwitchDiagInfo, NdmSwitchResultVO, Station } from '@/apis';
import { DeviceHardwareCard, DeviceHeaderCard, SwitchPortCard } from '@/components';
import destr from 'destr';
import { NFlex } from 'naive-ui';
import { computed, toRefs } from 'vue';
const props = defineProps<{
ndmDevice: NdmSwitchResultVO;
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 NdmSwitchDiagInfo;
});
const cpuUsage = computed(() => lastDiagInfo.value?.cpuRatio);
const memUsage = computed(() => lastDiagInfo.value?.memoryRatio);
const ports = computed(() => lastDiagInfo.value?.info?.portInfoList);
</script>
<template>
<NFlex vertical>
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
<SwitchPortCard :ports="ports" />
</NFlex>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,118 @@
<script setup lang="ts">
import type { NdmSwitchResultVO, Station } from '@/apis';
import {
DeviceAlarmHistoryCard,
DeviceIcmpHistoryCard,
DeviceUsageHistoryCard,
HistoryDiagFilterCard,
SwitchPortHistoryCard,
type DeviceAlarmHistoryCardProps,
type DeviceIcmpHistoryCardProps,
type DeviceUsageHistoryCardProps,
type SwitchPortHistoryCardProps,
} from '@/components';
import dayjs from 'dayjs';
import { NFlex, type SelectOption } from 'naive-ui';
import { onMounted, ref, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmSwitchResultVO;
station: Station;
}>();
const { ndmDevice, station } = toRefs(props);
const historyDiagOptions: SelectOption[] = [
{ label: '设备状态', value: 'icmp' },
{ label: '设备告警', value: 'alarm' },
{ label: '硬件占用', value: 'usage' },
{ label: '端口速率', value: 'port' },
];
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);
const portLoading = ref<boolean>(false);
watch([icmpLoading, alarmLoading, usageLoading, portLoading], (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 portHistoryQueryFn = ref<() => void>();
const onExposePortHistoryQueryFn: SwitchPortHistoryCardProps['onExposeQueryFn'] = (queryFn) => {
portHistoryQueryFn.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?.();
if (selected.value.includes('port')) portHistoryQueryFn.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="'cpuRatio'"
:mem-usage-field="'memoryRatio'"
v-model:range="range"
v-model:loading="usageLoading"
@expose-query-fn="onExposeUsageHistoryQueryFn"
/>
<SwitchPortHistoryCard
v-if="selected.includes('port')"
:ndm-device="ndmDevice"
:station="station"
v-model:range="range"
v-model:loading="portLoading"
@expose-query-fn="onExposePortHistoryQueryFn"
/>
</NFlex>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,154 @@
<script setup lang="ts">
import { detailSwitchApi, icmpEntityByDeviceId, updateSwitchApi, type NdmSwitchResultVO, type NdmSwitchUpdateVO, type Station } from '@/apis';
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: NdmSwitchResultVO;
station: Station;
}>();
const deviceStore = useDeviceStore();
const { ndmDevice, station } = toRefs(props);
const localDevice = ref<NdmSwitchUpdateVO>({ ...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}04\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: loading } = useMutation({
mutationFn: async () => {
await formInst.value?.validate().catch(() => {
window.$message.error('表单验证失败');
return;
});
abortController.value.abort();
abortController.value = new AbortController();
const stationCode = station.value.code;
const signal = abortController.value.signal;
await updateSwitchApi(localDevice.value, { stationCode, signal });
const result = await detailSwitchApi(`${localDevice.value.id}`, { stationCode, signal });
return result;
},
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="管理URL">
<NInput v-model:value="localDevice.manageUrl" />
</NFormItem>
<NFormItem label-placement="left" label="管理用户名">
<NInput v-model:value="localDevice.manageUsername" />
</NFormItem>
<NFormItem label-placement="left" label="管理密码">
<NInput v-model:value="localDevice.managePassword" />
</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="loading" @click="() => updateDevice()">更新</NButton>
</NFlex>
</template>
</NCard>
</template>
<style scoped lang="scss"></style>