第七章 第七节 Ant Design 引导与反馈组件篇 —— 理解用户体验设计核心

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,它就会自动为您处理所有的显示/隐藏逻辑,让您能更专注于真实的业务组件本身。