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:
@@ -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` 行为不变 |
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: '页脚' },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user