FxMenu 侧边栏菜单
侧边栏导航菜单,由 FxMenu(容器)、FxMenuItem(叶子项)、FxSubMenu(可折叠子菜单)三个组件组合使用。不依赖任何 UI 框架,内部复用 FxScrollbar(滚动 + 激活项自动滚入视)与 FxTooltip(折叠态叶子项提示),样式走 @fx/styles 的 --menu-* token,亮暗自动跟随。支持 rounded(圆润,默认)与 plain(朴素)两种风格模式。
基础用法
用 FxMenu 包裹若干 FxMenuItem / FxSubMenu。点击叶子项切换激活态,激活项会自动滚入可视区。
vue
<template>
<div class="menu-demo">
<FxMenu>
<FxMenuItem
title="首页"
icon="ep:home"
:active="active === 'home'"
@click="active = 'home'"
/>
<FxSubMenu title="系统管理" icon="ep:setting">
<FxMenuItem
title="用户管理"
icon="ep:user"
:active="active === 'user'"
@click="active = 'user'"
/>
<FxMenuItem
title="角色管理"
icon="ep:user-filled"
:active="active === 'role'"
@click="active = 'role'"
/>
<FxSubMenu title="权限" icon="ep:lock">
<FxMenuItem
title="菜单权限"
:active="active === 'menu-perm'"
@click="active = 'menu-perm'"
/>
<FxMenuItem
title="接口权限"
:active="active === 'api-perm'"
@click="active = 'api-perm'"
/>
</FxSubMenu>
</FxSubMenu>
<FxMenuItem
title="操作日志"
icon="ep:document"
:active="active === 'log'"
@click="active = 'log'"
/>
</FxMenu>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
const active = ref("home")
</script>
<style scoped>
.menu-demo {
width: 220px;
height: 360px;
padding: 8px 0;
display: flex;
flex-direction: column;
background: var(--fx-color-sidebar);
border: 1px solid var(--fx-border-color);
border-radius: var(--fx-border-radius-lg);
}
</style>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
54
55
56
57
58
59
60
61
62
63
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
54
55
56
57
58
59
60
61
62
63
折叠态(collapsed)
collapsed 为 true 时,侧边栏收起为图标列:叶子项文本隐藏,悬浮显示 tooltip;子菜单改为右侧弹层(支持嵌套悬浮与级联关闭)。has-active-descendant 用于告知子菜单"子树含激活项",从而高亮父级。
vue
<template>
<div class="demo">
<div class="menu-demo" :class="{ collapsed }">
<FxMenu :collapsed="collapsed">
<FxMenuItem
title="首页"
icon="ep:home"
:active="active === 'home'"
@click="active = 'home'"
/>
<FxSubMenu
title="系统管理"
icon="ep:setting"
:has-active-descendant="isSysActive"
>
<FxMenuItem
title="用户管理"
icon="ep:user"
:active="active === 'user'"
@click="active = 'user'"
/>
<FxMenuItem
title="角色管理"
icon="ep:user-filled"
:active="active === 'role'"
@click="active = 'role'"
/>
</FxSubMenu>
<FxMenuItem
title="操作日志"
icon="ep:document"
:active="active === 'log'"
@click="active = 'log'"
/>
</FxMenu>
</div>
<FxBtn @click="collapsed = !collapsed">
{{ collapsed ? "展开侧边栏" : "折叠侧边栏" }}
</FxBtn>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from "vue"
const collapsed = ref(false)
const active = ref("user")
const isSysActive = computed(() =>
["user", "role"].includes(active.value),
)
</script>
<style scoped>
.demo {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.menu-demo {
height: 320px;
padding: 8px 0;
display: flex;
flex-direction: column;
background: var(--fx-color-sidebar);
border: 1px solid var(--fx-border-color);
border-radius: var(--fx-border-radius-lg);
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
width: 220px;
}
.menu-demo.collapsed {
width: 64px;
}
</style>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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
手风琴模式(accordion)
默认开启:同级只展开一个子菜单,展开新的一项会自动收起兄弟项。设为 false 可同时展开多个。
vue
<template>
<div class="demo">
<div class="menu-demo">
<FxMenu :accordion="accordion">
<FxSubMenu title="分组 A" icon="ep:folder-opened">
<FxMenuItem title="A-1" />
<FxMenuItem title="A-2" />
<FxMenuItem title="A-3" />
</FxSubMenu>
<FxSubMenu title="分组 B" icon="ep:folder-opened">
<FxMenuItem title="B-1" />
<FxMenuItem title="B-2" />
</FxSubMenu>
<FxSubMenu title="分组 C" icon="ep:folder-opened">
<FxMenuItem title="C-1" />
</FxSubMenu>
</FxMenu>
</div>
<FxBtn @click="accordion = !accordion">
手风琴模式:{{ accordion ? "开启" : "关闭" }}
</FxBtn>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
const accordion = ref(true)
</script>
<style scoped>
.demo {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.menu-demo {
width: 220px;
height: 320px;
padding: 8px 0;
display: flex;
flex-direction: column;
background: var(--fx-color-sidebar);
border: 1px solid var(--fx-border-color);
border-radius: var(--fx-border-radius-lg);
}
</style>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
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
风格模式(style-type)
style-type 切换两种视觉风格(参考 vben):
rounded(圆润,默认):菜单项圆角 + 左右留白。plain(朴素):菜单项直角 + 顶满侧边栏(无外边距),更扁平紧凑。
两种模式的激活态一致——半透明主色底 + 主色字(无阴影),差异仅在圆角与外边距。切换纯靠 CSS 变量,不改结构。
vue
<template>
<div class="demo">
<div class="menu-demo">
<FxMenu :style-type="styleType">
<FxMenuItem
title="首页"
icon="ep:home"
:active="active === 'home'"
@click="active = 'home'"
/>
<FxSubMenu title="系统管理" icon="ep:setting">
<FxMenuItem
title="用户管理"
icon="ep:user"
:active="active === 'user'"
@click="active = 'user'"
/>
<FxMenuItem
title="角色管理"
icon="ep:user-filled"
:active="active === 'role'"
@click="active = 'role'"
/>
</FxSubMenu>
<FxMenuItem
title="操作日志"
icon="ep:document"
:active="active === 'log'"
@click="active = 'log'"
/>
</FxMenu>
</div>
<FxBtn @click="toggle">
当前:{{ styleType === "rounded" ? "圆润" : "朴素" }}(点击切换)
</FxBtn>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import type { MenuStyleType } from "@fx/components"
const styleType = ref<MenuStyleType>("rounded")
const active = ref("user")
function toggle() {
styleType.value = styleType.value === "rounded" ? "plain" : "rounded"
}
</script>
<style scoped>
.demo {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.menu-demo {
width: 220px;
height: 320px;
padding: 8px 0;
display: flex;
flex-direction: column;
background: var(--fx-color-sidebar);
border: 1px solid var(--fx-border-color);
border-radius: var(--fx-border-radius-lg);
}
</style>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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
业务层适配
FxMenu 只负责 UI,不关心数据来源。业务侧通常再包一层"数据适配组件",把后端菜单或路由表递归映射成 FxMenuItem / FxSubMenu,激活判断与路由跳转留在业务层:
vue
<FxMenu :collapsed="collapsed" :accordion="accordion">
<AdapterItem
v-for="node in menus"
:key="node.id"
:menu="node"
/>
</FxMenu>1
2
3
4
5
6
7
2
3
4
5
6
7
ts
// AdapterItem.vue(业务侧,递归)
<FxSubMenu
v-if="node.children?.length"
:title="node.name"
:icon="node.icon"
:has-active-descendant="isDescendantActive(node)"
>
<AdapterItem
v-for="child in node.children"
:key="child.id"
:menu="child"
:level="level + 1"
/>
</FxSubMenu>
<FxMenuItem
v-else
:title="node.name"
:icon="node.icon"
:active="isActive(node)"
:level="level"
@click="router.push(node.path)"
/>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
FxMenu API
Attributes
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| collapsed | 是否折叠(侧边栏收起为图标列) | boolean | false |
| accordion | 手风琴模式:同级只展开一个子菜单 | boolean | true |
| style-type | 风格模式:rounded 圆润 / plain 朴素 | 'rounded' | 'plain' | rounded |
Slots
| 插槽名 | 说明 |
|---|---|
| default | 菜单内容,通常是 FxMenuItem / FxSubMenu |
Exposes
| 属性 | 说明 |
|---|---|
| scrollActiveIntoView | 手动滚动使激活项进入可视区 |
| scrollbarRef | 内部 FxScrollbar 实例(透传,可调 scrollTo 等) |
FxMenuItem API
Attributes
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| title | 标题(折叠态作为 tooltip 文案) | string | '' |
| icon | 图标。字符串为 IconString 类型(即 FxIcon 图标名,如 ep:setting、ant-design:home、svg:xxx) | string | — |
| active | 是否激活 | boolean | false |
| level | 层级(控制缩进,业务层递归 +1) | number | 0 |
Slots
| 插槽名 | 说明 |
|---|---|
| default | 自定义标题内容(覆盖 title) |
Events
| 事件名 | 说明 |
|---|---|
| click | 点击菜单项 |
FxSubMenu API
Attributes
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| title | 子菜单标题 | string | '' |
| icon | FxIcon 图标名 | string | — |
| level | 层级(控制缩进) | number | 0 |
| has-active-descendant | 子树是否含激活项(业务层据路由判断后传入,驱动自动展开) | boolean | false |
Slots
| 插槽名 | 说明 |
|---|---|
| default | 子菜单内容(菜单项 / 嵌套子菜单) |
设计要点
- 状态解耦:折叠态 / 手风琴由
FxMenu的 props 驱动,经provide/inject下发,组件不耦合消费方的状态来源。 - 激活自动滚动:
FxMenu内部用MutationObserver监听.is-active变化,自动把激活项(含上下相邻项)滚入可视区。 - 折叠态 tooltip 仅在顶层 rail(
FxMenu直接子级叶子项)显示;子菜单内的项文本始终可见,不重复弹 tooltip。 - 配色 / 圆角 / 阴影 / 间距全部走
@fx/styles的--menu-*/--color-*token,暗色模式随 token 自动适配。 :::
