feat(vimp-resource): 优化资源面板,添加设备搜索与图标展示
- 新增bullet-camera、hemi-ptz-camera、ptz-camera三个自定义svg摄像头图标 - 替换告警和摄像头列表的文字前缀为对应图标展示 - 重构资源面板状态管理,简化搜索关键词的存储逻辑 - 为摄像头和告警树添加本地搜索过滤功能,搜索时自动展开所有节点 - 重构资源面板UI布局,添加折叠动画,优化搜索框显示逻辑与侧边栏样式
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NButton, NFlex, NIcon, NInput, NPageHeader, NTabPane, NTabs, type TabPaneProps } from 'naive-ui';
|
import { NButton, NFlex, NIcon, NInput, NPageHeader, NTabPane, NTabs, NText, type TabPaneProps } from 'naive-ui';
|
||||||
import { ref, type Component } from 'vue';
|
import { computed, ref, type Component } from 'vue';
|
||||||
import AlarmTree from './alarm-tree.vue';
|
import AlarmTree from './alarm-tree.vue';
|
||||||
import CameraTree from './camera-tree.vue';
|
import CameraTree from './camera-tree.vue';
|
||||||
import { ChevronLeftIcon } from 'lucide-vue-next';
|
import { ChevronLeftIcon, PanelBottom } from 'lucide-vue-next';
|
||||||
import { useResourcePanelStore } from '../stores';
|
import { useResourcePanelStore } from '../stores';
|
||||||
import { watchImmediate } from '@vueuse/core';
|
import { watchImmediate } from '@vueuse/core';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
|
const PANEL_WIDTH_EXPANDED = '480px';
|
||||||
|
const PANEL_WIDTH_COLLAPSED = '72px';
|
||||||
|
|
||||||
interface ResourceTabPane extends TabPaneProps {
|
interface ResourceTabPane extends TabPaneProps {
|
||||||
name: string;
|
name: string;
|
||||||
tab: string;
|
tab: string;
|
||||||
@@ -22,8 +25,12 @@ const resourceTabPanes: ResourceTabPane[] = [
|
|||||||
|
|
||||||
const activeTabName = ref(resourceTabPanes.at(0)?.name ?? '');
|
const activeTabName = ref(resourceTabPanes.at(0)?.name ?? '');
|
||||||
|
|
||||||
|
const showSearchInput = computed(() => {
|
||||||
|
return ['camera', 'alarm'].includes(activeTabName.value);
|
||||||
|
});
|
||||||
|
|
||||||
const resourcePanelStore = useResourcePanelStore();
|
const resourcePanelStore = useResourcePanelStore();
|
||||||
const { collapsed, searchInput } = storeToRefs(resourcePanelStore);
|
const { collapsed, searchPattern } = storeToRefs(resourcePanelStore);
|
||||||
|
|
||||||
const collapseResourcePanel = () => {
|
const collapseResourcePanel = () => {
|
||||||
if (!collapsed.value) {
|
if (!collapsed.value) {
|
||||||
@@ -39,17 +46,50 @@ const expandResourcePanel = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NFlex vertical :size="0" style="width: 480px; height: 100%; overflow: hidden">
|
<div
|
||||||
<NFlex style="width: 100%; min-height: 38px; padding: 8px 16px" :align="'center'">
|
class="resource-panel__wrapper"
|
||||||
<div>资源</div>
|
:style="{
|
||||||
<template v-if="!collapsed">
|
width: collapsed ? PANEL_WIDTH_COLLAPSED : PANEL_WIDTH_EXPANDED,
|
||||||
<NInput clearable :size="'tiny'" :placeholder="'搜索'" style="width: 360px; margin-left: auto" />
|
}"
|
||||||
|
>
|
||||||
|
<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">
|
<NButton text @click="collapseResourcePanel">
|
||||||
<NIcon :component="ChevronLeftIcon"></NIcon>
|
<NIcon :component="ChevronLeftIcon"></NIcon>
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</div>
|
||||||
</NFlex>
|
</div>
|
||||||
<NTabs :type="'line'" :placement="'left'" style="height: 100%; flex: 1" v-model:value="activeTabName">
|
<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
|
<NTabPane
|
||||||
v-for="{ name: resourceName, tab: resourceTab, component } in resourceTabPanes"
|
v-for="{ name: resourceName, tab: resourceTab, component } in resourceTabPanes"
|
||||||
:key="resourceName"
|
:key="resourceName"
|
||||||
@@ -57,12 +97,40 @@ const expandResourcePanel = () => {
|
|||||||
:name="resourceName"
|
:name="resourceName"
|
||||||
:tab-props="{ onClick: () => expandResourcePanel() }"
|
:tab-props="{ onClick: () => expandResourcePanel() }"
|
||||||
>
|
>
|
||||||
<template v-if="!collapsed && !!component">
|
<template v-if="!!component">
|
||||||
<component :is="component" />
|
<component :is="component" />
|
||||||
</template>
|
</template>
|
||||||
</NTabPane>
|
</NTabPane>
|
||||||
</NTabs>
|
</NTabs>
|
||||||
</NFlex>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<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>
|
||||||
|
|||||||
@@ -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));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ 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 { CctvIcon } from 'lucide-vue-next';
|
||||||
|
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 +135,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));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
export interface ResourcePanelSearchInput {
|
|
||||||
enabled: boolean; // 是否启用搜索输入框 (只有在资源面板展开且选择摄像机或警报器时才启用)
|
|
||||||
show: boolean; // 是否显示搜索输入框 (只有当enabled为true时才允许控制显示)
|
|
||||||
value: string; // 搜索输入框的值
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useResourcePanelStore = defineStore('vimp-resource-panel', () => {
|
export const useResourcePanelStore = defineStore('vimp-resource-panel', () => {
|
||||||
const collapsed = ref<boolean>(false);
|
const collapsed = ref<boolean>(false);
|
||||||
const searchInput = ref<ResourcePanelSearchInput>({
|
const searchPattern = ref<string>('');
|
||||||
enabled: false,
|
|
||||||
show: false,
|
|
||||||
value: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleCollapsed = () => {
|
const toggleCollapsed = () => {
|
||||||
collapsed.value = !collapsed.value;
|
collapsed.value = !collapsed.value;
|
||||||
@@ -21,7 +11,7 @@ export const useResourcePanelStore = defineStore('vimp-resource-panel', () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
collapsed,
|
collapsed,
|
||||||
searchInput,
|
searchPattern,
|
||||||
|
|
||||||
toggleCollapsed,
|
toggleCollapsed,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user