vue3 + TS 实现vue-count-to数字滚动插件

641 阅读2分钟

之前vue2使用vue-count-to很方便,直接安装使用。在vue3里面不支持vue-count-to插件,无法使用vue-count-to实现数字动效,数字自动分割,vue-count-to主要针对vue2使用,vue3按照会报错: TypeError: Cannot read properties of undefined (reading '_c') 找了一遍没发现有TS版的,于是动手在原本基础上简单改了一下,主要是在CountTo组件和animationFrame上改动

CountTo改动如下:

<script lang="ts" name="index" setup>
import { onMounted, onUnmounted, reactive, watch, computed } from "vue"
import { requestAnimationFrame, cancelAnimationFrame } from './animationFrame'

// 组件参数
const props = withDefaults(defineProps<{
  startVal?: number // 开始数字 默认 0
  endVal?: number // 结束数字 默认 2022
  duration?: number // 动画时间 默认 3000毫秒
  autoPlay?: boolean // 自动播放 默认 true
  decimals?: number // 保留小数位 默认不保留
  decimal?: string // 小数点
  separator?: string // 分隔符
  prefix?: string // 前缀
  suffix?: string // 后缀
  useEasing?: boolean // 使用缓和动画
  easingFn?: (t: number, b: number, c: number, d: number) => any // 缓和动画函数 
}>(), {
  startVal: 0,
  endVal: 2022,
  duration: 3000,
  autoPlay: true,
  decimals: 0,
  decimal: '.',
  separator: ',',
  prefix: '',
  suffix: '',
  useEasing: true,
  easingFn: (t: number, b: number, c: number, d: number) => (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b
})

const isNumber = (val: string) => !isNaN(parseFloat(val))

// 格式化数据,返回想要展示的数据格式
const formatNumber = (num: number) => {
  const params = `${num.toFixed(props.decimals)}`
  const x = params.split('.')
  let x1 = x[0]
  const x2 = x.length > 1 ? `${props.decimal}${x[1]}` : ''
  const rgx = /(\d+)(\d{3})/

  if (props.separator && !isNumber(props.separator)) {
    while (rgx.test(x1)) {
      x1 = x1.replace(rgx, '$1' + props.separator + '$2')
    }
  }

  return props.prefix + x1 + x2 + props.suffix
}

const state = reactive({
  localStart: props.startVal,
  displayValue: formatNumber(props.startVal),
  printVal: 0,
  paused: false,
  localDuration: props.duration,
  startTime: 0,
  timestamp: 0,
  remaining: 0,
  rAF: null,
})

// 定义一个计算属性,当开始数字大于结束数字时返回true
const stopCount = computed((): boolean => props.startVal > props.endVal)

// 定义父组件的自定义事件,子组件以触发父组件的自定义事件
const emits = defineEmits(['onMountedcallback', 'callback'])


const start = () => {
  state.localStart = props.startVal
  state.startTime = 0
  state.localDuration = props.duration
  state.paused = false
  state.rAF = requestAnimationFrame(count)
}

// 恢复计数
const resume = () => {
  state.startTime = 0
  state.localDuration = +state.remaining
  state.localStart = +state.printVal
  requestAnimationFrame(count)
}

// 暂停计数
const pause = () => {
  cancelAnimationFrame(state.rAF)
}

// 暂停重新计数
const pauseResume = () => {
  if (state.paused) {
    resume()
    state.paused = false
  } else {
    pause()
    state.paused = true
  }
}

// 重置
const reset = () => {
  state.startTime = 0
  cancelAnimationFrame(state.rAF)
  state.displayValue = formatNumber(props.startVal)
}

const count = (timestamp: number) => {
  if (!state.startTime) state.startTime = timestamp
  state.timestamp = timestamp
  const progress: number = timestamp - state.startTime
  state.remaining = state.localDuration - progress
  // 是否使用速度变化曲线
  if (props.useEasing) {
    if (stopCount.value) {
      state.printVal =
        state.localStart -
        props.easingFn(
          progress,
          0,
          state.localStart - props.endVal,
          state.localDuration
        );
    } else {
      state.printVal = props.easingFn(
        progress,
        state.localStart,
        props.endVal - state.localStart,
        state.localDuration
      );
    }
  } else {
    if (stopCount.value) {
      state.printVal =
        state.localStart -
        (state.localStart - props.endVal) * (progress / state.localDuration);
    } else {
      state.printVal =
        state.localStart +
        (props.endVal - state.localStart) * (progress / state.localDuration);
    }
  }
  if (stopCount.value) {
    state.printVal = state.printVal < props.endVal ? props.endVal : state.printVal;
  } else {
    state.printVal = state.printVal > props.endVal ? props.endVal : state.printVal;
  }

  state.displayValue = formatNumber(state.printVal);
  if (progress < state.localDuration) {
    state.rAF = requestAnimationFrame(count);
  } else {
    emits("callback");
  }
}

watch(
  () => props.startVal,
  () => {
    if (props.autoPlay) start()
  }
)

watch(
  () => props.endVal,
  () => {
    if (props.autoPlay) start()
  }
)

onMounted(() => {
  if(props.autoPlay) start()
  emits("onMountedcallback")
})

// 组件销毁时取消动画
onUnmounted(() => {
  cancelAnimationFrame(state.rAF)
})
</script>

<template>
  {{ state.displayValue }}
</template>

animationFrame改动如下:

let lastTime: number = 0
const prefixes: string[] = 'webkit moz ms o'.split(' ') // 各浏览器前缀

let requestAnimationFrame: any
let cancelAnimationFrame: any

// 判断是否是服务器环境
const isServer: boolean = typeof window === 'undefined'
if (isServer) {
  requestAnimationFrame = function () {
    return
  }
  cancelAnimationFrame = function () {
    return
  }
} else {
  requestAnimationFrame = window.requestAnimationFrame
  cancelAnimationFrame = window.cancelAnimationFrame
  let prefix: string
  // 通过遍历各浏览器前缀,来得到requestAnimationFrame和cancelAnimationFrame在当前浏览器的实现形式
  for (let i = 0; i < prefixes.length; i++) {
    if (requestAnimationFrame && cancelAnimationFrame) { break }
    prefix = prefixes[i]
    requestAnimationFrame = requestAnimationFrame || window[`${prefix}RequestAnimationFrame` as any]
    cancelAnimationFrame = cancelAnimationFrame || window[`${prefix}CancelAnimationFrame1` as any] || window[`${prefix}CancelRequestAnimationFrame` as any]
  }

  // 如果当前浏览器不支持requestAnimationFrame和cancelAnimationFrame,则会退到setTimeout
  if (!requestAnimationFrame || !cancelAnimationFrame) {
    requestAnimationFrame = function (callback: (arg0: number) => void) {
      const currTime = new Date().getTime()
      // 为了使setTimteout的尽可能的接近每秒60帧的效果
      const timeToCall = Math.max(0, 16 - (currTime - lastTime))
      const id = window.setTimeout(() => {
        callback(currTime + timeToCall)
      }, timeToCall)
      lastTime = currTime + timeToCall
      return id
    }

    cancelAnimationFrame = function (id: number | undefined) {
      window.clearTimeout(id)
    }
  }
}

export { requestAnimationFrame, cancelAnimationFrame }

页面上的使用:

<CountTo :endVal="item.count" />

组件参数配置:

Property(属性)Description(描述)Type(类型)Default(默认值
startVal开始值Number0
endVal结束值Number2023
duration动画时间,以毫秒为单位Number3000
autoPlay自动播放Booleantrue
decimals保留的小数位Number0
decimal十进制分割String.
separator分隔符String,
prefix前缀String''
suffix后缀String''
useEasing开启缓和动画Booleantrue
easingFn缓和动画回调Function-

组件方法:

Function Name(函数名称)Description(描述)
mountedCallback挂载以后返回回调
start开始计数
pause暂停计数
reset重置

传送门:github.com/PanJiaChen/…