Files
2026-09-24 16:25:22 +08:00

322 lines
8.1 KiB
Plaintext

<script lang="ts" setup>
import { ref, computed, watch, onMounted, nextTick } from 'vue'
import { getUid } from '../../core/util/xCoreUtil.uts';
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
/**
* @name 开关 xSwitch
* @description 开关,用于直观的展示选项表单的选择。
* @page /pages/index/switch
* @category 表单组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({ name: 'xSwitch' })
export type xSwitchPropsType = {
/**
* 激活时的背景色,空值时取全局的值。
*/
color : string,
/**
* 未激活时的背景
*/
bgColor : string,
/**
* 未激活时的暗黑背景
* 空取inputDarkColor
*/
darkBgColor : string,
/**
* 按钮的背景色
*/
btnColor : string,
/**
* 尺寸
*/
size : "small" | "noraml" | "large" | "normal",
/**
* 间隙,px单位
*/
space : number,
/**
* 当前打开的状态,默认为false
* 等同v-model=""
*/
modelValue : boolean,
/**
* 是否禁用
*/
disabled : boolean,
/**
* 是否加载中
*/
loading : boolean,
/**
* 开关文字数组第一个为开,后一个为关
*/
label : string[],
/**
* 圆角。空值时取全局值。
*/
round : string,
/**
* 激活时的按钮图标,不提供不显示
*/
activeIcon : string,
/**
* 未激活时的图标,不提供不显示
*/
icon : string
}
const props = withDefaults(defineProps<xSwitchPropsType>(), {
color: "",
bgColor: "info",
darkBgColor: "",
btnColor: "white",
size: "normal",
space: 2,
modelValue: false,
disabled: false,
loading: false,
label: [] as string[],
round: "",
activeIcon: "",
icon: ""
})
const emits = defineEmits([
/**
* 状态变换时触发。
* @param {boolean} status - 当前的开关状态
*/
'change',
/**
* 组件被点击时触发。
* @param {boolean} status - 当前的开关状态,这里的状态是在变更前。
*/
'click',
/**
* 等同v-model=""
*/
'update:modelValue'
])
// 响应式状态
const id = ref<string>("xSwitch" + getUid())
const boxwidth = ref<number>(0)
const boxheight = ref<number>(0)
const opened = ref<boolean>(false)
// 计算属性,对齐原有命名
const _activeIcon = computed<string>(() => props.activeIcon)
const _icon = computed<string>(() => props.icon)
const _round = computed<string>(() => {
if (props.round == "") {
return checkIsCssUnit(xConfig.switchRadius, xConfig.unit)
}
return checkIsCssUnit(props.round, xConfig.unit)
})
const _bgColor = computed<string>(() => {
if (xConfig.dark == 'dark') {
if (props.darkBgColor != '') return props.darkBgColor
return xConfig.inputDarkColor
}
return getDefaultColor(props.bgColor)
})
const _activeBgColor = computed<string>(() => {
if (props.color == "") {
return getDefaultColor(xConfig.color)
}
return getDefaultColor(props.color)
})
const _btnColor = computed<string>(() => getDefaultColor(props.btnColor))
const _size = computed<string>(() => {
if (props.size == 'small') return '22px'
if (props.size == 'large') return '38px'
return '32px'
})
const _fontSize = computed<string>(() => {
if (props.size == 'small') return '10px'
if (props.size == 'large') return '12px'
return '11px'
})
const _sizeWidth = computed<string>(() => {
if (props.size == 'small') return '44px'
if (props.size == 'large') return '76px'
return '64px'
})
const _space = computed<number>(() => props.space)
const _contentWidth = computed<number>(() => boxwidth.value - _space.value * 2)
const _contentHeight = computed<number>(() => boxheight.value - _space.value * 2)
const _maxLeftPos = computed<number>(() => _contentWidth.value - _contentHeight.value)
const _label = computed<string[]>(() => props.label)
const _loading = computed<boolean>(() => props.loading)
const _disabled = computed<boolean>(() => props.disabled)
const _animationFun = computed<string>(() => xConfig.animationFun)
watch(():boolean => props.modelValue, (newvalue : boolean) => {
if (newvalue != opened.value) {
opened.value = newvalue
}
})
function onClick() {
emits('click', opened.value)
if (_loading.value || _disabled.value) return;
opened.value = !opened.value
emits('update:modelValue', opened.value)
nextTick(() => {
emits('change', opened.value)
})
}
function getNodes() {
const width = parseInt(_sizeWidth.value)
const height = parseInt(_size.value)
boxwidth.value = width
boxheight.value = height
// 以下代码在sdk中有bug需要等sdk渲染完成才能具的知道结果。如何知道结果不清楚。
// uni.createSelectorQuery().in(this)
// .select(".xSwitch")
// .boundingClientRect().exec((ret) => {
// let nodeinfo = ret[0] as NodeInfo;
// boxwidth.value = nodeinfo.width! as number;
// boxheight.value = nodeinfo.height! as number;
// })
}
onMounted(() => {
opened.value = props.modelValue
getNodes()
})
</script>
<template>
<view @click="onClick" :id="id" class="xSwitch" ref="xSwitch" :style="{
height:_size,
width:_sizeWidth,
backgroundColor:_bgColor,
borderRadius:_round
}">
<view :class="[_disabled?'xSwitchDisabled':'',opened?'xSwitchBgOn':'xSwitchOff']" class="xSwitchBg"
:style="{backgroundColor:_activeBgColor,borderRadius:_round,'transition-timing-function':_animationFun}">
</view>
<view :class="[_disabled?'xSwitchDisabled':'']" class="xSwitchWrap" :style="{
left:_space+'px',
top:_space+'px',
width:_contentWidth+'px',
height:_contentHeight+'px',
borderRadius:_round
}">
<view v-if="_label.length==2" class="xSwitchText">
<text class="xSwitchTextLeft" :style="{fontSize:_fontSize}">{{!opened?'':_label[0]}}</text>
<text class="xSwitchTextRight" :style="{fontSize:_fontSize}">{{opened?'':_label[1]}}</text>
</view>
<view class="xSwitchBtn" :style="{
width:_contentHeight+'px',
height:_contentHeight+'px',
transform:`translateX(${opened?_maxLeftPos:0}px)`,
backgroundColor:_btnColor,
borderRadius:_round,
'transition-timing-function':_animationFun
}">
<x-icon v-if="_loading" :spin="true" font-size="13" name="loader-line" :color="_activeBgColor"></x-icon>
<view v-if="!_loading" :class="[opened?'xSwitchIconOn':'xSwitchIconOff','xSwitchIcon']">
<x-icon v-if="(opened?_activeIcon:_icon)!=''" :font-size="((_contentHeight*0.6).toString())"
:color="(opened?_activeBgColor:_bgColor)" :name="(opened?_activeIcon:_icon)"></x-icon>
</view>
</view>
</view>
</view>
</template>
<style scoped>
.xSwitchIcon {
transition-property: transform, opacity;
transition-timing-function: ease-in;
transition-duration: 250ms;
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.xSwitchDisabled {
opacity: 0.7;
/* #ifdef WEB */
cursor: no-drop !important;
/* #endif */
}
.xSwitchTextLeft {
color: white;
}
.xSwitchTextRight {
color: #acacac;
}
.xSwitch {
height: 32rpx;
position: relative;
/* #ifdef WEB */
cursor: pointer;
/* #endif */
}
.xSwitchBg {
width: 100%;
height: 100%;
transition-duration: 350ms;
transition-property: opacity, transform;
/* transition-timing-function: ease; */
}
.xSwitchBgOn {
opacity: 1;
transform: scale(1);
}
.xSwitchOff {
opacity: 0;
transform: scale(0.9);
}
.xSwitchWrap {
position: absolute;
}
.xSwitchText {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 12%;
box-sizing: border-box;
}
.xSwitchBtn {
position: absolute;
/* border-radius: 200rpx; */
transition-duration: 350ms;
left: 0px;
top: 0;
transition-property: transform;
/* transition-timing-function: cubic-bezier(.18,.89,.32,1); */
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
</style>