Compare commits
35 Commits
7467b54834
..
vimp
| Author | SHA1 | Date | |
|---|---|---|---|
| f78d3b32c0 | |||
| ad9a0011a5 | |||
| 8403a41cef | |||
| e8aa9b96f8 | |||
| 6f14370751 | |||
| 6fed499d78 | |||
| fa44554593 | |||
| cbeddebbc0 | |||
| c78c8b8419 | |||
| 148be10186 | |||
| 650ca78464 | |||
| 64449a22c5 | |||
| 0c7f3153ce | |||
| b9c4a3ca27 | |||
| 12c431b0ec | |||
| 176e35609f | |||
| 7cd59956b1 | |||
| df1c7deead | |||
| 3e88379eb9 | |||
| 9827eddae2 | |||
| 5b2315981e | |||
| c8eed3d2d1 | |||
| 7f5aa7bb82 | |||
| 0b1a0546dd | |||
| 7b15300ab7 | |||
| 8fae86d6ff | |||
| 2f38e97481 | |||
| d5b380e1e3 | |||
| bd1cc0483b | |||
| 67dccc5011 | |||
| a0048411a4 | |||
| d177956edd | |||
| d1b973be15 | |||
| 65603d469d | |||
| a92e47bc18 |
@@ -716,6 +716,12 @@
|
||||
"018507": { "name": "康桥站", "type": "station" },
|
||||
"018508": { "name": "御桥站", "type": "station" },
|
||||
|
||||
"021509": { "name": "申江南路", "type": "station" },
|
||||
"021575": { "name": "OCC", "type": "occ" },
|
||||
"021609": { "name": "申江南路", "type": "station" },
|
||||
"021675": { "name": "OCC", "type": "occ" },
|
||||
"021680": { "name": "六陈路车辆段", "type": "parking" },
|
||||
|
||||
"051501": { "name": "沈杜公路", "type": "station" },
|
||||
"051502": { "name": "三鲁公路", "type": "station" },
|
||||
"051503": { "name": "闵瑞路", "type": "station" },
|
||||
|
||||
@@ -13,9 +13,27 @@ import destr from 'destr';
|
||||
import { isFunction } from 'es-toolkit';
|
||||
import localforage from 'localforage';
|
||||
import { DownloadIcon, Trash2Icon, UploadIcon } from 'lucide-vue-next';
|
||||
import { NButton, NButtonGroup, NDivider, NDrawer, NDrawerContent, NDropdown, NFlex, NFormItem, NIcon, NInput, NInputNumber, NModal, NSwitch, NText, NTooltip, type DropdownOption } from 'naive-ui';
|
||||
import {
|
||||
NButton,
|
||||
NButtonGroup,
|
||||
NDivider,
|
||||
NDrawer,
|
||||
NDrawerContent,
|
||||
NDropdown,
|
||||
NFlex,
|
||||
NFormItem,
|
||||
NIcon,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NModal,
|
||||
NSwitch,
|
||||
NText,
|
||||
NTooltip,
|
||||
type DropdownOption,
|
||||
type InputInst,
|
||||
} from 'naive-ui';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
@@ -149,8 +167,14 @@ useEventListener('keydown', (event) => {
|
||||
});
|
||||
|
||||
const expectToShowDebugCodeInput = ref(false);
|
||||
const debugCodeInputRef = useTemplateRef<InputInst>('debug-code-input-ref');
|
||||
const onModalAfterEnter = () => {
|
||||
expectToShowDebugCodeInput.value = !debugMode.value;
|
||||
if (expectToShowDebugCodeInput.value) {
|
||||
nextTick(() => {
|
||||
debugCodeInputRef.value?.focus();
|
||||
});
|
||||
}
|
||||
};
|
||||
const onModalAfterLeave = () => {
|
||||
expectToShowDebugCodeInput.value = false;
|
||||
@@ -412,7 +436,7 @@ const onClickVersion = () => {
|
||||
<NText v-else>确认关闭调试模式</NText>
|
||||
</template>
|
||||
<template #default>
|
||||
<NInput v-if="expectToShowDebugCodeInput" v-model:value="debugCode" placeholder="输入调试码" @keyup.enter="enableDebugMode" />
|
||||
<NInput ref="debug-code-input-ref" v-if="expectToShowDebugCodeInput" v-model:value="debugCode" placeholder="输入调试码" @keyup.enter="enableDebugMode" />
|
||||
</template>
|
||||
<template #action>
|
||||
<NButton @click="showDebugCodeModal = false">取消</NButton>
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
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;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
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 @@
|
||||
export * from './vimp-client';
|
||||
@@ -0,0 +1,152 @@
|
||||
import type { AxiosError, AxiosRequestConfig, AxiosResponse, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios';
|
||||
import axios, { isAxiosError } from 'axios';
|
||||
import type { VimpResponse, VimpResult } from '../../types';
|
||||
import { useUserStore } from '@/stores';
|
||||
import { getAppEnvConfig } from '@/utils';
|
||||
import router from '@/router';
|
||||
|
||||
export interface VimpRequestOptions extends CreateAxiosDefaults {
|
||||
requestInterceptor?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>;
|
||||
responseInterceptor?: (resp: AxiosResponse) => AxiosResponse | Promise<AxiosResponse>;
|
||||
responseErrorInterceptor?: (error: any) => any;
|
||||
}
|
||||
|
||||
export const createVimpClient = (config?: VimpRequestOptions) => {
|
||||
const defaultRequestInterceptor = (config: InternalAxiosRequestConfig) => config;
|
||||
const defaultResponseInterceptor = (response: AxiosResponse) => response;
|
||||
const defaultResponseErrorInterceptor = (error: any) => {
|
||||
if (isAxiosError(error)) {
|
||||
if (error.status === 401) {
|
||||
// 处理 401 错误
|
||||
}
|
||||
if (error.status === 404) {
|
||||
// 处理 404 错误
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
};
|
||||
|
||||
const requestInterceptor = config?.requestInterceptor ?? defaultRequestInterceptor;
|
||||
const responseInterceptor = config?.responseInterceptor ?? defaultResponseInterceptor;
|
||||
const responseErrorInterceptor = config?.responseErrorInterceptor ?? defaultResponseErrorInterceptor;
|
||||
|
||||
const instance = axios.create(config);
|
||||
instance.interceptors.request.use(requestInterceptor);
|
||||
instance.interceptors.response.use(responseInterceptor, responseErrorInterceptor);
|
||||
|
||||
const vimpGet = <T>(url: string, options?: AxiosRequestConfig & { retRaw?: boolean }): Promise<VimpResponse<T>> => {
|
||||
const { retRaw, ...reqConfig } = options ?? {};
|
||||
return new Promise((resolve) => {
|
||||
instance
|
||||
.get(url, {
|
||||
...reqConfig,
|
||||
})
|
||||
.then((res) => {
|
||||
if (retRaw) {
|
||||
resolve([null, res.data as T, null]);
|
||||
} else {
|
||||
const resData = res.data as VimpResult<T>;
|
||||
resolve([null, resData.data, resData]);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
resolve([err as AxiosError, null, null]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const httpPut = <T>(url: string, data?: AxiosRequestConfig['data'], options?: Partial<Omit<AxiosRequestConfig, 'data'>>): Promise<VimpResponse<T>> => {
|
||||
const reqConfig = options ?? {};
|
||||
return new Promise((resolve) => {
|
||||
instance
|
||||
.put<VimpResult<T>>(url, data, { ...reqConfig })
|
||||
.then((res) => {
|
||||
resolve([null, res.data.data, res.data]);
|
||||
})
|
||||
.catch((err) => {
|
||||
resolve([err as AxiosError, null, null]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const httpDelete = <T>(url: string, idList: string[], options?: Partial<Omit<AxiosRequestConfig, 'data'>>): Promise<VimpResponse<T>> => {
|
||||
const reqConfig = options ?? {};
|
||||
return new Promise((resolve) => {
|
||||
instance
|
||||
.delete<VimpResult<T>>(url, { ...reqConfig, data: idList })
|
||||
.then((res) => {
|
||||
resolve([null, res.data.data, res.data]);
|
||||
})
|
||||
.catch((err) => {
|
||||
resolve([err as AxiosError, null, null]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
instance,
|
||||
get: vimpGet,
|
||||
post: vimpPost,
|
||||
put: httpPut,
|
||||
delete: httpDelete,
|
||||
};
|
||||
};
|
||||
|
||||
export 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`,
|
||||
requestInterceptor: (config) => {
|
||||
const userStore = useUserStore();
|
||||
const { lampAuthorization, lampClientId, lampClientSecret } = getAppEnvConfig();
|
||||
const newAuthorization = window.btoa(`${lampClientId}:${lampClientSecret}`);
|
||||
const authorization = lampAuthorization.trim() !== '' ? lampAuthorization : newAuthorization;
|
||||
config.headers.set('accept-language', 'zh-CN,zh;q=0.9');
|
||||
config.headers.set('accept', 'application/json, text/plain, */*');
|
||||
config.headers.set('Applicationid', '');
|
||||
config.headers.set('Tenantid', '1');
|
||||
config.headers.set('Authorization', authorization);
|
||||
config.headers.set('token', userStore.userLoginResult?.token ?? '');
|
||||
return config;
|
||||
},
|
||||
responseInterceptor: (response) => {
|
||||
return response;
|
||||
},
|
||||
responseErrorInterceptor: (error) => {
|
||||
const err = error as AxiosError;
|
||||
if (err.response?.status === 401) {
|
||||
window.$message.error('登录超时,请重新登录');
|
||||
const userStore = useUserStore();
|
||||
userStore.resetStore();
|
||||
router.push({ path: '/login' });
|
||||
}
|
||||
return Promise.reject(error);
|
||||
},
|
||||
});
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './client';
|
||||
export * from './model';
|
||||
export * from './query';
|
||||
export * from './request';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './vimp-channel';
|
||||
export * from './vimp-site';
|
||||
@@ -1,9 +1,3 @@
|
||||
export interface VimpStation {
|
||||
code: string;
|
||||
name: string;
|
||||
online: boolean;
|
||||
}
|
||||
|
||||
export interface VimpChannel {
|
||||
address: string;
|
||||
block: string;
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface VimpRawSite {
|
||||
code: string;
|
||||
name: string;
|
||||
online?: boolean;
|
||||
}
|
||||
|
||||
export interface VimpSite {
|
||||
code: string;
|
||||
name: string;
|
||||
online: boolean;
|
||||
}
|
||||
|
||||
export const normalizeVimpSite = (site: VimpRawSite): VimpSite => ({
|
||||
code: site.code,
|
||||
name: site.name,
|
||||
online: site.online ?? true,
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { unwrapVimpResponse, vimpClient } from '../client';
|
||||
import { normalizeVimpSite, type VimpRawSite } from '../model';
|
||||
|
||||
export const catalogAllDeviceApi = async (options?: { signal?: AbortSignal }) => {
|
||||
const { signal } = options ?? {};
|
||||
const client = vimpClient;
|
||||
const endpoint = `/catalog/allDevice`;
|
||||
const resp = await client.post<VimpRawSite[]>(endpoint, {}, { signal });
|
||||
const data = unwrapVimpResponse(resp);
|
||||
return data?.map(normalizeVimpSite) ?? null;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { unwrapVimpResponse, vimpClient } from '../client';
|
||||
import type { VimpChannel } from '../model';
|
||||
|
||||
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,2 @@
|
||||
export * from './catalog.channel';
|
||||
export * from './catalog.all-device';
|
||||
@@ -1,19 +1,17 @@
|
||||
<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 { NIcon, NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type TreeProps } from 'naive-ui';
|
||||
import { h, type CSSProperties } from 'vue';
|
||||
import { hasOwn } from '@vueuse/core';
|
||||
import { useAlarmStore } from '../stores';
|
||||
import { useAlarmStore, useResourcePanelStore } from '../stores';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useChannelsQuery } from '../composables';
|
||||
import { isAlarmNode, isAlarmSiteNode, isAlarmAreaNode } from '../types';
|
||||
import { SirenIcon } from 'lucide-vue-next';
|
||||
|
||||
const { isLoading } = useVimpDeviceQuery();
|
||||
const { isLoading } = useChannelsQuery();
|
||||
|
||||
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) {
|
||||
@@ -25,8 +23,8 @@ const overrideNodeClickBehavior: TreeOverrideNodeClickBehavior = ({ option }) =>
|
||||
|
||||
const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
|
||||
// 是车站节点
|
||||
if (hasOwn(option, 'online')) {
|
||||
const siteOnline = option['online'] as boolean;
|
||||
if (isAlarmSiteNode(option)) {
|
||||
const siteOnline = option.online;
|
||||
const siteNodeStyle: CSSProperties = {
|
||||
opacity: siteOnline ? 1 : 0.5,
|
||||
};
|
||||
@@ -34,8 +32,8 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
|
||||
}
|
||||
|
||||
// 是中间节点(一级/二级区域)
|
||||
if (!hasOwn(option, 'device') && hasOwn(option, 'site')) {
|
||||
const site = option['site'] as VimpStation;
|
||||
if (isAlarmAreaNode(option)) {
|
||||
const site = option.site;
|
||||
const nodeStyle: CSSProperties = {
|
||||
opacity: site.online ? 1 : 0.5,
|
||||
};
|
||||
@@ -43,9 +41,9 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
|
||||
}
|
||||
|
||||
// 是警报器节点
|
||||
if (hasOwn(option, 'alarm') && hasOwn(option, 'site')) {
|
||||
const alarm = option['alarm'] as VimpChannel;
|
||||
const site = option['site'] as VimpStation;
|
||||
if (isAlarmNode(option)) {
|
||||
const alarm = option.alarm;
|
||||
const site = option.site;
|
||||
|
||||
const alarmOnline = () => {
|
||||
return alarm.status === 1 && site.online;
|
||||
@@ -54,32 +52,57 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
|
||||
const alarmNodeStyle: CSSProperties = {
|
||||
opacity: alarmOnline() ? 1 : 0.5,
|
||||
cursor: alarmOnline() ? 'pointer' : 'not-allowed',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
};
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
style: alarmNodeStyle,
|
||||
draggable: alarm.status === 1,
|
||||
draggable: alarmOnline(),
|
||||
onDblclick() {
|
||||
if (alarm.status === 0) return;
|
||||
selectedDeviceGbCode.value = [alarm.code];
|
||||
if (!alarmOnline()) return;
|
||||
window.$message.info(`查看警报器:${JSON.stringify({ code: alarm.code, name: alarm.name })}`);
|
||||
},
|
||||
onDragstart(event) {
|
||||
if (alarm.status === 0) return;
|
||||
if (!alarmOnline()) return;
|
||||
console.log(event);
|
||||
event.dataTransfer?.setData('type', 'alarm');
|
||||
event.dataTransfer?.setData('code', alarm.code);
|
||||
event.dataTransfer?.setData('name', alarm.name);
|
||||
},
|
||||
},
|
||||
alarm.name,
|
||||
[h(NIcon, () => h(SirenIcon)), h('span', alarm.name)],
|
||||
);
|
||||
}
|
||||
|
||||
// 其他节点(兜底,理论上不会走到这里)
|
||||
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 { searchPattern } = storeToRefs(resourcePanelStore);
|
||||
|
||||
const searchFilter: TreeProps['filter'] = (pattern, node) => {
|
||||
if (!isAlarmNode(node)) return false;
|
||||
return node.alarm.name.includes(pattern);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -93,26 +116,53 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
|
||||
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"
|
||||
:render-suffix="renderNodeSuffix"
|
||||
:override-default-node-click-behavior="overrideNodeClickBehavior"
|
||||
:default-expand-all="searchPattern.trim().length > 0"
|
||||
:show-irrelevant-nodes="false"
|
||||
:data="lineTabPanes.at(0)?.alarmTree"
|
||||
:pattern="searchPattern"
|
||||
:filter="searchFilter"
|
||||
/>
|
||||
</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"
|
||||
/>
|
||||
<NTabs
|
||||
:type="'card'"
|
||||
:placement="'left'"
|
||||
style="height: 100%"
|
||||
:tab-style="{
|
||||
width: '64px',
|
||||
height: '36px',
|
||||
}"
|
||||
:style="{
|
||||
'--n-bar-color': '#0000',
|
||||
'--n-pane-padding-top': '0',
|
||||
'--n-tab-gap-vertical': '0',
|
||||
// '--n-tab-padding-vertical': '14px 12px'
|
||||
}"
|
||||
>
|
||||
<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>
|
||||
</NTabs>
|
||||
</template>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<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 { NIcon, NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type TreeProps } from 'naive-ui';
|
||||
import { h, type CSSProperties } from 'vue';
|
||||
import { hasOwn } from '@vueuse/core';
|
||||
import { useCameraStore } from '../stores';
|
||||
import { useCameraStore, useResourcePanelStore } from '../stores';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useChannelsQuery } from '../composables';
|
||||
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 } = useVimpDeviceQuery();
|
||||
const { isLoading } = useChannelsQuery();
|
||||
|
||||
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) {
|
||||
@@ -25,8 +25,8 @@ const overrideNodeClickBehavior: TreeOverrideNodeClickBehavior = ({ option }) =>
|
||||
|
||||
const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
|
||||
// 是车站节点
|
||||
if (hasOwn(option, 'online')) {
|
||||
const siteOnline = option['online'] as boolean;
|
||||
if (isCameraSiteNode(option)) {
|
||||
const siteOnline = option.online;
|
||||
const siteNodeStyle: CSSProperties = {
|
||||
opacity: siteOnline ? 1 : 0.5,
|
||||
};
|
||||
@@ -34,8 +34,8 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
|
||||
}
|
||||
|
||||
// 是中间节点(一级/二级区域)
|
||||
if (!hasOwn(option, 'device') && hasOwn(option, 'site')) {
|
||||
const site = option['site'] as VimpStation;
|
||||
if (isCameraAreaNode(option)) {
|
||||
const site = option.site;
|
||||
const nodeStyle: CSSProperties = {
|
||||
opacity: site.online ? 1 : 0.5,
|
||||
};
|
||||
@@ -43,9 +43,9 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
|
||||
}
|
||||
|
||||
// 是摄像机节点
|
||||
if (hasOwn(option, 'camera') && hasOwn(option, 'site')) {
|
||||
const camera = option['camera'] as VimpChannel;
|
||||
const site = option['site'] as VimpStation;
|
||||
if (isCameraNode(option)) {
|
||||
const camera = option.camera;
|
||||
const site = option.site;
|
||||
|
||||
const cameraOnline = () => {
|
||||
return camera.status === 1 && site.online;
|
||||
@@ -54,32 +54,58 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
|
||||
const cameraNodeStyle: CSSProperties = {
|
||||
opacity: cameraOnline() ? 1 : 0.5,
|
||||
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(
|
||||
'div',
|
||||
{
|
||||
style: cameraNodeStyle,
|
||||
draggable: camera.status === 1,
|
||||
draggable: cameraOnline(),
|
||||
onDblclick() {
|
||||
if (camera.status === 0) return;
|
||||
selectedDeviceGbCode.value = [camera.code];
|
||||
if (!cameraOnline()) return;
|
||||
window.$message.info(`播放:${JSON.stringify({ code: camera.code, name: camera.name })}`);
|
||||
},
|
||||
onDragstart(event) {
|
||||
if (camera.status === 0) return;
|
||||
if (!cameraOnline()) return;
|
||||
console.log(event);
|
||||
event.dataTransfer?.setData('type', 'camera');
|
||||
event.dataTransfer?.setData('code', camera.code);
|
||||
event.dataTransfer?.setData('name', camera.name);
|
||||
},
|
||||
},
|
||||
camera.name,
|
||||
[cameraIcon, h('span', camera.name)],
|
||||
);
|
||||
}
|
||||
|
||||
// 其他节点(兜底,理论上不会走到这里)
|
||||
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 { searchPattern } = storeToRefs(resourcePanelStore);
|
||||
|
||||
const searchFilter: TreeProps['filter'] = (pattern, node) => {
|
||||
if (!isCameraNode(node)) return false;
|
||||
return node.camera.name.includes(pattern);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -93,26 +119,53 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
|
||||
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"
|
||||
:render-suffix="renderNodeSuffix"
|
||||
:override-default-node-click-behavior="overrideNodeClickBehavior"
|
||||
:default-expand-all="searchPattern.trim().length > 0"
|
||||
:show-irrelevant-nodes="false"
|
||||
:data="lineTabPanes.at(0)?.cameraTree"
|
||||
:pattern="searchPattern"
|
||||
:filter="searchFilter"
|
||||
/>
|
||||
</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"
|
||||
/>
|
||||
<NTabs
|
||||
:type="'card'"
|
||||
:placement="'left'"
|
||||
style="height: 100%"
|
||||
:tab-style="{
|
||||
width: '64px',
|
||||
height: '36px',
|
||||
}"
|
||||
:style="{
|
||||
'--n-bar-color': '#0000',
|
||||
'--n-pane-padding-top': '0',
|
||||
'--n-tab-gap-vertical': '0',
|
||||
// '--n-tab-padding-vertical': '14px 12px'
|
||||
}"
|
||||
>
|
||||
<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>
|
||||
</NTabs>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg class="icon" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="m6.03 12.03l2 3.47l-2.53 3.18L2 12.62l4.03-.59M17 18v-2.71c.88-.39 1.5-1.26 1.5-2.29c0-.57-.2-1.1-.53-1.5l1.97-1.15c1.01-.59 1.36-1.88.77-2.89l-1.38-2.4a2.125 2.125 0 0 0-2.89-.78L8.31 9c-.95.53-1.28 1.75-.73 2.71l1.5 2.6c.55.95 1.78 1.28 2.73.73l1.88-1.08c.25.59.72 1.07 1.31 1.33V18c0 1.1.9 2 2 2h5v-2h-5Z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<svg class="icon" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-width="4">
|
||||
<path d="M8 10v14c0 8.837 7.163 16 16 16s16-7.163 16-16V10" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 10h40" />
|
||||
<path stroke-linejoin="round" d="M24 30a6 6 0 1 0 0-12a6 6 0 0 0 0 12Z" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg class="icon" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2 4.5A1.5 1.5 0 0 1 3.5 3h13a1.5 1.5 0 0 1 0 3h-13A1.5 1.5 0 0 1 2 4.5ZM10 9a3 3 0 1 0 0 6a3 3 0 0 0 0-6Zm-2 3a2 2 0 1 1 4 0a2 2 0 0 1-4 0ZM3 7h14v4a7 7 0 1 1-14 0V7Zm7 1a4 4 0 1 0 0 8a4 4 0 0 0 0-8Z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,146 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NIcon, NInput, NTabPane, NTabs, NText, type TabPaneProps } from 'naive-ui';
|
||||
import { computed, ref, type Component } from 'vue';
|
||||
import AlarmTree from './alarm-tree.vue';
|
||||
import CameraTree from './camera-tree.vue';
|
||||
import { ChevronLeftIcon, MapPinnedIcon, MonitorIcon, SirenIcon, WandSparklesIcon } from 'lucide-vue-next';
|
||||
import { useResourcePanelStore } from '../stores';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import BulletCamera from './icon/bullet-camera.vue';
|
||||
|
||||
const PANEL_WIDTH_EXPANDED = '480px';
|
||||
const PANEL_WIDTH_COLLAPSED = '72px';
|
||||
|
||||
interface ResourceTabPane extends TabPaneProps {
|
||||
name: string;
|
||||
tab: string;
|
||||
icon?: Component;
|
||||
component?: Component;
|
||||
}
|
||||
|
||||
const resourceTabPanes: ResourceTabPane[] = [
|
||||
{ name: 'camera', tab: '摄像头', icon: BulletCamera, component: CameraTree },
|
||||
{ name: 'alarm', tab: '警报器', icon: SirenIcon, component: AlarmTree },
|
||||
{ name: 'monitor', tab: '监视器', icon: MonitorIcon },
|
||||
{ name: 'combine-tech', tab: '复合技', icon: WandSparklesIcon },
|
||||
{ name: 'leaflet-map', tab: '地图', icon: MapPinnedIcon },
|
||||
];
|
||||
|
||||
const activeTabName = ref(resourceTabPanes.at(0)?.name ?? '');
|
||||
|
||||
const showSearchInput = computed(() => {
|
||||
return ['camera', 'alarm'].includes(activeTabName.value);
|
||||
});
|
||||
|
||||
const resourcePanelStore = useResourcePanelStore();
|
||||
const { collapsed, searchPattern } = storeToRefs(resourcePanelStore);
|
||||
|
||||
const collapseResourcePanel = () => {
|
||||
if (!collapsed.value) {
|
||||
resourcePanelStore.toggleCollapsed();
|
||||
}
|
||||
};
|
||||
|
||||
const expandResourcePanel = () => {
|
||||
if (collapsed.value) {
|
||||
resourcePanelStore.toggleCollapsed();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="resource-panel__wrapper"
|
||||
:style="{
|
||||
width: collapsed ? PANEL_WIDTH_COLLAPSED : PANEL_WIDTH_EXPANDED,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="resource-panel"
|
||||
:style="{
|
||||
width: PANEL_WIDTH_EXPANDED,
|
||||
}"
|
||||
>
|
||||
<div class="resource-panel__title">
|
||||
<div style="display: grid; place-items: center" :style="{ width: PANEL_WIDTH_COLLAPSED }">
|
||||
<NText>资源</NText>
|
||||
</div>
|
||||
<template v-if="showSearchInput">
|
||||
<div style="width: 240px; margin-left: auto">
|
||||
<NInput clearable :size="'tiny'" :placeholder="'搜索'" v-model:value="searchPattern" />
|
||||
</div>
|
||||
</template>
|
||||
<div style="margin: 0px 16px; display: grid; place-items: center" :style="{ marginLeft: showSearchInput ? '8px' : 'auto' }">
|
||||
<NButton text @click="collapseResourcePanel">
|
||||
<NIcon :component="ChevronLeftIcon"></NIcon>
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-panel__tabs-wrapper">
|
||||
<NTabs
|
||||
:type="'bar'"
|
||||
:size="'small'"
|
||||
:placement="'left'"
|
||||
v-model:value="activeTabName"
|
||||
:tab-style="{
|
||||
height: '64px',
|
||||
width: '72px',
|
||||
}"
|
||||
:style="{
|
||||
height: '100%', // 为了确保 tabs 高度和 panel 高度一致,否则设备树会超出 panel 高度,导致虚拟滚动失效
|
||||
'--n-pane-padding-top': '0',
|
||||
'--n-tab-gap-vertical': '0',
|
||||
// '--n-tab-padding-vertical': '14px 6px',
|
||||
}"
|
||||
>
|
||||
<NTabPane
|
||||
v-for="{ name: resourceName, tab: resourceTab, icon: resourceIcon, component } in resourceTabPanes"
|
||||
:key="resourceName"
|
||||
:name="resourceName"
|
||||
:tab-props="{ onClick: () => expandResourcePanel() }"
|
||||
>
|
||||
<template #tab>
|
||||
<div style="width: 48px; display: flex; flex-direction: column; justify-content: center; align-items: center">
|
||||
<NIcon :size="18" :component="resourceIcon"></NIcon>
|
||||
<div style="font-size: 12px">{{ resourceTab }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<template v-if="!!component">
|
||||
<component :is="component" />
|
||||
</template>
|
||||
</template>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.resource-panel__wrapper {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
transition: width 0.3s ease;
|
||||
|
||||
.resource-panel {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
&__title {
|
||||
min-height: 42px;
|
||||
max-height: 42px;
|
||||
padding: 8px 0px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__tabs-wrapper {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
export * from './query';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './use-channels-query';
|
||||
@@ -0,0 +1,151 @@
|
||||
import { useQuery } from '@tanstack/vue-query';
|
||||
import { computed } from 'vue';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import axios from 'axios';
|
||||
import { compileCodeAreas, type CodeArea, type CodeLines, type CodeSites } from '../../types';
|
||||
import { useCameraStore, useAlarmStore } from '../../stores';
|
||||
import { catalogAllDeviceApi, catalogChannelApi, type VimpChannel, type VimpSite } from '../../apis';
|
||||
import { VIMP_CHANNELS_QUERY_KEY } from '../../constants';
|
||||
|
||||
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: VimpSite[]) => {
|
||||
sites.sort(compareByCode);
|
||||
};
|
||||
const sortChannelsMapByCode = (siteCodeToChannelsMap: Map<string, VimpChannel[]>) => {
|
||||
for (const channels of siteCodeToChannelsMap.values()) {
|
||||
channels.sort(compareByCode);
|
||||
}
|
||||
};
|
||||
|
||||
export const useChannelsQuery = () => {
|
||||
const cameraStore = useCameraStore();
|
||||
const alarmStore = useAlarmStore();
|
||||
|
||||
return useQuery({
|
||||
queryKey: computed(() => [VIMP_CHANNELS_QUERY_KEY]),
|
||||
refetchInterval: 10 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
queryFn: async ({ signal }) => {
|
||||
// 请求所有码表
|
||||
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 compiledCodeAreas = compileCodeAreas({
|
||||
codeStationAreas,
|
||||
codeParkingAreas,
|
||||
codeOccAreas,
|
||||
codeTrainAreas,
|
||||
});
|
||||
|
||||
const sitesFromApi = await catalogAllDeviceApi({ signal });
|
||||
|
||||
// 从 /allDevice 接口获取的站点信息并不保证真实性和完整性,
|
||||
// 例如有一个站点的编码是 010699 开头,但是其下的通道是 010199 和 010599 开头,
|
||||
// 而 010699 是一个不存在的站点编码,所以需要基于通道的编码来确定所有的站点。
|
||||
const cameraSites: VimpSite[] = [];
|
||||
const alarmSites: VimpSite[] = [];
|
||||
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,
|
||||
codeSites,
|
||||
compiledCodeAreas,
|
||||
});
|
||||
cameraStore.buildCameraRecord(siteCodeToCamerasMap);
|
||||
|
||||
alarmStore.buildLineTabPanes({
|
||||
sites: alarmSites,
|
||||
siteCodeToAlarmsMap,
|
||||
codeLines,
|
||||
codeSites,
|
||||
compiledCodeAreas,
|
||||
});
|
||||
alarmStore.buildAlarmRecord(siteCodeToAlarmsMap);
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './query';
|
||||
@@ -0,0 +1 @@
|
||||
export const VIMP_CHANNELS_QUERY_KEY = 'vimp-channels';
|
||||
+102
-108
@@ -1,45 +1,53 @@
|
||||
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';
|
||||
import type { VimpChannel, VimpSite } from '../apis';
|
||||
import { shallowRef } from 'vue';
|
||||
import type { AlarmMainAreaNodeOption, AlarmNodeOption, CodeLines, CodeSites, AlarmLineTabPane, AlarmSiteNodeOption, AlarmSubAreaNodeOption, CompiledCodeAreas } from '../types';
|
||||
|
||||
interface BuildLineTabPanesParams {
|
||||
sites: VimpStation[] | null;
|
||||
siteAlarmsMap: Record<string, VimpChannel[]>;
|
||||
sites: VimpSite[];
|
||||
siteCodeToAlarmsMap: Map<string, VimpChannel[]>;
|
||||
codeLines: CodeLines;
|
||||
codeSites: CodeSites;
|
||||
codeStationAreas: CodeArea[];
|
||||
codeParkingAreas: CodeArea[];
|
||||
codeOccAreas: CodeArea[];
|
||||
codeTrainAreas: CodeArea[];
|
||||
compiledCodeAreas: CompiledCodeAreas;
|
||||
}
|
||||
|
||||
export const useAlarmStore = defineStore('vimp-alarm', () => {
|
||||
const lineTabPanes = ref<AlarmLineTabPane[]>([]);
|
||||
const buildMainAreaNodeKey = (siteCode: string, mainAreaCode: string) => `${siteCode}${mainAreaCode}`;
|
||||
const buildSubAreaNodeKey = (siteCode: string, areaCode: string) => `${siteCode}${areaCode}`;
|
||||
|
||||
export const useAlarmStore = defineStore('vimp-alarm-store', () => {
|
||||
const lineTabPanes = shallowRef<AlarmLineTabPane[]>([]);
|
||||
const alarmRecord = shallowRef<Record<string, VimpChannel>>({});
|
||||
|
||||
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: [],
|
||||
});
|
||||
}
|
||||
const { sites, siteCodeToAlarmsMap, codeLines, codeSites, compiledCodeAreas } = params;
|
||||
|
||||
const result: AlarmLineTabPane[] = [];
|
||||
|
||||
// 1. 线路索引 lineCode -> AlarmLineTabPane
|
||||
const linePaneMap = new Map<string, AlarmLineTabPane>();
|
||||
|
||||
// 遍历所有站点
|
||||
for (const site of sites) {
|
||||
const siteCode = site.code.substring(0, 6);
|
||||
const siteName = codeSites[siteCode]?.name;
|
||||
if (!siteName) continue;
|
||||
// 2. 站点节点 siteNode 只在当前轮次中顺序创建,不需要建立索引
|
||||
|
||||
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 = {
|
||||
@@ -49,79 +57,76 @@ export const useAlarmStore = defineStore('vimp-alarm', () => {
|
||||
stats: { online: 0, offline: 0, total: 0 },
|
||||
online: site.online,
|
||||
};
|
||||
_lineTabPanes.find((lineTabPane) => lineTabPane.lineCode === lineCode)?.alarmTree.push(siteNode);
|
||||
linePane.alarmTree.push(siteNode);
|
||||
|
||||
// 获取所有警报器
|
||||
const alarms = siteAlarmsMap[site.code];
|
||||
if (!alarms || alarms.length === 0) continue;
|
||||
const alarms = siteCodeToAlarmsMap.get(siteCode);
|
||||
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) {
|
||||
// 计算相关编码
|
||||
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);
|
||||
const alarmMainAreaCode = alarmAreaCode.slice(0, mainAreaCodeLength);
|
||||
|
||||
// 构造车站/基地/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; // 如果还是未找到区域,则跳过该警报器
|
||||
// 查找1级区域,如果未找到则跳过该警报器
|
||||
const mainArea = compiledCodeAreaMaps.mainAreaMap.get(alarmMainAreaCode);
|
||||
if (!mainArea) continue;
|
||||
|
||||
// 构造区域节点
|
||||
if (!siteNode.children?.find((areaNode) => areaNode.key === `${alarmSiteCode}${alarmMainAreaCode}`)) {
|
||||
const areaNode: AlarmAreaNodeOption = {
|
||||
key: `${alarmSiteCode}${alarmMainAreaCode}`,
|
||||
label: siteArea.name,
|
||||
// 尝试从索引中获取1级区域节点,若不存在则创建
|
||||
const mainAreaNodeKey = buildMainAreaNodeKey(siteCode, alarmMainAreaCode);
|
||||
let mainAreaNode = mainAreaNodeMap.get(mainAreaNodeKey);
|
||||
if (!mainAreaNode) {
|
||||
mainAreaNode = {
|
||||
key: mainAreaNodeKey,
|
||||
label: mainArea.name,
|
||||
children: [],
|
||||
stats: { online: 0, offline: 0, total: 0 },
|
||||
site: site,
|
||||
areaLevel: 1,
|
||||
};
|
||||
siteNode.children?.push(areaNode);
|
||||
mainAreaNodeMap.set(mainAreaNodeKey, mainAreaNode);
|
||||
siteNode.children?.push(mainAreaNode);
|
||||
}
|
||||
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; // 如果还是未找到子区域,则跳过该警报器
|
||||
// 查找2级区域,如果未找到则跳过该警报器
|
||||
const subArea = compiledCodeAreaMaps.subAreaMap.get(alarmAreaCode);
|
||||
if (!subArea) continue;
|
||||
|
||||
const subAreaNode: AlarmSubAreaNodeOption = {
|
||||
key: `${alarmSiteCode}${alarmAreaCode}`,
|
||||
// 尝试从索引中获取2级区域节点,若不存在则创建
|
||||
const subAreaNodeKey = buildSubAreaNodeKey(siteCode, alarmAreaCode);
|
||||
let subAreaNode = subAreaNodeMap.get(subAreaNodeKey);
|
||||
if (!subAreaNode) {
|
||||
subAreaNode = {
|
||||
key: subAreaNodeKey,
|
||||
label: subArea.name,
|
||||
children: [],
|
||||
stats: { online: 0, offline: 0, total: 0 },
|
||||
site: site,
|
||||
areaLevel: 2,
|
||||
};
|
||||
areaNode.children?.push(subAreaNode);
|
||||
subAreaNodeMap.set(subAreaNodeKey, subAreaNode);
|
||||
mainAreaNode.children?.push(subAreaNode);
|
||||
}
|
||||
const subAreaNode = areaNode.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 alarmNode: AlarmNodeOption = {
|
||||
key: alarmGbCode,
|
||||
@@ -129,53 +134,42 @@ export const useAlarmStore = defineStore('vimp-alarm', () => {
|
||||
type: alarmType,
|
||||
alarm: alarm,
|
||||
site: site,
|
||||
prefix: () => {
|
||||
return `[警报器]`;
|
||||
},
|
||||
};
|
||||
|
||||
// 添加警报器节点到子区域节点
|
||||
if (!subAreaNode.children?.find((alarmNode) => alarmNode.key === alarmGbCode)) {
|
||||
subAreaNode.children?.push(alarmNode);
|
||||
}
|
||||
subAreaNode.children?.push(alarmNode);
|
||||
|
||||
// 统计站点、区域、子区域的在线/离线/总警报器数量
|
||||
siteNode.stats.total++;
|
||||
areaNode.stats.total++;
|
||||
mainAreaNode.stats.total++;
|
||||
subAreaNode.stats.total++;
|
||||
if (alarm.status === 1) {
|
||||
siteNode.stats.online++;
|
||||
areaNode.stats.online++;
|
||||
mainAreaNode.stats.online++;
|
||||
subAreaNode.stats.online++;
|
||||
}
|
||||
if (alarm.status === 0) {
|
||||
} else if (alarm.status === 0) {
|
||||
siteNode.stats.offline++;
|
||||
areaNode.stats.offline++;
|
||||
mainAreaNode.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;
|
||||
};
|
||||
|
||||
const buildAlarmRecord = (siteCodeToAlarmsMap: Map<string, VimpChannel[]>) => {
|
||||
const record: Record<string, VimpChannel> = {};
|
||||
for (const [, alarms] of siteCodeToAlarmsMap) {
|
||||
for (const alarm of alarms) {
|
||||
record[alarm.code] = alarm;
|
||||
}
|
||||
}
|
||||
alarmRecord.value = record;
|
||||
};
|
||||
|
||||
return {
|
||||
lineTabPanes,
|
||||
alarmRecord,
|
||||
|
||||
buildLineTabPanes,
|
||||
buildAlarmRecord,
|
||||
};
|
||||
});
|
||||
|
||||
+104
-113
@@ -1,45 +1,53 @@
|
||||
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';
|
||||
import type { VimpChannel, VimpSite } from '../apis';
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import type { CameraMainAreaNodeOption, CameraNodeOption, CodeLines, CodeSites, CameraLineTabPane, CameraSiteNodeOption, CameraSubAreaNodeOption, CompiledCodeAreas } from '../types';
|
||||
|
||||
interface BuildLineTabPanesParams {
|
||||
sites: VimpStation[] | null;
|
||||
siteCamerasMap: Record<string, VimpChannel[]>;
|
||||
sites: VimpSite[];
|
||||
siteCodeToCamerasMap: Map<string, VimpChannel[]>;
|
||||
codeLines: CodeLines;
|
||||
codeSites: CodeSites;
|
||||
codeStationAreas: CodeArea[];
|
||||
codeParkingAreas: CodeArea[];
|
||||
codeOccAreas: CodeArea[];
|
||||
codeTrainAreas: CodeArea[];
|
||||
compiledCodeAreas: CompiledCodeAreas;
|
||||
}
|
||||
|
||||
export const useCameraStore = defineStore('vimp-camera', () => {
|
||||
const lineTabPanes = ref<CameraLineTabPane[]>([]);
|
||||
const buildMainAreaNodeKey = (siteCode: string, mainAreaCode: string) => `${siteCode}${mainAreaCode}`;
|
||||
const buildSubAreaNodeKey = (siteCode: string, areaCode: string) => `${siteCode}${areaCode}`;
|
||||
|
||||
export const useCameraStore = defineStore('vimp-camera-store', () => {
|
||||
const lineTabPanes = shallowRef<CameraLineTabPane[]>([]);
|
||||
const cameraRecord = shallowRef<Record<string, VimpChannel>>({});
|
||||
|
||||
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: [],
|
||||
});
|
||||
}
|
||||
const { sites, siteCodeToCamerasMap, codeLines, codeSites, compiledCodeAreas } = params;
|
||||
|
||||
const result: CameraLineTabPane[] = [];
|
||||
|
||||
// 1. 线路索引 lineCode -> CameraLineTabPane
|
||||
const linePaneMap = new Map<string, CameraLineTabPane>();
|
||||
|
||||
// 遍历所有站点
|
||||
for (const site of sites) {
|
||||
const siteCode = site.code.substring(0, 6);
|
||||
const siteName = codeSites[siteCode]?.name;
|
||||
if (!siteName) continue;
|
||||
// 2. 站点节点 siteNode 只在当前轮次中顺序创建,不需要建立索引
|
||||
|
||||
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 = {
|
||||
@@ -49,135 +57,118 @@ export const useCameraStore = defineStore('vimp-camera', () => {
|
||||
stats: { online: 0, offline: 0, total: 0 },
|
||||
online: site.online,
|
||||
};
|
||||
_lineTabPanes.find((lineTabPane) => lineTabPane.lineCode === lineCode)?.cameraTree.push(siteNode);
|
||||
linePane.cameraTree.push(siteNode);
|
||||
|
||||
// 获取所有摄像机
|
||||
const cameras = siteCamerasMap[site.code];
|
||||
if (!cameras || cameras.length === 0) continue;
|
||||
const cameras = siteCodeToCamerasMap.get(siteCode);
|
||||
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) {
|
||||
// 计算相关编码
|
||||
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);
|
||||
const cameraMainAreaCode = cameraAreaCode.slice(0, mainAreaCodeLength);
|
||||
|
||||
// 构造车站/基地/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,
|
||||
// 查找1级区域,如果未找到则跳过该摄像机
|
||||
const mainArea = compiledCodeAreaMaps.mainAreaMap.get(cameraMainAreaCode);
|
||||
if (!mainArea) continue;
|
||||
// 尝试从索引中获取1级区域节点,若不存在则创建
|
||||
const mainAreaNodeKey = buildMainAreaNodeKey(siteCode, cameraMainAreaCode);
|
||||
let mainAreaNode = mainAreaNodeMap.get(mainAreaNodeKey);
|
||||
if (!mainAreaNode) {
|
||||
mainAreaNode = {
|
||||
key: mainAreaNodeKey,
|
||||
label: mainArea.name,
|
||||
children: [],
|
||||
stats: { online: 0, offline: 0, total: 0 },
|
||||
site: site,
|
||||
areaLevel: 1,
|
||||
};
|
||||
siteNode.children?.push(areaNode);
|
||||
mainAreaNodeMap.set(mainAreaNodeKey, mainAreaNode);
|
||||
siteNode.children?.push(mainAreaNode);
|
||||
}
|
||||
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}`,
|
||||
// 查找2级区域,如果未找到则跳过该摄像机
|
||||
const subArea = compiledCodeAreaMaps.subAreaMap.get(cameraAreaCode);
|
||||
if (!subArea) continue;
|
||||
// 尝试从索引中获取2级区域节点,若不存在则创建
|
||||
const subAreaNodeKey = buildSubAreaNodeKey(siteCode, cameraAreaCode);
|
||||
let subAreaNode = subAreaNodeMap.get(subAreaNodeKey);
|
||||
if (!subAreaNode) {
|
||||
subAreaNode = {
|
||||
key: subAreaNodeKey,
|
||||
label: subArea.name,
|
||||
children: [],
|
||||
stats: { online: 0, offline: 0, total: 0 },
|
||||
site: site,
|
||||
areaLevel: 2,
|
||||
};
|
||||
areaNode.children?.push(subAreaNode);
|
||||
subAreaNodeMap.set(subAreaNodeKey, subAreaNode);
|
||||
mainAreaNode.children?.push(subAreaNode);
|
||||
}
|
||||
const subAreaNode = areaNode.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 = {
|
||||
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);
|
||||
}
|
||||
subAreaNode.children?.push(cameraNode);
|
||||
|
||||
// 统计站点、区域、子区域的在线/离线/总摄像机数量
|
||||
siteNode.stats.total++;
|
||||
areaNode.stats.total++;
|
||||
mainAreaNode.stats.total++;
|
||||
subAreaNode.stats.total++;
|
||||
if (camera.status === 1) {
|
||||
siteNode.stats.online++;
|
||||
areaNode.stats.online++;
|
||||
mainAreaNode.stats.online++;
|
||||
subAreaNode.stats.online++;
|
||||
}
|
||||
if (camera.status === 0) {
|
||||
} else if (camera.status === 0) {
|
||||
siteNode.stats.offline++;
|
||||
areaNode.stats.offline++;
|
||||
mainAreaNode.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;
|
||||
};
|
||||
|
||||
const buildCameraRecord = (siteCodeToCamerasMap: Map<string, VimpChannel[]>) => {
|
||||
const record: Record<string, VimpChannel> = {};
|
||||
for (const [, cameras] of siteCodeToCamerasMap) {
|
||||
for (const camera of cameras) {
|
||||
record[camera.code] = camera;
|
||||
}
|
||||
}
|
||||
cameraRecord.value = record;
|
||||
};
|
||||
|
||||
return {
|
||||
lineTabPanes,
|
||||
cameraRecord,
|
||||
|
||||
buildLineTabPanes,
|
||||
buildCameraRecord,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './camera';
|
||||
export * from './alarm';
|
||||
export * from './camera';
|
||||
export * from './resource-panel';
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export const useResourcePanelStore = defineStore('vimp-resource-panel', () => {
|
||||
const collapsed = ref<boolean>(false);
|
||||
const searchPattern = ref<string>('');
|
||||
|
||||
const toggleCollapsed = () => {
|
||||
collapsed.value = !collapsed.value;
|
||||
};
|
||||
|
||||
return {
|
||||
collapsed,
|
||||
searchPattern,
|
||||
|
||||
toggleCollapsed,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
export interface VimpResult<T = unknown> {
|
||||
code: number;
|
||||
data: T;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
export type VimpResponse<T> = [err: AxiosError | null, data: T | null, resp: VimpResult<T> | null];
|
||||
@@ -0,0 +1,146 @@
|
||||
import type { TabPaneProps, TreeOption } from 'naive-ui';
|
||||
import type { VimpChannel, VimpSite } from '../apis/model';
|
||||
|
||||
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 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 {
|
||||
online: number;
|
||||
offline: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 摄像机树相关类型
|
||||
// ==========================================
|
||||
export interface CameraNodeOption extends TreeOption {
|
||||
camera: VimpChannel;
|
||||
type: string;
|
||||
site: VimpSite;
|
||||
}
|
||||
|
||||
export interface CameraSubAreaNodeOption extends TreeOption {
|
||||
children?: CameraNodeOption[];
|
||||
stats: CountStats;
|
||||
site: VimpSite;
|
||||
areaLevel: 2;
|
||||
}
|
||||
|
||||
export interface CameraMainAreaNodeOption extends TreeOption {
|
||||
children?: CameraSubAreaNodeOption[];
|
||||
stats: CountStats;
|
||||
site: VimpSite;
|
||||
areaLevel: 1;
|
||||
}
|
||||
|
||||
export interface CameraSiteNodeOption extends TreeOption {
|
||||
children?: CameraMainAreaNodeOption[];
|
||||
stats: CountStats;
|
||||
online: boolean;
|
||||
}
|
||||
|
||||
export function isCameraSiteNode(option: TreeOption): option is CameraSiteNodeOption {
|
||||
return 'online' in option && !('camera' in option);
|
||||
}
|
||||
|
||||
export function isCameraAreaNode(option: TreeOption): option is CameraMainAreaNodeOption | CameraSubAreaNodeOption {
|
||||
return 'site' in option && !('camera' in option) && !('online' in option);
|
||||
}
|
||||
|
||||
export function isCameraNode(option: TreeOption): option is CameraNodeOption {
|
||||
return 'camera' in option && 'site' in option;
|
||||
}
|
||||
|
||||
export interface CameraLineTabPane extends TabPaneProps {
|
||||
lineCode: string;
|
||||
lineName: string;
|
||||
cameraTree: CameraSiteNodeOption[];
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 警报器树相关类型
|
||||
// ==========================================
|
||||
export interface AlarmNodeOption extends TreeOption {
|
||||
alarm: VimpChannel;
|
||||
type: string;
|
||||
site: VimpSite;
|
||||
}
|
||||
|
||||
export interface AlarmSubAreaNodeOption extends TreeOption {
|
||||
children?: AlarmNodeOption[];
|
||||
stats: CountStats;
|
||||
site: VimpSite;
|
||||
areaLevel: 2;
|
||||
}
|
||||
|
||||
export interface AlarmMainAreaNodeOption extends TreeOption {
|
||||
children?: AlarmSubAreaNodeOption[];
|
||||
stats: CountStats;
|
||||
site: VimpSite;
|
||||
areaLevel: 1;
|
||||
}
|
||||
|
||||
export interface AlarmSiteNodeOption extends TreeOption {
|
||||
children?: AlarmMainAreaNodeOption[];
|
||||
stats: CountStats;
|
||||
online: boolean;
|
||||
}
|
||||
|
||||
export function isAlarmSiteNode(option: TreeOption): option is AlarmSiteNodeOption {
|
||||
return 'online' in option && !('alarm' in option);
|
||||
}
|
||||
|
||||
export function isAlarmAreaNode(option: TreeOption): option is AlarmMainAreaNodeOption | AlarmSubAreaNodeOption {
|
||||
return 'site' in option && !('alarm' in option) && !('online' in option);
|
||||
}
|
||||
|
||||
export function isAlarmNode(option: TreeOption): option is AlarmNodeOption {
|
||||
return 'alarm' in option && 'site' in option;
|
||||
}
|
||||
|
||||
export interface AlarmLineTabPane extends TabPaneProps {
|
||||
lineCode: string;
|
||||
lineName: string;
|
||||
alarmTree: AlarmSiteNodeOption[];
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from './tree';
|
||||
export * from './axios';
|
||||
export * from './device-tree';
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
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[];
|
||||
}
|
||||
@@ -1,21 +1,5 @@
|
||||
<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]>(['']);
|
||||
import ResourcePanel from './components/resource-panel.vue';
|
||||
|
||||
const onDragover = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -32,13 +16,11 @@ const onDrop = (event: DragEvent) => {
|
||||
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 {
|
||||
}
|
||||
@@ -47,19 +29,10 @@ const onDrop = (event: DragEvent) => {
|
||||
|
||||
<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>
|
||||
<ResourcePanel />
|
||||
<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>
|
||||
|
||||
@@ -215,6 +215,7 @@ const apiProxyList: ProxyItem[] = [
|
||||
// { key: '/vimp/api', target: 'http://10.14.0.10:18080', rewrite: ['/vimp/api', '/api'] },
|
||||
// { key: '/vimp/api', target: 'http://10.18.128.6:18080', rewrite: ['/vimp/api', '/api'] },
|
||||
{ key: '/vimp/api', target: 'http://localhost:4000', rewrite: ['/vimp/api', '/api'] },
|
||||
// { key: '/vimp/api', target: 'http://10.24.17.6:18080', rewrite: ['/vimp/api', '/api'] },
|
||||
// { key: '/vimp/api', target: 'http://10.18.128.6:18080', rewrite: ['/vimp/api', '/api'] },
|
||||
{ key: '/cdn', target: `http://localhost:${SERVER_PORT}`, rewrite: ['/cdn', ''] },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user