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