feat: chat integration, tenant admin spec, backend chat service, miniprogram updates, DEMO moved to tmp, XCX-TEST removed, migrations & docs
This commit is contained in:
126
tmp/DEMO-miniprogram/miniprogram/utils/time.ts
Normal file
126
tmp/DEMO-miniprogram/miniprogram/utils/time.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 时间展示工具
|
||||
* 规范: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
|
||||
}
|
||||
Reference in New Issue
Block a user