react-button的封装

24 阅读2分钟

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; }
    }
  }
}