CountTo.vue 4.8 KB

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