布局组合

从 AppLayout 到 AdminLayout / RailLayout / ShellLayout / Sidebar / TopBar / Container 的常见组合、外壳切换与断点策略。

最后更新

Known 提供一组布局组件,能搭出"三栏管理台"或"窄边栏工作区"或"单栏内容站"或"整屏 Landing"四种主流形态, 两套桌面外壳还能让最终用户在顶栏自己挑。本指南给出最小可跑的几个组合,并说明断点与移动端的取舍。

三栏管理台(最常用)

@* App.razor:把后台骨架设为路由默认布局,页面无需逐页声明 *@
<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(AdminLayout)" />
    </Found>
</Router>
  • AdminLayout 内部按 240 / flex 两段拼:左侧 Sidebar(可折叠)+ 右侧顶部 TopBar 与其下的内容区。
  • 框架自己的 App.razor 把默认布局指向了 ShellLayout(不是 AdminLayout):这一层只负责按当前浏览器的偏好挑一个外壳渲染, 没选过就落回 Config.LayoutType(默认 typeof(AdminLayout))。直接指到 AdminLayout 也完全可用——等于把外壳钉死,用户换不了。
  • Container 是全局浮层宿主,负责渲染 UIService 发出的 Toast、确认框、加载条。 根组件至少要放一个 Container,否则消息无处挂。

Menus 参数结构(不传则由 Config.AddKnown 扫描页面类上的 [Menu] 特性自动装配):

private List<MenuItem> menus =
[
    new("首页", "home", "/"),
    new("订单", "file-text", "#")
    {
        Items =
        [
            new("列表", "list", "/order/list"),
            new("新建", "plus", "/order/new"),
        ]
    },
    new("客户", "user", "/customer"),
];

窄边栏工作区(RailLayout)

模块多、层级浅的工作空间(项目 / 工单 / 数据看板这类),240px 宽侧栏会把导航树撑得很长。 RailLayout 把一级模块压成 74px 图标窄边栏,二级子页面才展开成一列——同样零接线,换个类型即可:

Config.LayoutType = typeof(RailLayout);   // 启动时一次声明,全站换壳

但它更常被当作用户自己的选择,而不是宿主的编译期决定——见下一节的外壳切换。

它的三段形态:

  • 顶行:品牌位(与窄边栏同宽对齐)+ 横贯余宽的 TopBar。顶栏能力一个不减——搜索、刷新、通知、语言与偏好下拉、头像菜单全部复用。
  • 一级窄边栏:只渲染菜单树的顶层节点。分组点击跳它的第一个子页面(即模块首页),顶层链接直接跳;Badge 在图标右上角渲染成小红点。
  • 二级导航列:当前模块有多个可见子页面时才展开(186px),单页模块整块收起、内容区占满余量。
@* 也可逐页覆盖,与 AdminLayout 混用 *@
@layout RailLayout
@page "/project/board"

两个宽度是 CSS 变量,宿主覆写即可、不必改框架:

.rail-layout { --rail-width: 88px; --rail-sub-width: 210px; }

RailLayout 与 AdminLayout 共用同一个外壳基类 AppShell,登录门禁、[Menu] 装配、标题与面包屑推导、软刷新、多页签、维护横幅、水印、命令面板、返回顶部逐项一致;参数表也一致(SiteTitle / Logo / Menus / Notifications / ProfileUrl …)。差别只在侧栏形态:窄边栏没有可折叠的宽侧栏,因此没有 Collapsed。

外壳切换(ShellLayout)

同一套菜单、同一个用户,有人要宽侧栏、有人要窄边栏——这不该让宿主重新发版。框架的路由默认布局因此是一层分发:

@* App.razor:默认布局指向分发层,具体外壳由它按偏好挑 *@
<RouteView RouteData="@routeData" DefaultLayout="@typeof(ShellLayout)" />

