第七章(选修) - Ant Design 企业级应用UI组件库全篇

第七章 第一节:Ant Design 通用基础篇 —— 吃透核心组件原理,掌握 “查文档 + 用组件” 的底层逻辑

7.1. 导论:为何 Ant Design 是企业级应用的事实标准?

在启动任何一个中大型项目时,技术选型的第一步往往就是确定 UI 解决方案。这个决策直接关系到项目的开发效率、设计一致性、长期可维护性以及最终的用户体验。2025 年的 React 生态中,UI 库百花齐放,从追求极致灵活的无头组件到开箱即用的全功能套件,不一而足。

然而,在 B 端产品、后台管理系统、SaaS 应用 等复杂的企业级场景中,Ant Design 长期以来始终是众多团队的首选。本节将深入探讨这一现象背后的原因,首先通过一个清晰的横向对比来定位 Ant Design,然后剖析其内在的核心价值。

7.1.1. 四大主流 UI 库横向对比

为了精确理解 Ant Design 的定位,我们选取了 2025 年最具代表性的四种 UI 解决方案进行多维度对比。这四者分别代表了四种截然不同的设计哲学与开发模式。

对比维度Ant DesignMaterial-UI (MUI)Radix UIShadcn/ui
核心哲学(推荐) 开箱即用的企业级设计体系,提供丰富、预设样式的组件。遵循 Google Material Design 规范的全功能组件库。(无头 UI) 只提供组件的行为、交互和可访问性,不提供任何样式(组件即代码) 不是库,而是通过 CLI 将组件源码复制到项目中的集合。
样式方案Less + CSS-in-JS。内置默认主题,通过 Design Token 定制。默认使用 Emotion (CSS-in-JS),也支持 Styled Components 或纯 CSS。完全由开发者决定。通常与 Tailwind CSS、CSS Modules 等结合使用。Tailwind CSS。样式直接在组件源码中通过原子类实现。
可定制性中等。非常适合在 Ant Design 体系内进行深度定制,但若要完全颠覆其设计风格,则成本较高。中高。提供了强大的 sx 属性和主题系统,比 AntD 更灵活,但仍有其设计范式。极高。由于没有预设样式,开发者拥有 100% 的视觉控制权。极高。开发者直接拥有并可以修改组件的每一行源代码。
上手速度非常快。组件功能强大且文档完善,可快速搭建功能复杂的页面。非常快。与 AntD 类似,能够快速实现具备 Material Design 风格的界面。。需要开发者自行编写所有组件的 CSS,前期投入较大。中等。需要熟悉 React、Tailwind CSS 和 Radix 的基本概念。
包体积影响。必须配置按需加载,否则会引入整个组件库的样式和逻辑。。同样需要优化,但其模块化设计稍好,Tree-shaking 效果更佳。。只包含逻辑部分,体积非常小。极小零运行时依赖,最终打包产物中只包含你实际使用的代码。
最佳应用场景复杂后台管理、数据驱动型应用、中后台解决方案、CRM、ERP 等。企业级应用、遵循 Material Design 的项目、内部工具。需要高度品牌化、设计独特的网站;构建自己的设计系统。追求极致性能、高度定制化、希望完全掌控代码的各类项目。

通过上表,我们可以清晰地看到,每种方案都有其明确的适用场景。当项目的核心诉求是 快速开发、功能复杂、设计统一、长期维护 的企业级应用时,Ant Design 的优势便凸显出来。

7.1.2. 深度剖析:Ant Design 的核心价值与设计哲学

Ant Design 的成功远不止“提供了一堆好用的组件”这么简单。它的核心价值在于其背后沉淀多年的企业级应用设计思想和工程化实践。

  • 沉淀于真实业务的企业级设计体系
    Ant Design 不仅仅是一个组件库,它是一套完整、成熟、自洽的 设计语言和规范。它提炼了 B 端产品设计中的大量通用模式,从布局、色彩、字体,到交互反馈,都经过了深思熟虑。对于开发者而言,这意味着无需成为设计专家,也能构建出专业、一致且用户体验良好的界面,极大地降低了大型团队的沟通和协作成本。

  • 功能强大且高度抽象的“数据驱动”组件
    这是 Ant Design 与其他库拉开差距的关键。它的核心组件,特别是 `Form` 与 `Table`,是为处理复杂业务数据而生的。

    • Form:内置了强大的数据收集、校验和状态管理能力。
    • Table:提供了数据展示、排序、筛选、分页、自定义渲染等企业级表格所需的一切。这些组件极大地解放了生产力,让开发者能聚焦于业务逻辑,而非重复的 UI 实现。我们将在后续章节中深入这些组件。
  • 以稳定性与可预测性为优先
    企业级项目追求的是稳定和长期可维护。Ant Design 由蚂蚁集团的专业团队维护,拥有可预测的版本发布周期、严格的质量控制和全面的单元测试。它从诞生之初就 全面拥抱 TypeScript,为大型应用的类型安全提供了坚实保障。

  • 围绕“中后台”场景构建的完整生态系统
    Ant Design 的视野超越了基础组件。

    • Ant Design Pro: 提供了一套开箱即用的中后台前端解决方案,包含了登录、权限、布局等通用模板。
    • ProComponents: 在 antd 的基础上进一步封装,提供了更高级的 ProTable, ProForm 等组件,开发效率更高。
    • AntV: 强大的数据可视化解决方案,与 Ant Design 无缝集成。这个生态使得 Ant Design 成为了一个能够支撑复杂应用从 0 到 1 快速落地的“平台级”解决方案。

7.2. 基础篇:项目集成与全局配置

重要更新: Ant Design v5+ 与现代化构建工具(如 Vite, Webpack v5+)的结合,已经改变了其集成方式。本节内容完全基于 2025 年最新官方文档,特别是关于按需加载和 React 19 兼容性的部分。请忘记所有关于 babel-plugin-import 的旧有配置,那已成为历史。

本节将引导您完成将 Ant Design 正确集成到现代化 React 项目中的每一步。我们将严格遵循官方推荐的最佳实践,确保您的项目配置健壮、高效且面向未来。

7.2.1. 项目初始化与基础引入

官方指南: 首先,您需要一个 React 开发环境。官方推荐使用 Vite 来创建项目,它提供了极速的开发体验。

  1. 创建 Vite + React 项目
    如果您尚未创建项目,请使用以下命令初始化。官方文档提及,您可以使用 npm, yarn, pnpmbun。我们将统一使用 pnpm

    1
    2
    3
    4
    5
    6
    # 创建一个名为 antd-demo 的项目
    pnpm create vite antd-demo --template react-ts

    # 进入项目目录并安装依赖
    cd antd-demo
    pnpm install
  2. 安装并引入 Ant Design
    在项目根目录下,安装 antd 依赖。

    1
    pnpm add antd

    接下来,修改 src/App.tsx,引入并使用一个 antd 组件,例如 Button

    文件路径: src/App.tsx

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import { Button } from 'antd';

    const App = () => (
    <div style={{ padding: '50px' }}>
    <Button type="primary">Hello, Ant Design!</Button>
    </div>
    );

    export default App;
  3. 启动项目
    执行 pnpm run dev,此时浏览器会自动打开 http://localhost:5173/。您应该能看到页面上出现了一个蓝色的 Ant Design 按钮。
    核心变化: 请注意,我们没有在任何地方手动引入 CSS 文件。这就是现代化构建工具的优势,我们将在下一节详细解释。


7.2.2. 理解按需加载 (Tree Shaking) 的现代化演进

官方指南: “antd 默认支持基于 ES modules 的 tree shaking,直接引入 import { Button } from 'antd'; 就会有按需加载的效果。”

痛点背景: 在旧的 Ant Design 版本或构建工具中,为了避免加载整个组件库的样式和 JS,我们需要配置 babel-plugin-import。这个配置过程繁琐且容易出错。

解决方案: 现在不再需要任何额外配置

Vite、Webpack 5+ 等现代构建工具原生支持基于 ES Modules (ESM) 的 Tree Shaking。Ant Design v5 的产物全面拥抱 ESM。当您写下 import { Button } from 'antd'; 时,构建工具足够智能,能够分析出您只依赖 Button 组件,并在最终打包时,只会将 Button 相关的 JavaScript 和样式包含进来,其他未使用的组件将被自动“摇掉”(剔除)。
请彻底忘记 babel-plugin-import
如果您在其他旧教程或文章中看到任何关于为 antd 配置 babel-plugin-import 的内容,请注意:该方法已过时。在 Vite 或现代 Webpack 项目中继续使用它不仅毫无必要,反而可能引发未知问题。


7.2.3. 兼容 React 19:为未来做准备

官方指南: React 19 调整了 react-dom 的导出方式,导致 antd v5 的部分依赖 ReactDOM.render 的功能(如波纹特效、Modal.confirm() 等静态方法)无法正常工作。为此,官方提供了兼容方案。

解决方案: 在项目入口处引入官方提供的兼容包。这是最优先推荐的方式。
核心步骤: 安装并引入一个专门的补丁包。

  1. 安装兼容包

    1
    pnpm add @ant-design/v5-patch-for-react-19
  2. 在应用入口处引入
    在您的项目主入口文件(通常是 src/main.tsx)的 最顶部,引入该包。

    文件路径: src/main.tsx

    // [!code ++]
    1
    2
    3
    4
    5
    import '@ant-design/v5-patch-for-react-19';
    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import App from './App';
    // ... a rest of your code

    这个包会“劫持”并修正 antd 内部对旧 ReactDOM.render 的调用,使其与 React 19 的新机制兼容。这是最简单、最无侵入性的做法。


7.2.4. 全局化配置:统一应用风格与国际化

在上一节中,我们已经成功将 Ant Design 集成到项目中。然而,一个真实的企业级应用需要的远不止于此,它需要统一的品牌风格、一致的组件尺寸和多语言支持。这一切都可以通过 Ant Design 的全局配置组件 ConfigProvider 来实现。

核心概念: `ConfigProvider` 是一个提供全局化配置能力的 React 组件。通过它包裹整个应用,我们可以轻松地将配置下发给其包裹的所有 Ant Design 组件。

在项目中最外层使用 ConfigProvider 是最佳实践。

文件路径: src/App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { ConfigProvider, App as AntdApp, Button } from 'antd';
import zhCN from 'antd/locale/zh_CN'; // 引入中文语言包

const App = () => (
// 使用 ConfigProvider 包裹整个应用
<ConfigProvider
locale={zhCN} // 1. 全局配置国际化为中文
theme={{
token: {
colorPrimary: '#00b96b', // 2. 全局主色
},
}}
componentSize="middle" // 3. 全局组件尺寸
>
{/* 使用 antd 内置的 App 组件,
用于解决 message, notification, Modal.confirm 等静态方法无法消费 context 的问题
*/}
<AntdApp>
<MyApp />
</AntdApp>
</ConfigProvider>
);

// 你的业务应用
const MyApp = () => {
const { message } = AntdApp.useApp(); // 从 AntdApp 中获取 context-aware 的静态方法

const showMessage = () => {
message.success('这是一条成功消息!');
};

return <Button type="primary" onClick={showMessage}>显示消息</Button>;
};

export default App;

静态方法的 Context 消费问题: 直接调用 message.success()Modal.confirm() 等静态方法时,它们是通过 ReactDOM.render 动态创建的独立 React 应用,无法继承 ConfigProvider 提供的上下文(如自定义主题)。
解决方案 (2025 年推荐): 使用 Ant Design v5 提供的 <App /> 包裹组件。它会处理好 contextHolder 的植入,然后通过 App.useApp() hook 获取能正确消费上下文的 message, notification, modal 实例。


7.3. 进阶篇:主题定制释放品牌潜力

当基础配置无法满足您对产品视觉和品牌形象的独特要求时,Ant Design 5.0 强大的主题定制能力便派上了用场。得益于 CSS-in-JS 技术,v5 的主题系统实现了前所未有的灵活性,支持动态切换、多主题并存,以及精细到组件级别的样式微调。

7.3.1. Design Token:理解 Ant Design 的“设计基因”

Ant Design 将所有影响主题的最小设计元素称为 Design Token。它建立了一套三层派生结构,让主题定制既简单又强大。

  • Seed Token (种子变量): 设计的起源,例如 colorPrimary (主色)、borderRadius (圆角)。修改它们会引发一系列相关颜色的自动计算和应用,影响范围最广。
  • Map Token (梯度变量): 由 Seed Token 派生而来,形成一套有规律的梯度色板或尺寸序列,如 colorPrimaryBg (主色的背景色)。
  • Alias Token (别名变量): 用于控制特定场景下组件样式的 Token,通常是 Map Token 的别名,如 colorLink (链接颜色)。

在大多数场景下,我们只需要修改 Seed Token 就能满足绝大部分定制需求。

7.3.2. 全局换肤:修改 Token 与应用预设算法

定制主题最直接的方式,就是在 ConfigProvidertheme 属性中进行配置。

1. 直接修改 Token

通过 theme.token 属性,我们可以直接覆盖 Design Token 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Button, ConfigProvider, Space } from 'antd';
import React from 'react';

const App: React.FC = () => (
<ConfigProvider
theme={{
token: {
// 1. 修改 Seed Token影响范围大
colorPrimary: '#00b96b',
borderRadius: 2,

// 2. 覆盖派生的 Map Token影响范围小
colorBgContainer: '#f6ffed',
},
}}
>
<Space>
<Button type="primary">Primary</Button>
<Button>Default</Button>
</Space>
</ConfigProvider>
);

export default App;

2. 应用预设算法

Ant Design 内置了三种预设算法,可以通过 theme.algorithm 属性快速切换应用的整体风格。

  • theme.defaultAlgorithm: 默认算法
  • theme.darkAlgorithm: (常用) 暗色算法,一键切换到暗黑模式。
  • theme.compactAlgorithm: 紧凑算法,减小组件的内外边距,使页面布局更紧凑。

算法支持组合使用,例如,我们可以创建一个“暗黑紧凑”风格的主题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from 'react';
import { Button, ConfigProvider, Input, Space, theme } from 'antd';

const App: React.FC = () => (
<ConfigProvider
theme={{
// 组合使用暗色算法与紧凑算法
algorithm: [theme.darkAlgorithm, theme.compactAlgorithm],
token: {
colorPrimary: '#722ed1'
}
}}
>
<div style={{ background: '#000', padding: '20px', borderRadius: '4px' }}>
<Space>
<Input placeholder="紧凑的输入框" />
<Button type="primary">紧凑的按钮</Button>
</Space>
</div>
</ConfigProvider>
);

export default App;

7.3.3. 精准微调:深入组件级定制 (Component Token)

有时,我们希望对特定组件进行样式微调,而不是全局修改。theme.components 属性允许我们为单个组件定义独立的 Component Token。

痛点背景: 假设我们希望全局主色为蓝色,但唯独 Input 组件在激活时的高亮色(colorPrimary)是紫色,以作区分。

解决方案: 在 theme.components 中为 Input 单独配置 colorPrimary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React from 'react';
import { ConfigProvider, Button, Input, Space } from 'antd';

const App: React.FC = () => (
<ConfigProvider
theme={{
// 全局主色为蓝色
token: { colorPrimary: '#1677ff' },
components: {
// 针对 Input 组件的特殊配置
Input: {
colorPrimary: '#eb2f96', // Input 激活时的主色
algorithm: true, // 启用算法使其派生色 hover, active也随之改变
},
},
}}
>
<Space>
<Input placeholder="Focus me to see the magic" />
<Button type="primary">Primary Button</Button>
</Space>
</ConfigProvider>
);

export default App;

