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 | 类型 | 默认 | 说明 |
|
| prop | 类型 | 默认 | 说明 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `header` | `ReactNode` | — | 页头,默认吸顶(半透明 + 模糊) |
|
| `header` | `ReactNode` | — | 页头,默认吸顶(半透明 + 模糊) |
|
||||||
|
| `toolbar` | `ReactNode` | — | 页头与页身之间的工具条(搜索 / 筛选 / 操作带),宽度随容器,不参与内容区滚动 |
|
||||||
| `sider` | `ReactNode` | — | 左侧栏,固定宽度 |
|
| `sider` | `ReactNode` | — | 左侧栏,固定宽度 |
|
||||||
| `aside` | `ReactNode` | — | 右侧栏,固定宽度(目录 / 聚合 / 广告位) |
|
| `aside` | `ReactNode` | — | 右侧栏,固定宽度(目录 / 聚合 / 广告位) |
|
||||||
| `footer` | `ReactNode` | — | 页脚 |
|
| `footer` | `ReactNode` | — | 页脚 |
|
||||||
@@ -522,6 +523,7 @@
|
|||||||
| `contentId` | `string` | — | 内容区 id(供 ScrollTop 等按 id 滚动) |
|
| `contentId` | `string` | — | 内容区 id(供 ScrollTop 等按 id 滚动) |
|
||||||
|
|
||||||
- **规则**:Layout 不写业务内容,也不带背景装饰(需要渐变/光晕时由页面自己放绝对定位层);响应式在组件内处理——≤1200px 右栏堆到内容下方、≤1100px 侧栏堆到内容上方,**内容都不隐藏**。
|
- **规则**:Layout 不写业务内容,也不带背景装饰(需要渐变/光晕时由页面自己放绝对定位层);响应式在组件内处理——≤1200px 右栏堆到内容下方、≤1100px 侧栏堆到内容上方,**内容都不隐藏**。
|
||||||
|
- **`toolbar` 与 `contentOffset` 的分工**:`toolbar` 是**真实占位**的一层,搜索框照常参与文档流,侧栏与内容的顶边天然对齐(推荐);`contentOffset` 只用于「浮在页面上、不能占位」的场景(悬浮条、绝对定位的返回按钮),不要把两者叠加使用。
|
||||||
- **常用配方**:顶栏+内容(首页)、左栏+内容(管理页)、内容+右栏(文章)、三栏(工作台)、窄内容(设置/阅读),展示页用 `Segmented` 切换。
|
- **常用配方**:顶栏+内容(首页)、左栏+内容(管理页)、内容+右栏(文章)、三栏(工作台)、窄内容(设置/阅读),展示页用 `Segmented` 切换。
|
||||||
- **展示页**:`#layout`
|
- **展示页**:`#layout`
|
||||||
|
|
||||||
@@ -700,3 +702,4 @@
|
|||||||
| 2026-10-29 | 新增 `Textarea`(`rows` / `resize` / `invalid`),与 `Input` 共用边框与焦点样式。兼容性:纯新增 |
|
| 2026-10-29 | 新增 `Textarea`(`rows` / `resize` / `invalid`),与 `Input` 共用边框与焦点样式。兼容性:纯新增 |
|
||||||
| 2026-10-29 | 新增 `PasswordInput`(`icon` / `size` / `toggle`):锁图标 + 显隐切换 + `aria-pressed`,事件语义与 `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-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);
|
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 {
|
.layout-side {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react'
|
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 { Demo } from '../components/Demo'
|
||||||
import { PropsTable } from '../components/PropsTable'
|
import { PropsTable } from '../components/PropsTable'
|
||||||
import { Section } from '../components/Section'
|
import { Section } from '../components/Section'
|
||||||
@@ -214,9 +214,50 @@ export function PageLayout() {
|
|||||||
</div>
|
</div>
|
||||||
</Demo>
|
</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
|
<PropsTable
|
||||||
rows={[
|
rows={[
|
||||||
{ name: 'header', type: 'ReactNode', desc: '页头,默认吸顶' },
|
{ name: 'header', type: 'ReactNode', desc: '页头,默认吸顶' },
|
||||||
|
{ name: 'toolbar', type: 'ReactNode', desc: '页头与页身之间的工具条(搜索 / 筛选 / 操作带),宽度随容器' },
|
||||||
{ name: 'sider', type: 'ReactNode', desc: '左侧栏,固定宽度' },
|
{ name: 'sider', type: 'ReactNode', desc: '左侧栏,固定宽度' },
|
||||||
{ name: 'aside', type: 'ReactNode', desc: '右侧栏,固定宽度(目录 / 聚合 / 广告位)' },
|
{ name: 'aside', type: 'ReactNode', desc: '右侧栏,固定宽度(目录 / 聚合 / 广告位)' },
|
||||||
{ name: 'footer', type: 'ReactNode', desc: '页脚' },
|
{ name: 'footer', type: 'ReactNode', desc: '页脚' },
|
||||||
|
|||||||
@@ -82,6 +82,13 @@
|
|||||||
border-bottom: 1px solid var(--su-border);
|
border-bottom: 1px solid var(--su-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 页头与页身之间的工具条:宽度随容器(contained 时与两栏同宽),
|
||||||
|
在正常流里占位,不参与 body 的内部滚动(screen 模式下就是「固定带」)。 */
|
||||||
|
.toolbar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ import styles from './Layout.module.css'
|
|||||||
export type LayoutProps = {
|
export type LayoutProps = {
|
||||||
/** 页头:整宽、默认吸顶 */
|
/** 页头:整宽、默认吸顶 */
|
||||||
header?: ReactNode
|
header?: ReactNode
|
||||||
|
/**
|
||||||
|
* 工具条:页头与页身之间的一条带(搜索、筛选、面包屑下方的操作)。
|
||||||
|
* 宽度跟随容器(`contained` 时与侧栏 + 内容同宽),不参与内容区滚动。
|
||||||
|
*/
|
||||||
|
toolbar?: ReactNode
|
||||||
/** 左侧栏:固定宽度、与内容并排 */
|
/** 左侧栏:固定宽度、与内容并排 */
|
||||||
sider?: ReactNode
|
sider?: ReactNode
|
||||||
/** 右侧栏:固定宽度、与内容并排(资料聚合、目录、广告位) */
|
/** 右侧栏:固定宽度、与内容并排(资料聚合、目录、广告位) */
|
||||||
@@ -67,6 +72,7 @@ export type LayoutProps = {
|
|||||||
|
|
||||||
export function Layout({
|
export function Layout({
|
||||||
header,
|
header,
|
||||||
|
toolbar,
|
||||||
sider,
|
sider,
|
||||||
aside,
|
aside,
|
||||||
footer,
|
footer,
|
||||||
@@ -124,6 +130,8 @@ export function Layout({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={contained ? styles.container : undefined} style={containerStyle}>
|
<div className={contained ? styles.container : undefined} style={containerStyle}>
|
||||||
|
{toolbar !== undefined && <div className={styles.toolbar}>{toolbar}</div>}
|
||||||
|
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
{sider !== undefined && (
|
{sider !== undefined && (
|
||||||
<aside
|
<aside
|
||||||
|
|||||||
Reference in New Issue
Block a user