代码
<!-- AniNum组件:数字滚动动画组件 -->
<template>
<view class="ani-num-container">
<view class="ani-num-item" :style="{ height: `${aniItemHeight}rpx` }" v-for="(item, index) in displayList" :key="index">
<view class="text-num-box" :style="{ ...computedAniNumItemStyle, ...getAniNumItemStyle(index) }">
<view class="text-num" :style="textNumStyle" v-if="item === '.'">{{ item }}</view>
<view class="text-num" :style="textNumStyle" v-else v-for="(numItem, numIndex) in 10" :key="numIndex">{{ numIndex }}</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch, onUnmounted } from 'vue'
// Tip: 自定义的转换数字类型方法,可自行替换
import { toNum } from 'utils/index'
const props = defineProps({
initValue: {
type: [Number, String],
default: 0
},
value: {
type: [Number, String],
default: 0
},
fontSize: {
type: Number,
default: 32
},
color: {
type: String,
default: '#fff'
},
bold: {
type: Boolean,
default: false
},
duration: {
type: Number,
default: 1500
}
})
let timeout = null
let isInit = false
const prevNum = ref(0)
const curNum = ref(0)
const displayList = computed(() => {
const { endList } = formatNumStrList(prevNum.value, curNum.value)
console.log('%c [ displayList ]-32', 'font-size:13px; background:pink; color:#bf2c9f;', endList)
return endList
})
const computedAniNumItemStyle = computed(() => {
return {
transition: `transform ${props.duration}ms ease-in-out, opacity 0.5s linear`
}
})
const aniItemHeight = computed(() => {
return props.fontSize + 10
})
const textNumStyle = computed(() => {
return {
height: `${aniItemHeight.value}rpx`,
lineHeight: `${aniItemHeight.value}rpx`,
fontSize: `${props.fontSize}rpx`,
color: `${props.color}`,
fontWeight: `${props.bold ? 'bold' : 'normal'}`
}
})
onUnmounted(() => {
clearTimeout(timeout)
timeout = null
})
/** 更新数值
* @param {*} newVal 新的数值
* @param {*} delay 动画持续时间
*/
const updateValue = (newVal = props.value, delay = 0) => {
const commonHandle = newVal => {
prevNum.value = curNum.value
curNum.value = newVal
}
newVal = toNum(newVal)
console.log('%c [ updateValue ]-93', 'font-size:13px; background:pink; color:#bf2c9f;', newVal)
clearTimeout(timeout)
// 动画持续时间为0时,直接更新数值
if (delay === 0) {
commonHandle(newVal)
}
// 否则,延迟更新数值
else {
timeout = setTimeout(() => commonHandle(newVal), delay)
}
}
const getAniNumItemStyle = index => {
const curItem = displayList.value[index]
const isDot = curItem.type === 'dot' // 是否是小数点
const transY = isDot ? 0 : toNum(curItem.value) * 100
const isShow = curItem.type !== 'repairZero' // 是否显示
return {
transform: `translateY(-${transY}%)`,
opacity: isShow ? 1 : 0,
width: isShow ? 'auto' : 0
}
}
// 将数字字符串数字转化为相同的排列顺序,某位数不足的补0
const formatNumStrList = (start, end) => {
let { intStrList: startIntStrList, floatStrList: startFloatStrList } = getIntAndFloatOfNum(start)
let { intStrList: endIntStrList, floatStrList: endFloatStrList } = getIntAndFloatOfNum(end)
const intStrLenDiff = Math.abs(startIntStrList.length - endIntStrList.length)
if (intStrLenDiff > 0) {
const repairZeroList = []
for (let i = 0; i < intStrLenDiff; i++) {
repairZeroList.push({
value: '0',
type: 'repairZero'
})
}
if (startIntStrList.length > endIntStrList.length) {
endIntStrList.unshift(...repairZeroList)
} else {
startIntStrList.unshift(...repairZeroList)
}
}
const floatStrLenDiff = Math.abs(startFloatStrList.length - endFloatStrList.length)
if (floatStrLenDiff > 0) {
const repairZeroList = []
for (let i = 0; i < floatStrLenDiff; i++) {
repairZeroList.push({
value: '0',
type: 'repairZero'
})
}
if (startFloatStrList.length > endFloatStrList.length) {
endFloatStrList.push(...repairZeroList)
} else {
startFloatStrList.push(...repairZeroList)
}
}
const startList = [
...startIntStrList,
...(startFloatStrList.length
? [
{
value: '.',
type: 'dot'
},
...startFloatStrList
]
: [])
]
const endList = [
...endIntStrList,
...(endFloatStrList.length
? [
{
value: '.',
type: 'dot'
},
...endFloatStrList
]
: [])
]
return {
startList,
endList
}
}
const getIntAndFloatOfNum = num => {
const numStr = toNum(num).toString()
const int = numStr.split('.')[0]
const float = numStr.split('.')[1] || []
const intStrList = int.split('').map(item => ({
value: item,
type: 'int'
}))
const floatStrList = (float.length ? float.split('') : []).map(item => ({
value: item,
type: 'float'
}))
return {
intStrList,
floatStrList
}
}
watch(
() => props.initValue,
(newVal, oldVal) => {
if (isInit) return
console.log('%c [ props.initValue ]-193', 'font-size:13px; background:pink; color:#bf2c9f;', newVal, oldVal, props.initValue)
updateValue(newVal)
// 只有在initValue现在的值和之前的值都存在并且不相等时,才认为是初始化
isInit = newVal && oldVal && newVal !== oldVal
},
{
immediate: true
}
)
watch(
() => props.value,
newVal => {
if (!isInit) return
console.log('%c [ watch--value ]-225', 'font-size:13px; background:pink; color:#bf2c9f;', newVal)
updateValue(newVal, props.duration)
}
)
defineExpose({
updateValue
})
</script>
<style lang="scss" scoped>
.ani-num-container {
display: inline-flex;
align-items: flex-end;
.ani-num-item {
display: flex;
flex-direction: column;
overflow: hidden;
.text-num-box {
height: 100%;
display: flex;
flex-direction: column;
}
.text-num {
color: #fff;
text-align: center;
}
}
}
</style>
使用示例
<template>
<view>
<ani-num :init-value="initValue" :value="value" :duration="1000"></ani-num>
</view>
</template>
<script setup>
import { ref } from 'vue'
import AniNum from './AniNum.vue'
const initValue = ref(0)
const value = ref(100)
</script>
效果
