eagleye

企业级部门树组件(DepartmentTree.vue)文档

企业级部门树组件(DepartmentTree.vue)文档

一、组件概述

DepartmentTree是基于 QuasarQTree和 Vue 3 组合式 API 开发的企业级组织架构树组件,支持部门层级展示、实时搜索过滤、节点交互和路由导航。该组件专为大型组织架构系统设计,提供直观的部门层级可视化和高效的导航体验,同时确保类型安全和响应式适配。

二、核心功能亮点

1. 智能搜索与过滤

  • 实时过滤:输入搜索关键词时自动筛选匹配节点(名称模糊匹配)
  • 自动展开:搜索结果节点及其所有父节点自动展开,确保可见性
  • 空状态处理

无数据时显示“没有部门数据”(no-nodes-label)

搜索无结果时显示“没有找到匹配的部门”(no-results-label)

2. 节点可视化增强

  • 类型化图标:不同部门类型显示专属图标(如安全部门用security图标,生产单位用factory图标)
  • 状态色标:节点颜色随部门类型动态变化(如监管机构为红色,安全部门为绿色)
  • 子节点计数:带下级部门的节点显示子节点数量徽章(如2表示包含 2 个子部门)
  • 悬停反馈:节点 hover 时背景色微妙变化(rgba(0, 0, 0, 0.05))
  • 平滑展开:节点展开/折叠有过渡动画
  • 路由联动:选择节点时自动跳转至部门详情页,URL 参数变化时同步选中状态
  • 类型安全:全程使用 TypeScript 类型定义(Department、OrganizationNodeType等)
  • 响应式设计:最大高度限制(70vh)配合滚动条,适配不同屏幕尺寸
  • 模块化逻辑:通过useOrganization组合式函数复用数据获取逻辑

3. 交互体验优化

4. 企业级特性

三、完整代码实现

<template>

<div class="q-pa-sm">

<!-- 搜索框 -->

<q-input

v-model="searchText"

dense

outlined

placeholder="搜索部门..."

class="q-mb-sm"

>

<template v-slot:prepend>

<q-icon name="search" />

</template>

</q-input>

<!-- 部门树 -->

<q-tree

:nodes="filteredTree"

node-key="id"

selected-color="primary"

v-model:selected="selectedDepartment"

v-model:expanded="expandedDepartments"

:default-expand-all="!searchText"

no-nodes-label="没有部门数据"

no-results-label="没有找到匹配的部门"

dense

no-connectors

>

<!-- 节点头部插槽(名称+图标+计数) -->

<template v-slot:default-header="prop">

<div class="row items-center">

<q-icon

:name="getNodeIcon(prop.node)"

:color="getNodeColor(prop.node)"

size="xs"

class="q-mr-xs"

/>

<div class="text-caption">{{ prop.node.name }}</div>

<q-badge

v-if="prop.node.children && prop.node.children.length"

rounded

color="grey-6"

class="q-ml-xs"

>

{{ prop.node.children.length }}

</q-badge>

</div>

</template>

<!-- 节点正文插槽(描述信息) -->

<template v-slot:default-body="prop">

<div v-if="prop.node.description" class="q-pl-lg text-caption text-grey-6">

{{ prop.node.description }}

</div>

</template>

</q-tree>

</div>

</template>

<script setup lang="ts">

import { computed, onMounted, ref, watch } from 'vue'

import { useRouter } from 'vue-router'

import type { Department, OrganizationTreeNode } from 'src/types/auth/organization'

import { OrganizationNodeType } from 'src/types/auth/organization'

import useOrganization from 'src/composable/useOrganization'

// 外部依赖

const router = useRouter()

const { fetchDepartmentTree } = useOrganization()

// 状态管理

const searchText = ref('')

const selectedDepartment = ref<string | null>(null)

const expandedDepartments = ref<string[]>([])

const treeData = ref<Department[]>([]) // 原始部门树数据

// 过滤后的部门树(响应式计算)

