完善服务模块缺少付款页的bug
This commit is contained in:
268
utils/homeServiceBookingTime.uts
Normal file
268
utils/homeServiceBookingTime.uts
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user