完善服务模块缺少付款页的bug

This commit is contained in:
2026-06-02 11:35:31 +08:00
parent c3324d459a
commit 881262940c
35 changed files with 29069 additions and 557 deletions

View File

@@ -0,0 +1,268 @@
// ============================================================
// 居家上门服务预约时间工具
// 负责:动态日期生成、常用/自定义时间段生成、时间合法性校验
// 约束:所有计算基于真实当前时间,不写死任何展示文本
// ============================================================
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<BookingDayOptionType> {
const result: Array<BookingDayOptionType> = []
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<BookingTimeSlotType> {
const result: Array<BookingTimeSlotType> = []
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<BookingTimeSlotType> {
const result: Array<BookingTimeSlotType> = []
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()
}