Files
hospital-rescue-app-v2/uni_modules/tmx-ui/components/x-rolling-number/x-rolling-number.uvue
T
2026-09-24 16:25:22 +08:00

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>