first commit

This commit is contained in:
2026-02-26 09:25:47 +08:00
commit 40665dda67
708 changed files with 100122 additions and 0 deletions
+336
View File
@@ -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>
+125
View File
@@ -0,0 +1,125 @@
export default {
// 双向绑定的值
modelValue: {
type: [String, Number, Boolean],
default: false
},
space: {
type: [Number, String],
default: 4
},
// 开关尺寸,默认单位是rpx
size: {
type: [Number, String],
default: 56
},
textSize: {
type: [String, Number],
default: 26
},
// 选中的值
onVal: {
type: [String, Number, Boolean],
default: true
},
// 未选中的值
offVal: {
type: [String, Number, Boolean],
default: false
},
// 开启展示的值
onText: {
type: String,
default: ''
},
// 关闭展示的值
offText: {
type: String,
default: ''
},
textStyle: {
type: [String, Object],
default: ''
},
// 字体颜色
textColor: {
type: String,
default: '#666'
},
// 选中时字体颜色
textSelColor: {
type: String,
default: '#fff'
},
// 文字常显
textDisplay: {
type: Boolean,
default: false
},
// 滑槽阴影,同css的box-shadow
chuteShadow: {
type: String,
default: '-2rpx 2rpx 4rpx 0 rgba(0,0,0,.25)'
},
// 开关底座颜色(可以是渐变色), 球的轨道
chuteColor: {
type: String,
default: '#eee'
},
// 开关底座选中颜色, 球的轨道
chuteSelColor: {
type: String,
default: '#007aff'
},
// 只读
readonly: {
type: Boolean,
default: false
},
// 禁止滑动
disabled: {
type: Boolean,
default: false
},
// 禁止滑动的提示
disabledText: {
type: String,
default: ''
},
// 滑动球颜色
slideColor: {
type: String,
default: '#fff'
},
// 开关开启后滑动球颜色
slideSelColor: {
type: String,
default: '#fff'
},
// 形状
shape: {
type: String,
values: ['circle', 'square'],
default: 'circle'
},
// 异步关闭
async: {
type: Boolean,
default: false
},
// loading大小,默认单位rpx
loadingSize: {
type: [String, Number],
default: 40
},
// 加载状态
loading: {
type: Boolean,
default: false
},
// loading转圈颜色
loadingColor: {
type: String,
default: ''
}
}