|
|
/** 东八区日历日 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 '日期待定'
|
|
|
}
|
|
|
|
|
|
const RESERVATION_ONLINE = 'online'
|
|
|
|
|
|
/** 与后端一致:需在平台在线报名(需要预约) */
|
|
|
export function activityIsPlatformOnlineBooking(row: { reservation_type?: string | null }): boolean {
|
|
|
const t = String(row?.reservation_type ?? RESERVATION_ONLINE).trim()
|
|
|
return t === '' || t === RESERVATION_ONLINE
|
|
|
}
|
|
|
|
|
|
function listRowScheduleStatus(row: {
|
|
|
schedule_status?: string
|
|
|
start_at?: string | null
|
|
|
end_at?: string | null
|
|
|
}): ActivityScheduleStatus {
|
|
|
const s = row?.schedule_status
|
|
|
if (s === 'not_started' || s === 'ongoing' || s === 'ended') return s
|
|
|
return computeActivityScheduleStatus(row?.start_at, row?.end_at)
|
|
|
}
|
|
|
|
|
|
export type ActivityCoverCornerKind = 'brand' | 'muted' | 'accent'
|
|
|
|
|
|
/**
|
|
|
* 活动列表/首页封面右下角标签(抢票由页面单独处理)。
|
|
|
* - 平台预约:可预约 / 已报名x人(满员仍显示已报名x人)
|
|
|
* - 非平台预约:无需预约 / 科普研学(收费)
|
|
|
*/
|
|
|
export function activityListCoverCorner(row: {
|
|
|
list_kind?: string
|
|
|
reservation_type?: string | null
|
|
|
offline_reservation_method?: string | null
|
|
|
schedule_status?: string
|
|
|
start_at?: string | null
|
|
|
end_at?: string | null
|
|
|
registered_count?: number
|
|
|
is_bookable?: boolean
|
|
|
}): { text: string; kind: ActivityCoverCornerKind } | null {
|
|
|
if (row?.list_kind === 'ticket_grab') return null
|
|
|
|
|
|
const sched = listRowScheduleStatus(row)
|
|
|
const reg = Math.max(0, Number(row?.registered_count) || 0)
|
|
|
|
|
|
if (activityIsPlatformOnlineBooking(row)) {
|
|
|
if (sched === 'ended') {
|
|
|
return reg > 0 ? { text: `已报名${reg}人`, kind: 'brand' } : null
|
|
|
}
|
|
|
if (row?.is_bookable === true) {
|
|
|
return { text: reg > 0 ? `已报名${reg}人` : '可预约', kind: 'brand' }
|
|
|
}
|
|
|
if (reg > 0) {
|
|
|
return { text: `已报名${reg}人`, kind: 'brand' }
|
|
|
}
|
|
|
return null
|
|
|
}
|
|
|
|
|
|
const paid = String(row?.offline_reservation_method ?? '').trim() === 'paid'
|
|
|
return {
|
|
|
text: paid ? '科普研学' : '无需预约',
|
|
|
kind: paid ? 'accent' : 'muted',
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/** 列表行「立即预约 / 预约已满 / 截止预约(不可点) / 立即抢票」 */
|
|
|
export function activityListReserveButton(row: {
|
|
|
list_kind?: string
|
|
|
reservation_type?: string | null
|
|
|
start_at?: string | null
|
|
|
end_at?: string | null
|
|
|
schedule_status?: string
|
|
|
is_bookable?: boolean
|
|
|
registered_count?: number
|
|
|
/** 后端:各场次 day_quota 均已无余量(与是否在预约窗口内无关) */
|
|
|
all_slots_full?: boolean
|
|
|
/** 后端:预约时间已过但仍有未约满名额 */
|
|
|
booking_closed_not_full?: boolean
|
|
|
}): { show: boolean; text: string; muted?: boolean } {
|
|
|
if (row?.list_kind === 'ticket_grab') {
|
|
|
if (isActivityEndedByCalendar(row?.end_at)) return { show: false, text: '' }
|
|
|
if (row?.is_bookable === true) return { show: true, text: '立即抢票' }
|
|
|
return { show: false, text: '' }
|
|
|
}
|
|
|
if (!activityIsPlatformOnlineBooking(row)) {
|
|
|
return { show: false, text: '' }
|
|
|
}
|
|
|
const sched = listRowScheduleStatus(row)
|
|
|
if (sched === 'ended' || isActivityEndedByCalendar(row?.end_at)) {
|
|
|
return { show: false, text: '' }
|
|
|
}
|
|
|
if (row?.is_bookable === true) {
|
|
|
return { show: true, text: '立即预约' }
|
|
|
}
|
|
|
if (row?.all_slots_full === true) {
|
|
|
return { show: true, text: '预约已满', muted: true }
|
|
|
}
|
|
|
if (row?.booking_closed_not_full === true) {
|
|
|
return { show: true, text: '截止预约', muted: true }
|
|
|
}
|
|
|
return { show: false, text: '' }
|
|
|
}
|