/** * 寄卖「加好友赠送冷却」倒计时文案。 * * 后端只给一个冷却截止时间(giftAvailableAt),卖家端负责把它变成看得懂的剩余时间。 * 这里刻意写成纯函数:不依赖 React / antd,也不使用 `@/` 别名,方便被 * `node --experimental-strip-types --test` 直接跑单测。 */ export type GiftCooldownRemaining = { /** 展示文案;无有效截止时间时为空字符串(调用方不渲染倒计时) */ text: string /** 冷却是否已经结束 */ finished: boolean } const MINUTE_MS = 60_000 const HOUR_MS = 3_600_000 /** 冷却已结束的统一文案 */ const FINISHED_TEXT = '冷却已结束,可赠送' /** * 计算冷却剩余时间文案。 * * - 截止时间早于(或等于)now:`冷却已结束,可赠送`,`finished: true` * - 剩余不足 1 小时:`冷却中,剩余 X 分` * - 剩余不足 1 天:`冷却中,剩余 X 小时 Y 分`(整小时时省略「0 分」) * - 剩余 1 天以上:`冷却中,剩余 X 天 Y 小时`(整小时时省略「0 小时」) * * 空值 / 非法时间返回 `{ text: '', finished: false }`:既不展示倒计时,也不误判为已结束。 */ export function formatGiftCooldownRemaining( giftAvailableAt: string | null | undefined, now: number = Date.now(), ): GiftCooldownRemaining { const raw = String(giftAvailableAt ?? '').trim() if (!raw) { return { text: '', finished: false } } const targetMs = Date.parse(raw) if (!Number.isFinite(targetMs)) { return { text: '', finished: false } } const nowMs = Number.isFinite(now) ? Number(now) : Date.now() const remainingMs = targetMs - nowMs if (remainingMs <= 0) { return { text: FINISHED_TEXT, finished: true } } return { text: `冷却中,剩余 ${formatRemaining(remainingMs)}`, finished: false } } function formatRemaining(remainingMs: number) { // 不足 1 小时只按分钟展示,向上取整避免出现「剩余 0 分」 if (remainingMs < HOUR_MS) { return `${Math.max(1, Math.ceil(remainingMs / MINUTE_MS))} 分` } const totalHours = Math.ceil(remainingMs / HOUR_MS) if (totalHours < 24) { const totalMinutes = Math.ceil(remainingMs / MINUTE_MS) const hours = Math.floor(totalMinutes / 60) const minutes = totalMinutes % 60 return minutes === 0 ? `${hours} 小时` : `${hours} 小时 ${minutes} 分` } const days = Math.floor(totalHours / 24) const hours = totalHours % 24 return hours === 0 ? `${days} 天` : `${days} 天 ${hours} 小时` }