12 Commits

Author SHA1 Message Date
yangsy 20513557d7 release: v0.40.0 2026-04-10 15:58:29 +08:00
yangsy de241334a9 chore: 版本信息和更新日志 2026-04-10 15:43:12 +08:00
yangsy c75338cb70 feat: 添加视频服务器双机热备状态 2026-04-10 15:11:58 +08:00
yangsy 86e3e1726d fix: 修复设备查询缓存键冲突问题
- 将查询键中的设备ID改为由车站编码和设备ID组成的唯一键,防止不同车站下相同设备ID导致的数据混淆。
- 同时更新相关监控逻辑,当车站或设备IP变化时重置页面状态。
2026-04-10 10:15:32 +08:00
yangsy 943aa27de1 fix: 将“电路”统一更正为“空开” 2026-04-02 10:38:48 +08:00
yangsy b4442fe6c4 fix: 修复设备树节点key值生成逻辑,最大限度避免key值碰撞导致设备树渲染异常 2026-03-30 20:30:26 +08:00
yangsy f9f761b4e9 chore: 版本信息和更新日志 2026-03-11 15:20:03 +08:00
yangsy 4090c7e6c5 fix: 使用设备名称和IP地址组合作为设备树中设备节点的key 2026-03-11 15:17:19 +08:00
yangsy 38b43b1c45 feat: 改进设备树搜索功能,增加搜索类型选择 2026-03-11 15:17:19 +08:00
yangsy 9eafc7871b chore: 添加2号线API代理配置 2026-03-11 15:17:19 +08:00
yangsy 3eb5b06f59 fix: 录像缺失信息渲染条件 2026-03-11 15:17:19 +08:00
yangsy f2fc2e732d docs: 更新README.md 2026-03-11 15:17:19 +08:00
19 changed files with 265 additions and 60 deletions
+5 -2
View File
@@ -65,8 +65,11 @@ src/
vimp-log-page.vue # 视频平台日志页面
permission/
permission-page.vue # 权限管理页面
error/
not-found-page.vue # 404 页面
system/
changelog/
changelog-page.vue # 更新记录页面
error/
not-found-page.vue # 404 页面
```
## 数据轮询
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ndm-web-platform",
"version": "0.0.0",
"version": "0.40.0",
"private": true,
"type": "module",
"engines": {
+16
View File
@@ -1,4 +1,20 @@
[
{
"version": "0.40.0",
"date": "2026-04-10",
"changes": {
"fixes": [{ "content": "修复设备树搜索时节点错乱的问题" }, { "content": "将安防箱面板的“电路”统一更正为“空开”" }, { "content": "修复设备查询缓存键冲突问题" }],
"feats": [{ "content": "添加视频服务器双机热备状态" }]
}
},
{
"version": "0.39.0",
"date": "2026-03-02",
"changes": {
"fixes": [{ "content": "修复设备树搜索时节点错乱的问题" }],
"feats": [{ "content": "新版录像记录诊断卡片" }, { "content": "新增平台更新记录页面" }]
}
},
{
"version": "0.38.5",
"date": "2026-02-06",
+2 -2
View File
@@ -1,4 +1,4 @@
{
"version": "0.38.5",
"buildTime": "2026-02-06 10:25:07"
"version": "0.40.0",
"buildTime": "2026-04-10 15:42:03"
}
@@ -0,0 +1,5 @@
export interface HighAvailable {
pyip: string;
vip: string;
changeDate: string;
}
+1
View File
@@ -1,3 +1,4 @@
export * from './diag';
export * from './high-available';
export * from './link-description';
export * from './station';
@@ -1,5 +1,26 @@
import { ndmClient, userClient, type MediaServerStatus, type SendRtpInfo, type Station } from '@/apis';
import { unwrapResponse } from '@/utils';
import { ndmClient, userClient, type HighAvailable, type MediaServerStatus, type SendRtpInfo, type Station } from '@/apis';
import { unwrapNullableResponse, 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 }) => {
const { stationCode, signal } = options ?? {};
@@ -92,6 +92,8 @@ const abortController = ref<AbortController>(new AbortController());
const NVR_RECORD_CHECK_KEY = 'nvr-record-check-query';
const deviceUniqueKey = computed(() => [station.value.code, ndmDevice.value.id]);
const DAY_OFFSET = 90;
const {
@@ -99,7 +101,7 @@ const {
isFetching: loading,
refetch: refetchRecordChecks,
} = useQuery({
queryKey: computed(() => [NVR_RECORD_CHECK_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
queryKey: computed(() => [NVR_RECORD_CHECK_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value),
refetchInterval: 30 * 1000,
gcTime: 0,
@@ -408,13 +410,13 @@ const ndmRecordChecksPaged = computed(() => {
return ndmRecordChecksFiltered.value.slice(startIndex, endIndex);
});
// 当设备ID、最后诊断时间或筛选类型变化时,重置分页为第一页
watch([() => ndmDevice.value.id, () => ndmDevice.value.lastDiagTime, filterType, searchInputDebounced], () => {
// 当车站号、设备IP、最后诊断时间或筛选类型变化时,重置分页为第一页
watch([() => station.value.code, () => ndmDevice.value.ipAddress, () => ndmDevice.value.lastDiagTime, filterType, searchInputDebounced], () => {
page.value = 1;
});
// 当设备ID变化时,重置搜索内容,并将筛选类型重置为「全部」
watch([() => ndmDevice.value.id], () => {
// 当车站号、设备IP变化时,重置搜索内容,并将筛选类型重置为「全部」
watch([() => station.value.code, () => ndmDevice.value.ipAddress], () => {
searchInput.value = '';
filterType.value = 'all';
});
@@ -626,9 +628,14 @@ const columns: DataTableColumns<DailyLossItem['chunks'][number]> = [
<template #default>
<template v-if="!!dailyCheckContext.info">
<div>日期:{{ dailyCheckContext.info.date }}</div>
<div>缺失时长:{{ formatDuration(dailyCheckContext.info.total, { withinDay: true }) }}</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>
<template v-if="dailyCheckContext.info.percent > 0">
<div>缺失时长{{ formatDuration(dailyCheckContext.info.total, { withinDay: true }) }}</div>
<div>缺失比例:{{ dailyCheckContext.info.percent.toFixed(2) }}%</div>
<div style="font-size: xx-small; opacity: 0.5; cursor: pointer" @click="onClickDailyCheck">点击查看详情</div>
</template>
<template v-else>
<div>录像完整</div>
</template>
</template>
</template>
</NPopover>
@@ -202,7 +202,7 @@ const contextmenuOptions = computed<DropdownOption[]>(() => [
if (!lowerDevice) return;
window.$dialog.warning({
title: '确认解除关联吗?',
content: `将解除【电路${circuitIndex + 1}】与【${lowerDevice.name}】的关联关系。`,
content: `将解除【空开${circuitIndex + 1}】与【${lowerDevice.name}】的关联关系。`,
style: { width: '600px' },
contentStyle: { height: '60px' },
negativeText: '取消',
@@ -299,7 +299,7 @@ const { mutate: unlinkDevice } = useMutation({
<NCard v-if="showCard" hoverable size="small">
<template #header>
<NFlex align="center">
<span>电路状态</span>
<span>空开状态</span>
<NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => reboot()">
<template #trigger>
<NButton secondary size="small" :loading="rebooting">重合闸</NButton>
@@ -324,7 +324,7 @@ const { mutate: unlinkDevice } = useMutation({
<span>{{ getCircuitStatusText(circuit) }}</span>
</template>
</NTag>
<span>电路{{ circuitIndex + 1 }}</span>
<span>空开{{ circuitIndex + 1 }}</span>
</NFlex>
<NFlex justify="end" align="center">
<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" />
</template>
<template #default>
<span>确定要{{ circuit.status === 1 ? '关闭' : '开启' }}电路{{ circuitIndex + 1 }}吗?</span>
<span>确定要{{ circuit.status === 1 ? '关闭' : '开启' }}空开{{ circuitIndex + 1 }}吗?</span>
</template>
</NPopconfirm>
</NFlex>
@@ -60,7 +60,7 @@ const { mutate: linkPortToDevice, isPending: linking } = useMutation({
const upperDeviceDbId = ndmDevice.value.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('请选择要关联的设备');
const lowerDeviceType = tryGetDeviceType(lowerDevice.value?.deviceType);
@@ -195,7 +195,7 @@ const onCancel = () => {
<template #header>
<span>{{ ndmDevice.name }}</span>
<span> - </span>
<span>电路{{ circuitIndex ? circuitIndex + 1 : '-' }}</span>
<span>空开{{ circuitIndex ? circuitIndex + 1 : '-' }}</span>
<span> - </span>
<span>关联设备</span>
</template>
@@ -31,7 +31,7 @@ const { ndmDevice, station } = toRefs(props);
const showDetailModal = ref(false);
const detailTableColumns: DataTableColumns<SecurityBoxCircuitRowData> = [
{ title: '电路序号', key: 'number' },
{ title: '空开序号', key: 'number' },
{
title: '状态',
key: 'status',
@@ -98,7 +98,7 @@ const tableColumns: DataTableColumns<SecurityBoxRuntimeRowData> = [
},
// { title: '开关状态', key: 'switches' },
{
title: '电路状态',
title: '空开状态',
key: 'circuits',
render(rowData) {
const { info } = rowData.diagInfo;
@@ -1,8 +1,9 @@
import ServerAlive from './server-alive.vue';
import ServerCard from './server-card.vue';
import ServerCurrentDiag from './server-current-diag.vue';
import ServerHighAvailable from './server-high-available.vue';
import ServerHistoryDiag from './server-history-diag.vue';
import ServerStreamPush from './server-stream-push.vue';
import ServerUpdate from './server-update.vue';
export { ServerAlive, ServerCard, ServerCurrentDiag, ServerHistoryDiag, ServerUpdate, ServerStreamPush };
export { ServerAlive, ServerCard, ServerCurrentDiag, ServerHighAvailable, ServerHistoryDiag, ServerUpdate, ServerStreamPush };
@@ -23,8 +23,11 @@ const deviceType = computed(() => tryGetDeviceType(ndmDevice.value.deviceType));
const MEDIA_SERVER_ALIVE_QUERY_KEY = 'media-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({
queryKey: computed(() => [MEDIA_SERVER_ALIVE_QUERY_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
queryKey: computed(() => [MEDIA_SERVER_ALIVE_QUERY_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmMediaServer),
refetchInterval: 30 * 1000,
gcTime: 0,
@@ -34,7 +37,7 @@ const { data: isMediaServerAlive } = useQuery({
},
});
const { data: isSipServerAlive } = useQuery({
queryKey: computed(() => [VIDEO_SERVER_ALIVE_QUERY_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
queryKey: computed(() => [VIDEO_SERVER_ALIVE_QUERY_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmVideoServer),
refetchInterval: 30 * 1000,
gcTime: 0,
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { type NdmServerDiagInfo, type NdmServerResultVO, type Station } from '@/apis';
import { DeviceHardwareCard, DeviceHeaderCard, ServerAlive, ServerStreamPush } from '@/components';
import { DeviceHardwareCard, DeviceHeaderCard, ServerAlive, ServerHighAvailable, ServerStreamPush } from '@/components';
import destr from 'destr';
import { NFlex } from 'naive-ui';
import { computed, toRefs } from 'vue';
@@ -27,6 +27,7 @@ const runningTime = computed(() => lastDiagInfo.value?.commInfo?.系统运行时
<template>
<NFlex vertical>
<ServerHighAvailable :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" />
<ServerAlive :ndm-device="ndmDevice" :station="station" />
@@ -0,0 +1,67 @@
<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,8 +25,10 @@ const showCard = computed(() => deviceType.value === DEVICE_TYPE_LITERALS.ndmMed
const SERVER_STREAM_PUSH_KEY = 'server-stream-push-query';
const deviceUniqueKey = computed(() => [station.value.code, ndmDevice.value.id]);
const { data: streamPushes } = useQuery({
queryKey: computed(() => [SERVER_STREAM_PUSH_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
queryKey: computed(() => [SERVER_STREAM_PUSH_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && showCard.value),
refetchInterval: 30 * 1000,
gcTime: 0,
@@ -1,3 +1,9 @@
<script lang="ts">
const createDeviceNodeKey = (stationCode?: Station['code'], device?: NdmDeviceResultVO) => {
return `${stationCode ?? ''}-${device?.id ?? ''}`;
};
</script>
<script setup lang="ts">
import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis';
import { useDeviceTree, usePermission, type UseDeviceTreeReturn } from '@/composables';
@@ -11,15 +17,19 @@ import {
NButton,
NDropdown,
NFlex,
NGrid,
NGridItem,
NInput,
NRadio,
NRadioGroup,
NSelect,
NTab,
NTabs,
NTag,
NTree,
useThemeVars,
type DropdownOption,
type SelectOption,
type TagProps,
type TreeInst,
type TreeOption,
@@ -106,7 +116,7 @@ watchImmediate(selectedDeviceType, (newDeviceType) => {
}
});
const selectedKeys = computed(() => (selectedDevice.value?.id ? [selectedDevice.value.id] : undefined));
const selectedKeys = computed(() => (selectedDevice.value?.id ? [createDeviceNodeKey(selectedStationCode.value, selectedDevice.value)] : undefined));
watch([selectedKeys, selectedDevice, selectedStationCode], ([, device, code]) => {
if (device && code) {
onSelectDevice(device, code);
@@ -312,26 +322,26 @@ const lineDeviceTreeData = computed<Record<Station['code'], TreeOption[]>>(() =>
key: stationCode,
prefix: () => renderStationNodePrefix(station),
suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0),
children: nvrClusters.map<TreeOption>((nvrCluster) => {
children: nvrClusters.map<TreeOption>((cluster) => {
return {
label: `${nvrCluster.name}`,
key: nvrCluster.id ?? `${nvrCluster.name}`,
prefix: () => renderDeviceNodePrefix(nvrCluster, stationCode),
suffix: () => `${nvrCluster.ipAddress}`,
children: nvrSingletons.map<TreeOption>((nvr) => {
label: `${cluster.name}`,
key: createDeviceNodeKey(stationCode, cluster),
prefix: () => renderDeviceNodePrefix(cluster, stationCode),
suffix: () => `${cluster.ipAddress}`,
children: nvrSingletons.map<TreeOption>((device) => {
return {
label: `${nvr.name}`,
key: nvr.id ?? `${nvr.name}`,
prefix: () => renderDeviceNodePrefix(nvr, stationCode),
suffix: () => `${nvr.ipAddress}`,
label: `${device.name}`,
key: createDeviceNodeKey(stationCode, device),
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
stationCode,
device: nvr,
device: device,
};
}),
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
stationCode,
device: nvrCluster,
device: cluster,
};
}),
stationCode,
@@ -348,7 +358,7 @@ const lineDeviceTreeData = computed<Record<Station['code'], TreeOption[]>>(() =>
const device = dev as NdmDeviceResultVO;
return {
label: `${device.name}`,
key: `${device.id}`,
key: createDeviceNodeKey(stationCode, device),
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
@@ -379,16 +389,16 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
label: `${DEVICE_TYPE_NAMES[deviceType]}`,
key: deviceType,
suffix: () => renderIcmpStatistics(onlineCount, offlineCount, nvrs.length),
children: clusters.map<TreeOption>((device) => {
children: clusters.map<TreeOption>((cluster) => {
return {
label: `${device.name}`,
key: `${device.id}`,
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
label: `${cluster.name}`,
key: createDeviceNodeKey(stationCode, cluster),
prefix: () => renderDeviceNodePrefix(cluster, stationCode),
suffix: () => `${cluster.ipAddress}`,
children: singletons.map<TreeOption>((device) => {
return {
label: `${device.name}`,
key: `${device.id}`,
key: createDeviceNodeKey(stationCode, device),
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
stationCode,
@@ -396,7 +406,7 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
};
}),
stationCode,
device,
device: cluster,
};
}),
stationCode,
@@ -410,7 +420,7 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
children: stationDevices[deviceType].map<TreeOption>((device) => {
return {
label: `${device.name}`,
key: `${device.id}`,
key: createDeviceNodeKey(stationCode, device),
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
stationCode,
@@ -425,20 +435,26 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
// ========== 设备树搜索 ==========
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('');
// 设备树将搜索框单选框的值都交给NTree的pattern属性
// 设备树将搜索框、选择器以及单选框的值都交给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 });
return JSON.stringify({ search: searchInput.value, type: typeInput.value, status: statusInput.value });
});
const searchFilter = (pattern: string, node: TreeOption): boolean => {
const { search, status } = destr<{ search: string; status: string }>(pattern);
const { search, type, status } = destr<{ search: string; type: SearchType; 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 { deviceStatus } = device ?? {};
const searchMatched = !!device?.[type]?.includes(search);
const statusMatched = status === '' || status === deviceStatus;
return searchMatched && statusMatched;
};
@@ -452,6 +468,7 @@ const onFoldDeviceTree = () => {
};
const onLocateDeviceTree = async () => {
if (!selectedStationCode.value) return;
const stationCode = selectedStationCode.value;
if (!selectedDevice.value) return;
const deviceType = tryGetDeviceType(selectedDevice.value.deviceType);
if (!deviceType) return;
@@ -463,24 +480,24 @@ const onLocateDeviceTree = async () => {
activeTab.value = deviceType;
// 展开选择的车站
expandedKeys.value.push(selectedStationCode.value);
expandedKeys.value.push(stationCode);
// 当选择录像机时,如果不是集群,进一步展开该录像机所在的集群节点
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
const stationDevices = lineDevices.value[selectedStationCode.value];
const stationDevices = lineDevices.value[stationCode];
if (stationDevices) {
const selectedNvr = selectedDevice.value as NdmNvrResultVO;
if (!isNvrCluster(selectedNvr)) {
const nvrs = stationDevices[DEVICE_TYPE_LITERALS.ndmNvr];
const clusters = nvrs.filter((nvr) => isNvrCluster(nvr) && nvr.clusterList?.includes(selectedNvr.clusterList ?? ''));
expandedKeys.value.push(...clusters.map((nvr) => `${nvr.id}`));
expandedKeys.value.push(...clusters.map((nvr) => createDeviceNodeKey(stationCode, nvr)));
}
}
}
// 等待设备树展开完成,滚动到选择的设备
await nextTick();
deviceTreeInst.value.scrollTo({ key: `${selectedDevice.value.id}`, behavior: 'smooth' });
deviceTreeInst.value.scrollTo({ key: createDeviceNodeKey(stationCode, selectedDevice.value), behavior: 'smooth' });
animated.value = true;
};
@@ -523,7 +540,14 @@ onMounted(() => {
<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 />
<NGrid :cols="10" :x-gap="8">
<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">
<NRadioGroup v-model:value="statusInput">
<NRadio value="">全部</NRadio>
+11
View File
@@ -136,6 +136,17 @@ export const unwrapResponse = <T>(resp: HttpResponse<T>) => {
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>) => {
const [err, , result] = resp;
+43
View File
@@ -76,6 +76,44 @@ const line01ApiProxyList: ProxyItem[] = [
{ 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[] = [
{ 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'] },
@@ -143,6 +181,11 @@ const apiProxyList: ProxyItem[] = [
// { key: '/ws', target: 'ws://10.14.0.10:18103', ws: true },
...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: '/api', target: 'http://10.15.128.10:18760' },
// { key: '/ws', target: 'ws://10.15.128.10:18103', ws: true },