可自定义设置以下属性:
选中时的内容(checked),类型:string | slot,默认 undefined
选中时的值(checkedValue),类型:boolean | string | number,默认 true
未选中时的内容(unchecked),类型:string | slot,默认 undefined
未选中时的值(uncheckedValue),类型:boolean | string | number,默认 false
是否加载中(loading),类型:boolean,默认 false
是否禁用(disabled),类型:boolean,默认 false
开关大小(size),类型:'small' | 'middle' | 'large',默认 'middle'
点击时的波纹颜色(rippleColor),当自定义选中颜色时需要设置,类型:string,默认 '#1677ff'
圆点样式(circleStyle),类型:CSSProperties,默认 {}
指定当前是否选中(v-model:modelValue),类型:boolean | string | number,默认 false
效果如下图:在线预览
①创建开关组件Switch.vue:
<script setup lang="ts">
import { ref, nextTick } from 'vue'
import type { CSSProperties } from 'vue'
interface Props {
checked?: string // 选中时的内容 string | slot
checkedValue?: boolean | string | number // 选中时的值
unchecked?: string // 未选中时的内容 string | slot
uncheckedValue?: boolean | string | number // 未选中时的值
loading?: boolean // 是否加载中
disabled?: boolean // 是否禁用
size?: 'small' | 'middle' | 'large' // 开关大小
rippleColor?: string // 点击时的波纹颜色,当自定义选中颜色时需要设置
circleStyle?: CSSProperties // 圆点样式
modelValue?: boolean | string | number // (v-model) 指定当前是否选中
}
const props = withDefaults(defineProps<Props>(), {
checked: undefined,
checkedValue: true,
unchecked: undefined,
uncheckedValue: false,
loading: false,
disabled: false,
size: 'middle',
rippleColor: '#1677ff',
circleStyle: () => ({}),
modelValue: false
})
const wave = ref(false)
const emit = defineEmits(['update:modelValue', 'change'])
function onSwitch() {
if (props.modelValue === props.checkedValue) {
emit('update:modelValue', props.uncheckedValue)
emit('change', props.uncheckedValue)
} else {
emit('update:modelValue', props.checkedValue)
emit('change', props.checkedValue)
}
if (wave.value) {
wave.value = false
nextTick(() => {
wave.value = true
})
} else {
wave.value = true
}
}
function onWaveEnd() {
wave.value = false
}
</script>
<template>
<div
class="m-switch"
:class="{
'switch-loading': loading,
'switch-small': size === 'small',
'switch-large': size === 'large',
'switch-checked': modelValue === checkedValue,
'switch-disabled': disabled
}"
:style="`--ripple-color: ${rippleColor};`"
@click="disabled || loading ? () => false : onSwitch()"
>
<div class="switch-inner">
<span class="inner-checked">
<slot name="checked">{
{ checked }}</slot>
</span>
<span class="inner-unchecked">
<slot name="unchecked">{
{ unchecked }}</slot>
</span>
</div>
<div class="switch-circle" :style="circleStyle">
<svg v-if="loading" class="circular" viewBox="0 0 50 50">
<circle class="path" cx="25" cy="25" r="20" fill="none"></circle>
</svg>
<slot name="node" :checked="modelValue"></slot>
</div>
<div v-if="!disabled" class="switch-wave" :class="{ 'wave-active': wave }" @animationend="onWaveEnd"></div>
</div>
</template>
<style lang="less" scoped>
.m-switch {
position: relative;
display: inline-flex;
align-items: center;
vertical-align: middle;
min-width: 44px;
height: 22px;
color: rgba(0, 0, 0, 0.88);
font-size: 14px;
line-height: 22px;
background: rgba(0, 0, 0, 0.25);
border-radius: 100px;
cursor: pointer;
transition: all 0.2s;
user-select: none;
&:hover:not(.switch-disabled) {
background: rgba(0, 0, 0, 0.45);
}
.switch-inner {
display: block;
overflow: hidden;
border-radius: 100px;
height: 100%;
padding-left: 24px;
padding-right: 9px;
transition:
padding-left 0.2s ease-in-out,
padding-right 0.2s ease-in-out;
.inner-checked {
margin-left: calc(-100% + 22px - 48px);
margin-right: calc(100% - 22px + 48px);
display: block;
text-align: center;
color: #fff;
font-size: 14px;
transition:
margin-left 0.2s ease-in-out,
margin-right 0.2s ease-in-out;
pointer-events: none;
}
.inner-unchecked {
margin-top: -22px;
margin-left: 0;
margin-right: 0;
display: block;
text-align: center;
color: #fff;
font-size: 14px;
transition:
margin-left 0.2s ease-in-out,
margin-right 0.2s ease-in-out;
pointer-events: none;
}
}
.switch-circle {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
background: #fff;
border-radius: 100%;
transition: all 0.2s ease-in-out;
.circular {
position: absolute;
inset: 0;
margin: auto;
width: 14px;
height: 14px;
animation: loading-rotate 2s linear infinite;
-webkit-animation: loading-rotate 2s linear infinite;
@keyframes loading-rotate {
100% {
transform: rotate(360deg);
}
}
.path {
stroke-dasharray: 90, 150;
stroke-dashoffset: 0;
stroke: @themeColor;
stroke-width: 5;
stroke-linecap: round;
animation: loading-dash 1.5s ease-in-out infinite;
-webkit-animation: loading-dash 1.5s ease-in-out infinite;
@keyframes loading-dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -40px;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -120px;
}
}
}
}
}
}
.switch-loading {
cursor: not-allowed;
opacity: 0.65;
.switch-inner,
.switch-circle {
box-shadow: none;
cursor: not-allowed;
}
}
.switch-small {
min-width: 28px;
height: 16px;
line-height: 16px;
.switch-inner {
padding-left: 18px;
padding-right: 6px;
.inner-checked {
font-size: 12px;
margin-left: calc(-100% + 16px - 36px);
margin-right: calc(100% - 16px + 36px);
}
.inner-unchecked {
font-size: 12px;
margin-top: -16px;
}
}
.switch-circle {
width: 12px;
height: 12px;
.circular {
width: 10px;
height: 10px;
.path {
stroke-width: 4;
}
}
}
}
.switch-large {
min-width: 60px;
height: 28px;
line-height: 28px;
.switch-inner {
padding-left: 30px;
padding-right: 12px;
.inner-checked {
font-size: 18px;
margin-left: calc(-100% + 28px - 60px);
margin-right: calc(100% - 28px + 60px);
}
.inner-unchecked {
font-size: 18px;
margin-top: -28px;
}
}
.switch-circle {
width: 24px;
height: 24px;
.circular {
width: 20px;
height: 20px;
.path {
stroke-width: 6;
}
}
}
}
.switch-checked {
background: @themeColor;
&:hover:not(.switch-disabled) {
background: #4096ff;
}
.switch-inner {
padding-left: 9px;
padding-right: 24px;
.inner-checked {
margin-left: 0;
margin-right: 0;
}
.inner-unchecked {
margin-left: calc(100% - 22px + 48px);
margin-right: calc(-100% + 22px - 48px);
}
}
.switch-circle {
left: calc(100% - 20px);
}
}
.switch-small.switch-checked {
.switch-inner {
padding-left: 6px;
padding-right: 18px;
.inner-unchecked {
margin-left: calc(100% - 16px + 36px);
margin-right: calc(-100% + 16px - 36px);
}
}
.switch-circle {
left: calc(100% - 14px);
}
}
.switch-large.switch-checked {
.switch-inner {
padding-left: 12px;
padding-right: 30px;
.inner-unchecked {
margin-left: calc(100% - 28px + 60px);
margin-right: calc(-100% + 28px - 60px);
}
}
.switch-circle {
left: calc(100% - 26px);
}
}
.switch-disabled {
cursor: not-allowed;
opacity: 0.65;
.switch-circle {
cursor: not-allowed;
}
}
.switch-wave {
position: absolute;
pointer-events: none;
top: 0;
right: 0;
bottom: 0;
left: 0;
animation-iteration-count: 1;
animation-duration: 0.6s;
animation-timing-function: cubic-bezier(0, 0, 0.2, 1), cubic-bezier(0, 0, 0.2, 1);
border-radius: inherit;
}
.wave-active {
z-index: 1;
animation-name: wave-spread, wave-opacity;
@keyframes wave-spread {
from {
box-shadow: 0 0 0.5px 0 var(--ripple-color);
}
to {
box-shadow: 0 0 0.5px 5px var(--ripple-color);
}
}
@keyframes wave-opacity {
from {
opacity: 0.6;
}
to {
opacity: 0;
}
}
}
</style>
②在要使用的页面引入:
<script setup lang="ts">
import Switch from './Switch.vue'
import { ref, watchEffect } from 'vue'
const checked = ref(true)
const customValue1 = ref('no')
const customValue2 = ref(2)
function onChange(checked: boolean) {
console.log('checked:', checked)
}
watchEffect(() => {
console.log('checked:', checked.value)
})
</script>
<template>
<div>
<h1>{
{ $route.name }} {
{ $route.meta.title }}</h1>
<h2 class="mt30 mb10">基本使用</h2>
<Switch v-model="checked" @change="onChange" />
<h2 class="mt30 mb10">禁用开关</h2>
<Switch v-model="checked" disabled />
<h2 class="mt30 mb10">三种大小</h2>
<Space>
<Switch v-model="checked" size="small" />
<Switch v-model="checked" />
<Switch v-model="checked" size="large" />
</Space>
<h2 class="mt30 mb10">加载中</h2>
<Space>
<Switch v-model="checked" size="small" loading />
<Switch v-model="checked" loading />
<Switch v-model="checked" size="large" loading />
</Space>
<h2 class="mt30 mb10">带 文字 / 数字 / 字母 的开关</h2>
<Space>
<Switch v-model="checked" checked="开" unchecked="关" />
<Switch v-model="checked" checked="1" unchecked="0" />
<Switch v-model="checked" checked="yes" unchecked="no" />
</Space>
<h2 class="mt30 mb10">自定义图标和样式</h2>
<Switch
class="u-theme-switch"
v-model="checked"
ripple-color="#faad14"
:circle-style="{ background: checked ? '#001529' : '#fff' }"
>
<template #node="{ checked }">
<svg
v-if="checked"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
viewBox="0 0 24 24"
class="u-dark-svg"
>
<path
d="M12.1,22c-0.3,0-0.6,0-0.9,0c-5.5-0.5-9.5-5.4-9-10.9c0.4-4.8,4.2-8.6,9-9c0.4,0,0.8,0.2,1,0.5c0.2,0.3,0.2,0.8-0.1,1.1c-2,2.7-1.4,6.4,1.3,8.4c2.1,1.6,5,1.6,7.1,0c0.3-0.2,0.7-0.3,1.1-0.1c0.3,0.2,0.5,0.6,0.5,1c-0.2,2.7-1.5,5.1-3.6,6.8C16.6,21.2,14.4,22,12.1,22zM9.3,4.4c-2.9,1-5,3.6-5.2,6.8c-0.4,4.4,2.8,8.3,7.2,8.7c2.1,0.2,4.2-0.4,5.8-1.8c1.1-0.9,1.9-2.1,2.4-3.4c-2.5,0.9-5.3,0.5-7.5-1.1C9.2,11.4,8.1,7.7,9.3,4.4z"
></path>
</svg>
<svg
v-else
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
viewBox="0 0 24 24"
class="u-light-svg"
>
<path
d="M12,18c-3.3,0-6-2.7-6-6s2.7-6,6-6s6,2.7,6,6S15.3,18,12,18zM12,8c-2.2,0-4,1.8-4,4c0,2.2,1.8,4,4,4c2.2,0,4-1.8,4-4C16,9.8,14.2,8,12,8z"
></path>
<path d="M12,4c-0.6,0-1-0.4-1-1V1c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,3.6,12.6,4,12,4z"></path>
<path d="M12,24c-0.6,0-1-0.4-1-1v-2c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,23.6,12.6,24,12,24z"></path>
<path
d="M5.6,6.6c-0.3,0-0.5-0.1-0.7-0.3L3.5,4.9c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C6.2,6.5,5.9,6.6,5.6,6.6z"
></path>
<path
d="M19.8,20.8c-0.3,0-0.5-0.1-0.7-0.3l-1.4-1.4c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C20.3,20.7,20,20.8,19.8,20.8z"
></path>
<path d="M3,13H1c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S3.6,13,3,13z"></path>
<path d="M23,13h-2c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S23.6,13,23,13z"></path>
<path
d="M4.2,20.8c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C4.7,20.7,4.5,20.8,4.2,20.8z"
></path>
<path
d="M18.4,6.6c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C18.9,6.5,18.6,6.6,18.4,6.6z"
></path>
</svg>
</template>
</Switch>
<h2 class="mt30 mb10">自定义选中的值</h2>
<Space gap="large">
<Space vertical align="center">
<Switch v-model="customValue1" checked-value="on" unchecked-value="off">
<template #checked>on</template>
<template #unchecked>off</template>
</Switch>
Current Value: {
{ customValue1 }}
</Space>
<Space vertical align="center">
<Switch v-model="customValue2" :checked-value="1" :unchecked-value="2">
<template #checked>yes</template>
<template #unchecked>no</template>
</Switch>
Current Value: {
{ customValue2 }}
</Space>
</Space>
</div>
</template>
<style lang="less" scoped>
.u-theme-switch.switch-checked {
background: #faad14;
&:hover:not(.disabled) {
background: #e8b339;
}
}
.u-dark-svg {
width: 12px;
height: 12px;
fill: #fff;
}
.u-light-svg {
width: 12px;
height: 12px;
fill: rgba(60, 60, 67, 0.75);
}
</style>