修改样式
This commit is contained in:
@@ -1,75 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import {EventEnum} from "@/constant/enums";
|
||||
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];
|
||||
import { parseDeliveryScheduleTimes, isDeliveryScheduleDay } from '@/utils/deliverySchedule';
|
||||
|
||||
const allowedWeekdays = ref<number[]>([]);
|
||||
const isAllowedDay = (date: Date): boolean => {
|
||||
const dayIndex = date.getDay();
|
||||
return allowedWeekdays.includes(dayIndex);
|
||||
return isDeliveryScheduleDay(date, allowedWeekdays.value);
|
||||
};
|
||||
|
||||
// 解析商家营业时间的接口
|
||||
interface BusinessHours {
|
||||
days: string[]; // 营业的星期几
|
||||
startTime: string; // 开始时间 HH:mm
|
||||
endTime: string; // 结束时间 HH:mm
|
||||
days: string[];
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析商家营业时间字符串
|
||||
* @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, // 支持多个星期几共享同一营业时间
|
||||
days,
|
||||
startTime,
|
||||
endTime,
|
||||
});
|
||||
@@ -78,17 +57,11 @@ const parseBusinessHours = (businessHoursStr: string): BusinessHours[] => {
|
||||
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",
|
||||
@@ -100,44 +73,29 @@ const getBusinessHoursForDate = (date: Date): BusinessHours | null => {
|
||||
];
|
||||
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; // 如果没有营业时间限制,默认营业
|
||||
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 (!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();
|
||||
@@ -154,10 +112,8 @@ const dateOptions = computed(() => {
|
||||
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,
|
||||
@@ -169,35 +125,28 @@ const isTimeInBusinessHours = (
|
||||
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) {
|
||||
@@ -205,7 +154,6 @@ const hasAvailableTimeSlots = (date: Date): boolean => {
|
||||
nextMinute = 0;
|
||||
}
|
||||
|
||||
// 检查时间段结束时间是否超出营业时间
|
||||
if (!isTimeInBusinessHours(nextHour, nextMinute, businessHours)) {
|
||||
if (
|
||||
nextHour > endHour ||
|
||||
@@ -216,12 +164,10 @@ const hasAvailableTimeSlots = (date: Date): boolean => {
|
||||
}
|
||||
}
|
||||
|
||||
// 避免生成开始时间和结束时间相同的无效时间段
|
||||
if (hour === nextHour && minute === nextMinute) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果是今天,过滤掉已经过去的时间
|
||||
if (date.toDateString() === now.toDateString()) {
|
||||
if (
|
||||
hour < currentHour ||
|
||||
@@ -231,7 +177,6 @@ const hasAvailableTimeSlots = (date: Date): boolean => {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果能到这里,说明有可用时间段
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -239,69 +184,55 @@ const hasAvailableTimeSlots = (date: Date): boolean => {
|
||||
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];
|
||||
const userPickedDate = ref(false);
|
||||
|
||||
/** 从今天起查找最近可配送日(配送日 + 营业时间) */
|
||||
const findNearestDeliverableDate = (maxDays = 30): Date | null => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
for (let i = 0; i <= maxDays; i++) {
|
||||
const d = new Date(today);
|
||||
d.setDate(today.getDate() + i);
|
||||
if (isDateSelectable(d)) {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const initializeSelectedDate = (resetUserPick = false) => {
|
||||
if (resetUserPick) {
|
||||
userPickedDate.value = false;
|
||||
}
|
||||
if (userPickedDate.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 非仅选日期模式:保留原逻辑
|
||||
for (const date of dateOptions.value) {
|
||||
if (isDateSelectable(date)) {
|
||||
selectedDate.value = date;
|
||||
return;
|
||||
}
|
||||
const nearest = findNearestDeliverableDate(30);
|
||||
if (nearest) {
|
||||
selectedDate.value = nearest;
|
||||
selectedTimeSlot.value = "";
|
||||
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];
|
||||
}
|
||||
selectedDate.value = dateOptions.value[0] ?? new Date();
|
||||
selectedTimeSlot.value = "";
|
||||
};
|
||||
|
||||
// 监听dateOptions变化,初始化选中日期
|
||||
watch(
|
||||
dateOptions,
|
||||
() => [...allowedWeekdays.value],
|
||||
() => {
|
||||
if (!selectedDate.value) {
|
||||
initializeSelectedDate();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
initializeSelectedDate();
|
||||
}
|
||||
);
|
||||
|
||||
// 监听营业时间字符串变化,重新初始化选中日期
|
||||
watch(
|
||||
storeBusinessHours,
|
||||
() => {
|
||||
if (storeBusinessHours.value) {
|
||||
initializeSelectedDate();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
watch(storeBusinessHours, () => {
|
||||
initializeSelectedDate();
|
||||
});
|
||||
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");
|
||||
@@ -309,34 +240,21 @@ const formatCircleSubLine = (date: Date) => {
|
||||
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 ||
|
||||
@@ -361,21 +279,17 @@ const timeSlots = computed(() => {
|
||||
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) {
|
||||
@@ -383,9 +297,7 @@ const timeSlots = computed(() => {
|
||||
nextMinute = 0;
|
||||
}
|
||||
|
||||
// 检查时间段结束时间是否超出营业时间
|
||||
if (!isTimeInBusinessHours(nextHour, nextMinute, businessHours)) {
|
||||
// 如果结束时间超出营业时间,但开始时间在营业时间内,则调整结束时间为营业结束时间
|
||||
if (
|
||||
nextHour > endHour ||
|
||||
(nextHour === endHour && nextMinute > endMinute)
|
||||
@@ -395,12 +307,10 @@ const timeSlots = computed(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// 避免生成开始时间和结束时间相同的无效时间段(如 18:00 - 18:00)
|
||||
if (hour === nextHour && minute === nextMinute) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果是今天,过滤掉已经过去的时间
|
||||
if (selectedDate.value.toDateString() === now.toDateString()) {
|
||||
if (
|
||||
hour < currentHour ||
|
||||
@@ -423,11 +333,9 @@ const timeSlots = computed(() => {
|
||||
return slots;
|
||||
});
|
||||
|
||||
// 监听时间段变化,如果当前选中日期没有可用时间段,自动选择下一个营业日期
|
||||
watch(timeSlots, (newSlots) => {
|
||||
if (onlySelectDay.value) return; // 仅选日期模式不需要处理时间段
|
||||
if (onlySelectDay.value) return;
|
||||
if (selectedDate.value && newSlots.length === 0) {
|
||||
// 当前选中日期没有可用时间段,寻找下一个营业日期
|
||||
const nextBusinessDate = findNextBusinessDate(selectedDate.value);
|
||||
if (nextBusinessDate) {
|
||||
selectedDate.value = nextBusinessDate;
|
||||
@@ -441,14 +349,12 @@ watch(timeSlots, (newSlots) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 寻找下一个有可用时间段的营业日期
|
||||
const findNextBusinessDate = (currentDate: Date): Date | null => {
|
||||
const maxDays = 30; // 最多向前查找30天
|
||||
const maxDays = 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")
|
||||
);
|
||||
@@ -465,9 +371,7 @@ const findNextBusinessDate = (currentDate: Date): Date | null => {
|
||||
return null;
|
||||
};
|
||||
|
||||
// 选择日期
|
||||
const selectDate = (date: Date) => {
|
||||
// 检查日期是否可选择,如果不可选择则不允许选择
|
||||
if (!isDateSelectable(date)) {
|
||||
uni.showToast({
|
||||
title: t('pages.address.reservationTime.dateNotSelectable'),
|
||||
@@ -476,12 +380,11 @@ const selectDate = (date: Date) => {
|
||||
return;
|
||||
}
|
||||
|
||||
userPickedDate.value = true;
|
||||
selectedDate.value = date;
|
||||
// 选择新日期后,清空已选择的时间段
|
||||
selectedTimeSlot.value = "";
|
||||
};
|
||||
|
||||
// 选择时间段
|
||||
const selectTimeSlot = (timeSlot: string) => {
|
||||
selectedTimeSlot.value = timeSlot;
|
||||
};
|
||||
@@ -489,14 +392,12 @@ const selectTimeSlot = (timeSlot: string) => {
|
||||
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({
|
||||
@@ -507,13 +408,11 @@ const submitReservation = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 计算开始/结束时间
|
||||
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);
|
||||
@@ -533,7 +432,6 @@ const submitReservation = () => {
|
||||
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);
|
||||
@@ -557,6 +455,7 @@ const submitReservation = () => {
|
||||
});
|
||||
|
||||
uni.$emit(EventEnum.CHOOSE_APPOINTMENT_TIME, {
|
||||
merchantId: storeId.value,
|
||||
date: dateVal,
|
||||
timeSlot: onlySelectDay.value ? '' : selectedTimeSlot.value,
|
||||
startTime: startTime.valueOf(),
|
||||
@@ -573,19 +472,23 @@ const submitReservation = () => {
|
||||
|
||||
const storeId = ref(null);
|
||||
|
||||
// 页面加载时处理参数
|
||||
onLoad((options: any) => {
|
||||
if (options.storeId) {
|
||||
if (options.merchantId) {
|
||||
storeId.value = options.merchantId;
|
||||
} else if (options.storeId) {
|
||||
storeId.value = options.storeId;
|
||||
}
|
||||
if (options.deliveryScheduleTimes) {
|
||||
allowedWeekdays.value = parseDeliveryScheduleTimes(
|
||||
decodeURIComponent(options.deliveryScheduleTimes)
|
||||
);
|
||||
}
|
||||
if (options.storeBusinessHours) {
|
||||
storeBusinessHours.value = options.storeBusinessHours;
|
||||
// 如果传递了该参数,进入仅选日期模式
|
||||
storeBusinessHours.value = decodeURIComponent(options.storeBusinessHours);
|
||||
onlySelectDay.value = true;
|
||||
}
|
||||
// 无论是否传参,统一初始化选中日期,避免首屏未选导致不显示时间段
|
||||
nextTick(() => {
|
||||
initializeSelectedDate();
|
||||
initializeSelectedDate(true);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -635,7 +538,6 @@ onLoad((options: any) => {
|
||||
</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"
|
||||
@@ -648,7 +550,6 @@ onLoad((options: any) => {
|
||||
@click="selectTimeSlot(timeSlot)"
|
||||
>
|
||||
<text class="text-32rpx font-regular">{{ timeSlot }}</text>
|
||||
<!-- 单选按钮 -->
|
||||
<image
|
||||
:src="
|
||||
selectedTimeSlot === timeSlot
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
const { t } = useI18n();
|
||||
const keyword = ref('');
|
||||
function handleSearch(){
|
||||
console.log("1111111111");
|
||||
|
||||
}
|
||||
function handleClickItem(){
|
||||
|
||||
@@ -1,79 +1,30 @@
|
||||
<template>
|
||||
<view class="class-bullet-container">
|
||||
<!-- 第一行:跑马灯 + 手动拖动 -->
|
||||
<view
|
||||
class="scroll-row"
|
||||
@touchstart="onTouchStart1"
|
||||
@touchmove.stop.prevent="onTouchMove1"
|
||||
@touchend="onTouchEnd1"
|
||||
@touchcancel="onTouchEnd1"
|
||||
>
|
||||
<view class="marquee-viewport">
|
||||
<view class="marquee-track" :style="trackStyle1">
|
||||
<view class="track-group row1-group-a">
|
||||
<view
|
||||
v-for="(item, idx) in categories"
|
||||
:key="item.id + '-row1-a-' + idx"
|
||||
class="category-item"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<image v-if="item.categoryImage || item.logoUrl" :src="item.categoryImage || item.logoUrl" class="category-icon" mode="aspectFit" />
|
||||
<text class="category-text">{{ item.categoryName || item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="track-group">
|
||||
<view
|
||||
v-for="(item, idx) in categories"
|
||||
:key="item.id + '-row1-b-' + idx"
|
||||
class="category-item"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<image v-if="item.categoryImage || item.logoUrl" :src="item.categoryImage || item.logoUrl" class="category-icon" mode="aspectFit" />
|
||||
<text class="category-text">{{ item.categoryName || item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="category-grid">
|
||||
<view
|
||||
v-for="item in topCategories"
|
||||
:key="item.id"
|
||||
class="category-item"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<image
|
||||
v-if="item.categoryImage || item.logoUrl"
|
||||
:src="item.categoryImage || item.logoUrl"
|
||||
class="category-icon"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="category-text">{{ item.categoryName || item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 第二行:同向跑马灯 + 手动拖动 -->
|
||||
<view
|
||||
class="scroll-row"
|
||||
@touchstart="onTouchStart2"
|
||||
@touchmove.stop.prevent="onTouchMove2"
|
||||
@touchend="onTouchEnd2"
|
||||
@touchcancel="onTouchEnd2"
|
||||
>
|
||||
<view class="marquee-viewport">
|
||||
<view class="marquee-track" :style="trackStyle2">
|
||||
<view class="track-group row2-group-a">
|
||||
<view
|
||||
v-for="(item, idx) in categories"
|
||||
:key="item.id + '-row2-a-' + idx"
|
||||
class="category-item"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<image v-if="item.categoryImage || item.logoUrl" :src="item.categoryImage || item.logoUrl" class="category-icon" mode="aspectFit" />
|
||||
<text class="category-text">{{ item.categoryName || item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-for="(item, idx) in categories"
|
||||
:key="item.id + '-row2-b-' + idx"
|
||||
class="category-item"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<image v-if="item.categoryImage || item.logoUrl" :src="item.categoryImage || item.logoUrl" class="category-icon" mode="aspectFit" />
|
||||
<text class="category-text">{{ item.categoryName || item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="category-item" @click="handleMoreClick">
|
||||
<image src="@/static/app/images/more.png" class="category-icon" mode="aspectFit" />
|
||||
<text class="category-text">更多</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, onUnmounted, nextTick, getCurrentInstance, watch } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useCategoryNavStore } from '@/store'
|
||||
|
||||
// 定义分类项接口(与模板字段一致)
|
||||
@@ -118,114 +69,7 @@ const mockCategories: CategoryItem[] = [
|
||||
const categories = computed(() => {
|
||||
return props.categories.length > 0 ? props.categories : mockCategories
|
||||
})
|
||||
|
||||
const offset1 = ref(0)
|
||||
const offset2 = ref(0)
|
||||
const loopWidth1 = ref(0)
|
||||
const loopWidth2 = ref(0)
|
||||
const isTouching1 = ref(false)
|
||||
const isTouching2 = ref(false)
|
||||
const touchStartX1 = ref(0)
|
||||
const touchStartX2 = ref(0)
|
||||
const touchStartOffset1 = ref(0)
|
||||
const touchStartOffset2 = ref(0)
|
||||
|
||||
let resumeTimer1: ReturnType<typeof setTimeout> | null = null
|
||||
let resumeTimer2: ReturnType<typeof setTimeout> | null = null
|
||||
let raf1 = 0
|
||||
let raf2 = 0
|
||||
let lastTs1 = 0
|
||||
let lastTs2 = 0
|
||||
const RESUME_DELAY = 800
|
||||
const PX_PER_SEC_1 = 22
|
||||
const PX_PER_SEC_2 = 18
|
||||
|
||||
const trackStyle1 = computed(() => ({
|
||||
transform: `translate3d(${-offset1.value}px, 0, 0)`,
|
||||
}))
|
||||
|
||||
const trackStyle2 = computed(() => ({
|
||||
transform: `translate3d(${-offset2.value}px, 0, 0)`,
|
||||
}))
|
||||
|
||||
function normalizeOffset(value: number, loopWidth: number) {
|
||||
if (loopWidth <= 0) return value
|
||||
let next = value % loopWidth
|
||||
if (next < 0) next += loopWidth
|
||||
return next
|
||||
}
|
||||
|
||||
function onTouchStart1(e: any) {
|
||||
const touch = e?.touches?.[0]
|
||||
isTouching1.value = true
|
||||
touchStartX1.value = Number(touch?.clientX || 0)
|
||||
touchStartOffset1.value = offset1.value
|
||||
if (resumeTimer1) {
|
||||
clearTimeout(resumeTimer1)
|
||||
resumeTimer1 = null
|
||||
}
|
||||
}
|
||||
|
||||
function onTouchMove1(e: any) {
|
||||
if (!isTouching1.value) return
|
||||
const touch = e?.touches?.[0]
|
||||
const x = Number(touch?.clientX || 0)
|
||||
const delta = x - touchStartX1.value
|
||||
offset1.value = normalizeOffset(touchStartOffset1.value - delta, loopWidth1.value)
|
||||
}
|
||||
|
||||
function onTouchEnd1() {
|
||||
resumeTimer1 = setTimeout(() => {
|
||||
isTouching1.value = false
|
||||
resumeTimer1 = null
|
||||
}, RESUME_DELAY)
|
||||
}
|
||||
|
||||
function onTouchStart2(e: any) {
|
||||
const touch = e?.touches?.[0]
|
||||
isTouching2.value = true
|
||||
touchStartX2.value = Number(touch?.clientX || 0)
|
||||
touchStartOffset2.value = offset2.value
|
||||
if (resumeTimer2) {
|
||||
clearTimeout(resumeTimer2)
|
||||
resumeTimer2 = null
|
||||
}
|
||||
}
|
||||
|
||||
function onTouchMove2(e: any) {
|
||||
if (!isTouching2.value) return
|
||||
const touch = e?.touches?.[0]
|
||||
const x = Number(touch?.clientX || 0)
|
||||
const delta = x - touchStartX2.value
|
||||
offset2.value = normalizeOffset(touchStartOffset2.value - delta, loopWidth2.value)
|
||||
}
|
||||
|
||||
function onTouchEnd2() {
|
||||
resumeTimer2 = setTimeout(() => {
|
||||
isTouching2.value = false
|
||||
resumeTimer2 = null
|
||||
}, RESUME_DELAY)
|
||||
}
|
||||
|
||||
function tickRow1(ts: number) {
|
||||
if (!lastTs1) lastTs1 = ts
|
||||
const dt = ts - lastTs1
|
||||
lastTs1 = ts
|
||||
if (!isTouching1.value) {
|
||||
offset1.value = normalizeOffset(offset1.value + (PX_PER_SEC_1 * dt) / 1000, loopWidth1.value)
|
||||
}
|
||||
raf1 = requestAnimationFrame(tickRow1)
|
||||
}
|
||||
|
||||
function tickRow2(ts: number) {
|
||||
if (!lastTs2) lastTs2 = ts
|
||||
const dt = ts - lastTs2
|
||||
lastTs2 = ts
|
||||
if (!isTouching2.value) {
|
||||
offset2.value = normalizeOffset(offset2.value + (PX_PER_SEC_2 * dt) / 1000, loopWidth2.value)
|
||||
}
|
||||
raf2 = requestAnimationFrame(tickRow2)
|
||||
}
|
||||
const topCategories = computed(() => categories.value.slice(0, 4))
|
||||
|
||||
// 点击处理(列表页菜谱 id 来自 store,不放在 URL query)
|
||||
const handleItemClick = (item: CategoryItem) => {
|
||||
@@ -238,123 +82,46 @@ function navigateTo(url: string) {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
function measureLoopWidths() {
|
||||
const instance = getCurrentInstance()
|
||||
if (!instance) return
|
||||
const query = uni.createSelectorQuery().in(instance.proxy)
|
||||
query.select('.row1-group-a').boundingClientRect()
|
||||
query.select('.row2-group-a').boundingClientRect()
|
||||
query.exec((res: Array<{ width?: number } | null>) => {
|
||||
const w1 = Number(res?.[0]?.width || 0)
|
||||
const w2 = Number(res?.[1]?.width || 0)
|
||||
loopWidth1.value = w1 > 0 ? w1 : 0
|
||||
loopWidth2.value = w2 > 0 ? w2 : 0
|
||||
})
|
||||
function handleMoreClick() {
|
||||
navigateTo('/pages-store/pages/list/index')
|
||||
}
|
||||
|
||||
function ensureMeasuredWidths(retry = 0) {
|
||||
measureLoopWidths()
|
||||
if (retry >= 8) return
|
||||
setTimeout(() => {
|
||||
if (loopWidth1.value > 0 && loopWidth2.value > 0) return
|
||||
ensureMeasuredWidths(retry + 1)
|
||||
}, 220)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
ensureMeasuredWidths()
|
||||
})
|
||||
raf1 = requestAnimationFrame(tickRow1)
|
||||
raf2 = requestAnimationFrame(tickRow2)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => categories.value.length,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
ensureMeasuredWidths()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (raf1) cancelAnimationFrame(raf1)
|
||||
if (raf2) cancelAnimationFrame(raf2)
|
||||
if (resumeTimer1) {
|
||||
clearTimeout(resumeTimer1)
|
||||
resumeTimer1 = null
|
||||
}
|
||||
if (resumeTimer2) {
|
||||
clearTimeout(resumeTimer2)
|
||||
resumeTimer2 = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.class-bullet-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.category-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
min-width: 0;
|
||||
min-height: 108rpx;
|
||||
padding: 8rpx 4rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 54rpx;
|
||||
height: 54rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-text {
|
||||
font-size: 22rpx;
|
||||
color: #333;
|
||||
line-height: 1.2;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
.scroll-row {
|
||||
margin-bottom: 12rpx;
|
||||
overflow: hidden;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.marquee-viewport {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.marquee-track {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
width: max-content;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.track-group {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 120rpx;
|
||||
height: 60rpx;
|
||||
margin-right: 20rpx;
|
||||
padding: 0 20rpx;
|
||||
background: #fff;
|
||||
border: none;
|
||||
border-radius: 30rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:active {
|
||||
transform: translateY(0) scale(0.96);
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-right: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -79,14 +79,14 @@ function subtitleLine(item: any): string {
|
||||
<image
|
||||
:src="getCardImages(item)[0]"
|
||||
class="featured-card__media-half featured-card__media-half--top"
|
||||
mode="aspectFill"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
</template>
|
||||
<image
|
||||
v-else-if="getCardImages(item).length >= 1"
|
||||
:src="getCardImages(item)[0]"
|
||||
class="featured-card__media-single"
|
||||
mode="aspectFill"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view v-else class="featured-card__media-single featured-card__media-placeholder" />
|
||||
</view>
|
||||
@@ -148,9 +148,9 @@ function subtitleLine(item: any): string {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 620rpx;
|
||||
height: 256rpx;
|
||||
min-height: 256rpx;
|
||||
width: 660rpx;
|
||||
height: 306rpx;
|
||||
min-height: 306rpx;
|
||||
margin-left: 16rpx;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
@@ -162,7 +162,7 @@ function subtitleLine(item: any): string {
|
||||
}
|
||||
|
||||
.featured-card__media {
|
||||
width: 232rpx;
|
||||
width: 323rpx;
|
||||
flex-shrink: 0;
|
||||
background: #f0f0f0;
|
||||
display: flex;
|
||||
@@ -204,23 +204,17 @@ function subtitleLine(item: any): string {
|
||||
}
|
||||
|
||||
.featured-card__name {
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-size: 28rpx;
|
||||
line-height: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
.featured-card__name--primary {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
.featured-card__name--secondary {
|
||||
font-size: 28rpx;
|
||||
line-height: 34rpx;
|
||||
color: #1a1a1a;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.featured-card__media-placeholder {
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
<view class="home-skeleton">
|
||||
<!-- 顶部头部(与 home-top-header 对齐) -->
|
||||
<view class="header-section">
|
||||
<view class="header-left">
|
||||
<view class="header-toolbar">
|
||||
<view class="brand-row">
|
||||
<view class="logo-skeleton skeleton-item"></view>
|
||||
<view class="app-title-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
<view class="delivery-info-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
<view class="header-right">
|
||||
<view class="location-pill-skeleton skeleton-item"></view>
|
||||
<view class="cart-btn-skeleton skeleton-item"></view>
|
||||
<view class="header-right">
|
||||
<view class="location-pill-skeleton skeleton-item"></view>
|
||||
<view class="cart-btn-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="notice-row-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索栏 + 消息按钮 -->
|
||||
@@ -121,19 +121,22 @@
|
||||
.header-section {
|
||||
padding: 18rpx 24rpx 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12rpx;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
.header-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.brand-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.logo-skeleton {
|
||||
@@ -149,10 +152,9 @@
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.delivery-info-skeleton {
|
||||
margin-top: 12rpx;
|
||||
width: 300rpx;
|
||||
height: 30rpx;
|
||||
.notice-row-skeleton {
|
||||
width: 100%;
|
||||
height: 32rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
@@ -160,6 +162,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.location-pill-skeleton {
|
||||
|
||||
@@ -1,50 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
appName: string
|
||||
locationText: string
|
||||
appointmentTimeShow?: string
|
||||
cartBadgeTotal?: number
|
||||
isLogin?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'clickAddress'): void
|
||||
(e: 'clickLocation'): void
|
||||
(e: 'clickCart'): void
|
||||
}>()
|
||||
|
||||
const noticeTexts = computed(() => [
|
||||
t('pages.home.noticeBar.thursday'),
|
||||
t('pages.home.noticeBar.sunday'),
|
||||
t('pages.home.noticeBar.freshCatch'),
|
||||
])
|
||||
|
||||
/** 横向无缝滚动:多条拼接为一条,重复一遍避免循环时跳变 */
|
||||
const noticeScrollText = computed(() => {
|
||||
const items = noticeTexts.value.filter(Boolean)
|
||||
if (!items.length) return ''
|
||||
const once = items.join(' ')
|
||||
return `${once} ${once}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="home-top-header px-24rpx pt-12rpx pb-8rpx">
|
||||
<view class="flex items-center justify-between gap-12rpx">
|
||||
<view class="min-w-0 flex-1">
|
||||
<view class="flex items-center gap-14rpx min-w-0">
|
||||
<image src="@img/logo.png" class="w-52rpx h-52rpx shrink-0" style="border-radius: 50%;"></image>
|
||||
<text class="text-32rpx lh-36rpx text-#1a1a1a font-bold tracking-tight shrink-0">{{ appName }}</text>
|
||||
</view>
|
||||
<view @click="emit('clickAddress')" class="home-delivery-row mt-10rpx flex items-center min-w-0 text-26rpx lh-32rpx text-#00A76D">
|
||||
<text v-if="appointmentTimeShow">{{ t('pages.address.appTime') }}: {{ appointmentTimeShow }}</text>
|
||||
<text v-else>{{ t('pages.address.reservation') }}</text>
|
||||
<image src="@img/chef/119.png" class="w-22rpx h-22rpx ml-8rpx shrink-0"></image>
|
||||
</view>
|
||||
<view class="home-top-header__toolbar flex items-center justify-between gap-12rpx">
|
||||
<view class="flex items-center gap-14rpx min-w-0 flex-1">
|
||||
<image
|
||||
src="@img/logo.png"
|
||||
class="w-52rpx h-52rpx shrink-0"
|
||||
style="border-radius: 50%"
|
||||
/>
|
||||
<text class="text-32rpx lh-36rpx text-#1a1a1a font-bold tracking-tight line-clamp-1">
|
||||
{{ appName }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex items-center gap-10rpx shrink-0">
|
||||
<view class="home-loc-pill" @click="emit('clickLocation')">
|
||||
<text class="home-loc-pill__text line-clamp-1">{{ locationText || t('pages.home.default-location') }}</text>
|
||||
<image src="@img/chef/119.png" class="home-loc-pill__arrow w-20rpx h-20rpx shrink-0 op-50 mt-2rpx"></image>
|
||||
<text class="home-loc-pill__text line-clamp-1">
|
||||
{{ locationText || t('pages.home.default-location') }}
|
||||
</text>
|
||||
<image
|
||||
src="@img/chef/119.png"
|
||||
class="home-loc-pill__arrow w-20rpx h-20rpx shrink-0 op-50 mt-2rpx"
|
||||
/>
|
||||
</view>
|
||||
<view class="home-cart-btn" @click="emit('clickCart')">
|
||||
<view class="i-carbon:shopping-cart text-36rpx text-#14181b"></view>
|
||||
<view v-if="isLogin && (cartBadgeTotal || 0) > 0" class="home-cart-badge">
|
||||
<view class="i-carbon:shopping-cart text-36rpx text-#14181b" />
|
||||
<view
|
||||
v-if="isLogin && (cartBadgeTotal || 0) > 0"
|
||||
class="home-cart-badge"
|
||||
>
|
||||
{{ (cartBadgeTotal || 0) > 99 ? '99+' : cartBadgeTotal }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="home-notice-wrap mt-10rpx">
|
||||
<wd-notice-bar
|
||||
:text="noticeScrollText"
|
||||
:delay="1"
|
||||
:speed="60"
|
||||
color="#00A76D"
|
||||
background-color="transparent"
|
||||
custom-class="home-notice-bar"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -53,6 +84,30 @@ const emit = defineEmits<{
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.home-notice-wrap {
|
||||
width: 100%;
|
||||
|
||||
:deep(.home-notice-bar) {
|
||||
padding: 0;
|
||||
font-size: 26rpx;
|
||||
line-height: 32rpx;
|
||||
border-radius: 0;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
:deep(.wd-notice-bar__wrap) {
|
||||
height: 32rpx;
|
||||
line-height: 32rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.wd-notice-bar__content) {
|
||||
font-size: 26rpx;
|
||||
line-height: 32rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.home-loc-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -102,4 +157,3 @@ const emit = defineEmits<{
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useUserStore } from "@/store";
|
||||
import { useUserStore } from '@/store';
|
||||
const userStore = useUserStore();
|
||||
const expanded = ref(true)
|
||||
const touchStartX = ref(0)
|
||||
|
||||
function navigateTo(url: string) {
|
||||
if(userStore.checkLogin()) {
|
||||
@@ -13,46 +10,19 @@ function navigateTo(url: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function togglePanel() {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
|
||||
function onFabClick() {
|
||||
if (!expanded.value) {
|
||||
expanded.value = true
|
||||
return
|
||||
}
|
||||
navigateTo('/pages/ai/recommend/index')
|
||||
}
|
||||
|
||||
function onHandleTouchStart(e: any) {
|
||||
touchStartX.value = Number(e?.touches?.[0]?.clientX || 0)
|
||||
}
|
||||
|
||||
function onHandleTouchEnd(e: any) {
|
||||
const endX = Number(e?.changedTouches?.[0]?.clientX || 0)
|
||||
const delta = endX - touchStartX.value
|
||||
if (delta > 20) {
|
||||
expanded.value = true
|
||||
} else if (delta < -20) {
|
||||
expanded.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 仅 AI 跳转做右侧球体悬浮 -->
|
||||
<view
|
||||
class="ai-floating"
|
||||
:class="{ 'is-collapsed': !expanded }"
|
||||
@touchstart.stop="onHandleTouchStart"
|
||||
@touchend.stop="onHandleTouchEnd"
|
||||
@touchcancel.stop="onHandleTouchEnd"
|
||||
>
|
||||
<view v-if="expanded" class="ai-close" @click.stop="expanded = false">×</view>
|
||||
|
||||
<view class="ai-fab" @click="onFabClick">
|
||||
<image src="@img/chef/115.png" class="ai-icon"></image>
|
||||
<image src="@/static/app/images/aidiancan.gif" class="ai-icon"></image>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -72,38 +42,9 @@ function onHandleTouchEnd(e: any) {
|
||||
transform: translateY(-50%) translateX(55rpx);
|
||||
}
|
||||
|
||||
.ai-close {
|
||||
position: absolute;
|
||||
left: -18rpx;
|
||||
top: -15rpx;
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
z-index: 2;
|
||||
border-radius: 999rpx;
|
||||
background: #ffffff;
|
||||
border: 1rpx solid #e5e7eb;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #7b8794;
|
||||
font-size: 24rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.ai-fab {
|
||||
width: 92rpx;
|
||||
height: 92rpx;
|
||||
border-radius: 50%;
|
||||
background: #e2e7eb;
|
||||
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.18);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ai-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
width: 260rpx;
|
||||
height: 260rpx;
|
||||
margin-right:-70rpx !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps({
|
||||
/** 外部列表(菜谱页等);首页不传则使用固定五项 */
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
@@ -21,77 +25,105 @@ const props = defineProps({
|
||||
default: 'logoUrl',
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['changeType']);
|
||||
watchEffect(() => {
|
||||
if (props.currentId) {
|
||||
selectedIndex.value = props.currentId;
|
||||
}
|
||||
});
|
||||
|
||||
const selectedIndex = ref();
|
||||
const emit = defineEmits(['changeType'])
|
||||
const { t } = useI18n()
|
||||
|
||||
function selectTab(item: any) {
|
||||
selectedIndex.value = item[props.valueKey];
|
||||
// 触发父组件事件
|
||||
console.log('selectTab', item[props.valueKey]);
|
||||
emit('changeType', item[props.valueKey]);
|
||||
const fixedTabs = [
|
||||
{
|
||||
id: 'member-zone',
|
||||
nameKey: 'pages.home.quickTabs.memberZone',
|
||||
logoUrl: '/static/app/images/home/huiyuanzhuanqu.png',
|
||||
},
|
||||
{
|
||||
id: 'live-seafood-air',
|
||||
nameKey: 'pages.home.quickTabs.liveSeafoodAir',
|
||||
logoUrl: '/static/app/images/home/kongyunhaixian.png',
|
||||
},
|
||||
{
|
||||
id: 'must-eat-list',
|
||||
nameKey: 'pages.home.quickTabs.mustEatList',
|
||||
logoUrl: '/static/app/images/home/bichibang.png',
|
||||
},
|
||||
{
|
||||
id: 'new-calendar',
|
||||
nameKey: 'pages.home.quickTabs.newCalendar',
|
||||
logoUrl: '/static/app/images/home/shangxinrili.png',
|
||||
},
|
||||
{
|
||||
id: 'fresh-seafood-today',
|
||||
nameKey: 'pages.home.quickTabs.freshSeafoodToday',
|
||||
logoUrl: '/static/app/images/home/xiandahaixian.png',
|
||||
},
|
||||
]
|
||||
|
||||
const useFixedTabs = computed(() => !props.list || props.list.length === 0)
|
||||
|
||||
function selectTab(item: Record<string, unknown>) {
|
||||
emit('changeType', item[props.valueKey])
|
||||
}
|
||||
|
||||
function imageWidthByIndex(index: number) {
|
||||
return (index + 1) % 2 === 1 ? '104rpx' : '210rpx'
|
||||
}
|
||||
|
||||
function getTabName(item: Record<string, unknown>) {
|
||||
if (useFixedTabs.value && item.nameKey) {
|
||||
return t(String(item.nameKey))
|
||||
}
|
||||
return item[props.labelKey]
|
||||
}
|
||||
|
||||
function getImage(item: Record<string, unknown>) {
|
||||
return String(item[props.imgKey] ?? '')
|
||||
}
|
||||
|
||||
const displayList = computed(() =>
|
||||
useFixedTabs.value ? fixedTabs : props.list,
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<scroll-view :scroll-x="true">
|
||||
<view class="flex items-center">
|
||||
<view class="shrink-0 w-30rpx"></view>
|
||||
<template v-for="(item, index) in list" :key="index">
|
||||
<view
|
||||
:class="[index === 0 ? '' : 'ml-40rpx']"
|
||||
class="w-112rpx flex flex-col items-center"
|
||||
@click="selectTab(item)"
|
||||
>
|
||||
<view
|
||||
:class="['img-wrap', selectedIndex == item[props.valueKey] ? 'img-selected' : '']"
|
||||
>
|
||||
<image
|
||||
class="tab-img rounded-50% overflow-hidden bg-common"
|
||||
:src="item[props.imgKey]"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
<text
|
||||
:class="selectedIndex == item[props.valueKey] ? 'text-#CE7138' : 'text-#333'"
|
||||
class="line-clamp-1 text-20rpx lh-20rpx mt-12rpx font-500"
|
||||
>{{ item[props.labelKey] }}</text
|
||||
>
|
||||
</view>
|
||||
</template>
|
||||
<view
|
||||
v-for="(item, index) in displayList"
|
||||
:key="String((item as Record<string, unknown>)[valueKey] ?? (item as Record<string, unknown>).id ?? index)"
|
||||
:class="[index === 0 ? '' : 'ml-40rpx']"
|
||||
class="tab-item shrink-0 flex flex-col items-center"
|
||||
@click="selectTab(item as Record<string, unknown>)"
|
||||
>
|
||||
<image
|
||||
class="tab-img"
|
||||
:src="getImage(item as Record<string, unknown>)"
|
||||
mode="scaleToFill"
|
||||
:style="{ width: imageWidthByIndex(index) }"
|
||||
/>
|
||||
<text class="tab-label line-clamp-1">{{ getTabName(item as Record<string, unknown>) }}</text>
|
||||
</view>
|
||||
<view class="shrink-0 w-30rpx op-0">1</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.img-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s;
|
||||
width: 102rpx;
|
||||
height: 102rpx;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.07);
|
||||
.tab-item {
|
||||
min-width: 94rpx;
|
||||
}
|
||||
.img-selected {
|
||||
border: 4rpx solid #ce7138;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
|
||||
.tab-label {
|
||||
margin-top: 10rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 28rpx;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab-img {
|
||||
width: 98rpx;
|
||||
height: 98rpx;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
height: 144rpx;
|
||||
width: auto;
|
||||
display: block;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import useEventEmit from "@/hooks/useEventEmit";
|
||||
import {CollectionType, EventEnum} from "@/constant/enums";
|
||||
import { useConfigStore, useUserStore } from "@/store";
|
||||
@@ -17,7 +19,6 @@ import HomeSkeleton from "./components/home-skeleton.vue";
|
||||
import {
|
||||
appMarketActivityListPost,
|
||||
appMerchantCartListMerchantPost,
|
||||
appMerchantCategoryListGet,
|
||||
appMerchantFeaturedListPost,
|
||||
appMerchantLabelListGet,
|
||||
appMerchantNearbyListPost, appMerchantRecommendListPost,
|
||||
@@ -26,6 +27,10 @@ import {
|
||||
} from "@/service";
|
||||
import usePage from "@/hooks/usePage";
|
||||
import {getFeaturedDishList} from "@/pages-store/service";
|
||||
import {
|
||||
buildQuickTopicUrl,
|
||||
isQuickTopicSlug,
|
||||
} from '@/pages-store/pages/dishes/utils/quick-topic-route'
|
||||
import { formatSalesCount } from "@/utils/utils";
|
||||
const configStore = useConfigStore();
|
||||
const userStore = useUserStore();
|
||||
@@ -34,7 +39,35 @@ const props = defineProps<{
|
||||
price?: Array<string> | string;
|
||||
}>();
|
||||
const emit = defineEmits(["toggleScore", "togglePrice", "toggleNotOpen"]);
|
||||
const { t } = useI18n();
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
/** 首页运营图:按语言切换(中文 / 英文) */
|
||||
const HOME_PROMO_BANNERS = {
|
||||
memberUpgrade: {
|
||||
zh: 'https://www.howhowfresh.com/minio/ruoyi/2026/06/03/0b9a7f865aba442e84e51034a3ec900c.png',
|
||||
en: 'https://www.howhowfresh.com/minio/ruoyi/2026/06/03/c5b9df3a922d4d17ae1b6eb8c1d7524a.png',
|
||||
},
|
||||
deliveryTime: {
|
||||
zh: 'https://www.howhowfresh.com/minio/ruoyi/2026/06/03/c5673a8874594755bdde7ed7fcbd1982.jpg',
|
||||
en: 'https://www.howhowfresh.com/minio/ruoyi/2026/06/03/1da02f1e0af34cea91a4f643247176be.png',
|
||||
},
|
||||
} as const
|
||||
|
||||
const isEnglishLocale = computed(() =>
|
||||
String(locale.value || uni.getLocale() || '').toLowerCase().startsWith('en'),
|
||||
)
|
||||
|
||||
const memberUpgradeBannerSrc = computed(() =>
|
||||
isEnglishLocale.value
|
||||
? HOME_PROMO_BANNERS.memberUpgrade.en
|
||||
: HOME_PROMO_BANNERS.memberUpgrade.zh,
|
||||
)
|
||||
|
||||
const deliveryTimeBannerSrc = computed(() =>
|
||||
isEnglishLocale.value
|
||||
? HOME_PROMO_BANNERS.deliveryTime.en
|
||||
: HOME_PROMO_BANNERS.deliveryTime.zh,
|
||||
)
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -49,12 +82,6 @@ function getDishPromoLabel(item: Record<string, unknown>): string {
|
||||
return typeof raw === 'string' && raw.trim() ? raw.trim() : ''
|
||||
}
|
||||
|
||||
function getFeaturedDishDisplayPrice(item: Record<string, any>) {
|
||||
const firstSpecPrice = item?.merchantSideDishVoList?.[0]?.merchantSideDishItemVoList?.[0]?.actualSalePrice
|
||||
if (firstSpecPrice != null && String(firstSpecPrice) !== '') return firstSpecPrice
|
||||
if (item?.actualSalePrice != null && String(item.actualSalePrice) !== '') return item.actualSalePrice
|
||||
return item?.discountPrice ?? 0
|
||||
}
|
||||
|
||||
function navigateTo(url: string) {
|
||||
if(userStore.checkLogin()) {
|
||||
@@ -64,17 +91,15 @@ function navigateTo(url: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const swiperList = ref([]);
|
||||
const currentSwiper = ref(0);
|
||||
const swiperList = ref<any[]>([])
|
||||
|
||||
async function initData() {
|
||||
// 只在首次加载时显示骨架屏,避免切换时的白屏
|
||||
if(featuredList.value.length === 0) {
|
||||
loading.value = true
|
||||
}
|
||||
appMarketActivityList()
|
||||
getAppMarketActivityList()
|
||||
getAppMerchantLabelList()
|
||||
getAppMerchantCategoryList()
|
||||
getAppFeaturedList()
|
||||
getAppNearbyListPost()
|
||||
// 获取当前用户购物车信息
|
||||
@@ -104,12 +129,21 @@ defineExpose({
|
||||
init: getIndexList,
|
||||
});
|
||||
|
||||
// 获取轮播图列表
|
||||
function appMarketActivityList() {
|
||||
appMarketActivityListPost({}).then(res=> {
|
||||
console.log('互动列表', res)
|
||||
swiperList.value = res.data
|
||||
})
|
||||
/** 首页轮播:POST /app/marketActivity/list */
|
||||
function getAppMarketActivityList() {
|
||||
appMarketActivityListPost({})
|
||||
.then((res) => {
|
||||
const list = Array.isArray(res?.data) ? res.data : []
|
||||
swiperList.value = list
|
||||
.filter((item) => item?.activityImageUrl || item?.activityImage)
|
||||
.map((item) => ({
|
||||
...item,
|
||||
activityImage: item.activityImageUrl || item.activityImage,
|
||||
}))
|
||||
})
|
||||
.catch(() => {
|
||||
swiperList.value = []
|
||||
})
|
||||
}
|
||||
|
||||
// 滚动信息获取 APP-商家标签分类控制器(用户端首页上面左右滚动的)
|
||||
@@ -124,15 +158,7 @@ function getAppMerchantLabelList() {
|
||||
// appMerchantLabelList.value = res.data || []
|
||||
// })
|
||||
}
|
||||
// 查询所有商家分类数据
|
||||
const appMerchantCategoryList = ref([])
|
||||
const currentCategory = ref('')
|
||||
function getAppMerchantCategoryList() {
|
||||
appMerchantCategoryListGet({}).then((res: any) => {
|
||||
console.log('查询所有商家分类数据', res)
|
||||
appMerchantCategoryList.value = res.data || []
|
||||
})
|
||||
}
|
||||
|
||||
// 查询精选商家列表(首页)
|
||||
const featuredList = ref([])
|
||||
@@ -208,11 +234,24 @@ function handleItemClick(e) {
|
||||
merchantLabelId.value = e.id
|
||||
// paging.value.refresh()
|
||||
}
|
||||
function tabsTypeChange(id: string) {
|
||||
currentCategory.value = id
|
||||
navigateTo('/pages-store/pages/home-store/index?merchantCategoryIds=' + id)
|
||||
// console.log('分类切换', id)
|
||||
// paging.value.refresh()
|
||||
function tabsTypeChange(id: string | number) {
|
||||
const topic = String(id)
|
||||
if (!isQuickTopicSlug(topic)) {
|
||||
return
|
||||
}
|
||||
currentCategory.value = topic
|
||||
const titleKeys: Record<string, string> = {
|
||||
'member-zone': 'pages.home.quickTabs.memberZone',
|
||||
'live-seafood-air': 'pages.home.quickTabs.liveSeafoodAir',
|
||||
'must-eat-list': 'pages.home.quickTabs.mustEatList',
|
||||
'new-calendar': 'pages.home.quickTabs.newCalendar',
|
||||
'fresh-seafood-today': 'pages.home.quickTabs.freshSeafoodToday',
|
||||
}
|
||||
navigateTo(
|
||||
buildQuickTopicUrl(topic, {
|
||||
categoryName: t(titleKeys[topic]),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// 是否展示精选商家和附近商家 true 显示 false隐藏
|
||||
@@ -253,6 +292,9 @@ function onRefresh() {
|
||||
console.log('手动触发下拉刷新了')
|
||||
merchantLabelId.value = ''
|
||||
currentCategory.value = ''
|
||||
getAppMarketActivityList()
|
||||
getAppFeaturedList()
|
||||
getAppNearbyListPost()
|
||||
paging.value.refresh()
|
||||
}
|
||||
|
||||
@@ -268,9 +310,14 @@ function handleClickSwiper(item: any) {
|
||||
case 3: // 会员
|
||||
navigateTo('/pages-user/pages/member/index')
|
||||
break
|
||||
// case 4:
|
||||
// navigateTo('/pages/ai/chat/index')
|
||||
// break
|
||||
}
|
||||
}
|
||||
|
||||
const walletUrl = "pages-user/pages/balance/index";
|
||||
|
||||
function navigateToDishes(item: any) {
|
||||
uni.navigateTo({
|
||||
url: '/pages-store/pages/store/dishes?id=' + item.id + '&storeId=' + item.merchantId,
|
||||
@@ -313,10 +360,8 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
||||
<home-top-header
|
||||
:app-name="Config.appName"
|
||||
:location-text="userStore.userLocation.location"
|
||||
:appointment-time-show="userStore.appointmentTimeShow"
|
||||
:cart-badge-total="cartBadgeTotal"
|
||||
:is-login="userStore.isLogin"
|
||||
@click-address="navigateTo('/pages/address/index')"
|
||||
@click-location="navigateTo('/pages-user/pages/search-address/index')"
|
||||
@click-cart="goCart"
|
||||
/>
|
||||
@@ -330,17 +375,17 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
||||
<view class="px-24rpx pt-12rpx pb-8rpx">
|
||||
<search />
|
||||
<msg-box />
|
||||
<!-- 分类标签(双行横滑) -->
|
||||
<!-- 分类标签 -->
|
||||
<view class="mt-28rpx" v-if="appMerchantLabelList.length > 0">
|
||||
<class-bullet :categories="appMerchantLabelList" @itemClick="handleItemClick" />
|
||||
</view>
|
||||
</view>
|
||||
<!-- 轮播图:/app/marketActivity/list -->
|
||||
<swiper
|
||||
class="home-promo-swiper card-swiper"
|
||||
:circular="true"
|
||||
:autoplay="true"
|
||||
previous-margin="48rpx"
|
||||
next-margin="48rpx"
|
||||
v-if="swiperList.length > 0"
|
||||
class="card-swiper"
|
||||
:circular="swiperList.length > 1"
|
||||
:autoplay="swiperList.length > 1"
|
||||
>
|
||||
<swiper-item
|
||||
v-for="(item, sIdx) in swiperList"
|
||||
@@ -349,21 +394,37 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
||||
>
|
||||
<image
|
||||
:src="item.activityImage"
|
||||
class="swiper-item-content w-full h-100% rounded-32rpx bg-common"
|
||||
class="swiper-item-content w-full h-100%"
|
||||
mode="scaleToFill"
|
||||
></image>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
|
||||
<!-- 快捷入口(圆形分类) -->
|
||||
<tabs-type @changeType="tabsTypeChange" v-if="appMerchantCategoryList.length > 0" :list="appMerchantCategoryList" :currentId="currentCategory" class="mt-28rpx home-tabs-quick" />
|
||||
<!-- 快捷入口(固定五项,跳转精选菜品专题页) -->
|
||||
<tabs-type
|
||||
:current-id="currentCategory"
|
||||
class="mt-28rpx mb-24rpx home-tabs-quick"
|
||||
@change-type="tabsTypeChange"
|
||||
/>
|
||||
|
||||
<!-- 筛选工具 -->
|
||||
<!-- <filtrate-tool class="mt-32rpx" @togglePickup="togglePickup" @toggleDiscount="toggleDiscount" @toggleScore="toggleScore" @togglePrice="togglePrice" />-->
|
||||
|
||||
<image
|
||||
:src="memberUpgradeBannerSrc"
|
||||
class="w-100% h-[340rpx]"
|
||||
mode="widthFix"
|
||||
@click="navigateTo('/pages-user/pages/member/index')"
|
||||
/>
|
||||
<image
|
||||
:src="deliveryTimeBannerSrc"
|
||||
class="w-100% h-[200rpx] rounded-24rpx mt-4rpx"
|
||||
mode="widthFix"
|
||||
/>
|
||||
|
||||
<!-- 精选商家和附近商家 -->
|
||||
<view class="animate-in fade-in animate-ease-in animate-duration-300" v-if="isShowMerchant">
|
||||
<!-- Featured on ChefLink 精选商家(浅底 + 横向卡片,对齐设计稿) -->
|
||||
<view v-if="featuredList.length > 0" class="featured-merchants-section mt-36rpx px-24rpx pt-8rpx pb-16rpx">
|
||||
<view class="mb-24rpx px-6rpx text-32rpx lh-44rpx text-#1a1a1a">
|
||||
<view class="mb-24rpx px-6rpx text-28rpx lh-36rpx text-#1a1a1a">
|
||||
{{ t('pages.home.featured-on') }}
|
||||
</view>
|
||||
<featured-on :list="featuredList" />
|
||||
@@ -371,7 +432,7 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
||||
|
||||
<!-- Nearby Merchants 附近商家 -->
|
||||
<view v-if="nearbyList.length > 0" class="nearby-merchants-block mt-36rpx px-24rpx pb-16rpx">
|
||||
<view class="mb-24rpx px-6rpx text-32rpx lh-44rpx text-#1a1a1a ">
|
||||
<view class="mb-24rpx px-6rpx text-28rpx lh-36rpx text-#1a1a1a ">
|
||||
{{ t('pages.home.nearby-merchants') }}
|
||||
</view>
|
||||
<nearby-merchants :list="nearbyList" />
|
||||
@@ -380,7 +441,7 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
||||
|
||||
<!-- List 精选菜品瀑布流(浅底 + 白卡片 + 阴影,结构对齐设计稿) -->
|
||||
<view class="featured-dishes-section mt-36rpx px-24rpx pb-40rpx">
|
||||
<view class="mb-24rpx px-6rpx text-32rpx lh-44rpx text-#1a1a1a "
|
||||
<view class="mb-24rpx px-6rpx text-28rpx lh-36rpx text-#1a1a1a "
|
||||
>{{ t('pages.home.featured-dishes') }}</view>
|
||||
<view class="waterfall-row flex gap-16rpx items-start">
|
||||
<view
|
||||
@@ -492,10 +553,6 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
||||
padding-left: 2rpx;
|
||||
}
|
||||
|
||||
.home-promo-swiper {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.home-tabs-quick {
|
||||
padding-left: 8rpx;
|
||||
padding-right: 8rpx;
|
||||
@@ -511,19 +568,14 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
||||
}
|
||||
|
||||
.card-swiper {
|
||||
height: 400rpx;
|
||||
height: 420rpx;
|
||||
}
|
||||
.swiper-item-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: scale(0.94);
|
||||
border-radius: 32rpx;
|
||||
box-shadow: 0 8rpx 28rpx rgba(0, 0, 0, 0.08);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.swiper-item-active .swiper-item-content {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
|
||||
/* 精选商家 / 精选菜品 区块(与页面背景统一) */
|
||||
.featured-merchants-section,
|
||||
|
||||
+1
-11
@@ -42,16 +42,6 @@ const {paging, dataList, loading, queryList, firstLoaded} = usePage<MerchantOrde
|
||||
}
|
||||
})
|
||||
|
||||
function fillI18nParams(template: string, params: Record<string, string | number>) {
|
||||
let text = template
|
||||
Object.keys(params).forEach((key) => {
|
||||
const value = String(params[key] ?? '')
|
||||
text = text.replace(new RegExp(`\\{${key}\\}`, 'g'), value)
|
||||
text = text.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), value)
|
||||
})
|
||||
return text
|
||||
}
|
||||
|
||||
function normalizeTimestamp(input: unknown): number | null {
|
||||
if (input == null || input === '') return null
|
||||
|
||||
@@ -112,7 +102,7 @@ function getTotalDishCount(item: MerchantOrderVo) {
|
||||
}
|
||||
|
||||
function getTotalDishCountText(item: MerchantOrderVo) {
|
||||
return fillI18nParams(t('pages.order.totalItemCount'), {
|
||||
return t('pages.order.totalItemCount', {
|
||||
count: getTotalDishCount(item),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user