5 Commits

Author SHA1 Message Date
yangsy 0b1a0546dd style(vimp): 清理未使用的lucide图标导入 2026-05-29 00:36:57 +08:00
yangsy 7b15300ab7 feat(resource-pannel): 为资源面板添加标签页图标和新标签,清理未使用导入
- 优化资源面板标签页布局,将图标置于文字上方
- 新增复合技和地图两个标签页
更新标签页数据结构以支持图标配置
移除未使用的naive-ui和vueuse依赖导入
2026-05-29 00:35:42 +08:00
yangsy 8fae86d6ff feat(vimp-resource): 优化资源面板,添加设备搜索与图标展示
- 新增bullet-camera、hemi-ptz-camera、ptz-camera三个自定义svg摄像头图标
- 替换告警和摄像头列表的文字前缀为对应图标展示
- 重构资源面板状态管理,简化搜索关键词的存储逻辑
- 为摄像头和告警树添加本地搜索过滤功能,搜索时自动展开所有节点
- 重构资源面板UI布局,添加折叠动画,优化搜索框显示逻辑与侧边栏样式
2026-05-29 00:11:43 +08:00
yangsy 2f38e97481 fix(resource-pannel): 调整资源面板的展开触发方式为点击标签页
移除顶部资源标题的点击展开事件,为各标签页添加点击触发展开的事件并优化模板条件顺序,提升用户体验
2026-05-28 16:24:55 +08:00
yangsy d5b380e1e3 refactor(vimp): 提取资源面板为独立组件并添加pinia存储
- 将原内嵌的资源标签页逻辑提取为独立组件
- 新增专用pinia存储管理资源面板的折叠和搜索状态
- 统一折叠展开与搜索交互的逻辑实现
2026-05-28 16:23:42 +08:00
10 changed files with 233 additions and 42 deletions
+21 -5
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type TreeProps } from 'naive-ui'; import { NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type TreeProps } from 'naive-ui';
import { h, type CSSProperties } from 'vue'; import { h, type CSSProperties } from 'vue';
import { useAlarmStore } from '../stores'; import { useAlarmStore, useResourcePanelStore } from '../stores';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useDeviceCenterQuery } from '../composables'; import { useDeviceCenterQuery } from '../composables';
import { isAlarmNode, isAlarmSiteNode, isAlarmAreaNode } from '../types'; import { isAlarmNode, isAlarmSiteNode, isAlarmAreaNode } from '../types';
@@ -76,6 +76,14 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
// 其他节点(兜底,理论上不会走到这里) // 其他节点(兜底,理论上不会走到这里)
return option.label; return option.label;
}; };
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> </script>
<template> <template>
@@ -89,9 +97,13 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
show-line show-line
virtual-scroll virtual-scroll
style="height: 100%" style="height: 100%"
:data="lineTabPanes.at(0)?.alarmTree"
:override-default-node-click-behavior="overrideNodeClickBehavior"
:render-label="renderNodeLabel" :render-label="renderNodeLabel"
: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>
<template v-if="lineTabPanes.length > 1"> <template v-if="lineTabPanes.length > 1">
@@ -103,9 +115,13 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
show-line show-line
virtual-scroll virtual-scroll
style="height: 100%" style="height: 100%"
:data="alarmTree"
:override-default-node-click-behavior="overrideNodeClickBehavior"
:render-label="renderNodeLabel" :render-label="renderNodeLabel"
:override-default-node-click-behavior="overrideNodeClickBehavior"
:default-expand-all="false"
:show-irrelevant-nodes="false"
:data="alarmTree"
:pattern="searchPattern"
:filter="searchFilter"
/> />
</NTabPane> </NTabPane>
</NTabs> </NTabs>
+21 -5
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type TreeProps } from 'naive-ui'; import { NTabPane, NTabs, NTree, type TreeOverrideNodeClickBehavior, type TreeProps } from 'naive-ui';
import { h, type CSSProperties } from 'vue'; import { h, type CSSProperties } from 'vue';
import { useCameraStore } from '../stores'; import { useCameraStore, useResourcePanelStore } from '../stores';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useDeviceCenterQuery } from '../composables'; import { useDeviceCenterQuery } from '../composables';
import { isCameraNode, isCameraSiteNode, isCameraAreaNode } from '../types'; import { isCameraNode, isCameraSiteNode, isCameraAreaNode } from '../types';
@@ -76,6 +76,14 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
// 其他节点(兜底,理论上不会走到这里) // 其他节点(兜底,理论上不会走到这里)
return option.label; return option.label;
}; };
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> </script>
<template> <template>
@@ -89,9 +97,13 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
show-line show-line
virtual-scroll virtual-scroll
style="height: 100%" style="height: 100%"
:data="lineTabPanes.at(0)?.cameraTree"
:override-default-node-click-behavior="overrideNodeClickBehavior"
:render-label="renderNodeLabel" :render-label="renderNodeLabel"
: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>
<template v-if="lineTabPanes.length > 1"> <template v-if="lineTabPanes.length > 1">
@@ -103,9 +115,13 @@ const renderNodeLabel: TreeProps['renderLabel'] = ({ option }) => {
show-line show-line
virtual-scroll virtual-scroll
style="height: 100%" style="height: 100%"
:data="cameraTree"
:override-default-node-click-behavior="overrideNodeClickBehavior"
:render-label="renderNodeLabel" :render-label="renderNodeLabel"
:override-default-node-click-behavior="overrideNodeClickBehavior"
:default-expand-all="false"
:show-irrelevant-nodes="false"
:data="cameraTree"
:pattern="searchPattern"
:filter="searchFilter"
/> />
</NTabPane> </NTabPane>
</NTabs> </NTabs>
@@ -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>
+3 -1
View File
@@ -2,6 +2,8 @@ import { defineStore } from 'pinia';
import type { VimpChannel, VimpStation } from '../apis'; import type { VimpChannel, VimpStation } from '../apis';
import { h, ref } from 'vue'; import { h, ref } from 'vue';
import type { AlarmMainAreaNodeOption, AlarmNodeOption, CodeArea, CodeLines, CodeSites, AlarmLineTabPane, AlarmSiteNodeOption, AlarmSubAreaNodeOption } from '../types'; import type { AlarmMainAreaNodeOption, AlarmNodeOption, CodeArea, CodeLines, CodeSites, AlarmLineTabPane, AlarmSiteNodeOption, AlarmSubAreaNodeOption } from '../types';
import { NIcon } from 'naive-ui';
import { SirenIcon } from 'lucide-vue-next';
interface BuildLineTabPanesParams { interface BuildLineTabPanesParams {
sites: VimpStation[] | null; sites: VimpStation[] | null;
@@ -130,7 +132,7 @@ export const useAlarmStore = defineStore('vimp-alarm-store', () => {
alarm: alarm, alarm: alarm,
site: site, site: site,
prefix: () => { prefix: () => {
return `[警报器]`; return h(NIcon, h(SirenIcon));
}, },
}; };
+7 -3
View File
@@ -2,6 +2,10 @@ import { defineStore } from 'pinia';
import type { VimpChannel, VimpStation } from '../apis'; import type { VimpChannel, VimpStation } from '../apis';
import { h, ref } from 'vue'; import { h, ref } from 'vue';
import type { CameraMainAreaNodeOption, CameraNodeOption, CodeArea, CodeLines, CodeSites, CameraLineTabPane, CameraSiteNodeOption, CameraSubAreaNodeOption } from '../types'; import type { CameraMainAreaNodeOption, CameraNodeOption, CodeArea, CodeLines, CodeSites, CameraLineTabPane, CameraSiteNodeOption, CameraSubAreaNodeOption } 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: VimpStation[] | null;
@@ -130,9 +134,9 @@ export const useCameraStore = defineStore('vimp-camera-store', () => {
camera: camera, camera: camera,
site: site, site: site,
prefix: () => { prefix: () => {
if (cameraType === '004') return `[枪机]`; if (cameraType === '004') return h(NIcon, h(PtzCamera));
if (cameraType === '005') return `[半球]`; if (cameraType === '005') return h(NIcon, h(HemiPtzCamera));
if (cameraType === '006') return `[球机]`; if (cameraType === '006') return h(NIcon, h(BulletCamera));
}, },
}; };
+10 -4
View File
@@ -2,11 +2,17 @@ import { defineStore } from 'pinia';
import { ref } from 'vue'; import { ref } from 'vue';
export const useResourcePanelStore = defineStore('vimp-resource-panel', () => { export const useResourcePanelStore = defineStore('vimp-resource-panel', () => {
const showSearch = ref<boolean>(false); const collapsed = ref<boolean>(false);
const searchText = ref<string>(''); const searchPattern = ref<string>('');
const toggleCollapsed = () => {
collapsed.value = !collapsed.value;
};
return { return {
showSearch, collapsed,
searchText, searchPattern,
toggleCollapsed,
}; };
}); });
+2 -24
View File
@@ -1,19 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { NTabPane, NTabs, type TabPaneProps } from 'naive-ui'; import ResourcePanel from './components/resource-pannel.vue';
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 onDragover = (event: DragEvent) => { const onDragover = (event: DragEvent) => {
event.preventDefault(); event.preventDefault();
@@ -43,15 +29,7 @@ const onDrop = (event: DragEvent) => {
<template> <template>
<div style="height: 100%; overflow: hidden; display: flex"> <div style="height: 100%; overflow: hidden; display: flex">
<div style="width: 540px; height: 100%; overflow: hidden"> <ResourcePanel />
<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" />
</template>
</NTabPane>
</NTabs>
</div>
<div style="flex: 1"> <div style="flex: 1">
<div style="height: 480px; background-color: #666; display: grid; place-items: center" @dragover="onDragover" @drop="onDrop"> <div style="height: 480px; background-color: #666; display: grid; place-items: center" @dragover="onDragover" @drop="onDrop">
<div>这里是播放器</div> <div>这里是播放器</div>