algorithm: true 的作用: 默认情况下,组件级的 Token 仅仅是静态覆盖。设置为 true 后,组件会基于这个新的 Token 值(如 #eb2f96)重新计算相关的派生色(如 colorHover, colorActive),确保组件在不同交互状态下的视觉一致性。


7.3.4. 动态与嵌套:让主题“活”起来

1. 动态切换主题

ConfigProvidertheme 属性是响应式的。这意味着我们可以通过 React 的 state 动态改变主题,Ant Design 会自动、流畅地更新所有组件的样式,无需刷新页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { Button, ConfigProvider, Space, ColorPicker, Divider } from 'antd';
import type { Color } from 'antd/es/color-picker';
import React, { useState } from 'react';

const App: React.FC = () => {
const [primaryColor, setPrimaryColor] = useState('#1677ff');

const onColorChange = (color: Color) => {
setPrimaryColor(color.toHexString());
};

return (
<>
<ColorPicker showText value={primaryColor} onChangeComplete={onColorChange} />
<Divider />
<ConfigProvider
theme={{
token: {
colorPrimary: primaryColor,
},
}}
>
<Space>
<Button type="primary">Primary</Button>
<Button>Default</Button>
</Space>
</ConfigProvider>
</>
);
}

export default App;

img

2. 局部嵌套主题

通过嵌套 ConfigProvider,我们可以为页面的特定区域应用不同的主题,子主题会继承父主题中未被覆盖的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';
import { Button, ConfigProvider, Space } from 'antd';

const App: React.FC = () => (
<ConfigProvider theme={{ token: { colorPrimary: '#1677ff' } }}>
<Space>
<Button type="primary">全局主题 (蓝)</Button>
<ConfigProvider theme={{ token: { colorPrimary: '#00b96b' } }}>
<Button type="primary">局部主题 (绿)</Button>
</ConfigProvider>
</Space>
</ConfigProvider>
);

export default App;

7.3.5. 消费 Token:在业务代码中访问主题变量

有时我们需要在自定义组件或业务样式中,使用与 Ant Design 当前主题一致的颜色、边距等变量,以保持视觉统一。Ant Design 提供了两种方式来消费 Token。

1. 在 React 组件中:useToken Hook

在函数组件内部,可以通过 theme.useToken() hook 来获取当前上下文中的 Design Token。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React from 'react';
import { theme } from 'antd';

const { useToken } = theme;

const MyCustomCard: React.FC = () => {
const { token } = useToken(); // 获取 Token 对象

return (
<div
style={{
backgroundColor: token.colorBgContainer, // 使用容器背景色
padding: token.paddingLG, // 使用大号内边距
borderRadius: token.borderRadiusLG, // 使用大号圆角
boxShadow: token.boxShadow, // 使用标准阴影
border: `1px solid ${token.colorBorder}`, // 使用边框颜色
}}
>
这是一个自定义卡片,其样式完全由 Ant Design 的 Design Token 驱动。
</div>
);
};

export default MyCustomCard;

7.4. 性能优化:启用 CSS 变量模式

7.3 节中,我们掌握了 Ant Design 强大的主题定制能力。然而,当应用需要频繁切换主题或支持多套主题时,传统的 CSS-in-JS 方案可能会遇到性能瓶颈。为此,Ant Design v5.12.0 之后,重新引入并升级了 CSS 变量模式,旨在从根本上解决这些问题。

痛点背景:

  • 样式体积冗余: 在未使用 CSS 变量时,每切换一次主题(例如改变主色),都需要重新计算并生成一套全新的组件样式。如果应用内有多个主题,会产生大量重复的 CSS 规则,仅有颜色等少数值不同,导致最终的样式文件体积膨胀。
  • 主题切换性能: 动态切换主题时,需要销毁旧样式、注入新样式,这个过程涉及大量的 DOM 操作和浏览器重绘,可能导致界面卡顿,尤其是在复杂页面上。

解决方案: 启用 CSS 变量模式。其核心思想是将样式规则中的动态部分(如颜色、边距、圆角等 Design Token)替换为 CSS 变量,而静态的结构化样式则保持不变。这样,切换主题时不再需要重新生成全部样式,只需更新根元素上 CSS 变量的值即可,浏览器会以极高的性能完成重绘。

7.4.1. 快速上手:一行配置开启 CSS 变量

开启 CSS 变量模式非常简单,只需在顶层的 ConfigProvider 中添加 cssVar 配置即可。

文件路径: src/App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React from 'react';
import { ConfigProvider, Button, App as AntdApp, theme } from 'antd';
import zhCN from 'antd/locale/zh_CN';

// 假设这是我们的应用主组件
const MyApp: React.FC = () => (
<div style={{ padding: '20px' }}>
<Button type="primary">一个启用了 CSS 变量的按钮</Button>
</div>
);

const App: React.FC = () => {
return (
<ConfigProvider
locale={zhCN}
theme={{
token: {
colorPrimary: '#00b96b',
},
// 👇 核心配置开启 CSS 变量
cssVar: true,
}}
>
<AntdApp>
<MyApp />
</AntdApp>
</ConfigProvider>
);
};

export default App;

开启后,通过浏览器开发者工具审查元素,您会发现组件样式中的具体色值(如 #00b96b)被替换为了 CSS 变量(如 var(--ant-color-primary))。
React 版本兼容性: CSS 变量模式需要为每个主题实例生成一个唯一的 key 以隔离样式。

  • React 18+ 中,Ant Design 会自动使用 useId hook 生成 key,您无需任何额外操作。
  • React 17 或 16 中,useId 不可用,您 必须 手动提供一个唯一的 key,否则可能导致多主题并存时样式错乱。
1
2
3
4
// 仅在 React 17 或 16 中需要手动提供 key
<ConfigProvider theme={{ cssVar: { key: 'my-app-theme' } }}>
<App />
</ConfigProvider>

7.4.2. 进阶优化:关闭 Hash 进一步减小体积

Ant Design 5.0 引入了 hash 特性,为每个主题配置生成一个唯一的哈希值并附加到组件的 className 上,用于彻底隔离不同主题的样式。

然而,当我们启用了 CSS 变量后,组件的结构化样式变得固定,只有 CSS 变量在变化。这意味着,如果您整个应用中只使用一个版本的 antd,那么 hash 带来的样式隔离就不再是必需的。此时,我们可以选择关闭它来进一步优化打包体积。

文件路径: src/App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React from 'react';
import { ConfigProvider, Button, App as AntdApp, theme } from 'antd';
import zhCN from 'antd/locale/zh_CN';

const MyApp: React.FC = () => (
<div style={{ padding: '20px' }}>
<Button type="primary">一个无 Hash 的按钮</Button>
</div>
);

const App: React.FC = () => {
return (
<ConfigProvider
locale={zhCN}
theme={{
token: {
colorPrimary: '#00b96b',
},
// 开启 CSS 变量
cssVar: true,
// 👇 进阶优化关闭 hash
hashed: false,
}}
>
<AntdApp>
<MyApp />
</AntdApp>
</ConfigProvider>
);
};

export default App;

最佳实践: 对于依赖 Ant Design 主题能力的新项目,强烈推荐组合使用 cssVar: truehashed: false,并配合 服务端渲染(SSR)时的样式抽取,以实现最佳的加载性能和主题切换体验。


7.5. 实战演练:基础与布局组件通用 Demo

在前面的章节中,我们已经为项目配置好了强大的主题和 CSS 变量模式。现在,万事俱备,是时候真正“上手”使用 Ant Design 的组件来构建界面了。

从本节开始,我们将通过一系列目标明确的小型 Demo,逐一体验 Ant Design 中最高频的基础和布局组件。

核心学习理念: Ant Design 提供了数百个组件和数千个 API 属性。我们的目标 不是 死记硬背它们,而是 理解 每个组件的 核心用途和设计哲学。请记住,官方文档是您最好的朋友,本教程的目的是教会您如何高效地查阅和使用它,而不是替代它。


7.5.1. Demo 1:构建内容基础 - TypographyIcon

任何页面的起点都是内容的呈现。在 Ant Design 中,Typography 承担了所有文本内容的语义化和交互增强,而 Icon 则为界面增添了直观、生动的视觉引导。让我们深入探索这两个基础组件的强大能力。

第一部分:精通 Typography - 不只是静态文本

Typography 的核心价值远不止于展示格式化的标题和段落,它内置了企业级应用中常见的三大交互能力:可编辑、可复制、可省略

目标:创建一个包含标题、段落和链接的文章标题区域,并为其增加交互功能。

image-20250926182215871

文件路径: src/components/demos/ArticleHeader.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import React from "react";
import { Typography, Divider } from "antd";
import { BookOutlined } from "@ant-design/icons";

const { Title, Paragraph, Text, Link } = Typography;

const ArticleHeader: React.FC = () => {
return (
<Typography>
<Title level={2}>
<BookOutlined /> Ant Design 实战入门
</Title>

{/* 1. 可复制的文本:只需添加 `copyable` 属性 */}
<Paragraph>
欢迎来到 Ant Design 的世界。项目ID:{" "}
<Text copyable>20250926-AD-PRO</Text>
</Paragraph>

{/* 2. 可编辑的文本:添加 `editable` 属性 */}
<Paragraph editable={{ tooltip: "点击编辑标题" }}>
这是一个可编辑的副标题
</Paragraph>

{/* 3. 自动省略的多行文本:`ellipsis` 是一个功能强大的属性 */}
<Paragraph
ellipsis={{
rows: 2,
expandable: true,
symbol: "展开阅读",
}}
>
Ant Design
是一个服务于企业级产品的设计体系,基于『确定』和『自然』的设计价值观,
通过模块化的解决方案,降低冗余的生产成本,让设计者专注于更好的用户体验。
它提供了丰富的基础组件和业务组件,能够帮助开发者快速构建出高质量的后台管理系统。
Ant Design 的设计理念源于大量的企业级产品实践,它不仅仅是一套 UI 组件库,
更是一套完整的设计语言和前端解决方案。从最初的设计稿到最终的产品交付,
Ant Design 都能提供完整的工具链支持。它包含了设计原则、视觉规范、交互模式、
以及大量经过验证的最佳实践。无论是初学者还是经验丰富的开发者,
都能通过 Ant Design 快速上手并构建出专业级的用户界面。
其组件库涵盖了表单、表格、导航、反馈、数据展示等各个方面,
每个组件都经过精心设计和充分测试,确保在不同场景下的稳定性和易用性。

</Paragraph>

<Divider />

<Link href="https://ant.design/components/overview-cn" target="_blank">
访问官方文档
</Link>
</Typography>
);
};

export default ArticleHeader;

本部分核心知识点:

  • 交互增强: Typography 的核心优势在于其内置的交互属性,极大简化了开发:
    • copyable: 一键开启文本复制功能,还可以通过对象形式进行深度定制(如修改图标、提示文案)。
    • editable: 允许用户直接在页面上编辑文本,同样支持丰富的配置项。
    • ellipsis: 强大的文本省略功能,支持多行省略、自定义后缀、以及可展开/收起的交互。
  • 语义化: 始终使用 Title, Paragraph 等语义化组件,而非简单的 <div><span>,这有利于 SEO 和可访问性。

第二部分:探索 Icon - 从使用到定制

我们刚才在标题中使用了 <BookOutlined />,这是 @ant-design/icons 提供的标准图标。但一个成熟的项目,往往需要更丰富的图标表达方式。

1. 图标的三种主题
Ant Design 的图标内置了三种主题风格,通过组件后缀区分:

  • *Outlined: 线性描边风格 (默认)
  • *Filled: 实心填充风格
  • *TwoTone: 双色风格,可以通过 twoToneColor 属性定制主色
1
2
3
4
5
6
7
import { SmileOutlined, SmileFilled, SmileTwoTone } from '@ant-design/icons';

<Space>
<SmileOutlined style={{ fontSize: '24px' }} />
<SmileFilled style={{ fontSize: '24px', color: '#00b96b' }} />
<SmileTwoTone style={{ fontSize: '24px' }} twoToneColor="#eb2f96" />
</Space>

2. 动画与旋转
通过 spinrotate 属性,可以轻松实现动态效果。

1
2
3
4
5
6
7
8
9
import { LoadingOutlined, SyncOutlined } from '@ant-design/icons';

<Space>
{/* `spin` 属性使其持续旋转,常用于加载状态 */}
<LoadingOutlined style={{ fontSize: '24px' }} spin />

{/* `rotate` 属性使其静态旋转指定的角度 */}
<SyncOutlined style={{ fontSize: '24px' }} rotate={90} />
</Space>

🤔 思考与探索:如何集成项目专属的自定义图标?

在我们的 ArticleHeader Demo 中,使用了官方提供的 BookOutlined 图标。这是一个很好的起点,但在真实的企业级项目中,我们通常会面临以下挑战:

  1. 品牌一致性:UI/UX 设计师会提供一套符合公司品牌规范的专属 SVG 图标。
  2. 图标管理:项目可能会使用 iconfont.cn 这类平台来统一管理和维护一套图标库。

问题:我们如何摆脱 @ant-design/icons 的限制,将这些自定义图标无缝集成到 Ant Design 的体系中呢?

Ant Design 提供了两种主流的、强大的自定义图标集成方案。

方案一:使用 iconfont.cn (推荐用于团队协作)

这是最常用、最便于维护的方式。Ant Design 提供了一个工厂函数 createFromIconfontCN,可以轻松创建一个能消费 iconfont-阿里巴巴矢量图标库 项目图标的自定义 Icon 组件。

  1. iconfont.cn 创建项目:将所有需要的图标(无论是 SVG 还是其他格式)上传到 iconfont.cn 的一个项目中,并获取其在线生成的 JavaScript URL。

  2. 创建自定义 Icon 组件
    文件路径: src/components/MyIcon.tsx

    1
    2
    3
    4
    5
    6
    7
    8
    import { createFromIconfontCN } from '@ant-design/icons';

    const MyIcon = createFromIconfontCN({
    // 将 'scriptUrl' 替换为您在 iconfont.cn 上生成的真实 URL
    scriptUrl: '//at.alicdn.com/t/c/font_5031610_qqhzoiim5q.js',
    });

    export default MyIcon;
  3. 在业务组件中使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
        import { Space } from "antd";
    import MyIcon from "./components/MyIcon";
    import Title from "antd/es/typography/Title";

    const App: React.FC = () => {
    return (
    <>
    <Space>
    <Title level={2}>
    {/* `type` 属性的值对应您在 iconfont.cn 中为图标设置的 Font Class 名称 */}
    <MyIcon type="icon-dianzan" /> Ant Design 实战入门
    </Title>
    </Space>
    </>
    );
    };

    export default App;

方案二:直接使用 SVG 文件 (推荐用于少量、静态图标)

如果您的项目配置了 SVGR (Vite 和 Create React App 的新版本通常已内置或易于配置),您可以直接将 SVG 文件作为 React 组件导入,并配合 antd 的 <Icon /> 组件使用。

  1. 将 SVG 文件放入项目 (例如 src/assets/icons/custom-book.svg)

  2. 在业务组件中使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import Icon from '@ant-design/icons';
    // 导入 SVG 文件作为 React 组件
    import { ReactComponent as CustomBookIcon } from '../assets/icons/custom-book.svg';
    // Vite 用户请使用: import CustomBookIcon from '../assets/icons/custom-book.svg?react';

    // ...
    <Title level={2}>
    {/* 将导入的 SVG 组件传递给 antd Icon 的 `component` 属性 */}
    <Icon component={CustomBookIcon} /> Ant Design 实战入门
    </Title>
    // ...

这两种方法都极大地扩展了 Ant Design 的图标能力,使其能够适应任何项目的视觉需求。


7.5.2. 搭建页面骨架:精通 Ant Design 布局组件

我们已经学会了如何创造页面的“血肉”(通用组件),现在,是时候来搭建它的“骨架”了。Ant Design 提供了一套从微观到宏观、功能强大的布局组件系统,足以应对任何复杂的页面结构需求。

核心理念: 布局组件的选择取决于“尺度”。SpaceFlex 用于 组件之间 的微观布局;Grid 用于 页面内容区域 的宏观、响应式布局;而 Layout 则用于定义整个 应用级别 的框架结构。

第一部分:微观布局 - Space vs Flex

SpaceFlex 都用于处理一组元素之间的对齐和间距,但它们的适用场景和能力有所不同。

  • Space: 更轻量,专门用于为 一组并列的行内元素 提供等宽间距,是 ButtonTag 等组件的最佳伴侣。
  • Flex: 功能更强大,是原生 CSS Flexbox 的封装,可以实现任意 一维方向(水平或垂直)的复杂对齐、排序和空间分配。

目标:创建一个更复杂的工具栏,左侧是一组常规操作按钮,右侧是一组危险操作按钮,两组按钮分别对齐。

文件路径: src/components/demos/AdvancedToolbar.tsx

image-20250926194826703

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React from 'react';
import { Button, Flex, Space, Tooltip } from 'antd';
import {
EditOutlined,
ShareAltOutlined,
DeleteOutlined,
SaveOutlined,
} from '@ant-design/icons';

const AdvancedToolbar: React.FC = () => {
return (
// 1. 使用 Flex 容器作为父布局,实现左右对齐
// `justify='space-between'` 将子元素推向两端
// `align='center'` 确保子元素在交叉轴(垂直方向)上居中
<Flex justify="space-between" align="center">
{/* 左侧按钮组:使用 Space 来保证内部按钮的间距 */}
<Space>
<Button type="primary" icon={<SaveOutlined />}>
保存
</Button>
<Button icon={<ShareAltOutlined />}>分享</Button>
</Space>

{/* 右侧按钮组 */}
<Space>
<Tooltip title="编辑文章">
<Button shape="circle" icon={<EditOutlined />} />
</Tooltip>
<Tooltip title="删除文章">
<Button danger shape="circle" icon={<DeleteOutlined />} />
</Tooltip>
</Space>
</Flex>
);
};

export default AdvancedToolbar;

本部分核心知识点:

  • 组合使用: Flex 作为外层容器负责宏观对齐(如左右分离),Space 作为内层容器负责小组内元素的间距,这是一种非常常见的组合模式。
  • Flex 的核心属性: justify (主轴对齐) 和 align (交叉轴对齐) 是 Flex 组件的灵魂,熟练运用它们可以实现绝大多数一维布局需求。

第二部分:宏观布局 - Grid 栅格系统

当我们需要构建响应式页面,让布局在不同尺寸的设备(PC、平板、手机)上都能优雅地展示时,经典的 24 栏 Grid 栅格系统便登场了。

  • Row: 代表“行”,所有列必须放在 Row 内部。
  • Col: 代表“列”,通过 span 属性指定该列占据 24 份中的几份。

目标:创建一个响应式的数据卡片列表。在宽屏上每行显示 3 个,中等屏幕上每行 2 个,手机上每行 1 个。

img

文件路径: src/components/demos/ResponsiveCardGrid.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React from 'react';
import { Row, Col, Card } from 'antd';

const ResponsiveCardGrid: React.FC = () => {
const cardData = [
{ title: '数据分析', content: '深入分析用户行为数据。' },
{ title: '销售报告', content: '本季度销售额同比增长20%。' },
{ title: '库存监控', content: 'A-01 仓库库存告急。' },
];

return (
<div>
<h3 className="text-xl font-bold mb-4">关键指标</h3>
{/* `gutter` 属性用于设置 Col 之间的间距 */}
<Row gutter={[16, 16]}>
{cardData.map((card, index) => (
// 1. Col 的响应式属性是核心
// `xs`: extra small (<576px) -> 占据 24/24 = 100% 宽度
// `md`: medium (≥768px) -> 占据 24/12 = 50% 宽度
// `lg`: large (≥992px) -> 占据 24/8 = 33.3% 宽度
<Col key={index} xs={24} md={12} lg={8}>
<Card title={card.title} bordered={false}>
{card.content}
</Card>
</Col>
))}
</Row>
</div>
);
};

export default ResponsiveCardGrid;

第三部分:应用级框架 - Layout

对于后台管理系统这类典型的应用,页面结构通常是固定的“上/下”或“上/左/右”布局。Ant Design 为此提供了专用的 Layout 系列组件,让我们能快速搭建起专业的应用框架。

目标:搭建一个经典的后台管理系统布局,包含顶部导航、左侧菜单、主内容区和页脚。

image-20250926210205821

文件路径: src/components/demos/AdminLayout.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import React, { useState } from "react";
import { Layout, Menu, Breadcrumb, theme } from "antd";

import {
DesktopOutlined,
FileOutlined,
PieChartOutlined,
TeamOutlined,
UserOutlined,
} from "@ant-design/icons";

import type { MenuProps } from "antd";

// 从 Layout 中解构出所有子组件
const { Header, Content, Footer, Sider } = Layout;
// 定义 MenuItem 类型,这是 Ant Design 的 Menu 组件的类型定义
type MenuItem = Required<MenuProps>["items"][number];

// 在这个函数,我们可以获得一个 MenuItem 对象,他身上包含了
// 1. label: 菜单项的文本
// 2. key: 菜单项的唯一标识
// 3. icon: 菜单项的图标
// 4. children: 菜单项的子菜单
function getItem(
label: React.ReactNode,
key: React.Key,
icon?: React.ReactNode,
children?: MenuItem[]
): MenuItem {
return { key, icon, children, label } as MenuItem;
}

const items: MenuItem[] = [
getItem("数据看板", "1", <PieChartOutlined />),
getItem("订单管理", "2", <DesktopOutlined />),
getItem("用户中心", "sub1", <UserOutlined />, [
getItem("个人信息", "3"),
getItem("团队列表", "4"),
]),
getItem("系统设置", "sub2", <TeamOutlined />, [getItem("权限管理", "6")]),
getItem("文件", "9", <FileOutlined />),
];

const AdminLayout: React.FC = () => {
const [collapsed, setCollapsed] = useState(false);
const {
token: { colorBgContainer, borderRadiusLG },
} = theme.useToken();

return (
// Layout 组件可以嵌套,形成复杂布局
<Layout style={{ minHeight: "100vh" }}>
{/* Sider: 侧边栏,`collapsible` 属性使其可折叠 */}
<Sider
theme="dark"
collapsible
collapsed={collapsed}
onCollapse={(value) => setCollapsed(value)}
>
{/* 这里一般是Logo区域 */}
<div
className="demo-logo-vertical"
style={{
height: "32px",
margin: "16px",
background: "rgba(255, 255, 255, .2)",
}}
/>
<Menu
theme="dark"
defaultSelectedKeys={["1"]}
mode="inline"
items={items}
/>
</Sider>

<Layout>
{/* Header: 顶部导航栏 */}
<Header style={{ padding: 0, background: colorBgContainer }} />

{/* Content: 主内容区 */}
<Content style={{ margin: "0 16px" }}>
<Breadcrumb style={{ margin: "16px 0" }}>
<Breadcrumb.Item>用户中心</Breadcrumb.Item>
<Breadcrumb.Item>个人信息</Breadcrumb.Item>
</Breadcrumb>
<div
style={{
padding: 24,
minHeight: 360,
background: colorBgContainer,
borderRadius: borderRadiusLG,
}}
>
这里是主内容区域。我们可以在这里放置之前创建的 ResponsiveCardGrid
等组件。
</div>
</Content>

{/* Footer: 页脚 */}
<Footer style={{ textAlign: "center" }}>
Ant Design ©{new Date().getFullYear()} Created by Prorise
</Footer>
</Layout>
</Layout>
);
};

export default AdminLayout;

本部分核心知识点:

  • 组件化布局: Layout, Header, Sider, Content, Footer 是一套专用的布局组件,通过组合使用来定义应用框架。
  • Sider 的交互: collapsible 属性是 Sider 的精髓,它赋予了侧边栏可收起/展开的能力,极大地提升了空间利用率。

第七章 第二节:Ant Design 导航篇 —— 解析导航组件设计思路,掌握 Menu/Breadcrumb 实战应用技巧

7.6. 指引方向:实战导航(Navigation)组件

7.5 节中,我们搭建了专业的页面“骨架”。您可能还记得,在 AdminLayout 的侧边栏里,我们用 <Menu /> 组件构建了一个菜单,但对它的配置和用法并未深究。

一个优秀的应用,不仅要有稳固的骨架,更要有清晰的“路标”来指引用户。本节,我们将聚焦于 Ant Design 的核心导航组件,学会如何为用户提供从应用宏观跳转到页面微观定位的全方位指引。

本节目标:我们将模拟一个常见的后台管理页面——“文章列表页”,并在这个页面中,综合运用 Breadcrumb, Menu, DropdownAnchor 四个核心导航组件,构建一个功能完整、流线清晰的用户界面。

项目结构预览

1
2
3
4
5
6
# src/
├── components/
│ └── demos/
│ └── # ... 我们之前的 Demo ...
└── pages/
└── ArticleListPage.tsx # <-- 本节的核心,一个新的页面级组件

第一步:页面上下文 - Breadcrumb (面包屑)

面包屑是告知用户“我在哪里”的最直观方式,对于层级较深的应用至关重要。

目标:在我们的新页面顶部,添加一个面包屑导航,清晰地标示出当前页面所处的层级。

文件路径: src/pages/ArticleListPage.tsx (初始内容)

image-20250926210508767

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React from 'react';
import { Breadcrumb, Typography } from 'antd';
import { HomeOutlined, FileTextOutlined } from '@ant--design/icons';

const { Title } = Typography;

const ArticleListPage: React.FC = () => {
return (
<div>
{/* 1. Breadcrumb 用于展示页面层级 */}
{/* 现代 antd 推荐使用 `items` 属性来数据驱动地渲染面包屑 */}
<Breadcrumb
items={[
{
href: '/',
title: <HomeOutlined />, // 可以内嵌 Icon
},
{
title: '文章管理',
},
{
title: '文章列表',
},
]}
/>

<Title level={2} style={{ marginTop: '16px' }}>
文章列表
</Title>
</div>
);
};

export default ArticleListPage;

核心知识点:

  • items 属性: 这是 Breadcrumb 组件最推荐的使用方式。通过一个对象数组来定义导航路径,每个对象可以包含 title, href, onClick 等属性,甚至可以通过 menu 属性创建带下拉菜单的面包屑项。这种数据驱动的方式让面包屑的管理和动态生成变得非常简单。

第二步:页面内操作 - Dropdown (下拉菜单) 与 Menu (水平菜单)

一个页面通常包含多种操作。Dropdown 用于收纳一组折叠的操作命令,而水平模式的 Menu 则常用于页面内的分类切换。

目标:在页面标题下方,构建一个操作栏。左侧是“新建文章”等主要操作,其中一个操作通过 Dropdown 展示更多选项;右侧则是一个用于筛选文章状态的水平 Menu

文件路径: src/pages/ArticleListPage.tsx (添加操作栏)

image-20250926211232941

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import React from "react";
import {
Breadcrumb,
Typography,
Button,
Dropdown,
Menu,
Flex,
Divider,
} from "antd";
import type { MenuProps } from "antd";
import {
HomeOutlined,
PlusOutlined,
DownOutlined,
SettingOutlined,
CheckCircleOutlined,
SyncOutlined,
CloseCircleOutlined,
} from "@ant-design/icons";

const { Title } = Typography;

// Dropdown 的菜单项,其结构与 Menu 的 items 一致
const items: MenuProps["items"] = [
{ key: "1", label: "导出为 PDF" },
{ key: "2", label: "导出为 Excel" },
{ type: "divider" }, // 分割线
{ key: "3", label: "从模板导入", danger: true },
];

const MenuItems: MenuProps["items"] = [
{ key: "all", label: "全部文章" },
{ key: "published", label: "已发布", icon: <CheckCircleOutlined /> },
{ key: "draft", label: "草稿箱", icon: <SyncOutlined /> },
{
key: "trashed",
label: "回收站",
icon: <CloseCircleOutlined />,
danger: true,
},
];
const ArticleListPage: React.FC = () => {
return (
<div>
{/* 1. Breadcrumb 用于展示页面层级 */}
{/* 现代 antd 推荐使用 `items` 属性来数据驱动地渲染面包屑 */}
<Breadcrumb
items={[
{
href: "/",
title: <HomeOutlined />, // 可以内嵌 Icon
},
{
title: "文章管理",
},
{
title: "文章列表",
},
]}
/>

<Flex
justify="space-between"
align="center"
style={{ marginTop: "16px" }}
>
<Title level={2} style={{ margin: 0 }}>
文章列表
</Title>
<Flex gap="small">
<Button type="primary" icon={<PlusOutlined />}>
新建文章
</Button>
{/* Dropdown 用于收纳一组操作 */}
<Dropdown menu={{ items }}>
<Button>
更多操作 <DownOutlined />
</Button>
</Dropdown>
</Flex>
</Flex>

<Divider />
{/* 3. 水平 Menu 用于分类筛选 */}
<Menu mode="horizontal" items={MenuItems} defaultSelectedKeys={["all"]} />
</div>
);
};

export default ArticleListPage;

核心知识点:

  • Dropdown: 它的核心是由一个 触发元素(这里是 <Button>)和一个通过 menu 属性定义的 菜单列表 组成。menu 属性接收一个与 Menu 组件的 items 结构完全相同的对象,实现了组件之间的高度复用。
  • Menumode 属性: 通过将 mode 设置为 'horizontal'Menu 组件即可从垂直布局切换为水平布局,非常适合用作顶栏导航或页内标签式导航。

最后,我们将这个新建的页面组件放入我们的 AdminLayout 中。

文件路径: src/components/demos/AdminLayout.tsx (修改 Content 部分)

image-20250926211902440

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ... (AdminLayout 的其他代码)
import ArticleListPage from '../../pages/ArticleListPage'; // 导入新页面

// ...

const AdminLayout: React.FC = () => {
// ...
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider /* ... */ />
<Layout>
<Header /* ... */ />
{/* 👇 将 Content 内部替换为我们的新页面 */}
<Content style={{ margin: '0 16px', paddingTop: '16px' }}>
<ArticleListPage />
</Content>
<Footer /* ... */ />
</Layout>
</Layout>
);
}

export default AdminLayout;

通过构建一个真实的“文章管理”页面,我们一站式地掌握了 Ant Design 四大核心导航组件的实战用法,并理解了它们在应用中所扮演的不同角色:

  • Breadcrumb: 我在哪? —— 提供清晰的层级定位。
  • Menu: 我能去哪? —— 提供系统性的、可供选择的跳转路径(垂直或水平)。
  • Dropdown: 我能做什么? —— 收纳当前上下文中的一组操作命令。

7.7. 深入导航与全局操作:AnchorFloatButton

在本节中,我们将采用全新的“原子化 Demo”模式,为两个功能独特但非常实用的组件——Anchor (锚点) 和 FloatButton (悬浮按钮)——创建独立的、聚焦的实战示例。

7.7.1. Anchor:长页面的“任意门”

当一个页面承载了大量内容,需要用户频繁滚动时,一个清晰的页面内导航(锚点)就显得至关重要。

目标:创建一个独立的、可滚动的页面,并使用 Anchor 组件为其提供一个固定的、可交互的目录。

文件路径: src/components/demos/AnchorDemoPage.tsx

PixPin_2025-09-26_21-59-33

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import React from "react";
import { Row, Col, Anchor, Typography, Divider } from "antd";

const { Title, Paragraph } = Typography;
const AnchorDemoPage = () => {
// 模拟长篇内容
const renderContent = (title: string, count: number) => {
return (
<div>
<Title level={3} id={title.toLowerCase().replace(/\s/g, "-")}>
{title}
</Title>
{Array.from({ length: count }, (_, i) => (
<Paragraph key={i}>
这是关于“{title}”的第 {i + 1} 段详细内容。Lorem ipsum dolor sit
amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi
ista probare, quae sunt a te dicta? Refert tamen, quo modo.
</Paragraph>
))}
</div>
);
};

return (
<div>
<Row gutter={24}>
{/* 左侧:可滚动的内容区 */}
<Col span={18}>
<Typography>
{renderContent("基本介绍", 5)}
<Divider />
{renderContent("安装与配置", 8)}
<Divider />
{renderContent("API 详解", 10)}
<Divider />
{renderContent("常见问题 FAQ", 6)}
</Typography>
</Col>

{/* 右侧:锚点导航 */}
<Col span={6}>
{/* Anchor 默认是 affix (固定) 模式,会自动固定在屏幕上。
`targetOffset` 属性非常有用,可以设置一个偏移量,
避免跳转后的标题被顶部的固定导航栏遮挡。
*/}
<Anchor targetOffset={80}>
<Anchor.Link href="#基本介绍" title="基本介绍" />
<Anchor.Link href="#安装与配置" title="安装与配置" />
<Anchor.Link href="#api-详解" title="API 详解" />
<Anchor.Link href="#常见问题-faq" title="常见问题 FAQ" />
</Anchor>
</Col>
</Row>
</div>
);
};

export default AnchorDemoPage;

App.tsx 中独立使用此 Demo:

文件路径: src/App.tsx

1
2
3
4
5
6
7
8
9
10
11
import React from 'react';
import AnchorDemoPage from './components/demos/AnchorDemoPage';

const App: React.FC = () => {
return (
// 为了让 Anchor 的滚动效果生效,确保 App 或其父级没有设置 overflow: hidden
<AnchorDemoPage />
);
};

export default App;

本 Demo 核心知识点:

  • 关联 idhref: Anchor 的工作原理是通过 items 数组中每个对象的 href 属性(如 '#基本介绍')来寻找页面上具有相同 id 的元素(如 <Title id="基本介绍">),并滚动到该位置。
  • targetOffset: 这是一个在实战中几乎必配的属性。现代网页大多有固定的顶部导航栏,若无偏移,锚点跳转后,标题会被导航栏遮住。targetOffset 就是为了解决这个问题而存在的。
  • 数据驱动: 再次强调,使用 items 属性来定义导航结构,是 Ant Design 现代、推荐的做法,它让动态生成 Anchor 变得轻而易举。

7.7.2. FloatButton:全局操作的“快捷方式”

FloatButton 提供了一种不打断用户当前浏览流,但又始终可用的全局操作入口。除了单个按钮,它还支持更强大的“按钮组”和“回到顶部”功能。

目标:在一个可滚动的内容区域,展示 FloatButton 的多种形态:带徽标的通知按钮、可展开的菜单式按钮组、以及智能的“回到顶部”按钮。

img

文件路径: src/components/demos/FloatButtonDemo.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React from 'react';
import { FloatButton, Typography, Divider, Badge } from 'antd';
import { CustomerServiceOutlined, CommentOutlined, QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons';

const { Title, Paragraph } = Typography;

const FloatButtonDemo: React.FC = () => {
return (
// 创建一个有固定高度、可滚动的外层容器,来模拟长页面
<div style={{ height: '300vh', padding: '24px', position: 'relative' }}>
<Title>悬浮按钮功能展示</Title>
<Paragraph>请向下滚动页面,以查看右下角的悬浮按钮效果。</Paragraph>
<Divider />

{/* 1. 单个悬浮按钮:可以带 Badge 徽标 */}
<Badge count={5}>
<FloatButton icon={<CommentOutlined />} tooltip="查看消息" />
</Badge>

{/* 2. 菜单式按钮组:通过 trigger='click' 或 'hover' 触发 */}
<FloatButton.Group
trigger="click"
type="primary"
style={{ right: 94 }} // 调整位置以避免重叠
icon={<CustomerServiceOutlined />}
tooltip="联系客服"
>
<FloatButton icon={<QuestionCircleOutlined />} />
<FloatButton icon={<SyncOutlined />} />
</FloatButton.Group>

{/* 3. 回到顶部:一个功能强大的专用悬浮按钮 */}
{/* `visibilityHeight` 控制滚动多少像素后按钮才出现 */}
<FloatButton.BackTop visibilityHeight={300} tooltip="回到顶部" />
</div>
);
};

export default FloatButtonDemo;

App.tsx 中独立使用此 Demo:

文件路径: src/App.tsx

1
2
3
4
5
6
7
8
9
10
import React from 'react';
import FloatButtonDemo from './components/demos/FloatButtonDemo';

const App: React.FC = () => {
return (
<FloatButtonDemo />
);
};

export default App;

本 Demo 核心知识点:

  • FloatButton.Group: 按钮组是 FloatButton 的一大特色。通过 trigger 属性可以将其变为一个可展开收起的“菜单”,极大地节省了屏幕空间。
  • FloatButton.BackTop: 内置了完整的“回到顶部”逻辑,我们只需将它放置在页面上,无需手动监听滚动事件或编写滚动动画。visibilityHeight 是其最常用的配置,用于智能地控制按钮的显隐。
  • 组合性: FloatButton 可以与 Badge (徽标数) 等组件无缝组合,实现更丰富的视觉提示效果。

7.8. 流程与内容分层:StepsTabs

在复杂的业务场景中,清晰地引导用户流程、合理地组织内容层次,是提升用户体验的关键。本节我们将学习两个专门用于此目的的组件:Steps (步骤条) 用于线性流程引导,Tabs (标签页) 用于非线性内容分层。

7.8.1. Steps:将复杂任务拆解为清晰流程

当一个操作需要多个步骤才能完成时(如注册、申请、购买流程),使用 Steps 组件可以给用户一个清晰的“路线图”,让他们明确自己“当前在哪一步”以及“总共有几步”,从而降低用户的操作焦虑。

目标:创建一个独立的、交互式的多步骤注册流程。用户可以通过点击按钮在不同步骤间切换,并能直观地看到当前进度和状态。

img

文件路径: src/components/demos/MultiStepRegistration.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import React, { useState } from 'react';
import { Steps, Button, Form, Input, Result, Typography, message, Divider } from 'antd';
import { UserOutlined, SolutionOutlined, CheckCircleOutlined, SmileOutlined } from '@ant-design/icons';

const { Title } = Typography;

// 步骤一的内容组件
const Step1Content = () => (
<Form layout="vertical">
<Form.Item label="用户名" required>
<Input placeholder="请输入您的用户名" prefix={<UserOutlined />} />
</Form.Item>
<Form.Item label="密码" required>
<Input.Password placeholder="请输入您的密码" />
</Form.Item>
</Form>
);

// 步骤二的内容组件
const Step2Content = () => (
<Form layout="vertical">
<Form.Item label="真实姓名" required>
<Input placeholder="请输入您的真实姓名" prefix={<SolutionOutlined />} />
</Form.Item>
<Form.Item label="身份证号" required>
<Input placeholder="请输入您的身份证号" />
</Form.Item>
</Form>
);

// 步骤三的内容组件
const Step3Content = () => (
<Result
icon={<CheckCircleOutlined />}
title="信息确认"
subTitle="请仔细核对您填写的信息,确认无误后即可完成注册。"
/>
);

const MultiStepRegistration: React.FC = () => {
// 使用 useState 控制当前步骤
const [current, setCurrent] = useState(0);

// 使用数据驱动的方式定义步骤条的内容
const steps = [
{
title: '账户信息',
description: '设置用户名和密码',
icon: <UserOutlined />,
content: <Step1Content />,
},
{
title: '实名认证',
description: '填写您的真实信息',
icon: <SolutionOutlined />,
content: <Step2Content />,
},
{
title: '完成注册',
description: '确认信息并提交',
icon: <CheckCircleOutlined />,
content: <Step3Content />,
},
];

const next = () => setCurrent(current + 1);
const prev = () => setCurrent(current - 1);

const onFinish = () => {
message.success('恭喜您,注册成功!');
// 这里可以添加提交表单到后端的逻辑
};

return (
<div style={{ padding: '24px', background: '#fff', borderRadius: '8px' }}>
<Title level={3} style={{ textAlign: 'center' }}>新用户注册流程</Title>
<Divider />

{/* 步骤条主体 */}
<Steps current={current} items={steps.map(item => ({ key: item.title, title: item.title, description: item.description, icon: item.icon }))} />

{/* 内容展示区 */}
<div style={{ marginTop: '24px', padding: '24px', border: '1px dashed #e9e9e9', borderRadius: '8px' }}>
{steps[current].content}
</div>

{/* 操作按钮区 */}
<div style={{ marginTop: '24px', textAlign: 'center' }}>
{current > 0 && <Button style={{ margin: '0 8px' }} onClick={prev}>上一步</Button>}
{current < steps.length - 1 && <Button type="primary" onClick={next}>下一步</Button>}
{current === steps.length - 1 && <Button type="primary" onClick={onFinish}>完成</Button>}
</div>
</div>
);
};

export default MultiStepRegistration;

App.tsx 中独立使用此 Demo:

文件路径: src/App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';
import MultiStepRegistration from './components/demos/MultiStepRegistration';
import { Flex } from 'antd';

const App: React.FC = () => {
return (
<Flex align="center" justify="center" style={{ minHeight: '100vh', background: '#f0f2f5' }}>
<div style={{ width: '600px' }}>
<MultiStepRegistration />
</div>
</Flex>
);
};

export default App;

本 Demo 核心知识点:

  • 受控模式: Steps 组件的核心用法是 受控模式。我们通过 useState 维护一个 current 状态,并将其传递给 Stepscurrent prop。当用户点击“下一步”或“上一步”时,我们更新这个 current 状态,Steps 组件的 UI 就会自动响应变化。
  • 数据驱动 items: 与其他导航组件一样,通过 items 属性来定义每一个步骤的内容(title, description, icon, status 等)是最佳实践。
  • 内容与步骤分离: 步骤条本身只负责展示流程状态,而每一步对应的具体内容(表单、结果等)应该在步骤条外部根据 current 状态来条件渲染。这是一种清晰的关注点分离模式。

🤔 思考与探索:如何实现“点状步骤条”与“可点击切换”?

上面的 Demo 已经非常实用了。但在某些场景下,我们可能需要更精简的视觉或更灵活的交互。
问题

  1. 如果步骤条只是用来展示进度,而不需要详细的标题和描述,我们如何将其变为一个更简洁的 点状步骤条
  2. 如果业务允许用户自由跳转到已经完成的步骤,我们如何实现 可点击的步骤条

答案就在 Steps 组件的另外两个常用属性中。

1. 点状步骤条 (progressDot)
StepsForm 结合使用时,点状步骤条能提供更紧凑的布局。只需添加 progressDot 属性即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 将 `progressDot` 设置为 true
<Steps current={current} progressDot items={...} />

// 甚至可以自定义点的渲染方式
<Steps
current={current}
progressDot={(dot, { status, index }) => (
<Popover content={<span>Step {index} status: {status}</span>}>
{dot}
</Popover>
)}
items={...}
/>

2. 可点击切换 (onChange)
Steps 组件提供了一个 onChange 回调函数。当用户点击某个步骤时,该函数会被触发,并返回被点击步骤的索引。我们可以利用这个函数来更新 current 状态,从而实现步骤的跳转。

1
2
3
4
5
6
7
8
9
const onChange = (value: number) => {
console.log('onChange:', value);
// 可以在这里加入逻辑,比如只允许跳转到已完成的步骤
if (value < current) {
setCurrent(value);
}
};

<Steps current={current} onChange={onChange} items={...} />

掌握了这两个属性,您就可以根据不同的业务需求,灵活地定制 Steps 组件的外观和行为了。


7.8.2. Tabs:在有限空间内组织多维内容

在我们学会了用 Steps 来引导 线性流程 后,现在我们来解决另一个常见问题:如何在同一个页面空间内,组织和展示 非线性的、平级的内容Tabs (标签页) 组件就是为此而生的完美解决方案。

第一部分:基础用法 - 内容区域切换

Tabs 的核心功能是在多个内容面板之间进行切换,保持界面整洁。

目标:创建一个独立的用户中心页面,使用 Tabs 来分别展示“个人资料”、“账户安全”和“消息通知”三个面板。我们将在这个 Demo 中探索 Tabs 的不同布局和附加功能。

img

文件路径: src/components/demos/UserSettingsTabs.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import React, { useState } from 'react';
import { Tabs, Radio, Button, Typography, Space } from 'antd';
import type { RadioChangeEvent, TabsProps } from 'antd';
import { UserOutlined, SafetyCertificateOutlined, BellOutlined, SaveOutlined } from '@ant-design/icons';

const { Paragraph } = Typography;
type TabPosition = 'left' | 'right' | 'top' | 'bottom';

const UserSettingsTabs: React.FC = () => {
// 用于控制 Tabs 位置的状态
const [tabPosition, setTabPosition] = useState<TabPosition>('top');

const changeTabPosition = (e: RadioChangeEvent) => {
setTabPosition(e.target.value);
};

// 1. 使用数据驱动的 `items` 属性来定义标签页
const items: TabsProps['items'] = [
{
key: '1',
label: '个人资料',
icon: <UserOutlined />,
children: <Paragraph>这里是个人资料的表单和内容。</Paragraph>,
},
{
key: '2',
label: '账户安全',
icon: <SafetyCertificateOutlined />,
children: <Paragraph>这里是修改密码、绑定手机等安全设置。</Paragraph>,
disabled: true, // 演示禁用某个标签页
},
{
key: '3',
label: '消息通知',
icon: <BellOutlined />,
children: <Paragraph>这里是各类消息通知的开关设置。</Paragraph>,
},
];

return (
<div style={{ padding: '24px', background: '#fff', borderRadius: '8px' }}>
<Space direction="vertical" style={{ width: '100%' }}>
<div>
<span style={{ marginRight: '16px' }}>标签页位置:</span>
<Radio.Group value={tabPosition} onChange={changeTabPosition}>
<Radio.Button value="top">Top</Radio.Button>
<Radio.Button value="bottom">Bottom</Radio.Button>
<Radio.Button value="left">Left</Radio.Button>
<Radio.Button value="right">Right</Radio.Button>
</Radio.Group>
</div>

<Tabs
tabPosition={tabPosition} // 2. 动态控制标签页的位置
defaultActiveKey="1"
items={items}
// 3. `tabBarExtraContent` 允许在标签栏添加额外元素非常实用
tabBarExtraContent={<Button icon={<SaveOutlined />}>保存设置</Button>}
/>
</Space>
</div>
);
};

export default UserSettingsTabs;

App.tsx 中独立使用此 Demo:

文件路径: src/App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';
import UserSettingsTabs from './components/demos/UserSettingsTabs';
import { Flex } from 'antd';

const App: React.FC = () => {
return (
<Flex align="center" justify="center" style={{ minHeight: '100vh', background: '#f0f2f5' }}>
<div style={{ width: '800px' }}>
<UserSettingsTabs />
</div>
</Flex>
);
};

export default App;

本 Demo 核心知识点:

  • tabPosition: 控制标签页的方位,支持 top, bottom, left, right 四个方向,可以轻松实现垂直标签页布局。
  • tabBarExtraContent: 这是一个非常实用的属性,它允许我们在标签栏的右侧(或左侧)添加自定义的操作按钮或其他元素,而无需修改组件的内部结构。
  • itemsdisabled 属性: 可以方便地禁用某一个标签页,使其不可点击。

第二部分:进阶用法 - 可增减的“卡片式”标签页

在某些应用中,例如代码编辑器、多文档浏览器,我们需要允许用户动态地添加和关闭标签页。Ant Design 的 type="editable-card" 模式就是为此而生。

目标:创建一个简易的多文档编辑器界面,用户可以自由添加新文档(标签页),也可以关闭任何一个已打开的文档。

文件路径: src/components/demos/EditableTabsEditor.tsx

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import React, { useState, useRef } from "react";
import { Tabs, Button } from "antd";
import type { TabsProps } from "antd";

const EditableTabsEditor: React.FC = () => {
// 使用 useRef 来创建一个自增的唯一 key
const newTabIndex = useRef(0);
// 初始的标签页数据
const initialItems = [
{ label: "文档 1", children: "这是文档 1 的内容", key: "1" },
{ label: "文档 2", children: "这是文档 2 的内容", key: "2" },
];

// 使用 useState 来管理活动的 key 和所有的标签页
const [activeKey, setActiveKey] = useState(initialItems[0].key);
const [items, setItems] = useState(initialItems);

const onChange = (newActiveKey: string) => {
setActiveKey(newActiveKey);
};

// 核心逻辑:处理增加和删除事件
const onEdit = (
targetKey: React.MouseEvent | React.KeyboardEvent | string,
action: "add" | "remove"
) => {
if (action === "add") {
add();
} else {
remove(targetKey as string);
}
};

const add = () => {
const newActiveKey = `newTab${newTabIndex.current++}`;
const newPanes = [...items];
newPanes.push({
label: `新文档 ${newTabIndex.current}`,
children: `这是新文档 ${newTabIndex.current} 的内容`,
key: newActiveKey,
});
setItems(newPanes);
setActiveKey(newActiveKey);
};
const remove = (targetKey: string) => {
const newPanes = items.filter((item) => item.key !== targetKey);
setItems(newPanes);

// 如果删除的是当前活动的标签页,则切换到第一个标签页
if (activeKey === targetKey && newPanes.length > 0) {
setActiveKey(newPanes[0].key);
}
};

return (
<div style={{ padding: "24px", background: "#fff", borderRadius: "8px" }}>
<Tabs
type="editable-card"
onChange={onChange}
activeKey={activeKey}
onEdit={onEdit}
items={items}
/>
</div>
);
};
export default EditableTabsEditor;

本 Demo 核心知识点:

  • type="editable-card": 开启“编辑模式”的开关。它会为标签页带来卡片式外观,并自动显示“新增”和“关闭”图标。
  • 受控模式: 这种模式 必须 是受控的。我们需要自己维护一个 items 数组的状态,以及一个 activeKey 的状态。
  • onEdit 回调: 这是编辑模式的灵魂。所有的新增和删除操作都会触发这个回调函数。我们需要在这里编写更新 items 状态数组的逻辑,从而实现对标签页的动态管理。

本节小结
通过对 StepsTabs 的学习,我们掌握了引导用户流程和组织页面内容的核心工具。Steps 适用于 引导用户完成一个线性的、有时序性的任务;而 Tabs 则适用于 将同一主题下的不同维度内容进行归类和分层展示,让用户可以自由切换。

理解它们的适用场景,是构建清晰、易用、不让用户迷路的应用界面的关键。


第七章 第三节:Ant Design 数据录入篇 —— 深挖表单设计逻辑,教你用 Form/Input 组件高效解决录入场景问题

7.9. 数据录入(一):基础输入组件

从本节开始,我们正式进入 Ant Design 的核心——数据录入。我们将遵循“按难度分级”的原则,首先从最基础、最高频的输入组件入手。

本节核心目标:通过一个独立的“用户注册”场景,掌握处理文本 (Input)、数字 (InputNumber)、布尔值 (Switch) 和单选/多选 (Radio/Checkbox) 这五类基础组件,并彻底理解它们背后共通的“受控模式”。

img

文件路径: src/components/demos/BasicRegistrationForm.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import React, { useState } from "react";
import {
Input,
InputNumber,
Radio,
Checkbox,
Switch,
Button,
Form,
Space,
Typography,
Divider,
} from "antd";
import type { CheckboxValueType } from "antd/es/checkbox/Group";

const { Title, Text } = Typography;

const BasicRegistrationForm: React.FC = () => {
// 1. 为每个录入项创建一个独立的 state
// 姓名
const [username, setUsername] = useState<string>("");
// 年龄
const [age, setAge] = useState<number | null>(18);
// 账户类型
const [accountType, setAccountType] = useState<string>("personal");
// 兴趣
const [interests, setInterests] = useState<CheckboxValueType[]>(["coding"]);
// 用户协议
const [agree, setAgree] = useState<boolean>(false);



const handleSubmit = () => {
if (!agree) {
alert("请先同意用户协议!");
return;
}
const formData = { username, age, accountType, interests, agree };
console.log("提交的基础表单数据:", formData);
alert("注册信息已打印到控制台,请按 F12 查看。");
};


return (
<div
style={{
width: "100%",
maxWidth: "400px",
margin: "0 auto",
padding: "24px",
background: "#fff",
borderRadius: "8px",
}}
>
<Title level={4} style={{ textAlign: "center" }}>
创建新账户
</Title>
<Divider />
{/* 我们暂时使用 Form 和 Form.Item 来进行布局和标签对齐,
关于 Form 的数据管理能力,将在后续章节深入讲解。
*/}
<Form layout="vertical">
{/* Input: 基础文本输入 */}
<Form.Item label="用户名" required>
<Input
placeholder="请输入用户名"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</Form.Item>

{/* InputNumber: 数字输入 */}
<Form.Item label="年龄">
<InputNumber
style={{ width: "100%" }}
min={18}
max={100}
value={age}
onChange={(value) => setAge(value)}
/>
</Form.Item>

{/* Radio.Group: 单选 */}
<Form.Item label="账户类型">
<Radio.Group
value={accountType}
onChange={(e) => setAccountType(e.target.value)}
>
<Radio value="personal">个人账户</Radio>
<Radio value="corporate">企业账户</Radio>
</Radio.Group>
</Form.Item>

{/* Checkbox.Group: 多选 */}
<Form.Item label="兴趣爱好">
<Checkbox.Group
options={["阅读", "编码", "旅行", "健身"]}
value={interests}
onChange={(checkedValues) => setInterests(checkedValues)}
/>
</Form.Item>

{/* Switch: 开关 (用于布尔值) */}
<Form.Item>
<Space>
<Switch checked={agree} onChange={(checked) => setAgree(checked)} />
<span>
我已阅读并同意<Text type="secondary">(用户协议)</Text>
</span>
</Space>
</Form.Item>

<Divider />

<Form.Item>
<Button type="primary" block onClick={handleSubmit} disabled={!agree}>
立即注册
</Button>
</Form.Item>
</Form>
</div>
);
};

export default BasicRegistrationForm;

App.tsx 中独立使用此 Demo:

文件路径: src/App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react';
import BasicRegistrationForm from './components/demos/BasicRegistrationForm';
import { Flex } from 'antd';

const App: React.FC = () => {
return (
<Flex align="center" justify="center" style={{ padding: '24px', background: '#f0f2f5', minHeight: '100vh' }}>
<BasicRegistrationForm />
</Flex>
);
};

export default App;

本节核心知识点

  • 万能的“受控模式”: 我们通过实践证明,无论是 Inputvalue,还是 Switchchecked,所有基础录入组件都共享同一个核心模式:useState 定义状态,通过 value/checked 绑定状态,通过 onChange 更新状态。掌握这一点,比记住几十个 API 都重要。
  • 组件选型:
    • 需要用户输入任意文本/数字时,使用 Input/InputNumber
    • 需要在 多个互斥选项中选择一个 时,使用 Radio.Group
    • 需要在 多个选项中选择零个或多个 时,使用 Checkbox.Group
    • 需要用户进行 是/否 的二元选择时,使用 Switch

🤔 思考与探索:让 Switch 更具表现力

在我们的 Demo 中,Switch 只是一个简单的开关。但在很多场景下,我们希望开关在“开”和“关”的状态下,能显示不同的文字或图标,例如“开/关”、“启用/禁用”、“✓/✕”。

问题:查阅 Ant Design 的 Switch 组件文档,您能找到是哪个属性可以实现这个功能吗?请尝试修改上面的 Demo,让 Switch 在开启时显示“同意”,关闭时显示“未同意”。

答案是使用 checkedChildrenunCheckedChildren 属性。

1
2
3
4
5
6
<Switch 
checked={agree}
onChange={(checked) => setAgree(checked)}
checkedChildren="同意"
unCheckedChildren="未同意"
/>

这两个属性非常直观,可以接受字符串或图标(ReactNode),让 Switch 的状态表达更清晰。

我们已经掌握了处理文本、数字和布尔值的基础组件。在下一节 7.10 中,我们将进入第二个层级:选择器(Picker)组件,学习如何使用 Select, DatePicker 等组件,来处理下拉选项、日期时间等更复杂的数据类型,并看看“受控模式”在它们身上是如何应用的。


7.10. 数据录入(二):选择器(Picker)组件

在上一节中,我们掌握了处理文本、数字和布尔值的基础输入组件。现在,我们将“难度”升级,来学习“选择器”家族。这类组件的核心场景是:当用户的输入不是自由的文本,而是需要从一个预设的、结构化的数据集中进行选择时,例如选择一个城市、一个日期或一个评分。

本节核心目标:通过一个独立的“创建活动”场景,掌握 SelectDatePickerTimePickerSliderRateColorPicker 的用法。我们将重点观察,上一节学到的“受控模式”,是如何无缝适配这些组件更丰富的数据类型的。

文件路径: src/components/demos/CreateEventForm.tsx

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import React, { useState } from 'react';
import {
Select,
DatePicker,
TimePicker,
Slider,
Rate,
ColorPicker,
Button,
Form,
Typography,
Divider,
Space,
} from 'antd';
import type { Color } from 'antd/escolor-picker';
import type { Dayjs } from 'dayjs';

const { Title, Text } = Typography;
const { Option } = Select;
const { RangePicker } = DatePicker;

const CreateEventForm: React.FC = () => {
// 1. 为每个选择器创建 state,注意它们的数据类型各不相同
const [eventType, setEventType] = useState<string>('conference');
const [participants, setParticipants] = useState<string[]>(['jack']);
const [eventDates, setEventDates] = useState<[Dayjs | null, Dayjs | null] | null>(null);
const [eventTime, setEventTime] = useState<Dayjs | null>(null);
const [priority, setPriority] = useState<number>(3);
const [budget, setBudget] = useState<number>(5000);
const [themeColor, setThemeColor] = useState<Color | string>('#1677ff');

const handleSubmit = () => {
const formData = {
eventType,
participants,
eventDates: eventDates?.map(date => date?.format('YYYY-MM-DD')),
eventTime: eventTime?.format('HH:mm'),
priority,
budget,
themeColor: typeof themeColor === 'string' ? themeColor : themeColor.toHexString(),
};
console.log('提交的选择器表单数据:', formData);
alert('活动信息已打印到控制台,请按 F12 查看。');
};

return (
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '24px', background: '#fff', borderRadius: '8px' }}>
<Title level={4} style={{ textAlign: 'center' }}>创建新活动</Title>
<Divider />

<Form layout="vertical">
{/* Select: 下拉选择器 */}
<Form.Item label="活动类型">
<Select
value={eventType}
onChange={(value) => setEventType(value)}
options={[
{ value: 'conference', label: '学术会议' },
{ value: 'webinar', label: '线上讲座' },
{ value: 'meetup', label: '线下沙龙', disabled: true },
]}
/>
</Form.Item>


{/* Select (多选模式) */}
<Form.Item label="参会人员 (多选)">
<Select
mode="multiple"
allowClear
placeholder="请选择参会人员"
value={participants}
onChange={(values) => setParticipants(values)}
options={[
{ value: 'jack', label: 'Jack' },
{ value: 'lucy', label: 'Lucy' },
{ value: 'tom', label: 'Tom' },
]}
/>
</Form.Item>

{/* DatePicker 和 TimePicker */}
<Form.Item label="活动周期">
<RangePicker style={{ width: '100%' }} value={eventDates} onChange={(dates) => setEventDates(dates)} />
</Form.Item>

<Form.Item label="开始时间">
<TimePicker style={{ width: '100%' }} value={eventTime} onChange={(time) => setEventTime(time)} />
</Form.Item>

{/* Rate: 评分 */}
<Form.Item label="重要等级">
<Rate value={priority} onChange={(value) => setPriority(value)} />
{priority > 3 && <Text type="danger" style={{marginLeft: '8px'}}>高优先级</Text>}
</Form.Item>

{/* Slider: 滑动输入条 */}
<Form.Item label={`预算: ${budget} `}>
<Slider
min={1000}
max={10000}
step={500}
value={budget}
onChange={(value) => setBudget(value)}
/>
</Form.Item>

{/* ColorPicker: 颜色选择器 */}
<Form.Item label="主题颜色">
<ColorPicker value={themeColor} onChangeComplete={(color) => setThemeColor(color)} />
</Form.Item>

<Divider />
<Form.Item>
<Button type="primary" block onClick={handleSubmit}>创建活动</Button>
</Form.Item>
</Form>
</div>
);
};

export default CreateEventForm;

App.tsx 中独立使用此 Demo:

文件路径: src/App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react';
import CreateEventForm from './components/demos/CreateEventForm';
import { Flex } from 'antd';
// DatePicker/TimePicker 使用 dayjs,需确保全局国际化配置正确
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<Flex align="center" justify="center" style={{ padding: '24px', background: '#f0f2f5', minHeight: '100vh' }}>
<CreateEventForm />
</Flex>
</ConfigProvider>
);
};

export default App;

本节核心知识点

  • “受控模式”的万能适应性: 我们再次验证了“受控模式”的强大。尽管 Selectvalue 是字符串数组,DatePickervalueDayjs 对象,但“用 state 控制 value,用 onChange 更新 state”的核心思想完全不变。
  • 处理 Dayjs 对象: DatePickerTimePickervalueonChange 回调参数都是 Dayjs 对象,而不是字符串。Dayjs 是一个功能强大的日期库,我们可以用它进行格式化 (.format('YYYY-MM-DD'))、计算等各种操作。
  • 数据驱动的 options: 对于 Select 这类选项繁多的组件,始终推荐使用 options 属性传入一个数组来生成选项,这让代码更清晰,也便于动态从服务器获取选项数据。

🤔 思考与探索:如何实现“远程搜索”功能?

在我们的 Demo 中,“参会人员”列表是硬编码的。在一个真实的应用中,用户列表可能非常庞大(成千上万),一次性加载所有用户到前端会让页面崩溃。

问题:我们如何改造 Select 组件,让它在用户输入文字时,才动态地向服务器发送请求,获取并展示匹配的用户列表?

答案在于 Select 组件的几个关键“搜索”属性的组合使用:

  1. showSearch: 必须设置为 true,以启用搜索功能。
  2. onSearch: 提供一个回调函数。当用户在搜索框中输入时,此函数会被触发,并携带用户输入的文本。这是我们 发起 API 请求 的入口。
  3. filterOption: 将其设置为 false。因为我们的选项是动态从远程获取的,需要禁用 Ant Design 的默认前端筛选逻辑。
  4. loading: 在我们等待 API 返回数据的期间,将此属性设为 trueSelect 会显示一个加载指示器,提升用户体验。
  5. options: 在 onSearch 触发的 API 请求成功后,用返回的数据去更新一个 options state,Select 就会自动展示最新的搜索结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const [options, setOptions] = useState([]);