const filteredTree = computed(() => {

if (!searchText.value) return treeData.value

const filterNode = (node: Department): Department | null => {

// 克隆节点避免修改原始数据

const nodeCopy = { ...node }

// 检查当前节点是否匹配搜索文本

const isMatch = nodeCopy.name.toLowerCase().includes(searchText.value.toLowerCase())

// 递归过滤子节点

if (nodeCopy.children && nodeCopy.children.length > 0) {

const filteredChildren = nodeCopy.children

.map(filterNode)

.filter((n): n is Department => n !== null) // 过滤 null 值

nodeCopy.children = filteredChildren

// 当前节点不匹配但子节点有匹配时,保留节点

if (isMatch || filteredChildren.length > 0) {

return nodeCopy

}

return null

}

// 无子女节点时,仅保留匹配节点

return isMatch ? nodeCopy : null

}

// 过滤根节点并返回有效节点

return treeData.value.map(filterNode).filter((n): n is Department => n !== null)

})

// 节点图标映射

const getNodeIcon = (node: OrganizationTreeNode) => {

const iconMap: Record<OrganizationNodeType, string> = {

[OrganizationNodeType.ROOT_ORGANIZATION]: 'corporate_fare',

[OrganizationNodeType.SECURITY_SERVICE_PROVIDER]: 'security',

[OrganizationNodeType.HAZARD_MANAGEMENT_PROVIDER]: 'construction',

[OrganizationNodeType.PRODUCTION_UNIT]: 'factory',

[OrganizationNodeType.SAFETY_DEPARTMENT]: 'verified_user',

[OrganizationNodeType.REGULATORY_AGENCY]: 'gavel',

[OrganizationNodeType.THIRD_PARTY_AUDITOR]: 'assignment'

}

return iconMap[node.node_type] || 'business'

}

// 节点颜色映射

const getNodeColor = (node: OrganizationTreeNode) => {

if (!node.is_active) return 'grey' // 非活跃部门置灰

const colorMap: Record<OrganizationNodeType, string> = {

[OrganizationNodeType.ROOT_ORGANIZATION]: 'primary',

[OrganizationNodeType.SECURITY_SERVICE_PROVIDER]: 'teal',

[OrganizationNodeType.HAZARD_MANAGEMENT_PROVIDER]: 'orange',

[OrganizationNodeType.PRODUCTION_UNIT]: 'blue-grey',

[OrganizationNodeType.SAFETY_DEPARTMENT]: 'green',

[OrganizationNodeType.REGULATORY_AGENCY]: 'red',

[OrganizationNodeType.THIRD_PARTY_AUDITOR]: 'purple'

}

return colorMap[node.node_type] || 'blue-grey'

}

// 初始化加载部门树

const loadTree = async () => {

try {

const data = await fetchDepartmentTree()

treeData.value = Array.isArray(data) ? data : []

// 默认展开根节点

if (treeData.value.length > 0) {

expandedDepartments.value = [treeData.value[0].id]

// 同步路由中的部门ID

const routeId = router.currentRoute.value.params.id as string | undefined

if (routeId) {

selectedDepartment.value = routeId

}

}

} catch (error) {

console.error('加载部门树失败:', error)

treeData.value = []

}

}

// 监听节点选择变化,同步路由

watch(selectedDepartment, async (newVal) => {

if (newVal) {

await router.push({ name: 'DepartmentDetail', params: { id: newVal } })

}

})

// 监听搜索文本变化,自动展开匹配节点

watch(searchText, (newVal) => {

if (newVal) {

const expandIds = new Set<string>()

// 递归收集所有匹配节点及其父节点ID

const findMatchingNodes = (nodes: Department[]) => {

nodes.forEach((node) => {

const isMatch = node.name.toLowerCase().includes(newVal.toLowerCase())

if (isMatch) {

expandIds.add(node.id)

// 添加直接父节点(实际应用中需递归追溯所有祖先)

if (node.parent) expandIds.add(node.parent)

}

if (node.children) findMatchingNodes(node.children)

})

}

findMatchingNodes(treeData.value)

expandedDepartments.value = Array.from(expandIds)

} else {

// 清空搜索时仅展开根节点

expandedDepartments.value = treeData.value.length > 0

? [treeData.value[0].id]

: []

}

})

