337 lines
11 KiB
Vue
337 lines
11 KiB
Vue
<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>
|