日常学习开发记录-switch组件
日常学习开发记录-switch组件
- 实现思路与过程
- 第一阶段:基础开关
- 第二阶段:增强功能
- 第三阶段:高级功能
- 技术要点总结
- 渐进式开发流程
实现思路与过程
我们将从简单到复杂逐步实现这个Switch组件,展示整个实现过程和思路。
最终实现效果:
第一阶段:基础开关
最基础的开关组件实现了以下核心功能:
-
HTML结构设计
- 外层容器div作为组件主体
- 内部一个span作为滑动轨道
- 一个内嵌的span作为滑块按钮
-
CSS样式设计
- 设置基础样式(大小、颜色、边框)
- 为轨道设置圆角和背景色
- 为滑块设置圆形样式和初始位置
- 添加过渡效果实现平滑切换
-
基础交互逻辑
- 使用v-model双向绑定值
- 添加点击事件切换状态
- 根据状态动态改变样式类名
- 添加disabled属性支持
<!-- 基础结构 -->
<div class="my-switch" @click="handleClick">
<span class="my-switch__core">
<span class="my-switch__button"></span>
</span>
</div>
- my-switch__core: 是开关的轨道部分,作为滑块的容器和运动轨迹。它提供了:
一个背景区域来表示开关的状态(开/关)
圆角边框形成开关的外观
提供了滑块移动的轨道
当状态改变时变换颜色(从inactive颜色到active颜色) - my-switch__button: 是可见的滑块按钮,它:
在用户切换开关状态时移动
通过transform属性平滑地从左侧移动到右侧
视觉上指示当前状态
通常为圆形,有光滑的阴影效果增强立体感
当开关状态改变时会有平滑的动画效果
实现重点:
- 使用CSS类名修改不同状态的样式
- 通过transform来移动滑块按钮
- 添加transition实现平滑过渡效果
第二阶段:增强功能
在基础功能的基础上,添加以下增强功能:
-
文本标签
- 增加activeText和inactiveText属性
- 根据状态显示对应文本
- 添加适当的样式和间距
-
自定义颜色
- 添加activeColor和inactiveColor属性
- 使用CSS变量传递颜色值
- 根据状态动态改变颜色
-
自定义宽度
- 添加width属性控制组件宽度
- 动态计算滑块位置
实现代码扩展:
实现重点:
- 使用CSS变量实现动态颜色
- 使用计算属性处理样式逻辑
- 根据文本内容动态调整布局
第三阶段:高级功能
最后实现更高级的功能,使组件更完善:
- 表单支持
- 添加hidden input元素支持表单提交
- 增加name属性绑定到input上
- 处理input的change事件
待定。
-
键盘可访问性
- 添加tabindex属性使组件可以获取焦点
- 监听键盘事件(空格键)触发状态切换
- 添加focus和blur状态样式
-
自定义值支持
- 从仅支持布尔值扩展到支持字符串、数字等值
- 添加activeValue和inactiveValue属性
- 使用计算属性处理值的转换
实现重点:
- 使用计算属性和侦听器处理复杂的数据流
- 添加辅助样式增强可访问性
- 处理键盘事件提高用户体验
技术要点总结
-
响应式数据处理
- 使用Vue的计算属性处理状态转换
- 使用侦听器同步内外部数据
- 处理自定义值的双向绑定
-
CSS技巧
- 使用CSS变量实现动态样式
- 结合transform和transition实现平滑动画
- 使用嵌套选择器组织样式代码
- 添加焦点状态增强可访问性
-
交互优化
- 支持键盘导航
- 添加过渡效果使切换平滑
- 保持表单功能完整性
<template>
<div
class="my-switch"
:class="{
'is-checked': value === activeValue,
'is-disabled': disabled,
'is-focus': focus,
}"
@click="handleClick"
:style="{
'--active-color': activeColor,
'--inactive-color': inactiveColor,
'--switch-width': width + 'px',
'--button-translate-x': buttonTranslateX + 'px',
}"
tabindex="0"
@keydown.space.prevent="handleClick"
@focus="handleFocus"
@blur="handleBlur"
role="switch"
:aria-checked="value === activeValue"
:aria-disabled="disabled"
>
<span v-if="inactiveText" class="my-switch__label my-switch__label--left">
{{ inactiveText }}
</span>
<span class="my-switch__core">
<span class="my-switch__button"></span>
</span>
<span v-if="activeText" class="my-switch__label my-switch__label--right">
{{ activeText }}
</span>
</div>
</template>
<script>
export default {
name: 'MySwitch',
props: {
value: {
type: [Boolean, String, Number],
default: false,
},
activeValue: {
type: [Boolean, String, Number],
default: true,
},
inactiveValue: {
type: [Boolean, String, Number],
default: false,
},
activeText: {
type: String,
default: '',
},
inactiveText: {
type: String,
default: '',
},
activeColor: {
type: String,
default: '#409eff',
},
inactiveColor: {
type: String,
default: '#dcdfe6',
},
disabled: {
type: Boolean,
default: false,
},
width: {
type: [Number, String],
default: 40,
},
},
data() {
return {
focus: false,
}
},
computed: {
buttonTranslateX() {
const buttonWidth = 16
const widthNumber = Number(this.width)
return widthNumber - buttonWidth - 2 // 2px是按钮到边缘的距离
},
},
methods: {
handleClick() {
if (this.disabled) return
const newValue = this.value === this.activeValue ? this.inactiveValue : this.activeValue
this.$emit('input', newValue)
this.$emit('change', newValue)
},
handleFocus(event) {
this.focus = true
this.$emit('focus', event)
},
handleBlur(event) {
this.focus = false
this.$emit('blur', event)
},
},
}
</script>
<style lang="scss" scoped>
.my-switch {
display: inline-flex;
align-items: center;
position: relative;
font-size: 14px;
line-height: 20px;
vertical-align: middle;
cursor: pointer;
outline: none;
&.is-disabled {
cursor: not-allowed;
opacity: 0.6;
}
&.is-focus {
.my-switch__core {
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.3);
}
}
&.is-checked {
.my-switch__core {
border-color: var(--active-color, #409eff);
background-color: var(--active-color, #409eff);
.my-switch__button {
transform: translateX(var(--button-translate-x, 22px));
}
}
.my-switch__label--right {
color: var(--active-color, #409eff);
}
}
&:not(.is-checked) {
.my-switch__core {
border-color: var(--inactive-color, #dcdfe6);
background-color: var(--inactive-color, #dcdfe6);
}
.my-switch__label--left {
color: var(--inactive-color, #dcdfe6);
}
}
&__label {
font-size: 14px;
color: #606266;
transition: color 0.3s;
&--left {
margin-right: 10px;
}
&--right {
margin-left: 10px;
}
}
&__core {
margin: 0;
display: inline-block;
position: relative;
width: var(--switch-width, 40px);
height: 20px;
border-radius: 10px;
box-sizing: border-box;
transition: border-color 0.3s, background-color 0.3s, box-shadow 0.3s;
.my-switch__button {
position: absolute;
top: 2px;
left: 2px;
border-radius: 100%;
transition: all 0.3s;
width: 16px;
height: 16px;
background-color: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
}
}
</style>
- tabindex=“0”:
使div元素可以获得焦点
当用户按Tab键时,可以导航到这个开关组件
默认情况下,div是不能获得焦点的,这个属性让它变成了可聚焦元素 - @keydown.space.prevent=“handleClick”:
监听空格键按下事件
当组件获得焦点时,按空格键可以触发开关切换
.prevent阻止空格键的默认行为(页面滚动)
这样用户就可以完全用键盘操作开关 - @focus=“handleFocus” 和 @blur=“handleBlur”:
处理组件获得和失去焦点的事件
当组件获得焦点时,会添加一个蓝色阴影效果
提供视觉反馈,让用户知道当前哪个元素被选中
这些事件也会触发相应的focus/blur事件通知父组件 - role=“switch”:
告诉屏幕阅读器这是一个开关控件
让使用屏幕阅读器的用户知道这是一个可以切换的开关
符合WAI-ARIA规范,提高无障碍性 - :aria-checked=“value === activeValue”:
告诉屏幕阅读器开关的当前状态(开启/关闭)
当状态改变时,屏幕阅读器会读出新的状态
帮助视障用户了解开关的当前状态 - :aria-disabled=“disabled”:
告诉屏幕阅读器开关是否被禁用
当开关被禁用时,屏幕阅读器会提示用户
帮助视障用户了解开关是否可用
这些属性的组合确保了:
键盘用户可以完全操作这个开关
屏幕阅读器用户可以理解和使用这个开关
提供了适当的视觉反馈
符合Web Content Accessibility Guidelines (WCAG)标准
渐进式开发流程
- 首先实现最基础的开关功能,确保状态切换正常
- 添加样式和过渡效果,使组件看起来美观
- 实现v-model双向绑定,保证数据流通
- 添加disabled状态,处理禁用逻辑
- 增加文本标签显示,提升用户体验
- 实现自定义颜色功能,增强组件灵活性
- 添加焦点和模糊状态,提高可访问性
- 实现键盘导航,支持无鼠标操作
- 支持自定义值,增强组件通用性