Button 按钮组件
概述
Button 是一个功能丰富的按钮组件,提供了多种样式、尺寸和状态,用于触发操作和交互。该组件基于 React 实现,支持完整的 TypeScript 类型定义。
特性
- 🎨 多种类型:支持 primary、normal、dashed、link、text 五种按钮类型
- 📏 三种尺寸:small、medium、large 可选
- 🔘 多种形状:支持 circle(圆形)和 round(圆角)形状
- 🎯 图标支持:支持图标按钮和图标位置配置(start | end)
- ⚠️ 危险按钮:支持 danger 危险状态样式
- 👻 幽灵按钮:支持 ghost 透明背景样式
- ⏳ 加载状态:内置 loading 状态支持,带旋转图标动画
- 🚫 禁用状态:支持 disabled 禁用状态
- 📱 块级按钮:支持 block 块级显示
- ♿ 完全可访问:支持键盘导航和屏幕阅读器
引入方式
import Button from "@/button";
// 或者
import Button from "@/button/index";
API 参数
ButtonProps
| 参数 | 说明 | 类型 | 默认值 | 必填 |
|---|---|---|---|---|
| 基础属性 | ||||
| className | 自定义类名 | string | - | 否 |
| style | 自定义样式 | React.CSSProperties | - | 否 |
| children | 按钮内容 | ReactNode | - | 否 |
| htmlType | 原生 button 的 type 属性 | 'button' | 'submit' | 'reset' | 'button' | 否 |
| 样式属性 | ||||
| type | 按钮类型 | 'normal' | 'primary' | 'dashed' | 'link' | 'text' | 'normal' | 否 |
| size | 按钮尺寸 | 'small' | 'medium' | 'large' | 'medium' | 否 |
| shape | 按钮形状 | 'circle' | 'round' | - | 否 |
| danger | 设置危险按钮 | boolean | false | 否 |
| ghost | 设置幽灵按钮(透明背景) | boolean | false | 否 |
| 图标属性 | ||||
| icon | 设置按钮的图标组件 | ReactNode | - | 否 |
| iconPosition | 设置图标的位置 | 'start' | 'end' | 'start' | 否 |
| 功能属性 | ||||
| loading | 设置按钮载入状态 | boolean | false | 否 |
| disabled | 设置按钮禁用状态 | boolean | false | 否 |
| block | 将按钮宽度调整为其父宽度 | boolean | false | 否 |
| 事件处理 | ||||
| onClick | 点击按钮时的回调 | React.MouseEventHandler<HTMLButtonElement> | - | 否 |
| onBlur | 失去焦点时的回调 | React.FocusEventHandler<HTMLButtonElement> | - | 否 |
| onFocus | 获取焦点时的回调 | React.FocusEventHandler<HTMLButtonElement> | - | 否 |
其他 HTML 属性
组件还支持所有原生 <button> 元素的属性,如 id、name、title 等。
使用示例
基础用法
import Button from "@/button";
function BasicExample() {
return (
<div>
<Button type="primary">主要按钮</Button>
<Button>默认按钮</Button>
<Button type="dashed">虚线按钮</Button>
<Button type="text">文本按钮</Button>
<Button type="link">链接按钮</Button>
</div>
);
}
图标按钮
按钮可以配置图标,提供更好的视觉效果和用户体验。
import Button from "@/button";
import {
SearchOutlined,
DownloadOutlined,
PlusOutlined,
} from "@ant-design/icons";
function IconExample() {
return (
<div>
{/* 带文本的图标按钮 */}
<Button type="primary" icon={<SearchOutlined />}>
搜索
</Button>
<Button icon={<DownloadOutlined />}>下载</Button>
<Button type="dashed" icon={<PlusOutlined />}>
添加
</Button>
</div>
);
}
纯图标按钮
只有图标,没有文字的按钮。适合在空间有限的场景使用。
import Button from "@/button";
import {
SearchOutlined,
EditOutlined,
DeleteOutlined,
} from "@ant-design/icons";
function IconOnlyExample() {
return (
<div>
<Button type="primary" icon={<SearchOutlined />} />
<Button icon={<EditOutlined />} />
<Button type="primary" shape="circle" icon={<SearchOutlined />} />
<Button danger shape="circle" icon={<DeleteOutlined />} />
</div>
);
}
图标位置
可以通过 iconPosition 属性控制图标在文本的左侧或右侧。
import Button from "@/button";
import { LeftOutlined, RightOutlined } from "@ant-design/icons";
function IconPositionExample() {
return (
<div>
<Button type="primary" icon={<LeftOutlined />}>
上一页
</Button>
<Button type="primary" icon={<RightOutlined />} iconPosition="end">
下一页
</Button>
</div>
);
}
按钮尺寸
function SizeExample() {
return (
<div>
<Button type="primary" size="large">
大号按钮
</Button>
<Button type="primary" size="medium">
中号按钮
</Button>
<Button type="primary" size="small">
小号按钮
</Button>
</div>
);
}
按钮形状
function ShapeExample() {
return (
<div>
<Button type="primary" shape="round">
圆角按钮
</Button>
<Button type="primary" shape="circle">
+
</Button>
<Button type="primary" shape="circle" size="large">
✓
</Button>
</div>
);
}
危险按钮
function DangerExample() {
return (
<div>
<Button type="primary" danger>
删除
</Button>
<Button danger>危险操作</Button>
<Button type="link" danger>
危险链接
</Button>
<Button type="text" danger>
危险文本
</Button>
</div>
);
}
幽灵按钮
适合用在有色背景上。
function GhostExample() {
return (
<div style={{ background: "#1890ff", padding: 20 }}>
<Button type="primary" ghost>
主要按钮
</Button>
<Button ghost>默认按钮</Button>
<Button type="dashed" ghost>
虚线按钮
</Button>
</div>
);
}
加载状态
用于异步操作的反馈。加载状态会显示旋转的加载图标,并自动禁用按钮。
import { useState } from "react";
import Button from "@/button";
import { UploadOutlined } from "@ant-design/icons";
function LoadingExample() {
const [loading, setLoading] = useState(false);
const handleClick = async () => {
setLoading(true);
try {
// 模拟异步操作
await new Promise((resolve) => setTimeout(resolve, 2000));
console.log("操作完成");
} finally {
setLoading(false);
}
};
return (
<div>
{/* 普通加载按钮 */}
<Button type="primary" loading={loading} onClick={handleClick}>
{loading ? "处理中..." : "点击提交"}
</Button>
{/* 带图标的加载按钮,loading 时会替换为加载图标 */}
<Button
type="primary"
icon={<UploadOutlined />}
loading={loading}
onClick={handleClick}
>
{loading ? "上传中..." : "上传文件"}
</Button>
</div>
);
}
禁用状态
function DisabledExample() {
return (
<div>
<Button type="primary" disabled>
主要按钮
</Button>
<Button disabled>默认按钮</Button>
<Button type="link" disabled>
链接按钮
</Button>
</div>
);
}
块级按钮
适合在移动端或需要按钮占满容器宽度的场景。
function BlockExample() {
return (
<div>
<Button type="primary" block>
主要按钮
</Button>
<Button block style={{ marginTop: 8 }}>
默认按钮
</Button>
<Button type="dashed" block style={{ marginTop: 8 }}>
虚线按钮
</Button>
</div>
);
}
表单提交
function FormExample() {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log("表单已提交");
};
return (
<form onSubmit={handleSubmit}>
<input type="text" placeholder="输入内容" />
<Button htmlType="submit" type="primary">
提交
</Button>
<Button htmlType="reset">重置</Button>
</form>
);
}
组合使用
function CombinedExample() {
return (
<div>
{/* 大号危险按钮 */}
<Button type="primary" danger size="large">
删除用户
</Button>
{/* 圆形按钮组 */}
<Button type="primary" shape="circle">
✓
</Button>
<Button type="primary" shape="circle" danger>
✗
</Button>
{/* 幽灵风格危险按钮 */}
<div style={{ background: "#1890ff", padding: 20 }}>
<Button type="primary" ghost>
接受
</Button>
<Button type="primary" danger ghost>
拒绝
</Button>
</div>
</div>
);
}
使用 Ref
import { useRef } from "react";
import Button from "@/button";
function RefExample() {
const btnRef = useRef<HTMLButtonElement>(null);
const focusButton = () => {
btnRef.current?.focus();
};
return (
<div>
<Button type="primary" ref={btnRef}>
目标按钮
</Button>
<Button onClick={focusButton}>聚焦目标按钮</Button>
</div>
);
}
最佳实践
1. 按钮类型选择
- Primary 主要按钮:用于页面的主要操作,一个页面建议只出现一个主要按钮
- Default 默认按钮:用于次要操作
- Dashed 虚线按钮:常用于添加操作
- Text 文本按钮:用于最弱的操作,如取消
- Link 链接按钮:用于次要或外链跳转
// ✅ 推荐:主次分明
<div>
<Button type="primary">确认提交</Button>
<Button>取消</Button>
</div>
// ❌ 不推荐:多个主要按钮
<div>
<Button type="primary">操作A</Button>
<Button type="primary">操作B</Button>
<Button type="primary">操作C</Button>
</div>
2. 图标使用建议
合理使用图标可以提升用户体验和识别度。
import { SearchOutlined, PlusOutlined, DeleteOutlined } from "@ant-design/icons";
// ✅ 推荐:图标语义明确
<div>
<Button type="primary" icon={<SearchOutlined />}>搜索</Button>
<Button icon={<PlusOutlined />}>添加</Button>
<Button danger icon={<DeleteOutlined />}>删除</Button>
</div>
// ✅ 推荐:空间有限时使用纯图标按钮
<div>
<Button type="primary" shape="circle" icon={<SearchOutlined />} />
<Button shape="circle" icon={<PlusOutlined />} />
</div>
// ✅ 推荐:导航按钮使用合适的图标位置
<div>
<Button icon={<LeftOutlined />}>上一页</Button>
<Button icon={<RightOutlined />} iconPosition="end">下一页</Button>
</div>
// ❌ 不推荐:图标语义不清晰
<Button icon={<QuestionOutlined />}>确认</Button>
3. 危险操作确认
对于删除等危险操作,建议使用 danger 属性,并配合二次确认。
import { DeleteOutlined } from "@ant-design/icons";
function DeleteExample() {
const handleDelete = () => {
if (window.confirm("确认删除吗?")) {
// 执行删除操作
console.log("已删除");
}
};
return (
<Button
type="primary"
danger
icon={<DeleteOutlined />}
onClick={handleDelete}
>
删除数据
</Button>
);
}
4. 异步操作处理
对于异步操作,应该使用 loading 状态,避免重复提交。
// ✅ 推荐:使用 loading 状态
function GoodAsyncExample() {
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
setLoading(true);
try {
await submitData();
} finally {
setLoading(false);
}
};
return (
<Button type="primary" loading={loading} onClick={handleSubmit}>
提交
</Button>
);
}
// ❌ 不推荐:没有 loading 反馈
function BadAsyncExample() {
const handleSubmit = async () => {
await submitData(); // 用户可能重复点击
};
return (
<Button type="primary" onClick={handleSubmit}>
提交
</Button>
);
}
5. 按钮组间距
多个按钮并排时,应该保持适当间距。
// ✅ 推荐:设置合适的间距
<div>
<Button type="primary">确认</Button>
<Button style={{ marginLeft: 8 }}>取消</Button>
</div>
// 或使用容器样式
<div style={{ display: 'flex', gap: 8 }}>
<Button type="primary">确认</Button>
<Button>取消</Button>
</div>
6. 响应式布局
在移动端,考虑使用 block 属性使按钮占满宽度。
function ResponsiveButton() {
const isMobile = window.innerWidth < 768;
return (
<Button type="primary" block={isMobile}>
提交订单
</Button>
);
}
样式定制
通过 className 定制
// 在你的样式文件中
.custom-button {
border-radius: 8px;
text-transform: uppercase;
font-weight: 600;
}
// 使用
<Button type="primary" className="custom-button">
自定义按钮
</Button>
通过 style 定制
<Button
type="primary"
style={{
borderRadius: 8,
textTransform: "uppercase",
fontWeight: 600,
}}
>
自定义样式
</Button>
覆盖主题色
如需全局修改按钮主题色,可以修改 index.scss 文件中的颜色变量:
// src/button/index.scss
$color-primary: #1890ff; // 主色
$color-primary-hover: #40a9ff; // 悬停色
$color-primary-active: #096dd9; // 激活色
$color-danger: #ff4d4f; // 危险色
$color-danger-hover: #ff7875; // 危险悬停色
$color-danger-active: #d9363e; // 危险激活色
技术实现要点
1. 组件结构
组件使用 React.forwardRef 实现,支持 ref 转发到原生 button 元素:
interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
// 基础属性
className?: string;
style?: React.CSSProperties;
children?: ReactNode;
htmlType?: "button" | "submit" | "reset";
// 样式属性
type?: "normal" | "primary" | "dashed" | "link" | "text";
size?: "small" | "medium" | "large";
shape?: "circle" | "round";
danger?: boolean;
ghost?: boolean;
// 功能属性
loading?: boolean;
disabled?: boolean;
block?: boolean;
// 事件
onClick?: React.MouseEventHandler<HTMLButtonElement>;
onBlur?: React.FocusEventHandler<HTMLButtonElement>;
onFocus?: React.FocusEventHandler<HTMLButtonElement>;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
2. 类名生成
使用 classnames 库动态生成类名,保证样式的灵活组合:
const cls = classNames({
"ant-btn": true,
[`ant-btn-${sizeClass}`]: sizeClass,
[`ant-btn-${type}`]: type,
[`ant-btn-${shape}`]: shape,
"ant-btn-danger": danger && type !== "text",
"ant-btn-ghost": ghost,
"ant-btn-block": block,
"ant-btn-loading": loading,
[className as string]: !!className,
});
3. 加载状态处理
加载状态会自动禁用按钮,防止重复点击:
const isDisabled = disabled || loading;
4. 样式架构
样式采用 SCSS 编写,使用 Mixin 实现样式复用:
@mixin disabled-state($color-text: $color-text-disabled) {
color: $color-text;
border-color: $color-border-disabled;
background: $color-bg-disabled;
text-shadow: none;
box-shadow: none;
@include innerA();
}
@mixin innerA() {
>a:only-child {
color: currentColor;
&:after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: transparent;
content: '';
}
}
}
@mixin button-type($color, $border, $bg) {
color: $color;
border-color: $border;
background: $bg;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12);
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.045);
@include innerA();
&:hover, &:focus {
color: $color-text-white;
border-color: $color-primary-hover;
background: $color-primary-hover;
@include innerA();
}
&:active {
color: $color-text-white;
border-color: $color-primary-active;
background: $color-primary-active;
@include innerA();
}
&[disabled] { @include disabled-state(); }
}
@mixin button-plain($color, $border: transparent, $bg: transparent, $box-shadow: none) {
color: $color;
border-color: $border;
background: $bg;
box-shadow: $box-shadow;
@include innerA();
&:hover, &:focus {
color: $color-primary-hover;
border-color: $color-primary-hover;
@include innerA();
}
&:active {
color: $color-primary-active;
border-color: $color-primary-active;
@include innerA();
}
&[disabled] { @include disabled-state(); }
}
常见问题
Q1: 按钮点击事件触发两次?
A: 检查是否在 loading 状态下没有禁用按钮。组件内部会自动处理,但如果自定义了点击逻辑,需要手动防抖:
import { useState } from "react";
function Example() {
const [loading, setLoading] = useState(false);
const handleClick = async () => {
if (loading) return; // 手动防止重复点击
setLoading(true);
try {
await someAsyncOperation();
} finally {
setLoading(false);
}
};
return (
<Button loading={loading} onClick={handleClick}>
提交
</Button>
);
}
Q2: 如何实现按钮组?
A: 可以使用容器包裹多个按钮:
function ButtonGroup() {
return (
<div style={{ display: "inline-flex", gap: 0 }}>
<Button>左侧</Button>
<Button>中间</Button>
<Button>右侧</Button>
</div>
);
}
Q3: 如何添加图标?
A: 使用 icon 属性添加图标:
import { SearchOutlined } from "@ant-design/icons";
function IconButton() {
return (
<div>
{/* 推荐方式:使用 icon 属性 */}
<Button type="primary" icon={<SearchOutlined />}>
搜索
</Button>
{/* 纯图标按钮 */}
<Button type="primary" icon={<SearchOutlined />} />
{/* 图标在右侧 */}
<Button icon={<SearchOutlined />} iconPosition="end">
搜索
</Button>
</div>
);
}
Q4: loading 图标是什么?
A: loading 状态会显示 Ant Design 的 LoadingOutlined 旋转图标,并自动禁用按钮。如果按钮同时设置了 icon 和 loading={true},loading 图标会替换原来的图标。
import { UploadOutlined } from "@ant-design/icons";
// loading 时会显示旋转的加载图标,替换上传图标
<Button icon={<UploadOutlined />} loading={true}>
上传
</Button>;
注意事项
- 避免过度使用主要按钮:一个页面/对话框中建议只有一个主要按钮(primary)
- 危险操作需二次确认:使用 danger 属性的按钮,建议加上确认弹窗
- 异步操作要有反馈:使用 loading 状态避免用户重复点击
- 移动端适配:在小屏幕上考虑使用 block 属性
- 加载状态禁用自动处理:loading 为 true 时,按钮会自动禁用
- 文本按钮慎用:text 和 link 类型按钮容易被忽略,不适合重要操作
- 保持一致性:在同一个项目中,相同功能的按钮应使用相同的类型和样式
- 图标语义要清晰:使用图标时确保图标的含义清晰,必要时配合文字说明
- 纯图标按钮需谨慎:只有图标的按钮可能让用户困惑,建议配合 Tooltip 提示
- loading 状态会替换图标:当按钮处于 loading 状态时,会显示加载图标替换原有图标
维护指南
文件结构
src/button/
├── index.tsx # 组件主文件
├── index.scss # 组件样式
├── index.test.tsx # 单元测试
└── Button.stories.tsx # Storybook 文档
修改建议
-
添加新的按钮类型:
- 在
ButtonProps的type属性中添加新类型 - 在
index.scss中添加对应样式 - 在
Button.stories.tsx中添加示例
- 在
-
修改默认样式:
- 修改
index.scss中的变量定义 - 注意保持向后兼容性
- 修改
-
添加新功能:
- 在
ButtonProps接口中添加新属性 - 在组件实现中处理新属性
- 更新文档和示例
- 在
测试
运行单元测试:
npm test src/button/index.test.tsx
运行 Storybook:
npm run storybook
更新日志
v1.1.0 (2025-10-27)
- ✨ 新增
icon属性,支持配置按钮图标 - ✨ 新增
iconPosition属性,支持设置图标位置(start | end) - ✨ 实现真实的 loading 旋转图标(LoadingOutlined)
- ✨ 自动识别纯图标按钮(icon-only)
- ✨ 图标和文本之间自动添加合适间距
- ✅ 新增 11 个图标相关测试用例
- 📝 完善图标使用文档和最佳实践
v1.0.0
- ✅ 支持五种按钮类型
- ✅ 支持三种尺寸
- ✅ 支持圆形和圆角形状
- ✅ 支持危险和幽灵样式
- ✅ 支持加载和禁用状态
- ✅ 支持块级显示
- ✅ 完整的 TypeScript 类型定义
- ✅ 支持 ref 转发
相关组件
- Icon:图标组件,常与按钮配合使用
- Dropdown:下拉菜单,可以与按钮组合
- Tooltip:工具提示,可以包裹按钮提供说明
贡献
如有问题或建议,请联系组件维护团队。