You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

201 lines
6.7 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/** 东八区日历日 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: '' }
}