tn-count-to.vue 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. <template>
  2. <view
  3. class="tn-count-num-class tn-count-num"
  4. :class="[fontColorClass]"
  5. :style="{
  6. fontSize: fontSizeStyle || '50rpx',
  7. fontWeight: bold ? 'bold' : 'normal',
  8. color: fontColorStyle || '#080808'
  9. }"
  10. >
  11. {{ displayValue }}
  12. </view>
  13. </template>
  14. <script>
  15. import componentsColorMixin from '../../libs/mixin/components_color.js'
  16. export default {
  17. name: 'tn-count-to',
  18. mixins: [componentsColorMixin],
  19. props: {
  20. // 开始的数值,默认为0
  21. startVal: {
  22. type: Number,
  23. default: 0
  24. },
  25. // 结束目标数值
  26. endVal: {
  27. type: Number,
  28. default: 0,
  29. required: true
  30. },
  31. // 是否自动开始
  32. autoplay: {
  33. type: Boolean,
  34. default: true
  35. },
  36. // 滚动到目标值的持续时间,单位为毫秒
  37. duration: {
  38. type: Number,
  39. default: 2000
  40. },
  41. // 是否在即将结束的时候使用缓慢滚动的效果
  42. useEasing: {
  43. type: Boolean,
  44. default: true
  45. },
  46. // 显示的小数位数
  47. decimals: {
  48. type: Number,
  49. default: 0
  50. },
  51. // 十进制的分割符
  52. decimalSeparator: {
  53. type: String,
  54. default: '.'
  55. },
  56. // 千分位的分隔符
  57. // 类似金额的分割(¥23,321.05中的",")
  58. thousandthsSeparator: {
  59. type: String,
  60. default: ''
  61. },
  62. // 是否显示加粗字体
  63. bold: {
  64. type: Boolean,
  65. default: false
  66. }
  67. },
  68. computed: {
  69. countDown() {
  70. return this.startVal > this.endVal
  71. }
  72. },
  73. data() {
  74. return {
  75. localStartVal: this.startVal,
  76. localDuration: this.duration,
  77. // 显示的数值
  78. displayValue: this.formatNumber(this.startVal),
  79. // 打印的数值
  80. printValue: null,
  81. // 是否暂停
  82. paused: false,
  83. // 开始时间戳
  84. startTime: null,
  85. // 停留时间戳
  86. remainingTime: null,
  87. // 当前时间戳
  88. timestamp: null,
  89. // 上一次的时间戳
  90. lastTime: 0,
  91. rAF: null
  92. }
  93. },
  94. watch: {
  95. startVal() {
  96. this.autoplay && this.start()
  97. },
  98. endVal() {
  99. this.autoplay && this.start()
  100. }
  101. },
  102. mounted() {
  103. this.autoplay && this.start()
  104. },
  105. methods: {
  106. // 开始滚动
  107. start() {
  108. this.localStartVal = this.startVal
  109. this.startTime = null
  110. this.localDuration = this.duration
  111. this.paused = false
  112. this.rAF = this.requestAnimationFrame(this.count)
  113. },
  114. // 重新开始
  115. reStart() {
  116. if (this.paused) {
  117. this.resume()
  118. this.paused = false
  119. } else {
  120. this.stop()
  121. this.paused = true
  122. }
  123. },
  124. // 停止
  125. stop() {
  126. this.cancelAnimationFrame(this.rAF)
  127. },
  128. // 恢复
  129. resume() {
  130. this.startTime = null
  131. this.localDuration = this.remainingTime
  132. this.localStartVal = this.printValue
  133. this.requestAnimationFrame(this.count)
  134. },
  135. // 重置
  136. reset() {
  137. this.startTime = null
  138. this.cnacelAnimationFrame(this.rAF)
  139. this.displayValue = this.formatNumber(this.startVal)
  140. },
  141. // 销毁组件
  142. destroyed() {
  143. this.cancelAnimationFrame(this.rAF)
  144. },
  145. // 累加时间
  146. count(timestamp) {
  147. if (!this.startTime) this.startTime = timestamp
  148. this.timestamp = timestamp
  149. const progress = timestamp - this.startTime
  150. this.remainingTime = this.localDuration - progress
  151. if (this.useEasing) {
  152. if (this.countDown) {
  153. this.printValue = this.localStartVal - this.easingFn(progress, 0, this.localStartVal - this.endVal, this.localDuration)
  154. } {
  155. this.printValue = this.easingFn(progress, this.localStartVal, this.endVal - this.localStartVal, this.localDuration)
  156. }
  157. } else {
  158. if (this.countDown) {
  159. this.printValue = this.localStartVal - (this.localStartVal - this.endVal) * (progress / this.localDuration)
  160. } else {
  161. this.printValue = this.localStartVal + (this.endVal - this.localStartVal) * (progress / this.localDuration)
  162. }
  163. }
  164. if (this.countDown) {
  165. this.printValue = this.printValue < this.endVal ? this.endVal : this.printValue
  166. } else {
  167. this.printValue = this.printValue > this.endVal ? this.endVal : this.printValue
  168. }
  169. this.displayValue = this.formatNumber(this.printValue)
  170. if (progress < this.localDuration) {
  171. this.rAF = this.requestAnimationFrame(this.count)
  172. } else {
  173. this.$emit('end')
  174. }
  175. },
  176. // 缓动时间计算
  177. easingFn(t, b, c, d) {
  178. return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b
  179. },
  180. // 请求帧动画
  181. requestAnimationFrame(cb) {
  182. const currentTime = new Date().getTime()
  183. // 为了使setTimteout的尽可能的接近每秒60帧的效果
  184. const timeToCall = Math.max(0, 16 - (currentTime - this.lastTime))
  185. const timerId = setTimeout(() => {
  186. cb && cb(currentTime + timeToCall)
  187. }, timeToCall)
  188. this.lastTime = currentTime + timeToCall
  189. return timerId
  190. },
  191. // 清除帧动画
  192. clearAnimationFrame(timerId) {
  193. clearTimeout(timerId)
  194. },
  195. // 格式化数值
  196. formatNumber(number) {
  197. const reg = /(\d+)(\d{3})/
  198. number = Number(number)
  199. number = number.toFixed(Number(this.decimals))
  200. number += ''
  201. const numberArray = number.split('.')
  202. let num1 = numberArray[0]
  203. const num2 = numberArray.length > 1 ? this.decimalSeparator + numberArray[1] : ''
  204. if (this.thousandthsSeparator && !this.isNumber(this.thousandthsSeparator)) {
  205. while(reg.test(num1)) {
  206. num1 = num1.replace(reg, '$1' + this.thousandthsSeparator + '$2')
  207. }
  208. }
  209. return num1 + num2
  210. },
  211. // 判断是否为数字
  212. isNumber(val) {
  213. return !isNaN(parseFloat(val))
  214. }
  215. }
  216. }
  217. </script>
  218. <style lang="scss" scoped>
  219. .tn-count-num {
  220. /* #ifndef APP-NVUE */
  221. display: inline-flex;
  222. /* #endif */
  223. text-align: center;
  224. line-height: 1;
  225. }
  226. </style>