2 Commits

Author SHA1 Message Date
yangsy 1c71151a6b docs: 更新README.md 2026-03-02 15:39:06 +08:00
yangsy 68c5d12e14 chore: 版本信息和更新日志 2026-03-02 15:24:15 +08:00
18 changed files with 58 additions and 253 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ndm-web-platform", "name": "ndm-web-platform",
"version": "0.40.0", "version": "0.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"engines": { "engines": {
-9
View File
@@ -1,17 +1,8 @@
[ [
{
"version": "0.40.0",
"date": "2026-04-10",
"changes": {
"fixes": [{ "content": "修复设备树搜索时节点错乱的问题" }, { "content": "将安防箱面板的“电路”统一更正为“空开”" }, { "content": "修复设备查询缓存键冲突问题" }],
"feats": [{ "content": "添加视频服务器双机热备状态" }]
}
},
{ {
"version": "0.39.0", "version": "0.39.0",
"date": "2026-03-02", "date": "2026-03-02",
"changes": { "changes": {
"fixes": [{ "content": "修复设备树搜索时节点错乱的问题" }],
"feats": [{ "content": "新版录像记录诊断卡片" }, { "content": "新增平台更新记录页面" }] "feats": [{ "content": "新版录像记录诊断卡片" }, { "content": "新增平台更新记录页面" }]
} }
}, },
+2 -2
View File
@@ -1,4 +1,4 @@
{ {
"version": "0.40.0", "version": "0.39.0",
"buildTime": "2026-04-10 15:42:03" "buildTime": "2026-03-02 15:14:53"
} }
@@ -1,5 +0,0 @@
export interface HighAvailable {
pyip: string;
vip: string;
changeDate: string;
}
-1
View File
@@ -1,4 +1,3 @@
export * from './diag'; export * from './diag';
export * from './high-available';
export * from './link-description'; export * from './link-description';
export * from './station'; export * from './station';
@@ -1,26 +1,5 @@
import { ndmClient, userClient, type HighAvailable, type MediaServerStatus, type SendRtpInfo, type Station } from '@/apis'; import { ndmClient, userClient, type MediaServerStatus, type SendRtpInfo, type Station } from '@/apis';
import { unwrapNullableResponse, unwrapResponse } from '@/utils'; import { unwrapResponse } from '@/utils';
import destr from 'destr';
// {
// "code": 0,
// "data": "{pyip:\"10.18.128.14\",vip:\"10.18.128.6\",changeDate:\"2026-03-23 15:55:00\"}",
// "msg": "ok",
// "path": null,
// "extra": null,
// "timestamp": "1774421387908",
// "errorMsg": "",
// "isSuccess": true
// }
export const getHighAvailableApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmServiceAvailable/highAvailable/get`;
const resp = await client.get<string | null>(endpoint, { signal });
const data = unwrapNullableResponse(resp);
return destr<HighAvailable | null>(data);
};
export const getAllPushApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => { export const getAllPushApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {}; const { stationCode, signal } = options ?? {};
@@ -92,8 +92,6 @@ const abortController = ref<AbortController>(new AbortController());
const NVR_RECORD_CHECK_KEY = 'nvr-record-check-query'; const NVR_RECORD_CHECK_KEY = 'nvr-record-check-query';
const deviceUniqueKey = computed(() => [station.value.code, ndmDevice.value.id]);
const DAY_OFFSET = 90; const DAY_OFFSET = 90;
const { const {
@@ -101,7 +99,7 @@ const {
isFetching: loading, isFetching: loading,
refetch: refetchRecordChecks, refetch: refetchRecordChecks,
} = useQuery({ } = useQuery({
queryKey: computed(() => [NVR_RECORD_CHECK_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]), queryKey: computed(() => [NVR_RECORD_CHECK_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value), enabled: computed(() => activeRequests.value),
refetchInterval: 30 * 1000, refetchInterval: 30 * 1000,
gcTime: 0, gcTime: 0,
@@ -410,13 +408,13 @@ const ndmRecordChecksPaged = computed(() => {
return ndmRecordChecksFiltered.value.slice(startIndex, endIndex); return ndmRecordChecksFiltered.value.slice(startIndex, endIndex);
}); });
// 当车站号、设备IP、最后诊断时间或筛选类型变化时,重置分页为第一页 // 当设备ID、最后诊断时间或筛选类型变化时,重置分页为第一页
watch([() => station.value.code, () => ndmDevice.value.ipAddress, () => ndmDevice.value.lastDiagTime, filterType, searchInputDebounced], () => { watch([() => ndmDevice.value.id, () => ndmDevice.value.lastDiagTime, filterType, searchInputDebounced], () => {
page.value = 1; page.value = 1;
}); });
// 当车站号、设备IP变化时,重置搜索内容,并将筛选类型重置为「全部」 // 当设备ID变化时,重置搜索内容,并将筛选类型重置为「全部」
watch([() => station.value.code, () => ndmDevice.value.ipAddress], () => { watch([() => ndmDevice.value.id], () => {
searchInput.value = ''; searchInput.value = '';
filterType.value = 'all'; filterType.value = 'all';
}); });
@@ -628,14 +626,9 @@ const columns: DataTableColumns<DailyLossItem['chunks'][number]> = [
<template #default> <template #default>
<template v-if="!!dailyCheckContext.info"> <template v-if="!!dailyCheckContext.info">
<div>日期:{{ dailyCheckContext.info.date }}</div> <div>日期:{{ dailyCheckContext.info.date }}</div>
<template v-if="dailyCheckContext.info.percent > 0"> <div>缺失时长:{{ formatDuration(dailyCheckContext.info.total, { withinDay: true }) }}</div>
<div>缺失时长{{ formatDuration(dailyCheckContext.info.total, { withinDay: true }) }}</div> <div>缺失比例{{ dailyCheckContext.info.percent.toFixed(2) }}%</div>
<div>缺失比例:{{ dailyCheckContext.info.percent.toFixed(2) }}%</div> <div v-if="dailyCheckContext.info.percent > 0" style="font-size: xx-small; opacity: 0.5; cursor: pointer" @click="onClickDailyCheck">点击查看详情</div>
<div style="font-size: xx-small; opacity: 0.5; cursor: pointer" @click="onClickDailyCheck">点击查看详情</div>
</template>
<template v-else>
<div>录像完整</div>
</template>
</template> </template>
</template> </template>
</NPopover> </NPopover>
@@ -202,7 +202,7 @@ const contextmenuOptions = computed<DropdownOption[]>(() => [
if (!lowerDevice) return; if (!lowerDevice) return;
window.$dialog.warning({ window.$dialog.warning({
title: '确认解除关联吗?', title: '确认解除关联吗?',
content: `将解除【空开${circuitIndex + 1}】与【${lowerDevice.name}】的关联关系。`, content: `将解除【电路${circuitIndex + 1}】与【${lowerDevice.name}】的关联关系。`,
style: { width: '600px' }, style: { width: '600px' },
contentStyle: { height: '60px' }, contentStyle: { height: '60px' },
negativeText: '取消', negativeText: '取消',
@@ -299,7 +299,7 @@ const { mutate: unlinkDevice } = useMutation({
<NCard v-if="showCard" hoverable size="small"> <NCard v-if="showCard" hoverable size="small">
<template #header> <template #header>
<NFlex align="center"> <NFlex align="center">
<span>空开状态</span> <span>电路状态</span>
<NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => reboot()"> <NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => reboot()">
<template #trigger> <template #trigger>
<NButton secondary size="small" :loading="rebooting">重合闸</NButton> <NButton secondary size="small" :loading="rebooting">重合闸</NButton>
@@ -324,7 +324,7 @@ const { mutate: unlinkDevice } = useMutation({
<span>{{ getCircuitStatusText(circuit) }}</span> <span>{{ getCircuitStatusText(circuit) }}</span>
</template> </template>
</NTag> </NTag>
<span>空开{{ circuitIndex + 1 }}</span> <span>电路{{ circuitIndex + 1 }}</span>
</NFlex> </NFlex>
<NFlex justify="end" align="center"> <NFlex justify="end" align="center">
<NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => turnStatus({ circuitIndex: circuitIndex, newStatus: circuit.status !== 1 })"> <NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => turnStatus({ circuitIndex: circuitIndex, newStatus: circuit.status !== 1 })">
@@ -332,7 +332,7 @@ const { mutate: unlinkDevice } = useMutation({
<NSwitch size="small" :value="circuit.status === 1" :loading="turning" /> <NSwitch size="small" :value="circuit.status === 1" :loading="turning" />
</template> </template>
<template #default> <template #default>
<span>确定要{{ circuit.status === 1 ? '关闭' : '开启' }}空开{{ circuitIndex + 1 }}吗?</span> <span>确定要{{ circuit.status === 1 ? '关闭' : '开启' }}电路{{ circuitIndex + 1 }}吗?</span>
</template> </template>
</NPopconfirm> </NPopconfirm>
</NFlex> </NFlex>
@@ -60,7 +60,7 @@ const { mutate: linkPortToDevice, isPending: linking } = useMutation({
const upperDeviceDbId = ndmDevice.value.id; const upperDeviceDbId = ndmDevice.value.id;
if (!upperDeviceDbId) throw new Error('本设备没有ID'); if (!upperDeviceDbId) throw new Error('本设备没有ID');
if (circuitIndex.value === undefined) throw new Error('该空开不存在'); if (circuitIndex.value === undefined) throw new Error('该电路不存在');
if (!lowerDevice.value) throw new Error('请选择要关联的设备'); if (!lowerDevice.value) throw new Error('请选择要关联的设备');
const lowerDeviceType = tryGetDeviceType(lowerDevice.value?.deviceType); const lowerDeviceType = tryGetDeviceType(lowerDevice.value?.deviceType);
@@ -195,7 +195,7 @@ const onCancel = () => {
<template #header> <template #header>
<span>{{ ndmDevice.name }}</span> <span>{{ ndmDevice.name }}</span>
<span> - </span> <span> - </span>
<span>空开{{ circuitIndex ? circuitIndex + 1 : '-' }}</span> <span>电路{{ circuitIndex ? circuitIndex + 1 : '-' }}</span>
<span> - </span> <span> - </span>
<span>关联设备</span> <span>关联设备</span>
</template> </template>
@@ -31,7 +31,7 @@ const { ndmDevice, station } = toRefs(props);
const showDetailModal = ref(false); const showDetailModal = ref(false);
const detailTableColumns: DataTableColumns<SecurityBoxCircuitRowData> = [ const detailTableColumns: DataTableColumns<SecurityBoxCircuitRowData> = [
{ title: '空开序号', key: 'number' }, { title: '电路序号', key: 'number' },
{ {
title: '状态', title: '状态',
key: 'status', key: 'status',
@@ -98,7 +98,7 @@ const tableColumns: DataTableColumns<SecurityBoxRuntimeRowData> = [
}, },
// { title: '开关状态', key: 'switches' }, // { title: '开关状态', key: 'switches' },
{ {
title: '空开状态', title: '电路状态',
key: 'circuits', key: 'circuits',
render(rowData) { render(rowData) {
const { info } = rowData.diagInfo; const { info } = rowData.diagInfo;
@@ -1,9 +1,8 @@
import ServerAlive from './server-alive.vue'; import ServerAlive from './server-alive.vue';
import ServerCard from './server-card.vue'; import ServerCard from './server-card.vue';
import ServerCurrentDiag from './server-current-diag.vue'; import ServerCurrentDiag from './server-current-diag.vue';
import ServerHighAvailable from './server-high-available.vue';
import ServerHistoryDiag from './server-history-diag.vue'; import ServerHistoryDiag from './server-history-diag.vue';
import ServerStreamPush from './server-stream-push.vue'; import ServerStreamPush from './server-stream-push.vue';
import ServerUpdate from './server-update.vue'; import ServerUpdate from './server-update.vue';
export { ServerAlive, ServerCard, ServerCurrentDiag, ServerHighAvailable, ServerHistoryDiag, ServerUpdate, ServerStreamPush }; export { ServerAlive, ServerCard, ServerCurrentDiag, ServerHistoryDiag, ServerUpdate, ServerStreamPush };
@@ -23,11 +23,8 @@ const deviceType = computed(() => tryGetDeviceType(ndmDevice.value.deviceType));
const MEDIA_SERVER_ALIVE_QUERY_KEY = 'media-server-alive-query'; const MEDIA_SERVER_ALIVE_QUERY_KEY = 'media-server-alive-query';
const VIDEO_SERVER_ALIVE_QUERY_KEY = 'video-server-alive-query'; const VIDEO_SERVER_ALIVE_QUERY_KEY = 'video-server-alive-query';
const deviceUniqueKey = computed(() => [station.value.code, ndmDevice.value.id]);
const { data: isMediaServerAlive } = useQuery({ const { data: isMediaServerAlive } = useQuery({
queryKey: computed(() => [MEDIA_SERVER_ALIVE_QUERY_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]), queryKey: computed(() => [MEDIA_SERVER_ALIVE_QUERY_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmMediaServer), enabled: computed(() => activeRequests.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmMediaServer),
refetchInterval: 30 * 1000, refetchInterval: 30 * 1000,
gcTime: 0, gcTime: 0,
@@ -37,7 +34,7 @@ const { data: isMediaServerAlive } = useQuery({
}, },
}); });
const { data: isSipServerAlive } = useQuery({ const { data: isSipServerAlive } = useQuery({
queryKey: computed(() => [VIDEO_SERVER_ALIVE_QUERY_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]), queryKey: computed(() => [VIDEO_SERVER_ALIVE_QUERY_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmVideoServer), enabled: computed(() => activeRequests.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmVideoServer),
refetchInterval: 30 * 1000, refetchInterval: 30 * 1000,
gcTime: 0, gcTime: 0,
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { type NdmServerDiagInfo, type NdmServerResultVO, type Station } from '@/apis'; import { type NdmServerDiagInfo, type NdmServerResultVO, type Station } from '@/apis';
import { DeviceHardwareCard, DeviceHeaderCard, ServerAlive, ServerHighAvailable, ServerStreamPush } from '@/components'; import { DeviceHardwareCard, DeviceHeaderCard, ServerAlive, ServerStreamPush } from '@/components';
import destr from 'destr'; import destr from 'destr';
import { NFlex } from 'naive-ui'; import { NFlex } from 'naive-ui';
import { computed, toRefs } from 'vue'; import { computed, toRefs } from 'vue';
@@ -27,7 +27,6 @@ const runningTime = computed(() => lastDiagInfo.value?.commInfo?.系统运行时
<template> <template>
<NFlex vertical> <NFlex vertical>
<ServerHighAvailable :ndm-device="ndmDevice" :station="station" />
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" /> <DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
<DeviceHardwareCard running-time-label="服务器运行时间" :cpu-usage="cpuUsage" :mem-usage="memUsage" :disk-usage="diskUsage" :running-time="runningTime" /> <DeviceHardwareCard running-time-label="服务器运行时间" :cpu-usage="cpuUsage" :mem-usage="memUsage" :disk-usage="diskUsage" :running-time="runningTime" />
<ServerAlive :ndm-device="ndmDevice" :station="station" /> <ServerAlive :ndm-device="ndmDevice" :station="station" />
@@ -1,67 +0,0 @@
<script setup lang="ts">
import { getHighAvailableApi, type NdmServerResultVO, type Station } from '@/apis';
import { DEVICE_TYPE_LITERALS, tryGetDeviceType } from '@/enums';
import { useSettingStore } from '@/stores';
import { useQuery, useQueryClient } from '@tanstack/vue-query';
import { NAlert, NCard, NFlex } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmServerResultVO;
station: Station;
}>();
const settingStore = useSettingStore();
const { activeRequests } = storeToRefs(settingStore);
const queryClient = useQueryClient();
const { ndmDevice, station } = toRefs(props);
const deviceType = computed(() => tryGetDeviceType(ndmDevice.value.deviceType));
const isVideoServer = computed(() => deviceType.value === DEVICE_TYPE_LITERALS.ndmVideoServer);
const SERVER_HIGH_AVAILABLE_QUERY_KEY = 'server-high-available-query';
const deviceUniqueKey = computed(() => [station.value.code, ndmDevice.value.id]);
const { data: highAvailable } = useQuery({
queryKey: computed(() => [SERVER_HIGH_AVAILABLE_QUERY_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && isVideoServer.value),
refetchInterval: 30 * 1000,
gcTime: 0,
queryFn: async ({ signal }) => {
const highAvailable = await getHighAvailableApi({ stationCode: station.value.code, signal });
return highAvailable;
},
});
watch(activeRequests, (active) => {
if (!active) {
queryClient.cancelQueries({ queryKey: [SERVER_HIGH_AVAILABLE_QUERY_KEY] });
}
});
const showCard = computed(() => {
const { pyip: physicalIp } = highAvailable.value ?? {};
const ipAddressMatched = physicalIp === ndmDevice.value.ipAddress;
return isVideoServer.value && ipAddressMatched;
});
</script>
<template>
<NAlert v-if="showCard && !!highAvailable" :bordered="false" type="success">
<template #header>
<NFlex :align="'center'">
<div>正在提供服务</div>
<NFlex :align="'center'" style="font-size: smaller">
<div>虚拟IP: {{ highAvailable.vip }}</div>
<div>启用时间: {{ highAvailable.changeDate }}</div>
</NFlex>
</NFlex>
</template>
</NAlert>
</template>
<style scoped></style>
@@ -25,10 +25,8 @@ const showCard = computed(() => deviceType.value === DEVICE_TYPE_LITERALS.ndmMed
const SERVER_STREAM_PUSH_KEY = 'server-stream-push-query'; const SERVER_STREAM_PUSH_KEY = 'server-stream-push-query';
const deviceUniqueKey = computed(() => [station.value.code, ndmDevice.value.id]);
const { data: streamPushes } = useQuery({ const { data: streamPushes } = useQuery({
queryKey: computed(() => [SERVER_STREAM_PUSH_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]), queryKey: computed(() => [SERVER_STREAM_PUSH_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && showCard.value), enabled: computed(() => activeRequests.value && showCard.value),
refetchInterval: 30 * 1000, refetchInterval: 30 * 1000,
gcTime: 0, gcTime: 0,
@@ -1,9 +1,3 @@
<script lang="ts">
const createDeviceNodeKey = (stationCode?: Station['code'], device?: NdmDeviceResultVO) => {
return `${stationCode ?? ''}-${device?.id ?? ''}`;
};
</script>
<script setup lang="ts"> <script setup lang="ts">
import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis'; import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis';
import { useDeviceTree, usePermission, type UseDeviceTreeReturn } from '@/composables'; import { useDeviceTree, usePermission, type UseDeviceTreeReturn } from '@/composables';
@@ -17,19 +11,15 @@ import {
NButton, NButton,
NDropdown, NDropdown,
NFlex, NFlex,
NGrid,
NGridItem,
NInput, NInput,
NRadio, NRadio,
NRadioGroup, NRadioGroup,
NSelect,
NTab, NTab,
NTabs, NTabs,
NTag, NTag,
NTree, NTree,
useThemeVars, useThemeVars,
type DropdownOption, type DropdownOption,
type SelectOption,
type TagProps, type TagProps,
type TreeInst, type TreeInst,
type TreeOption, type TreeOption,
@@ -116,7 +106,7 @@ watchImmediate(selectedDeviceType, (newDeviceType) => {
} }
}); });
const selectedKeys = computed(() => (selectedDevice.value?.id ? [createDeviceNodeKey(selectedStationCode.value, selectedDevice.value)] : undefined)); const selectedKeys = computed(() => (selectedDevice.value?.id ? [selectedDevice.value.id] : undefined));
watch([selectedKeys, selectedDevice, selectedStationCode], ([, device, code]) => { watch([selectedKeys, selectedDevice, selectedStationCode], ([, device, code]) => {
if (device && code) { if (device && code) {
onSelectDevice(device, code); onSelectDevice(device, code);
@@ -322,26 +312,26 @@ const lineDeviceTreeData = computed<Record<Station['code'], TreeOption[]>>(() =>
key: stationCode, key: stationCode,
prefix: () => renderStationNodePrefix(station), prefix: () => renderStationNodePrefix(station),
suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0), suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0),
children: nvrClusters.map<TreeOption>((cluster) => { children: nvrClusters.map<TreeOption>((nvrCluster) => {
return { return {
label: `${cluster.name}`, label: `${nvrCluster.name}`,
key: createDeviceNodeKey(stationCode, cluster), key: nvrCluster.id ?? `${nvrCluster.name}`,
prefix: () => renderDeviceNodePrefix(cluster, stationCode), prefix: () => renderDeviceNodePrefix(nvrCluster, stationCode),
suffix: () => `${cluster.ipAddress}`, suffix: () => `${nvrCluster.ipAddress}`,
children: nvrSingletons.map<TreeOption>((device) => { children: nvrSingletons.map<TreeOption>((nvr) => {
return { return {
label: `${device.name}`, label: `${nvr.name}`,
key: createDeviceNodeKey(stationCode, device), key: nvr.id ?? `${nvr.name}`,
prefix: () => renderDeviceNodePrefix(device, stationCode), prefix: () => renderDeviceNodePrefix(nvr, stationCode),
suffix: () => `${device.ipAddress}`, suffix: () => `${nvr.ipAddress}`,
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站 // 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
stationCode, stationCode,
device: device, device: nvr,
}; };
}), }),
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站 // 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
stationCode, stationCode,
device: cluster, device: nvrCluster,
}; };
}), }),
stationCode, stationCode,
@@ -358,7 +348,7 @@ const lineDeviceTreeData = computed<Record<Station['code'], TreeOption[]>>(() =>
const device = dev as NdmDeviceResultVO; const device = dev as NdmDeviceResultVO;
return { return {
label: `${device.name}`, label: `${device.name}`,
key: createDeviceNodeKey(stationCode, device), key: `${device.id}`,
prefix: () => renderDeviceNodePrefix(device, stationCode), prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`, suffix: () => `${device.ipAddress}`,
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站 // 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
@@ -389,16 +379,16 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
label: `${DEVICE_TYPE_NAMES[deviceType]}`, label: `${DEVICE_TYPE_NAMES[deviceType]}`,
key: deviceType, key: deviceType,
suffix: () => renderIcmpStatistics(onlineCount, offlineCount, nvrs.length), suffix: () => renderIcmpStatistics(onlineCount, offlineCount, nvrs.length),
children: clusters.map<TreeOption>((cluster) => { children: clusters.map<TreeOption>((device) => {
return { return {
label: `${cluster.name}`, label: `${device.name}`,
key: createDeviceNodeKey(stationCode, cluster), key: `${device.id}`,
prefix: () => renderDeviceNodePrefix(cluster, stationCode), prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${cluster.ipAddress}`, suffix: () => `${device.ipAddress}`,
children: singletons.map<TreeOption>((device) => { children: singletons.map<TreeOption>((device) => {
return { return {
label: `${device.name}`, label: `${device.name}`,
key: createDeviceNodeKey(stationCode, device), key: `${device.id}`,
prefix: () => renderDeviceNodePrefix(device, stationCode), prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`, suffix: () => `${device.ipAddress}`,
stationCode, stationCode,
@@ -406,7 +396,7 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
}; };
}), }),
stationCode, stationCode,
device: cluster, device,
}; };
}), }),
stationCode, stationCode,
@@ -420,7 +410,7 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
children: stationDevices[deviceType].map<TreeOption>((device) => { children: stationDevices[deviceType].map<TreeOption>((device) => {
return { return {
label: `${device.name}`, label: `${device.name}`,
key: createDeviceNodeKey(stationCode, device), key: `${device.id}`,
prefix: () => renderDeviceNodePrefix(device, stationCode), prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`, suffix: () => `${device.ipAddress}`,
stationCode, stationCode,
@@ -435,26 +425,20 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
// ========== 设备树搜索 ========== // ========== 设备树搜索 ==========
const searchInput = ref(''); const searchInput = ref('');
const searchTypeOptions: SelectOption[] = [
{ label: '设备名称', value: 'name' },
{ label: 'IP地址', value: 'ipAddress' },
];
type SearchType = 'name' | 'ipAddress';
const typeInput = ref<SearchType>('name');
const statusInput = ref(''); const statusInput = ref('');
// 设备树将搜索框、选择器以及单选框的值都交给NTree的pattern属性 // 设备树将搜索框单选框的值都交给NTree的pattern属性
// 但是如果一个车站下没有匹配的设备,那么这个车站节点也不会显示 // 但是如果一个车站下没有匹配的设备,那么这个车站节点也不会显示
const searchPattern = computed(() => { const searchPattern = computed(() => {
const search = searchInput.value; const search = searchInput.value;
const status = statusInput.value; const status = statusInput.value;
if (!search && !status) return ''; // 如果pattern非空会导致NTree组件认为筛选完成,UI上发生全量匹配 if (!search && !status) return ''; // 如果pattern非空会导致NTree组件认为筛选完成,UI上发生全量匹配
return JSON.stringify({ search: searchInput.value, type: typeInput.value, status: statusInput.value }); return JSON.stringify({ search: searchInput.value, status: statusInput.value });
}); });
const searchFilter = (pattern: string, node: TreeOption): boolean => { const searchFilter = (pattern: string, node: TreeOption): boolean => {
const { search, type, status } = destr<{ search: string; type: SearchType; status: string }>(pattern); const { search, status } = destr<{ search: string; status: string }>(pattern);
const device = node['device'] as NdmDeviceResultVO | undefined; const device = node['device'] as NdmDeviceResultVO | undefined;
const { deviceStatus } = device ?? {}; const { name, ipAddress, deviceId, deviceStatus } = device ?? {};
const searchMatched = !!device?.[type]?.includes(search); const searchMatched = (name ?? '').includes(search) || (ipAddress ?? '').includes(search) || (deviceId ?? '').includes(search);
const statusMatched = status === '' || status === deviceStatus; const statusMatched = status === '' || status === deviceStatus;
return searchMatched && statusMatched; return searchMatched && statusMatched;
}; };
@@ -468,7 +452,6 @@ const onFoldDeviceTree = () => {
}; };
const onLocateDeviceTree = async () => { const onLocateDeviceTree = async () => {
if (!selectedStationCode.value) return; if (!selectedStationCode.value) return;
const stationCode = selectedStationCode.value;
if (!selectedDevice.value) return; if (!selectedDevice.value) return;
const deviceType = tryGetDeviceType(selectedDevice.value.deviceType); const deviceType = tryGetDeviceType(selectedDevice.value.deviceType);
if (!deviceType) return; if (!deviceType) return;
@@ -480,24 +463,24 @@ const onLocateDeviceTree = async () => {
activeTab.value = deviceType; activeTab.value = deviceType;
// 展开选择的车站 // 展开选择的车站
expandedKeys.value.push(stationCode); expandedKeys.value.push(selectedStationCode.value);
// 当选择录像机时,如果不是集群,进一步展开该录像机所在的集群节点 // 当选择录像机时,如果不是集群,进一步展开该录像机所在的集群节点
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) { if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
const stationDevices = lineDevices.value[stationCode]; const stationDevices = lineDevices.value[selectedStationCode.value];
if (stationDevices) { if (stationDevices) {
const selectedNvr = selectedDevice.value as NdmNvrResultVO; const selectedNvr = selectedDevice.value as NdmNvrResultVO;
if (!isNvrCluster(selectedNvr)) { if (!isNvrCluster(selectedNvr)) {
const nvrs = stationDevices[DEVICE_TYPE_LITERALS.ndmNvr]; const nvrs = stationDevices[DEVICE_TYPE_LITERALS.ndmNvr];
const clusters = nvrs.filter((nvr) => isNvrCluster(nvr) && nvr.clusterList?.includes(selectedNvr.clusterList ?? '')); const clusters = nvrs.filter((nvr) => isNvrCluster(nvr) && nvr.clusterList?.includes(selectedNvr.clusterList ?? ''));
expandedKeys.value.push(...clusters.map((nvr) => createDeviceNodeKey(stationCode, nvr))); expandedKeys.value.push(...clusters.map((nvr) => `${nvr.id}`));
} }
} }
} }
// 等待设备树展开完成,滚动到选择的设备 // 等待设备树展开完成,滚动到选择的设备
await nextTick(); await nextTick();
deviceTreeInst.value.scrollTo({ key: createDeviceNodeKey(stationCode, selectedDevice.value), behavior: 'smooth' }); deviceTreeInst.value.scrollTo({ key: `${selectedDevice.value.id}`, behavior: 'smooth' });
animated.value = true; animated.value = true;
}; };
@@ -540,14 +523,7 @@ onMounted(() => {
<div style="height: 100%; display: flex; flex-direction: column"> <div style="height: 100%; display: flex; flex-direction: column">
<!-- 搜索和筛选 --> <!-- 搜索和筛选 -->
<div style="padding: 12px; flex: 0 0 auto"> <div style="padding: 12px; flex: 0 0 auto">
<NGrid :cols="10" :x-gap="8"> <NInput v-model:value="searchInput" placeholder="搜索设备名称、设备ID或IP地址" clearable />
<NGridItem :span="7">
<NInput v-model:value="searchInput" placeholder="搜索设备名称或IP地址" clearable />
</NGridItem>
<NGridItem :span="3">
<NSelect v-model:value="typeInput" :options="searchTypeOptions" placeholder="搜索类型" />
</NGridItem>
</NGrid>
<NFlex align="center"> <NFlex align="center">
<NRadioGroup v-model:value="statusInput"> <NRadioGroup v-model:value="statusInput">
<NRadio value="">全部</NRadio> <NRadio value="">全部</NRadio>
-11
View File
@@ -136,17 +136,6 @@ export const unwrapResponse = <T>(resp: HttpResponse<T>) => {
return data; return data;
}; };
export const unwrapNullableResponse = <T>(resp: HttpResponse<T>) => {
const [err, data, result] = resp;
if (err) throw err;
if (result) {
const { isSuccess, path, msg, errorMsg } = result;
if (!isSuccess) throw new Error(`${path ? `${path}: ` : ''}${msg || errorMsg || '请求失败'}`);
}
if (data === undefined) throw new Error('响应数据不存在');
return data;
};
// 针对没有数据的响应,直接判断是否存在错误 // 针对没有数据的响应,直接判断是否存在错误
export const unwrapVoidResponse = (resp: HttpResponse<void>) => { export const unwrapVoidResponse = (resp: HttpResponse<void>) => {
const [err, , result] = resp; const [err, , result] = resp;
-43
View File
@@ -76,44 +76,6 @@ const line01ApiProxyList: ProxyItem[] = [
{ key: '/0128/api', target: 'http://10.14.55.10:18760', rewrite: ['/0128/api', '/api'] }, { key: '/0128/api', target: 'http://10.14.55.10:18760', rewrite: ['/0128/api', '/api'] },
]; ];
const line02ApiProxyList: ProxyItem[] = [
{ key: '/0275/api', target: 'http://10.14.128.10:18760', rewrite: ['/0275/api', '/api'] },
{ key: '/0202/api', target: 'http://10.14.129.10:18760', rewrite: ['/0202/api', '/api'] },
{ key: '/0203/api', target: 'http://10.14.131.10:18760', rewrite: ['/0203/api', '/api'] },
{ key: '/0204/api', target: 'http://10.14.133.10:18760', rewrite: ['/0204/api', '/api'] },
{ key: '/0205/api', target: 'http://10.14.135.10:18760', rewrite: ['/0205/api', '/api'] },
{ key: '/0206/api', target: 'http://10.14.137.10:18760', rewrite: ['/0206/api', '/api'] },
{ key: '/0207/api', target: 'http://10.14.139.10:18760', rewrite: ['/0207/api', '/api'] },
{ key: '/0208/api', target: 'http://10.14.141.10:18760', rewrite: ['/0208/api', '/api'] },
{ key: '/0209/api', target: 'http://10.14.143.10:18760', rewrite: ['/0209/api', '/api'] },
{ key: '/0210/api', target: 'http://10.14.145.10:18760', rewrite: ['/0210/api', '/api'] },
{ key: '/0211/api', target: 'http://10.14.147.10:18760', rewrite: ['/0211/api', '/api'] },
{ key: '/0212/api', target: 'http://10.14.149.10:18760', rewrite: ['/0212/api', '/api'] },
{ key: '/0213/api', target: 'http://10.14.151.10:18760', rewrite: ['/0213/api', '/api'] },
{ key: '/0214/api', target: 'http://10.14.153.10:18760', rewrite: ['/0214/api', '/api'] },
{ key: '/0215/api', target: 'http://10.14.155.10:18760', rewrite: ['/0215/api', '/api'] },
{ key: '/0216/api', target: 'http://10.14.157.10:18760', rewrite: ['/0216/api', '/api'] },
{ key: '/0217/api', target: 'http://10.14.159.10:18760', rewrite: ['/0217/api', '/api'] },
{ key: '/0224/api', target: 'http://10.14.161.10:18760', rewrite: ['/0224/api', '/api'] },
{ key: '/0225/api', target: 'http://10.14.163.10:18760', rewrite: ['/0225/api', '/api'] },
{ key: '/0226/api', target: 'http://10.14.165.10:18760', rewrite: ['/0226/api', '/api'] },
{ key: '/0227/api', target: 'http://10.14.167.10:18760', rewrite: ['/0227/api', '/api'] },
{ key: '/0228/api', target: 'http://10.14.169.10:18760', rewrite: ['/0228/api', '/api'] },
{ key: '/0229/api', target: 'http://10.14.171.10:18760', rewrite: ['/0229/api', '/api'] },
{ key: '/0230/api', target: 'http://10.14.173.10:18760', rewrite: ['/0230/api', '/api'] },
{ key: '/0231/api', target: 'http://10.14.175.10:18760', rewrite: ['/0231/api', '/api'] },
{ key: '/0232/api', target: 'http://10.14.177.10:18760', rewrite: ['/0232/api', '/api'] },
{ key: '/0233/api', target: 'http://10.14.179.10:18760', rewrite: ['/0233/api', '/api'] },
{ key: '/0234/api', target: 'http://10.14.181.10:18760', rewrite: ['/0234/api', '/api'] },
{ key: '/0235/api', target: 'http://10.14.183.10:18760', rewrite: ['/0235/api', '/api'] },
{ key: '/0236/api', target: 'http://10.14.185.10:18760', rewrite: ['/0236/api', '/api'] },
{ key: '/0237/api', target: 'http://10.14.187.10:18760', rewrite: ['/0237/api', '/api'] },
{ key: '/0238/api', target: 'http://10.14.191.10:18760', rewrite: ['/0238/api', '/api'] },
{ key: '/0280/api', target: 'http://10.14.244.10:18760', rewrite: ['/0280/api', '/api'] },
{ key: '/0281/api', target: 'http://10.14.248.10:18760', rewrite: ['/0281/api', '/api'] },
{ key: '/0282/api', target: 'http://10.14.252.10:18760', rewrite: ['/0282/api', '/api'] },
];
const line04ApiProxyList: ProxyItem[] = [ const line04ApiProxyList: ProxyItem[] = [
{ key: '/0475/api', target: 'http://10.15.128.10:18760', rewrite: ['/0475/api', '/api'] }, { key: '/0475/api', target: 'http://10.15.128.10:18760', rewrite: ['/0475/api', '/api'] },
{ key: '/0480/api', target: 'http://10.15.244.10:18760', rewrite: ['/0480/api', '/api'] }, { key: '/0480/api', target: 'http://10.15.244.10:18760', rewrite: ['/0480/api', '/api'] },
@@ -181,11 +143,6 @@ const apiProxyList: ProxyItem[] = [
// { key: '/ws', target: 'ws://10.14.0.10:18103', ws: true }, // { key: '/ws', target: 'ws://10.14.0.10:18103', ws: true },
...line01ApiProxyList, ...line01ApiProxyList,
// { key: '/minio', target: 'http://10.14.128.10:9000', rewrite: ['/minio', ''] },
// { key: '/api', target: 'http://10.14.128.10:18760' },
// { key: '/ws', target: 'ws://10.14.128.10:18103', ws: true },
...line02ApiProxyList,
// { key: '/minio', target: 'http://10.15.128.10:9000', rewrite: ['/minio', ''] }, // { key: '/minio', target: 'http://10.15.128.10:9000', rewrite: ['/minio', ''] },
// { key: '/api', target: 'http://10.15.128.10:18760' }, // { key: '/api', target: 'http://10.15.128.10:18760' },
// { key: '/ws', target: 'ws://10.15.128.10:18103', ws: true }, // { key: '/ws', target: 'ws://10.15.128.10:18103', ws: true },