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 只需在根布局出现一次;它内部按渲染顺序堆叠:
- 顶部细加载条(
UIService.ShowLoading触发); - 右上 Toast 队列(
Success/Error/Info); - 中间确认对话框(
ConfirmAsync); - 遮罩层 + 抽屉。
不要在页面级 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 群