Files
cheflinkmerchant/src/components/ca-switch/ca-switch.vue
T
2026-02-26 09:25:47 +08:00

337 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view
class="ca-switch"
:class="[{ 'is-active': isChecked, 'is-loading': loading, 'is-disabled': disabled }, `is-${shape}`]"
:style="[switchStyle]" @click="handleClick()">
<!-- 球的轨道 -->
<view class="ca-switch__chute" :style="[chuteStyle]">
<!-- -->
<view class="ca-switch__slide" :style="[slideStyle]">
<text v-if="loading" class="ca-switch__loading" :style="[loadingStyle]" />
</view>
<!-- 选中文字 -->
<text
v-if="onText"
class="ca-switch__text is-on"
:style="[textStyle_, trendsTextStyle(true), textStyle]">
{{ onText }}
</text>
<!-- 未选中文字 -->
<text
v-if="offText"
class="ca-switch__text is-off"
:style="[textStyle_, trendsTextStyle(false), textStyle]">
{{ offText }}
</text>
</view>
</view>
</template>
<script>
import props from './props'
import { defineComponent, computed, watch, ref, getCurrentInstance, nextTick } from 'vue'
export default defineComponent({
name: 'CaSwitch',
props,
emits: ['click', 'update:modelValue', 'change', 'asyncChange'],
setup(props, { emit }) {
// 组件宽度
const switchWidth = ref(0)
const { proxy } = getCurrentInstance()
// 球跟轨道之间的间隙
const spaceSize = parseInt(addUnit(props.space))
// 滑块大小
const switchHeight = parseInt(addUnit(props.size))
// 球大小
const slideSize = switchHeight - 2 * spaceSize
// 文本的宽度(要取最大值)
const textMaxWidth = ref(0)
// 双向绑定
const switchValue = ref(props.offVal)
const options = {
immediate: true,
deep: true
}
watch(() => props.modelValue, (value) => {
switchValue.value = value
}, options)
watch(switchValue, (value) => {
emit('update:modelValue', value)
emit('change', value)
}, options)
// 是否激活
const isChecked = computed(() => switchValue.value === props.onVal)
// 文字颜色(on,off
const textStyle_ = computed(() => {
return {
fontSize: addUnit(props.textSize),
minWidth: `${textMaxWidth.value}px`,
zIndex: `${props.textDisplay ? 3 : 1}`
}
})
// 文字颜色
const trendsTextStyle = computed(() => {
return (isOn) => {
return isOn
? {
opacity: props.textDisplay || isChecked.value ? 1 : 0,
color: props.textDisplay ? (isChecked.value ? props.textColor : props.textSelColor) : (isChecked.value ? props.textSelColor : props.textColor)
}
: {
opacity: props.textDisplay || !isChecked.value ? 1 : 0,
color: props.textDisplay ? (isChecked.value ? props.textSelColor : props.textColor) : (isChecked.value ? props.textSelColor : props.textColor)
}
}
})
// switch外层样式
const switchStyle = computed(() => {
return {
width: `${spaceSize > 0 ? switchWidth.value : switchWidth.value - 2 * spaceSize}px`,
height: `${spaceSize > 0 ? switchHeight.value : switchHeight.value - 2 * spaceSize}px`,
opacity: switchWidth.value ? props.disabled ? 0.6 : 1 : 0
}
})
// 滑槽样式
const chuteStyle = computed(() => {
const chuteColor = isChecked.value ? props.chuteSelColor : props.chuteColor
const chuteStyle = {
height: `${switchHeight}px`,
width: `${switchWidth.value}px`,
[chuteColor.includes('linear-gradient') ? 'background' : 'backgroundColor']: chuteColor,
borderRadius: props.shape === 'circle' ? `${switchHeight}px` : '6rpx',
padding: spaceSize > 0 ? `${spaceSize}px` : 0,
opacity: switchWidth.value ? 1 : 0
}
return chuteStyle
})
// 球样式
const slideStyle = computed(() => {
const slideStyle = {
width: `${props.textDisplay ? textMaxWidth.value : slideSize}px`,
borderRadius: props.shape === 'circle' ? `${Math.ceil(slideSize / 2)}px` : '4rpx',
height: `${slideSize}px`,
transform: `translateX(${isChecked.value ? textMaxWidth.value : spaceSize > 0 ? 0 : spaceSize}px)`,
boxShadow: props.chuteShadow
}
return slideStyle
})
const loadingStyle = computed(() => {
const size = addUnit(props.loadingSize)
return {
width: size,
height: size,
color: props.loadingColor || (isChecked.value ? props.chuteSelColor : props.chuteColor)
}
})
watch(() => [props.onText, props.offText], () => {
getSwitchWidth()
}, {
immediate: true
})
// 获取switch高度
async function getSwitchWidth() {
const slideRect = await getRect('.ca-switch__text')
const textInfo = slideRect ? ([].concat(slideRect)).map(o => o.width) : []
// 文字最小宽度
textMaxWidth.value = Math.ceil(Math.max(...textInfo, slideSize * 0.8))
if (props.textDisplay) {
switchWidth.value = (textMaxWidth.value + spaceSize) * 2
} else {
switchWidth.value = slideSize + textMaxWidth.value + spaceSize * 2
}
}
// 状态切换
async function handleClick() {
if (props.readonly || props.loading) return
if (props.disabled) {
props.disabledText && uni.showToast({ title: props.disabledText })
return
}
const res = isChecked.value ? props.offVal : props.onVal
if (props.async) {
emit('asyncChange', res)
return
}
switchValue.value = res
}
/**
* 单位转换
* value [String, Number] 要转换的值
* unit [String] 默认单位
* expUnit [String] 期望单位
*/
function addUnit(value = 'auto', unit = 'rpx', expUnit = 'px') {
if (!value) return `0${unit}`
value = isNaN(value) ? value : `${value}${unit}`
if (expUnit === 'px' && /upx|rpx/.test(value)) {
// 所有数值(含单位)
const strArr = value.replace(/\s+/g, ' ').split(' ')
return strArr.map(o => {
const matcher = o.match(/^(-?\d+)(.*)/)
return `${/upx|rpx/.test(matcher[2]) ? uni.upx2px(matcher[1]) : matcher[1]}${expUnit}`
}).join(' ')
}
return value
}
/**
* @description: 获取节点信息
* @param {String} selecter 要查询节点的id或者class名称
* @param {Object} fields 需要查询返回的额外节点信息
* @return {Promise} 返回需要查询的节点信息
*/
function getRect(selecter, fields) {
function formatReturn(data) {
return Array.isArray(data) ? (data.length > 1 ? data : data[0]) : data
}
return new Promise((resolve, reject) => {
nextTick(() => {
const querySelect = uni.createSelectorQuery()
// #ifndef MP-ALIPAY
querySelect.in(proxy)
// #endif
querySelect.selectAll(selecter)
.fields(
{
size: true,
rect: true,
...fields
},
data => {
resolve(formatReturn(data))
}
)
.exec()
})
})
}
return {
chuteStyle,
isChecked,
textStyle_,
slideStyle,
slideSize,
switchStyle,
loadingStyle,
trendsTextStyle,
handleClick
}
}
})
</script>
<style scoped>
.ca-switch {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s linear;
will-change: opacity, width, height;
box-sizing: border-box;
}
.ca-switch__chute {
transition: opacity 0.3s linear, background-color 0.3s linear;
will-change: opacity, width, height;
box-sizing: border-box;
display: inline-flex;
align-items: center;
}
.ca-switch__slide {
background-color: #fff;
border-radius: 100%;
transition: transform 0.2s cubic-bezier(0.65, 0.05, 0.36, 1);
transform: translateX(0);
will-change: transform;
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.ca-switch__loading{
color: rgb(41, 121, 255);
width: 30rpx;
height: 30rpx;
border-radius: 100%;
animation: rotate 1s linear infinite;
box-sizing: border-box;
}
.ca-switch__loading::after,.ca-switch__loading::before{
content: '';
color: inherit;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
border-radius: 100%;
border: 2px solid currentColor;
box-sizing: border-box;
}
.ca-switch__loading::before{
z-index: 1;
opacity: 0.15;
}
.ca-switch__loading::after{
z-index: 2;
border-color: currentColor transparent transparent;
}
.ca-switch__text {
font-size: 24rpx;
color: #ffffff;
position: absolute;
z-index: 1;
opacity: 0;
top: 0;
bottom: 0;
display: inline-flex;
align-items: center;
justify-content: center;
transition: opacity 0.1s linear;
white-space: nowrap;
padding: 0 12rpx;
line-height: 1;
box-sizing: border-box;
}
.ca-switch__text.is-on{
left: 0;
}
.ca-switch__text.is-off{
right: 0;
}
/* 解决视觉偏差问题 */
.ca-switch.is-circle .is-on{
padding: 0 12rpx 0 18rpx;
}
/* 解决视觉偏差问题 */
.ca-switch.is-circle .is-off{
padding: 0 18rpx 0 12rpx;
}
.ca-switch.is-disabled .ca-switch__chute {
cursor: no-drop;
pointer-events: none;
}
@keyframes rotate{
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(1turn);
transform: rotate(1turn);
}
}
</style>