const [loading, setLoading] = useState(false);

const handleSearch = (value: string) => {
if (value) {
setLoading(true);
// 模拟 API 请求
fetch(`/api/users?q=${value}`)
.then(res => res.json())
.then(data => {
setOptions(data); // 使用返回的数据更新选项
setLoading(false);
});
} else {
setOptions([]);
}
};

<Select
showSearch
value={selectedUser}
placeholder="输入用户名搜索"
onSearch={handleSearch}
onChange={handleChange}
filterOption={false}
loading={loading}
options={options}
/>

掌握了这套“组合拳”,您就具备了处理海量数据选择的核心能力。

我们已经掌握了从预设列表中进行选择的“选择器”组件。但如果数据结构本身是 层级嵌套 的呢?例如,选择“国家 -> 省份 -> 城市”。或者,如果我们需要在输入文本时,优雅地 提及 (@) 某个用户呢?

在下一节 7.11 中,我们将挑战第三个层级:高级选择器组件,并正式引入 json-server 来搭建模拟 API,让我们的 Demo 更加贴近实战。


7.11. 数据录入(三):高级选择器、异步数据与 Tailwind CSS(重点)

在前两级中,我们掌握了处理基础数据和预设选项的组件。现在,我们将面临企业级开发中最真实的挑战:处理层级嵌套的复杂数据,并从服务器 异步加载 选项。

本节将是一次全方位的升级,我们将依次完成:

  1. 环境升级:为项目集成 Tailwind CSS,告别行内样式。
  2. 工具引入:学习使用 json-serverfaker-js 搭建专业级的 Mock API 服务器。
  3. 实战演练:通过“增量构建”的方式,一步步完成一个集成了多个高级选择器的“内容发布”表单。

7.11.1. AutoComplete:从零搭建远程搜索

img

第一步:为何选择 AutoComplete?(何时使用)

当我们需要一个 输入框,但又希望在用户输入时,能根据输入内容提供 建议列表 以供选择时,AutoComplete 就是最佳选择。它完美结合了 Input 的自由输入和 Select 的选择辅助。

  • 核心场景:搜索引擎、文章标签输入、收件人邮箱联想等。
  • Select 的区别Select 的核心是“选择”,用户通常只能在 限定的选项 中选择;而 AutoComplete 的核心是“辅助输入”,用户 可以输入任意内容,选项只是建议。

第二步:环境升级 - 集成 Tailwind CSS

为了让我们的组件样式更专业、可维护,我们首先为项目集成 Tailwind CSS

  1. 安装依赖

    1
    pnpm add -D tailwindcss @tailwindcss/vite
  2. 配置 vite
    编辑项目根目录下的 vite.config.js (或 .ts) 文件,引入并使用 Tailwind CSS 插件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import tailwindcss from '@tailwindcss/vite'

    export default defineConfig({
    plugins: [
    vue(),
    tailwindcss(),
    ],
    })
  3. 在主 CSS 文件中引入 Tailwind 指令

    文件路径: src/index.css

    1
    @import "tailwindcss";

    现在,我们的项目已成功集成 Tailwind CSS。

第三步:工具引入 - 搭建 Mock API 服务器

1. 为什么要用 Mock API?
真实项目的数据都来自后端 API。为了高度仿真这个过程,我们需要一个能模拟 API 请求的工具,而不是在前端硬编码数据。

  • json-server: 一个能让你在 30 秒内,用一个 db.json 文件搭建出一个功能完备的 REST API 服务器的“神器”。
  • @faker-js/faker: 一个能生成海量、逼真假数据的库,例如用户名、地址、文章等。

2. 安装依赖

1
pnpm add -D json-server@0.17.4 @faker-js/faker lodash @types/lodash

3. 创建 Mock API (v0.1)
我们只为 AutoComplete 组件创建它所需要的“标签”数据。

文件路径: scripts/generate-mock-data.mjs (新建文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import {
faker
} from '@faker-js/faker';
import fs from 'fs';

// 1. 只创建生成标签数据的函数
const generateTags = (count) => {
const tags = [];
for (let i = 0; i < count; i++) {
tags.push({
id: i + 1,
name: faker.food.adjective()
});
}
return tags;
}

// 2. db.json 中暂时也只包含 tags
const db = {
tags: generateTags(50),
};

fs.writeFileSync('db.json', JSON.stringify(db, null, 2));
console.log('Mock data (tags only) generated successfully!');

4. 启动服务
package.json 中添加脚本:

1
2
3
4
"scripts": {
"mock:generate": "node ./scripts/generate-mock-data.mjs",
"mock:api": "json-server --watch db.json --port 3001"
},

现在,先执行 pnpm mock:generate,然后新开一个终端执行 pnpm mock:api,API 服务器就启动了!

第四步:前置知识 - useCallbackdebounce

在编写核心逻辑之前,我们必须先理解两个关键的工具:React 的 useCallback Hook 和 Lodash 库的 debounce 函数。

  • useCallback 是什么?
    useCallback 是 React 提供的一个性能优化 Hook。在 React 中,当一个组件重新渲染时,它内部定义的所有函数都会被 重新创建。如果一个函数被作为 prop 传递给子组件,这可能会导致不必要的子组件重复渲染。useCallback(fn, deps) 会“记住” fn 这个函数,只有当 deps (依赖项数组) 中的值发生变化时,它才会重新创建一个新的函数。这确保了传递给子组件的函数引用是稳定的,从而避免了不必要的性能损耗。

  • debounce (防抖) 是什么?
    防抖 是一种编程技巧,用于控制高频事件的触发次数。想象一下用户在搜索框里快速输入 “React”,会触发 5 次输入事件。如果我们每次都去请求 API,会造成巨大的性能浪费。debounce(fn, delay) 会包装我们的 fn 函数,使得它只有在 停止触发 delay 毫秒后(例如 300ms)才会真正执行一次。这正是远程搜索场景下最完美的解决方案。

第五步:编码实现

文件路径: src/hooks/useContentPublishForm.ts (新建文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useState, useCallback } from 'react';
import debounce from 'lodash/debounce';

export const useContentPublishForm = () => {
const [tagOptions, setTagOptions] = useState<{ value: string }[]>([]);

const handleTagSearch = useCallback(
debounce((searchText: string) => {
if (!searchText) { setTagOptions([]); return; }
fetch(`http://localhost: 3001/tags?name_like =${searchText}`)
.then(res => res.json())
.then(data => setTagOptions(data.map((tag: any) => ({ value: tag.name }))));
}, 300),
[]
);

return { tagOptions, handleTagSearch };
};

文件路径: src/components/demos/ContentPublishForm.tsx (新建文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React, { useState } from 'react';
import { AutoComplete, Form, Button, Typography, Divider, message } from 'antd';
import { useContentPublishForm } from '../../hooks/useContentPublishForm';

const { Title } = Typography;

const ContentPublishForm: React.FC = () => {
const { tagOptions, handleTagSearch } = useContentPublishForm();
const [tagsValue, setTagsValue] = useState<string>();

const handleSubmit = () => {
message.success("表单内容已打印到控制台");
console.log({ tagsValue });
}

return (
<div className="max-w-2xl mx-auto p-8 bg-white rounded-lg shadow-lg">
<Title level={4} className="text-center !mb-6">发布新内容</Title>
<Form layout="vertical">
<Form.Item label="文章标签 (远程搜索)">
<AutoComplete
options={tagOptions}
onSearch={handleTagSearch}
onChange={setTagsValue}
placeholder="输入并搜索标签"
/>
</Form.Item>
<Divider />
<Button type="primary" block onClick={handleSubmit}>发布文章</Button>
</Form>
</div>
);
};

export default ContentPublishForm;

第六步:最终集成

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import ContentPublishForm from './components/demos/ContentPublishForm';

const App: React.FC = () => {
return (
// antd 国际化配置
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8">
<ContentPublishForm />
</div>
</ConfigProvider>
);
};

export default App;

7.11.2. Cascader:优雅处理层级数据

在企业级应用中,我们经常需要处理具有层级关系的数据,例如“产品分类 -> 型号 -> 规格”,或是公司的“组织架构 -> 部门 -> 小组”。面对这类需求,Cascader (级联选择器) 提供了一种比多个 Select 联动远为优雅和高效的解决方案。

核心价值Cascader 能让用户在 同一个 浮层中,完成从根节点到任意子节点的选择,将复杂的多级联动简化为一次连贯的操作,极大提升了用户体验。

第一步:基础入门 - 静态数据与核心结构

在接触真实 API 之前,我们先通过一个最简单的静态数据 Demo,来彻底理解 Cascader 的“基因”——一个由 valuelabelchildren 构成的递归嵌套结构。

目标:创建一个独立的“商品分类”选择器。

文件路径: src/components/demos/StaticCascaderDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import React from 'react';
import { Cascader, Typography } from 'antd';
import type { CascaderProps } from 'antd';

const { Title } = Typography;

// 1. 定义符合 Cascader 要求的嵌套数据结构
interface CategoryOption {
value: string;
label: string;
children?: CategoryOption[];
}

const productCategories: CategoryOption[] = [
{
value: 'electronics',
label: '电子产品',
children: [
{
value: 'phones',
label: '智能手机',
children: [
{ value: 'iphone-15', label: 'iPhone 15' },
{ value: 'galaxy-s25', label: 'Galaxy S25' },
],
},
{
value: 'laptops',
label: '笔记本电脑',
children: [{ value: 'macbook-pro', label: 'MacBook Pro' }],
},
],
},
{
value: 'books',
label: '图书音像',
children: [
{
value: 'fiction',
label: '小说',
children: [{ value: 'the-three-body-problem', label: '三体' }],
},
],
},
];

const StaticCascaderDemo: React.FC = () => {
const onChange: CascaderProps<CategoryOption>['onChange'] = (value, selectedOptions) => {
// `value` 是一个路径数组, e.g., ['electronics', 'phones', 'iphone-15']
console.log('Selected Path:', value);
// `selectedOptions` 是包含完整节点对象的路径数组
console.log('Selected Options:', selectedOptions);
};

return (
<div className="max-w-md mx-auto p-8 bg-white rounded-lg shadow-lg">
<Title level={4} className="text-center !mb-6">选择商品分类 (静态数据)</Title>
<Cascader
options={productCategories}
onChange={onChange}
placeholder="请选择分类"
expandTrigger="hover" // 演示移入展开体验更流畅
/>
</div>
);
};

export default StaticCascaderDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import StaticCascaderDemo from './components/demos/StaticCascaderDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8">
<StaticCascaderDemo />
</div>
</ConfigProvider>
);
};

export default App;

这个 Demo 让我们掌握了 Cascader 的基本功。但在实战中,数据都来自后端 API,且字段名几乎不可能是 valuelabel。接下来,我们将解决这个核心痛点。

第二步:实战进阶 - 适配 API 数据 (fieldNames)

目标:改造我们的 Demo,使其从 json-server 获取分类数据,并适配后端返回的自定义字段。

1. Mock API 升级 (v0.2) - 准备“不标准”的数据

为了模拟真实场景,我们刻意在 db.json 中使用 catId, catName, subCategories 作为字段名。

文件路径: /src/db.json (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
{
"categories": [
{
"catId": "electronics",
"catName": "电子产品",
"subCategories": [
{
"catId": "smartphones",
"catName": "智能手机",
"subCategories": [
{
"catId": "apple-iphone",
"catName": "Apple iPhone"
},
{
"catId": "android-phones",
"catName": "Android 手机"
}
]
},
{
"catId": "computers",
"catName": "电脑办公",
"subCategories": [
{
"catId": "laptops",
"catName": "笔记本电脑"
},
{
"catId": "desktops",
"catName": "台式机"
},
{
"catId": "monitors",
"catName": "显示器"
}
]
},
{
"catId": "cameras",
"catName": "摄影摄像"
}
]
},
{
"catId": "books-media",
"catName": "图书音像",
"subCategories": [
{
"catId": "literature",
"catName": "文学",
"subCategories": [
{
"catId": "sci-fi",
"catName": "科幻小说"
},
{
"catId": "classic-novels",
"catName": "经典名著"
}
]
},
{
"catId": "technology-books",
"catName": "计算机与科技"
},
{
"catId": "music",
"catName": "音乐",
"subCategories": [
{
"catId": "vinyl-records",
"catName": "黑胶唱片"
},
{
"catId": "cds",
"catName": "CD"
}
]
}
]
},
{
"catId": "home-living",
"catName": "家居生活",
"subCategories": [
{
"catId": "kitchenware",
"catName": "厨房用品"
},
{
"catId": "furniture",
"catName": "家具"
}
]
}
]
}

2. 编写新的 API 版本 Demo

文件路径: src/components/demos/ApiCascaderDemo.tsx (新建文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import React, { useState, useEffect } from 'react';
import { Cascader, Spin, Typography } from 'antd';

const { Title } = Typography;

// 1. 定义与 API 返回结构完全一致的 TypeScript 接口
interface ApiCategoryOption {
catId: string;
catName: string;
subCategories?: ApiCategoryOption[];
}

const ApiCascaderDemo: React.FC = () => {
const [loading, setLoading] = useState(true);
const [options, setOptions] = useState<ApiCategoryOption[]>([]);

useEffect(() => {
fetch('http://localhost:3001/categories')
.then(res => res.json())
.then(data => {
setOptions(data);
setLoading(false);
});
}, []); // 空依赖数组,确保只在组件挂载时执行一次

if (loading) {
return (
<div className="max-w-md mx-auto p-8 bg-white rounded-lg shadow-lg text-center">
<Spin tip="正在加载分类数据..." />
</div>
);
}

return (
<div className="max-w-md mx-auto p-8 bg-white rounded-lg shadow-lg">
<Title level={4} className="text-center !mb-6">选择商品分类 (API 数据)</Title>
<Cascader
options={options}
placeholder="请选择分类"
// 2. 核心使用 fieldNames API 字段映射 antd 组件可识别的字段
fieldNames={{
label: 'catName',
value: 'catId',
children: 'subCategories',
}}
/>
</div>
);
};

export default ApiCascaderDemo;

3. 在 App.tsx 中切换到新 Demo

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { ConfigProvider } from "antd";
import zhCN from "antd/locale/zh_CN";
import "dayjs/locale/zh-cn";
import ApiCascaderDemo from "./components/demos/ApiCascaderDemo";

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8">
{/* <StaticCascaderDemo /> */}
<ApiCascaderDemo />
</div>
</ConfigProvider>
);
};

export default App;

现在运行项目,您会看到 Cascader 成功加载并渲染了来自 API 的、具有自定义字段名的数据。

  • fieldNames 属性: 这是处理真实 API 数据时最有用的属性之一。它就像一个“翻译器”,告诉 Cascader 组件:“请把 API 返回的 catId 当作 valuecatName 当作 labelsubCategories 当作 children”。这让我们无需在前端手动遍历和转换整个数据树,极大简化了代码。

7.11.4. Mentions: 在文本中实现智能提及

在现代协作和社交应用中,我们经常需要在文本输入框中 @ 某个用户或 # 某个话题。这种功能不仅能精准地通知相关人员,还能将非结构化的文本与应用内的实体(用户、项目、标签)关联起来。Ant Design 的 Mentions 组件就是为实现这一功能而生的专业工具。

img

核心价值Mentions 提供了一个带智能建议列表的文本输入区,当用户输入特定前缀(默认为 @)时,会自动弹出可供选择的选项,极大地提升了协作效率和用户体验。

重要升级提示 (v5.1.0+): 在 antd v5.1.0 之后,官方 强烈推荐 使用 options 属性来数据驱动地渲染选项列表。旧有的通过 JSX 嵌套 <Mentions.Option> 的写法已被废弃。本教程将完全遵循 2025 年的最佳实践,只使用现代的 options 写法。

第一步:基础入门 - 静态选项

让我们从一个最基础的例子开始,熟悉 Mentions 组件的核心 API。我们将创建一个简单的评论框,其中包含一组固定的、可供 @ 的用户列表。

目标:创建一个独立的 Mentions 组件,使用静态数据源。

文件路径: src/components/demos/StaticMentionsDemo.tsx (新建文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import React, { useState } from "react";
import { Mentions, Typography, type MentionProps } from "antd";

const { Title } = Typography;

const StaticMentionsDemo: React.FC = () => {
const [value, setValue] = useState("");

const onChange: MentionProps["onChange"] = (text) => {
console.log("onChange:", text);
setValue(text);
};

const onSelect: MentionProps["onSelect"] = (option) => {
console.log("select", option);
};

const options = [
{
value: "张三",
label: "张三 - 前端开发工程师",
},
{
value: "李四",
label: "李四 - 后端开发工程师",
},
{
value: "王五",
label: "王五 - UI/UX 设计师",
},
{
value: "赵六",
label: "赵六 - 产品经理",
},
{
value: "钱七",
label: "钱七 - 测试工程师",
},
{
value: "孙八",
label: "孙八 - 运维工程师",
},
];

return (
<div className="max-w-xl mx-auto p-8 bg-white rounded-lg shadow-lg">
<Title level={4} className="text-center !mb-6">
发布团队动态
</Title>
<Mentions
className="w-max min-h-[100px]"
onChange={onChange}
onSelect={onSelect}
placeholder="在这里输入内容,使用 @ 提及团队成员"
options={options}
></Mentions>
</div>
);
};

export default StaticMentionsDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import StaticMentionsDemo from './components/demos/StaticMentionsDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8">
<StaticMentionsDemo />
</div>
</ConfigProvider>
);
};

export default App;

这个 Demo 让我们掌握了 Mentions 的基本用法。但真正的挑战在于,用户列表通常是动态的,需要从服务器异步获取。

第二步:实战进阶 - 异步搜索用户

现在,我们将 Mentions 与我们的 Mock API 服务器结合,实现一个在用户输入时动态搜索并展示用户建议列表的真实场景。

1. Mock API 升级 (v0.4) - 准备用户数据

我们需要一个用户列表的 API 端点。

文件路径: db.json (修改文件,在 categories 同级添加 users)

1
2
3
4
5
6
7
8
9
10
{
"users": [
{"id": 1,"name": "张三","profession": "张三 - 前端开发工程师"},
{"id": 2,"name": "李四","profession": "李四 - 后端开发工程师"},
{"id": 3,"name": "王五","profession": "王五 - UI/UX 设计师"},
{"id": 4,"name": "赵六","profession": "赵六 - 产品经理"},
{"id": 5,"name": "钱七","profession": "钱七 - 测试工程师"},
{"id": 6,"name": "孙八","profession": "孙八 - 运维工程师"}
]
}

2. 编写异步版本 Demo

文件路径: src/components/demos/AsyncMentionsDemo.tsx (新建文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import React, { useState, useCallback } from "react";
import { Mentions, Spin } from "antd";
import type { MentionProps } from "antd";
import debounce from "lodash/debounce";

const AsyncMentionsDemo: React.FC = () => {
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState<MentionProps["options"]>([]);
// 1. 核心搜索逻辑,使用 debounce 防止高频请求
const loadUser = async (search: string) => {
setLoading(true);

try {
// 如果没有搜索词,获取所有用户;否则使用 q 参数进行全文搜索
const url = !search
? `http://localhost:3001/users`
: `http://localhost:3001/users?q=${search}`;

const res = await fetch(url);
const users = await res.json();

// 2. 将 API 返回的数据映射为 Mentions 需要的格式
const userOptions = users.map(
(user: { id: string; name: string; profession: string }) => ({
key: user.id,
value: user.name,
label: user.profession,
})
);

setOptions(userOptions);
} catch (error) {
setOptions([]);
}

setLoading(false);
};

// 3. 使用 useCallback 和 debounce 包装搜索函数
const debouncedLoadUsers = useCallback(debounce(loadUser, 800), []);

const onSearch: MentionProps["onSearch"] = (search) => {
// onSearch 在用户输入 @ 后触发,我们将调用防抖后的搜索函数
debouncedLoadUsers(search);
};

return (
<div className="max-w-xl mx-auto p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">
项目评论 (异步搜索)
</h3>
<Mentions
style={{ width: "100%" }}
loading={loading}
onSearch={onSearch}
options={options}
placeholder="输入 @ 搜索并提及项目成员"
/>
</div>
);
};

export default AsyncMentionsDemo;

3. 在 App.tsx 中切换到新 Demo

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import AsyncMentionsDemo from './components/demos/AsyncMentionsDemo';
import StaticMentionsDemo from './components/demos/StaticMentionsDemo';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/es/locale/zh_CN';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8">
<AsyncMentionsDemo />
</div>
</ConfigProvider>
);
};

export default App;

现在,当您在输入框中键入 @ 并开始输入时,组件会在短暂延迟后显示一个加载指示器,然后从 API 获取匹配的用户列表并展示出来,一个专业级的“提及”功能就完成了。

本节核心知识点:

  • onSearch 回调: 这是实现异步加载的灵魂。当用户输入触发字符(如 @)并继续输入时,该回调函数会被触发,并传入搜索的关键词。这是我们发起 API 请求的最佳时机。
  • loading 属性: 通过将 loading 属性与我们的 API 请求状态绑定,可以为用户提供清晰的加载反馈,提升体验。
  • 防抖 (debounce): 在处理用户输入触发的搜索时,始终 应该使用 debounce 来包装请求函数。这可以避免在用户快速输入时发送大量不必要的 API 请求,是性能优化的关键一环。

7.11.5. TreeSelect: 驾驭复杂树形选择的终极方案

到目前为止,我们已经掌握了 Select(处理扁平列表)和 Cascader(处理严格的单路径层级)。但如果我们的需求是 在一个层级结构中,自由地选择多个节点,甚至包括父节点和子节点 呢?

想象一个文件系统,您需要同时选择 “/文档” 这个文件夹,以及 “/图片/旅行/” 下的 京都风景.jpg 这个文件。Cascader 无法做到这一点。这,就是 TreeSelect 的主场。

核心价值TreeSelect 完美结合了 Tree 的层级展示能力和 Select 的选择器交互形态。它是处理如 权限分配、组织架构选择、文件目录勾选 等复杂、非线性树形选择场景下的终极解决方案。

第一步:基础入门 - 从 treeData 开始

Tree 组件一样,TreeSelect 的最佳实践也是通过 treeData 属性,传入一个带有 title, value, 和 children 字段的数组来渲染整个树形结构。

目标:创建一个独立的“权限分配”选择器,使用静态数据源,实现最基本的单选功能。

文件路径: src/components/demos/PermissionSelectorDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import React, { useState } from "react";
import { TreeSelect, Typography } from "antd";

const { Title } = Typography;

// 1. 定义我们的权限树数据结构
const permissionTreeData = [
{ title: "仪表盘", value: "dashboard", key: "dashboard" },
{
title: "用户管理",
value: "user-management",
key: "user-management",
children: [
{
title: "用户列表",
value: "user-list",
key: "user-list",
children: [
{ title: "查看用户", value: "view-user", key: "view-user" },
{ title: "编辑用户", value: "edit-user", key: "edit-user" },
{ title: "删除用户", value: "delete-user", key: "delete-user" }
]
},
{
title: "角色分配",
value: "role-assignment",
key: "role-assignment",
children: [
{ title: "分配角色", value: "assign-role", key: "assign-role" },
{ title: "撤销角色", value: "revoke-role", key: "revoke-role" }
]
}
],
},
{
title: "文章管理",
value: "article-management",
key: "article-management",
children: [
{ title: "文章列表", value: "article-list", key: "article-list" },
{ title: "文章分类", value: "article-category", key: "article-category" },
{ title: "文章标签", value: "article-tag", key: "article-tag" },
],
},
];

const PermissionSelectorDemo: React.FC = () => {
const [value, setValue] = useState<string | undefined>();
const onChange = (newValue: string) => {
console.log("Selected Permission:", newValue);
setValue(newValue);
};

return (
<div className="max-w-md mx-auto p-8 bg-white rounded-lg shadow-lg">
<Title level={5}>选择权限</Title>
<TreeSelect
style={{ width: "100%" }}
value={value}
popupMatchSelectWidth={400}
treeData={permissionTreeData}
placeholder="请选择一项权限"
treeDefaultExpandAll // 默认展开所有节点便于查看
onChange={onChange}
/>
</div>
);
};

export default PermissionSelectorDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import PermissionSelectorDemo from './components/demos/PermissionSelectorDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8">
<PermissionSelectorDemo />
</div>
</ConfigProvider>
);
};

export default App;

第二步:释放核心能力 - 可勾选的多选 (treeCheckable)

单选功能 Select 也能做,TreeSelect 真正的威力在于其强大的多选能力。通过 treeCheckable 属性,我们可以为每个树节点生成一个 Checkbox,允许用户自由勾选。

修改 PermissionSelectorDemo.tsx:

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React, { useState } from 'react';
import { TreeSelect, Typography, Tag } from 'antd';

// ... (permissionTreeData 保持不变)

const PermissionSelectorDemo: React.FC = () => {
// 1. 将 state 初始化为数组,以支持多选
const [value, setValue] = useState<string[]>(['dashboard']);

const onChange = (newValue: string[]) => {
console.log('Selected Permissions:', newValue);
setValue(newValue);
};

return (
<div className="max-w-md mx-auto p-8 bg-white rounded-lg shadow-lg">
<Title level={4} className="text-center !mb-6">为角色分配权限 (多选)</Title>
<TreeSelect
style={{ width: "100%" }}
value={value}
popupMatchSelectWidth={400}
treeData={permissionTreeData}
// 👇 核心变更
multiple={true} // 开启多选模式
treeCheckable={true} // 开启勾选框
placeholder="请选择一项权限"
treeDefaultExpandAll // 默认展开所有节点便于查看
onChange={onChange}
/>
<div className="mt-4">
<span className="font-semibold">当前权限: </span>
{value.map(v => <Tag color="blue" key={v}>{v}</Tag>)}
</div>
</div>
);
};

export default PermissionSelectorDemo;

现在,选择器已经变为一个强大的多选工具。您会注意到一个默认行为:当您勾选一个父节点时,它所有的子节点都会被自动选中。这在很多场景下非常有用,但如果我们想更精细地控制回填到输入框中的内容呢?

第三步:精细控制 - 勾选策略 (showCheckedStrategy)

showCheckedStrategy 属性决定了当用户勾选节点后,哪些值最终会显示在选择框中。这是 TreeSelect 最重要、也最需要理解的配置之一。

策略: 只显示被选中的 子节点。如果一个父节点的所有子节点都被选中了,也只显示这些子节点,不显示父节点。

场景: 当您只关心最末端的具体权限时。例如,给用户分配了 “用户列表” 和 “角色分配” 权限,您不关心他是否拥有整个 “用户管理” 的权限。

代码:

1
2
3
4
<TreeSelect
// ... 其他 props
showCheckedStrategy={TreeSelect.SHOW_CHILD}
/>

效果: 勾选“用户管理”后,选择框中显示的是 [用户列表, 角色分配]

策略: 优先显示 父节点。只有当一个父节点下的所有子节点都被选中时,才将它们合并为父节点进行显示。如果只选中了部分子节点,则依然显示这些子节点。

场景: 当您希望权限表示更概括、更简洁时。例如,如果用户拥有了用户管理下的所有权限,直接显示一个 “用户管理” 标签,比显示一长串子权限更清晰。

代码:

1
2
3
4
<TreeSelect
// ... 其他 props
showCheckedStrategy={TreeSelect.SHOW_PARENT}
/>

效果: 勾选“用户管理”后,选择框中只显示 [用户管理]

策略: 显示 所有 被选中的节点,无论父子。

场景: 极为少见,通常只在需要完整回溯所有选中状态时使用。大部分情况下会造成信息冗余。

代码:

1
2
3
4
<TreeSelect
// ... 其他 props
showCheckedStrategy={TreeSelect.SHOW_ALL}
/>

效果: 勾选“用户管理”后,选择框中会显示 [用户管理, 用户列表, 角色分配]

第四步:终极解耦 - 严格模式 (treeCheckStrictly)

默认的父子联动勾选行为,在某些严格的权限场景下会成为阻碍。例如:

痛点:我想给某个角色“用户管理”这个模块的 访问权限(即勾选父节点),但不给他下面任何具体的 操作权限(即不勾选任何子节点)。

在默认模式下,这是不可能的。一旦勾选父节点,子节点也会被勾选。treeCheckStrictly 就是为了解决这个问题而生的。

修改 PermissionSelectorDemo.tsx:

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 1. 需要从 antd 导入类型
import type { DefaultOptionType } from "antd/es/select";

// ... (permissionTreeData 保持不变)

const PermissionSelectorDemo: React.FC = () => {
// 2. 启用 treeCheckStrictly 后,value 的类型会变为对象数组
const [value, setValue] = useState<DefaultOptionType[]>([
{ value: "user-management", label: "用户管理" },
]);

const onChange = (newValue: DefaultOptionType[]) => {
console.log("Selected Permissions (Strictly):", newValue);
setValue(newValue);
};

return (
<div className="max-w-md mx-auto p-8 bg-white rounded-lg shadow-lg">
<Title level={4} className="text-center !mb-6">权限分配 (严格模式)</Title>
<TreeSelect
style={{ width: '100%' }}
value={value}
treeData={permissionTreeData}
placeholder="请选择权限"
treeDefaultExpandAll
onChange={onChange}
multiple={true}
treeCheckable={true}
// 👇 核心变更
treeCheckStrictly={true}
// 启用 treeCheckStrictly 组件会自动开启 labelInValue
// 这意味着 value/onChange 的值不再是 string[], 而是 { value, label }[]
showCheckedStrategy={TreeSelect.SHOW_CHILD} // 在严格模式下通常配合 SHOW_CHILD
/>
<div className="mt-4">
<span className="font-semibold">当前权限: </span>
{value.map(v => <Tag color="purple" key={v.value}>{v.label}</Tag>)}
</div>
</div>
);
};

export default PermissionSelectorDemo;

核心变化: 启用 treeCheckStrictly={true} 后,TreeSelect 的行为会发生根本性改变:

  1. 父子解绑: 勾选父节点不再影响子节点,反之亦然。每个节点的勾选状态都是完全独立的。
  2. 强制 labelInValue: 组件会自动将 labelInValue 设为 true。这意味着 valueonChange 的值不再是简单的 string[],而是 [{ value: string, label: ReactNode }] 格式的对象数组。您的 useState 和类型定义必须做出相应调整。

通过这四步的层层递进,我们不仅学会了 TreeSelect 的基础用法,更深入理解了其在多选场景下的核心配置 showCheckedStrategytreeCheckStrictly。掌握了它们,您就能够从容应对任何复杂的树形数据选择需求。


7.11.6. Upload: 实现专业级文件上传

至此,我们已经征服了各类“选择”型输入组件。现在,我们将进入数据录入的另一大核心领域:文件上传。无论是发布文章的封面、上传用户头像,还是提交附件,Upload 组件都是任何现代应用不可或缺的一环。它将复杂的文件处理流程——从选择、预览到发送 HTTP 请求——封装成了一个优雅、可高度定制的交互界面。

第一步:备好“弹药” - 搭建 Mock 服务 (CLI 模式)

Upload 组件需要一个后端接收文件。为了模拟这个过程,我们将使用 json-server 的命令行工具,并为其提供一个“中间件”脚本来专门处理文件上传。这种方式比编写完整的服务器脚本更简单。

1. 安装依赖
我们需要 multer 来帮助我们解析上传的文件。

1
pnpm add -D multer

2. 创建上传“中间件”
中间件是一个函数,它会在 json-server 处理常规 API 请求之前执行。我们可以用它来拦截并处理我们的文件上传请求。

文件路径: scripts/upload-middleware.cjs (新建文件,注意是 .cjs 后缀)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const multer = require('multer');
const path = require('path');
const fs = require('fs');

// 确保 uploads 文件夹存在
const uploadDir = 'uploads';
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}

// 配置 multer 用于文件存储
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
},
});

const upload = multer({ storage: storage });

// 导出一个中间件函数
module.exports = (req, res, next) => {
// 1. 检查请求是否是我们的上传请求
if (req.method === 'POST' && req.path === '/api/upload') {
// 2. 如果是,使用 multer 处理它
upload.single('file')(req, res, (err) => {
if (err) {
return res.status(500).json({ error: err.message });
}
if (!req.file) {
return res.status(400).send({ status: 'error', message: 'No file uploaded.' });
}
// 3. 返回成功的响应,注意 URL 路径
res.status(200).json({
status: 'success',
message: 'File uploaded successfully!',
// CLI 模式下,--static 会将文件服务到根路径
url: `http://localhost:3001/${req.file.filename}`,
});
});
} else {
// 4. 如果不是上传请求,交给 json-server 继续处理
next();
}
};

3. 添加启动脚本
现在,我们在 package.json 中组合一个完整的 json-server 命令。

1
2
3
4
"scripts": {
// ... 其他脚本
"mock:api": "json-server --watch db.json --port 3001 --static ./uploads --middlewares ./scripts/upload-middleware.cjs"
},

这个命令做了三件事:

  • --watch db.json: 启动标准的 JSON API 服务。
  • --static ./uploads: uploads 文件夹作为静态资源目录,并映射到服务器根路径
  • --middlewares ./scripts/upload-middleware.cjs: 加载我们刚刚编写的中间件,使其具备处理文件上传的能力。

现在,新开一个终端,运行 pnpm mock:api,我们的 Mock 服务就以最简单的方式启动了!

第二步:核心机理 - 理解 Upload 的生命周期

有了后端服务后,我们再来审视 Upload 组件的生命周期,一切将变得豁然开朗。一次成功的文件上传,会经历以下流程:

Upload 生命周期

  1. 文件选择:用户通过点击或拖拽选择了文件。
  2. beforeUpload 拦截:在客户端进行文件类型、大小的校验。
  3. 发起请求:组件向 action URL (http://localhost:3001/api/upload) 发起 POST 请求。
  4. onChange 状态更新file.status 依次变为 uploading -> doneerror
  5. 服务端响应:我们的 upload-middleware.js 接收文件,返回 JSON。onChange 会在 file.status 变为 done 时,将这个 JSON 存入 file.response 字段。

重要提示: 后续所有 Upload 组件示例中的 action 属性,都将指向我们通过中间件定义的 http://localhost:3001/api/upload 地址。

第三步:基础入门 - 最简单的点击上传

现在我们可以来编写前端代码了,内容与之前完全相同,因为它不需要关心后端是如何实现的。

目标:实现一个基础的、带文件列表的点击上传功能。

文件路径: src/components/demos/BasicUploadDemo.tsx (新建文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React from 'react';
import { UploadOutlined } from '@ant-design/icons';
import { Button, message, Upload } from 'antd';
import type { UploadProps } from 'antd';

const BasicUploadDemo: React.FC = () => {
const props: UploadProps = {
name: 'file', // 必须与后端 multer 的 'file' 字段名一致
action: 'http://localhost:3001/api/upload', // 指向我们自己的 Mock API
headers: {
authorization: 'authorization-text',
},
onChange(info) {
if (info.file.status !== 'uploading') {
console.log('File list changed:', info.file, info.fileList);
}
if (info.file.status === 'done') {
message.success(`${info.file.name} 文件上传成功`);
// 服务端返回的 URL 在 info.file.response.url
console.log('Server Response:', info.file.response);
} else if (info.file.status === 'error') {
message.error(`${info.file.name} 文件上传失败.`);
}
},
};

return (
<div className="max-w-md mx-auto p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">基础文件上传</h3>
<Upload {...props}>
<Button icon={<UploadOutlined />}>点击上传</Button>
</Upload>
</div>
);
};

export default BasicUploadDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicUploadDemo from './components/demos/BasicUploadDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicUploadDemo />
</div>
</ConfigProvider>
);
};

export default App;

现在,确保您的 pnpm mock:api 服务正在运行,然后启动前端项目。尝试上传一个文件,一切将如预期般工作。这种 CLI + 中间件的模式,是 json-server 功能扩展的最佳实践之一。


第四步:体验升级 - 实现用户头像上传

基础的文件列表样式显然不适合“用户头像”这种场景。我们需要一个方形或圆形的区域,点击后上传,成功后直接显示图片预览。

痛点:如何隐藏默认的文件列表,并在上传成功后将组件本身变为图片预览区?如何在前端就拦截掉不符合要求的文件?

解决方案

  1. 使用 listType="picture-card"listType="picture-circle" 改变组件外观。
  2. 设置 showUploadList={false} 来完全接管 UI 展示。
  3. 利用 beforeUpload 钩子在客户端进行文件类型和大小的校验。
  4. (核心变更)onChange 回调中,当上传成功时,直接使用服务器返回的 URL 来更新预览图。

文件路径: src/components/demos/AvatarUploadDemo.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import React, { useState } from "react";
import { LoadingOutlined, PlusOutlined } from "@ant-design/icons";
import { Flex, message, Upload } from "antd";
import type { GetProp, UploadProps } from "antd";

// antd 的工具类型,用于精确获取 `beforeUpload` 的 file 类型
type FileType = Parameters<GetProp<UploadProps, "beforeUpload">>[0];

const AvatarUploadDemo: React.FC = () => {
const [loading, setLoading] = useState(false);
const [imageUrl, setImageUrl] = useState<string>();

const beforeUpload = (file: FileType) => {
const isJpgOrPng = file.type === "image/jpeg" || file.type === "image/png";
if (!isJpgOrPng) {
message.error("您只能上传 JPG/PNG 格式的图片!");
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error("图片大小不能超过 2MB!");
}
return isJpgOrPng && isLt2M;
};

const handleChange: UploadProps["onChange"] = (info) => {
if (info.file.status === "uploading") {
setLoading(true);
return;
}
if (info.file.status === "done") {
// **核心变更**: 不再使用 getBase64 本地预览
// 直接从服务器的响应中获取 URL 并更新 imageUrl state
const serverUrl = info.file.response?.url;
if (serverUrl) {
setImageUrl(serverUrl);
message.success(`${info.file.name} 上传成功`);
} else {
// 如果服务器没有返回 url,则认为上传失败
message.error(`${info.file.name} 上传失败,服务器未返回有效链接。`);
}
setLoading(false);
} else if (info.file.status === "error") {
setLoading(false);
message.error(`${info.file.name} 上传失败.`);
}
};

const uploadButton = (
<button style={{ border: 0, background: "none" }} type="button">
{loading ? <LoadingOutlined /> : <PlusOutlined />}
<div style={{ marginTop: 8 }}>上传头像</div>
</button>
);

return (
<div className="max-w-md mx-auto p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">用户头像上传</h3>
<Flex gap="middle" justify="center">
<Upload
name="file" // 注意这里的 name 需要和服务端 multer 匹配我们的中间件是 'file'
listType="picture-card"
className="avatar-uploader"
showUploadList={false}
action="http://localhost:3001/api/upload"
beforeUpload={beforeUpload}
onChange={handleChange}
>
{imageUrl ? (
<img src={imageUrl} alt="avatar" style={{ width: "100%" }} />
) : (
uploadButton
)}
</Upload>
<Upload
name="file" // 注意这里的 name 需要和服务端 multer 匹配我们的中间件是 'file'
listType="picture-circle"
className="avatar-uploader"
showUploadList={false}
action="http://localhost:3001/api/upload"
beforeUpload={beforeUpload}
onChange={handleChange}
>
{imageUrl ? (
<img
src={imageUrl}
alt="avatar"
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
) : (
uploadButton
)}
</Upload>
</Flex>
</div>
);
};

