企业级组织架构面包屑导航组件(OrganizationBreadcrumb.vue)文档
企业级组织架构面包屑导航组件(OrganizationBreadcrumb.vue)文档
一、组件概述
OrganizationBreadcrumb是基于 Quasar 框架和 Vue 3 组合式 API 开发的企业级导航组件,专为大型组织架构系统设计。该组件通过可视化路径展示当前部门在企业层级中的位置,支持动态路径构建、智能折叠、响应式布局和路由导航,满足复杂组织架构的导航需求。
二、核心功能亮点
1. 智能响应式设计
- 屏幕适配:自动根据设备尺寸调整显示策略
o 移动端(<600px):仅显示根部门和当前部门
o 桌面端(≥600px):显示更多层级,超出部分折叠
- 动态折叠:当部门层级超过 3 级时,中间层级自动折叠为下拉菜单
- 实时尺寸监听:窗口大小变化时自动重新计算布局
- 部门类型图标:不同部门类型显示专属图标(如企业总部、安全管理部门等)
- 路径完整性:自动构建从根部门到当前部门的完整路径
- 折叠徽标:折叠菜单旁显示数字徽标,提示隐藏的部门数量
- 悬停效果:面包屑项悬停时轻微上浮(transform: translateY(-2px))
- 加载状态:数据加载中显示沙漏图标和"加载中..."文本
- 平滑过渡:路由切换时路径更新有过渡动画
- 路由集成:点击面包屑项自动导航至对应部门详情页
- 类型安全:全程使用 TypeScript 类型定义(如Department接口、OrganizationNodeType枚举)
- 模块化设计:通过组合式 API 分离数据获取、路径构建和 UI 逻辑
2. 层级可视化
3. 交互体验优化
4. 企业级功能
三、完整代码实现
<template>
<q-breadcrumbs class="organization-breadcrumb" active-color="white">
<!-- 自定义分隔符 -->
<template v-slot:separator>
<q-icon name="chevron_right" size="xs" />
</template>
<!-- 根部门 -->
<q-breadcrumbs-el
v-if="rootDepartment"
:label="rootDepartment.name"
icon="corporate_fare"
:to="getDepartmentRoute(rootDepartment)"
/>
<!-- 折叠的中间部门(响应式) -->
<q-breadcrumbs-el v-if="collapsedItems.length > 0">
<q-menu auto-close anchor="bottom middle" self="top middle">
<q-list dense class="breadcrumb-menu">
<q-item
v-for="dept in collapsedItems"
:key="dept.id"
clickable
:to="getDepartmentRoute(dept)"
>
<q-item-section avatar>
<q-icon :name="getDepartmentIcon(dept.node_type)" size="sm" />
</q-item-section>
<q-item-section>
<div class="text-weight-medium">{{ dept.name }}</div>
<div class="text-caption text-grey-6">{{ dept.node_type_display }}</div>
</q-item-section>
</q-item>
</q-list>
</q-menu>
<q-btn flat round dense icon="more_horiz" size="sm" class="collapsed-btn">
<q-badge v-if="collapsedItems.length" color="primary" floating transparent>
{{ collapsedItems.length }}
</q-badge>
</q-btn>
</q-breadcrumbs-el>
<!-- 当前部门(非点击态) -->
<q-breadcrumbs-el
v-if="currentDepartment"
:label="currentDepartment.name"
:icon="getDepartmentIcon(currentDepartment.node_type)"
/>
<!-- 加载状态占位 -->
<q-breadcrumbs-el v-if="loading" label="加载中..." icon="hourglass_empty" />
</q-breadcrumbs>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useQuasar } from 'quasar'
import type { Department } from 'src/types/auth/organization'
import { OrganizationNodeType } from 'src/types/auth/organization'
import { useOrganization } from 'src/composable/useOrganization'
// 外部依赖
const $q = useQuasar()
const route = useRoute()
const router = useRouter()
const { fetchDepartment, loading } = useOrganization()
// 状态管理
const rootDepartment = ref<Department | null>(null)
const currentDepartment = ref<Department | null>(null)
const departmentPath = ref<Department[]>([]) // 完整路径数组
const screenWidth = ref(window.innerWidth)
// 计算属性:折叠的中间部门
const collapsedItems = computed(() => {
if (departmentPath.value.length <= 3) return []
// 移动端折叠更多项
const collapseThreshold = $q.screen.lt.md ? 1 : 2
return departmentPath.value.slice(collapseThreshold, -1)
})
// 监听路由参数变化(部门ID变更)
watch(
() => route.params.id,
async (newId) => {
if (newId) {
await loadDepartment(newId as string)
}
},
{ immediate: true } // 初始加载时执行
)
// 监听窗口尺寸变化
onMounted(() => {
const handleResize = () => {
screenWidth.value = window.innerWidth
}
window.addEventListener('resize', handleResize)
// 清理监听
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
})
// 加载部门数据并构建路径
const loadDepartment = async (departmentId: string) => {
const dept = await fetchDepartment(departmentId)
if (dept) {
currentDepartment.value = dept
await buildDepartmentPath(dept)
}
}
// 递归构建部门路径(从当前部门到根部门)
const buildDepartmentPath = async (dept: Department) => {
const path: Department[] = [dept]
let current = dept
// 向上追溯所有父部门
while (current.parent) {
try {
const parent = await fetchDepartment(current.parent)
if (parent) {
path.unshift(parent) // 父部门插入到路径头部
current = parent
} else {
break // 父部门不存在时终止
}
} catch (err) {
console.error('获取父部门失败:', err)
break
}
}
departmentPath.value = path
rootDepartment.value = path[0] || null // 根部门为路径第一项
}
// 生成部门详情页路由
const getDepartmentRoute = (dept: Department) => {
return {
name: 'DepartmentDetail',
params: { id: dept.id }
}
}
// 根据部门类型获取图标
const getDepartmentIcon = (nodeType: OrganizationNodeType): string => {
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[nodeType] || 'business'
}
</script>
<style lang="scss" scoped>
.organization-breadcrumb {
padding: 0 12px;
max-width: 600px;
overflow: hidden;
// 面包屑项样式
:deep(.q-breadcrumbs__el) {
max-width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
}
}
// 折叠按钮样式
.collapsed-btn {
position: relative;
background: rgba(255, 255, 255, 0.15);
border-radius: 50%;
width: 28px;
height: 28px;
&:hover {
background: rgba(255, 255, 255, 0.25);
}
.q-badge {
top: -4px;
right: -4px;
font-size: 8px;
min-width: 16px;
min-height: 16px;
border: 1px solid $primary;
}
}
}
// 折叠菜单样式
.breadcrumb-menu {
min-width: 250px;
max-height: 300px;
overflow-y: auto;
.q-item {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
&:last-child {
border-bottom: none;
}
&:hover {
// 浅蓝背景突出
}
}
}
// 移动端适配
@media (max-width: 600px) {
.organization-breadcrumb {
max-width: 300px;
:deep(.q-breadcrumbs__el) {
max-width: 100px;
}
}
}
</style>
四、使用指南
1. 安装与引入
<!-- 在布局组件中引入 -->
<template>
<q-header>
<q-toolbar>
<!-- 其他工具栏内容 -->
<organization-breadcrumb />
</q-toolbar>
</q-header>
</template>
<script setup>
import OrganizationBreadcrumb from 'src/components/organization/OrganizationBreadcrumb.vue'
</script>
2. 依赖要求
- 路由配置:需定义名为DepartmentDetail的路由,接受id参数
- 类型定义:需提供Department接口和OrganizationNodeType枚举
- 数据服务:需实现useOrganization组合式函数,提供fetchDepartment方法
3. 数据结构示例
// Department 接口示例
interface Department {
id: string
name: string
parent?: string // 父部门ID(根部门无此属性)
node_type: OrganizationNodeType
node_type_display: string // 部门类型显示名
// 其他属性...
}
// OrganizationNodeType 枚举示例
enum OrganizationNodeType {
ROOT_ORGANIZATION = 10,
SECURITY_SERVICE_PROVIDER = 21,
HAZARD_MANAGEMENT_PROVIDER = 22,
// 其他类型...
}
五、技术实现详解
1. 路径构建机制
- 递归追溯:从当前部门出发,通过parent字段递归获取所有父部门,构建完整路径
- 路径缓存:部门数据存储在departmentPath数组中,避免重复请求
- 异常处理:父部门获取失败时终止递归,确保组件稳定性
2. 响应式折叠逻辑
// 核心逻辑:根据屏幕尺寸和路径长度决定折叠范围
const collapsedItems = computed(() => {
if (departmentPath.value.length <= 3) return []
const collapseThreshold = $q.screen.lt.md ? 1 : 2 // 移动端折叠阈值更低
return departmentPath.value.slice(collapseThreshold, -1)
})
3. 路由集成设计
- 双向绑定:路由参数变化时自动加载对应部门路径,部门点击时自动更新路由
- 类型安全:通过泛型约束确保路由参数和部门ID类型一致
- 导航反馈:加载状态通过loading属性从useOrganization注入,自动显示/隐藏
- 全程使用 TypeScript 类型定义,避免运行时类型错误
- 部门类型通过枚举OrganizationNodeType严格约束,防止非法值
- 路由参数和 API 响应均有接口定义,确保数据结构一致性
- 事件监听清理:onUnmounted中移除窗口 resize 监听,避免内存泄漏
- 请求限制:递归获取父部门时设置异常捕获,防止无限请求
- DOM 优化:通过v-if条件渲染减少不必要的 DOM 节点
- 逻辑分离:数据获取(useOrganization)、路由处理、UI 逻辑分层管理
- 命名规范:变量/方法命名遵循业务语义(如buildDepartmentPath、getDepartmentIcon)
- 样式隔离:使用scoped样式和深度选择器:deep(),避免样式污染
六、企业级特性
1. 类型安全保障
2. 性能优化
3. 可维护性设计
七、注意事项
1. 路由配置:确保路由中存在name: 'DepartmentDetail'的路由定义,否则导航功能失效
2. 数据接口:fetchDepartment方法需正确处理 404 错误(部门不存在场景)
3. 样式覆盖:如需自定义样式,可通过深度选择器:deep()修改内部元素样式
4. 性能监控:对于超大型组织(层级 >10),建议添加路径缓存机制减少 API 调用
完整代码示例可参考企业级前端组件库GitCode 仓库。