Files
BYW-VoiceHub/app/utils/timeUtils.ts
T

189 lines
5.8 KiB
TypeScript

import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc.js'
import timezone from 'dayjs/plugin/timezone.js'
dayjs.extend(utc)
dayjs.extend(timezone)
const BEIJING_TIMEZONE = 'Asia/Shanghai'
/**
* 时间工具函数
* 使用 dayjs 处理北京时间(UTC+8)相关逻辑
*
* 核心原则:
* 1. 系统内部传递的 Date 对象保持为真实的 UTC 时间。
* 2. 数据库中存储的 Timestamp 字段应为真实 UTC 时间。
* 3. 只有在需要"显示"或"按北京时间逻辑计算(如每日限额)"时,才转换为北京时间处理。
* 4. 所有"当前时间"均使用同步后的时间(服务端用 getServerTimestamp,客户端用 getSyncedTimestamp)。
*/
/** 获取同步后的当前时间戳 (毫秒) */
function getSyncedNow(): number {
if (import.meta.server) {
return (globalThis as any).getServerTimestamp?.() ?? Date.now()
}
return (globalThis as any).getSyncedTimestamp?.() ?? Date.now()
}
/**
* 获取当前时间 (真实 UTC)
* 使用同步后的时间
*/
export function getBeijingTime(): Date {
return new Date(getSyncedNow())
}
/**
* 转换时间 (保留函数签名以兼容旧代码)
*/
export function toBeijingTime(date: Date): Date {
return date
}
/**
* 创建北京时间对应的 UTC Date
*/
export function createBeijingTime(
year: number,
month: number,
day: number,
hour: number = 0,
minute: number = 0,
second: number = 0
): Date {
return dayjs()
.tz(BEIJING_TIMEZONE)
.set('year', year)
.set('month', month)
.set('date', day)
.set('hour', hour)
.set('minute', minute)
.set('second', second)
.set('millisecond', 0)
.toDate()
}
/**
* 格式化时间为北京时间字符串
*/
export function formatDateTime(date: Date, format: string = 'YYYY/M/D H:mm:ss'): string {
return dayjs(date).tz(BEIJING_TIMEZONE).format(format)
}
/**
* 获取时间戳 (Date 对象)
*/
export function getBeijingTimestamp(): Date {
return new Date(getSyncedNow())
}
/**
* 解析北京时间字符串为真实 UTC Date
*/
export function parseToBeijingTime(dateString: string): Date {
return dayjs.tz(dateString, BEIJING_TIMEZONE).toDate()
}
/**
* 获取当前北京时间的 ISO 字符串
*/
export function getBeijingTimeISOString(): string {
return dayjs(getSyncedNow()).tz(BEIJING_TIMEZONE).format('YYYY-MM-DDTHH:mm:ss')
}
/**
* 获取北京时间的小时 (0-23)
*/
export function getBeijingHour(date: Date): number {
return dayjs(date).tz(BEIJING_TIMEZONE).hour()
}
/**
* 获取"北京时间某天开始"对应的真实 UTC 时间
*/
export function getBeijingStartOfDay(date?: Date): Date {
const target = date ? dayjs(date).tz(BEIJING_TIMEZONE) : dayjs(getSyncedNow()).tz(BEIJING_TIMEZONE)
return target.startOf('day').toDate()
}
/**
* 获取"北京时间某天结束"对应的真实 UTC 时间
*/
export function getBeijingEndOfDay(date?: Date): Date {
const target = date ? dayjs(date).tz(BEIJING_TIMEZONE) : dayjs(getSyncedNow()).tz(BEIJING_TIMEZONE)
return target.endOf('day').toDate()
}
/**
* 获取"北京时间本周开始"对应的真实 UTC 时间 (周一 00:00:00)
*/
export function getBeijingStartOfWeek(date?: Date): Date {
const target = date ? dayjs(date).tz(BEIJING_TIMEZONE) : dayjs(getSyncedNow()).tz(BEIJING_TIMEZONE)
const day = target.day()
const diff = day === 0 ? 6 : day - 1
return target.subtract(diff, 'day').startOf('day').toDate()
}
/**
* 获取"北京时间本周结束"对应的真实 UTC 时间 (周日 23:59:59)
*/
export function getBeijingEndOfWeek(date?: Date): Date {
const startOfWeek = dayjs(getBeijingStartOfWeek(date)).tz(BEIJING_TIMEZONE)
return startOfWeek.add(6, 'day').endOf('day').toDate()
}
/**
* 获取"北京时间本月开始"对应的真实 UTC 时间
*/
export function getBeijingStartOfMonth(date?: Date): Date {
const target = date ? dayjs(date).tz(BEIJING_TIMEZONE) : dayjs(getSyncedNow()).tz(BEIJING_TIMEZONE)
return target.startOf('month').toDate()
}
/**
* 获取"北京时间本月结束"对应的真实 UTC 时间
*/
export function getBeijingEndOfMonth(date?: Date): Date {
const target = date ? dayjs(date).tz(BEIJING_TIMEZONE) : dayjs(getSyncedNow()).tz(BEIJING_TIMEZONE)
return target.endOf('month').toDate()
}
/**
* 格式化歌曲/音频时长(秒 → 分:秒 / 时:分:秒)
*/
export function formatDuration(seconds: number): string {
const total = Number(seconds)
if (!isFinite(total) || total < 0) return ''
const hours = Math.floor(total / 3600)
const minutes = Math.floor((total % 3600) / 60)
const secs = Math.floor(total % 60)
const minuteText = hours > 0 ? minutes.toString().padStart(2, '0') : String(minutes)
const secondText = secs.toString().padStart(2, '0')
return hours > 0 ? `${hours}:${minuteText}:${secondText}` : `${minuteText}:${secondText}`
}
/**
* 字符串日期(YYYY-MM-DD)增加指定天数
*/
export function addDaysToString(dateStr: string, days: number): string {
const match = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(dateStr)
if (!match) return dateStr
const date = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]) + days))
const pad = (n: number) => (n < 10 ? '0' + n : '' + n)
return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`
}
/**
* 计算两个字符串日期之间的天数差(end - start)
* 负数表示 start 晚于 end
*/
export function getDaysBetween(startStr: string, endStr: string): number {
const startMatch = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(startStr)
const endMatch = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(endStr)
if (!startMatch || !endMatch) return -1
const start = new Date(Date.UTC(Number(startMatch[1]), Number(startMatch[2]) - 1, Number(startMatch[3]))).getTime()
const end = new Date(Date.UTC(Number(endMatch[1]), Number(endMatch[2]) - 1, Number(endMatch[3]))).getTime()
return Math.round((end - start) / 86400000)
}