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>
|
||||
72
src/components/device/device-renderer/device-renderer.vue
Normal file
72
src/components/device/device-renderer/device-renderer.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
NdmAlarmHostResultVO,
|
||||
NdmCameraResultVO,
|
||||
NdmDecoderResultVO,
|
||||
NdmDeviceResultVO,
|
||||
NdmKeyboardResultVO,
|
||||
NdmNvrResultVO,
|
||||
NdmSecurityBoxResultVO,
|
||||
NdmServerResultVO,
|
||||
NdmSwitchResultVO,
|
||||
Station,
|
||||
} from '@/apis';
|
||||
import { DEVICE_TYPE_LITERALS, tryGetDeviceType } from '@/enums';
|
||||
import { computed, defineAsyncComponent, toRefs } from 'vue';
|
||||
|
||||
const AlarmHostCard = defineAsyncComponent(() => import('@/components/device/device-card/ndm-alarm-host/alarm-host-card.vue'));
|
||||
const CameraCard = defineAsyncComponent(() => import('@/components/device/device-card/ndm-camera/camera-card.vue'));
|
||||
const DecoderCard = defineAsyncComponent(() => import('@/components/device/device-card/ndm-decoder/decoder-card.vue'));
|
||||
const KeyboardCard = defineAsyncComponent(() => import('@/components/device/device-card/ndm-keyboard/keyboard-card.vue'));
|
||||
const NvrCard = defineAsyncComponent(() => import('@/components/device/device-card/ndm-nvr/nvr-card.vue'));
|
||||
const SecurityBoxCard = defineAsyncComponent(() => import('@/components/device/device-card/ndm-security-box/security-box-card.vue'));
|
||||
const ServerCard = defineAsyncComponent(() => import('@/components/device/device-card/ndm-server/server-card.vue'));
|
||||
const SwitchCard = defineAsyncComponent(() => import('@/components/device/device-card/ndm-switch/switch-card.vue'));
|
||||
|
||||
const props = defineProps<{
|
||||
ndmDevice: NdmDeviceResultVO;
|
||||
station: Station;
|
||||
}>();
|
||||
|
||||
const { ndmDevice, station } = toRefs(props);
|
||||
|
||||
const deviceType = computed(() => tryGetDeviceType(ndmDevice.value.deviceType));
|
||||
|
||||
const ndmAlarmHost = computed(() => ndmDevice.value as NdmAlarmHostResultVO);
|
||||
const ndmCamera = computed(() => ndmDevice.value as NdmCameraResultVO);
|
||||
const ndmDecoder = computed(() => ndmDevice.value as NdmDecoderResultVO);
|
||||
const ndmKeyboard = computed(() => ndmDevice.value as NdmKeyboardResultVO);
|
||||
const ndmNvr = computed(() => ndmDevice.value as NdmNvrResultVO);
|
||||
const ndmSecurityBox = computed(() => ndmDevice.value as NdmSecurityBoxResultVO);
|
||||
const ndmServer = computed(() => ndmDevice.value as NdmServerResultVO);
|
||||
const ndmSwitch = computed(() => ndmDevice.value as NdmSwitchResultVO);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="deviceType === DEVICE_TYPE_LITERALS.ndmAlarmHost">
|
||||
<AlarmHostCard :ndmDevice="ndmAlarmHost" :station="station" />
|
||||
</template>
|
||||
<template v-if="deviceType === DEVICE_TYPE_LITERALS.ndmCamera">
|
||||
<CameraCard :ndmDevice="ndmCamera" :station="station" />
|
||||
</template>
|
||||
<template v-if="deviceType === DEVICE_TYPE_LITERALS.ndmDecoder">
|
||||
<DecoderCard :ndmDevice="ndmDecoder" :station="station" />
|
||||
</template>
|
||||
<template v-if="deviceType === DEVICE_TYPE_LITERALS.ndmKeyboard">
|
||||
<KeyboardCard :ndmDevice="ndmKeyboard" :station="station" />
|
||||
</template>
|
||||
<template v-if="deviceType === DEVICE_TYPE_LITERALS.ndmNvr">
|
||||
<NvrCard :ndmDevice="ndmNvr" :station="station" />
|
||||
</template>
|
||||
<template v-if="deviceType === DEVICE_TYPE_LITERALS.ndmSecurityBox">
|
||||
<SecurityBoxCard :ndmDevice="ndmSecurityBox" :station="station" />
|
||||
</template>
|
||||
<template v-if="deviceType === DEVICE_TYPE_LITERALS.ndmMediaServer || deviceType === DEVICE_TYPE_LITERALS.ndmVideoServer">
|
||||
<ServerCard :ndmDevice="ndmServer" :station="station" />
|
||||
</template>
|
||||
<template v-if="deviceType === DEVICE_TYPE_LITERALS.ndmSwitch">
|
||||
<SwitchCard :ndmDevice="ndmSwitch" :station="station" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
3
src/components/device/device-renderer/index.ts
Normal file
3
src/components/device/device-renderer/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import DeviceRenderer from './device-renderer.vue';
|
||||
|
||||
export { DeviceRenderer };
|
||||
475
src/components/device/device-tree/device-tree.export.vue
Normal file
475
src/components/device/device-tree/device-tree.export.vue
Normal file
@@ -0,0 +1,475 @@
|
||||
<script setup lang="ts">
|
||||
import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis';
|
||||
import { useDeviceTree } from '@/composables';
|
||||
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType } from '@/enums';
|
||||
import { isNvrCluster } from '@/helpers';
|
||||
import { useDeviceStore, useStationStore } from '@/stores';
|
||||
import { sleep } from '@/utils';
|
||||
import { watchDebounced, watchImmediate } from '@vueuse/core';
|
||||
import destr from 'destr';
|
||||
import { isFunction } from 'es-toolkit';
|
||||
import {
|
||||
NButton,
|
||||
NDropdown,
|
||||
NFlex,
|
||||
NInput,
|
||||
NRadio,
|
||||
NRadioGroup,
|
||||
NTab,
|
||||
NTabs,
|
||||
NTag,
|
||||
NTree,
|
||||
useThemeVars,
|
||||
type DropdownOption,
|
||||
type TagProps,
|
||||
type TreeInst,
|
||||
type TreeOption,
|
||||
type TreeOverrideNodeClickBehavior,
|
||||
type TreeProps,
|
||||
} from 'naive-ui';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, h, onMounted, ref, toRefs, useTemplateRef, watch, type CSSProperties } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
station?: Station; // 支持渲染指定车站的设备树
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
selectDevice: [device: NdmDeviceResultVO, stationCode: Station['code']];
|
||||
}>();
|
||||
|
||||
const { station } = toRefs(props);
|
||||
|
||||
const themeVars = useThemeVars();
|
||||
|
||||
const { selectedStationCode, selectedDeviceType, selectedDevice, initFromRoute, selectDevice, routeDevice } = useDeviceTree();
|
||||
|
||||
const onSelectDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
|
||||
selectDevice(device, stationCode);
|
||||
emit('selectDevice', device, stationCode);
|
||||
};
|
||||
|
||||
const onRouteDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
|
||||
routeDevice(device, stationCode, { path: '/device' });
|
||||
emit('selectDevice', device, stationCode);
|
||||
};
|
||||
|
||||
const stationStore = useStationStore();
|
||||
const { stations } = storeToRefs(stationStore);
|
||||
const deviceStore = useDeviceStore();
|
||||
const { lineDevices } = storeToRefs(deviceStore);
|
||||
|
||||
onMounted(() => {
|
||||
initFromRoute(lineDevices.value);
|
||||
});
|
||||
|
||||
// lineDevices是shallowRef,因此需要深度侦听才能获取内部变化,
|
||||
// 而单纯的深度侦听又可能会引发性能问题,因此尝试使用防抖侦听
|
||||
watchDebounced(
|
||||
lineDevices,
|
||||
(newLineDevices) => {
|
||||
initFromRoute(newLineDevices);
|
||||
},
|
||||
{
|
||||
debounce: 500,
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
|
||||
const deviceTabPanes = Object.values(DEVICE_TYPE_LITERALS).map((deviceType) => ({
|
||||
name: deviceType,
|
||||
tab: DEVICE_TYPE_NAMES[deviceType],
|
||||
}));
|
||||
const activeTab = ref<DeviceType>(deviceTabPanes.at(0)!.name);
|
||||
watchImmediate(selectedDeviceType, (newDeviceType) => {
|
||||
if (newDeviceType) {
|
||||
activeTab.value = newDeviceType;
|
||||
}
|
||||
});
|
||||
|
||||
const selectedKeys = computed(() => (selectedDevice.value?.id ? [selectedDevice.value.id] : undefined));
|
||||
watch([selectedKeys, selectedDevice, selectedStationCode], ([, device, code]) => {
|
||||
if (device && code) {
|
||||
onSelectDevice(device, code);
|
||||
}
|
||||
});
|
||||
|
||||
const contextmenu = ref<{ x: number; y: number; stationCode?: Station['code']; deviceType: DeviceType | null }>({ x: 0, y: 0, deviceType: null });
|
||||
const showContextmenu = ref(false);
|
||||
const contextmenuOptions: DropdownOption[] = [
|
||||
{
|
||||
label: '导出设备',
|
||||
key: 'export-device',
|
||||
onSelect: () => {
|
||||
// 需要拿到当前选中的设备类型和车站编号
|
||||
const { stationCode, deviceType } = contextmenu.value;
|
||||
console.log(stationCode, deviceType);
|
||||
showContextmenu.value = false;
|
||||
},
|
||||
},
|
||||
];
|
||||
const onSelectDropdownOption = (key: string, option: DropdownOption) => {
|
||||
const onSelect = option['onSelect'];
|
||||
if (isFunction(onSelect)) {
|
||||
onSelect();
|
||||
}
|
||||
};
|
||||
|
||||
// ========== 设备树节点交互 ==========
|
||||
const override: TreeOverrideNodeClickBehavior = ({ option }) => {
|
||||
const hasChildren = (option.children?.length ?? 0) > 0;
|
||||
const isDeviceNode = !!option['device'];
|
||||
if (hasChildren || !isDeviceNode) {
|
||||
return 'toggleExpand';
|
||||
} else {
|
||||
return 'none';
|
||||
}
|
||||
};
|
||||
const nodeProps: TreeProps['nodeProps'] = ({ option }) => {
|
||||
return {
|
||||
onDblclick: (payload) => {
|
||||
if (option['device']) {
|
||||
payload.stopPropagation();
|
||||
const device = option['device'] as NdmDeviceResultVO;
|
||||
const stationCode = option['stationCode'] as string;
|
||||
// 区分是否需要跳转路由
|
||||
if (!station.value) {
|
||||
onSelectDevice(device, stationCode);
|
||||
} else {
|
||||
onRouteDevice(device, station.value.code);
|
||||
}
|
||||
}
|
||||
},
|
||||
// TODO: 支持右键点击车站导出设备列表
|
||||
onContextmenu: (payload) => {
|
||||
payload.stopPropagation();
|
||||
payload.preventDefault();
|
||||
if (!option['device']) {
|
||||
const { clientX, clientY } = payload;
|
||||
const stationCode = option['stationCode'] as string;
|
||||
const deviceType = option['deviceType'] as DeviceType;
|
||||
contextmenu.value = { x: clientX, y: clientY, stationCode, deviceType };
|
||||
showContextmenu.value = true;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// ========== 设备树数据 ==========
|
||||
const renderStationNodePrefix = (station: Station) => {
|
||||
const { online } = station;
|
||||
const tagType: TagProps['type'] = online ? 'success' : 'error';
|
||||
const tagText = online ? '在线' : '离线';
|
||||
return h(NTag, { type: tagType, size: 'tiny' }, () => tagText);
|
||||
};
|
||||
const renderIcmpStatistics = (onlineCount: number, offlineCount: number, count: number) => {
|
||||
return h('span', null, [
|
||||
'(',
|
||||
h('span', { style: { color: themeVars.value.successColor } }, `${onlineCount}`),
|
||||
'/',
|
||||
h('span', { style: { color: themeVars.value.errorColor } }, `${offlineCount}`),
|
||||
'/',
|
||||
`${count}`,
|
||||
')',
|
||||
]);
|
||||
};
|
||||
const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: string) => {
|
||||
const renderViewDeviceButton = (device: NdmDeviceResultVO, stationCode: string) => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
size: 'tiny',
|
||||
type: 'info',
|
||||
style: {
|
||||
marginRight: 8,
|
||||
} as CSSProperties,
|
||||
onClick: (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
// 选择设备
|
||||
// 区分是否需要跳转路由
|
||||
if (!station.value) {
|
||||
onSelectDevice(device, stationCode);
|
||||
} else {
|
||||
onRouteDevice(device, station.value.code);
|
||||
}
|
||||
},
|
||||
},
|
||||
() => '查看',
|
||||
);
|
||||
};
|
||||
const renderDeviceStatusTag = (device: NdmDeviceResultVO) => {
|
||||
const { deviceStatus } = device;
|
||||
const color = deviceStatus === '10' ? themeVars.value.successColor : deviceStatus === '20' ? themeVars.value.errorColor : themeVars.value.warningColor;
|
||||
return h('div', { style: { color } }, { default: () => '◉' });
|
||||
};
|
||||
return h(NFlex, { size: 'small' }, { default: () => [renderViewDeviceButton(device, stationCode), renderDeviceStatusTag(device)] });
|
||||
};
|
||||
// 全线设备树
|
||||
const lineDeviceTreeData = computed<Record<string, TreeOption[]>>(() => {
|
||||
const treeData: Record<string, TreeOption[]> = {};
|
||||
deviceTabPanes.forEach(({ name: paneName /* , tab: paneTab */ }) => {
|
||||
treeData[paneName] = stations.value.map<TreeOption>((station) => {
|
||||
const { name: stationName, code: stationCode } = station;
|
||||
const devices = lineDevices.value[stationCode]?.[paneName] ?? ([] as NdmDeviceResultVO[]);
|
||||
const onlineDevices = devices?.filter((device) => device.deviceStatus === '10');
|
||||
const offlineDevices = devices?.filter((device) => device.deviceStatus === '20');
|
||||
// 对于录像机,需要根据clusterList字段以分号分隔设备IP,进一步形成子树结构
|
||||
if (paneName === DEVICE_TYPE_LITERALS.ndmNvr) {
|
||||
const nvrs = devices as NdmNvrResultVO[];
|
||||
const nvrClusters: NdmNvrResultVO[] = [];
|
||||
const nvrSingletons: NdmNvrResultVO[] = [];
|
||||
for (const device of nvrs) {
|
||||
if (isNvrCluster(device)) {
|
||||
nvrClusters.push(device);
|
||||
} else {
|
||||
nvrSingletons.push(device);
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: stationName,
|
||||
key: stationCode,
|
||||
prefix: () => renderStationNodePrefix(station),
|
||||
suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0),
|
||||
children: nvrClusters.map<TreeOption>((nvrCluster) => {
|
||||
return {
|
||||
label: `${nvrCluster.name}`,
|
||||
key: nvrCluster.id ?? `${nvrCluster.name}`,
|
||||
prefix: () => renderDeviceNodePrefix(nvrCluster, stationCode),
|
||||
suffix: () => `${nvrCluster.ipAddress}`,
|
||||
children: nvrSingletons.map<TreeOption>((nvr) => {
|
||||
return {
|
||||
label: `${nvr.name}`,
|
||||
key: nvr.id ?? `${nvr.name}`,
|
||||
prefix: () => renderDeviceNodePrefix(nvr, stationCode),
|
||||
suffix: () => `${nvr.ipAddress}`,
|
||||
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
|
||||
stationCode,
|
||||
device: nvr,
|
||||
};
|
||||
}),
|
||||
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
|
||||
stationCode,
|
||||
device: nvrCluster,
|
||||
};
|
||||
}),
|
||||
stationCode,
|
||||
deviceType: activeTab.value,
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: stationName,
|
||||
key: stationCode,
|
||||
prefix: () => renderStationNodePrefix(station),
|
||||
suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0),
|
||||
children:
|
||||
lineDevices.value[stationCode]?.[paneName]?.map<TreeOption>((dev) => {
|
||||
const device = dev as NdmDeviceResultVO;
|
||||
return {
|
||||
label: `${device.name}`,
|
||||
key: device.id ?? `${device.name}`,
|
||||
prefix: () => renderDeviceNodePrefix(device, stationCode),
|
||||
suffix: () => `${device.ipAddress}`,
|
||||
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
|
||||
stationCode,
|
||||
device,
|
||||
};
|
||||
}) ?? [],
|
||||
stationCode,
|
||||
deviceType: activeTab.value,
|
||||
};
|
||||
});
|
||||
});
|
||||
return treeData;
|
||||
});
|
||||
// 车站设备树
|
||||
const stationDeviceTreeData = computed<TreeOption[]>(() => {
|
||||
const stationCode = station.value?.code;
|
||||
if (!stationCode) return [];
|
||||
return Object.values(DEVICE_TYPE_LITERALS).map<TreeOption>((deviceType) => {
|
||||
const stationDevices = lineDevices.value[stationCode] ?? initStationDevices();
|
||||
const onlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '10').length;
|
||||
const offlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '20').length;
|
||||
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
|
||||
const nvrs = stationDevices[deviceType] as NdmNvrResultVO[];
|
||||
const clusters = nvrs.filter((nvr) => isNvrCluster(nvr));
|
||||
const singletons = nvrs.filter((nvr) => !isNvrCluster(nvr));
|
||||
return {
|
||||
label: `${DEVICE_TYPE_NAMES[deviceType]}`,
|
||||
key: deviceType,
|
||||
suffix: () => renderIcmpStatistics(onlineCount, offlineCount, nvrs.length),
|
||||
children: clusters.map<TreeOption>((device) => {
|
||||
return {
|
||||
label: `${device.name}`,
|
||||
key: device.id ?? `${device.name}`,
|
||||
prefix: () => renderDeviceNodePrefix(device, stationCode),
|
||||
suffix: () => `${device.ipAddress}`,
|
||||
children: singletons.map<TreeOption>((device) => {
|
||||
return {
|
||||
label: `${device.name}`,
|
||||
key: device.id ?? `${device.name}`,
|
||||
prefix: () => renderDeviceNodePrefix(device, stationCode),
|
||||
suffix: () => `${device.ipAddress}`,
|
||||
stationCode,
|
||||
device,
|
||||
};
|
||||
}),
|
||||
stationCode,
|
||||
device,
|
||||
};
|
||||
}),
|
||||
stationCode,
|
||||
deviceType,
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: `${DEVICE_TYPE_NAMES[deviceType]}`,
|
||||
key: deviceType,
|
||||
suffix: () => renderIcmpStatistics(onlineCount, offlineCount, stationDevices[deviceType].length),
|
||||
children: stationDevices[deviceType].map<TreeOption>((device) => {
|
||||
return {
|
||||
label: `${device.name}`,
|
||||
key: device.id ?? `${device.name}`,
|
||||
prefix: () => renderDeviceNodePrefix(device, stationCode),
|
||||
suffix: () => `${device.ipAddress}`,
|
||||
stationCode,
|
||||
device,
|
||||
};
|
||||
}),
|
||||
stationCode,
|
||||
deviceType,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// ========== 设备树搜索 ==========
|
||||
const searchInput = ref('');
|
||||
const statusInput = ref('');
|
||||
// 设备树将搜索框和单选框的值都交给NTree的pattern属性
|
||||
// 但是如果一个车站下没有匹配的设备,那么这个车站节点也不会显示
|
||||
const searchPattern = computed(() => {
|
||||
const search = searchInput.value;
|
||||
const status = statusInput.value;
|
||||
if (!search && !status) return ''; // 如果pattern非空会导致NTree组件认为筛选完成,UI上发生全量匹配
|
||||
return JSON.stringify({ search: searchInput.value, status: statusInput.value });
|
||||
});
|
||||
const searchFilter = (pattern: string, node: TreeOption): boolean => {
|
||||
const { search, status } = destr<{ search: string; status: string }>(pattern);
|
||||
const device = node['device'] as NdmDeviceResultVO | undefined;
|
||||
const { name, ipAddress, deviceId, deviceStatus } = device ?? {};
|
||||
const searchMatched = (name ?? '').includes(search) || (ipAddress ?? '').includes(search) || (deviceId ?? '').includes(search);
|
||||
const statusMatched = status === '' || status === deviceStatus;
|
||||
return searchMatched && statusMatched;
|
||||
};
|
||||
|
||||
// ========== 设备树交互 ==========
|
||||
const expandedKeys = ref<string[]>();
|
||||
const deviceTreeInst = useTemplateRef<TreeInst>('deviceTreeInst');
|
||||
const onFoldDeviceTree = () => {
|
||||
expandedKeys.value = [];
|
||||
};
|
||||
const onLocateDeviceTree = () => {
|
||||
const stationCode = selectedStationCode.value;
|
||||
const device = selectedDevice.value;
|
||||
if (!stationCode || !device?.id) return;
|
||||
const deviceTypeVal = tryGetDeviceType(device.deviceType);
|
||||
if (!!deviceTypeVal) {
|
||||
activeTab.value = deviceTypeVal;
|
||||
}
|
||||
|
||||
const expanded = [stationCode];
|
||||
if (activeTab.value === DEVICE_TYPE_LITERALS.ndmNvr) {
|
||||
const nvrs = lineDevices.value[stationCode]?.[DEVICE_TYPE_LITERALS.ndmNvr];
|
||||
if (nvrs) {
|
||||
const clusterKeys = nvrs.filter((nvr) => !!nvr.clusterList?.trim() && nvr.clusterList !== nvr.ipAddress).map((nvr) => String(nvr.id));
|
||||
expanded.push(...clusterKeys);
|
||||
}
|
||||
}
|
||||
expandedKeys.value = expanded;
|
||||
|
||||
// 由于数据量大所以开启虚拟滚动,
|
||||
// 但是无法知晓NTree内部的虚拟列表容器何时创建完成,所以使用setTimeout延迟固定时间后执行滚动
|
||||
scrollDeviceTreeToSelectedDevice();
|
||||
};
|
||||
async function scrollDeviceTreeToSelectedDevice() {
|
||||
await sleep(350);
|
||||
const inst = deviceTreeInst.value;
|
||||
inst?.scrollTo({ key: selectedDevice?.value?.id ?? `${selectedDevice.value?.name}`, behavior: 'smooth' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="height: 100%; display: flex; flex-direction: column">
|
||||
<!-- 搜索和筛选 -->
|
||||
<div style="padding: 12px; flex: 0 0 auto">
|
||||
<NInput v-model:value="searchInput" placeholder="搜索设备名称、设备ID或IP地址" clearable />
|
||||
<NFlex align="center">
|
||||
<NRadioGroup v-model:value="statusInput">
|
||||
<NRadio value="">全部</NRadio>
|
||||
<NRadio value="10">在线</NRadio>
|
||||
<NRadio value="20">离线</NRadio>
|
||||
</NRadioGroup>
|
||||
<NButton text size="tiny" type="info" @click="onFoldDeviceTree" style="margin-left: auto">收起</NButton>
|
||||
<NButton text size="tiny" type="info" @click="onLocateDeviceTree">定位</NButton>
|
||||
</NFlex>
|
||||
</div>
|
||||
<!-- 设备树 -->
|
||||
<div style="overflow: hidden; flex: 1 1 auto; display: flex">
|
||||
<template v-if="!station">
|
||||
<div style="height: 100%; flex: 0 0 auto">
|
||||
<NTabs v-model:value="activeTab" animated type="line" placement="left" style="height: 100%">
|
||||
<NTab v-for="pane in deviceTabPanes" :key="pane.name" :name="pane.name" :tab="pane.tab"></NTab>
|
||||
</NTabs>
|
||||
</div>
|
||||
<div style="min-width: 0; flex: 1 1 auto">
|
||||
<NTree
|
||||
style="height: 100%"
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
block-line
|
||||
block-node
|
||||
show-line
|
||||
virtual-scroll
|
||||
:ref="'deviceTreeInst'"
|
||||
:selected-keys="selectedKeys"
|
||||
:data="lineDeviceTreeData[activeTab]"
|
||||
:show-irrelevant-nodes="false"
|
||||
:pattern="searchPattern"
|
||||
:filter="searchFilter"
|
||||
:override-default-node-click-behavior="override"
|
||||
:node-props="nodeProps"
|
||||
:default-expand-all="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<NTree
|
||||
style="height: 100%"
|
||||
block-line
|
||||
block-node
|
||||
show-line
|
||||
virtual-scroll
|
||||
:data="stationDeviceTreeData"
|
||||
:show-irrelevant-nodes="false"
|
||||
:pattern="searchPattern"
|
||||
:filter="searchFilter"
|
||||
:override-default-node-click-behavior="override"
|
||||
:node-props="nodeProps"
|
||||
:default-expand-all="false"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NDropdown
|
||||
placement="bottom-start"
|
||||
trigger="manual"
|
||||
:show="showContextmenu"
|
||||
:x="contextmenu.x"
|
||||
:y="contextmenu.y"
|
||||
:options="contextmenuOptions"
|
||||
@select="onSelectDropdownOption"
|
||||
@clickoutside="() => (showContextmenu = false)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
420
src/components/device/device-tree/device-tree.vue
Normal file
420
src/components/device/device-tree/device-tree.vue
Normal file
@@ -0,0 +1,420 @@
|
||||
<script setup lang="ts">
|
||||
import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis';
|
||||
import { useDeviceTree } from '@/composables';
|
||||
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType } from '@/enums';
|
||||
import { isNvrCluster } from '@/helpers';
|
||||
import { useDeviceStore, useStationStore } from '@/stores';
|
||||
import { sleep } from '@/utils';
|
||||
import { watchDebounced, watchImmediate } from '@vueuse/core';
|
||||
import destr from 'destr';
|
||||
import {
|
||||
NButton,
|
||||
NFlex,
|
||||
NInput,
|
||||
NRadio,
|
||||
NRadioGroup,
|
||||
NTab,
|
||||
NTabs,
|
||||
NTag,
|
||||
NTree,
|
||||
useThemeVars,
|
||||
type TagProps,
|
||||
type TreeInst,
|
||||
type TreeOption,
|
||||
type TreeOverrideNodeClickBehavior,
|
||||
type TreeProps,
|
||||
} from 'naive-ui';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, h, onMounted, ref, toRefs, useTemplateRef, watch, type CSSProperties } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
station?: Station; // 支持渲染指定车站的设备树
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
selectDevice: [device: NdmDeviceResultVO, stationCode: Station['code']];
|
||||
}>();
|
||||
|
||||
const { station } = toRefs(props);
|
||||
|
||||
const themeVars = useThemeVars();
|
||||
|
||||
const { selectedStationCode, selectedDeviceType, selectedDevice, initFromRoute, selectDevice, routeDevice } = useDeviceTree();
|
||||
|
||||
const onSelectDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
|
||||
selectDevice(device, stationCode);
|
||||
emit('selectDevice', device, stationCode);
|
||||
};
|
||||
|
||||
const onRouteDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
|
||||
routeDevice(device, stationCode, { path: '/device' });
|
||||
emit('selectDevice', device, stationCode);
|
||||
};
|
||||
|
||||
const stationStore = useStationStore();
|
||||
const { stations } = storeToRefs(stationStore);
|
||||
const deviceStore = useDeviceStore();
|
||||
const { lineDevices } = storeToRefs(deviceStore);
|
||||
|
||||
onMounted(() => {
|
||||
initFromRoute(lineDevices.value);
|
||||
});
|
||||
|
||||
// lineDevices是shallowRef,因此需要深度侦听才能获取内部变化,
|
||||
// 而单纯的深度侦听又可能会引发性能问题,因此尝试使用防抖侦听
|
||||
watchDebounced(
|
||||
lineDevices,
|
||||
(newLineDevices) => {
|
||||
initFromRoute(newLineDevices);
|
||||
},
|
||||
{
|
||||
debounce: 500,
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
|
||||
const deviceTabPanes = Object.values(DEVICE_TYPE_LITERALS).map((deviceType) => ({
|
||||
name: deviceType,
|
||||
tab: DEVICE_TYPE_NAMES[deviceType],
|
||||
}));
|
||||
const activeTab = ref<DeviceType>(deviceTabPanes.at(0)!.name);
|
||||
watchImmediate(selectedDeviceType, (newDeviceType) => {
|
||||
if (newDeviceType) {
|
||||
activeTab.value = newDeviceType;
|
||||
}
|
||||
});
|
||||
|
||||
const selectedKeys = computed(() => (selectedDevice.value?.id ? [selectedDevice.value.id] : undefined));
|
||||
watch([selectedKeys, selectedDevice, selectedStationCode], ([, device, code]) => {
|
||||
if (device && code) {
|
||||
onSelectDevice(device, code);
|
||||
}
|
||||
});
|
||||
|
||||
// ========== 设备树节点交互 ==========
|
||||
const override: TreeOverrideNodeClickBehavior = ({ option }) => {
|
||||
const hasChildren = (option.children?.length ?? 0) > 0;
|
||||
const isDeviceNode = !!option['device'];
|
||||
if (hasChildren || !isDeviceNode) {
|
||||
return 'toggleExpand';
|
||||
} else {
|
||||
return 'none';
|
||||
}
|
||||
};
|
||||
const nodeProps: TreeProps['nodeProps'] = ({ option }) => {
|
||||
return {
|
||||
onDblclick: (payload) => {
|
||||
if (option['device']) {
|
||||
payload.stopPropagation();
|
||||
const device = option['device'] as NdmDeviceResultVO;
|
||||
const stationCode = option['stationCode'] as string;
|
||||
// 区分是否需要跳转路由
|
||||
if (!station.value) {
|
||||
onSelectDevice(device, stationCode);
|
||||
} else {
|
||||
onRouteDevice(device, station.value.code);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// ========== 设备树数据 ==========
|
||||
const renderStationNodePrefix = (station: Station) => {
|
||||
const { online } = station;
|
||||
const tagType: TagProps['type'] = online ? 'success' : 'error';
|
||||
const tagText = online ? '在线' : '离线';
|
||||
return h(NTag, { type: tagType, size: 'tiny' }, () => tagText);
|
||||
};
|
||||
const renderIcmpStatistics = (onlineCount: number, offlineCount: number, count: number) => {
|
||||
return h('span', null, [
|
||||
'(',
|
||||
h('span', { style: { color: themeVars.value.successColor } }, `${onlineCount}`),
|
||||
'/',
|
||||
h('span', { style: { color: themeVars.value.errorColor } }, `${offlineCount}`),
|
||||
'/',
|
||||
`${count}`,
|
||||
')',
|
||||
]);
|
||||
};
|
||||
const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: string) => {
|
||||
const renderViewDeviceButton = (device: NdmDeviceResultVO, stationCode: string) => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
size: 'tiny',
|
||||
type: 'info',
|
||||
style: {
|
||||
marginRight: 8,
|
||||
} as CSSProperties,
|
||||
onClick: (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
// 选择设备
|
||||
// 区分是否需要跳转路由
|
||||
if (!station.value) {
|
||||
onSelectDevice(device, stationCode);
|
||||
} else {
|
||||
onRouteDevice(device, station.value.code);
|
||||
}
|
||||
},
|
||||
},
|
||||
() => '查看',
|
||||
);
|
||||
};
|
||||
const renderDeviceStatusTag = (device: NdmDeviceResultVO) => {
|
||||
const { deviceStatus } = device;
|
||||
const color = deviceStatus === '10' ? themeVars.value.successColor : deviceStatus === '20' ? themeVars.value.errorColor : themeVars.value.warningColor;
|
||||
return h('div', { style: { color } }, { default: () => '◉' });
|
||||
};
|
||||
return h(NFlex, { size: 'small' }, { default: () => [renderViewDeviceButton(device, stationCode), renderDeviceStatusTag(device)] });
|
||||
};
|
||||
// 全线设备树
|
||||
const lineDeviceTreeData = computed<Record<string, TreeOption[]>>(() => {
|
||||
const treeData: Record<string, TreeOption[]> = {};
|
||||
deviceTabPanes.forEach(({ name: paneName /* , tab: paneTab */ }) => {
|
||||
treeData[paneName] = stations.value.map<TreeOption>((station) => {
|
||||
const { name: stationName, code: stationCode } = station;
|
||||
const devices = lineDevices.value[stationCode]?.[paneName] ?? ([] as NdmDeviceResultVO[]);
|
||||
const onlineDevices = devices?.filter((device) => device.deviceStatus === '10');
|
||||
const offlineDevices = devices?.filter((device) => device.deviceStatus === '20');
|
||||
// 对于录像机,需要根据clusterList字段以分号分隔设备IP,进一步形成子树结构
|
||||
if (paneName === DEVICE_TYPE_LITERALS.ndmNvr) {
|
||||
const nvrs = devices as NdmNvrResultVO[];
|
||||
const nvrClusters: NdmNvrResultVO[] = [];
|
||||
const nvrSingletons: NdmNvrResultVO[] = [];
|
||||
for (const device of nvrs) {
|
||||
if (isNvrCluster(device)) {
|
||||
nvrClusters.push(device);
|
||||
} else {
|
||||
nvrSingletons.push(device);
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: stationName,
|
||||
key: stationCode,
|
||||
prefix: () => renderStationNodePrefix(station),
|
||||
suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0),
|
||||
children: nvrClusters.map<TreeOption>((nvrCluster) => {
|
||||
return {
|
||||
label: `${nvrCluster.name}`,
|
||||
key: nvrCluster.id ?? `${nvrCluster.name}`,
|
||||
prefix: () => renderDeviceNodePrefix(nvrCluster, stationCode),
|
||||
suffix: () => `${nvrCluster.ipAddress}`,
|
||||
children: nvrSingletons.map<TreeOption>((nvr) => {
|
||||
return {
|
||||
label: `${nvr.name}`,
|
||||
key: nvr.id ?? `${nvr.name}`,
|
||||
prefix: () => renderDeviceNodePrefix(nvr, stationCode),
|
||||
suffix: () => `${nvr.ipAddress}`,
|
||||
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
|
||||
stationCode,
|
||||
device: nvr,
|
||||
};
|
||||
}),
|
||||
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
|
||||
stationCode,
|
||||
device: nvrCluster,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: stationName,
|
||||
key: stationCode,
|
||||
prefix: () => renderStationNodePrefix(station),
|
||||
suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0),
|
||||
children:
|
||||
lineDevices.value[stationCode]?.[paneName]?.map<TreeOption>((dev) => {
|
||||
const device = dev as NdmDeviceResultVO;
|
||||
return {
|
||||
label: `${device.name}`,
|
||||
key: device.id ?? `${device.name}`,
|
||||
prefix: () => renderDeviceNodePrefix(device, stationCode),
|
||||
suffix: () => `${device.ipAddress}`,
|
||||
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
|
||||
stationCode,
|
||||
device,
|
||||
};
|
||||
}) ?? [],
|
||||
};
|
||||
});
|
||||
});
|
||||
return treeData;
|
||||
});
|
||||
// 车站设备树
|
||||
const stationDeviceTreeData = computed<TreeOption[]>(() => {
|
||||
const stationCode = station.value?.code;
|
||||
if (!stationCode) return [];
|
||||
return Object.values(DEVICE_TYPE_LITERALS).map<TreeOption>((deviceType) => {
|
||||
const stationDevices = lineDevices.value[stationCode] ?? initStationDevices();
|
||||
const onlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '10').length;
|
||||
const offlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '20').length;
|
||||
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
|
||||
const nvrs = stationDevices[deviceType] as NdmNvrResultVO[];
|
||||
const clusters = nvrs.filter((nvr) => isNvrCluster(nvr));
|
||||
const singletons = nvrs.filter((nvr) => !isNvrCluster(nvr));
|
||||
return {
|
||||
label: `${DEVICE_TYPE_NAMES[deviceType]}`,
|
||||
key: deviceType,
|
||||
suffix: () => renderIcmpStatistics(onlineCount, offlineCount, nvrs.length),
|
||||
children: clusters.map<TreeOption>((device) => {
|
||||
return {
|
||||
label: `${device.name}`,
|
||||
key: device.id ?? `${device.name}`,
|
||||
prefix: () => renderDeviceNodePrefix(device, stationCode),
|
||||
suffix: () => `${device.ipAddress}`,
|
||||
children: singletons.map<TreeOption>((device) => {
|
||||
return {
|
||||
label: `${device.name}`,
|
||||
key: device.id ?? `${device.name}`,
|
||||
prefix: () => renderDeviceNodePrefix(device, stationCode),
|
||||
suffix: () => `${device.ipAddress}`,
|
||||
stationCode,
|
||||
device,
|
||||
};
|
||||
}),
|
||||
stationCode,
|
||||
device,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: `${DEVICE_TYPE_NAMES[deviceType]}`,
|
||||
key: deviceType,
|
||||
suffix: () => renderIcmpStatistics(onlineCount, offlineCount, stationDevices[deviceType].length),
|
||||
children: stationDevices[deviceType].map<TreeOption>((device) => {
|
||||
return {
|
||||
label: `${device.name}`,
|
||||
key: device.id ?? `${device.name}`,
|
||||
prefix: () => renderDeviceNodePrefix(device, stationCode),
|
||||
suffix: () => `${device.ipAddress}`,
|
||||
stationCode,
|
||||
device,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// ========== 设备树搜索 ==========
|
||||
const searchInput = ref('');
|
||||
const statusInput = ref('');
|
||||
// 设备树将搜索框和单选框的值都交给NTree的pattern属性
|
||||
// 但是如果一个车站下没有匹配的设备,那么这个车站节点也不会显示
|
||||
const searchPattern = computed(() => {
|
||||
const search = searchInput.value;
|
||||
const status = statusInput.value;
|
||||
if (!search && !status) return ''; // 如果pattern非空会导致NTree组件认为筛选完成,UI上发生全量匹配
|
||||
return JSON.stringify({ search: searchInput.value, status: statusInput.value });
|
||||
});
|
||||
const searchFilter = (pattern: string, node: TreeOption): boolean => {
|
||||
const { search, status } = destr<{ search: string; status: string }>(pattern);
|
||||
const device = node['device'] as NdmDeviceResultVO | undefined;
|
||||
const { name, ipAddress, deviceId, deviceStatus } = device ?? {};
|
||||
const searchMatched = (name ?? '').includes(search) || (ipAddress ?? '').includes(search) || (deviceId ?? '').includes(search);
|
||||
const statusMatched = status === '' || status === deviceStatus;
|
||||
return searchMatched && statusMatched;
|
||||
};
|
||||
|
||||
// ========== 设备树交互 ==========
|
||||
const expandedKeys = ref<string[]>();
|
||||
const deviceTreeInst = useTemplateRef<TreeInst>('deviceTreeInst');
|
||||
const onFoldDeviceTree = () => {
|
||||
expandedKeys.value = [];
|
||||
};
|
||||
const onLocateDeviceTree = () => {
|
||||
const stationCode = selectedStationCode.value;
|
||||
const device = selectedDevice.value;
|
||||
if (!stationCode || !device?.id) return;
|
||||
const deviceTypeVal = tryGetDeviceType(device.deviceType);
|
||||
if (!!deviceTypeVal) {
|
||||
activeTab.value = deviceTypeVal;
|
||||
}
|
||||
|
||||
const expanded = [stationCode];
|
||||
if (activeTab.value === DEVICE_TYPE_LITERALS.ndmNvr) {
|
||||
const nvrs = lineDevices.value[stationCode]?.[DEVICE_TYPE_LITERALS.ndmNvr];
|
||||
if (nvrs) {
|
||||
const clusterKeys = nvrs.filter((nvr) => !!nvr.clusterList?.trim() && nvr.clusterList !== nvr.ipAddress).map((nvr) => String(nvr.id));
|
||||
expanded.push(...clusterKeys);
|
||||
}
|
||||
}
|
||||
expandedKeys.value = expanded;
|
||||
|
||||
// 由于数据量大所以开启虚拟滚动,
|
||||
// 但是无法知晓NTree内部的虚拟列表容器何时创建完成,所以使用setTimeout延迟固定时间后执行滚动
|
||||
scrollDeviceTreeToSelectedDevice();
|
||||
};
|
||||
async function scrollDeviceTreeToSelectedDevice() {
|
||||
await sleep(350);
|
||||
const inst = deviceTreeInst.value;
|
||||
inst?.scrollTo({ key: selectedDevice?.value?.id ?? `${selectedDevice.value?.name}`, behavior: 'smooth' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="height: 100%; display: flex; flex-direction: column">
|
||||
<!-- 搜索和筛选 -->
|
||||
<div style="padding: 12px; flex: 0 0 auto">
|
||||
<NInput v-model:value="searchInput" placeholder="搜索设备名称、设备ID或IP地址" clearable />
|
||||
<NFlex align="center">
|
||||
<NRadioGroup v-model:value="statusInput">
|
||||
<NRadio value="">全部</NRadio>
|
||||
<NRadio value="10">在线</NRadio>
|
||||
<NRadio value="20">离线</NRadio>
|
||||
</NRadioGroup>
|
||||
<NButton text size="tiny" type="info" @click="onFoldDeviceTree" style="margin-left: auto">收起</NButton>
|
||||
<NButton text size="tiny" type="info" @click="onLocateDeviceTree">定位</NButton>
|
||||
</NFlex>
|
||||
</div>
|
||||
<!-- 设备树 -->
|
||||
<div style="overflow: hidden; flex: 1 1 auto; display: flex">
|
||||
<template v-if="!station">
|
||||
<div style="height: 100%; flex: 0 0 auto">
|
||||
<NTabs v-model:value="activeTab" animated type="line" placement="left" style="height: 100%">
|
||||
<NTab v-for="pane in deviceTabPanes" :key="pane.name" :name="pane.name" :tab="pane.tab"></NTab>
|
||||
</NTabs>
|
||||
</div>
|
||||
<div style="min-width: 0; flex: 1 1 auto">
|
||||
<NTree
|
||||
style="height: 100%"
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
block-line
|
||||
block-node
|
||||
show-line
|
||||
virtual-scroll
|
||||
:ref="'deviceTreeInst'"
|
||||
:selected-keys="selectedKeys"
|
||||
:data="lineDeviceTreeData[activeTab]"
|
||||
:show-irrelevant-nodes="false"
|
||||
:pattern="searchPattern"
|
||||
:filter="searchFilter"
|
||||
:override-default-node-click-behavior="override"
|
||||
:node-props="nodeProps"
|
||||
:default-expand-all="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<NTree
|
||||
style="height: 100%"
|
||||
block-line
|
||||
block-node
|
||||
show-line
|
||||
virtual-scroll
|
||||
:data="stationDeviceTreeData"
|
||||
:show-irrelevant-nodes="false"
|
||||
:pattern="searchPattern"
|
||||
:filter="searchFilter"
|
||||
:override-default-node-click-behavior="override"
|
||||
:node-props="nodeProps"
|
||||
:default-expand-all="false"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
6
src/components/device/device-tree/index.ts
Normal file
6
src/components/device/device-tree/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { ComponentInstance } from 'vue';
|
||||
import DeviceTree from './device-tree.vue';
|
||||
|
||||
export type DeviceTreeProps = ComponentInstance<typeof DeviceTree>['$props'];
|
||||
|
||||
export { DeviceTree };
|
||||
3
src/components/device/index.ts
Normal file
3
src/components/device/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './device-card';
|
||||
export * from './device-renderer';
|
||||
export * from './device-tree';
|
||||
12
src/components/global/global-feedback/global-feedback.vue
Normal file
12
src/components/global/global-feedback/global-feedback.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { useDialog, useLoadingBar, useMessage, useNotification } from 'naive-ui';
|
||||
|
||||
window.$dialog = useDialog();
|
||||
window.$loadingBar = useLoadingBar();
|
||||
window.$message = useMessage();
|
||||
window.$notification = useNotification();
|
||||
</script>
|
||||
|
||||
<template></template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
3
src/components/global/global-feedback/index.ts
Normal file
3
src/components/global/global-feedback/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import GlobalFeedback from './global-feedback.vue';
|
||||
|
||||
export { GlobalFeedback };
|
||||
3
src/components/global/index.ts
Normal file
3
src/components/global/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './global-feedback';
|
||||
export * from './settings-drawer';
|
||||
export * from './theme-switch';
|
||||
3
src/components/global/settings-drawer/index.ts
Normal file
3
src/components/global/settings-drawer/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import SettingsDrawer from './settings-drawer.vue';
|
||||
|
||||
export { SettingsDrawer };
|
||||
277
src/components/global/settings-drawer/settings-drawer.vue
Normal file
277
src/components/global/settings-drawer/settings-drawer.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<script setup lang="ts">
|
||||
import type { LineAlarms, LineDevices, NdmDeviceResultVO, Station, VersionInfo } from '@/apis';
|
||||
import { ThemeSwitch } from '@/components';
|
||||
import { NDM_ALARM_STORE_ID, NDM_DEVICE_STORE_ID, NDM_STATION_STORE_ID } from '@/constants';
|
||||
import { usePollingStore, useSettingStore } from '@/stores';
|
||||
import { downloadByData, getAppEnvConfig, parseErrorFeedback, sleep } from '@/utils';
|
||||
import { useMutation } from '@tanstack/vue-query';
|
||||
import { DeleteOutlined, ExportOutlined, ImportOutlined } from '@vicons/antd';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import axios from 'axios';
|
||||
import destr from 'destr';
|
||||
import { isFunction } from 'es-toolkit';
|
||||
import localforage from 'localforage';
|
||||
import { NButton, NDivider, NDrawer, NDrawerContent, NDropdown, NFlex, NFormItem, NIcon, NInput, NInputNumber, NModal, NSwitch, NText, type DropdownOption } from 'naive-ui';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const show = defineModel<boolean>('show', { default: false });
|
||||
|
||||
const settingsStore = useSettingStore();
|
||||
const { menuCollpased, stationGridCols, debugModeEnabled, offlineDev } = storeToRefs(settingsStore);
|
||||
|
||||
const versionInfo = ref<VersionInfo>({ version: '', buildTime: '' });
|
||||
|
||||
const { mutate: getVersionInfo } = useMutation({
|
||||
mutationFn: async () => {
|
||||
const { data } = await axios.get<VersionInfo>(`/manifest.json?t=${Date.now()}`);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
versionInfo.value = data;
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
|
||||
const showDebugCodeModal = ref(false);
|
||||
const debugCode = ref('');
|
||||
const enableDebugMode = () => {
|
||||
const { debugCode: expectedDebugCode } = getAppEnvConfig();
|
||||
if (debugCode.value !== expectedDebugCode) {
|
||||
window.$message.error('调试授权码错误');
|
||||
return;
|
||||
}
|
||||
showDebugCodeModal.value = false;
|
||||
settingsStore.enableDebugMode();
|
||||
};
|
||||
const disableDebugMode = () => {
|
||||
showDebugCodeModal.value = false;
|
||||
settingsStore.disableDebugMode();
|
||||
};
|
||||
useEventListener('keydown', (event) => {
|
||||
const { ctrlKey, altKey, code } = event;
|
||||
if (ctrlKey && altKey && code === 'KeyD') {
|
||||
showDebugCodeModal.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
const expectToShowDebugCodeInput = ref(false);
|
||||
const onModalAfterEnter = () => {
|
||||
expectToShowDebugCodeInput.value = !debugModeEnabled.value;
|
||||
};
|
||||
const onModalAfterLeave = () => {
|
||||
expectToShowDebugCodeInput.value = false;
|
||||
debugCode.value = '';
|
||||
};
|
||||
|
||||
const pollingStore = usePollingStore();
|
||||
const { pollingEnabled } = storeToRefs(pollingStore);
|
||||
const onPollingEnabledUpdate = (enabled: boolean) => {
|
||||
if (enabled) {
|
||||
pollingStore.startPolling();
|
||||
} else {
|
||||
pollingStore.stopPolling();
|
||||
}
|
||||
};
|
||||
|
||||
type IndexedDbStoreId = typeof NDM_STATION_STORE_ID | typeof NDM_DEVICE_STORE_ID | typeof NDM_ALARM_STORE_ID;
|
||||
type IndexedDbStoreStates = {
|
||||
[NDM_STATION_STORE_ID]: { stations: Station[] };
|
||||
[NDM_DEVICE_STORE_ID]: { lineDevices: LineDevices };
|
||||
[NDM_ALARM_STORE_ID]: { lineAlarms: LineAlarms; unreadLineAlarms: LineAlarms };
|
||||
};
|
||||
const exportFromIndexedDB = async <K extends IndexedDbStoreId>(storeId: K, options?: { errorMsg?: string }) => {
|
||||
const { errorMsg } = options ?? {};
|
||||
const data = await localforage.getItem<IndexedDbStoreStates[K]>(storeId);
|
||||
if (!data) {
|
||||
window.$message.error(errorMsg ?? '导出数据失败');
|
||||
return;
|
||||
}
|
||||
downloadByData(JSON.stringify(data, null, 2), `${storeId}.json`);
|
||||
};
|
||||
const importToIndexedDB = async <K extends IndexedDbStoreId>(storeId: K, options?: { successMsg?: string; errorMsg?: string }) => {
|
||||
const { successMsg, errorMsg } = options ?? {};
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = '.json';
|
||||
fileInput.click();
|
||||
fileInput.onchange = async () => {
|
||||
const file = fileInput.files?.[0];
|
||||
if (!file) {
|
||||
window.$message.error(errorMsg ?? '导入数据失败');
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file, 'utf-8');
|
||||
reader.onload = async () => {
|
||||
const data = destr<IndexedDbStoreStates[K]>(reader.result as string);
|
||||
await localforage.setItem(storeId, data);
|
||||
window.$message.success(successMsg ?? '导入数据成功');
|
||||
await sleep(2000);
|
||||
window.location.reload();
|
||||
};
|
||||
};
|
||||
};
|
||||
const deleteFromIndexedDB = async (storeId: IndexedDbStoreId) => {
|
||||
await localforage.removeItem(storeId).catch((error) => {
|
||||
window.$message.error(`${error}`);
|
||||
return;
|
||||
});
|
||||
window.$message.success('删除成功');
|
||||
await sleep(2000);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const exportDropdownOptions: DropdownOption[] = [
|
||||
{
|
||||
label: '导出车站',
|
||||
key: 'exportStations',
|
||||
onSelect: () => exportFromIndexedDB(NDM_STATION_STORE_ID),
|
||||
},
|
||||
{
|
||||
label: '导出设备',
|
||||
key: 'exportDevices',
|
||||
onSelect: () => exportFromIndexedDB(NDM_DEVICE_STORE_ID),
|
||||
},
|
||||
{
|
||||
label: '导出告警',
|
||||
key: 'exportAlarms',
|
||||
onSelect: () => exportFromIndexedDB(NDM_ALARM_STORE_ID),
|
||||
},
|
||||
];
|
||||
const importDropdownOptions: DropdownOption[] = [
|
||||
{
|
||||
label: '导入车站',
|
||||
key: 'importStations',
|
||||
onSelect: () => importToIndexedDB(NDM_STATION_STORE_ID),
|
||||
},
|
||||
{
|
||||
label: '导入设备',
|
||||
key: 'importDevices',
|
||||
onSelect: () => importToIndexedDB(NDM_DEVICE_STORE_ID),
|
||||
},
|
||||
{
|
||||
label: '导入告警',
|
||||
key: 'importAlarms',
|
||||
onSelect: () => importToIndexedDB(NDM_ALARM_STORE_ID),
|
||||
},
|
||||
];
|
||||
const deleteDropdownOptions: DropdownOption[] = [
|
||||
{
|
||||
label: '删除车站',
|
||||
key: 'deleteStations',
|
||||
onSelect: () => deleteFromIndexedDB(NDM_STATION_STORE_ID),
|
||||
},
|
||||
{
|
||||
label: '删除设备',
|
||||
key: 'deleteDevices',
|
||||
onSelect: () => deleteFromIndexedDB(NDM_DEVICE_STORE_ID),
|
||||
},
|
||||
{
|
||||
label: '删除告警',
|
||||
key: 'deleteAlarms',
|
||||
onSelect: () => deleteFromIndexedDB(NDM_ALARM_STORE_ID),
|
||||
},
|
||||
];
|
||||
|
||||
const onSelectDropdownOption = (key: string, option: DropdownOption) => {
|
||||
const onSelect = option['onSelect'];
|
||||
if (isFunction(onSelect)) {
|
||||
onSelect();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getVersionInfo();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="show" :width="560" :auto-focus="false">
|
||||
<NDrawerContent closable title="系统设置" :native-scrollbar="false">
|
||||
<NFlex vertical>
|
||||
<NDivider>主题</NDivider>
|
||||
<NFormItem label="深色模式" label-placement="left">
|
||||
<ThemeSwitch size="small" />
|
||||
</NFormItem>
|
||||
|
||||
<NDivider>布局</NDivider>
|
||||
<NFormItem label="折叠菜单" label-placement="left">
|
||||
<NSwitch size="small" v-model:value="menuCollpased" />
|
||||
</NFormItem>
|
||||
<template v-if="route.path === '/station'">
|
||||
<NFormItem label="车站列数" label-placement="left">
|
||||
<NInputNumber v-model:value="stationGridCols" :min="1" :max="10" />
|
||||
</NFormItem>
|
||||
</template>
|
||||
|
||||
<template v-if="debugModeEnabled">
|
||||
<NDivider title-placement="center">调试</NDivider>
|
||||
<NFormItem label="启用轮询" label-placement="left">
|
||||
<NSwitch size="small" :value="pollingEnabled" @update:value="onPollingEnabledUpdate" />
|
||||
</NFormItem>
|
||||
<NFormItem label="离线开发" label-placement="left">
|
||||
<NSwitch size="small" v-model:value="offlineDev" />
|
||||
</NFormItem>
|
||||
<NFormItem label="本地数据库" label-placement="left">
|
||||
<NFlex>
|
||||
<NDropdown trigger="click" :options="exportDropdownOptions" @select="onSelectDropdownOption">
|
||||
<NButton secondary size="small">
|
||||
<template #icon>
|
||||
<NIcon :component="ExportOutlined" />
|
||||
</template>
|
||||
<template #default>导出</template>
|
||||
</NButton>
|
||||
</NDropdown>
|
||||
<NDropdown trigger="click" :options="importDropdownOptions" @select="onSelectDropdownOption">
|
||||
<NButton secondary size="small">
|
||||
<template #icon>
|
||||
<NIcon :component="ImportOutlined" />
|
||||
</template>
|
||||
<template #default>导入</template>
|
||||
</NButton>
|
||||
</NDropdown>
|
||||
<NDropdown trigger="click" :options="deleteDropdownOptions" @select="onSelectDropdownOption">
|
||||
<NButton secondary size="small">
|
||||
<template #icon>
|
||||
<NIcon :component="DeleteOutlined" />
|
||||
</template>
|
||||
<template #default>删除</template>
|
||||
</NButton>
|
||||
</NDropdown>
|
||||
</NFlex>
|
||||
</NFormItem>
|
||||
</template>
|
||||
</NFlex>
|
||||
<template #footer>
|
||||
<NFlex vertical justify="flex-end" align="center" style="width: 100%; font-size: 12px; gap: 4px">
|
||||
<NText :depth="3">平台版本: {{ versionInfo.version }} ({{ versionInfo.buildTime }})</NText>
|
||||
</NFlex>
|
||||
</template>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
|
||||
<NModal v-model:show="showDebugCodeModal" preset="dialog" type="info" @after-enter="onModalAfterEnter" @after-leave="onModalAfterLeave">
|
||||
<template #header>
|
||||
<NText v-if="!debugModeEnabled">请输入调试码</NText>
|
||||
<NText v-else>确认关闭调试模式</NText>
|
||||
</template>
|
||||
<template #default>
|
||||
<NInput v-if="expectToShowDebugCodeInput" v-model:value="debugCode" placeholder="输入调试码" @keyup.enter="enableDebugMode" />
|
||||
</template>
|
||||
<template #action>
|
||||
<NButton @click="showDebugCodeModal = false">取消</NButton>
|
||||
<NButton v-if="!debugModeEnabled" type="primary" @click="enableDebugMode">启用</NButton>
|
||||
<NButton v-else type="primary" @click="disableDebugMode">确认</NButton>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
3
src/components/global/theme-switch/index.ts
Normal file
3
src/components/global/theme-switch/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import ThemeSwitch from './theme-switch.vue';
|
||||
|
||||
export { ThemeSwitch };
|
||||
71
src/components/global/theme-switch/theme-switch.vue
Normal file
71
src/components/global/theme-switch/theme-switch.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettingStore } from '@/stores';
|
||||
import { NIcon, NSwitch } from 'naive-ui';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import type { ComponentInstance } from 'vue';
|
||||
|
||||
const settingsStore = useSettingStore();
|
||||
const { darkThemeEnabled } = storeToRefs(settingsStore);
|
||||
|
||||
// 使外部能够获取NSwitch的类型提示
|
||||
defineExpose({} as ComponentInstance<typeof NSwitch>);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSwitch v-model:value="darkThemeEnabled">
|
||||
<template #unchecked-icon>
|
||||
<NIcon>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="bzzmode-light" clip-path="url(#clip0_543_2115)">
|
||||
<path id="fill1" d="M19 12C19 15.866 15.866 19 12 19C8.13401 19 5 15.866 5 12C5 8.13401 8.13401 5 12 5C15.866 5 19 8.13401 19 12Z" fill="transparent" />
|
||||
<path
|
||||
id="stroke1"
|
||||
d="M19 12C19 15.866 15.866 19 12 19C8.13401 19 5 15.866 5 12C5 8.13401 8.13401 5 12 5C15.866 5 19 8.13401 19 12Z"
|
||||
stroke-linecap="square"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
<g id="bzzstroke2">
|
||||
<path
|
||||
d="M19.7819 19.7762 19.7791 19.779 19.7764 19.7762 19.7791 19.7734 19.7819 19.7762ZM23.0029 11.9961V12H22.999V11.9961H23.0029ZM19.7791 4.2168 19.7819 4.21956 19.7791 4.22232 19.7764 4.21956 19.7791 4.2168ZM11.999.996094H12.0029V1H11.999V.996094ZM4.22525 4.21956 4.22249 4.22232 4.21973 4.21956 4.22249 4.2168 4.22525 4.21956ZM1.00293 11.9961V12H.999023V11.9961H1.00293ZM4.22249 19.7734 4.22525 19.7762 4.22249 19.779 4.21973 19.7762 4.22249 19.7734ZM11.999 22.9961H12.0029V23H11.999V22.9961Z"
|
||||
stroke-linecap="square"
|
||||
id="stroke2"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</NIcon>
|
||||
</template>
|
||||
<template #checked-icon>
|
||||
<NIcon>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="bzxmode-dark">
|
||||
<path
|
||||
id="fill1"
|
||||
d="M20.5387 14.8522C20.0408 14.9492 19.5263 15 19 15C14.5817 15 11 11.4183 11 7C11 5.54296 11.3194 4.17663 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C15.9737 21 19.3459 18.4248 20.5387 14.8522Z"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
id="stroke1"
|
||||
d="M20.5387 14.8522C20.0408 14.9492 19.5263 15 19 15C14.5817 15 11 11.4183 11 7C11 5.54296 11.3194 4.17663 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C15.9737 21 19.3459 18.4248 20.5387 14.8522Z"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
<g id="bzxstroke2">
|
||||
<path
|
||||
d="M16.625 4 16.6692 4.08081 16.75 4.125 16.6692 4.16919 16.625 4.25 16.5808 4.16919 16.5 4.125 16.5808 4.08081 16.625 4ZM20.5 8.5 20.6768 8.82322 21 9 20.6768 9.17678 20.5 9.5 20.3232 9.17678 20 9 20.3232 8.82322 20.5 8.5Z"
|
||||
id="stroke2"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</NIcon>
|
||||
</template>
|
||||
</NSwitch>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
3
src/components/index.ts
Normal file
3
src/components/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './device';
|
||||
export * from './global';
|
||||
export * from './station';
|
||||
144
src/components/station/alarm-detail-modal/alarm-detail-modal.vue
Normal file
144
src/components/station/alarm-detail-modal/alarm-detail-modal.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<script setup lang="ts">
|
||||
import type { NdmDeviceAlarmLogResultVO, Station } from '@/apis';
|
||||
import { ALARM_TYPES, DEVICE_TYPE_LITERALS, DEVICE_TYPE_NAMES, FAULT_LEVELS, tryGetDeviceType } from '@/enums';
|
||||
import { renderAlarmDateCell, renderAlarmTypeCell, renderDeviceTypeCell, renderFaultLevelCell } from '@/helpers';
|
||||
import { useAlarmStore } from '@/stores';
|
||||
import { downloadByData } from '@/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import { NButton, NDataTable, NFlex, NGrid, NGridItem, NModal, NStatistic, NTag, type DataTableBaseColumn, type DataTableRowData, type PaginationProps } from 'naive-ui';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, h, reactive, ref, toRefs } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
station?: Station;
|
||||
}>();
|
||||
|
||||
const show = defineModel<boolean>('show', { default: false });
|
||||
|
||||
const { station } = toRefs(props);
|
||||
|
||||
const alarmStore = useAlarmStore();
|
||||
const { lineAlarms } = storeToRefs(alarmStore);
|
||||
|
||||
const classifiedAlarmCounts = computed<{ label: string; count: number }[]>(() => {
|
||||
const stationCode = station.value?.code;
|
||||
if (!stationCode) return [];
|
||||
const stationAlarms = lineAlarms.value[stationCode];
|
||||
if (!stationAlarms) return [];
|
||||
return Object.values(DEVICE_TYPE_LITERALS).map<{ label: string; count: number }>((deviceType) => {
|
||||
return {
|
||||
label: DEVICE_TYPE_NAMES[deviceType],
|
||||
count: stationAlarms[deviceType].length,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const tableColumns = ref<DataTableBaseColumn<NdmDeviceAlarmLogResultVO>[]>([
|
||||
{ title: '告警流水号', key: 'alarmNo' },
|
||||
{ title: '告警时间', key: 'alarmDate', render: renderAlarmDateCell },
|
||||
{ title: '设备类型', key: 'deviceType', render: renderDeviceTypeCell },
|
||||
{ title: '设备名称', key: 'deviceName' },
|
||||
{ title: '告警类型', key: 'alarmType', align: 'center', render: renderAlarmTypeCell },
|
||||
{ title: '故障级别', key: 'faultLevel', align: 'center', render: renderFaultLevelCell },
|
||||
// { title: '故障编码', key: 'faultCode', align: 'center' },
|
||||
// { title: '故障位置', key: 'faultLocation' },
|
||||
{ title: '故障描述', key: 'faultDescription' },
|
||||
{ title: '修复建议', key: 'alarmRepairSuggestion' },
|
||||
{ title: '是否恢复', key: 'alarmCategory', align: 'center', render: (rowData) => (rowData.alarmCategory === '2' ? '是' : '否') },
|
||||
{ title: '恢复时间', key: 'updatedTime' },
|
||||
{
|
||||
title: '告警确认',
|
||||
key: 'alarmConfirm',
|
||||
align: 'center',
|
||||
render: (rowData) => (rowData.alarmConfirm === '1' ? h(NTag, { type: 'default' }, { default: () => '已确认' }) : h(NTag, { type: 'warning' }, { default: () => '未确认' })),
|
||||
},
|
||||
// { title: '设备ID', key: 'deviceId' },
|
||||
]);
|
||||
|
||||
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],
|
||||
prefix: ({ itemCount }) => {
|
||||
return h('div', {}, { default: () => `共${itemCount}条` });
|
||||
},
|
||||
onUpdatePage: (page: number) => {
|
||||
pagination.page = page;
|
||||
},
|
||||
onUpdatePageSize: (pageSize: number) => {
|
||||
pagination.pageSize = pageSize;
|
||||
pagination.page = 1;
|
||||
},
|
||||
});
|
||||
|
||||
const tableData = computed<DataTableRowData[]>(() => {
|
||||
const stationCode = station.value?.code;
|
||||
if (!stationCode) return [];
|
||||
const stationAlarms = lineAlarms.value[stationCode];
|
||||
if (!stationAlarms) return [];
|
||||
return stationAlarms['unclassified'];
|
||||
});
|
||||
|
||||
const onAfterLeave = () => {
|
||||
pagination.page = 1;
|
||||
pagination.pageSize = 10;
|
||||
};
|
||||
|
||||
const exportAlarms = () => {
|
||||
const keys = tableColumns.value.map((column) => column.key);
|
||||
const csvHeader = `${tableColumns.value.map((column) => column.title).join(',')}\n`;
|
||||
let csvRows = '';
|
||||
for (const row of tableData.value) {
|
||||
const alarm = row as NdmDeviceAlarmLogResultVO;
|
||||
const csvRow = `${keys
|
||||
.map((key) => {
|
||||
const fieldKey = key as keyof NdmDeviceAlarmLogResultVO;
|
||||
if (fieldKey === 'alarmDate') return dayjs(Number(alarm[fieldKey])).format('YYYY-MM-DD HH:mm:ss');
|
||||
if (fieldKey === 'deviceType') {
|
||||
const deviceType = tryGetDeviceType(alarm[fieldKey]);
|
||||
if (!deviceType) return '-';
|
||||
return DEVICE_TYPE_NAMES[deviceType];
|
||||
}
|
||||
if (fieldKey === 'alarmType') return ALARM_TYPES[alarm[fieldKey] ?? ''];
|
||||
if (fieldKey === 'faultLevel') return FAULT_LEVELS[alarm[fieldKey] ?? ''];
|
||||
if (fieldKey === 'alarmCategory') return alarm[fieldKey] === '2' ? '是' : '否';
|
||||
if (fieldKey === 'alarmConfirm') return alarm[fieldKey] === '1' ? '已确认' : '未确认';
|
||||
return alarm[fieldKey];
|
||||
})
|
||||
.join(',')}\n`;
|
||||
csvRows = csvRows.concat(csvRow);
|
||||
}
|
||||
const csvContent = csvHeader.concat(csvRows);
|
||||
const time = dayjs().format('YYYY-MM-DD_HH-mm-ss');
|
||||
downloadByData(csvContent, `${station.value?.name}_设备告警记录_${time}.csv`, 'text/csv;charset=utf-8', '\ufeff');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal v-model:show="show" preset="card" style="width: 100vw; height: 100vh" :close-on-esc="false" :mask-closable="false" @after-leave="onAfterLeave">
|
||||
<template #header>
|
||||
<span>{{ `${station?.name} - 设备告警详情` }}</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<NFlex vertical :size="12" style="height: 100%">
|
||||
<NGrid cols="9" style="flex: 0 0 auto">
|
||||
<NGridItem v-for="item in classifiedAlarmCounts" :key="item.label" span="1">
|
||||
<NStatistic :label="item.label + '告警'" :value="item.count" />
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
|
||||
<NFlex align="center" style="flex: 0 0 auto">
|
||||
<div style="font-size: medium">今日设备告警列表</div>
|
||||
<NButton type="primary" style="margin-left: auto" @click="exportAlarms">导出</NButton>
|
||||
</NFlex>
|
||||
|
||||
<NDataTable flex-height style="height: 100%; min-height: 0; flex: 1 1 auto" :single-line="false" :columns="tableColumns" :data="tableData" :pagination="pagination" />
|
||||
</NFlex>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
3
src/components/station/alarm-detail-modal/index.ts
Normal file
3
src/components/station/alarm-detail-modal/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import AlarmDetailModal from './alarm-detail-modal.vue';
|
||||
|
||||
export { AlarmDetailModal };
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { Station } from '@/apis';
|
||||
import { DeviceTree } from '@/components';
|
||||
import { NModal } from 'naive-ui';
|
||||
import { toRefs } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
station?: Station;
|
||||
}>();
|
||||
|
||||
const show = defineModel<boolean>('show', { default: false });
|
||||
|
||||
const { station } = toRefs(props);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal v-model:show="show" preset="card" style="width: 600px; height: 600px" :title="`${station?.name} - 设备详情`" :content-style="{ height: '100%', overflow: 'hidden' }">
|
||||
<template #default>
|
||||
<DeviceTree :station="station" />
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
3
src/components/station/device-detail-modal/index.ts
Normal file
3
src/components/station/device-detail-modal/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import DeviceDetailModal from './device-detail-modal.vue';
|
||||
|
||||
export { DeviceDetailModal };
|
||||
@@ -0,0 +1,258 @@
|
||||
<script lang="ts">
|
||||
// 设备参数配置在系统中的key前缀
|
||||
const DEVICE_PARAM_PREFIXES = {
|
||||
Switch: 'SWITCH_',
|
||||
Server: 'SERVER_',
|
||||
Decoder: 'DECODER_',
|
||||
Nvr: 'NVR_',
|
||||
Box: 'BOX_',
|
||||
Monitor: 'MONITOR_',
|
||||
} as const;
|
||||
|
||||
type DeviceParamPrefix = (typeof DEVICE_PARAM_PREFIXES)[keyof typeof DEVICE_PARAM_PREFIXES];
|
||||
|
||||
// 渲染时的数据结构
|
||||
interface DeviceParamItem {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
numValue?: number;
|
||||
timeValue?: string;
|
||||
suffix?: string;
|
||||
step?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
// 一些参数值是零点几,一些参数值是好几十,需要根据参数名称中的关键词来做预处理
|
||||
const parseNumericValue = (name: string, value: string) => {
|
||||
let val = parseFloat(value);
|
||||
const needMultiply = name.includes('流量') || name.includes('占用率');
|
||||
if (needMultiply) val *= 100;
|
||||
return val;
|
||||
};
|
||||
|
||||
// 在保存参数时需要反向处理
|
||||
const deparseNumericValue = (name: string, value: number) => {
|
||||
let val = value;
|
||||
const needMultiply = name.includes('流量') || name.includes('占用率');
|
||||
if (needMultiply) val /= 100;
|
||||
return val;
|
||||
};
|
||||
|
||||
const getItemStep = (name: string) => {
|
||||
if (name.includes('转速')) return 100;
|
||||
return 1;
|
||||
};
|
||||
|
||||
const getItemMax = (name: string) => {
|
||||
if (name.includes('转速')) return 50000;
|
||||
return 100;
|
||||
};
|
||||
|
||||
const getItemSuffix = (name: string) => {
|
||||
const percentLike = name.includes('流量') || name.includes('占用率') || name.includes('湿度');
|
||||
const secondLike = name.includes('忽略丢失');
|
||||
const currentLike = name.includes('电流');
|
||||
const voltageLike = name.includes('电压');
|
||||
const temperatureLike = name.includes('温');
|
||||
const rpmLike = name.includes('转速');
|
||||
if (percentLike) return '%';
|
||||
if (secondLike) return '秒';
|
||||
if (currentLike) return 'A';
|
||||
if (voltageLike) return 'V';
|
||||
if (temperatureLike) return '℃';
|
||||
if (rpmLike) return '转/分';
|
||||
return '';
|
||||
};
|
||||
|
||||
const tabPanes = [
|
||||
{
|
||||
tab: '交换机阈值',
|
||||
name: DEVICE_PARAM_PREFIXES.Switch,
|
||||
},
|
||||
{
|
||||
tab: '服务器阈值',
|
||||
name: DEVICE_PARAM_PREFIXES.Server,
|
||||
},
|
||||
{
|
||||
tab: '解码器阈值',
|
||||
name: DEVICE_PARAM_PREFIXES.Decoder,
|
||||
},
|
||||
{
|
||||
tab: '录像机阈值',
|
||||
name: DEVICE_PARAM_PREFIXES.Nvr,
|
||||
},
|
||||
{
|
||||
tab: '安防箱阈值',
|
||||
name: DEVICE_PARAM_PREFIXES.Box,
|
||||
},
|
||||
{
|
||||
tab: '显示器计划',
|
||||
name: DEVICE_PARAM_PREFIXES.Monitor,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { pageDefParameterApi, resetMonitorScheduleApi, updateDefParameterApi, type Station } from '@/apis';
|
||||
import { parseErrorFeedback } from '@/utils';
|
||||
import { useMutation } from '@tanstack/vue-query';
|
||||
import { NFlex, NForm, NFormItemGi, NGrid, NInputNumber, NModal, NSpin, NTabPane, NTabs, NTimePicker } from 'naive-ui';
|
||||
import { ref, toRefs } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
station?: Station;
|
||||
}>();
|
||||
|
||||
const show = defineModel<boolean>('show', { required: true });
|
||||
|
||||
const { station } = toRefs(props);
|
||||
|
||||
const activeTabName = ref<DeviceParamPrefix>(DEVICE_PARAM_PREFIXES.Switch);
|
||||
|
||||
const deviceParams = ref<DeviceParamItem[]>([]);
|
||||
|
||||
const { mutate: getDeviceParams, isPending: paramsLoading } = useMutation({
|
||||
mutationFn: async (params: { deviceKeyPrefix: string }) => {
|
||||
const { deviceKeyPrefix } = params;
|
||||
const { records } = await pageDefParameterApi(
|
||||
{
|
||||
model: {},
|
||||
extra: { key_likeRight: deviceKeyPrefix },
|
||||
current: 1,
|
||||
size: 1000,
|
||||
sort: 'id',
|
||||
order: 'descending',
|
||||
},
|
||||
{
|
||||
stationCode: station.value?.code,
|
||||
},
|
||||
);
|
||||
return records;
|
||||
},
|
||||
onSuccess: (records) => {
|
||||
deviceParams.value = records.map<DeviceParamItem>((record) => {
|
||||
if (record.key?.includes(DEVICE_PARAM_PREFIXES.Monitor)) {
|
||||
return {
|
||||
id: record.id ?? '',
|
||||
key: record.key ?? '',
|
||||
name: record.name ?? '',
|
||||
timeValue: record.value ?? '',
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: record.id ?? '',
|
||||
key: record.key ?? '',
|
||||
name: record.name ?? '',
|
||||
numValue: parseNumericValue(record.name ?? '', record.value ?? '0'),
|
||||
suffix: getItemSuffix(record.name ?? ''),
|
||||
step: getItemStep(record.name ?? ''),
|
||||
min: 0,
|
||||
max: getItemMax(record.name ?? ''),
|
||||
};
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: saveDeviceParams } = useMutation({
|
||||
mutationFn: async (params: { tabName: string; items: DeviceParamItem[] }) => {
|
||||
const { tabName, items } = params;
|
||||
for (const item of items) {
|
||||
if (tabName.includes(DEVICE_PARAM_PREFIXES.Monitor)) {
|
||||
await updateDefParameterApi(
|
||||
{
|
||||
id: item.id,
|
||||
key: item.key,
|
||||
name: item.name,
|
||||
value: item.timeValue,
|
||||
},
|
||||
{
|
||||
stationCode: station.value?.code,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await updateDefParameterApi(
|
||||
{
|
||||
id: item.id,
|
||||
key: item.key,
|
||||
name: item.name,
|
||||
value: `${deparseNumericValue(item.name, item.numValue ?? 0)}`,
|
||||
},
|
||||
{
|
||||
stationCode: station.value?.code,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
await resetMonitorScheduleApi({ stationCode: station.value?.code });
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
|
||||
const onBeforeTabLeave = (name: string, oldName: string): boolean | Promise<boolean> => {
|
||||
saveDeviceParams({ tabName: oldName, items: deviceParams.value });
|
||||
getDeviceParams({ deviceKeyPrefix: name });
|
||||
return true;
|
||||
};
|
||||
|
||||
const onAfterModalEnter = () => {
|
||||
getDeviceParams({ deviceKeyPrefix: activeTabName.value });
|
||||
};
|
||||
|
||||
const onBeforeModalLeave = () => {
|
||||
saveDeviceParams({ tabName: activeTabName.value, items: deviceParams.value });
|
||||
activeTabName.value = DEVICE_PARAM_PREFIXES.Switch;
|
||||
deviceParams.value = [];
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal
|
||||
v-model:show="show"
|
||||
preset="card"
|
||||
style="width: 800px; height: 600px"
|
||||
:title="`${station?.name} - 设备参数配置`"
|
||||
:auto-focus="false"
|
||||
:close-on-esc="false"
|
||||
:mask-closable="false"
|
||||
@after-enter="onAfterModalEnter"
|
||||
@before-leave="onBeforeModalLeave"
|
||||
>
|
||||
<NTabs v-model:value="activeTabName" type="card" @before-leave="onBeforeTabLeave">
|
||||
<NTabPane v-for="pane in tabPanes" :key="pane.name" :tab="pane.tab" :name="pane.name">
|
||||
<NFlex v-if="paramsLoading" :justify="'center'" :align="'center'">
|
||||
<NSpin :show="paramsLoading" description="加载设备参数中..." />
|
||||
</NFlex>
|
||||
<NForm v-else>
|
||||
<NGrid cols="1">
|
||||
<NFormItemGi v-for="item in deviceParams" :key="item.key" span="1" label-placement="left" :label="item.name">
|
||||
<!-- 监视器计划配置渲染时间选择器,其他配置项渲染数字输入框 -->
|
||||
<template v-if="activeTabName === DEVICE_PARAM_PREFIXES.Monitor">
|
||||
<NTimePicker v-model:formatted-value="item.timeValue" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<NInputNumber v-model:value="item.numValue" :step="item.step" :min="item.min" :max="item.max" style="width: 100%">
|
||||
<template #suffix>
|
||||
<span>{{ item.suffix }}</span>
|
||||
</template>
|
||||
</NInputNumber>
|
||||
</template>
|
||||
</NFormItemGi>
|
||||
</NGrid>
|
||||
</NForm>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,3 @@
|
||||
import DeviceParamConfigModal from './device-param-config.modal.vue';
|
||||
|
||||
export { DeviceParamConfigModal };
|
||||
127
src/components/station/icmp-export-modal/icmp-export-modal.vue
Normal file
127
src/components/station/icmp-export-modal/icmp-export-modal.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import { exportIcmpByStationApi, type Station } from '@/apis';
|
||||
import { DEVICE_TYPE_LITERALS } from '@/enums';
|
||||
import { useDeviceStore } from '@/stores';
|
||||
import { downloadByData, parseErrorFeedback } from '@/utils';
|
||||
import { useMutation } from '@tanstack/vue-query';
|
||||
import { isCancel } from 'axios';
|
||||
import dayjs from 'dayjs';
|
||||
import { NButton, NFlex, NGrid, NGridItem, NModal, NRadio, NRadioGroup, NStatistic } from 'naive-ui';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, ref, toRefs } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
stations: Station[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
afterLeave: [];
|
||||
}>();
|
||||
|
||||
const show = defineModel<boolean>('show');
|
||||
|
||||
const { stations } = toRefs(props);
|
||||
|
||||
const deviceStore = useDeviceStore();
|
||||
const { lineDevices } = storeToRefs(deviceStore);
|
||||
|
||||
const status = ref('');
|
||||
|
||||
const abortController = ref<AbortController>(new AbortController());
|
||||
|
||||
const { mutate: exportIcmp, isPending: loading } = useMutation({
|
||||
mutationFn: async (params: { status: string }) => {
|
||||
abortController.value.abort();
|
||||
abortController.value = new AbortController();
|
||||
const data = await exportIcmpByStationApi(
|
||||
stations.value.map((station) => station.code),
|
||||
params.status,
|
||||
{
|
||||
signal: abortController.value.signal,
|
||||
},
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (data, variables) => {
|
||||
const { status } = variables;
|
||||
let fileName = '全部设备列表';
|
||||
if (status === '10') {
|
||||
fileName = '在线设备列表';
|
||||
} else if (status === '20') {
|
||||
fileName = '离线设备列表';
|
||||
}
|
||||
const time = dayjs().format('YYYY-MM-DD_HH-mm-ss');
|
||||
downloadByData(data, `${fileName}_${time}.xlsx`);
|
||||
},
|
||||
onError: (error) => {
|
||||
if (isCancel(error)) return;
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
|
||||
const onAfterLeave = () => {
|
||||
abortController.value.abort();
|
||||
status.value = '';
|
||||
emit('afterLeave');
|
||||
};
|
||||
|
||||
const onlineDeviceCount = computed(() => {
|
||||
let count = 0;
|
||||
for (const station of stations.value) {
|
||||
if (station.online) {
|
||||
const stationDevices = lineDevices.value[station.code];
|
||||
Object.values(DEVICE_TYPE_LITERALS).forEach((deviceType) => {
|
||||
const onlineDeviceList = stationDevices?.[deviceType]?.filter((device) => device.deviceStatus === '10') ?? [];
|
||||
count += onlineDeviceList.length;
|
||||
});
|
||||
}
|
||||
}
|
||||
return count;
|
||||
});
|
||||
const offlineDeviceCount = computed(() => {
|
||||
let count = 0;
|
||||
for (const station of stations.value) {
|
||||
if (station.online) {
|
||||
const stationDevices = lineDevices.value[station.code];
|
||||
Object.values(DEVICE_TYPE_LITERALS).forEach((deviceType) => {
|
||||
const onlineDeviceList = stationDevices?.[deviceType]?.filter((device) => device.deviceStatus === '20') ?? [];
|
||||
count += onlineDeviceList.length;
|
||||
});
|
||||
}
|
||||
}
|
||||
return count;
|
||||
});
|
||||
const deviceCount = computed(() => onlineDeviceCount.value + offlineDeviceCount.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal v-model:show="show" preset="card" title="导出设备列表" @after-leave="onAfterLeave" style="width: 800px; height: 300px">
|
||||
<template #default>
|
||||
<NGrid :cols="3" :x-gap="24" :y-gap="8">
|
||||
<NGridItem>
|
||||
<NStatistic label="全部设备" :value="deviceCount" />
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<NStatistic label="在线设备" :value="onlineDeviceCount" :value-style="{ color: '#18a058' }" />
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<NStatistic label="离线设备" :value="offlineDeviceCount" :value-style="{ color: '#d03050' }" />
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
</template>
|
||||
<template #action>
|
||||
<NFlex justify="flex-end" align="center">
|
||||
<NRadioGroup v-model:value="status">
|
||||
<NRadio value="">全部</NRadio>
|
||||
<NRadio value="10">在线</NRadio>
|
||||
<NRadio value="20">离线</NRadio>
|
||||
</NRadioGroup>
|
||||
<NButton secondary :loading="loading" @click="() => exportIcmp({ status })">导出</NButton>
|
||||
</NFlex>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
3
src/components/station/icmp-export-modal/index.ts
Normal file
3
src/components/station/icmp-export-modal/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import IcmpExportModal from './icmp-export-modal.vue';
|
||||
|
||||
export { IcmpExportModal };
|
||||
7
src/components/station/index.ts
Normal file
7
src/components/station/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './alarm-detail-modal';
|
||||
export * from './device-detail-modal';
|
||||
export * from './device-param-config-modal';
|
||||
export * from './icmp-export-modal';
|
||||
export * from './record-check-export-modal';
|
||||
export * from './station-card';
|
||||
export * from './sync-camera-result-modal';
|
||||
@@ -0,0 +1,3 @@
|
||||
import RecordCheckExportModal from './record-check-export-modal.vue';
|
||||
|
||||
export { RecordCheckExportModal };
|
||||
@@ -0,0 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
import { getRecordCheckApi, type NdmNvrResultVO, type Station } from '@/apis';
|
||||
import { exportRecordDiagCsv, isNvrCluster, transformRecordChecks } from '@/helpers';
|
||||
import { useDeviceStore } from '@/stores';
|
||||
import { parseErrorFeedback } from '@/utils';
|
||||
import { useMutation } from '@tanstack/vue-query';
|
||||
import { isCancel } from 'axios';
|
||||
import { NButton, NGrid, NGridItem, NModal, NScrollbar, NSpin } from 'naive-ui';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, ref, toRefs } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
stations: Station[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
afterLeave: [];
|
||||
}>();
|
||||
|
||||
const show = defineModel<boolean>('show');
|
||||
|
||||
const deviceStore = useDeviceStore();
|
||||
const { lineDevices } = storeToRefs(deviceStore);
|
||||
|
||||
const { stations } = toRefs(props);
|
||||
|
||||
const nvrClusterRecord = computed(() => {
|
||||
const clusterMap: Record<Station['code'], { stationName: Station['name']; clusters: NdmNvrResultVO[] }> = {};
|
||||
stations.value.forEach((station) => {
|
||||
clusterMap[station.code] = {
|
||||
stationName: station.name,
|
||||
clusters: [],
|
||||
};
|
||||
const stationDevices = lineDevices.value[station.code];
|
||||
const nvrs = stationDevices?.['ndmNvr'] ?? [];
|
||||
nvrs.forEach((nvr) => {
|
||||
if (isNvrCluster(nvr)) {
|
||||
clusterMap[station.code]?.clusters?.push(nvr);
|
||||
}
|
||||
});
|
||||
});
|
||||
return clusterMap;
|
||||
});
|
||||
|
||||
const abortController = ref<AbortController>(new AbortController());
|
||||
|
||||
const { mutate: exportRecordDiags, isPending: exporting } = useMutation({
|
||||
mutationFn: async (params: { clusters: NdmNvrResultVO[]; stationCode: Station['code'] }) => {
|
||||
const { clusters, stationCode } = params;
|
||||
if (clusters.length === 0) {
|
||||
const stationName = nvrClusterRecord.value[stationCode]?.stationName ?? '';
|
||||
window.$message.info(`${stationName} 没有录像诊断数据`);
|
||||
return;
|
||||
}
|
||||
const cluster = clusters.at(0);
|
||||
if (!cluster) return;
|
||||
abortController.value.abort();
|
||||
abortController.value = new AbortController();
|
||||
const checks = await getRecordCheckApi(cluster, 90, [], { stationCode: stationCode, signal: abortController.value.signal });
|
||||
return checks;
|
||||
},
|
||||
onSuccess: (checks, { stationCode }) => {
|
||||
if (!checks || checks.length === 0) return;
|
||||
const recordDiags = transformRecordChecks(checks);
|
||||
exportRecordDiagCsv(recordDiags, nvrClusterRecord.value[stationCode]?.stationName ?? '');
|
||||
},
|
||||
onError: (error) => {
|
||||
if (isCancel(error)) return;
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
|
||||
const onAfterLeave = () => {
|
||||
emit('afterLeave');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal v-model:show="show" preset="card" title="导出录像诊断" @after-leave="onAfterLeave" style="width: 800px">
|
||||
<template #default>
|
||||
<NScrollbar style="height: 300px">
|
||||
<NSpin size="small" :show="exporting">
|
||||
<NGrid :cols="6">
|
||||
<template v-for="({ stationName, clusters }, code) in nvrClusterRecord" :key="code">
|
||||
<NGridItem>
|
||||
<NButton text type="info" style="height: 30px" @click="() => exportRecordDiags({ clusters, stationCode: code })">{{ stationName }}</NButton>
|
||||
</NGridItem>
|
||||
</template>
|
||||
</NGrid>
|
||||
</NSpin>
|
||||
</NScrollbar>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
6
src/components/station/station-card/index.ts
Normal file
6
src/components/station/station-card/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { ComponentInstance } from 'vue';
|
||||
import StationCard from './station-card.vue';
|
||||
|
||||
export type StationCardProps = ComponentInstance<typeof StationCard>['$props'];
|
||||
|
||||
export { StationCard };
|
||||
164
src/components/station/station-card/station-card.vue
Normal file
164
src/components/station/station-card/station-card.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<script setup lang="ts">
|
||||
import type { Station, StationAlarms, StationDevices } from '@/apis';
|
||||
import { DEVICE_TYPE_LITERALS } from '@/enums';
|
||||
import { EllipsisOutlined, MoreOutlined } from '@vicons/antd';
|
||||
import axios from 'axios';
|
||||
import { isFunction } from 'es-toolkit';
|
||||
import { NButton, NCard, NCheckbox, NDropdown, NFlex, NIcon, NTag, NTooltip, useThemeVars, type DropdownOption } from 'naive-ui';
|
||||
import { computed, toRefs } from 'vue';
|
||||
|
||||
const themeVars = useThemeVars();
|
||||
|
||||
const props = defineProps<{
|
||||
station: Station;
|
||||
devices: StationDevices;
|
||||
alarms: StationAlarms;
|
||||
selectable?: boolean;
|
||||
}>();
|
||||
|
||||
const selected = defineModel<boolean>('selected', { default: false });
|
||||
|
||||
const emit = defineEmits<{
|
||||
clickDetail: [type: 'device' | 'alarm', station: Station];
|
||||
clickConfig: [station: Station];
|
||||
}>();
|
||||
|
||||
const { station, devices, alarms, selectable } = toRefs(props);
|
||||
|
||||
const onlineDeviceCount = computed(() => {
|
||||
return Object.values(DEVICE_TYPE_LITERALS).reduce((count, deviceType) => {
|
||||
const onlineDevices = devices.value[deviceType].filter((device) => device.deviceStatus === '10');
|
||||
return count + onlineDevices.length;
|
||||
}, 0);
|
||||
});
|
||||
const offlineDeviceCount = computed(() => {
|
||||
return Object.values(DEVICE_TYPE_LITERALS).reduce((count, deviceType) => {
|
||||
const offlineDevices = devices.value[deviceType].filter((device) => device.deviceStatus === '20');
|
||||
return count + offlineDevices.length;
|
||||
}, 0);
|
||||
});
|
||||
const deviceCount = computed(() => {
|
||||
return Object.values(DEVICE_TYPE_LITERALS).reduce((count, deviceType) => {
|
||||
return count + devices.value[deviceType].length;
|
||||
}, 0);
|
||||
});
|
||||
|
||||
const alarmCount = computed(() => {
|
||||
return alarms.value.unclassified.length;
|
||||
});
|
||||
|
||||
const openVideoPlatform = async () => {
|
||||
try {
|
||||
const response = await axios.get<Record<string, string>>('/minio/ndm/ndm-vimps.json');
|
||||
const url = response.data[station.value.code];
|
||||
if (!url) {
|
||||
window.$message.warning(`未找到车站编码 ${station.value.code} 对应的视频平台URL`);
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
} catch (error) {
|
||||
console.error('获取视频平台URL失败:', error);
|
||||
window.$message.error('获取视频平台URL失败');
|
||||
}
|
||||
};
|
||||
|
||||
const openDeviceConfigModal = () => {
|
||||
if (!station.value.online) {
|
||||
window.$message.error('当前车站离线,无法查看');
|
||||
return;
|
||||
}
|
||||
emit('clickConfig', station.value);
|
||||
};
|
||||
|
||||
const dropdownOptions: DropdownOption[] = [
|
||||
{
|
||||
label: '视频平台',
|
||||
key: 'video-platform',
|
||||
onSelect: openVideoPlatform,
|
||||
},
|
||||
{
|
||||
label: '设备配置',
|
||||
key: 'device-config',
|
||||
onSelect: openDeviceConfigModal,
|
||||
},
|
||||
];
|
||||
|
||||
const onSelectDropdownOption = (key: string, option: DropdownOption) => {
|
||||
const onSelect = option['onSelect'];
|
||||
if (isFunction(onSelect)) {
|
||||
onSelect();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard bordered hoverable size="medium" :header-style="{ padding: `6px` }" :content-style="{ padding: `0px 6px 6px 6px` }">
|
||||
<template #header>
|
||||
<template v-if="station.ip">
|
||||
<NTooltip trigger="click">
|
||||
<template #trigger>
|
||||
<span style="font-size: smaller">{{ station.name }}</span>
|
||||
</template>
|
||||
<span>{{ station.ip }}</span>
|
||||
</NTooltip>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span style="font-size: smaller">{{ station.name }}</span>
|
||||
</template>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<NFlex align="center" :size="4">
|
||||
<NCheckbox v-if="selectable" v-model:checked="selected" :disabled="!station.online" />
|
||||
<NTag :type="station.online ? 'success' : 'error'" size="small">
|
||||
{{ station.online ? '在线' : '离线' }}
|
||||
</NTag>
|
||||
<NDropdown trigger="click" :options="dropdownOptions" @select="onSelectDropdownOption">
|
||||
<NButton quaternary size="tiny" :focusable="false">
|
||||
<template #icon>
|
||||
<NIcon :component="MoreOutlined" />
|
||||
</template>
|
||||
</NButton>
|
||||
</NDropdown>
|
||||
</NFlex>
|
||||
</template>
|
||||
<template #default>
|
||||
<NFlex vertical :size="6" :style="{ opacity: station.online ? '1' : '0.5' }">
|
||||
<NFlex vertical :size="4">
|
||||
<NFlex justify="flex-end" align="center" :size="2">
|
||||
<span>{{ deviceCount }} 台设备</span>
|
||||
<NButton quaternary size="tiny" :focusable="false" @click="() => emit('clickDetail', 'device', station)">
|
||||
<template #icon>
|
||||
<NIcon :component="EllipsisOutlined" />
|
||||
</template>
|
||||
</NButton>
|
||||
</NFlex>
|
||||
|
||||
<NFlex justify="flex-end" align="center" :size="2">
|
||||
<div>
|
||||
<span :style="{ color: onlineDeviceCount > 0 ? themeVars.successColor : '' }">在线{{ onlineDeviceCount }}台</span>
|
||||
<span> · </span>
|
||||
<span :style="{ color: offlineDeviceCount > 0 ? themeVars.errorColor : '' }">离线 {{ offlineDeviceCount }}台</span>
|
||||
</div>
|
||||
<!-- 占位按钮,对齐布局 -->
|
||||
<NButton quaternary size="tiny" :focusable="false" style="visibility: hidden">
|
||||
<template #icon>
|
||||
<NIcon :component="EllipsisOutlined" />
|
||||
</template>
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
|
||||
<NFlex justify="flex-end" align="center" :size="2">
|
||||
<span :style="{ color: alarmCount > 0 ? themeVars.warningColor : '' }">今日 {{ alarmCount }} 条告警</span>
|
||||
<NButton quaternary size="tiny" :focusable="false" @click="() => emit('clickDetail', 'alarm', station)">
|
||||
<template #icon>
|
||||
<NIcon :component="EllipsisOutlined" />
|
||||
</template>
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</template>
|
||||
</NCard>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
3
src/components/station/sync-camera-result-modal/index.ts
Normal file
3
src/components/station/sync-camera-result-modal/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import SyncCameraResultModal from './sync-camera-result-modal.vue';
|
||||
|
||||
export { SyncCameraResultModal };
|
||||
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { watchDebounced } from '@vueuse/core';
|
||||
import { NFlex, NIcon, NList, NListItem, NModal, NScrollbar, NStatistic, NText, NThing } from 'naive-ui';
|
||||
import { computed, ref, toRefs } from 'vue';
|
||||
import { useStationStore } from '@/stores';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import type { Station, SyncCameraResult } from '@/apis';
|
||||
import { DeleteFilled, EditFilled, PlusCircleFilled } from '@vicons/antd';
|
||||
|
||||
const props = defineProps<{
|
||||
syncCameraResult: Record<Station['code'], SyncCameraResult>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
afterLeave: [];
|
||||
}>();
|
||||
|
||||
const stationStore = useStationStore();
|
||||
const { stations } = storeToRefs(stationStore);
|
||||
|
||||
const { syncCameraResult } = toRefs(props);
|
||||
|
||||
const show = ref(false);
|
||||
|
||||
watchDebounced(
|
||||
[syncCameraResult],
|
||||
([result]) => {
|
||||
show.value = Object.keys(result).length > 0;
|
||||
},
|
||||
{
|
||||
debounce: 500,
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
|
||||
const onAfterLeave = () => {
|
||||
emit('afterLeave');
|
||||
};
|
||||
|
||||
const syncList = computed(() => {
|
||||
return Object.values(syncCameraResult.value).map((sync) => {
|
||||
const { stationCode, startTime, endTime, insertList, updateList, deleteList } = sync;
|
||||
const stationName = stations.value.find((station) => station.code === stationCode)?.name;
|
||||
return { stationName, startTime, endTime, insertList, updateList, deleteList };
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal v-model:show="show" preset="card" title="摄像机同步结果" style="width: 600px" @after-leave="onAfterLeave">
|
||||
<NScrollbar style="max-height: 400px">
|
||||
<NList hoverable clickable>
|
||||
<NListItem v-for="{ stationName, endTime, insertList, updateList, deleteList } in syncList" :key="stationName">
|
||||
<NThing title-independent>
|
||||
<template #header>
|
||||
<NText strong>{{ stationName }}</NText>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<NText depth="3"> {{ endTime }} 完成 </NText>
|
||||
</template>
|
||||
<NFlex justify="space-around" :size="24" style="margin-top: 8px">
|
||||
<NStatistic label="新增">
|
||||
<template #prefix>
|
||||
<NIcon :component="PlusCircleFilled" />
|
||||
</template>
|
||||
{{ insertList.length }}
|
||||
</NStatistic>
|
||||
<NStatistic label="更新">
|
||||
<template #prefix>
|
||||
<NIcon :component="EditFilled" />
|
||||
</template>
|
||||
{{ updateList.length }}
|
||||
</NStatistic>
|
||||
<NStatistic label="删除">
|
||||
<template #prefix>
|
||||
<NIcon :component="DeleteFilled" />
|
||||
</template>
|
||||
{{ deleteList.length }}
|
||||
</NStatistic>
|
||||
</NFlex>
|
||||
</NThing>
|
||||
</NListItem>
|
||||
</NList>
|
||||
</NScrollbar>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
Reference in New Issue
Block a user