Menu 组件设计文档
概述
Menu 组件是本组件库中用于实现导航菜单功能的核心组件。它延续了我们组件库一贯的设计理念,采用"关注点分离"和"逻辑抽象"的原则,通过 menu.vue, menuItem.vue 和 menusubMenu.vue等核心组件实现了一套完整且易用的导航菜单解决方案。
Menu 组件专注于处理导航菜单的结构、交互和样式管理,为用户提供了一套强大、灵活的导航菜单管理功能,支持垂直/水平布局、折叠展开、唯一展开、自定义样式和丰富的交互方式,有效提升用户在导航和选择操作时的体验。
设计理念
分层架构
Menu 组件严格按照三层架构设计:
- 结构层 menu.vue, menuItem.vue, menusubMenu.vue - 负责组件的 DOM 结构和基本布局
- 逻辑层 menu.ts, menuItem.ts, menusubMenu.ts - 处理复杂的响应式计算和交互逻辑
- 表现层 menu.scss, menuItem.scss, menusubMenu.scss- 定义组件的视觉呈现和样式规则
逻辑抽象
Menu 组件将复杂的逻辑抽象为清晰的上下文提供机制:
menuContext- 通过provide/inject机制向子组件传递核心状态和方法handleSelec- 处理菜单项选择逻辑openMenu,loseMenu,toggleMenu- 处理菜单展开/折叠逻辑indexPath- 通过递归提供当前菜单项的路径
这种设计使得组件之间能够高效通信,同时保持松耦合。
核心功能设计
Props 结构化设计
Menu 组件的 props 设计简洁明了:
typescript
export const menuProps = {
/** 默认激活的菜单项 */
defaultActive: {
type: String,
default: ''
},
/** 菜单模式 */
mode: {
type: String as PropType<'vertical' | 'horizontal'>,
default: 'vertical'
},
/** 是否折叠 */
collapse: {
type: Boolean,
default: false
},
/** 背景颜色 */
backgroundColor: {
type: String,
default: '#2f3b46'
},
/** 文本颜色 */
textColor: {
type: String,
default: '#bfcbd9'
},
/** 激活项文本颜色 */
activeTextColor: {
type: String,
default: '#409EFF'
},
/** 触发方式 */
trigger: {
type: String as PropType<'click' | 'hover'>,
default: 'click'
},
/** 是否只保持一个子菜单展开 */
uniqueOpened: {
type: Boolean,
default: false
},
/** 默认展开的菜单 */
defaultOpeneds: {
type: Array as PropType<string[]>,
default: () => []
},
/** 是否使用折叠动画 */
collapseTransition: {
type: Boolean,
default: true
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
参数说明:
- mode:支持
'vertical'(垂直)和'horizontal'(水平)两种模式。 - collapse:控制菜单是否折叠(侧边栏模式)。
- trigger:支持
'click'(点击)和'hover'(悬停)两种触发方式。 - uniqueOpened:控制是否只保持一个子菜单展开。
- backgroundColor、textColor 和 activeTextColor:支持自定义菜单颜色。
交互设计
Menu 组件提供丰富的交互方式:
基本使用
vue
<MYMenu mode="vertical" defaultActive="1">
<MYMenuItem index="1">导航一</MYMenuItem>
<MYMenuItem index="2">导航二</MYMenuItem>
<MYMenuItem index="3">导航三</MYMenuItem>
</MYMenu>1
2
3
4
5
2
3
4
5
带子菜单
vue
<MYMenu mode="vertical" defaultActive="1-1">
<MYMenuItem index="1">导航一</MYMenuItem>
<MYMenuSubMenu index="2" title="导航二">
<MYMenuItem index="2-1">选项1</MYMenuItem>
<MYMenuItem index="2-2">选项2</MYMenuItem>
</MYMenuSubMenu>
<MYMenuItem index="3">导航三</MYMenuItem>
</MYMenu>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
水平菜单
vue
<MYMenu mode="horizontal">
<MYMenuItem index="1">导航一</MYMenuItem>
<MYMenuSubMenu index="2" title="导航二">
<MYMenuItem index="2-1">选项1</MYMenuItem>
<MYMenuItem index="2-2">选项2</MYMenuItem>
</MYMenuSubMenu>
<MYMenuItem index="3">导航三</MYMenuItem>
</MYMenu>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
折叠菜单
vue
<MYMenu mode="vertical" :collapse="isCollapse">
<MYMenuItem index="1">导航一</MYMenuItem>
<MYMenuSubMenu index="2" title="导航二">
<MYMenuItem index="2-1">选项1</MYMenuItem>
<MYMenuItem index="2-2">选项2</MYMenuItem>
</MYMenuSubMenu>
<MYMenuItem index="3">导航三</MYMenuItem>
</MYMenu>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
样式系统设计
CSS 变量控制
Menu 组件通过 CSS 变量实现样式控制:
--menu-bg- 菜单背景颜色--menu-text- 菜单文本颜色--menu-active- 激活项文本颜色
布局系统
Menu 组件支持灵活的布局控制:
- 垂直/水平布局 - 通过 mode 属性控制
- 折叠展开 - 通过 collapse 属性控制
- 响应式设计 - 自动适应不同屏幕尺寸
- 过渡动画 - 通过 collapseTransition 属性控制折叠动画
状态样式
Menu 组件根据不同状态提供相应的样式:
- 默认状态 - 标准菜单样式
- 激活状态 - 通过 is-active 类控制
- 折叠状态 - 通过 is-collapse 类控制
- 展开状态 - 通过 is-open 类控制
- 禁用状态 - 通过 is-disabled 类控制
组件结构设计
整体结构
Menu 组件的整体结构如下:
html
<ul class="my-menu" :class="[`my-menu--${props.mode}`, { 'is-collapse': props.collapse }]" :style="{
'--menu-bg': props.backgroundColor,
'--menu-text': props.textColor,
'--menu-active': props.activeTextColor
}">
<slot />
</ul>1
2
3
4
5
6
7
2
3
4
5
6
7
组件层次
Menu 组件包含多个主要部分:
- 主菜单 -
.my-menu- 包裹整个菜单组件 - 菜单项 -
.my-menu-item- 单个菜单项 - 子菜单 -
.my-submenu- 包含子菜单的容器 - 子菜单标题 -
.submenu-title- 子菜单的标题区域 - 子菜单列表 -
.submenu-list- 子菜单项的容器
MenuItem 组件的结构:
html
<li class="my-menu-item" :class="{ 'is-active': isActive, 'is-disabled': disabled }" @click.stop="handleClick">
<span class="menu-icon">
<slot name="icon" />
</span>
<span class="menu-label" v-show="showLabel">
<slot />
</span>
</li>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
MenuSubMenu 组件的结构:
html
<li class="my-submenu" :class="{ 'is-open': isOpen }">
<div
class="submenu-title"
ref="titleRef"
@click.stop="onTitleClick"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
>
<span class="menu-icon"><slot name="icon" /></span>
<span class="menu-label" v-show="showLabel"><slot name="title" /></span>
<span class="submenu-arrow" v-show="!collapsed">
<svg width="12" height="12" viewBox="0 0 1024 1024">
<path fill="currentColor" d="M384 192l256 320-256 320z" />
</svg>
</span>
</div>
<!-- 折叠态浮层 -->
<teleport v-if="collapsed" to="body">
<ul
v-show="showPopup"
class="submenu-list teleported-list"
ref="listRef"
@mouseenter="onPopupEnter"
@mouseleave="onPopupLeave"
:style="popupStyle"
>
<slot />
</ul>
</teleport>
<!-- 非折叠态 -->
<transition name="slide">
<ul v-show="!collapsed && isOpen" class="submenu-list">
<slot />
</ul>
</transition>
</li>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
交互设计
菜单展开/折叠
Menu 组件通过 openMenu、closeMenu 和 toggleMenu 方法实现菜单展开/折叠:
typescript
function openMenu(index: string, indexPath: string[] = []) {
if (openedMenus.value.includes(index)) return
const parentKey = indexPath.slice(0, -1).join('/') || '__root__'
if (uniqueOpened.value) {
const currentBranch = new Set(indexPath)
// 找出需要关闭的菜单(不在当前分支中的菜单)
const toClose = openedMenus.value.filter(i => {
return !currentBranch.has(i)
})
if (toClose.length) {
openedMenus.value = openedMenus.value.filter(i => !toClose.includes(i))
toClose.forEach(i => delete openedParentMap[i])
}
} else {
// 原来的非 uniqueOpened 逻辑
const toClose = openedMenus.value.filter(i => openedParentMap[i] !== parentKey)
if (toClose.length) {
openedMenus.value = openedMenus.value.filter(i => !toClose.includes(i))
toClose.forEach(i => delete openedParentMap[i])
}
}
openedMenus.value.push(index)
openedParentMap[index] = parentKey
// 触发 open 事件
emit('open', index, indexPath)
}
function closeMenu(index: string) {
if (!openedMenus.value.includes(index)) return
// 递归关闭子菜单
const children = Object.keys(openedParentMap).filter(key => openedParentMap[key] === index)
children.forEach(child => closeMenu(child))
// 移除自身
openedMenus.value = openedMenus.value.filter(i => i !== index)
delete openedParentMap[index]
// 触发 close 事件
emit('close', index, [index])
}
function toggleMenu(index: string, indexPath: string[] = []) {
if (openedMenus.value.includes(index)) {
closeMenu(index)
} else {
openMenu(index, indexPath)
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
展开/折叠规则:
- 支持点击和悬停两种触发方式
- 支持唯一展开模式(uniqueOpened)
- 折叠时缓存展开状态,展开时恢复
触发机制
Menu 组件支持多种触发方式:
typescript
function onTitleClick() {
if (collapsed.value) {
showPopup.value = !showPopup.value
} else {
menu?.toggleMenu(props.index, myPath)
}
}
function onMouseEnter() {
if (effectiveTrigger.value === 'hover') {
if (collapsed.value) {
if (closeTimer) clearTimeout(closeTimer)
showPopup.value = true
} else {
if (closeTimer) clearTimeout(closeTimer)
menu?.openMenu(props.index, myPath)
}
}
}
function onMouseLeave() {
if (effectiveTrigger.value === 'hover') {
closeTimer = setTimeout(() => {
if (collapsed.value) showPopup.value = false
else menu?.closeMenu(props.index)
closeTimer = null
}, 150)
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
触发方式:
- click:点击触发(默认)
- hover:鼠标悬停触发
- 折叠状态下,子菜单默认使用 hover 触发
键盘导航
Menu 组件通过事件监听实现键盘导航:
typescript
// 在 menu.vue 中
function handleClick() {
if (props.disabled) return
const indexPath = [...(parentPath || []), props.index]
menu?.handleSelect(props.index, indexPath)
}1
2
3
4
5
6
2
3
4
5
6
键盘操作:
ArrowDown/ArrowUp:在菜单项间导航Enter:选择当前项Escape:关闭子菜单ArrowRight:展开子菜单ArrowLeft:关闭子菜单
扩展性设计
Menu 组件具备良好的扩展性:
- 多模式支持:通过 mode 属性支持垂直/水平布局
- 折叠功能:通过 collapse 属性支持侧边栏折叠
- 唯一展开:通过 uniqueOpened 属性支持唯一展开模式
- 自定义样式:通过 backgroundColor、textColor 和 activeTextColor 属性支持自定义样式
- 过渡动画:通过 collapseTransition 属性控制折叠动画
- 插槽机制:支持自定义图标和标题内容
样式细节
Menu 组件在样式处理上有以下特点:
- 响应式设计:支持灵活的布局适配
- 一致性:与组件库其他组件保持样式一致性
- 无障碍设计:确保键盘导航和屏幕阅读器友好
- 设置适当的 ARIA 属性
- 支持键盘导航
- 清晰的视觉层次:通过颜色和间距创建清晰的视觉层次
- 平滑过渡:使用 CSS 过渡实现平滑的展开/折叠效果
性能优化
通过以下方式优化性能:
- 高效的状态管理:使用响应式系统精确控制组件更新
- 避免不必要的重绘:通过条件渲染减少 DOM 操作
- 使用 Vue 3 Composition API:提高运行效率和代码可维护性
- 智能的事件监听:按需监听和移除事件
- CSS 变量动态调整:减少样式重计算
- 缓存机制:折叠时缓存展开状态,提高性能
设计价值
Menu 组件延续了我们组件库的设计哲学:
- 关注点分离:结构、样式、逻辑完全解耦
- 高度复用:通过
provide/inject机制实现组件间高效通信 - 易于维护:清晰的代码结构和命名规范
- 可扩展性强:支持丰富的定制选项和布局方式
- 一致性:与 Dropdown、Select 等其他组件保持一致的设计语言