// ============================================================ // 居家上门服务预约时间工具 // 负责:动态日期生成、常用/自定义时间段生成、时间合法性校验 // 约束:所有计算基于真实当前时间,不写死任何展示文本 // ============================================================ export const BOOKING_DAYS_LIMIT = 7 export const MIN_ADVANCE_MINUTES = 30 export const CUSTOM_SLOT_DURATION_MINUTES = 60 export const CUSTOM_SLOT_STEP_MINUTES = 30 export const PRESET_SLOT_RANGES = [ '09:00-10:00', '10:00-11:00', '14:00-15:00', '15:00-16:00', '18:00-19:00' ] export type BookingDayOptionType = { id: string dateKey: string label: string dateText: string weekday: string timestamp: number available: boolean disabledReason: string } export type BookingTimeSlotType = { id: string label: string startAt: number endAt: number source: string available: boolean disabledReason: string } export type BookingValidationResultType = { valid: boolean message: string } const WEEKDAY_LABELS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] function padLeft(num: number, length: number): string { let result = String(num) while (result.length < length) { result = '0' + result } return result } function parseDateKey(dateKey: string): { year: number, month: number, day: number } | null { if (dateKey.length != 10) { return null } const year = parseInt(dateKey.substring(0, 4)) const month = parseInt(dateKey.substring(5, 7)) - 1 const day = parseInt(dateKey.substring(8, 10)) if (isNaN(year) || isNaN(month) || isNaN(day)) { return null } return { year, month, day } } function parseTimeRange(range: string): { startH: number, startM: number, endH: number, endM: number } | null { const parts = range.split('-') if (parts.length != 2) { return null } const startParts = parts[0].split(':') const endParts = parts[1].split(':') if (startParts.length != 2 || endParts.length != 2) { return null } const startH = parseInt(startParts[0]) const startM = parseInt(startParts[1]) const endH = parseInt(endParts[0]) const endM = parseInt(endParts[1]) if (isNaN(startH) || isNaN(startM) || isNaN(endH) || isNaN(endM)) { return null } return { startH, startM, endH, endM } } export function buildBookingDays(now: Date): Array { const result: Array = [] const base = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0) for (let i = 0; i < BOOKING_DAYS_LIMIT; i++) { const d = new Date(base.getTime() + i * 24 * 60 * 60 * 1000) const year = d.getFullYear() const month = d.getMonth() + 1 const day = d.getDate() const weekdayIndex = d.getDay() const dateKey = year + '-' + padLeft(month, 2) + '-' + padLeft(day, 2) let label = '' if (i == 0) { label = '今天' } else if (i == 1) { label = '明天' } else if (i == 2) { label = '后天' } else { label = WEEKDAY_LABELS[weekdayIndex] } const dateText = padLeft(month, 2) + '月' + padLeft(day, 2) + '日' const weekday = WEEKDAY_LABELS[weekdayIndex] result.push({ id: 'day-' + String(i), dateKey, label, dateText, weekday, timestamp: d.getTime(), available: true, disabledReason: '' }) } return result } export function buildPresetSlots(selectedDateKey: string, now: Date): Array { const result: Array = [] const parsed = parseDateKey(selectedDateKey) if (parsed == null) { return result } const minStartTime = now.getTime() + MIN_ADVANCE_MINUTES * 60 * 1000 for (let i = 0; i < PRESET_SLOT_RANGES.length; i++) { const range = PRESET_SLOT_RANGES[i] const time = parseTimeRange(range) if (time == null) { continue } const startDate = new Date(parsed.year, parsed.month, parsed.day, time.startH, time.startM, 0, 0) const endDate = new Date(parsed.year, parsed.month, parsed.day, time.endH, time.endM, 0, 0) const startAt = startDate.getTime() const endAt = endDate.getTime() const available = startAt >= minStartTime result.push({ id: 'preset-' + String(i), label: range, startAt, endAt, source: 'preset', available, disabledReason: available ? '' : '该时段距离当前不足30分钟,无法保证服务人员按时到达' }) } return result } export function buildCustomSlots(selectedDateKey: string, now: Date): Array { const result: Array = [] const parsed = parseDateKey(selectedDateKey) if (parsed == null) { return result } const minStartTime = now.getTime() + MIN_ADVANCE_MINUTES * 60 * 1000 for (let h = 0; h < 24; h++) { for (let m = 0; m < 60; m += CUSTOM_SLOT_STEP_MINUTES) { const startDate = new Date(parsed.year, parsed.month, parsed.day, h, m, 0, 0) const endDate = new Date(parsed.year, parsed.month, parsed.day, h, m + CUSTOM_SLOT_DURATION_MINUTES, 0, 0) const startAt = startDate.getTime() const endAt = endDate.getTime() const available = startAt >= minStartTime const startLabel = padLeft(h, 2) + ':' + padLeft(m, 2) const endLabel = padLeft(endDate.getHours(), 2) + ':' + padLeft(endDate.getMinutes(), 2) result.push({ id: 'custom-' + startLabel + '-' + endLabel, label: startLabel + '-' + endLabel, startAt, endAt, source: 'custom', available, disabledReason: available ? '' : '该时段距离当前不足30分钟,无法保证服务人员按时到达' }) } } return result } export function hasAnyAvailableSlots(dateKey: string, now: Date): boolean { const presets = buildPresetSlots(dateKey, now) for (let i = 0; i < presets.length; i++) { if (presets[i].available) { return true } } const customs = buildCustomSlots(dateKey, now) for (let i = 0; i < customs.length; i++) { if (customs[i].available) { return true } } return false } export function validateSelectedAppointmentTime( selectedDay: BookingDayOptionType | null, selectedSlot: BookingTimeSlotType | null, now: Date ): BookingValidationResultType { if (selectedDay == null) { return { valid: false, message: '请选择预约日期' } } if (selectedSlot == null) { return { valid: false, message: '请选择预约时间段' } } const slotStartDate = new Date(selectedSlot.startAt) const dayDate = new Date(selectedDay.timestamp) const sameDay = slotStartDate.getFullYear() == dayDate.getFullYear() && slotStartDate.getMonth() == dayDate.getMonth() && slotStartDate.getDate() == dayDate.getDate() if (!sameDay) { return { valid: false, message: '所选时间段不属于当前选中的日期' } } const minStartTime = now.getTime() + MIN_ADVANCE_MINUTES * 60 * 1000 if (selectedSlot.startAt < minStartTime) { return { valid: false, message: '当前选择的上门时间已失效,请重新选择稍晚的时间段' } } const maxDate = new Date(now.getTime() + BOOKING_DAYS_LIMIT * 24 * 60 * 60 * 1000) const maxDateStart = new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate(), 0, 0, 0, 0) if (selectedDay.timestamp > maxDateStart.getTime()) { return { valid: false, message: '预约日期超出可预约范围' } } return { valid: true, message: '' } } export function formatAppointmentDisplayText( day: BookingDayOptionType | null, slot: BookingTimeSlotType | null ): string { if (day == null || slot == null) { return '请选择上门时间' } return day.label + ' ' + day.dateText + ' ' + day.weekday + ' ' + slot.label } export function formatStandardAppointmentTime( day: BookingDayOptionType | null, slot: BookingTimeSlotType | null ): string { if (day == null || slot == null) { return '' } return new Date(slot.startAt).toISOString() }