Files
cheflinkuser/src/pages/address/reservation-time.vue
T
2026-04-11 11:55:03 +08:00

761 lines
21 KiB
Vue
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.
<script setup lang="ts">
import {EventEnum} from "@/constant/enums";
const { t } = useI18n();
import dayjs from "dayjs";
// 店铺的营业时间(示例:周一到周五营业,周六周日不营业)
// 测试用的营业时间字符串
// MONDAY/TUESDAY/WEDNESDAY 09:00-18:00;THURSDAY/FRIDAY 08:00-09:00; // 周一到周五9点到18点,周四到周五8点到9点
// MONDAY/TUESDAY/WEDNESDAY 09:00-18:00;THURSDAY/FRIDAY 08:00-12:00;SATURDAY/SUNDAY 10:00-20:00 // 周六周日10点到20点
// MONDAY/TUESDAY/WEDNESDAY 09:00-16:00
// MONDAY 09:00-18:00;TUESDAY 09:00-10:00;WEDNESDAY 09:00-18:00;THURSDAY 09:00-18:00;FRIDAY 08:00-09:00
const storeBusinessHours = ref('');
// 是否仅选择日期(当前需求:只选哪一天配送,不选具体时间段)
const onlySelectDay = ref(true);
// 业务规则:只能预约周一 / 周四 / 周五
// JS 中:0-周日 1-周一 ... 4-周四 5-周五
const allowedWeekdays = [1, 4, 5];
const isAllowedDay = (date: Date): boolean => {
const dayIndex = date.getDay();
return allowedWeekdays.includes(dayIndex);
};
// 解析商家营业时间的接口
interface BusinessHours {
days: string[]; // 营业的星期几
startTime: string; // 开始时间 HH:mm
endTime: string; // 结束时间 HH:mm
}
/**
* 解析商家营业时间字符串
* @param businessHoursStr 营业时间字符串,支持多种格式:
* - MONDAY/TUESDAY/WEDNESDAY 09:00-18:00;THURSDAY/FRIDAY/SATURDAY/SUNDAY 10:00-20:00
* - MONDAY/TUESDAY/WEDNESDAY 09:00-18:00
* - MONDAY 09:00-18:00;TUESDAY 09:00-10:00;WEDNESDAY 09:00-18:00
* @returns 解析后的营业时间数组
*/
const parseBusinessHours = (businessHoursStr: string): BusinessHours[] => {
if (!businessHoursStr) return [];
const businessHours: BusinessHours[] = [];
// 使用分号分割不同的营业时间段
const segments = businessHoursStr.split(";");
segments.forEach((segment) => {
const trimmedSegment = segment.trim();
if (!trimmedSegment) return;
// 使用空格分割星期几和时间
const parts = trimmedSegment.split(" ");
if (parts.length !== 2) return;
const dayStr = parts[0].trim().toUpperCase();
const timeStr = parts[1];
// 解析时间范围
const timeRange = timeStr.split("-");
if (timeRange.length !== 2) return;
const startTime = timeRange[0].trim();
const endTime = timeRange[1].trim();
// 按斜杠分割星期几,支持 MONDAY/TUESDAY/WEDNESDAY 格式
const days = dayStr
.split("/")
.map((day) => day.trim())
.filter((day) => day);
businessHours.push({
days, // 支持多个星期几共享同一营业时间
startTime,
endTime,
});
});
return businessHours;
};
/**
* 获取指定日期的营业时间
* @param date 日期
* @returns 营业时间对象,如果不营业返回null
*/
const getBusinessHoursForDate = (date: Date): BusinessHours | null => {
if (!storeBusinessHours.value) return null;
const businessHours = parseBusinessHours(storeBusinessHours.value);
// 获取星期几的英文名称(确保是大写)(不要国际化导致判断失效)
const dayNames = [
"SUNDAY",
"MONDAY",
"TUESDAY",
"WEDNESDAY",
"THURSDAY",
"FRIDAY",
"SATURDAY",
];
const dayName = dayNames[date.getDay()];
// 查找包含当前星期几的营业时间
return businessHours.find((hours) => hours.days.includes(dayName)) || null;
};
/**
* 检查日期是否营业
* @param date 日期
* @returns 是否营业
*/
const isDateOpen = (date: Date): boolean => {
if (!storeBusinessHours.value) return true; // 如果没有营业时间限制,默认营业
return getBusinessHoursForDate(date) !== null;
};
/**
* 检查日期是否应该显示为可选择状态(营业且有可用时间段)
* @param date 日期
* @returns 是否可选择
*/
const isDateSelectable = (date: Date): boolean => {
// 新增:限制只能预约周一 / 周四 / 周五
if (!isAllowedDay(date)) {
return false;
}
// 如果只选日期模式,营业即可选择
if (onlySelectDay.value) {
if (!storeBusinessHours.value) return true;
return isDateOpen(date);
}
// 原逻辑:需要营业且有可用时间段
if (!storeBusinessHours.value) return true; // 如果没有营业时间限制,默认可选择
if (!isDateOpen(date)) return false;
return hasAvailableTimeSlots(date);
};
// 生成未来 7 天:设计稿为「本周」5 个圆 +「下周」2 个圆
const dateOptions = computed(() => {
const dates: Date[] = [];
const today = new Date();
for (let i = 0; i < 7; i++) {
const date = new Date(today);
date.setDate(today.getDate() + i);
dates.push(date);
}
return dates;
});
const thisWeekDates = computed(() => dateOptions.value.slice(0, 5));
const nextWeekDates = computed(() => dateOptions.value.slice(5, 7));
// 状态管理 - 初始化为第一个营业日期
const selectedDate = ref<Date>();
// 检查指定时间是否在营业时间内
const isTimeInBusinessHours = (
hour: number,
minute: number,
businessHours: BusinessHours
): boolean => {
const timeStr = `${hour.toString().padStart(2, "0")}:${minute
.toString()
.padStart(2, "0")}`;
return timeStr >= businessHours.startTime && timeStr <= businessHours.endTime;
};
// 检查指定日期是否有可用时间段
const hasAvailableTimeSlots = (date: Date): boolean => {
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
// 获取指定日期的营业时间
const businessHours = getBusinessHoursForDate(date);
// 如果没有营业时间,返回false
if (!businessHours) {
return false;
}
// 解析营业时间的开始和结束时间
const [startHour, startMinute] = businessHours.startTime
.split(":")
.map(Number);
const [endHour, endMinute] = businessHours.endTime.split(":").map(Number);
// 检查是否有可用时间段
for (let hour = startHour; hour <= endHour; hour++) {
for (let minute = 0; minute < 60; minute += 30) {
// 检查时间段开始时间是否在营业时间内
if (!isTimeInBusinessHours(hour, minute, businessHours)) {
continue;
}
// 计算时间段结束时间
let nextHour = hour;
let nextMinute = minute + 30;
if (nextMinute >= 60) {
nextHour++;
nextMinute = 0;
}
// 检查时间段结束时间是否超出营业时间
if (!isTimeInBusinessHours(nextHour, nextMinute, businessHours)) {
if (
nextHour > endHour ||
(nextHour === endHour && nextMinute > endMinute)
) {
nextHour = endHour;
nextMinute = endMinute;
}
}
// 避免生成开始时间和结束时间相同的无效时间段
if (hour === nextHour && minute === nextMinute) {
continue;
}
// 如果是今天,过滤掉已经过去的时间
if (date.toDateString() === now.toDateString()) {
if (
hour < currentHour ||
(hour === currentHour && minute <= currentMinute)
) {
continue;
}
}
// 如果能到这里,说明有可用时间段
return true;
}
}
return false;
};
// 初始化选中日期为第一个有可用时间段的营业日期
const initializeSelectedDate = () => {
if (onlySelectDay.value) {
// 仅选日期模式:选择第一个“允许预约且营业”的日期(或第一个允许的日期)
const firstOpen = dateOptions.value.find(
(d) => isAllowedDay(d) && isDateOpen(d)
);
const firstAllowed = firstOpen || dateOptions.value.find((d) => isAllowedDay(d));
selectedDate.value = firstAllowed || dateOptions.value[0];
return;
}
// 非仅选日期模式:保留原逻辑
for (const date of dateOptions.value) {
if (isDateSelectable(date)) {
selectedDate.value = date;
return;
}
}
const today = new Date();
const nextBusinessDate = findNextBusinessDate(today);
if (nextBusinessDate) {
selectedDate.value = nextBusinessDate;
nextTick(() => {
uni.showToast({
title: t('pages.address.reservationTime.currentTimeExpired'),
icon: "none",
duration: 2000,
});
});
} else {
selectedDate.value = dateOptions.value[0];
}
};
// 监听dateOptions变化,初始化选中日期
watch(
dateOptions,
() => {
if (!selectedDate.value) {
initializeSelectedDate();
}
},
{ immediate: true }
);
// 监听营业时间字符串变化,重新初始化选中日期
watch(
storeBusinessHours,
() => {
if (storeBusinessHours.value) {
initializeSelectedDate();
}
},
{ immediate: true }
);
const selectedTimeSlot = ref<string>("");
/** 圆圈内上行:星期(与全局 dayjs 语言一致) */
const formatWeekdayCircle = (date: Date) => dayjs(date).format("dddd");
/** 圆圈内下行:MM/DD;不可选时显示文案 */
const formatCircleSubLine = (date: Date) => {
if (!isDateSelectable(date)) {
return t("pages.address.reservationTime.notAvailable");
}
return dayjs(date).format("MM/DD");
};
/**
* 检查时间是否在营业时间内
* @param hour 小时
* @param minute 分钟
* @param businessHours 营业时间对象
* @returns 是否在营业时间内
*/
// 生成时间段选项(根据商家营业时间过滤)
const timeSlots = computed(() => {
const slots: string[] = [];
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
// 如果还没有选中日期,返回空数组
if (!selectedDate.value) {
return slots;
}
// 获取选中日期的营业时间
const businessHours = getBusinessHoursForDate(selectedDate.value);
// 如果没有营业时间限制,使用原有逻辑(0-24小时)
if (!businessHours) {
for (let hour = 0; hour < 24; hour++) {
for (let minute = 0; minute < 60; minute += 30) {
// 如果是今天,过滤掉已经过去的时间
if (selectedDate.value.toDateString() === now.toDateString()) {
if (
hour < currentHour ||
(hour === currentHour && minute <= currentMinute)
) {
continue;
}
}
const startHour = hour.toString().padStart(2, "0");
const startMinute = minute.toString().padStart(2, "0");
const endHour =
minute === 30
? (hour + 1).toString().padStart(2, "0")
: hour.toString().padStart(2, "0");
const endMinute = minute === 30 ? "00" : "30";
const timeSlot = `${startHour}:${startMinute} - ${endHour}:${endMinute}`;
slots.push(timeSlot);
}
}
return slots;
}
// 解析营业时间的开始和结束时间
const [startHour, startMinute] = businessHours.startTime
.split(":")
.map(Number);
const [endHour, endMinute] = businessHours.endTime.split(":").map(Number);
// 生成营业时间内的时间段
for (let hour = startHour; hour <= endHour; hour++) {
for (let minute = 0; minute < 60; minute += 30) {
// 检查时间段开始时间是否在营业时间内
if (!isTimeInBusinessHours(hour, minute, businessHours)) {
continue;
}
// 计算时间段结束时间
let nextHour = hour;
let nextMinute = minute + 30;
if (nextMinute >= 60) {
nextHour++;
nextMinute = 0;
}
// 检查时间段结束时间是否超出营业时间
if (!isTimeInBusinessHours(nextHour, nextMinute, businessHours)) {
// 如果结束时间超出营业时间,但开始时间在营业时间内,则调整结束时间为营业结束时间
if (
nextHour > endHour ||
(nextHour === endHour && nextMinute > endMinute)
) {
nextHour = endHour;
nextMinute = endMinute;
}
}
// 避免生成开始时间和结束时间相同的无效时间段(如 18:00 - 18:00
if (hour === nextHour && minute === nextMinute) {
continue;
}
// 如果是今天,过滤掉已经过去的时间
if (selectedDate.value.toDateString() === now.toDateString()) {
if (
hour < currentHour ||
(hour === currentHour && minute <= currentMinute)
) {
continue;
}
}
const startHourStr = hour.toString().padStart(2, "0");
const startMinuteStr = minute.toString().padStart(2, "0");
const endHourStr = nextHour.toString().padStart(2, "0");
const endMinuteStr = nextMinute.toString().padStart(2, "0");
const timeSlot = `${startHourStr}:${startMinuteStr} - ${endHourStr}:${endMinuteStr}`;
slots.push(timeSlot);
}
}
return slots;
});
// 监听时间段变化,如果当前选中日期没有可用时间段,自动选择下一个营业日期
watch(timeSlots, (newSlots) => {
if (onlySelectDay.value) return; // 仅选日期模式不需要处理时间段
if (selectedDate.value && newSlots.length === 0) {
// 当前选中日期没有可用时间段,寻找下一个营业日期
const nextBusinessDate = findNextBusinessDate(selectedDate.value);
if (nextBusinessDate) {
selectedDate.value = nextBusinessDate;
selectedTimeSlot.value = ""; // 清空已选择的时间段
uni.showToast({
title: t('pages.address.reservationTime.noAvailableTime'),
icon: "none",
duration: 2000,
});
}
}
});
// 寻找下一个有可用时间段的营业日期
const findNextBusinessDate = (currentDate: Date): Date | null => {
const maxDays = 30; // 最多向前查找30天
for (let i = 1; i <= maxDays; i++) {
const nextDate = new Date(currentDate);
nextDate.setDate(currentDate.getDate() + i);
// 检查是否在dateOptions范围内
const isInRange = dateOptions.value.some((date) =>
dayjs(date).isSame(dayjs(nextDate), "day")
);
if (
isInRange &&
isAllowedDay(nextDate) &&
isDateOpen(nextDate) &&
hasAvailableTimeSlots(nextDate)
) {
return nextDate;
}
}
return null;
};
// 选择日期
const selectDate = (date: Date) => {
// 检查日期是否可选择,如果不可选择则不允许选择
if (!isDateSelectable(date)) {
uni.showToast({
title: t('pages.address.reservationTime.dateNotSelectable'),
icon: "none",
});
return;
}
selectedDate.value = date;
// 选择新日期后,清空已选择的时间段
selectedTimeSlot.value = "";
};
// 选择时间段
const selectTimeSlot = (timeSlot: string) => {
selectedTimeSlot.value = timeSlot;
};
const isDateSelected = (date: Date) =>
!!selectedDate.value && dayjs(selectedDate.value).isSame(dayjs(date), "day");
// 提交预约
const submitReservation = () => {
const dateVal = selectedDate.value;
if (!dateVal) {
return;
}
// 非仅选日期模式,需要选择时间段
if (!onlySelectDay.value) {
if (!selectedTimeSlot.value) {
uni.showToast({
title: t('pages.address.reservationTime.selectTimeSlot'),
icon: "none",
});
return;
}
}
// 计算开始/结束时间
const selectedDateDayjs = dayjs(dateVal);
let startTime: dayjs.Dayjs;
let endTime: dayjs.Dayjs;
if (onlySelectDay.value) {
// 仅选日期:优先使用营业时间范围;若无营业时间限制,则使用当天起止
const bh = getBusinessHoursForDate(dateVal);
if (bh) {
const [startHour, startMinute] = bh.startTime.split(':').map(Number);
const [endHour, endMinute] = bh.endTime.split(':').map(Number);
startTime = selectedDateDayjs
.hour(startHour)
.minute(startMinute)
.second(0)
.millisecond(0);
endTime = selectedDateDayjs
.hour(endHour)
.minute(endMinute)
.second(0)
.millisecond(0);
} else {
startTime = selectedDateDayjs.startOf('day');
endTime = selectedDateDayjs.endOf('day');
}
} else {
// 选择了时间段:解析并生成起止时间
const [startTimeStr, endTimeStr] = selectedTimeSlot.value.split(' - ');
const [startHour, startMinute] = startTimeStr.split(':').map(Number);
const [endHour, endMinute] = endTimeStr.split(':').map(Number);
startTime = selectedDateDayjs
.hour(startHour)
.minute(startMinute)
.second(0)
.millisecond(0);
endTime = selectedDateDayjs
.hour(endHour)
.minute(endMinute)
.second(0)
.millisecond(0);
}
console.log("预约信息:", {
date: dateVal,
timeSlot: onlySelectDay.value ? '' : selectedTimeSlot.value,
startTime: startTime.valueOf(),
endTime: endTime.valueOf(),
});
uni.$emit(EventEnum.CHOOSE_APPOINTMENT_TIME, {
date: dateVal,
timeSlot: onlySelectDay.value ? '' : selectedTimeSlot.value,
startTime: startTime.valueOf(),
endTime: endTime.valueOf(),
});
uni.showToast({
title: t('pages.address.reservationTime.reservationSuccess'),
icon: "none",
});
uni.navigateBack();
};
const storeId = ref(null);
// 页面加载时处理参数
onLoad((options: any) => {
if (options.storeId) {
storeId.value = options.storeId;
}
if (options.storeBusinessHours) {
storeBusinessHours.value = options.storeBusinessHours;
// 如果传递了该参数,进入仅选日期模式
onlySelectDay.value = true;
}
// 无论是否传参,统一初始化选中日期,避免首屏未选导致不显示时间段
nextTick(() => {
initializeSelectedDate();
});
});
</script>
<template>
<view class="reservation-time-page min-h-screen pb-180rpx">
<navbar
:title="t('pages.address.reservationTime.pageTitle')"
circle-back
custom-class="reservation-time-navbar"
/>
<view class="reservation-time-body px-40rpx pt-40rpx">
<view class="date-section">
<text class="section-label">{{ t("pages.address.reservationTime.thisWeek") }}</text>
<view class="date-row">
<view
v-for="(item, index) in thisWeekDates"
:key="index"
class="date-circle"
:class="{
'date-circle--selected': isDateSelected(item),
'date-circle--disabled': !isDateSelectable(item),
}"
@click="selectDate(item)"
>
<text class="date-circle__weekday">{{ formatWeekdayCircle(item) }}</text>
<text class="date-circle__sub">{{ formatCircleSubLine(item) }}</text>
</view>
</view>
</view>
<view v-if="nextWeekDates.length" class="date-section date-section--next">
<text class="section-label">{{ t("pages.address.reservationTime.nextWeek") }}</text>
<view class="date-row">
<view
v-for="(item, index) in nextWeekDates"
:key="index"
class="date-circle"
:class="{
'date-circle--selected': isDateSelected(item),
'date-circle--disabled': !isDateSelectable(item),
}"
@click="selectDate(item)"
>
<text class="date-circle__weekday">{{ formatWeekdayCircle(item) }}</text>
<text class="date-circle__sub">{{ formatCircleSubLine(item) }}</text>
</view>
</view>
</view>
</view>
<!-- 时间段选择区域在仅选日期模式下隐藏 -->
<view v-if="!onlySelectDay" class="pb-138rpx mx-40rpx bg-white rounded-24rpx overflow-hidden mt-24rpx">
<view
v-for="(timeSlot, index) in timeSlots"
:key="index"
class="h-108rpx flex-center-sb px-30rpx"
:class="[
index === 0 ? '' : 'border-top',
timeSlots.length - 1 === index ? 'border-bottom' : '',
]"
@click="selectTimeSlot(timeSlot)"
>
<text class="text-32rpx font-regular">{{ timeSlot }}</text>
<!-- 单选按钮 -->
<image
:src="
selectedTimeSlot === timeSlot
? '/static/images/chef/133.png'
: '/static/images/chef/134.png'
"
class="w-48rpx h-48rpx shrink-0"
mode="aspectFit"
/>
</view>
</view>
<fixed-bottom-large-btn
class="z-100"
fixed
:text="`${t('common.submit')}`"
@click="submitReservation"
/>
</view>
</template>
<style scoped lang="scss">
.reservation-time-body {
box-sizing: border-box;
}
.date-section {
margin-bottom: 48rpx;
&--next {
margin-bottom: 0;
}
}
.section-label {
display: block;
font-size: 32rpx;
line-height: 44rpx;
color: #111;
font-weight: 500;
margin-bottom: 24rpx;
}
.date-row {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 20rpx;
justify-content: flex-start;
}
.date-circle {
width: 118rpx;
height: 118rpx;
border-radius: 50%;
background-color: #fff;
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-sizing: border-box;
}
.date-circle__weekday {
font-size: 26rpx;
line-height: 32rpx;
color: #111;
font-weight: 500;
}
.date-circle__sub {
font-size: 22rpx;
line-height: 28rpx;
margin-top: 6rpx;
color: #111;
opacity: 0.88;
}
.date-circle--selected {
background-color: #111;
box-shadow: none;
.date-circle__weekday,
.date-circle__sub {
color: #fff;
opacity: 1;
}
}
.date-circle--disabled {
opacity: 0.45;
}
.date-circle--disabled.date-circle--selected {
opacity: 1;
}
</style>
<style lang="scss">
page {
background-color: #f7f7f9;
}
.reservation-time-page :deep(.reservation-time-navbar.wd-navbar) {
background-color: #f7f7f9 !important;
}
</style>