/** * 时间展示工具 * 规范:docs/miniprogram-dev/design-system/DATETIME-DISPLAY-STANDARD.md * * 规则(由近及远): * < 120s → 刚刚 * 2min ~ 59min → N分钟前 * 1h ~ 23h → N小时前 * 1d ~ 3d → N天前 * > 3d 同年 → MM-DD * > 3d 跨年 → YYYY-MM-DD */ /** * 将时间戳或 ISO 字符串格式化为相对时间文案 * @param value Unix 毫秒时间戳 或 ISO 8601 字符串(如 "2026-03-10T16:30:00Z") * @returns 格式化文案,如「刚刚」「2分钟前」「03-10」 */ /** * 课时格式化 * 整数 → Nh;非整数保留1位 → N.Nh;零值 → 0h;空值 → -- */ export function formatHours(hours: number | null | undefined): string { if (hours === null || hours === undefined) return '--' if (hours === 0) return '0h' return hours % 1 === 0 ? `${hours}h` : `${hours.toFixed(1)}h` } /** * 任务截止日期语义化格式化 * 规范:docs/miniprogram-dev/design-system/DISPLAY-STANDARDS-2.md §7 * * @returns { text, style } * style: 'muted'(灰) | 'normal'(正常) | 'warning'(橙/今天) | 'danger'(红/逾期) */ export function formatDeadline( deadline: string | null | undefined, ): { text: string; style: 'normal' | 'warning' | 'danger' | 'muted' } { if (!deadline) return { text: '--', style: 'muted' } const now = new Date() const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()) const target = new Date(deadline) const targetDay = new Date(target.getFullYear(), target.getMonth(), target.getDate()) const diff = Math.round((targetDay.getTime() - today.getTime()) / 86400000) if (diff < 0) return { text: `逾期 ${Math.abs(diff)} 天`, style: 'danger' } if (diff === 0) return { text: '今天到期', style: 'warning' } if (diff <= 7) return { text: `还剩 ${diff} 天`, style: 'normal' } const m = String(target.getMonth() + 1).padStart(2, '0') const d = String(target.getDate()).padStart(2, '0') return { text: `${m}-${d}`, style: 'muted' } } export function formatRelativeTime(value: number | string | undefined | null): string { if (value === undefined || value === null || value === '') return '--' const normalized = typeof value === 'string' ? value.replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})/, '$1T$2') : value const ts = typeof normalized === 'number' ? normalized : new Date(normalized).getTime() if (isNaN(ts)) return '--' const now = Date.now() const diff = Math.floor((now - ts) / 1000) // 秒 // 未来时间(服务端时钟偏差)按「刚刚」处理 if (diff < 120) return '刚刚' if (diff < 3600) return `${Math.floor(diff / 60)}分钟前` if (diff < 86400) return `${Math.floor(diff / 3600)}小时前` if (diff < 259200) return `${Math.floor(diff / 86400)}天前` const date = new Date(ts) const nowDate = new Date(now) const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') if (year === nowDate.getFullYear()) return `${month}-${day}` return `${year}-${month}-${day}` } /** * IM 气泡内时间戳格式 * 今天内 → HH:mm * 今年非今天 → MM-DD HH:mm * 跨年 → YYYY-MM-DD HH:mm */ export function formatIMTime(value: number | string | undefined | null): string { if (value === undefined || value === null || value === '') return '' const normalized = typeof value === 'string' ? value.replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})/, '$1T$2') : value const ts = typeof normalized === 'number' ? normalized : new Date(normalized).getTime() if (isNaN(ts)) return '' const date = new Date(ts) const now = new Date() const hh = String(date.getHours()).padStart(2, '0') const mn = String(date.getMinutes()).padStart(2, '0') const timeStr = `${hh}:${mn}` const isSameDay = date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate() if (isSameDay) return timeStr const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') if (date.getFullYear() === now.getFullYear()) return `${month}-${day} ${timeStr}` return `${date.getFullYear()}-${month}-${day} ${timeStr}` } /** * 判断相邻消息是否需要显示时间分割线(间隔 >= 5 分钟,首条始终显示) */ export function shouldShowTimeDivider( prevTimestamp: number | string | null | undefined, currTimestamp: number | string | undefined | null, ): boolean { if (!prevTimestamp) return true const normPrev = typeof prevTimestamp === 'string' ? prevTimestamp.replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})/, '$1T$2') : prevTimestamp const normCurr = typeof currTimestamp === 'string' ? (currTimestamp as string).replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})/, '$1T$2') : currTimestamp const prev = typeof normPrev === 'number' ? normPrev : new Date(normPrev).getTime() const curr = typeof normCurr === 'number' ? normCurr : new Date(normCurr as string).getTime() if (isNaN(prev) || isNaN(curr)) return false return curr - prev >= 5 * 60 * 1000 }