import { h } from '@ui-arch/frame'; import type { RelativeTimeProps } from './types.js'; // 缺省刷新间隔与切换绝对日期的阈值:30s 够用又不吵,7 天后再用「N 天前」已不如日期直观 const TICK = 30_000; const MAX_AGO = 7 * 86_400; /*** 目标时间归一成毫秒时间戳;非法输入给 NaN,调用方按「未知」处理 ***/ function toMs(value: string | number | Date): number { if (value instanceof Date) return value.getTime(); if (typeof value === 'number') return value < 1e12 ? value * 1000 : value; // 10 位按秒、13 位按毫秒 return new Date(value).getTime(); } /*** 相对文案:刚刚 / N 分钟前 / N 小时前 / N 天前;未来时间反过来说「还有」 ***/ function phrase(diffSec: number): string { const future = diffSec < 0; const s = Math.abs(diffSec); const unit = (n: number, name: string): string => (future ? `还有 ${n} ${name}` : `${n} ${name}前`); if (s < 60) return future ? '即将' : '刚刚'; if (s < 3600) return unit(Math.floor(s / 60), '分钟'); if (s < 86400) return unit(Math.floor(s / 3600), '小时'); return unit(Math.floor(s / 86400), '天'); } /*** 补零:绝对日期要定宽,列表里才不会一行一个宽度 ***/ const pad = (n: number): string => String(n).padStart(2, '0'); /*** 绝对日期:超出阈值后显示,同时作悬停提示(精确到分钟就够,秒是噪音) ***/ function absolute(ms: number): string { const d = new Date(ms); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; } /*** 相对时间:把时间戳渲染成「3 分钟前」并按间隔自动刷新;超阈值改显示绝对日期,title 始终给精确值 ***/ export function relativeTime(props: RelativeTimeProps): HTMLElement { const ms = toMs(props.value); // 目标时间(毫秒) const tick = props.refresh ?? TICK; // 自动刷新间隔;传 0 关闭 const maxAgo = props.threshold ?? MAX_AGO; // 超过多少秒改用绝对日期 const el = h('time', { class: 'ua-reltime', datetime: Number.isNaN(ms) ? '' : new Date(ms).toISOString() }) as HTMLTimeElement; if (Number.isNaN(ms)) { el.textContent = props.fallback ?? '—'; return el; } // 非法时间:不猜,给占位(缺省破折号) el.title = absolute(ms); // 悬停即精确时间:相对文案永远不需要「点开看」 if (props.ariaLabel) el.setAttribute('aria-label', props.ariaLabel); /*** 一次重绘:算差值 → 超阈值用绝对日期,否则相对文案 ***/ const paint = (): void => { const diffSec = Math.round((Date.now() - ms) / 1000); el.textContent = Math.abs(diffSec) > maxAgo ? absolute(ms) : phrase(diffSec); }; paint(); if (tick > 0) { const timer = window.setInterval(() => { if (!el.isConnected) { window.clearInterval(timer); return; } // 已卸载:自断,避免列表反复挂载累积计时器 paint(); }, tick); } return el; }