AppShell
Overview
SAppShell 是中后台应用的外壳聚合组件。一个 mode、一份菜单树,加上少量属性,就能组装出完整外壳:布局区域(侧栏、顶栏、页签、内容、页脚),以及填充这些区域的一切——导航、面包屑、页签、品牌区与页脚。
当应用需要在路由页面外用一层常驻外壳时使用它:侧栏或顶栏导航、顶栏面包屑、页签条、页脚,都不必自己连线。若组合方式不匹配,直接用底层组件(SLayout、SSplitNav、STreeMenu、STreeNav、SPageTabs、SBreadcrumb)——「布局手写对照」示例展示了同一外壳的手写写法。
SAppShell 是 layout 族的聚合层,只负责底层组件无法负责的一件事:模式骨架——用哪种布局方向、哪个菜单渲染器、每个菜单面板挂到哪个区域、以及这些面板需要的侧栏宽度。其余全部留在各自归属的族里:SLayout 渲染区域,SSplitNav / STreeMenu / STreeNav 渲染菜单,SBreadcrumb 与 SPageTabs 渲染各自的栏。
与 SLayout 一样,外壳与路由无关:菜单树、面包屑项、页签项都以数据传入,交互通过事件回传。路由、权限、页签集合与缓存策略都由宿主掌控,示例 给出了连线写法。
Usage
Features
- 🧭 六种模式 ——
sidebar(侧栏嵌套菜单)、top(顶栏弹出菜单),以及SSplitNav的四个模式dual-vertical/vertical-horizontal/horizontal-vertical/horizontal-dual-vertical,字面量原样转发。 - 📐 侧栏跟随菜单变化 —— 含嵌套面板的模式只按"实际渲染出来的面板"计算侧栏宽度:一级菜单有子菜单时为
轨道 + 树,否则为"轨道 + 折叠后的面板"或只有轨道。 - 🧩 按模式选择渲染器 —— 同一份
items树可喂给STreeMenu、STreeNav或SSplitNav;menuProps/menuUi只转发给当前生效的渲染器。 - 🌲 菜单跟随路由展开 ——
expandStrategy(默认selected,可选keep)作用于嵌套的STreeMenu与分栏面板:当前路由所在的那条链路保持展开,本次会话里访问过的其它分支会收起;top模式渲染的是STreeNav,没有展开策略。 - 🪆 挂载点传送 —— 属于其他区域的面板(顶栏一级 + 侧栏树)会被挂载到外壳自有的挂载目标;id 由外壳生成,并通过
menu插槽暴露。 - 🍞 面包屑来自菜单数据 —— 顶栏面包屑由
items与当前激活值推导:从根到激活菜单的路径,其中每个"自身有子菜单"的上级都会展开其子菜单的下拉,并与菜单本身一样保留层级——下拉项自己也有子菜单时继续展开二级菜单;传入breadcrumbs则改为渲染你自己的数据。 - 📑 页签与页脚 ——
tabs渲染在页签区,页脚为插槽;两者都支持各自的 props 与ui覆盖。 - 🗂️ 折叠与面板同步 —— 侧栏折叠时嵌套面板一并收折:一级轨道保留,面板折成自己的图标栏,侧栏只占它真正显示的那几列,而不是缩窄的轨道旁边继续挂着一棵完整的树;折叠按钮默认在顶栏,一级在顶栏的模式则放在侧栏右下角。
- 🏷️ 品牌区为插槽 —— 外壳不渲染任何自带 logo:标记图通过
#logo注入,应用名通过#title注入。外壳负责让它们与菜单对齐:单列侧栏下整块区域就是一行菜单(标记图与标题同在一行,随菜单一起收进图标宽度);双竖栏侧栏下则把两者交给菜单自己的顶部格子——标记图落在一级轨道上、标题覆盖其下的面板。logoPlacement则决定整块区域放在侧栏顶部、侧栏底部还是顶栏。 - 📱 移动端抽屉 ——
isMobile不传时跟随视口,侧栏自动换成布局自带的对话框抽屉;此时外壳无论在哪个模式下都按sidebar骨架渲染,因此抽屉里始终是全量菜单树——品牌区加一棵嵌套STreeMenu——顶栏条与轨道都无处安放。抽屉始终展示展开态的导航:桌面端选择的折叠不会跟着内容进入抽屉。 - 🎛️ 三层覆盖 ——
ui管外壳自身节点,layoutUi管它代为着色的布局区域,menuUi管菜单渲染器;每个区域同时提供插槽。 - ♿ 无障碍来自组合链 —— 键盘、焦点、ARIA、RTL 均由被组合的组件族承担,外壳只加结构与类名。
Demos
基础
折叠与移动端
页签与面包屑
自定义插槽
布局手写对照
API
Aria 层
3AppShell
属性
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
class | ClassValue | — | 应用于 shell 根元素的额外类名。 |
ui | Partial<UiClass<AppShellUiSlot>> | — | 该组件的按槽类重写。 |
size | ThemeSize | 'md' | 组件的视觉尺寸。 |
mode | AppShellMode | 'sidebar' | Shell 骨架,同时驱动布局区域和菜单结构。
**桌面**骨架:在移动视图中,Shell 会渲染 `sidebar`
,因此抽屉中包含一个嵌套树,其中存放着完整的菜单。 |
side | import("@vean/aria").HorizontalSide | 'left' | 侧边栏所在的位置。 |
open | boolean | — | 侧边栏的受控展开状态。可通过 `v-model:open` 进行绑定。 |
defaultOpen | boolean | true | 侧边栏在初始渲染时的展开状态。 |
mobileOpen | boolean | — | 移动端抽屉的受控打开状态。可通过 `v-model:mobileOpen` 进行绑定。
Shell 通过 `SLayout` 渲染抽屉,其中抽屉是
桌面侧边栏的移动端对应组件,并拥有独立的状态。 |
defaultMobileOpen | boolean | false | 移动抽屉在初始渲染时的展开状态。 |
isMobile | boolean | — | 外壳是否处于移动视图模式。
若省略该参数,则将其作为未设置值传递,由 `SLayout` 解析模式:即
主机通过 `provideViewportContext` 发布的视口,随后
`useMediaQuery('(max-width: 767.9px)')`。传递一个显式的布尔值可强制
设置该模式,例如用于服务器端检测。 |
layoutProps | Omit<LayoutCompactProps, 'open' | 'defaultOpen' | 'class' | 'pxToRem' | 'mobileOpen' | 'defaultMobileOpen' | 'orientation' | 'sidebarVisible' | 'isMobile'> | — | 转发给 `SLayout` 的属性。
Shell 拥有的选项(`open`、`orientation`、`sidebarVisible`、`isMobile`、
`pxToRem`)由 `mode` 衍生而来;`sidebarWidth` 和
`collapsedSidebarWidth` 默认采用该模式所需的宽度——仅当菜单面板无需对齐时才应
覆盖这些值。 |
layoutUi | Partial<LayoutUi> | — | 针对内部 `SLayout` 的按槽类重写。优先级高于
shell 的 `layout*` UI 槽。 |
logoPlacement | AppShellLogoPlacement | 'auto' | 品牌区域的放置位置。
`auto` 遵循 Shell 渲染的骨架;`sidebar` 和 `sidebar-bottom`
需要一个其侧边栏始终作为独立列存在的骨架——参见
`AppShellLogoPlacement` 以了解支持底部放置的模式。在
移动视图中,该骨架为 `sidebar` 类型,因此无论绑定何种模式,`auto` 都会将品牌
放置在抽屉栏中。 |
expandStrategy | TreeMenuExpandStrategy | 'selected' | 渲染菜单的展开策略。
- `keep`:保持当前展开状态;手动展开或折叠的菜单不会因激活其他菜单而受到影响。选中菜单的可折叠父菜单会在挂载时以及每次选中菜单从外部发生变化时(例如由外部路由驱动)展开,从而保持其可见性。
- `selected`:仅展开当前选中的菜单及其所有父级菜单;当选中菜单发生变化时,未选中的菜单将被折叠。
Shell 的默认行为是 `selected`:导航反映当前路由,
因此一条路径保持展开状态,且活动条目会保留其位置,而不是
与会话中较早打开的分支相互冲突。
适用于单个 `STreeMenu`(`sidebar` 模式)以及 `SSplitNav` 的嵌套垂直
窗格(拆分模式)。`top` 模式渲染 `STreeNav`,该组件
不支持展开策略。 `menuProps.tree` / `menuProps.split` 的优先级
高于此属性。 |
items | AppShellMenuOptionData[] | — | 由 shell 渲染的菜单树。 |
modelValue | string | — | 受控的“活动菜单”值。可通过 `v-model` 进行绑定。
“活动菜单”仅受控:壳层在渲染前会根据该值推导出侧边栏的几何尺寸
和面包屑导航,因此它会读取所给定的值,
而非保留自身的值。 |
menuProps | AppShellMenuProps | — | 转发至渲染菜单的属性。 |
menuUi | AppShellMenuUi | — | 按槽位覆盖的类将转发至渲染后的菜单。 |
breadcrumbs | BreadcrumbOptionData[] | — | 在页眉中渲染的面包屑导航项。
若需省略该内容,可从 `items` 和当前选中项推导出面包屑路径:该
路径从根菜单延伸至当前选中项,其中每个菜单包含子项的祖先节点
都会展开其子项的下拉菜单——其嵌套结构与
菜单本身一致,因此拥有子项的条目将保留其子菜单。
提供此选项可改用您自己的数据渲染普通面包屑导航。 |
breadcrumbVisible | boolean | true | 当导航路径不为空时,面包屑区域是否显示。
无论哪种情况,该区域都会被隐藏在 `md` 断点下方:手机页眉
会保留其触发条件和后续操作。绑定 `ui.breadcrumb`(例如
`lt-md:flex`)即可将其恢复至该位置。 |
breadcrumbProps | Omit<BreadcrumbCompactProps<BreadcrumbOptionData>, 'items'> | — | 这些属性将转发至 `SBreadcrumb`。 |
breadcrumbUi | Partial<BreadcrumbUi> | — | 针对 `SBreadcrumb` 的按槽类重写。 |
tabs | PageTabsOptionData[] | — | 页面标签在标签区域中渲染。 |
tabValue | string | — | 受控的活动选项卡值。可通过 `v-model:tabValue` 进行绑定。 |
tabProps | Omit<PageTabsCompactProps<PageTabsOptionData>, 'modelValue' | 'defaultValue' | 'items'> | — | 转发给 `SPageTabs` 的属性。 |
tabUi | Partial<PageTabsUi> | — | 针对 `SPageTabs` 的按槽类重写。 |
triggerVisible | boolean | true | 侧边栏触发器是否在页眉中渲染。 |
事件
| 事件名 | 参数 | 描述 |
|---|---|---|
update:open | boolean | 当侧边栏的展开状态发生变化时触发。 |
update:mobileOpen | boolean | 在移动端抽屉栏的打开状态发生变化时触发。 |
update:modelValue | string | 当活动菜单值发生变化时触发。 |
select | string, event?: Event | undefined | 当选中菜单叶节点时触发。 |
open | AppShellMenuOptionData, event?: Event | undefined | 当菜单父项打开其嵌套面板时触发。 |
update:tabValue | string | 当活动标签页发生变化时触发。 |
update:tabs | PageTabsOptionData[] | 当标签页集合发生变化时触发(关闭、固定、拖动重新排序)。 |
tabClick | PageTabsOptionData | 点击标签时触发。 |
tabClose | PageTabsOptionData | 在关闭标签页时触发。 |
tabPin | PageTabsOptionData | 当标签页被固定或取消固定时触发。 |
tabContextmenu | PageTabsOptionData | 当 Tab 快捷键右键菜单的目标发生变化时触发。 |
tabSelectContextMenu | PageTabsContextMenuOptionData, tab: PageTabsOptionData | 当选中“制表符”上下文菜单项时触发。 |
tabDragStart | PageTabsDragEvent<PageTabsOptionData> | 在开始拖动选项卡时触发。 |
tabDragMove | PageTabsDragEvent<PageTabsOptionData> | 在拖动选项卡时触发。 |
tabDragReorder | PageTabsDragEvent<PageTabsOptionData> | 当被拖动的标签页在原位置重新排序时触发。 |
tabDragEnd | PageTabsDragEvent<PageTabsOptionData> | 在拖动标签页结束时触发。 |
breadcrumbClick | BreadcrumbOptionData | 当点击面包屑导航项时触发。 |
插槽
| 插槽名 | 参数 | 描述 |
|---|---|---|
default | — | 默认插槽(即页面内容)的自定义内容。 |
logo | AppShellBrandSlotProps | 品牌标识的自定义内容。
壳程序将其渲染在独立的单元格中,以便品牌标识与
菜单对齐:在轨道模式下位于一级轨道上方,且当
侧边栏折叠时居中显示。 在双垂直形状的侧边栏布局中,
该单元格是菜单自身的 `top-left` 单元格,因此轨道分隔线会
穿过品牌标识,而非由 shell 绘制第二条分隔线;其他所有
布局都会将该区域渲染在菜单旁边。 只要
提供了此插槽,该区域就会立即渲染;`title` 为可选参数。 |
title | AppShellBrandSlotProps | 品牌标题的自定义内容。
在“轨道”模式下,该内容渲染在标记旁,并与侧边栏的第二列(嵌套窗格)
对齐——在双垂直布局中,则通过菜单自身的 `top-right` 单元格
进行对齐。 当侧边栏折叠时(即其对齐的
列被折叠隐藏),以及在没有可对齐的
窗格列的布局中,该内容将被隐藏。 |
sidebar-start | — | 侧边栏顶部渲染的自定义内容。 |
sidebar-end | — | 侧边栏底部渲染的自定义内容。 |
menu | AppShellMenuSlotProps | 菜单的自定义内容。替换菜单实例,而非挂载目标。 |
header-start | — | 页眉区域的自定义内容。 |
header | — | 页眉中央区域的自定义内容。 |
header-end | — | 页眉末尾区域的自定义内容。 |
breadcrumb | { items: BreadcrumbOptionData[]; } | 面包屑导航区域的自定义内容。 |
tabs | — | 标签页区域的自定义内容。 |
footer | — | 页脚区域的自定义内容。 |
AppShellBrand
AppShellMenu
属性
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
tree | Omit<TreeMenuCompactProps<import("@vean/aria").TreeMenuBaseOptionData>, 'modelValue' | 'defaultValue' | 'items' | 'collapsed' | 'defaultCollapsed' | 'collapsedWidth' | 'pxToRem'> | — | 转发至嵌套的 `STreeMenu`(`sidebar` 模式)。 |
split | Omit<SplitNavRootProps<SplitNavBaseOptionData>, 'modelValue' | 'defaultValue' | 'as' | 'class' | 'items' | 'collapsed' | 'defaultCollapsed' | 'collapsedWidth' | 'mode' | 'horizontalMountedId' | 'verticalMountedId'> | — | 转发至 `SSplitNav`(四种拆分模式)。 |
treeNav | Omit<TreeNavCompactProps, 'modelValue' | 'defaultValue' | 'class' | 'items'> | — | 已转发至 `STreeNav`(`top` 模式)。 |
Notes
模式骨架
mode 决定布局方向、菜单渲染器与菜单面板的挂载位置。模式名描述的是菜单形态,不是布局方向——布局方向由「一级菜单是纵向还是横向」推导。
mode | 一级位置 | 渲染器 | layoutProps.orientation | 品牌区 | 折叠按钮 | 菜单面板 |
|---|---|---|---|---|---|---|
sidebar | 侧栏 | STreeMenu | horizontal | 侧栏 | 顶栏 | 侧栏中一棵嵌套树 |
top | 顶栏 | STreeNav | vertical | 顶栏 | — | 顶栏一条导航条,可由 menuProps.treeNav 折叠 |
dual-vertical | 侧栏 | SSplitNav | horizontal | 侧栏 | 顶栏 | 侧栏内轨道 + 树并排 |
vertical-horizontal | 侧栏 | SSplitNav | horizontal | 顶栏 | 顶栏 | 轨道在侧栏,横向子导航在顶栏 |
horizontal-vertical | 顶栏 | SSplitNav | vertical | 顶栏 | 侧栏右下角 | 顶栏一级条,树在侧栏 |
horizontal-dual-vertical | 顶栏 | SSplitNav | vertical | 顶栏 | 侧栏右下角 | 顶栏一级条,轨道 + 树在侧栏 |
本表描述的是桌面视图。手机上外壳对所有模式都按 sidebar 骨架渲染——见 移动端视图。
折叠按钮跟着模式的「一级位置」走:顶栏一级的两个模式(horizontal-vertical、horizontal-dual-vertical)把它放在侧栏右下角——展开时右对齐、贴齐它要折叠的那一列,折叠后居中于该列折成的图标栏——因为它们的一级已经在顶栏里;其余带侧栏的模式都把它留在顶栏、紧邻品牌区。triggerVisible 在两种位置都能把它隐藏。
这两种放在侧栏的位置,只有在那个面板列存在时才渲染按钮:只剩下轨道时并没有可折叠的东西,因此从顶栏展开一个分支才是按钮出现的时机。按钮所在的这一行同样对齐各列——第一个单元格与轨道等宽、并带上与面板前缘相同的那条分割线,于是这条线越过菜单一直画到侧栏底边。
方向列的规则:一级菜单在侧栏时,侧栏满高、顶栏从它右侧开始(orientation="horizontal");一级菜单在顶栏时,顶栏横跨整宽、侧栏压在它下方(orientation="vertical")。这与 SLayout 原始的 prop 名称含义相反,请以本表为准,不要凭直觉猜。
移动端视图
手机上侧栏就是布局的抽屉,而抽屉只有一列:顶栏一级条、轨道、面板在里面都无处安放。因此外壳对所有模式都按 sidebar 骨架渲染——品牌区在上,其下一棵嵌套 STreeMenu 承载全量菜单树,由顶栏的 trigger 打开;其余区域保持原位,页签、内容、页脚都不受影响。所以 top 模式在手机上依然有导航:它的一级条会移进抽屉,而不会撑在一条放不下的顶栏里。
顶栏在手机上会让出面包屑。trail 和 trigger、尾部操作挤在同一条 390px 的栏里没有位置,而且它的优先级高于两者:它会按自身内容宽度撑开,把操作区挤到视口之外,整页跟着出现横向滚动。因此面包屑在 md 断点以下隐藏——与外壳切换视图的 767.9px 是同一个断点。这是样式规则而不是渲染规则:手机上该区域仍在标记里,只是不可见,因此服务端渲染的首帧也不会闪出 trail。传 ui.breadcrumb(例如 lt-md:flex)可把它放回来,传 breadcrumbVisible 则可全局去掉。
在放得下的顶栏里,深层 trail 会截断而不是换行或挤走操作:前导区里只有面包屑这一项会收缩,各项保持单行,每个标签用省略号收尾——无论 trail 多长,尾部操作都保有自己的一份宽度。
mode 属性不会被改写:它描述的是桌面形态,data-mode 始终回报你绑定的值,#menu 插槽拿到的也仍是它。离开移动端视图,桌面形态自然就回来了。
外壳解析视口的方式与 SLayout 完全一致——显式 isMobile、宿主通过 provideViewportContext 发布的视口、再到 useMediaQuery 读取共享的 767.9px 断点——因此两者对「当前处于哪种视图」不可能出现分歧。要从外部强制该形态,走外壳同一条路即可:绑定 isMobile,或在设备预览里通过 provideViewportContext 发布视口。
按 sidebar 骨架渲染还带来两个结果:品牌区跟着它走——logoPlacement="auto" 会把品牌区放进抽屉,即使该模式的桌面默认位置是顶栏;自定义 #menu 插槽则按你写的内容原样渲染,替换菜单等于同时接管手机形态,因为抽屉只有一列。
嵌套面板只在"有内容可显示"时占位,外壳不按模式猜,而是向 split-nav 家族询问当前存在哪些列:dual-vertical 的侧栏在激活的一级菜单有子菜单时是"轨道 + 树",否则只有轨道;horizontal-vertical 没有轨道,侧栏要么是树、要么完全不出现。horizontal-dual-vertical 需要多看一遍:它的侧栏承载的是一级菜单的 dual-vertical 面板,因此轨道装的是该一级菜单的子菜单、面板装的是再下一级——激活的一级菜单是叶子,或只是打开了它但还没打开其中任何一项时,侧栏就只剩轨道、甚至一列都不占。一列都不占的侧栏既不占位也不绘制:它把「列不参与布局」发布到布局根上(data-sidebar-flow="false"),据此收回 floating/inset 变体加在侧栏旁边的那一个间距,并隐藏这列空侧栏——但区域本身仍留在树里,因为 split 面板要传送到它的挂载点上,而承载顶栏一级的菜单根也在这里。面板跟随的是每一次菜单激活,而不是激活值:激活父级则展开,激活叶子则收起,重复激活当前已经激活的叶子同样会收起——这次激活会重置菜单内部的路径,但不会改变 v-model。侧栏折叠时面板随之收折:侧栏变为"轨道 + 面板折叠后的图标栏"(无轨道的模式就是该图标栏本身),面板自身一级以下的层级依然可以从折叠菜单弹出的浮层进入。
品牌区位置
#logo 与 #title 渲染在同一块区域里;logoPlacement 决定这块区域放在哪里,模式则决定它能与什么对齐。侧栏内的摆放完全镜像菜单渲染出的列——标记图占第一列、标题占第二列——所以品牌正好压在轨道与其下的面板上,而不是被侧栏统一内边距顶偏。在 dual-vertical 形态下这两个格子就是菜单自己的:外壳把标记图交给菜单的 top-left、标题交给 top-right,于是面板列的前缘分割线直接贯穿这条品牌带,而不需要外壳再画一条。
mode | 默认位置 | sidebar | sidebar-bottom | 区域与什么对齐 |
|---|---|---|---|---|
sidebar | 侧栏 | ✅ | ✅ | 只有一列:标记与标题同一行,折叠时居中。 |
top | 顶栏 | ❌ | ❌ | 没有侧栏区域,两种侧栏位置都回落到顶栏。 |
dual-vertical | 侧栏 | ✅ | ✅ | 菜单自己的 top-left(轨道)与 top-right(面板)。 |
vertical-horizontal | 顶栏 | ⚠️ | ❌ | 侧栏只有轨道,因此只渲染标记图。 |
horizontal-vertical | 顶栏 | ⚠️ | ❌ | 侧栏就是当前一级菜单的面板。 |
horizontal-dual-vertical | 顶栏 | ⚠️ | ❌ | 侧栏承载当前一级菜单的轨道与面板。 |
❌ 表示外壳保持该模式的默认位置,而不是把区域渲染在留不住的地方。⚠️ 表示位置被采纳,但这些侧栏只有在"当前一级菜单有子级"时才存在列——品牌区会随菜单一起出现/消失;这正是这些模式拒绝 sidebar-bottom 的原因:钉在一个反复消失的列上会被当成 bug,而顶栏里的品牌区从不动。
移动端视图下外壳按 sidebar 骨架渲染,因此无论绑定的是哪个模式,auto 都会把品牌区放进抽屉——见 移动端视图。
三条规则在所有位置都成立:底部区域渲染在菜单与 sidebar-end 之后,因此贴在侧栏底边;侧栏折叠时标题一定隐藏,因为它对齐的面板列已折起;标记图要么在变窄的侧栏里居中(sidebar),要么留在仍保有该列的轨道上(双竖栏形态下它就是菜单的 top-left 格)。
分割线由后面那一列负责:SSplitNav 让面板以前缘分割线开场,因此它贯穿该列整高——包括菜单项之上的品牌带——再由底部的折叠按钮行把这条线续到侧栏底边。外壳不再为品牌单元格补画分割线;侧栏只剩轨道时内部完全没有分割线。菜单各列同样撑满侧栏高度,也是同一个道理——分割线只画到内容底部会被看成是缺陷。
架构与同类对比
| 关注点 | SoybeanUI SAppShell | Ant Design Pro ProLayout | Element Plus | shadcn/ui |
|---|---|---|---|---|
| 分层 | Aria layout 族之上的样式聚合层;SLayout 仍可单独使用 | 单一样式包 | 单一样式包 | 复制即用块,非库 |
| 模式数量 | 6(sidebar、top + 4 种分栏形态),由单个 mode 驱动 | 6(side/top/mix 及顶栏变体) | — | 1 |
| 菜单树 | 一份 items 树,按模式交给 STreeMenu / STreeNav / SSplitNav | 路由/属性 | el-menu 配置 | 手写组合 |
| 侧栏几何 | 由 mode + size 推导,并与菜单面板对齐 | 数值属性 | 数值属性 | 手写 CSS |
| 品牌区 | #logo / #title 插槽,与菜单列对齐 | logo / title 属性 | el-aside 内容 | 手写组合 |
| 面包屑 / 页签 | breadcrumbs / tabs 数据属性,交给 SBreadcrumb / SPageTabs 渲染 | menu 配置 + PageContainer | 手写组合 | 手写组合 |
| 路由依赖 | 无——数据进、事件出 | 默认依赖 Vue Router | 无 | 无 |
| 覆盖能力 | ui / layoutUi / menuUi + 每区域插槽 | token / 插槽 | CSS 变量 | 改复制来的源码 |
| 无障碍 | 由被组合的 Aria 组件族承担 | 包级实现 | 包级实现 | 手动 |
注意事项
- 品牌区完全由你实现。 没有
logo/title属性:标记图放#logo,应用名与副标题放#title。外壳只渲染区域,并把实时的collapsed与解析后的placement传给两个插槽,让注入内容自行适配。#title可省略——侧栏折叠时它会被隐藏,因为它对齐的那一列已经折起;没有#logo时整块品牌区都不渲染。侧栏为单列时,该区域自身按一行 tree-menu 菜单渲染:取菜单根的 inset,并随菜单一起收进菜单项的图标宽度,这样折叠后标记图仍落在菜单的图标列上;折叠后的行只放标记图,行自身的内边距会让标记图在内容盒里更宽,因此该行会把标记图居中,避免贴着行尾显得右偏。在双竖栏形态下标记图与标题改为注入菜单的top-left/top-right格,分割线因此归菜单所有;顶栏与底部摆放时区域仍保留自己的单元格。该行本身不是交互元素——品牌需要跳转时请在插槽内注入自己的链接或触发器。 isMobile默认跟随视口,且两层都如此。 外壳自己也要解析视图来选择骨架 —— 组件上的显式isMobile、宿主通过provideViewportContext发布的视口,或useMediaQuery(mobileViewportQuery)(即样式层隐藏侧栏的767.9px断点);而传给SLayout的仍是「未传」的原始属性,布局首帧的lt-md兜底因此照旧生效。传入显式布尔值(例如服务端判断)可一次覆盖两层。见 移动端视图。- 侧栏宽度是推导值,不要随意覆盖。
layoutProps.sidebarWidth/collapsedSidebarWidth的优先级高于推导值,但在分栏模式下既会导致侧栏与菜单面板失准,也会失去下面的动态行为。 - 切换模式会重建菜单。 不同模式渲染不同的菜单实例,因此面板的展开状态(保存在
SSplitNav/STreeMenu内部)会在模式切换时回到激活路径 —— 桌面形态与移动形态之间的切换同样会换掉渲染器。 - 顶栏是
STreeNav,不是单纯的菜单条。 它和侧栏一样由modelValue驱动选中:激活叶子会发出select/update:modelValue,有子菜单的条目只展开弹层,带to/href的条目同时执行跳转。设置menuProps.treeNav.collapsible可把放不下的条目收进末尾的「更多」弹层。 - 页签集合会被就地修改。
SPageTabs关闭页签、切换pinned时直接修改传入的tabs数组;update:tabs只在批量操作(关闭其他/全部)与拖拽排序时触发。请保持集合为响应式(用深ref,不要用shallowRef),并监听tabClose/tabPin观察这两类动作。 - 侧栏折叠时嵌套面板一并收折。 轨道宽度不变,面板折成自己的图标栏,侧栏正好是这两列——不会有浮层盖住内容。折叠后的菜单保持自己的行为:叶子显示提示气泡,父级弹出浮层,因此各层级都仍然可达。若菜单树需要一眼看全,请保持侧栏展开、让面板占一列。折叠按钮遵循同一套骨架:一级在顶栏的模式把它放在侧栏右下角——且只在面板列存在时才渲染——展开时右对齐于面板、折叠后居中于折成的图标栏,其余模式留在顶栏。
- 侧栏内容不在任何地标内。 布局的侧栏区域是普通元素,没有地标角色,因此环绕菜单注入的内容(品牌区、
sidebar-start、sidebar-end)不在任何地标内。若要做无障碍审计,请自行包裹地标。 - 激活菜单是受控的——没有初始值属性。 外壳在渲染前就要用这个值推导侧栏几何与面包屑,因此它读取传入的值,不自己保存一份。请绑定
v-model(或传modelValue并处理update:modelValue):不给值时既没有激活菜单,也没有面包屑,侧栏的面板列也不会出现。 - 菜单默认沿路由展开。
expandStrategy默认为selected:只有激活菜单所在的那条链路保持展开,所以你先展开、随后跳走的分支会收起,而不会在面板里越积越多。若希望手动展开的分支能跨导航保留,传expand-strategy="keep";只想让其中一个渲染器不同时,用menuProps.tree/menuProps.split单独设置——它们优先于外壳的同名属性。top模式渲染的是STreeNav,它没有展开策略,该属性也不会出现在它的标记里。 - 品牌区跟着列走,且只有两种模式能把它钉在底部。 侧栏内的品牌区里,标记图占菜单渲染的第一列、标题占第二列——双竖栏形态下走菜单自己的
top-left/top-right格,其余位置走外壳自己的区域——因此二者分别与一级轨道及其下的面板对齐;不存在的列会直接丢掉对应单元格——这就是侧栏没有面板列时(尚未打开任何父级,或侧栏折叠)标题被隐藏的原因。sidebar-bottom还要求侧栏始终是独立的一列:sidebar与dual-vertical支持,其余模式保持各自默认位置,而不是把品牌钉在会随菜单出现/消失的地方。详见下方「品牌区位置」一节。
FAQ
如何让面包屑与路由保持同步?
只要让 v-model 与路由保持同步,面包屑会自动跟随:它由 items 加激活值推导,路径、文案与下拉项都来自同一份菜单树。它在顶栏放得下时才渲染在顶栏里,手机上是隐藏的——见 移动端视图。当路径需要与菜单不同(例如按权限裁剪、标题与菜单文案不一致)时,传入自己的 breadcrumbs,外壳就改为渲染这份数据。breadcrumbClick 报告的是面包屑条目的激活;而点击上级下拉里的菜单与点侧栏菜单效果一致——自身有子菜单的条目展开二级菜单,叶子发出 select / update:modelValue。
为什么切换菜单时侧栏宽度会变?
因为嵌套面板只在"存在"时才属于侧栏。没有子菜单的一级菜单没有面板可显示,侧栏就回落到只有轨道;激活有子菜单的菜单时再变宽。若连一列都不占——无轨道的模式(如 horizontal-vertical)停在一级叶子上——侧栏既不占位也不绘制,而不是留下列空壳。折叠态遵循同一条规则,只是面板处于收折状态:宽度为"轨道 + 面板的图标栏",没有内容可显示时只剩轨道。覆盖 layoutProps.sidebarWidth 会得到固定宽度,同时失去这个行为。
为什么我展开的分支在跳转后收起了?
因为菜单默认跟随路由。expandStrategy 取值为 selected,选中项一旦变化,展开集合就会被替换成激活菜单所在的那条链路:面板始终与当前页面对齐,而不是把整个会话里展开过的分支堆积在一起。若希望手动展开的分支跨导航保留:
<SAppShell v-model="active" expand-strategy="keep" :items="items" />两种取值下,激活菜单所在链路都会在挂载时、以及值被外部改变时(例如由路由驱动 v-model)自动展开,所以当前页面始终可见。该策略作用于嵌套的 STreeMenu(sidebar 模式)与分栏模式的面板;top 模式渲染的是没有分支可展开的 STreeNav,因此对它无效。
怎么把品牌区放到侧栏底部?
在支持该位置的模式下(sidebar 或 dual-vertical)设置 logo-placement="sidebar-bottom"。品牌区会渲染在菜单与 sidebar-end 之后,而菜单区域会撑满侧栏,因此品牌落在底边上:
<SAppShell v-model="active" mode="sidebar" logo-placement="sidebar-bottom" :items="items" />品牌区的其余行为不变:标记与标题仍与上方各列对齐,侧栏折叠时标题仍会隐藏、标记要么在变窄的侧栏中居中,要么(dual-vertical)继续留在轨道上——此时它就是菜单的 top-left 格。其余模式保持各自默认位置,具体见下方「品牌区位置」表格。
为什么品牌标题会消失?
因为标题单元格本身也是侧栏的一列:它的宽度取自嵌套面板,而面板列只在"一级菜单有子级且侧栏展开"时才存在。尚未打开任何父级,或侧栏折叠(面板折成图标栏)时,没有列可供标题对齐,外壳就不再渲染它,而不是预留侧栏并不占用的宽度——SSplitNav 的标题格根本收不到内容。标记图始终保留——它对齐的是一级轨道,那才是折叠后仍在的列。
怎么调整侧栏宽度?
sidebar 模式下设置 layoutProps.sidebarWidth / collapsedSidebarWidth。分栏模式下请保持默认:宽度由当前 size 的面板度量推导,覆盖会让侧栏与菜单错位。
如何打开或关闭移动端抽屉?
绑定 v-model:mobileOpen(非受控时用 default-mobile-open)。移动端布局会用抽屉替换桌面侧栏,因此抽屉在 open 之外有自己的状态:open / v-model:open 始终控制桌面折叠,外壳的 trigger 则切换当前模式实际渲染的那一个。单个绑定无法兼顾两者 —— 桌面侧栏默认展开,而抽屉必须默认关闭。
如何让顶栏与路由保持同步?
和侧栏一样:保持 v-model 同步,顶栏就会高亮当前条目,嵌套层级同样生效——子项激活时其上级也会被标记。带 to / href 的条目既跳转也上报,因此接入路由的应用既可以让路由驱动 v-model,也可以直接用外壳上报的激活。
如何在页签上方加页头或页脚?
#header 插槽替换顶栏中区(#header-start / #header-end 覆盖首尾区域),#footer 插槽填充页脚。若要在内容区内部加页面级页头,请在默认插槽里组合 SCard 或普通 header——外壳没有页面页头区域。
如何整体替换菜单?
使用 #menu 插槽。它替换的是菜单实例而非挂载目标:外壳仍渲染区域与传送目标,并把 headerMountId / sidebarMountId 传入插槽,供你自己的 SSplitNav 指向。