Vue3+TS实现数字滚动效果CountTo组件

Vue
322
0
0
2023-06-18
标签   Vue组件
目录
  • 前言
  • 思考
  • 实践
  • 定义参数
  • 定义一个开始函数
  • 核心方法
  • 配置项
  • 功能
  • 组件

前言

最近开发有个需求需要酷炫的文字滚动效果,发现vue2版本的CountTo组件不适用与Vue3,没有轮子咋办,那咱造一个呗。其实大多数版本更替导致公共组件不可用,最简单的做法就是在原版本的基础上进行修改调整,总体来讲花费的时间成本以及精力成本最低。

思考

先看下效果,明确需求,然后开始搬砖。

明确基础功能

  • 有开始值、结束值以及动画持续时间
  • 默认分隔符、自动播放

扩展功能

  • 自动播放可配置
  • 分隔符可自定义
  • 前、后缀
  • 动画配置项

实践

定义参数

const props = {
  start: {
    type: Number,
    required: false,
    default:
  },
  end: {
    type: Number,
    required: false,
    default:
  },
  duration: {
    type: Number,
    required: false,
    default:
  },
  autoPlay: {
    type: Boolean,
    required: false,
    default: true
  },
  decimals: {
    type: Number,
    required: false,
    default:,
    validator(value) {
      return value >=
    }
  },
  decimal: {
    type: String,
    required: false,
    default: '.'
  },
  separator: {
    type: String,
    required: false,
    default: ','
  },
  prefix: {
    type: String,
    required: false,
    default: ''
  },
  suffix: {
    type: String,
    required: false,
    default: ''
  },
  useEasing: {
    type: Boolean,
    required: false,
    default: true
  },
  easingFn: {
    type: Function,
    default(t, b, c, d) {
      return c * (-Math.pow(, -10 * t / d) + 1) * 1024 / 1023 + b
    }
  }
}

定义一个开始函数

// 定义一个计算属性,当开始数字大于结束数字时返回true
const stopCount = computed(() => {
  return props.start > props.end
})
const startCount = () => {
  state.localStart = props.start
  state.startTime = null
  state.localDuration = props.duration
  state.paused = false
  state.rAF = requestAnimationFrame(count)
}
watch(() => props.start, () => {
  if (props.autoPlay) {
    startCount()
  }
})
  watch(() => props.end, () => {
  if (props.autoPlay) {
    startCount()
  }
})
// dom挂在完成后执行一些操作
onMounted(() => {
  if (props.autoPlay) {
    startCount()
  }
  emit('onMountedcallback')
})
 // 组件销毁时取消动画
onUnmounted(() => {
  cancelAnimationFrame(state.rAF)
})

核心方法

const count = (timestamp) => {
  if (!state.startTime) state.startTime = timestamp
  state.timestamp = timestamp
  const progress = timestamp - state.startTime
  state.remaining = state.localDuration - progress
  // 是否使用速度变化曲线
  if (props.useEasing) {
    if (stopCount.value) {
      state.printVal = state.localStart - props.easingFn(progress,, state.localStart - props.end, state.localDuration)
    } else {
      state.printVal = props.easingFn(progress, state.localStart, props.end - state.localStart, state.localDuration)
    }
  } else {
    if (stopCount.value) {
      state.printVal = state.localStart - ((state.localStart - props.end) * (progress / state.localDuration))
    } else {
      state.printVal = state.localStart + (props.end - state.localStart) * (progress / state.localDuration)
    }
  }
  if (stopCount.value) {
    state.printVal = state.printVal < props.end ? props.end : state.printVal
  } else {
    state.printVal = state.printVal > props.end ? props.end : state.printVal
  }
    state.displayValue = formatNumber(state.printVal)
  if (progress < state.localDuration) {
    state.rAF = requestAnimationFrame(count)
  } else {
    emit('callback')
  }
}

配置项

属性

描述

类型

默认值

startVal

开始值

Number

0

endVal

结束值

Number

0

duration

持续时间

Number

0

autoplay

自动播放

Boolean

true

decimals

要显示的小数位数

Number

0

decimal

十进制分割

String

,

separator

分隔符

String

,

prefix

前缀

String

''

suffix

后缀

String

''

useEasing

使用缓和功能

Boolean

true

easingFn

缓和回调

Function

-

注:当autoplay:true时,它将在startVal或endVal更改时自动启动