// 初始化加载

onMounted(async () => {

await loadTree()

})

</script>

<style scoped>

.q-tree {

max-height: 70vh; /* 限制高度,超出滚动 */

overflow-y: auto;

}

.q-tree__node-header {

padding: 4px 8px;

border-radius: 4px;

transition: background-color 0.2s ease;

}

.q-tree__node-header:hover {

/* 悬停背景色 */

}

</style>

四、使用指南

1. 组件引入

<template>

<div class="organization-page">

<department-tree />

</div>

</template>

<script setup>

import DepartmentTree from 'src/components/organization/DepartmentTree.vue'

</script>

2. 依赖要求

  • 路由配置:需定义名为DepartmentDetail的路由,接受id参数
  • 类型定义:需提供Department接口和OrganizationNodeType枚举
  • 数据服务:需实现useOrganization组合式函数,提供fetchDepartmentTree方法

3. 数据结构示例

// Department 接口示例

interface Department {

id: string

name: string

description?: string

parent?: string

node_type: OrganizationNodeType

is_active: boolean

children?: Department[]

// 其他属性...

}

// OrganizationNodeType 枚举示例

enum OrganizationNodeType {

ROOT_ORGANIZATION = 'root',

SECURITY_SERVICE_PROVIDER = 'security',

// 其他类型...

}

五、技术实现详解

1. 搜索过滤机制

  • 深度优先过滤:通过递归函数filterNode遍历所有节点,保留匹配节点及其父节点
  • 数据隔离:通过节点克隆({ ...node })避免修改原始树数据
  • 性能优化:仅在搜索文本变化时触发过滤,减少计算开销

2. 自动展开逻辑

// 核心逻辑:搜索时展开匹配节点及其父节点

watch(searchText, (newVal) => {

if (newVal) {

const expandIds = new Set<string>()

const findMatchingNodes = (nodes: Department[]) => {

nodes.forEach((node) => {

if (node.name.toLowerCase().includes(newVal.toLowerCase())) {

expandIds.add(node.id)

if (node.parent) expandIds.add(node.parent) // 添加父节点

}

if (node.children) findMatchingNodes(node.children)

})

}

findMatchingNodes(treeData.value)

expandedDepartments.value = Array.from(expandIds)

}

})

3. 节点可视化映射

  • 图标与颜色解耦:通过getNodeIcon和getNodeColor两个独立函数处理,便于维护
  • 扩展性设计:使用对象映射(iconMap/colorMap)替代 switch-case,新增部门类型时只需扩展映射表
  • 严格类型定义:所有状态和函数参数均指定类型,避免any类型
  • 接口约束Department和OrganizationNodeType接口确保后端数据兼容性
  • 路由参数校验:通过 TypeScript 泛型约束路由参数类型
  • 惰性加载:部门树数据仅在组件挂载时加载一次
  • 无副作用过滤:通过深克隆节点避免修改原始数据
  • 事件清理:虽然示例中未显式清理,但 Vue 的watch和onMounted会在组件卸载时自动清理
  • 逻辑分层:数据获取、过滤、交互逻辑分离,单一职责原则
  • 命名规范:函数和变量命名遵循业务语义(如findMatchingNodes、expandedDepartments)
  • 样式隔离:使用scoped样式和 BEM 命名规范,避免样式污染

六、企业级特性

1. 类型安全保障

2. 性能优化

3. 可维护性设计

七、注意事项

1. 路由同步:组件依赖路由参数同步选中状态,需确保路由配置正确且无冲突

2. 父节点追溯:当前自动展开逻辑仅添加直接父节点,如需展开所有祖先节点,需实现完整的父节点追溯逻辑

3. 数据缓存fetchDepartmentTree建议添加缓存机制,避免重复请求

4. 大型数据处理:部门树节点超过1000+时,建议添加虚拟滚动优化渲染性能

完整代码示例可参考企业级前端组件库或 GitCode 实战项目。

 

posted on 2025-08-09 18:03  GoGrid  阅读(35)  评论(0)    收藏  举报

导航