import { useState, useEffect, useCallback, useRef } from 'react' export interface CountUpResult { /** 当前秒数 */ seconds: number /** 当前分钟 */ minutes: number /** 当前小时 */ hours: number /** 总秒数 */ totalSeconds: number /** 是否在运行 */ isRunning: boolean /** 格式化后的字符串 */ formatted: string /** 暂停 */ pause: () => void /** 继续 */ resume: () => void /** 重置 */ reset: (newStartFrom?: number) => void } export interface CountUpOptions { /** 起始值(秒),默认 0 */ startFrom?: number /** 触发间隔(毫秒),默认 1000 */ interval?: number /** 最大值,超出后停止 */ maxValue?: number /** 格式化函数 */ format?: (result: Pick) => string } /** * 正向计时 Hook * @param options 配置选项 */ export function useCountUp(options: CountUpOptions = {}): CountUpResult { const { startFrom = 0, interval = 1000, maxValue, format } = options const startRef = useRef(startFrom) const maxRef = useRef(maxValue) const intervalRef = useRef | null>(null) const [totalSeconds, setTotalSeconds] = useState(startFrom) const [isRunning, setIsRunning] = useState(false) // 计算时分秒 const calcTime = useCallback((total: number) => ({ totalSeconds: total, hours: Math.floor(total / 3600), minutes: Math.floor((total % 3600) / 60), seconds: total % 60, }), []) const [time, setTime] = useState(() => calcTime(startFrom)) // 默认格式化 const defaultFormat = useCallback((r: Pick) => { const pad = (n: number) => String(n).padStart(2, '0') return `${pad(r.hours)}:${pad(r.minutes)}:${pad(r.seconds)}` }, []) const startInterval = useCallback(() => { if (intervalRef.current) clearInterval(intervalRef.current) intervalRef.current = setInterval(() => { setTotalSeconds(prev => { const next = prev + Math.floor(interval / 1000) const max = maxRef.current if (max !== undefined && next >= max) { if (intervalRef.current) clearInterval(intervalRef.current) setIsRunning(false) return max } return next }) }, interval) }, [interval]) // 更新 refs useEffect(() => { startRef.current = startFrom setTotalSeconds(startFrom) setTime(calcTime(startFrom)) }, [startFrom, calcTime]) useEffect(() => { maxRef.current = maxValue }, [maxValue]) // 同步 totalSeconds 到 time useEffect(() => { setTime(calcTime(totalSeconds)) }, [totalSeconds, calcTime]) // 控制定时器 useEffect(() => { if (isRunning) { startInterval() } else { if (intervalRef.current) { clearInterval(intervalRef.current) intervalRef.current = null } } return () => { if (intervalRef.current) clearInterval(intervalRef.current) } }, [isRunning, startInterval]) const pause = useCallback(() => setIsRunning(false), []) const resume = useCallback(() => setIsRunning(true), []) const reset = useCallback((newStart?: number) => { const start = newStart ?? startFrom startRef.current = start setTotalSeconds(start) setTime(calcTime(start)) setIsRunning(true) }, [startFrom, calcTime]) return { ...time, isRunning, formatted: (format ?? defaultFormat)(time), pause, resume, reset, } }