export default AvatarUploadDemo;

代码校正:请注意,我们的 upload-middleware.js 中使用的是 upload.single('file'),因此为了匹配,Upload 组件的 name 属性理论上应该设为 "file"。不过,由于 multer 并不严格校验 name 属性(它主要关心 Content-Disposition header 中的字段名),所以即使设为 "avatar" 也能工作。但在生产环境中,保持前端 name 与后端 multer 字段名一致是最佳实践。

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// import BasicUploadDemo from './components/demos/BasicUploadDemo';
import AvatarUploadDemo from './components/demos/AvatarUploadDemo';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <BasicUploadDemo /> */}
<AvatarUploadDemo />
</div>
</ConfigProvider>
);
};

export default App;

这个进阶案例是 Upload 组件定制化能力的绝佳体现。通过组合 listType, showUploadList, beforeUploadonChange,我们完全掌控了组件的外观和行为,并实现了从客户端校验到服务端存储、再到真实 URL 回显的完整闭环。


第五步:交互革新 - 优雅的拖拽上传

对于需要上传大量文件或较大文件的场景,拖拽无疑是比点击选择更高效、更现代的交互方式。Ant Design 为此提供了开箱即用的解决方案:Upload.Dragger

目标:创建一个支持点击和拖拽两种方式,且能同时上传多个文件的大尺寸上传区域。

文件路径: src/components/demos/DragUploadDemo.tsx (新建文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import React from "react";
import { InboxOutlined } from "@ant-design/icons";
import type { UploadProps } from "antd";
import { message, Upload } from "antd";

const { Dragger } = Upload;

const DragUploadDemo: React.FC = () => {
const props: UploadProps = {
name: "file",
multiple: true,
action: "http://localhost:3001/api/upload",
onChange: (info) => {
const { status } = info.file;
if (status === "done") {
message.success(`${info.file.name} file uploaded successfully.`);
} else if (status === "error") {
message.error(`${info.file.name} file upload failed.`);
}
},
onDrop(e) {
console.log("Dropped files", e.dataTransfer.files);
},
};

return (
<div className="w-[500px] mx-auto p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">拖拽上传</h3>
<Dragger {...props}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">点击或拖拽文件到此区域以上传</p>
<p className="ant-upload-hint">
支持单个或批量上传。严禁上传公司内部资料及其他违禁文件。
</p>
</Dragger>
</div>
);
};

export default DragUploadDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// import AvatarUploadDemo from './components/demos/AvatarUploadDemo';
import DragUploadDemo from './components/demos/DragUploadDemo';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <AvatarUploadDemo /> */}
<DragUploadDemo />
</div>
</ConfigProvider>
);
};

export default App;

UploadDragger 的切换成本极低。你只需要将 <Upload> 标签换成 <Dragger>,并传入完全相同的 props,即可瞬间为你的应用赋予专业级的拖拽上传能力。这正是 Ant Design 组件化设计思想的魅力所在。

通过以上几步,我们已经掌握了 Upload 组件从基础到高级的核心用法。无论是简单的文件提交,还是定制化的头像上传,亦或是现代化的拖拽交互,Upload 都为我们提供了强大而灵活的武器。


7.12. 基础数据展示组件 - 每一个角落不可缺少的小型组件

摘要:本章将系统性地学习 Ant Design 中所有核心的数据展示组件。这些组件的用途是将结构化数据显示为清晰、易于理解的界面。我们将遵循由简到繁的学习路径,从用于展示单一信息的基础组件开始,逐步深入到用于组织内容的容器组件,最后掌握处理复杂数据集的高级组件。

7.12.1. Tag: 信息的微观标记

我们数据展示之旅的第一站,是 Tag (标签) 组件。Tag 是 UI 中最基础、也最常见的信息单元之一,专门用于对信息进行小维度的标记、分类和高亮。它的核心价值在于用极小的视觉空间,提供清晰的上下文信息。

核心应用场景:

  • 状态标记:标记文章的“已发布”、“草稿”、“待审核”状态。
  • 属性归类:标记商品的“新品”、“热销”、“电子产品”属性。
  • 关键词展示:在用户配置、文章详情等页面展示用户的技能标签或文章关键词。

第一步:基础用法 - 颜色、图标与状态

Tag 组件最基础的用法就是展示文本,并通过不同的颜色来传递语义。Ant Design 为我们内置了多套预设颜色,同时也支持完全自定义。

核心属性:

  • color: 设置标签的颜色。它可以接受三种类型的值:
    1. 预设状态色success, processing, error, warning, default,用于表达明确的状态语义。
    2. 预设调色板色magenta, red, volcano, orange, gold 等,提供了丰富的色彩选择。
    3. 自定义色:标准的 HEXRGB 值,如 #87d068
  • icon: 在标签文本前添加一个图标,增强视觉表现力。

文件路径: src/components/demos/BasicTagDemo.tsx (新建文件)

image-20250929141416678

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import React from 'react';
import {
SyncOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
ExclamationCircleOutlined,
ClockCircleOutlined,
MinusCircleOutlined,
} from '@ant-design/icons';
import { Flex, Tag, Divider } from 'antd';

const BasicTagDemo: React.FC = () => (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">基础标签用法</h3>

<Divider orientation="left">预设状态色</Divider>
<Flex gap="4px 0" wrap>
<Tag icon={<CheckCircleOutlined />} color="success">
成功
</Tag>
<Tag icon={<SyncOutlined spin />} color="processing">
处理中
</Tag>
<Tag icon={<CloseCircleOutlined />} color="error">
失败
</Tag>
<Tag icon={<ExclamationCircleOutlined />} color="warning">
警告
</Tag>
<Tag icon={<ClockCircleOutlined />} color="default">
等待
</Tag>
<Tag icon={<MinusCircleOutlined />} color="default">
暂停
</Tag>
</Flex>

<Divider orientation="left">多彩标签</Divider>
<Flex gap="4px 0" wrap>
<Tag color="magenta">magenta</Tag>
<Tag color="red">red</Tag>
<Tag color="volcano">volcano</Tag>
<Tag color="orange">orange</Tag>
<Tag color="gold">gold</Tag>
<Tag color="lime">lime</Tag>
<Tag color="green">green</Tag>
<Tag color="cyan">cyan</Tag>
<Tag color="blue">blue</Tag>
<Tag color="geekblue">geekblue</Tag>
<Tag color="purple">purple</Tag>
</Flex>

<Divider orientation="left">自定义颜色</Divider>
<Flex gap="4px 0" wrap>
<Tag color="#f50">#f50</Tag>
<Tag color="#2db7f5">#2db7f5</Tag>
<Tag color="#87d068">#87d068</Tag>
<Tag color="#108ee9">#108ee9</Tag>
</Flex>
</div>
);

export default BasicTagDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicTagDemo from './components/demos/BasicTagDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicTagDemo />
</div>
</ConfigProvider>
);
};

export default App;

第二步:动态管理 - 添加与删除标签

在真实业务中,标签列表常常是动态变化的,比如用户给自己添加技能标签,或者为文章增删关键词。这要求我们能够通过 state 来动态管理标签数组。

核心思路:

  1. 使用 useState 维护一个存储所有标签的数组。
  2. 使用数组的 map 方法,将 state 中的每个标签渲染成一个可关闭的 Tag 组件。
  3. 为每个 TagonClose 事件绑定一个处理函数,该函数从 state 数组中移除对应的标签。
  4. 提供一个 InputButton(或类似交互)来向 state 数组中添加新的标签。

文件路径: src/components/demos/DynamicTagDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import React, { useEffect, useRef, useState } from 'react';
import { PlusOutlined } from '@ant-design/icons';
import { Flex, Input, Tag, Tooltip } from 'antd';
import type { InputRef } from 'antd';

const DynamicTagDemo: React.FC = () => {
const [tags, setTags] = useState(['技术', '设计', '产品']);
const [inputVisible, setInputVisible] = useState(false);
const [inputValue, setInputValue] = useState('');
const inputRef = useRef<InputRef>(null);

useEffect(() => {
if (inputVisible) {
inputRef.current?.focus();
}
}, [inputVisible]);

const handleClose = (removedTag: string) => {
const newTags = tags.filter((tag) => tag !== removedTag);
setTags(newTags);
};

const showInput = () => {
setInputVisible(true);
};

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};

const handleInputConfirm = () => {
if (inputValue && !tags.includes(inputValue)) {
setTags([...tags, inputValue]);
}
setInputVisible(false);
setInputValue('');
};

return (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">动态添加和删除</h3>
<Flex gap="4px 0" wrap>
{tags.map<React.ReactNode>((tag, index) => (
<Tag
key={tag}
closable // 设置为可关闭
onClose={() => handleClose(tag)}
>
{tag}
</Tag>
))}
{inputVisible ? (
<Input
ref={inputRef}
type="text"
size="small"
style={{ width: 78 }}
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputConfirm}
onPressEnter={handleInputConfirm}
/>
) : (
<Tag
style={{ borderStyle: 'dashed' }}
icon={<PlusOutlined />}
onClick={showInput}
>
新标签
</Tag>
)}
</Flex>
</div>
);
};

export default DynamicTagDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from "react";
import { ConfigProvider } from "antd";
import zhCN from "antd/locale/zh_CN";
import "dayjs/locale/zh-cn";
import DynamicTagDemo from "./components/demos/DynamicTagDemo";
const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<DynamicTagDemo />
</div>
</ConfigProvider>
);
};

export default App;

第三步:特殊形态 - 可选择标签

有时,我们需要一组外观像 Tag,但行为像 CheckboxRadio 的组件,用于筛选或分类选择。为此,antd 提供了一个特殊的子组件:Tag.CheckableTag

核心要点:

  • Tag.CheckableTag 是一个 完全受控 的组件。
  • 它的选中状态由外部传入的 checked 属性决定。
  • 当用户点击时,它会触发 onChange 回调,并传入一个新的布尔值(truefalse),我们需要在这个回调中更新我们自己的 state。

文件路径: src/components/demos/CheckableTagDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React, { useState } from 'react';
import { Tag } from 'antd';

const { CheckableTag } = Tag;

const categories = ['技术', '人文', '历史', '设计', '产品', '运营'];

const CheckableTagDemo: React.FC = () => {
const [selectedTags, setSelectedTags] = useState<string[]>(['技术', '设计']);

const handleChange = (tag: string, checked: boolean) => {
const nextSelectedTags = checked
? [...selectedTags, tag]
: selectedTags.filter((t) => t !== tag);
setSelectedTags(nextSelectedTags);
};

return (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">可选择标签</h3>
<span style={{ marginRight: 8 }}>分类:</span>
{categories.map<React.ReactNode>((tag) => (
<CheckableTag
key={tag}
checked={selectedTags.includes(tag)}
onChange={(checked) => handleChange(tag, checked)}
>
{tag}
</CheckableTag>
))}
<p className="mt-4">
<span className="font-semibold">已选分类: </span>
{selectedTags.join(', ')}
</p>
</div>
);
};

export default CheckableTagDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import DynamicTagDemo from './components/demos/DynamicTagDemo';
import CheckableTagDemo from './components/demos/CheckableTagDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <DynamicTagDemo /> */}
<CheckableTagDemo />
</div>
</ConfigProvider>
);
};

通过这三个层层递进的示例,我们已经完全掌握了 Tag 组件从静态展示到动态管理,再到特殊选择形态的全部核心用法。虽然它只是一个小组件,但却是构建精细化、高信息密度界面的关键一环。


7.12.2. Badge: 角标信息的聚合与传达

学习了 Tag 之后,我们来掌握它的“近亲”——Badge (徽标数)。如果说 Tag 是一个独立的信息标签,那么 Badge 则更像是一个 附属的、用于提醒的角标。它通常依附于另一个元素的角落,以一种不打扰主内容的方式,提供关键的聚合信息或状态提示。

核心应用场景:

  • 消息通知:在“小铃铛”图标右上角显示未读消息数量。
  • 购物应用:在购物车图标上显示已添加的商品数量。
  • 状态提醒:在用户头像上显示一个绿点表示“在线”,或在待办事项上显示一个红点表示“有更新”。

第一步:基础用法 - 数字与红点

Badge 最核心的功能,就是作为一个 包装组件,为它的子元素(children)添加角标。其最常见的形态是展示一个具体的数字,或一个简单的“小红点”。

核心属性:

  • count: 需要展示的数字或内容
  • overflowCount: 封顶数值。当 count 超过这个值时,会自动显示为 99+(默认)。
  • dot: 只展示一个红点,不展示具体数字。适用于仅需提示“有更新”而无需关心具体数量的场景。dot 的优先级高于 count
  • showZero: 当 count0 时,默认不显示徽标。设置此属性为 true 可以强制显示 0

文件路径: src/components/demos/BasicBadgeDemo.tsx (新建文件)

image-20250929152409427

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React from 'react';
import { Avatar, Badge, Space } from 'antd';
import { BellOutlined, ShoppingCartOutlined } from '@ant-design/icons';

const BasicBadgeDemo: React.FC = () => (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">基础徽标用法</h3>
<Space size="large">
{/* 1. 基础用法:为 BellOutlined 图标添加数字角标 */}
<Badge count={5}>
<Avatar shape="square" size="large" icon={<BellOutlined />} />
</Badge>

{/* 2. 封顶数字:count 为 100,超过默认的 overflowCount={99} */}
<Badge count={100}>
<Avatar shape="square" size="large" icon={<ShoppingCartOutlined />} />
</Badge>

{/* 3. 自定义封顶数字 */}
<Badge count={1000} overflowCount={999}>
<Avatar shape="square" size="large" icon={<ShoppingCartOutlined />} />
</Badge>

{/* 4. 小红点:不关心具体数量,只提示有更新 */}
<Badge dot>
<Avatar shape="square" size="large" icon={<BellOutlined />} />
</Badge>

{/* 5. 强制显示0 */}
<Badge count={0} showZero>
<Avatar shape="square" size="large" />
</Badge>
</Space>
</div>
);

export default BasicBadgeDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicBadgeDemo from './components/demos/BasicBadgeDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicBadgeDemo />
</div>
</ConfigProvider>
);
};

export default App;

第二步:独立使用与状态点

除了作为角标,Badge 还可以 独立使用,作为一个带有状态色彩和文本的指示器。这在展示任务状态、服务器状态等场景中非常实用。

核心属性:

  • status: 设置徽标为状态点模式。可选值为 success, processing, default, error, warning
  • text: 配合 status 使用,在状态点旁显示的文本
  • color: 当预设的状态色不满足需求时,用于自定义状态点的颜色。

文件路径: src/components/demos/StatusBadgeDemo.tsx (新建文件)

image-20250929152649819

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import { Badge, Space } from 'antd';

const StatusBadgeDemo: React.FC = () => (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">状态点</h3>
<Space direction="vertical" size="middle">
<Badge status="success" text="成功" />
<Badge status="error" text="失败" />
<Badge status="default" text="默认" />
<Badge status="processing" text="处理中" />
<Badge status="warning" text="警告" />
<Badge color="magenta" text="自定义颜色" />
</Space>
</div>
);

export default StatusBadgeDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { ConfigProvider } from "antd";
import StatusBadgeDemo from "./components/demos/StatusBadgeDemo";
import zhCN from "antd/locale/zh_CN";

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<StatusBadgeDemo />
</div>
</ConfigProvider>
);
};

export default App;

status 属性为 processing 时,徽标会呈现一个非常醒目的“呼吸”动画,非常适合用于提醒用户有后台任务正在进行中。


第三步:特殊形态 - 缎带 (Badge.Ribbon)

Badge.RibbonBadge 的一个特殊子组件,它以“缎带”的形式包裹一个容器(例如 Card),用于标记整个区块的属性,例如“新品”、“推荐”、“折扣”等。

核心属性:

  • text: 显示在缎带上的文本
  • color: 自定义缎带的颜色。
  • placement: 缎带的位置,可选值为 startend(默认为 end)。

文件路径: src/components/demos/RibbonBadgeDemo.tsx (新建文件)

image-20250929154550110

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react';
import { Badge, Card, Space } from 'antd';

const RibbonBadgeDemo: React.FC = () => (
<div className="p-8 bg-white rounded-lg shadow-lg w-[400px]">
<h3 className="text-xl font-bold mb-4 text-center">缎带徽标</h3>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Badge.Ribbon text="新品推荐" color="magenta">
<Card title="新款智能手机" size="small">
搭载最新 A20 仿生芯片,性能卓越。
</Card>
</Badge.Ribbon>
<Badge.Ribbon text="限时折扣" color="volcano" placement="start">
<Card title="高端机械键盘" size="small">
原价 ¥999,现价 ¥699。
</Card>
</Badge.Ribbon>
</Space>
</div>
);

export default RibbonBadgeDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import StatusBadgeDemo from './components/demos/StatusBadgeDemo';
import RibbonBadgeDemo from './components/demos/RibbonBadgeDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <StatusBadgeDemo /> */}
<RibbonBadgeDemo />
</div>
</ConfigProvider>
);
};

通过这三个示例,我们掌握了 Badge 组件的三种核心形态:作为数字/红点角标、作为独立的状态指示器,以及作为包裹容器的缎带。灵活运用 Badge,能够极大地提升界面信息传达的效率和视觉吸引力。


7.12.3. Avatar: 身份与标识的视觉载体

在任何涉及用户的界面中,Avatar (头像) 都是不可或缺的组件。它为用户提供了一个直观的视觉标识,极大地增强了应用的亲和力和可识别性。Ant Design 的 Avatar 组件功能强大且灵活,支持图片、图标和字符三种核心形态,能够满足绝大多数的用户标识需求。

核心应用场景:

  • 用户中心:展示当前登录用户的头像。
  • 评论列表:在每条评论前显示评论者的头像。
  • 项目成员:在一系列堆叠的头像中,展示所有参与项目的成员。
  • 内容发布者:在文章或动态旁,标识作者信息。

第一步:三种核心形态与自定义

Avatar 组件的设计核心是其内容的 多样性。通过不同的属性,我们可以轻松创建三种不同类型的头像,并对其尺寸、形状进行自定义。

核心属性/用法:

  • src: 传入一个图片 URL,创建图片头像
  • icon: 传入一个 ReactNode (通常是 antd 的 Icon),创建图标头像。常用于匿名用户或系统账户。
  • children: 传入字符串,创建字符头像。通常用于显示用户姓名的首字母。
  • size: 设置头像尺寸,可以是 small, default, large,也可以是具体的数值 number
  • shape: 设置头像形状,可选值为 circle (默认) 或 square

文件路径: src/components/demos/BasicAvatarDemo.tsx (新建文件)

image-20250929161526234

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React from 'react';
import { Avatar, Space, Divider } from 'antd';
import { AntDesignOutlined, UserOutlined } from '@ant-design/icons';

const BasicAvatarDemo: React.FC = () => (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">头像核心形态</h3>

<Divider orientation="left">三种内容类型</Divider>
<Space size="middle">
<Avatar size={64} icon={<UserOutlined />} />
<Avatar size={64}>U</Avatar>
<Avatar size={64} src="https://api.dicebear.com/7.x/miniavs/svg?seed=1" />
</Space>

<Divider orientation="left">不同尺寸</Divider>
<Space size="middle">
<Avatar size="large" icon={<UserOutlined />} />
<Avatar icon={<UserOutlined />} />
<Avatar size="small" icon={<UserOutlined />} />
<Avatar size={24} icon={<UserOutlined />} />
</Space>

<Divider orientation="left">不同形状</Divider>
<Space size="middle">
<Avatar shape="square" size={64} icon={<UserOutlined />} />
<Avatar shape="circle" size={64} icon={<UserOutlined />} />
</Space>
</div>
);

export default BasicAvatarDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicAvatarDemo from './components/demos/BasicAvatarDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicAvatarDemo />
</div>
</ConfigProvider>
);
};

export default App;

第二步:动态调整与加载失败回退

在真实项目中,我们面临两个常见问题:1) 用户名长度不一,如何让字符头像自动适应? 2) 图片链接可能失效,如何优雅地处理“破图”问题?Ant Design 对此都提供了内置的解决方案。

1. 字符头像的动态调整
正如您所建议的,一个常见的需求是截取用户名的首个字符作为头像。Avatar 组件内置了 字体大小自动调整 的逻辑。当 children 为字符串时,它会自动根据头像的 size 和字符串的长度,计算出最合适的 font-size,确保字符优雅地居中显示。

2. 图片加载失败的回退机制 (Fallback)
src 提供的图片链接加载失败时,Avatar 会自动寻找替代方案进行渲染,避免了显示浏览器默认的“破图”图标。

  • 回退优先级: `icon` \> `children`
  • 工作流程:
    1. Avatar 尝试加载 src 中的图片。
    2. 如果加载失败,它会检查是否传入了 icon 属性。如果传入了,则渲染该图标。
    3. 如果没传入 icon,它会继续检查是否传入了 children。如果传入了,则渲染该字符。
    4. 如果 iconchildren 都没有,则会渲染一个默认的 UserOutlined 图标。

文件路径: src/components/demos/AdvancedAvatarDemo.tsx (新建文件)

image-20250929161557420

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import React, { useState } from 'react';
import { Avatar, Slider, Space, Divider, Button } from 'antd';
import { UserOutlined } from '@ant-design/icons';

const AdvancedAvatarDemo: React.FC = () => {
const [size, setSize] = useState(64);
const [name, setName] = useState('Prorise');

// 动态获取名字首字母
const getInitial = (str: string) => (str ? str[0].toUpperCase() : '');

return (
<div className="p-8 bg-white rounded-lg shadow-lg max-w-lg">
<h3 className="text-xl font-bold mb-4 text-center">动态调整与回退</h3>

<Divider orientation="left">字符头像自动调整</Divider>
<Space direction="vertical" style={{ width: '100%' }}>
<Space wrap>
<Avatar size={size}>{getInitial(name)}</Avatar>
<Button onClick={() => setName('Prorise Blog')}>换个长名字</Button>
<Button onClick={() => setName('Ant')}>换个短名字</Button>
</Space>
<Slider
value={size}
onChange={setSize}
min={24}
max={128}
step={1}
/>
<span>当前尺寸: {size}px</span>
</Space>

<Divider orientation="left">图片加载失败回退</Divider>
<Space size="large">
<div>
<Avatar
size={64}
src="https://a.bad.url/not-exist.png" // 无效的图片地址
icon={<UserOutlined />} // 提供了 icon 作为回退
/>
<p className="text-center text-xs mt-1">回退到 icon</p>
</div>
<div>
<Avatar
size={64}
src="https://a.bad.url/not-exist.png" // 无效的图片地址
>
P
</Avatar>
<p className="text-center text-xs mt-1">回退到 children</p>
</div>
<div>
<Avatar
size={64}
src="https://a.bad.url/not-exist.png" // 无效的图片地址
icon={<UserOutlined />}
>
P
</Avatar>
<p className="text-center text-xs mt-1">icon 优先</p>
</div>
</Space>
</div>
);
};

export default AdvancedAvatarDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import BasicAvatarDemo from './components/demos/BasicAvatarDemo';
import AdvancedAvatarDemo from './components/demos/AdvancedAvatarDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <BasicAvatarDemo /> */}
<AdvancedAvatarDemo />
</div>
</ConfigProvider>
);
};

第三步:组合形态 - 头像组 (Avatar.Group)

当需要展示多个相关用户(如项目成员、群组成员)时,将多个头像堆叠在一起是一种非常节省空间且美观的模式。Avatar.Group 子组件就是为此而生的。

核心属性:

  • max.count: 设置最多显示的头像数量。超出该数量的头像将被收起到一个 +N 的徽标中。
  • max.style: 用于自定义 +N 徽标的样式。
  • max.popover: 用于配置 `+N` 徽标的 Popover 行为,方便在悬浮时展示所有被折叠的用户。

文件路径: src/components/demos/GroupAvatarDemo.tsx (新建文件)

image-20250929161619804

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React from 'react';
import { Avatar, Tooltip, Divider } from 'antd';
import { UserOutlined } from '@ant-design/icons';

const GroupAvatarDemo: React.FC = () => (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">头像组</h3>

<Divider orientation="left">基础头像组</Divider>
<Avatar.Group>
<Avatar src="https://api.dicebear.com/7.x/miniavs/svg?seed=1" />
<Avatar style={{ backgroundColor: '#f56a00' }}>K</Avatar>
<Tooltip title="Ant User" placement="top">
<Avatar style={{ backgroundColor: '#87d068' }} icon={<UserOutlined />} />
</Tooltip>
<Avatar style={{ backgroundColor: '#1677ff' }} icon={<UserOutlined />} />
</Avatar.Group>

<Divider orientation="left">限制数量与 Popover</Divider>
<Avatar.Group
max={{
count: 2,
popover: { title: '所有成员', trigger: 'hover' },
}}
>
<Avatar src="https://api.dicebear.com/7.x/miniavs/svg?seed=1" />
<Avatar style={{ backgroundColor: '#f56a00' }}>K</Avatar>
<Tooltip title="Ant User" placement="top">
<Avatar style={{ backgroundColor: '#87d068' }} icon={<UserOutlined />} />
</Tooltip>
<Avatar style={{ backgroundColor: '#1677ff' }} icon={<UserOutlined />} />
</Avatar.Group>
</div>
);

export default GroupAvatarDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import AdvancedAvatarDemo from './components/demos/AdvancedAvatarDemo';
import GroupAvatarDemo from './components/demos/GroupAvatarDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <AdvancedAvatarDemo /> */}
<GroupAvatarDemo />
</div>
</ConfigProvider>
);
};

通过 Avatar.Group,我们可以轻松实现优雅的成员列表展示,并通过 max 属性的精细化配置,在简洁性与完整性之间取得完美平衡。掌握 Avatar 的这些用法,将为您的应用增添更多人性化的细节。


7.12.4. QRCode: 信息的编码与分享

在移动互联网时代,QRCode (二维码) 已成为连接线上与线下、桌面端与移动端不可或缺的桥梁。Ant Design 的 QRCode 组件提供了一个极其简单的方式,在您的 React 应用中动态生成二维码,将任意文本信息(通常是 URL)编码成可被移动设备快速扫描的图形。

核心应用场景:

  • 扫码登录:在 PC 网站上显示二维码,用户使用手机 App 扫码后即可授权登录。
  • App 下载:引导用户扫描二维码,直接跳转到应用商店的下载页面。
  • 内容分享:将当前页面的链接生成二维码,方便用户分享给他人。
  • 支付收款:展示收款二维码,简化支付流程。

第一步:基础用法与动态生成

生成一个二维码最少只需要一个属性:value。这是一个完全由数据驱动的组件,value 属性的任何变化都会立刻反映为二维码图形的更新。

核心属性:

  • value: 需要被编码的字符串。这是 QRCode 组件唯一必需的属性。

为了让体验更直观,我们将创建一个带有输入框的示例,让您可以实时修改 value 并观察二维码的变化。

文件路径: src/components/demos/BasicQRCodeDemo.tsx (新建文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { useState } from 'react';
import { QRCode, Input, Space } from 'antd';

const BasicQRCodeDemo: React.FC = () => {
const [text, setText] = useState('https://ant.design/');

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-[400px]">
<h3 className="text-xl font-bold mb-4 text-center">基础二维码</h3>
<Space direction="vertical" align="center" style={{ width: '100%' }}>
{/* 1. QRCode 组件接收 text state 作为 value */}
<QRCode value={text || '-'} />
{/* 2. Input 输入框用于动态修改 text state */}
<Input
placeholder="请输入链接或文本"
maxLength={150}
value={text}
onChange={(e) => setText(e.target.value)}
/>
</Space>
</div>
);
};

export default BasicQRCodeDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicQRCodeDemo from './components/demos/BasicQRCodeDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicQRCodeDemo />
</div>
</ConfigProvider>
);
};

export default App;

第二步:自定义外观 - 尺寸、颜色与图标

为了让二维码更好地融入您的产品设计,QRCode 组件提供了丰富的外观自定义选项。

核心属性:

  • size: 设置二维码的尺寸,单位为 px,默认为 160。
  • color: 设置二维码前景色(深色部分)。
  • bgColor: 设置二维码背景色(浅色部分)。
  • icon: 在二维码中心嵌入一个 Logo 或图标,通常为图片 URL。
  • iconSize: 控制中心 icon 的尺寸。

可用性提示:在自定义 colorbgColor 时,请务必保持足够的颜色对比度,否则可能导致二维码难以被扫描识别。

文件路径: src/components/demos/CustomQRCodeDemo.tsx (新建文件)

image-20250929170753460

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React from 'react';
import { QRCode, Space, Divider } from 'antd';

const CustomQRCodeDemo: React.FC = () => (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">自定义外观</h3>
<Space size="large" wrap>
<div>
<p className="text-center mb-2">自定义尺寸</p>
<QRCode value="https://ant.design/" size={100} />
</div>
<div>
<p className="text-center mb-2">自定义颜色</p>
<QRCode
value="https://ant.design/"
color="#1677ff"
bgColor="#e6f4ff"
/>
</div>
<div>
<p className="text-center mb-2">嵌入图标</p>
<QRCode
value="https://ant.design/"
icon="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
/>
</div>
</Space>
</div>
);

export default CustomQRCodeDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import BasicQRCodeDemo from './components/demos/BasicQRCodeDemo';
import CustomQRCodeDemo from './components/demos/CustomQRCodeDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <BasicQRCodeDemo /> */}
<CustomQRCodeDemo />
</div>
</ConfigProvider>
);
};

第三步:状态与纠错等级

QRCode 不仅是一个静态的图形,它还可以拥有自己的“状态”,例如“已过期”、“加载中”。同时,我们还可以设置其“纠错等级”,这在嵌入图标时尤为重要。

核心属性:

  • status: 设置二维码的当前状态。可选值为 active (默认), expired (已过期), loading (加载中), scanned (已扫描)。组件会根据不同状态渲染不同的遮罩层。
  • onRefresh: 当 statusexpired 时,用户点击“点击刷新”的回调函数。
  • errorLevel: 设置二维码的纠错等级。可选值为 'L', 'M', 'Q', 'H',纠错能力依次增强。

纠错等级 (Error Level):
二维码的一部分被遮挡后仍能被正常扫描的能力。等级越高,可被遮挡的面积越大,但二维码的像素点也会越密集。

  • L: ~7%
  • M: ~15% (默认)
  • Q: ~25%
  • H: ~30%

最佳实践:当您使用 icon 属性在二维码中嵌入图标时,强烈建议errorLevel 设置为 'H'。因为图标本身就遮挡了一部分二维码信息,提高纠错等级可以确保二维码在有遮挡的情况下依然保持高识别率。

文件路径: src/components/demos/AdvancedQRCodeDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import React, { useState } from 'react';
import { QRCode, Space, Button, message, Divider } from 'antd';

const AdvancedQRCodeDemo: React.FC = () => {
const [status, setStatus] = useState<"active" | "expired" | "loading" | "scanned">('active');

const handleRefresh = () => {
message.success('二维码已刷新!');
setStatus('active');
}

return (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">状态与纠错等级</h3>

<Divider orientation="left">状态控制</Divider>
<Space direction="vertical" align="center">
<QRCode
value="https://ant.design/"
status={status}
onRefresh={handleRefresh}
/>
<Button.Group>
<Button onClick={() => setStatus('active')}>设为有效</Button>
<Button onClick={() => setStatus('scanned')}>设为已扫描</Button>
<Button onClick={() => setStatus('expired')}>设为过期</Button>
</Button.Group>
</Space>

<Divider orientation="left">纠错等级 (嵌入图标时推荐 H)</Divider>
<Space size="large">
<div>
<p className="text-center mb-2">默认 (M)</p>
<QRCode
value="https://ant.design/"
icon="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
errorLevel="M"
/>
</div>
<div>
<p className="text-center mb-2">高纠错 (H)</p>
<QRCode
value="https://ant.design/"
icon="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
errorLevel="H"
/>
</div>
</Space>
</div>
);
};

export default AdvancedQRCodeDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import CustomQRCodeDemo from './components/demos/CustomQRCodeDemo';
import AdvancedQRCodeDemo from './components/demos/AdvancedQRCodeDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <CustomQRCodeDemo /> */}
<AdvancedQRCodeDemo />
</div>
</ConfigProvider>
);
};

通过掌握 QRCode 的基础用法、外观定制以及高级的状态和纠错等级配置,您可以轻松地在应用中集成强大而美观的二维码功能,有效打通不同设备和场景间的信息壁垒。


7.12.5. Image: 优雅的图片展示与交互

在现代 Web 应用中,图片是内容传达不可或缺的一部分。然而,原生的 <img> 标签功能有限,无法很好地处理加载失败、大图预览等常见场景。Ant Design 的 Image 组件正是为此而生,它在原生功能的基础上,内置了 加载占位失败回退点击预览 以及 相册模式 等一系列强大功能,极大提升了图片展示的用户体验。

核心应用场景:

  • 文章内容插图:在文章中嵌入图片,并提供点击查看大图的功能。
  • 商品详情页:展示商品的多张图片,并以相册的形式进行预览切换。
  • 个人资料页:展示用户上传的个人照片墙。

第一步:基础用法与容错处理

Image 组件的基础用法与 <img> 标签非常相似,但它通过 fallback 属性,提供了一种优雅处理图片加载失败的机制。

核心属性:

  • src: 图片的 URL 地址
  • width / height: 设置图片显示的宽度和高度。
  • fallback: 当 `src` 加载失败时,用于替代显示的容错图片 URL
  • placeholder: 加载占位符。当设置为 true 时,会在图片加载过程中显示一个默认的占位动画。

文件路径: src/components/demos/BasicImageDemo.tsx (新建文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React from 'react';
import { Image, Space } from 'antd';

const BasicImageDemo: React.FC = () => (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">基础用法与容错</h3>
<Space size="large" wrap>
<div>
<p className="text-center mb-2">基础用法</p>
<Image
width={200}
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
/>
</div>
<div>
<p className="text-center mb-2">加载失败回退</p>
<Image
width={200}
src="https://a.bad.url/error.png" // 故意使用一个无效链接
fallback="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png?x-oss-process=image/blur,r_50,s_50/quality,q_1/resize,m_mfit,h_200,w_200"
/>
</div>
<div>
<p className="text-center mb-2">渐进加载 (Placeholder)</p>
<Image
width={200}
src="https://images.unsplash.com/photo-1593642532454-e138e28a63f4?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8d29ya3xlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&w=1000&q=80" // 一个较大的图片
placeholder
/>
</div>
</Space>
</div>
);

export default BasicImageDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicImageDemo from './components/demos/BasicImageDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicImageDemo />
</div>
</ConfigProvider>
);
};

export default App;

第二步:核心功能 - 图片预览与相册模式 (PreviewGroup)

Image 组件最强大的功能之一就是其内置的预览能力。默认情况下,所有 Image 组件都是可以点击预览的。当我们需要将页面上 一组 相关的图片组织成一个相册,让用户可以在预览模式下左右切换时,就需要使用 Image.PreviewGroup

核心用法:

  • 将多个 Image 组件包裹在 <Image.PreviewGroup> 标签内。
  • Image.PreviewGroup 会自动收集所有子 Imagesrc,形成一个图片数组。
  • 点击其中任意一张图片,都会以相册模式打开预览,并允许用户在所有图片间导航。

文件路径: src/components/demos/GroupImageDemo.tsx (新建文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React from 'react';
import { Image, Space } from 'antd';

const GroupImageDemo: React.FC = () => (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">多图预览 (相册模式)</h3>
<Image.PreviewGroup>
<Space size="large" wrap>
<Image
width={200}
src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
/>
<Image
width={200}
src="https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg"
/>
<Image
width={200}
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
/>
</Space>
</Image.PreviewGroup>
</div>
);

export default GroupImageDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import BasicImageDemo from './components/demos/BasicImageDemo';
import GroupImageDemo from './components/demos/GroupImageDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <BasicImageDemo /> */}
<GroupImageDemo />
</div>
</ConfigProvider>
);
};

第三步:受控预览与自定义

在某些复杂场景下,我们可能需要通过 编程方式 来控制预览的打开和关闭,而不是依赖用户的点击。例如,点击一个按钮来打开一个图片相册。这可以通过 preview 属性实现受控。

核心属性:

  • preview: 预览参数配置,可以是一个对象
  • preview.visible: 控制预览模态框的显示与隐藏。
  • preview.onVisibleChange: 当 visible 状态变化时(用户手动关闭预览)的回调,用于同步我们自己的 state。
  • preview.current: 在 PreviewGroup 中,用于指定默认打开哪一张图片。

文件路径: src/components/demos/ControlledImageDemo.tsx (新建文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import React, { useState } from "react";
import { Image, Button, Space } from "antd";

const imageList = [
"https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png",
"https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg",
"https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg",
];

const ControlledImageDemo: React.FC = () => {
const [visible, setVisible] = useState(false);
const [current, setCurrent] = useState(0);

const showPreview = (index: number) => {
setCurrent(index);
setVisible(true);
};

const previewProps = {
visible,
current,
onVisibleChange: (vis: boolean) => setVisible(vis),
};

return (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">受控预览</h3>
<Space>
{imageList.map((imgSrc, index) => (
<Button key={imgSrc} onClick={() => showPreview(index)}>
查看图片 {index + 1}
</Button>
))}
</Space>

{/* 这是一个隐藏的 PreviewGroup */}
<div className="hidden">
<Image.PreviewGroup preview={previewProps}>
{imageList.map((imgSrc, index) => (
<Image key={imgSrc} src={imgSrc} />
))}
</Image.PreviewGroup>
</div>
</div>
);
};

export default ControlledImageDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import GroupImageDemo from './components/demos/GroupImageDemo';
import ControlledImageDemo from './components/demos/ControlledImageDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <GroupImageDemo /> */}
<ControlledImageDemo />
</div>
</ConfigProvider>
);
};

通过 preview 对象的精细化配置,我们获得了对预览行为的完全控制权。Image 组件的这些高级功能,使其不仅仅是一个图片展示工具,更是一个功能完备的图片交互解决方案。


7.12.6. Tooltip: 轻量级的悬浮提示

