当前位置: 首页 > news >正文

三开关VUE组件

一、使用效果

请添加图片描述请添加图片描述

请添加图片描述

<template><QqThreeSwitch v-model="value" /><!-- <SqThreeSwitch v-model="value" :options="['test1', 'test2', 'test3']"><template #left-action><div style="display: flex"><IconMoon /></div></template><template #middle-action><div style="display: flex"><IconSunny /></div></template><template #right-action><div style="display: flex"><IconSystem /></div></template></SqThreeSwitch> -->
</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><divref="controlRef"class="control plane-border"@click="handleClick"@mouseenter="handleMouseEnter"@mouseleave="handleMouseLeave"@mousemove="debouncedHandleMouseMove"></div><div class="plane"></div><div class="badge-dots"><divv-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(/** @type { MouseEvent } */ event) {/** @type { Element } */const eventTarget = event.targetconst rect = eventTarget.getBoundingClientRect()const clickX = event.clientX - rect.leftconst oneThirdWidth = rect.width / 3if (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.leftconst oneThirdWidth = rect.width / 3if (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;/* scale: 5;transform-origin: 0% 0%;z-index: 1; */
}
.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>
http://www.lryc.cn/news/492236.html

相关文章:

  • SpringCloud+SpringCloudAlibaba学习笔记
  • 牛客小白月赛105(A~E)
  • OSPF协议整理
  • Java中的多线程
  • 什么是聚簇索引、非聚簇索引、回表查询
  • 探索 Spring 框架核心组件:构建强大 Java 应用的基石
  • Android 13 Aosp 默认允许应用动态权限
  • 【C++知识总结1】c++第一篇,简单了解一下命名空间是什么
  • 从0开始深度学习(32)——循环神经网络的从零开始实现
  • GitLab使用操作v1.0
  • cuda conda yolov11 环境搭建
  • 解决SpringBoot连接Websocket报:请求路径 404 No static resource websocket.
  • element-plus的组件数据配置化封装 - table
  • 【二维动态规划:交错字符串】
  • goframe开发一个企业网站 MongoDB 完整工具包18
  • 在vue中,根据后端接口返回的文件流实现word文件弹窗预览
  • 动态规划之背包问题
  • 【Python】 深入理解Python的单元测试:用unittest和pytest进行测试驱动开发
  • Java集合1.0
  • Leetcode 336 回文对
  • 实现一个可配置的TCP设备模拟器,支持交互和解析配置
  • 算法的空间复杂度
  • 自定义协议
  • 在 Taro 中实现系统主题适配:亮/暗模式
  • autogen框架中使用chatglm4模型实现react
  • 读《Effective Java》笔记 - 条目9
  • 【软件入门】Git快速入门
  • nextjs window is not defined
  • C语言实现冒泡排序:从基础到优化全解析
  • windows11下git与 openssl要注意的问题