仿 ElementPlus 组件库(九)—— Switch 组件实现

在仿 ElementPlus 组件库的开发旅程中,我们已经顺利攻克了 Input 等组件的实现,每一次的成功都让我们的组件库更加丰富和完善。现在,让我们聚焦于 Switch 组件的实现。

一、什么是 Switch 组件

Switch 组件,作为用户界面中常见的交互元素,其核心功能在于实现两种状态之间的灵活切换,典型的如开与关、启用与禁用的状态转换。它为用户提供了一种直观且便捷的操作方式,用于控制应用程序中的各种功能或设置。

从基本构成来看,Switch 组件通常由轨道和滑块两部分组成。轨道代表了开关的整体范围,而滑块则在轨道上滑动,以指示当前的状态。其工作原理基于简单的交互逻辑:当用户点击或触摸滑块时,滑块在轨道上移动,从而实现状态的切换。

常见应用场景

  • 系统设置中的通知开关:在各类操作系统和应用程序的设置界面中,Switch 组件常被用于控制通知的开启或关闭。例如,用户可以通过切换开关来决定是否接收来自某个应用的消息通知。
  • 权限管理中的功能启用开关:在权限管理系统中,Switch 组件可用于控制不同用户或角色对某些功能的访问权限。例如,管理员可以通过切换开关来启用或禁用某个用户对特定文件的编辑权限。
  • 界面布局的显示隐藏切换:在一些应用的界面布局中,Switch 组件可用于控制某些元素或区域的显示与隐藏。例如,在一款图像编辑应用中,用户可以通过切换开关来显示或隐藏侧边栏的工具面板。

二、实现 Switch 组件

(一)组件目录

目录 复制代码
components
├── Switch
    ├── Switch.vue
    ├── types.ts
    ├── style.css   

(二)实现 Switch 组件基本功能

  • 组件结构搭建 :完成 Switch 组件的基本结构和样式绑定,能够根据不同的属性值动态渲染开关的外观。
  • 属性类型定义 :明确 Switch 组件可接收的属性类型和可触发的事件类型,增强了代码的可读性和可维护性,同时为 TypeScript 类型检查提供了支持。
  • 双向数据绑定实现 :通过 modelValue 属性和 'update:modelValue' 事件,实现 Switch 组件与父组件之间的双向数据绑定。父组件可以通过 v-model 绑定一个布尔值到 Switch 组件,组件内部状态的变化会自动更新父组件的绑定值。
  • 开关切换功能 :实现开关的点击切换功能,当点击开关时,会切换其状态,并触发 'change' 事件,通知父组件状态已改变。
  • 禁用状态处理 :支持开关的禁用状态,当 disabled 属性为 true 时,开关不可点击,确保用户无法进行操作。
types.ts 复制代码
export interface SwtichProps {
  modelValue: boolean;
  disabled?: boolean;
  activeText?: string;
  inactiveText?: string;
  name?: string;
  id?: string;
  size?: 'small' | 'large';
}

export interface SwtichEmits {
  (e: 'update:modelValue', value: boolean) : void;
  (e: 'change', value: boolean): void;

}
Switch.vue 复制代码
<template>
  <div
    class="yl-switch"
    :class="{
      [`yl-switch--${size}`]: size,
      'is-disabled': disabled,
      'is-checked': checked,
    }"
    @click="switchValue"
  >
    <input
      class="yl-swtich__input"
      type="checkbox"
      role="switch"
      :name="name"
      :disabled="disabled"
    />
    <div class="yl-switch__core">
      <div class="yl-switch__core-action"></div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import type { SwitchEmits, SwitchProps } from './types';
defineOptions({
  name: 'YlSwitch',
  inheritAttrs: false,
})

const props = defineProps<SwitchProps>()
const emits = defineEmits<SwitchEmits>()

const innerValue = ref(props.modelValue)
const checked = computed(() => innerValue.value)
const switchValue = () => {
  if (props.disabled) return
  innerValue.value = !checked.value
  emits('update:modelValue', innerValue.value)
  emits('change', innerValue.value)
}
</script>

(三)实现 Switch 组件与内部input关联

实现组件与内部 input 元素的关联 :通过 ref 引用和 onMountedwatch 函数,确保了 Switch 组件内部维护的开关状态(checked)与 input 元素的 checked 属性始终保持一致。无论是组件内部状态改变还是直接操作 input 元素,两者的状态都会同步更新。

