u-count-to.vue 6.8 KB

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