Compare commits
23 Commits
0b1a0546dd
..
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 |
@@ -716,6 +716,12 @@
|
|||||||
"018507": { "name": "康桥站", "type": "station" },
|
"018507": { "name": "康桥站", "type": "station" },
|
||||||
"018508": { "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" },
|
"051501": { "name": "沈杜公路", "type": "station" },
|
||||||
"051502": { "name": "三鲁公路", "type": "station" },
|
"051502": { "name": "三鲁公路", "type": "station" },
|
||||||
"051503": { "name": "闵瑞路", "type": "station" },
|
"051503": { "name": "闵瑞路", "type": "station" },
|
||||||
|
|||||||
@@ -13,9 +13,27 @@ import destr from 'destr';
|
|||||||
import { isFunction } from 'es-toolkit';
|
import { isFunction } from 'es-toolkit';
|
||||||
import localforage from 'localforage';
|
import localforage from 'localforage';
|
||||||
import { DownloadIcon, Trash2Icon, UploadIcon } from 'lucide-vue-next';
|
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 { storeToRefs } from 'pinia';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -149,8 +167,14 @@ useEventListener('keydown', (event) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const expectToShowDebugCodeInput = ref(false);
|
const expectToShowDebugCodeInput = ref(false);
|
||||||
|
const debugCodeInputRef = useTemplateRef<InputInst>('debug-code-input-ref');
|
||||||
const onModalAfterEnter = () => {
|
const onModalAfterEnter = () => {
|
||||||
expectToShowDebugCodeInput.value = !debugMode.value;
|
expectToShowDebugCodeInput.value = !debugMode.value;
|
||||||
|
if (expectToShowDebugCodeInput.value) {
|
||||||
|
nextTick(() => {
|
||||||
|
debugCodeInputRef.value?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const onModalAfterLeave = () => {
|
const onModalAfterLeave = () => {
|
||||||
expectToShowDebugCodeInput.value = false;
|
expectToShowDebugCodeInput.value = false;
|
||||||
@@ -412,7 +436,7 @@ const onClickVersion = () => {
|
|||||||
<NText v-else>确认关闭调试模式</NText>
|
<NText v-else>确认关闭调试模式</NText>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<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>
|
||||||
<template #action>
|
<template #action>
|
||||||
<NButton @click="showDebugCodeModal = false">取消</NButton>
|
<NButton @click="showDebugCodeModal = false">取消</NButton>
|
||||||
|
|||||||
@@ -1,9 +1,59 @@
|
|||||||
import type { AxiosError, AxiosRequestConfig, CreateAxiosDefaults } from 'axios';
|
import type { AxiosError, AxiosRequestConfig, AxiosResponse, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios';
|
||||||
import axios from 'axios';
|
import axios, { isAxiosError } from 'axios';
|
||||||
import type { VimpResponse, VimpResult } from '../../types';
|
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;
|
||||||
|
|
||||||
export const createVimpClient = (config?: CreateAxiosDefaults) => {
|
|
||||||
const instance = axios.create(config);
|
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 vimpPost = <T>(url: string, data?: AxiosRequestConfig['data'], options?: Partial<Omit<AxiosRequestConfig, 'data'>> & { retRaw?: boolean; upload?: boolean }): Promise<VimpResponse<T>> => {
|
||||||
const { retRaw, upload, ...reqConfig } = options ?? {};
|
const { retRaw, upload, ...reqConfig } = options ?? {};
|
||||||
@@ -24,9 +74,40 @@ export const createVimpClient = (config?: CreateAxiosDefaults) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
instance,
|
instance,
|
||||||
|
get: vimpGet,
|
||||||
post: vimpPost,
|
post: vimpPost,
|
||||||
|
put: httpPut,
|
||||||
|
delete: httpDelete,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,4 +123,30 @@ export const unwrapVimpResponse = <T>(resp: VimpResponse<T>) => {
|
|||||||
|
|
||||||
export const vimpClient = createVimpClient({
|
export const vimpClient = createVimpClient({
|
||||||
baseURL: `/vimp/api/client`,
|
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,2 +1,2 @@
|
|||||||
export * from './vimp-channel';
|
export * from './vimp-channel';
|
||||||
export * from './vimp-station';
|
export * from './vimp-site';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export interface VimpStation {
|
|
||||||
code: string;
|
|
||||||
name: string;
|
|
||||||
online: boolean;
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { unwrapVimpResponse, vimpClient } from '../client';
|
import { unwrapVimpResponse, vimpClient } from '../client';
|
||||||
import type { VimpStation } from '../model';
|
import { normalizeVimpSite, type VimpRawSite } from '../model';
|
||||||
|
|
||||||
export const catalogAllDeviceApi = async (options?: { signal?: AbortSignal }) => {
|
export const catalogAllDeviceApi = async (options?: { signal?: AbortSignal }) => {
|
||||||
const { signal } = options ?? {};
|
const { signal } = options ?? {};
|
||||||
const client = vimpClient;
|
const client = vimpClient;
|
||||||
const endpoint = `/catalog/allDevice`;
|
const endpoint = `/catalog/allDevice`;
|
||||||
const resp = await client.post<VimpStation[]>(endpoint, {}, { signal });
|
const resp = await client.post<VimpRawSite[]>(endpoint, {}, { signal });
|
||||||
const data = unwrapVimpResponse(resp);
|
const data = unwrapVimpResponse(resp);
|
||||||
return data;
|
return data?.map(normalizeVimpSite) ?? null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type TreeProps } from 'naive-ui';
|
import { NIcon, NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type TreeProps } from 'naive-ui';
|
||||||
import { h, type CSSProperties } from 'vue';
|
import { h, type CSSProperties } from 'vue';
|
||||||
import { useAlarmStore, useResourcePanelStore } from '../stores';
|
import { useAlarmStore, useResourcePanelStore } from '../stores';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useDeviceCenterQuery } from '../composables';
|
import { useChannelsQuery } from '../composables';
|
||||||
import { isAlarmNode, isAlarmSiteNode, isAlarmAreaNode } from '../types';
|
import { isAlarmNode, isAlarmSiteNode, isAlarmAreaNode } from '../types';
|
||||||
|
import { SirenIcon } from 'lucide-vue-next';
|
||||||
|
|
||||||
const { isLoading } = useDeviceCenterQuery();
|
const { isLoading } = useChannelsQuery();
|
||||||
|
|
||||||
const alarmStore = useAlarmStore();
|
const alarmStore = useAlarmStore();
|
||||||
const { lineTabPanes } = storeToRefs(alarmStore);
|
const { lineTabPanes } = storeToRefs(alarmStore);
|
||||||
@@ -51,6 +52,9 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
|
|||||||
const alarmNodeStyle: CSSProperties = {
|
const alarmNodeStyle: CSSProperties = {
|
||||||
opacity: alarmOnline() ? 1 : 0.5,
|
opacity: alarmOnline() ? 1 : 0.5,
|
||||||
cursor: alarmOnline() ? 'pointer' : 'not-allowed',
|
cursor: alarmOnline() ? 'pointer' : 'not-allowed',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '6px',
|
||||||
};
|
};
|
||||||
return h(
|
return h(
|
||||||
'div',
|
'div',
|
||||||
@@ -69,7 +73,7 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
|
|||||||
event.dataTransfer?.setData('name', alarm.name);
|
event.dataTransfer?.setData('name', alarm.name);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
alarm.name,
|
[h(NIcon, () => h(SirenIcon)), h('span', alarm.name)],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +81,21 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
|
|||||||
return option.label;
|
return option.label;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderNodeSuffix: TreeProps['renderSuffix'] = ({ option }) => {
|
||||||
|
if (isAlarmSiteNode(option)) {
|
||||||
|
const { online, offline, total } = option.stats;
|
||||||
|
return `(${online}/${offline}/${total})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAlarmAreaNode(option)) {
|
||||||
|
const { online, offline, total } = option.stats;
|
||||||
|
const suffixStyle: CSSProperties = option.areaLevel === 1 ? { marginRight: '8px', opacity: 0.6 } : { marginRight: '16px', opacity: 0.4 };
|
||||||
|
return h('div', { style: suffixStyle }, `(${online}/${offline}/${total})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const resourcePanelStore = useResourcePanelStore();
|
const resourcePanelStore = useResourcePanelStore();
|
||||||
const { searchPattern } = storeToRefs(resourcePanelStore);
|
const { searchPattern } = storeToRefs(resourcePanelStore);
|
||||||
|
|
||||||
@@ -98,6 +117,7 @@ const searchFilter: TreeProps['filter'] = (pattern, node) => {
|
|||||||
virtual-scroll
|
virtual-scroll
|
||||||
style="height: 100%"
|
style="height: 100%"
|
||||||
:render-label="renderNodeLabel"
|
:render-label="renderNodeLabel"
|
||||||
|
:render-suffix="renderNodeSuffix"
|
||||||
:override-default-node-click-behavior="overrideNodeClickBehavior"
|
:override-default-node-click-behavior="overrideNodeClickBehavior"
|
||||||
:default-expand-all="searchPattern.trim().length > 0"
|
:default-expand-all="searchPattern.trim().length > 0"
|
||||||
:show-irrelevant-nodes="false"
|
:show-irrelevant-nodes="false"
|
||||||
@@ -107,22 +127,42 @@ const searchFilter: TreeProps['filter'] = (pattern, node) => {
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="lineTabPanes.length > 1">
|
<template v-if="lineTabPanes.length > 1">
|
||||||
<NTabs :type="'card'" :placement="'left'" style="height: 100%">
|
<NTabs
|
||||||
<NTabPane v-for="{ lineCode, lineName, alarmTree } in lineTabPanes" :key="lineCode" :name="lineName" :tab="lineName">
|
:type="'card'"
|
||||||
<NTree
|
:placement="'left'"
|
||||||
block-line
|
style="height: 100%"
|
||||||
block-node
|
:tab-style="{
|
||||||
show-line
|
width: '64px',
|
||||||
virtual-scroll
|
height: '36px',
|
||||||
style="height: 100%"
|
}"
|
||||||
:render-label="renderNodeLabel"
|
:style="{
|
||||||
:override-default-node-click-behavior="overrideNodeClickBehavior"
|
'--n-bar-color': '#0000',
|
||||||
:default-expand-all="false"
|
'--n-pane-padding-top': '0',
|
||||||
:show-irrelevant-nodes="false"
|
'--n-tab-gap-vertical': '0',
|
||||||
:data="alarmTree"
|
// '--n-tab-padding-vertical': '14px 12px'
|
||||||
:pattern="searchPattern"
|
}"
|
||||||
:filter="searchFilter"
|
>
|
||||||
/>
|
<NTabPane v-for="{ lineCode, lineName, alarmTree } in lineTabPanes" :key="lineCode" :name="lineName">
|
||||||
|
<template #tab>
|
||||||
|
<span style="font-size: 12px">{{ lineName }}</span>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<NTree
|
||||||
|
block-line
|
||||||
|
block-node
|
||||||
|
show-line
|
||||||
|
virtual-scroll
|
||||||
|
style="height: 100%"
|
||||||
|
:render-label="renderNodeLabel"
|
||||||
|
:render-suffix="renderNodeSuffix"
|
||||||
|
:override-default-node-click-behavior="overrideNodeClickBehavior"
|
||||||
|
:default-expand-all="false"
|
||||||
|
:show-irrelevant-nodes="false"
|
||||||
|
:data="alarmTree"
|
||||||
|
:pattern="searchPattern"
|
||||||
|
:filter="searchFilter"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</NTabPane>
|
</NTabPane>
|
||||||
</NTabs>
|
</NTabs>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type TreeProps } from 'naive-ui';
|
import { NIcon, NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type TreeProps } from 'naive-ui';
|
||||||
import { h, type CSSProperties } from 'vue';
|
import { h, type CSSProperties } from 'vue';
|
||||||
import { useCameraStore, useResourcePanelStore } from '../stores';
|
import { useCameraStore, useResourcePanelStore } from '../stores';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useDeviceCenterQuery } from '../composables';
|
import { useChannelsQuery } from '../composables';
|
||||||
import { isCameraNode, isCameraSiteNode, isCameraAreaNode } from '../types';
|
import { isCameraNode, isCameraSiteNode, isCameraAreaNode } from '../types';
|
||||||
|
import PtzCamera from './icon/ptz-camera.vue';
|
||||||
|
import HemiPtzCamera from './icon/hemi-ptz-camera.vue';
|
||||||
|
import BulletCamera from './icon/bullet-camera.vue';
|
||||||
|
|
||||||
const { isLoading } = useDeviceCenterQuery();
|
const { isLoading } = useChannelsQuery();
|
||||||
|
|
||||||
const cameraStore = useCameraStore();
|
const cameraStore = useCameraStore();
|
||||||
const { lineTabPanes } = storeToRefs(cameraStore);
|
const { lineTabPanes } = storeToRefs(cameraStore);
|
||||||
@@ -51,7 +54,11 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
|
|||||||
const cameraNodeStyle: CSSProperties = {
|
const cameraNodeStyle: CSSProperties = {
|
||||||
opacity: cameraOnline() ? 1 : 0.5,
|
opacity: cameraOnline() ? 1 : 0.5,
|
||||||
cursor: cameraOnline() ? 'pointer' : 'not-allowed',
|
cursor: cameraOnline() ? 'pointer' : 'not-allowed',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '6px',
|
||||||
};
|
};
|
||||||
|
const cameraIcon = option.type === '004' ? h(NIcon, () => h(PtzCamera)) : option.type === '005' ? h(NIcon, () => h(HemiPtzCamera)) : option.type === '006' ? h(NIcon, () => h(BulletCamera)) : null;
|
||||||
return h(
|
return h(
|
||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
@@ -69,7 +76,7 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
|
|||||||
event.dataTransfer?.setData('name', camera.name);
|
event.dataTransfer?.setData('name', camera.name);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
camera.name,
|
[cameraIcon, h('span', camera.name)],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +84,21 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
|
|||||||
return option.label;
|
return option.label;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderNodeSuffix: TreeProps['renderSuffix'] = ({ option }) => {
|
||||||
|
if (isCameraSiteNode(option)) {
|
||||||
|
const { online, offline, total } = option.stats;
|
||||||
|
return `(${online}/${offline}/${total})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCameraAreaNode(option)) {
|
||||||
|
const { online, offline, total } = option.stats;
|
||||||
|
const suffixStyle: CSSProperties = option.areaLevel === 1 ? { marginRight: '8px', opacity: 0.6 } : { marginRight: '16px', opacity: 0.4 };
|
||||||
|
return h('div', { style: suffixStyle }, `(${online}/${offline}/${total})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const resourcePanelStore = useResourcePanelStore();
|
const resourcePanelStore = useResourcePanelStore();
|
||||||
const { searchPattern } = storeToRefs(resourcePanelStore);
|
const { searchPattern } = storeToRefs(resourcePanelStore);
|
||||||
|
|
||||||
@@ -98,6 +120,7 @@ const searchFilter: TreeProps['filter'] = (pattern, node) => {
|
|||||||
virtual-scroll
|
virtual-scroll
|
||||||
style="height: 100%"
|
style="height: 100%"
|
||||||
:render-label="renderNodeLabel"
|
:render-label="renderNodeLabel"
|
||||||
|
:render-suffix="renderNodeSuffix"
|
||||||
:override-default-node-click-behavior="overrideNodeClickBehavior"
|
:override-default-node-click-behavior="overrideNodeClickBehavior"
|
||||||
:default-expand-all="searchPattern.trim().length > 0"
|
:default-expand-all="searchPattern.trim().length > 0"
|
||||||
:show-irrelevant-nodes="false"
|
:show-irrelevant-nodes="false"
|
||||||
@@ -107,22 +130,42 @@ const searchFilter: TreeProps['filter'] = (pattern, node) => {
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="lineTabPanes.length > 1">
|
<template v-if="lineTabPanes.length > 1">
|
||||||
<NTabs :type="'card'" :placement="'left'" style="height: 100%">
|
<NTabs
|
||||||
<NTabPane v-for="{ lineCode, lineName, cameraTree } in lineTabPanes" :key="lineCode" :name="lineName" :tab="lineName">
|
:type="'card'"
|
||||||
<NTree
|
:placement="'left'"
|
||||||
block-line
|
style="height: 100%"
|
||||||
block-node
|
:tab-style="{
|
||||||
show-line
|
width: '64px',
|
||||||
virtual-scroll
|
height: '36px',
|
||||||
style="height: 100%"
|
}"
|
||||||
:render-label="renderNodeLabel"
|
:style="{
|
||||||
:override-default-node-click-behavior="overrideNodeClickBehavior"
|
'--n-bar-color': '#0000',
|
||||||
:default-expand-all="false"
|
'--n-pane-padding-top': '0',
|
||||||
:show-irrelevant-nodes="false"
|
'--n-tab-gap-vertical': '0',
|
||||||
:data="cameraTree"
|
// '--n-tab-padding-vertical': '14px 12px'
|
||||||
:pattern="searchPattern"
|
}"
|
||||||
:filter="searchFilter"
|
>
|
||||||
/>
|
<NTabPane v-for="{ lineCode, lineName, cameraTree } in lineTabPanes" :key="lineCode" :name="lineName">
|
||||||
|
<template #tab>
|
||||||
|
<span style="font-size: 12px">{{ lineName }}</span>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<NTree
|
||||||
|
block-line
|
||||||
|
block-node
|
||||||
|
show-line
|
||||||
|
virtual-scroll
|
||||||
|
style="height: 100%"
|
||||||
|
:render-label="renderNodeLabel"
|
||||||
|
:render-suffix="renderNodeSuffix"
|
||||||
|
:override-default-node-click-behavior="overrideNodeClickBehavior"
|
||||||
|
:default-expand-all="false"
|
||||||
|
:show-irrelevant-nodes="false"
|
||||||
|
:data="cameraTree"
|
||||||
|
:pattern="searchPattern"
|
||||||
|
:filter="searchFilter"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</NTabPane>
|
</NTabPane>
|
||||||
</NTabs>
|
</NTabs>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export * from './use-device-center-query';
|
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;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/vue-query';
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { catalogChannelApi, catalogAllDeviceApi } from '../../apis/request';
|
|
||||||
import type { AxiosRequestConfig } from 'axios';
|
|
||||||
import axios from 'axios';
|
|
||||||
import type { CodeArea, CodeLines, CodeSites } from '../../types';
|
|
||||||
import { useCameraStore, useAlarmStore } from '../../stores';
|
|
||||||
import type { VimpChannel } from '../../apis/model';
|
|
||||||
|
|
||||||
export const useDeviceCenterQuery = () => {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
siteCamerasMap[site.code] = cameras;
|
|
||||||
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 @@
|
|||||||
|
export * from './query';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const VIMP_CHANNELS_QUERY_KEY = 'vimp-channels';
|
||||||
+100
-108
@@ -1,47 +1,53 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import type { VimpChannel, VimpStation } from '../apis';
|
import type { VimpChannel, VimpSite } from '../apis';
|
||||||
import { h, ref } from 'vue';
|
import { shallowRef } from 'vue';
|
||||||
import type { AlarmMainAreaNodeOption, AlarmNodeOption, CodeArea, CodeLines, CodeSites, AlarmLineTabPane, AlarmSiteNodeOption, AlarmSubAreaNodeOption } from '../types';
|
import type { AlarmMainAreaNodeOption, AlarmNodeOption, CodeLines, CodeSites, AlarmLineTabPane, AlarmSiteNodeOption, AlarmSubAreaNodeOption, CompiledCodeAreas } from '../types';
|
||||||
import { NIcon } from 'naive-ui';
|
|
||||||
import { SirenIcon } from 'lucide-vue-next';
|
|
||||||
|
|
||||||
interface BuildLineTabPanesParams {
|
interface BuildLineTabPanesParams {
|
||||||
sites: VimpStation[] | null;
|
sites: VimpSite[];
|
||||||
siteAlarmsMap: Record<string, VimpChannel[]>;
|
siteCodeToAlarmsMap: Map<string, VimpChannel[]>;
|
||||||
codeLines: CodeLines;
|
codeLines: CodeLines;
|
||||||
codeSites: CodeSites;
|
codeSites: CodeSites;
|
||||||
codeStationAreas: CodeArea[];
|
compiledCodeAreas: CompiledCodeAreas;
|
||||||
codeParkingAreas: CodeArea[];
|
|
||||||
codeOccAreas: CodeArea[];
|
|
||||||
codeTrainAreas: CodeArea[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildMainAreaNodeKey = (siteCode: string, mainAreaCode: string) => `${siteCode}${mainAreaCode}`;
|
||||||
|
const buildSubAreaNodeKey = (siteCode: string, areaCode: string) => `${siteCode}${areaCode}`;
|
||||||
|
|
||||||
export const useAlarmStore = defineStore('vimp-alarm-store', () => {
|
export const useAlarmStore = defineStore('vimp-alarm-store', () => {
|
||||||
const lineTabPanes = ref<AlarmLineTabPane[]>([]);
|
const lineTabPanes = shallowRef<AlarmLineTabPane[]>([]);
|
||||||
|
const alarmRecord = shallowRef<Record<string, VimpChannel>>({});
|
||||||
|
|
||||||
const buildLineTabPanes = (params: BuildLineTabPanesParams) => {
|
const buildLineTabPanes = (params: BuildLineTabPanesParams) => {
|
||||||
const { sites, siteAlarmsMap, codeLines, codeSites, codeStationAreas, codeParkingAreas, codeOccAreas, codeTrainAreas } = params;
|
const { sites, siteCodeToAlarmsMap, codeLines, codeSites, compiledCodeAreas } = params;
|
||||||
if (!sites) {
|
|
||||||
lineTabPanes.value = [];
|
const result: AlarmLineTabPane[] = [];
|
||||||
return;
|
|
||||||
}
|
// 1. 线路索引 lineCode -> AlarmLineTabPane
|
||||||
// 构造线路TabPane
|
const linePaneMap = new Map<string, AlarmLineTabPane>();
|
||||||
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) {
|
for (const site of sites) {
|
||||||
const siteCode = site.code.substring(0, 6);
|
// 2. 站点节点 siteNode 只在当前轮次中顺序创建,不需要建立索引
|
||||||
const siteName = codeSites[siteCode]?.name;
|
|
||||||
if (!siteName) continue;
|
const lineCode = site.code.substring(0, 3);
|
||||||
|
const lineName = codeLines[lineCode]?.name ?? '';
|
||||||
|
|
||||||
|
let linePane = linePaneMap.get(lineCode);
|
||||||
|
if (!linePane) {
|
||||||
|
linePane = { lineCode, lineName, alarmTree: [] };
|
||||||
|
linePaneMap.set(lineCode, linePane);
|
||||||
|
result.push(linePane);
|
||||||
|
}
|
||||||
|
|
||||||
|
const siteCode = site.code;
|
||||||
|
const siteMeta = codeSites[siteCode];
|
||||||
|
if (!siteMeta) continue;
|
||||||
|
const siteName = siteMeta.name;
|
||||||
|
const siteType = siteMeta.type;
|
||||||
|
|
||||||
|
const compiledCodeAreaMaps = compiledCodeAreas[siteType];
|
||||||
|
const mainAreaCodeLength = siteType === 'train' ? 3 : 2;
|
||||||
|
|
||||||
// 构造站点节点
|
// 构造站点节点
|
||||||
const siteNode: AlarmSiteNodeOption = {
|
const siteNode: AlarmSiteNodeOption = {
|
||||||
@@ -51,79 +57,76 @@ export const useAlarmStore = defineStore('vimp-alarm-store', () => {
|
|||||||
stats: { online: 0, offline: 0, total: 0 },
|
stats: { online: 0, offline: 0, total: 0 },
|
||||||
online: site.online,
|
online: site.online,
|
||||||
};
|
};
|
||||||
_lineTabPanes.find((lineTabPane) => lineTabPane.lineCode === lineCode)?.alarmTree.push(siteNode);
|
linePane.alarmTree.push(siteNode);
|
||||||
|
|
||||||
// 获取所有警报器
|
// 获取所有警报器
|
||||||
const alarms = siteAlarmsMap[site.code];
|
const alarms = siteCodeToAlarmsMap.get(siteCode);
|
||||||
if (!alarms || alarms.length === 0) continue;
|
if (!alarms) continue;
|
||||||
|
|
||||||
|
// 3. 1级区域索引 mainAreaNodeKey -> AlarmMainAreaNodeOption
|
||||||
|
// mainAreaNodeKey = ${siteCode}${alarmMainAreaCode}
|
||||||
|
const mainAreaNodeMap = new Map<string, AlarmMainAreaNodeOption>();
|
||||||
|
// 4. 2级区域索引 subAreaNodeKey -> AlarmSubAreaNodeOption
|
||||||
|
// subAreaNodeKey = ${siteCode}${alarmAreaCode}
|
||||||
|
const subAreaNodeMap = new Map<string, AlarmSubAreaNodeOption>();
|
||||||
|
// 5. 警报器索引 subAreaNodeKey -> Set<AlarmGbCode>
|
||||||
|
const subAreaNodeKeyToAlarmGbCodeSetMap = new Map<string, Set<string>>();
|
||||||
|
|
||||||
// 遍历警报器
|
// 遍历警报器
|
||||||
for (const alarm of alarms) {
|
for (const alarm of alarms) {
|
||||||
// 计算相关编码
|
// 计算相关编码
|
||||||
const { code: alarmGbCode, name: alarmName } = alarm;
|
const { code: alarmGbCode, name: alarmName } = alarm;
|
||||||
const alarmSiteCode = alarmGbCode.substring(0, 6);
|
|
||||||
const alarmSiteType = codeSites[alarmSiteCode]?.type;
|
|
||||||
const alarmAreaCode = alarmGbCode.substring(6, 11);
|
const alarmAreaCode = alarmGbCode.substring(6, 11);
|
||||||
const alarmMainAreaCode = alarmAreaCode.slice(0, 2);
|
const alarmMainAreaCode = alarmAreaCode.slice(0, mainAreaCodeLength);
|
||||||
|
|
||||||
// 构造车站/基地/OCC/车次区域
|
// 查找1级区域,如果未找到则跳过该警报器
|
||||||
let siteArea: CodeArea | undefined = undefined;
|
const mainArea = compiledCodeAreaMaps.mainAreaMap.get(alarmMainAreaCode);
|
||||||
if (alarmSiteType === 'station') {
|
if (!mainArea) continue;
|
||||||
siteArea = codeStationAreas.find((area) => area.code === alarmMainAreaCode);
|
|
||||||
} else if (alarmSiteType === 'parking') {
|
|
||||||
siteArea = codeParkingAreas.find((area) => area.code === alarmMainAreaCode);
|
|
||||||
} else if (alarmSiteType === 'occ') {
|
|
||||||
siteArea = codeOccAreas.find((area) => area.code === alarmMainAreaCode);
|
|
||||||
} else if (alarmSiteType === 'train') {
|
|
||||||
siteArea = codeTrainAreas.find((area) => area.code === alarmMainAreaCode);
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!siteArea) continue; // 如果还是未找到区域,则跳过该警报器
|
|
||||||
|
|
||||||
// 构造1级区域节点
|
// 尝试从索引中获取1级区域节点,若不存在则创建
|
||||||
if (!siteNode.children?.find((areaNode) => areaNode.key === `${alarmSiteCode}${alarmMainAreaCode}`)) {
|
const mainAreaNodeKey = buildMainAreaNodeKey(siteCode, alarmMainAreaCode);
|
||||||
const mainAreaNode: AlarmMainAreaNodeOption = {
|
let mainAreaNode = mainAreaNodeMap.get(mainAreaNodeKey);
|
||||||
key: `${alarmSiteCode}${alarmMainAreaCode}`,
|
if (!mainAreaNode) {
|
||||||
label: siteArea.name,
|
mainAreaNode = {
|
||||||
|
key: mainAreaNodeKey,
|
||||||
|
label: mainArea.name,
|
||||||
children: [],
|
children: [],
|
||||||
stats: { online: 0, offline: 0, total: 0 },
|
stats: { online: 0, offline: 0, total: 0 },
|
||||||
site: site,
|
site: site,
|
||||||
|
areaLevel: 1,
|
||||||
};
|
};
|
||||||
|
mainAreaNodeMap.set(mainAreaNodeKey, mainAreaNode);
|
||||||
siteNode.children?.push(mainAreaNode);
|
siteNode.children?.push(mainAreaNode);
|
||||||
}
|
}
|
||||||
const targetMainAreaNode = siteNode.children?.find((areaNode) => areaNode.key === `${alarmSiteCode}${alarmMainAreaCode}`);
|
|
||||||
if (!targetMainAreaNode) continue; // 如果1级区域节点不存在,则跳过该警报器
|
|
||||||
|
|
||||||
// 构造2级区域节点
|
// 查找2级区域,如果未找到则跳过该警报器
|
||||||
if (!targetMainAreaNode.children?.find((subAreaNode) => subAreaNode.key === `${alarmSiteCode}${alarmAreaCode}`)) {
|
const subArea = compiledCodeAreaMaps.subAreaMap.get(alarmAreaCode);
|
||||||
let subArea: CodeArea['subs'][number] | undefined = undefined;
|
if (!subArea) continue;
|
||||||
if (alarmSiteType === 'station') {
|
|
||||||
subArea = codeStationAreas.find((area) => area.code === alarmMainAreaCode)?.subs.find((subArea) => subArea.code === alarmAreaCode);
|
|
||||||
} else if (alarmSiteType === 'parking') {
|
|
||||||
subArea = codeParkingAreas.find((area) => area.code === alarmMainAreaCode)?.subs.find((subArea) => subArea.code === alarmAreaCode);
|
|
||||||
} else if (alarmSiteType === 'occ') {
|
|
||||||
subArea = codeOccAreas.find((area) => area.code === alarmMainAreaCode)?.subs.find((subArea) => subArea.code === alarmAreaCode);
|
|
||||||
} else if (alarmSiteType === 'train') {
|
|
||||||
subArea = codeTrainAreas.find((area) => area.code === alarmMainAreaCode)?.subs.find((subArea) => subArea.code === alarmAreaCode);
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!subArea) continue; // 如果还是未找到子区域,则跳过该警报器
|
|
||||||
|
|
||||||
const subAreaNode: AlarmSubAreaNodeOption = {
|
// 尝试从索引中获取2级区域节点,若不存在则创建
|
||||||
key: `${alarmSiteCode}${alarmAreaCode}`,
|
const subAreaNodeKey = buildSubAreaNodeKey(siteCode, alarmAreaCode);
|
||||||
|
let subAreaNode = subAreaNodeMap.get(subAreaNodeKey);
|
||||||
|
if (!subAreaNode) {
|
||||||
|
subAreaNode = {
|
||||||
|
key: subAreaNodeKey,
|
||||||
label: subArea.name,
|
label: subArea.name,
|
||||||
children: [],
|
children: [],
|
||||||
stats: { online: 0, offline: 0, total: 0 },
|
stats: { online: 0, offline: 0, total: 0 },
|
||||||
site: site,
|
site: site,
|
||||||
|
areaLevel: 2,
|
||||||
};
|
};
|
||||||
targetMainAreaNode.children?.push(subAreaNode);
|
subAreaNodeMap.set(subAreaNodeKey, subAreaNode);
|
||||||
|
mainAreaNode.children?.push(subAreaNode);
|
||||||
}
|
}
|
||||||
const subAreaNode = targetMainAreaNode.children?.find((subAreaNode) => subAreaNode.key === `${alarmSiteCode}${alarmAreaCode}`);
|
|
||||||
if (!subAreaNode) continue; // 如果子区域节点不存在,则跳过该警报器
|
|
||||||
|
|
||||||
// 构造警报器节点
|
// 构造警报器节点
|
||||||
|
let alarmGbCodeSet = subAreaNodeKeyToAlarmGbCodeSetMap.get(subAreaNodeKey);
|
||||||
|
if (!alarmGbCodeSet) {
|
||||||
|
alarmGbCodeSet = new Set<string>();
|
||||||
|
subAreaNodeKeyToAlarmGbCodeSetMap.set(subAreaNodeKey, alarmGbCodeSet);
|
||||||
|
}
|
||||||
|
if (alarmGbCodeSet.has(alarmGbCode)) continue;
|
||||||
|
alarmGbCodeSet.add(alarmGbCode);
|
||||||
const alarmType = alarm.code.substring(11, 14);
|
const alarmType = alarm.code.substring(11, 14);
|
||||||
const alarmNode: AlarmNodeOption = {
|
const alarmNode: AlarmNodeOption = {
|
||||||
key: alarmGbCode,
|
key: alarmGbCode,
|
||||||
@@ -131,53 +134,42 @@ export const useAlarmStore = defineStore('vimp-alarm-store', () => {
|
|||||||
type: alarmType,
|
type: alarmType,
|
||||||
alarm: alarm,
|
alarm: alarm,
|
||||||
site: site,
|
site: site,
|
||||||
prefix: () => {
|
|
||||||
return h(NIcon, h(SirenIcon));
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
subAreaNode.children?.push(alarmNode);
|
||||||
// 添加警报器节点到子区域节点
|
|
||||||
if (!subAreaNode.children?.find((alarmNode) => alarmNode.key === alarmGbCode)) {
|
|
||||||
subAreaNode.children?.push(alarmNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统计站点、区域、子区域的在线/离线/总警报器数量
|
// 统计站点、区域、子区域的在线/离线/总警报器数量
|
||||||
siteNode.stats.total++;
|
siteNode.stats.total++;
|
||||||
targetMainAreaNode.stats.total++;
|
mainAreaNode.stats.total++;
|
||||||
subAreaNode.stats.total++;
|
subAreaNode.stats.total++;
|
||||||
if (alarm.status === 1) {
|
if (alarm.status === 1) {
|
||||||
siteNode.stats.online++;
|
siteNode.stats.online++;
|
||||||
targetMainAreaNode.stats.online++;
|
mainAreaNode.stats.online++;
|
||||||
subAreaNode.stats.online++;
|
subAreaNode.stats.online++;
|
||||||
}
|
} else if (alarm.status === 0) {
|
||||||
if (alarm.status === 0) {
|
|
||||||
siteNode.stats.offline++;
|
siteNode.stats.offline++;
|
||||||
targetMainAreaNode.stats.offline++;
|
mainAreaNode.stats.offline++;
|
||||||
subAreaNode.stats.offline++;
|
subAreaNode.stats.offline++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
siteNode.suffix = () => {
|
|
||||||
const { online, offline, total } = siteNode.stats;
|
|
||||||
return `(${online}/${offline}/${total})`;
|
|
||||||
};
|
|
||||||
siteNode.children?.forEach((areaNode) => {
|
|
||||||
areaNode.suffix = () => {
|
|
||||||
const { online, offline, total } = areaNode.stats;
|
|
||||||
return h('div', { style: { marginRight: '8px', opacity: 0.6 } }, `(${online}/${offline}/${total})`);
|
|
||||||
};
|
|
||||||
areaNode.children?.forEach((subAreaNode) => {
|
|
||||||
subAreaNode.suffix = () => {
|
|
||||||
const { online, offline, total } = subAreaNode.stats;
|
|
||||||
return h('div', { style: { marginRight: '16px', opacity: 0.4 } }, `(${online}/${offline}/${total})`);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
lineTabPanes.value = _lineTabPanes;
|
lineTabPanes.value = result;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
lineTabPanes,
|
lineTabPanes,
|
||||||
|
alarmRecord,
|
||||||
|
|
||||||
buildLineTabPanes,
|
buildLineTabPanes,
|
||||||
|
buildAlarmRecord,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
+102
-115
@@ -1,49 +1,53 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import type { VimpChannel, VimpStation } from '../apis';
|
import type { VimpChannel, VimpSite } from '../apis';
|
||||||
import { h, ref } from 'vue';
|
import { ref, shallowRef } from 'vue';
|
||||||
import type { CameraMainAreaNodeOption, CameraNodeOption, CodeArea, CodeLines, CodeSites, CameraLineTabPane, CameraSiteNodeOption, CameraSubAreaNodeOption } from '../types';
|
import type { CameraMainAreaNodeOption, CameraNodeOption, CodeLines, CodeSites, CameraLineTabPane, CameraSiteNodeOption, CameraSubAreaNodeOption, CompiledCodeAreas } from '../types';
|
||||||
import { NIcon } from 'naive-ui';
|
|
||||||
import BulletCamera from '../components/icon/bullet-camera.vue';
|
|
||||||
import PtzCamera from '../components/icon/ptz-camera.vue';
|
|
||||||
import HemiPtzCamera from '../components/icon/hemi-ptz-camera.vue';
|
|
||||||
|
|
||||||
interface BuildLineTabPanesParams {
|
interface BuildLineTabPanesParams {
|
||||||
sites: VimpStation[] | null;
|
sites: VimpSite[];
|
||||||
siteCamerasMap: Record<string, VimpChannel[]>;
|
siteCodeToCamerasMap: Map<string, VimpChannel[]>;
|
||||||
codeLines: CodeLines;
|
codeLines: CodeLines;
|
||||||
codeSites: CodeSites;
|
codeSites: CodeSites;
|
||||||
codeStationAreas: CodeArea[];
|
compiledCodeAreas: CompiledCodeAreas;
|
||||||
codeParkingAreas: CodeArea[];
|
|
||||||
codeOccAreas: CodeArea[];
|
|
||||||
codeTrainAreas: CodeArea[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildMainAreaNodeKey = (siteCode: string, mainAreaCode: string) => `${siteCode}${mainAreaCode}`;
|
||||||
|
const buildSubAreaNodeKey = (siteCode: string, areaCode: string) => `${siteCode}${areaCode}`;
|
||||||
|
|
||||||
export const useCameraStore = defineStore('vimp-camera-store', () => {
|
export const useCameraStore = defineStore('vimp-camera-store', () => {
|
||||||
const lineTabPanes = ref<CameraLineTabPane[]>([]);
|
const lineTabPanes = shallowRef<CameraLineTabPane[]>([]);
|
||||||
|
const cameraRecord = shallowRef<Record<string, VimpChannel>>({});
|
||||||
|
|
||||||
const buildLineTabPanes = (params: BuildLineTabPanesParams) => {
|
const buildLineTabPanes = (params: BuildLineTabPanesParams) => {
|
||||||
const { sites, siteCamerasMap, codeLines, codeSites, codeStationAreas, codeParkingAreas, codeOccAreas, codeTrainAreas } = params;
|
const { sites, siteCodeToCamerasMap, codeLines, codeSites, compiledCodeAreas } = params;
|
||||||
if (!sites) {
|
|
||||||
lineTabPanes.value = [];
|
const result: CameraLineTabPane[] = [];
|
||||||
return;
|
|
||||||
}
|
// 1. 线路索引 lineCode -> CameraLineTabPane
|
||||||
// 构造线路TabPane
|
const linePaneMap = new Map<string, CameraLineTabPane>();
|
||||||
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) {
|
for (const site of sites) {
|
||||||
const siteCode = site.code.substring(0, 6);
|
// 2. 站点节点 siteNode 只在当前轮次中顺序创建,不需要建立索引
|
||||||
const siteName = codeSites[siteCode]?.name;
|
|
||||||
if (!siteName) continue;
|
const lineCode = site.code.substring(0, 3);
|
||||||
|
const lineName = codeLines[lineCode]?.name ?? '';
|
||||||
|
|
||||||
|
let linePane = linePaneMap.get(lineCode);
|
||||||
|
if (!linePane) {
|
||||||
|
linePane = { lineCode, lineName, cameraTree: [] };
|
||||||
|
linePaneMap.set(lineCode, linePane);
|
||||||
|
result.push(linePane);
|
||||||
|
}
|
||||||
|
|
||||||
|
const siteCode = site.code;
|
||||||
|
const siteMeta = codeSites[siteCode];
|
||||||
|
if (!siteMeta) continue;
|
||||||
|
const siteName = siteMeta.name;
|
||||||
|
const siteType = siteMeta.type;
|
||||||
|
|
||||||
|
const compiledCodeAreaMaps = compiledCodeAreas[siteType];
|
||||||
|
const mainAreaCodeLength = siteType === 'train' ? 3 : 2;
|
||||||
|
|
||||||
// 构造站点节点
|
// 构造站点节点
|
||||||
const siteNode: CameraSiteNodeOption = {
|
const siteNode: CameraSiteNodeOption = {
|
||||||
@@ -53,135 +57,118 @@ export const useCameraStore = defineStore('vimp-camera-store', () => {
|
|||||||
stats: { online: 0, offline: 0, total: 0 },
|
stats: { online: 0, offline: 0, total: 0 },
|
||||||
online: site.online,
|
online: site.online,
|
||||||
};
|
};
|
||||||
_lineTabPanes.find((lineTabPane) => lineTabPane.lineCode === lineCode)?.cameraTree.push(siteNode);
|
linePane.cameraTree.push(siteNode);
|
||||||
|
|
||||||
// 获取所有摄像机
|
// 获取所有摄像机
|
||||||
const cameras = siteCamerasMap[site.code];
|
const cameras = siteCodeToCamerasMap.get(siteCode);
|
||||||
if (!cameras || cameras.length === 0) continue;
|
if (!cameras) continue;
|
||||||
|
|
||||||
|
// 3. 1级区域索引 mainAreaNodeKey -> CameraMainAreaNodeOption
|
||||||
|
// mainAreaNodeKey = ${siteCode}${cameraMainAreaCode}
|
||||||
|
const mainAreaNodeMap = new Map<string, CameraMainAreaNodeOption>();
|
||||||
|
// 4. 2级区域索引 subAreaNodeKey -> CameraSubAreaNodeOption
|
||||||
|
// subAreaNodeKey = ${siteCode}${cameraAreaCode}
|
||||||
|
const subAreaNodeMap = new Map<string, CameraSubAreaNodeOption>();
|
||||||
|
// 5. 摄像机索引 subAreaNodeKey -> Set<CameraGbCode>
|
||||||
|
const subAreaNodeKeyToCameraGbCodeSetMap = new Map<string, Set<string>>();
|
||||||
|
|
||||||
// 遍历摄像机
|
// 遍历摄像机
|
||||||
for (const camera of cameras) {
|
for (const camera of cameras) {
|
||||||
// 计算相关编码
|
// 计算相关编码
|
||||||
const { code: cameraGbCode, name: cameraName } = camera;
|
const { code: cameraGbCode, name: cameraName } = camera;
|
||||||
const cameraSiteCode = cameraGbCode.substring(0, 6);
|
|
||||||
const cameraSiteType = codeSites[cameraSiteCode]?.type;
|
|
||||||
const cameraAreaCode = cameraGbCode.substring(6, 11);
|
const cameraAreaCode = cameraGbCode.substring(6, 11);
|
||||||
const cameraMainAreaCode = cameraAreaCode.slice(0, 2);
|
const cameraMainAreaCode = cameraAreaCode.slice(0, mainAreaCodeLength);
|
||||||
|
|
||||||
// 构造车站/基地/OCC/车次区域
|
// 查找1级区域,如果未找到则跳过该摄像机
|
||||||
let siteArea: CodeArea | undefined = undefined;
|
const mainArea = compiledCodeAreaMaps.mainAreaMap.get(cameraMainAreaCode);
|
||||||
if (cameraSiteType === 'station') {
|
if (!mainArea) continue;
|
||||||
siteArea = codeStationAreas.find((area) => area.code === cameraMainAreaCode);
|
// 尝试从索引中获取1级区域节点,若不存在则创建
|
||||||
} else if (cameraSiteType === 'parking') {
|
const mainAreaNodeKey = buildMainAreaNodeKey(siteCode, cameraMainAreaCode);
|
||||||
siteArea = codeParkingAreas.find((area) => area.code === cameraMainAreaCode);
|
let mainAreaNode = mainAreaNodeMap.get(mainAreaNodeKey);
|
||||||
} else if (cameraSiteType === 'occ') {
|
if (!mainAreaNode) {
|
||||||
siteArea = codeOccAreas.find((area) => area.code === cameraMainAreaCode);
|
mainAreaNode = {
|
||||||
} else if (cameraSiteType === 'train') {
|
key: mainAreaNodeKey,
|
||||||
siteArea = codeTrainAreas.find((area) => area.code === cameraMainAreaCode);
|
label: mainArea.name,
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!siteArea) continue; // 如果还是未找到区域,则跳过该摄像机
|
|
||||||
|
|
||||||
// 构造1级区域节点
|
|
||||||
if (!siteNode.children?.find((areaNode) => areaNode.key === `${cameraSiteCode}${cameraMainAreaCode}`)) {
|
|
||||||
const mainAreaNode: CameraMainAreaNodeOption = {
|
|
||||||
key: `${cameraSiteCode}${cameraMainAreaCode}`,
|
|
||||||
label: siteArea.name,
|
|
||||||
children: [],
|
children: [],
|
||||||
stats: { online: 0, offline: 0, total: 0 },
|
stats: { online: 0, offline: 0, total: 0 },
|
||||||
site: site,
|
site: site,
|
||||||
|
areaLevel: 1,
|
||||||
};
|
};
|
||||||
|
mainAreaNodeMap.set(mainAreaNodeKey, mainAreaNode);
|
||||||
siteNode.children?.push(mainAreaNode);
|
siteNode.children?.push(mainAreaNode);
|
||||||
}
|
}
|
||||||
const targetMainAreaNode = siteNode.children?.find((areaNode) => areaNode.key === `${cameraSiteCode}${cameraMainAreaCode}`);
|
|
||||||
if (!targetMainAreaNode) continue; // 如果1级区域节点不存在,则跳过该摄像机
|
|
||||||
|
|
||||||
// 构造2级区域节点
|
// 查找2级区域,如果未找到则跳过该摄像机
|
||||||
if (!targetMainAreaNode.children?.find((subAreaNode) => subAreaNode.key === `${cameraSiteCode}${cameraAreaCode}`)) {
|
const subArea = compiledCodeAreaMaps.subAreaMap.get(cameraAreaCode);
|
||||||
let subArea: CodeArea['subs'][number] | undefined = undefined;
|
if (!subArea) continue;
|
||||||
if (cameraSiteType === 'station') {
|
// 尝试从索引中获取2级区域节点,若不存在则创建
|
||||||
subArea = codeStationAreas.find((area) => area.code === cameraMainAreaCode)?.subs.find((subArea) => subArea.code === cameraAreaCode);
|
const subAreaNodeKey = buildSubAreaNodeKey(siteCode, cameraAreaCode);
|
||||||
} else if (cameraSiteType === 'parking') {
|
let subAreaNode = subAreaNodeMap.get(subAreaNodeKey);
|
||||||
subArea = codeParkingAreas.find((area) => area.code === cameraMainAreaCode)?.subs.find((subArea) => subArea.code === cameraAreaCode);
|
if (!subAreaNode) {
|
||||||
} else if (cameraSiteType === 'occ') {
|
subAreaNode = {
|
||||||
subArea = codeOccAreas.find((area) => area.code === cameraMainAreaCode)?.subs.find((subArea) => subArea.code === cameraAreaCode);
|
key: subAreaNodeKey,
|
||||||
} else if (cameraSiteType === 'train') {
|
|
||||||
subArea = codeTrainAreas.find((area) => area.code === cameraMainAreaCode)?.subs.find((subArea) => subArea.code === cameraAreaCode);
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!subArea) continue; // 如果还是未找到2级区域,则跳过该摄像机
|
|
||||||
|
|
||||||
const subAreaNode: CameraSubAreaNodeOption = {
|
|
||||||
key: `${cameraSiteCode}${cameraAreaCode}`,
|
|
||||||
label: subArea.name,
|
label: subArea.name,
|
||||||
children: [],
|
children: [],
|
||||||
stats: { online: 0, offline: 0, total: 0 },
|
stats: { online: 0, offline: 0, total: 0 },
|
||||||
site: site,
|
site: site,
|
||||||
|
areaLevel: 2,
|
||||||
};
|
};
|
||||||
targetMainAreaNode.children?.push(subAreaNode);
|
subAreaNodeMap.set(subAreaNodeKey, subAreaNode);
|
||||||
|
mainAreaNode.children?.push(subAreaNode);
|
||||||
}
|
}
|
||||||
const subAreaNode = targetMainAreaNode.children?.find((subAreaNode) => subAreaNode.key === `${cameraSiteCode}${cameraAreaCode}`);
|
|
||||||
if (!subAreaNode) continue; // 如果子区域节点不存在,则跳过该摄像机
|
|
||||||
|
|
||||||
// 构造摄像机节点
|
// 构造摄像机节点
|
||||||
const cameraType = camera.code.substring(11, 14);
|
let cameraGbCodeSet = subAreaNodeKeyToCameraGbCodeSetMap.get(subAreaNodeKey);
|
||||||
|
if (!cameraGbCodeSet) {
|
||||||
|
cameraGbCodeSet = new Set<string>();
|
||||||
|
subAreaNodeKeyToCameraGbCodeSetMap.set(subAreaNodeKey, cameraGbCodeSet);
|
||||||
|
}
|
||||||
|
if (cameraGbCodeSet.has(cameraGbCode)) continue;
|
||||||
|
cameraGbCodeSet.add(cameraGbCode);
|
||||||
|
const cameraType = cameraGbCode.substring(11, 14);
|
||||||
const cameraNode: CameraNodeOption = {
|
const cameraNode: CameraNodeOption = {
|
||||||
key: cameraGbCode,
|
key: cameraGbCode,
|
||||||
label: cameraName,
|
label: cameraName,
|
||||||
type: cameraType,
|
type: cameraType,
|
||||||
camera: camera,
|
camera: camera,
|
||||||
site: site,
|
site: site,
|
||||||
prefix: () => {
|
|
||||||
if (cameraType === '004') return h(NIcon, h(PtzCamera));
|
|
||||||
if (cameraType === '005') return h(NIcon, h(HemiPtzCamera));
|
|
||||||
if (cameraType === '006') return h(NIcon, h(BulletCamera));
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
subAreaNode.children?.push(cameraNode);
|
||||||
// 添加摄像机节点到子区域节点
|
|
||||||
if (!subAreaNode.children?.find((cameraNode) => cameraNode.key === cameraGbCode)) {
|
|
||||||
subAreaNode.children?.push(cameraNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统计站点、区域、子区域的在线/离线/总摄像机数量
|
// 统计站点、区域、子区域的在线/离线/总摄像机数量
|
||||||
siteNode.stats.total++;
|
siteNode.stats.total++;
|
||||||
targetMainAreaNode.stats.total++;
|
mainAreaNode.stats.total++;
|
||||||
subAreaNode.stats.total++;
|
subAreaNode.stats.total++;
|
||||||
if (camera.status === 1) {
|
if (camera.status === 1) {
|
||||||
siteNode.stats.online++;
|
siteNode.stats.online++;
|
||||||
targetMainAreaNode.stats.online++;
|
mainAreaNode.stats.online++;
|
||||||
subAreaNode.stats.online++;
|
subAreaNode.stats.online++;
|
||||||
}
|
} else if (camera.status === 0) {
|
||||||
if (camera.status === 0) {
|
|
||||||
siteNode.stats.offline++;
|
siteNode.stats.offline++;
|
||||||
targetMainAreaNode.stats.offline++;
|
mainAreaNode.stats.offline++;
|
||||||
subAreaNode.stats.offline++;
|
subAreaNode.stats.offline++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
siteNode.suffix = () => {
|
|
||||||
const { online, offline, total } = siteNode.stats;
|
|
||||||
return `(${online}/${offline}/${total})`;
|
|
||||||
};
|
|
||||||
siteNode.children?.forEach((areaNode) => {
|
|
||||||
areaNode.suffix = () => {
|
|
||||||
const { online, offline, total } = areaNode.stats;
|
|
||||||
return h('div', { style: { marginRight: '8px', opacity: 0.6 } }, `(${online}/${offline}/${total})`);
|
|
||||||
};
|
|
||||||
areaNode.children?.forEach((subAreaNode) => {
|
|
||||||
subAreaNode.suffix = () => {
|
|
||||||
const { online, offline, total } = subAreaNode.stats;
|
|
||||||
return h('div', { style: { marginRight: '16px', opacity: 0.4 } }, `(${online}/${offline}/${total})`);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
lineTabPanes.value = _lineTabPanes;
|
|
||||||
|
lineTabPanes.value = result;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
lineTabPanes,
|
lineTabPanes,
|
||||||
|
cameraRecord,
|
||||||
|
|
||||||
buildLineTabPanes,
|
buildLineTabPanes,
|
||||||
|
buildCameraRecord,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,50 @@
|
|||||||
import type { TabPaneProps, TreeOption } from 'naive-ui';
|
import type { TabPaneProps, TreeOption } from 'naive-ui';
|
||||||
import type { VimpChannel, VimpStation } from '../apis/model';
|
import type { VimpChannel, VimpSite } from '../apis/model';
|
||||||
|
|
||||||
export type SiteType = 'station' | 'parking' | 'occ' | 'train';
|
export type SiteType = 'station' | 'parking' | 'occ' | 'train';
|
||||||
export type CodeLines = Record<string, { name: string; color: string }>;
|
export type CodeLines = Record<string, { name: string; color: string }>;
|
||||||
export type CodeSites = Record<string, { name: string; type: SiteType }>;
|
export type CodeSites = Record<string, { name: string; type: SiteType }>;
|
||||||
export type CodeArea = { code: string; name: string; subs: { code: string; name: string }[] };
|
export type CodeArea = { code: string; name: string; subs: { code: string; name: string }[] };
|
||||||
|
|
||||||
|
export type CompiledCodeAreaMaps = {
|
||||||
|
mainAreaMap: Map<string, CodeArea>;
|
||||||
|
subAreaMap: Map<string, CodeArea['subs'][number]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CompiledCodeAreas = Record<SiteType, CompiledCodeAreaMaps>;
|
||||||
|
|
||||||
|
interface CompileCodeAreasParams {
|
||||||
|
codeStationAreas: CodeArea[];
|
||||||
|
codeParkingAreas: CodeArea[];
|
||||||
|
codeOccAreas: CodeArea[];
|
||||||
|
codeTrainAreas: CodeArea[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const compileCodeAreaMaps = (areas: CodeArea[]): CompiledCodeAreaMaps => {
|
||||||
|
const mainAreaMap = new Map<string, CodeArea>();
|
||||||
|
const subAreaMap = new Map<string, CodeArea['subs'][number]>();
|
||||||
|
for (const area of areas) {
|
||||||
|
mainAreaMap.set(area.code, area);
|
||||||
|
for (const subArea of area.subs) {
|
||||||
|
subAreaMap.set(subArea.code, subArea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mainAreaMap,
|
||||||
|
subAreaMap,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const compileCodeAreas = (parmas: CompileCodeAreasParams): CompiledCodeAreas => {
|
||||||
|
const { codeStationAreas, codeParkingAreas, codeOccAreas, codeTrainAreas } = parmas;
|
||||||
|
return {
|
||||||
|
station: compileCodeAreaMaps(codeStationAreas),
|
||||||
|
parking: compileCodeAreaMaps(codeParkingAreas),
|
||||||
|
occ: compileCodeAreaMaps(codeOccAreas),
|
||||||
|
train: compileCodeAreaMaps(codeTrainAreas),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export interface CountStats {
|
export interface CountStats {
|
||||||
online: number;
|
online: number;
|
||||||
offline: number;
|
offline: number;
|
||||||
@@ -18,19 +57,21 @@ export interface CountStats {
|
|||||||
export interface CameraNodeOption extends TreeOption {
|
export interface CameraNodeOption extends TreeOption {
|
||||||
camera: VimpChannel;
|
camera: VimpChannel;
|
||||||
type: string;
|
type: string;
|
||||||
site: VimpStation;
|
site: VimpSite;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CameraSubAreaNodeOption extends TreeOption {
|
export interface CameraSubAreaNodeOption extends TreeOption {
|
||||||
children?: CameraNodeOption[];
|
children?: CameraNodeOption[];
|
||||||
stats: CountStats;
|
stats: CountStats;
|
||||||
site: VimpStation;
|
site: VimpSite;
|
||||||
|
areaLevel: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CameraMainAreaNodeOption extends TreeOption {
|
export interface CameraMainAreaNodeOption extends TreeOption {
|
||||||
children?: CameraSubAreaNodeOption[];
|
children?: CameraSubAreaNodeOption[];
|
||||||
stats: CountStats;
|
stats: CountStats;
|
||||||
site: VimpStation;
|
site: VimpSite;
|
||||||
|
areaLevel: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CameraSiteNodeOption extends TreeOption {
|
export interface CameraSiteNodeOption extends TreeOption {
|
||||||
@@ -63,19 +104,21 @@ export interface CameraLineTabPane extends TabPaneProps {
|
|||||||
export interface AlarmNodeOption extends TreeOption {
|
export interface AlarmNodeOption extends TreeOption {
|
||||||
alarm: VimpChannel;
|
alarm: VimpChannel;
|
||||||
type: string;
|
type: string;
|
||||||
site: VimpStation;
|
site: VimpSite;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AlarmSubAreaNodeOption extends TreeOption {
|
export interface AlarmSubAreaNodeOption extends TreeOption {
|
||||||
children?: AlarmNodeOption[];
|
children?: AlarmNodeOption[];
|
||||||
stats: CountStats;
|
stats: CountStats;
|
||||||
site: VimpStation;
|
site: VimpSite;
|
||||||
|
areaLevel: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AlarmMainAreaNodeOption extends TreeOption {
|
export interface AlarmMainAreaNodeOption extends TreeOption {
|
||||||
children?: AlarmSubAreaNodeOption[];
|
children?: AlarmSubAreaNodeOption[];
|
||||||
stats: CountStats;
|
stats: CountStats;
|
||||||
site: VimpStation;
|
site: VimpSite;
|
||||||
|
areaLevel: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AlarmSiteNodeOption extends TreeOption {
|
export interface AlarmSiteNodeOption extends TreeOption {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ResourcePanel from './components/resource-pannel.vue';
|
import ResourcePanel from './components/resource-panel.vue';
|
||||||
|
|
||||||
const onDragover = (event: DragEvent) => {
|
const onDragover = (event: DragEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -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.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://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://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: '/vimp/api', target: 'http://10.18.128.6:18080', rewrite: ['/vimp/api', '/api'] },
|
||||||
{ key: '/cdn', target: `http://localhost:${SERVER_PORT}`, rewrite: ['/cdn', ''] },
|
{ key: '/cdn', target: `http://localhost:${SERVER_PORT}`, rewrite: ['/cdn', ''] },
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user