// 全站统一的金额 / 时间格式化。 // // 后端有两种时间口径,序列化后从字符串无法区分,必须按字段语义选对函数: // 1. UTC 口径:`server_default=func.now()` 的字段(用户/提现单/审计日志/广告发奖记录)。 // 入库是 UTC,SQLite 下序列化不带时区、Postgres 下带 +00:00。统一当 UTC 解析后转北京显示。 // 2. 北京 wall-clock 口径:钱包流水(coin_transaction / cash_transaction)。入库即 // `datetime.now(CN_TZ).replace(tzinfo=None)`,字面就是北京时间且无时区,**不能再做时区换算**。 // 历史上各页面混用 `new Date().toLocaleString`(按本地解析)与 `apiTime`(补 Z 当 UTC), // 导致提现详情里现金流水时间快 8 小时。此模块收口,按口径区分。 import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; import relativeTime from 'dayjs/plugin/relativeTime'; import 'dayjs/locale/zh-cn'; dayjs.extend(utc); dayjs.extend(timezone); dayjs.extend(relativeTime); dayjs.locale('zh-cn'); const TZ = 'Asia/Shanghai'; /** 金额:分 → ¥元(两位小数)。 */ export const yuan = (cents: number) => `¥${(cents / 100).toFixed(2)}`; const hasTz = (v: string) => /(?:Z|[+-]\d{2}:?\d{2})$/i.test(v); /** 把 UTC 口径字符串解析为带时区的 dayjs(无时区后缀的补 Z 当 UTC)。 */ export const utcDayjs = (v: string) => dayjs(hasTz(v) ? v : `${v}Z`); /** UTC 口径时间 → 北京时间显示(用户/提现单/审计/广告发奖记录)。 */ export function formatUtcTime(v?: string | null, fmt = 'YYYY-MM-DD HH:mm'): string { if (!v) return '-'; return utcDayjs(v).tz(TZ).format(fmt); } /** UTC 口径相对时间(如「3 分钟前」)。 */ export function utcFromNow(v?: string | null): string { if (!v) return '-'; return utcDayjs(v).fromNow(); } /** 北京 wall-clock 口径时间 → 原样格式化,不做时区换算(钱包金币/现金流水)。 */ export function formatWallTime(v?: string | null, fmt = 'YYYY-MM-DD HH:mm'): string { if (!v) return '-'; return dayjs(v).format(fmt); }