From 7467b5483467b5e686027f6036feaf47b3b35f00 Mon Sep 17 00:00:00 2001 From: yangsy Date: Wed, 27 May 2026 02:33:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(vimp):=20=E8=AE=BE=E5=A4=87=E6=A0=91?= =?UTF-8?q?=E5=8E=9F=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/layouts/app-layout.vue | 7 +- src/pages/vimp/api/index.ts | 3 + src/pages/vimp/api/model.ts | 21 +++ src/pages/vimp/api/query.ts | 105 +++++++++++++ src/pages/vimp/api/request.ts | 71 +++++++++ src/pages/vimp/components/alarm-tree.vue | 121 ++++++++++++++ src/pages/vimp/components/camera-tree.vue | 121 ++++++++++++++ src/pages/vimp/stores/alarm.ts | 181 +++++++++++++++++++++ src/pages/vimp/stores/camera.ts | 183 ++++++++++++++++++++++ src/pages/vimp/stores/index.ts | 2 + src/pages/vimp/types/index.ts | 1 + src/pages/vimp/types/tree.ts | 79 ++++++++++ src/pages/vimp/vimp-page.vue | 68 ++++++++ src/router/index.ts | 4 + 14 files changed, 966 insertions(+), 1 deletion(-) create mode 100644 src/pages/vimp/api/index.ts create mode 100644 src/pages/vimp/api/model.ts create mode 100644 src/pages/vimp/api/query.ts create mode 100644 src/pages/vimp/api/request.ts create mode 100644 src/pages/vimp/components/alarm-tree.vue create mode 100644 src/pages/vimp/components/camera-tree.vue create mode 100644 src/pages/vimp/stores/alarm.ts create mode 100644 src/pages/vimp/stores/camera.ts create mode 100644 src/pages/vimp/stores/index.ts create mode 100644 src/pages/vimp/types/index.ts create mode 100644 src/pages/vimp/types/tree.ts create mode 100644 src/pages/vimp/vimp-page.vue diff --git a/src/layouts/app-layout.vue b/src/layouts/app-layout.vue index d63d626..caa176f 100644 --- a/src/layouts/app-layout.vue +++ b/src/layouts/app-layout.vue @@ -4,7 +4,7 @@ import { useLineStationsQuery, useStompClient, useUserPermissionQuery, useVerify import { LINE_ALARMS_QUERY_KEY, LINE_DEVICES_QUERY_KEY, LINE_STATIONS_MUTATION_KEY, LINE_STATIONS_QUERY_KEY, STATION_ALARMS_MUTATION_KEY, STATION_DEVICES_MUTATION_KEY } from '@/constants'; import { useSettingStore, useUnreadStore, useUserStore } from '@/stores'; import { useIsFetching, useIsMutating } from '@tanstack/vue-query'; -import { ChevronDownIcon, ChevronsLeftIcon, ChevronsRightIcon, ComputerIcon, KeyRoundIcon, LogOutIcon, LogsIcon, MapPinIcon, SettingsIcon, SirenIcon } from 'lucide-vue-next'; +import { ChevronDownIcon, ChevronsLeftIcon, ChevronsRightIcon, ComputerIcon, KeyRoundIcon, LogOutIcon, LogsIcon, MapPinIcon, MonitorPlayIcon, SettingsIcon, SirenIcon } from 'lucide-vue-next'; import { NBadge, NButton, @@ -111,6 +111,11 @@ const menuOptions = computed(() => [ show: isLamp.value, icon: renderIcon(KeyRoundIcon), }, + { + label: () => h(RouterLink, { to: '/vimp' }, { default: () => '视频综合管理平台' }), + key: '/vimp', + icon: renderIcon(MonitorPlayIcon), + }, ]); const dropdownOptions: DropdownOption[] = [ diff --git a/src/pages/vimp/api/index.ts b/src/pages/vimp/api/index.ts new file mode 100644 index 0000000..bdcf6f5 --- /dev/null +++ b/src/pages/vimp/api/index.ts @@ -0,0 +1,3 @@ +export * from './model'; +export * from './query'; +export * from './request'; diff --git a/src/pages/vimp/api/model.ts b/src/pages/vimp/api/model.ts new file mode 100644 index 0000000..a004f56 --- /dev/null +++ b/src/pages/vimp/api/model.ts @@ -0,0 +1,21 @@ +export interface VimpStation { + code: string; + name: string; + online: boolean; +} + +export interface VimpChannel { + address: string; + block: string; + civilCode: string; + code: string; + latitude: number; + longitude: number; + manufacture: string; + model: string; + name: string; + owner: string; + parentId: string; + parental: number; + status: number; +} diff --git a/src/pages/vimp/api/query.ts b/src/pages/vimp/api/query.ts new file mode 100644 index 0000000..49cf840 --- /dev/null +++ b/src/pages/vimp/api/query.ts @@ -0,0 +1,105 @@ +import { useQuery } from '@tanstack/vue-query'; +import { computed } from 'vue'; +import { catalogChannelApi, catalogAllDeviceApi } from './request'; +import type { AxiosRequestConfig } from 'axios'; +import axios from 'axios'; +import type { CodeArea, CodeLines, CodeSites } from '../types'; +import type { VimpChannel } from '.'; +import { useCameraStore, useAlarmStore } from '../stores'; + +export const useVimpDeviceQuery = () => { + const cameraStore = useCameraStore(); + const alarmStore = useAlarmStore(); + + return useQuery({ + queryKey: computed(() => ['vimp-device']), + refetchInterval: 10 * 1000, + refetchOnWindowFocus: false, + 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('/cdn/vimp/codes/codeLines.json', config)).data; + const codeSites = (await axios.get('/cdn/vimp/codes/codeStations.json', config)).data; + const codeStationAreas = (await axios.get('/cdn/vimp/codes/codeStationAreas.json', config)).data; + const codeParkingAreas = (await axios.get('/cdn/vimp/codes/codeParkingAreas.json', config)).data; + const codeOccAreas = (await axios.get('/cdn/vimp/codes/codeOccAreas.json', config)).data; + const codeTrainAreas = buildTrainAreas(); + + const siteCamerasMap: Record = {}; + const siteAlarmsMap: Record = {}; + const sites = await catalogAllDeviceApi({ signal }); + + if (!!sites) { + for (const site of sites) { + 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); + } + }); + + if (cameras.length > 0) { + siteCamerasMap[site.code] = cameras; + } + if (alarms.length > 0) { + siteAlarmsMap[site.code] = alarms; + } + } + } + + cameraStore.buildLineTabPanes({ + sites, + siteCamerasMap, + codeLines, + codeSites, + codeStationAreas, + codeParkingAreas, + codeOccAreas, + codeTrainAreas, + }); + + alarmStore.buildLineTabPanes({ + sites, + siteAlarmsMap, + codeLines, + codeSites, + codeStationAreas, + codeParkingAreas, + codeOccAreas, + codeTrainAreas, + }); + + return null; + }, + }); +}; diff --git a/src/pages/vimp/api/request.ts b/src/pages/vimp/api/request.ts new file mode 100644 index 0000000..7e780c3 --- /dev/null +++ b/src/pages/vimp/api/request.ts @@ -0,0 +1,71 @@ +import type { VimpChannel, VimpStation } from './model'; +import type { AxiosError, AxiosRequestConfig, CreateAxiosDefaults } from 'axios'; +import axios from 'axios'; + +interface VimpResult { + code: number; + data: T; + msg: string; +} + +type VimpResponse = [err: AxiosError | null, data: T | null, resp: VimpResult | null]; + +const createVimpClient = (config?: CreateAxiosDefaults) => { + const instance = axios.create(config); + + const vimpPost = (url: string, data?: AxiosRequestConfig['data'], options?: Partial> & { retRaw?: boolean; upload?: boolean }): Promise> => { + const { retRaw, upload, ...reqConfig } = options ?? {}; + return new Promise((resolve) => { + instance + .post(url, data, { headers: { 'content-type': upload ? 'multipart/form-data' : 'application/json' }, ...reqConfig }) + .then((res) => { + const resData = res.data; + if (retRaw) { + resolve([null, resData as T, null]); + } else { + resolve([null, resData.data as T, resData as VimpResult]); + } + }) + .catch((err) => { + resolve([err as AxiosError, null, null]); + }); + }); + }; + + return { + instance, + post: vimpPost, + }; +}; + +const unwrapVimpResponse = (resp: VimpResponse) => { + const [err, data, result] = resp; + if (err) throw err; + if (result) { + const { code, msg } = result; + if (code !== 0 && code !== 200) throw new Error(`${msg || '请求失败'}`); + } + return data; +}; + +export const vimpClient = createVimpClient({ + baseURL: `/vimp/api/client`, +}); + +export const catalogAllDeviceApi = async (options?: { signal?: AbortSignal }) => { + const { signal } = options ?? {}; + const client = vimpClient; + const endpoint = `/catalog/allDevice`; + const resp = await client.post(endpoint, {}, { signal }); + const data = unwrapVimpResponse(resp); + return data; +}; + +export const catalogChannelApi = async (code: string, options?: { signal?: AbortSignal }) => { + const { signal } = options ?? {}; + const client = vimpClient; + const endpoint = `/catalog/channel`; + const resp = await client.post(endpoint, { code, time: '' }, { signal }); + const data = unwrapVimpResponse(resp); + return data; +}; diff --git a/src/pages/vimp/components/alarm-tree.vue b/src/pages/vimp/components/alarm-tree.vue new file mode 100644 index 0000000..babaa2d --- /dev/null +++ b/src/pages/vimp/components/alarm-tree.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/src/pages/vimp/components/camera-tree.vue b/src/pages/vimp/components/camera-tree.vue new file mode 100644 index 0000000..391fa70 --- /dev/null +++ b/src/pages/vimp/components/camera-tree.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/src/pages/vimp/stores/alarm.ts b/src/pages/vimp/stores/alarm.ts new file mode 100644 index 0000000..eefa229 --- /dev/null +++ b/src/pages/vimp/stores/alarm.ts @@ -0,0 +1,181 @@ +import { defineStore } from 'pinia'; +import type { VimpChannel, VimpStation } from '../api'; +import { h, ref } from 'vue'; +import type { AlarmAreaNodeOption, AlarmNodeOption, CodeArea, CodeLines, CodeSites, AlarmLineTabPane, AlarmSiteNodeOption, AlarmSubAreaNodeOption } from '../types'; + +interface BuildLineTabPanesParams { + sites: VimpStation[] | null; + siteAlarmsMap: Record; + codeLines: CodeLines; + codeSites: CodeSites; + codeStationAreas: CodeArea[]; + codeParkingAreas: CodeArea[]; + codeOccAreas: CodeArea[]; + codeTrainAreas: CodeArea[]; +} + +export const useAlarmStore = defineStore('vimp-alarm', () => { + const lineTabPanes = ref([]); + + const buildLineTabPanes = (params: BuildLineTabPanesParams) => { + const { sites, siteAlarmsMap, codeLines, codeSites, codeStationAreas, codeParkingAreas, codeOccAreas, codeTrainAreas } = params; + if (!sites) { + lineTabPanes.value = []; + return; + } + // 构造线路TabPane + const _lineTabPanes: AlarmLineTabPane[] = []; + const lineCode = sites.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 sites) { + const siteCode = site.code.substring(0, 6); + const siteName = codeSites[siteCode]?.name; + if (!siteName) continue; + + // 构造站点节点 + const siteNode: AlarmSiteNodeOption = { + key: siteCode, + label: siteName, + children: [], + stats: { online: 0, offline: 0, total: 0 }, + online: site.online, + }; + _lineTabPanes.find((lineTabPane) => lineTabPane.lineCode === lineCode)?.alarmTree.push(siteNode); + + // 获取所有警报器 + const alarms = siteAlarmsMap[site.code]; + if (!alarms || alarms.length === 0) continue; + + // 遍历警报器 + for (const alarm of alarms) { + // 计算相关编码 + const { code: alarmGbCode, name: alarmName } = alarm; + const alarmSiteCode = alarmGbCode.substring(0, 6); + const alarmSiteType = codeSites[alarmSiteCode]?.type; + const alarmAreaCode = alarmGbCode.substring(6, 11); + const alarmMainAreaCode = alarmAreaCode.slice(0, 2); + + // 构造车站/基地/OCC/车次区域 + let siteArea: CodeArea | undefined = undefined; + if (alarmSiteType === 'station') { + 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; // 如果还是未找到区域,则跳过该警报器 + + // 构造区域节点 + if (!siteNode.children?.find((areaNode) => areaNode.key === `${alarmSiteCode}${alarmMainAreaCode}`)) { + const areaNode: AlarmAreaNodeOption = { + key: `${alarmSiteCode}${alarmMainAreaCode}`, + label: siteArea.name, + children: [], + stats: { online: 0, offline: 0, total: 0 }, + site: site, + }; + siteNode.children?.push(areaNode); + } + const areaNode = siteNode.children?.find((areaNode) => areaNode.key === `${alarmSiteCode}${alarmMainAreaCode}`); + if (!areaNode) continue; // 如果区域节点不存在,则跳过该警报器 + + // 构造子区域节点 + if (!areaNode.children?.find((subAreaNode) => subAreaNode.key === `${alarmSiteCode}${alarmAreaCode}`)) { + let subArea: CodeArea['subs'][number] | undefined = undefined; + 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 = { + key: `${alarmSiteCode}${alarmAreaCode}`, + label: subArea.name, + children: [], + stats: { online: 0, offline: 0, total: 0 }, + site: site, + }; + areaNode.children?.push(subAreaNode); + } + const subAreaNode = areaNode.children?.find((subAreaNode) => subAreaNode.key === `${alarmSiteCode}${alarmAreaCode}`); + if (!subAreaNode) continue; // 如果子区域节点不存在,则跳过该警报器 + + // 构造警报器节点 + const alarmType = alarm.code.substring(11, 14); + const alarmNode: AlarmNodeOption = { + key: alarmGbCode, + label: alarmName, + type: alarmType, + alarm: alarm, + site: site, + prefix: () => { + return `[警报器]`; + }, + }; + + // 添加警报器节点到子区域节点 + if (!subAreaNode.children?.find((alarmNode) => alarmNode.key === alarmGbCode)) { + subAreaNode.children?.push(alarmNode); + } + + // 统计站点、区域、子区域的在线/离线/总警报器数量 + siteNode.stats.total++; + areaNode.stats.total++; + subAreaNode.stats.total++; + if (alarm.status === 1) { + siteNode.stats.online++; + areaNode.stats.online++; + subAreaNode.stats.online++; + } + if (alarm.status === 0) { + siteNode.stats.offline++; + areaNode.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; + }; + + return { + lineTabPanes, + buildLineTabPanes, + }; +}); diff --git a/src/pages/vimp/stores/camera.ts b/src/pages/vimp/stores/camera.ts new file mode 100644 index 0000000..099b787 --- /dev/null +++ b/src/pages/vimp/stores/camera.ts @@ -0,0 +1,183 @@ +import { defineStore } from 'pinia'; +import type { VimpChannel, VimpStation } from '../api'; +import { h, ref } from 'vue'; +import type { CameraAreaNodeOption, CameraNodeOption, CodeArea, CodeLines, CodeSites, CameraLineTabPane, CameraSiteNodeOption, CameraSubAreaNodeOption } from '../types'; + +interface BuildLineTabPanesParams { + sites: VimpStation[] | null; + siteCamerasMap: Record; + codeLines: CodeLines; + codeSites: CodeSites; + codeStationAreas: CodeArea[]; + codeParkingAreas: CodeArea[]; + codeOccAreas: CodeArea[]; + codeTrainAreas: CodeArea[]; +} + +export const useCameraStore = defineStore('vimp-camera', () => { + const lineTabPanes = ref([]); + + const buildLineTabPanes = (params: BuildLineTabPanesParams) => { + const { sites, siteCamerasMap, codeLines, codeSites, codeStationAreas, codeParkingAreas, codeOccAreas, codeTrainAreas } = params; + if (!sites) { + lineTabPanes.value = []; + return; + } + // 构造线路TabPane + const _lineTabPanes: CameraLineTabPane[] = []; + const lineCode = sites.at(0)?.code.substring(0, 3) ?? ''; + const lineName = codeLines[lineCode]?.name ?? ''; + if (!_lineTabPanes.some((lineNode) => lineNode.lineCode === lineCode)) { + _lineTabPanes.push({ + lineCode, + lineName, + cameraTree: [], + }); + } + + // 遍历所有站点 + for (const site of sites) { + const siteCode = site.code.substring(0, 6); + const siteName = codeSites[siteCode]?.name; + if (!siteName) continue; + + // 构造站点节点 + const siteNode: CameraSiteNodeOption = { + key: siteCode, + label: siteName, + children: [], + stats: { online: 0, offline: 0, total: 0 }, + online: site.online, + }; + _lineTabPanes.find((lineTabPane) => lineTabPane.lineCode === lineCode)?.cameraTree.push(siteNode); + + // 获取所有摄像机 + const cameras = siteCamerasMap[site.code]; + if (!cameras || cameras.length === 0) continue; + + // 遍历摄像机 + for (const camera of cameras) { + // 计算相关编码 + const { code: cameraGbCode, name: cameraName } = camera; + const cameraSiteCode = cameraGbCode.substring(0, 6); + const cameraSiteType = codeSites[cameraSiteCode]?.type; + const cameraAreaCode = cameraGbCode.substring(6, 11); + const cameraMainAreaCode = cameraAreaCode.slice(0, 2); + + // 构造车站/基地/OCC/车次区域 + let siteArea: CodeArea | undefined = undefined; + if (cameraSiteType === 'station') { + siteArea = codeStationAreas.find((area) => area.code === cameraMainAreaCode); + } else if (cameraSiteType === 'parking') { + siteArea = codeParkingAreas.find((area) => area.code === cameraMainAreaCode); + } else if (cameraSiteType === 'occ') { + siteArea = codeOccAreas.find((area) => area.code === cameraMainAreaCode); + } else if (cameraSiteType === 'train') { + siteArea = codeTrainAreas.find((area) => area.code === cameraMainAreaCode); + } else { + continue; + } + if (!siteArea) continue; // 如果还是未找到区域,则跳过该摄像机 + + // 构造区域节点 + if (!siteNode.children?.find((areaNode) => areaNode.key === `${cameraSiteCode}${cameraMainAreaCode}`)) { + const areaNode: CameraAreaNodeOption = { + key: `${cameraSiteCode}${cameraMainAreaCode}`, + label: siteArea.name, + children: [], + stats: { online: 0, offline: 0, total: 0 }, + site: site, + }; + siteNode.children?.push(areaNode); + } + const areaNode = siteNode.children?.find((areaNode) => areaNode.key === `${cameraSiteCode}${cameraMainAreaCode}`); + if (!areaNode) continue; // 如果区域节点不存在,则跳过该摄像机 + + // 构造子区域节点 + if (!areaNode.children?.find((subAreaNode) => subAreaNode.key === `${cameraSiteCode}${cameraAreaCode}`)) { + let subArea: CodeArea['subs'][number] | undefined = undefined; + if (cameraSiteType === 'station') { + subArea = codeStationAreas.find((area) => area.code === cameraMainAreaCode)?.subs.find((subArea) => subArea.code === cameraAreaCode); + } else if (cameraSiteType === 'parking') { + subArea = codeParkingAreas.find((area) => area.code === cameraMainAreaCode)?.subs.find((subArea) => subArea.code === cameraAreaCode); + } else if (cameraSiteType === 'occ') { + subArea = codeOccAreas.find((area) => area.code === cameraMainAreaCode)?.subs.find((subArea) => subArea.code === cameraAreaCode); + } else if (cameraSiteType === 'train') { + subArea = codeTrainAreas.find((area) => area.code === cameraMainAreaCode)?.subs.find((subArea) => subArea.code === cameraAreaCode); + } else { + continue; + } + if (!subArea) continue; // 如果还是未找到子区域,则跳过该摄像机 + + const subAreaNode: CameraSubAreaNodeOption = { + key: `${cameraSiteCode}${cameraAreaCode}`, + label: subArea.name, + children: [], + stats: { online: 0, offline: 0, total: 0 }, + site: site, + }; + areaNode.children?.push(subAreaNode); + } + const subAreaNode = areaNode.children?.find((subAreaNode) => subAreaNode.key === `${cameraSiteCode}${cameraAreaCode}`); + if (!subAreaNode) continue; // 如果子区域节点不存在,则跳过该摄像机 + + // 构造摄像机节点 + const cameraType = camera.code.substring(11, 14); + const cameraNode: CameraNodeOption = { + key: cameraGbCode, + label: cameraName, + type: cameraType, + camera: camera, + site: site, + prefix: () => { + if (cameraType === '004') return `[枪机]`; + if (cameraType === '005') return `[半球]`; + if (cameraType === '006') return `[球机]`; + }, + }; + + // 添加摄像机节点到子区域节点 + if (!subAreaNode.children?.find((cameraNode) => cameraNode.key === cameraGbCode)) { + subAreaNode.children?.push(cameraNode); + } + + // 统计站点、区域、子区域的在线/离线/总摄像机数量 + siteNode.stats.total++; + areaNode.stats.total++; + subAreaNode.stats.total++; + if (camera.status === 1) { + siteNode.stats.online++; + areaNode.stats.online++; + subAreaNode.stats.online++; + } + if (camera.status === 0) { + siteNode.stats.offline++; + areaNode.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; + }; + + return { + lineTabPanes, + buildLineTabPanes, + }; +}); diff --git a/src/pages/vimp/stores/index.ts b/src/pages/vimp/stores/index.ts new file mode 100644 index 0000000..feb9b73 --- /dev/null +++ b/src/pages/vimp/stores/index.ts @@ -0,0 +1,2 @@ +export * from './camera'; +export * from './alarm'; diff --git a/src/pages/vimp/types/index.ts b/src/pages/vimp/types/index.ts new file mode 100644 index 0000000..50842b5 --- /dev/null +++ b/src/pages/vimp/types/index.ts @@ -0,0 +1 @@ +export * from './tree'; diff --git a/src/pages/vimp/types/tree.ts b/src/pages/vimp/types/tree.ts new file mode 100644 index 0000000..5592179 --- /dev/null +++ b/src/pages/vimp/types/tree.ts @@ -0,0 +1,79 @@ +import type { TabPaneProps, TreeOption } from 'naive-ui'; +import type { VimpChannel, VimpStation } from '../api'; + +export type SiteType = 'station' | 'parking' | 'occ' | 'train'; +export type CodeLines = Record; +export type CodeSites = Record; +export type CodeArea = { code: string; name: string; subs: { code: string; name: string }[] }; + +export interface CountStats { + online: number; + offline: number; + total: number; +} + +// ========================================== +// 摄像机树相关类型 +// ========================================== +export interface CameraNodeOption extends TreeOption { + camera: VimpChannel; + type: string; + site: VimpStation; +} + +export interface CameraSubAreaNodeOption extends TreeOption { + children?: CameraNodeOption[]; + stats: CountStats; + site: VimpStation; +} + +export interface CameraAreaNodeOption extends TreeOption { + children?: CameraSubAreaNodeOption[]; + stats: CountStats; + site: VimpStation; +} + +export interface CameraSiteNodeOption extends TreeOption { + children?: CameraAreaNodeOption[]; + stats: CountStats; + online: boolean; +} + +export interface CameraLineTabPane extends TabPaneProps { + lineCode: string; + lineName: string; + cameraTree: CameraSiteNodeOption[]; +} + +// ========================================== +// 警报器树相关类型 +// ========================================== +export interface AlarmNodeOption extends TreeOption { + alarm: VimpChannel; + type: string; + site: VimpStation; +} + +export interface AlarmSubAreaNodeOption extends TreeOption { + children?: AlarmNodeOption[]; + stats: CountStats; + site: VimpStation; +} + +export interface AlarmAreaNodeOption extends TreeOption { + children?: AlarmSubAreaNodeOption[]; + stats: CountStats; + site: VimpStation; +} + +export interface AlarmSiteNodeOption extends TreeOption { + children?: AlarmAreaNodeOption[]; + stats: CountStats; + online: boolean; +} + +export interface AlarmLineTabPane extends TabPaneProps { + lineCode: string; + lineName: string; + alarmTree: AlarmSiteNodeOption[]; +} diff --git a/src/pages/vimp/vimp-page.vue b/src/pages/vimp/vimp-page.vue new file mode 100644 index 0000000..fc8834f --- /dev/null +++ b/src/pages/vimp/vimp-page.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/src/router/index.ts b/src/router/index.ts index e48a374..1819bbe 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -60,6 +60,10 @@ const router = createRouter({ path: 'changelog', component: () => import('@/pages/system/changelog/changelog-page.vue'), }, + { + path: 'vimp', + component: () => import('@/pages/vimp/vimp-page.vue'), + }, { path: '/:pathMatch(.*)*', component: () => import('@/pages/system/error/not-found-page.vue'),