button的封装
Button 是一个功能丰富的按钮组件,提供了多种样式、尺寸和状态,用于触发操作和交互。该组件基于 React 实现,支持完整的 TypeScript 类型定义。
import { LoadingOutlined } from "@ant-design/icons";
import classNames from "classnames";
import React, { ReactNode } from "react";
import "./index.scss";
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;
// 图标属性
icon?: ReactNode;
iconPosition?: "start" | "end";
// 功能属性
loading?: boolean;
disabled?: boolean;
block?: boolean;
// 事件
onClick?: React.MouseEventHandler<HTMLButtonElement>;
onBlur?: React.FocusEventHandler<HTMLButtonElement>;
onFocus?: React.FocusEventHandler<HTMLButtonElement>;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(props: ButtonProps, ref) => {
const {
className,
type = "normal",
size = "medium",
shape,
danger = false,
ghost = false,
icon,
iconPosition = "start",
loading = false,
disabled = false,
block = false,
children,
style,
onClick,
onBlur,
onFocus,
htmlType = "button",
...others
} = props;
// 大小映射
const sizeMap: { [key: string]: string } = {
small: "sm",
medium: "",
large: "lg",
};
const sizeClass = sizeMap[size];
// 判断是否只有图标(没有文本内容)
const isIconOnly = !children && (icon || loading);
// 构建 className
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,
"ant-btn-icon-only": isIconOnly,
[className as string]: !!className,
});
// 处理加载状态下的禁用
const isDisabled = disabled || loading;
// 渲染图标元素
const iconNode = loading ? <LoadingOutlined /> : icon ? icon : null;
// 渲染按钮内容
const renderContent = () => {
// 只有图标,没有文本
if (isIconOnly) {
return iconNode;
}
// 有文本和图标
if (iconNode && children) {
if (iconPosition === "end") {
return (
<>
<span>{children}</span>
{iconNode}
</>
);
} else {
return (
<>
{iconNode}
<span>{children}</span>
</>
);
}
}
// 只有文本
return children;
};
return (
<button
{...others}
type={htmlType}
ref={ref}
className={cls}
style={style}
disabled={isDisabled}
onClick={onClick}
onBlur={onBlur}
onFocus={onFocus}
>
{renderContent()}
</button>
);
}
);
Button.displayName = "Button";
export default Button;
scss样式
/* stylelint-disable at-rule-empty-line-before,at-rule-name-space-after,at-rule-no-unknown */
/* stylelint-disable no-duplicate-selectors, declaration-bang-space-before,string-no-newline */
// ==================== 变量定义 ====================
$btn-prefix: ".ant-btn";
$ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1);
// 颜色变量
$color-text-disabled: rgba(0, 0, 0, 0.25);
$color-text-primary: rgba(0, 0, 0, 0.85);
$color-text-white: #fff;
$color-border-disabled: #d9d9d9;
$color-bg-disabled: #f5f5f5;
$color-bg-white: #fff;
$color-primary: #1890ff;
$color-primary-hover: #40a9ff;
$color-primary-active: #096dd9;
$color-danger: #ff4d4f;
$color-danger-hover: #ff7875;
$color-danger-active: #d9363e;
// ==================== 公共 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(); }
}
// ==================== 按钮基础样式 ====================
#{$btn-prefix} {
line-height: 1.5715;
position: relative;
display: inline-block;
font-weight: 400;
white-space: nowrap;
text-align: center;
background-image: none;
border: 1px solid transparent;
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.015);
cursor: pointer;
transition: all 0.3s $ease-in-out-cubic;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
touch-action: manipulation;
height: 32px;
padding: 4px 15px;
font-size: 14px;
border-radius: 2px;
color: $color-text-primary;
border-color: $color-border-disabled;
background: $color-bg-white;
>.anticon { line-height: 1; }
&, &:active, &:focus { outline: 0; }
&:not([disabled]) &:active { outline: 0; box-shadow: none; }
&[disabled] {
cursor: not-allowed;
>* { pointer-events: none; }
}
// ==================== 大小变体 ====================
&-lg {
height: 40px;
padding: 6.4px 15px;
font-size: 16px;
}
&-sm {
height: 24px;
padding: 0px 7px;
font-size: 14px;
}
&>span { display: inline-block; }
// ==================== 图标间距 ====================
>.anticon+span,
>span+.anticon {
margin-left: 8px;
}
// ==================== 按钮类型 ====================
@include innerA();
&:hover, &:focus {
color: $color-primary-hover;
border-color: $color-primary-hover;
background: $color-bg-white;
@include innerA();
}
&[disabled] { @include disabled-state(); }
&:hover, &:focus, &:active { text-decoration: none; background: $color-bg-white; }
&-primary { @include button-type($color-text-white, $color-primary, $color-primary); }
&-ghost { @include button-type($color-text-primary, $color-border-disabled, transparent); }
&-dashed {
color: $color-text-primary;
border-color: $color-border-disabled;
background: $color-bg-white;
border-style: dashed;
@include innerA();
&:hover, &:focus {
color: $color-primary-hover;
border-color: $color-primary-hover;
background: $color-bg-white;
@include innerA();
}
&:active {
color: $color-primary-active;
border-color: $color-primary-active;
background: $color-bg-white;
@include innerA();
}
&[disabled] { @include disabled-state(); }
}
&-danger {
color: $color-text-white;
border-color: $color-danger;
background: $color-danger;
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-danger-hover;
background: $color-danger-hover;
@include innerA();
}
&:active {
color: $color-text-white;
border-color: $color-danger-active;
background: $color-danger-active;
@include innerA();
}
&[disabled] { @include disabled-state(); }
&#{$btn-prefix}-primary {
@include button-type($color-text-white, $color-danger, $color-danger);
&:hover, &:focus {
border-color: $color-danger-hover;
background: $color-danger-hover;
}
&:active {
border-color: $color-danger-active;
background: $color-danger-active;
}
}
&#{$btn-prefix}-link {
color: $color-danger;
&:hover, &:focus { color: $color-danger-hover; }
&:active { color: $color-danger-active; }
}
&#{$btn-prefix}-text {
color: $color-danger;
&:hover, &:focus { color: $color-danger-hover; background: rgba(0, 0, 0, 0.018); }
&:active { color: $color-danger-active; background: rgba(0, 0, 0, 0.028); }
}
}
&-link { @include button-plain($color-primary, transparent); }
&-text {
@include button-plain($color-text-primary, transparent);
&:hover, &:focus {
color: $color-text-primary;
background: rgba(0, 0, 0, 0.018);
border-color: transparent;
}
&:active {
color: $color-text-primary;
background: rgba(0, 0, 0, 0.028);
border-color: transparent;
}
}
&-dangerous {
color: $color-danger;
border-color: $color-danger;
background: $color-bg-white;
@include innerA();
&:hover, &:focus {
color: $color-danger-hover;
border-color: $color-danger-hover;
background: $color-bg-white;
@include innerA();
}
&:active {
color: $color-danger-active;
border-color: $color-danger-active;
background: $color-bg-white;
@include innerA();
}
&[disabled] { @include disabled-state(); }
}
// ==================== 形状和大小变体 ====================
&-icon-only {
width: 32px;
height: 32px;
padding: 2.4px 0;
font-size: 16px;
vertical-align: -3px;
>* { font-size: 16px; }
&.ant-btn-lg {
width: 40px;
height: 40px;
padding: 4.9px 0;
font-size: 18px;
>* { font-size: 18px; }
}
&.ant-btn-sm {
width: 24px;
height: 24px;
padding: 0;
font-size: 14px;
>* { font-size: 14px; }
}
>.anticon { display: flex; justify-content: center; }
}
&-round {
height: 32px;
padding: 4px 16px;
border-radius: 32px;
&.ant-btn-lg {
height: 40px;
padding: 6.4px 20px;
border-radius: 40px;
}
&.ant-btn-sm {
height: 24px;
padding: 0 12px;
border-radius: 24px;
}
&.ant-btn-icon-only { width: auto; }
}
&-circle {
min-width: 32px;
padding-right: 0;
padding-left: 0;
text-align: center;
border-radius: 50%;
&.ant-btn-lg { min-width: 40px; }
&.ant-btn-sm { min-width: 24px; }
}
// ==================== 加载和特殊状态 ====================
&::before {
position: absolute;
top: -1px;
right: -1px;
bottom: -1px;
left: -1px;
z-index: 1;
display: none;
background: $color-bg-white;
border-radius: inherit;
opacity: 0.35;
transition: opacity 0.2s;
content: '';
pointer-events: none;
}
.anticon { transition: margin-left 0.3s $ease-in-out-cubic; }
.anticon-plus>svg, .anticon-minus>svg { shape-rendering: optimizeSpeed; }
&-loading {
position: relative;
cursor: default;
opacity: 0.6;
&::before { display: block; }
}
>.ant-btn-loading-icon {
transition: width 0.3s $ease-in-out-cubic, opacity 0.3s $ease-in-out-cubic;
.anticon {
padding-right: 8px;
animation: loadingCircle 1s infinite linear;
}
.anticon svg { animation: loadingCircle 1s infinite linear; }
}
>.ant-btn-loading-icon:only-child .anticon { padding-right: 0; }
&-block { width: 100%; }
// ==================== 辅助样式 ====================
.ant-btn:empty {
display: inline-block;
width: 0;
visibility: hidden;
content: '\a0';
}
a.ant-btn {
padding-top: 0.01px !important;
line-height: 30px;
&.ant-btn-lg { line-height: 38px; }
&.ant-btn-sm { line-height: 22px; }
}
// ==================== RTL 支持 ====================
.ant-btn-rtl { direction: rtl; }
.ant-btn-group-rtl.ant-btn-group {
.ant-btn-primary:last-child:not(:first-child),
.ant-btn-primary+.ant-btn-primary {
border-right-color: $color-primary-hover;
border-left-color: $color-border-disabled;
&[disabled] {
border-right-color: $color-border-disabled;
border-left-color: $color-primary-hover;
}
}
}
.ant-btn-rtl.ant-btn>.ant-btn-loading-icon .anticon {
padding-right: 0;
padding-left: 8px;
}
.ant-btn>.ant-btn-loading-icon:only-child .anticon { padding-right: 0; padding-left: 0; }
.ant-btn-rtl.ant-btn>.anticon+span,
.ant-btn-rtl.ant-btn>span+.anticon {
margin-right: 8px;
margin-left: 0;
}
&-group #{$btn-prefix}-primary {
&:not(:first-child):not(:last-child) {
border-right-color: $color-primary-hover;
border-left-color: $color-primary-hover;
&:disabled { border-color: $color-border-disabled; }
}
&:first-child:not(:last-child) {
border-right-color: $color-primary-hover;
&[disabled] { border-color: $color-border-disabled; }
}
&:last-child:not(:first-child),
+.ant-btn-primary {
border-left-color: $color-primary-hover;
&[disabled] { border-left-color: $color-border-disabled; }
}
}
}