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