houzhongjian
2024-08-08 820397e43a0b64d35c6d31d2a55475061438593b
提交 | 用户 | 时间
820397 1 <script lang="ts" setup>
H 2 import { PropType } from 'vue'
3 import { isNumber } from '@/utils/is'
4 import { propTypes } from '@/utils/propTypes'
5 import { useDesign } from '@/hooks/web/useDesign'
6
7 defineOptions({ name: 'CountTo' })
8
9 const { getPrefixCls } = useDesign()
10
11 const prefixCls = getPrefixCls('count-to')
12
13 const props = defineProps({
14   startVal: propTypes.number.def(0), // 开始播放值
15   endVal: propTypes.number.def(2021), // 最终值
16   duration: propTypes.number.def(3000), // 动画时长
17   autoplay: propTypes.bool.def(true), // 是否自动播放动画, 默认播放
18   decimals: propTypes.number.validate((value: number) => value >= 0).def(0), // 显示的小数位数, 默认不显示小数
19   decimal: propTypes.string.def('.'), // 小数分隔符号, 默认为点
20   separator: propTypes.string.def(','), // 数字每三位的分隔符, 默认为逗号
21   prefix: propTypes.string.def(''), // 前缀, 数值前面显示的内容
22   suffix: propTypes.string.def(''), // 后缀, 数值后面显示的内容
23   useEasing: propTypes.bool.def(true), // 是否使用缓动效果, 默认启用
24   easingFn: {
25     type: Function as PropType<(t: number, b: number, c: number, d: number) => number>,
26     default(t: number, b: number, c: number, d: number) {
27       return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b
28     } // 缓动函数
29   }
30 })
31
32 const emit = defineEmits(['mounted', 'callback'])
33
34 const formatNumber = (num: number | string) => {
35   const { decimals, decimal, separator, suffix, prefix } = props
36   num = Number(num).toFixed(decimals)
37   num += ''
38   const x = num.split('.')
39   let x1 = x[0]
40   const x2 = x.length > 1 ? decimal + x[1] : ''
41   const rgx = /(\d+)(\d{3})/
42   if (separator && !isNumber(separator)) {
43     while (rgx.test(x1)) {
44       x1 = x1.replace(rgx, '$1' + separator + '$2')
45     }
46   }
47   return prefix + x1 + x2 + suffix
48 }
49
50 const state = reactive<{
51   localStartVal: number
52   printVal: number | null
53   displayValue: string
54   paused: boolean
55   localDuration: number | null
56   startTime: number | null
57   timestamp: number | null
58   rAF: any
59   remaining: number | null
60 }>({
61   localStartVal: props.startVal,
62   displayValue: formatNumber(props.startVal),
63   printVal: null,
64   paused: false,
65   localDuration: props.duration,
66   startTime: null,
67   timestamp: null,
68   remaining: null,
69   rAF: null
70 })
71
72 const displayValue = toRef(state, 'displayValue')
73
74 onMounted(() => {
75   if (props.autoplay) {
76     start()
77   }
78   emit('mounted')
79 })
80
81 const getCountDown = computed(() => {
82   return props.startVal > props.endVal
83 })
84
85 watch([() => props.startVal, () => props.endVal], () => {
86   if (props.autoplay) {
87     start()
88   }
89 })
90
91 const start = () => {
92   const { startVal, duration } = props
93   state.localStartVal = startVal
94   state.startTime = null
95   state.localDuration = duration
96   state.paused = false
97   state.rAF = requestAnimationFrame(count)
98 }
99
100 const pauseResume = () => {
101   if (state.paused) {
102     resume()
103     state.paused = false
104   } else {
105     pause()
106     state.paused = true
107   }
108 }
109
110 const pause = () => {
111   cancelAnimationFrame(state.rAF)
112 }
113
114 const resume = () => {
115   state.startTime = null
116   state.localDuration = +(state.remaining as number)
117   state.localStartVal = +(state.printVal as number)
118   requestAnimationFrame(count)
119 }
120
121 const reset = () => {
122   state.startTime = null
123   cancelAnimationFrame(state.rAF)
124   state.displayValue = formatNumber(props.startVal)
125 }
126
127 const count = (timestamp: number) => {
128   const { useEasing, easingFn, endVal } = props
129   if (!state.startTime) state.startTime = timestamp
130   state.timestamp = timestamp
131   const progress = timestamp - state.startTime
132   state.remaining = (state.localDuration as number) - progress
133   if (useEasing) {
134     if (unref(getCountDown)) {
135       state.printVal =
136         state.localStartVal -
137         easingFn(progress, 0, state.localStartVal - endVal, state.localDuration as number)
138     } else {
139       state.printVal = easingFn(
140         progress,
141         state.localStartVal,
142         endVal - state.localStartVal,
143         state.localDuration as number
144       )
145     }
146   } else {
147     if (unref(getCountDown)) {
148       state.printVal =
149         state.localStartVal -
150         (state.localStartVal - endVal) * (progress / (state.localDuration as number))
151     } else {
152       state.printVal =
153         state.localStartVal +
154         (endVal - state.localStartVal) * (progress / (state.localDuration as number))
155     }
156   }
157   if (unref(getCountDown)) {
158     state.printVal = state.printVal < endVal ? endVal : state.printVal
159   } else {
160     state.printVal = state.printVal > endVal ? endVal : state.printVal
161   }
162   state.displayValue = formatNumber(state.printVal!)
163   if (progress < (state.localDuration as number)) {
164     state.rAF = requestAnimationFrame(count)
165   } else {
166     emit('callback')
167   }
168 }
169
170 defineExpose({
171   pauseResume,
172   reset,
173   start,
174   pause
175 })
176 </script>
177
178 <template>
179   <span :class="prefixCls">
180     {{ displayValue }}
181   </span>
182 </template>