修改样式
This commit is contained in:
@@ -18,15 +18,17 @@ function handleClickSearch() {
|
||||
chooseAddress: (data: any) => {
|
||||
console.log('搜索的地址信息', data)
|
||||
if (data) {
|
||||
addressStore.clearAddressInfo()
|
||||
addressStore.setAddressLocation({
|
||||
displayName: data.displayName,
|
||||
formattedAddress: data.formattedAddress,
|
||||
longitude: data.location.lng,
|
||||
latitude: data.location.lat
|
||||
})
|
||||
addressStore.pendingIntroBuildingType = true
|
||||
setTimeout(()=> {
|
||||
uni.navigateTo({
|
||||
url: '/pages/address/choose-type'
|
||||
url: '/pages/address/save-address/other'
|
||||
})
|
||||
}, 300)
|
||||
}
|
||||
|
||||
@@ -137,13 +137,12 @@ const isDateSelectable = (date: Date): boolean => {
|
||||
return hasAvailableTimeSlots(date);
|
||||
};
|
||||
|
||||
// 生成未来的日期(显示所有日期,但标记营业状态)
|
||||
// 生成未来 7 天:设计稿为「本周」5 个圆 +「下周」2 个圆
|
||||
const dateOptions = computed(() => {
|
||||
const dates: Date[] = [];
|
||||
const today = new Date();
|
||||
|
||||
// 生成连续的5天日期(包括不营业的日期)
|
||||
for (let i = 0; i < 5; i++) {
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const date = new Date(today);
|
||||
date.setDate(today.getDate() + i);
|
||||
dates.push(date);
|
||||
@@ -152,6 +151,9 @@ const dateOptions = computed(() => {
|
||||
return dates;
|
||||
});
|
||||
|
||||
const thisWeekDates = computed(() => dateOptions.value.slice(0, 5));
|
||||
const nextWeekDates = computed(() => dateOptions.value.slice(5, 7));
|
||||
|
||||
// 状态管理 - 初始化为第一个营业日期
|
||||
const selectedDate = ref<Date>();
|
||||
|
||||
@@ -246,7 +248,6 @@ const initializeSelectedDate = () => {
|
||||
);
|
||||
const firstAllowed = firstOpen || dateOptions.value.find((d) => isAllowedDay(d));
|
||||
selectedDate.value = firstAllowed || dateOptions.value[0];
|
||||
nextTick(() => updateScrollPosition());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -254,9 +255,6 @@ const initializeSelectedDate = () => {
|
||||
for (const date of dateOptions.value) {
|
||||
if (isDateSelectable(date)) {
|
||||
selectedDate.value = date;
|
||||
nextTick(() => {
|
||||
updateScrollPosition();
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -266,7 +264,6 @@ const initializeSelectedDate = () => {
|
||||
if (nextBusinessDate) {
|
||||
selectedDate.value = nextBusinessDate;
|
||||
nextTick(() => {
|
||||
updateScrollPosition();
|
||||
uni.showToast({
|
||||
title: t('pages.address.reservationTime.currentTimeExpired'),
|
||||
icon: "none",
|
||||
@@ -275,9 +272,6 @@ const initializeSelectedDate = () => {
|
||||
});
|
||||
} else {
|
||||
selectedDate.value = dateOptions.value[0];
|
||||
nextTick(() => {
|
||||
updateScrollPosition();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -304,59 +298,15 @@ watch(
|
||||
);
|
||||
const selectedTimeSlot = ref<string>("");
|
||||
|
||||
// 横向滚动距离
|
||||
const scrollLeft = ref<number>(0);
|
||||
/** 圆圈内上行:星期(与全局 dayjs 语言一致) */
|
||||
const formatWeekdayCircle = (date: Date) => dayjs(date).format("dddd");
|
||||
|
||||
// 计算并设置横向滚动距离
|
||||
const updateScrollPosition = () => {
|
||||
if (!selectedDate.value) return;
|
||||
|
||||
// 找到选中日期在 dateOptions 中的索引
|
||||
const selectedIndex = dateOptions.value.findIndex(date =>
|
||||
dayjs(date).isSame(dayjs(selectedDate.value), 'day')
|
||||
);
|
||||
|
||||
if (selectedIndex === -1) return;
|
||||
|
||||
// 每个日期卡片的宽度:240rpx + 28rpx 间距 = 268rpx
|
||||
// 但第一个卡片没有左边距,所以需要特殊处理
|
||||
const cardWidth = 240; // rpx
|
||||
const cardMargin = 28; // rpx
|
||||
|
||||
// 计算滚动距离,让选中的卡片尽量居中显示
|
||||
let scrollDistance = 0;
|
||||
if (selectedIndex > 0) {
|
||||
// 第一个卡片没有左边距,从第二个开始每个卡片占用 240 + 28 = 268rpx
|
||||
scrollDistance = selectedIndex * (cardWidth + cardMargin);
|
||||
|
||||
// 减去一些距离让选中项更居中(可根据屏幕宽度调整)
|
||||
scrollDistance = Math.max(0, scrollDistance - 100);
|
||||
}
|
||||
|
||||
scrollLeft.value = scrollDistance;
|
||||
};
|
||||
|
||||
// 格式化日期显示
|
||||
const formatDateDisplay = (date: Date) => {
|
||||
const today = dayjs();
|
||||
const targetDate = dayjs(date);
|
||||
|
||||
if (targetDate.isSame(today, "day")) {
|
||||
return "Today";
|
||||
} else if (targetDate.isSame(today.add(1, "day"), "day")) {
|
||||
return "Tomorrow";
|
||||
} else {
|
||||
// 返回星期几
|
||||
return targetDate.format("dddd");
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化日期为月份和日期(不包含年份),不可选择日期显示"不营业"
|
||||
const formatDateOnly = (date: Date) => {
|
||||
/** 圆圈内下行:MM/DD;不可选时显示文案 */
|
||||
const formatCircleSubLine = (date: Date) => {
|
||||
if (!isDateSelectable(date)) {
|
||||
return t('pages.address.reservationTime.notAvailable')
|
||||
return t("pages.address.reservationTime.notAvailable");
|
||||
}
|
||||
return dayjs(date).format('MMMM D')
|
||||
return dayjs(date).format("MM/DD");
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -536,8 +486,16 @@ 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) {
|
||||
@@ -550,13 +508,13 @@ const submitReservation = () => {
|
||||
}
|
||||
|
||||
// 计算开始/结束时间
|
||||
const selectedDateDayjs = dayjs(selectedDate.value);
|
||||
const selectedDateDayjs = dayjs(dateVal);
|
||||
let startTime: dayjs.Dayjs;
|
||||
let endTime: dayjs.Dayjs;
|
||||
|
||||
if (onlySelectDay.value) {
|
||||
// 仅选日期:优先使用营业时间范围;若无营业时间限制,则使用当天起止
|
||||
const bh = getBusinessHoursForDate(selectedDate.value);
|
||||
const bh = getBusinessHoursForDate(dateVal);
|
||||
if (bh) {
|
||||
const [startHour, startMinute] = bh.startTime.split(':').map(Number);
|
||||
const [endHour, endMinute] = bh.endTime.split(':').map(Number);
|
||||
@@ -592,14 +550,14 @@ const submitReservation = () => {
|
||||
}
|
||||
|
||||
console.log("预约信息:", {
|
||||
date: selectedDate.value,
|
||||
date: dateVal,
|
||||
timeSlot: onlySelectDay.value ? '' : selectedTimeSlot.value,
|
||||
startTime: startTime.valueOf(),
|
||||
endTime: endTime.valueOf(),
|
||||
});
|
||||
|
||||
uni.$emit(EventEnum.CHOOSE_APPOINTMENT_TIME, {
|
||||
date: selectedDate.value,
|
||||
date: dateVal,
|
||||
timeSlot: onlySelectDay.value ? '' : selectedTimeSlot.value,
|
||||
startTime: startTime.valueOf(),
|
||||
endTime: endTime.valueOf(),
|
||||
@@ -633,45 +591,52 @@ onLoad((options: any) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="">
|
||||
<navbar />
|
||||
<view class="mt-20rpx px-30rpx text-46rpx lh-46rpx text-#333 font-bold">
|
||||
{{ t("pages.address.appTime") }}
|
||||
</view>
|
||||
<view class="px-30rpx pt-52rpx pb-50rpx w-screen bg-white">
|
||||
<scroll-view class="w-full whitespace-nowrap" scroll-x="true" :scroll-left="scrollLeft">
|
||||
<template v-for="(item, index) in dateOptions" :key="index">
|
||||
<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
|
||||
@click="selectDate(item)"
|
||||
:class="[
|
||||
index === 0 ? '' : 'ml-28rpx',
|
||||
selectedDate && dayjs(selectedDate).isSame(dayjs(item), 'day')
|
||||
? 'border-#333'
|
||||
: 'border-#D8D8D8',
|
||||
!isDateSelectable(item)
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'cursor-pointer',
|
||||
]"
|
||||
class="inline-block border-solid border-1px w-240rpx h-140rpx rounded-20rpx px-32rpx py-36rpx"
|
||||
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)"
|
||||
>
|
||||
<view
|
||||
:class="!isDateSelectable(item) ? 'text-#999' : 'text-#333'"
|
||||
class="text-28rpx lh-28rpx mb-12rpx"
|
||||
>
|
||||
{{ formatDateDisplay(item) }}
|
||||
</view>
|
||||
<view
|
||||
:class="!isDateSelectable(item) ? 'text-#CCC' : 'text-#7D7D7D'"
|
||||
class="text-28rpx"
|
||||
>
|
||||
{{ formatDateOnly(item) }}
|
||||
</view>
|
||||
<text class="date-circle__weekday">{{ formatWeekdayCircle(item) }}</text>
|
||||
<text class="date-circle__sub">{{ formatCircleSubLine(item) }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</scroll-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">
|
||||
<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"
|
||||
@@ -705,8 +670,91 @@ onLoad((options: any) => {
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
page {
|
||||
<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>
|
||||
|
||||
@@ -99,6 +99,13 @@ onLoad(()=> {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onReady(() => {
|
||||
if (addressStore.pendingIntroBuildingType && !addressStore.addressInfo.id) {
|
||||
buildingTypeRef.value?.openIntroSheet?.()
|
||||
}
|
||||
})
|
||||
|
||||
onUnload(()=> {
|
||||
if(!isSwitch.value) {
|
||||
addressStore.clearAddressInfo()
|
||||
@@ -176,7 +183,11 @@ function chooseStateConfirm(data: any) {
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.choose-type.navTitle') }}</view>
|
||||
<view @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
|
||||
<text class="text-30rpx lh-30rpx text-#333">
|
||||
{{ `${t(`pages.address.choose-type.${addressStore.addressInfo.type}`)}` }}
|
||||
{{
|
||||
addressStore.addressInfo.type
|
||||
? t(`pages.address.choose-type.${addressStore.addressInfo.type}`)
|
||||
: t('common.placeholder.pleaseSelect')
|
||||
}}
|
||||
</text>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
|
||||
</view>
|
||||
|
||||
@@ -1,70 +1,228 @@
|
||||
<script setup lang="ts">
|
||||
import {UserAddressType} from "@/constant/enums";
|
||||
import { UserAddressType } from '@/constant/enums';
|
||||
import { useAddressStore } from '@/pages/address/store/address';
|
||||
|
||||
const { t } = useI18n();
|
||||
const show = ref(false);
|
||||
const value = ref('house')
|
||||
const addressStore = useAddressStore();
|
||||
|
||||
const emit = defineEmits(['submit'])
|
||||
/** 新地址首次进入保存页:与原先 choose-type 全页等价的介绍弹窗 */
|
||||
const showIntro = ref(false);
|
||||
/** 点击表单「建筑类型」行:原有滚轮选择器 */
|
||||
const showPicker = ref(false);
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [value: string];
|
||||
}>();
|
||||
|
||||
const typeList = computed(() => [
|
||||
{
|
||||
label: t('pages.address.choose-type.house'),
|
||||
desc: t('pages.address.choose-type.houseDescription'),
|
||||
value: UserAddressType.HOUSE,
|
||||
icon: '/static/images/chef/147.png',
|
||||
},
|
||||
{
|
||||
label: t('pages.address.choose-type.apartment'),
|
||||
desc: t('pages.address.choose-type.apartmentDescription'),
|
||||
value: UserAddressType.APARTMENT,
|
||||
icon: '/static/images/chef/148.png',
|
||||
},
|
||||
{
|
||||
label: t('pages.address.choose-type.office'),
|
||||
desc: t('pages.address.choose-type.officeDescription'),
|
||||
value: UserAddressType.OFFICE,
|
||||
icon: '/static/images/chef/149.png',
|
||||
},
|
||||
{
|
||||
label: t('pages.address.choose-type.hotel'),
|
||||
desc: t('pages.address.choose-type.hotelDescription'),
|
||||
value: UserAddressType.HOTEL,
|
||||
icon: '/static/images/chef/150.png',
|
||||
},
|
||||
{
|
||||
label: t('pages.address.choose-type.other'),
|
||||
desc: t('pages.address.choose-type.otherDescription'),
|
||||
value: UserAddressType.OTHER,
|
||||
icon: '/static/images/chef/151.png',
|
||||
},
|
||||
]);
|
||||
|
||||
const columns = computed(() => [
|
||||
{ label: t('pages.address.choose-type.house'), value: UserAddressType.HOUSE },
|
||||
{ label: t('pages.address.choose-type.apartment'), value: UserAddressType.APARTMENT },
|
||||
{ label: t('pages.address.choose-type.office'), value: UserAddressType.OFFICE },
|
||||
{ label: t('pages.address.choose-type.hotel'), value: UserAddressType.HOTEL },
|
||||
{ label: t('pages.address.choose-type.other'), value: UserAddressType.OTHER },
|
||||
]);
|
||||
|
||||
const pickerValue = ref<string>(UserAddressType.HOUSE);
|
||||
|
||||
function routeForType(type: string): string {
|
||||
switch (type) {
|
||||
case UserAddressType.HOUSE:
|
||||
return 'pages/address/save-address/house';
|
||||
case UserAddressType.APARTMENT:
|
||||
return 'pages/address/save-address/apartment';
|
||||
case UserAddressType.OFFICE:
|
||||
return 'pages/address/save-address/office';
|
||||
case UserAddressType.HOTEL:
|
||||
return 'pages/address/save-address/hotel';
|
||||
case UserAddressType.OTHER:
|
||||
return 'pages/address/save-address/other';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function currentSaveAddressRoute(): string {
|
||||
const pages = getCurrentPages();
|
||||
return pages[pages.length - 1]?.route ?? '';
|
||||
}
|
||||
|
||||
/** 点击表单行:滚轮选择器(原效果) */
|
||||
function onOpen() {
|
||||
show.value = true;
|
||||
}
|
||||
function handleClose() {
|
||||
show.value = false;
|
||||
}
|
||||
function handleSubmit() {
|
||||
console.log('value', value.value)
|
||||
emit('submit', value.value)
|
||||
handleClose()
|
||||
pickerValue.value =
|
||||
addressStore.addressInfo.type && `${addressStore.addressInfo.type}`.length > 0
|
||||
? (addressStore.addressInfo.type as string)
|
||||
: UserAddressType.HOUSE;
|
||||
showPicker.value = true;
|
||||
}
|
||||
|
||||
const columns = ref(
|
||||
[
|
||||
{
|
||||
label: t('pages.address.choose-type.house'),
|
||||
value: UserAddressType.HOUSE,
|
||||
},
|
||||
{
|
||||
label: t('pages.address.choose-type.apartment'),
|
||||
value: UserAddressType.APARTMENT,
|
||||
},
|
||||
{
|
||||
label: t('pages.address.choose-type.office'),
|
||||
value: UserAddressType.OFFICE,
|
||||
},
|
||||
{
|
||||
label: t('pages.address.choose-type.hotel'),
|
||||
value: UserAddressType.HOTEL,
|
||||
},
|
||||
{
|
||||
label: t('pages.address.choose-type.other'),
|
||||
value: UserAddressType.OTHER,
|
||||
},
|
||||
]
|
||||
)
|
||||
function onChange({picker, value, index}) {
|
||||
function handlePickerClose() {
|
||||
showPicker.value = false;
|
||||
}
|
||||
|
||||
function handlePickerCancel() {
|
||||
showPicker.value = false;
|
||||
}
|
||||
|
||||
function handlePickerConfirm() {
|
||||
addressStore.addressInfo.type = pickerValue.value;
|
||||
showPicker.value = false;
|
||||
const next = routeForType(pickerValue.value);
|
||||
if (next && currentSaveAddressRoute() !== next) {
|
||||
emit('submit', pickerValue.value);
|
||||
}
|
||||
}
|
||||
|
||||
/** 新地址首次进入页面时由父级调用 */
|
||||
function openIntroSheet() {
|
||||
showIntro.value = true;
|
||||
}
|
||||
|
||||
function handleIntroClose() {
|
||||
showIntro.value = false;
|
||||
}
|
||||
|
||||
/** 蒙层关闭且尚未完成选择时,与「跳过」一致 */
|
||||
function onIntroPopupClosed() {
|
||||
if (!addressStore.pendingIntroBuildingType) return;
|
||||
addressStore.pendingIntroBuildingType = false;
|
||||
addressStore.addressInfo.type = UserAddressType.OTHER;
|
||||
const next = routeForType(UserAddressType.OTHER);
|
||||
if (next && currentSaveAddressRoute() !== next) {
|
||||
emit('submit', UserAddressType.OTHER);
|
||||
}
|
||||
}
|
||||
|
||||
function chooseIntroType(item: { value: string }) {
|
||||
addressStore.pendingIntroBuildingType = false;
|
||||
addressStore.addressInfo.type = item.value;
|
||||
showIntro.value = false;
|
||||
const next = routeForType(item.value);
|
||||
if (next && currentSaveAddressRoute() !== next) {
|
||||
emit('submit', item.value);
|
||||
}
|
||||
}
|
||||
|
||||
function handleIntroSkip() {
|
||||
addressStore.pendingIntroBuildingType = false;
|
||||
addressStore.addressInfo.type = UserAddressType.OTHER;
|
||||
showIntro.value = false;
|
||||
const next = routeForType(UserAddressType.OTHER);
|
||||
if (next && currentSaveAddressRoute() !== next) {
|
||||
emit('submit', UserAddressType.OTHER);
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
onOpen,
|
||||
openIntroSheet,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 新地址首次:建筑类型介绍底部弹窗 -->
|
||||
<wd-popup
|
||||
v-model="show"
|
||||
position="bottom"
|
||||
@close="handleClose"
|
||||
v-model="showIntro"
|
||||
position="bottom"
|
||||
:z-index="120"
|
||||
custom-style="background: transparent;"
|
||||
@close="onIntroPopupClosed"
|
||||
>
|
||||
<view class="bg-white rounded-t-32rpx overflow-hidden pb-[calc(24rpx+env(safe-area-inset-bottom))]">
|
||||
<view class="relative px-30rpx pt-40rpx pb-8rpx">
|
||||
<text
|
||||
class="absolute right-30rpx top-40rpx z-1 text-32rpx lh-32rpx text-#CE7138"
|
||||
@click="handleIntroSkip"
|
||||
>
|
||||
{{ t('common.skip') }}
|
||||
</text>
|
||||
<view class="text-center text-36rpx lh-44rpx text-#333 font-500 pr-100rpx pl-100rpx">
|
||||
{{ t('pages.address.choose-type.title') }}
|
||||
</view>
|
||||
<view class="mt-24rpx text-center text-28rpx lh-36rpx text-#666">
|
||||
{{ t('pages.address.choose-type.description') }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="box-border max-h-[62vh] px-30rpx pb-8rpx pt-28rpx">
|
||||
<view
|
||||
v-for="(item, index) in typeList"
|
||||
:key="item.value"
|
||||
:class="[
|
||||
index === 0 ? '' : 'mt-28rpx',
|
||||
item.value === addressStore.addressInfo.type
|
||||
? 'border-#000 border-4rpx'
|
||||
: 'border-#DFDFDF border-2rpx',
|
||||
'border-solid rounded-16rpx px-28rpx py-34rpx flex items-center',
|
||||
]"
|
||||
@click="chooseIntroType(item)"
|
||||
>
|
||||
<image :src="item.icon" class="h-48rpx mr-28rpx shrink-0 w-48rpx" />
|
||||
<view class="min-w-0 flex-1">
|
||||
<view class="text-36rpx lh-40rpx text-#333 font-500">
|
||||
{{ item.label }}
|
||||
</view>
|
||||
<view class="mt-18rpx text-28rpx lh-32rpx text-#6D6D6D">
|
||||
{{ item.desc }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<!-- 点击建筑类型行:原滚轮选择 -->
|
||||
<wd-popup
|
||||
v-model="showPicker"
|
||||
position="bottom"
|
||||
:z-index="121"
|
||||
@close="handlePickerClose"
|
||||
>
|
||||
<view>
|
||||
<view class="flex-center-sb h-102rpx bg-#F7F7F7 px-30rpx">
|
||||
<view @click="handleClose" class="text-30rpx text-#999">{{ t('common.cancel') }}</view>
|
||||
<view class="text-30rpx text-#999" @click="handlePickerCancel">{{ t('common.cancel') }}</view>
|
||||
<view class="text-34rpx text-#333">{{ t('common.buildingType') }}</view>
|
||||
<view @click="handleSubmit" class="text-30rpx text-#FF6106">{{ t('common.confirm') }}</view>
|
||||
<view class="text-30rpx text-#FF6106" @click="handlePickerConfirm">{{ t('common.confirm') }}</view>
|
||||
</view>
|
||||
<view class="bg-#fff px-54rpx py-56rpx">
|
||||
<wd-picker-view :columns="columns" v-model="value" @change="onChange" label-key="label" value-key="value" />
|
||||
<wd-picker-view
|
||||
:columns="columns"
|
||||
v-model="pickerValue"
|
||||
label-key="label"
|
||||
value-key="value"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
@@ -90,4 +248,4 @@ defineExpose({
|
||||
:deep(.uni-picker-view-indicator) {
|
||||
height: 94rpx !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -98,6 +98,13 @@ onLoad(()=> {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onReady(() => {
|
||||
if (addressStore.pendingIntroBuildingType && !addressStore.addressInfo.id) {
|
||||
buildingTypeRef.value?.openIntroSheet?.()
|
||||
}
|
||||
})
|
||||
|
||||
onUnload(()=> {
|
||||
if(!isSwitch.value) {
|
||||
addressStore.clearAddressInfo()
|
||||
@@ -173,7 +180,11 @@ function chooseStateConfirm(data: any) {
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.choose-type.navTitle') }}</view>
|
||||
<view @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
|
||||
<text class="text-30rpx lh-30rpx text-#333">
|
||||
{{ `${t(`pages.address.choose-type.${addressStore.addressInfo.type}`)}` }}
|
||||
{{
|
||||
addressStore.addressInfo.type
|
||||
? t(`pages.address.choose-type.${addressStore.addressInfo.type}`)
|
||||
: t('common.placeholder.pleaseSelect')
|
||||
}}
|
||||
</text>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
|
||||
</view>
|
||||
|
||||
@@ -98,6 +98,13 @@ onLoad(()=> {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onReady(() => {
|
||||
if (addressStore.pendingIntroBuildingType && !addressStore.addressInfo.id) {
|
||||
buildingTypeRef.value?.openIntroSheet?.()
|
||||
}
|
||||
})
|
||||
|
||||
onUnload(()=> {
|
||||
if(!isSwitch.value) {
|
||||
addressStore.clearAddressInfo()
|
||||
@@ -173,7 +180,11 @@ function chooseStateConfirm(data: any) {
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.choose-type.navTitle') }}</view>
|
||||
<view @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
|
||||
<text class="text-30rpx lh-30rpx text-#333">
|
||||
{{ `${t(`pages.address.choose-type.${addressStore.addressInfo.type}`)}` }}
|
||||
{{
|
||||
addressStore.addressInfo.type
|
||||
? t(`pages.address.choose-type.${addressStore.addressInfo.type}`)
|
||||
: t('common.placeholder.pleaseSelect')
|
||||
}}
|
||||
</text>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
|
||||
</view>
|
||||
|
||||
@@ -98,6 +98,13 @@ onLoad(()=> {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onReady(() => {
|
||||
if (addressStore.pendingIntroBuildingType && !addressStore.addressInfo.id) {
|
||||
buildingTypeRef.value?.openIntroSheet?.()
|
||||
}
|
||||
})
|
||||
|
||||
onUnload(()=> {
|
||||
if(!isSwitch.value) {
|
||||
addressStore.clearAddressInfo()
|
||||
@@ -173,7 +180,11 @@ function chooseStateConfirm(data: any) {
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.choose-type.navTitle') }}</view>
|
||||
<view @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
|
||||
<text class="text-30rpx lh-30rpx text-#333">
|
||||
{{ `${t(`pages.address.choose-type.${addressStore.addressInfo.type}`)}` }}
|
||||
{{
|
||||
addressStore.addressInfo.type
|
||||
? t(`pages.address.choose-type.${addressStore.addressInfo.type}`)
|
||||
: t('common.placeholder.pleaseSelect')
|
||||
}}
|
||||
</text>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
|
||||
</view>
|
||||
|
||||
@@ -98,6 +98,13 @@ onLoad(()=> {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onReady(() => {
|
||||
if (addressStore.pendingIntroBuildingType && !addressStore.addressInfo.id) {
|
||||
buildingTypeRef.value?.openIntroSheet?.()
|
||||
}
|
||||
})
|
||||
|
||||
onUnload(()=> {
|
||||
if(!isSwitch.value) {
|
||||
addressStore.clearAddressInfo()
|
||||
@@ -173,7 +180,11 @@ function chooseStateConfirm(data: any) {
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.choose-type.navTitle') }}</view>
|
||||
<view @click="openBuildingType" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
|
||||
<text class="text-30rpx lh-30rpx text-#333">
|
||||
{{ `${t(`pages.address.choose-type.${addressStore.addressInfo.type}`)}` }}
|
||||
{{
|
||||
addressStore.addressInfo.type
|
||||
? t(`pages.address.choose-type.${addressStore.addressInfo.type}`)
|
||||
: t('common.placeholder.pleaseSelect')
|
||||
}}
|
||||
</text>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
|
||||
</view>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { defineStore } from "pinia";
|
||||
import type { UserAddressBo } from '@/service/types';
|
||||
export const useAddressStore = defineStore('store-address', () => {
|
||||
/** 从地图搜索新增地址进入保存页时,首次需弹出建筑类型介绍层(与原先进入 choose-type 页等价) */
|
||||
const pendingIntroBuildingType = ref(false)
|
||||
|
||||
const addressInfo = ref<UserAddressBo>({
|
||||
type: '',
|
||||
/** 配送类型 1-亲自送达 2-放门口,默认放门口 */
|
||||
@@ -57,6 +60,7 @@ export const useAddressStore = defineStore('store-address', () => {
|
||||
}
|
||||
|
||||
function clearAddressInfo() {
|
||||
pendingIntroBuildingType.value = false
|
||||
addressInfo.value = {
|
||||
id: '',
|
||||
type: '',
|
||||
@@ -102,6 +106,7 @@ export const useAddressStore = defineStore('store-address', () => {
|
||||
return {
|
||||
addressInfo,
|
||||
addressLocation,
|
||||
pendingIntroBuildingType,
|
||||
setAddressLocation,
|
||||
clearAddressInfo
|
||||
}
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import useEventEmit from "@/hooks/useEventEmit";
|
||||
import {CollectionType, EventEnum} from "@/constant/enums";
|
||||
import { debounce } from 'throttle-debounce'
|
||||
import Search from "../tabbar-home/components/search.vue";
|
||||
import { useConfigStore, useUserStore } from "@/store";
|
||||
import MsgBox from "../tabbar-home/components/msg-box.vue";
|
||||
import Collection from "@/components/collection/index.vue";
|
||||
import BrowseSkeleton from "./components/browse-skeleton.vue";
|
||||
import {
|
||||
appMerchantDishNearbyListPost,
|
||||
appSearchSearchRecipePost,
|
||||
appCollectCollectPost,
|
||||
} from "@/service";
|
||||
import {thumbnailImg} from "@/utils/utils";
|
||||
const configStore = useConfigStore();
|
||||
const userStore = useUserStore();
|
||||
const emit = defineEmits(["toggleNotOpen"]);
|
||||
|
||||
const loading = ref(false);
|
||||
const recipePreviewLimit = 4;
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -29,10 +23,6 @@ function navigateTo(url: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleNotOpen() {
|
||||
emit("toggleNotOpen");
|
||||
}
|
||||
|
||||
async function initData() {
|
||||
if(!recipeData.value) {
|
||||
loading.value = true;
|
||||
@@ -43,13 +33,14 @@ async function initData() {
|
||||
}
|
||||
|
||||
// 获取菜谱数据
|
||||
const recipeData = ref([]);
|
||||
const recipeData = ref<any[]>([]);
|
||||
function getRecipeData() {
|
||||
appSearchSearchRecipePost({
|
||||
body: {
|
||||
params: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
}
|
||||
},
|
||||
body: {}
|
||||
}).then(res=> {
|
||||
console.log('菜谱数据', res)
|
||||
recipeData.value = res.rows;
|
||||
@@ -57,30 +48,16 @@ function getRecipeData() {
|
||||
loading.value = false;
|
||||
})
|
||||
}
|
||||
// 收藏菜品
|
||||
function handleSubmitCollectRecipe(item: any) {
|
||||
collectRecipe(item)
|
||||
}
|
||||
// 防抖处理函数
|
||||
const collectRecipe = debounce(1000, (item: any) => {
|
||||
appCollectCollectPost({
|
||||
body: {
|
||||
targetId: item.id,
|
||||
targetType: CollectionType.RECIPE
|
||||
}
|
||||
}).then(res=> {
|
||||
item.isCollect = !item.isCollect;
|
||||
})
|
||||
}, {
|
||||
atBegin: true, // 立即触发
|
||||
});
|
||||
|
||||
function navigateToRecipeDetail(id: string | number) {
|
||||
navigateTo(`/pages-user/pages/recipe/index?id=${id}`)
|
||||
}
|
||||
|
||||
function navigateToRecipeList() {
|
||||
navigateTo('/pages-user/pages/recipe/list')
|
||||
}
|
||||
|
||||
// 获取附近的菜品
|
||||
const dishData = ref([]);
|
||||
const dishData = ref<any[]>([]);
|
||||
function appMerchantDishNearbyList() {
|
||||
appMerchantDishNearbyListPost({
|
||||
params: {
|
||||
@@ -88,8 +65,8 @@ function appMerchantDishNearbyList() {
|
||||
pageSize: 10,
|
||||
},
|
||||
body: {
|
||||
lat: userStore.userLocation.latitude,
|
||||
lng: userStore.userLocation.longitude,
|
||||
lat: String(userStore.userLocation.latitude ?? ''),
|
||||
lng: String(userStore.userLocation.longitude ?? ''),
|
||||
}
|
||||
}).then(res=> {
|
||||
console.log('菜品数据', res)
|
||||
@@ -100,6 +77,27 @@ function handleClickDish(item: any) {
|
||||
navigateTo(`/pages-store/pages/store/index?id=${item.merchantId}`)
|
||||
}
|
||||
|
||||
function getMerchantName(item: any) {
|
||||
return item?.merchantVo?.merchantName || item?.merchantName||item?.dishName || '--'
|
||||
}
|
||||
|
||||
function getMerchantLogo(item: any) {
|
||||
return item?.merchantVo?.logo || item?.logo || item?.dishImage?.split?.(',')?.[0] || item?.dishImage || ''
|
||||
}
|
||||
|
||||
function getMerchantRate(item: any) {
|
||||
const rating = Number(item?.merchantVo?.rating ?? item?.rating ?? 0)
|
||||
return Number.isFinite(rating) && rating > 0 ? rating.toFixed(1) : '5.0'
|
||||
}
|
||||
|
||||
function getPreviewRecipeList() {
|
||||
return recipeData.value.slice(0, recipePreviewLimit)
|
||||
}
|
||||
|
||||
function showRecipeMore() {
|
||||
return recipeData.value.length > recipePreviewLimit
|
||||
}
|
||||
|
||||
async function getPlatformDefaultStoreInfo() {}
|
||||
|
||||
defineExpose({
|
||||
@@ -131,65 +129,57 @@ defineExpose({
|
||||
v-show="!loading"
|
||||
class="animate-in fade-in animate-ease-in animate-duration-300"
|
||||
>
|
||||
<view class="flex-center-sb px-30rpx pt-16rpx">
|
||||
<view class="text-56rpx text-#333 lh-56rpx font-bold">{{
|
||||
t("tabBar.browse")
|
||||
}}</view>
|
||||
<msg-box @toggleNotOpen="toggleNotOpen" />
|
||||
</view>
|
||||
<view class="px-30rpx mt-44rpx">
|
||||
<view class="px-24rpx pt-16rpx">
|
||||
<search />
|
||||
</view>
|
||||
<view class="mt-50rpx px-30rpx">
|
||||
<view @click="navigateTo('/pages-user/pages/recipe/list')" class="flex-center-sb">
|
||||
<text class="text-36rpx lh-36rpx text-#333 font-bold">{{
|
||||
t("pages.browse.titleRecipes")
|
||||
}}</text>
|
||||
<image src="@img/chef/116.png" class="w-64rpx h-64rpx"></image>
|
||||
</view>
|
||||
<scroll-view scroll-x="true" class="mt-16rpx">
|
||||
<view class="flex gap-30rpx">
|
||||
<template v-for="item in recipeData">
|
||||
<view @click="navigateToRecipeDetail(item.id)" class="w-312rpx">
|
||||
<view class="browse-wrap px-24rpx">
|
||||
<view class="section-title mt-36rpx">{{ t("pages.browse.titleRecipes") }}</view>
|
||||
<view class="mt-28rpx">
|
||||
<scroll-view scroll-x class="recipe-scroll" :show-scrollbar="false" :enable-flex="true">
|
||||
<view class="recipe-track">
|
||||
<view
|
||||
v-for="item in getPreviewRecipeList()"
|
||||
:key="item.id"
|
||||
class="recipe-item"
|
||||
@click="navigateToRecipeDetail(item.id)"
|
||||
>
|
||||
<image
|
||||
:src="thumbnailImg(item?.recipeImage?.split(',')[0])"
|
||||
class="w-310rpx h-296rpx rounded-24rpx mb-26rpx bg-common"
|
||||
class="recipe-avatar"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view class="flex-center-sb">
|
||||
<text class="text-30rpx lh-30rpx text-#333 line-clamp-1 tracking-[.04em] font-500"
|
||||
>{{ item.recipeName }}</text
|
||||
>
|
||||
<view class="w-40rpx h-40rpx ml-14rpx shrink-0">
|
||||
<collection
|
||||
:is-collected="item.isCollect"
|
||||
@collectionChange="handleSubmitCollectRecipe(item)"
|
||||
/>
|
||||
</view>
|
||||
/>
|
||||
<text class="recipe-name line-clamp-1">{{ item.recipeName }}</text>
|
||||
</view>
|
||||
<view v-if="showRecipeMore()" class="recipe-more" @click="navigateToRecipeList">
|
||||
<view class="recipe-more__text-wrap">
|
||||
<text class="recipe-more__text">{{ t("pages.browse.moreRecipes") }}</text>
|
||||
</view>
|
||||
<i class="i-carbon:chevron-right recipe-more__icon"></i>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view class="section-title mt-54rpx">{{ t("pages.browse.titleCuisine") }}</view>
|
||||
<scroll-view scroll-x class="store-scroll mt-28rpx pb-40rpx" :show-scrollbar="false" :enable-flex="true">
|
||||
<view class="store-track">
|
||||
<view v-for="item in dishData" :key="item.id" @click="handleClickDish(item)" class="store-card">
|
||||
<image
|
||||
:src="thumbnailImg(getMerchantLogo(item))"
|
||||
class="store-card__cover"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="store-card__right">
|
||||
<view class="store-card__name line-clamp-2">{{ getMerchantName(item) }}</view>
|
||||
<view class="store-card__rating">★★★★★ {{ getMerchantRate(item) }}</view>
|
||||
<view class="store-card__brand">{{ t("pages.browse.brandTag") }}</view>
|
||||
<view class="store-card__arrow center">
|
||||
<i class="i-carbon:chevron-right text-30rpx text-white"></i>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view
|
||||
class="mt-50rpx mb-28rpx text-36rpx lh-36rpx text-#333 font-bold"
|
||||
>{{ t("pages.browse.titleCuisine") }}</view
|
||||
>
|
||||
<view class="grid grid-cols-2 gap-x-30rpx gap-y-46rpx pb-40rpx">
|
||||
<template v-for="item in dishData">
|
||||
<view @click="handleClickDish(item)" class="w-330rpx overflow-hidden">
|
||||
<image
|
||||
:src="thumbnailImg(item?.dishImage?.split(',')[0])"
|
||||
class="w-full h-186rpx rounded-24rpx mb-16rpx bg-common"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<text class="text-30rpx lh-30rpx text-#333 line-clamp-1 tracking-[.04em] font-500"
|
||||
>{{ item.dishName }}</text
|
||||
>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -201,4 +191,148 @@ defineExpose({
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
.browse-wrap {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
line-height: 48rpx;
|
||||
// font-weight: 700;
|
||||
color: #1c1c1c;
|
||||
}
|
||||
|
||||
.recipe-scroll,
|
||||
.store-scroll {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.recipe-track {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 22rpx;
|
||||
padding-right: 24rpx;
|
||||
}
|
||||
|
||||
.recipe-item {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.recipe-avatar {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 56rpx;
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.recipe-name {
|
||||
max-width: 140rpx;
|
||||
margin-top: 14rpx;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
line-height: 32rpx;
|
||||
color: #222;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.recipe-more {
|
||||
flex-shrink: 0;
|
||||
height: 112rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 4rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
}
|
||||
|
||||
.recipe-more__text-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.recipe-more__text {
|
||||
font-size: 24rpx;
|
||||
line-height: 28rpx;
|
||||
color: #8a8a8a;
|
||||
font-weight: 500;
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: upright;
|
||||
letter-spacing: 2rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.recipe-more__icon {
|
||||
font-size: 20rpx;
|
||||
color: #8a8a8a;
|
||||
}
|
||||
|
||||
.store-track {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 22rpx;
|
||||
padding-right: 24rpx;
|
||||
}
|
||||
|
||||
.store-card {
|
||||
width: 404rpx;
|
||||
height: 220rpx;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.store-card__cover {
|
||||
width: 184rpx;
|
||||
height: 220rpx;
|
||||
background: #f2f2f2;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.store-card__right {
|
||||
flex: 1;
|
||||
padding: 16rpx 14rpx 14rpx 16rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.store-card__name {
|
||||
font-size: 28rpx;
|
||||
line-height: 34rpx;
|
||||
color: #191919;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.store-card__rating {
|
||||
margin-top: 8rpx;
|
||||
color: #111;
|
||||
font-size: 26rpx;
|
||||
line-height: 30rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.store-card__brand {
|
||||
margin-top: 12rpx;
|
||||
color: #d39a48;
|
||||
font-size: 24rpx;
|
||||
line-height: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.store-card__arrow {
|
||||
position: absolute;
|
||||
right: 10rpx;
|
||||
bottom: 12rpx;
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
border-radius: 50%;
|
||||
background: #111;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- 第一行:自动缓慢滚动 + 可手动滑动 -->
|
||||
<view class="scroll-row first-row">
|
||||
<scroll-view
|
||||
class="ab-scroll mb-22rpx"
|
||||
class="ab-scroll mb-10rpx"
|
||||
scroll-x
|
||||
show-scrollbar="false"
|
||||
:scroll-left="scrollLeft1"
|
||||
@@ -77,6 +77,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useCategoryNavStore } from '@/store'
|
||||
|
||||
// 定义分类项接口(与模板字段一致)
|
||||
interface CategoryItem {
|
||||
@@ -97,6 +98,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
categories: () => []
|
||||
})
|
||||
|
||||
const categoryNavStore = useCategoryNavStore()
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
/** 点击分类项事件 */
|
||||
@@ -241,10 +244,11 @@ function onScroll2(e: any) {
|
||||
}
|
||||
}
|
||||
|
||||
// 点击处理
|
||||
// 点击处理(列表页菜谱 id 来自 store,不放在 URL query)
|
||||
const handleItemClick = (item: CategoryItem) => {
|
||||
emit('itemClick', item)
|
||||
navigateTo('/pages-store/pages/list/index?id=' + item.id)
|
||||
categoryNavStore.setPendingRecipeCategoryId(item.id)
|
||||
navigateTo('/pages-store/pages/list/index')
|
||||
}
|
||||
|
||||
function navigateTo(url: string) {
|
||||
@@ -301,14 +305,16 @@ onUnmounted(() => {
|
||||
min-width: 120rpx;
|
||||
height: 60rpx;
|
||||
padding: 0 20rpx;
|
||||
border: 1px solid #C8C8C8;
|
||||
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.95);
|
||||
transform: translateY(0) scale(0.96);
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
|
||||
@@ -1,40 +1,277 @@
|
||||
<script setup lang="ts">
|
||||
import {thumbnailImg} from "@/utils/utils";
|
||||
import { thumbnailImg } from '@/utils/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
list: any[];
|
||||
}>();
|
||||
const { t } = useI18n();
|
||||
list: any[]
|
||||
}>()
|
||||
const { t } = useI18n()
|
||||
|
||||
function handleClickFood(item: any) {
|
||||
uni.navigateTo({
|
||||
url: '/pages-store/pages/store/index?id=' + item.id
|
||||
url: '/pages-store/pages/store/index?id=' + item.id,
|
||||
})
|
||||
}
|
||||
|
||||
function getCardImages(item: any): string[] {
|
||||
const raw = item?.shopImages
|
||||
let urls: string[] = []
|
||||
if (raw && typeof raw === 'string') {
|
||||
urls = raw
|
||||
.split(',')
|
||||
.map((s: string) => s.trim())
|
||||
.filter(Boolean)
|
||||
.map((u: string) => thumbnailImg(u) || u)
|
||||
}
|
||||
if (urls.length === 0 && item?.logo) {
|
||||
const logo = thumbnailImg(item.logo) || item.logo
|
||||
urls = logo ? [logo] : []
|
||||
}
|
||||
return urls
|
||||
}
|
||||
|
||||
function getTitleLines(item: any): { line1: string; line2: string } {
|
||||
const en = item?.merchantCategoryNamesEn?.[0]
|
||||
const zh = item?.merchantCategoryNamesZh?.[0]
|
||||
const name = String(item?.merchantName || '').trim()
|
||||
if (en && zh) return { line1: String(en), line2: String(zh) }
|
||||
if (en && !zh) return { line1: String(en), line2: name && name !== String(en) ? name : '' }
|
||||
if (zh && !en) return { line1: String(zh), line2: name && name !== String(zh) ? name : '' }
|
||||
const parts = name.split(/\n|\r|\||/).map((s) => s.trim()).filter(Boolean)
|
||||
if (parts.length >= 2) return { line1: parts[0], line2: parts[1] }
|
||||
return { line1: name, line2: '' }
|
||||
}
|
||||
|
||||
function formatMerchantRating(item: any): string {
|
||||
const r = Number(item?.rating)
|
||||
if (Number.isFinite(r) && r > 0) return r.toFixed(1)
|
||||
return '5.0'
|
||||
}
|
||||
|
||||
function subtitleLine(item: any): string {
|
||||
const comments = item?.commentCount ?? 0
|
||||
const dt = item?.deliveryTime
|
||||
if (dt === undefined || dt === null || dt === '') {
|
||||
return `(${comments})`
|
||||
}
|
||||
const n = Number(dt)
|
||||
if (Number.isFinite(n) && n > 0) {
|
||||
const unit = n === 1 ? t('common.day') : t('common.days')
|
||||
return `(${comments}) · ${n}${unit}`
|
||||
}
|
||||
return `(${comments}) · ${dt}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<scroll-view scroll-x="true">
|
||||
<view class="flex">
|
||||
<view class="w-30rpx shrink-0"></view>
|
||||
<template v-for="(item, index) in list" :key="item?.id ?? index">
|
||||
<view @click="handleClickFood(item)" :class="[index === 0 ? '' : 'ml-28rpx']">
|
||||
<image :src="thumbnailImg(item?.shopImages?.split(',')[0])" class="w-448rpx h-252rpx rounded-24rpx mb-20rpx bg-common" mode="aspectFill"></image>
|
||||
<text class="text-30rpx lh-30rpx text-#333 font-500 line-clamp-1">{{ item?.merchantName }}</text>
|
||||
<view v-if="+item.deliveryService === 1" class="text-#CE7138 text-24rpx lh-24rpx mt-12rpx">
|
||||
<scroll-view class="featured-scroll" scroll-x enable-flex :show-scrollbar="false">
|
||||
<view class="featured-track">
|
||||
<view class="featured-track__pad" />
|
||||
<view
|
||||
v-for="(item, index) in list"
|
||||
:key="item?.id ?? index"
|
||||
class="featured-card"
|
||||
:class="{ 'featured-card--first': index === 0 }"
|
||||
@click="handleClickFood(item)"
|
||||
>
|
||||
<!-- 左侧图:单图或与示例一致的双图上下分栏 -->
|
||||
<view class="featured-card__media">
|
||||
<template v-if="getCardImages(item).length >= 2">
|
||||
<image
|
||||
:src="getCardImages(item)[0]"
|
||||
class="featured-card__media-half featured-card__media-half--top"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<image
|
||||
:src="getCardImages(item)[1]"
|
||||
class="featured-card__media-half featured-card__media-half--bottom"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</template>
|
||||
<image
|
||||
v-else-if="getCardImages(item).length >= 1"
|
||||
:src="getCardImages(item)[0]"
|
||||
class="featured-card__media-single"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="featured-card__media-single featured-card__media-placeholder" />
|
||||
</view>
|
||||
|
||||
<view class="featured-card__body">
|
||||
<view class="featured-card__titles">
|
||||
<text v-if="getTitleLines(item).line1" class="featured-card__name featured-card__name--primary">{{
|
||||
getTitleLines(item).line1
|
||||
}}</text>
|
||||
<text v-if="getTitleLines(item).line2" class="featured-card__name featured-card__name--secondary">{{
|
||||
getTitleLines(item).line2
|
||||
}}</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-if="+item.deliveryService === 1"
|
||||
class="featured-card__fee"
|
||||
>
|
||||
{{ t('pages-store.store.tips5') }} ${{ item.deliveryFee }}{{ t('pages-store.store.start') }}
|
||||
</view>
|
||||
<view class="text-24rpx lh-24rpx flex items-center mt-12rpx">
|
||||
<text class="text-#333 font-500">{{ item.rating }}</text>
|
||||
<image src="@img/chef/124.png" class="w-24rpx h-24rpx mx-4rpx mt-2rpx"></image>
|
||||
<text class="text-#7D7D7D">({{ item.commentCount }}) • {{ item.deliveryTime }} {{ Number(item.deliveryTime) === 1 ? t('common.day') : t('common.days') }}</text>
|
||||
|
||||
<view class="featured-card__rating">
|
||||
<text class="featured-card__stars">★★★★★</text>
|
||||
<text class="featured-card__rate-num">{{ formatMerchantRating(item) }}</text>
|
||||
</view>
|
||||
|
||||
<text class="featured-card__sub">{{ subtitleLine(item) }}</text>
|
||||
|
||||
<view class="featured-card__arrow center">
|
||||
<view class="i-carbon:chevron-right text-32rpx text-white"></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<view class="w-30rpx shrink-0 op-0">1</view>
|
||||
</view>
|
||||
<view class="featured-track__pad featured-track__pad--end" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.featured-scroll {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
.featured-track {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
padding-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.featured-track__pad {
|
||||
flex-shrink: 0;
|
||||
width: 6rpx;
|
||||
}
|
||||
.featured-track__pad--end {
|
||||
width: 24rpx;
|
||||
}
|
||||
|
||||
.featured-card {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 620rpx;
|
||||
min-height: 256rpx;
|
||||
margin-left: 16rpx;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.featured-card--first {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.featured-card__media {
|
||||
width: 232rpx;
|
||||
flex-shrink: 0;
|
||||
background: #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.featured-card__media-single {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 256rpx;
|
||||
}
|
||||
|
||||
.featured-card__media-half {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.featured-card__media-half--top {
|
||||
border-bottom: 3rpx solid #fff;
|
||||
}
|
||||
|
||||
.featured-card__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 20rpx 20rpx 18rpx 20rpx;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.featured-card__titles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.featured-card__name {
|
||||
font-size: 28rpx;
|
||||
line-height: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
.featured-card__name--primary {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.featured-card__name--secondary {
|
||||
font-size: 28rpx;
|
||||
line-height: 34rpx;
|
||||
color: #1a1a1a;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.featured-card__media-placeholder {
|
||||
min-height: 256rpx;
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.featured-card__fee {
|
||||
margin-top: 10rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #d48806;
|
||||
}
|
||||
|
||||
.featured-card__rating {
|
||||
margin-top: 14rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.featured-card__stars {
|
||||
font-size: 22rpx;
|
||||
line-height: 1;
|
||||
color: #1a1a1a;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.featured-card__rate-num {
|
||||
font-size: 26rpx;
|
||||
line-height: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.featured-card__sub {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.featured-card__arrow {
|
||||
position: absolute;
|
||||
right: 16rpx;
|
||||
bottom: 16rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background: #14181b;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -61,11 +61,16 @@ function handleCollectionChange(value: boolean) {
|
||||
</script>
|
||||
<template>
|
||||
<view @click="handleClickFood" class="mb-52rpx">
|
||||
<image
|
||||
:src="item?.dishImage?.split(',')[0]||item?.logo"
|
||||
mode="aspectFill"
|
||||
class="w-100% h-400rpx rounded-24rpx bg-common"
|
||||
></image>
|
||||
<view class="food-box-img-wrap">
|
||||
<view v-if="item.isNew == 1" class="dish-new-ribbon">
|
||||
<text class="dish-new-ribbon__text">NEW</text>
|
||||
</view>
|
||||
<image
|
||||
:src="item?.dishImage?.split(',')[0]||item?.logo"
|
||||
mode="aspectFill"
|
||||
class="w-100% h-400rpx rounded-24rpx bg-common"
|
||||
></image>
|
||||
</view>
|
||||
<view class="flex justify-between items-start mt-14rpx">
|
||||
<view>
|
||||
<text class="text-30rpx lh-30rpx text-#333 font-500 line-clamp-1"
|
||||
@@ -86,3 +91,37 @@ function handleCollectionChange(value: boolean) {
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.food-box-img-wrap {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.dish-new-ribbon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 184rpx;
|
||||
height: 184rpx;
|
||||
overflow: hidden;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
|
||||
&__text {
|
||||
position: absolute;
|
||||
top: 26rpx;
|
||||
left: -66rpx;
|
||||
width: 240rpx;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
letter-spacing: 2rpx;
|
||||
line-height: 44rpx;
|
||||
background: #E23636;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from "@/store";
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 首页顶栏紧凑模式:更小图标与间距 */
|
||||
compact?: boolean
|
||||
}>(),
|
||||
{ compact: false }
|
||||
)
|
||||
const emit = defineEmits(['toggleNotOpen']);
|
||||
const userStore = useUserStore();
|
||||
function navigateTo(url: string) {
|
||||
@@ -12,12 +19,25 @@ function navigateTo(url: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="flex items-center">
|
||||
<view @click="navigateTo('/pages-user/pages/message/index')" class="w-40rpx h-40rpx mr-42rpx relative">
|
||||
<view v-if="userStore.isLogin && userStore.unreadMessageCount > 0" class="w-32rpx h-32rpx bg-#E23636 absolute z-2 top--16rpx right--16rpx rounded-50% text-24rpx text-#fff text-center line-height-32rpx">{{ userStore.unreadMessageCount }}</view>
|
||||
<image src="@img/chef/114.png" class="w-40rpx h-40rpx"></image>
|
||||
<view class="flex items-center" :class="compact ? 'gap-20rpx' : ''">
|
||||
<view
|
||||
@click="navigateTo('/pages-user/pages/message/index')"
|
||||
:class="compact ? 'w-34rpx h-34rpx mr-0' : 'w-40rpx h-40rpx mr-42rpx'"
|
||||
class="relative shrink-0"
|
||||
>
|
||||
<view
|
||||
v-if="userStore.isLogin && userStore.unreadMessageCount > 0"
|
||||
:class="compact ? 'h-26rpx top--10rpx right--10rpx text-20rpx px-8rpx' : 'w-32rpx h-32rpx top--16rpx right--16rpx text-24rpx line-height-32rpx'"
|
||||
class="bg-#E23636 absolute z-2 rounded-50% text-#fff text-center font-500"
|
||||
>{{ userStore.unreadMessageCount }}</view>
|
||||
<image src="@img/chef/114.png" :class="compact ? 'w-34rpx h-34rpx' : 'w-40rpx h-40rpx'"></image>
|
||||
</view>
|
||||
<image @click="emit('toggleNotOpen')" src="@img/chef/115.png" class="w-40rpx h-40rpx"></image>
|
||||
<image
|
||||
@click="emit('toggleNotOpen')"
|
||||
src="@img/chef/115.png"
|
||||
:class="compact ? 'w-34rpx h-34rpx' : 'w-40rpx h-40rpx'"
|
||||
class="shrink-0"
|
||||
></image>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -23,12 +23,22 @@ function handleClickSearch() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view @click="handleClickSearch" class="flex items-center h-88rpx bg-#F2F3F6 rounded-44rpx pl-36rpx">
|
||||
<image src="@img/chef/100222.png" class="w-28rpx h-28rpx"></image>
|
||||
<text class="text-30rpx text-#434343 ml-16rpx tracking-[.04em] font-500">{{ t('components.search.placeholder') }}</text>
|
||||
</view>
|
||||
<view
|
||||
@click="handleClickSearch"
|
||||
class="home-search-bar flex items-center h-88rpx rounded-44rpx pl-36rpx pr-28rpx bg-white"
|
||||
>
|
||||
<image src="@img/chef/100222.png" class="w-28rpx h-28rpx shrink-0"></image>
|
||||
<text class="home-search-placeholder text-28rpx ml-16rpx tracking-[.04em] font-500 truncate">{{
|
||||
t('components.search.placeholder')
|
||||
}}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.home-search-bar {
|
||||
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.home-search-placeholder {
|
||||
color: #9a9a9a;
|
||||
}
|
||||
</style>
|
||||
@@ -75,8 +75,11 @@ function selectTab(item: any) {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s;
|
||||
width: 132rpx;
|
||||
height: 132rpx;
|
||||
width: 102rpx;
|
||||
height: 102rpx;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
.img-selected {
|
||||
border: 4rpx solid #ce7138;
|
||||
@@ -86,8 +89,8 @@ function selectTab(item: any) {
|
||||
}
|
||||
|
||||
.tab-img {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
width: 98rpx;
|
||||
height: 98rpx;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "@/service";
|
||||
import usePage from "@/hooks/usePage";
|
||||
import {getFeaturedDishList} from "@/pages-store/service";
|
||||
import { formatSalesCount } from "@/utils/utils";
|
||||
const configStore = useConfigStore();
|
||||
const userStore = useUserStore();
|
||||
const props = defineProps<{
|
||||
@@ -41,6 +42,12 @@ function isSoldOutStock(stockLike: unknown) {
|
||||
return !Number.isNaN(n) && n <= 0
|
||||
}
|
||||
|
||||
/** 底部营销条文案(接口若返回 marketingLabel / hotSaleTag 等则展示) */
|
||||
function getDishPromoLabel(item: Record<string, unknown>): string {
|
||||
const raw = item.marketingLabel ?? item.hotSaleTag ?? item.rankTag ?? item.promotionLabel
|
||||
return typeof raw === 'string' && raw.trim() ? raw.trim() : ''
|
||||
}
|
||||
|
||||
function navigateTo(url: string) {
|
||||
if(userStore.checkLogin()) {
|
||||
uni.navigateTo({
|
||||
@@ -209,6 +216,30 @@ const isShowMerchant = computed(()=> {
|
||||
}
|
||||
})
|
||||
|
||||
// 精选菜品瀑布流:按序均分到两列(0,2,4… / 1,3,5…),分页追加后仍交错排列
|
||||
const featuredDishColumns = computed(() => {
|
||||
const list = dataList.value
|
||||
return [
|
||||
list.filter((_, i) => i % 2 === 0),
|
||||
list.filter((_, i) => i % 2 === 1),
|
||||
]
|
||||
})
|
||||
|
||||
/** 顶栏购物车角标(多商家购物车汇总件数) */
|
||||
const cartBadgeTotal = computed(() => {
|
||||
const list = userStore.userCartAllData
|
||||
if (!Array.isArray(list) || list.length === 0) return 0
|
||||
let n = 0
|
||||
for (const m of list) {
|
||||
n += (m as { merchantCartVoList?: unknown[] })?.merchantCartVoList?.length || 0
|
||||
}
|
||||
return n
|
||||
})
|
||||
|
||||
function goCart() {
|
||||
navigateTo('/pages-user/pages/cart/index')
|
||||
}
|
||||
|
||||
// 手动触发下拉刷新了
|
||||
function onRefresh() {
|
||||
console.log('手动触发下拉刷新了')
|
||||
@@ -221,7 +252,7 @@ function handleClickSwiper(item: any) {
|
||||
console.log(item, '点击轮播图')
|
||||
switch (Number(item.activityType)) {
|
||||
case 1: // 商家列表
|
||||
navigateTo('/pages-store/pages/list/index?id=')
|
||||
navigateTo('/pages-store/pages/list/index')
|
||||
break
|
||||
case 2: // 活动菜品列表
|
||||
navigateTo('/pages-store/pages/dishes/index?id=' + item.id)
|
||||
@@ -261,7 +292,7 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
||||
|
||||
<template>
|
||||
<view
|
||||
class="bg-#fff"
|
||||
class="home-page-root"
|
||||
:style="[
|
||||
{
|
||||
height: configStore.windowHeight + 'px',
|
||||
@@ -271,20 +302,34 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
||||
<z-paging @onRefresh="onRefresh" ref="paging" v-model="dataList" :auto="false" @query="queryList" @scroll="onPageScroll" :refresher-enabled="true" :auto-show-back-to-top="false">
|
||||
<template #top>
|
||||
<status-bar />
|
||||
<view class="flex items-center pt-18rpx px-30rpx pb-20rpx">
|
||||
<!-- <text class="text-52rpx lh-52rpx text-#333 font-bold shrink-0">{{Config.appName}}</text>-->
|
||||
<image
|
||||
src="@img/logo.png"
|
||||
class="w-52rpx h-52rpx shrink-0"
|
||||
></image>
|
||||
<view class="bg-#D8D8D8 w-1rpx h-40rpx mx-14rpx"></view>
|
||||
<view @click="navigateTo('/pages/address/index')" class="text-#00A76D text-28rpx lh-28rpx flex items-center">
|
||||
<text v-if="userStore.appointmentTimeShow">{{ t('pages.address.appTime') }}:{{ userStore.appointmentTimeShow }}</text>
|
||||
<text v-else>{{ t('pages.address.reservation') }}</text>
|
||||
<image
|
||||
src="@img/chef/119.png"
|
||||
class="w-24rpx h-24rpx ml-6rpx mt-4rpx shrink-0"
|
||||
></image>
|
||||
<!-- 设计稿:品牌行 + 右侧地址胶囊、消息/客服、购物车 -->
|
||||
<view class="home-top-header px-24rpx pt-12rpx pb-8rpx">
|
||||
<view class="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%;"></image>
|
||||
<text class="text-32rpx lh-36rpx text-#1a1a1a font-bold tracking-tight shrink-0">{{ Config.appName }}</text>
|
||||
</view>
|
||||
<view class="flex items-center gap-10rpx shrink-0">
|
||||
<view class="home-loc-pill" @click="navigateTo('/pages-user/pages/search-address/index')">
|
||||
<!-- <image src="@img/chef/101.png" class="home-loc-pill__pin w-22rpx h-22rpx shrink-0"></image> -->
|
||||
<text class="home-loc-pill__text line-clamp-1">{{
|
||||
userStore.userLocation.location || 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>
|
||||
</view>
|
||||
<view class="home-cart-btn" @click="goCart">
|
||||
<view class="i-carbon:shopping-cart text-36rpx text-#14181b"></view>
|
||||
<view v-if="userStore.isLogin && cartBadgeTotal > 0" class="home-cart-badge">{{ cartBadgeTotal > 99 ? '99+' : cartBadgeTotal }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="home-delivery-actions-row mt-14rpx flex items-center justify-between gap-16rpx">
|
||||
<view @click="navigateTo('/pages/address/index')" class="home-delivery-row flex items-center min-w-0 flex-1 text-26rpx lh-32rpx text-#00A76D">
|
||||
<text v-if="userStore.appointmentTimeShow">{{ t('pages.address.appTime') }}: {{ userStore.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>
|
||||
<msg-box compact class="shrink-0" @toggleNotOpen="toggleNotOpen" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -294,169 +339,415 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
||||
>
|
||||
<home-skeleton />
|
||||
</view>
|
||||
<view class="flex-center-sb px-30rpx pt-34rpx">
|
||||
<!--展示用户的定位城市,如果用户没有使用定位则展示选择的城市,用户选择城市后,需要更新定位城市-->
|
||||
<view @click="navigateTo('/pages-user/pages/search-address/index')" class="flex items-center text-30rpx text-#333 font-500">
|
||||
<text class="line-clamp-1">
|
||||
{{ userStore.userLocation.location || t('pages.home.default-location') }}
|
||||
</text>
|
||||
<image
|
||||
src="@img/chef/101.png"
|
||||
class="w-24rpx h-24rpx ml-10rpx mt-6rpx shrink-0"
|
||||
></image>
|
||||
</view>
|
||||
<view class="shrink-0 ml-40rpx">
|
||||
<msg-box @toggleNotOpen="toggleNotOpen" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="px-30rpx mt-32rpx pb-22rpx">
|
||||
<view class="px-24rpx pt-12rpx pb-8rpx">
|
||||
<search />
|
||||
<!-- 分类滚动区域 -->
|
||||
<view class="mt-40rpx" v-if="appMerchantLabelList.length > 0">
|
||||
<!-- 分类标签(双行横滑) -->
|
||||
<view class="mt-28rpx" v-if="appMerchantLabelList.length > 0">
|
||||
<class-bullet :categories="appMerchantLabelList" @itemClick="handleItemClick" />
|
||||
</view>
|
||||
</view>
|
||||
<swiper
|
||||
class="card-swiper"
|
||||
class="home-promo-swiper card-swiper"
|
||||
:circular="true"
|
||||
:autoplay="true"
|
||||
previous-margin="60rpx"
|
||||
next-margin="60rpx"
|
||||
previous-margin="48rpx"
|
||||
next-margin="48rpx"
|
||||
>
|
||||
<template v-for="item in swiperList" :key="item.id">
|
||||
<swiper-item @click="handleClickSwiper(item)" class="">
|
||||
<image
|
||||
:src="item.activityImage"
|
||||
class="swiper-item-content w-full h-100% rounded-24rpx bg-common"
|
||||
></image>
|
||||
</swiper-item>
|
||||
</template>
|
||||
<swiper-item
|
||||
v-for="(item, sIdx) in swiperList"
|
||||
:key="item.id ?? sIdx"
|
||||
@click="handleClickSwiper(item)"
|
||||
>
|
||||
<image
|
||||
:src="item.activityImage"
|
||||
class="swiper-item-content w-full h-100% rounded-32rpx bg-common"
|
||||
></image>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
|
||||
<!-- 分类滚动区域 -->
|
||||
<tabs-type @changeType="tabsTypeChange" v-if="appMerchantCategoryList.length > 0" :list="appMerchantCategoryList" :currentId="currentCategory" class="mt-22rpx" />
|
||||
<!-- 快捷入口(圆形分类) -->
|
||||
<tabs-type @changeType="tabsTypeChange" v-if="appMerchantCategoryList.length > 0" :list="appMerchantCategoryList" :currentId="currentCategory" class="mt-28rpx home-tabs-quick" />
|
||||
|
||||
<!-- 筛选工具 -->
|
||||
<!-- <filtrate-tool class="mt-32rpx" @togglePickup="togglePickup" @toggleDiscount="toggleDiscount" @toggleScore="toggleScore" @togglePrice="togglePrice" />-->
|
||||
|
||||
<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="mt-56rpx">
|
||||
<view
|
||||
class="mb-30rpx pl-30rpx text-36rpx lh-36rpx text-#333 font-bold"
|
||||
>{{ t('pages.home.featured-on') }}</view>
|
||||
<!-- 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">
|
||||
{{ t('pages.home.featured-on') }}
|
||||
</view>
|
||||
<featured-on :list="featuredList" />
|
||||
</view>
|
||||
|
||||
<!-- Nearby Merchants 附近商家 -->
|
||||
<view v-if="nearbyList.length > 0" class="mt-56rpx">
|
||||
<view
|
||||
class="mb-32rpx pl-30rpx text-36rpx lh-36rpx text-#333 font-bold"
|
||||
>{{ t('pages.home.nearby-merchants') }}</view>
|
||||
<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 ">
|
||||
{{ t('pages.home.nearby-merchants') }}
|
||||
</view>
|
||||
<nearby-merchants :list="nearbyList" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- List -->
|
||||
<view class="mt-56rpx px-30rpx">
|
||||
<view class="mb-32rpx text-36rpx lh-36rpx text-#333 font-bold"
|
||||
<!-- List 精选菜品瀑布流(浅底 + 白卡片 + 阴影,结构对齐设计稿) -->
|
||||
<view class="featured-dishes-section mt-36rpx px-24rpx pb-40rpx">
|
||||
<view class="mb-24rpx px-6rpx text-32rpx lh-44rpx text-#1a1a1a "
|
||||
>{{ t('pages.home.featured-dishes') }}</view>
|
||||
<template v-for="(item, index) in dataList" :key="index">
|
||||
<view @click="navigateToDishes(item)" class="w-100% mb-30rpx">
|
||||
<view class="relative h-448rpx rounded-24rpx mb-28rpx">
|
||||
<view @click.stop="handleDishCollectionClick(item)" class="w-68rpx h-68rpx absolute z-2 top-0 right-0">
|
||||
<view class="waterfall-row flex gap-16rpx items-start">
|
||||
<view
|
||||
v-for="(col, colIndex) in featuredDishColumns"
|
||||
:key="colIndex"
|
||||
class="waterfall-col flex-1 min-w-0 flex flex-col gap-16rpx"
|
||||
>
|
||||
<view
|
||||
v-for="item in col"
|
||||
:key="item.id || String(item.merchantId) + '-' + item.dishName"
|
||||
@click="navigateToDishes(item)"
|
||||
class="featured-dish-card w-full"
|
||||
>
|
||||
<view class="featured-dish-image">
|
||||
<view v-if="item.isNew == 1" class="dish-new-ribbon">
|
||||
<text class="dish-new-ribbon__text">NEW</text>
|
||||
</view>
|
||||
<image
|
||||
v-if="!item.isCollect"
|
||||
src="@img-store/1334.png"
|
||||
:src="item?.dishImage?.split(',')[0]"
|
||||
mode="aspectFill"
|
||||
class="w-full h-full"
|
||||
class="featured-dish-img"
|
||||
/>
|
||||
<view
|
||||
v-if="isSoldOutStock(item?.stock)"
|
||||
class="featured-dish-sold-dim"
|
||||
/>
|
||||
<image
|
||||
v-else
|
||||
src="@img-store/1337.png"
|
||||
mode="aspectFill"
|
||||
class="w-full h-full"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
v-if="isSoldOutStock(item?.stock)"
|
||||
class="absolute z-2 left-20rpx top-20rpx px-16rpx h-52rpx rounded-26rpx bg-#14181B/70 center text-26rpx text-#fff font-500"
|
||||
>
|
||||
已售完
|
||||
</view>
|
||||
<image
|
||||
:src="item?.dishImage?.split(',')[0]"
|
||||
v-if="isSoldOutStock(item?.stock)"
|
||||
src="/static/app/images/SoldOut.png"
|
||||
mode="aspectFill"
|
||||
class="w-full h-full rounded-24rpx bg-common"
|
||||
/>
|
||||
</view>
|
||||
<view class="line-clamp-1 text-30rpx text-#333 font-500">
|
||||
{{ item?.dishName }}
|
||||
</view>
|
||||
<view class="flex-center-sb mt-12rpx">
|
||||
<text class="text-32rpx lh-30rpx text-#333 font-500">US${{ item?.discountPrice }}</text>
|
||||
<view
|
||||
v-if="Number(item?.memberPrice) > 0"
|
||||
class="member-price-tag text-[#FBE3C3] font-500 text-30rpx lh-30rpx center pl-6rpx break-all"
|
||||
>
|
||||
<text>{{ t('pages-store.store.members') }}: </text>
|
||||
${{ item?.memberPrice }}
|
||||
class="featured-dish-sold-overlay"
|
||||
/>
|
||||
<view
|
||||
@click.stop="handleDishCollectionClick(item)"
|
||||
class="featured-dish-collect w-56rpx h-56rpx absolute z-4 top-12rpx right-12rpx center"
|
||||
>
|
||||
<image
|
||||
v-if="!item.isCollect"
|
||||
src="@img-store/1334.png"
|
||||
mode="aspectFill"
|
||||
class="w-44rpx h-44rpx featured-dish-collect-icon"
|
||||
/>
|
||||
<image
|
||||
v-else
|
||||
src="@img-store/1337.png"
|
||||
mode="aspectFill"
|
||||
class="w-44rpx h-44rpx featured-dish-collect-icon"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex-center-sb mt-12rpx">
|
||||
<view class="text-28rpx text-#999">
|
||||
<view class="line-through">US${{ item?.originalPrice }}</view>
|
||||
<view>{{ t('pages-store.store.sales') }}:{{ item?.salesCount }}</view>
|
||||
</view>
|
||||
|
||||
<view class="center w-64rpx h-64rpx rounded-50% bg-white shadow-lg">
|
||||
<image
|
||||
src="@img/chef/1285.png"
|
||||
class="w-30rpx h-30rpx shrink-0"
|
||||
></image>
|
||||
<view class="featured-dish-body">
|
||||
<view class="featured-dish-meta flex items-start justify-between gap-12rpx mb-14rpx">
|
||||
<view class="min-w-0 flex-1">
|
||||
<text class="featured-dish-price">US${{ item?.discountPrice }}</text>
|
||||
<text
|
||||
v-if="Number(item?.originalPrice) > Number(item?.discountPrice)"
|
||||
class="featured-dish-original"
|
||||
>US${{ item?.originalPrice }}</text>
|
||||
</view>
|
||||
<text class="featured-dish-sales shrink-0">{{ t('pages-store.store.sales') }}: {{ formatSalesCount(item?.salesCount) }}</text>
|
||||
</view>
|
||||
<view class="featured-dish-title line-clamp-2 mb-16rpx">
|
||||
{{ item?.dishName }}
|
||||
</view>
|
||||
<view class="flex items-center justify-between gap-12rpx">
|
||||
<view
|
||||
v-if="Number(item?.memberPrice) > 0"
|
||||
class="featured-dish-member shrink min-w-0"
|
||||
>
|
||||
<text class="featured-dish-member-inner">{{ t('pages-store.store.members') }}: US${{ item?.memberPrice }}</text>
|
||||
</view>
|
||||
<view v-else class="flex-1 min-w-0"></view>
|
||||
<view class="featured-dish-add center shrink-0">
|
||||
<image
|
||||
src="@img/chef/1285.png"
|
||||
class="w-28rpx h-28rpx"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="getDishPromoLabel(item as Record<string, unknown>)"
|
||||
class="featured-dish-promo mt-16rpx"
|
||||
>
|
||||
<text class="featured-dish-promo-text">{{ getDishPromoLabel(item as Record<string, unknown>) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
<template #bottom>
|
||||
<view class="h-50px"></view>
|
||||
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
|
||||
</template>
|
||||
</z-paging>
|
||||
<view v-if="userStore.isLogin && userStore.userCartAllData.length > 0" @click="navigateTo('/pages-user/pages/cart/index')" class="fixed bottom-138rpx left-50% translate-x--50% px-26rpx h-88rpx bg-#14181B rounded-44rpx center text-28rpx text-#fff font-500">
|
||||
<image src="@img/chef/125.png" class="w-28rpx h-28rpx shrink-0"></image>
|
||||
<view class="ml-10rpx whitespace-nowrap">{{ userStore.userCartAllData[0]?.merchantName }}</view>
|
||||
<view class="w-8rpx h-8rpx rounded-50% bg-white mx-8rpx"></view>
|
||||
<text>{{ userStore.userCartAllData[0]?.merchantCartVoList?.length || 0 }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 回到顶部按钮 -->
|
||||
<view v-if="showBackToTop" @click="scrollToTop" class="fixed bottom-148rpx left-30rpx w-88rpx h-88rpx bg-#14181B rounded-50% center shadow-lg">
|
||||
<!-- 回到顶部(购物车已并入顶栏,不再使用底部浮条) -->
|
||||
<view v-if="showBackToTop" @click="scrollToTop" class="home-back-top fixed left-30rpx w-88rpx h-88rpx bg-#14181B rounded-50% center shadow-lg">
|
||||
<image src="@img/chef/119.png" class="w-40rpx h-40rpx shrink-0 rotate-180"></image>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.home-page-root {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.home-top-header {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.home-loc-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
max-width: 200rpx;
|
||||
padding: 12rpx 18rpx;
|
||||
background: #fff;
|
||||
border-radius: 999rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.home-loc-pill__text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 22rpx;
|
||||
line-height: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.home-cart-btn {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
.home-cart-badge {
|
||||
position: absolute;
|
||||
top: 4rpx;
|
||||
right: 4rpx;
|
||||
min-width: 28rpx;
|
||||
height: 28rpx;
|
||||
padding: 0 6rpx;
|
||||
font-size: 18rpx;
|
||||
line-height: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background: #e23636;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.home-delivery-row {
|
||||
padding-left: 2rpx;
|
||||
}
|
||||
|
||||
.home-promo-swiper {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.home-tabs-quick {
|
||||
padding-left: 8rpx;
|
||||
padding-right: 8rpx;
|
||||
}
|
||||
|
||||
.nearby-merchants-block {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
/* 为底栏 Tab 预留空间,避免被遮挡 */
|
||||
.home-back-top {
|
||||
bottom: calc(100rpx + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.card-swiper {
|
||||
height: 420rpx;
|
||||
height: 400rpx;
|
||||
}
|
||||
.swiper-item-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: scale(0.95);
|
||||
border-radius: 20rpx;
|
||||
transition: transform 0.3s;
|
||||
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);
|
||||
}
|
||||
.member-price-tag {
|
||||
min-width: 220rpx;
|
||||
height: 42rpx;
|
||||
background-image: url("/static/images/chef/1282.png");
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
/* 精选商家 / 精选菜品 区块(与页面背景统一) */
|
||||
.featured-merchants-section,
|
||||
.featured-dishes-section {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.featured-dish-collect-icon {
|
||||
filter: drop-shadow(0 2rpx 6rpx rgba(0, 0, 0, 0.18));
|
||||
}
|
||||
|
||||
/* 瀑布流商品图:固定 1:1 区域,便于「已售完」等角标统一 */
|
||||
.featured-dish-image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
padding-bottom: 100%;
|
||||
background: #f0f0f0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.featured-dish-img {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.featured-dish-sold-dim {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
background: rgba(20, 24, 27, 0.42);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.featured-dish-sold-overlay {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.featured-dish-card {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.featured-dish-body {
|
||||
padding: 20rpx 20rpx 22rpx;
|
||||
}
|
||||
|
||||
.featured-dish-price {
|
||||
color: #e02e24;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.featured-dish-original {
|
||||
margin-left: 10rpx;
|
||||
color: #b3b3b3;
|
||||
font-size: 22rpx;
|
||||
font-weight: 400;
|
||||
text-decoration: line-through;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.featured-dish-sales {
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.35;
|
||||
max-width: 48%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.featured-dish-title {
|
||||
color: #1a1a1a;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* 会员价:暖色胶囊,替代长条形切图 */
|
||||
.featured-dish-member {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: calc(100% - 88rpx);
|
||||
}
|
||||
|
||||
.featured-dish-member-inner {
|
||||
display: inline-block;
|
||||
padding: 6rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(180deg, #fff5eb 0%, #ffe8d6 100%);
|
||||
color: #c45c1a;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.featured-dish-add {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background: #14181b;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/* 可选营销条(淡红底 + 文案) */
|
||||
.featured-dish-promo {
|
||||
padding: 12rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #fff0f0;
|
||||
}
|
||||
|
||||
.featured-dish-promo-text {
|
||||
color: #e02e24;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.dish-new-ribbon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 184rpx;
|
||||
height: 184rpx;
|
||||
overflow: hidden;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
|
||||
&__text {
|
||||
position: absolute;
|
||||
top: 26rpx;
|
||||
left: -66rpx;
|
||||
width: 240rpx;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
letter-spacing: 2rpx;
|
||||
line-height: 44rpx;
|
||||
background: #E23636;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -194,7 +194,7 @@ const poster = computed(() => ({
|
||||
type: 'view',
|
||||
},
|
||||
{
|
||||
text: 'http://192.168.5.118:8011/h5/'+ `pages-login/pages/sign-up/index?invitationCode=${userStore.userInfo.invitationCode}`,
|
||||
text: 'http://192.168.5.118:8091/#/'+ `pages-login/pages/sign-up/index?invitationCode=${userStore.userInfo.invitationCode}`,
|
||||
type: 'qrcode',
|
||||
css: {
|
||||
width: '124rpx',
|
||||
|
||||
@@ -83,10 +83,16 @@ defineExpose({
|
||||
</view>
|
||||
<view class="shrink-0 ml-20rpx" v-if="userStore.userInfo.invitationCode">
|
||||
<uqrcode ref="uqrcode" canvas-id="qrcode"
|
||||
:value="'http://192.168.5.118:8011/h5/' + `pages-login/pages/sign-up/index?invitationCode=${userStore.userInfo.invitationCode}`"
|
||||
:value="'http://192.168.5.118:8091/#/' + `pages-login/pages/sign-up/index?invitationCode=${userStore.userInfo.invitationCode}`"
|
||||
:size="124"
|
||||
sizeUnit="rpx"
|
||||
:options="{}"></uqrcode>
|
||||
|
||||
<!-- <uqrcode ref="uqrcode" canvas-id="qrcode"
|
||||
:value="'http://192.168.5.118:8011/h5/' + `pages-login/pages/sign-up/index?invitationCode=${userStore.userInfo.invitationCode}`"
|
||||
:size="124"
|
||||
sizeUnit="rpx"
|
||||
:options="{}"></uqrcode> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -499,8 +499,8 @@ margin: 0 30rpx;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
width: 38rpx;
|
||||
height: 38rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
+397
-74
@@ -3,10 +3,11 @@ import {
|
||||
appMerchantOrderOrderListPost,
|
||||
type MerchantOrderVo
|
||||
} from "@/service";
|
||||
import {callPhone, formatTimestampWithWeekday} from "@/utils/utils";
|
||||
import {callPhone} from "@/utils/utils";
|
||||
import {OrderCancelStatus, OrderStatus} from "@/constant/enums";
|
||||
import {useUserStore} from "@/store";
|
||||
import HomeSkeleton from "@/pages/home/components/tabbar-home/components/home-skeleton.vue";
|
||||
import {dayjs} from "@/plugin";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
@@ -31,7 +32,7 @@ const {paging, dataList, loading, queryList, firstLoaded} = usePage<MerchantOrde
|
||||
},
|
||||
body: {
|
||||
userPort: 1,
|
||||
orderStatusList: props.status ? [Number(props.status + 1)] : [],
|
||||
orderStatusList: props.status !== null && props.status !== '' ? [Number(props.status)] : [],
|
||||
}
|
||||
})
|
||||
} else {
|
||||
@@ -41,7 +42,65 @@ const {paging, dataList, loading, queryList, firstLoaded} = usePage<MerchantOrde
|
||||
}
|
||||
})
|
||||
|
||||
function handleClick(item: any) {
|
||||
function normalizeTimestamp(input: unknown): number | null {
|
||||
if (input == null || input === '') return null
|
||||
|
||||
let value: number
|
||||
if (typeof input === 'string') {
|
||||
const trimmed = input.trim()
|
||||
if (!trimmed) return null
|
||||
value = Number(trimmed)
|
||||
} else {
|
||||
value = Number(input)
|
||||
}
|
||||
|
||||
if (!Number.isFinite(value) || value <= 0) return null
|
||||
|
||||
// 兼容后端可能返回秒级时间戳
|
||||
if (value < 1e12) {
|
||||
value = value * 1000
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function formatOrderCardTime(item: MerchantOrderVo) {
|
||||
// 优先使用下单时间 createTime;没有则回退到预约结束时间
|
||||
const ts = normalizeTimestamp((item as any)?.createTime ?? item?.endScheduledTime)
|
||||
if (!ts) return '--'
|
||||
return dayjs(ts).format('YYYY/MM/DD HH:mm')
|
||||
}
|
||||
|
||||
function formatOrderPrice(item: MerchantOrderVo) {
|
||||
return (item.paidAmount ?? item.actualPrice ?? 0).toFixed(2)
|
||||
}
|
||||
|
||||
function getOrderDishes(item: MerchantOrderVo): Array<Record<string, any>> {
|
||||
const list = item.merchantOrderDishVoList as unknown as Array<Record<string, any>> | null | undefined
|
||||
if (!list || !Array.isArray(list)) return []
|
||||
return list
|
||||
}
|
||||
|
||||
function dishCover(dish: Record<string, any>) {
|
||||
const img = dish?.merchantDishVo?.dishImage
|
||||
if (!img || typeof img !== 'string') return ''
|
||||
return img.split(',')[0] || ''
|
||||
}
|
||||
|
||||
function dishTitle(dish: Record<string, any>) {
|
||||
return dish?.merchantDishVo?.dishName ?? ''
|
||||
}
|
||||
|
||||
function getTotalDishCount(item: MerchantOrderVo) {
|
||||
return getOrderDishes(item).reduce((s, d) => s + (d.count || 0), 0)
|
||||
}
|
||||
|
||||
function handleClick(item: MerchantOrderVo) {
|
||||
uni.navigateTo({
|
||||
url: '/pages-store/pages/order/index?id=' + item.id
|
||||
})
|
||||
}
|
||||
|
||||
function handleCancelClick(item: MerchantOrderVo) {
|
||||
uni.navigateTo({
|
||||
url: '/pages-store/pages/order/index?id=' + item.id
|
||||
})
|
||||
@@ -66,66 +125,65 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="h-full">
|
||||
<view class="list-root h-full">
|
||||
<z-paging ref="paging" v-model="dataList" @query="queryList" :fixed="false" :auto="false">
|
||||
<view class="p-30rpx">
|
||||
<view class="page-pad">
|
||||
<view
|
||||
v-show="loading"
|
||||
class="animate-in fade-in animate-ease-out animate-duration-300"
|
||||
>
|
||||
<template v-for="item in 3">
|
||||
<view class="w-full px-50rpx pt-43rpx pb-36rpx bg-white rounded-36rpx mb-30rpx last:mb-0">
|
||||
<!-- 头部时间和状态骨架屏 -->
|
||||
<view class="flex-center-sb mb-28rpx">
|
||||
<view class="w-280rpx h-34rpx skeleton-item rounded-8rpx"></view>
|
||||
<view class="w-120rpx h-34rpx skeleton-item rounded-8rpx"></view>
|
||||
<view v-for="sk in 3" :key="'sk-' + sk" class="order-card order-card--skeleton mb-24rpx">
|
||||
<view class="flex-center-sb mb-24rpx">
|
||||
<view class="w-280rpx h-32rpx skeleton-item rounded-8rpx"></view>
|
||||
<view class="w-120rpx h-32rpx skeleton-item rounded-8rpx"></view>
|
||||
</view>
|
||||
|
||||
<!-- 商家信息骨架屏 -->
|
||||
<view class="flex items-center my-28rpx">
|
||||
<view class="flex items-center mb-24rpx">
|
||||
<view class="w-32rpx h-32rpx skeleton-item rounded-6rpx mr-10rpx"></view>
|
||||
<view class="w-200rpx h-30rpx skeleton-item rounded-6rpx"></view>
|
||||
<view class="w-60rpx h-32rpx skeleton-item rounded-6rpx ml-20rpx"></view>
|
||||
<view class="w-240rpx h-28rpx skeleton-item rounded-6rpx"></view>
|
||||
<view class="w-72rpx h-28rpx skeleton-item rounded-20rpx ml-16rpx"></view>
|
||||
</view>
|
||||
|
||||
<!-- 轮播图骨架屏 -->
|
||||
<view class="mb-30rpx">
|
||||
<view class="w-100% h-508rpx skeleton-item rounded-36rpx mb-10rpx"></view>
|
||||
<view class="w-180rpx h-30rpx skeleton-item rounded-6rpx"></view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮区域骨架屏 -->
|
||||
<view class="flex-center-sb">
|
||||
<view class="w-56rpx h-56rpx skeleton-item rounded-28rpx"></view>
|
||||
<view class="flex items-center gap-20rpx">
|
||||
<view class="w-170rpx h-64rpx skeleton-item rounded-64rpx"></view>
|
||||
<view class="w-170rpx h-64rpx skeleton-item rounded-64rpx"></view>
|
||||
<view class="flex gap-16rpx mb-24rpx">
|
||||
<view v-for="i in 3" :key="i" class="sk-thumb">
|
||||
<view class="sk-thumb-img skeleton-item"></view>
|
||||
<view class="w-100rpx h-22rpx skeleton-item rounded-4rpx mt-10rpx"></view>
|
||||
</view>
|
||||
<view class="flex-1"></view>
|
||||
<view class="w-100rpx flex flex-col items-end gap-12rpx pt-8rpx">
|
||||
<view class="w-88rpx h-30rpx skeleton-item rounded-4rpx"></view>
|
||||
<view class="w-72rpx h-22rpx skeleton-item rounded-4rpx"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<view class="flex justify-end">
|
||||
<view class="sk-pill skeleton-item"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="animate-in fade-in animate-ease-in animate-duration-300"
|
||||
v-if="!loading"
|
||||
>
|
||||
<template v-for="(item,index) in dataList">
|
||||
<view @click="handleClick(item)" :class="[index === 0 ? '' : 'mt-30rpx']" class="w-full px-50rpx pt-43rpx pb-36rpx bg-white rounded-36rpx">
|
||||
<view class="flex-center-sb text-34rpx lh-34rpx font-bold">
|
||||
<text class="text-#333 tracking-[.04em]">{{ formatTimestampWithWeekday(item.endScheduledTime) }}</text>
|
||||
<text class="text-#00A76D tracking-[.04em] shrink-0">
|
||||
<view
|
||||
v-for="(item,index) in dataList"
|
||||
:key="item.id"
|
||||
:class="[index === 0 ? '' : 'mt-24rpx']"
|
||||
class="order-card"
|
||||
@click="handleClick(item)"
|
||||
>
|
||||
<view class="flex-center-sb items-start gap-16rpx">
|
||||
<text class="order-time">{{ formatOrderCardTime(item) }}</text>
|
||||
<text class="order-status shrink-0">
|
||||
<template v-if="
|
||||
+item.refundStatus === OrderCancelStatus.APPLIED ||
|
||||
+item.refundStatus === OrderCancelStatus.APPROVED ||
|
||||
+item.refundStatus === OrderCancelStatus.REJECTED
|
||||
item.refundStatus === OrderCancelStatus.APPLIED ||
|
||||
item.refundStatus === OrderCancelStatus.APPROVED ||
|
||||
item.refundStatus === OrderCancelStatus.REJECTED
|
||||
">
|
||||
<template v-if="+item.refundStatus === OrderCancelStatus.APPLIED">
|
||||
<template v-if="item.refundStatus === OrderCancelStatus.APPLIED">
|
||||
{{ t('pages-store.order.orderStatus.refund') }}
|
||||
</template>
|
||||
<template v-else-if="+item.refundStatus === OrderCancelStatus.APPROVED">
|
||||
<template v-else-if="item.refundStatus === OrderCancelStatus.APPROVED">
|
||||
{{ t('pages-store.order.orderStatus.agreeRefund') }}
|
||||
</template>
|
||||
<template v-else-if="+item.refundStatus === OrderCancelStatus.REJECTED">
|
||||
<template v-else-if="item.refundStatus === OrderCancelStatus.REJECTED">
|
||||
{{ t('pages-store.order.orderStatus.rejectRefund') }}
|
||||
</template>
|
||||
</template>
|
||||
@@ -133,54 +191,91 @@ defineExpose({
|
||||
<template v-if="item.orderStatus === OrderStatus.CANCELLED">{{ t('pages-store.order.orderStatus.cancelled') }}</template>
|
||||
<template v-if="item.orderStatus === OrderStatus.PENDING_PAYMENT">{{ t('pages-store.order.orderStatus.pendingPayment') }}</template>
|
||||
<template v-if="item.orderStatus === OrderStatus.HAS_PENDING_PAYMENT">{{ t('pages-store.order.orderStatus.hasPendingPayment') }}</template>
|
||||
<!--配送订单-->
|
||||
<template v-if="item.orderStatus === OrderStatus.MERCHANT_ACCEPTED">{{ t('pages-store.order.orderStatus.received') }}</template>
|
||||
<template v-if="+item.receiveMethod === 1 && item.orderStatus === OrderStatus.DELIVERING">{{ t('pages-store.order.orderStatus.delivering') }}</template>
|
||||
<template v-if="item.receiveMethod === 1 && item.orderStatus === OrderStatus.DELIVERING">{{ t('pages-store.order.orderStatus.delivering') }}</template>
|
||||
<template v-if="item.orderStatus === OrderStatus.COMPLETED">{{ t('pages-store.order.orderStatus.delivered') }}</template>
|
||||
|
||||
<!--商家拒绝接单-->
|
||||
<template v-if="item.orderStatus === OrderStatus.MERCHANT_REJECTED">
|
||||
<text class="text-#FF6106">{{ t('pages-store.store.orderStatus.rejected') }}</text>
|
||||
</template>
|
||||
</template>
|
||||
</text>
|
||||
</view>
|
||||
<view class="text-30rpx lh-30rpx text-#333 font-500 flex items-center my-28rpx ">
|
||||
<image src="@img/chef/126.png" class="w-32rpx h-32rpx mr-10rpx"></image>
|
||||
{{ item.merchantVo.merchantName }}
|
||||
<!--收货方式(1-派送 2-自取)-->
|
||||
<view v-if="+item.receiveMethod === 1" class="bg-#FF6106 rounded-6rpx h-32rpx text-#fff text-22rpx center px-10rpx ml-20rpx">
|
||||
|
||||
<view class="store-row">
|
||||
<image src="@img/chef/126.png" class="store-icon" mode="aspectFit"></image>
|
||||
<text class="store-name line-clamp-1">{{ item.merchantVo?.merchantName }}</text>
|
||||
<view v-if="item.receiveMethod === 1" class="recv-tag recv-tag--del">
|
||||
{{ t('pages.order.DEL') }}
|
||||
</view>
|
||||
<view v-if="+item.receiveMethod === 2" class="bg-#00A76D rounded-6rpx h-32rpx text-#fff text-22rpx center px-10rpx ml-20rpx">
|
||||
<view v-if="item.receiveMethod === 2" class="recv-tag recv-tag--pu">
|
||||
{{ t('pages.order.PU') }}
|
||||
</view>
|
||||
</view>
|
||||
<swiper class="h-568rpx mb-30rpx" :autoplay="true">
|
||||
<swiper-item v-for="img in item.merchantOrderDishVoList">
|
||||
<image
|
||||
:src="img.merchantDishVo.dishImage?.split(',')[0]"
|
||||
mode="aspectFill"
|
||||
class="w-100% h-508rpx rounded-36rpx bg-common"
|
||||
></image>
|
||||
<view class="mt-10rpx text-30rpx font-500 line-clamp-1">{{ img.merchantDishVo.dishName }}</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view class="flex-center-sb">
|
||||
<view>
|
||||
<image v-if="+item.receiveMethod === 2" src="@img/chef/127.png" class="w-56rpx h-56rpx"></image>
|
||||
<image @click.stop="callPhone(item.merchantVo.phone)" v-if="item.orderStatus >= OrderStatus.MERCHANT_ACCEPTED && +item.receiveMethod === 1" src="@img/chef/128.png" class="w-56rpx h-56rpx"></image>
|
||||
|
||||
<view class="goods-row">
|
||||
<scroll-view scroll-x class="goods-scroll" :show-scrollbar="false" :enable-flex="true">
|
||||
<view class="goods-track">
|
||||
<view
|
||||
v-for="(dish, di) in getOrderDishes(item)"
|
||||
:key="dish.id ?? di"
|
||||
class="goods-cell"
|
||||
>
|
||||
<view class="goods-img-wrap">
|
||||
<image
|
||||
:src="dishCover(dish)"
|
||||
mode="aspectFill"
|
||||
class="goods-img"
|
||||
/>
|
||||
<view
|
||||
v-if="dish.count > 1"
|
||||
class="goods-qty"
|
||||
>x{{ dish.count }}</view>
|
||||
</view>
|
||||
<text class="goods-caption">{{ dishTitle(dish) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="price-block shrink-0">
|
||||
<text class="price-num">${{ formatOrderPrice(item) }}</text>
|
||||
<text class="price-meta">{{ t('pages.order.totalItemCount', { count: getTotalDishCount(item) }) }}</text>
|
||||
</view>
|
||||
<view class="flex items-center gap-20rpx font-500">
|
||||
</view>
|
||||
|
||||
<view class="action-row" @click.stop>
|
||||
<view class="action-left">
|
||||
<image
|
||||
v-if="item.receiveMethod === 2 && item.orderStatus === OrderStatus.MERCHANT_ACCEPTED"
|
||||
src="@img/chef/127.png"
|
||||
class="action-icon"
|
||||
/>
|
||||
<image
|
||||
v-if="item.orderStatus >= OrderStatus.MERCHANT_ACCEPTED && item.receiveMethod === 1"
|
||||
src="@img/chef/128.png"
|
||||
class="action-icon"
|
||||
@click="callPhone(item.merchantVo?.phone)"
|
||||
/>
|
||||
</view>
|
||||
<view class="action-btns">
|
||||
<template v-if="item.refundStatus !== OrderCancelStatus.APPLIED && item.refundStatus !== OrderCancelStatus.APPROVED">
|
||||
<view v-if="item.orderStatus !== OrderStatus.CANCELLED && +item.orderStatus !== OrderStatus.COMPLETED && item.orderStatus !== OrderStatus.MERCHANT_REJECTED" class="w-170rpx h-64rpx center rounded-64rpx border-solid border-#666666 border-1rpx">{{ t('common.cancel') }}</view>
|
||||
<view v-if="+item.receiveMethod === 2 && +item.orderStatus === OrderStatus.MERCHANT_ACCEPTED" class="w-170rpx h-64rpx center rounded-64rpx bg-#14181B text-28rpx text-#fff">{{ t('pages-store.order.writeOff') }}</view>
|
||||
<view v-if="+item.orderStatus === OrderStatus.COMPLETED && item?.dishReviewVoList.length === 0" class="w-170rpx h-64rpx center rounded-64rpx bg-#14181B text-28rpx text-#fff">{{ t('common.evaluate') }}</view>
|
||||
<view
|
||||
v-if="item.orderStatus !== OrderStatus.CANCELLED && item.orderStatus !== OrderStatus.COMPLETED && item.orderStatus !== OrderStatus.MERCHANT_REJECTED"
|
||||
class="btn-outline"
|
||||
@click="handleCancelClick(item)"
|
||||
>{{ t('pages.order.cancelOrder') }}</view>
|
||||
<view
|
||||
v-if="item.receiveMethod === 2 && item.orderStatus === OrderStatus.MERCHANT_ACCEPTED"
|
||||
class="btn-primary"
|
||||
@click="handleClick(item)"
|
||||
>{{ t('pages-store.order.writeOff') }}</view>
|
||||
<view
|
||||
v-if="item.orderStatus === OrderStatus.COMPLETED && item?.dishReviewVoList?.length === 0"
|
||||
class="btn-primary"
|
||||
@click="handleClick(item)"
|
||||
>{{ t('common.evaluate') }}</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</z-paging>
|
||||
@@ -188,5 +283,233 @@ defineExpose({
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.list-root {
|
||||
background: #F5F5F5;
|
||||
}
|
||||
|
||||
</style>
|
||||
.page-pad {
|
||||
padding: 24rpx 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.order-card {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 28rpx 24rpx 24rpx;
|
||||
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.order-card--skeleton {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.order-time {
|
||||
font-size: 28rpx;
|
||||
line-height: 36rpx;
|
||||
color: #14181B;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.order-status {
|
||||
font-size: 28rpx;
|
||||
line-height: 36rpx;
|
||||
color: #00A76D;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.store-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 20rpx;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.store-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-right: 10rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.store-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 28rpx;
|
||||
line-height: 36rpx;
|
||||
color: #14181B;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.recv-tag {
|
||||
flex-shrink: 0;
|
||||
margin-left: 16rpx;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 28rpx;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.recv-tag--del {
|
||||
background: #FF6106;
|
||||
}
|
||||
|
||||
.recv-tag--pu {
|
||||
background: #00A76D;
|
||||
}
|
||||
|
||||
.goods-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-top: 24rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.goods-scroll {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
align-self: flex-start;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.goods-track {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16rpx;
|
||||
padding-right: 8rpx;
|
||||
}
|
||||
|
||||
.goods-cell {
|
||||
width: 128rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.goods-img-wrap {
|
||||
position: relative;
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
background: #F2F2F2;
|
||||
}
|
||||
|
||||
.goods-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.goods-qty {
|
||||
position: absolute;
|
||||
right: 6rpx;
|
||||
bottom: 6rpx;
|
||||
min-width: 36rpx;
|
||||
padding: 4rpx 10rpx;
|
||||
background: rgba(20, 24, 27, 0.85);
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
line-height: 24rpx;
|
||||
border-radius: 8rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.goods-caption {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 28rpx;
|
||||
color: #333;
|
||||
min-height: 56rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.price-block {
|
||||
text-align: right;
|
||||
padding-top: 8rpx;
|
||||
max-width: 200rpx;
|
||||
}
|
||||
|
||||
.price-num {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
line-height: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #14181B;
|
||||
}
|
||||
|
||||
.price-meta {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.action-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
}
|
||||
|
||||
.action-btns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 16rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
padding: 16rpx 32rpx;
|
||||
border-radius: 999rpx;
|
||||
border: 2rpx solid #14181B;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #14181B;
|
||||
background: #fff;
|
||||
line-height: 32rpx;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 16rpx 32rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #14181B;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
line-height: 32rpx;
|
||||
}
|
||||
|
||||
.sk-thumb {
|
||||
flex-shrink: 0;
|
||||
width: 128rpx;
|
||||
}
|
||||
|
||||
.sk-thumb-img {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.sk-pill {
|
||||
width: 176rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 9999rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -67,32 +67,30 @@ defineExpose({
|
||||
<z-paging-swiper>
|
||||
<template #top>
|
||||
<status-bar/>
|
||||
<view class="pl-30rpx pt-18rpx text-56rpx lh-56rpx text-#333 font-bold tracking-[.04em]">{{ t('pages.order.title') }}</view>
|
||||
<view class="tab pl-20rpx mt-24rpx">
|
||||
<!-- <view class="pl-30rpx pt-18rpx text-56rpx lh-56rpx text-#333 font-bold tracking-[.04em]">{{ t('pages.order.title') }}</view> -->
|
||||
<view class="tab mt-16rpx bg-white">
|
||||
<wd-tabs
|
||||
slidable="always"
|
||||
key="tab"
|
||||
color="#333"
|
||||
inactiveColor="#999"
|
||||
:line-height="3"
|
||||
:line-width="30"
|
||||
color="#14181B"
|
||||
inactiveColor="#B0B0B0"
|
||||
:line-height="6"
|
||||
:line-width="48"
|
||||
v-model="currentIndex"
|
||||
@click="handleClickTab"
|
||||
>
|
||||
<template v-for="(item, index) in tabList" :key="index">
|
||||
<wd-tab :title="item.name"></wd-tab>
|
||||
</template>
|
||||
<wd-tab v-for="(item, index) in tabList" :key="index" :title="item.name"></wd-tab>
|
||||
</wd-tabs>
|
||||
<view class="px-20rpx mt--2rpx">
|
||||
<!-- <view class="px-20rpx mt--2rpx">
|
||||
<view class="border-b-solid border-b-4rpx border-b-#999"></view>
|
||||
</view>
|
||||
</view> -->
|
||||
</view>
|
||||
</template>
|
||||
<swiper class="h-full"
|
||||
<swiper class="h-full order-swiper"
|
||||
:current="currentIndex"
|
||||
@change="handleSwiperChange">
|
||||
<swiper-item class="swiper-item" v-for="(item, index) in tabList" :key="index">
|
||||
<order-swiper-list ref="orderSwiperListRef" :currentIndex="currentIndex" :status="index"></order-swiper-list>
|
||||
<order-swiper-list ref="orderSwiperListRef" :currentIndex="currentIndex" :status="item.value"></order-swiper-list>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<template #bottom>
|
||||
@@ -106,22 +104,44 @@ defineExpose({
|
||||
<style scoped lang="scss">
|
||||
.tab {
|
||||
:deep(.wd-tabs) {
|
||||
background: #fff !important;
|
||||
.wd-tabs__line {
|
||||
bottom: 0 !important;
|
||||
z-index: 99 !important;
|
||||
border-radius: 0 !important;
|
||||
border-radius: 2rpx !important;
|
||||
background: #14181B !important;
|
||||
}
|
||||
.wd-tabs__nav-item {
|
||||
padding: 0 22rpx !important;
|
||||
padding: 0 !important;
|
||||
flex: 1 1 0 !important;
|
||||
display: flex !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
.wd-tabs__nav-item-text{
|
||||
font-size: 32rpx !important;
|
||||
.wd-tabs__nav-item-text {
|
||||
font-size: 26rpx !important;
|
||||
text-overflow: unset !important;
|
||||
font-family: 'UberMove', sans-serif !important;
|
||||
// font-weight: 500 !important;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.wd-tabs__nav-item.is-active .wd-tabs__nav-item-text {
|
||||
font-weight: 700 !important;
|
||||
color: #14181B !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.wd-tabs__nav-container) {
|
||||
display: flex !important;
|
||||
width: 100% !important;
|
||||
justify-content: space-between !important;
|
||||
}
|
||||
}
|
||||
.bg {
|
||||
background: linear-gradient(180deg, #FFFFFF 0%, #FFFFFF 51%, rgba(255, 255, 255, 0) 100%);
|
||||
background: linear-gradient(180deg, #FFFFFF 0%, #FFFFFF 40%, rgba(245, 245, 245, 0) 100%);
|
||||
}
|
||||
|
||||
.order-swiper {
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import Config from '@/config/index'
|
||||
import { formatSalesCount } from '@/utils/utils'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
type DishItem = {
|
||||
id: number | string
|
||||
merchantId?: number | string
|
||||
dishName?: string
|
||||
dishImage?: string
|
||||
discountPrice?: number | string
|
||||
originalPrice?: number | string
|
||||
memberPrice?: number | string
|
||||
salesCount?: number | string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
item: DishItem
|
||||
}>()
|
||||
|
||||
function goDetail() {
|
||||
const { item } = props
|
||||
if (item.merchantId) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-store/pages/store/dishes?id=${item.id}&storeId=${item.merchantId}`,
|
||||
})
|
||||
} else {
|
||||
uni.navigateTo({
|
||||
url: `/pages-store/pages/store/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const cover = computed(() => {
|
||||
const raw = props.item?.dishImage
|
||||
if (typeof raw === 'string' && raw) return raw.split(',')[0]
|
||||
return ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="search-dish-item" @click="goDetail">
|
||||
<view class="search-dish-item__img-wrap">
|
||||
<view v-if="item.isNew == 1" class="dish-new-ribbon">
|
||||
<text class="dish-new-ribbon__text">NEW</text>
|
||||
</view>
|
||||
<image :src="cover" mode="aspectFill" class="search-dish-item__img" />
|
||||
</view>
|
||||
<view class="search-dish-item__main">
|
||||
<text class="search-dish-item__title line-clamp-2">{{ item.dishName || '' }}</text>
|
||||
<view class="search-dish-item__price-row">
|
||||
<text class="search-dish-item__price-current">
|
||||
${{ item.discountPrice ?? item.originalPrice }}
|
||||
</text>
|
||||
<text
|
||||
v-if="
|
||||
Number(item.originalPrice) > 0 &&
|
||||
Number(item.originalPrice) > Number(item.discountPrice ?? item.originalPrice)
|
||||
"
|
||||
class="search-dish-item__price-old"
|
||||
>${{ item.originalPrice }}</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="Number(item.memberPrice) > 0"
|
||||
class="search-dish-item__member"
|
||||
>
|
||||
<text class="search-dish-item__member-diamond">◆</text>
|
||||
<text class="search-dish-item__member-text">
|
||||
{{ Config.appName }} {{ t('pages.search.member-price-line') }}: ${{ item.memberPrice }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="search-dish-item__sales-wrap">
|
||||
<text class="search-dish-item__sales-tag">
|
||||
{{ t('pages.search.weekly-sales') }}:{{ formatSalesCount(item.salesCount) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="search-dish-item__add center" @click.stop="goDetail">
|
||||
<image src="@img/chef/1285.png" class="search-dish-item__add-icon" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-dish-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
padding: 28rpx 24rpx;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #ebebeb;
|
||||
}
|
||||
|
||||
.search-dish-item__img {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
background: #f2f2f2;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search-dish-item__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 24rpx;
|
||||
margin-right: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.search-dish-item__title {
|
||||
font-size: 28rpx;
|
||||
line-height: 40rpx;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.search-dish-item__price-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.search-dish-item__price-current {
|
||||
font-size: 32rpx;
|
||||
line-height: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #e02e24;
|
||||
}
|
||||
|
||||
.search-dish-item__price-old {
|
||||
font-size: 24rpx;
|
||||
line-height: 28rpx;
|
||||
color: #999;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.search-dish-item__member {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 12rpx;
|
||||
padding: 6rpx 0;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.search-dish-item__member-diamond {
|
||||
font-size: 20rpx;
|
||||
line-height: 1;
|
||||
color: #b8860b;
|
||||
}
|
||||
|
||||
.search-dish-item__member-text {
|
||||
font-size: 24rpx;
|
||||
line-height: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #8b6914;
|
||||
}
|
||||
|
||||
.search-dish-item__sales-wrap {
|
||||
margin-top: auto;
|
||||
padding-top: 12rpx;
|
||||
}
|
||||
|
||||
.search-dish-item__sales-tag {
|
||||
display: inline-block;
|
||||
padding: 8rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #f5f5f5;
|
||||
font-size: 22rpx;
|
||||
line-height: 28rpx;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.search-dish-item__add {
|
||||
align-self: flex-end;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
background: #f2f2f2;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.search-dish-item__add-icon {
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
}
|
||||
|
||||
.search-dish-item__img-wrap {
|
||||
position: relative;
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dish-new-ribbon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 184rpx;
|
||||
height: 184rpx;
|
||||
overflow: hidden;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
|
||||
&__text {
|
||||
position: absolute;
|
||||
top: 26rpx;
|
||||
left: -66rpx;
|
||||
width: 240rpx;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
letter-spacing: 2rpx;
|
||||
line-height: 44rpx;
|
||||
background: #E23636;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,56 +0,0 @@
|
||||
<template>
|
||||
<view class="pt-52rpx pl-30rpx bg-#fff" v-if="searchStore.historyList?.length">
|
||||
<view class="text-40rpx lh-40rpx text-#333 font-bold mb-24rpx tracking-[.04em]">{{ t('pages.search.recently') }}</view>
|
||||
<view class="">
|
||||
<template v-for="(item, index) in searchStore.historyList" :key="index">
|
||||
<wd-swipe-action>
|
||||
<view @click="handleSearch(item,index)" class="w-full h-110rpx flex items-center">
|
||||
<image src="@img/chef/130.png" class="w-28rpx h-28rpx mr-44rpx"></image>
|
||||
<view class="flex-1 border-b-solid border-b-1rpx border-b-#DFDFDF h-full flex items-center text-30rpx text-primary font-500 tracking-[.06em] line-clamp-1">{{ item.text }}</view>
|
||||
</view>
|
||||
<template #right>
|
||||
<view @click="handleRemove(index)" class="w-110rpx h-110rpx bg-#FF4848 center">
|
||||
<image src="@img/chef/129.png" class="w-44rpx h-50rpx"></image>
|
||||
</view>
|
||||
</template>
|
||||
</wd-swipe-action>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
<wd-message-box/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {useSearchStore} from "@/store";
|
||||
import {useMessage,} from "wot-design-uni";
|
||||
|
||||
const {t} = useI18n()
|
||||
|
||||
const message = useMessage()
|
||||
const searchStore = useSearchStore()
|
||||
const isRemove = ref(false)
|
||||
|
||||
|
||||
function handleSearch(item: { text: string }, index: number) {
|
||||
if (isRemove.value) {
|
||||
handleRemove(index)
|
||||
return
|
||||
}
|
||||
console.log(item)
|
||||
nextTick(() => {
|
||||
searchStore.setHistoryList(item.text)
|
||||
uni.navigateTo({
|
||||
url: `/pages/search/result?keyword=${item.text}`,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function handleRemove(index: number) {
|
||||
searchStore.historyList.splice(index, 1)
|
||||
if (searchStore.historyList.length === 0) {
|
||||
isRemove.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -1,49 +1,38 @@
|
||||
<template>
|
||||
<view class="search-skeleton pt-88rpx">
|
||||
<!-- 头部区域 -->
|
||||
<view class="header-section">
|
||||
<view class="back-button-skeleton skeleton-item"></view>
|
||||
<view class="search-bar-skeleton skeleton-item"></view>
|
||||
<view class="search-skeleton">
|
||||
<view class="search-skeleton__header px-24rpx pt-8rpx pb-12rpx flex items-center justify-between gap-16rpx">
|
||||
<view class="search-skeleton__circle skeleton-item"></view>
|
||||
<view class="search-skeleton__pill flex-1 skeleton-item"></view>
|
||||
<view class="search-skeleton__circle skeleton-item"></view>
|
||||
</view>
|
||||
|
||||
<!-- 最近搜索区域 -->
|
||||
<view class="recent-section">
|
||||
<view class="section-title-skeleton skeleton-item"></view>
|
||||
<view class="recent-list">
|
||||
<view
|
||||
v-for="i in 3"
|
||||
:key="i"
|
||||
class="recent-item-skeleton skeleton-item"
|
||||
></view>
|
||||
<view class="search-skeleton__section">
|
||||
<view class="search-skeleton__title-row">
|
||||
<view class="search-skeleton__title skeleton-item"></view>
|
||||
<view class="search-skeleton__clear skeleton-item"></view>
|
||||
</view>
|
||||
<view class="search-skeleton__tags">
|
||||
<view v-for="i in 5" :key="'r-' + i" class="search-skeleton__tag skeleton-item"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 热门分类区域 -->
|
||||
<view class="popular-section">
|
||||
<view class="section-title-skeleton skeleton-item"></view>
|
||||
<template v-for="item in 6">
|
||||
<view class="flex items-center h-144rpx">
|
||||
<view class="w-64rpx h-64rpx mr-28rpx rounded-50% skeleton-item"></view>
|
||||
<view class="w-420rpx h-60rpx rounded-10rpx skeleton-item"></view>
|
||||
</view>
|
||||
</template>
|
||||
<view class="search-skeleton__section">
|
||||
<view class="search-skeleton__title skeleton-item search-skeleton__title--wide"></view>
|
||||
<view class="search-skeleton__tags">
|
||||
<view v-for="i in 8" :key="'h-' + i" class="search-skeleton__tag skeleton-item"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 搜索页面骨架屏组件
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 通用骨架屏样式
|
||||
.skeleton-item {
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background: linear-gradient(90deg, #ececec 25%, #e0e0e0 50%, #ececec 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
// 闪烁动画
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
@@ -54,75 +43,60 @@
|
||||
}
|
||||
|
||||
.search-skeleton {
|
||||
background-color: #fff;
|
||||
min-height: 100vh;
|
||||
background-color: #f7f8fa;
|
||||
padding-top: env(safe-area-inset-top, 0px);
|
||||
}
|
||||
|
||||
// 状态栏
|
||||
.status-bar {
|
||||
height: 88rpx;
|
||||
width: 100%;
|
||||
.search-skeleton__circle {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// 头部区域
|
||||
.header-section {
|
||||
padding: 0 30rpx 40rpx;
|
||||
.search-skeleton__pill {
|
||||
height: 80rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.search-skeleton__section {
|
||||
padding: 28rpx 24rpx 8rpx;
|
||||
}
|
||||
|
||||
.search-skeleton__title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
|
||||
.back-button-skeleton {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 22rpx;
|
||||
}
|
||||
|
||||
.search-bar-skeleton {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
}
|
||||
justify-content: space-between;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
// 最近搜索区域
|
||||
.recent-section {
|
||||
padding: 0 30rpx 40rpx;
|
||||
|
||||
.section-title-skeleton {
|
||||
width: 200rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.recent-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
|
||||
.recent-item-skeleton {
|
||||
height: 60rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
.search-skeleton__title {
|
||||
width: 180rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
.search-skeleton__title--wide {
|
||||
width: 220rpx;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
// 热门分类区域
|
||||
.popular-section {
|
||||
padding: 40rpx 30rpx 0;
|
||||
|
||||
.section-title-skeleton {
|
||||
width: 300rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
.search-skeleton__clear {
|
||||
width: 56rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 750rpx) {
|
||||
.category-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.search-skeleton__tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx 14rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
.search-skeleton__tag {
|
||||
width: 132rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
interface SearchTagItem {
|
||||
key: string | number
|
||||
text: string
|
||||
/** 左侧「热门」火焰标 */
|
||||
hot?: boolean
|
||||
/** 显示右侧删除(搜索记录) */
|
||||
removable?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title: string
|
||||
tags: SearchTagItem[]
|
||||
/** 标题右侧「清空」 */
|
||||
showClear?: boolean
|
||||
}>(),
|
||||
{
|
||||
showClear: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [item: SearchTagItem]
|
||||
remove: [index: number]
|
||||
clear: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view v-if="tags.length > 0" class="search-tag-cloud">
|
||||
<view class="search-tag-cloud__head">
|
||||
<text class="search-tag-cloud__title">{{ title }}</text>
|
||||
<text v-if="showClear" class="search-tag-cloud__clear" @click="emit('clear')">{{ t('pages.search.clear-history') }}</text>
|
||||
</view>
|
||||
<view class="search-tag-cloud__wrap">
|
||||
<view
|
||||
v-for="(item, index) in tags"
|
||||
:key="item.key"
|
||||
class="search-tag-cloud__pill"
|
||||
@click="emit('select', item)"
|
||||
>
|
||||
<view v-if="item.hot" class="i-carbon:fire search-tag-cloud__flame shrink-0"></view>
|
||||
<text class="search-tag-cloud__text">{{ item.text }}</text>
|
||||
<view
|
||||
v-if="item.removable"
|
||||
class="search-tag-cloud__remove center shrink-0"
|
||||
@click.stop="emit('remove', index)"
|
||||
>
|
||||
<text class="search-tag-cloud__remove-x">×</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-tag-cloud {
|
||||
padding: 36rpx 24rpx 8rpx;
|
||||
}
|
||||
|
||||
.search-tag-cloud__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.search-tag-cloud__title {
|
||||
font-size: 32rpx;
|
||||
line-height: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.search-tag-cloud__clear {
|
||||
font-size: 26rpx;
|
||||
line-height: 32rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.search-tag-cloud__wrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx 14rpx;
|
||||
}
|
||||
|
||||
.search-tag-cloud__pill {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
padding: 14rpx 22rpx;
|
||||
background: #fff;
|
||||
border-radius: 999rpx;
|
||||
box-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.search-tag-cloud__flame {
|
||||
margin-right: 8rpx;
|
||||
font-size: 26rpx;
|
||||
color: #e02020;
|
||||
}
|
||||
|
||||
.search-tag-cloud__text {
|
||||
font-size: 28rpx;
|
||||
line-height: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 320rpx;
|
||||
}
|
||||
|
||||
.search-tag-cloud__remove {
|
||||
margin-left: 10rpx;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.search-tag-cloud__remove-x {
|
||||
font-size: 32rpx;
|
||||
line-height: 32rpx;
|
||||
color: #666;
|
||||
font-weight: 300;
|
||||
margin-top: -4rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import FiltrateTool from "@/components/filtrate-tool/index.vue";
|
||||
import FoodBox from "@/pages/home/components/tabbar-home/components/food-box/index.vue";
|
||||
import SearchDishResultItem from "@/pages/search/components/search-dish-result-item.vue";
|
||||
import TabsType from "@/pages/home/components/tabbar-home/components/tabs-type.vue";
|
||||
import AnimatedButton from "../animated-button/animated-button.vue";
|
||||
import Collection from "@/components/collection/index.vue";
|
||||
@@ -189,14 +189,11 @@ defineExpose({
|
||||
>
|
||||
<!-- 筛选工具 -->
|
||||
<!-- <filtrate-tool class="my-36rpx" @togglePickup="togglePickup" @toggleDiscount="toggleDiscount" @toggleScore="emit('toggleScore')" @togglePrice="emit('togglePrice')" /> -->
|
||||
<view
|
||||
class="pl-30rpx pb-36rpx text-36rpx lh-36rpx text-#333 font-500 tracking-[.04em]"
|
||||
>{{ foodTotal }} {{ t('pages.search.result.result') }}</view
|
||||
>
|
||||
<view class="px-30rpx">
|
||||
<template v-for="item in dataList">
|
||||
<food-box :item="item" />
|
||||
</template>
|
||||
<view class="search-food-toolbar">
|
||||
<text class="search-food-toolbar__count">{{ foodTotal }} {{ t('pages.search.result.result') }}</text>
|
||||
</view>
|
||||
<view class="search-food-list">
|
||||
<search-dish-result-item v-for="item in dataList" :key="String(item.id)" :item="item" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -282,6 +279,22 @@ defineExpose({
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-food-toolbar {
|
||||
padding: 20rpx 24rpx 8rpx;
|
||||
background: #fff;
|
||||
}
|
||||
.search-food-toolbar__count {
|
||||
font-size: 26rpx;
|
||||
line-height: 34rpx;
|
||||
color: #999;
|
||||
font-weight: 400;
|
||||
}
|
||||
.search-food-list {
|
||||
background: #fff;
|
||||
}
|
||||
.search-food-list :deep(.search-dish-item:last-child) {
|
||||
border-bottom: none;
|
||||
}
|
||||
// 确保过渡动画平滑
|
||||
.transition-transform {
|
||||
transition: transform 0.6s ease-in-out;
|
||||
|
||||
+115
-41
@@ -1,79 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import SearchHistory from './components/search-history/index.vue'
|
||||
import SearchSkeleton from './components/search-skeleton.vue'
|
||||
import {useSearchStore} from '@/store'
|
||||
import {appSearchListPost,appRecipeCategoryListGet} from '@/service'
|
||||
import SearchTagCloud from './components/search-tag-cloud.vue'
|
||||
import { useSearchStore } from '@/store'
|
||||
import { appRecipeCategoryListGet } from '@/service'
|
||||
import { decodeRouteQueryValue } from '@/utils/utils'
|
||||
|
||||
const {t} = useI18n()
|
||||
const { t } = useI18n()
|
||||
const searchStore = useSearchStore()
|
||||
|
||||
const keyword = ref('')
|
||||
function handleSearch() {
|
||||
nextTick(() => {
|
||||
if (!keyword.value) {
|
||||
return uni.showToast({title: t('common.prompt.please-enter-keyword-search'), icon: 'none'})
|
||||
return uni.showToast({
|
||||
title: t('common.prompt.please-enter-keyword-search'),
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
searchStore.setHistoryList(keyword.value)
|
||||
const kw = decodeRouteQueryValue(keyword.value)
|
||||
if (!kw) {
|
||||
return uni.showToast({
|
||||
title: t('common.prompt.please-enter-keyword-search'),
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
searchStore.setHistoryList(kw)
|
||||
uni.navigateTo({
|
||||
url: `/pages/search/result?keyword=${keyword.value}`,
|
||||
url: `/pages/search/result?keyword=${encodeURIComponent(kw)}`,
|
||||
})
|
||||
keyword.value = ''
|
||||
})
|
||||
}
|
||||
|
||||
function handleHotSearch(item: any) {
|
||||
nextTick(() => {
|
||||
searchStore.setHistoryList(item.categoryName)
|
||||
uni.navigateTo({
|
||||
url: `/pages/search/result?keyword=${item.categoryName}`,
|
||||
})
|
||||
function goResultWithKeyword(k: string) {
|
||||
const kw = decodeRouteQueryValue(k)
|
||||
if (!kw) return
|
||||
searchStore.setHistoryList(kw)
|
||||
uni.navigateTo({
|
||||
url: `/pages/search/result?keyword=${encodeURIComponent(kw)}`,
|
||||
})
|
||||
}
|
||||
|
||||
function handleHotSearch(item: Record<string, unknown>) {
|
||||
const name = String(item?.categoryName ?? '')
|
||||
goResultWithKeyword(name)
|
||||
}
|
||||
|
||||
function onHistorySelect(tag: { text: string }) {
|
||||
goResultWithKeyword(tag.text)
|
||||
}
|
||||
|
||||
function onHistoryRemove(index: number) {
|
||||
searchStore.historyList.splice(index, 1)
|
||||
}
|
||||
|
||||
function onHistoryClear() {
|
||||
searchStore.clearHistory()
|
||||
}
|
||||
|
||||
const loading = ref(true)
|
||||
onMounted(() => {
|
||||
loading.value = true
|
||||
// 查询热门搜索词列表
|
||||
getHotSearchList()
|
||||
})
|
||||
|
||||
const hotSearchList = ref([])
|
||||
const hotSearchList = ref<Record<string, unknown>[]>([])
|
||||
function getHotSearchList() {
|
||||
appRecipeCategoryListGet({}).then(res=> {
|
||||
console.log('热门搜索词列表', res)
|
||||
hotSearchList.value = res.data
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
appRecipeCategoryListGet({})
|
||||
.then((res) => {
|
||||
hotSearchList.value = (res.data as Record<string, unknown>[]) || []
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const historyTags = computed(() =>
|
||||
searchStore.historyList.map((h, i) => ({
|
||||
key: `${h.time}-${i}`,
|
||||
text: decodeRouteQueryValue(h.text),
|
||||
removable: true,
|
||||
})),
|
||||
)
|
||||
|
||||
const hotTags = computed(() =>
|
||||
hotSearchList.value.map((item, i) => ({
|
||||
key: String(item.id ?? `hot-${i}`),
|
||||
text: String(item.categoryName ?? ''),
|
||||
hot: i < 3,
|
||||
})),
|
||||
)
|
||||
|
||||
function onHotSelect(tag: { key: string | number }) {
|
||||
const idx = hotTags.value.findIndex((x) => x.key === tag.key)
|
||||
const raw = idx >= 0 ? hotSearchList.value[idx] : null
|
||||
if (raw) handleHotSearch(raw)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="">
|
||||
<view class="search-page">
|
||||
<view
|
||||
class="animate-in fade-in animate-ease-out animate-duration-300"
|
||||
v-show="loading"
|
||||
class="animate-in fade-in animate-ease-out animate-duration-300"
|
||||
v-show="loading"
|
||||
>
|
||||
<search-skeleton />
|
||||
</view>
|
||||
<view
|
||||
class="animate-in fade-in animate-ease-in animate-duration-300"
|
||||
v-show="!loading"
|
||||
class="animate-in fade-in animate-ease-in animate-duration-300 search-page__content"
|
||||
v-show="!loading"
|
||||
>
|
||||
<header-search focus class="" v-model="keyword" :placeholder="t('components.search.placeholder')" @search="handleSearch"/>
|
||||
<search-history/>
|
||||
<view class="pl-30rpx mt-52rpx">
|
||||
<view class="text-40rpx lh-40rpx text-#333 font-bold mb-24rpx tracking-[.04em]">
|
||||
{{ t('pages.search.hot-title') }}
|
||||
</view>
|
||||
<template v-for="item in hotSearchList">
|
||||
<view @click="handleHotSearch(item)" class="w-full h-144rpx flex items-center">
|
||||
<image :src="item?.categoryImage" class="w-64rpx h-64rpx mr-28rpx rounded-50%" mode="aspectFill"></image>
|
||||
<view class="flex-1 border-b-solid border-b-1rpx border-b-#DFDFDF h-full flex items-center text-30rpx text-primary font-500 tracking-[.06em]">
|
||||
{{ item?.categoryName }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<header-search
|
||||
v-model="keyword"
|
||||
focus
|
||||
trailing-cart
|
||||
class="search-page__header"
|
||||
:placeholder="t('components.search.placeholder')"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<view class="search-page__body">
|
||||
<search-tag-cloud
|
||||
v-if="historyTags.length > 0"
|
||||
:title="t('pages.search.recently')"
|
||||
:tags="historyTags"
|
||||
show-clear
|
||||
@select="onHistorySelect"
|
||||
@remove="onHistoryRemove"
|
||||
@clear="onHistoryClear"
|
||||
/>
|
||||
<search-tag-cloud
|
||||
v-if="hotTags.length > 0"
|
||||
:title="t('pages.search.hot-title')"
|
||||
:tags="hotTags"
|
||||
@select="onHotSelect"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -81,6 +141,20 @@ function getHotSearchList() {
|
||||
|
||||
<style lang="scss">
|
||||
page {
|
||||
background-color: #fff;
|
||||
background-color: #f7f8fa;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
.search-page__content {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
.search-page__body {
|
||||
padding-bottom: 48rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useSearchStore } from "@/store";
|
||||
import TabSwitcher from "./components/tab-switcher.vue";
|
||||
import { decodeRouteQueryValue } from "@/utils/utils";
|
||||
import SwiperList from "./components/swiper-list/swiper-list.vue";
|
||||
import Score from "@/components/filtrate-tool/components/score.vue";
|
||||
import PriceChoose from "@/components/filtrate-tool/components/price-choose.vue";
|
||||
import SortPopup from "@/pages/search/components/sort-popup.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
const _props = defineProps<{
|
||||
/** 少数构建下可能由路由注入;实际以 onLoad 的 query 为准 */
|
||||
keyword?: string;
|
||||
}>();
|
||||
|
||||
@@ -14,7 +15,15 @@ const { t } = useI18n();
|
||||
|
||||
const searchStore = useSearchStore();
|
||||
|
||||
const keyword = ref(props.keyword || "");
|
||||
const keyword = ref(decodeRouteQueryValue(_props.keyword) || '');
|
||||
|
||||
/** uni-app:?keyword= 只在 onLoad 的 options 里;部分端会对已编码参数再编一层,需循环 decode */
|
||||
onLoad((options?: Record<string, string | undefined>) => {
|
||||
const q = options?.keyword
|
||||
if (q == null || String(q).trim() === '') return
|
||||
const plain = decodeRouteQueryValue(q)
|
||||
if (plain) keyword.value = plain
|
||||
})
|
||||
|
||||
function handleSearch() {
|
||||
nextTick(() => {
|
||||
@@ -99,12 +108,15 @@ onMounted(()=> {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="">
|
||||
<view class="search-result-page">
|
||||
<z-paging-swiper>
|
||||
<template #top>
|
||||
<header-search
|
||||
class="pb-42rpx"
|
||||
v-model="keyword"
|
||||
trailing-cart
|
||||
trailing-surface="white"
|
||||
class="search-result-page__header"
|
||||
:placeholder="t('components.search.placeholder')"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<!-- <tab-switcher
|
||||
@@ -147,7 +159,22 @@ onMounted(()=> {
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-result-page {
|
||||
min-height: 100vh;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.search-result-page__header {
|
||||
padding-bottom: 8rpx;
|
||||
}
|
||||
|
||||
:deep(.wd-tabs__nav-item) {
|
||||
font-size: 40rpx;
|
||||
color: #666666;
|
||||
|
||||
Reference in New Issue
Block a user