Files
xinlong-shop-taro/src/hooks/useCountDown.ts
赵忠林 f3886664f7 fix(api): 替换所有接口请求的域名为 guilixu-api.websoft.top
- 将获取 STS Token 的接口地址由 paopao-api 改为 guilixu-api
- 更新图片上传接口地址为新的 guilixu-api 域名
- 修改用户推广页面中邀请码链接和二维码接口的域名
- 更改注册页微信登录接口请求的域名为 guilixu-api
2026-06-16 17:15:59 +08:00

146 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback, useRef } from 'react'
export interface CountDownResult {
/** 剩余秒数 */
seconds: number
/** 剩余分钟 */
minutes: number
/** 剩余小时 */
hours: number
/** 剩余天数 */
days: number
/** 总剩余秒数 */
totalSeconds: number
/** 是否在运行 */
isRunning: boolean
/** 是否已结束 */
isExpired: boolean
/** 格式化后的字符串 */
formatted: string
/** 暂停 */
pause: () => void
/** 继续 */
resume: () => void
/** 重置 */
reset: (newTargetDate?: number) => void
}
/**
* 倒计时 Hook
* @param targetDate 目标时间戳毫秒0 表示立即结束
* @param options 配置选项
*/
export function useCountDown(
targetDate: number,
options: {
/** 触发间隔(毫秒),默认 1000 */
interval?: number
/** 结束时的回调 */
onComplete?: () => void
/** 是否自动开始,默认 true */
autoStart?: boolean
/** 格式化函数,默认返回 HH:MM:SS 或 DD天HH:MM:SS */
format?: (result: Pick<CountDownResult, 'days' | 'hours' | 'minutes' | 'seconds'>) => string
} = {}
): CountDownResult {
const { interval = 1000, onComplete, autoStart = true, format } = options
const targetRef = useRef(targetDate)
const onCompleteRef = useRef(onComplete)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const [state, setState] = useState(() => calculateTimeLeft(targetDate))
const [isRunning, setIsRunning] = useState(autoStart)
// 更新回调引用
useEffect(() => {
onCompleteRef.current = onComplete
}, [onComplete])
// 更新目标时间
useEffect(() => {
targetRef.current = targetDate
setState(calculateTimeLeft(targetDate))
}, [targetDate])
// 计算剩余时间
function calculateTimeLeft(target: number) {
const now = Date.now()
const diff = Math.max(0, Math.floor((target - now) / 1000))
return {
totalSeconds: diff,
days: Math.floor(diff / 86400),
hours: Math.floor((diff % 86400) / 3600),
minutes: Math.floor((diff % 3600) / 60),
seconds: diff % 60,
}
}
// 默认格式化
const defaultFormat = useCallback((r: Pick<CountDownResult, 'days' | 'hours' | 'minutes' | 'seconds'>) => {
const pad = (n: number) => String(n).padStart(2, '0')
if (r.days > 0) {
return `${r.days}${pad(r.hours)}:${pad(r.minutes)}:${pad(r.seconds)}`
}
return `${pad(r.hours)}:${pad(r.minutes)}:${pad(r.seconds)}`
}, [])
// 启动定时器
const startInterval = useCallback(() => {
if (intervalRef.current) clearInterval(intervalRef.current)
intervalRef.current = setInterval(() => {
const left = calculateTimeLeft(targetRef.current)
setState(left)
if (left.totalSeconds <= 0) {
if (intervalRef.current) clearInterval(intervalRef.current)
setIsRunning(false)
onCompleteRef.current?.()
}
}, interval)
}, [interval])
// 控制定时器
useEffect(() => {
if (isRunning) {
// 立即执行一次
const left = calculateTimeLeft(targetRef.current)
setState(left)
if (left.totalSeconds <= 0) {
setIsRunning(false)
onCompleteRef.current?.()
} else {
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(() => {
if (state.totalSeconds > 0) setIsRunning(true)
}, [state.totalSeconds])
const reset = useCallback((newTarget?: number) => {
const target = newTarget ?? targetDate
targetRef.current = target
setState(calculateTimeLeft(target))
setIsRunning(true)
}, [targetDate])
return {
...state,
isRunning,
isExpired: state.totalSeconds <= 0,
formatted: (format ?? defaultFormat)(state),
pause,
resume,
reset,
}
}