/** 东八区日历日 YYYY-MM-DD(与后端活动状态一致,勿对 ISO 直接 slice(0,10)) */ export function ymdInShanghai(iso?: string | null): string { if (!iso) return '' const d = new Date(String(iso)) if (Number.isNaN(d.getTime())) return String(iso).slice(0, 10) return new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', }).format(d) } function todayYmdShanghai(): string { return new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', }).format(new Date()) } function compareYmd(a: string, b: string): number { if (!a || !b) return 0 if (a < b) return -1 if (a > b) return 1 return 0 } /** 与后台活动列表一致:未开始 / 进行中 / 已结束 */ export type ActivityScheduleStatus = 'not_started' | 'ongoing' | 'ended' export function computeActivityScheduleStatus( startAt?: string | null, endAt?: string | null, ): ActivityScheduleStatus { const today = todayYmdShanghai() const startD = ymdInShanghai(startAt) const endD = ymdInShanghai(endAt) if (!startD && !endD) return 'ongoing' if (startD && !endD) { return compareYmd(today, startD) < 0 ? 'not_started' : 'ongoing' } if (!startD && endD) { return compareYmd(today, endD) > 0 ? 'ended' : 'ongoing' } if (compareYmd(today, startD) < 0) return 'not_started' if (compareYmd(today, endD) > 0) return 'ended' return 'ongoing' } export function activityScheduleStatusLabel(s: ActivityScheduleStatus): string { if (s === 'not_started') return '未开始' if (s === 'ended') return '已结束' return '进行中' } /** 活动是否已按「日历日」结束:结束日期早于今天则视为不可报名 */ export function isActivityEndedByCalendar(endAt?: string | null): boolean { if (!endAt) return false const endDay = ymdInShanghai(endAt) if (!endDay) return false const todayStr = todayYmdShanghai() return endDay < todayStr } /** 活动日期:同年为「2026年4月1日至5月3日」,跨年则两年份都写 */ export function formatActivityCnRange(startAt?: string | null, endAt?: string | null) { const parse = (raw?: string | null) => { if (!raw) return null const s = String(raw).trim() if (/^\d{4}-\d{2}-\d{2}$/.test(s)) { const p = s.split('-').map(Number) if (p.length !== 3) return null const [y, m, d] = p if (!Number.isFinite(y) || !Number.isFinite(m) || !Number.isFinite(d)) return null return { y, m, d } } const ymd = ymdInShanghai(s) if (!ymd) return null const p = ymd.split('-').map(Number) if (p.length !== 3) return null const [y, m, d] = p if (!Number.isFinite(y) || !Number.isFinite(m) || !Number.isFinite(d)) return null return { y, m, d } } const s = parse(startAt) const e = parse(endAt) if (!s && !e) return '日期待定' if (s && !e) return `${s.y}年${s.m}月${s.d}日` if (!s && e) return `${e.y}年${e.m}月${e.d}日` if (s && e) { if (s.y === e.y && s.m === e.m && s.d === e.d) { return `${s.y}年${s.m}月${s.d}日` } if (s.y === e.y) { return `${s.y}年${s.m}月${s.d}日至${e.m}月${e.d}日` } return `${s.y}年${s.m}月${s.d}日至${e.y}年${e.m}月${e.d}日` } return '日期待定' }