first commit
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user