Tooltip (文字提示) 是 UI 中用于提供“即时帮助”的最常用组件。当界面上的某个元素功能不够直观(如图标按钮),或者信息无法完全展示时(如被截断的文本),Tooltip 能在用户鼠标悬浮时,提供一个轻量级的、非侵入式的文字气泡,用以补充说明。

核心应用场景:

  • 解释图标按钮:为一个删除(垃圾桶)图标按钮提供 “删除此项” 的文字提示。
  • 显示完整文本:当表格单元格或标签因空间不足而截断文本时,用 Tooltip 显示完整内容。
  • 提供额外信息:为一个禁用的按钮解释其不可用的原因。

Tooltip vs Popover

  • Tooltip: 设计用来承载 简单的、纯文本 的提示。它的结构轻量,交互单一(悬浮显示/移出隐藏)。
  • Popover: 设计用来承载 更复杂的内容,例如标题、正文、按钮组或表单。它是一个功能更全面的“气泡卡片”。

第一步:基础用法

Tooltip 的本质是一个 包装组件。您只需要将需要触发提示的目标元素(children)包裹在 <Tooltip> 标签内,并通过 title 属性提供提示的文本内容即可。

核心属性:

  • title: 提示的文本内容。这是 Tooltip 组件唯一必需的属性。

文件路径: src/components/demos/BasicTooltipDemo.tsx (新建文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React from 'react';
import { Button, Tooltip } from 'antd';
import { DeleteOutlined } from '@ant-design/icons';

const BasicTooltipDemo: React.FC = () => (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">基础用法</h3>
<div className="flex justify-center items-center gap-4">
{/* 1. 包裹一个文本元素 */}
<Tooltip title="这是一个文字提示">
<span>悬浮在我上方</span>
</Tooltip>

{/* 2. 包裹一个按钮,解释其功能 */}
<Tooltip title="这是一个普通的按钮">
<Button>按钮</Button>
</Tooltip>

{/* 3. 最常见的用法:解释一个图标按钮 */}
<Tooltip title="删除">
<Button danger shape="circle" icon={<DeleteOutlined />} />
</Tooltip>
</div>
</div>
);

export default BasicTooltipDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicTooltipDemo from './components/demos/BasicTooltipDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicTooltipDemo />
</div>
</ConfigProvider>
);
};

export default App;

重要提示Tooltip 的触发依赖于其子元素的 onMouseEnter, onMouseLeave 等事件。如果子元素本身被禁用了(例如 <Button disabled />),它将无法响应这些鼠标事件,从而导致 Tooltip 不会显示。
解决方案:在禁用的元素外再包裹一层 <span> 标签,将 Tooltip 应用于这个 <span> 上。
<Tooltip title="..."><span className="cursor-not-allowed"><Button disabled>...</Button></span></Tooltip>


第二步:位置与自定义样式

为了应对复杂的布局,Tooltip 提供了 12 个 不同的弹出位置。同时,您也可以自定义其背景颜色以更好地融入您的设计系统。

核心属性:

  • placement: 设置提示框的弹出位置。例如 top, left, bottomRight 等。
  • color: 自定义提示框的背景颜色。antd 内置了多种预设颜色,也支持传入标准的 HEX 色值。
  • arrow: 控制是否显示指向目标元素的箭头。

文件路径: src/components/demos/PlacementTooltipDemo.tsx (新建文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import React from 'react';
import { Button, Tooltip, Divider, Space, Row, Col } from 'antd';

const BasicTooltipDemo: React.FC = () => (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">位置与颜色</h3>

<Divider orientation="left">4 个弹出位置</Divider>
<div className="w-[400px]">
<Row justify="center" style={{ marginBottom: 8 }}>
<Col>
<Tooltip placement="top" title="顶部提示">
<Button>Top</Button>
</Tooltip>
</Col>
</Row>
<Row justify="space-between" style={{ marginBottom: 8 }}>
<Col>
<Tooltip placement="left" title="左侧提示">
<Button>Left</Button>
</Tooltip>
</Col>
<Col>
<Tooltip placement="right" title="右侧提示">
<Button>Right</Button>
</Tooltip>
</Col>
</Row>
<Row justify="center">
<Col>
<Tooltip placement="bottom" title="底部提示">
<Button>Bottom</Button>
</Tooltip>
</Col>
</Row>
</div>

<Divider orientation="left">多彩提示</Divider>
<Space wrap>
<Tooltip title="温柔的粉色主题" color="pink">
<Button>粉色</Button>
</Tooltip>
<Tooltip title="热情的红色主题" color="red">
<Button>红色</Button>
</Tooltip>
<Tooltip title="活力的橙色主题" color="orange">
<Button>橙色</Button>
</Tooltip>
<Tooltip title="清新的蓝色主题" color="#108ee9">
<Button>蓝色</Button>
</Tooltip>
</Space>
</div>
);

export default BasicTooltipDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import BasicTooltipDemo from './components/demos/BasicTooltipDemo';
import PlacementTooltipDemo from './components/demos/PlacementTooltipDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <BasicTooltipDemo /> */}
<PlacementTooltipDemo />
</div>
</ConfigProvider>
);
};

通过灵活运用 placementcolor 属性,您可以让 Tooltip 精准地出现在期望的位置,并与您的整体 UI 风格保持一致。掌握 Tooltip 是提升应用易用性和细节体验的重要一步。


7.12.7. Popover: 承载复杂内容的浮层卡片

Popover (气泡卡片) 是 Tooltip 的功能扩展。当一个简单的文字提示(Tooltip)已不足以承载所需信息,而您希望在一个浮层中展示 标题、多行文本、链接、按钮 甚至更复杂的组件时,Popover 便是最佳选择。它同样由用户操作(如悬浮或点击)触发,但提供了一个功能更完备的“卡片”作为容器。

核心应用场景:

  • 用户卡片:点击用户头像,弹出一个包含用户名、简介和“关注”按钮的 Popover
  • 快捷操作:点击表格行末尾的“更多”(...)图标,弹出一个包含“编辑”、“删除”等操作链接的 Popover
  • 产品快速预览:在电商列表中,鼠标悬浮于商品图片上,弹出一个包含商品名称、价格和“加入购物车”按钮的 Popover

第一步:基础用法 - 标题与内容

Tooltip 类似,Popover 也是一个包装组件。但它提供了两个核心属性来构建卡片式结构:title 用于定义卡片头部,content 用于填充卡片主体。content 属性的强大之处在于它可以接收任意的 ReactNode

核心属性:

  • title: 设置气泡卡片的标题
  • content: 设置气泡卡片的主体内容。这里可以放入文本、链接、按钮等任意 React 元素。

文件路径: src/components/demos/BasicPopoverDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React from 'react';
import { Button, Popover } from 'antd';

const BasicPopoverDemo: React.FC = () => {
const popoverTitle = <span>个人资料</span>;

const popoverContent = (
<div>
<p>姓名:张三</p>
<p>职位:前端开发工程师</p>
<a href="https://ant.design" target="_blank" rel="noopener noreferrer">
查看更多
</a>
</div>
);

return (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">基础用法</h3>
<div className="flex justify-center">
<Popover
content={popoverContent}
title={popoverTitle}
trigger="hover" // 默认触发方式是 hover
>
<Button type="primary">悬浮以显示用户信息</Button>
</Popover>
</div>
</div>
);
};

export default BasicPopoverDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicPopoverDemo from './components/demos/BasicPopoverDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicPopoverDemo />
</div>
</Provider>
);
};

export default App;

第二步:触发方式与受控模式

Popover 提供了多种方式来控制其显示和隐藏,以适应不同的交互需求。

1. 触发方式 (trigger)

  • trigger: 设置触发 Popover 显示的行为
    • hover: 鼠标悬浮(默认)。
    • focus: 元素获得焦点时,通常用于输入框。
    • click: 鼠标点击时。
    • contextMenu: 鼠标右键点击时。

2. 受控模式
在某些场景下,我们需要通过编程来精确控制 Popover 的可见性,例如,实现一个可以从浮层内部关闭自身的 Popover。这需要使用受控模式。

  • open: 使用 React State 来控制 Popover 是否可见
  • onOpenChange: 当 Popover 的可见状态因用户操作(如点击外部)而应发生变化时,触发此回调。我们需要在此回调中更新 open 绑定的 state。

文件路径: src/components/demos/TriggerPopoverDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { useState } from "react";
import { Button, Popover, Divider, Space, Input } from "antd";

const TriggerPopoverDemo: React.FC = () => {
const [open, setOpen] = useState(false);

const hide = () => {
setOpen(false);
};

const handleOpenChange = (newOpen: boolean) => {
setOpen(newOpen);
};

return (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">触发方式与受控</h3>
<Divider orientation="left">受控模式 (从浮层内关闭)</Divider>
<Space>
<Popover
content={<a onClick={hide}>点我关闭</a>}
title="这是一个受控的 Popover"
trigger="click"
open={open}
onOpenChange={handleOpenChange}
>
<Button type="primary">点击我</Button>
</Popover>
</Space>
</div>
);
};
export default TriggerPopoverDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// import BasicPopoverDemo from './components/demos/BasicPopoverDemo';
import { ConfigProvider } from 'antd';
import TriggerPopoverDemo from './components/demos/TriggerPopoverDemo';
import zhCN from 'antd/locale/zh_CN';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <BasicPopoverDemo /> */}
<TriggerPopoverDemo />
</div>
</ConfigProvider>
);
};

export default App;

通过 trigger 属性,我们可以轻松适配不同的交互场景。而受控模式则为我们实现复杂的、需要内部状态与外部状态同步的 Popover 交互提供了可能,是构建高级浮层操作面板的关键。掌握 Popover,意味着您已经具备了在有限的界面空间内,优雅地组织和呈现丰富信息与操作的能力。


7.12.8. Statistic: 关键数据的强调与展示

Statistic (统计数值) 是一个专门用于 突出展示关键绩效指标 (KPI) 或特定数值 的组件。与简单地将数字作为文本显示不同,Statistic 提供了一套标准的视觉结构(标题、数值、前/后缀),让数据一目了然,极大地增强了信息的可读性和视觉冲击力。它是各类仪表盘、数据报表和概览页面的核心构成元素。

核心应用场景:

  • 仪表盘 KPI:展示“今日活跃用户”、“本月销售额”、“同比增长率”等核心指标。
  • 个人中心:显示用户的“账户余额”、“积分”、“等级”等信息。
  • 活动页面:以倒计时的形式展示“距活动结束还剩”的时间。

第一步:基础用法与格式化

Statistic 的基础用法非常直观,通过 titlevalue 两个核心属性即可定义一个统计项。更进一步,我们可以通过前缀、后缀、精度控制等属性,对数值进行丰富的格式化。

核心属性:

  • title: 数值的标题,说明该数字的含义
  • value: 需要展示的核心数值
  • prefix: 在数值 添加的前缀,可以是文本或图标(如货币符号 $)。
  • suffix: 在数值 添加的后缀,可以是文本或图标(如单位 或百分号 %)。
  • precision: 设置数值的小数点精度。
  • valueStyle: 用于单独设置数值区域的 CSS 样式,常用于根据数值正负改变颜色。

文件路径: src/components/demos/BasicStatisticDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import React, { useState } from "react";
import { Statistic, Card, Row, Col, Slider } from "antd";
import {
LikeOutlined,
} from "@ant-design/icons";

const BasicStatisticDemo: React.FC = () => {
const [rating, setRating] = useState(93);

const getRatingColor = (value: number) => {
if (value < 40) return "#ff4d4f"; // 红色 - 差
if (value < 70) return "#faad14"; // 橙色 - 一般
return "#3f8600"; // 绿色 - 好
};

return (
<div className="p-8 bg-gray-100 rounded-lg shadow-inner w-[800px]">
<h3 className="text-xl font-bold mb-4 text-center">基础用法与格式化</h3>
<Row gutter={16}>
<Col span={12}>
<Card>
<Statistic title="活跃用户 (个)" value={112893} />
</Card>
</Col>
<Col span={12}>
<Card>
<Statistic title="账户余额 (元)" value={112893.2} precision={2} />
</Card>
</Col>
<Col span={12}>
<Card>
<Statistic
title="好评率"
value={rating}
precision={2}
valueStyle={{ color: getRatingColor(rating) }}
prefix={<LikeOutlined />}
suffix="%"
/>
<div style={{ marginTop: 16 }}>
<Slider
min={0}
max={100}
value={rating}
onChange={setRating}
/>
</div>
</Card>
</Col>
</Row>
</div>
);
};

export default BasicStatisticDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicStatisticDemo from './components/demos/BasicStatisticDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicStatisticDemo />
</div>
</ConfigProvider>
);
};

export default App;

第二步:动态数值 - 计时器 (Statistic.Timer)

除了展示静态数值,Statistic 还提供了一个强大的子组件 Statistic.Timer,专门用于处理动态变化的计时需求,如倒计时和正计时。

版本提示 (2025 年): 旧版的 Statistic.Countdown 组件在 antd v5.25.0 后已被废弃。请 始终使用 功能更全面、API 更统一的 Statistic.Timer 组件。

核心属性:

  • type: 设置计时器类型'countdown' 表示倒计时,'countup' 表示正计时。
  • value: 计时器的目标或起始时间戳。通常使用 Date.now() 加上或减去一个时间段来设定。
  • format: 格式化时间的字符串,遵循 day.js 的格式规范 (如 HH:mm:ss)。
  • onFinish: 仅在 type='countdown' 时有效,当倒计时结束时触发的回调函数。

文件路径: src/components/demos/TimerStatisticDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import React from 'react';
import { Statistic, Card, Row, Col, message } from 'antd';
import dayjs from 'dayjs';

const { Timer } = Statistic;

// 设置一个未来时间点用于倒计时(当前时间 + 30秒)
const deadline = dayjs().add(30, 'second').valueOf();
// 设置一个过去时间点用于正计时(当前时间 - 1小时)
const startTime = dayjs().subtract(1, 'hour').valueOf();

const TimerStatisticDemo: React.FC = () => {
const onFinish = () => {
message.success('倒计时结束!');
};

return (
<div className="p-8 bg-gray-100 rounded-lg shadow-inner w-[800px]">
<h3 className="text-xl font-bold mb-4 text-center">计时器</h3>
<Row gutter={16}>
<Col span={12}>
<Card>
<Timer
title="活动倒计时"
type="countdown"
value={deadline}
onFinish={onFinish}
format="HH:mm:ss"
/>
</Card>
</Col>
<Col span={12}>
<Card>
<Timer
title="通话时长"
type="countup"
value={startTime}
format="HH:mm:ss"
/>
</Card>
</Col>
</Row>
</div>
);
};

export default TimerStatisticDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import BasicStatisticDemo from './components/demos/BasicStatisticDemo';
import TimerStatisticDemo from './components/demos/TimerStatisticDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <BasicStatisticDemo /> */}
<TimerStatisticDemo />
</div>
</ConfigProvider>
);
};

Statistic.Timer 内部封装了 requestAnimationFramesetInterval 等复杂的计时和重渲染逻辑,让我们能够以极低的成本,在界面中实现精准、高效的动态计时功能。掌握 Statistic 组件,是构建数据驱动型应用和信息可视化界面的重要一步。


7.12.9. Empty: 优雅地处理“无”

在任何数据驱动的应用中,“没有数据”是一种常态,而非例外。Empty (空状态) 组件的使命,就是将这种原本冰冷、空白的界面,转化为一个友好的、信息明确的、甚至能引导用户进行下一步操作的交互节点。它避免了用户在看到空白页面时产生“是出错了还是真的没有?”的困惑。

核心应用场景:

  • 列表为空:当表格、列表或卡片组在当前筛选条件下没有数据时。
  • 搜索无结果:当用户执行搜索后,没有找到任何匹配项。
  • 初始化引导:在一个新功能或新项目中,没有任何内容时,引导用户“创建第一个项目”。

无处不在的 Empty:Ant Design 的美妙之处在于,Empty 组件已经 内置 到了所有可能出现空状态的组件中,例如 Table, List, Select, Cascader 等。当这些组件的 dataSource 为空时,它们会自动渲染一个默认的 Empty 状态。因此,我们单独学习它,更多是为了在自定义布局或全局替换默认样式时使用。


第一步:基础用法与不同样式

Empty 组件最基础的用法就是直接渲染它,它会显示一个默认的插图和“暂无数据”的描述。Ant Design 还提供了一个更简洁的 simple 样式,适用于空间有限的场景。

核心属性:

  • image: 设置显示的图片。可以直接传入 antd 内置的 Empty.PRESENTED_IMAGE_SIMPLE,也可以传入一个图片 URL 字符串。
  • description: 自定义图片下方的描述文本。传入 false 可以隐藏描述。

文件路径: src/components/demos/BasicEmptyDemo.tsx (新建文件)

image-20250930083817771

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react';
import { Empty, Divider } from 'antd';

const BasicEmptyDemo: React.FC = () => (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">基础用法与内置样式</h3>

<Divider orientation="left">默认样式</Divider>
<Empty />

<Divider orientation="left">简洁样式 (Simple)</Divider>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />

<Divider orientation="left">无描述</Divider>
<Empty description={false} />
</div>
);

export default BasicEmptyDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicEmptyDemo from './components/demos/BasicEmptyDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicEmptyDemo />
</div>
</ConfigProvider>
);
};

export default App;

第二步:完全自定义与操作引导

Empty 组件最强大的地方在于它的 高度可定制性。我们可以替换它的图片、文字,甚至可以在其下方添加 操作按钮,将一个消极的“空状态”转变为一个积极的“操作起点”。

核心用法:

  • image: 传入一个完整的 <img> 标签或任何 ReactNode 来作为自定义图片。
  • children: 在 Empty 组件的标签内部添加的任何子元素,都会被渲染在描述文字的下方。这通常是放置“创建”或“刷新”按钮的最佳位置。

文件路径: src/components/demos/CustomizeEmptyDemo.tsx (新建文件)

image-20250930084644391

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from 'react';
import { Empty, Button } from 'antd';

const CustomizeEmptyDemo: React.FC = () => (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">自定义与操作引导</h3>
<Empty
image="https://gw.alipayobjects.com/zos/antfincdn/ZHrcdLPrvN/empty.svg"
imageStyle={{ height: 60 }}
description={
<span>
暂无项目,快来 <a href="#API">创建一个</a> 吧!
</span>
}
>
<Button type="primary">立即创建</Button>
</Empty>
</div>
);

export default CustomizeEmptyDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import BasicEmptyDemo from './components/demos/BasicEmptyDemo';
import CustomizeEmptyDemo from './components/demos/CustomizeEmptyDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <BasicEmptyDemo /> */}
<CustomizeEmptyDemo />
</div>
</ConfigProvider>
);
};

通过自定义 imagedescriptionchildren,我们可以创造出完全符合产品调性和业务逻辑的空状态,将原本可能导致用户流失的空白页面,变成提升用户体验和转化率的有效触点。

至此,我们已经完成了第一阶段所有基础数据展示组件的学习。接下来,我们将进入 阶段二:内容容器组件,学习如何将这些基础组件有效地组织起来。


7.13. 布局与容器组件:构建信息骨架

摘要:在本章中,我们将学习如何使用 Ant Design 提供的容器与布局组件,将第一阶段学习的各种基础(原子化)信息有效地 组织和包裹 起来。这些组件是构建页面结构、划分信息层级的“骨架”,是从零散元素到完整页面的关键一步。

7.13.1. Card: 信息聚合的基础容器

Card (卡片) 是 Web UI 中最通用、最基础的内容容器。它的核心价值在于,将一组相关的信息(无论是文字、图片、列表还是图表)收纳在一个独立的、带有视觉边界的矩形区域内,从而为用户提供清晰、规整、易于聚焦的信息区块。

核心应用场景:

  • 仪表盘(Dashboard):每一个 KPI 指标、每一个图表,都可以用一个 Card 来承载。
  • 内容展示:文章摘要、商品信息、用户资料,都可以用 Card 来呈现。
  • 信息分组:在复杂的表单或设置页面中,使用 Card 将相关的配置项分组。

第一步:基础卡片结构

一个标准的 Card 由头部(title, extra)、内容(children)和底部(actions)三部分构成。此外,它还内置了加载状态,可以在数据获取期间显示优雅的骨架屏。

img

核心属性:

  • title: 卡片头部的标题
  • extra: 显示在卡片头部右上角的额外元素,通常是一个链接或操作按钮。
  • actions: 出现在卡片底部的操作按钮组,接收一个 React 节点数组。
  • loading: 布尔值,当为 true 时,卡片内容区会显示为加载中的骨架屏。
  • hoverable: 鼠标悬浮时显示阴影,提供可交互的视觉反馈。

文件路径: src/components/demos/BasicCardDemo.tsx (新建文件)

image-20250930085904173

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React from 'react';
import { Card, Space } from 'antd';
import { EditOutlined, EllipsisOutlined, SettingOutlined } from '@ant-design/icons';

const BasicCardDemo: React.FC = () => (
<div className="p-8 bg-gray-100 rounded-lg shadow-inner">
<h3 className="text-xl font-bold mb-4 text-center">基础卡片结构</h3>
<Space>
{/* 1. 一个标准的卡片 */}
<Card
title="标准卡片"
extra={<a href="#">更多</a>}
style={{ width: 300 }}
hoverable
actions={[
<SettingOutlined key="setting" />,
<EditOutlined key="edit" />,
<EllipsisOutlined key="ellipsis" />,
]}
>
<p>这是卡片的内容区。</p>
<p>可以放入任意文本和组件。</p>
</Card>

{/* 2. 一个加载中的卡片 */}
<Card
title="加载中..."
extra={<a href="#">More</a>}
style={{ width: 300 }}
loading={true}
>
<p>这里的内容会被骨架屏替代。</p>
</Card>
</Space>
</div>
);

export default BasicCardDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicCardDemo from './components/demos/BasicCardDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicCardDemo />
</div>
</ConfigProvider>
);
};

export default App;

第二步:丰富内容 - 封面与 Card.Meta

对于需要展示图片和结构化描述的场景(如文章、商品),Card 提供了 cover 属性和 Card.Meta 子组件来快速构建图文并茂的卡片。

核心属性/组件:

  • cover: 在卡片顶部(标题之下,内容之上)渲染一个封面图片或视频
  • Card.Meta: 一个用于生成标准元数据布局的辅助组件,它通常包含 avatar (头像)、title (标题) 和 description (描述) 三部分,是 cover 的绝佳搭档。

文件路径: src/components/demos/MetaCardDemo.tsx (新建文件)

image-20250930091143999

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React from 'react';
import { Avatar, Card } from 'antd';
import { EditOutlined, EllipsisOutlined, SettingOutlined } from '@ant-design/icons';

const { Meta } = Card;

const MetaCardDemo: React.FC = () => (
<div className="p-8 bg-gray-100 rounded-lg shadow-inner">
<h3 className="text-xl font-bold mb-4 text-center">图文卡片 (Meta)</h3>
<Card
style={{ width: 300 }}
cover={
<img
alt="example"
src="https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png"
/>
}
actions={[
<SettingOutlined key="setting" />,
<EditOutlined key="edit" />,
<EllipsisOutlined key="ellipsis" />,
]}
>
<Meta
avatar={<Avatar src="https://api.dicebear.com/7.x/miniavs/svg?seed=1" />}
title="欧洲风情"
description="这是对卡片内容的详细描述,展示在标题下方。"
/>
</Card>
</div>
);

export default MetaCardDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import MetaCardDemo from './components/demos/MetaCardDemo';


const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<MetaCardDemo />
</div>
</ConfigProvider>
);
};

export default App;

第三步:内部布局 - 网格与内联卡片

Card 不仅能作为外部容器,还提供了 Card.Gridtype="inner" 两种方式来组织其 内部 的内容。

1. Card.Grid (网格型内嵌卡片)
Card.Grid 允许您在卡片内部创建一块块 uniform 的网格。每个网格都像一个可点击的、没有边距的迷你卡片,非常适合展示一组同类的入口或项目。

2. type="inner" (内部卡片)
当您需要在一个 Card 内部,再嵌套一个带有标题和边框的、结构完整的子卡片时,可以为内部的 Card 添加 type="inner" 属性。它会以一种更紧凑、视觉上更协调的样式进行渲染。

文件路径: src/components/demos/LayoutCardDemo.tsx (新建文件)

image-20250930090932716

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import React from "react";
import { Card } from "antd";

const LayoutCardDemo: React.FC = () => {
const gridData = [
{ title: "总访问量", value: "1234" },
{ title: "新用户", value: "567" },
{ title: "订单数", value: "890" },
{ title: "销售额", value: "12.3K" },
{ title: "转化率", value: "3.2%" },
{ title: "活跃用户", value: "456" },
{ title: "增长率", value: "8.5%" },
{ title: "活跃用户", value: "456" },
];

return (
<div className="p-8 bg-gray-100 rounded-lg shadow-inner">
<h3 className="text-xl font-bold mb-4 text-center">内部布局</h3>
{/* 1. 网格型卡片 */}
<Card title="项目仪表盘" className="mb-4">
{gridData.map((item, index) => (
<Card.Grid key={index} style={{ width: "25%" }}>
<div className="text-center">
<p>{item.title}</p>
<p>{item.value}</p>
</div>
</Card.Grid>
))}
</Card>

{/* 2. 内部卡片 */}
<Card title="外部卡片">
<p>这里是外部卡片的内容。</p>
<Card type="inner" title="内部卡片标题" extra={<a href="#">更多</a>}>
这里是内部卡片的内容。
</Card>
<Card type="inner" title="另一个内部卡片" style={{ marginTop: 16 }}>
这里是另一个内部卡片的内容。
</Card>
</Card>
</div>
);
};

export default LayoutCardDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import MetaCardDemo from './components/demos/MetaCardDemo';
import LayoutCardDemo from './components/demos/LayoutCardDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <MetaCardDemo /> */}
<LayoutCardDemo />
</div>
</ConfigProvider>
);
};

通过这三个层层递进的示例,我们掌握了 Card 组件从外到内的全部核心用法。它既能作为承载万物的通用容器,又能通过 Meta, Grid 等子组件实现精细化的内部布局,是构建结构化页面的绝对主力。

7.13.2. Segmented: 视图切换的开关

Segmented (分段控制器) 是一个轻量级、现代化的选择控件。它的核心功能是在一组互斥的选项中,提供清晰的单选能力。相比于传统的 TabsRadio 按钮组,Segmented 在视觉上更紧凑、整体感更强,非常适合用作工具栏中的视图切换器或状态筛选器。

核心应用场景:

  • 视图切换:在“地图模式”与“列表模式”之间切换。
  • 数据周期选择:在“日”、“周”、“月”、“年”等时间维度间切换图表数据。
  • 状态筛选:在“全部”、“处理中”、“已完成”等状态间筛选任务列表。

第一步:基础用法与视图切换

Segmented 最核心的用途就是通过其值的变化,来驱动页面其他部分内容的条件渲染。这是一个典型的 受控组件 用法。

核心属性:

  • options: 设置分段器的所有选项。最简单的用法是传入一个字符串数组。
  • value: 当前选中的值。该属性需要绑定到一个 React State。
  • onChange: 选项变化时的回调函数。当用户点击不同选项时触发,我们需要在此函数中更新 value 绑定的 State。

文件路径: src/components/demos/BasicSegmentedDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React, { useState } from 'react';
import { Segmented, Card } from 'antd';

// 模拟不同视图的组件
const ListView = () => <Card style={{ backgroundColor: '#e6f4ff' }}>这是列表视图内容</Card>;
const CardView = () => <Card style={{ backgroundColor: '#f6ffed' }}>这是卡片视图内容</Card>;
const KanbanView = () => <Card style={{ backgroundColor: '#fffbe6' }}>这是看板视图内容</Card>;

const BasicSegmentedDemo: React.FC = () => {
// 1. 使用 useState 管理当前选中的值,默认为 'List'
const [currentView, setCurrentView] = useState<string | number>('List');

// 2. 一个渲染函数,根据当前选中的值返回对应的组件
const renderView = () => {
switch (currentView) {
case 'Card':
return <CardView />;
case 'Kanban':
return <KanbanView />;
case 'List':
default:
return <ListView />;
}
};

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-[500px]">
<h3 className="text-xl font-bold mb-4 text-center">基础用法与视图切换</h3>
<Segmented
options={['List', 'Card', 'Kanban']} // 3. 传入简单的字符串数组
value={currentView} // 4. 绑定 state
onChange={setCurrentView} // 5. 传入 state 的更新函数
block // 使分段控制器宽度撑满父容器
/>
<div className="mt-4 p-4 border rounded">
{renderView()}
</div>
</div>
);
};

export default BasicSegmentedDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicSegmentedDemo from './components/demos/BasicSegmentedDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicSegmentedDemo />
</div>
</ConfigProvider>
);
};

export default App;

这个示例完整地展示了 Segmented 的核心工作流:用户点击选项 -> onChange 触发 -> 更新 state -> state 变化导致页面重新渲染,从而显示出与新 state 匹配的视图内容。


第二步:自定义渲染 - 图标与复杂选项

当纯文本无法满足需求时,我们可以通过传入一个 对象数组options 属性,来实现更丰富的自定义渲染,例如为选项添加图标,或者禁用某个特定选项。

options 的对象结构:
{ label, value, icon, disabled, className }

  • label: 显示的文本或 React 节点
  • value: 该选项的唯一值
  • icon: 在 label 前显示的图标。
  • disabled: 禁用该选项。

文件路径: src/components/demos/CustomSegmentedDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React from 'react';
import { Segmented } from 'antd';
import { AppstoreOutlined, BarsOutlined, TableOutlined, UserOutlined } from '@ant-design/icons';

const CustomSegmentedDemo: React.FC = () => (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">自定义渲染</h3>
<Segmented
options={[
{
label: '列表',
value: 'List',
icon: <BarsOutlined />,
},
{
label: '卡片',
value: 'Card',
icon: <AppstoreOutlined />,
},
{
label: '表格',
value: 'Table',
icon: <TableOutlined />,
disabled: true, // 禁用这个选项
},
{
// 只有图标的选项
value: 'User',
icon: <UserOutlined />,
},
]}
/>
</div>
);

export default CustomSegmentedDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import BasicSegmentedDemo from './components/demos/BasicSegmentedDemo';
import CustomSegmentedDemo from './components/demos/CustomSegmentedDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <BasicSegmentedDemo /> */}
<CustomSegmentedDemo />
</div>
</ConfigProvider>
);
};

通过使用对象数组作为 options,我们获得了对每个分段项的完全控制权,可以轻松地构建出既美观又功能丰富的选择器,极大地提升了用户交互的清晰度和效率。


7.13.3. Collapse: 折叠收纳的内容面板

Collapse (折叠面板) 是一种高效的内容组织容器,它允许您将大量信息按逻辑分组,并默认将它们收起,只展示标题。用户可以按需点击标题,展开或收起对应的内容区域。这种交互模式对于保持页面整洁、减少信息过载、让用户聚焦于当前任务至关重要。

核心应用场景:

  • FAQ 页面:将每个“问题”作为面板标题,将“答案”作为可折叠的内容。
  • 配置中心:将复杂的设置项按功能分组,收纳在不同的面板中。
  • 版本日志:展示多个版本的更新记录,用户可以自由展开感兴趣的版本详情。

重要升级提示 (v5.6.0+): 在 antd v5.6.0 之后,官方 强烈推荐 使用 items 属性来数据驱动地配置面板内容。旧有的通过 JSX 嵌套 <Collapse.Panel> 的写法已被废弃。本教程将完全遵循 2025 年的最佳实践,只使用现代的 items 写法。

第一步:基础用法与数据驱动 (items)

创建 Collapse 组件的最佳方式是为其提供一个 items 数组。数组中的每一个对象都代表一个可折叠的面板,这种数据驱动的方式让代码更清晰、更易于维护。

核心属性:

  • items: 一个对象数组,用于定义所有面板。每个对象的核心结构为:
    • key: 每个面板的唯一标识符。
    • label: 显示在面板头部的标题内容。
    • children: 折叠区域内的主体内容。
  • defaultActiveKey: 一个包含 key 的数组,用于指定初始状态下哪些面板是展开的。
  • onChange: 当展开的面板发生变化时触发的回调,参数为当前所有展开面板的 key 组成的数组。

文件路径: src/components/demos/BasicCollapseDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import React from 'react';
import { Collapse } from 'antd';
import type { CollapseProps } from 'antd';

const text = `
Ant Design, a design language for background applications, is refined by Ant UED Team.
It is a set of enterprise-class UI design language and React UI library with a set of easy-to-use demos.
`;

// 1. 使用 items 数组来定义面板数据
const items: CollapseProps['items'] = [
{
key: '1',
label: '这是面板标题 1',
children: <p>{text}</p>,
},
{
key: '2',
label: '这是面板标题 2',
children: <p>{text}</p>,
},
{
key: '3',
label: '这是面板标题 3',
children: <p>{text}</p>,
},
];

const BasicCollapseDemo: React.FC = () => {
const onChange = (key: string | string[]) => {
console.log('当前展开的面板 key:', key);
};

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-[600px]">
<h3 className="text-xl font-bold mb-4 text-center">基础折叠面板</h3>
<Collapse
items={items}
defaultActiveKey={['1']}
onChange={onChange}
/>
</div>
);
};

export default BasicCollapseDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicCollapseDemo from './components/demos/BasicCollapseDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicCollapseDemo />
</div>
</ConfigProvider>
);
};

export default App;

第二步:手风琴模式与自定义

除了允许多个面板同时展开,Collapse 还支持一种特殊的“手风琴”模式,即 任何时候只允许一个面板展开。同时,我们还可以通过其他属性对样式和内容进行自定义。

核心属性:

  • accordion: {布尔值,设置为 true 即可开启手风琴模式。
  • bordered: 布尔值,设置是否显示边框,false 可创建更简洁的外观。
  • extra: 在 items 的对象中定义,用于在面板头的右上角添加额外节点(如图标或操作按钮)。
  • expandIconPosition: 设置展开/收起图标的位置,可选 startend

文件路径: src/components/demos/AccordionCollapseDemo.tsx (新建文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import React from 'react';
import { Collapse, Space } from 'antd';
import type { CollapseProps } from 'antd';
import { SettingOutlined } from '@ant-design/icons';

const text = `
A dog is a type of domesticated animal.
Known for its loyalty and faithfulness,
it can be found as a welcome guest in many households across the world.
`;

const getItems = (extraNode: React.ReactNode): CollapseProps['items'] => [
{
key: '1',
label: '面板 1',
children: <p>{text}</p>,
extra: extraNode,
},
{
key: '2',
label: '面板 2',
children: <p>{text}</p>,
extra: extraNode,
},
{
key: '3',
label: '面板 3 (不可展开)',
children: <p>{text}</p>,
collapsible: 'disabled', // 禁用此面板的展开功能
},
];

const AccordionCollapseDemo: React.FC = () => (
<div className="p-8 bg-white rounded-lg shadow-lg w-[600px]">
<h3 className="text-xl font-bold mb-4 text-center">手风琴模式与自定义</h3>
<Space direction="vertical" style={{width: '100%'}}>
<p>手风琴模式 (Accordion)</p>
<Collapse
accordion
items={getItems(<SettingOutlined />)}
/>

<p className='mt-4'>无边框样式 (bordered=false)</p>
<Collapse
bordered={false}
defaultActiveKey={['1']}
items={getItems(<SettingOutlined />)}
/>

<p className='mt-4'>图标位置 (expandIconPosition='end')</p>
<Collapse
expandIconPosition="end"
defaultActiveKey={['1']}
items={getItems(<SettingOutlined />)}
/>
</Space>
</div>
);

export default AccordionCollapseDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import BasicCollapseDemo from './components/demos/BasicCollapseDemo';
import AccordionCollapseDemo from './components/demos/AccordionCollapseDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <BasicCollapseDemo /> */}
<AccordionCollapseDemo />
</div>
</ConfigProvider>
);
};

通过 accordion 属性,我们可以轻松切换折叠面板的核心交互模式。结合 borderedextra 等自定义属性,Collapse 能够灵活地适应各种信息收纳场景,是优化长页面内容展示、提升信息架构清晰度的得力工具。


7.13.4. Descriptions: 结构化的详情展示

Descriptions (描述列表) 是一个专为 展示键值对(Key-Value)信息 而设计的组件。当您需要在一个详情页或弹窗中,清晰、整齐地陈列多个只读字段时,Descriptions 能够自动处理对齐、列分布和响应式布局,省去了手动编写表格或栅格布局的繁琐工作。

核心应用场景:

  • 用户详情页:展示用户的姓名、电话、邮箱、地址等。
  • 订单详情:展示订单号、下单时间、支付状态、收货地址等。
  • 系统信息:展示应用的名称、版本号、服务器 IP、运行状态等。

重要升级提示 (v5.8.0+): 在 antd v5.8.0 之后,官方 强烈推荐 使用 items 属性来数据驱动地配置描述列表。旧有的通过 JSX 嵌套 <Descriptions.Item> 的写法已被废弃。本教程将完全遵循 2025 年的最佳实践,只使用现代的 items 写法。

第一步:基础用法与 span 布局

创建 Descriptions 的最佳方式是为其提供一个 items 数组。其布局基于一个响应式的网格系统,我们可以通过 columnspan 属性来精确控制每一项的排列。

核心属性:

  • title: 描述列表的整体标题
  • items: 一个对象数组,用于定义所有描述项。每个对象的核心结构为:
    • key: 唯一标识符。
    • label: 描述项的标签(Key)。
    • children: 描述项的内容(Value)。
    • span: 该项占据的 列数
  • column: 描述列表的总列数,默认为 3。所有 itemsspan 值将在这个总列数内进行分配。

文件路径: src/components/demos/BasicDescriptionsDemo.tsx (新建文件)

image-20250930093146004

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React from "react";
import { Descriptions } from "antd";
import type { DescriptionsProps } from "antd";

const BasicDescriptionsDemo: React.FC = () => {
// 使用 items 数组定义所有描述项
const items: DescriptionsProps["items"] = [
{ key: "1", label: "产品名称", children: "Ant Design Pro" },
{ key: "2", label: "计费模式", children: "预付费" },
{ key: "3", label: "创建时间", children: "2023-01-10" },
{ key: "4", label: "关联订单", children: "123456789" },
{ key: "5", label: "生效时间", children: "2023-01-10" },
{
key: "6",
label: "配置信息",
span: 2,
children: (
<>
CPU: 4 核<br />
内存: 16 GB
<br />
带宽: 5 Mbps
</>
),
},
{ key: "7", label: "备注", children: "无" },
];

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-[800px]">
<h3 className="text-xl font-bold mb-4 text-center">基础用法</h3>
<Descriptions items={items} column={3} title="产品配置详情" />
</div>
);
};
export default BasicDescriptionsDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicDescriptionsDemo from './components/demos/BasicDescriptionsDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicDescriptionsDemo />
</div>
</ConfigProvider>
);
};