支持键盘操作 :为 input 元素添加 @keydown.enter 事件监听器,使得用户可以通过按下回车键来切换开关状态,提升了组件的可访问性和操作的便捷性。

Switch.vue 复制代码
import { ref, computed, onMounted, watch } from 'vue'


 <input
      class="yl-swtich__input"
      ref="input"
      @keydown.enter="switchValue"
//...
    />

const input = ref<HTMLInputElement>()
onMounted(() => {
  input.value!.checked = checked.value
})
watch(checked, (val) => {
  input.value!.checked = val
})
watch(
  () => props.modelValue,
  (newValue) => {
    innerValue.value = newValue
  },
)

(四)实现 Switch 组件对更多类型的支持

  • 支持更多类型的值 :定义 SwitchValueType 联合类型,Switch 组件可以处理布尔值、字符串和数字类型的值,提升了组件的通用性和灵活性。例如,在某些场景下,用户可能希望开关开启时对应的值为字符串 "on",关闭时对应 "off",或者是数字 10 等。
  • 自定义开关状态值 :新增的 activeValueinactiveValue 属性允许开发者自定义开关处于开启和关闭状态时对应的值。
  • 事件传递多类型值 :更新后的 SwitchEmits 接口和 switchValue 方法确保了 'update:modelValue''change' 事件能正确传递不同类型的值给父组件,保持组件与父组件之间数据传递的一致性和正确性。
types.ts 复制代码
export type SwitchValueType = boolean | string | number;

export interface SwitchProps {
  modelValue: SwitchValueType;
  activeValue?: SwitchValueType
  inactiveValue?: SwitchValueType
  //...
}

export interface SwitchEmits {
  (e: 'update:modelValue', value: SwitchValueType) : void;
  (e: 'change', value: SwitchValueType): void;

}
Switch.vue 复制代码
const props = withDefaults(defineProps<SwtichProps>(), {
  activeValue: true,
  inactiveValue: false,
})

const checked = computed(() => innerValue.value === props.activeValue)
const switchValue = () => {
  if (props.disabled) return
  const newValue = checked.value ? props.inactiveValue : props.activeValue
  innerValue.value = newValue
  emits('update:modelValue', newValue)
  emits('change', newValue)
}

(五)对 Switch 组件添加文字描述

  • 开关状态文字描述显示 :为 Switch 组件添加文字描述功能,允许在开关的视觉元素旁边显示对应的文字说明。当开关处于不同状态时,能清晰地向用户展示当前开关的状态含义,例如 "开启""关闭",或者更具业务含义的描述,如 "启用通知""禁用通知" 等。
  • 条件渲染优化 :通过 v-if 指令,只有在 activeTextinactiveText 被设置时,才会渲染文字描述元素,避免了在不需要显示文字时占用额外的页面空间。
  • 动态更新文字内容:文字描述内容会根据开关的状态动态更新,确保用户始终能看到与当前开关状态对应的准确描述。
Switch.vue 复制代码
    <div class="yl-switch__core">
      <div class="yl-switch__core-inner">
        <span v-if="activeText || inactiveText" class="yl-switch__core-inner-text">
          {{ checked ? activeText : inactiveText }}
        </span>
      </div>
      <div class="yl-switch__core-action"></div>
    </div>

(六)对 Switch 组件添加样式

  • 样式定制和统一管理:通过自定义 CSS 属性,实现对 Switch 组件不同状态下颜色、边框等样式的集中管理,方便开发者根据项目需求快速调整样式,保持项目整体风格的一致性。
  • 基本外观和交互样式实现:为 Switch 组件定义基本的外观和交互样式,包括开关的整体布局、滑块的位置和样式、文字描述区域的显示等,使开关在不同状态(开启、关闭、禁用)下都能呈现出符合预期的视觉效果。
  • 不同尺寸支持:实现对 Switch 组件不同尺寸(大、小)的支持,开发者可以根据实际需求选择合适的尺寸,增强了组件的通用性和适应性。
  • 样式整合和应用 :通过在 styles/index.css 中引入 Switch 组件的样式文件,成功将 Switch 组件的样式应用到项目中,确保组件在页面中能够正确显示和交互。
