Vite 构建 Vue3 组件库之路: 按钮组件

281 阅读5分钟

image.png

需求分析

按钮组件的核心目标是为用户提供直观、高效的交互方式,因此需要具备以下关键特性:

  1. 插槽支持:通过允许用户自定义按钮的内容,无论是文本、图标还是其他复杂的 HTML 结构,都能轻松嵌入按钮内部,极大地增强了组件的灵活性和复用性,使其能够适应各种不同的场景和设计要求。
  2. 事件传递:支持按钮点击事件是实现交互功能的基础。组件应能够准确地捕获用户的点击操作,并将相关的事件信息传递给父组件,以便进行进一步的业务逻辑处理,如提交表单、触发弹窗、执行特定的函数等。

实现细节

定义 Props

为赋予按钮组件高度的可定制性,首先需精确定义其属性(Props),以便外部使用者依据具体需求灵活配置按钮的功能与样式。

// 定义按钮的尺寸类型,包括大、小两种尺寸,方便后续样式设置和组件使用时的类型检查
type ButtonSize = "large" | "small";
// 定义按钮的类型,如主要按钮、危险按钮等,每种类型对应不同的样式风格
type ButtonType = "primary" | "danger";

// 接口定义按钮组件接受的属性
interface ButtonProps {
  // 是否禁用按钮,默认为 false,当设置为 true 时,按钮将不可点击且显示为禁用状态
  disabled?: boolean;
  // 自定义类名,用于外部样式覆盖或添加额外的样式类,增强样式的扩展性
  className?: string;
  // 按钮尺寸,可选值为之前定义的 ButtonSize 类型,控制按钮的大小样式
  size?: ButtonSize;
  // 按钮类型,可选值为之前定义的 ButtonType 类型,决定按钮的颜色和样式主题
  btnType?: ButtonType;
}

定义 Emits

除了接收外部传入的属性,按钮组件还需要向外发送事件,以便与父组件进行交互。在本组件中,我们主要关注点击事件的处理和传递。

// 定义按钮组件向外发射的事件接口
interface ButtonEmits {
  // 当按钮被点击时,发射 'click' 事件,并携带鼠标事件对象作为参数,以便父组件获取更多点击相关的信息
  (event: "click", payload: MouseEvent): void;
}

编写 Template

模板部分是按钮组件的核心结构,它负责将 Props 和插槽内容结合起来,构建出最终呈现给用户的按钮元素。

<template>
  <button
    ref="button"
    :class="classes"
    :disabled="props.disabled"
    v-bind="$attrs"
    @click="handleClick"
  >
    <!-- 插槽用于插入用户自定义的内容,如文本、图标等 -->
    <slot></slot>
  </button>
</template>

组件样式

为了确保按钮组件在视觉上具有吸引力且符合现代设计风格,我们使用 SCSS 来精心定义按钮的样式。通过变量和混合宏的使用,实现了样式的可维护性和可扩展性,能够轻松地调整按钮的各种样式属性,如颜色、大小、边框等,以适应不同的设计系统和主题要求。

// 引入 sass:color 模块,用于颜色的调整和操作
@use 'sass:color';
// 引入自定义的样式变量文件,包含颜色、字体、边框等通用变量,方便统一管理和修改样式
@use "../../styles/variables" as *;

// 按钮的基本字体权重,默认为正常字体权重
$btn-font-weight: $font-weight-normal!default;
// 按钮在垂直方向上的内边距,默认为 0.375rem
$btn-padding-y: 0.375rem!default;
// 按钮在水平方向上的内边距,默认为 0.75rem
$btn-padding-x: 0.75rem!default;
// 按钮的字体家族,默认为基础字体家族
$btn-font-family: $font-family-base!default;
// 按钮的字体大小,默认为基础字体大小
$btn-font-size: $font-size-base!default;
// 按钮的行高,默认为基础行高
$btn-line-height: $line-height-base!default;

// 不同大小按钮的垂直和水平内边距以及字体大小设置
$btn-padding-y-sm: 0.25rem!default;
$btn-padding-x-sm: 0.5rem!default;
$btn-font-size-sm: $font-size-sm!default;
$btn-padding-y-lg: 0.5rem!default;
$btn-padding-x-lg: 1rem!default;
$btn-font-size-lg: $font-size-lg!default;

// 按钮的边框宽度,默认为基础边框宽度
$btn-border-width: $border-width!default;

// 按钮的 box-shadow 属性,用于添加阴影效果,增强按钮的立体感
$btn-box-shadow: inset 0 1px 0 rgba($white, 0.15), 0 1px 1px rgba($black, 0.075)!default;
// 按钮在禁用状态下的透明度,默认为 0.65,使其呈现出不可用的视觉效果
$btn-disabled-opacity: 0.65!default;

// 按钮的边框圆角半径,默认为基础边框圆角半径
$btn-border-radius: $border-radius!default;
// 大尺寸按钮的边框圆角半径,默认为大尺寸边框圆角半径
$btn-border-radius-lg: $border-radius-lg!default;
// 小尺寸按钮的边框圆角半径,默认为小尺寸边框圆角半径
$btn-border-radius-sm: $border-radius-sm!default;

