127 lines
5.1 KiB
TypeScript
127 lines
5.1 KiB
TypeScript
/**
|
||
* 时间展示工具
|
||
* 规范: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
|
||
}
|