import { _decorator } from 'cc'; import { Singleton } from '../../tools/Singleton'; const { ccclass } = _decorator; const MS_PER_MINUTE = 60 * 1000; const MS_PER_DAY = 24 * 60 * MS_PER_MINUTE; @ccclass('TimeModule') export class TimeModule extends Singleton { /** ISO 或已解析的 UTC 毫秒;热路径传 `number` 可避免 `new Date` 与字符串解析 */ private static utcMs(t: string | number): number { return typeof t === 'number' ? t : new Date(t).getTime(); } /** 当前 UTC 时间毫秒数(无 Date 分配,适合每帧调用) */ get now(): number { return Date.now(); } /** * 将本地日历时刻按「数值等同 UTC」方式表示的毫秒数(常用于与服务器约定字段对齐) * 等价:UTC 时间与本地墙钟相同数字时的 epoch 毫秒 */ get localNow(): number { const d = new Date(); return d.getTime() + d.getTimezoneOffset() * MS_PER_MINUTE; } /** 解析带时区的时间字符串为 UTC 毫秒,如 2026-02-08T00:00:00+08:00 */ getUTCTimeByLocalTimeStr(timeStr: string): number { return new Date(timeStr).getTime(); } /** 带时区字符串 → 本地墙钟对齐的「伪 UTC」毫秒(与 localNow 同一套坐标系) */ getLocalTimeByLocalTimeStr(timeStr: string): number { const d = new Date(timeStr); return d.getTime() + d.getTimezoneOffset() * MS_PER_MINUTE; } /** 本地墙钟对齐毫秒 → 真实 UTC 毫秒 */ getUTCTimeByLocalTime(localTime: number): number { const d = new Date(localTime); return d.getTime() - d.getTimezoneOffset() * MS_PER_MINUTE; } /** * 根据时间段判断当前时间状态(比较均在真实 UTC 时间轴上) * @param timeNow 当前 UTC 毫秒 * @param startTime 活动开始:ISO 字符串(含时区)或已解析的 UTC 毫秒 * @param endTime 活动结束:同上 * @param name 可选;非空会打日志,勿在高频逻辑中传入 */ checkTimeStatus(timeNow: number, startTime: string | number, endTime: string | number, name = ''): 0 | 1 | 2 { const start = TimeModule.utcMs(startTime); const end = TimeModule.utcMs(endTime); if (name) { console.log('活动时间:', name, timeNow, start, end); } if (timeNow > end) return 2; if (timeNow >= start) return 1; return 0; } /** * 当天剩余毫秒数(到下一次「本地/UTC」的 0 点) * 仅分配 **一个** `Date`(原实现为两个) */ getRemainTimeForCurrentDay(timeNow: number | null = null, isLocal = true): number { const d = new Date(timeNow ?? Date.now()); const now = d.getTime(); if (isLocal) { d.setHours(24, 0, 0, 0); } else { d.setUTCHours(24, 0, 0, 0); } return d.getTime() - now; } /** * 距离活动结束的剩余毫秒,负表示已过期 * @param endTime 结束时刻:ISO 字符串(含时区)或 UTC 毫秒 * @param timeNow 当前 UTC 毫秒,默认 `Date.now()` */ getRemainTime(endTime: string | number, timeNow: number = Date.now()): number { return TimeModule.utcMs(endTime) - timeNow; } /** 自 startTime 起算的第几天(满 24h 计一天,向下取整);`startTime` 为 number 时零分配 */ getDayAfterStartTime(timeNow: number, startTime: number | string): number { const start = typeof startTime === 'string' ? new Date(startTime).getTime() : startTime; return Math.floor((timeNow - start) / MS_PER_DAY); } /** 与当前真实时间是否同一自然日(本地时区);仅 **一个** `Date` 实例 */ isSameDay(lastLoginTime: number): boolean { const d = new Date(lastLoginTime); const y = d.getFullYear(); const m = d.getMonth(); const day = d.getDate(); d.setTime(Date.now()); return d.getFullYear() === y && d.getMonth() === m && d.getDate() === day; } }