guangchao.xu | 070005a | 2020-12-07 09:56:40 +0800 | [diff] [blame^] | 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 | {{ 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 | */ |
| 32 | export 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> |