time.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. /**
  2. * 时间格式化 —— **全项目唯一实现**
  3. *
  4. * 依据《数据访问层设计.md》§五硬约束 6:存量有 4 份重复的日期格式化,
  5. * v2 只允许这一处。组件里禁止自建 `toLocaleDateString` 之类的逻辑。
  6. *
  7. * 🔴 云函数返回的时间可能是 `{ __type: 'Date', iso }` 对象(Parse 序列化形式),
  8. * `String()` 会得到 `[object Object]` —— 必须统一经 `tsOf` 归一。
  9. */
  10. /** 归一任意时间形态为毫秒时间戳;无法解析返回 NaN */
  11. export function tsOf(value: unknown): number {
  12. if (value == null) return NaN;
  13. if (typeof value === 'number') return value;
  14. if (value instanceof Date) return value.getTime();
  15. if (typeof value === 'string') {
  16. const t = Date.parse(value);
  17. return Number.isNaN(t) ? NaN : t;
  18. }
  19. if (typeof value === 'object') {
  20. const obj = value as Record<string, unknown>;
  21. // Parse 的 Date 形式:{ __type: 'Date', iso: '...' }
  22. if (typeof obj['iso'] === 'string') return Date.parse(obj['iso']);
  23. if (typeof obj['__type'] === 'string' && typeof obj['value'] === 'string') {
  24. return Date.parse(obj['value']);
  25. }
  26. }
  27. return NaN;
  28. }
  29. function pad(n: number): string {
  30. return n < 10 ? `0${n}` : String(n);
  31. }
  32. function dateOf(value: unknown): Date | null {
  33. const t = tsOf(value);
  34. return Number.isNaN(t) ? null : new Date(t);
  35. }
  36. /** `10:42` */
  37. export function fmtTime(value: unknown): string {
  38. const d = dateOf(value);
  39. if (!d) return '—';
  40. return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
  41. }
  42. /** `2026-10-14` */
  43. export function fmtDate(value: unknown): string {
  44. const d = dateOf(value);
  45. if (!d) return '—';
  46. return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
  47. }
  48. /** `2026-10-14 10:42` */
  49. export function fmtDateTime(value: unknown): string {
  50. const d = dateOf(value);
  51. if (!d) return '—';
  52. return `${fmtDate(value)} ${fmtTime(value)}`;
  53. }
  54. /** `10 月 14 日,星期三` */
  55. export function fmtDayLabel(value: unknown): string {
  56. const d = dateOf(value);
  57. if (!d) return '—';
  58. const week = ['日', '一', '二', '三', '四', '五', '六'][d.getDay()];
  59. return `${d.getMonth() + 1} 月 ${d.getDate()} 日,星期${week}`;
  60. }
  61. /** 列表里用的相对时间:今天 → `10:42`;昨天 → `昨天 16:30`;更早 → `10-13` */
  62. export function fmtRelTime(value: unknown, now = new Date()): string {
  63. const d = dateOf(value);
  64. if (!d) return '—';
  65. const sameDay = (a: Date, b: Date) =>
  66. a.getFullYear() === b.getFullYear() &&
  67. a.getMonth() === b.getMonth() &&
  68. a.getDate() === b.getDate();
  69. if (sameDay(d, now)) return fmtTime(value);
  70. const yesterday = new Date(now);
  71. yesterday.setDate(now.getDate() - 1);
  72. if (sameDay(d, yesterday)) return `昨天 ${fmtTime(value)}`;
  73. if (d.getFullYear() === now.getFullYear()) {
  74. return `${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
  75. }
  76. return fmtDate(value);
  77. }
  78. /** 事件流的分组键(同一天归为一组) */
  79. export function dayKeyOf(value: unknown): string {
  80. const d = dateOf(value);
  81. if (!d) return '未知时间';
  82. const today = new Date();
  83. const yesterday = new Date(today);
  84. yesterday.setDate(today.getDate() - 1);
  85. const same = (a: Date, b: Date) =>
  86. a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
  87. if (same(d, today)) return '今天';
  88. if (same(d, yesterday)) return '昨天';
  89. return fmtDate(value);
  90. }
  91. /** 相对时长(用于「已阻塞 436h」这类表达) */
  92. export function fmtDuration(hours?: number): string {
  93. if (hours == null || Number.isNaN(hours)) return '—';
  94. if (hours < 1) return `${Math.round(hours * 60)} 分钟`;
  95. if (hours < 48) return `${Math.round(hours)} 小时`;
  96. return `${Math.round(hours / 24)} 天`;
  97. }