一、使用效果
< template>
< QqThreeSwitch v-model = " value" />
</ template>
< script setup >
import SqThreeSwitch from './components/SqThreeSwitch.vue'
import { ref } from 'vue'
const value = ref ( 0 )
</ script>
二、SqThreeSwitch.vue源码
< template>
< div class = " sq-three-switch" >
< button class = " focus-btn" :style = " focusBtnStyle" @click = " handleBtnClick" >
按下空格切换主题, 当前选择:{{ selectedOption }}
</ button>
< div v-show = " isMouseEnter" class = " tooltip" tabindex = " -1" :style = " tooltipStyle" >
< div class = " tip-text" > {{ tooltipText }}</ div>
< svg class = " tip-arrow" width = " 16px" height = " 8px" :style = " tipArrowStyle" >
< polygon points = " 0,-1 8,7 16,-1" />
</ svg>
</ div>
< div ref = " selectedOptionRef" class = " selected-option" >
< span> {{ selectedOption }}</ span>
</ div>
< div
ref = " controlRef"
class = " control plane-border"
@click = " handleClick"
@mouseenter = " handleMouseEnter"
@mouseleave = " handleMouseLeave"
@mousemove = " debouncedHandleMouseMove"
> </ div>
< div class = " plane" > </ div>
< div class = " badge-dots" >
< div
v-for = " (dot, index) in [0, 1, 2]"
:key = " index"
class = " dot"
:class = " { ' dot-animate' : dotAnimateFlag }"
@animationend = " handleAnimationEnd"
> </ div>
</ div>
< div class = " handle" :style = " handleStyle" >
< slot v-if = " modelValue === 0" name = " left-action" > </ slot>
< slot v-if = " modelValue === 1" name = " middle-action" > </ slot>
< slot v-if = " modelValue === 2" name = " right-action" > </ slot>
</ div>
</ div>
</ template>
< script setup >
import { ref, watch, computed, nextTick, onMounted, onBeforeUnmount } from 'vue'
import { useDebounceFn } from '@vueuse/core'
const props = defineProps ( {
modelValue: {
type: Number,
default : 0
} ,
options: {
type: Array,
default : ( ) => [ '选项A' , '选项B' , '选项C' ]
}
} )
const emit = defineEmits ( [ 'update:modelValue' ] )
const selectedOptionRef = ref ( null )
const focusBtnStyle = ref ( { } )
nextTick ( ( ) => {
focusBtnStyle. value = {
width: ` ${ selectedOptionRef. value. getBoundingClientRect ( ) . width + 50 } px `
}
} )
watch (
( ) => props. modelValue,
( ) => {
nextTick ( ( ) => {
focusBtnStyle. value = {
width: ` ${ selectedOptionRef. value. getBoundingClientRect ( ) . width + 50 } px `
}
} )
}
)
const controlRef = ref ( null )
const haveTooltipSpace = ref ( false )
const tipArrowStyle = computed ( ( ) => {
return {
transform: haveTooltipSpace. value ? '' : 'translateY(-26px) rotate(180deg)'
}
} )
function checkTooltipSpace ( deadline ) {
if ( deadline. timeRemaining ( ) > 0 ) {
const rect = controlRef. value?. getBoundingClientRect ( )
if ( rect) {
haveTooltipSpace. value = rect. top >= 20
}
}
}
const debouncedCheckTooltipSpace = useDebounceFn (
( ) => requestIdleCallback ( checkTooltipSpace, { timeout: 200 } ) ,
200
)
let intervalId
onMounted ( ( ) => {
debouncedCheckTooltipSpace ( )
window. addEventListener ( 'scroll' , debouncedCheckTooltipSpace)
window. addEventListener ( 'resize' , debouncedCheckTooltipSpace)
intervalId = setInterval ( debouncedCheckTooltipSpace, 2000 )
console. log ( '作者主页: https://blog.csdn.net/qq_39124701' )
} )
onBeforeUnmount ( ( ) => {
window. removeEventListener ( 'scroll' , debouncedCheckTooltipSpace)
window. removeEventListener ( 'resize' , debouncedCheckTooltipSpace)
if ( intervalId !== null ) {
clearInterval ( intervalId)
}
} )
const isMouseEnter = ref ( false )
const tooltipText = ref ( props. options[ props. modelValue] )
const tooltipStyle = ref ( {
left: props. modelValue === 0 ? '0px' : props. modelValue === 1 ? '20px' : '40px' ,
top: haveTooltipSpace. value ? '0px' : '54px'
} )
const selectedOption = computed ( ( ) => {
return props. options[ props. modelValue]
} )
const dotAnimateFlag = ref ( false )
const handleStyle = ref ( {
left:
props. modelValue === 0
? '2px'
: props. modelValue === 1
? 'calc(50% - 9px)'
: 'calc(100% - 19px)'
} )
watch (
( ) => props. modelValue,
( newValue ) => {
handleStyle. value = {
left: newValue === 0 ? '2px' : newValue === 1 ? 'calc(50% - 9px)' : 'calc(100% - 19px)'
}
}
)
function handleClick ( event) {
const eventTarget = event. target
const rect = eventTarget. getBoundingClientRect ( )
const clickX = event. clientX - rect. left
const oneThirdWidth = rect. width / 3
if ( clickX < oneThirdWidth) {
if ( props. modelValue === 0 ) {
dotAnimateFlag. value = true
}
emit ( 'update:modelValue' , 0 )
} else if ( clickX > oneThirdWidth * 2 ) {
if ( props. modelValue === 2 ) {
dotAnimateFlag. value = true
}
emit ( 'update:modelValue' , 2 )
} else {
if ( props. modelValue === 1 ) {
dotAnimateFlag. value = true
}
emit ( 'update:modelValue' , 1 )
}
}
function handleBtnClick ( ) {
if ( props. modelValue === 0 ) {
emit ( 'update:modelValue' , 1 )
} else if ( props. modelValue === 1 ) {
emit ( 'update:modelValue' , 2 )
} else if ( props. modelValue === 2 ) {
emit ( 'update:modelValue' , 0 )
}
}
function handleMouseEnter ( ) {
isMouseEnter. value = true
}
function handleMouseLeave ( ) {
isMouseEnter. value = false
}
const debouncedHandleMouseMove = useDebounceFn ( handleMouseMove, 40 )
function handleMouseMove ( event ) {
if ( ! isMouseEnter. value) {
return
}
const rect = event. target. getBoundingClientRect ( )
const clickX = event. clientX - rect. left
const oneThirdWidth = rect. width / 3
if ( clickX < oneThirdWidth) {
tooltipText. value = props. options[ 0 ]
tooltipStyle. value = { left: '0px' , top: haveTooltipSpace. value ? '0px' : '54px' }
} else if ( clickX > oneThirdWidth * 2 ) {
tooltipText. value = props. options[ 2 ]
tooltipStyle. value = { left: 'calc(100% - 21px)' , top: haveTooltipSpace. value ? '0px' : '54px' }
} else {
tooltipText. value = props. options[ 1 ]
tooltipStyle. value = { left: 'calc(50% - 11px)' , top: haveTooltipSpace. value ? '0px' : '54px' }
}
}
function handleAnimationEnd ( ) {
dotAnimateFlag. value = false
}
</ script>
< style scoped >
.sq-three-switch {
position : relative;
width : 60px;
height : 20px;
}
.sq-three-switch > * {
position : absolute;
}
.sq-three-switch > .plane,
.sq-three-switch > .badge-dots,
.sq-three-switch > .handle {
pointer-events : none;
}
.sq-three-switch > .focus-btn {
height : 100%;
border-radius : 10px;
border : 0;
outline-offset : 1px;
font-size : 0;
}
.sq-three-switch > .focus-btn:focus {
outline : 2px solid #409eff;
}
.sq-three-switch > .tooltip {
z-index : 1;
transform : translateY ( -27px) ;
white-space : nowrap;
background-color : #e6e6e6;
border : 1px solid gray;
border-radius : 4px;
padding : 1px 11px;
transition : left 0.2s;
}
.sq-three-switch > .tooltip > .tip-text {
font-size : 12px;
color : black;
}
.sq-three-switch > .tooltip > .tip-arrow {
position : absolute;
top : 18px;
left : 1px;
}
.sq-three-switch > .tooltip > .tip-arrow polygon {
fill : #e6e6e6;
stroke : gray;
stroke-width : 1;
}
.sq-three-switch > .selected-option {
height : 100%;
background : linear-gradient ( to right, #a8d4ff, #409eff 16px) ;
border-radius : 10px;
border-top-left-radius : 0;
border-bottom-left-radius : 0;
transform : translateX ( 50px) ;
display : flex;
justify-content : center;
font-size : 14px;
color : white;
white-space : nowrap;
}
.sq-three-switch > .selected-option > span {
padding-left : 16px;
padding-right : 10px;
user-select : none;
}
.sq-three-switch > .control {
width : 100%;
height : 20px;
border-radius : 10px;
background : #409eff;
cursor : pointer;
}
.sq-three-switch > .plane {
top : 1px;
left : 1px;
width : calc ( 100% - 2px) ;
height : 18px;
border-radius : 10px;
background : #409eff;
}
.sq-three-switch > .badge-dots > .dot {
position : absolute;
top : 8px;
left : 8px;
width : 4px;
height : 4px;
border-radius : 100%;
transition : all 0.3s cubic-bezier ( 0.22, 0.61, 0.36, 1) ;
background-color : white;
}
.sq-three-switch > .badge-dots > .dot:nth-child(2) {
left : 27px;
}
.sq-three-switch > .badge-dots > .dot:nth-child(3) {
left : 47px;
}
.dot-animate {
animation : dotAnimation 0.3s;
}
@keyframes dotAnimation {
0% {
background-color : white;
}
25% {
background-color : black;
}
50% {
background-color : white;
}
75% {
background-color : black;
}
100% {
background-color : white;
}
}
.sq-three-switch > .handle {
top : 2px;
left : 2px;
width : 16px;
height : 16px;
border-radius : 100%;
transition : all 0.3s cubic-bezier ( 0.22, 0.61, 0.36, 1) ;
background-color : white;
}
html.dark .sq-three-switch > .tooltip {
background-color : #303133;
}
html.dark .sq-three-switch > .tooltip > .tip-arrow polygon {
fill : #303133;
}
html.dark .sq-three-switch > .tooltip > .tip-text {
color : white;
}
</ style>