ShellLayout 不画任何东西,只做三件事:读一次本地存储里的偏好(known.layout)、在 Config.Shells 里命中候选、把 @Body 交给选中的外壳模板。读不到(预渲染、没有 JS 运行时、旧版 web.js 缺这个模块)就按 Config.LayoutType 渲染——探测失败只是少一次自定义,绝不会把后台挂成空白。

候选出厂内置两项,宿主用 Config.AddShell 追加自家外壳:

// 出厂内置:admin = AdminLayout、rail = RailLayout
Config.AddShell("workbench", "工作台", typeof(WorkbenchLayout), "columns", 3);
Config.LayoutType = typeof(AdminLayout);   // 谁都没选过时的落点

入口在顶栏头像下拉的「布局」段控(位于密度与导航之间),点一下整块后台骨架当场换掉,不重载浏览器;选择存进本地存储,下次访问沿用。

几处口径值得说明:

  • 按浏览器各存一份。LocalService 是 Scoped,偏好跟着电路走,同一台服务端上的不同用户各选各的;Server 托管不会串用户。
  • Config.LayoutType 只是出厂默认。改它只影响「没选过的人」,不会把某个用户的选择冲掉;反过来,用户选过之后,宿主改这一行对他也不生效。
  • 候选不足两项即视为钉死。Config.Shells.Clear() 或只留一项,顶栏那一行整块不渲染,ShellLayout 连旧偏好一并作废,两条口径不会各说各话。
  • 换壳=换组件实例。旧模板的登录门禁、菜单装配、Ctrl+K 热键与顶部加载条随其 Dispose 一并收尾,不留悬空订阅——这也是把探测做成一层分发、而不是让每个模板各读一遍偏好的原因。
  • 宿主的 @layout 覆盖照旧优先。逐页声明的布局不经过这一层,想钉死某些页面(如全屏看板)仍在页面里写 @layout。

顶栏:标题、面包屑与刷新

AdminLayout / RailLayout 作为路由默认布局,页面无法逐页向其传参,因此顶栏左、右上角的三件事都由布局从 Menus 与当前 URL 自动推导,无需额外接线:

  • 标题:默认取当前页在菜单树中命中项的 Name。若菜单里没有这一项(如 Hidden 页或非菜单页),标题会空白——此时用 PageTitle 显式覆盖。
  • 面包屑:标题左侧并排显示命中链路的上级路径(顶层分组 → … → 当前页的父级),不含当前页本身(它已是标题,避免重复)。顶层页面或未命中时不渲染面包屑。分组节点只作纯文本、可点链接则可跳转。
  • 刷新:右上角刷新按钮对当前页做软刷新——递增重挂键令 @Body 容器重建,触发页面组件 Dispose+重新初始化、重新取数,而不重载浏览器。

标题推导的前提是"当前页在 Menus 里"。想给脱离菜单的页面补标题,就在宿主自己的布局里传 PageTitle(AdminLayout / RailLayout 会优先用它);<Container /> 同样只需在根组件出现一次。

要单独复用 TopBar(不走 AdminLayout)时,这几个能力对应它的参数:Breadcrumb(List<BreadcrumbItem>)、ShowRefresh(默认 true)、OnRefresh(宿主接管的软刷新回调;未接则回退 location.reload 整页重载)。

单栏内容页(文章、设置)

<TopBar SiteTitle="设置" BackHref="/" />
<main class="container-narrow">
    @Body
</main>

不用 Sidebar,让内容占满宽度。适合设置页 / 帮助中心 / 表单流程页。

Landing 全屏

<main class="landing">
    <Hero />
    <FeatureGrid />
    <Pricing />
    <Footer />
</main>

无 AdminLayout、无 Sidebar;根布局直接返回 @Body 即可。 (营销站 Website 用的 MainLayout 就是这个形态。)

移动端 App 外壳

