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


第七章 第二节: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 则适用于 将同一主题下的不同维度内容进行归类和分层展示,让用户可以自由切换。

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