// 按钮的过渡效果,包括颜色、背景色、边框颜色和 box-shadow 的过渡,使按钮在状态变化时具有平滑的动画效果
$btn-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,
border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out!default;

// 定义混合宏用于设置按钮的大小,接收垂直内边距、水平内边距、字体大小和边框圆角半径作为参数
@mixin button-size($padding-y, $padding-x, $font-size, $border-radius) {
  padding: $padding-y $padding-x;
  font-size: $font-size;
  border-radius: $border-radius;
}

// 定义混合宏用于设置按钮的样式,包括背景色、边框颜色、文字颜色以及鼠标悬停时的样式变化
@mixin button-style(
  $background,
  $border,
  $color,
  $hover-background: color.adjust($background, $lightness:7.5%),
  $hover-border: color.adjust($border, $lightness:10%),
  $hover-color: $color
) {
  background: $background;
  border-color: $border;
  color: $color;
  &:hover {
    background: $hover-background;
    border-color: $hover-border;
    color: $hover-color;
  }
  &:focus,
  &.focus {
    background: $hover-background;
    border-color: $hover-border;
    color: $hover-color;
  }
  &:disabled,
  &.disabled {
    background: $background;
    border-color: $border;
    color: $color;
  }
}

// 基础按钮类,设置了按钮的通用样式,如字体、颜色、内边距、边框、阴影、过渡效果等
.ld-button {
  position: relative;
  display: inline-block;
  font-weight: $btn-font-weight;
  line-height: $btn-line-height;
  color: $body-color;
  white-space: nowrap;
  text-align: center;
  vertical-align: middle;
  background-image: none;
  border: $btn-border-width solid transparent;
  @include button-size(
                  $btn-padding-y,
                  $btn-padding-x,
                  $btn-font-size,
                  $btn-border-radius
  );
  @include button-style(
                  $white,
                  $gray-400,
                  $body-color,
                  $white,
                  $primary,
                  $primary
  );
  box-shadow: $btn-box-shadow;
  cursor: pointer;
  transition: $btn-transition;

  &.disabled,
  &[disabled] {
    cursor: not-allowed;
    opacity: $btn-disabled-opacity;
    box-sizing: unset;

    > * {
      pointer-events: none; // 确保按钮内部的元素在按钮禁用时也不接受鼠标事件
    }
  }

  & +.ld-button {
    margin-left: 0.5rem; // 设置按钮之间的间距,增强布局的合理性
  }
}

// 大尺寸按钮类,通过混合宏应用大尺寸按钮的特定样式
.ld-button-large {
  @include button-size(
                  $btn-padding-y-lg,
                  $btn-padding-x-lg,
                  $btn-font-size-lg,
                  $btn-border-radius-lg
  );
}

// 小尺寸按钮类,通过混合宏应用小尺寸按钮的特定样式
.ld-button-small {
  @include button-size(
                  $btn-padding-y-sm,
                  $btn-padding-x-sm,
                  $btn-font-size-sm,
                  $btn-border-radius-sm
  );
}

// 主要按钮类,通过混合宏应用主要按钮的特定样式,如颜色、背景色等
.ld-button-primary {
  @include button-style($primary, $primary, $white);
}

// 危险按钮类,通过混合宏应用危险按钮的特定样式,通常用于表示删除、取消等具有潜在风险的操作
.ld-button-danger {
  @include button-style($danger, $danger, $white);
}

业务场景组件

在业务场景中,按钮常用于查询操作。若不加以处理,很容易导致频繁触发按钮。针对这种情况,我们可以采取以下措施:

  1. 节流防抖:采用节流防抖操作来控制点击事件,避免其过于频繁地触发。
  2. 前端接口 API 封装:对相同接口请求进行处理,取消之前尚未返回的相同接口请求,仅返回最后一次请求的数据。
  3. 控制按钮可点击状态:通过改变按钮的disabled属性来控制按钮的可点击状态。

对于第 3 点,如果在项目中不提取公共处理逻辑,那么将会有许多变量用于控制按钮状态。为此,我们可以基于基础的按钮组件来封装一个加载组件。当按钮被点击后,将其设置为disabled状态,使其不可操作,待按钮事件结束(请求结束后),再恢复按钮的可操作状态。

<script lang="ts" setup>
import { ref, useAttrs } from "vue";
import LdButton from "./Button.vue";

defineOptions({
  name: "LoadingButton",
  inheritAttrs: false,
});
const isLoading = ref(false);
const attrs = useAttrs();
const handleClick = async () => {
  isLoading.value = true;
  try {
    await Promise.resolve().then(attrs?.onClick as () => Promise<void>);
  } finally {
    isLoading.value = false;
  }
};
</script>

<template>
  <ld-button :disabled="isLoading" @click="handleClick">
    // 自定义加载icon
    <i v-if="isLoading" class="loading"></i>
    <slot></slot>
  </ld-button>
</template>

加载按钮的使用

<script lang="ts" setup>
const handleClick = () => {
  console.log("操作开始");
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(true);
    }, 2000);
  }).then(res => {
    console.log("操作完成", res);
  });
};
</script>

<loading-button @click="handleClick">确定</loading-button>

感谢阅读,敬请斧正!