u-count-to.vue 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. <template>
  2. <view class="u-count-num" :style="{
  3. fontSize: fontSize + 'rpx',
  4. fontWeight: bold ? 'bold' : 'normal'
  5. }">{{ displayValue }}</view>
  6. </template>
  7. <script>
  8. /**
  9. * countTo 数字滚动
  10. * @description 该组件一般用于需要滚动数字到某一个值的场景,目标要求是一个递增的值。
  11. * @tutorial https://www.uviewui.com/components/countTo.html
  12. * @property {String Number} start-val 开始值
  13. * @property {String Number} end-val 结束值
  14. * @property {String Number} duration 滚动过程所需的时间,单位ms(默认2000)
  15. * @property {Boolean} autoplay 是否自动开始滚动(默认true)
  16. * @property {String Number} decimals 要显示的小数位数,见官网说明(默认0)
  17. * @property {Boolean} use-easing 滚动结束时,是否缓动结尾,见官网说明(默认true)
  18. * @property {String} separator 千位分隔符,见官网说明
  19. * @property {String} color 字体颜色(默认#303133)
  20. * @property {String Number} font-size 字体大小,单位rpx(默认50)
  21. * @property {Boolean} bold 字体是否加粗(默认false)
  22. * @event {Function} end 数值滚动到目标值时触发
  23. * @example <u-count-to ref="uCountTo" :end-val="endVal" :autoplay="autoplay"></u-count-to>
  24. */
  25. export default {
  26. name: 'u-count-to',
  27. props: {
  28. // 开始的数值,默认从0增长到某一个数
  29. startVal: {
  30. type: [Number, String],
  31. default: 0
  32. },
  33. // 要滚动的目标数值,必须
  34. endVal: {
  35. type: [Number, String],
  36. default: 0,
  37. required: true
  38. },
  39. // 滚动到目标数值的动画持续时间,单位为毫秒(ms)
  40. duration: {
  41. type: [Number, String],
  42. default: 2000
  43. },
  44. // 设置数值后是否自动开始滚动
  45. autoplay: {
  46. type: Boolean,
  47. default: true
  48. },
  49. // 要显示的小数位数
  50. decimals: {
  51. type: [Number, String],
  52. default: 0
  53. },
  54. // 是否在即将到达目标数值的时候,使用缓慢滚动的效果
  55. useEasing: {
  56. type: Boolean,
  57. default: true
  58. },
  59. // 十进制分割
  60. decimal: {
  61. type: [Number, String],
  62. default: '.'
  63. },
  64. // 字体颜色
  65. color: {
  66. type: String,
  67. default: '#303133'
  68. },
  69. // 字体大小
  70. fontSize: {
  71. type: [Number, String],
  72. default: 50
  73. },
  74. // 是否加粗字体
  75. bold: {
  76. type: Boolean,
  77. default: false
  78. },
  79. // 千位分隔符,类似金额的分割(¥23,321.05中的",")
  80. separator: {
  81. type: String,
  82. default: ''
  83. }
  84. },
  85. data() {
  86. return {
  87. localStartVal: this.startVal,
  88. displayValue: this.formatNumber(this.startVal),
  89. printVal: null,
  90. paused: false, // 是否暂停
  91. localDuration: Number(this.duration),
  92. startTime: null, // 开始的时间
  93. timestamp: null, // 时间戳
  94. remaining: null, // 停留的时间
  95. rAF: null,
  96. lastTime: 0 // 上一次的时间
  97. };
  98. },
  99. computed: {
  100. countDown() {
  101. return this.startVal > this.endVal;
  102. }
  103. },
  104. watch: {
  105. startVal() {
  106. this.autoplay && this.start();
  107. },
  108. endVal() {
  109. this.autoplay && this.start();
  110. }
  111. },
  112. mounted() {
  113. this.autoplay && this.start();
  114. },
  115. methods: {
  116. easingFn(t, b, c, d) {
  117. return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;
  118. },
  119. requestAnimationFrame(callback) {
  120. const currTime = new Date().getTime();
  121. // 为了使setTimteout的尽可能的接近每秒60帧的效果
  122. const timeToCall = Math.max(0, 16 - (currTime - this.lastTime));
  123. const id = setTimeout(() => {
  124. callback(currTime + timeToCall);
  125. }, timeToCall);
  126. this.lastTime = currTime + timeToCall;
  127. return id;
  128. },
  129. cancelAnimationFrame(id) {
  130. clearTimeout(id);
  131. },
  132. // 开始滚动数字
  133. start() {
  134. this.localStartVal = this.startVal;
  135. this.startTime = null;
  136. this.localDuration = this.duration;
  137. this.paused = false;
  138. this.rAF = this.requestAnimationFrame(this.count);
  139. },
  140. // 暂定状态,重新再开始滚动;或者滚动状态下,暂停
  141. reStart() {
  142. if (this.paused) {
  143. this.resume();
  144. this.paused = false;
  145. } else {
  146. this.stop();
  147. this.paused = true;
  148. }
  149. },
  150. // 暂停
  151. stop() {
  152. this.cancelAnimationFrame(this.rAF);
  153. },
  154. // 重新开始(暂停的情况下)
  155. resume() {
  156. this.startTime = null;
  157. this.localDuration = this.remaining;
  158. this.localStartVal = this.printVal;
  159. this.requestAnimationFrame(this.count);
  160. },
  161. // 重置
  162. reset() {
  163. this.startTime = null;
  164. this.cancelAnimationFrame(this.rAF);
  165. this.displayValue = this.formatNumber(this.startVal);
  166. },
  167. count(timestamp) {
  168. if (!this.startTime) this.startTime = timestamp;
  169. this.timestamp = timestamp;
  170. const progress = timestamp - this.startTime;
  171. this.remaining = this.localDuration - progress;
  172. if (this.useEasing) {
  173. if (this.countDown) {
  174. this.printVal = this.localStartVal - this.easingFn(progress, 0, this.localStartVal - this.endVal, this.localDuration);
  175. } else {
  176. this.printVal = this.easingFn(progress, this.localStartVal, this.endVal - this.localStartVal, this.localDuration);
  177. }
  178. } else {
  179. if (this.countDown) {
  180. this.printVal = this.localStartVal - (this.localStartVal - this.endVal) * (progress / this.localDuration);
  181. } else {
  182. this.printVal = this.localStartVal + (this.endVal - this.localStartVal) * (progress / this.localDuration);
  183. }
  184. }
  185. if (this.countDown) {
  186. this.printVal = this.printVal < this.endVal ? this.endVal : this.printVal;
  187. } else {
  188. this.printVal = this.printVal > this.endVal ? this.endVal : this.printVal;
  189. }
  190. this.displayValue = this.formatNumber(this.printVal);
  191. if (progress < this.localDuration) {
  192. this.rAF = this.requestAnimationFrame(this.count);
  193. } else {
  194. this.$emit('end');
  195. }
  196. },
  197. // 判断是否数字
  198. isNumber(val) {
  199. return !isNaN(parseFloat(val));
  200. },
  201. formatNumber(num) {
  202. // 将num转为Number类型,因为其值可能为字符串数值,调用toFixed会报错
  203. num = Number(num);
  204. num = num.toFixed(Number(this.decimals));
  205. num += '';
  206. const x = num.split('.');
  207. let x1 = x[0];
  208. const x2 = x.length > 1 ? this.decimal + x[1] : '';
  209. const rgx = /(\d+)(\d{3})/;
  210. if (this.separator && !this.isNumber(this.separator)) {
  211. while (rgx.test(x1)) {
  212. x1 = x1.replace(rgx, '$1' + this.separator + '$2');
  213. }
  214. }
  215. return x1 + x2;
  216. },
  217. destroyed() {
  218. this.cancelAnimationFrame(this.rAF);
  219. }
  220. }
  221. };
  222. </script>
  223. <style lang="scss" scoped>
  224. @import "../../libs/css/style.components.scss";
  225. .u-count-num {
  226. display: inline-block;
  227. text-align: center;
  228. }
  229. </style>