13 Commits

Author SHA1 Message Date
yangsy fa44554593 style(vimp-tree): 优化线路树组件的标签页样式与文本展示
修改了alarm-tree.vue和camera-tree.vue两个组件,优化NTabs的样式配置,自定义标签尺寸与文本格式,调整内部布局细节
2026-06-01 20:14:44 +08:00
yangsy cbeddebbc0 refactor(alarm-tree, camera-tree): 整合节点图标到标签并优化渲染逻辑
为树形节点标签添加弹性布局以正确对齐图标与文字,将图标渲染逻辑从单独的前缀函数整合到标签渲染函数中,为摄像机树形组件新增按类型匹配对应图标的逻辑,并移除了冗余的前缀渲染属性与函数
2026-06-01 03:19:44 +08:00
yangsy c78c8b8419 refactor(vimp-alarm): 重构告警树数据构建逻辑并优化节点显示
- 统一区域数据处理流程,使用compiledCodeAreas替代原有的四个独立区域数组
- 使用Map索引替代线性查找,提升数据构建性能
- 将节点的后缀/前缀渲染从节点属性迁移至Tree组件的renderPrefix和renderSuffix方法
- 为区域节点添加areaLevel字段以适配不同层级的样式
- 为告警节点添加警笛图标前缀,为各层级节点添加统计信息后缀
2026-06-01 03:13:38 +08:00
yangsy 148be10186 修复(vimp设备中心): 按编码排序站点与通道以稳定UI显示顺序
- 新增通用排序工具函数,对站点和通道数组按code字段排序,确保线路面板、站点节点和通道节点的显示顺序稳定一致,避免UI因数据源顺序波动出现跳动。
2026-06-01 02:54:49 +08:00
yangsy 650ca78464 style(use-device-center-query): 移除未使用的类型导入 2026-06-01 02:24:30 +08:00
yangsy 64449a22c5 perf(vimp): 预编译码表区域索引优化查询性能
- 新增compileCodeAreas工具函数,将区域数据预构建为Map索引,降低区域查找的时间复杂度
- 重构camera store,移除冗余的区域遍历查找逻辑,改用预编译索引数据
新增节点areaLevel字段,适配不同层级节点的统计后缀样式间距
- 将摄像头图标和节点统计后缀的渲染逻辑迁移至camera-tree组件
- 清理调试控制台日志,简化空通道判断逻辑
2026-06-01 02:22:55 +08:00
yangsy 0c7f3153ce refactor(vimp): 重命名映射变量并优化摄像头商店逻辑
1. 统一重命名站点告警、摄像头的映射变量为更具可读性的命名格式
2. 重构摄像头商店的buildLineTabPanes方法,提取重复逻辑为独立工具函数
3. 使用Map和Set优化节点索引与查找流程,避免重复添加摄像头节点
4. 统一统计后缀的渲染逻辑,简化代码结构
2026-05-31 17:08:30 +08:00
yangsy b9c4a3ca27 perf(vimp): 优化存储响应式并重构设备查询逻辑
将 camera 和 alarm 存储的 lineTabPanes 从 ref 替换为 shallowRef,减少大型数组的响应式开销。重构设备查询组合式函数,拆分相机和告警的站点与通道数据,添加调试控制台日志。
2026-05-30 23:27:22 +08:00
yangsy 12c431b0ec refactor(vimp): 修复重复站点问题并重构告警逻辑
- 新增站点去重逻辑避免重复条目
- 更新告警面板构建函数的参数与内部实现
- 修正摄像机和告警区域的注释表述
2026-05-30 22:17:11 +08:00
yangsy 176e35609f fix(device-center-query): 简化并修正设备站点与通道的处理逻辑
原先的逻辑先按API返回的站点分组通道,再通过通道编码重建站点列表,存在冗余且可能出错。现在改为单次循环直接基于通道编码生成正确的站点,并按站点分组相机和告警通道,同时完善了相关注释说明。
2026-05-30 22:02:52 +08:00
yangsy 7cd59956b1 style(use-device-center-query): 调整代码空行优化可读性 2026-05-30 16:56:11 +08:00
yangsy df1c7deead refactor(vimp): 重构摄像头站点处理逻辑,修正站点匹配问题
将站点聚合逻辑从摄像头store移至设备中心查询模块,基于通道编码生成正确的站点列表,解决接口返回站点编码不匹配的问题
简化buildLineTabPanes函数的参数和内部处理流程
移除未使用的@vueuse/core的objectEntries导入
2026-05-30 13:48:20 +08:00
yangsy 3e88379eb9 refactor(vimp): 将普通对象替换为Map并优化代码逻辑 (不包含警报器树)
- 将站点摄像机和告警的映射存储从普通对象改为Map,提升查询性能
- 新增站点在线状态缓存和已访问站点集合,简化重复站点判断逻辑
- 移动buildTrainAreas和axios配置对象至顶层作用域
- 调整代码结构并临时注释告警存储的线路面板构建调用
2026-05-30 09:42:14 +08:00
6 changed files with 446 additions and 349 deletions
+58 -18
View File
@@ -1,10 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type TreeProps } from 'naive-ui'; import { NIcon, NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type TreeProps } from 'naive-ui';
import { h, type CSSProperties } from 'vue'; import { h, type CSSProperties } from 'vue';
import { useAlarmStore, useResourcePanelStore } from '../stores'; import { useAlarmStore, useResourcePanelStore } from '../stores';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useDeviceCenterQuery } from '../composables'; import { useDeviceCenterQuery } from '../composables';
import { isAlarmNode, isAlarmSiteNode, isAlarmAreaNode } from '../types'; import { isAlarmNode, isAlarmSiteNode, isAlarmAreaNode } from '../types';
import { SirenIcon } from 'lucide-vue-next';
const { isLoading } = useDeviceCenterQuery(); const { isLoading } = useDeviceCenterQuery();
@@ -51,6 +52,9 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
const alarmNodeStyle: CSSProperties = { const alarmNodeStyle: CSSProperties = {
opacity: alarmOnline() ? 1 : 0.5, opacity: alarmOnline() ? 1 : 0.5,
cursor: alarmOnline() ? 'pointer' : 'not-allowed', cursor: alarmOnline() ? 'pointer' : 'not-allowed',
display: 'flex',
alignItems: 'center',
gap: '6px',
}; };
return h( return h(
'div', 'div',
@@ -69,7 +73,7 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
event.dataTransfer?.setData('name', alarm.name); event.dataTransfer?.setData('name', alarm.name);
}, },
}, },
alarm.name, [h(NIcon, () => h(SirenIcon)), h('span', alarm.name)],
); );
} }
@@ -77,6 +81,21 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
return option.label; return option.label;
}; };
const renderNodeSuffix: TreeProps['renderSuffix'] = ({ option }) => {
if (isAlarmSiteNode(option)) {
const { online, offline, total } = option.stats;
return `(${online}/${offline}/${total})`;
}
if (isAlarmAreaNode(option)) {
const { online, offline, total } = option.stats;
const suffixStyle: CSSProperties = option.areaLevel === 1 ? { marginRight: '8px', opacity: 0.6 } : { marginRight: '16px', opacity: 0.4 };
return h('div', { style: suffixStyle }, `(${online}/${offline}/${total})`);
}
return null;
};
const resourcePanelStore = useResourcePanelStore(); const resourcePanelStore = useResourcePanelStore();
const { searchPattern } = storeToRefs(resourcePanelStore); const { searchPattern } = storeToRefs(resourcePanelStore);
@@ -98,6 +117,7 @@ const searchFilter: TreeProps['filter'] = (pattern, node) => {
virtual-scroll virtual-scroll
style="height: 100%" style="height: 100%"
:render-label="renderNodeLabel" :render-label="renderNodeLabel"
:render-suffix="renderNodeSuffix"
:override-default-node-click-behavior="overrideNodeClickBehavior" :override-default-node-click-behavior="overrideNodeClickBehavior"
:default-expand-all="searchPattern.trim().length > 0" :default-expand-all="searchPattern.trim().length > 0"
:show-irrelevant-nodes="false" :show-irrelevant-nodes="false"
@@ -107,22 +127,42 @@ const searchFilter: TreeProps['filter'] = (pattern, node) => {
/> />
</template> </template>
<template v-if="lineTabPanes.length > 1"> <template v-if="lineTabPanes.length > 1">
<NTabs :type="'card'" :placement="'left'" style="height: 100%"> <NTabs
<NTabPane v-for="{ lineCode, lineName, alarmTree } in lineTabPanes" :key="lineCode" :name="lineName" :tab="lineName"> :type="'card'"
<NTree :placement="'left'"
block-line style="height: 100%"
block-node :tab-style="{
show-line width: '64px',
virtual-scroll height: '36px',
style="height: 100%" }"
:render-label="renderNodeLabel" :style="{
:override-default-node-click-behavior="overrideNodeClickBehavior" '--n-bar-color': '#0000',
:default-expand-all="false" '--n-pane-padding-top': '0',
:show-irrelevant-nodes="false" '--n-tab-gap-vertical': '0',
:data="alarmTree" // '--n-tab-padding-vertical': '14px 12px'
:pattern="searchPattern" }"
:filter="searchFilter" >
/> <NTabPane v-for="{ lineCode, lineName, alarmTree } in lineTabPanes" :key="lineCode" :name="lineName">
<template #tab>
<span style="font-size: 12px">{{ lineName }}</span>
</template>
<template #default>
<NTree
block-line
block-node
show-line
virtual-scroll
style="height: 100%"
:render-label="renderNodeLabel"
:render-suffix="renderNodeSuffix"
:override-default-node-click-behavior="overrideNodeClickBehavior"
:default-expand-all="false"
:show-irrelevant-nodes="false"
:data="alarmTree"
:pattern="searchPattern"
:filter="searchFilter"
/>
</template>
</NTabPane> </NTabPane>
</NTabs> </NTabs>
</template> </template>
+61 -18
View File
@@ -1,10 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type TreeProps } from 'naive-ui'; import { NIcon, NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type TreeProps } from 'naive-ui';
import { h, type CSSProperties } from 'vue'; import { h, type CSSProperties } from 'vue';
import { useCameraStore, useResourcePanelStore } from '../stores'; import { useCameraStore, useResourcePanelStore } from '../stores';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useDeviceCenterQuery } from '../composables'; import { useDeviceCenterQuery } from '../composables';
import { isCameraNode, isCameraSiteNode, isCameraAreaNode } from '../types'; import { isCameraNode, isCameraSiteNode, isCameraAreaNode } from '../types';
import PtzCamera from './icon/ptz-camera.vue';
import HemiPtzCamera from './icon/hemi-ptz-camera.vue';
import BulletCamera from './icon/bullet-camera.vue';
const { isLoading } = useDeviceCenterQuery(); const { isLoading } = useDeviceCenterQuery();
@@ -51,7 +54,11 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
const cameraNodeStyle: CSSProperties = { const cameraNodeStyle: CSSProperties = {
opacity: cameraOnline() ? 1 : 0.5, opacity: cameraOnline() ? 1 : 0.5,
cursor: cameraOnline() ? 'pointer' : 'not-allowed', cursor: cameraOnline() ? 'pointer' : 'not-allowed',
display: 'flex',
alignItems: 'center',
gap: '6px',
}; };
const cameraIcon = option.type === '004' ? h(NIcon, () => h(PtzCamera)) : option.type === '005' ? h(NIcon, () => h(HemiPtzCamera)) : option.type === '006' ? h(NIcon, () => h(BulletCamera)) : null;
return h( return h(
'div', 'div',
{ {
@@ -69,7 +76,7 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
event.dataTransfer?.setData('name', camera.name); event.dataTransfer?.setData('name', camera.name);
}, },
}, },
camera.name, [cameraIcon, h('span', camera.name)],
); );
} }
@@ -77,6 +84,21 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
return option.label; return option.label;
}; };
const renderNodeSuffix: TreeProps['renderSuffix'] = ({ option }) => {
if (isCameraSiteNode(option)) {
const { online, offline, total } = option.stats;
return `(${online}/${offline}/${total})`;
}
if (isCameraAreaNode(option)) {
const { online, offline, total } = option.stats;
const suffixStyle: CSSProperties = option.areaLevel === 1 ? { marginRight: '8px', opacity: 0.6 } : { marginRight: '16px', opacity: 0.4 };
return h('div', { style: suffixStyle }, `(${online}/${offline}/${total})`);
}
return null;
};
const resourcePanelStore = useResourcePanelStore(); const resourcePanelStore = useResourcePanelStore();
const { searchPattern } = storeToRefs(resourcePanelStore); const { searchPattern } = storeToRefs(resourcePanelStore);
@@ -98,6 +120,7 @@ const searchFilter: TreeProps['filter'] = (pattern, node) => {
virtual-scroll virtual-scroll
style="height: 100%" style="height: 100%"
:render-label="renderNodeLabel" :render-label="renderNodeLabel"
:render-suffix="renderNodeSuffix"
:override-default-node-click-behavior="overrideNodeClickBehavior" :override-default-node-click-behavior="overrideNodeClickBehavior"
:default-expand-all="searchPattern.trim().length > 0" :default-expand-all="searchPattern.trim().length > 0"
:show-irrelevant-nodes="false" :show-irrelevant-nodes="false"
@@ -107,22 +130,42 @@ const searchFilter: TreeProps['filter'] = (pattern, node) => {
/> />
</template> </template>
<template v-if="lineTabPanes.length > 1"> <template v-if="lineTabPanes.length > 1">
<NTabs :type="'card'" :placement="'left'" style="height: 100%"> <NTabs
<NTabPane v-for="{ lineCode, lineName, cameraTree } in lineTabPanes" :key="lineCode" :name="lineName" :tab="lineName"> :type="'card'"
<NTree :placement="'left'"
block-line style="height: 100%"
block-node :tab-style="{
show-line width: '64px',
virtual-scroll height: '36px',
style="height: 100%" }"
:render-label="renderNodeLabel" :style="{
:override-default-node-click-behavior="overrideNodeClickBehavior" '--n-bar-color': '#0000',
:default-expand-all="false" '--n-pane-padding-top': '0',
:show-irrelevant-nodes="false" '--n-tab-gap-vertical': '0',
:data="cameraTree" // '--n-tab-padding-vertical': '14px 12px'
:pattern="searchPattern" }"
:filter="searchFilter" >
/> <NTabPane v-for="{ lineCode, lineName, cameraTree } in lineTabPanes" :key="lineCode" :name="lineName">
<template #tab>
<span style="font-size: 12px">{{ lineName }}</span>
</template>
<template #default>
<NTree
block-line
block-node
show-line
virtual-scroll
style="height: 100%"
:render-label="renderNodeLabel"
:render-suffix="renderNodeSuffix"
:override-default-node-click-behavior="overrideNodeClickBehavior"
:default-expand-all="false"
:show-irrelevant-nodes="false"
:data="cameraTree"
:pattern="searchPattern"
:filter="searchFilter"
/>
</template>
</NTabPane> </NTabPane>
</NTabs> </NTabs>
</template> </template>
@@ -2,9 +2,47 @@ import { useQuery } from '@tanstack/vue-query';
import { computed } from 'vue'; import { computed } from 'vue';
import type { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
import axios from 'axios'; import axios from 'axios';
import type { CodeArea, CodeLines, CodeSites } from '../../types'; import { compileCodeAreas, type CodeArea, type CodeLines, type CodeSites } from '../../types';
import { useCameraStore, useAlarmStore } from '../../stores'; import { useCameraStore, useAlarmStore } from '../../stores';
import { catalogAllDeviceApi, catalogChannelApi, type VimpChannel } from '../../apis'; import { catalogAllDeviceApi, catalogChannelApi, type VimpChannel, type VimpStation } from '../../apis';
const config: AxiosRequestConfig = {
headers: {
'Cache-Control': 'no-store',
},
};
const buildTrainAreas = () => {
const codeTrainAreas: CodeArea[] = [];
for (let i = 0; i < 999; i++) {
const codeTrain = i.toString().padStart(3, '0');
// 市域线name为车组,改造线name为车次
const area: CodeArea = { code: codeTrain, name: '车次' + codeTrain, subs: [] };
for (let j = 0; j <= 99; j++) {
const codeCarriage = j.toString().padStart(2, '0');
const subArea: CodeArea['subs'][number] = { code: codeTrain + codeCarriage, name: '车厢' + codeCarriage };
area.subs.push(subArea);
}
// const areaPreserve: CodeArea['subs'][number] = { code: codeTrain + '51', name: '预留' };
// area.subs.push(areaPreserve);
codeTrainAreas.push(area);
}
return codeTrainAreas;
};
const compareByCode = <T extends { code: string }>(a: T, b: T) => {
if (a.code < b.code) return -1;
if (a.code > b.code) return 1;
return 0;
};
const sortSitesByCode = (sites: VimpStation[]) => {
sites.sort(compareByCode);
};
const sortChannelsMapByCode = (siteCodeToChannelsMap: Map<string, VimpChannel[]>) => {
for (const channels of siteCodeToChannelsMap.values()) {
channels.sort(compareByCode);
}
};
export const useDeviceCenterQuery = () => { export const useDeviceCenterQuery = () => {
const cameraStore = useCameraStore(); const cameraStore = useCameraStore();
@@ -15,84 +53,93 @@ export const useDeviceCenterQuery = () => {
refetchInterval: 10 * 1000, refetchInterval: 10 * 1000,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
queryFn: async ({ signal }) => { queryFn: async ({ signal }) => {
const config: AxiosRequestConfig = { // 请求所有码表
headers: {
'Cache-Control': 'no-store',
},
};
const buildTrainAreas = () => {
const codeTrainAreas: CodeArea[] = [];
for (let i = 0; i < 999; i++) {
const codeTrain = i.toString().padStart(3, '0');
// 市域线name为车组,改造线name为车次
const area: CodeArea = { code: codeTrain, name: '车次' + codeTrain, subs: [] };
for (let j = 0; j <= 99; j++) {
const codeCarriage = j.toString().padStart(2, '0');
const subArea: CodeArea['subs'][number] = { code: codeTrain + codeCarriage, name: '车厢' + codeCarriage };
area.subs.push(subArea);
}
// const areaPreserve: CodeArea['subs'][number] = { code: codeTrain + '51', name: '预留' };
// area.subs.push(areaPreserve);
codeTrainAreas.push(area);
}
return codeTrainAreas;
};
const codeLines = (await axios.get<CodeLines>('/cdn/vimp/codes/codeLines.json', config)).data; const codeLines = (await axios.get<CodeLines>('/cdn/vimp/codes/codeLines.json', config)).data;
const codeSites = (await axios.get<CodeSites>('/cdn/vimp/codes/codeStations.json', config)).data; const codeSites = (await axios.get<CodeSites>('/cdn/vimp/codes/codeStations.json', config)).data;
const codeStationAreas = (await axios.get<CodeArea[]>('/cdn/vimp/codes/codeStationAreas.json', config)).data; const codeStationAreas = (await axios.get<CodeArea[]>('/cdn/vimp/codes/codeStationAreas.json', config)).data;
const codeParkingAreas = (await axios.get<CodeArea[]>('/cdn/vimp/codes/codeParkingAreas.json', config)).data; const codeParkingAreas = (await axios.get<CodeArea[]>('/cdn/vimp/codes/codeParkingAreas.json', config)).data;
const codeOccAreas = (await axios.get<CodeArea[]>('/cdn/vimp/codes/codeOccAreas.json', config)).data; const codeOccAreas = (await axios.get<CodeArea[]>('/cdn/vimp/codes/codeOccAreas.json', config)).data;
const codeTrainAreas = buildTrainAreas(); const codeTrainAreas = buildTrainAreas();
// 预编译区域码表索引 (性能优化)
const siteCamerasMapFromApi: Record<string, VimpChannel[]> = {}; const compiledCodeAreas = compileCodeAreas({
const siteAlarmsMapFromApi: Record<string, VimpChannel[]> = {};
const sitesFromApi = await catalogAllDeviceApi({ signal });
if (!!sitesFromApi) {
for (const site of sitesFromApi) {
const channels = await catalogChannelApi(site.code, { signal });
if (!channels || channels.length === 0) continue;
const cameras: VimpChannel[] = [];
const alarms: VimpChannel[] = [];
channels.forEach((channel) => {
const typeCode = Number(channel.code.substring(11, 14));
if (typeCode >= 4 && typeCode <= 6) {
cameras.push(channel);
} else if ((typeCode >= 101 && typeCode <= 108) || (typeCode >= 810 && typeCode <= 815)) {
alarms.push(channel);
}
});
const siteCode = site.code.substring(0, 6);
siteCamerasMapFromApi[siteCode] = cameras;
siteAlarmsMapFromApi[siteCode] = alarms;
}
}
cameraStore.buildLineTabPanes({
sitesFromApi,
siteCamerasMapFromApi,
codeLines,
codeSites,
codeStationAreas, codeStationAreas,
codeParkingAreas, codeParkingAreas,
codeOccAreas, codeOccAreas,
codeTrainAreas, codeTrainAreas,
}); });
alarmStore.buildLineTabPanes({ const sitesFromApi = await catalogAllDeviceApi({ signal });
sitesFromApi,
siteAlarmsMapFromApi, // 从 /allDevice 接口获取的站点信息并不保证真实性和完整性,
// 例如有一个站点的编码是 010699 开头,但是其下的通道是 010199 和 010599 开头,
// 而 010699 是一个不存在的站点编码,所以需要基于通道的编码来确定所有的站点。
const cameraSites: VimpStation[] = [];
const alarmSites: VimpStation[] = [];
const cameraBuiltSitesSet = new Set<string>();
const alarmBuiltSitesSet = new Set<string>();
const siteCodeToCamerasMap = new Map<string, VimpChannel[]>();
const siteCodeToAlarmsMap = new Map<string, VimpChannel[]>();
for (const siteFromApi of sitesFromApi ?? []) {
const channels = await catalogChannelApi(siteFromApi.code, { signal });
if (!channels) continue;
channels.forEach((channel) => {
const siteCode = channel.code.substring(0, 6);
const typeCode = Number(channel.code.substring(11, 14));
const isCamera = typeCode >= 4 && typeCode <= 6;
const isAlarm = (typeCode >= 101 && typeCode <= 108) || (typeCode >= 810 && typeCode <= 815);
if (isCamera) {
if (!cameraBuiltSitesSet.has(siteCode)) {
cameraSites.push({
code: siteCode,
name: codeSites[siteCode]?.name ?? '',
online: siteFromApi.online,
});
cameraBuiltSitesSet.add(siteCode);
}
if (!siteCodeToCamerasMap.has(siteCode)) {
siteCodeToCamerasMap.set(siteCode, []);
}
siteCodeToCamerasMap.get(siteCode)!.push(channel);
} else if (isAlarm) {
if (!alarmBuiltSitesSet.has(siteCode)) {
alarmSites.push({
code: siteCode,
name: codeSites[siteCode]?.name ?? '',
online: siteFromApi.online,
});
alarmBuiltSitesSet.add(siteCode);
}
if (!siteCodeToAlarmsMap.has(siteCode)) {
siteCodeToAlarmsMap.set(siteCode, []);
}
siteCodeToAlarmsMap.get(siteCode)!.push(channel);
}
});
}
// 1. 站点数组排序:稳定线路面板顺序和站点节点顺序
sortSitesByCode(cameraSites);
sortSitesByCode(alarmSites);
// 2. 每站通道数组排序:稳定区域节点顺序和通道节点顺序
sortChannelsMapByCode(siteCodeToCamerasMap);
sortChannelsMapByCode(siteCodeToAlarmsMap);
cameraStore.buildLineTabPanes({
sites: cameraSites,
siteCodeToCamerasMap: siteCodeToCamerasMap,
codeLines, codeLines,
codeSites, codeSites,
codeStationAreas, compiledCodeAreas,
codeParkingAreas, });
codeOccAreas,
codeTrainAreas, alarmStore.buildLineTabPanes({
sites: alarmSites,
siteCodeToAlarmsMap,
codeLines,
codeSites,
compiledCodeAreas,
}); });
return null; return null;
+86 -108
View File
@@ -1,47 +1,52 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import type { VimpChannel, VimpStation } from '../apis'; import type { VimpChannel, VimpStation } from '../apis';
import { h, ref } from 'vue'; import { shallowRef } from 'vue';
import type { AlarmMainAreaNodeOption, AlarmNodeOption, CodeArea, CodeLines, CodeSites, AlarmLineTabPane, AlarmSiteNodeOption, AlarmSubAreaNodeOption } from '../types'; import type { AlarmMainAreaNodeOption, AlarmNodeOption, CodeLines, CodeSites, AlarmLineTabPane, AlarmSiteNodeOption, AlarmSubAreaNodeOption, CompiledCodeAreas } from '../types';
import { NIcon } from 'naive-ui';
import { SirenIcon } from 'lucide-vue-next';
interface BuildLineTabPanesParams { interface BuildLineTabPanesParams {
sitesFromApi: VimpStation[] | null; sites: VimpStation[];
siteAlarmsMapFromApi: Record<string, VimpChannel[]>; siteCodeToAlarmsMap: Map<string, VimpChannel[]>;
codeLines: CodeLines; codeLines: CodeLines;
codeSites: CodeSites; codeSites: CodeSites;
codeStationAreas: CodeArea[]; compiledCodeAreas: CompiledCodeAreas;
codeParkingAreas: CodeArea[];
codeOccAreas: CodeArea[];
codeTrainAreas: CodeArea[];
} }
const buildMainAreaNodeKey = (siteCode: string, mainAreaCode: string) => `${siteCode}${mainAreaCode}`;
const buildSubAreaNodeKey = (siteCode: string, areaCode: string) => `${siteCode}${areaCode}`;
export const useAlarmStore = defineStore('vimp-alarm-store', () => { export const useAlarmStore = defineStore('vimp-alarm-store', () => {
const lineTabPanes = ref<AlarmLineTabPane[]>([]); const lineTabPanes = shallowRef<AlarmLineTabPane[]>([]);
const buildLineTabPanes = (params: BuildLineTabPanesParams) => { const buildLineTabPanes = (params: BuildLineTabPanesParams) => {
const { sitesFromApi, siteAlarmsMapFromApi, codeLines, codeSites, codeStationAreas, codeParkingAreas, codeOccAreas, codeTrainAreas } = params; const { sites, siteCodeToAlarmsMap, codeLines, codeSites, compiledCodeAreas } = params;
if (!sitesFromApi) {
lineTabPanes.value = []; const result: AlarmLineTabPane[] = [];
return;
} // 1. 线路索引 lineCode -> AlarmLineTabPane
// 构造线路TabPane const linePaneMap = new Map<string, AlarmLineTabPane>();
const _lineTabPanes: AlarmLineTabPane[] = [];
const lineCode = sitesFromApi.at(0)?.code.substring(0, 3) ?? '';
const lineName = codeLines[lineCode]?.name ?? '';
if (!_lineTabPanes.some((lineNode) => lineNode.lineCode === lineCode)) {
_lineTabPanes.push({
lineCode,
lineName,
alarmTree: [],
});
}
// 遍历所有站点 // 遍历所有站点
for (const site of sitesFromApi) { for (const site of sites) {
const siteCode = site.code.substring(0, 6); // 2. 站点节点 siteNode 只在当前轮次中顺序创建,不需要建立索引
const siteName = codeSites[siteCode]?.name;
if (!siteName) continue; const lineCode = site.code.substring(0, 3);
const lineName = codeLines[lineCode]?.name ?? '';
let linePane = linePaneMap.get(lineCode);
if (!linePane) {
linePane = { lineCode, lineName, alarmTree: [] };
linePaneMap.set(lineCode, linePane);
result.push(linePane);
}
const siteCode = site.code;
const siteMeta = codeSites[siteCode];
if (!siteMeta) continue;
const siteName = siteMeta.name;
const siteType = siteMeta.type;
const compiledCodeAreaMaps = compiledCodeAreas[siteType];
const mainAreaCodeLength = siteType === 'train' ? 3 : 2;
// 构造站点节点 // 构造站点节点
const siteNode: AlarmSiteNodeOption = { const siteNode: AlarmSiteNodeOption = {
@@ -51,79 +56,76 @@ export const useAlarmStore = defineStore('vimp-alarm-store', () => {
stats: { online: 0, offline: 0, total: 0 }, stats: { online: 0, offline: 0, total: 0 },
online: site.online, online: site.online,
}; };
_lineTabPanes.find((lineTabPane) => lineTabPane.lineCode === lineCode)?.alarmTree.push(siteNode); linePane.alarmTree.push(siteNode);
// 获取所有警报器 // 获取所有警报器
const alarms = siteAlarmsMapFromApi[siteCode]; const alarms = siteCodeToAlarmsMap.get(siteCode);
if (!alarms || alarms.length === 0) continue; if (!alarms) continue;
// 3. 1级区域索引 mainAreaNodeKey -> AlarmMainAreaNodeOption
// mainAreaNodeKey = ${siteCode}${alarmMainAreaCode}
const mainAreaNodeMap = new Map<string, AlarmMainAreaNodeOption>();
// 4. 2级区域索引 subAreaNodeKey -> AlarmSubAreaNodeOption
// subAreaNodeKey = ${siteCode}${alarmAreaCode}
const subAreaNodeMap = new Map<string, AlarmSubAreaNodeOption>();
// 5. 警报器索引 subAreaNodeKey -> Set<AlarmGbCode>
const subAreaNodeKeyToAlarmGbCodeSetMap = new Map<string, Set<string>>();
// 遍历警报器 // 遍历警报器
for (const alarm of alarms) { for (const alarm of alarms) {
// 计算相关编码 // 计算相关编码
const { code: alarmGbCode, name: alarmName } = alarm; const { code: alarmGbCode, name: alarmName } = alarm;
const alarmSiteCode = alarmGbCode.substring(0, 6);
const alarmSiteType = codeSites[alarmSiteCode]?.type;
const alarmAreaCode = alarmGbCode.substring(6, 11); const alarmAreaCode = alarmGbCode.substring(6, 11);
const alarmMainAreaCode = alarmAreaCode.slice(0, alarmSiteType === 'train' ? 3 : 2); const alarmMainAreaCode = alarmAreaCode.slice(0, mainAreaCodeLength);
// 构造车站/基地/OCC/车次区域 // 查找1级区域,如果未找到则跳过该警报器
let siteArea: CodeArea | undefined = undefined; const mainArea = compiledCodeAreaMaps.mainAreaMap.get(alarmMainAreaCode);
if (alarmSiteType === 'station') { if (!mainArea) continue;
siteArea = codeStationAreas.find((area) => area.code === alarmMainAreaCode);
} else if (alarmSiteType === 'parking') {
siteArea = codeParkingAreas.find((area) => area.code === alarmMainAreaCode);
} else if (alarmSiteType === 'occ') {
siteArea = codeOccAreas.find((area) => area.code === alarmMainAreaCode);
} else if (alarmSiteType === 'train') {
siteArea = codeTrainAreas.find((area) => area.code === alarmMainAreaCode);
} else {
continue;
}
if (!siteArea) continue; // 如果还是未找到区域,则跳过该警报器
// 构造1级区域节点 // 尝试从索引中获取1级区域节点,若不存在则创建
if (!siteNode.children?.find((areaNode) => areaNode.key === `${alarmSiteCode}${alarmMainAreaCode}`)) { const mainAreaNodeKey = buildMainAreaNodeKey(siteCode, alarmMainAreaCode);
const mainAreaNode: AlarmMainAreaNodeOption = { let mainAreaNode = mainAreaNodeMap.get(mainAreaNodeKey);
key: `${alarmSiteCode}${alarmMainAreaCode}`, if (!mainAreaNode) {
label: siteArea.name, mainAreaNode = {
key: mainAreaNodeKey,
label: mainArea.name,
children: [], children: [],
stats: { online: 0, offline: 0, total: 0 }, stats: { online: 0, offline: 0, total: 0 },
site: site, site: site,
areaLevel: 1,
}; };
mainAreaNodeMap.set(mainAreaNodeKey, mainAreaNode);
siteNode.children?.push(mainAreaNode); siteNode.children?.push(mainAreaNode);
} }
const targetMainAreaNode = siteNode.children?.find((areaNode) => areaNode.key === `${alarmSiteCode}${alarmMainAreaCode}`);
if (!targetMainAreaNode) continue; // 如果1级区域节点不存在,则跳过该警报器
// 构造2级区域节点 // 查找2级区域,如果未找到则跳过该警报器
if (!targetMainAreaNode.children?.find((subAreaNode) => subAreaNode.key === `${alarmSiteCode}${alarmAreaCode}`)) { const subArea = compiledCodeAreaMaps.subAreaMap.get(alarmAreaCode);
let subArea: CodeArea['subs'][number] | undefined = undefined; if (!subArea) continue;
if (alarmSiteType === 'station') {
subArea = codeStationAreas.find((area) => area.code === alarmMainAreaCode)?.subs.find((subArea) => subArea.code === alarmAreaCode);
} else if (alarmSiteType === 'parking') {
subArea = codeParkingAreas.find((area) => area.code === alarmMainAreaCode)?.subs.find((subArea) => subArea.code === alarmAreaCode);
} else if (alarmSiteType === 'occ') {
subArea = codeOccAreas.find((area) => area.code === alarmMainAreaCode)?.subs.find((subArea) => subArea.code === alarmAreaCode);
} else if (alarmSiteType === 'train') {
subArea = codeTrainAreas.find((area) => area.code === alarmMainAreaCode)?.subs.find((subArea) => subArea.code === alarmAreaCode);
} else {
continue;
}
if (!subArea) continue; // 如果还是未找到子区域,则跳过该警报器
const subAreaNode: AlarmSubAreaNodeOption = { // 尝试从索引中获取2级区域节点,若不存在则创建
key: `${alarmSiteCode}${alarmAreaCode}`, const subAreaNodeKey = buildSubAreaNodeKey(siteCode, alarmAreaCode);
let subAreaNode = subAreaNodeMap.get(subAreaNodeKey);
if (!subAreaNode) {
subAreaNode = {
key: subAreaNodeKey,
label: subArea.name, label: subArea.name,
children: [], children: [],
stats: { online: 0, offline: 0, total: 0 }, stats: { online: 0, offline: 0, total: 0 },
site: site, site: site,
areaLevel: 2,
}; };
targetMainAreaNode.children?.push(subAreaNode); subAreaNodeMap.set(subAreaNodeKey, subAreaNode);
mainAreaNode.children?.push(subAreaNode);
} }
const subAreaNode = targetMainAreaNode.children?.find((subAreaNode) => subAreaNode.key === `${alarmSiteCode}${alarmAreaCode}`);
if (!subAreaNode) continue; // 如果子区域节点不存在,则跳过该警报器
// 构造警报器节点 // 构造警报器节点
let alarmGbCodeSet = subAreaNodeKeyToAlarmGbCodeSetMap.get(subAreaNodeKey);
if (!alarmGbCodeSet) {
alarmGbCodeSet = new Set<string>();
subAreaNodeKeyToAlarmGbCodeSetMap.set(subAreaNodeKey, alarmGbCodeSet);
}
if (alarmGbCodeSet.has(alarmGbCode)) continue;
alarmGbCodeSet.add(alarmGbCode);
const alarmType = alarm.code.substring(11, 14); const alarmType = alarm.code.substring(11, 14);
const alarmNode: AlarmNodeOption = { const alarmNode: AlarmNodeOption = {
key: alarmGbCode, key: alarmGbCode,
@@ -131,49 +133,25 @@ export const useAlarmStore = defineStore('vimp-alarm-store', () => {
type: alarmType, type: alarmType,
alarm: alarm, alarm: alarm,
site: site, site: site,
prefix: () => {
return h(NIcon, () => h(SirenIcon));
},
}; };
subAreaNode.children?.push(alarmNode);
// 添加警报器节点到子区域节点
if (!subAreaNode.children?.find((alarmNode) => alarmNode.key === alarmGbCode)) {
subAreaNode.children?.push(alarmNode);
}
// 统计站点、区域、子区域的在线/离线/总警报器数量 // 统计站点、区域、子区域的在线/离线/总警报器数量
siteNode.stats.total++; siteNode.stats.total++;
targetMainAreaNode.stats.total++; mainAreaNode.stats.total++;
subAreaNode.stats.total++; subAreaNode.stats.total++;
if (alarm.status === 1) { if (alarm.status === 1) {
siteNode.stats.online++; siteNode.stats.online++;
targetMainAreaNode.stats.online++; mainAreaNode.stats.online++;
subAreaNode.stats.online++; subAreaNode.stats.online++;
} } else if (alarm.status === 0) {
if (alarm.status === 0) {
siteNode.stats.offline++; siteNode.stats.offline++;
targetMainAreaNode.stats.offline++; mainAreaNode.stats.offline++;
subAreaNode.stats.offline++; subAreaNode.stats.offline++;
} }
} }
siteNode.suffix = () => {
const { online, offline, total } = siteNode.stats;
return `(${online}/${offline}/${total})`;
};
siteNode.children?.forEach((areaNode) => {
areaNode.suffix = () => {
const { online, offline, total } = areaNode.stats;
return h('div', { style: { marginRight: '8px', opacity: 0.6 } }, `(${online}/${offline}/${total})`);
};
areaNode.children?.forEach((subAreaNode) => {
subAreaNode.suffix = () => {
const { online, offline, total } = subAreaNode.stats;
return h('div', { style: { marginRight: '16px', opacity: 0.4 } }, `(${online}/${offline}/${total})`);
};
});
});
} }
lineTabPanes.value = _lineTabPanes; lineTabPanes.value = result;
}; };
return { return {
+85 -139
View File
@@ -1,76 +1,52 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import type { VimpChannel, VimpStation } from '../apis'; import type { VimpChannel, VimpStation } from '../apis';
import { h, ref } from 'vue'; import { shallowRef } from 'vue';
import type { CameraMainAreaNodeOption, CameraNodeOption, CodeArea, CodeLines, CodeSites, CameraLineTabPane, CameraSiteNodeOption, CameraSubAreaNodeOption } from '../types'; import type { CameraMainAreaNodeOption, CameraNodeOption, CodeLines, CodeSites, CameraLineTabPane, CameraSiteNodeOption, CameraSubAreaNodeOption, CompiledCodeAreas } from '../types';
import { NIcon } from 'naive-ui';
import BulletCamera from '../components/icon/bullet-camera.vue';
import PtzCamera from '../components/icon/ptz-camera.vue';
import HemiPtzCamera from '../components/icon/hemi-ptz-camera.vue';
import { objectEntries } from '@vueuse/core';
interface BuildLineTabPanesParams { interface BuildLineTabPanesParams {
sitesFromApi: VimpStation[] | null; sites: VimpStation[];
siteCamerasMapFromApi: Record<string, VimpChannel[]>; siteCodeToCamerasMap: Map<string, VimpChannel[]>;
codeLines: CodeLines; codeLines: CodeLines;
codeSites: CodeSites; codeSites: CodeSites;
codeStationAreas: CodeArea[]; compiledCodeAreas: CompiledCodeAreas;
codeParkingAreas: CodeArea[];
codeOccAreas: CodeArea[];
codeTrainAreas: CodeArea[];
} }
const buildMainAreaNodeKey = (siteCode: string, mainAreaCode: string) => `${siteCode}${mainAreaCode}`;
const buildSubAreaNodeKey = (siteCode: string, areaCode: string) => `${siteCode}${areaCode}`;
export const useCameraStore = defineStore('vimp-camera-store', () => { export const useCameraStore = defineStore('vimp-camera-store', () => {
const lineTabPanes = ref<CameraLineTabPane[]>([]); const lineTabPanes = shallowRef<CameraLineTabPane[]>([]);
const buildLineTabPanes = (params: BuildLineTabPanesParams) => { const buildLineTabPanes = (params: BuildLineTabPanesParams) => {
const { sitesFromApi, siteCamerasMapFromApi, codeLines, codeSites, codeStationAreas, codeParkingAreas, codeOccAreas, codeTrainAreas } = params; const { sites, siteCodeToCamerasMap, codeLines, codeSites, compiledCodeAreas } = params;
if (!sitesFromApi) {
lineTabPanes.value = [];
return;
}
// 构造线路TabPane
const _lineTabPanes: CameraLineTabPane[] = [];
const lineCode = sitesFromApi.at(0)?.code.substring(0, 3) ?? '';
const lineName = codeLines[lineCode]?.name ?? '';
if (!_lineTabPanes.some((lineNode) => lineNode.lineCode === lineCode)) {
_lineTabPanes.push({
lineCode,
lineName,
cameraTree: [],
});
}
// 从 /allDevice 接口获取的站点信息并不保证真实性和完整性, const result: CameraLineTabPane[] = [];
// 例如有一个站点的编码是 010699 开头,但是其下的通道是 010199 和 010599 开头,
// 而 010699 是一个不存在的站点编码,所以需要基于通道的编码来确定所有的站点。
const sites: VimpStation[] = [];
const siteCamerasMap: Record<string, VimpChannel[]> = {};
objectEntries(siteCamerasMapFromApi).forEach(([siteFullCode, cameras]) => {
const siteCode = siteFullCode.substring(0, 6);
for (const camera of cameras) {
const { code: cameraGbCode } = camera;
const cameraSiteCode = cameraGbCode.substring(0, 6);
if (!(cameraSiteCode in siteCamerasMap)) { // 1. 线路索引 lineCode -> CameraLineTabPane
siteCamerasMap[cameraSiteCode] = []; const linePaneMap = new Map<string, CameraLineTabPane>();
}
siteCamerasMap[cameraSiteCode]?.push(camera);
if (!(cameraSiteCode in codeSites)) continue;
if (sites.some((site) => site.code === cameraSiteCode)) continue;
sites.push({
code: cameraSiteCode,
name: codeSites[cameraSiteCode]?.name ?? '',
online: sitesFromApi.find((site) => site.code.substring(0, 6) === siteCode)?.online ?? false,
});
}
});
// 遍历所有站点 // 遍历所有站点
for (const site of sites) { for (const site of sites) {
const siteCode = site.code.substring(0, 6); // 2. 站点节点 siteNode 只在当前轮次中顺序创建,不需要建立索引
const siteName = codeSites[siteCode]?.name;
if (!siteName) continue; const lineCode = site.code.substring(0, 3);
const lineName = codeLines[lineCode]?.name ?? '';
let linePane = linePaneMap.get(lineCode);
if (!linePane) {
linePane = { lineCode, lineName, cameraTree: [] };
linePaneMap.set(lineCode, linePane);
result.push(linePane);
}
const siteCode = site.code;
const siteMeta = codeSites[siteCode];
if (!siteMeta) continue;
const siteName = siteMeta.name;
const siteType = siteMeta.type;
const compiledCodeAreaMaps = compiledCodeAreas[siteType];
const mainAreaCodeLength = siteType === 'train' ? 3 : 2;
// 构造站点节点 // 构造站点节点
const siteNode: CameraSiteNodeOption = { const siteNode: CameraSiteNodeOption = {
@@ -80,131 +56,101 @@ export const useCameraStore = defineStore('vimp-camera-store', () => {
stats: { online: 0, offline: 0, total: 0 }, stats: { online: 0, offline: 0, total: 0 },
online: site.online, online: site.online,
}; };
_lineTabPanes.find((lineTabPane) => lineTabPane.lineCode === lineCode)?.cameraTree.push(siteNode); linePane.cameraTree.push(siteNode);
// 获取所有摄像机 // 获取所有摄像机
const cameras = siteCamerasMap[siteCode]; const cameras = siteCodeToCamerasMap.get(siteCode);
if (!cameras || cameras.length === 0) continue; if (!cameras) continue;
// 3. 1级区域索引 mainAreaNodeKey -> CameraMainAreaNodeOption
// mainAreaNodeKey = ${siteCode}${cameraMainAreaCode}
const mainAreaNodeMap = new Map<string, CameraMainAreaNodeOption>();
// 4. 2级区域索引 subAreaNodeKey -> CameraSubAreaNodeOption
// subAreaNodeKey = ${siteCode}${cameraAreaCode}
const subAreaNodeMap = new Map<string, CameraSubAreaNodeOption>();
// 5. 摄像机索引 subAreaNodeKey -> Set<CameraGbCode>
const subAreaNodeKeyToCameraGbCodeSetMap = new Map<string, Set<string>>();
// 遍历摄像机 // 遍历摄像机
for (const camera of cameras) { for (const camera of cameras) {
// 计算相关编码 // 计算相关编码
const { code: cameraGbCode, name: cameraName } = camera; const { code: cameraGbCode, name: cameraName } = camera;
const cameraSiteCode = cameraGbCode.substring(0, 6);
const cameraSiteType = codeSites[cameraSiteCode]?.type;
const cameraAreaCode = cameraGbCode.substring(6, 11); const cameraAreaCode = cameraGbCode.substring(6, 11);
const cameraMainAreaCode = cameraAreaCode.slice(0, cameraSiteType === 'train' ? 3 : 2); const cameraMainAreaCode = cameraAreaCode.slice(0, mainAreaCodeLength);
// 构造车站/基地/OCC/车次区域 // 查找1级区域,如果未找到则跳过该摄像机
let siteArea: CodeArea | undefined = undefined; const mainArea = compiledCodeAreaMaps.mainAreaMap.get(cameraMainAreaCode);
if (cameraSiteType === 'station') { if (!mainArea) continue;
siteArea = codeStationAreas.find((area) => area.code === cameraMainAreaCode); // 尝试从索引中获取1级区域节点,若不存在则创建
} else if (cameraSiteType === 'parking') { const mainAreaNodeKey = buildMainAreaNodeKey(siteCode, cameraMainAreaCode);
siteArea = codeParkingAreas.find((area) => area.code === cameraMainAreaCode); let mainAreaNode = mainAreaNodeMap.get(mainAreaNodeKey);
} else if (cameraSiteType === 'occ') { if (!mainAreaNode) {
siteArea = codeOccAreas.find((area) => area.code === cameraMainAreaCode); mainAreaNode = {
} else if (cameraSiteType === 'train') { key: mainAreaNodeKey,
siteArea = codeTrainAreas.find((area) => area.code === cameraMainAreaCode); label: mainArea.name,
} else {
continue;
}
if (!siteArea) continue; // 如果还是未找到区域,则跳过该摄像机
// 构造1级区域节点
if (!siteNode.children?.find((areaNode) => areaNode.key === `${cameraSiteCode}${cameraMainAreaCode}`)) {
const mainAreaNode: CameraMainAreaNodeOption = {
key: `${cameraSiteCode}${cameraMainAreaCode}`,
label: siteArea.name,
children: [], children: [],
stats: { online: 0, offline: 0, total: 0 }, stats: { online: 0, offline: 0, total: 0 },
site: site, site: site,
areaLevel: 1,
}; };
mainAreaNodeMap.set(mainAreaNodeKey, mainAreaNode);
siteNode.children?.push(mainAreaNode); siteNode.children?.push(mainAreaNode);
} }
const targetMainAreaNode = siteNode.children?.find((areaNode) => areaNode.key === `${cameraSiteCode}${cameraMainAreaCode}`);
if (!targetMainAreaNode) continue; // 如果1级区域节点不存在,则跳过该摄像机
// 构造2级区域节点 // 查找2级区域,如果未找到则跳过该摄像机
if (!targetMainAreaNode.children?.find((subAreaNode) => subAreaNode.key === `${cameraSiteCode}${cameraAreaCode}`)) { const subArea = compiledCodeAreaMaps.subAreaMap.get(cameraAreaCode);
let subArea: CodeArea['subs'][number] | undefined = undefined; if (!subArea) continue;
if (cameraSiteType === 'station') { // 尝试从索引中获取2级区域节点,若不存在则创建
subArea = codeStationAreas.find((area) => area.code === cameraMainAreaCode)?.subs.find((subArea) => subArea.code === cameraAreaCode); const subAreaNodeKey = buildSubAreaNodeKey(siteCode, cameraAreaCode);
} else if (cameraSiteType === 'parking') { let subAreaNode = subAreaNodeMap.get(subAreaNodeKey);
subArea = codeParkingAreas.find((area) => area.code === cameraMainAreaCode)?.subs.find((subArea) => subArea.code === cameraAreaCode); if (!subAreaNode) {
} else if (cameraSiteType === 'occ') { subAreaNode = {
subArea = codeOccAreas.find((area) => area.code === cameraMainAreaCode)?.subs.find((subArea) => subArea.code === cameraAreaCode); key: subAreaNodeKey,
} else if (cameraSiteType === 'train') {
subArea = codeTrainAreas.find((area) => area.code === cameraMainAreaCode)?.subs.find((subArea) => subArea.code === cameraAreaCode);
} else {
continue;
}
if (!subArea) continue; // 如果还是未找到2级区域,则跳过该摄像机
const subAreaNode: CameraSubAreaNodeOption = {
key: `${cameraSiteCode}${cameraAreaCode}`,
label: subArea.name, label: subArea.name,
children: [], children: [],
stats: { online: 0, offline: 0, total: 0 }, stats: { online: 0, offline: 0, total: 0 },
site: site, site: site,
areaLevel: 2,
}; };
targetMainAreaNode.children?.push(subAreaNode); subAreaNodeMap.set(subAreaNodeKey, subAreaNode);
mainAreaNode.children?.push(subAreaNode);
} }
const subAreaNode = targetMainAreaNode.children?.find((subAreaNode) => subAreaNode.key === `${cameraSiteCode}${cameraAreaCode}`);
if (!subAreaNode) continue; // 如果子区域节点不存在,则跳过该摄像机
// 构造摄像机节点 // 构造摄像机节点
const cameraType = camera.code.substring(11, 14); let cameraGbCodeSet = subAreaNodeKeyToCameraGbCodeSetMap.get(subAreaNodeKey);
if (!cameraGbCodeSet) {
cameraGbCodeSet = new Set<string>();
subAreaNodeKeyToCameraGbCodeSetMap.set(subAreaNodeKey, cameraGbCodeSet);
}
if (cameraGbCodeSet.has(cameraGbCode)) continue;
cameraGbCodeSet.add(cameraGbCode);
const cameraType = cameraGbCode.substring(11, 14);
const cameraNode: CameraNodeOption = { const cameraNode: CameraNodeOption = {
key: cameraGbCode, key: cameraGbCode,
label: cameraName, label: cameraName,
type: cameraType, type: cameraType,
camera: camera, camera: camera,
site: site, site: site,
prefix: () => {
if (cameraType === '004') return h(NIcon, () => h(PtzCamera));
if (cameraType === '005') return h(NIcon, () => h(HemiPtzCamera));
if (cameraType === '006') return h(NIcon, () => h(BulletCamera));
},
}; };
subAreaNode.children?.push(cameraNode);
// 添加摄像机节点到子区域节点
if (!subAreaNode.children?.find((cameraNode) => cameraNode.key === cameraGbCode)) {
subAreaNode.children?.push(cameraNode);
}
// 统计站点、区域、子区域的在线/离线/总摄像机数量 // 统计站点、区域、子区域的在线/离线/总摄像机数量
siteNode.stats.total++; siteNode.stats.total++;
targetMainAreaNode.stats.total++; mainAreaNode.stats.total++;
subAreaNode.stats.total++; subAreaNode.stats.total++;
if (camera.status === 1) { if (camera.status === 1) {
siteNode.stats.online++; siteNode.stats.online++;
targetMainAreaNode.stats.online++; mainAreaNode.stats.online++;
subAreaNode.stats.online++; subAreaNode.stats.online++;
} } else if (camera.status === 0) {
if (camera.status === 0) {
siteNode.stats.offline++; siteNode.stats.offline++;
targetMainAreaNode.stats.offline++; mainAreaNode.stats.offline++;
subAreaNode.stats.offline++; subAreaNode.stats.offline++;
} }
} }
siteNode.suffix = () => {
const { online, offline, total } = siteNode.stats;
return `(${online}/${offline}/${total})`;
};
siteNode.children?.forEach((areaNode) => {
areaNode.suffix = () => {
const { online, offline, total } = areaNode.stats;
return h('div', { style: { marginRight: '8px', opacity: 0.6 } }, `(${online}/${offline}/${total})`);
};
areaNode.children?.forEach((subAreaNode) => {
subAreaNode.suffix = () => {
const { online, offline, total } = subAreaNode.stats;
return h('div', { style: { marginRight: '16px', opacity: 0.4 } }, `(${online}/${offline}/${total})`);
};
});
});
} }
lineTabPanes.value = _lineTabPanes;
lineTabPanes.value = result;
}; };
return { return {
+43
View File
@@ -6,6 +6,45 @@ export type CodeLines = Record<string, { name: string; color: string }>;
export type CodeSites = Record<string, { name: string; type: SiteType }>; export type CodeSites = Record<string, { name: string; type: SiteType }>;
export type CodeArea = { code: string; name: string; subs: { code: string; name: string }[] }; export type CodeArea = { code: string; name: string; subs: { code: string; name: string }[] };
export type CompiledCodeAreaMaps = {
mainAreaMap: Map<string, CodeArea>;
subAreaMap: Map<string, CodeArea['subs'][number]>;
};
export type CompiledCodeAreas = Record<SiteType, CompiledCodeAreaMaps>;
interface CompileCodeAreasParams {
codeStationAreas: CodeArea[];
codeParkingAreas: CodeArea[];
codeOccAreas: CodeArea[];
codeTrainAreas: CodeArea[];
}
const compileCodeAreaMaps = (areas: CodeArea[]): CompiledCodeAreaMaps => {
const mainAreaMap = new Map<string, CodeArea>();
const subAreaMap = new Map<string, CodeArea['subs'][number]>();
for (const area of areas) {
mainAreaMap.set(area.code, area);
for (const subArea of area.subs) {
subAreaMap.set(subArea.code, subArea);
}
}
return {
mainAreaMap,
subAreaMap,
};
};
export const compileCodeAreas = (parmas: CompileCodeAreasParams): CompiledCodeAreas => {
const { codeStationAreas, codeParkingAreas, codeOccAreas, codeTrainAreas } = parmas;
return {
station: compileCodeAreaMaps(codeStationAreas),
parking: compileCodeAreaMaps(codeParkingAreas),
occ: compileCodeAreaMaps(codeOccAreas),
train: compileCodeAreaMaps(codeTrainAreas),
};
};
export interface CountStats { export interface CountStats {
online: number; online: number;
offline: number; offline: number;
@@ -25,12 +64,14 @@ export interface CameraSubAreaNodeOption extends TreeOption {
children?: CameraNodeOption[]; children?: CameraNodeOption[];
stats: CountStats; stats: CountStats;
site: VimpStation; site: VimpStation;
areaLevel: 2;
} }
export interface CameraMainAreaNodeOption extends TreeOption { export interface CameraMainAreaNodeOption extends TreeOption {
children?: CameraSubAreaNodeOption[]; children?: CameraSubAreaNodeOption[];
stats: CountStats; stats: CountStats;
site: VimpStation; site: VimpStation;
areaLevel: 1;
} }
export interface CameraSiteNodeOption extends TreeOption { export interface CameraSiteNodeOption extends TreeOption {
@@ -70,12 +111,14 @@ export interface AlarmSubAreaNodeOption extends TreeOption {
children?: AlarmNodeOption[]; children?: AlarmNodeOption[];
stats: CountStats; stats: CountStats;
site: VimpStation; site: VimpStation;
areaLevel: 2;
} }
export interface AlarmMainAreaNodeOption extends TreeOption { export interface AlarmMainAreaNodeOption extends TreeOption {
children?: AlarmSubAreaNodeOption[]; children?: AlarmSubAreaNodeOption[];
stats: CountStats; stats: CountStats;
site: VimpStation; site: VimpStation;
areaLevel: 1;
} }
export interface AlarmSiteNodeOption extends TreeOption { export interface AlarmSiteNodeOption extends TreeOption {