style.css 复制代码
.yl-switch {
  --yl-switch-on-color: var(--yl-color-primary);
  --yl-switch-off-color: var(--yl-border-color);
  --yl-switch-on-border-color: var(--yl-color-primary);
  --yl-switch-off-border-color: var(--yl-border-color);
}

.yl-switch {
  display: inline-flex;
  align-items: center;
  font-size: 14px;
  line-height: 20px;
  height: 32px;
  .yl-swtich__input {
    position: absolute;
    width: 0;
    height: 0;
    opacity: 0;
    margin: 0;
    &:focus-visible {
      & ~ .yl-switch__core {
        outline: 2px solid var(--yl-switch-on-color);
        outline-offset: 1px;
      }
    }
  }
  &.is-disabled {
    opacity: .6;
    .yl-switch__core {
      cursor: not-allowed;
    }
  }
  &.is-checked {
    .yl-switch__core {
      border-color:var(--yl-switch-on-border-color);
      background-color: var(--yl-switch-on-color);
      .yl-switch__core-action {
        left: calc(100% - 17px);
      }
      .yl-switch__core-inner {
        padding: 0 18px 0 4px;
      }
    }
  }
}
.yl-switch--large {
  font-size: 14px;
  line-height: 24px;
  height: 40px;
  .yl-switch__core {
    min-width: 50px;
    height: 24px;
    border-radius: 12px;
    .yl-switch__core-action {
      width: 20px;
      height: 20px;
    }
  }
  &.is-checked {
    .yl-switch__core .yl-switch__core-action {
      left: calc(100% - 21px);
      color: var(--yl-switch-on-color);
    }
  }
}
.yl-switch--small {
  font-size: 12px;
  line-height: 16px;
  height: 24px;
  .yl-switch__core {
    min-width: 30px;
    height: 16px;
    border-radius: 8px;
    .yl-switch__core-action {
      width: 12px;
      height: 12px;
    }
  }
  &.is-checked {
    .yl-switch__core .yl-switch-core-action {
      left: calc(100% - 13px);
      color: var(--yl-switch-on-color);
    }
  }
}
.yl-switch__core {
  display: inline-flex;
  align-items: center;
  position: relative;
  height: 20px;
  min-width: 40px;
  border: 1px solid var(--yl-switch-off-border-color);
  outline: none;
  border-radius: 10px;
  box-sizing: border-box;
  background: var(--yl-switch-off-color);
  cursor: pointer;
  transition: border-color var(--yl-transition-duration),background-color var(--yl-transition-duration);
  .yl-switch__core-action {
    position: absolute;
    left: 1px;
    border-radius: var(--yl-border-radius-circle);
    width: 16px;
    height: 16px;
    background-color: var(--yl-color-white);
    transition: all var(--yl-transition-duration);
  }
  .yl-switch__core-inner {
    width: 100%;
    transition: all var(--yl-transition-duration);
    height: 16px;
    display: flex;
    justify-content: center;
    align-items: center;
    overflow: hidden;
    padding: 0 4px 0 18px;
    .yl-switch__core-inner-text {
      font-size: 12px;
      color: var(--yl-color-white);
      user-select: none;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
  }
}
styles/index.css 复制代码
@import '../components/Switch/style.css';
相关推荐
云浮万里_11 分钟前
保姆级教程:Vue3 + Django + MySQL 前后端联调(PyCharm+VSCode版)
vue.js·vscode·mysql·pycharm·django
程序员小刚23 分钟前
基于SpringBoot + Vue 的考勤管理系统
vue.js·spring boot·后端
uhakadotcom44 分钟前
刚刚发布的React 19.1提供了什么新能力?
前端·javascript·面试
uhakadotcom1 小时前
Expo 简介:跨平台移动应用开发的强大工具
前端·javascript·面试
markzzw1 小时前
浏览器插件钱包(一) - 区块链世界的入口
前端·web3·区块链
夕水1 小时前
终于,我也能够写出一款代码编辑器
前端
red润1 小时前
npm包autocannon牛逼的后台压力测试库
前端·javascript·node.js
黄蘑菇1 小时前
white-space、word-break、overflow-wrap(原名word-wrap)的区别
前端
渔樵江渚上1 小时前
JavaScript函数柯里化:优雅的函数式编程实践
前端·javascript·面试
the_one1 小时前
如何判断一个属性是否存在
前端·javascript·面试