4 Commits

Author SHA1 Message Date
yangsy ad9a0011a5 feat(vimp): 新增摄像头和告警通道的全局索引记录及构建方法
在摄像头、告警的Pinia存储中新增对应的数据记录与构建函数,并在渠道查询组合式函数中调用生成全局索引映射数据
2026-06-09 13:17:59 +08:00
yangsy 8403a41cef feat(settings-drawer): 为调试码输入框添加自动聚焦功能
更新了相关导入,添加输入框模板引用并在调试模态框打开时自动聚焦输入框
2026-06-09 13:16:33 +08:00
yangsy e8aa9b96f8 refactor(vimp): 重命名设备查询为通道查询并更新相关引用
- 新增 vimp 常量文件,定义查询键 VIMP_CHANNELS_QUERY_KEY
- 重命名设备中心查询组合式函数为通道查询组合式函数
- 更新 alarm-tree.vue 和 camera-tree.vue 中的查询调用
- 优化通道数据排序与存储更新流程
2026-06-08 14:40:58 +08:00
yangsy 6f14370751 refactor(vimp): 拆分站点模型为 VimpRawSite / VimpSite 并归一化 online 字段
- 将 VimpStation 重命名为 VimpSite,统一"站点"语义贯穿全模块
- 新增 VimpRawSite 表示 /catalog/allDevice 接口原样数据,online 字段可选
- 抽离 normalizeVimpSite 纯函数,集中处理 online 字段缺失时的默认值
- catalogAllDeviceApi 入参改为 VimpRawSite[],出参统一归一化为 VimpSite[] | null
- 下游 store、composable、树节点类型同步从 VimpStation 切换到 VimpSite
- 内部业务模型保持 online: boolean 必填,避免缺失字段沿调用链向下传播
2026-06-08 13:38:38 +08:00
14 changed files with 104 additions and 35 deletions
@@ -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 -1
View File
@@ -1,2 +1,2 @@
export * from './vimp-channel';
export * from './vimp-station';
export * from './vimp-site';
+17
View File
@@ -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 type { VimpStation } from '../model';
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<VimpStation[]>(endpoint, {}, { signal });
const resp = await client.post<VimpRawSite[]>(endpoint, {}, { signal });
const data = unwrapVimpResponse(resp);
return data;
return data?.map(normalizeVimpSite) ?? null;
};
+2 -2
View File
@@ -3,11 +3,11 @@ import { NIcon, NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type
import { h, type CSSProperties } from 'vue';
import { useAlarmStore, useResourcePanelStore } from '../stores';
import { storeToRefs } from 'pinia';
import { useDeviceCenterQuery } from '../composables';
import { useChannelsQuery } from '../composables';
import { isAlarmNode, isAlarmSiteNode, isAlarmAreaNode } from '../types';
import { SirenIcon } from 'lucide-vue-next';
const { isLoading } = useDeviceCenterQuery();
const { isLoading } = useChannelsQuery();
const alarmStore = useAlarmStore();
const { lineTabPanes } = storeToRefs(alarmStore);
+2 -2
View File
@@ -3,13 +3,13 @@ import { NIcon, NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type
import { h, type CSSProperties } from 'vue';
import { useCameraStore, useResourcePanelStore } from '../stores';
import { storeToRefs } from 'pinia';
import { useDeviceCenterQuery } from '../composables';
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 } = useDeviceCenterQuery();
const { isLoading } = useChannelsQuery();
const cameraStore = useCameraStore();
const { lineTabPanes } = storeToRefs(cameraStore);
+1 -1
View File
@@ -1 +1 @@
export * from './use-device-center-query';
export * from './use-channels-query';
@@ -4,7 +4,8 @@ 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 VimpStation } from '../../apis';
import { catalogAllDeviceApi, catalogChannelApi, type VimpChannel, type VimpSite } from '../../apis';
import { VIMP_CHANNELS_QUERY_KEY } from '../../constants';
const config: AxiosRequestConfig = {
headers: {
@@ -35,7 +36,7 @@ const compareByCode = <T extends { code: string }>(a: T, b: T) => {
if (a.code > b.code) return 1;
return 0;
};
const sortSitesByCode = (sites: VimpStation[]) => {
const sortSitesByCode = (sites: VimpSite[]) => {
sites.sort(compareByCode);
};
const sortChannelsMapByCode = (siteCodeToChannelsMap: Map<string, VimpChannel[]>) => {
@@ -44,12 +45,12 @@ const sortChannelsMapByCode = (siteCodeToChannelsMap: Map<string, VimpChannel[]>
}
};
export const useDeviceCenterQuery = () => {
export const useChannelsQuery = () => {
const cameraStore = useCameraStore();
const alarmStore = useAlarmStore();
return useQuery({
queryKey: computed(() => ['vimp-device']),
queryKey: computed(() => [VIMP_CHANNELS_QUERY_KEY]),
refetchInterval: 10 * 1000,
refetchOnWindowFocus: false,
queryFn: async ({ signal }) => {
@@ -73,8 +74,8 @@ export const useDeviceCenterQuery = () => {
// 从 /allDevice 接口获取的站点信息并不保证真实性和完整性,
// 例如有一个站点的编码是 010699 开头,但是其下的通道是 010199 和 010599 开头,
// 而 010699 是一个不存在的站点编码,所以需要基于通道的编码来确定所有的站点。
const cameraSites: VimpStation[] = [];
const alarmSites: VimpStation[] = [];
const cameraSites: VimpSite[] = [];
const alarmSites: VimpSite[] = [];
const cameraBuiltSitesSet = new Set<string>();
const alarmBuiltSitesSet = new Set<string>();
const siteCodeToCamerasMap = new Map<string, VimpChannel[]>();
@@ -133,6 +134,7 @@ export const useDeviceCenterQuery = () => {
codeSites,
compiledCodeAreas,
});
cameraStore.buildCameraRecord(siteCodeToCamerasMap);
alarmStore.buildLineTabPanes({
sites: alarmSites,
@@ -141,6 +143,7 @@ export const useDeviceCenterQuery = () => {
codeSites,
compiledCodeAreas,
});
alarmStore.buildAlarmRecord(siteCodeToAlarmsMap);
return null;
},
+1
View File
@@ -0,0 +1 @@
export * from './query';
+1
View File
@@ -0,0 +1 @@
export const VIMP_CHANNELS_QUERY_KEY = 'vimp-channels';
+16 -2
View File
@@ -1,10 +1,10 @@
import { defineStore } from 'pinia';
import type { VimpChannel, VimpStation } from '../apis';
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[];
sites: VimpSite[];
siteCodeToAlarmsMap: Map<string, VimpChannel[]>;
codeLines: CodeLines;
codeSites: CodeSites;
@@ -16,6 +16,7 @@ const buildSubAreaNodeKey = (siteCode: string, areaCode: string) => `${siteCode}
export const useAlarmStore = defineStore('vimp-alarm-store', () => {
const lineTabPanes = shallowRef<AlarmLineTabPane[]>([]);
const alarmRecord = shallowRef<Record<string, VimpChannel>>({});
const buildLineTabPanes = (params: BuildLineTabPanesParams) => {
const { sites, siteCodeToAlarmsMap, codeLines, codeSites, compiledCodeAreas } = params;
@@ -154,8 +155,21 @@ export const useAlarmStore = defineStore('vimp-alarm-store', () => {
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,
};
});
+17 -3
View File
@@ -1,10 +1,10 @@
import { defineStore } from 'pinia';
import type { VimpChannel, VimpStation } from '../apis';
import { shallowRef } from 'vue';
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[];
sites: VimpSite[];
siteCodeToCamerasMap: Map<string, VimpChannel[]>;
codeLines: CodeLines;
codeSites: CodeSites;
@@ -16,6 +16,7 @@ const buildSubAreaNodeKey = (siteCode: string, areaCode: string) => `${siteCode}
export const useCameraStore = defineStore('vimp-camera-store', () => {
const lineTabPanes = shallowRef<CameraLineTabPane[]>([]);
const cameraRecord = shallowRef<Record<string, VimpChannel>>({});
const buildLineTabPanes = (params: BuildLineTabPanesParams) => {
const { sites, siteCodeToCamerasMap, codeLines, codeSites, compiledCodeAreas } = params;
@@ -153,8 +154,21 @@ export const useCameraStore = defineStore('vimp-camera-store', () => {
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,
};
});
+7 -7
View File
@@ -1,5 +1,5 @@
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 CodeLines = Record<string, { name: string; color: string }>;
@@ -57,20 +57,20 @@ export interface CountStats {
export interface CameraNodeOption extends TreeOption {
camera: VimpChannel;
type: string;
site: VimpStation;
site: VimpSite;
}
export interface CameraSubAreaNodeOption extends TreeOption {
children?: CameraNodeOption[];
stats: CountStats;
site: VimpStation;
site: VimpSite;
areaLevel: 2;
}
export interface CameraMainAreaNodeOption extends TreeOption {
children?: CameraSubAreaNodeOption[];
stats: CountStats;
site: VimpStation;
site: VimpSite;
areaLevel: 1;
}
@@ -104,20 +104,20 @@ export interface CameraLineTabPane extends TabPaneProps {
export interface AlarmNodeOption extends TreeOption {
alarm: VimpChannel;
type: string;
site: VimpStation;
site: VimpSite;
}
export interface AlarmSubAreaNodeOption extends TreeOption {
children?: AlarmNodeOption[];
stats: CountStats;
site: VimpStation;
site: VimpSite;
areaLevel: 2;
}
export interface AlarmMainAreaNodeOption extends TreeOption {
children?: AlarmSubAreaNodeOption[];
stats: CountStats;
site: VimpStation;
site: VimpSite;
areaLevel: 1;
}