功能

函数名

描述

mountedCallback

挂载以后返回回调

start

开始计数

pause

暂停计数

reset

重置countTo

组件

组件同步在git组件库了https://github.com/kinoaa/kinoaa-components/tree/main/countTo

import {defineComponent, reactive, computed, onMounted, watch, onUnmounted
} from 'vue'
const props = {start: {
type: Number,
required: false,
default:
  },end: {
type: Number,
required: false,
default:
  },duration: {
type: Number,
required: false,
default:
  },autoPlay: {
type: Boolean,
required: false,
default: true
  },decimals: {
type: Number,
required: false,
default:,
validator(value) {
  return value >=
}
  },decimal: {
type: String,
required: false,
default: '.'
  },separator: {
type: String,
required: false,
default: ','
  },prefix: {
type: String,
required: false,
default: ''
  },suffix: {
type: String,
required: false,
default: ''
  },useEasing: {
type: Boolean,
required: false,
default: true
  },easingFn: {
type: Function,
default(t, b, c, d) {
  return c * (-Math.pow(, -10 * t / d) + 1) * 1024 / 1023 + b
}
  }
}
export default defineComponent({name: 'CountTo',
  props: props,emits: ['onMountedcallback', 'callback'],
  setup(props, {emit}) {  const isNumber = (val) => {
  return !isNaN(parseFloat(val))
}
// 格式化数据,返回想要展示的数据格式
const formatNumber = (val) => {
  val = val.toFixed(props.start)
  val += ''
  const x = val.split('.')
  let x = x[0]
  const x = x.length > 1 ? props.decimal + x[1] : ''
  const rgx = /(\d+)(\d{})/
  if (props.separator && !isNumber(props.separator)) {
    while (rgx.test(x)) {
      x = x1.replace(rgx, '$1' + props.separator + '$2')
    }
  }
  return props.prefix + x + x2 + props.suffix
}
const state = reactive<{
  localStart: number
  displayValue: number|string
  printVal: any
  paused: boolean
  localDuration: any
  startTime: any
  timestamp: any
  remaining: any
  rAF: any
}>({
  localStart: props.start,
  displayValue: formatNumber(props.start),
  printVal: null,
  paused: false,
  localDuration: props.duration,
  startTime: null,
  timestamp: null,
  remaining: null,
  rAF: null
})
// 定义一个计算属性,当开始数字大于结束数字时返回true
const stopCount = computed(() => {
  return props.start > props.end
})
const startCount = () => {
  state.localStart = props.start
  state.startTime = null
  state.localDuration = props.duration
  state.paused = false
  state.rAF = requestAnimationFrame(count)
}
  watch(() => props.start, () => {
  if (props.autoPlay) {
    startCount()
  }
})
  watch(() => props.end, () => {
  if (props.autoPlay) {
    startCount()
  }
})
// dom挂在完成后执行一些操作
onMounted(() => {
  if (props.autoPlay) {
    startCount()
  }
  emit('onMountedcallback')
})
const count = (timestamp) => {
  if (!state.startTime) state.startTime = timestamp
  state.timestamp = timestamp
  const progress = timestamp - state.startTime
  state.remaining = state.localDuration - progress
  // 是否使用速度变化曲线
  if (props.useEasing) {
    if (stopCount.value) {
      state.printVal = state.localStart - props.easingFn(progress,, state.localStart - props.end, state.localDuration)
    } else {
      state.printVal = props.easingFn(progress, state.localStart, props.end - state.localStart, state.localDuration)
    }
  } else {
    if (stopCount.value) {
      state.printVal = state.localStart - ((state.localStart - props.end) * (progress / state.localDuration))
    } else {
      state.printVal = state.localStart + (props.end - state.localStart) * (progress / state.localDuration)
    }
  }
  if (stopCount.value) {
    state.printVal = state.printVal < props.end ? props.end : state.printVal
  } else {
    state.printVal = state.printVal > props.end ? props.end : state.printVal
  }
    state.displayValue = formatNumber(state.printVal)
  if (progress < state.localDuration) {
    state.rAF = requestAnimationFrame(count)
  } else {
    emit('callback')
  }
}
// 组件销毁时取消动画
onUnmounted(() => {
  cancelAnimationFrame(state.rAF)
})
return () => (
  state.displayValue
)
  }
})