import { ref, watch, onScopeDispose } from 'vue' const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches // 数字滚动:只在值变化时做一次 500ms 缓动,尊重系统减弱动效设置 export function useCountUp(source, duration = 500) { const display = ref(0) let raf = 0 const run = (to) => { const target = Number(to) if (!isFinite(target)) { display.value = 0 return } if (reduceMotion || target === display.value) { display.value = target return } cancelAnimationFrame(raf) const from = Number(display.value) || 0 const start = performance.now() const step = (now) => { const p = Math.min((now - start) / duration, 1) const eased = 1 - Math.pow(1 - p, 3) display.value = from + (target - from) * eased if (p < 1) raf = requestAnimationFrame(step) } raf = requestAnimationFrame(step) } watch(source, (v) => run(v), { immediate: true }) onScopeDispose(() => cancelAnimationFrame(raf)) return display } // 千分位格式化(保留整数或小数) export function formatNum(n, digits) { const num = Number(n || 0) if (digits !== undefined) { return num.toLocaleString('zh-CN', { minimumFractionDigits: digits, maximumFractionDigits: digits }) } return num.toLocaleString('zh-CN') }