From c86fb21b4717abe99e7809d9140aec53651e2d37 Mon Sep 17 00:00:00 2001 From: rambuild Date: Thu, 28 Aug 2025 16:57:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20useCountdown=20=E6=96=B0=E5=A2=9E=20cur?= =?UTF-8?q?rentServerTime=20=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/src/useCountDown/demo/demo4.tsx | 20 +++++++ .../hooks/src/useCountDown/index.en-US.md | 10 +++- packages/hooks/src/useCountDown/index.ts | 52 +++++++++++++++---- .../hooks/src/useCountDown/index.zh-CN.md | 12 ++++- 4 files changed, 82 insertions(+), 12 deletions(-) create mode 100644 packages/hooks/src/useCountDown/demo/demo4.tsx diff --git a/packages/hooks/src/useCountDown/demo/demo4.tsx b/packages/hooks/src/useCountDown/demo/demo4.tsx new file mode 100644 index 0000000000..c1717ff81e --- /dev/null +++ b/packages/hooks/src/useCountDown/demo/demo4.tsx @@ -0,0 +1,20 @@ +/** + * title: Configure the server time + * desc: Because users may change their local system time, the countdown can become inaccurate when using targetDate. + * + * title.zh-CN: 配置服务器时间 + * desc.zh-CN: 防止用户修改本地时间,导致配置 targetDate 时倒计时不准确,通过 currentServerTime 配置当前服务器时间 + */ + +import React, { useMemo } from "react"; +import { useCountDown } from "ahooks"; + +const App: React.FC = () => { + // should be get from server + const currentServerTime = useMemo(() => Date.now(), []); + + const [countdown] = useCountDown({ leftTime: 60 * 1000, currentServerTime }); + return

{countdown}

; +}; + +export default App; diff --git a/packages/hooks/src/useCountDown/index.en-US.md b/packages/hooks/src/useCountDown/index.en-US.md index 65f1bf8d32..dc18abca68 100644 --- a/packages/hooks/src/useCountDown/index.en-US.md +++ b/packages/hooks/src/useCountDown/index.en-US.md @@ -19,6 +19,10 @@ A hook for manage countdown. +## Config currentServerTime + + + ## API ```typescript @@ -37,6 +41,7 @@ const [countdown, formattedRes] = useCountDown( leftTime, targetDate, interval, + currentServerTime, onEnd } ); @@ -53,6 +58,8 @@ If you only need to be accurate to the second, you can use it like this `Math.ro If both `leftTime` and `targetDate` are passed, the `targetDate` is ignored, the `leftTime` is dominant. +If 'currentServerTime' is not configured, the local time is used for the countdown. + ### Params | Property | Description | Type | Default | @@ -60,6 +67,7 @@ If both `leftTime` and `targetDate` are passed, the `targetDate` is ignored, the | leftTime | The rest of time, in milliseconds | `number` | - | | targetDate | Target time | `TDate` | - | | interval | Time interval between ticks, in milliseconds | `number` | `1000` | +| currentServerTime | Current server time, in milliseconds | `number` | - | | onEnd | Function to call when countdown completes | `() => void` | - | ### Return @@ -71,4 +79,4 @@ If both `leftTime` and `targetDate` are passed, the `targetDate` is ignored, the ## Remark -`leftTime`、`targetDate`、`interval`、`onEnd` support dynamic change. +`leftTime`、`targetDate`、`interval`、`currentServerTime`、`onEnd` support dynamic change. diff --git a/packages/hooks/src/useCountDown/index.ts b/packages/hooks/src/useCountDown/index.ts index bcb8866b76..dfd77291c0 100644 --- a/packages/hooks/src/useCountDown/index.ts +++ b/packages/hooks/src/useCountDown/index.ts @@ -9,6 +9,7 @@ export interface Options { leftTime?: number; targetDate?: TDate; interval?: number; + currentServerTime?: number; onEnd?: () => void; } @@ -20,12 +21,23 @@ export interface FormattedRes { milliseconds: number; } -const calcLeft = (target?: TDate) => { +const calcLeft = ( + target?: TDate, + momentTimeInfo?: { + serverTime: number; + localTime: number; + } +) => { if (!target) { return 0; } // https://stackoverflow.com/questions/4310953/invalid-date-in-safari - const left = dayjs(target).valueOf() - Date.now(); + const left = + // 如果服务器时间存在,则使用服务器时间,并动态计算时间差值,否则使用本地时间 + dayjs(target).valueOf() - + (momentTimeInfo?.serverTime + ? momentTimeInfo.serverTime + Date.now() - momentTimeInfo.localTime + : Date.now()); return left < 0 ? 0 : left; }; @@ -40,15 +52,37 @@ const parseMs = (milliseconds: number): FormattedRes => { }; const useCountdown = (options: Options = {}) => { - const { leftTime, targetDate, interval = 1000, onEnd } = options || {}; + const { + leftTime, + targetDate, + interval = 1000, + currentServerTime, + onEnd, + } = options || {}; + + /** 缓存此刻时间,保存当前服务器时间和本地时间,用于动态计算时间差,单位ms */ + const momentTimeInfo = useMemo( + () => ({ + serverTime: currentServerTime || 0, + localTime: currentServerTime ? Date.now() : 0, + }), + [currentServerTime] + ); const memoLeftTime = useMemo(() => { - return isNumber(leftTime) && leftTime > 0 ? Date.now() + leftTime : undefined; - }, [leftTime]); + return isNumber(leftTime) && leftTime > 0 + ? // 如果传入服务器时间,则使用服务器时间,并动态计算时间差值,否则使用本地时间 + (momentTimeInfo.serverTime + ? momentTimeInfo.serverTime + Date.now() - momentTimeInfo.localTime + : Date.now()) + leftTime + : undefined; + }, [leftTime, momentTimeInfo]); const target = 'leftTime' in options ? memoLeftTime : targetDate; - const [timeLeft, setTimeLeft] = useState(() => calcLeft(target)); + const [timeLeft, setTimeLeft] = useState(() => + calcLeft(target, momentTimeInfo) + ); const onEndRef = useLatest(onEnd); @@ -60,10 +94,10 @@ const useCountdown = (options: Options = {}) => { } // 立即执行一次 - setTimeLeft(calcLeft(target)); + setTimeLeft(calcLeft(target, momentTimeInfo)); const timer = setInterval(() => { - const targetLeft = calcLeft(target); + const targetLeft = calcLeft(target, momentTimeInfo); setTimeLeft(targetLeft); if (targetLeft === 0) { clearInterval(timer); @@ -72,7 +106,7 @@ const useCountdown = (options: Options = {}) => { }, interval); return () => clearInterval(timer); - }, [target, interval]); + }, [target, interval, momentTimeInfo]); const formattedRes = useMemo(() => parseMs(timeLeft), [timeLeft]); diff --git a/packages/hooks/src/useCountDown/index.zh-CN.md b/packages/hooks/src/useCountDown/index.zh-CN.md index 557dc21414..fd4b502804 100644 --- a/packages/hooks/src/useCountDown/index.zh-CN.md +++ b/packages/hooks/src/useCountDown/index.zh-CN.md @@ -19,6 +19,10 @@ nav: +## 通过 currentServerTime 配置当前服务器时间 + + + **说明** useCountDown 的精度为毫秒,可能会造成以下几个问题 @@ -28,7 +32,9 @@ useCountDown 的精度为毫秒,可能会造成以下几个问题 如果你的精度只要到秒就好了,可以这样用 `Math.round(countdown / 1000)`。 -如果同时传了 `leftTime` 和 `targetDate`,则会忽略 `targetDate`,以 `leftTime` 为主 +如果同时传了 `leftTime` 和 `targetDate`,则会忽略 `targetDate`,以 `leftTime` 为主。 + +如果没有配置 `currentServerTime` ,则会使用本地时间进行倒计时 。 ## API @@ -48,6 +54,7 @@ const [countdown, formattedRes] = useCountDown( leftTime, targetDate, interval, + currentServerTime, onEnd } ); @@ -60,6 +67,7 @@ const [countdown, formattedRes] = useCountDown( | leftTime | 剩余时间(毫秒) | `number` | - | | targetDate | 目标时间 | `TDate` | - | | interval | 变化时间间隔(毫秒) | `number` | `1000` | +| currentServerTime | 当前服务器时间(毫秒) | `number` | - | | onEnd | 倒计时结束触发 | `() => void` | - | ### Result @@ -71,4 +79,4 @@ const [countdown, formattedRes] = useCountDown( ## 备注 -`leftTime`、`targetDate`、`interval`、`onEnd` 支持动态变化 +`leftTime`、`targetDate`、`interval`、`currentServerTime`、`onEnd` 支持动态变化