export default App;

第二步:布局与样式 - 边框、垂直与尺寸

除了默认的无边框、水平布局,Descriptions 还提供了多种样式变体,以适应不同的展示需求,例如更正式的、类似表格的带边框样式。

核心属性:

  • bordered: 布尔值,设置为 true 可为描述列表添加边框和背景色,使其外观类似表格。
  • layout: 布局方式,可选 horizontal (水平,默认) 或 vertical (垂直,标签在内容上方)。
  • size: 设置列表的尺寸,可选 default, middle, small此属性仅在 bordered={true} 时生效,用于创建更紧凑的列表。
  • extra: 在 title 同一行的右上角添加额外操作区,例如一个“编辑”按钮。

文件路径: src/components/demos/StyledDescriptionsDemo.tsx (新建文件)

image-20250930093440206

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React from 'react';
import { Descriptions, Badge, Button } from 'antd';
import type { DescriptionsProps } from 'antd';

const items: DescriptionsProps['items'] = [
{ key: '1', label: '产品', children: '云服务器 ECS' },
{ key: '2', label: '计费模式', children: '按量付费' },
{ key: '3', label: '自动续费', children: '是' },
{ key: '4', label: '订单号', children: '1234567890' },
{ key: '5', label: '创建时间', children: '2023-01-10 18:00:00' },
{ key: '6', label: '状态', span: 3, children: <Badge status="processing" text="运行中" /> },
{ key: '7', label: '折扣', children: '¥ 20.00' },
{ key: '8', label: '总计', children: '¥ 60.00' },
{ key: '9', label: '实付', children: '¥ 40.00' },
];


const StyledDescriptionsDemo: React.FC = () => (
<div className="p-8 bg-white rounded-lg shadow-lg w-[800px]">
<h3 className="text-xl font-bold mb-4 text-center">布局与样式</h3>

{/* 1. 带边框的样式 */}
<Descriptions
title="订单详情 (带边框)"
bordered
extra={<Button type="primary">编辑</Button>}
items={items}
className="mb-8"
/>

{/* 2. 垂直布局 + 小尺寸 */}
<Descriptions
title="订单详情 (垂直 + 小尺寸)"
bordered
layout="vertical"
size="small"
items={items}
/>
</div>
);

export default StyledDescriptionsDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import BasicDescriptionsDemo from './components/demos/BasicDescriptionsDemo';
import StyledDescriptionsDemo from './components/demos/StyledDescriptionsDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <BasicDescriptionsDemo /> */}
<StyledDescriptionsDemo />
</div>
</ConfigProvider>
);
};

通过 bordered, layout, size 等属性的组合,Descriptions 能够灵活地在“简洁信息列表”和“正式详情表格”两种风格之间切换。其数据驱动的 items API 和强大的 span 布局能力,使其成为详情页开发中不可或缺的效率工具。


Carousel (走马灯),在网页设计中通常也被称为“Slider”或“轮播图”,是一种在有限的界面空间内,循环展示多个内容面板(通常是图片或卡片)的容器组件。它通过自动或手动切换,有效地利用了黄金展示区域,是官网首页、产品宣传等场景的理想选择。

核心应用场景:

  • 网站首页 Banner:轮播展示最新的活动、产品或新闻。
  • 产品图集:在商品详情页,以轮播形式展示多张高清图片。
  • 功能介绍:在新用户引导流程中,轮播展示核心功能的介绍卡片。

第一步:基础用法与切换效果

创建一个 Carousel 非常简单,您只需将需要轮播的各个面板作为其 children 传入即可。同时,Carousel 也提供了多种切换效果和指示器位置的配置。

核心属性:

  • children: 需要轮播的 React 节点数组。每个直接子元素都会被视为一个独立的面板。
  • effect: 切换的动画效果。可选值为 'scrollx' (水平滚动,默认) 或 'fade' (淡入淡出)。
  • dotPosition: 面板指示点(小圆点)的位置,可选 top, bottom (默认), left, right

文件路径: src/components/demos/BasicCarouselDemo.tsx (新建文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import React from "react";
import { Carousel, Divider } from "antd";

// 为轮播面板统一定义样式
const contentStyle: React.CSSProperties = {
margin: 0,
height: "160px",
color: "#fff",
lineHeight: "160px",
textAlign: "center",
background: "#364d79",
};

const BasicCarouselDemo: React.FC = () => (
<div className="p-8 bg-white rounded-lg shadow-lg w-[600px]">
<h3 className="text-xl font-bold mb-4 text-center">基础用法与切换效果</h3>
<Divider orientation="left">默认滚动效果 (scrollx)</Divider>
<Carousel autoplay>
<div>
<h3 style={contentStyle}>面板 1</h3>
</div>
<div>
<h3 style={contentStyle}>面板 2</h3>
</div>
<div>
<h3 style={contentStyle}>面板 3</h3>
</div>
</Carousel>

<Divider orientation="left">垂直滚动效果 (vertical)</Divider>
<Carousel vertical autoplay>
<div>
<h3 style={contentStyle}>面板 1</h3>
</div>
<div>
<h3 style={contentStyle}>面板 2</h3>
</div>
</Carousel>

<Carousel effect="fade" autoplay>
<div>
<h3 style={contentStyle}>面板 1</h3>
</div>
<div>
<h3 style={{...contentStyle, background: '#5a8d9b'}}>面板 2</h3>
</div>
<div>
<h3 style={{...contentStyle, background: '#8ab4c2'}}>面板 3</h3>
</div>
</Carousel>
</div>
);


export default BasicCarouselDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicCarouselDemo from './components/demos/BasicCarouselDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicCarouselDemo />
</div>
</ConfigProvider>
);
};

export default App;

第二步:自动播放与程序化控制

在大多数场景下,我们希望走马灯能够自动播放。有时,我们还需要通过外部的按钮来手动控制其切换,这就需要使用 ref 来获取 Carousel 实例并调用其方法。

核心属性/方法:

  • autoplay: 布尔值,设置为 true 即可开启自动播放。
  • ref: 用于获取 Carousel 组件的实例。
  • ref.current.next(): 切换到下一个面板。
  • ref.current.prev(): 切换到上一个面板。
  • ref.current.goTo(slideNumber): 跳转到指定索引的面板。

文件路径: src/components/demos/ControlledCarouselDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import React, { useRef } from 'react';
import { Carousel, Button, Space, Divider } from 'antd';
import type { CarouselRef } from 'antd/es/carousel';

const contentStyle: React.CSSProperties = {
margin: 0,
height: '160px',
color: '#fff',
lineHeight: '160px',
textAlign: 'center',
background: '#364d79',
};

const ControlledCarouselDemo: React.FC = () => {
// 1. 使用 useRef 创建一个 ref 来引用 Carousel 实例
const carouselRef = useRef<CarouselRef>(null);

const handleNext = () => {
carouselRef.current?.next(); // 2. 调用 next 方法
};

const handlePrev = () => {
carouselRef.current?.prev(); // 3. 调用 prev 方法
};

const handleGoTo = () => {
carouselRef.current?.goTo(0); // 4. 调用 goTo 方法跳转到第一页
}

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-[600px]">
<h3 className="text-xl font-bold mb-4 text-center">自动播放与手动控制</h3>

<Divider orientation="left">自动播放</Divider>
<Carousel autoplay>
<div><h3 style={contentStyle}>面板 1</h3></div>
<div><h3 style={contentStyle}>面板 2</h3></div>
<div><h3 style={contentStyle}>面板 3</h3></div>
</Carousel>

<Divider orientation="left">程序化控制</Divider>
<Carousel ref={carouselRef}>
<div><h3 style={contentStyle}>面板 1</h3></div>
<div><h3 style={{...contentStyle, background: '#5a8d9b'}}>面板 2</h3></div>
<div><h3 style={{...contentStyle, background: '#8ab4c2'}}>面板 3</h3></div>
</Carousel>
<Space className="mt-4">
<Button onClick={handlePrev}>上一张</Button>
<Button onClick={handleNext}>下一张</Button>
<Button onClick={handleGoTo}>回到第一张</Button>
</Space>
</div>
);
};

export default ControlledCarouselDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import BasicCarouselDemo from './components/demos/BasicCarouselDemo';
import ControlledCarouselDemo from './components/demos/ControlledCarouselDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <BasicCarouselDemo /> */}
<ControlledCarouselDemo />
</div>
</ConfigProvider>
);
};

通过 autoplay 属性和 ref 的程序化控制,我们可以灵活地管理 Carousel 的动态行为,使其既能自动展示内容,也能响应用户的自定义交互。

至此,我们已经完成了第二阶段所有核心容器组件的学习。我们学会了如何使用 Card 聚合信息,用 CollapseDescriptions 组织详情,以及用 Carousel 动态轮播内容。接下来,我们将进入 阶段三:高级数据组件,挑战 List, Table 等功能更强大的组件。


7.14. 高级数据组件:驾驭复杂数据集

摘要:在本章中,我们将深入学习 Ant Design 中为处理结构化、大规模数据集而设计的核心组件。这些组件是企业级应用(尤其是后台管理系统)的绝对主力。我们将从最基础的序列数据展示(List)开始,逐步掌握层级数据(Tree)和二维表格数据(Table)的渲染与交互,最终构建出功能强大的数据驱动界面。

7.14.1. List: 灵活的序列数据展示

List (列表) 是用于展示 序列数据(即数组)最基础也最重要的组件。相比于在代码中手动 map 一个数组来渲染 JSX,List 组件提供了更丰富、更标准化的功能,如加载状态、分页、头部/尾部、多种布局以及栅格化展示,是 Table 组件在简单场景下的轻量级替代方案。

核心应用场景:

  • 新闻/文章列表:展示文章的标题、摘要、作者头像等。
  • 消息通知中心:展示一系列通知信息。
  • 产品或图片墙:以网格布局展示多个商品或图片。

第一步:准备数据 - Mock API 升级

为了真实地模拟从后端获取列表数据的过程,我们需要再次请出 json-server,并为它准备一份列表数据。这次,我们将通过脚本生成一份模拟的新闻文章列表。

1. 安装/确认依赖
请确保您的 devDependencies 中已包含 @faker-js/faker,用于生成逼真的假数据。

2. 创建数据生成脚本
我们将创建一个脚本,用于生成包含 50 条新闻的 db.json 文件。

文件路径: scripts/generate-mock-data.mjs (新建或修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { fakerZH_CN as faker } from '@faker-js/faker';
import fs from 'fs';

// 使用中文语言环境的 Faker 实例

// 生成新闻列表的函数
const generateNewsList = (count) => {
const newsList = [];
for (let i = 0; i< count;i++){
newsList.push({
id: faker.string.uuid(),
title: faker.word.words({ count: { min: 8, max: 15 } }), // 8-15 个中文词作为标题
avatar: faker.image.avatar(),
description: faker.word.words({ count: { min: 20, max: 40 } }), // 20-40 个中文词作为描述
content: faker.word.words({ count: { min: 100, max: 200 } }), // 100-200 个中文词作为内容
})
}
return newsList;
}

const db = {
news : generateNewsList(50),
}


fs.writeFileSync('db.json', JSON.stringify(db, null, 2));
console.log('Mock data (news list) generated successfully!');

3. 生成数据并启动服务
首先,在 package.json 中添加或修改 mock:generate 脚本。

1
2
3
4
5
"scripts": {
// ... 其他脚本
"mock:generate": "node ./scripts/generate-mock-data.mjs",
"mock:api": "json-server --watch db.json --port 3001"
},

现在,依次执行以下命令:

  1. pnpm mock:generate (只需执行一次,用于生成 db.json)
  2. pnpm mock:api (启动 API 服务)

您的 API http://localhost:3001/news 现在已经可以提供 50 条新闻数据了。


第二步:基础列表与数据绑定

List 组件的核心是 数据驱动。我们需要从 API 获取数据,并将其传递给 List,然后通过 renderItem 函数定义每一项的渲染方式。

核心属性:

  • dataSource: 列表的数据源数组
  • renderItem: 渲染列表中每一项的函数。该函数接收 itemindex 作为参数,返回一个 React 节点。
  • loading: 布尔值,用于控制列表的加载状态,true 时会显示骨架屏。
  • List.Item / List.Item.Meta: 用于快速构建标准列表项结构的辅助组件。

文件路径: src/components/demos/BasicListDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import React, { useEffect, useState } from "react";
import { Avatar, List } from "antd";
// 定义数据项的 TypeScript 类型
interface NewsItem {
id: string;
title: string;
avatar: string;
description: string;
content: string;
}

const BasicListDemo: React.FC = () => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<NewsItem[]>([]);

const renderItem = (item: NewsItem) => (
<List.Item>
<List.Item.Meta
avatar={<Avatar src={item.avatar} />}
title={item.title}
description={item.description}
/>
</List.Item>
);

useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch("http://localhost:3001/news");
const data = await response.json();
setData(data);
} catch (error) {
console.error("Error fetching data:", error);
} finally {
setLoading(false);
}
};

fetchData();
}, []);

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-[800px]">
<h3 className="text-xl font-bold mb-4 text-center">基础新闻列表</h3>
<List
loading={loading}
itemLayout="horizontal"
dataSource={data}
renderItem={renderItem}
/>
</div>
);
};

export default BasicListDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicListDemo from './components/demos/BasicListDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicListDemo />
</div>
</ConfigProvider>
);
};

export default App;

第三步:增强功能 - 分页与栅格布局

当数据量巨大时,一次性加载所有数据是不现实的。List 内置了强大的 分页栅格 功能,以应对不同场景。

核心属性:

  • pagination: 配置分页器。传入一个对象,可以精细化控制分页行为,如 pageSize (每页条数)。List 会自动处理客户端的数据分割。
  • grid: 开启栅格布局。传入一个对象,可以配置每行显示的列数 (column)、间距 (gutter) 以及响应式断点。

文件路径: src/components/demos/AdvancedListDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import React, { useEffect, useState } from 'react';
import { List, Card } from 'antd';

interface NewsItem {
id: string;
title: string;
avatar: string;
}

const AdvancedListDemo: React.FC = () => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<NewsItem[]>([]);

useEffect(() => {
fetch('http://localhost:3001/news')
.then((res) => res.json())
.then((res) => {
setData(res);
setLoading(false);
});
}, []);

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-[1000px]">
<h3 className="text-xl font-bold mb-4 text-center">分页与栅格列表</h3>
<List
loading={loading}
dataSource={data}
// 1. 开启栅格布局每行 4
grid={{
gutter: 16,
xs: 1,
sm: 2,
md: 4,
lg: 4,
xl: 4,
xxl: 4,
}}
// 2. 开启分页每页 8
pagination={{
onChange: (page) => {
console.log(page);
},
pageSize: 8,
align: 'center',
}}
renderItem={(item) => (
<List.Item>
<Card title={item.title.substring(0, 15) + '...'}>Card content</Card>
</List.Item>
)}
/>
</div>
);
};

export default AdvancedListDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import BasicListDemo from './components/demos/BasicListDemo';
import AdvancedListDemo from './components/demos/AdvancedListDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <BasicListDemo /> */}
<AdvancedListDemo />
</div>
</ConfigProvider>
);
};

通过 paginationgrid 属性的简单配置,我们就能轻松地将一个长列表,转化为带分页的、响应式的卡片网格,这在构建产品展示墙、图片库等场景中极为高效。List 组件的灵活性和可扩展性,使其成为处理序列数据的首选方案。


7.14.3. Table (Part 1 - 基础篇): 构建你的第一个数据表格

Table (表格) 是所有后台管理、数据分析、信息管理类应用的心脏。当您的数据拥有多个维度(列),并且需要进行结构化的行列展示时,Table 便无可替代。相比于 ListTable 提供了列对齐、排序、筛选、分页、行选择等一系列专为复杂数据集设计的强大功能。

在本篇中,我们将专注于掌握 Table 的两大基石,并结合 json-server 构建一个功能完备的、带 异步加载客户端分页 功能的基础表格。


第一步:两大核心概念 - dataSourcecolumns

要渲染一个 Table,您只需要向它提供两样东西:数据在哪里(dataSource),以及数据如何展示(columns)。
dataSource (数据源)

  • 它是一个 对象数组,数组中的每个对象都代表表格中的 一行 数据。
  • 关键要求:数组中的每个对象都 必须 有一个 唯一key 属性,React 依赖它来进行高效的渲染和 diff。如果您的数据中没有 key 字段,您可以使用 rowKey 属性来指定另一个唯一标识符(例如 id)。

columns (列定义)

  • 它也是一个 对象数组,数组中的每个对象都定义了表格中的 一列
  • 核心属性:
    • title: 列头显示的标题文本。
    • dataIndex: 核心关联字段。它告诉 Table,这一列要显示 dataSource 中每个对象的哪个属性值。例如,dataIndex: 'name' 就会去取 dataSource 中每个对象的 name 属性。
    • key: 列的唯一标识符。如果 dataIndex 已经唯一,通常可以将其设为与 dataIndex 相同的值。

第二步:实战演练 - 异步加载与客户端分页

现在,我们将结合 json-server,从零开始构建一个“用户管理”表格。

1. 准备用户数据 API

我们需要一个能提供大量用户数据的 API。请修改您的 scripts/generate-mock-data.mjs 脚本,为 db.json 添加 users 数据。

文件路径: scripts/generate-mock-data.mjs (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { fakerZH_CN as faker } from '@faker-js/faker';
import fs from 'fs';

// 新增:生成用户列表的函数
const generateUsers = (count) => {
const users = [];
for (let i=0;i<count;i++) {
users.push({
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
jobTitle: faker.person.jobTitle(),
country: faker.location.country(),
});
}
return users;
};


const db = {
users: generateUsers(100),
};

fs.writeFileSync("db.json", JSON.stringify(db, null, 2));
console.log('Mock data (users list) generated successfully!');

修改完成后,请务必重新执行 pnpm mock:generate 来生成新的 db.json 文件。然后,启动 pnpm mock:api,您的 http://localhost:3001/users 接口现在就可以使用了。

2. 编写表格组件

我们将创建一个组件,在加载时从 API 获取用户数据,并用带分页的 Table 将其展示出来。

文件路径: src/components/demos/BasicTableDemo.tsx (新建文件)

image-20250930111655744

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import React, { useEffect, useState } from "react";
import { message, Table } from "antd";
import type { TableColumnsType } from "antd";

// 1. 定义 TypeScript 类型,增强代码健壮性
interface User {
id: string;
name: string;
email: string;
jobTitle: string;
country: string;
}

// 2. 定义列 (columns)
const columns: TableColumnsType<User> = [
{ title: "姓名", dataIndex: "name", key: "name" },
{ title: "邮箱", dataIndex: "email", key: "email" },
{ title: "职位", dataIndex: "jobTitle", key: "jobTitle" },
{ title: "国家", dataIndex: "country", key: "country" },
];

const BasicTableDemo: React.FC = () => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<User[]>([]);

useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch("http://localhost:3001/users");
const data = await response.json();
setData(data);
} catch (error) {
message.error("Error fetching data");
} finally {
setLoading(false);
}
};
fetchData();
}, []);

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-full max-w-4xl">
<h3 className="text-xl font-bold mb-4 text-center">
用户数据表格 (基础篇)
</h3>
<Table
columns={columns}
dataSource={data}
loading={loading}
rowKey="id"
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `共 ${total} 条`,
}}
bordered
/>
</div>
);
};

export default BasicTableDemo;

3. 重点解析:Pagination (分页器)

虽然 Pagination 是一个独立的组件,但它最常以内置的形式,通过 TableListpagination 属性进行使用。

  • 客户端分页 (我们当前使用的): 当您的 dataSource 包含了 所有 数据时,Table 组件会非常智能地在 前端 自动为您完成分页逻辑。您只需在 pagination 对象中配置好每页的条数(pageSize)等显示选项即可。
  • 服务端分页 (后续会讲): 当数据量极大时(成千上万条),一次性加载所有数据是不现实的。这时,我们需要在每次切换页面时,重新向后端发起请求,获取当前页的数据。这被称为服务端分页,需要配合 onChange 事件来处理。

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicTableDemo from './components/demos/BasicTableDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicTableDemo />
</div>
</ConfigProvider>
);
};

export default App;

至此,我们已经成功构建了一个功能完备的基础数据表格。您已经掌握了 Table 最核心的 dataSourcecolumns 配置,并学会了如何处理异步加载和实现客户端分页。

Part 2 - 交互篇 中,我们将在此基础上,为表格添加 排序、筛选和行选择 等强大的交互功能。


7.14.4. Table (Part 2 - 交互篇): 排序、筛选与选择

在基础篇中,我们成功地将数据显示在了表格里。但一个真正的企业级表格,远不止于静态展示,它必须是 可交互的。用户需要能够根据自己的需求,对数据进行排序、筛选和选择,以便快速定位和处理信息。

在本篇中,我们将为上一节的表格添加三种最核心的交互能力:

  1. 排序 (sorter): 允许用户点击表头,对该列数据进行升序或降序排列。
  2. 筛选 (filters): 提供一个筛选菜单,让用户可以根据特定条件过滤数据。
  3. 行选择 (rowSelection): 允许用户勾选一行或多行,以进行批量操作。

第一步:升级 Mock API - 添加可排序与筛选的数据

为了更好地演示排序和筛选功能,我们需要对 db.json 的数据结构做一点小小的升级:

  1. 为用户添加一个 age 字段,用于演示 数字排序
  2. country 字段的随机值范围缩小,以便于我们进行 分类筛选

文件路径: scripts/generate-mock-data.mjs (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 新增:生成用户列表的函数
const generateUsers = (count) => {
const users = [];
// 预设的国家列表,方便筛选
const countries = ["中国", "美国", "日本", "英国", "澳大利亚"];
for (let i = 0; i < count; i++) {
users.push({
id: faker.string.uuid(),
name: faker.person.fullName(),
// 新增:年龄字段,用于排序
age: faker.number.int({
min: 20,
max: 60,
}),
email: faker.internet.email(),
jobTitle: faker.person.jobTitle(),
country: faker.helpers.arrayElement(countries),
});
}
return users;
};

修改后,请务必再次运行 pnpm mock:generate 更新您的 db.json 文件,然后重启 pnpm mock:api 服务。


第二步:实现交互功能

现在,我们将创建一个新的表格组件,并在 columns 定义和 Table 属性中添加交互配置。

文件路径: src/components/demos/InteractiveTableDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import React, { useEffect, useState } from "react";
import { Table, Button, Space, message } from "antd";
import type { TableColumnsType, TableProps } from "antd";
// TypeScript 接口定义
interface User {
id: string;
name: string;
email: string;
age: number;
jobTitle: string;
country: string;
}

const InteractiveTableDemo: React.FC = () => {
// 状态管理
const [loading, setLoading] = useState(true);
const [data, setData] = useState<User[]>([]);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);

// 表格列定义
const columns: TableColumnsType<User> = [
{
title: "姓名",
dataIndex: "name",
key: "name",
},
{
title: "邮箱",
dataIndex: "email",
key: "email",
},
{
title: "年龄",
dataIndex: "age",
key: "age",
sorter: (record1, record2) => record1.age - record2.age,
},
{
title: "职位",
dataIndex: "jobTitle",
key: "jobTitle",
},
{
title: "国家",
dataIndex: "country",
key: "country",
filters: [
{ text: "中国", value: "中国" },
{ text: "美国", value: "美国" },
{ text: "英国", value: "英国" },
],
// onFilter会接受两个属性:
// value: 当前筛选的值
// record: 当前行数据
onFilter: (value, record) => record.country.includes(value as string),
},
{
title: "操作",
key: "action",
render: (text, record) => (
<Space>
<Button type="link">编辑</Button>
<Button type="link">删除</Button>
</Space>
),
},
];

useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch("http://localhost:3001/users");
const data = await response.json();
setData(data);
} catch (error) {
message.error("Error fetching data");
} finally {
setLoading(false);
}
};
fetchData();
}, []);

const handleBulkAction = () => {
alert(`即将对 ${selectedRowKeys.length} 个用户进行批量操作!`);
};

const hasSelected: boolean = selectedRowKeys.length > 0;

// 3. 定义行选择相关的处理函数
const onSelectChange = (newSelectdRowKeys: React.Key[]) => {
message.info(`当前选中的行: ${newSelectdRowKeys}`);
setSelectedRowKeys(newSelectdRowKeys);
};

// 4. 定义 rowSelection 对象
const rowSelection = {
selectedRowKeys,
onChange: onSelectChange,
};

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-full max-w-4xl">
<h3 className="text-xl font-bold mb-4 text-center">
交互式表格 (排序、筛选、选择)
</h3>

<Space className="mb-4">
<Button
type="primary"
onClick={handleBulkAction}
disabled={!hasSelected}
>
批量操作
</Button>
<span className="ml-2">
{hasSelected ? `已选择 ${selectedRowKeys.length} 项` : ""}
</span>
</Space>
<Table
rowSelection={rowSelection}
columns={columns}
dataSource={data}
loading={loading}
rowKey="id"
bordered
pagination={{
pageSize: 5,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `共 ${total} 条`,
}}
size="small"
/>
</div>
);
};

export default InteractiveTableDemo;

第三步:重点解析

  1. 排序 (sorter)

    • 我们在需要排序的列定义中添加了 sorter 属性。
    • sorter: (a, b) => a.age - b.age 是一个 比较函数Table 组件会用它在 客户端 对数据进行排序。它的工作方式与 JavaScript 的 Array.prototype.sort() 完全相同。对于字符串,我们使用 localeCompare 来进行正确的比较。
  2. 筛选 (filters & onFilter)

    • filters: 一个对象数组,定义了筛选菜单中可用的选项。text 是显示给用户的文本,value 是筛选时真正使用的值。
    • onFilter: 一个函数,用于执行筛选逻辑。Table 会遍历 dataSource 中的每一条 record,并用 filters 中选中的 value 来调用此函数。如果函数返回 true,该行数据则被保留。
  3. 行选择 (rowSelection)

    • 行选择是一个典型的 受控 功能。
    • 我们必须提供一个 selectedRowKeys 数组(来自我们的 state)来告诉 Table 当前哪些行被选中。
    • 我们必须提供一个 onChange 回调函数 (onSelectChange)。当用户勾选或取消勾选时,Table 会调用这个函数,并传入 最新的 selectedRowKeys 数组。我们的职责就是在这个回调里,用 setSelectedRowKeys 来更新我们的 state,从而完成数据流的闭环。

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
// import BasicTableDemo from './components/demos/BasicTableDemo';
import InteractiveTableDemo from './components/demos/InteractiveTableDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <BasicTableDemo /> */}
<InteractiveTableDemo />
</div>
</ConfigProvider>
);
};

export default App;

现在,您的表格已经从一个静态的“展示板”变成了一个动态的“工作台”。用户可以自由地对数据进行排序、筛选和选择。

Part 3 - 高级篇 中,我们将探索 Table 更深层次的自定义能力,包括:自定义单元格渲染、固定头与列、可展开行


7.14.5. Table (Part 3 - 高级篇): 固定列、自定义渲染与服务端数据

在前两篇中,我们已经掌握了表格的数据绑定和核心交互。现在,我们将进入企业级应用中最为常见的复杂场景:处理列数超多的宽表格,以及在单元格内渲染自定义组件(如操作按钮、状态标签)。


第一步:升级 Mock API - 构造一个宽表格数据源

为了模拟真实的后台管理场景,一个表格往往有十几个甚至更多的列。我们需要为 users 数据添加更多字段,以便创建一个需要横向滚动的“宽表格”。

文件路径: scripts/generate-mock-data.mjs (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 新增:生成用户列表的函数
const generateUsers = (count) => {
const users = [];
// 预设的国家列表,方便筛选
const countries = ["中国", "美国", "日本", "英国", "澳大利亚"];
for (let i = 0; i < count; i++) {
users.push({
id: faker.string.uuid(),
name: faker.person.fullName(),
// 新增:年龄字段,用于排序
age: faker.number.int({
min: 20,
max: 60,
}),
// 新增:更多字段
phone: faker.phone.number(),
status: faker.helpers.arrayElement(["active", "pending", "inactive"]),
email: faker.internet.email(),
jobTitle: faker.person.jobTitle(),
country: faker.helpers.arrayElement(countries),
createdAt: faker.date.past(2).toISOString(), // 创建日期
});
}
return users;
};

同样,修改后请再次运行 pnpm mock:generate 更新 db.json,并重启 pnpm mock:api 服务。


第二步:实战演练 - 构建一个带固定列和自定义渲染的表格

我们将综合运用 renderscrollfixed 三个核心 API,来构建一个功能强大的高级表格。

核心 API:

  • render: 列定义中的自定义渲染函数。它允许我们返回任意 React 节点,而不仅仅是文本。render 函数接收三个参数 (text, record, index),其中 record 代表 当前行的完整数据对象,这对于构建“操作”列至关重要。
  • scroll: Table 的顶层属性,用于设置表格的滚动行为。当 scroll.x 的值超过表格容器的宽度时,将出现横向滚动条。
  • fixed: 列定义中的属性,用于固定列。可设为 'left''right',使该列在水平滚动时保持固定。注意:要使 fixed 生效,必须先设置 scroll.x

文件路径: src/components/demos/AdvancedTableDemo.tsx (新建文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import React, { useEffect, useState } from "react";
import { Table, Tag, Space, Button } from "antd";
import type { TableColumnsType } from "antd";
import dayjs from "dayjs";

// 更新 TypeScript 类型以匹配新数据
interface User {
id: string;
name: string;
email: string;
age: number;
jobTitle: string;
country: string;
status: "active" | "pending" | "inactive";
createdAt: string;
}

const AdvancedTableDemo: React.FC = () => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<User[]>([]);
// 1. 定义列,并添加 render, fixed, width 等高级属性
const columns: TableColumnsType<User> = [
{ title: "姓名", dataIndex: "name", key: "name" },
{ title: "邮箱", dataIndex: "email", key: "email" },
{
title: "年龄",
dataIndex: "age",
key: "age",
sorter: (a, b) => a.age - b.age,
},
{ title: "职位", dataIndex: "jobTitle", key: "jobTitle" },
{
title: "国家",
dataIndex: "country",
key: "country",
filters: [
{ text: "中国", value: "中国" },
{ text: "美国", value: "美国" },
{ text: "英国", value: "英国" },
],
onFilter: (value, record) => record.country.includes(value as string),
},

{
title: "状态",
dataIndex: "status",
key: "status",
filters: [
{ text: "活跃", value: "active" },
{ text: "待处理", value: "pending" },
{ text: "禁用", value: "inactive" },
],
onFilter: (value, record) => record.status === value,
},
{
title: "创建时间",
dataIndex: "createdAt",
key: "createdAt",
render: (text) => dayjs(text).format("YYYY-MM-DD HH:mm:ss"),
},
{
title: "操作",
dataIndex: "action",
key: "action",
fixed: "right", // 固定在右侧
width: 100, // 设置宽度
render: (text, record) => (
<Space>
<Button type="link">编辑</Button>
<Button type="link">删除</Button>
</Space>
),
},
];

useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch("http://localhost:3001/users");
const data = await response.json();
setData(data);
} catch (error) {
console.error("Error fetching data:", error);
} finally {
setLoading(false);
}
};
fetchData();
}, []);

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-full max-w-6xl">
<h3 className="text-xl font-bold mb-4 text-center">
高级表格 (固定列与自定义渲染)
</h3>
<Table
columns={columns}
dataSource={data}
loading={loading}
rowKey="id"
bordered
size="small"
pagination={{
pageSize: 5,
}}
// 2. 设置 scroll.x 使表格可横向滚动
scroll={{ x: 1500 }}
/>
</div>
);
};

export default AdvancedTableDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
// import InteractiveTableDemo from './components/demos/InteractiveTableDemo';
import AdvancedTableDemo from './components/demos/AdvancedTableDemo';


const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <InteractiveTableDemo /> */}
<AdvancedTableDemo />
</div>
</ConfigProvider>
);
};

export default App;

7.14.3. Tree: 层级数据的展示与交互

Tree (树形控件) 是专门用于 展示和操作层级(或嵌套)数据 的组件。当您的数据本身具有父子关系时,例如文件目录、组织架构、商品分类等,使用 Tree 组件可以直观地反映这种结构,并提供展开、收起、选择等丰富的交互功能。

核心应用场景:

  • 文件/目录浏览器:以树状结构展示文件夹和文件。
  • 组织架构:展示公司的部门层级关系。
  • 权限管理:在树状的权限列表中,为角色勾选其拥有的权限。
  • 多级分类选择:在电商后台,为商品选择其所属的多级分类。

第一步:基础用法 - treeData 与受控操作

CollapseDescriptions 等现代 antd 组件一样,构建 Tree 的最佳实践是使用 treeData 属性,以 数据驱动 的方式进行渲染。同时,Tree 的核心交互(如展开、选中)都是通过 受控模式 来管理的。

核心属性:

  • treeData: 一个对象数组,用于定义整个树的结构。每个对象的核心属性为:
    • title: 节点的标题。
    • key: 节点的唯一标识符,在整棵树中必须唯一
    • children: 子节点数组,其结构与父级相同。
  • expandedKeys: (受控) 一个包含 key 的数组,用于控制当前哪些节点是展开的。
  • selectedKeys: (受控) 一个包含 key 的数组,用于控制当前哪些节点是被选中的。
  • onExpand: 展开/收起节点时的回调函数,我们需要在此函数中更新 expandedKeys state。
  • onSelect: 点击并选中节点时的回调函数,我们需要在此函数中更新 selectedKeys state。

文件路径: src/components/demos/BasicTreeDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import React, from 'react';
import { Tree } from 'antd';
import type { DataNode, TreeProps } from 'antd/es/tree';

// 1. 定义 treeData 的数据结构
const treeData: DataNode[] = [
{
title: '总部',
key: '0-0',
children: [
{
title: '研发部',
key: '0-0-0',
children: [
{ title: '前端组', key: '0-0-0-0' },
{ title: '后端组', key: '0-0-0-1' },
],
},
{
title: '产品部',
key: '0-0-1',
children: [{ title: '产品设计组', key: '0-0-1-0' }],
},
],
},
];

const BasicTreeDemo: React.FC = () => {
// 2. 使用 state 管理 expandedKeys 和 selectedKeys
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>(['0-0', '0-0-0']);
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);

// 3. 定义 onExpand 和 onSelect 回调来更新 state
const onExpand: TreeProps['onExpand'] = (expandedKeysValue) => {
console.log('onExpand', expandedKeysValue);
setExpandedKeys(expandedKeysValue);
};

const onSelect: TreeProps['onSelect'] = (selectedKeysValue, info) => {
console.log('onSelect', info);
setSelectedKeys(selectedKeysValue);
};

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-[400px]">
<h3 className="text-xl font-bold mb-4 text-center">基础树与受控操作</h3>
<Tree
onExpand={onExpand}
expandedKeys={expandedKeys}
onSelect={onSelect}
selectedKeys={selectedKeys}
treeData={treeData}
defaultExpandAll // 方便演示默认展开所有
/>
</div>
);
};

export default BasicTreeDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import BasicTreeDemo from './components/demos/BasicTreeDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<BasicTreeDemo />
</div>
</ConfigProvider>
);
};

export default App;

第二步:复选框与父子联动 (checkable & checkStrictly)

Tree 组件最强大的应用场景之一就是权限分配,这需要为每个节点提供复选框。checkable 属性可以开启此功能,而 checkStrictly 属性则用于控制父子节点之间的联动逻辑。

核心属性:

  • checkable: 布尔值,设置为 true 可为每个节点添加复选框。
  • checkedKeys: (受控) 选中复选框的 key 数组。
  • onCheck: 点击复选框时的回调函数,用于更新 checkedKeys state。
  • checkStrictly: 布尔值,用于控制父子节点的勾选行为是否关联。

checkStrictly 的两种模式

  • false (默认): 父子联动模式。勾选父节点,会自动勾选其所有子孙节点;勾选一个父节点下的所有子节点,父节点会自动变为勾选状态。这是最常见的“权限包”逻辑。
  • true: 父子解绑模式。每个节点的勾选状态都是完全独立的,互不影响。在这种模式下,checkedKeys 属性需要传入一个对象 { checked: string[], halfChecked: string[] },以区分全选和半选状态。

文件路径: src/components/demos/CheckableTreeDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import React, { useState } from "react";
import { Tree, Divider, message } from "antd";
import type { DataNode, TreeProps } from "antd/es/tree";

const treeData: DataNode[] = [
{
title: "系统权限",
key: "0-0",
children: [
{
title: "用户管理",
key: "0-0-0",
children: [
{ title: "查看用户", key: "0-0-0-0" },
{ title: "编辑用户", key: "0-0-0-1" },
],
},
{ title: "文章管理", key: "0-0-1" },
],
},
];

const CheckableTreeDemo: React.FC = () => {
// 默认联动模式的 state
const [checkedKeys, setCheckedKeys] = useState<React.Key[]>(["0-0-0-0"]);
// 严格模式的 state
const [strictlyCheckedKeys, setStrictlyCheckedKeys] = useState<React.Key[]>([
"0-0-1",
]);

const onCheck: TreeProps["onCheck"] = (checkedKeysValue) => {
message.info(`checked: ${checkedKeysValue}`);
setCheckedKeys(checkedKeysValue as React.Key[]);
};

const onStrictlyCheck: TreeProps["onCheck"] = (checkedKeysValue) => {
message.info(`checked: ${checkedKeysValue}`);
// 在 checkStrictly 模式下,checkedKeysValue 是一个对象或数组,我们只关心它的 checked 部分
const keys = Array.isArray(checkedKeysValue)
? checkedKeysValue
: checkedKeysValue.checked;
setStrictlyCheckedKeys(keys);
};

return (
<div className="p-8 bg-white rounded-lg shadow-lg flex gap-8">
<div className="w-[300px]">
<h3 className="text-lg font-bold mb-2">父子联动 (默认)</h3>
<Tree
checkable
onCheck={onCheck}
checkedKeys={checkedKeys}
treeData={treeData}
defaultExpandAll
/>
</div>
<Divider type="vertical" style={{ height: "auto" }} />
<div className="w-[300px]">
<h3 className="text-lg font-bold mb-2">父子解绑 (checkStrictly)</h3>
<Tree
checkable
checkStrictly
onCheck={onStrictlyCheck}
checkedKeys={strictlyCheckedKeys}
treeData={treeData}
defaultExpandAll
/>
</div>
</div>
);
};

export default CheckableTreeDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import BasicTreeDemo from './components/demos/BasicTreeDemo';
import CheckableTreeDemo from './components/demos/CheckableTreeDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <BasicTreeDemo /> */}
<CheckableTreeDemo />
</div>
</ConfigProvider>
);
};

