feat(ui): Layout 新增 toolbar 插槽

页头与页身之间的一条真实占位带(搜索 / 筛选 / 操作),宽度跟随容器,
contained 时与侧栏 + 内容同宽,height=screen 下不参与内容区滚动。
用它替代「搜索框 fixed + contentOffset 让位」的写法。

- Layout.tsx / Layout.module.css:新增 toolbar prop 与样式
- 展示页 #layout 增加「页头下的工具条」示例
- COMPONENTS.md 补契约、与 contentOffset 的分工说明、changelog
This commit is contained in:
2026-11-01 15:15:14 +08:00
parent ebe4af3dab
commit 93bb5cbb28
5 changed files with 73 additions and 1 deletions
+3
View File
@@ -500,6 +500,7 @@
| prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| `header` | `ReactNode` | — | 页头,默认吸顶(半透明 + 模糊) |
| `toolbar` | `ReactNode` | — | 页头与页身之间的工具条(搜索 / 筛选 / 操作带),宽度随容器,不参与内容区滚动 |
| `sider` | `ReactNode` | — | 左侧栏,固定宽度 |
| `aside` | `ReactNode` | — | 右侧栏,固定宽度(目录 / 聚合 / 广告位) |
| `footer` | `ReactNode` | — | 页脚 |
@@ -522,6 +523,7 @@
| `contentId` | `string` | — | 内容区 id(供 ScrollTop 等按 id 滚动) |
- **规则**:Layout 不写业务内容,也不带背景装饰(需要渐变/光晕时由页面自己放绝对定位层);响应式在组件内处理——≤1200px 右栏堆到内容下方、≤1100px 侧栏堆到内容上方,**内容都不隐藏**。
- **`toolbar` 与 `contentOffset` 的分工**:`toolbar` 是**真实占位**的一层,搜索框照常参与文档流,侧栏与内容的顶边天然对齐(推荐);`contentOffset` 只用于「浮在页面上、不能占位」的场景(悬浮条、绝对定位的返回按钮),不要把两者叠加使用。
- **常用配方**:顶栏+内容(首页)、左栏+内容(管理页)、内容+右栏(文章)、三栏(工作台)、窄内容(设置/阅读),展示页用 `Segmented` 切换。
- **展示页**:`#layout`
@@ -700,3 +702,4 @@
| 2026-10-29 | 新增 `Textarea`(`rows` / `resize` / `invalid`),与 `Input` 共用边框与焦点样式。兼容性:纯新增 |
| 2026-10-29 | 新增 `PasswordInput`(`icon` / `size` / `toggle`):锁图标 + 显隐切换 + `aria-pressed`,事件语义与 `Input` 一致。兼容性:纯新增 |
| 2026-10-30 | **修复**:`PasswordInput` 的眼睛按钮移入输入框内部(`Input` 新增 `suffix` 插槽);`Input` 隐藏 Edge 原生 `::-ms-reveal` / `::-ms-clear`,避免与自带显隐按钮叠成两个眼睛。兼容性:`Input` 新增可选 prop,无破坏性;`PasswordInput` API 不变、仅外观变化 |
| 2026-10-31 | `Layout` 新增 `toolbar` 插槽:页头与页身之间的一条真实占位带(搜索 / 筛选 / 操作),宽度跟随容器(`contained` 时与侧栏 + 内容同宽),在 `height="screen"` 下不参与内部滚动。用它替代「搜索框 fixed + `contentOffset` 让位」的写法,页面不再需要用 JS 改 DOM 尺寸。兼容性:纯新增可选 prop,`contentOffset` 行为不变 |
+13
View File
@@ -562,6 +562,19 @@ code {
gap: var(--su-space-1);
}
/* 工具条:页头与页身之间的一条搜索带(toolbar 插槽) */
.layout-toolbar {
display: flex;
align-items: center;
justify-content: center;
padding: var(--su-space-3) var(--su-space-5);
border-bottom: 1px solid var(--su-border);
}
.layout-toolbar > * {
width: min(360px, 100%);
}
.layout-side {
display: flex;
flex-direction: column;
+42 -1
View File
@@ -1,5 +1,5 @@
import { useState } from 'react'
import { Card, Grid, Layout, Segmented, Space, Tag, Text, Title } from '../../src'
import { Card, Grid, Input, Layout, Segmented, Space, Tag, Text, Title } from '../../src'
import { Demo } from '../components/Demo'
import { PropsTable } from '../components/PropsTable'
import { Section } from '../components/Section'
@@ -214,9 +214,50 @@ export function PageLayout() {
</div>
</Demo>
<Demo
title="页头下的工具条"
desc="toolbar 是真实占位的一层:宽度跟随容器,坐在页头与页身之间;整屏模式下它不参与内容区滚动,搜索框不用再 fixed + 手算 offset。"
layout="column"
code={`<Layout
header={<Header />}
toolbar={<Search />}
sider={<Nav />}
height="screen"
scrollable
>
<Content />
</Layout>`}
>
<div className="layout-frame" style={{ height: 320, overflow: 'hidden' }}>
<Layout
header={<DemoHeader />}
toolbar={
<div className="layout-toolbar">
<Input size="lg" icon="search_3_line" placeholder="搜索本地项目…" readOnly />
</div>
}
sider={<DemoSider compact />}
headerHeight={56}
height="screen"
scrollable
hideScrollbar
style={{ height: 320 }}
>
<div className="layout-content">
{Array.from({ length: 6 }, (_, index) => (
<Card padding="md" key={index}>
<Text size="sm">第 {index + 1} 行:工具条固定在页头下方,只有这里滚动。</Text>
</Card>
))}
</div>
</Layout>
</div>
</Demo>
<PropsTable
rows={[
{ name: 'header', type: 'ReactNode', desc: '页头,默认吸顶' },
{ name: 'toolbar', type: 'ReactNode', desc: '页头与页身之间的工具条(搜索 / 筛选 / 操作带),宽度随容器' },
{ name: 'sider', type: 'ReactNode', desc: '左侧栏,固定宽度' },
{ name: 'aside', type: 'ReactNode', desc: '右侧栏,固定宽度(目录 / 聚合 / 广告位)' },
{ name: 'footer', type: 'ReactNode', desc: '页脚' },
+7
View File
@@ -82,6 +82,13 @@
border-bottom: 1px solid var(--su-border);
}
/* 页头与页身之间的工具条:宽度随容器(contained 时与两栏同宽),
在正常流里占位,不参与 body 的内部滚动(screen 模式下就是「固定带」)。 */
.toolbar {
flex-shrink: 0;
width: 100%;
}
.body {
display: flex;
flex: 1;
+8
View File
@@ -15,6 +15,11 @@ import styles from './Layout.module.css'
export type LayoutProps = {
/** 页头:整宽、默认吸顶 */
header?: ReactNode
/**
* 工具条:页头与页身之间的一条带(搜索、筛选、面包屑下方的操作)。
* 宽度跟随容器(`contained` 时与侧栏 + 内容同宽),不参与内容区滚动。
*/
toolbar?: ReactNode
/** 左侧栏:固定宽度、与内容并排 */
sider?: ReactNode
/** 右侧栏:固定宽度、与内容并排(资料聚合、目录、广告位) */
@@ -67,6 +72,7 @@ export type LayoutProps = {
export function Layout({
header,
toolbar,
sider,
aside,
footer,
@@ -124,6 +130,8 @@ export function Layout({
)}
<div className={contained ? styles.container : undefined} style={containerStyle}>
{toolbar !== undefined && <div className={styles.toolbar}>{toolbar}</div>}
<div className={styles.body}>
{sider !== undefined && (
<aside