Files
medical-mall/utils/homeServiceBookingTime.uts

269 lines
7.6 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 居家上门服务预约时间工具
// 负责:动态日期生成、常用/自定义时间段生成、时间合法性校验
// 约束:所有计算基于真实当前时间,不写死任何展示文本
// ============================================================
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()
}