7.15. 引导与反馈组件:提升用户体验

7.15.1. Tour: 创建沉浸式用户引导

Tour (漫游式引导) 是一种用于创建分步式、聚焦式产品功能介绍的强大组件。当您发布新功能或希望引导新用户快速上手时,Tour 可以通过高亮特定 UI 元素并附加解释说明的方式,手把手地带领用户走查一遍核心操作流程。

它的核心价值在于将 被动的文档阅读 转变为 主动的上下文学习。用户不再需要离开当前界面去查阅帮助手册,而是在实际的操作环境中,被一步步地指引和教育。这极大地降低了用户的学习成本和挫败感,能有效提升新功能的采用率和用户的整体满意度。


第一步:基础用法 - 创建你的第一个单步引导

场景: 假设我们的页面上有一个非常重要的“创建”按钮,我们希望在新用户第一次访问时,能有一个气泡提示明确地告诉用户这个按钮的作用。

解决方案:
要实现这个最简单的单步引导,我们只需要三样东西:

  1. 一个指向“创建”按钮的引用 (ref)。
  2. 一个描述引导内容的 steps 数组。
  3. 一个控制引导显示/隐藏的状态 (open)。

文件路径: src/components/demos/SingleStepTourDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import React, { useRef, useState } from 'react';
import { Button, Tour } from 'antd';
import type { TourProps } from 'antd';

const SingleStepTourDemo: React.FC = () => {
// 1. 为目标按钮创建一个 ref
const createBtnRef = useRef(null);

// 2. 使用 state 控制引导的显示/隐藏
const [open, setOpen] = useState(false);

// 3. 定义引导步骤(当前只有一个)
const steps: TourProps['steps'] = [
{
title: '创建新项目',
description: '点击这个按钮,立即开始创建您的第一个项目!',
// 关键:将步骤的目标指向我们创建的 ref
target: () => createBtnRef.current,
},
];

return (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">单步引导</h3>

{/* 这是我们要引导的目标按钮,并绑定 ref */}
<Button type="primary" ref={createBtnRef}>
创建项目
</Button>

{/* 这个按钮用于手动打开引导 */}
<Button style={{ marginLeft: 8 }} onClick={() => setOpen(true)}>
显示引导
</Button>

{/* Tour 组件本体 */}
<Tour
open={open}
onClose={() => setOpen(false)}
steps={steps}
/>
</div>
);
};

export default SingleStepTourDemo;

App.tsx 中使用此 Demo:

文件路径: src/App.tsx (修改文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import SingleStepTourDemo from './components/demos/SingleStepTourDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<SingleStepTourDemo />
</div>
</ConfigProvider>
);
};

export default App;

代码深度解析

  • useRef: 我们使用 const createBtnRef = useRef(null); 创建了一个 ref 对象。可以把它想象成一个容器,它的 .current 属性用来存放我们想要引用的 DOM 元素。通过 <Button ref={createBtnRef}>,React 在渲染后就会把这个按钮的 DOM 实例放入 createBtnRef.current 中。
  • useStateopen/onClose: 这是最基础的受控模式。open 属性完全决定了 Tour 是否可见,而 onClose 是在用户点击关闭按钮或遮罩时 必须调用 的回调,我们用它来更新 open 状态,否则引导将无法关闭。
  • stepstarget: steps 数组是引导的“剧本”。target: () => createBtnRef.current 的写法至关重要。之所以使用一个 函数,是因为 steps 数组在组件首次渲染时就已定义,那时 createBtnRef.current 还是 null。使用函数可以“延迟”取值的时机,Tour 组件只在 即将显示该步骤 时才调用此函数,从而确保能获取到已经挂载好的 DOM 节点。

第二步:进阶 - 构建多步引导流程

场景:
单个引导点不足以介绍一个完整的页面。我们需要将多个引导点串联起来,形成一个连贯的“旅程”,带领用户熟悉整个界面的布局和功能。

解决方案:
我们只需扩展我们的 UI,为每个引导点创建一个 ref,然后在 steps 数组中添加对应的步骤描述即可。Tour 组件会自动处理“上一步”、“下一步”的逻辑。

文件路径: src/components/demos/MultiStepTourDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import React, { useRef, useState } from 'react';
import { Button, Tour, Divider, Space, Avatar } from 'antd';
import { UserOutlined } from '@ant-design/icons';
import type { TourProps } from 'antd';

const MultiStepTourDemo: React.FC = () => {
// 1. 为所有目标元素创建 ref
const ref1 = useRef(null);
const ref2 = useRef(null);
const ref3 = useRef(null);

const [open, setOpen] = useState(false);

// 2. 扩展 steps 数组,定义完整的引导流程
const steps: TourProps['steps'] = [
{
title: '个人中心',
description: '点击您的头像,可以进入个人中心。',
target: () => ref1.current,
// 为不同步骤指定不同的气泡位置,避免遮挡
placement: 'bottomRight',
},
{
title: '主操作按钮',
description: '这是页面的核心操作按钮。',
target: () => ref2.current,
},
{
title: '更多选项',
description: '点击这里可以展开更多高级选项。',
target: () => ref3.current,
placement: 'topLeft',
},
];

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-[600px]">
<div className="flex justify-between items-center">
{/* 3. 绑定所有 ref */}
<div ref={ref1}>
<Avatar size="large" icon={<UserOutlined />} />
</div>
<Button ref={ref2} type="primary">
主操作
</Button>
<div ref={ref3}>
<Button>更多</Button>
</div>
</div>

<Divider />
<Button type="default" onClick={() => setOpen(true)}>
开始多步引导
</Button>

<Tour open={open} onClose={() => setOpen(false)} steps={steps} />
</div>
);
};

export default MultiStepTourDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import MultiStepTourDemo from './components/demos/MultiStepTourDemo';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<MultiStepTourDemo />
</div>
</ConfigProvider>
);
};

export default App;

代码深度解析

  • 自动流程控制: 当 steps 数组包含多个对象时,Tour 组件会自动渲染出“上一步”、“下一步”按钮以及底部的指示器(小圆点),我们无需进行任何额外配置。
  • placement 的重要性: 在多步引导中,不同的目标元素位于页面的不同位置。合理地为每个步骤配置 placement 属性,可以确保气泡卡片总是在最合适的位置弹出,不会遮挡住用户即将需要关注的下一个目标元素。

第三步:完全控制与自定义

场景:
在某些高级场景下,我们可能想知道用户当前正在看第几步,或者想在引导的最后一步将“下一步”按钮改为“完成”。这就需要我们对 Tour 的当前步骤进行完全控制,并利用更丰富的 API 进行自定义。

解决方案:
我们将引入 currentonChange 属性来控制当前步骤,并利用 covernextButtonProps 等属性来丰富步骤的外观和行为。

文件路径: src/components/demos/ControlledTourDemo.tsx (新建文件)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import React, { useRef, useState } from 'react';
// ... (之前的 imports)
import { Button, Tour, Divider, Space, Avatar } from 'antd';
import { UserOutlined } from '@ant-design/icons';
import type { TourProps } from 'antd';


const ControlledTourDemo: React.FC = () => {
const ref1 = useRef(null);
const ref2 = useRef(null);
const ref3 = useRef(null);

const [open, setOpen] = useState(false);
// 1. 新增 state 用于控制当前步骤
const [current, setCurrent] = useState(0);

const steps: TourProps['steps'] = [
{
title: '个人中心',
description: '点击您的头像,可以进入个人中心。',
target: () => ref1.current,
placement: 'bottomRight',
},
{
title: '主操作按钮',
description: '这是页面的核心操作按钮。',
// 2. 为步骤添加封面图
cover: (
<img
alt="tour.png"
src="https://user-images.githubusercontent.com/5378891/197385811-54fa9a76-2B02-44c6-96b2-1A1230903434.png"
/>
),
target: () => ref2.current,
},
{
title: '完成引导',
description: '您已了解所有核心功能,点击下方按钮完成引导。',
target: () => ref3.current,
placement: 'topLeft',
// 3. 自定义最后一个步骤的“下一步”按钮
nextButtonProps: {
children: '知道了',
},
},
];

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-[600px]">
<div className="flex justify-between items-center">
<div ref={ref1}><Avatar size="large" icon={<UserOutlined />} /></div>
<Button ref={ref2} type="primary">主操作</Button>
<div ref={ref3}><Button>更多</Button></div>
</div>
<Divider />
<Button type="default" onClick={() => { setOpen(true); setCurrent(0); }}>
开始完全受控的引导
</Button>

<Tour
open={open}
onClose={() => setOpen(false)}
steps={steps}
// 4. 传入 current 和 onChange 实现完全控制
current={current}
onChange={setCurrent}
/>
</div>
);
};

export default ControlledTourDemo;

App.tsx 中切换到新 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import MultiStepTourDemo from './components/demos/MultiStepTourDemo';
import ControlledTourDemo from './components/demos/ControlledTourDemo';
// ... 其他 import ...

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* <MultiStepTourDemo /> */}
<ControlledTourDemo />
</div>
</ConfigProvider>
);
};
  • current & onChange: 这组属性将 Tour 的步骤状态完全交由我们控制。current 决定了当前显示第几步,而 onChange 是在用户点击“上一步”/“下一步”时,Tour 组件通知我们“期望的下一步是哪一步”的回调。我们必须使用 setCurrent 来更新状态,否则即使用户点击,步骤也不会变化。
  • 丰富的 steps 属性: 除了 targetdescriptionsteps 对象还支持 covernextButtonProps 等丰富的自定义属性,让我们可以创建出内容更生动、交互更友好的引导卡片。

通过这三个由浅入深的步骤,我们已经从根本上掌握了 Tour 组件的设计思想和全部核心用法,完全有能力为应用打造出专业且体验优秀的漫游式引导功能。


7.15.2. Alert: 页面级的静态警告提示

Alert (警告提示) 是用于在页面上展示需要用户关注的 静态信息 的核心组件。与那些会自动消失的全局提示(Message, Notification)不同,Alert 作为页面布局的一部分,会 持续存在,直到用户手动关闭或条件发生改变。它非常适合展示那些希望用户在浏览页面时能随时看到的持久性信息。

  • Notification/Message 的区别Alert 是“嵌入式”的,属于页面内容流的一部分,不会自动消失。而 NotificationMessage 是“浮层式”的,用于提供短暂、即时的反馈,通常在几秒后自动消失。
  • Modal 的区别Alert 是“非阻塞”的,用户可以忽略它并继续与页面的其他部分交互。而 Modal 是“阻塞式”的,会打断用户的工作流,强制用户进行处理。

核心应用场景:

  • 系统状态通知:“您的账户将于 3 天后到期,请及时续费。”
  • 功能说明:“当前功能处于 Beta 测试阶段,部分数据可能不准确。”
  • 操作结果提示:“文件上传失败,请检查您的网络连接后重试。”

第一步:基础用法与四种样式

我们最常见的需求,就是在页面的某个地方,根据不同的情况显示一条成功、信息、警告或错误提示。这可以通过 messagetype 两个核心属性轻松实现。

文件路径: src/App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React from 'react';
import { ConfigProvider, Alert, Space } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex justify-center">
<div className="p-8 bg-white rounded-lg shadow-lg w-[800px]">
<h3 className="text-xl font-bold mb-4 text-center">基础用法:四种样式</h3>
<Space direction="vertical" style={{ width: '100%' }}>
<Alert message="操作成功" type="success" />
<Alert message="这是一条普通信息" type="info" />
<Alert message="请注意,此操作可能存在风险" type="warning" />
<Alert message="发生了一个错误" type="error" />
</Space>
</div>
</div>
</ConfigProvider>
);
};

export default App;

代码深度解析

  • message: 这是 Alert 最核心的内容属性,用于显示主要的提示信息,通常是一句简短的话。
  • type: 这个属性决定了 Alert 的“情绪”和外观。它会改变背景色、边框色和默认图标,向用户传达明确的语义:
    • success: 绿色样式,用于表示一个成功的、积极的结果。
    • info: 蓝色样式,用于展示中性的、普通的信息或提示。(这是 type 的默认值)。
    • warning: 橙色样式,用于提醒用户注意,某个操作可能有潜在的、非致命的风险。
    • error: 红色样式,用于明确地指出一个错误已经发生,或者某个操作失败了。

第二步:丰富内容与可关闭性

有时,一句简单的 message 不足以解释清楚情况,我们需要提供更详细的描述。同时,我们也可能希望用户在阅读完提示后,能够手动将其关闭。

文件路径: src/App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React from 'react';
import { ConfigProvider, Alert, Space } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const App: React.FC = () => {
const handleClose = () => {
console.log('警告框被关闭了');
};

return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex justify-center">
<div className="p-8 bg-white rounded-lg shadow-lg w-[800px]">
<h3 className="text-xl font-bold mb-4 text-center">详细描述与可关闭</h3>
<Space direction="vertical" style={{ width: '100%' }}>
<Alert
message="请求成功"
description="您的请求已成功处理,详细数据正在后台同步中,请稍后刷新查看。"
type="success"
/>
<Alert
message="这是一个可关闭的警告"
type="warning"
closable
onClose={handleClose}
/>
</Space>
</div>
</div>
</ConfigProvider>
);
};

export default App;

代码深度解析

  • description: 此属性用于提供比 message 更详尽的补充说明。在渲染时,message 会自动成为加粗的标题,而 description 会成为下方的正文内容,形成了清晰的视觉层级,非常适合展示“标题+正文”结构的信息。
  • closable: 一个简单的布尔属性。当设置为 true 时,Alert 的右上角会自动渲染一个关闭(“x”)图标。
  • onClose: 这是一个与 closable 配套的回调函数,当用户点击关闭按钮后被触发。您可以利用这个回调来执行一些状态更新或记录日志等操作。

第三步:高级用法 - 图标与自定义操作

为了让提示更醒目,或在提示框内直接提供操作项(如“撤销”、“查看详情”),我们可以使用 showIconaction 属性。

文件路径: src/App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import React from 'react';
import { ConfigProvider, Alert, Space, Button } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex justify-center">
<div className="p-8 bg-white rounded-lg shadow-lg w-[800px]">
<h3 className="text-xl font-bold mb-4 text-center">图标与自定义操作</h3>
<Space direction="vertical" style={{ width: '100%' }}>
<Alert message="成功提示,强制显示图标" type="success" showIcon />
<Alert
message="您的设置已保存"
type="success"
showIcon
action={
<Button size="small" type="text">
撤销
</Button>
}
closable
/>
<Alert
message="发现新的系统更新"
description="V5.25.0 版本已发布,包含多项性能优化和新功能。"
type="info"
showIcon
action={
<Space direction="vertical">
<Button size="small" type="primary">
立即更新
</Button>
<Button size="small" ghost type="primary">
查看详情
</Button>
</Space>
}
closable
/>
</Space>
</div>
</div>
</ConfigProvider>
);
};

export default App;

代码深度解析

  • showIcon: 默认情况下,只有带 descriptionAlert 才会自动显示图标。将 showIcon 设为 true 可以强制所有 Alert 都显示一个与其 type 对应的默认图标,使提示更加醒目和易于区分。
  • action: 这是一个功能强大的“插槽”属性,它允许您在 Alert 的右侧(关闭按钮左侧)嵌入任意 React 节点。这使得 Alert 不再仅仅是一个静态的信息展示板,而可以成为一个轻量级的交互区域,让用户可以直接在提示信息上执行相关操作,极大地缩短了用户的操作路径。

7.15.3. Message: 轻量级的全局反馈

当用户完成一个操作(如提交表单、删除数据)后,我们需要给予及时、明确的反馈。Message (全局提示) 就是为此而生的轻量级反馈组件。它以 浮层 的形式在页面顶部中心位置弹出,并在几秒后 自动消失,整个过程不会打断用户的当前操作流。

  • Alert 的核心区别Alert 是“嵌入式”的、静态的,用于展示需要 持续存在 的页面级信息。而 Message 是“浮层式”的、动态的,用于提供 短暂、即时 的操作反馈。

核心应用场景:

  • 操作成功:“您的设置已保存成功。”
  • 操作失败:“网络错误,请稍后重试。”
  • 加载状态:“正在保存中,请稍候…”

第一步:核心用法 - useMessage Hook (官方推荐)

场景分析:
在 React 的世界里,组件的行为(如主题、语言等;)都依赖于上下文(Context)。过去直接调用 message.success() 的静态方法,会创建一个独立的 React 应用实例,导致它无法获取到您主应用的 ConfigProvider 配置(如主题色、语言包等)。

为了解决这个问题,Ant Design 提供了 message.useMessage() Hook。这是在 2025 年使用 Message唯一推荐方式。它能确保您弹出的全局提示与您的主应用共享同一个上下文,保证视觉和功能的一致性。

解决方案:
message.useMessage() Hook 会返回一个包含两项的数组 [api, contextHolder]

  • api: 一个对象,包含了 success, error, warning 等所有可调用的提示方法。
  • contextHolder: 一个特殊的 React 节点,您 必须 将它渲染到您的组件树中。它的作用就像一个“传送门”,api 调用的提示内容会通过它被渲染出来,并正确地继承上下文。

关于之前我们频繁使用的 message 静态方法为了让您更清晰地理解,请看下面的对比表格:

特性message.success() (静态方法)api.success() (来自 useMessage)
调用方式全局直接调用,无需准备必须先调用 useMessage Hook,并在组件中渲染 contextHolder
底层原理创建一个独立的 React 渲染根节点在当前应用的 React 组件树中渲染
优点调用简单,不依赖于组件上下文功能完整,能获取所有 React Context
缺点无法获取 Context (主题、语言等),导致样式和行为不一致,是 已被官方不推荐 的用法写法上需要预先设置 contextHolder
2025 年推荐度极低(仅用于无自定义配置的极简 Demo)极高(真实项目开发的标准实践)

文件路径: src/App.tsx

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import React from "react";
import { ConfigProvider, message, Button, Space } from "antd";
import zhCN from "antd/locale/zh_CN";
import "dayjs/locale/zh-cn";

// 为了演示清晰,我们将使用 useMessage 的部分封装在一个子组件中
const MyApp: React.FC = () => {
// 1. 调用 useMessage Hook
const [messageApi, contextHolder] = message.useMessage();

const showSuccess = () => {
messageApi.success("操作已成功!");
};

const showError = () => {
// 我们在这里故意用一个静态的message来看到区别
message.error("操作失败,请检查您的输入。");
};

const showWarning = () => {
messageApi.warning("部分选填项未填写。");
};

return (
<div className="p-8 bg-white rounded-lg shadow-lg w-[800px]">
{/* 2. 必须在页面中渲染 contextHolder */}
{contextHolder}
<h3 className="text-xl font-bold mb-4 text-center">基础全局提示</h3>
<Space wrap>
{/* 3. 通过 api 对象调用提示方法 */}
<Button type="primary" onClick={showSuccess}>
显示成功提示
</Button>
<Button danger onClick={showError}>
显示失败提示(错误的调用方法)
</Button>
<Button onClick={showWarning}>显示警告提示</Button>
</Space>
</div>
);
};

// 整个应用
const App: React.FC = () => {
return (
<ConfigProvider
locale={zhCN}
// 自定义主题
theme={{
token: {
colorPrimary: "#1677ff",
},
// 自定义消息组件的样式
components: {
Message: {
contentBg: "#ffe7ba",
},
},
}}
>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<MyApp />
</div>
</ConfigProvider>
);
};

export default App;

代码深度解析

  • message.useMessage() 的位置: 这个 Hook 必须在函数组件的顶层调用,就像 useState 一样。
  • contextHolder 的重要性: contextHolder 是连接 api 调用和 React 渲染树的桥梁。如果您忘记在组件中渲染它,messageApi 的所有调用都将静默失败,不会显示任何提示。 它应该被放置在您希望它能访问到的所有 Context Provider 的内部。在上面的例子中,它被放在了 ConfigProvider 的内部,因此它可以获取到中文语言包的配置。
  • messageApi 对象: 这个 api 对象在使用上与旧的静态 message 对象几乎完全一样,包含了 .success(), .error(), .warning(), .info(), .loading() 等方法,让您可以平滑迁移。

第二步:处理异步流程 - 加载中与更新提示

场景分析:
一个非常常见的场景是处理异步操作,例如向服务器提交数据。我们希望在请求开始时显示“正在加载…”,然后在请求结束后,将 同一个提示 更新为“成功”或“失败”,而不是弹出一个新的提示。

解决方案:
messageapi.open() 方法允许我们通过一个唯一的 key 来创建和更新提示。

文件路径: src/App.tsx

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import React from 'react';
import { ConfigProvider, message, Button, Space } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const App: React.FC = () => {
const [messageApi, contextHolder] = message.useMessage();
const messageKey = 'saveRequest';

const handleAsyncSave = () => {
// 1. 显示 "加载中" 提示,并设置一个唯一的 key。
// duration 设置为 0 表示该提示不会自动消失。
messageApi.open({
key: messageKey,
type: 'loading',
content: '正在保存中...',
duration: 0,
});

// 2. 模拟一个 2 秒的异步网络请求
setTimeout(() => {
// 3. 模拟请求成功
// 使用相同的 key 来更新之前的 "加载中" 提示
messageApi.open({
key: messageKey,
type: 'success',
content: '数据保存成功!',
duration: 2, // 成功提示在 2 秒后消失
});
}, 2000);
};

return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{contextHolder}
<div className="p-8 bg-white rounded-lg shadow-lg w-[800px]">
<h3 className="text-xl font-bold mb-4 text-center">异步流程提示</h3>
<Button type="primary" onClick={handleAsyncSave}>
模拟异步保存
</Button>
</div>
</div>
</ConfigProvider>
);
};

export default App;

  • api.open(config): 这是 message 最灵活的调用方式。它接受一个配置对象,允许我们精细化控制提示的各个方面。
  • key: 这是实现提示 更新 的关键。当我们使用一个已经存在的 key 来调用 api.open() 时,Ant Design 不会创建一个新的提示,而是会找到具有相同 key 的已有提示,并用新的内容和类型去更新它。这正是我们实现 Loading -> Success/Error 流程的核心机制。
  • duration: 提示的持续时间,单位是秒。将其设置为 0 意味着该提示 永不自动关闭,这对于需要等待异步操作完成的“加载中”提示非常有用。当后续的 successerror 提示更新它时,可以再设置一个新的 duration 让它自动关闭。

7.15.4. Notification: 更丰富的全局通知提醒

Notification (通知提醒框) 是 antd 提供的另一个全局反馈组件。与 Message 相比,Notification 在视觉上“更重”,它以卡片的形式出现在屏幕的角落,能够承载更丰富、更结构化的信息。

择使用 Notification(通知提醒框)和 Message(全局提示)时,您可以参考下表中的核心区别,以匹配您的应用场景。

特性Message (全局提示)Notification (通知提醒框)
使用场景用于需要 轻量级、纯文本 的即时反馈。用于传达 更重要或更复杂 的系统级通知。
内容复杂度通常只包含 纯文本 内容,结构简单。包含独立的 标题详细描述,内容结构更丰富。
交互性交互性弱,通常用于信息展示,然后自动消失。交互性强,可以包含自定义按钮等操作,引导用户进行下一步。
视觉位置通常出现在页面 顶部中心通常出现在页面的 四个角落 之一(默认为右上角)。
典型案例“操作成功”、“已复制到剪贴板”、“处理中…”“您有一条新消息”、“版本更新通知”、“任务执行失败”
总结反馈(Feedback):对用户当前操作的简单、即时响应。通知(Notification):系统主动推送的、需要用户关注的重要信息。

第一步:核心用法 - useNotification Hook

Message 组件完全一样,Notification 的静态调用方法(如 notification.success())也存在无法获取 React Context 的问题。因此,在 2025 年,使用 notification.useNotification() Hook 是唯一推荐的标准实践

它的工作原理与 message.useMessage() 完全相同:

  1. 调用 notification.useNotification() Hook,获取 [api, contextHolder]
  2. 在您的组件树中渲染 contextHolder 节点。
  3. 使用 api 对象来调用 success, error 等方法。

文件路径: src/App.tsx

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import React from 'react';
import { ConfigProvider, notification, Button, Space } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const App: React.FC = () => {
// 1. 调用 useNotification Hook
const [api, contextHolder] = notification.useNotification();

const openNotification = (type: 'success' | 'info' | 'warning' | 'error') => {
// 2. 使用 api 对象调用通知
api[type]({
message: `这是一个 ${type} 通知`,
description:
'这是通知的详细描述内容,这里可以放入更长的文本,对通知标题进行补充说明。',
});
};

return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* 3. 必须渲染 contextHolder 以使通知能够弹出 */}
{contextHolder}
<div className="p-8 bg-white rounded-lg shadow-lg w-[800px]">
<h3 className="text-xl font-bold mb-4 text-center">基础通知提醒框</h3>
<Space wrap>
<Button type="primary" onClick={() => openNotification('success')}>
显示成功通知
</Button>
<Button onClick={() => openNotification('info')}>
显示信息通知
</Button>
<Button onClick={() => openNotification('warning')}>
显示警告通知
</Button>
<Button danger onClick={() => openNotification('error')}>
显示失败通知
</Button>
</Space>
</div>
</div>
</ConfigProvider>
);
};

export default App;

代码深度解析

  • useNotificationcontextHolder: 其原理和重要性与 message.useMessage 完全一致。contextHolder 是一个必须被渲染的“传送门”,它使得通过 api 调用的 Notification 能够继承 ConfigProvider 提供的所有上下文信息(如主题、语言等),从而保证了视觉和功能的一致性。
  • message vs description: 这是 NotificationMessage 最直观的区别。api.success(config) 接受一个配置对象,其中:
    • message: 作为通知的 标题,通常是加粗的、简短的文本。
    • description: 作为通知的 正文,用于提供更详细的补充信息。

第二步:自定义配置 - 位置、时长与交互按钮

Notification 的强大之处在于其高度的可配置性,我们可以轻松控制它的弹出位置、持续时间,甚至在其中嵌入可交互的按钮。

文件路径: src/App.tsx

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import React from 'react';
import { ConfigProvider, notification, Button, Space } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import type { NotificationArgsProps } from 'antd';

const App: React.FC = () => {
const [api, contextHolder] = notification.useNotification();

const openCustomNotification = () => {
const key = `open${Date.now()}`; // 生成一个唯一的 key

const btn = (
<Space>
<Button type="link" size="small" onClick={() => api.destroy(key)}>
稍后处理
</Button>
<Button type="primary" size="small" onClick={() => alert('正在执行操作...')}>
立即执行
</Button>
</Space>
);

const config: NotificationArgsProps = {
key,
message: '您有一个新的待办事项',
description: '一个高优先级的任务需要您立即处理,请确认是否执行。',
placement: 'bottomLeft', // 从左下角弹出
duration: 0, // 不会自动关闭
actions: btn, // 自定义操作按钮 (v5.24.0+ 推荐)
// btn: btn, // 旧版 API,v5.24.0 之前使用
onClose: () => {
console.log(`通知 (key: ${key}) 已关闭`);
},
};

api.open(config);
};

return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{contextHolder}
<div className="p-8 bg-white rounded-lg shadow-lg w-[800px]">
<h3 className="text-xl font-bold mb-4 text-center">自定义通知</h3>
<Button type="primary" onClick={openCustomNotification}>
打开一个带按钮且不会自动关闭的通知
</Button>
</div>
</div>
</ConfigProvider>
);
};

export default App;
  • placement: Notification 最具特色的属性之一。您可以指定 topLeft, topRight (默认), bottomLeft, bottomRight 四个角落中的任意一个作为弹出位置,以适应不同的页面布局。
  • duration: 与 Message 类似,它控制通知的自动关闭延时(单位:秒)。默认值是 4.5 秒。设置为 0null 则表示通知永不自动关闭,必须由用户手动点击关闭按钮,或者通过代码调用 api.destroy(key) 来关闭。
  • actions: 这是 Notification 的一个核心高级功能,它允许您在通知卡片的右下角嵌入自定义的 React 节点(通常是按钮)。这使得通知不再只是一个“只读”的信息,而是一个 可交互 的面板,用户可以直接在通知上执行“确认”、“撤销”、“查看”等操作,极大地提升了交互效率。
  • keyapi.destroy(key): 和 Message 一样,为 Notification 指定一个唯一的 key,可以让我们在之后通过 api.destroy(key) 来精确地、程序化地关闭这个特定的通知。在上面的例子中,我们用它实现了“稍后处理”按钮的功能。

7.15.5. Result: 清晰的操作结果反馈

当用户完成一个重要的、多步骤的操作流程(如支付、注册、表单提交)后,他们需要一个非常清晰、明确的页面来告知他们最终的结果。Result (结果) 组件就是专为此类“最终状态”反馈而设计的。它通过一个醒目的图标、标题和描述,占据页面的主要区域,为用户的操作画上一个圆满的句号。

  • Alert 的核心区别Alert 是一个 行内 的提示条,用于在当前页面内容中给出提示,不打断流程。而 Result 是一个 块级 的大面积组件,它本身就 构成了一个页面或页面的核心内容,用于展示一个任务的终态。

核心应用场景:

  • 操作成功页:“恭喜您,订单提交成功!”
  • 操作失败页:“抱歉,由于网络问题,您的操作失败了。”
  • HTTP 状态页:为用户展示 403 (无权限)、404 (页面未找到)、500 (服务器错误) 等页面。

第一步:基础用法 - 四种语义状态

Result 组件通过 status 属性来驱动其核心外观和语义。最常用的有四种语义状态:success, error, info, warning。同时,title, subTitle, 和 extra 属性则用于填充内容和后续操作。

文件路径: src/App.tsx

image-20251001092558401

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import React from 'react';
import { ConfigProvider, Result, Button, Space, Divider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-white min-h-screen p-8">
<h3 className="text-2xl font-bold mb-8 text-center">Result 组件 - 语义状态</h3>

{/* 1. 成功状态 */}
<Result
status="success"
title="项目创建成功!"
subTitle="项目编号:202510010924,您可以在项目列表中查看详情。"
extra={[
<Button type="primary" key="console">返回控制台</Button>,
<Button key="buy">再创建一个</Button>,
]}
/>

<Divider />

{/* 2. 失败状态 */}
<Result
status="error"
title="提交失败"
subTitle="请检查并修改以下信息后,再重新提交。"
extra={[
<Button type="primary" key="console">返回修改</Button>,
<Button key="buy">联系客服</Button>,
]}
/>

{/* 我们可以将其他状态放在 Tabs 或其他容器中进行展示,
这里为了清晰,我们只展示最常用的 success 和 error。
info 和 warning 的用法完全相同。
*/}
</div>
</ConfigProvider>
);
};

export default App;
  • status: 这是 Result 最核心的属性。当您传入一个预设值(如 'success''error')时,组件会自动为您渲染对应的图标(对勾、叉号)和主色调。
  • title: 结果页的主标题,用于直接告知用户最核心的结果信息。
  • subTitle: 副标题,用于提供更详细的上下文信息,例如订单号、下一步指引等。
  • extra: 这是“额外操作区”,它接收一个 React 节点数组。这里是放置 后续操作按钮 的最佳位置,例如“返回首页”、“查看详情”、“再试一次”等,旨在引导用户进行下一步操作,避免流程中断。

第二步:特殊场景 - HTTP 状态码

Web 开发中一个常见的需求是为各种 HTTP 错误(如 404, 403, 500)创建友好的提示页面。Result 组件内置了对这些常见状态码的支持,可以一键生成符合设计规范的错误页面。

文件路径: src/App.tsx

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import React from 'react';
import { ConfigProvider, Result, Button, Tabs } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const App: React.FC = () => {
const items = [
{
key: '403',
label: '403 页面',
children: (
<Result
status="403"
title="403"
subTitle="抱歉,您无权访问此页面。"
extra={<Button type="primary">返回首页</Button>}
/>
),
},
{
key: '404',
label: '404 页面',
children: (
<Result
status="404"
title="404"
subTitle="抱歉,您访问的页面不存在。"
extra={<Button type="primary">返回首页</Button>}
/>
),
},
{
key: '500',
label: '500 页面',
children: (
<Result
status="500"
title="500"
subTitle="抱歉,服务器出错了。"
extra={<Button type="primary">返回首页</Button>}
/>
),
},
];

return (
<ConfigProvider locale={zhCN}>
<div className="bg-white min-h-screen p-8">
<h3 className="text-2xl font-bold mb-8 text-center">Result 组件 - HTTP 状态</h3>
<Tabs defaultActiveKey="404" items={items} centered />
</div>
</ConfigProvider>
);
};

export default App;
  • status="404" | "403" | "500": 当 status 属性被设置为这些特定的 HTTP 状态码字符串时,Result 组件会渲染出 antd 官方设计的、符合场景的插图和默认文案。这极大地简化了我们创建标准错误页面的工作量。
  • 自定义文案: 尽管 antd 提供了默认的 titlesubTitle,您依然可以像上一个示例一样,传入您自己的 titlesubTitle 属性来覆盖它们,以满足更具体的业务需求。
  • icon 属性: 如果您对所有预设的图标都不满意,还可以通过 icon 属性传入一个自定义的 React 节点(例如 <SmileOutlined />)来完全替换默认图标,实现更高程度的定制化。

7.15.6. Progress & Spin: 展示确定与不确定的等待状态

在任何需要与服务器交互或执行耗时操作的应用中,向用户提供清晰的“等待”反馈是至关重要的。它能有效缓解用户的焦虑,并告知他们系统正在正常工作。Ant Design 为此提供了两个核心组件:ProgressSpin

理解它们之间的核心区别是正确使用它们的第一步:

  • 使用 Progress (进度条):当一个操作的 进度是可量化的、可预测的 时候使用。它明确地回答了“任务完成了多少?”这个问题。

  • 典型场景:文件上传进度、多步骤表单的完成度、视频转码进度。

  • 使用 Spin (加载中):当一个操作的 耗时不确定,或者进度无法量化 的时候使用。它只回答一个问题:“系统正在忙,请稍候”。

  • 典型场景:从服务器获取数据、提交表单后的等待、页面初次加载。


第一部分: Progress - 展示可量化的进度

Progress 组件通过直观的图形(线条或圆环)来展示一个任务的完成百分比。

核心用法与不同形态

Progress 的核心由 percent 属性驱动,同时 type 属性决定了它的外观形态。

文件路径: src/App.tsx

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import React, { useState } from 'react';
import { ConfigProvider, Progress, Button, Space, Divider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const App: React.FC = () => {
const [percent, setPercent] = useState(30);

const increase = () => {
setPercent((prevPercent) => (prevPercent >= 100 ? 100 : prevPercent + 10));
};

const decline = () => {
setPercent((prevPercent) => (prevPercent <= 0 ? 0 : prevPercent - 10));
};

return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex justify-center">
<div className="p-8 bg-white rounded-lg shadow-lg w-[800px]">
<h3 className="text-xl font-bold mb-4 text-center">Progress 组件演示</h3>

<Divider orientation="left">线状进度条 (type="line")</Divider>
<Progress percent={percent} />
<Progress percent={50} status="active" />
<Progress percent={70} status="exception" />
<Progress percent={100} />
<Progress percent={50} showInfo={false} />

<Divider orientation="left">圆形进度条 (type="circle")</Divider>
<Space wrap>
<Progress type="circle" percent={percent} />
<Progress type="circle" percent={70} status="exception" />
<Progress type="circle" percent={100} />
</Space>

<Divider orientation="left">仪表盘 (type="dashboard")</Divider>
<Progress type="dashboard" percent={percent} />

<Divider orientation="left">动态控制</Divider>
<Button.Group>
<Button onClick={decline}>-</Button>
<Button onClick={increase}>+</Button>
</Button.Group>
</div>
</div>
</ConfigProvider>
);
};

export default App;
  • percent: 核心属性,一个从 0 到 100 的数字,直接控制进度条的填充程度。如示例所示,我们可以通过 useState 来动态地更新它。
  • type: 决定了进度条的形态。
    • line: 默认值,标准的水平线状进度条。
    • circle: 圆形进度圈。
    • dashboard: 类似汽车仪表盘的形态。
  • status: 控制进度条的当前状态,会影响其颜色。
    • normal: 默认状态。
    • active: (仅线状)蓝色,并带有动态光效,表示“正在进行中”。
    • exception: 红色,表示出现了错误。
    • success: 绿色(默认 percent={100} 时的颜色),表示已成功完成。
  • format: 一个函数,用于自定义显示的文本内容。默认 (percent) => percent + '%'. 您可以用它来实现更复杂的显示

第二部分: Spin - 表明“正在处理”

Spin 用于向用户表明,当前有一个操作正在后台处理,但具体需要多长时间是未知的。它最强大的用法是作为 包装器,给一个特定区域(如卡片或表单)添加加载中的遮罩和指示器。

核心用法与包裹模式

Spin 通过 spinning 属性来控制其是否可见。

文件路径: src/App.tsx

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import React, { useState } from 'react';
import { ConfigProvider, Spin, Switch, Card, Alert } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const App: React.FC = () => {
const [loading, setLoading] = useState(false);

const toggle = (checked: boolean) => {
setLoading(checked);
};

return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex justify-center">
<div className="p-8 bg-white rounded-lg shadow-lg w-[800px]">
<h3 className="text-xl font-bold mb-4 text-center">Spin 组件演示</h3>

<div className="mb-4">
<p className="mb-2">基础用法:</p>
<Spin />
</div>

<div className="mb-4">
<p className="mb-2">作为容器,包裹一个区域:</p>
<div className="p-12 relative border rounded">
{/*
1. 使用 Spin 包裹需要显示加载状态的区域。
2. spinning 属性控制 Spin 是否可见。
3. tip 属性可以添加加载提示文案。
*/}
<Spin spinning={loading} tip="正在加载数据...">
<Alert
message="这里是内容区域"
description="当 spinning 为 true 时,这块内容会被遮罩覆盖。"
type="info"
/>
</Spin>
</div>
<div style={{ marginTop: 16 }}>
切换加载状态:
<Switch checked={loading} onChange={toggle} />
</div>
</div>
</div>
</div>
</ConfigProvider>
);
};

export default App;
  • 独立使用: Spin 可以作为一个独立的组件直接使用(<Spin />),它会渲染一个旋转的加载指示器。您可以通过 size (small, default, large) 属性控制其大小。
  • 包裹模式: 这是 Spin 最常用、最强大的用法。当您将 Spin 作为容器包裹其他组件时:
    • spinning: 这是一个布尔值,是 Spin核心开关。当 spinning={true} 时,Spin 会在被包裹的子元素上层渲染一个半透明的遮罩和居中的加载指示器。当 spinning={false} 时,Spin 则完全不可见,只渲染其子元素。
    • tip: 可以在加载指示器下方显示一行文字,用于告诉用户“正在加载什么”。
  • delay (重要 UX 优化): Spin 还有一个 delay 属性(单位:毫秒)。如果一个异步操作完成得非常快(比如小于 200 毫秒),加载指示器“闪一下”就消失会给用户带来不好的体验。设置 delay={500} 意味着,只有当 spinning 状态持续为 true 超过 500 毫秒后,加载指示器才会真正显示出来。这是一个非常重要的用户体验优化技巧。

