323 lines
7.5 KiB
Plaintext
323 lines
7.5 KiB
Plaintext
<script lang="ts" setup>
|
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue"
|
|
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
|
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
|
|
import { xAnimate } from '../../core/util/xAnimate.uts';
|
|
import { XANIMATE_OPIONS } from "../../interface";
|
|
import { xConfig } from "../../config/xConfig.uts"
|
|
|
|
/**
|
|
* @name 数字翻滚 xRollingNumber
|
|
* @description 是否小数点,取决与你的的endVal目标值,如果它带小数点,那动画也是相应带小数点。
|
|
* @page /pages/index/rolling-number
|
|
* @category 展示组件
|
|
* @constant 平台兼容
|
|
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
|
*/
|
|
defineOptions({name:"xRollingNumber"})
|
|
|
|
type xRollingNumberPropsType = {
|
|
/**
|
|
* 文字大小
|
|
*/
|
|
fontSize: string,
|
|
/**
|
|
* 文字颜色
|
|
*/
|
|
fontColor: string,
|
|
/**
|
|
* 暗黑时的文字颜色,如果为空取白
|
|
*/
|
|
darkFontColor: string,
|
|
/**
|
|
* 起始值
|
|
*/
|
|
startVal: number,
|
|
/**
|
|
* 目标值(当前需要显示的值)
|
|
*/
|
|
endVal: number,
|
|
/**
|
|
* 动画速率。控制翻滚动的动画效果。
|
|
*/
|
|
duration: number,
|
|
/**
|
|
* 缓动函数类型
|
|
*/
|
|
easing: string,
|
|
/**
|
|
* 是否显示千分位分隔符
|
|
*/
|
|
useGrouping: boolean,
|
|
/**
|
|
* 小数位数
|
|
*/
|
|
decimals: number,
|
|
/**
|
|
* 前缀符号(如货币符号)
|
|
*/
|
|
prefix: string,
|
|
/**
|
|
* 后缀符号(如单位)
|
|
*/
|
|
suffix: string,
|
|
/**
|
|
* 是否启用动画
|
|
*/
|
|
enableAnimation: boolean
|
|
}
|
|
|
|
const props = withDefaults(defineProps<xRollingNumberPropsType>(), {
|
|
fontSize: "32",
|
|
fontColor: "black",
|
|
darkFontColor: "",
|
|
startVal: 0,
|
|
endVal: 0,
|
|
duration: 600,
|
|
easing: "easeIn",
|
|
useGrouping: false,
|
|
decimals: -1,
|
|
prefix: "",
|
|
suffix: "",
|
|
enableAnimation: true
|
|
})
|
|
|
|
const emits = defineEmits([
|
|
/**
|
|
* 动画开始时触发
|
|
*/
|
|
'animationStart',
|
|
/**
|
|
* 动画完成时触发
|
|
*/
|
|
'animationComplete',
|
|
/**
|
|
* 动画暂停时触发
|
|
*/
|
|
'animationPause',
|
|
/**
|
|
* 动画恢复时触发
|
|
*/
|
|
'animationResume',
|
|
/**
|
|
* 动画停止时触发
|
|
*/
|
|
'animationStop',
|
|
/**
|
|
* 数值变化时触发
|
|
*/
|
|
'valueChange'
|
|
])
|
|
|
|
defineSlots<{
|
|
default(props: { value: string, formattedValue: string }): any
|
|
}>()
|
|
|
|
// 响应式数据
|
|
const ani = ref<null | xAnimate>(null)
|
|
const endValue = ref<number>(0)
|
|
const oldValue = ref<number>(0)
|
|
const targetValue = ref<string>('0')
|
|
const isAnimating = ref<boolean>(false)
|
|
|
|
// 计算属性
|
|
const _fontSize = computed((): string => {
|
|
let fontSize = checkIsCssUnit(props.fontSize, xConfig.unit);
|
|
if (xConfig.fontScale == 1) return fontSize;
|
|
let sizeNumber = parseInt(fontSize)
|
|
if (isNaN(sizeNumber)) {
|
|
sizeNumber = 32
|
|
}
|
|
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
|
|
})
|
|
|
|
const _fontColor = computed((): string => {
|
|
if (xConfig.dark == 'dark') {
|
|
if (props.darkFontColor != '') {
|
|
return getDefaultColor(props.darkFontColor)
|
|
}
|
|
return "#FFFFFF"
|
|
}
|
|
return getDefaultColor(props.fontColor)
|
|
})
|
|
|
|
const _dmit = computed((): number => {
|
|
if (props.decimals >= 0) {
|
|
return props.decimals
|
|
}
|
|
let str = props.endVal.toString();
|
|
let pasa = str.indexOf(".")
|
|
let endStrNum = 0
|
|
if (pasa > -1) {
|
|
endStrNum = str.substring(pasa + 1).length
|
|
}
|
|
return endStrNum
|
|
})
|
|
|
|
// 格式化数值
|
|
const formatNumber = (value: number): string => {
|
|
let formatted = value.toFixed(_dmit.value)
|
|
|
|
if (props.useGrouping) {
|
|
// 添加千分位分隔符
|
|
const parts = formatted.split('.')
|
|
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
|
formatted = parts.join('.')
|
|
}
|
|
|
|
return props.prefix + formatted + props.suffix
|
|
}
|
|
|
|
// 格式化后的显示值
|
|
const formattedValue = computed((): string => {
|
|
return formatNumber(parseFloat(targetValue.value))
|
|
})
|
|
|
|
// 方法
|
|
function createAnimation(): xAnimate {
|
|
return new xAnimate(null, {
|
|
duration: props.duration,
|
|
timingFunction: props.easing,
|
|
frame(propress: number) {
|
|
const currentValue = propress * (endValue.value - oldValue.value) + oldValue.value
|
|
targetValue.value = currentValue.toFixed(_dmit.value)
|
|
emits('valueChange', parseFloat(targetValue.value))
|
|
},
|
|
start() {
|
|
emits('animationStart')
|
|
},
|
|
complete() {
|
|
targetValue.value = endValue.value.toFixed(_dmit.value)
|
|
isAnimating.value = false
|
|
emits('animationComplete')
|
|
emits('valueChange', endValue.value)
|
|
}
|
|
} as XANIMATE_OPIONS)
|
|
}
|
|
|
|
function play(): void {
|
|
if (!props.enableAnimation) {
|
|
targetValue.value = endValue.value.toFixed(_dmit.value)
|
|
emits('valueChange', endValue.value)
|
|
return
|
|
}
|
|
|
|
if (isAnimating.value) {
|
|
ani.value?.stop()
|
|
}
|
|
|
|
ani.value = createAnimation()
|
|
isAnimating.value = true
|
|
|
|
ani.value!.stop()
|
|
ani.value!.attr('run', '0', '100')
|
|
ani.value!.setDurations(props.duration)
|
|
ani.value!.play()
|
|
}
|
|
|
|
function pause(): void {
|
|
ani.value?.pause()
|
|
isAnimating.value = false
|
|
emits('animationPause')
|
|
}
|
|
|
|
function resume(): void {
|
|
ani.value?.play()
|
|
isAnimating.value = true
|
|
emits('animationResume')
|
|
}
|
|
|
|
function stop(): void {
|
|
ani.value?.stop()
|
|
isAnimating.value = false
|
|
targetValue.value = endValue.value.toFixed(_dmit.value)
|
|
emits('animationStop')
|
|
emits('valueChange', endValue.value)
|
|
}
|
|
|
|
function reset(): void {
|
|
stop()
|
|
oldValue.value = props.startVal
|
|
endValue.value = props.startVal
|
|
targetValue.value = props.startVal.toFixed(_dmit.value)
|
|
emits('valueChange', props.startVal)
|
|
}
|
|
|
|
// 监听器
|
|
watch((): number => props.endVal, (newValue: number, oldValue_s: number): void => {
|
|
targetValue.value = `${oldValue_s}`
|
|
endValue.value = newValue
|
|
oldValue.value = oldValue_s
|
|
|
|
play()
|
|
})
|
|
|
|
// 监听动画开关
|
|
watch((): boolean => props.enableAnimation, (newVal: boolean): void => {
|
|
if (!newVal && isAnimating.value) {
|
|
stop()
|
|
}
|
|
})
|
|
|
|
// 生命周期
|
|
onMounted(() => {
|
|
oldValue.value = props.startVal
|
|
endValue.value = props.endVal
|
|
targetValue.value = props.startVal.toFixed(_dmit.value)
|
|
|
|
// 如果起始值和目标值不同且启用动画,则播放动画
|
|
if (props.startVal !== props.endVal && props.enableAnimation) {
|
|
play()
|
|
} else if (!props.enableAnimation) {
|
|
// 如果禁用动画,直接显示目标值
|
|
targetValue.value = props.endVal.toFixed(_dmit.value)
|
|
}
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
ani.value?.stop()
|
|
ani.value = null;
|
|
})
|
|
|
|
// 暴露方法给父组件
|
|
defineExpose({
|
|
/** 播放动画 **/
|
|
play: () => play(),
|
|
/** 暂停动画 **/
|
|
pause: () => pause(),
|
|
/** 恢复动画 **/
|
|
resume: () => resume(),
|
|
/** 停止动画 **/
|
|
stop: () => stop(),
|
|
/** 重置到起始值 **/
|
|
reset: () => reset(),
|
|
/** 是否正在动画中 **/
|
|
isAnimating: (): boolean => isAnimating.value,
|
|
/** 获取当前值 **/
|
|
getValue: (): number => parseFloat(targetValue.value),
|
|
/** 获取格式化后的值 **/
|
|
getFormattedValue: (): string => formattedValue.value
|
|
})
|
|
</script>
|
|
<template>
|
|
<view class="xRolling">
|
|
<!--
|
|
@slot 默认插槽
|
|
@prop {string} value - 当前的值
|
|
@prop {string} formattedValue - 格式化后的值
|
|
-->
|
|
<slot :value="targetValue" :formattedValue="formattedValue">
|
|
<text :style="{fontSize:_fontSize,color:_fontColor}">{{formattedValue}}</text>
|
|
</slot>
|
|
</view>
|
|
</template>
|
|
<style scoped>
|
|
.xRolling {
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: nowrap;
|
|
justify-content: center;
|
|
}
|
|
</style> |