feat(vimp): 设备树原型
This commit is contained in:
@@ -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 { 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 { useSettingStore, useUnreadStore, useUserStore } from '@/stores';
|
||||||
import { useIsFetching, useIsMutating } from '@tanstack/vue-query';
|
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 {
|
import {
|
||||||
NBadge,
|
NBadge,
|
||||||
NButton,
|
NButton,
|
||||||
@@ -111,6 +111,11 @@ const menuOptions = computed<MenuOption[]>(() => [
|
|||||||
show: isLamp.value,
|
show: isLamp.value,
|
||||||
icon: renderIcon(KeyRoundIcon),
|
icon: renderIcon(KeyRoundIcon),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: () => h(RouterLink, { to: '/vimp' }, { default: () => '视频综合管理平台' }),
|
||||||
|
key: '/vimp',
|
||||||
|
icon: renderIcon(MonitorPlayIcon),
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const dropdownOptions: DropdownOption[] = [
|
const dropdownOptions: DropdownOption[] = [
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './model';
|
||||||
|
export * from './query';
|
||||||
|
export * from './request';
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<CodeLines>('/cdn/vimp/codes/codeLines.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 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 codeTrainAreas = buildTrainAreas();
|
||||||
|
|
||||||
|
const siteCamerasMap: Record<string, VimpChannel[]> = {};
|
||||||
|
const siteAlarmsMap: Record<string, VimpChannel[]> = {};
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import type { VimpChannel, VimpStation } from './model';
|
||||||
|
import type { AxiosError, AxiosRequestConfig, CreateAxiosDefaults } from 'axios';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface VimpResult<T = unknown> {
|
||||||
|
code: number;
|
||||||
|
data: T;
|
||||||
|
msg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type VimpResponse<T> = [err: AxiosError | null, data: T | null, resp: VimpResult<T> | null];
|
||||||
|
|
||||||
|
const createVimpClient = (config?: CreateAxiosDefaults) => {
|
||||||
|
const instance = axios.create(config);
|
||||||
|
|
||||||
|
const vimpPost = <T>(url: string, data?: AxiosRequestConfig['data'], options?: Partial<Omit<AxiosRequestConfig, 'data'>> & { retRaw?: boolean; upload?: boolean }): Promise<VimpResponse<T>> => {
|
||||||
|
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<T>]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
resolve([err as AxiosError, null, null]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
instance,
|
||||||
|
post: vimpPost,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const unwrapVimpResponse = <T>(resp: VimpResponse<T>) => {
|
||||||
|
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<VimpStation[]>(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<VimpChannel[]>(endpoint, { code, time: '' }, { signal });
|
||||||
|
const data = unwrapVimpResponse(resp);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type TreeProps } from 'naive-ui';
|
||||||
|
import { useVimpDeviceQuery } from '../api/query';
|
||||||
|
import type { VimpChannel, VimpStation } from '../api/model';
|
||||||
|
import { h, type CSSProperties } from 'vue';
|
||||||
|
import { hasOwn } from '@vueuse/core';
|
||||||
|
import { useAlarmStore } from '../stores';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
|
const { isLoading } = useVimpDeviceQuery();
|
||||||
|
|
||||||
|
const alarmStore = useAlarmStore();
|
||||||
|
const { lineTabPanes } = storeToRefs(alarmStore);
|
||||||
|
|
||||||
|
const selectedDeviceGbCode = defineModel<[string]>('selectedDeviceGbCode', { default: () => [''] });
|
||||||
|
|
||||||
|
const overrideNodeClickBehavior: TreeOverrideNodeClickBehavior = ({ option }) => {
|
||||||
|
const hasChildren = (option.children?.length ?? 0) > 0;
|
||||||
|
if (hasChildren) {
|
||||||
|
return 'toggleExpand';
|
||||||
|
} else {
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
|
||||||
|
// 是车站节点
|
||||||
|
if (hasOwn(option, 'online')) {
|
||||||
|
const siteOnline = option['online'] as boolean;
|
||||||
|
const siteNodeStyle: CSSProperties = {
|
||||||
|
opacity: siteOnline ? 1 : 0.5,
|
||||||
|
};
|
||||||
|
return h('div', { style: siteNodeStyle }, option.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是中间节点(一级/二级区域)
|
||||||
|
if (!hasOwn(option, 'device') && hasOwn(option, 'site')) {
|
||||||
|
const site = option['site'] as VimpStation;
|
||||||
|
const nodeStyle: CSSProperties = {
|
||||||
|
opacity: site.online ? 1 : 0.5,
|
||||||
|
};
|
||||||
|
return h('div', { style: nodeStyle }, option.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是警报器节点
|
||||||
|
if (hasOwn(option, 'alarm') && hasOwn(option, 'site')) {
|
||||||
|
const alarm = option['alarm'] as VimpChannel;
|
||||||
|
const site = option['site'] as VimpStation;
|
||||||
|
|
||||||
|
const alarmOnline = () => {
|
||||||
|
return alarm.status === 1 && site.online;
|
||||||
|
};
|
||||||
|
|
||||||
|
const alarmNodeStyle: CSSProperties = {
|
||||||
|
opacity: alarmOnline() ? 1 : 0.5,
|
||||||
|
cursor: alarmOnline() ? 'pointer' : 'not-allowed',
|
||||||
|
};
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
style: alarmNodeStyle,
|
||||||
|
draggable: alarm.status === 1,
|
||||||
|
onDblclick() {
|
||||||
|
if (alarm.status === 0) return;
|
||||||
|
selectedDeviceGbCode.value = [alarm.code];
|
||||||
|
window.$message.info(`查看警报器:${JSON.stringify({ code: alarm.code, name: alarm.name })}`);
|
||||||
|
},
|
||||||
|
onDragstart(event) {
|
||||||
|
if (alarm.status === 0) return;
|
||||||
|
console.log(event);
|
||||||
|
event.dataTransfer?.setData('type', 'alarm');
|
||||||
|
event.dataTransfer?.setData('code', alarm.code);
|
||||||
|
event.dataTransfer?.setData('name', alarm.name);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
alarm.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他节点(兜底,理论上不会走到这里)
|
||||||
|
return option.label;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<template v-if="isLoading">
|
||||||
|
<div>loading...</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="lineTabPanes.length === 1">
|
||||||
|
<NTree
|
||||||
|
block-line
|
||||||
|
block-node
|
||||||
|
show-line
|
||||||
|
virtual-scroll
|
||||||
|
style="height: 100%"
|
||||||
|
v-model:selected-keys="selectedDeviceGbCode"
|
||||||
|
:data="lineTabPanes.at(0)?.alarmTree"
|
||||||
|
:override-default-node-click-behavior="overrideNodeClickBehavior"
|
||||||
|
:render-label="renderNodeLabel"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-if="lineTabPanes.length > 1">
|
||||||
|
<NTabs :type="'card'" :placement="'left'" style="height: 100%">
|
||||||
|
<NTabPane v-for="{ lineCode, lineName, alarmTree } in lineTabPanes" :key="lineCode" :name="lineName" :tab="lineName">
|
||||||
|
<NTree
|
||||||
|
block-line
|
||||||
|
block-node
|
||||||
|
show-line
|
||||||
|
virtual-scroll
|
||||||
|
style="height: 100%"
|
||||||
|
v-model:selected-keys="selectedDeviceGbCode"
|
||||||
|
:data="alarmTree"
|
||||||
|
:override-default-node-click-behavior="overrideNodeClickBehavior"
|
||||||
|
:render-label="renderNodeLabel"
|
||||||
|
/>
|
||||||
|
</NTabPane>
|
||||||
|
</NTabs>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type TreeProps } from 'naive-ui';
|
||||||
|
import { useVimpDeviceQuery } from '../api/query';
|
||||||
|
import type { VimpChannel, VimpStation } from '../api/model';
|
||||||
|
import { h, type CSSProperties } from 'vue';
|
||||||
|
import { hasOwn } from '@vueuse/core';
|
||||||
|
import { useCameraStore } from '../stores';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
|
const { isLoading } = useVimpDeviceQuery();
|
||||||
|
|
||||||
|
const cameraStore = useCameraStore();
|
||||||
|
const { lineTabPanes } = storeToRefs(cameraStore);
|
||||||
|
|
||||||
|
const selectedDeviceGbCode = defineModel<[string]>('selectedDeviceGbCode', { default: () => [''] });
|
||||||
|
|
||||||
|
const overrideNodeClickBehavior: TreeOverrideNodeClickBehavior = ({ option }) => {
|
||||||
|
const hasChildren = (option.children?.length ?? 0) > 0;
|
||||||
|
if (hasChildren) {
|
||||||
|
return 'toggleExpand';
|
||||||
|
} else {
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
|
||||||
|
// 是车站节点
|
||||||
|
if (hasOwn(option, 'online')) {
|
||||||
|
const siteOnline = option['online'] as boolean;
|
||||||
|
const siteNodeStyle: CSSProperties = {
|
||||||
|
opacity: siteOnline ? 1 : 0.5,
|
||||||
|
};
|
||||||
|
return h('div', { style: siteNodeStyle }, option.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是中间节点(一级/二级区域)
|
||||||
|
if (!hasOwn(option, 'device') && hasOwn(option, 'site')) {
|
||||||
|
const site = option['site'] as VimpStation;
|
||||||
|
const nodeStyle: CSSProperties = {
|
||||||
|
opacity: site.online ? 1 : 0.5,
|
||||||
|
};
|
||||||
|
return h('div', { style: nodeStyle }, option.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是摄像机节点
|
||||||
|
if (hasOwn(option, 'camera') && hasOwn(option, 'site')) {
|
||||||
|
const camera = option['camera'] as VimpChannel;
|
||||||
|
const site = option['site'] as VimpStation;
|
||||||
|
|
||||||
|
const cameraOnline = () => {
|
||||||
|
return camera.status === 1 && site.online;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cameraNodeStyle: CSSProperties = {
|
||||||
|
opacity: cameraOnline() ? 1 : 0.5,
|
||||||
|
cursor: cameraOnline() ? 'pointer' : 'not-allowed',
|
||||||
|
};
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
style: cameraNodeStyle,
|
||||||
|
draggable: camera.status === 1,
|
||||||
|
onDblclick() {
|
||||||
|
if (camera.status === 0) return;
|
||||||
|
selectedDeviceGbCode.value = [camera.code];
|
||||||
|
window.$message.info(`播放:${JSON.stringify({ code: camera.code, name: camera.name })}`);
|
||||||
|
},
|
||||||
|
onDragstart(event) {
|
||||||
|
if (camera.status === 0) return;
|
||||||
|
console.log(event);
|
||||||
|
event.dataTransfer?.setData('type', 'camera');
|
||||||
|
event.dataTransfer?.setData('code', camera.code);
|
||||||
|
event.dataTransfer?.setData('name', camera.name);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
camera.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他节点(兜底,理论上不会走到这里)
|
||||||
|
return option.label;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<template v-if="isLoading">
|
||||||
|
<div>loading...</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="lineTabPanes.length === 1">
|
||||||
|
<NTree
|
||||||
|
block-line
|
||||||
|
block-node
|
||||||
|
show-line
|
||||||
|
virtual-scroll
|
||||||
|
style="height: 100%"
|
||||||
|
v-model:selected-keys="selectedDeviceGbCode"
|
||||||
|
:data="lineTabPanes.at(0)?.cameraTree"
|
||||||
|
:override-default-node-click-behavior="overrideNodeClickBehavior"
|
||||||
|
:render-label="renderNodeLabel"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-if="lineTabPanes.length > 1">
|
||||||
|
<NTabs :type="'card'" :placement="'left'" style="height: 100%">
|
||||||
|
<NTabPane v-for="{ lineCode, lineName, cameraTree } in lineTabPanes" :key="lineCode" :name="lineName" :tab="lineName">
|
||||||
|
<NTree
|
||||||
|
block-line
|
||||||
|
block-node
|
||||||
|
show-line
|
||||||
|
virtual-scroll
|
||||||
|
style="height: 100%"
|
||||||
|
v-model:selected-keys="selectedDeviceGbCode"
|
||||||
|
:data="cameraTree"
|
||||||
|
:override-default-node-click-behavior="overrideNodeClickBehavior"
|
||||||
|
:render-label="renderNodeLabel"
|
||||||
|
/>
|
||||||
|
</NTabPane>
|
||||||
|
</NTabs>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -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<string, VimpChannel[]>;
|
||||||
|
codeLines: CodeLines;
|
||||||
|
codeSites: CodeSites;
|
||||||
|
codeStationAreas: CodeArea[];
|
||||||
|
codeParkingAreas: CodeArea[];
|
||||||
|
codeOccAreas: CodeArea[];
|
||||||
|
codeTrainAreas: CodeArea[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAlarmStore = defineStore('vimp-alarm', () => {
|
||||||
|
const lineTabPanes = ref<AlarmLineTabPane[]>([]);
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -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<string, VimpChannel[]>;
|
||||||
|
codeLines: CodeLines;
|
||||||
|
codeSites: CodeSites;
|
||||||
|
codeStationAreas: CodeArea[];
|
||||||
|
codeParkingAreas: CodeArea[];
|
||||||
|
codeOccAreas: CodeArea[];
|
||||||
|
codeTrainAreas: CodeArea[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCameraStore = defineStore('vimp-camera', () => {
|
||||||
|
const lineTabPanes = ref<CameraLineTabPane[]>([]);
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './camera';
|
||||||
|
export * from './alarm';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './tree';
|
||||||
@@ -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<string, { name: string; color: string }>;
|
||||||
|
export type CodeSites = Record<string, { name: string; type: SiteType }>;
|
||||||
|
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[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { NTabPane, NTabs, type TabPaneProps } from 'naive-ui';
|
||||||
|
import { ref, type Component } from 'vue';
|
||||||
|
import CameraTree from './components/camera-tree.vue';
|
||||||
|
import AlarmTree from './components/alarm-tree.vue';
|
||||||
|
|
||||||
|
interface ResourceTabPane extends TabPaneProps {
|
||||||
|
name: string;
|
||||||
|
tab: string;
|
||||||
|
component?: Component;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourceTabPanes: ResourceTabPane[] = [
|
||||||
|
{ name: 'camera', tab: '摄像头', component: CameraTree },
|
||||||
|
{ name: 'alarm', tab: '警报器', component: AlarmTree },
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedDeviceGbCode = ref<[string]>(['']);
|
||||||
|
|
||||||
|
const onDragover = (event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (event.dataTransfer) {
|
||||||
|
event.dataTransfer.dropEffect = 'copy';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDrop = (event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const type = event.dataTransfer?.getData('type');
|
||||||
|
if (!type) return;
|
||||||
|
if (type === 'camera') {
|
||||||
|
const code = event.dataTransfer?.getData('code');
|
||||||
|
if (!code) return;
|
||||||
|
const name = event.dataTransfer?.getData('name');
|
||||||
|
selectedDeviceGbCode.value = [code];
|
||||||
|
window.$message.info(`播放:${JSON.stringify({ code, name })}`);
|
||||||
|
} else if (type === 'alarm') {
|
||||||
|
const code = event.dataTransfer?.getData('code');
|
||||||
|
if (!code) return;
|
||||||
|
const name = event.dataTransfer?.getData('name');
|
||||||
|
selectedDeviceGbCode.value = [code];
|
||||||
|
window.$message.info(`查看警报器:${JSON.stringify({ code, name })}`);
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div style="height: 100%; overflow: hidden; display: flex">
|
||||||
|
<div style="width: 540px; height: 100%; overflow: hidden">
|
||||||
|
<NTabs :type="'line'" :placement="'left'" style="height: 100%">
|
||||||
|
<NTabPane v-for="{ name: resourceName, tab: resourceTab, component } in resourceTabPanes" :key="resourceName" :tab="resourceTab" :name="resourceName">
|
||||||
|
<template v-if="!!component">
|
||||||
|
<component :is="component" v-model:selected-device-gb-code="selectedDeviceGbCode" />
|
||||||
|
</template>
|
||||||
|
</NTabPane>
|
||||||
|
</NTabs>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1">
|
||||||
|
<div style="height: 480px; background-color: #666; display: grid; place-items: center" @dragover="onDragover" @drop="onDrop">
|
||||||
|
<div>这里是播放器</div>
|
||||||
|
<div>{{ selectedDeviceGbCode }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -60,6 +60,10 @@ const router = createRouter({
|
|||||||
path: 'changelog',
|
path: 'changelog',
|
||||||
component: () => import('@/pages/system/changelog/changelog-page.vue'),
|
component: () => import('@/pages/system/changelog/changelog-page.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'vimp',
|
||||||
|
component: () => import('@/pages/vimp/vimp-page.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
component: () => import('@/pages/system/error/not-found-page.vue'),
|
component: () => import('@/pages/system/error/not-found-page.vue'),
|
||||||
|
|||||||
Reference in New Issue
Block a user