7.15.7. Modal: 强交互的对话框

Modal (对话框) 是一个在当前页面之上、屏幕中央弹出的浮层。它会 中断 用户的当前工作流,并强制用户与其进行交互。这种 阻塞式 的特性,决定了它专门用于处理那些需要用户高度聚焦、必须立即处理的事务。

Modal 有两种主要的用法:

  1. 声明式 <Modal /> 组件:用于承载表单等复杂内容。
  2. 命令式 modal.confirm() 等方法:通过 Modal.useModal() Hook 调用,用于快速弹出简单的确认框。

我们将分步学习这两种用法。

第一步:基础用法 - 声明式 <Modal /> 组件

当您需要在弹窗中展示自定义内容(如一个表单、一段复杂的文本或图片)时,就应该使用声明式的 <Modal /> 组件。它的显示和隐藏,通过 open 属性进行完全受控。

文件路径: src/App.tsx

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import React, { useState } from 'react';
import { ConfigProvider, Modal, Button } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const App: React.FC = () => {
// 1. 使用 state 控制 Modal 的显示/隐藏
const [isModalOpen, setIsModalOpen] = useState(false);

const showModal = () => {
setIsModalOpen(true);
};

const handleOk = () => {
console.log('点击了 OK');
setIsModalOpen(false);
};

const handleCancel = () => {
console.log('点击了 Cancel');
setIsModalOpen(false);
};

return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8">
{/* 2. 按钮用于打开 Modal */}
<Button type="primary" onClick={showModal}>
打开基础对话框
</Button>

{/* 3. Modal 组件本体 */}
<Modal
title="基础对话框"
open={isModalOpen} // 绑定 state
onOk={handleOk} // 点击确定按钮时的回调
onCancel={handleCancel} // 点击取消关闭图标或遮罩时的回调
>
<p>这里是对话框的内容...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</Modal>
</div>
</ConfigProvider>
);
};

export default App;
  • 受控模式: Modalopen 属性是其是否可见的唯一来源。与 Drawer 一样,我们必须通过 useState 来管理其可见性。
  • onOkonCancel: 这是 Modal 的两个核心回调函数。
    • onOk: 当用户点击页脚的“确定”按钮时触发。
    • onCancel: 当用户点击“取消”按钮、右上角的关闭图标(‘x’)、或按下 Esc 键时触发。在这两个回调中,我们通常都需要调用 setIsModalOpen(false) 来关闭对话框,从而完成受控模式的数据闭环。
  • footer: 如果您不想要默认的“确定/取消”按钮,可以将 footer 属性设置为 null (footer={null}),或者传入自定义的 React 节点来完全替换它。

第二步:异步操作与确认按钮加载

一个非常常见的场景是:在 Modal 中提交一个表单,需要调用一个 API。在这个过程中,我们希望“确定”按钮显示加载状态,并且只有在 API 调用成功后才关闭 Modal

文件路径: src/App.tsx

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import React, { useState } from 'react';
import { ConfigProvider, Modal, Button } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const App: React.FC = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
// 1. 新增一个 state 用于控制确认按钮的 loading 状态
const [confirmLoading, setConfirmLoading] = useState(false);

const showModal = () => { setIsModalOpen(true); };

const handleOk = () => {
// 2. 开始异步操作,设置 loading 为 true
setConfirmLoading(true);
console.log('开始提交...');

// 3. 模拟一个 2 秒的异步网络请求
setTimeout(() => {
// 4. 异步操作结束,关闭 loading,并关闭 Modal
setConfirmLoading(false);
setIsModalOpen(false);
console.log('提交成功!');
}, 2000);
};

const handleCancel = () => {
setIsModalOpen(false);
};

return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8">
<Button type="primary" onClick={showModal}>
打开异步对话框
</Button>
<Modal
title="异步操作"
open={isModalOpen}
onOk={handleOk}
onCancel={handleCancel}
// 5. loading state 绑定到 confirmLoading 属性
confirmLoading={confirmLoading}
>
<p>点击“确定”后,按钮将进入加载状态,2秒后对话框关闭。</p>
</Modal>
</div>
</ConfigProvider>
);
};

export default App;
  • confirmLoading: 这是一个布尔属性,专门用于控制“确定”按钮的加载状态。当它为 true 时,按钮会自动显示加载图标并被禁用,防止用户重复点击。
  • 异步 onOk: 通过将 onOk 回调设计成一个异步流程的启动器,我们获得了对 Modal 关闭时机的完全控制。只有当我们的异步逻辑(如 API 调用)执行完毕,并手动调用 setIsModalOpen(false) 时,对话框才会关闭。这对于处理需要等待服务器响应的操作至关重要。

第三步:快捷确认框 - Modal.useModal()

对于“您确定要删除吗?”这类简单的确认场景,每次都写一套 useState 来控制显隐会显得很繁琐。为此,Ant Design 提供了命令式的 API。与 MessageNotification 一样,Modal.useModal() 是官方推荐的、能够获取 React Context 的标准用法

文件路径: src/App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import React from 'react';
import { ConfigProvider, Modal, Button } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const App: React.FC = () => {
// 1. 调用 useModal Hook
const [modal, contextHolder] = Modal.useModal();

const showConfirm = async () => {
// 2. 使用 modal 对象调用 confirm 方法
try {
await modal.confirm({
title: '确认删除',
content: '您确定要删除这条记录吗?此操作不可恢复。',
okText: '确认',
cancelText: '取消',
// onOk 和 onCancel 也可以是 Promise
onOk: () => {
console.log('用户点击了确认');
// 这里可以执行删除的 API 调用
},
});
console.log('Promise resolved: 用户点击了确认');
} catch (error) {
console.log('Promise rejected: 用户点击了取消或关闭');
}
};

return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8">
{/* 3. 必须渲染 contextHolder */}
{contextHolder}
<Button danger onClick={showConfirm}>
显示确认对话框
</Button>
</div>
</ConfigProvider>
);
};

export default App;

代码深度解析

  • Modal.useModal(): 它与 message.useMessage 的工作原理完全相同,返回 [modal, contextHolder]contextHolder 必须被渲染,以确保弹出的 Modal 能够正确继承主题、语言等上下文信息。
  • modal.confirm(config): modal 对象上提供了 .info(), .success(), .error(), .warning(), .confirm() 等便捷方法。它们都接受一个配置对象来定义对话框的内容。这种命令式的调用方式非常适合触发简单的、一次性的信息或确认框,可以极大简化代码。
  • Promise 接口: modal.confirm 等方法返回一个 Promise。如果用户点击“确定”,Promise 会被 resolve;如果用户点击“取消”或关闭,Promise 会被 reject。这使得我们可以使用 async/await 语法来优雅地处理用户的选择,如示例中所示。

7.15.8. Drawer: 屏幕边缘滑出的浮层面板

Drawer (抽屉) 是一种从屏幕边缘滑出的浮层面板。当您需要在不离开当前页面的情况下,展示辅助信息、设置面板或一个完整的表单时,Drawer 提供了一种比 Modal 更为平滑、侵入性更低的解决方案。

在需要中断用户当前流程或提供补充信息时,Modal(对话框)和 Drawer(抽屉)是两种常见的组件。它们的核心区别在于交互模式和适用场景。

特性Modal (对话框)Drawer (抽屉)
交互模式强打断、阻塞式。通常出现在屏幕 中央,需要用户必须处理完当前任务才能继续与页面其他部分交互。非阻塞、扩展式。从屏幕 侧边 滑出,保留了主界面的上下文,感觉更像是主界面的一个补充面板。
视觉焦点强制用户聚焦于对话框内的任务,是当前流程的 核心作为主流程的 补充,允许用户在视觉上保留对主界面的感知。
适用场景需要用户高度聚焦的 短任务重要确认承载更 复杂的、非核心 的补充任务,且不希望完全打断用户。
典型案例确认删除、简单的信息录入、登录/注册、最终警告。复杂的创建/编辑表单、高级筛选面板、详情预览、应用设置。
总结聚焦任务:强制用户处理一个独立的、高优先级的任务。补充内容:在不离开当前视图的情况下提供额外的、复杂的内容或工具。

第一步:基础抽屉与受控模式

Modal 一样,Drawer 是一个完全受控的组件,它的显示和隐藏完全由 open 属性决定。

核心流程:

  1. 使用 useState 创建一个布尔类型的状态(例如 open)。
  2. open 状态绑定到 Draweropen 属性。
  3. 提供一个触发器(如 Button)来将 open 状态设置为 true
  4. DraweronClose 回调中,将 open 状态设置为 false

文件路径: src/App.tsx

image-20251001095250696

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React, { useState } from 'react';
import { ConfigProvider, Drawer, Button } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const App: React.FC = () => {
// 1. 使用 state 控制抽屉的显示/隐藏
const [open, setOpen] = useState(false);

const showDrawer = () => {
setOpen(true);
};

const onClose = () => {
setOpen(false);
};

return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8">
{/* 2. 按钮用于打开抽屉 */}
<Button type="primary" onClick={showDrawer}>
打开抽屉
</Button>

{/* 3. Drawer 组件本体 */}
<Drawer
title="基础抽屉"
placement="right" // 控制滑出位置
onClose={onClose} // 点击关闭图标或遮罩时调用
open={open} // 绑定 state
>
<p>这里是抽屉的内容...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</Drawer>
</div>
</ConfigProvider>
);
};

export default App;
  • 受控模式: Draweropen 属性是其是否可见的唯一来源。onClose 事件则是 Drawer 通知外部“用户想要关闭我”的信号。我们必须在 onClose 回调中更新 open 状态,否则抽屉将无法关闭,这是 React 受控组件最核心的模式。
  • placement: 决定了抽屉从哪个方向滑出,可选值为 'top', 'right', 'bottom', 'left''right' 是最常用的默认值。

第二步:抽屉中的表单与页脚

Drawer 最常见的用途之一就是承载一个用于 创建编辑 数据的表单。footer 属性为我们提供了一个标准的位置来放置“提交”、“取消”等操作按钮。

文件路径: src/App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import React, { useState } from 'react';
import {
ConfigProvider, Drawer, Button, Form, Input, Select, Space
} from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const { Option } = Select;

const App: React.FC = () => {
const [open, setOpen] = useState(false);
const [form] = Form.useForm();

const showDrawer = () => { setOpen(true); };
const onClose = () => { setOpen(false); };

const onFinish = (values: any) => {
console.log('表单提交:', values);
onClose(); // 提交后关闭抽屉
};

return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8">
<Button type="primary" onClick={showDrawer}>
创建新用户
</Button>
<Drawer
title="创建新用户"
width={520} // 可以自定义宽度
onClose={onClose}
open={open}
// 使用 footer 属性来放置操作按钮
footer={
<Space style={{ textAlign: 'right', width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={onClose}>取消</Button>
<Button onClick={() => form.submit()} type="primary">
提交
</Button>
</Space>
}
>
<Form form={form} layout="vertical" onFinish={onFinish}>
<Form.Item name="name" label="姓名" rules={[{ required: true, message: '请输入姓名' }]}>
<Input placeholder="请输入姓名" />
</Form.Item>
<Form.Item name="gender" label="性别" rules={[{ required: true, message: '请选择性别' }]}>
<Select placeholder="请选择性别">
<Option value="male"></Option>
<Option value="female"></Option>
</Select>
</Form.Item>
</Form>
</Drawer>
</div>
</ConfigProvider>
);
};

export default App;
  • width / height: 当 placement'left''right' 时,使用 width 来控制抽屉的宽度。当 placement'top''bottom' 时,则使用 height
  • footer: 这是一个非常重要的属性。它为抽屉提供了一个 页脚区域,通常用于放置与抽屉内容相关的主要操作按钮。相比于将按钮直接放在表单内部,使用 footer 可以获得更好的样式和布局一致性,并且在内容区域滚动时,页脚通常会保持固定可见。

第三步:嵌套抽屉

在某些复杂的交互流程中,我们可能需要从一个抽屉中打开另一个抽屉,例如在一个“编辑文章”的抽屉中,点击“选择分类”按钮,再弹出一个“分类选择”的抽屉。

文件路径: src/App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import React, { useState } from 'react';
import { ConfigProvider, Drawer, Button } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const App: React.FC = () => {
// 为每个抽屉创建独立的 state
const [openFirst, setOpenFirst] = useState(false);
const [openSecond, setOpenSecond] = useState(false);

return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8">
<Button type="primary" onClick={() => setOpenFirst(true)}>
打开第一层抽屉
</Button>

{/* 第一层抽屉 */}
<Drawer
title="第一层抽屉"
width={520}
onClose={() => setOpenFirst(false)}
open={openFirst}
>
<p>这是第一层抽屉的内容。</p>
<Button type="primary" onClick={() => setOpenSecond(true)}>
打开第二层抽屉
</Button>

{/* 第二层抽屉 */}
<Drawer
title="第二层抽屉"
width={320}
onClose={() => setOpenSecond(false)}
open={openSecond}
>
<p>这是嵌套在内部的第二层抽屉。</p>
</Drawer>
</Drawer>
</div>
</ConfigProvider>
);
};

export default App;
  • 独立的状态管理: 实现嵌套抽屉的关键在于,为 每一个 抽屉都维护一个 独立open 状态。
  • push 行为: 您会注意到,当第二层抽屉打开时,第一层抽屉会自动向左推动一段距离。这是 antd Drawer 内置的 push 行为,它通过视觉上的位移,清晰地向用户展示了抽屉之间的层级关系。您可以通过 push 属性来关闭或自定义这个行为,例如 push={false}

7.15.9 优雅的处理 contextHolder

在我们之前的学习中,当我们在一个页面或组件中同时使用了 Modal.useModal(), message.useMessage(), notification.useNotification() 时,会得到多个 contextHolder

核心原则是:每一个 contextHolder 都必须被渲染出来,因为它们各自负责渲染不同类型的组件(对话框、全局提示、通知提醒框)。

那么,如何优雅地处理它们呢?我们有三种由浅入深的方案。


方法一:直接全部渲染

这是最直接、最简单的方法。如果您只是在单个组件中使用它们,可以直接将所有的 contextHolder 并列渲染出来。使用 React Fragment (<>...</>) 是一个很好的方式,因为它不会在 DOM 中添加额外的节点。

场景: 适用于仅在少数几个、不相关的组件中需要这些反馈功能的情况。

文件路径: src/App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React from 'react';
import { ConfigProvider, Modal, message, notification, Button, Space } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const App: React.FC = () => {
// 1. 分别实例化每一个 hook
const [modalApi, modalHolder] = Modal.useModal();
const [messageApi, messageHolder] = message.useMessage();
const [notificationApi, notificationHolder] = notification.useNotification();

const showAll = () => {
messageApi.success('这是一条 Message 消息');
notificationApi.info({
message: '这是一条 Notification',
description: '它从右上角弹出',
});
modalApi.warning({
title: '这是一个 Modal',
content: '这是一个确认对话框',
});
};

return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
{/* 2. 将所有的 contextHolder 渲染出来 */}
{modalHolder}
{messageHolder}
{notificationHolder}

<div className="p-8 bg-white rounded-lg shadow-lg">
<Button type="primary" onClick={showAll}>
同时触发所有反馈
</Button>
</div>
</div>
</ConfigProvider>
);
};

export default App;
  • 优点: 简单直接,易于理解。
  • 缺点: 如果项目中有大量的组件都需要这些反馈功能,您需要在每个组件里都重复实例化和渲染 contextHolder,这会导致代码冗余。

方法二:集中管理

一个更优雅、更符合大型项目实践的方式,是在您的应用顶层或布局(Layout)组件中,一次性 实例化所有的 contextHolder,然后通过 React Context 将 api 对象传递给所有子组件。

场景: 适用于整个应用各处都需要调用反馈功能的标准项目。

文件路径: src/App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import React, { useContext } from 'react';
import { ConfigProvider, Modal, message, notification, Button, Space } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import type { MessageInstance } from 'antd/es/message/interface';
import type { ModalStaticFunctions } from 'antd/es/modal/confirm';
import type { NotificationInstance } from 'antd/es/notification/interface';

// 1. 创建一个 Context 用于传递 api 对象
interface FeedbackApi {
message: MessageInstance;
notification: NotificationInstance;
modal: Omit<ModalStaticFunctions, 'warn'>;
}
const FeedbackContext = React.createContext<FeedbackApi | null>(null);

// 这是一个子组件,它不知道 contextHolder 的存在
const MyPage: React.FC = () => {
// 3. 在子组件中,通过 useContext 轻松获取 api
const feedback = useContext(FeedbackContext);

const showMessage = () => {
feedback?.message.success('通过 Context 调用的 Message!');
};

return (
<div className="p-8 bg-white rounded-lg shadow-lg">
<Button type="primary" onClick={showMessage}>
从子组件调用 Message
</Button>
</div>
);
};

const App: React.FC = () => {
// 2. 在顶层组件中,统一实例化并渲染 contextHolder
const [modalApi, modalHolder] = Modal.useModal();
const [messageApi, messageHolder] = message.useMessage();
const [notificationApi, notificationHolder] = notification.useNotification();

const feedbackApi: FeedbackApi = {
message: messageApi,
notification: notificationApi,
modal: modalApi,
};

return (
<ConfigProvider locale={zhCN}>
{/* 将 contextHolder 渲染在顶层 */}
{modalHolder}
{messageHolder}
{notificationHolder}

{/* 通过 Provider 将 api 对象传递下去 */}
<FeedbackContext.Provider value={feedbackApi}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<MyPage />
</div>
</FeedbackContext.Provider>
</ConfigProvider>
);
};

export default App;
  • 优点: 遵循了 DRY (Don’t Repeat Yourself) 原则,将所有 contextHolder 的设置逻辑集中在一个地方,子组件只需消费 Context 即可,代码非常整洁。
  • 缺点: 需要手动创建和维护一个 React Context,有一定的设置成本。

方法三(官方最佳实践):使用 <App /> 组件

Ant Design 官方已经预见到了这个普遍需求,并提供了一个终极解决方案:<App /> 组件

<App /> 组件是一个特殊的容器,它在内部 自动 为您处理好了 message, notification, modal 的所有 use... Hook 和 contextHolder 的设置。您只需要用它包裹您的应用,就可以在任何子组件中通过一个静态方法 App.useApp() 轻松获取到上下文感知的 api 实例。

文件路径: src/App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import React from 'react';
import { ConfigProvider, App, Button, Space } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

// 这是一个子组件
const MyPage: React.FC = () => {
// 2. 在任何子组件中,调用 App.useApp()
const { message, notification, modal } = App.useApp();

const showAll = () => {
// 3. 直接使用,无需关心 contextHolder
message.success('由 <App /> 组件管理的消息!');
notification.info({ message: '由 <App /> 组件管理的通知!' });
modal.confirm({ title: '由 <App /> 组件管理的对话框!' });
};

return (
<div className="p-8 bg-white rounded-lg shadow-lg">
<Button type="primary" onClick={showAll}>
通过 App.useApp() 调用
</Button>
</div>
);
};

// 顶层应用
const Root: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
{/* 1. 用 antd 的 <App> 组件包裹您的整个应用 */}
<App>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<MyPage />
</div>
</App>
</ConfigProvider>
);
};

export default Root;
  • 优点: 代码最简洁,心智负担最低。您再也不用关心 contextHolder 的存在,antd<App> 组件为您处理了一切。这是官方提供的、解决此类问题的 最终方案和最佳实践
  • 缺点: 您的整个应用或需要使用这些反馈功能的区域,必须被包裹在 <App> 组件内。

结论

方案优点缺点推荐场景
方法一:直接渲染最简单直接代码冗余,不易维护临时、小范围的 Demo
方法二:集中管理遵循 DRY,代码整洁需要手动设置 Context标准大型项目
方法三:<App /> 组件官方推荐,最简洁,心智负担最低需要将应用包裹在 <App>所有新项目

因此,对于任何新的 Ant Design 项目,强烈建议您直接使用 <App /> 组件来包裹您的应用,这是处理 message, notification, modal 上下文问题的最完美方案。


7.15.10. Popconfirm: 轻量级的内联确认

Popconfirm (气泡确认框) 是一个专门用于 危险操作二次确认 的轻量级组件。当用户点击一个具有破坏性(如删除、清空)或不可逆的操作按钮时,Popconfirm 会在按钮旁弹出一个小气泡,要求用户再次确认,从而有效防止误操作。

两者都用于实现用户的二次确认,以防止误操作,但它们的交互重量级和适用场景有显著不同。

特性Modal.confirm (模态确认框)Popconfirm (气泡确认框)
交互重量级重量级轻量级
交互模式阻塞式。弹出居中的模态对话框,打断用户的整个工作流,需要用户强制处理。内联式。在触发元素旁边弹出,交互更平滑,不完全中断用户流程。
上下文感知上下文丢失。由于是居中模态框,用户的视觉焦点会从触发操作的元素上移开。上下文保留。气泡紧挨着触发元素,用户能清晰地知道正在对哪个对象进行操作。
适用场景适用于 非常重要、罕见、或具有全局影响 的破坏性操作。适用于 常规的、高频的、局部 的危险操作,尤其是在列表或表格中。
典型案例“您确定要删除您的账户吗?”、“确认发布此版本吗?”删除表格中的某一行、取消列表中的某个项目、停用某个用户。
总结严肃的最终警告:用于需要用户深思熟虑的、不可逆的关键操作。便捷的即时确认:用于常规操作的快速二次确认,以减少误触。

第一步:基础用法

Popconfirm 的本质是一个 包装组件,您将需要确认的操作按钮(或其他元素)作为其子元素。其核心是通过 onConfirm 回调来执行真正的危险操作。

核心属性:

  • title: 确认框中显示的提问文本。
  • onConfirm: 核心回调。只有当用户点击“确定”按钮时,此函数才会被触发。
  • onCancel: 用户点击“取消”按钮时触发的回调。

文件路径: src/App.tsx

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import React, { useRef } from 'react';
import { ConfigProvider, Popconfirm, Button, message, App } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

// 使用 App 组件来提供 message 的上下文
const MyPage: React.FC = () => {
const { message } = App.useApp();
const buttonRef = useRef<HTMLButtonElement>(null);
const confirm = () => {
message.success('操作已确认,执行删除!');
// 删除掉按钮
buttonRef.current?.remove();
};

const cancel = () => {
message.error('操作已取消');
};

return (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">基础气泡确认框</h3>
<Popconfirm
title="删除任务"
description="您确定要删除这个任务吗?" // v5.1.0+ 支持更详细的描述
onConfirm={confirm}
onCancel={cancel}
okText="确定"
cancelText="取消"
>
<Button danger ref={buttonRef}>删除</Button>
</Popconfirm>
</div>
);
};

const Root: React.FC = () => (
<ConfigProvider locale={zhCN}>
{/* 使用 antd 的 App 组件包裹,以便 message.useApp() 能正常工作 */}
<App>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<MyPage />
</div>
</App>
</ConfigProvider>
);

export default Root;
  • 包裹模式: 和 Tooltip 类似,<Popconfirm> 包裹了触发它的 <Button>。当用户点击这个按钮时,Popconfirm 会拦截点击事件,并首先弹出确认气泡。
  • onConfirm 的重要性: 这是 Popconfirm 的灵魂。所有危险的、需要被保护的逻辑(例如 API 调用)都应该放在 onConfirm 回调函数中。只有在用户明确点击了“确定”之后,这段代码才会被执行,从而确保了操作的安全性。
  • description: 除了 title 之外,v5.1.0 引入了 description 属性,允许您提供更详细的上下文描述,让用户在做决定时能获取更充分的信息。

第二步:自定义与异步操作

在真实项目中,确认操作通常是异步的(例如调用后端的删除接口)。我们希望在异步操作期间,确认按钮能显示加载状态,以提供明确的反馈。

文件路径: src/App.tsx

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import React, { useState } from 'react';
import { ConfigProvider, Popconfirm, Button, App } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

// 模拟一个需要 2 秒才能完成的异步删除函数
const deleteItem = (): Promise<void> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 2000);
});
};

const MyPage: React.FC = () => {
const { message } = App.useApp();
// 1. 使用 state 控制确认按钮的 loading 状态
const [loading, setLoading] = useState(false);

const handleConfirm = async () => {
// 2. 开始异步操作,设置 loading
setLoading(true);
try {
await deleteItem(); // 调用异步函数
message.success('项目已成功删除');
} catch {
message.error('删除失败,请重试');
} finally {
// 3. 异步操作结束,无论成功失败,都关闭 loading
setLoading(false);
// Popconfirm 会在 onConfirm 的 Promise resolve 后自动关闭
}
};

return (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">异步操作确认</h3>
<Popconfirm
title="确认删除该项目吗?"
description="此操作不可恢复,将永久删除该项目及其所有关联数据。"
onConfirm={handleConfirm}
// 4. loading 状态绑定到 okButtonProps
okButtonProps={{ loading, danger: true }}
okText="确认删除"
>
<Button danger>删除一个重要项目</Button>
</Popconfirm>
</div>
);
};


const Root: React.FC = () => (
<ConfigProvider locale={zhCN}>
<App>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<MyPage />
</div>
</App>
</ConfigProvider>
);

export default Root;
  • 异步 onConfirm: PopconfirmonConfirm 的异步操作有很好的原生支持。如果 onConfirm 返回一个 Promiseasync 函数默认返回 Promise),Popconfirm 会自动等待这个 Promise 完成。在此期间,气泡框会保持打开状态。
  • okButtonProps: 这是一个非常有用的属性,它允许我们将 Button 的所有属性(如 loading, danger, disabled 等)传递给内部的“确定”按钮。
    • loading: 在我们的示例中,通过将 loading 状态绑定到 okButtonProps={{ loading }}Popconfirm 会在我们的异步操作期间,自动为“确定”按钮显示加载指示器。
    • danger: 对于删除等危险操作,将确认按钮设置为红色 (danger: true) 是一种非常好的用户体验实践,可以再次警示用户。
  • 自动关闭: 当 onConfirmPromiseresolve 时,Popconfirm 会自动关闭。如果 Promisereject,它会保持打开状态(但在我们的 finally 逻辑中,loading 状态会被解除)。

7.15.11. Skeleton: 优雅的加载占位方案

Skeleton (骨架屏) 是一种用于 提升数据加载体验 的占位组件。在等待异步数据返回时,相比于显示一个空白区域或一个简单的加载指示器(Spin),骨架屏能够预先渲染出页面内容的 大致轮廓,让用户对即将加载的内容有心理预期,从而有效降低等待的焦虑感,并让内容加载完成的瞬间显得更加自然平滑。

Skeleton(骨架屏)和 Spin(加载中)都用于向用户展示等待状态,但它们传递的信息和用户体验完全不同,适用于不同的加载场景。

特性Spin (加载中)Skeleton (骨架屏)
传达信息我正在忙,请等待”。
它是一个通用的、不提供任何内容上下文的等待指示器。
内容即将呈现,它大概长这个样子”。
它是一个模拟真实内容布局的占位符,提供了内容的结构预期。
核心用途表示一个 操作 正在进行中。表示一块 内容区域 正在加载中。
用户体验体验较为单调,用户只知道在等待,但不知道在等什么,可能会增加焦虑感。通过提供页面结构的轮廓,有效减少了用户等待时的不确定性和感知加载时间,体验更优。
适用场景适用于 耗时不确定或无法预测内容结构 的异步操作。适用于 内容结构相对固定 的区域加载,在数据返回前进行占位。
典型案例表单提交、数据计算、文件上传。加载文章详情、卡片列表、个人资料页、仪表盘数据块。
总结过程指示器:聚焦于一个正在执行的、抽象的“动作”。内容占位符:聚焦于即将渲染的、具象的“内容”。

第一步:基础用法与自由组合

Skeleton 的基础用法非常灵活,它提供了 avatar, title, paragraph 等原子化的占位符,您可以像搭积木一样,自由组合出您需要的任何内容轮廓。

文件路径: src/App.tsx

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React from 'react';
import { ConfigProvider, Skeleton, Space, Divider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex justify-center">
<div className="p-8 bg-white rounded-lg shadow-lg w-[800px]">
<h3 className="text-xl font-bold mb-4 text-center">基础骨架屏与组合</h3>

<Divider orientation="left">默认形态</Divider>
<Skeleton />

<Divider orientation="left">带动画效果</Divider>
<Skeleton active />

<Divider orientation="left">复杂组合</Divider>
<Skeleton avatar title={{ width: 150 }} paragraph={{ rows: 2 }} active />

<Divider orientation="left">仅段落</Divider>
<Skeleton title={false} paragraph={{ rows: 4, width: '100%' }} />

</div>
</div>
</ConfigProvider>
);
};

export default App;
  • active: 一个非常重要的布尔属性。当设置为 true 时,骨架屏会显示 动态的、流光闪烁 的动画效果,这比静态的灰色块更能有效地传达“正在加载中”的状态。在实际项目中,建议始终开启。
  • 组合属性:
    • avatar: 布尔值。设为 true 时,会显示一个头像占位符。您也可以传入一个对象(如 { shape: 'square', size: 'large' })进行更详细的配置。
    • title: 布尔值。设为 true (默认) 时,会显示一个标题占位符(短条)。您也可以传入一个对象(如 { width: 200 })来控制其宽度。
    • paragraph: 布尔值。设为 true (默认) 时,会显示多行段落占位符。传入一个对象(如 { rows: 4, width: ['100%', '100%', '50%'] })可以精确控制段落的行数和每一行的宽度。

第二步:包裹模式 - 优雅地切换真实内容

手动编写 loading ? <Skeleton /> : <MyComponent /> 这样的三元表达式来切换加载状态虽然可行,但略显繁琐。Skeleton 提供了一种更优雅的 包裹模式

Skeleton 组件包含 children 时,它就会自动切换为包裹模式。此时,loading 属性将成为控制显示逻辑的“总开关”。

文件路径: src/App.tsx

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import React, { useState } from "react";
import {
EditOutlined,
EllipsisOutlined,
SettingOutlined,
} from "@ant-design/icons";
import { Avatar, Card, Flex, Skeleton, Switch } from "antd";

const actions: React.ReactNode[] = [
<EditOutlined key="edit" />,
<SettingOutlined key="setting" />,
<EllipsisOutlined key="ellipsis" />,
];

const App: React.FC = () => {
const [loading, setLoading] = useState<boolean>(true);
return (
<div className="min-h-screen flex flex-col items-center justify-center bg-white p-8">
<Switch checked={!loading} onChange={(checked) => setLoading(!checked)} />

<div className="flex gap-8 mt-8 w-full max-w-4xl">
<div className="flex-1">
<h2 className="text-xl font-bold mb-4">卡片原生的loading效果</h2>
<Card loading={loading} actions={actions} style={{ minWidth: 300 }}>
<Card.Meta
avatar={
<Avatar src="https://api.dicebear.com/7.x/miniavs/svg?seed=1" />
}
title="Card title"
description={
<>
<p>This is the description</p>
<p>This is the description</p>
</>
}
/>
</Card>
</div>

<div className="flex-1">
<h2 className="text-xl font-bold mb-4">使用骨架屏包裹实现的效果</h2>
<Skeleton loading={loading} active>
<Card actions={actions} style={{ minWidth: 300 }}>
<Card.Meta
avatar={<Avatar src="https://api.dicebear.com/7.x/miniavs/svg?seed=1" />}
title="Card title"
/>
</Card>
</Skeleton>
</div>
</div>
</div>
);
};

export default App;
  • 包裹模式的工作流:

    1. 我们将真实的、需要等待数据才能渲染的组件(如此处的 <Meta>)作为 <Skeleton>children 传入。
    2. 我们将 loading 状态(通常来自您的数据请求逻辑)传递给 Skeletonloading 属性。
    3. loading={true} 时, Skeleton自动隐藏 它的 children,并根据自身的 avatar, title 等属性渲染出对应的骨架占位符。
    4. loading={false} 时, Skeleton自动隐藏 骨架占位符,并 渲染 它内部的 children
  • 优点: 这种模式的代码非常 声明式整洁。您不需要编写任何 if/else 或三元表达式,只需将加载状态传递给 Skeleton,它就会自动为您处理所有的显示/隐藏逻辑,让您能更专注于真实的业务组件本身。


7.16. 终章:App 组件 - 连接一切的顶层容器

在之前的章节中,我们为了解决 message, notification, modal 无法获取 React Context 的问题,已经多次接触并使用了 Ant Design 的 <App /> 组件。现在,我们将正式地、深入地剖析这个组件,理解它为何是 所有新 antd 项目的官方推荐起点

App 组件的核心使命有两个:

  1. 提供可消费上下文的静态方法:它在内部优雅地处理了 useMessage, useModal, useNotification 的所有 contextHolder 逻辑,让我们能以最简洁的方式在应用的任何地方调用这些反馈组件。
  2. 提供全局重置样式:它会为其包裹的所有子元素提供一个基于 .ant-app 的默认样式重置,确保您应用中的原生 HTML 标签(如 <h1>, <p>, <a> 等)与 antd 的组件在视觉风格上保持和谐统一。

第一部分:App 组件的核心功能一 - 优雅地解决 contextHolder 问题

场景回顾
我们已经知道,直接调用 message.success() 等静态方法无法继承 ConfigProvider 提供的主题、语言等上下文。虽然可以通过在每个需要的页面上手动实例化 const [api, contextHolder] = useMessage() 来解决,但这无疑是繁琐和重复的。

解决方案 (<App /> 组件):
<App> 组件正是为了终结这种繁琐而生。它就像一个“后勤总管”,在应用顶层一次性为您处理好所有 contextHolder 的配置。

文件路径: src/App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import React from 'react';
// 1. 从 antd 引入 App 组件
import { ConfigProvider, App, Button, Space } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

// 这是一个普通的子组件,它可以位于应用的任何层级
const MyPage: React.FC = () => {
// 2. 在子组件中,调用 App.useApp() 即可获取上下文感知的 api
const { message, notification, modal } = App.useApp();

const showFeedbacks = () => {
// 3. 直接使用这些 api,它们能正确读取到 ConfigProvider 的配置
message.success('这是一条成功的 Message!');
notification.info({
message: '通知',
description: '这是一条 Notification.',
});
modal.confirm({
title: '确认',
content: '这是一个 Modal 确认框。',
});
};

return (
<div className="p-8 bg-white rounded-lg shadow-lg">
<h3 className="text-xl font-bold mb-4 text-center">
通过 App.useApp() 调用反馈
</h3>
<Button type="primary" onClick={showFeedbacks}>
显示所有反馈
</Button>
</div>
);
};

// 顶层应用
const Root: React.FC = () => (
<ConfigProvider locale={zhCN}>
{/* 4. 用 antd 的 <App> 组件包裹您的整个应用或页面 */}
<App>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<MyPage />
</div>
</App>
</ConfigProvider>
);

export default Root;
  • <App> 包裹: 我们将整个应用的主体内容包裹在从 antd 导入的 <App> 组件内。这一步是所有魔法发生的前提。<App> 组件在内部调用了 useMessage, useModal, useNotification,并渲染了它们各自的 contextHolder
  • App.useApp(): 这是一个特殊的 Hook,它只能在 <App> 组件的 子组件 中被调用。它的作用是获取由最近的父级 <App> 组件所管理的 message, modal, notificationapi 实例。
  • 告别 contextHolder: 一旦使用了 <App>App.useApp(),您就再也 不需要 在业务代码中手动管理任何 contextHolder 了。这极大地简化了代码,降低了心智负担,是官方提供的最终解决方案。

第二部分:App 组件的核心功能二 - 全局样式重置

场景分析:
在混合使用 antd 组件和原生 HTML 标签时,您可能会发现它们的样式格格不入。例如,原生的 <h1> 标题可能比 antd 的 Typography.Title 更大、颜色更深;原生 <p> 的行高和边距也与 antd 的设计规范不符。

解决方案 (<App /> 组件):
<App> 组件的另一个重要作用,就是为其包裹的所有子元素提供一层 antd 风格的 基础样式重置

文件路径: src/App.tsx

image-20251001103550722

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React from 'react';
import { ConfigProvider, App, Button, Divider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';

const AppContent: React.FC = () => (
<div className="p-8 bg-white rounded-lg shadow-lg w-[800px]">
<h3 className="text-xl font-bold mb-4 text-center">全局样式重置对比</h3>

<Divider orientation="left">未使用 antd App 组件包裹</Divider>
<div className="p-4 border rounded">
<h1>这是一个原生的 H1 标题</h1>
<p>这是一个原生的 p 段落。这里有一个 <a href="#">a 链接</a></p>
<Button type="primary">这是一个 antd 按钮</Button>
</div>

<Divider orientation="left">使用 antd App 组件包裹</Divider>
<App>
<div className="p-4 border rounded">
<h1>这是一个被 App 包裹的 H1 标题</h1>
<p>这是一个被 App 包裹的 p 段落。这里有一个 <a href="#">a 链接</a></p>
<Button type="primary">这是一个 antd 按钮</Button>
</div>
</App>
</div>
);


const Root: React.FC = () => (
<ConfigProvider locale={zhCN}>
<div className="bg-gray-100 min-h-screen p-8 flex items-center justify-center">
<AppContent />
</div>
</ConfigProvider>
);

export default Root;
  • .ant-app: 当您使用 <App> 组件时,它会渲染一个带有 .ant-app CSS 类的 div 作为根容器(您也可以通过 component={false} 禁用这个 div,但不推荐)。
  • 样式覆盖: antd 的样式表中包含了一系列针对 .ant-app 内部原生元素的重置规则,例如:
    1
    2
    3
    .ant-app h1 { font-size: 22px; /* antd 规范的字号 */ }
    .ant-app p { margin-bottom: 1em; /* antd 规范的段落间距 */ }
    .ant-app a { color: #1677ff; /* antd 规范的链接颜色 */ }
  • 视觉统一: 通过这些重置,<App> 组件确保了即使您使用原生 HTML 标签,其最终渲染出的视觉效果也能与 antd 的组件设计体系保持高度一致,从而保证了整个应用的视觉统一性和专业性。

最终结语

至此,我们已经完整地学习了 Ant Design 中从“数据录入”到“数据展示”再到“反馈”的全部核心组件,并最终以 App 这个承上启下的顶层组件完美收官。

您现在所掌握的,不仅仅是几十个独立的组件 API,更是一整套构建专业、成熟、用户体验卓越的 React 应用的 设计思想和工程实践

感谢您的一路相伴,希望这份“立体知识库”能成为您在未来开发道路上,随时可以查阅、回顾的得力助手。祝您编码愉快!