手机浏览器上,侧边栏 + 顶栏的桌面后台并不合适。Known 另给了一套移动端母版 AppLayout: 顶部 App 栏 + 可滚动内容区 + 底部 Tab 导航。页面用 @layout AppLayout 挂上即可,底部 Tab 用类级 [AppMenu] 特性在外壳页集中声明一次、由框架装配,顶栏标题按当前命中的 Tab 自动推导:

@page "/app"
@layout AppLayout
@attribute [AppMenu("首页", Icon = "home", Url = "/app", Order = 1)]
@attribute [AppMenu("我的", Icon = "user", Url = "/app/mine", Order = 2)]

@* 移动端内容 *@

登录成功后,框架会依据设备判定自动落位:移动端进 Config.AppHomeRoute(默认 /app),桌面端进 /。 完整用法——设备判定、[AppMenu] 字段、软刷新与常见坑——见移动端应用外壳。

断点策略

断点 AdminLayout RailLayout
> 1200px 侧栏 240px,折叠态 72px 图标栏 窄边栏 74px + 二级列 186px
≤ 900px 顶栏收起全局搜索框与头像旁的姓名,内容区留白收紧;侧栏宽度不变 同左
≤ 860px —— 二级列收进左侧滑出的抽屉,顶栏品牌位右侧出现开关;换路由自动收起

侧栏折叠不是按宽度自动发生的:它由 Collapsed 参数或「系统设置 · 侧栏默认折叠」决定,用户也可随时点折叠开关,偏好落在本地。 窄边栏的两个宽度则是纯 CSS 变量(--rail-width / --rail-sub-width),宿主覆写样式即可。 手机浏览器上这两套桌面外壳都不合适,请用AppLayout。

全局浮层:Container

Container 只需在根布局出现一次;它内部按渲染顺序堆叠:

  1. 顶部细加载条(UIService.ShowLoading 触发);
  2. 右上 Toast 队列(Success / Error / Info);
  3. 中间确认对话框(ConfirmAsync);
  4. 遮罩层 + 抽屉。

不要在页面级 razor 里嵌 Container——会重复渲染出多条 Toast 队列。

常见坑

  • 忘了 <Container /> → UIService.Success(...) 调用后什么都没弹出。
  • 顶栏「布局」点了没反应 → App.razor 的 DefaultLayout 还指着具体模板;偏好要经 ShellLayout 这层分发才生效(或直接 Config.Shells 只剩一项,入口本就不出现)。
  • 在 Modal 里再放 Container → 双重渲染、关闭时状态错乱。
  • Menus 结构循环引用 → Sidebar 无限递归;加 [JsonIgnoreReferenceLoop] 或改成 parentId 扁平表。
  • 移动端汉堡无反应 → 检查 site.js 里的原生增强是否加载;Known 组件在 WASM 里靠 @onclick 是通的,在营销站 SSR 场景需要 JS 岛接管。

相关

  • 想在顶栏右上角挂出自己的功能按钮(命令 / 外链 / 下拉面板,可按角色显隐),见顶栏自定义功能。
  • AppShell 只把"侧栏长什么样"留给子类:登录门禁、[Menu] 装配、标题与面包屑推导、软刷新、页签记账、水印与命令面板都在基类里,AdminLayout / RailLayout 是它的两个形态。再要一种侧栏(如顶部横向主导航)就继承 AppShell、只写 @Body 外围那几行,然后用 Config.AddShell 登记,用户即可在顶栏「布局」里选到它。
  • 外壳怎么按浏览器各存一份、Config.Shells 与 Config.LayoutType 谁说了算,见上文外壳切换(ShellLayout)。
  • 手机端的 AppLayout 母版与 [AppMenu] 底部 Tab、登录后按设备自动落位,见移动端应用外壳。
  • 主题切换(--surface --line)在管理台里怎么落色,见深浅色主题。
  • 权限驱动的菜单可见性(MenuItem.Hidden)属 SB-03 团队与权限切片,Batch 3 之后。
微信公众号二维码 扫码关注公众号
QQ 交流群二维码 扫码加入 QQ 群