first commit
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import {UserAddressType} from "@/constant/enums";
|
||||
import {useAddressStore} from "@/pages/address/store/address";
|
||||
const addressStore = useAddressStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const typeList = ref([
|
||||
{
|
||||
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',
|
||||
},
|
||||
])
|
||||
|
||||
function chooseType(item: any) {
|
||||
addressStore.addressInfo.type = item.value
|
||||
switch (item.value) {
|
||||
case UserAddressType.HOUSE:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/house',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.APARTMENT:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/apartment',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.OFFICE:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/office',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.HOTEL:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/hotel',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.OTHER:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/other',
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<navbar :title="t('pages.address.choose-type.navTitle')">
|
||||
<template #right>
|
||||
<text @click="chooseType({
|
||||
value: UserAddressType.OTHER
|
||||
})" class="text-#CE7138 text-36rpx">{{ t('common.skip') }}</text>
|
||||
</template>
|
||||
</navbar>
|
||||
<view class="px-30rpx pt-64rpx">
|
||||
<view class="text-40rpx lh-40rpx text-#333 font-500">{{ t('pages.address.choose-type.title') }}</view>
|
||||
<view class="mt-28rpx text-28rpx lh-32rpx text-#666 mb-52rpx">
|
||||
{{ t('pages.address.choose-type.description') }}
|
||||
</view>
|
||||
|
||||
<template v-for="(item, index) in typeList">
|
||||
<view @click="chooseType(item)" :class="[index === 0 ? '' : 'mt-36rpx']" class="w-full border-#DFDFDF border-solid border-1px rounded-16rpx px-28rpx py-34rpx flex-center-sb">
|
||||
<view class="flex items-center">
|
||||
<image :src="item.icon" class="w-48rpx h-48rpx shrink-0 mr-28rpx"></image>
|
||||
<view>
|
||||
<view class="text-36rpx lh-36rpx text-#333 font-500">{{ item.label }}</view>
|
||||
<view class="text-28rpx lh-28rpx text-#6D6D6D mt-18rpx">{{ item.desc }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0"></image>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
const {t} = useI18n();
|
||||
const emit = defineEmits(['confirm'])
|
||||
|
||||
const show = ref(false)
|
||||
const stateList = ref<Record<string, any>>([])
|
||||
const stateListValue = ref('')
|
||||
|
||||
const handleClickStatePicker = () => {
|
||||
show.value = false
|
||||
emit('confirm', {
|
||||
id: stateListValue.value,
|
||||
name: stateList.value.find(item => item.id === stateListValue.value)?.name || ''
|
||||
})
|
||||
}
|
||||
|
||||
function init(data: any, id: any) {
|
||||
stateList.value = data
|
||||
stateListValue.value = id
|
||||
show.value = true
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
init
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wd-popup v-model="show" :safe-area-inset-bottom="true" custom-style="border-radius:20rpx 20rpx 0 0;"
|
||||
position="bottom">
|
||||
<view class="px-30rpx">
|
||||
<view class="flex-center-sb h-102rpx">
|
||||
<view class="text-30rpx text-#999" @click="show = false">{{ t('common.cancel') }}</view>
|
||||
<view class="text-34rpx text-#333">{{ t('common.select') }}</view>
|
||||
<view class="text-30rpx text-#FF6106" @click="handleClickStatePicker">{{ t('common.confirm') }}</view>
|
||||
</view>
|
||||
|
||||
<wd-picker-view v-model="stateListValue" :columns="stateList" label-key="name" value-key="id"/>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 骨架屏组件
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,224 @@
|
||||
<script setup lang="ts">
|
||||
import Search from "@/pages/home/components/tabbar-home/components/search.vue";
|
||||
import {useUserStore} from "@/store";
|
||||
import useEventEmit from "@/hooks/useEventEmit";
|
||||
import {EventEnum, UserAddressType} from "@/constant/enums";
|
||||
import {appAppointmentTimeUpdateAppointmentTimePost, appUserAddressListPost, appUserAddressRemovePost} from "@/service";
|
||||
const { t } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
import {useAddressStore} from "./store/address";
|
||||
const addressStore = useAddressStore()
|
||||
import {useMessage} from "wot-design-uni";
|
||||
const message = useMessage();
|
||||
|
||||
function handleClickSearch() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-user/pages/search-address/index',
|
||||
});
|
||||
}
|
||||
|
||||
useEventEmit(EventEnum.CHOOSE_ADDRESS, (data) => {
|
||||
console.log('搜索的地址信息', data)
|
||||
if(data) {
|
||||
// 从addressComponents中提取州名/省名
|
||||
// let stateName = '';
|
||||
// if (data.addressComponents && Array.isArray(data.addressComponents)) {
|
||||
// // 先判断是否为中国地址
|
||||
// const countryComponent = data.addressComponents.find(component =>
|
||||
// component.types && component.types.includes('country')
|
||||
// );
|
||||
// const isChina = countryComponent && (countryComponent.shortText === 'CN' || countryComponent.longText === '中国');
|
||||
//
|
||||
// if (isChina) {
|
||||
// // 中国地址:优先取市级(locality),如果没有则取省级(administrative_area_level_1)
|
||||
// const cityComponent = data.addressComponents.find(component =>
|
||||
// component.types && component.types.includes('locality')
|
||||
// );
|
||||
// const provinceComponent = data.addressComponents.find(component =>
|
||||
// component.types && component.types.includes('administrative_area_level_1')
|
||||
// );
|
||||
//
|
||||
// if (cityComponent) {
|
||||
// stateName = cityComponent.longText || cityComponent.shortText || '';
|
||||
// } else if (provinceComponent) {
|
||||
// stateName = provinceComponent.longText || provinceComponent.shortText || '';
|
||||
// }
|
||||
// } else {
|
||||
// // 美国等其他国家:取州级(administrative_area_level_1)
|
||||
// const stateComponent = data.addressComponents.find(component =>
|
||||
// component.types && component.types.includes('administrative_area_level_1')
|
||||
// );
|
||||
// if (stateComponent) {
|
||||
// stateName = stateComponent.longText || stateComponent.shortText || '';
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
addressStore.setAddressLocation({
|
||||
displayName: data.displayName,
|
||||
formattedAddress: data.formattedAddress,
|
||||
longitude: data.location.lng,
|
||||
latitude: data.location.lat
|
||||
})
|
||||
setTimeout(()=> {
|
||||
uni.navigateTo({
|
||||
url: '/pages/address/choose-type'
|
||||
})
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
|
||||
function reservationTime() {
|
||||
uni.navigateTo({ url: '/pages/address/reservation-time' })
|
||||
}
|
||||
|
||||
// 获取用户地址列表
|
||||
const addressesList = ref([])
|
||||
function getAddressList() {
|
||||
appUserAddressListPost({
|
||||
params: {
|
||||
pageNum: 1,
|
||||
pageSize: 100,
|
||||
}
|
||||
}).then(res => {
|
||||
console.log('appAddressListGet', res)
|
||||
addressesList.value = res.rows
|
||||
})
|
||||
}
|
||||
|
||||
onShow(()=> {
|
||||
getAddressList()
|
||||
})
|
||||
|
||||
function chooseType(item: any) {
|
||||
addressStore.addressInfo = item
|
||||
switch (item.type) {
|
||||
case UserAddressType.HOUSE:
|
||||
uni.navigateTo({
|
||||
url: '/pages/address/save-address/house',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.APARTMENT:
|
||||
uni.navigateTo({
|
||||
url: '/pages/address/save-address/apartment',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.OFFICE:
|
||||
uni.navigateTo({
|
||||
url: '/pages/address/save-address/office',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.HOTEL:
|
||||
uni.navigateTo({
|
||||
url: '/pages/address/save-address/hotel',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.OTHER:
|
||||
uni.navigateTo({
|
||||
url: '/pages/address/save-address/other',
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
function deleteAddress(item: any) {
|
||||
message
|
||||
.confirm({
|
||||
title: t("common.prompt.system-prompt"),
|
||||
msg: `${t("common.prompt.system-prompt-delete")}`,
|
||||
confirmButtonText: t("common.yes"),
|
||||
cancelButtonText: t("common.no"),
|
||||
cancelButtonProps: {
|
||||
customClass:
|
||||
"!h-88rpx !w-258rpx !min-w-auto !text-30rpx !lh-42rpx !font-bold !border-#666666 !rounded-20rpx",
|
||||
},
|
||||
confirmButtonProps: {
|
||||
customClass:
|
||||
"!h-88rpx !w-258rpx !min-w-auto !text-30rpx !lh-42rpx !font-bold !bg-primary !rounded-20rpx",
|
||||
},
|
||||
})
|
||||
.then(async () => {
|
||||
appUserAddressRemovePost({
|
||||
body: [item.id]
|
||||
}).then((res) => {
|
||||
console.log('删除地址', res)
|
||||
uni.showToast({
|
||||
title: t('toast.deleteSuccess'),
|
||||
icon: 'none'
|
||||
})
|
||||
getAddressList()
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
});
|
||||
}
|
||||
|
||||
useEventEmit(EventEnum.CHOOSE_APPOINTMENT_TIME, (data) => {
|
||||
console.log('CHOOSE_APPOINTMENT_TIME', data)
|
||||
if(data) {
|
||||
appAppointmentTimeUpdateAppointmentTimePost({
|
||||
body: {
|
||||
endTime: data.endTime,
|
||||
startTime: data.startTime,
|
||||
}
|
||||
}).then(res=> {
|
||||
userStore.getAppointmentTime()
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<navbar :title="t('pages.address.title')" />
|
||||
<view class="mt-32rpx px-30rpx">
|
||||
<search :is-auto-jump="false" @clickSearch="handleClickSearch" />
|
||||
</view>
|
||||
<view class="mt-64rpx text-40rpx lh-40rpx text-#333 font-bold pl-30rpx pb-24rpx">
|
||||
{{ t('pages.address.savedAddresses') }}
|
||||
</view>
|
||||
<template v-for="item in addressesList">
|
||||
<wd-swipe-action>
|
||||
<!--:class="item === 1 ? 'bg-#F3F3F3' : ''" -->
|
||||
<view @click="chooseType(item)" class="w-full h-156rpx flex-center-sb px-30rpx">
|
||||
<view class="flex items-center">
|
||||
<!-- <image v-if="item === 1" src="@img/chef/143.png" class="w-44rpx h-44rpx shrink-0 mr-28rpx"></image>-->
|
||||
<image src="@img/chef/145.png" class="w-44rpx h-44rpx shrink-0 mr-28rpx"></image>
|
||||
<view class="flex-1 h-156rpx pt-40rpx">
|
||||
<view class="text-32rpx lh-32rpx text-#333 font-500 mb-16rpx line-clamp-1">{{ item.formattedAddress }}</view>
|
||||
<view class="text-28rpx lh-28rpx text-#6D6D6D">{{ item.displayName || '' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<image src="@img/chef/144.png" class="w-44rpx h-44rpx shrink-0 pl-30rpx"></image>
|
||||
</view>
|
||||
<template #right>
|
||||
<view class="action flex items-center text-30rpx lh-30rpx text-#fff">
|
||||
<view class="w-152rpx h-156rpx bg-#FF2828 center" @click="deleteAddress(item)">{{ t('common.delete') }}</view>
|
||||
</view>
|
||||
</template>
|
||||
</wd-swipe-action>
|
||||
</template>
|
||||
<template v-if="addressesList.length === 0">
|
||||
<view class="py-100rpx center">
|
||||
<image class="w-250rpx h-250rpx" src="@img/chef/100.png"></image>
|
||||
</view>
|
||||
</template>
|
||||
<view class="mt-44rpx text-40rpx lh-40rpx text-#333 font-bold pl-30rpx">
|
||||
{{ t('pages.address.appointmentTime') }}
|
||||
</view>
|
||||
<view @click="reservationTime" class="flex-center-sb px-30rpx mt-70rpx">
|
||||
<view class="flex items-center">
|
||||
<image src="@img/chef/146.png" class="w-44rpx h-44rpx shrink-0 mr-28rpx"></image>
|
||||
<text class="text-32rpx lh-32rpx text-#333 font-500">{{ t('pages.address.immediateDelivery') }}</text>
|
||||
</view>
|
||||
<view class="h-56rpx px-22rpx bg-#F2F2F2 rounded-56rpx center text-28rpx text-#333">
|
||||
{{ userStore.appointmentTimeShow || t('pages.address.reservation') }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,691 @@
|
||||
<script setup lang="ts">
|
||||
import {EventEnum} from "@/constant/enums";
|
||||
|
||||
const { t } = useI18n();
|
||||
import dayjs from "dayjs";
|
||||
|
||||
// 店铺的营业时间(示例:周一到周五营业,周六周日不营业)
|
||||
// 测试用的营业时间字符串
|
||||
// MONDAY/TUESDAY/WEDNESDAY 09:00-18:00;THURSDAY/FRIDAY 08:00-09:00; // 周一到周五9点到18点,周四到周五8点到9点
|
||||
// MONDAY/TUESDAY/WEDNESDAY 09:00-18:00;THURSDAY/FRIDAY 08:00-12:00;SATURDAY/SUNDAY 10:00-20:00 // 周六周日10点到20点
|
||||
// MONDAY/TUESDAY/WEDNESDAY 09:00-16:00
|
||||
// MONDAY 09:00-18:00;TUESDAY 09:00-10:00;WEDNESDAY 09:00-18:00;THURSDAY 09:00-18:00;FRIDAY 08:00-09:00
|
||||
const storeBusinessHours = ref('');
|
||||
// 是否仅选择日期(当进入页面传递了 storeBusinessHours 时开启)
|
||||
const onlySelectDay = ref(false);
|
||||
|
||||
// 解析商家营业时间的接口
|
||||
interface BusinessHours {
|
||||
days: string[]; // 营业的星期几
|
||||
startTime: string; // 开始时间 HH:mm
|
||||
endTime: string; // 结束时间 HH:mm
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析商家营业时间字符串
|
||||
* @param businessHoursStr 营业时间字符串,支持多种格式:
|
||||
* - MONDAY/TUESDAY/WEDNESDAY 09:00-18:00;THURSDAY/FRIDAY/SATURDAY/SUNDAY 10:00-20:00
|
||||
* - MONDAY/TUESDAY/WEDNESDAY 09:00-18:00
|
||||
* - MONDAY 09:00-18:00;TUESDAY 09:00-10:00;WEDNESDAY 09:00-18:00
|
||||
* @returns 解析后的营业时间数组
|
||||
*/
|
||||
const parseBusinessHours = (businessHoursStr: string): BusinessHours[] => {
|
||||
if (!businessHoursStr) return [];
|
||||
|
||||
const businessHours: BusinessHours[] = [];
|
||||
// 使用分号分割不同的营业时间段
|
||||
const segments = businessHoursStr.split(";");
|
||||
|
||||
segments.forEach((segment) => {
|
||||
const trimmedSegment = segment.trim();
|
||||
if (!trimmedSegment) return;
|
||||
|
||||
// 使用空格分割星期几和时间
|
||||
const parts = trimmedSegment.split(" ");
|
||||
if (parts.length !== 2) return;
|
||||
|
||||
const dayStr = parts[0].trim().toUpperCase();
|
||||
const timeStr = parts[1];
|
||||
|
||||
// 解析时间范围
|
||||
const timeRange = timeStr.split("-");
|
||||
if (timeRange.length !== 2) return;
|
||||
|
||||
const startTime = timeRange[0].trim();
|
||||
const endTime = timeRange[1].trim();
|
||||
|
||||
// 按斜杠分割星期几,支持 MONDAY/TUESDAY/WEDNESDAY 格式
|
||||
const days = dayStr
|
||||
.split("/")
|
||||
.map((day) => day.trim())
|
||||
.filter((day) => day);
|
||||
|
||||
businessHours.push({
|
||||
days, // 支持多个星期几共享同一营业时间
|
||||
startTime,
|
||||
endTime,
|
||||
});
|
||||
});
|
||||
|
||||
return businessHours;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取指定日期的营业时间
|
||||
* @param date 日期
|
||||
* @returns 营业时间对象,如果不营业返回null
|
||||
*/
|
||||
const getBusinessHoursForDate = (date: Date): BusinessHours | null => {
|
||||
if (!storeBusinessHours.value) return null;
|
||||
|
||||
const businessHours = parseBusinessHours(storeBusinessHours.value);
|
||||
|
||||
// 获取星期几的英文名称(确保是大写)(不要国际化导致判断失效)
|
||||
const dayNames = [
|
||||
"SUNDAY",
|
||||
"MONDAY",
|
||||
"TUESDAY",
|
||||
"WEDNESDAY",
|
||||
"THURSDAY",
|
||||
"FRIDAY",
|
||||
"SATURDAY",
|
||||
];
|
||||
const dayName = dayNames[date.getDay()];
|
||||
|
||||
// 查找包含当前星期几的营业时间
|
||||
return businessHours.find((hours) => hours.days.includes(dayName)) || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查日期是否营业
|
||||
* @param date 日期
|
||||
* @returns 是否营业
|
||||
*/
|
||||
const isDateOpen = (date: Date): boolean => {
|
||||
if (!storeBusinessHours.value) return true; // 如果没有营业时间限制,默认营业
|
||||
return getBusinessHoursForDate(date) !== null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查日期是否应该显示为可选择状态(营业且有可用时间段)
|
||||
* @param date 日期
|
||||
* @returns 是否可选择
|
||||
*/
|
||||
const isDateSelectable = (date: Date): boolean => {
|
||||
// 如果只选日期模式,营业即可选择
|
||||
if (onlySelectDay.value) {
|
||||
if (!storeBusinessHours.value) return true;
|
||||
return isDateOpen(date);
|
||||
}
|
||||
|
||||
// 原逻辑:需要营业且有可用时间段
|
||||
if (!storeBusinessHours.value) return true; // 如果没有营业时间限制,默认可选择
|
||||
if (!isDateOpen(date)) return false;
|
||||
return hasAvailableTimeSlots(date);
|
||||
};
|
||||
|
||||
// 生成未来的日期(显示所有日期,但标记营业状态)
|
||||
const dateOptions = computed(() => {
|
||||
const dates: Date[] = [];
|
||||
const today = new Date();
|
||||
|
||||
// 生成连续的5天日期(包括不营业的日期)
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const date = new Date(today);
|
||||
date.setDate(today.getDate() + i);
|
||||
dates.push(date);
|
||||
}
|
||||
|
||||
return dates;
|
||||
});
|
||||
|
||||
// 状态管理 - 初始化为第一个营业日期
|
||||
const selectedDate = ref<Date>();
|
||||
|
||||
// 检查指定时间是否在营业时间内
|
||||
const isTimeInBusinessHours = (
|
||||
hour: number,
|
||||
minute: number,
|
||||
businessHours: BusinessHours
|
||||
): boolean => {
|
||||
const timeStr = `${hour.toString().padStart(2, "0")}:${minute
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
return timeStr >= businessHours.startTime && timeStr <= businessHours.endTime;
|
||||
};
|
||||
|
||||
// 检查指定日期是否有可用时间段
|
||||
const hasAvailableTimeSlots = (date: Date): boolean => {
|
||||
const now = new Date();
|
||||
const currentHour = now.getHours();
|
||||
const currentMinute = now.getMinutes();
|
||||
|
||||
// 获取指定日期的营业时间
|
||||
const businessHours = getBusinessHoursForDate(date);
|
||||
|
||||
// 如果没有营业时间,返回false
|
||||
if (!businessHours) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 解析营业时间的开始和结束时间
|
||||
const [startHour, startMinute] = businessHours.startTime
|
||||
.split(":")
|
||||
.map(Number);
|
||||
const [endHour, endMinute] = businessHours.endTime.split(":").map(Number);
|
||||
|
||||
// 检查是否有可用时间段
|
||||
for (let hour = startHour; hour <= endHour; hour++) {
|
||||
for (let minute = 0; minute < 60; minute += 30) {
|
||||
// 检查时间段开始时间是否在营业时间内
|
||||
if (!isTimeInBusinessHours(hour, minute, businessHours)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 计算时间段结束时间
|
||||
let nextHour = hour;
|
||||
let nextMinute = minute + 30;
|
||||
if (nextMinute >= 60) {
|
||||
nextHour++;
|
||||
nextMinute = 0;
|
||||
}
|
||||
|
||||
// 检查时间段结束时间是否超出营业时间
|
||||
if (!isTimeInBusinessHours(nextHour, nextMinute, businessHours)) {
|
||||
if (
|
||||
nextHour > endHour ||
|
||||
(nextHour === endHour && nextMinute > endMinute)
|
||||
) {
|
||||
nextHour = endHour;
|
||||
nextMinute = endMinute;
|
||||
}
|
||||
}
|
||||
|
||||
// 避免生成开始时间和结束时间相同的无效时间段
|
||||
if (hour === nextHour && minute === nextMinute) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果是今天,过滤掉已经过去的时间
|
||||
if (date.toDateString() === now.toDateString()) {
|
||||
if (
|
||||
hour < currentHour ||
|
||||
(hour === currentHour && minute <= currentMinute)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果能到这里,说明有可用时间段
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// 初始化选中日期为第一个有可用时间段的营业日期
|
||||
const initializeSelectedDate = () => {
|
||||
if (onlySelectDay.value) {
|
||||
// 仅选日期模式:选择第一个营业日期(或第一个日期)
|
||||
const firstOpen = dateOptions.value.find((d) => isDateOpen(d));
|
||||
selectedDate.value = firstOpen || dateOptions.value[0];
|
||||
nextTick(() => updateScrollPosition());
|
||||
return;
|
||||
}
|
||||
|
||||
// 非仅选日期模式:保留原逻辑
|
||||
for (const date of dateOptions.value) {
|
||||
if (isDateSelectable(date)) {
|
||||
selectedDate.value = date;
|
||||
nextTick(() => {
|
||||
updateScrollPosition();
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const nextBusinessDate = findNextBusinessDate(today);
|
||||
if (nextBusinessDate) {
|
||||
selectedDate.value = nextBusinessDate;
|
||||
nextTick(() => {
|
||||
updateScrollPosition();
|
||||
uni.showToast({
|
||||
title: t('pages.address.reservationTime.currentTimeExpired'),
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
selectedDate.value = dateOptions.value[0];
|
||||
nextTick(() => {
|
||||
updateScrollPosition();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 监听dateOptions变化,初始化选中日期
|
||||
watch(
|
||||
dateOptions,
|
||||
() => {
|
||||
if (!selectedDate.value) {
|
||||
initializeSelectedDate();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听营业时间字符串变化,重新初始化选中日期
|
||||
watch(
|
||||
storeBusinessHours,
|
||||
() => {
|
||||
if (storeBusinessHours.value) {
|
||||
initializeSelectedDate();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
const selectedTimeSlot = ref<string>("");
|
||||
|
||||
// 横向滚动距离
|
||||
const scrollLeft = ref<number>(0);
|
||||
|
||||
// 计算并设置横向滚动距离
|
||||
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) => {
|
||||
if (!isDateSelectable(date)) {
|
||||
return t('pages.address.reservationTime.notAvailable')
|
||||
}
|
||||
return dayjs(date).format('MMMM D')
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查时间是否在营业时间内
|
||||
* @param hour 小时
|
||||
* @param minute 分钟
|
||||
* @param businessHours 营业时间对象
|
||||
* @returns 是否在营业时间内
|
||||
*/
|
||||
|
||||
// 生成时间段选项(根据商家营业时间过滤)
|
||||
const timeSlots = computed(() => {
|
||||
const slots: string[] = [];
|
||||
const now = new Date();
|
||||
const currentHour = now.getHours();
|
||||
const currentMinute = now.getMinutes();
|
||||
|
||||
// 如果还没有选中日期,返回空数组
|
||||
if (!selectedDate.value) {
|
||||
return slots;
|
||||
}
|
||||
|
||||
// 获取选中日期的营业时间
|
||||
const businessHours = getBusinessHoursForDate(selectedDate.value);
|
||||
|
||||
// 如果没有营业时间限制,使用原有逻辑(0-24小时)
|
||||
if (!businessHours) {
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
for (let minute = 0; minute < 60; minute += 30) {
|
||||
// 如果是今天,过滤掉已经过去的时间
|
||||
if (selectedDate.value.toDateString() === now.toDateString()) {
|
||||
if (
|
||||
hour < currentHour ||
|
||||
(hour === currentHour && minute <= currentMinute)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const startHour = hour.toString().padStart(2, "0");
|
||||
const startMinute = minute.toString().padStart(2, "0");
|
||||
const endHour =
|
||||
minute === 30
|
||||
? (hour + 1).toString().padStart(2, "0")
|
||||
: hour.toString().padStart(2, "0");
|
||||
const endMinute = minute === 30 ? "00" : "30";
|
||||
|
||||
const timeSlot = `${startHour}:${startMinute} - ${endHour}:${endMinute}`;
|
||||
slots.push(timeSlot);
|
||||
}
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
|
||||
// 解析营业时间的开始和结束时间
|
||||
const [startHour, startMinute] = businessHours.startTime
|
||||
.split(":")
|
||||
.map(Number);
|
||||
const [endHour, endMinute] = businessHours.endTime.split(":").map(Number);
|
||||
|
||||
// 生成营业时间内的时间段
|
||||
for (let hour = startHour; hour <= endHour; hour++) {
|
||||
for (let minute = 0; minute < 60; minute += 30) {
|
||||
// 检查时间段开始时间是否在营业时间内
|
||||
if (!isTimeInBusinessHours(hour, minute, businessHours)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 计算时间段结束时间
|
||||
let nextHour = hour;
|
||||
let nextMinute = minute + 30;
|
||||
if (nextMinute >= 60) {
|
||||
nextHour++;
|
||||
nextMinute = 0;
|
||||
}
|
||||
|
||||
// 检查时间段结束时间是否超出营业时间
|
||||
if (!isTimeInBusinessHours(nextHour, nextMinute, businessHours)) {
|
||||
// 如果结束时间超出营业时间,但开始时间在营业时间内,则调整结束时间为营业结束时间
|
||||
if (
|
||||
nextHour > endHour ||
|
||||
(nextHour === endHour && nextMinute > endMinute)
|
||||
) {
|
||||
nextHour = endHour;
|
||||
nextMinute = endMinute;
|
||||
}
|
||||
}
|
||||
|
||||
// 避免生成开始时间和结束时间相同的无效时间段(如 18:00 - 18:00)
|
||||
if (hour === nextHour && minute === nextMinute) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果是今天,过滤掉已经过去的时间
|
||||
if (selectedDate.value.toDateString() === now.toDateString()) {
|
||||
if (
|
||||
hour < currentHour ||
|
||||
(hour === currentHour && minute <= currentMinute)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const startHourStr = hour.toString().padStart(2, "0");
|
||||
const startMinuteStr = minute.toString().padStart(2, "0");
|
||||
const endHourStr = nextHour.toString().padStart(2, "0");
|
||||
const endMinuteStr = nextMinute.toString().padStart(2, "0");
|
||||
|
||||
const timeSlot = `${startHourStr}:${startMinuteStr} - ${endHourStr}:${endMinuteStr}`;
|
||||
slots.push(timeSlot);
|
||||
}
|
||||
}
|
||||
|
||||
return slots;
|
||||
});
|
||||
|
||||
// 监听时间段变化,如果当前选中日期没有可用时间段,自动选择下一个营业日期
|
||||
watch(timeSlots, (newSlots) => {
|
||||
if (onlySelectDay.value) return; // 仅选日期模式不需要处理时间段
|
||||
if (selectedDate.value && newSlots.length === 0) {
|
||||
// 当前选中日期没有可用时间段,寻找下一个营业日期
|
||||
const nextBusinessDate = findNextBusinessDate(selectedDate.value);
|
||||
if (nextBusinessDate) {
|
||||
selectedDate.value = nextBusinessDate;
|
||||
selectedTimeSlot.value = ""; // 清空已选择的时间段
|
||||
uni.showToast({
|
||||
title: t('pages.address.reservationTime.noAvailableTime'),
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 寻找下一个有可用时间段的营业日期
|
||||
const findNextBusinessDate = (currentDate: Date): Date | null => {
|
||||
const maxDays = 30; // 最多向前查找30天
|
||||
for (let i = 1; i <= maxDays; i++) {
|
||||
const nextDate = new Date(currentDate);
|
||||
nextDate.setDate(currentDate.getDate() + i);
|
||||
|
||||
// 检查是否在dateOptions范围内
|
||||
const isInRange = dateOptions.value.some((date) =>
|
||||
dayjs(date).isSame(dayjs(nextDate), "day")
|
||||
);
|
||||
|
||||
if (isInRange && isDateOpen(nextDate) && hasAvailableTimeSlots(nextDate)) {
|
||||
return nextDate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 选择日期
|
||||
const selectDate = (date: Date) => {
|
||||
// 检查日期是否可选择,如果不可选择则不允许选择
|
||||
if (!isDateSelectable(date)) {
|
||||
uni.showToast({
|
||||
title: t('pages.address.reservationTime.dateNotSelectable'),
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
selectedDate.value = date;
|
||||
// 选择新日期后,清空已选择的时间段
|
||||
selectedTimeSlot.value = "";
|
||||
};
|
||||
|
||||
// 选择时间段
|
||||
const selectTimeSlot = (timeSlot: string) => {
|
||||
selectedTimeSlot.value = timeSlot;
|
||||
};
|
||||
|
||||
// 提交预约
|
||||
const submitReservation = () => {
|
||||
// 非仅选日期模式,需要选择时间段
|
||||
if (!onlySelectDay.value) {
|
||||
if (!selectedTimeSlot.value) {
|
||||
uni.showToast({
|
||||
title: t('pages.address.reservationTime.selectTimeSlot'),
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算开始/结束时间
|
||||
const selectedDateDayjs = dayjs(selectedDate.value);
|
||||
let startTime: dayjs.Dayjs;
|
||||
let endTime: dayjs.Dayjs;
|
||||
|
||||
if (onlySelectDay.value) {
|
||||
// 仅选日期:优先使用营业时间范围;若无营业时间限制,则使用当天起止
|
||||
const bh = getBusinessHoursForDate(selectedDate.value);
|
||||
if (bh) {
|
||||
const [startHour, startMinute] = bh.startTime.split(':').map(Number);
|
||||
const [endHour, endMinute] = bh.endTime.split(':').map(Number);
|
||||
startTime = selectedDateDayjs
|
||||
.hour(startHour)
|
||||
.minute(startMinute)
|
||||
.second(0)
|
||||
.millisecond(0);
|
||||
endTime = selectedDateDayjs
|
||||
.hour(endHour)
|
||||
.minute(endMinute)
|
||||
.second(0)
|
||||
.millisecond(0);
|
||||
} else {
|
||||
startTime = selectedDateDayjs.startOf('day');
|
||||
endTime = selectedDateDayjs.endOf('day');
|
||||
}
|
||||
} else {
|
||||
// 选择了时间段:解析并生成起止时间
|
||||
const [startTimeStr, endTimeStr] = selectedTimeSlot.value.split(' - ');
|
||||
const [startHour, startMinute] = startTimeStr.split(':').map(Number);
|
||||
const [endHour, endMinute] = endTimeStr.split(':').map(Number);
|
||||
startTime = selectedDateDayjs
|
||||
.hour(startHour)
|
||||
.minute(startMinute)
|
||||
.second(0)
|
||||
.millisecond(0);
|
||||
endTime = selectedDateDayjs
|
||||
.hour(endHour)
|
||||
.minute(endMinute)
|
||||
.second(0)
|
||||
.millisecond(0);
|
||||
}
|
||||
|
||||
console.log("预约信息:", {
|
||||
date: selectedDate.value,
|
||||
timeSlot: onlySelectDay.value ? '' : selectedTimeSlot.value,
|
||||
startTime: startTime.valueOf(),
|
||||
endTime: endTime.valueOf(),
|
||||
});
|
||||
|
||||
uni.$emit(EventEnum.CHOOSE_APPOINTMENT_TIME, {
|
||||
date: selectedDate.value,
|
||||
timeSlot: onlySelectDay.value ? '' : selectedTimeSlot.value,
|
||||
startTime: startTime.valueOf(),
|
||||
endTime: endTime.valueOf(),
|
||||
});
|
||||
|
||||
uni.showToast({
|
||||
title: t('pages.address.reservationTime.reservationSuccess'),
|
||||
icon: "none",
|
||||
});
|
||||
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
const storeId = ref(null);
|
||||
|
||||
// 页面加载时处理参数
|
||||
onLoad((options: any) => {
|
||||
if (options.storeId) {
|
||||
storeId.value = options.storeId;
|
||||
}
|
||||
if (options.storeBusinessHours) {
|
||||
storeBusinessHours.value = options.storeBusinessHours;
|
||||
// 如果传递了该参数,进入仅选日期模式
|
||||
onlySelectDay.value = true;
|
||||
}
|
||||
// 无论是否传参,统一初始化选中日期,避免首屏未选导致不显示时间段
|
||||
nextTick(() => {
|
||||
initializeSelectedDate();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="">
|
||||
<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
|
||||
@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"
|
||||
>
|
||||
<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>
|
||||
</view>
|
||||
</template>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<!-- 时间段选择区域:在仅选日期模式下隐藏 -->
|
||||
<view v-if="!onlySelectDay" class="pb-138rpx">
|
||||
<view
|
||||
v-for="(timeSlot, index) in timeSlots"
|
||||
:key="index"
|
||||
class="h-108rpx flex-center-sb px-30rpx"
|
||||
:class="[
|
||||
index === 0 ? '' : 'border-top',
|
||||
timeSlots.length - 1 === index ? 'border-bottom' : '',
|
||||
]"
|
||||
@click="selectTimeSlot(timeSlot)"
|
||||
>
|
||||
<text class="text-32rpx font-regular">{{ timeSlot }}</text>
|
||||
<!-- 单选按钮 -->
|
||||
<image
|
||||
:src="
|
||||
selectedTimeSlot === timeSlot
|
||||
? '/static/images/chef/133.png'
|
||||
: '/static/images/chef/134.png'
|
||||
"
|
||||
class="w-48rpx h-48rpx shrink-0"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<fixed-bottom-large-btn
|
||||
class="z-100"
|
||||
fixed
|
||||
:text="`${t('common.submit')}`"
|
||||
@click="submitReservation"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,317 @@
|
||||
<script setup lang="ts">
|
||||
import ChooseImage from "@/components/choose-image/choose-image.vue";
|
||||
import chooseState from "../components/choose-state.vue";
|
||||
import { getStateListApi } from '@/pages-user/service'
|
||||
|
||||
|
||||
const { t } = useI18n()
|
||||
import VisitMethod from '@/components/visit-method/index.vue'
|
||||
import BuildingType from './components/building-type.vue'
|
||||
import {useAddressStore} from "../store/address";
|
||||
import {appUserAddressAddPost, appUserAddressEditPost} from "@/service";
|
||||
import {UserAddressType} from "@/constant/enums";
|
||||
const addressStore = useAddressStore()
|
||||
|
||||
const visitMethodRef = ref<InstanceType<typeof VisitMethod>>()
|
||||
const buildingTypeRef = ref<InstanceType<typeof BuildingType>>()
|
||||
function openVisitMethod() {
|
||||
visitMethodRef.value?.onOpen()
|
||||
}
|
||||
|
||||
const visitMethod = ref(t('components.visit.leaveItToMePersonally')) // 默认选择亲自送达
|
||||
function visitMethodConfirm(data: any) {
|
||||
console.log('visitMethodConfirm', data)
|
||||
visitMethod.value = data.label;
|
||||
addressStore.addressInfo.deliveryType = data.value + 1; // 更新地址信息中的送达方式
|
||||
}
|
||||
|
||||
function openBuildingType() {
|
||||
buildingTypeRef.value?.onOpen()
|
||||
}
|
||||
|
||||
|
||||
// 选择图片 - 添加安全检查
|
||||
const chooseImageRef = ref<InstanceType<typeof ChooseImage>>()
|
||||
const images = ref<string>('')
|
||||
function chooseImage() {
|
||||
if (chooseImageRef.value?.init) {
|
||||
chooseImageRef.value.init();
|
||||
}
|
||||
}
|
||||
|
||||
// 图片选择回调 - 修复类型问题
|
||||
function onImageChange(files: string[]) {
|
||||
if (files.length > 0) {
|
||||
// 确保 images 数组不为空
|
||||
images.value = files[0]; // 只取第一个图片
|
||||
} else {
|
||||
images.value = ''; // 如果没有选择图片,清空
|
||||
}
|
||||
}
|
||||
|
||||
function removeImage() {
|
||||
images.value = ''
|
||||
}
|
||||
|
||||
function saveAddress() {
|
||||
console.log('保存地址信息', addressStore.addressInfo);
|
||||
if(addressStore.addressInfo.id) {
|
||||
appUserAddressEditPost({
|
||||
body: {
|
||||
...addressStore.addressInfo,
|
||||
deliveryImage: images.value
|
||||
}
|
||||
}).then(res=> {
|
||||
uni.navigateBack()
|
||||
addressStore.clearAddressInfo()
|
||||
})
|
||||
} else {
|
||||
appUserAddressAddPost({
|
||||
body: {
|
||||
...addressStore.addressInfo,
|
||||
deliveryImage: images.value
|
||||
}
|
||||
}).then(res=> {
|
||||
uni.navigateBack()
|
||||
addressStore.clearAddressInfo()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(()=> {
|
||||
isSwitch.value = false
|
||||
if(addressStore.addressInfo.deliveryImage) {
|
||||
images.value = addressStore.addressInfo.deliveryImage
|
||||
}
|
||||
// 回显送达偏好
|
||||
if(+addressStore.addressInfo.deliveryType === 1) {
|
||||
visitMethod.value = t('components.visit.leaveItToMePersonally')
|
||||
}
|
||||
if(+addressStore.addressInfo.deliveryType === 2) {
|
||||
visitMethod.value = t('components.visit.putItAtTheDoor')
|
||||
}
|
||||
|
||||
getStateListApi().then(res=> {
|
||||
stateList.value = res.data
|
||||
if(addressStore.addressInfo.state) {
|
||||
addressStore.addressInfo.stateName = res.data.find(item => item.id === addressStore.addressInfo.state)?.name || ''
|
||||
}
|
||||
})
|
||||
})
|
||||
onUnload(()=> {
|
||||
if(!isSwitch.value) {
|
||||
addressStore.clearAddressInfo()
|
||||
}
|
||||
})
|
||||
|
||||
const isSwitch = ref(false)
|
||||
function submitType(value: string) {
|
||||
addressStore.addressInfo.type = value
|
||||
isSwitch.value = true
|
||||
switch (value) {
|
||||
case UserAddressType.HOUSE:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/house',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.APARTMENT:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/apartment',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.OFFICE:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/office',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.HOTEL:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/hotel',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.OTHER:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/other',
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const chooseStateRef = ref()
|
||||
const stateList = ref([])
|
||||
function openState() {
|
||||
chooseStateRef.value?.init(stateList.value, addressStore.addressInfo.state)
|
||||
}
|
||||
function chooseStateConfirm(data: any) {
|
||||
addressStore.addressInfo.state = data.id
|
||||
addressStore.addressInfo.stateName = data.name
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<navbar :title="t('pages.address.titleDetail')" />
|
||||
<!-- 地址搜索结果名字 -->
|
||||
<view class="px-30rpx pt-38rpx border-bottom pb-52rpx">
|
||||
<view class="text-32rpx lh-32rpx text-#333 mb-62rpx">
|
||||
{{ addressStore.addressInfo.displayName }}
|
||||
{{ addressStore.addressInfo.formattedAddress }}
|
||||
</view>
|
||||
|
||||
|
||||
<!-- State -->
|
||||
<view class="mb-44rpx">
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('common.state') }}</view>
|
||||
<view @click="openState" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
|
||||
<text class="text-30rpx lh-30rpx text-#333">
|
||||
{{ addressStore.addressInfo.stateName ? addressStore.addressInfo.stateName : t('common.placeholder.pleaseSelect') }}
|
||||
</text>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 房屋类型 -->
|
||||
<view>
|
||||
<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}`)}` }}
|
||||
</text>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Apartment/Unit/Floor (required) -->
|
||||
<view class="mt-44rpx">
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.apartment.titleApartment') }}</view>
|
||||
<view class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex items-center">
|
||||
<wd-input
|
||||
v-model.trim="addressStore.addressInfo.apartmentUnitFloor"
|
||||
:cursorSpacing="20"
|
||||
:focus-when-clear="false"
|
||||
:maxlength="20"
|
||||
:placeholder="t('pages.address.apartment.titleApartmentTips')"
|
||||
clearable
|
||||
custom-class="flex-1 !text-30rpx lh-30rpx !text-#333 !bg-transparent"
|
||||
no-border
|
||||
placeholderStyle="font-size: 30rpx;color: #999;"
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
</view>
|
||||
<!-- Building Name -->
|
||||
<view class="mt-44rpx">
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.apartment.titleBuilding') }}</view>
|
||||
<view class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex items-center">
|
||||
<wd-input
|
||||
v-model.trim="addressStore.addressInfo.buildingName"
|
||||
:cursorSpacing="20"
|
||||
:focus-when-clear="false"
|
||||
:maxlength="20"
|
||||
:placeholder="t('pages.address.apartment.titleBuildingTips')"
|
||||
clearable
|
||||
custom-class="flex-1 !text-30rpx lh-30rpx !text-#333 !bg-transparent"
|
||||
no-border
|
||||
placeholderStyle="font-size: 30rpx;color: #999;"
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
</view>
|
||||
<!-- Entry Code -->
|
||||
<view class="mt-44rpx">
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.apartment.titleEntry') }}</view>
|
||||
<view class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex items-center">
|
||||
<wd-input
|
||||
v-model.trim="addressStore.addressInfo.entranceCodeApartment"
|
||||
:cursorSpacing="20"
|
||||
:focus-when-clear="false"
|
||||
:maxlength="20"
|
||||
:placeholder="t('pages.address.apartment.titleEntryTips')"
|
||||
clearable
|
||||
custom-class="flex-1 !text-30rpx lh-30rpx !text-#333 !bg-transparent"
|
||||
no-border
|
||||
placeholderStyle="font-size: 30rpx;color: #999;"
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="px-30rpx py-52rpx">
|
||||
<!-- Delivery Point Information -->
|
||||
<view class="mb-44rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.deliveryPointInfo') }}</view>
|
||||
|
||||
<view @click="openVisitMethod" class="flex-center-sb">
|
||||
<view>
|
||||
<view class="text-32rpx lh-32rpx text-#333 mb-12rpx">{{ visitMethod }}</view>
|
||||
<view class="text-32rpx lh-32rpx text-#999">{{ t('pages.address.moreOptions') }}</view>
|
||||
</view>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0"></image>
|
||||
</view>
|
||||
|
||||
<view class="mt-52rpx">
|
||||
<view class="text-36rpx lh-36rpx text-#333 font-500 mb-24rpx">{{ t('pages.address.deliveryInstructions') }}</view>
|
||||
<view
|
||||
class="min-h-240rpx box-border bg-#F6F6F6 rounded-20rpx overflow-hidden p-20rpx"
|
||||
>
|
||||
<wd-textarea
|
||||
:maxlength="250"
|
||||
custom-class="!bg-#F6F6F6"
|
||||
custom-textarea-container-class="!bg-#F6F6F6"
|
||||
no-border
|
||||
auto-height
|
||||
v-model="addressStore.addressInfo.deliveryRemark"
|
||||
:placeholder="t('pages.address.pleaseTip')"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mt-24rpx relative w-240rpx h-240rpx" @click="chooseImage">
|
||||
<image
|
||||
v-if="images"
|
||||
class="absolute top--10rpx right--10rpx z-1 w-36rpx h-36rpx"
|
||||
src="@img/chef/113.png"
|
||||
@click.stop="removeImage"
|
||||
></image>
|
||||
<view v-if="!images" class="flex flex-col items-center justify-center w-240rpx h-240rpx bg-#f6f6f6 rounded-20rpx">
|
||||
<wd-icon name="add" size="32px" color="#3d3d3d"></wd-icon>
|
||||
<text class="text-30rpx lh-30rpx text-#333 mt-22rpx">{{ t('common.addPicture') }}</text>
|
||||
</view>
|
||||
<image
|
||||
v-else
|
||||
:src="images"
|
||||
class="w-240rpx h-240rpx rounded-20rpx"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<fixed-bottom-large-btn
|
||||
class="z-100"
|
||||
fixed
|
||||
:text="t('common.saveAndContinue')"
|
||||
@click="saveAddress"
|
||||
/>
|
||||
|
||||
<visit-method @confirm="visitMethodConfirm" ref="visitMethodRef" />
|
||||
<!-- 房屋类型 -->
|
||||
<building-type ref="buildingTypeRef" @submit="submitType" />
|
||||
|
||||
<ChooseImage ref="chooseImageRef" @change="onImageChange"/>
|
||||
|
||||
<!-- 所在州 -->
|
||||
<choose-state ref="chooseStateRef" @confirm="chooseStateConfirm" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.wd-textarea) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import {UserAddressType} from "@/constant/enums";
|
||||
|
||||
const { t } = useI18n();
|
||||
const show = ref(false);
|
||||
const value = ref('house')
|
||||
|
||||
const emit = defineEmits(['submit'])
|
||||
|
||||
function onOpen() {
|
||||
show.value = true;
|
||||
}
|
||||
function handleClose() {
|
||||
show.value = false;
|
||||
}
|
||||
function handleSubmit() {
|
||||
console.log('value', value.value)
|
||||
emit('submit', value.value)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
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}) {
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
onOpen,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wd-popup
|
||||
v-model="show"
|
||||
position="bottom"
|
||||
@close="handleClose"
|
||||
>
|
||||
<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-34rpx text-#333">{{ t('common.buildingType') }}</view>
|
||||
<view @click="handleSubmit" class="text-30rpx text-#FF6106">{{ 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" />
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.uni-picker-view-wrapper) {
|
||||
& > uni-picker-view-column:first-of-type .uni-picker-view-group {
|
||||
.uni-picker-view-indicator {
|
||||
border-radius: 20rpx 0 0 20rpx !important;
|
||||
&:after {
|
||||
border: none;
|
||||
}
|
||||
&:before {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.wd-picker-view-column__item) {
|
||||
line-height: 94rpx !important;
|
||||
}
|
||||
:deep(.uni-picker-view-indicator) {
|
||||
height: 94rpx !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,295 @@
|
||||
<script setup lang="ts">
|
||||
import ChooseImage from "@/components/choose-image/choose-image.vue";
|
||||
import chooseState from "../components/choose-state.vue";
|
||||
import { getStateListApi } from '@/pages-user/service'
|
||||
|
||||
const { t } = useI18n()
|
||||
import VisitMethod from '@/components/visit-method/index.vue'
|
||||
import BuildingType from './components/building-type.vue'
|
||||
import {useAddressStore} from "../store/address";
|
||||
import {appUserAddressAddPost, appUserAddressEditPost} from "@/service";
|
||||
import {UserAddressType} from "@/constant/enums";
|
||||
const addressStore = useAddressStore()
|
||||
|
||||
const visitMethodRef = ref<InstanceType<typeof VisitMethod>>()
|
||||
const buildingTypeRef = ref<InstanceType<typeof BuildingType>>()
|
||||
function openVisitMethod() {
|
||||
visitMethodRef.value?.onOpen()
|
||||
}
|
||||
|
||||
const visitMethod = ref(t('components.visit.leaveItToMePersonally')) // 默认选择亲自送达
|
||||
function visitMethodConfirm(data: any) {
|
||||
console.log('visitMethodConfirm', data)
|
||||
visitMethod.value = data.label;
|
||||
addressStore.addressInfo.deliveryType = data.value + 1; // 更新地址信息中的送达方式
|
||||
}
|
||||
|
||||
function openBuildingType() {
|
||||
buildingTypeRef.value?.onOpen()
|
||||
}
|
||||
|
||||
|
||||
// 选择图片 - 添加安全检查
|
||||
const chooseImageRef = ref<InstanceType<typeof ChooseImage>>()
|
||||
const images = ref<string>('')
|
||||
function chooseImage() {
|
||||
if (chooseImageRef.value?.init) {
|
||||
chooseImageRef.value.init();
|
||||
}
|
||||
}
|
||||
|
||||
// 图片选择回调 - 修复类型问题
|
||||
function onImageChange(files: string[]) {
|
||||
if (files.length > 0) {
|
||||
// 确保 images 数组不为空
|
||||
images.value = files[0]; // 只取第一个图片
|
||||
} else {
|
||||
images.value = ''; // 如果没有选择图片,清空
|
||||
}
|
||||
}
|
||||
|
||||
function removeImage() {
|
||||
images.value = ''
|
||||
}
|
||||
|
||||
function saveAddress() {
|
||||
console.log('保存地址信息', addressStore.addressInfo);
|
||||
if(addressStore.addressInfo.id) {
|
||||
appUserAddressEditPost({
|
||||
body: {
|
||||
...addressStore.addressInfo,
|
||||
deliveryImage: images.value
|
||||
}
|
||||
}).then(res=> {
|
||||
uni.navigateBack()
|
||||
addressStore.clearAddressInfo()
|
||||
})
|
||||
} else {
|
||||
appUserAddressAddPost({
|
||||
body: {
|
||||
...addressStore.addressInfo,
|
||||
deliveryImage: images.value
|
||||
}
|
||||
}).then(res=> {
|
||||
uni.navigateBack()
|
||||
addressStore.clearAddressInfo()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(()=> {
|
||||
isSwitch.value = false
|
||||
if(addressStore.addressInfo.deliveryImage) {
|
||||
images.value = addressStore.addressInfo.deliveryImage
|
||||
}
|
||||
// 回显送达偏好
|
||||
if(+addressStore.addressInfo.deliveryType === 1) {
|
||||
visitMethod.value = t('components.visit.leaveItToMePersonally')
|
||||
}
|
||||
if(+addressStore.addressInfo.deliveryType === 2) {
|
||||
visitMethod.value = t('components.visit.putItAtTheDoor')
|
||||
}
|
||||
|
||||
getStateListApi().then(res=> {
|
||||
stateList.value = res.data
|
||||
if(addressStore.addressInfo.state) {
|
||||
addressStore.addressInfo.stateName = res.data.find(item => item.id === addressStore.addressInfo.state)?.name || ''
|
||||
}
|
||||
})
|
||||
})
|
||||
onUnload(()=> {
|
||||
if(!isSwitch.value) {
|
||||
addressStore.clearAddressInfo()
|
||||
}
|
||||
})
|
||||
|
||||
const isSwitch = ref(false)
|
||||
function submitType(value: string) {
|
||||
addressStore.addressInfo.type = value
|
||||
isSwitch.value = true
|
||||
switch (value) {
|
||||
case UserAddressType.HOUSE:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/house',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.APARTMENT:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/apartment',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.OFFICE:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/office',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.HOTEL:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/hotel',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.OTHER:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/other',
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
const chooseStateRef = ref()
|
||||
const stateList = ref([])
|
||||
function openState() {
|
||||
chooseStateRef.value?.init(stateList.value, addressStore.addressInfo.state)
|
||||
}
|
||||
function chooseStateConfirm(data: any) {
|
||||
addressStore.addressInfo.state = data.id
|
||||
addressStore.addressInfo.stateName = data.name
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<navbar :title="t('pages.address.titleDetail')" />
|
||||
<!-- 地址搜索结果名字 -->
|
||||
<view class="px-30rpx pt-38rpx border-bottom pb-52rpx">
|
||||
<view class="text-32rpx lh-32rpx text-#333 mb-62rpx">
|
||||
{{ addressStore.addressInfo.displayName }}
|
||||
{{ addressStore.addressInfo.formattedAddress }}
|
||||
</view>
|
||||
|
||||
<!-- State -->
|
||||
<view class="mb-44rpx">
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('common.state') }}</view>
|
||||
<view @click="openState" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
|
||||
<text class="text-30rpx lh-30rpx text-#333">
|
||||
{{ addressStore.addressInfo.stateName ? addressStore.addressInfo.stateName : t('common.placeholder.pleaseSelect') }}
|
||||
</text>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 房屋类型 -->
|
||||
<view>
|
||||
<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}`)}` }}
|
||||
</text>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Hotel Name (required) -->
|
||||
<view class="mt-44rpx">
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.hotel.titleHotel') }}</view>
|
||||
<view class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex items-center">
|
||||
<wd-input
|
||||
v-model.trim="addressStore.addressInfo.hotelName"
|
||||
:cursorSpacing="20"
|
||||
:focus-when-clear="false"
|
||||
:maxlength="20"
|
||||
:placeholder="t('pages.address.hotel.titleHotelTips')"
|
||||
clearable
|
||||
custom-class="flex-1 !text-30rpx lh-30rpx !text-#333 !bg-transparent"
|
||||
no-border
|
||||
placeholderStyle="font-size: 30rpx;color: #999;"
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
</view>
|
||||
<!-- Room/Floor -->
|
||||
<view class="mt-44rpx">
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.hotel.titleRoom') }}</view>
|
||||
<view class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex items-center">
|
||||
<wd-input
|
||||
v-model.trim="addressStore.addressInfo.hotelRoom"
|
||||
:cursorSpacing="20"
|
||||
:focus-when-clear="false"
|
||||
:maxlength="20"
|
||||
:placeholder="t('pages.address.hotel.titleRoomTips')"
|
||||
clearable
|
||||
custom-class="flex-1 !text-30rpx lh-30rpx !text-#333 !bg-transparent"
|
||||
no-border
|
||||
placeholderStyle="font-size: 30rpx;color: #999;"
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="px-30rpx py-52rpx">
|
||||
<!-- Delivery Point Information -->
|
||||
<view class="mb-44rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.deliveryPointInfo') }}</view>
|
||||
|
||||
<view @click="openVisitMethod" class="flex-center-sb">
|
||||
<view>
|
||||
<view class="text-32rpx lh-32rpx text-#333 mb-12rpx">{{ visitMethod }}</view>
|
||||
<view class="text-32rpx lh-32rpx text-#999">{{ t('pages.address.moreOptions') }}</view>
|
||||
</view>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0"></image>
|
||||
</view>
|
||||
|
||||
<view class="mt-52rpx">
|
||||
<view class="text-36rpx lh-36rpx text-#333 font-500 mb-24rpx">{{ t('pages.address.deliveryInstructions') }}</view>
|
||||
<view
|
||||
class="min-h-240rpx box-border bg-#F6F6F6 rounded-20rpx overflow-hidden p-20rpx"
|
||||
>
|
||||
<wd-textarea
|
||||
:maxlength="250"
|
||||
custom-class="!bg-#F6F6F6"
|
||||
custom-textarea-container-class="!bg-#F6F6F6"
|
||||
no-border
|
||||
auto-height
|
||||
v-model="addressStore.addressInfo.deliveryRemark"
|
||||
:placeholder="t('pages.address.pleaseTip')"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mt-24rpx relative w-240rpx h-240rpx" @click="chooseImage">
|
||||
<image
|
||||
v-if="images"
|
||||
class="absolute top--10rpx right--10rpx z-1 w-36rpx h-36rpx"
|
||||
src="@img/chef/113.png"
|
||||
@click.stop="removeImage"
|
||||
></image>
|
||||
<view v-if="!images" class="flex flex-col items-center justify-center w-240rpx h-240rpx bg-#f6f6f6 rounded-20rpx">
|
||||
<wd-icon name="add" size="32px" color="#3d3d3d"></wd-icon>
|
||||
<text class="text-30rpx lh-30rpx text-#333 mt-22rpx">{{ t('common.addPicture') }}</text>
|
||||
</view>
|
||||
<image
|
||||
v-else
|
||||
:src="images"
|
||||
class="w-240rpx h-240rpx rounded-20rpx"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<fixed-bottom-large-btn
|
||||
class="z-100"
|
||||
fixed
|
||||
:text="t('common.saveAndContinue')"
|
||||
@click="saveAddress"
|
||||
/>
|
||||
|
||||
<visit-method @confirm="visitMethodConfirm" ref="visitMethodRef" />
|
||||
<!-- 房屋类型 -->
|
||||
<building-type ref="buildingTypeRef" @submit="submitType" />
|
||||
|
||||
<ChooseImage ref="chooseImageRef" @change="onImageChange"/>
|
||||
<!-- 所在州 -->
|
||||
<choose-state ref="chooseStateRef" @confirm="chooseStateConfirm" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.wd-textarea) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,277 @@
|
||||
<script setup lang="ts">
|
||||
import ChooseImage from "@/components/choose-image/choose-image.vue";
|
||||
import chooseState from "../components/choose-state.vue";
|
||||
import { getStateListApi } from '@/pages-user/service'
|
||||
|
||||
const { t } = useI18n()
|
||||
import VisitMethod from '@/components/visit-method/index.vue'
|
||||
import BuildingType from './components/building-type.vue'
|
||||
import {useAddressStore} from "../store/address";
|
||||
import {appUserAddressAddPost, appUserAddressEditPost} from "@/service";
|
||||
import {UserAddressType} from "@/constant/enums";
|
||||
const addressStore = useAddressStore()
|
||||
|
||||
const visitMethodRef = ref<InstanceType<typeof VisitMethod>>()
|
||||
const buildingTypeRef = ref<InstanceType<typeof BuildingType>>()
|
||||
function openVisitMethod() {
|
||||
visitMethodRef.value?.onOpen()
|
||||
}
|
||||
|
||||
const visitMethod = ref(t('components.visit.leaveItToMePersonally')) // 默认选择亲自送达
|
||||
function visitMethodConfirm(data: any) {
|
||||
console.log('visitMethodConfirm', data)
|
||||
visitMethod.value = data.label;
|
||||
addressStore.addressInfo.deliveryType = data.value + 1; // 更新地址信息中的送达方式
|
||||
}
|
||||
|
||||
function openBuildingType() {
|
||||
buildingTypeRef.value?.onOpen()
|
||||
}
|
||||
|
||||
|
||||
// 选择图片 - 添加安全检查
|
||||
const chooseImageRef = ref<InstanceType<typeof ChooseImage>>()
|
||||
const images = ref<string>('')
|
||||
function chooseImage() {
|
||||
if (chooseImageRef.value?.init) {
|
||||
chooseImageRef.value.init();
|
||||
}
|
||||
}
|
||||
|
||||
// 图片选择回调 - 修复类型问题
|
||||
function onImageChange(files: string[]) {
|
||||
if (files.length > 0) {
|
||||
// 确保 images 数组不为空
|
||||
images.value = files[0]; // 只取第一个图片
|
||||
} else {
|
||||
images.value = ''; // 如果没有选择图片,清空
|
||||
}
|
||||
}
|
||||
|
||||
function removeImage() {
|
||||
images.value = ''
|
||||
}
|
||||
|
||||
function saveAddress() {
|
||||
console.log('保存地址信息', addressStore.addressInfo);
|
||||
if(addressStore.addressInfo.id) {
|
||||
appUserAddressEditPost({
|
||||
body: {
|
||||
...addressStore.addressInfo,
|
||||
deliveryImage: images.value
|
||||
}
|
||||
}).then(res=> {
|
||||
uni.navigateBack()
|
||||
addressStore.clearAddressInfo()
|
||||
})
|
||||
} else {
|
||||
appUserAddressAddPost({
|
||||
body: {
|
||||
...addressStore.addressInfo,
|
||||
deliveryImage: images.value
|
||||
}
|
||||
}).then(res=> {
|
||||
uni.navigateBack()
|
||||
addressStore.clearAddressInfo()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(()=> {
|
||||
isSwitch.value = false
|
||||
if(addressStore.addressInfo.deliveryImage) {
|
||||
images.value = addressStore.addressInfo.deliveryImage
|
||||
}
|
||||
// 回显送达偏好
|
||||
if(+addressStore.addressInfo.deliveryType === 1) {
|
||||
visitMethod.value = t('components.visit.leaveItToMePersonally')
|
||||
}
|
||||
if(+addressStore.addressInfo.deliveryType === 2) {
|
||||
visitMethod.value = t('components.visit.putItAtTheDoor')
|
||||
}
|
||||
|
||||
getStateListApi().then(res=> {
|
||||
stateList.value = res.data
|
||||
if(addressStore.addressInfo.state) {
|
||||
addressStore.addressInfo.stateName = res.data.find(item => item.id === addressStore.addressInfo.state)?.name || ''
|
||||
}
|
||||
})
|
||||
})
|
||||
onUnload(()=> {
|
||||
if(!isSwitch.value) {
|
||||
addressStore.clearAddressInfo()
|
||||
}
|
||||
})
|
||||
|
||||
const isSwitch = ref(false)
|
||||
function submitType(value: string) {
|
||||
addressStore.addressInfo.type = value
|
||||
isSwitch.value = true
|
||||
switch (value) {
|
||||
case UserAddressType.HOUSE:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/house',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.APARTMENT:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/apartment',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.OFFICE:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/office',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.HOTEL:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/hotel',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.OTHER:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/other',
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
const chooseStateRef = ref()
|
||||
const stateList = ref([])
|
||||
function openState() {
|
||||
chooseStateRef.value?.init(stateList.value, addressStore.addressInfo.state)
|
||||
}
|
||||
function chooseStateConfirm(data: any) {
|
||||
addressStore.addressInfo.state = data.id
|
||||
addressStore.addressInfo.stateName = data.name
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<navbar :title="t('pages.address.titleDetail')" />
|
||||
<!-- 地址搜索结果名字 -->
|
||||
<view class="px-30rpx pt-38rpx border-bottom pb-52rpx">
|
||||
<view class="text-32rpx lh-32rpx text-#333 mb-62rpx">
|
||||
{{ addressStore.addressInfo.displayName }}
|
||||
{{ addressStore.addressInfo.formattedAddress }}
|
||||
</view>
|
||||
|
||||
<!-- State -->
|
||||
<view class="mb-44rpx">
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('common.state') }}</view>
|
||||
<view @click="openState" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
|
||||
<text class="text-30rpx lh-30rpx text-#333">
|
||||
{{ addressStore.addressInfo.stateName ? addressStore.addressInfo.stateName : t('common.placeholder.pleaseSelect') }}
|
||||
</text>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 房屋类型 -->
|
||||
<view>
|
||||
<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}`)}` }}
|
||||
</text>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Other Details -->
|
||||
<view class="mt-44rpx">
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.otherDetails') }}</view>
|
||||
<view class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex items-center">
|
||||
<wd-input
|
||||
v-model.trim="addressStore.addressInfo.houseExtra"
|
||||
:cursorSpacing="20"
|
||||
:focus-when-clear="false"
|
||||
:maxlength="20"
|
||||
:placeholder="t('pages.address.house.tips')"
|
||||
clearable
|
||||
custom-class="flex-1 !text-30rpx lh-30rpx !text-#333 !bg-transparent"
|
||||
no-border
|
||||
placeholderStyle="font-size: 30rpx;color: #999;"
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="px-30rpx py-52rpx">
|
||||
<!-- Delivery Point Information -->
|
||||
<view class="mb-44rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.deliveryPointInfo') }}</view>
|
||||
|
||||
<view @click="openVisitMethod" class="flex-center-sb">
|
||||
<view>
|
||||
<view class="text-32rpx lh-32rpx text-#333 mb-12rpx">{{ visitMethod }}</view>
|
||||
<view class="text-32rpx lh-32rpx text-#999">{{ t('pages.address.moreOptions') }}</view>
|
||||
</view>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0"></image>
|
||||
</view>
|
||||
|
||||
<view class="mt-52rpx">
|
||||
<view class="text-36rpx lh-36rpx text-#333 font-500 mb-24rpx">{{ t('pages.address.deliveryInstructions') }}</view>
|
||||
<view
|
||||
class="min-h-240rpx box-border bg-#F6F6F6 rounded-20rpx overflow-hidden p-20rpx"
|
||||
>
|
||||
<wd-textarea
|
||||
:maxlength="250"
|
||||
custom-class="!bg-#F6F6F6"
|
||||
custom-textarea-container-class="!bg-#F6F6F6"
|
||||
no-border
|
||||
auto-height
|
||||
v-model="addressStore.addressInfo.deliveryRemark"
|
||||
:placeholder="t('pages.address.pleaseTip')"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mt-24rpx relative w-240rpx h-240rpx" @click="chooseImage">
|
||||
<image
|
||||
v-if="images"
|
||||
class="absolute top--10rpx right--10rpx z-1 w-36rpx h-36rpx"
|
||||
src="@img/chef/113.png"
|
||||
@click.stop="removeImage"
|
||||
></image>
|
||||
<view v-if="!images" class="flex flex-col items-center justify-center w-240rpx h-240rpx bg-#f6f6f6 rounded-20rpx">
|
||||
<wd-icon name="add" size="32px" color="#3d3d3d"></wd-icon>
|
||||
<text class="text-30rpx lh-30rpx text-#333 mt-22rpx">{{ t('common.addPicture') }}</text>
|
||||
</view>
|
||||
<image
|
||||
v-else
|
||||
:src="images"
|
||||
class="w-240rpx h-240rpx rounded-20rpx"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<fixed-bottom-large-btn
|
||||
class="z-100"
|
||||
fixed
|
||||
:text="t('common.saveAndContinue')"
|
||||
@click="saveAddress"
|
||||
/>
|
||||
|
||||
<visit-method @confirm="visitMethodConfirm" ref="visitMethodRef" />
|
||||
<!-- 房屋类型 -->
|
||||
<building-type ref="buildingTypeRef" @submit="submitType" />
|
||||
|
||||
<ChooseImage ref="chooseImageRef" @change="onImageChange"/>
|
||||
<!-- 所在州 -->
|
||||
<choose-state ref="chooseStateRef" @confirm="chooseStateConfirm" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.wd-textarea) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,295 @@
|
||||
<script setup lang="ts">
|
||||
import ChooseImage from "@/components/choose-image/choose-image.vue";
|
||||
import chooseState from "../components/choose-state.vue";
|
||||
import { getStateListApi } from '@/pages-user/service'
|
||||
|
||||
const { t } = useI18n()
|
||||
import VisitMethod from '@/components/visit-method/index.vue'
|
||||
import BuildingType from './components/building-type.vue'
|
||||
import {useAddressStore} from "../store/address";
|
||||
import {appUserAddressAddPost, appUserAddressEditPost} from "@/service";
|
||||
import {UserAddressType} from "@/constant/enums";
|
||||
const addressStore = useAddressStore()
|
||||
|
||||
const visitMethodRef = ref<InstanceType<typeof VisitMethod>>()
|
||||
const buildingTypeRef = ref<InstanceType<typeof BuildingType>>()
|
||||
function openVisitMethod() {
|
||||
visitMethodRef.value?.onOpen()
|
||||
}
|
||||
|
||||
const visitMethod = ref(t('components.visit.leaveItToMePersonally')) // 默认选择亲自送达
|
||||
function visitMethodConfirm(data: any) {
|
||||
console.log('visitMethodConfirm', data)
|
||||
visitMethod.value = data.label;
|
||||
addressStore.addressInfo.deliveryType = data.value + 1; // 更新地址信息中的送达方式
|
||||
}
|
||||
|
||||
function openBuildingType() {
|
||||
buildingTypeRef.value?.onOpen()
|
||||
}
|
||||
|
||||
|
||||
// 选择图片 - 添加安全检查
|
||||
const chooseImageRef = ref<InstanceType<typeof ChooseImage>>()
|
||||
const images = ref<string>('')
|
||||
function chooseImage() {
|
||||
if (chooseImageRef.value?.init) {
|
||||
chooseImageRef.value.init();
|
||||
}
|
||||
}
|
||||
|
||||
// 图片选择回调 - 修复类型问题
|
||||
function onImageChange(files: string[]) {
|
||||
if (files.length > 0) {
|
||||
// 确保 images 数组不为空
|
||||
images.value = files[0]; // 只取第一个图片
|
||||
} else {
|
||||
images.value = ''; // 如果没有选择图片,清空
|
||||
}
|
||||
}
|
||||
|
||||
function removeImage() {
|
||||
images.value = ''
|
||||
}
|
||||
|
||||
function saveAddress() {
|
||||
console.log('保存地址信息', addressStore.addressInfo);
|
||||
if(addressStore.addressInfo.id) {
|
||||
appUserAddressEditPost({
|
||||
body: {
|
||||
...addressStore.addressInfo,
|
||||
deliveryImage: images.value
|
||||
}
|
||||
}).then(res=> {
|
||||
uni.navigateBack()
|
||||
addressStore.clearAddressInfo()
|
||||
})
|
||||
} else {
|
||||
appUserAddressAddPost({
|
||||
body: {
|
||||
...addressStore.addressInfo,
|
||||
deliveryImage: images.value
|
||||
}
|
||||
}).then(res=> {
|
||||
uni.navigateBack()
|
||||
addressStore.clearAddressInfo()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(()=> {
|
||||
isSwitch.value = false
|
||||
if(addressStore.addressInfo.deliveryImage) {
|
||||
images.value = addressStore.addressInfo.deliveryImage
|
||||
}
|
||||
// 回显送达偏好
|
||||
if(+addressStore.addressInfo.deliveryType === 1) {
|
||||
visitMethod.value = t('components.visit.leaveItToMePersonally')
|
||||
}
|
||||
if(+addressStore.addressInfo.deliveryType === 2) {
|
||||
visitMethod.value = t('components.visit.putItAtTheDoor')
|
||||
}
|
||||
|
||||
getStateListApi().then(res=> {
|
||||
stateList.value = res.data
|
||||
if(addressStore.addressInfo.state) {
|
||||
addressStore.addressInfo.stateName = res.data.find(item => item.id === addressStore.addressInfo.state)?.name || ''
|
||||
}
|
||||
})
|
||||
})
|
||||
onUnload(()=> {
|
||||
if(!isSwitch.value) {
|
||||
addressStore.clearAddressInfo()
|
||||
}
|
||||
})
|
||||
|
||||
const isSwitch = ref(false)
|
||||
function submitType(value: string) {
|
||||
addressStore.addressInfo.type = value
|
||||
isSwitch.value = true
|
||||
switch (value) {
|
||||
case UserAddressType.HOUSE:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/house',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.APARTMENT:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/apartment',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.OFFICE:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/office',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.HOTEL:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/hotel',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.OTHER:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/other',
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
const chooseStateRef = ref()
|
||||
const stateList = ref([])
|
||||
function openState() {
|
||||
chooseStateRef.value?.init(stateList.value, addressStore.addressInfo.state)
|
||||
}
|
||||
function chooseStateConfirm(data: any) {
|
||||
addressStore.addressInfo.state = data.id
|
||||
addressStore.addressInfo.stateName = data.name
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<navbar :title="t('pages.address.titleDetail')" />
|
||||
<!-- 地址搜索结果名字 -->
|
||||
<view class="px-30rpx pt-38rpx border-bottom pb-52rpx">
|
||||
<view class="text-32rpx lh-32rpx text-#333 mb-62rpx">
|
||||
{{ addressStore.addressInfo.displayName }}
|
||||
{{ addressStore.addressInfo.formattedAddress }}
|
||||
</view>
|
||||
|
||||
<!-- State -->
|
||||
<view class="mb-44rpx">
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('common.state') }}</view>
|
||||
<view @click="openState" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
|
||||
<text class="text-30rpx lh-30rpx text-#333">
|
||||
{{ addressStore.addressInfo.stateName ? addressStore.addressInfo.stateName : t('common.placeholder.pleaseSelect') }}
|
||||
</text>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 房屋类型 -->
|
||||
<view>
|
||||
<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}`)}` }}
|
||||
</text>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Corporate Name (required) -->
|
||||
<view class="mt-44rpx">
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.office.titleCorporate') }}</view>
|
||||
<view class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex items-center">
|
||||
<wd-input
|
||||
v-model.trim="addressStore.addressInfo.companyName"
|
||||
:cursorSpacing="20"
|
||||
:focus-when-clear="false"
|
||||
:maxlength="20"
|
||||
:placeholder="t('pages.address.office.titleCorporateTips')"
|
||||
clearable
|
||||
custom-class="flex-1 !text-30rpx lh-30rpx !text-#333 !bg-transparent"
|
||||
no-border
|
||||
placeholderStyle="font-size: 30rpx;color: #999;"
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
</view>
|
||||
<!-- Suite/Floor -->
|
||||
<view class="mt-44rpx">
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.office.titleSuite') }}</view>
|
||||
<view class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex items-center">
|
||||
<wd-input
|
||||
v-model.trim="addressStore.addressInfo.officeFloor"
|
||||
:cursorSpacing="20"
|
||||
:focus-when-clear="false"
|
||||
:maxlength="20"
|
||||
:placeholder="t('pages.address.office.titleSuiteTips')"
|
||||
clearable
|
||||
custom-class="flex-1 !text-30rpx lh-30rpx !text-#333 !bg-transparent"
|
||||
no-border
|
||||
placeholderStyle="font-size: 30rpx;color: #999;"
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="px-30rpx py-52rpx">
|
||||
<!-- Delivery Point Information -->
|
||||
<view class="mb-44rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.deliveryPointInfo') }}</view>
|
||||
|
||||
<view @click="openVisitMethod" class="flex-center-sb">
|
||||
<view>
|
||||
<view class="text-32rpx lh-32rpx text-#333 mb-12rpx">{{ visitMethod }}</view>
|
||||
<view class="text-32rpx lh-32rpx text-#999">{{ t('pages.address.moreOptions') }}</view>
|
||||
</view>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0"></image>
|
||||
</view>
|
||||
|
||||
<view class="mt-52rpx">
|
||||
<view class="text-36rpx lh-36rpx text-#333 font-500 mb-24rpx">{{ t('pages.address.deliveryInstructions') }}</view>
|
||||
<view
|
||||
class="min-h-240rpx box-border bg-#F6F6F6 rounded-20rpx overflow-hidden p-20rpx"
|
||||
>
|
||||
<wd-textarea
|
||||
:maxlength="250"
|
||||
custom-class="!bg-#F6F6F6"
|
||||
custom-textarea-container-class="!bg-#F6F6F6"
|
||||
no-border
|
||||
auto-height
|
||||
v-model="addressStore.addressInfo.deliveryRemark"
|
||||
:placeholder="t('pages.address.pleaseTip')"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mt-24rpx relative w-240rpx h-240rpx" @click="chooseImage">
|
||||
<image
|
||||
v-if="images"
|
||||
class="absolute top--10rpx right--10rpx z-1 w-36rpx h-36rpx"
|
||||
src="@img/chef/113.png"
|
||||
@click.stop="removeImage"
|
||||
></image>
|
||||
<view v-if="!images" class="flex flex-col items-center justify-center w-240rpx h-240rpx bg-#f6f6f6 rounded-20rpx">
|
||||
<wd-icon name="add" size="32px" color="#3d3d3d"></wd-icon>
|
||||
<text class="text-30rpx lh-30rpx text-#333 mt-22rpx">{{ t('common.addPicture') }}</text>
|
||||
</view>
|
||||
<image
|
||||
v-else
|
||||
:src="images"
|
||||
class="w-240rpx h-240rpx rounded-20rpx"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<fixed-bottom-large-btn
|
||||
class="z-100"
|
||||
fixed
|
||||
:text="t('common.saveAndContinue')"
|
||||
@click="saveAddress"
|
||||
/>
|
||||
|
||||
<visit-method @confirm="visitMethodConfirm" ref="visitMethodRef" />
|
||||
<!-- 房屋类型 -->
|
||||
<building-type ref="buildingTypeRef" @submit="submitType" />
|
||||
|
||||
<ChooseImage ref="chooseImageRef" @change="onImageChange"/>
|
||||
<!-- 所在州 -->
|
||||
<choose-state ref="chooseStateRef" @confirm="chooseStateConfirm" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.wd-textarea) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,295 @@
|
||||
<script setup lang="ts">
|
||||
import ChooseImage from "@/components/choose-image/choose-image.vue";
|
||||
import chooseState from "../components/choose-state.vue";
|
||||
import { getStateListApi } from '@/pages-user/service'
|
||||
|
||||
const { t } = useI18n()
|
||||
import VisitMethod from '@/components/visit-method/index.vue'
|
||||
import BuildingType from './components/building-type.vue'
|
||||
import {useAddressStore} from "../store/address";
|
||||
import {appUserAddressAddPost, appUserAddressEditPost} from "@/service";
|
||||
import {UserAddressType} from "@/constant/enums";
|
||||
const addressStore = useAddressStore()
|
||||
|
||||
const visitMethodRef = ref<InstanceType<typeof VisitMethod>>()
|
||||
const buildingTypeRef = ref<InstanceType<typeof BuildingType>>()
|
||||
function openVisitMethod() {
|
||||
visitMethodRef.value?.onOpen()
|
||||
}
|
||||
|
||||
const visitMethod = ref(t('components.visit.leaveItToMePersonally')) // 默认选择亲自送达
|
||||
function visitMethodConfirm(data: any) {
|
||||
console.log('visitMethodConfirm', data)
|
||||
visitMethod.value = data.label;
|
||||
addressStore.addressInfo.deliveryType = data.value + 1; // 更新地址信息中的送达方式
|
||||
}
|
||||
|
||||
function openBuildingType() {
|
||||
buildingTypeRef.value?.onOpen()
|
||||
}
|
||||
|
||||
|
||||
// 选择图片 - 添加安全检查
|
||||
const chooseImageRef = ref<InstanceType<typeof ChooseImage>>()
|
||||
const images = ref<string>('')
|
||||
function chooseImage() {
|
||||
if (chooseImageRef.value?.init) {
|
||||
chooseImageRef.value.init();
|
||||
}
|
||||
}
|
||||
|
||||
// 图片选择回调 - 修复类型问题
|
||||
function onImageChange(files: string[]) {
|
||||
if (files.length > 0) {
|
||||
// 确保 images 数组不为空
|
||||
images.value = files[0]; // 只取第一个图片
|
||||
} else {
|
||||
images.value = ''; // 如果没有选择图片,清空
|
||||
}
|
||||
}
|
||||
|
||||
function removeImage() {
|
||||
images.value = ''
|
||||
}
|
||||
|
||||
function saveAddress() {
|
||||
console.log('保存地址信息', addressStore.addressInfo);
|
||||
if(addressStore.addressInfo.id) {
|
||||
appUserAddressEditPost({
|
||||
body: {
|
||||
...addressStore.addressInfo,
|
||||
deliveryImage: images.value
|
||||
}
|
||||
}).then(res=> {
|
||||
uni.navigateBack()
|
||||
addressStore.clearAddressInfo()
|
||||
})
|
||||
} else {
|
||||
appUserAddressAddPost({
|
||||
body: {
|
||||
...addressStore.addressInfo,
|
||||
deliveryImage: images.value
|
||||
}
|
||||
}).then(res=> {
|
||||
uni.navigateBack()
|
||||
addressStore.clearAddressInfo()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(()=> {
|
||||
isSwitch.value = false
|
||||
if(addressStore.addressInfo.deliveryImage) {
|
||||
images.value = addressStore.addressInfo.deliveryImage
|
||||
}
|
||||
// 回显送达偏好
|
||||
if(+addressStore.addressInfo.deliveryType === 1) {
|
||||
visitMethod.value = t('components.visit.leaveItToMePersonally')
|
||||
}
|
||||
if(+addressStore.addressInfo.deliveryType === 2) {
|
||||
visitMethod.value = t('components.visit.putItAtTheDoor')
|
||||
}
|
||||
|
||||
getStateListApi().then(res=> {
|
||||
stateList.value = res.data
|
||||
if(addressStore.addressInfo.state) {
|
||||
addressStore.addressInfo.stateName = res.data.find(item => item.id === addressStore.addressInfo.state)?.name || ''
|
||||
}
|
||||
})
|
||||
})
|
||||
onUnload(()=> {
|
||||
if(!isSwitch.value) {
|
||||
addressStore.clearAddressInfo()
|
||||
}
|
||||
})
|
||||
|
||||
const isSwitch = ref(false)
|
||||
function submitType(value: string) {
|
||||
addressStore.addressInfo.type = value
|
||||
isSwitch.value = true
|
||||
switch (value) {
|
||||
case UserAddressType.HOUSE:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/house',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.APARTMENT:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/apartment',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.OFFICE:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/office',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.HOTEL:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/hotel',
|
||||
})
|
||||
break;
|
||||
case UserAddressType.OTHER:
|
||||
uni.redirectTo({
|
||||
url: '/pages/address/save-address/other',
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
const chooseStateRef = ref()
|
||||
const stateList = ref([])
|
||||
function openState() {
|
||||
chooseStateRef.value?.init(stateList.value, addressStore.addressInfo.state)
|
||||
}
|
||||
function chooseStateConfirm(data: any) {
|
||||
addressStore.addressInfo.state = data.id
|
||||
addressStore.addressInfo.stateName = data.name
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<navbar :title="t('pages.address.titleDetail')" />
|
||||
<!-- 地址搜索结果名字 -->
|
||||
<view class="px-30rpx pt-38rpx border-bottom pb-52rpx">
|
||||
<view class="text-32rpx lh-32rpx text-#333 mb-62rpx">
|
||||
{{ addressStore.addressInfo.displayName }}
|
||||
{{ addressStore.addressInfo.formattedAddress }}
|
||||
</view>
|
||||
|
||||
<!-- State -->
|
||||
<view class="mb-44rpx">
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('common.state') }}</view>
|
||||
<view @click="openState" class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex-center-sb">
|
||||
<text class="text-30rpx lh-30rpx text-#333">
|
||||
{{ addressStore.addressInfo.stateName ? addressStore.addressInfo.stateName : t('common.placeholder.pleaseSelect') }}
|
||||
</text>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 房屋类型 -->
|
||||
<view>
|
||||
<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}`)}` }}
|
||||
</text>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0 rotate-90"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Hotel Name (required) -->
|
||||
<view class="mt-44rpx">
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.other.titleApartment') }}</view>
|
||||
<view class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex items-center">
|
||||
<wd-input
|
||||
v-model.trim="addressStore.addressInfo.hotelName"
|
||||
:cursorSpacing="20"
|
||||
:focus-when-clear="false"
|
||||
:maxlength="20"
|
||||
:placeholder="t('pages.address.other.titleApartmentTips')"
|
||||
clearable
|
||||
custom-class="flex-1 !text-30rpx lh-30rpx !text-#333 !bg-transparent"
|
||||
no-border
|
||||
placeholderStyle="font-size: 30rpx;color: #999;"
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
</view>
|
||||
<!-- Room/Floor -->
|
||||
<view class="mt-44rpx">
|
||||
<view class="mb-20rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.other.titleCompany') }}</view>
|
||||
<view class="bg-#F6F6F6 rounded-16rpx px-24rpx h-98rpx flex items-center">
|
||||
<wd-input
|
||||
v-model.trim="addressStore.addressInfo.hotelRoom"
|
||||
:cursorSpacing="20"
|
||||
:focus-when-clear="false"
|
||||
:maxlength="20"
|
||||
:placeholder="t('pages.address.other.titleCompanyTips')"
|
||||
clearable
|
||||
custom-class="flex-1 !text-30rpx lh-30rpx !text-#333 !bg-transparent"
|
||||
no-border
|
||||
placeholderStyle="font-size: 30rpx;color: #999;"
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="px-30rpx py-52rpx">
|
||||
<!-- Delivery Point Information -->
|
||||
<view class="mb-44rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('pages.address.deliveryPointInfo') }}</view>
|
||||
|
||||
<view @click="openVisitMethod" class="flex-center-sb">
|
||||
<view>
|
||||
<view class="text-32rpx lh-32rpx text-#333 mb-12rpx">{{ visitMethod }}</view>
|
||||
<view class="text-32rpx lh-32rpx text-#999">{{ t('pages.address.moreOptions') }}</view>
|
||||
</view>
|
||||
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0"></image>
|
||||
</view>
|
||||
|
||||
<view class="mt-52rpx">
|
||||
<view class="text-36rpx lh-36rpx text-#333 font-500 mb-24rpx">{{ t('pages.address.deliveryInstructions') }}</view>
|
||||
<view
|
||||
class="min-h-240rpx box-border bg-#F6F6F6 rounded-20rpx overflow-hidden p-20rpx"
|
||||
>
|
||||
<wd-textarea
|
||||
:maxlength="250"
|
||||
custom-class="!bg-#F6F6F6"
|
||||
custom-textarea-container-class="!bg-#F6F6F6"
|
||||
no-border
|
||||
auto-height
|
||||
v-model="addressStore.addressInfo.deliveryRemark"
|
||||
:placeholder="t('pages.address.pleaseTip')"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mt-24rpx relative w-240rpx h-240rpx" @click="chooseImage">
|
||||
<image
|
||||
v-if="images"
|
||||
class="absolute top--10rpx right--10rpx z-1 w-36rpx h-36rpx"
|
||||
src="@img/chef/113.png"
|
||||
@click.stop="removeImage"
|
||||
></image>
|
||||
<view v-if="!images" class="flex flex-col items-center justify-center w-240rpx h-240rpx bg-#f6f6f6 rounded-20rpx">
|
||||
<wd-icon name="add" size="32px" color="#3d3d3d"></wd-icon>
|
||||
<text class="text-30rpx lh-30rpx text-#333 mt-22rpx">{{ t('common.addPicture') }}</text>
|
||||
</view>
|
||||
<image
|
||||
v-else
|
||||
:src="images"
|
||||
class="w-240rpx h-240rpx rounded-20rpx"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<fixed-bottom-large-btn
|
||||
class="z-100"
|
||||
fixed
|
||||
:text="t('common.saveAndContinue')"
|
||||
@click="saveAddress"
|
||||
/>
|
||||
|
||||
<visit-method @confirm="visitMethodConfirm" ref="visitMethodRef" />
|
||||
<!-- 房屋类型 -->
|
||||
<building-type ref="buildingTypeRef" @submit="submitType" />
|
||||
|
||||
<ChooseImage ref="chooseImageRef" @change="onImageChange"/>
|
||||
<!-- 所在州 -->
|
||||
<choose-state ref="chooseStateRef" @confirm="chooseStateConfirm" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.wd-textarea) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n();
|
||||
const keyword = ref('');
|
||||
function handleSearch(){
|
||||
console.log("1111111111");
|
||||
|
||||
}
|
||||
function handleClickItem(){
|
||||
uni.navigateTo({
|
||||
url: '/pages/address/choose-type'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<z-paging>
|
||||
<template #top>
|
||||
<navbar :title="t('pages.address.title')" />
|
||||
<view class="px-30rpx mt-32rpx">
|
||||
<view
|
||||
class="px-36rpx h-88rpx bg-#F2F3F6 flex items-center rounded-44rpx "
|
||||
>
|
||||
<image src="@img/chef/100222.png" class="w-28rpx h-28rpx mr-18rpx"></image>
|
||||
<wd-input
|
||||
no-border
|
||||
clearable
|
||||
:focus-when-clear="false"
|
||||
:focus="true"
|
||||
confirm-type="search"
|
||||
use-prefix-slot
|
||||
custom-class="flex items-center !text-30rpx !bg-transparent flex-1"
|
||||
placeholderStyle="font-size: 30rpx;color: #6D6D6D; font-weight: 500;"
|
||||
:modelValue="keyword"
|
||||
:placeholder="t('components.search.placeholder')"
|
||||
@confirm="handleSearch"
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<view class="pl-30rpx mt-24rpx">
|
||||
<template v-for="item in 6">
|
||||
<view @click="handleClickItem" class="flex items-center">
|
||||
<image src="@img/chef/145.png" class="w-44rpx h-44rpx shrink-0 mr-28rpx"></image>
|
||||
<view class="flex-1 h-156rpx border-bottom pt-40rpx">
|
||||
<view class="text-32rpx lh-32rpx text-#333 font-500 mb-16rpx">818 Avenue du Mont-Royal E</view>
|
||||
<view class="text-28rpx lh-28rpx text-#6D6D6D">Montreal,QC</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</z-paging>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,108 @@
|
||||
import { defineStore } from "pinia";
|
||||
import type { UserAddressBo } from '@/service/types';
|
||||
export const useAddressStore = defineStore('store-address', () => {
|
||||
const addressInfo = ref<UserAddressBo>({
|
||||
type: '',
|
||||
/** 配送类型 1 2 */
|
||||
deliveryType: '1',
|
||||
/** 配送说明 */
|
||||
deliveryRemark: '',
|
||||
/** 配送图片 */
|
||||
deliveryImage: '',
|
||||
/** 门牌号(住宅专用) */
|
||||
houseNumber: '',
|
||||
/** 其他信息(住宅专用) */
|
||||
houseExtra: '',
|
||||
/** 楼宇名称(公寓专用) */
|
||||
buildingName: '',
|
||||
/** 公寓单元楼层(公寓专用) */
|
||||
apartmentUnitFloor: '',
|
||||
/** 入口代码(公寓专用) */
|
||||
entranceCodeApartment: '',
|
||||
/** 公司名称(公司专用) */
|
||||
companyName: '',
|
||||
/** 办公楼楼层(公司专用) */
|
||||
officeFloor: '',
|
||||
/** 酒店名称(酒店专用) */
|
||||
hotelName: '',
|
||||
/** 酒店房间号(酒店专用) */
|
||||
hotelRoom: '',
|
||||
/** 入口代码(酒店专用) */
|
||||
entranceCodeHotel: '',
|
||||
/** 公寓/套房/楼层(其他专用) */
|
||||
otherLocation: '',
|
||||
/** 公司/楼宇名称(其他专用) */
|
||||
otherBuilding: '',
|
||||
longitude: '',
|
||||
latitude: '',
|
||||
displayName: '',
|
||||
formattedAddress: '',
|
||||
state: '', // 州信息
|
||||
stateName: '', // 州名称
|
||||
})
|
||||
|
||||
// 地图经纬度信息
|
||||
const addressLocation = ref({
|
||||
location: '',
|
||||
longitude: '',
|
||||
latitude: '',
|
||||
});
|
||||
|
||||
function setAddressLocation(data: any) {
|
||||
addressLocation.value = data
|
||||
addressInfo.value.longitude = data.longitude
|
||||
addressInfo.value.latitude = data.latitude
|
||||
addressInfo.value.displayName = data.displayName
|
||||
addressInfo.value.formattedAddress = data.formattedAddress
|
||||
}
|
||||
|
||||
function clearAddressInfo() {
|
||||
addressInfo.value = {
|
||||
id: '',
|
||||
type: '',
|
||||
/** 配送类型 1 2 */
|
||||
deliveryType: '1',
|
||||
/** 配送说明 */
|
||||
deliveryRemark: '',
|
||||
/** 配送图片 */
|
||||
deliveryImage: '',
|
||||
/** 门牌号(住宅专用) */
|
||||
houseNumber: '',
|
||||
/** 其他信息(住宅专用) */
|
||||
houseExtra: '',
|
||||
/** 楼宇名称(公寓专用) */
|
||||
buildingName: '',
|
||||
/** 公寓单元楼层(公寓专用) */
|
||||
apartmentUnitFloor: '',
|
||||
/** 入口代码(公寓专用) */
|
||||
entranceCodeApartment: '',
|
||||
/** 公司名称(公司专用) */
|
||||
companyName: '',
|
||||
/** 办公楼楼层(公司专用) */
|
||||
officeFloor: '',
|
||||
/** 酒店名称(酒店专用) */
|
||||
hotelName: '',
|
||||
/** 酒店房间号(酒店专用) */
|
||||
hotelRoom: '',
|
||||
/** 入口代码(酒店专用) */
|
||||
entranceCodeHotel: '',
|
||||
/** 公寓/套房/楼层(其他专用) */
|
||||
otherLocation: '',
|
||||
/** 公司/楼宇名称(其他专用) */
|
||||
otherBuilding: '',
|
||||
longitude: '',
|
||||
latitude: '',
|
||||
displayName: '',
|
||||
formattedAddress: '',
|
||||
state: '', // 州信息
|
||||
stateName: '', // 州名称
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
addressInfo,
|
||||
addressLocation,
|
||||
setAddressLocation,
|
||||
clearAddressInfo
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import { appAgreementGetByCodePost } from '@/service'
|
||||
|
||||
const props = defineProps<{
|
||||
code?: string
|
||||
}>()
|
||||
|
||||
|
||||
const detailInfo = ref<Partial<Agreement>>({})
|
||||
|
||||
async function initData() {
|
||||
if (props.code) {
|
||||
try {
|
||||
const res = await appAgreementGetByCodePost({
|
||||
body: {
|
||||
code: props.code
|
||||
}
|
||||
})
|
||||
console.log('res', res)
|
||||
|
||||
detailInfo.value = res.data || {}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="">
|
||||
<navbar/>
|
||||
<view class="p-[20rpx+30rpx]">
|
||||
<mp-html
|
||||
selectable
|
||||
:preview-img="false"
|
||||
:show-img-menu="false"
|
||||
:tag-style="{
|
||||
div: 'white-space: pre-wrap;',
|
||||
p: 'white-space: pre-wrap;',
|
||||
img: 'width:100%;max-width: 100%;height:auto;',
|
||||
}"
|
||||
:content="(detailInfo.content)"
|
||||
></mp-html>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<view class="browse-skeleton">
|
||||
<!-- 头部标题区域 -->
|
||||
<view class="header-section">
|
||||
<view class="title-skeleton skeleton-item"></view>
|
||||
<view class="header-actions">
|
||||
<view class="action-icon-skeleton skeleton-item"></view>
|
||||
<view class="action-icon-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-section">
|
||||
<view class="search-bar-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
|
||||
<!-- 精选食谱区域 -->
|
||||
<view class="selected-recipes-section">
|
||||
<view class="section-header">
|
||||
<view class="section-title-skeleton skeleton-item"></view>
|
||||
<view class="section-action-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
<view class="recipes-grid">
|
||||
<view
|
||||
v-for="i in 3"
|
||||
:key="i"
|
||||
class="recipe-card-skeleton skeleton-item"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 附近美食区域 -->
|
||||
<view class="nearby-cuisine-section">
|
||||
<view class="section-title-skeleton skeleton-item"></view>
|
||||
<view class="cuisine-grid">
|
||||
<view
|
||||
v-for="i in 8"
|
||||
:key="i"
|
||||
class="cuisine-card-skeleton 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-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
// 闪烁动画
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.browse-skeleton {
|
||||
background-color: #fff;
|
||||
min-height: 100vh;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
// 头部区域
|
||||
.header-section {
|
||||
padding: 16rpx 0 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.title-skeleton {
|
||||
width: 200rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
|
||||
.action-icon-skeleton {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索栏
|
||||
.search-section {
|
||||
padding: 48rpx 0 0;
|
||||
|
||||
.search-bar-skeleton {
|
||||
width: 690rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 精选食谱区域
|
||||
.selected-recipes-section {
|
||||
padding: 56rpx 0 0;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
.section-title-skeleton {
|
||||
width: 292rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.section-action-skeleton {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.recipes-grid {
|
||||
display: flex;
|
||||
gap: 30rpx;
|
||||
overflow-x: auto;
|
||||
|
||||
.recipe-card-skeleton {
|
||||
flex-shrink: 0;
|
||||
width: 310rpx;
|
||||
height: 296rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 附近美食区域
|
||||
.nearby-cuisine-section {
|
||||
padding: 62rpx 0 0;
|
||||
|
||||
.section-title-skeleton {
|
||||
width: 252rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.cuisine-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30rpx;
|
||||
|
||||
.cuisine-card-skeleton {
|
||||
height: 186rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 750rpx) {
|
||||
.recipes-grid {
|
||||
padding-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.cuisine-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,204 @@
|
||||
<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 { t } = useI18n();
|
||||
|
||||
function navigateTo(url: string) {
|
||||
if(userStore.checkLogin()) {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleNotOpen() {
|
||||
emit("toggleNotOpen");
|
||||
}
|
||||
|
||||
async function initData() {
|
||||
if(!recipeData.value) {
|
||||
loading.value = true;
|
||||
}
|
||||
getRecipeData()
|
||||
// 获取菜品数据
|
||||
appMerchantDishNearbyList()
|
||||
}
|
||||
|
||||
// 获取菜谱数据
|
||||
const recipeData = ref([]);
|
||||
function getRecipeData() {
|
||||
appSearchSearchRecipePost({
|
||||
body: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
}
|
||||
}).then(res=> {
|
||||
console.log('菜谱数据', res)
|
||||
recipeData.value = res.rows;
|
||||
}).finally(()=> {
|
||||
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}`)
|
||||
}
|
||||
|
||||
// 获取附近的菜品
|
||||
const dishData = ref([]);
|
||||
function appMerchantDishNearbyList() {
|
||||
appMerchantDishNearbyListPost({
|
||||
params: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
body: {
|
||||
lat: userStore.userLocation.latitude,
|
||||
lng: userStore.userLocation.longitude,
|
||||
}
|
||||
}).then(res=> {
|
||||
console.log('菜品数据', res)
|
||||
dishData.value = res.rows;
|
||||
})
|
||||
}
|
||||
function handleClickDish(item: any) {
|
||||
navigateTo(`/pages-store/pages/store/index?id=${item.merchantId}`)
|
||||
}
|
||||
|
||||
async function getPlatformDefaultStoreInfo() {}
|
||||
|
||||
defineExpose({
|
||||
initData,
|
||||
init: getPlatformDefaultStoreInfo,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
class="bg-#fff"
|
||||
:style="[
|
||||
{
|
||||
height: configStore.windowHeight + 'px',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<z-paging ref="paging">
|
||||
<template #top>
|
||||
<status-bar />
|
||||
</template>
|
||||
<view
|
||||
v-show="loading"
|
||||
class="animate-in fade-in animate-ease-out animate-duration-300"
|
||||
>
|
||||
<browse-skeleton />
|
||||
</view>
|
||||
<view
|
||||
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">
|
||||
<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">
|
||||
<image
|
||||
:src="thumbnailImg(item?.recipeImage?.split(',')[0])"
|
||||
class="w-310rpx h-296rpx rounded-24rpx mb-26rpx bg-common"
|
||||
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>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</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>
|
||||
|
||||
<template #bottom>
|
||||
<view class="h-50px"></view>
|
||||
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
|
||||
</template>
|
||||
</z-paging>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<view class="class-bullet-container">
|
||||
<!-- 第一行(可拖动 + 自动滚动) -->
|
||||
<view class="scroll-row first-row">
|
||||
<scroll-view
|
||||
class="ab-scroll mb-22rpx"
|
||||
scroll-x
|
||||
show-scrollbar="false"
|
||||
id="sv-1"
|
||||
style="--ab-scroll-timeout: 50s"
|
||||
:scroll-left="scrollLeft1"
|
||||
@touchstart="onTouchStart1"
|
||||
@touchend="onTouchEnd1"
|
||||
@touchcancel="onTouchEnd1"
|
||||
@scroll="onScroll1"
|
||||
>
|
||||
<view id="sv-inner-1" :class="['sv-inner', { 'paused': isDragging1 }]">
|
||||
<!-- 一份内容 -->
|
||||
<view
|
||||
v-for="(item, idx) in categories"
|
||||
:key="item.id + '-a-' + idx"
|
||||
class="category-item"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<image :src="item.logoUrl" class="category-icon" mode="aspectFit" />
|
||||
<text class="category-text">{{ item.name }}</text>
|
||||
</view>
|
||||
<!-- 第二份重复内容(用于无缝循环) -->
|
||||
<view
|
||||
v-for="(item, idx) in categories"
|
||||
:key="item.id + '-b-' + idx"
|
||||
class="category-item"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<image :src="item.logoUrl" class="category-icon" mode="aspectFit" />
|
||||
<text class="category-text">{{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 第二行(可拖动 + 自动滚动,速度不同) -->
|
||||
<view class="scroll-row">
|
||||
<scroll-view
|
||||
class="ab-scroll"
|
||||
scroll-x
|
||||
show-scrollbar="false"
|
||||
id="sv-2"
|
||||
style="--ab-scroll-timeout: 70s"
|
||||
:scroll-left="scrollLeft2"
|
||||
@touchstart="onTouchStart2"
|
||||
@touchend="onTouchEnd2"
|
||||
@touchcancel="onTouchEnd2"
|
||||
@scroll="onScroll2"
|
||||
>
|
||||
<view id="sv-inner-2" :class="['sv-inner', { 'paused': isDragging2 }]">
|
||||
<view
|
||||
v-for="(item, idx) in categoriesReversed"
|
||||
:key="item.id + '-c-' + idx"
|
||||
class="category-item"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<image :src="item.logoUrl" class="category-icon" mode="aspectFit" />
|
||||
<text class="category-text">{{ item.name }}</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="(item, idx) in categoriesReversed"
|
||||
:key="item.id + '-d-' + idx"
|
||||
class="category-item"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<image :src="item.logoUrl" class="category-icon" mode="aspectFit" />
|
||||
<text class="category-text">{{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, nextTick, getCurrentInstance } from 'vue'
|
||||
|
||||
// 定义分类项接口(与模板字段一致)
|
||||
interface CategoryItem {
|
||||
id: number
|
||||
logoUrl: string
|
||||
name: string
|
||||
}
|
||||
|
||||
// 定义组件属性
|
||||
interface Props {
|
||||
/** 分类数据数组 */
|
||||
categories?: CategoryItem[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
categories: () => []
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
/** 点击分类项事件 */
|
||||
'itemClick': [item: CategoryItem]
|
||||
}>()
|
||||
|
||||
// 模拟数据
|
||||
const mockCategories: CategoryItem[] = [
|
||||
{ id: 1, logoUrl: '/src/static/images/ls/6.png', name: '外卖点餐' },
|
||||
{ id: 2, logoUrl: '/src/static/images/ls/7.png', name: '净菜商城' },
|
||||
{ id: 3, logoUrl: '/src/static/images/ls/8.png', name: '生鲜冷链' },
|
||||
{ id: 4, logoUrl: '/src/static/images/ls/9.png', name: '肉类' },
|
||||
{ id: 5, logoUrl: '/src/static/images/ls/10.png', name: '调料' },
|
||||
{ id: 6, logoUrl: '/src/static/images/ls/11.png', name: '零食' },
|
||||
{ id: 7, logoUrl: '/src/static/images/ls/12.png', name: '海鲜' }
|
||||
]
|
||||
|
||||
// 使用传入的数据或默认数据
|
||||
const categories = computed(() => {
|
||||
return props.categories.length > 0 ? props.categories : mockCategories
|
||||
})
|
||||
// 第二行使用反向顺序以形成与第一行相反的视觉效果
|
||||
const categoriesReversed = computed(() => {
|
||||
const list = categories.value || []
|
||||
return list.slice().reverse()
|
||||
})
|
||||
|
||||
// 自动滚动(CSS 动画)+ 手动拖动时暂停
|
||||
const isDragging1 = ref(false)
|
||||
const isDragging2 = ref(false)
|
||||
const RESUME_DELAY_MS = 1200
|
||||
let resumeTimer1: any
|
||||
let resumeTimer2: any
|
||||
|
||||
// 中心定位与边界重置,避免滚动到底卡住
|
||||
const scrollLeft1 = ref(0)
|
||||
const scrollLeft2 = ref(0)
|
||||
const copyWidth1Px = ref(0)
|
||||
const copyWidth2Px = ref(0)
|
||||
const viewportWidth1Px = ref(0)
|
||||
const viewportWidth2Px = ref(0)
|
||||
|
||||
function measureAndInit() {
|
||||
const ins = getCurrentInstance()
|
||||
const q = uni.createSelectorQuery().in(ins?.proxy as any)
|
||||
q.select('#sv-1').boundingClientRect()
|
||||
.select('#sv-inner-1').boundingClientRect()
|
||||
.select('#sv-2').boundingClientRect()
|
||||
.select('#sv-inner-2').boundingClientRect()
|
||||
.exec((res) => {
|
||||
const sv1 = res?.[0]
|
||||
const inner1 = res?.[1]
|
||||
const sv2 = res?.[2]
|
||||
const inner2 = res?.[3]
|
||||
if (sv1?.width) viewportWidth1Px.value = sv1.width
|
||||
if (inner1?.width) copyWidth1Px.value = inner1.width / 2
|
||||
if (sv2?.width) viewportWidth2Px.value = sv2.width
|
||||
if (inner2?.width) copyWidth2Px.value = inner2.width / 2
|
||||
// 初始化到中间位置
|
||||
if (copyWidth1Px.value) scrollLeft1.value = copyWidth1Px.value
|
||||
if (copyWidth2Px.value) scrollLeft2.value = copyWidth2Px.value
|
||||
})
|
||||
}
|
||||
|
||||
function recenter1(currentLeft?: number) {
|
||||
const cw = copyWidth1Px.value
|
||||
const vw = viewportWidth1Px.value
|
||||
if (!cw || !vw) return
|
||||
const left = currentLeft ?? scrollLeft1.value
|
||||
const mod = left % cw
|
||||
scrollLeft1.value = cw + mod
|
||||
}
|
||||
|
||||
function recenter2(currentLeft?: number) {
|
||||
const cw = copyWidth2Px.value
|
||||
const vw = viewportWidth2Px.value
|
||||
if (!cw || !vw) return
|
||||
const left = currentLeft ?? scrollLeft2.value
|
||||
const mod = left % cw
|
||||
scrollLeft2.value = cw + mod
|
||||
}
|
||||
|
||||
function onTouchStart1() {
|
||||
isDragging1.value = true
|
||||
if (resumeTimer1) clearTimeout(resumeTimer1)
|
||||
}
|
||||
function onTouchEnd1() {
|
||||
// 手指松开后,若在边界附近,立即重置到中间位置,保持无缝
|
||||
recenter1(scrollLeft1.value)
|
||||
resumeTimer1 = setTimeout(() => {
|
||||
isDragging1.value = false
|
||||
}, RESUME_DELAY_MS)
|
||||
}
|
||||
function onScroll1(e: any) {
|
||||
isDragging1.value = true
|
||||
if (resumeTimer1) clearTimeout(resumeTimer1)
|
||||
const left = e?.detail?.scrollLeft ?? 0
|
||||
const cw = copyWidth1Px.value
|
||||
const vw = viewportWidth1Px.value
|
||||
if (cw && vw) {
|
||||
const total = cw * 2
|
||||
// 临界阈值,靠近起点或终点时重置
|
||||
const threshold = 10
|
||||
if (left <= threshold || left >= total - vw - threshold) {
|
||||
recenter1(left)
|
||||
}
|
||||
}
|
||||
resumeTimer1 = setTimeout(() => {
|
||||
isDragging1.value = false
|
||||
}, RESUME_DELAY_MS)
|
||||
}
|
||||
function onTouchStart2() {
|
||||
isDragging2.value = true
|
||||
if (resumeTimer2) clearTimeout(resumeTimer2)
|
||||
}
|
||||
function onTouchEnd2() {
|
||||
recenter2(scrollLeft2.value)
|
||||
resumeTimer2 = setTimeout(() => {
|
||||
isDragging2.value = false
|
||||
}, RESUME_DELAY_MS)
|
||||
}
|
||||
function onScroll2(e: any) {
|
||||
isDragging2.value = true
|
||||
if (resumeTimer2) clearTimeout(resumeTimer2)
|
||||
const left = e?.detail?.scrollLeft ?? 0
|
||||
const cw = copyWidth2Px.value
|
||||
const vw = viewportWidth2Px.value
|
||||
if (cw && vw) {
|
||||
const total = cw * 2
|
||||
const threshold = 10
|
||||
if (left <= threshold || left >= total - vw - threshold) {
|
||||
recenter2(left)
|
||||
}
|
||||
}
|
||||
resumeTimer2 = setTimeout(() => {
|
||||
isDragging2.value = false
|
||||
}, RESUME_DELAY_MS)
|
||||
}
|
||||
|
||||
// 点击处理
|
||||
const handleItemClick = (item: CategoryItem) => {
|
||||
emit('itemClick', item)
|
||||
navigateTo('/pages-store/pages/list/index?id=' + item.id)
|
||||
}
|
||||
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
measureAndInit()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.class-bullet-container {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.scroll-row {
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ab-scroll {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sv-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 20rpx;
|
||||
animation: ab-move-marquee var(--ab-scroll-timeout) linear infinite;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 120rpx;
|
||||
height: 60rpx;
|
||||
padding: 0 16rpx;
|
||||
border: 1px solid #C8C8C8;
|
||||
border-radius: 30rpx;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:active {
|
||||
transform: translateY(0) scale(0.95);
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.category-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GPU 动画,无缝循环(两份内容)
|
||||
@keyframes ab-move-marquee {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.paused {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import {thumbnailImg} from "@/utils/utils";
|
||||
const props = defineProps<{
|
||||
list: object[];
|
||||
}>();
|
||||
const { t } = useI18n();
|
||||
|
||||
function handleClickFood(item: any) {
|
||||
uni.navigateTo({
|
||||
url: '/pages-store/pages/store/index?id=' + item.id
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<scroll-view scroll-x="true">
|
||||
<view class="flex">
|
||||
<view class="w-30rpx shrink-0"></view>
|
||||
<template v-for="(item, index) in list">
|
||||
<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">${{ item.deliveryFee }} {{ t('pages.home.deliveryFee') }}</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 }} {{ t('common.minutes') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<view class="w-30rpx shrink-0 op-0">1</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import Collection from "@/components/collection/index.vue";
|
||||
import {appCollectCollectPost} from "@/service";
|
||||
import {CollectionType} from "@/constant/enums";
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
item: object;
|
||||
}>();
|
||||
|
||||
function handleClickFood() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-store/pages/store/index?id=' + props.item.id
|
||||
})
|
||||
}
|
||||
|
||||
function handleCollectionChange(value: boolean) {
|
||||
appCollectCollectPost({
|
||||
body: {
|
||||
targetId: props.item.id,
|
||||
targetType: CollectionType.STORE
|
||||
}
|
||||
}).then(res=> {
|
||||
props.item.isCollect = value;
|
||||
}).catch(err=> {
|
||||
props.item.isCollect = !value;
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view @click="handleClickFood" class="mb-52rpx">
|
||||
<image
|
||||
:src="item?.shopImages?.split(',')[0]"
|
||||
mode="aspectFill"
|
||||
class="w-100% h-636rpx rounded-24rpx bg-common"
|
||||
></image>
|
||||
<view class="flex justify-between items-start mt-14rpx">
|
||||
<view>
|
||||
<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">${{ item.deliveryFee }} {{ t('pages.home.deliveryFee') }}</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 }}{{ t('common.minutes') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<collection :is-collected="item.isCollect" @collectionChange="handleCollectionChange" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<view class="home-skeleton">
|
||||
<!-- 头部区域 -->
|
||||
<view class="header-section">
|
||||
<view class="app-title-skeleton skeleton-item"></view>
|
||||
<view class="delivery-info-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
|
||||
<!-- 位置和通知区域 -->
|
||||
<view class="location-notification">
|
||||
<view class="location-skeleton skeleton-item"></view>
|
||||
<view class="notification-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-section">
|
||||
<view class="search-bar-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
|
||||
<!-- 分类滚动区域 -->
|
||||
<view class="category-section">
|
||||
<view class="category-list">
|
||||
<view
|
||||
v-for="i in 7"
|
||||
:key="i"
|
||||
class="category-item-skeleton skeleton-item"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 轮播图区域 -->
|
||||
<view class="swiper-section">
|
||||
<view class="swiper-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
|
||||
<!-- 食品类型分类 -->
|
||||
<view class="food-type-section">
|
||||
<view class="food-type-list">
|
||||
<view
|
||||
v-for="i in 5"
|
||||
:key="i"
|
||||
class="food-type-item-skeleton skeleton-item"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选工具 -->
|
||||
<view class="filter-section">
|
||||
<view class="filter-list">
|
||||
<view
|
||||
v-for="i in 4"
|
||||
:key="i"
|
||||
class="filter-item-skeleton skeleton-item"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Featured on ChefLink 区域 -->
|
||||
<view class="featured-section">
|
||||
<view class="section-title-skeleton skeleton-item"></view>
|
||||
<view class="featured-cards">
|
||||
<view class="featured-card-skeleton skeleton-item"></view>
|
||||
<view class="featured-card-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Nearby Merchants 区域 -->
|
||||
<view class="nearby-section">
|
||||
<view class="section-title-skeleton skeleton-item"></view>
|
||||
<view class="nearby-list">
|
||||
<view
|
||||
v-for="i in 4"
|
||||
:key="i"
|
||||
class="nearby-item-skeleton skeleton-item"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- All Merchants 区域 -->
|
||||
<view class="all-merchants-section">
|
||||
<view class="section-title-skeleton skeleton-item"></view>
|
||||
<view class="merchant-card-skeleton skeleton-item"></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-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
// 闪烁动画
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.home-skeleton {
|
||||
background-color: #fff;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
// 头部区域
|
||||
.header-section {
|
||||
padding: 18rpx 30rpx 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
|
||||
.app-title-skeleton {
|
||||
width: 241rpx;
|
||||
height: 52rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.delivery-info-skeleton {
|
||||
width: 266rpx;
|
||||
height: 28rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 位置和通知区域
|
||||
.location-notification {
|
||||
padding: 22rpx 30rpx 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.location-skeleton {
|
||||
width: 329rpx;
|
||||
height: 30rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.notification-skeleton {
|
||||
width: 132rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 30rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索栏
|
||||
.search-section {
|
||||
padding: 32rpx 30rpx 0;
|
||||
|
||||
.search-bar-skeleton {
|
||||
width: 690rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 分类滚动区域
|
||||
.category-section {
|
||||
padding: 40rpx 30rpx 22rpx;
|
||||
|
||||
.category-list {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
overflow-x: auto;
|
||||
|
||||
.category-item-skeleton {
|
||||
flex-shrink: 0;
|
||||
width: 214rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 33rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 轮播图区域
|
||||
.swiper-section {
|
||||
padding: 0 30rpx;
|
||||
|
||||
.swiper-skeleton {
|
||||
width: 690rpx;
|
||||
height: 420rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 食品类型分类
|
||||
.food-type-section {
|
||||
padding: 22rpx 30rpx;
|
||||
|
||||
.food-type-list {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
overflow-x: auto;
|
||||
|
||||
.food-type-item-skeleton {
|
||||
flex-shrink: 0;
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 56rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选工具
|
||||
.filter-section {
|
||||
padding: 32rpx 30rpx;
|
||||
|
||||
.filter-list {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
|
||||
.filter-item-skeleton {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Featured on ChefLink 区域
|
||||
.featured-section {
|
||||
padding: 56rpx 30rpx 0;
|
||||
|
||||
.section-title-skeleton {
|
||||
width: 300rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.featured-cards {
|
||||
display: flex;
|
||||
gap: 28rpx;
|
||||
|
||||
.featured-card-skeleton {
|
||||
flex: 1;
|
||||
height: 252rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nearby Merchants 区域
|
||||
.nearby-section {
|
||||
padding: 56rpx 30rpx 0;
|
||||
|
||||
.section-title-skeleton {
|
||||
width: 303rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.nearby-list {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
overflow-x: auto;
|
||||
|
||||
.nearby-item-skeleton {
|
||||
flex-shrink: 0;
|
||||
width: 156rpx;
|
||||
height: 156rpx;
|
||||
border-radius: 78rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All Merchants 区域
|
||||
.all-merchants-section {
|
||||
padding: 56rpx 30rpx 62rpx;
|
||||
|
||||
.section-title-skeleton {
|
||||
width: 224rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.merchant-card-skeleton {
|
||||
width: 690rpx;
|
||||
height: 766rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部导航栏
|
||||
.bottom-nav {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 108rpx;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
box-shadow: 0 -6rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 750rpx) {
|
||||
.category-list,
|
||||
.food-type-list,
|
||||
.nearby-list {
|
||||
padding-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.featured-cards {
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from "@/store";
|
||||
const emit = defineEmits(['toggleNotOpen']);
|
||||
const userStore = useUserStore();
|
||||
function navigateTo(url: string) {
|
||||
if(userStore.checkLogin()) {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
});
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<image @click="emit('toggleNotOpen')" src="@img/chef/115.png" class="w-40rpx h-40rpx"></image>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
list: object[];
|
||||
}>();
|
||||
const { t } = useI18n();
|
||||
|
||||
function handleClickFood(item: any) {
|
||||
uni.navigateTo({
|
||||
url: '/pages-store/pages/store/index?id=' + item.id
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<scroll-view :scroll-x="true">
|
||||
<view class="flex items-center">
|
||||
<view class="shrink-0 w-30rpx"></view>
|
||||
<template v-for="(item, index) in list" :key="index">
|
||||
<view @click="handleClickFood(item)" :class="[index === 0 ? '' : 'ml-42rpx']">
|
||||
<image
|
||||
class="w-156rpx h-156rpx rounded-50% bg-common"
|
||||
:src="item.logo"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view class="w-156rpx text-center line-clamp-1 text-#333 text-28rpx lh-28rpx mt-12rpx font-500">
|
||||
{{ item.merchantName }}
|
||||
</view>
|
||||
<view v-if="+item.deliveryService === 1" class="mt-12rpx text-center text-24rpx lh-24rpx text-#7D7D7D">{{ item.deliveryTime }}{{ t('common.minutes') }}</view>
|
||||
</view>
|
||||
</template>
|
||||
<view class="shrink-0 w-30rpx op-0">1</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n();
|
||||
const show = ref(false);
|
||||
|
||||
function onOpen() {
|
||||
show.value = true;
|
||||
}
|
||||
function handleClose() {
|
||||
show.value = false;
|
||||
}
|
||||
defineExpose({
|
||||
onOpen,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wd-popup
|
||||
v-model="show"
|
||||
position="center"
|
||||
@close="handleClose"
|
||||
custom-style="border-radius:20rpx;"
|
||||
>
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<view class="w-590rpx rounded-20rpx relative">
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-PLUS -->
|
||||
<view class="w-590rpx h-570rpx rounded-20rpx relative">
|
||||
<!-- #endif -->
|
||||
<view class="flex flex-col items-center mb-34rpx">
|
||||
<image src="@img/chef/100.png" class="w-318rpx h-318rpx"></image>
|
||||
<view class="text-40rpx lh-40rpx text-#333 font-500 mt--20rpx mb-22rpx">{{ t('pages.shop.tips') }}</view>
|
||||
<view class="text-28rpx text-#333 text-center px-40rpx tracking-[.04em]">
|
||||
{{ t('components.noOpen.title') }}
|
||||
</view>
|
||||
</view>
|
||||
<view @click="handleClose" class="absolute bottom-0 left-0 w-full border-top h-98rpx center text-30rpx text-#333">
|
||||
{{ t('common.gotIt') }}
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
isAutoJump: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(e: 'clickSearch'): void
|
||||
}>()
|
||||
|
||||
function handleClickSearch() {
|
||||
if(props.isAutoJump) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/search/index',
|
||||
})
|
||||
} else {
|
||||
emit('clickSearch')
|
||||
}
|
||||
}
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
currentId: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
labelKey: {
|
||||
type: String,
|
||||
default: 'name',
|
||||
},
|
||||
valueKey: {
|
||||
type: String,
|
||||
default: 'id',
|
||||
},
|
||||
imgKey: {
|
||||
type: String,
|
||||
default: 'logoUrl',
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['changeType']);
|
||||
watchEffect(() => {
|
||||
if (props.currentId) {
|
||||
selectedIndex.value = props.currentId;
|
||||
}
|
||||
});
|
||||
|
||||
const selectedIndex = ref();
|
||||
|
||||
function selectTab(item: any) {
|
||||
selectedIndex.value = item[props.valueKey];
|
||||
// 触发父组件事件
|
||||
console.log('selectTab', item[props.valueKey]);
|
||||
emit('changeType', item[props.valueKey]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<scroll-view :scroll-x="true">
|
||||
<view class="flex items-center">
|
||||
<view class="shrink-0 w-30rpx"></view>
|
||||
<template v-for="(item, index) in list" :key="index">
|
||||
<view
|
||||
:class="[index === 0 ? '' : 'ml-40rpx']"
|
||||
class="w-112rpx flex flex-col items-center"
|
||||
@click="selectTab(item)"
|
||||
>
|
||||
<view
|
||||
:class="['img-wrap', selectedIndex == item[props.valueKey] ? 'img-selected' : '']"
|
||||
>
|
||||
<image
|
||||
class="tab-img rounded-50% overflow-hidden bg-common"
|
||||
:src="item[props.imgKey]"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
<text
|
||||
:class="selectedIndex == item[props.valueKey] ? 'text-#CE7138' : 'text-#333'"
|
||||
class="line-clamp-1 text-20rpx lh-20rpx mt-12rpx font-500"
|
||||
>{{ item[props.labelKey] }}</text
|
||||
>
|
||||
</view>
|
||||
</template>
|
||||
<view class="shrink-0 w-30rpx op-0">1</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.img-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s;
|
||||
width: 132rpx;
|
||||
height: 132rpx;
|
||||
}
|
||||
.img-selected {
|
||||
border: 4rpx solid #ce7138;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tab-img {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,424 @@
|
||||
<script setup lang="ts">
|
||||
import useEventEmit from "@/hooks/useEventEmit";
|
||||
import {CollectionType, EventEnum} from "@/constant/enums";
|
||||
import { useConfigStore, useUserStore } from "@/store";
|
||||
import Config from '@/config/index'
|
||||
import { debounce } from 'throttle-debounce'
|
||||
import MsgBox from "@/pages/home/components/tabbar-home/components/msg-box.vue";
|
||||
import Search from "@/pages/home/components/tabbar-home/components/search.vue";
|
||||
import ClassBullet from "./components/class-bullet.vue";
|
||||
import TabsType from "./components/tabs-type.vue";
|
||||
import FeaturedOn from "./components/featured-on/index.vue";
|
||||
import FiltrateTool from "@/components/filtrate-tool/index.vue";
|
||||
import NearbyMerchants from "./components/nearby-merchants/index.vue";
|
||||
import FoodBox from "./components/food-box/index.vue";
|
||||
import HomeSkeleton from "./components/home-skeleton.vue";
|
||||
import {
|
||||
appMarketActivityListPost,
|
||||
appMerchantCartListMerchantPost,
|
||||
appMerchantCategoryListGet,
|
||||
appMerchantFeaturedListPost,
|
||||
appMerchantLabelListGet,
|
||||
appMerchantNearbyListPost, appMerchantRecommendListPost,
|
||||
appCollectCollectPost
|
||||
} from "@/service";
|
||||
import usePage from "@/hooks/usePage";
|
||||
import {getFeaturedDishList} from "@/pages-store/service";
|
||||
const configStore = useConfigStore();
|
||||
const userStore = useUserStore();
|
||||
const props = defineProps<{
|
||||
scoreRange?: string;
|
||||
price?: Array<string> | string;
|
||||
}>();
|
||||
const emit = defineEmits(["toggleScore", "togglePrice", "toggleNotOpen"]);
|
||||
const { t } = useI18n();
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
function navigateTo(url: string) {
|
||||
if(userStore.checkLogin()) {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const swiperList = ref([]);
|
||||
const currentSwiper = ref(0);
|
||||
|
||||
async function initData() {
|
||||
// 只在首次加载时显示骨架屏,避免切换时的白屏
|
||||
if(featuredList.value.length === 0) {
|
||||
loading.value = true
|
||||
}
|
||||
appMarketActivityList()
|
||||
getAppMerchantLabelList()
|
||||
getAppMerchantCategoryList()
|
||||
getAppFeaturedList()
|
||||
getAppNearbyListPost()
|
||||
// 获取当前用户购物车信息
|
||||
userStore.getUserCartAllData()
|
||||
}
|
||||
|
||||
// 筛选事件触发
|
||||
function toggleScore() {
|
||||
emit("toggleScore");
|
||||
}
|
||||
function togglePrice() {
|
||||
emit("togglePrice");
|
||||
}
|
||||
function toggleNotOpen() {
|
||||
emit("toggleNotOpen");
|
||||
}
|
||||
|
||||
async function getIndexList() {
|
||||
// 切换时立即刷新列表,无需等待nextTick
|
||||
if (paging.value) {
|
||||
paging.value.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
initData,
|
||||
init: getIndexList,
|
||||
});
|
||||
|
||||
// 获取轮播图列表
|
||||
function appMarketActivityList() {
|
||||
appMarketActivityListPost({}).then(res=> {
|
||||
console.log('互动列表', res)
|
||||
swiperList.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
// 滚动信息获取 APP-商家标签分类控制器(用户端首页上面左右滚动的)
|
||||
const appMerchantLabelList = ref([])
|
||||
function getAppMerchantLabelList() {
|
||||
appMerchantLabelListGet({}).then(res => {
|
||||
console.log('滚动信息获取 APP-商家标签分类控制器(用户端首页上面左右滚动的)', res)
|
||||
appMerchantLabelList.value = res.data || []
|
||||
})
|
||||
}
|
||||
// 查询所有商家分类数据
|
||||
const appMerchantCategoryList = ref([])
|
||||
const currentCategory = ref('')
|
||||
function getAppMerchantCategoryList() {
|
||||
appMerchantCategoryListGet({}).then((res: any) => {
|
||||
console.log('查询所有商家分类数据', res)
|
||||
appMerchantCategoryList.value = res.data || []
|
||||
})
|
||||
}
|
||||
|
||||
// 查询精选商家列表(首页)
|
||||
const featuredList = ref([])
|
||||
function getAppFeaturedList() {
|
||||
appMerchantFeaturedListPost({
|
||||
body: {
|
||||
lat: userStore.userLocation.latitude,
|
||||
lng: userStore.userLocation.longitude,
|
||||
}
|
||||
}).then(res=> {
|
||||
featuredList.value = res.data || []
|
||||
}).finally(()=> {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 查询附近商家列表(首页) /app/merchant/nearbyList
|
||||
const nearbyList = ref([])
|
||||
function getAppNearbyListPost() {
|
||||
appMerchantNearbyListPost({
|
||||
body: {
|
||||
lat: userStore.userLocation.latitude,
|
||||
lng: userStore.userLocation.longitude,
|
||||
}
|
||||
}).then(res=> {
|
||||
nearbyList.value = res.data || []
|
||||
})
|
||||
}
|
||||
|
||||
// 是否自提
|
||||
const selfPickup = ref<number | null>(null)
|
||||
function togglePickup(value: number) {
|
||||
selfPickup.value = value;
|
||||
// paging.value.refresh()
|
||||
}
|
||||
// 是否有折扣
|
||||
const discount = ref<number | null>(null)
|
||||
function toggleDiscount(value: number) {
|
||||
discount.value = value;
|
||||
// paging.value.refresh()
|
||||
}
|
||||
const {paging, dataList, queryList} = usePage(getList)
|
||||
function getList(pageNum: number, pageSize: number) {
|
||||
return new Promise(resolve => {
|
||||
getFeaturedDishList({
|
||||
pageNum,
|
||||
pageSize,
|
||||
}).then(res => {
|
||||
console.log('查询精选菜品列表', res)
|
||||
resolve({rows: res.rows})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 点击头部分类
|
||||
const merchantLabelId = ref('')
|
||||
function handleItemClick(e) {
|
||||
console.log(e, '点击头部分类')
|
||||
merchantLabelId.value = e.id
|
||||
// paging.value.refresh()
|
||||
}
|
||||
function tabsTypeChange(id: string) {
|
||||
currentCategory.value = id
|
||||
navigateTo('/pages-store/pages/home-store/index?merchantCategoryIds=' + id)
|
||||
// console.log('分类切换', id)
|
||||
// paging.value.refresh()
|
||||
}
|
||||
|
||||
// 是否展示精选商家和附近商家 true 显示 false隐藏
|
||||
const isShowMerchant = computed(()=> {
|
||||
if(!selfPickup.value && !discount.value && !props.scoreRange && !props.price && !currentCategory.value) {
|
||||
return true // 没有筛选条件时显示
|
||||
} else {
|
||||
return false // 有筛选条件时隐藏
|
||||
}
|
||||
})
|
||||
|
||||
// 手动触发下拉刷新了
|
||||
function onRefresh() {
|
||||
console.log('手动触发下拉刷新了')
|
||||
merchantLabelId.value = ''
|
||||
currentCategory.value = ''
|
||||
paging.value.refresh()
|
||||
}
|
||||
|
||||
function handleClickSwiper(item: any) {
|
||||
console.log(item, '点击轮播图')
|
||||
switch (Number(item.activityType)) {
|
||||
case 1: // 商家列表
|
||||
navigateTo('/pages-store/pages/list/index?id=')
|
||||
break
|
||||
case 2: // 活动菜品列表
|
||||
navigateTo('/pages-store/pages/dishes/index?id=' + item.id)
|
||||
break
|
||||
case 3: // 会员
|
||||
navigateTo('/pages-user/pages/member/index')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToDishes(item: any) {
|
||||
uni.navigateTo({
|
||||
url: '/pages-store/pages/store/dishes?id=' + item.id + '&storeId=' + item.merchantId,
|
||||
})
|
||||
}
|
||||
// 收藏菜品
|
||||
function handleDishCollectionClick(item: any) {
|
||||
debouncedEmit(item.isCollect, item.id, CollectionType.DISH, ()=> {
|
||||
item.isCollect = !item.isCollect
|
||||
})
|
||||
}
|
||||
// 防抖处理函数
|
||||
const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: CollectionType, callback: ()=> void) => {
|
||||
// 收藏接口
|
||||
appCollectCollectPost({
|
||||
body: {
|
||||
targetId: id,
|
||||
targetType: type
|
||||
}
|
||||
}).then(res=> {
|
||||
callback()
|
||||
})
|
||||
}, {
|
||||
atBegin: true, // 立即触发
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
class="bg-#fff"
|
||||
:style="[
|
||||
{
|
||||
height: configStore.windowHeight + 'px',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<z-paging @onRefresh="onRefresh" ref="paging" v-model="dataList" :auto="false" @query="queryList" :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>
|
||||
</view>
|
||||
</template>
|
||||
<view
|
||||
class="animate-in fade-in animate-ease-out animate-duration-300"
|
||||
v-show="loading && featuredList.length === 0"
|
||||
>
|
||||
<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">
|
||||
<search />
|
||||
<!-- 分类滚动区域 -->
|
||||
<view class="mt-40rpx" v-if="appMerchantLabelList.length > 0">
|
||||
<class-bullet :categories="appMerchantLabelList" @itemClick="handleItemClick" />
|
||||
</view>
|
||||
</view>
|
||||
<swiper
|
||||
class="card-swiper"
|
||||
:circular="true"
|
||||
:autoplay="true"
|
||||
previous-margin="60rpx"
|
||||
next-margin="60rpx"
|
||||
>
|
||||
<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>
|
||||
|
||||
<!-- 分类滚动区域 -->
|
||||
<tabs-type @changeType="tabsTypeChange" v-if="appMerchantCategoryList.length > 0" :list="appMerchantCategoryList" :currentId="currentCategory" class="mt-22rpx" />
|
||||
|
||||
<!-- 筛选工具 -->
|
||||
<!-- <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 :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>
|
||||
<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"
|
||||
>{{ 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">
|
||||
<image
|
||||
v-if="!item.isCollect"
|
||||
src="@img-store/1334.png"
|
||||
mode="aspectFill"
|
||||
class="w-full h-full"
|
||||
/>
|
||||
<image
|
||||
v-else
|
||||
src="@img-store/1337.png"
|
||||
mode="aspectFill"
|
||||
class="w-full h-full"
|
||||
/>
|
||||
</view>
|
||||
<image
|
||||
:src="item?.dishImage?.split(',')[0]"
|
||||
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 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 }}
|
||||
</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>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.card-swiper {
|
||||
height: 420rpx;
|
||||
}
|
||||
.swiper-item-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: scale(0.95);
|
||||
border-radius: 20rpx;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import {appCustomerServiceGetInfoGet} from "@/service";
|
||||
import {callPhone} from "@/utils/utils";
|
||||
const {t, locale} = useI18n();
|
||||
|
||||
const show = ref(false);
|
||||
const serviceData = ref({})
|
||||
function init() {
|
||||
appCustomerServiceGetInfoGet({}).then(res=> {
|
||||
console.log('客服信息', res)
|
||||
serviceData.value = res.data
|
||||
})
|
||||
show.value = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
show.value = false;
|
||||
}
|
||||
|
||||
function handleDial() {
|
||||
callPhone(serviceData.value.servicePhone)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
init,
|
||||
close,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<wd-popup
|
||||
custom-class="!bg-transparent"
|
||||
position="bottom"
|
||||
safe-area-inset-bottom
|
||||
v-model="show"
|
||||
@close="close"
|
||||
>
|
||||
<view class="p-[40rpx+20rpx]">
|
||||
<view
|
||||
class="mb-18rpx py-32rpx text-center bg-white shadow-[0rpx_3rpx_6rpx_rgba(0,0,0,0.16)] rounded-16rpx"
|
||||
>
|
||||
<view class="text-30rpx text-#333 font-bold mb-22rpx">{{ t("pages.mine.customer-service-phone") }}: {{ serviceData.servicePhone }}</view>
|
||||
<view class="text-28rpx text-#999">{{ t("pages.mine.work-time") }}: {{ locale === 'en' ? serviceData.workingHoursEn : serviceData.workingHoursZh }}</view>
|
||||
</view>
|
||||
<view
|
||||
class="h-100rpx flex items-center justify-center text-28rpx text-primary font-bold shadow-[0rpx_3rpx_6rpx_rgba(0,0,0,0.16)] bg-white rounded-16rpx"
|
||||
@click="handleDial"
|
||||
>
|
||||
{{ t("pages.mine.dial") }}
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,286 @@
|
||||
<script setup lang="ts">
|
||||
import {useUserStore} from '@/store'
|
||||
import Config from "@/config";
|
||||
|
||||
const emits = defineEmits<{
|
||||
success: []
|
||||
}>()
|
||||
|
||||
|
||||
const {t} = useI18n()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const painterRef = ref(null)
|
||||
|
||||
const path = ref('')
|
||||
|
||||
const poster = computed(() => ({
|
||||
css: {
|
||||
width: '658rpx',
|
||||
height: '1052rpx',
|
||||
},
|
||||
views: [
|
||||
{
|
||||
src: '/static/images/50@2x.png',
|
||||
type: 'image',
|
||||
css: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
|
||||
views: [
|
||||
{
|
||||
type: 'view',
|
||||
css: {
|
||||
height: '706rpx',
|
||||
},
|
||||
views: [
|
||||
{
|
||||
// src: '/static/images/1334@2x.png',
|
||||
src: Config.shareImage,
|
||||
type: 'image',
|
||||
css: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'view',
|
||||
css: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: '14rpx',
|
||||
display: 'flex',
|
||||
"justifyContent": 'center'
|
||||
},
|
||||
views: [
|
||||
// {
|
||||
// src: '/static/images/img_word@2x.png',
|
||||
// type: 'image',
|
||||
// css: {
|
||||
// width: '592rpx',
|
||||
// height: '238rpx',
|
||||
// },
|
||||
// },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
views: [
|
||||
{
|
||||
css: {
|
||||
padding: '24rpx 24rpx',
|
||||
},
|
||||
type: 'view',
|
||||
views: [
|
||||
{
|
||||
views: [
|
||||
{
|
||||
text: `${t('pages.mine.activity-description')}`,
|
||||
type: 'text',
|
||||
css: {
|
||||
color: '#000',
|
||||
fontSize: '28rpx',
|
||||
fontWeight: 'bold',
|
||||
lineHeight: '42rpx'
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'view',
|
||||
},
|
||||
{
|
||||
css: {
|
||||
marginTop: '8rpx',
|
||||
},
|
||||
views: [
|
||||
{
|
||||
text: Config.shareDesc,
|
||||
type: 'text',
|
||||
css: {
|
||||
color: '#666',
|
||||
fontSize: '24rpx',
|
||||
fontWeight: 'bold',
|
||||
lineHeight: '34rpx'
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'view',
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
css: {
|
||||
marginTop: '2rpx',
|
||||
borderTop: '1rpx dashed #D1D1D1',
|
||||
},
|
||||
type: 'view',
|
||||
},
|
||||
{
|
||||
css: {
|
||||
padding: '24rpx 24rpx',
|
||||
display: 'flex',
|
||||
'align-items': 'center',
|
||||
},
|
||||
views: [
|
||||
{
|
||||
css: {
|
||||
flex: '1',
|
||||
display: 'flex',
|
||||
'align-items': 'center',
|
||||
},
|
||||
views: [
|
||||
{
|
||||
src: userStore.userInfo.avatar,
|
||||
type: 'image',
|
||||
css: {
|
||||
width: '94rpx',
|
||||
height: '94rpx',
|
||||
borderRadius: '50%',
|
||||
},
|
||||
},
|
||||
{
|
||||
css: {
|
||||
display: 'inline-block',
|
||||
marginLeft: '24rpx',
|
||||
},
|
||||
views: [
|
||||
{
|
||||
views: [
|
||||
{
|
||||
text: `${userStore.userInfo.firstName} ${userStore.userInfo.surname}`,
|
||||
type: 'text',
|
||||
css: {
|
||||
color: '#000',
|
||||
fontSize: '30rpx',
|
||||
fontWeight: 'bold',
|
||||
lineHeight: '42rpx'
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'view',
|
||||
},
|
||||
{
|
||||
css: {
|
||||
marginTop: '8rpx',
|
||||
},
|
||||
views: [
|
||||
{
|
||||
text: `${t('pages.mine.invitation-code')}:${userStore.userInfo.invitationCode}`,
|
||||
type: 'text',
|
||||
css: {
|
||||
color: '#999',
|
||||
fontSize: '24rpx',
|
||||
fontWeight: 'bold',
|
||||
lineHeight: '34rpx'
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'view',
|
||||
},
|
||||
],
|
||||
type: 'view',
|
||||
},
|
||||
],
|
||||
type: 'view',
|
||||
},
|
||||
{
|
||||
text: Config.shareLink + `pages-login/pages/sign-up/index?invitationCode=${userStore.userInfo.invitationCode}`,
|
||||
type: 'qrcode',
|
||||
css: {
|
||||
width: '124rpx',
|
||||
height: '124rpx',
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'view',
|
||||
},
|
||||
],
|
||||
type: 'view',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
|
||||
function save() {
|
||||
onAppSaveToPhone()
|
||||
}
|
||||
|
||||
function onAppSaveToPhone() {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: path.value,
|
||||
success: function () {
|
||||
console.log('save success')
|
||||
uni.showToast({
|
||||
title: t('common.prompt.save-successfully'),
|
||||
icon: 'none',
|
||||
})
|
||||
emits('success')
|
||||
},
|
||||
fail(error) {
|
||||
console.log(error)
|
||||
uni.showToast({
|
||||
title: t('common.prompt.save-failed'),
|
||||
icon: 'none',
|
||||
})
|
||||
},
|
||||
complete() {
|
||||
uni.hideLoading()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function init() {
|
||||
console.log('init')
|
||||
console.log(userStore.userInfo)
|
||||
uni.showLoading({
|
||||
title: t('common.loading') + '...',
|
||||
})
|
||||
painterRef.value?.render(poster.value)
|
||||
painterRef.value?.canvasToTempFilePathSync({
|
||||
fileType: 'png',
|
||||
// 如果返回的是base64是无法使用 saveImageToPhotosAlbum,需要设置 pathType为url
|
||||
pathType: 'url',
|
||||
success: (res: any) => {
|
||||
console.log('res', res)
|
||||
path.value = res.tempFilePath
|
||||
save()
|
||||
},
|
||||
fail(error: Error) {
|
||||
console.log(error)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: t('common.failure'),
|
||||
icon: 'none',
|
||||
})
|
||||
},
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: "InvitePoster",
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
path,
|
||||
init,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<l-painter ref="painterRef" path-type="url" hidden custom-style="position: fixed; left: 200%"/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import InvitePoster from "@/pages/home/components/tabbar-mine/components/invite-poster/invite-poster.vue";
|
||||
import {throttle} from "throttle-debounce";
|
||||
import Config from "@/config";
|
||||
import {useUserStore} from "@/store";
|
||||
import {setClipboardData} from "@/utils/utils";
|
||||
const {t} = useI18n();
|
||||
const userStore = useUserStore()
|
||||
|
||||
|
||||
const invitePosterRef = ref<InstanceType<typeof InvitePoster>>()
|
||||
const show = ref(false);
|
||||
|
||||
function init() {
|
||||
show.value = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
show.value = false;
|
||||
}
|
||||
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
})
|
||||
}
|
||||
|
||||
const handleSave = throttle(Config.throttleTime, () => {
|
||||
invitePosterRef.value?.init()
|
||||
}, {})
|
||||
|
||||
function copyLink() {
|
||||
setClipboardData(Config.shareLink + `pages-login/pages/sign-up/index?invitationCode=${userStore.userInfo.invitationCode}`)
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: "InviteUser",
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
init,
|
||||
close,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<wd-popup
|
||||
custom-class="!bg-transparent"
|
||||
safe-area-inset-bottom
|
||||
v-model="show"
|
||||
@close="close"
|
||||
>
|
||||
<view class="box-border px-46rpx w-750rpx">
|
||||
<view class="flex justify-end">
|
||||
<image class="w-66rpx h-66rpx" src="@img/14952@2x.png" @click="close"></image>
|
||||
</view>
|
||||
<view class="mt-36rpx relative h-1052rpx">
|
||||
<image class="absolute top-0 left-0 w-full h-full" src="@img/50@2x.png"></image>
|
||||
<!-- <view class="z-9 absolute top-14rpx left-0 right-0 center text-0rpx">-->
|
||||
<!-- <image class="w-592rpx h-238rpx" src="@img/img_word@2x.png"></image>-->
|
||||
<!-- </view>-->
|
||||
<view class="relative z-2 flex flex-col">
|
||||
<!-- <image class="w-full h-706rpx" src="@img/1334@2x.png"></image>-->
|
||||
<image class="w-full h-706rpx" :src="Config.shareImage"></image>
|
||||
<view class="py-24rpx px-24rpx">
|
||||
<view class="text-28-bold">{{ t('pages.mine.activity-description') }}</view>
|
||||
<view class="mt-8rpx text-24rpx text-#666 font-bold lh-34rpx">
|
||||
{{ Config.shareDesc }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-2rpx border-1rpx border-dashed border-#D1D1D1"></view>
|
||||
<view class="py-24rpx px-24rpx flex items-center justify-between">
|
||||
<view class="flex items-center">
|
||||
<image class="shrink-0 mr-24rpx w-94rpx h-94rpx rounded-full" :src="userStore.userInfo.avatar"></image>
|
||||
<view class="">
|
||||
<view class="text-30-bold">{{ `${userStore.userInfo.firstName} ${userStore.userInfo.surname}` }}</view>
|
||||
<view class="mt-8rpx text-24rpx text-#999 font-bold lh-34rpx">
|
||||
<text>{{ t('pages.mine.invitation-code') }}:</text>
|
||||
<text>{{ userStore.userInfo.invitationCode }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="shrink-0 ml-20rpx" v-if="userStore.userInfo.invitationCode">
|
||||
<uqrcode ref="uqrcode" canvas-id="qrcode"
|
||||
:value="Config.shareLink + `pages-login/pages/sign-up/index?invitationCode=${userStore.userInfo.invitationCode}`"
|
||||
:size="124"
|
||||
sizeUnit="rpx"
|
||||
:options="{}"></uqrcode>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-34rpx flex justify-between items-center ">
|
||||
<wd-button
|
||||
custom-class="!box-border shrink-0 !min-w-auto !h-98rpx !w-316rpx !text-#333 !text-30rpx !lh-42rpx !rounded-16rpx !bg-#fff"
|
||||
@click="copyLink"
|
||||
>
|
||||
{{ t('common.copyLink') }}
|
||||
</wd-button>
|
||||
<wd-button
|
||||
custom-class="!box-border shrink-0 !min-w-auto !h-98rpx !w-316rpx !text-30rpx !lh-42rpx !rounded-16rpx"
|
||||
@click="handleSave"
|
||||
>
|
||||
{{ t('pages.mine.save-picture') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
<invite-poster ref="invitePosterRef" @success="close"/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<view class="mine-skeleton">
|
||||
<!-- 用户信息区域 -->
|
||||
<view class="user-info-section flex-center-sb">
|
||||
<view class="user-name-skeleton skeleton-item"></view>
|
||||
<view class="user-avatar-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
|
||||
<!-- 统计卡片区域 -->
|
||||
<view class="stats-section">
|
||||
<view class="stats-list">
|
||||
<view
|
||||
v-for="i in 3"
|
||||
:key="i"
|
||||
class="stat-card-skeleton skeleton-item"
|
||||
>
|
||||
<view class="stat-number-skeleton skeleton-item"></view>
|
||||
<view class="stat-label-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 会员横幅区域 -->
|
||||
<view class="member-banner-section">
|
||||
<view class="member-banner-skeleton skeleton-item">
|
||||
<view class="banner-content">
|
||||
<view class="banner-title-skeleton skeleton-item"></view>
|
||||
<view class="banner-subtitle-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
<view class="banner-icon-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 菜单列表区域 -->
|
||||
<view class="menu-section">
|
||||
<view class="menu-list">
|
||||
<view
|
||||
v-for="i in 9"
|
||||
:key="i"
|
||||
class="menu-item-skeleton"
|
||||
>
|
||||
<view class="menu-icon-skeleton skeleton-item"></view>
|
||||
<view class="menu-text-skeleton skeleton-item"></view>
|
||||
<view class="menu-arrow-skeleton skeleton-item"></view>
|
||||
</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-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
// 闪烁动画
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mine-skeleton {
|
||||
background-color: #fff;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
// 用户信息区域
|
||||
.user-info-section {
|
||||
padding: 36rpx 0 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
|
||||
.user-avatar-skeleton {
|
||||
width: 130rpx;
|
||||
height: 130rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.user-name-skeleton {
|
||||
width: 233rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 统计卡片区域
|
||||
.stats-section {
|
||||
padding: 90rpx 0 0;
|
||||
|
||||
.stats-list {
|
||||
display: flex;
|
||||
gap: 36rpx;
|
||||
|
||||
.stat-card-skeleton {
|
||||
flex: 1;
|
||||
height: 194rpx;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
|
||||
.stat-number-skeleton {
|
||||
width: 80rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.stat-label-skeleton {
|
||||
width: 120rpx;
|
||||
height: 28rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 会员横幅区域
|
||||
.member-banner-section {
|
||||
padding: 40rpx 0 0;
|
||||
|
||||
.member-banner-skeleton {
|
||||
width: 690rpx;
|
||||
height: 152rpx;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 28rpx;
|
||||
|
||||
.banner-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
|
||||
.banner-title-skeleton {
|
||||
width: 457rpx;
|
||||
height: 57rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.banner-subtitle-skeleton {
|
||||
width: 497rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.banner-icon-skeleton {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单列表区域
|
||||
.menu-section {
|
||||
padding: 80rpx 0 0;
|
||||
|
||||
.menu-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
|
||||
.menu-item-skeleton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 22rpx 0;
|
||||
// border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.menu-icon-skeleton {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.menu-text-skeleton {
|
||||
flex: 1;
|
||||
height: 30rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.menu-arrow-skeleton {
|
||||
width: 22rpx;
|
||||
height: 30rpx;
|
||||
border-radius: 4rpx;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 750rpx) {
|
||||
.stats-list {
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.member-banner-skeleton {
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
padding: 20rpx;
|
||||
|
||||
.banner-content {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,331 @@
|
||||
<script setup lang="ts">
|
||||
import * as R from "ramda";
|
||||
import dayjs from 'dayjs'
|
||||
import useEventEmit from "@/hooks/useEventEmit";
|
||||
import { EventEnum } from "@/constant/enums";
|
||||
import { useConfigStore, useUserStore } from "@/store";
|
||||
import MineSkeleton from "./components/mine-skeleton.vue";
|
||||
const configStore = useConfigStore();
|
||||
const userStore = useUserStore();
|
||||
import { Agreement } from "@/constant/enums";
|
||||
import Config from '@/config/index'
|
||||
import {formatTimestampWithMonthName, loadWeixinService} from "@/utils/utils";
|
||||
const { t } = useI18n();
|
||||
|
||||
const loading = ref(true);
|
||||
|
||||
const emits = defineEmits<{
|
||||
chooseLanguage: [];
|
||||
logOut: [];
|
||||
inviteUser: [];
|
||||
customerService: [];
|
||||
changeOrder: [];
|
||||
}>();
|
||||
interface Tab {
|
||||
iconPath: string;
|
||||
path: string;
|
||||
text: string;
|
||||
code: string;
|
||||
isLogin: boolean;
|
||||
}
|
||||
|
||||
const tabBarList = ref<Tab[]>([
|
||||
{
|
||||
iconPath: "/static/images/chef/100201.png",
|
||||
path: "/pages-user/pages/coupon/index",
|
||||
text: t("pages.mine.discount"),
|
||||
code: "discount",
|
||||
isLogin: true,
|
||||
},
|
||||
{
|
||||
iconPath: "/static/images/chef/100200.png",
|
||||
path: "/pages-user/pages/faqs/index",
|
||||
text: t("pages.mine.help"),
|
||||
code: "help",
|
||||
isLogin: true,
|
||||
},
|
||||
{
|
||||
iconPath: "/static/images/chef/100199.png",
|
||||
path: "/pages-user/pages/coupon/index",
|
||||
text: t("pages.mine.inviteFriends"),
|
||||
code: "inviteFriends",
|
||||
isLogin: true,
|
||||
},
|
||||
{
|
||||
iconPath: "/static/images/100203.png",
|
||||
path: "/pages-user/pages/invited-person/index",
|
||||
text: t('pages.mine.my-invitations'),
|
||||
code: "myInvitations",
|
||||
isLogin: true,
|
||||
},
|
||||
{
|
||||
iconPath: "/static/images/chef/100198.png",
|
||||
path: "/pages-user/pages/store-settle-in/index",
|
||||
text: t("pages.mine.storeSettled"),
|
||||
code: "storeSettled",
|
||||
isLogin: false,
|
||||
},
|
||||
{
|
||||
iconPath: "/static/images/chef/100197.png",
|
||||
path: "/pages-user/pages/coupon/index",
|
||||
text: t("pages.mine.support"),
|
||||
code: "support",
|
||||
isLogin: true,
|
||||
},
|
||||
{
|
||||
iconPath: "/static/images/chef/100196.png",
|
||||
path: "/pages/agreement/index?code=" + Agreement.CHEF_PLATFORM_AGREEMENT,
|
||||
text: t("pages.mine.platformAgreement"),
|
||||
code: "platformAgreement",
|
||||
isLogin: true,
|
||||
},
|
||||
{
|
||||
iconPath: "/static/images/chef/100195.png",
|
||||
path: "/pages/agreement/index?code=" + Agreement.PRIVACY_POLICY,
|
||||
text: t("pages.mine.privacyPolicy"),
|
||||
code: "privacyPolicy",
|
||||
isLogin: true,
|
||||
},
|
||||
{
|
||||
iconPath: "/static/images/chef/100194.png",
|
||||
path: "/pages-user/pages/complaints/index",
|
||||
text: t("pages.mine.complaintsAndSuggestions"),
|
||||
code: "complaintsAndSuggestions",
|
||||
isLogin: true,
|
||||
},
|
||||
{
|
||||
iconPath: "/static/images/chef/100192.png",
|
||||
path: "/pages-user/pages/setting/index",
|
||||
text: t("pages.mine.set"),
|
||||
code: "set",
|
||||
isLogin: true,
|
||||
},
|
||||
]);
|
||||
|
||||
function navigateTo(url: string) {
|
||||
if(userStore.checkLogin()) {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function checkNeedLogin(isNeedLogin: boolean) {
|
||||
return isNeedLogin ? userStore.checkLogin() : true;
|
||||
}
|
||||
|
||||
// 用户会员状态是否已开通
|
||||
const isUserMember = computed(()=> {
|
||||
if(!userStore.userInfo.userMembershipVo) return false
|
||||
if(userStore.userInfo.userMembershipVo && userStore.userInfo.userMembershipVo.expireTime ){
|
||||
return dayjs().isBefore(dayjs(Number(userStore.userInfo.userMembershipVo.expireTime)))
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
function handleTabClick(item: Tab) {
|
||||
switch (item.code) {
|
||||
case "inviteFriends": {
|
||||
// R.both(checkNeedLogin, R.pipe(() => emits("inviteUser"), R.T))(item.isLogin)
|
||||
emits("inviteUser");
|
||||
break;
|
||||
}
|
||||
case "support": {
|
||||
// emits("customerService");
|
||||
loadWeixinService()
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// navigateTo(item.path)
|
||||
if (item.code === "set") {
|
||||
navigateTo("/pages-user/pages/setting/index");
|
||||
} else {
|
||||
R.both(
|
||||
checkNeedLogin,
|
||||
R.pipe(() => navigateTo(item.path), R.T)
|
||||
)(item.isLogin);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function changeOrderFn() {
|
||||
emits("changeOrder");
|
||||
}
|
||||
|
||||
async function initData() {
|
||||
loading.value = false;
|
||||
// setTimeout(() => {
|
||||
// loading.value = false;
|
||||
// }, 300);
|
||||
userStore.getUserInfo()
|
||||
}
|
||||
|
||||
async function getPlatformDefaultStoreInfo() {}
|
||||
|
||||
defineExpose({
|
||||
initData,
|
||||
init: getPlatformDefaultStoreInfo,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
class="bg-#fff"
|
||||
:style="[
|
||||
{
|
||||
height: configStore.windowHeight + 'px',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<z-paging ref="paging">
|
||||
<template #top>
|
||||
<status-bar />
|
||||
</template>
|
||||
<view
|
||||
v-show="loading"
|
||||
class="animate-in fade-in animate-ease-out animate-duration-300"
|
||||
>
|
||||
<mine-skeleton />
|
||||
</view>
|
||||
<view
|
||||
v-show="!loading"
|
||||
class="animate-in fade-in animate-ease-in animate-duration-300"
|
||||
>
|
||||
<view class="px-30rpx">
|
||||
<view
|
||||
@click="navigateTo('/pages-user/pages/user-info/index')"
|
||||
class="flex-center-sb mt-32rpx mb-82rpx"
|
||||
>
|
||||
<!-- <text class="text-56rpx text-#333 leading-56rpx font-bold tracking-[.04em]"-->
|
||||
<!-- >{{ userStore.isLogin ? `${userStore.userInfo.firstName} ${userStore.userInfo.surname}` : t('common.pleaseLogin') }}-->
|
||||
<!-- </text>-->
|
||||
|
||||
<text
|
||||
class="text-56rpx text-#333 leading-56rpx font-bold tracking-[.04em]"
|
||||
>
|
||||
{{
|
||||
userStore.isLogin
|
||||
? ([userStore.userInfo.firstName, userStore.userInfo.surname].filter(Boolean).join(' ') || t('common.unknownUser'))
|
||||
: t('common.pleaseLogin')
|
||||
}}
|
||||
</text>
|
||||
|
||||
<image
|
||||
v-if="userStore.isLogin"
|
||||
:src="userStore.userInfo.avatar"
|
||||
class="w-130rpx h-130rpx rounded-50%"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<image
|
||||
v-else
|
||||
class="w-130rpx h-130rpx rounded-50%"
|
||||
mode="aspectFill"
|
||||
src="@img/chef/default_avatar.png"
|
||||
/>
|
||||
</view>
|
||||
<view class="flex-center-sb mb-36rpx">
|
||||
<view
|
||||
@click="navigateTo('/pages-user/pages/collection/index')"
|
||||
class="flex-1 flex-col center bg-#F6F6F6 rounded-24rpx h-194rpx"
|
||||
>
|
||||
<view
|
||||
class="text-44rpx text-#333 leading-44rpx font-bold mb-24rpx"
|
||||
>{{ userStore.userInfo.collectNum || 0 }}</view
|
||||
>
|
||||
<view class="text-28rpx text-#333 leading-28rpx tracking-[.04em]">{{
|
||||
t("pages.mine.collection")
|
||||
}}</view>
|
||||
</view>
|
||||
<view
|
||||
@click="navigateTo('/pages-user/pages/balance/index')"
|
||||
class="flex-1 mx-32rpx flex-col center bg-#F6F6F6 rounded-24rpx h-194rpx"
|
||||
>
|
||||
<view
|
||||
class="text-44rpx text-#333 leading-44rpx font-bold mb-24rpx"
|
||||
>{{ userStore.userInfo?.balance || 0 }}</view
|
||||
>
|
||||
<view class="text-28rpx text-#333 leading-28rpx tracking-[.04em]">{{
|
||||
t("pages.mine.wallet")
|
||||
}}</view>
|
||||
</view>
|
||||
<view
|
||||
@click="changeOrderFn"
|
||||
class="flex-1 flex-col center bg-#F6F6F6 rounded-24rpx h-194rpx"
|
||||
>
|
||||
<view
|
||||
class="text-44rpx text-#333 leading-44rpx font-bold mb-24rpx"
|
||||
>{{ userStore.userInfo.orderNum || 0 }}</view
|
||||
>
|
||||
<view class="text-28rpx text-#333 leading-28rpx tracking-[.04em]">{{
|
||||
t("pages.mine.order")
|
||||
}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<template v-if="isUserMember">
|
||||
<view @click="navigateTo('/pages-user/pages/member/index')" class="w-full h-152rpx relative mb-52rpx">
|
||||
<image src="@img/chef/100203.png" class="w-full h-full absolute top-0 left-0"></image>
|
||||
<view class="pl-28rpx py-22rpx pr-165rpx relative z-1 h-full flex flex-col justify-between">
|
||||
<view class="text-40rpx text-#333 font-bold">
|
||||
{{ Config.appName }}
|
||||
</view>
|
||||
<view class="text-24rpx lh-24rpx text-#935D04 tracking-[.08em]">
|
||||
{{ t('common.expireTime') }}:{{ formatTimestampWithMonthName(userStore.userInfo.userMembershipVo?.expireTime) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else>
|
||||
<view @click="navigateTo('/pages-user/pages/member/index')" class="w-full h-152rpx relative mb-52rpx">
|
||||
<image src="@img/chef/100203.png" class="w-full h-full absolute top-0 left-0"></image>
|
||||
<view class="pl-28rpx py-22rpx pr-165rpx relative z-1 h-full flex flex-col justify-between">
|
||||
<view class="text-40rpx text-#333 font-bold">
|
||||
<!--用户没有试用过会员-->
|
||||
<template v-if="!userStore.userInfo.userMembershipVo">{{ t('pages.mine.member-title') }}</template>
|
||||
<template v-else>{{ t('pages.mine.join') }} {{ Config.appName }}</template>
|
||||
</view>
|
||||
<view class="text-24rpx lh-24rpx text-#935D04 tracking-[.08em]">{{ t('pages.mine.member-desc') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
<view
|
||||
class="flex-center-sb py-28rpx bg-#fff"
|
||||
v-for="(item, index) in tabBarList"
|
||||
:key="item.code"
|
||||
:class="[
|
||||
index === tabBarList.length - 1 ? 'mb-58rpx' : 'border-bottom',
|
||||
]"
|
||||
@click="handleTabClick(item)"
|
||||
>
|
||||
<view class="flex items-center">
|
||||
<image
|
||||
class="w-44rpx h-44rpx shrink-0 mr-18rpx"
|
||||
:src="item.iconPath"
|
||||
></image>
|
||||
<text class="text-30rpx text-primary font-500 lh-30rpx tracking-[.04em]">{{
|
||||
item.text
|
||||
}}</text>
|
||||
</view>
|
||||
<view class="flex items-center shrink-0 ml-20rpx">
|
||||
<image class="w-22rpx h-30rpx" src="@img/chef/100202.png"></image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<template #bottom>
|
||||
<view class="h-50px"></view>
|
||||
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
|
||||
</template>
|
||||
</z-paging>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.border-bottom {
|
||||
border-bottom: 1rpx solid #dfdfdf;
|
||||
}
|
||||
</style>
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
appMerchantOrderOrderListPost,
|
||||
type MerchantOrderVo
|
||||
} from "@/service";
|
||||
import {callPhone, formatTimestampWithWeekday} 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";
|
||||
const userStore = useUserStore();
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
currentIndex: number
|
||||
status: null | number | string
|
||||
}>()
|
||||
|
||||
watch(() => props.currentIndex, (newVal, oldVal) => {
|
||||
if (newVal === props.status && !firstLoaded.value) {
|
||||
paging.value?.refresh()
|
||||
}
|
||||
}, {
|
||||
deep: true,
|
||||
})
|
||||
|
||||
const {paging, dataList, loading, queryList, firstLoaded} = usePage<MerchantOrderVo[]>((pageNum, pageSize)=>{
|
||||
if(userStore.isLogin) {
|
||||
return appMerchantOrderOrderListPost({
|
||||
params: {
|
||||
pageNum,
|
||||
pageSize,
|
||||
},
|
||||
body: {
|
||||
userPort: 1,
|
||||
orderStatusList: props.status ? [Number(props.status + 1)] : [],
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return new Promise(resolve => {
|
||||
resolve([])
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function handleClick(item: any) {
|
||||
uni.navigateTo({
|
||||
url: '/pages-store/pages/order/index?id=' + item.id
|
||||
})
|
||||
}
|
||||
|
||||
function reload() {
|
||||
paging.value?.reload()
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
paging.value?.refresh()
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'OrderSwiperList',
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
reload,
|
||||
refresh,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="h-full">
|
||||
<z-paging ref="paging" v-model="dataList" @query="queryList" :fixed="false" :auto="false">
|
||||
<view class="p-30rpx">
|
||||
<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>
|
||||
|
||||
<!-- 商家信息骨架屏 -->
|
||||
<view class="flex items-center my-28rpx">
|
||||
<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>
|
||||
|
||||
<!-- 轮播图骨架屏 -->
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</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">
|
||||
<template v-if="
|
||||
+item.refundStatus === OrderCancelStatus.APPLIED ||
|
||||
+item.refundStatus === OrderCancelStatus.APPROVED ||
|
||||
+item.refundStatus === OrderCancelStatus.REJECTED
|
||||
">
|
||||
<template v-if="+item.refundStatus === OrderCancelStatus.APPLIED">
|
||||
{{ t('pages-store.order.orderStatus.refund') }}
|
||||
</template>
|
||||
<template v-else-if="+item.refundStatus === OrderCancelStatus.APPROVED">
|
||||
{{ t('pages-store.order.orderStatus.agreeRefund') }}
|
||||
</template>
|
||||
<template v-else-if="+item.refundStatus === OrderCancelStatus.REJECTED">
|
||||
{{ t('pages-store.order.orderStatus.rejectRefund') }}
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<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.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">
|
||||
{{ 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">
|
||||
{{ 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>
|
||||
<view class="flex items-center gap-20rpx font-500">
|
||||
<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>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</z-paging>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import {OrderStatus} from "@/constant/enums";
|
||||
import OrderSwiperList from "./components/order-swiper-list/order-swiper-list.vue";
|
||||
import {useConfigStore} from "@/store";
|
||||
import {debounce} from "throttle-debounce";
|
||||
const configStore = useConfigStore()
|
||||
const {t} = useI18n()
|
||||
|
||||
const orderSwiperListRef = ref(null)
|
||||
// async function initData() {
|
||||
// orderSwiperListRef.value[currentIndex.value].reload()
|
||||
// }
|
||||
const initData = debounce(500, () => {
|
||||
nextTick(() => {
|
||||
// listItem.value[currentTab.value].reload()
|
||||
orderSwiperListRef.value[currentIndex.value].reload()
|
||||
})
|
||||
}, {atBegin: true})
|
||||
|
||||
|
||||
|
||||
async function getPlatformDefaultStoreInfo() {}
|
||||
|
||||
const currentIndex = ref(0)
|
||||
const tabList = ref([
|
||||
{
|
||||
name: t('pages.order.all'),
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
name: t('pages.order.confirmReady'), // 待接单待取餐
|
||||
value: OrderStatus.HAS_PENDING_PAYMENT,
|
||||
},
|
||||
{
|
||||
name: t('pages.order.accept'), // 已接单
|
||||
value: OrderStatus.MERCHANT_ACCEPTED,
|
||||
},
|
||||
{
|
||||
name: t('pages.order.onTheWay'), // 配送中
|
||||
value: OrderStatus.DELIVERING,
|
||||
},
|
||||
{
|
||||
name: t('pages.order.completed'), // 已送达
|
||||
value: OrderStatus.COMPLETED,
|
||||
},
|
||||
])
|
||||
function handleClickTab(event: any) {
|
||||
currentIndex.value = event.index
|
||||
}
|
||||
function handleSwiperChange(event: any){
|
||||
currentIndex.value = event.detail.current
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
initData,
|
||||
init: getPlatformDefaultStoreInfo,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
:style="[{
|
||||
height: configStore.windowHeight+'px',
|
||||
}]"
|
||||
>
|
||||
<view class="bg h-360rpx fixed top-0 left-0 right-0"></view>
|
||||
<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">
|
||||
<wd-tabs
|
||||
slidable="always"
|
||||
key="tab"
|
||||
color="#333"
|
||||
inactiveColor="#999"
|
||||
:line-height="3"
|
||||
:line-width="30"
|
||||
v-model="currentIndex"
|
||||
@click="handleClickTab"
|
||||
>
|
||||
<template v-for="(item, index) in tabList" :key="index">
|
||||
<wd-tab :title="item.name"></wd-tab>
|
||||
</template>
|
||||
</wd-tabs>
|
||||
<view class="px-20rpx mt--2rpx">
|
||||
<view class="border-b-solid border-b-4rpx border-b-#999"></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<swiper class="h-full"
|
||||
: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>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<template #bottom>
|
||||
<view class="h-50px"></view>
|
||||
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
|
||||
</template>
|
||||
</z-paging-swiper>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tab {
|
||||
:deep(.wd-tabs) {
|
||||
.wd-tabs__line {
|
||||
bottom: 0 !important;
|
||||
z-index: 99 !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
.wd-tabs__nav-item {
|
||||
padding: 0 22rpx !important;
|
||||
}
|
||||
.wd-tabs__nav-item-text{
|
||||
font-size: 32rpx !important;
|
||||
text-overflow: unset !important;
|
||||
font-family: 'UberMove', sans-serif !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.bg {
|
||||
background: linear-gradient(180deg, #FFFFFF 0%, #FFFFFF 51%, rgba(255, 255, 255, 0) 100%);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import {useConfigStore} from "@/store";
|
||||
const configStore = useConfigStore()
|
||||
const {t} = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-#fff"
|
||||
:style="[{
|
||||
height: configStore.windowHeight+'px',
|
||||
}]"
|
||||
>
|
||||
<z-paging ref="paging">
|
||||
<template #top>
|
||||
<status-bar/>
|
||||
</template>
|
||||
<view class="text-56rpx text-#333 lh-56rpx font-bold pl-30rpx pt-16rpx">{{ t('tabBar.global') }}</view>
|
||||
<view class="flex flex-col items-center mt-306rpx">
|
||||
<image src="@img/chef/100.png" class="w-318rpx h-318rpx"></image>
|
||||
<view class="text-40rpx lh-40rpx text-#333 font-500 mt--20rpx mb-22rpx">{{ t('pages.shop.tips') }}</view>
|
||||
<view class="text-28rpx text-#333 text-center px-108rpx tracking-[.04em]">{{ t('pages.shop.description') }}</view>
|
||||
</view>
|
||||
<template #bottom>
|
||||
<view class="h-50px"></view>
|
||||
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
|
||||
</template>
|
||||
</z-paging>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,652 @@
|
||||
<script setup lang="ts">
|
||||
import * as R from "ramda";
|
||||
import { useConfigStore, useUserStore } from "@/store";
|
||||
import { appUpdate } from "@/utils/update";
|
||||
import TabbarHome from "@/pages/home/components/tabbar-home/tabbar-home.vue";
|
||||
import TabbarShop from "@/pages/home/components/tabbar-shop/tabbar-shop.vue";
|
||||
import TabbarOrder from "@/pages/home/components/tabbar-order/tabbar-order.vue";
|
||||
import TabbarMine from "@/pages/home/components/tabbar-mine/tabbar-mine.vue";
|
||||
import TabbarBrowse from "@/pages/home/components/tabbar-browse/tabbar-browse.vue";
|
||||
import InviteUser from "@/pages/home/components/tabbar-mine/components/invite-user/invite-user.vue";
|
||||
import CustomerService from "@/pages/home/components/tabbar-mine/components/customer-service/customer-service.vue";
|
||||
import NotOpen from "@/pages/home/components/tabbar-home/components/not-open.vue";
|
||||
import { useMessage } from "wot-design-uni";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import Config from "@/config";
|
||||
import { EventEnum } from "@/constant/enums";
|
||||
import type UseCode from "@/components/use-code/use-code.vue";
|
||||
import Score from "@/components/filtrate-tool/components/score.vue";
|
||||
import PriceChoose from "@/components/filtrate-tool/components/price-choose.vue";
|
||||
import useEventEmit from "@/hooks/useEventEmit";
|
||||
import {
|
||||
appAppointmentTimeQueryAppointmentTimePost,
|
||||
appMessageUnreadCountPost,
|
||||
} from "@/service";
|
||||
import SwiperList from "@/pages/search/components/swiper-list/swiper-list.vue";
|
||||
import { getDictFineList } from "@/pages-store/service";
|
||||
import {getCouponReceiveListApi, receiveAllCouponApi} from "@/pages-user/service";
|
||||
|
||||
// useEventEmit(EventEnum.STAR_RATING_FILTER, ()=> {
|
||||
// scoreRef.value.onOpen()
|
||||
// })
|
||||
// useEventEmit(EventEnum.PRICE_FILTER, ()=> {
|
||||
// priceChooseRef.value.onOpen()
|
||||
// })
|
||||
|
||||
const userStore = useUserStore();
|
||||
const configStore = useConfigStore();
|
||||
const message = useMessage();
|
||||
const { t } = useI18n();
|
||||
const tabbarHomeRef = ref<InstanceType<typeof TabbarHome>>();
|
||||
const tabbarShopRef = ref<InstanceType<typeof TabbarShop>>();
|
||||
const tabbarOrderRef = ref<InstanceType<typeof TabbarOrder>>();
|
||||
const tabBarMineRef = ref<InstanceType<typeof TabbarMine>>();
|
||||
const tabbarBrowseRef = ref<InstanceType<typeof TabbarBrowse>>();
|
||||
const scoreRef = ref<InstanceType<typeof Score>>();
|
||||
const priceChooseRef = ref<InstanceType<typeof PriceChoose>>();
|
||||
const notOpenRef = ref<InstanceType<typeof NotOpen>>();
|
||||
const showCoupon = ref(false);
|
||||
const couponList = ref([]);
|
||||
|
||||
const inviteUserRef = ref<InstanceType<typeof InviteUser>>();
|
||||
const useCodeRef = ref<InstanceType<typeof UseCode>>();
|
||||
const customerServiceRef = ref<InstanceType<typeof CustomerService>>();
|
||||
|
||||
// 字体加载状态管理
|
||||
const fontLoadingState = ref({
|
||||
isLoading: false,
|
||||
isLoaded: false,
|
||||
loadedFonts: new Set(),
|
||||
retryCount: 0,
|
||||
maxRetries: 3,
|
||||
});
|
||||
|
||||
// 接口调用状态管理
|
||||
const apiLoadingState = ref({
|
||||
isLoadingUserData: false,
|
||||
lastLoadTime: 0,
|
||||
debounceDelay: 1000,
|
||||
});
|
||||
|
||||
const activeIndex = ref("home");
|
||||
const tabBarList = ref([
|
||||
{
|
||||
iconPath: "/static/tabbar/home.png",
|
||||
selectedIconPath: "/static/tabbar/homeHL.png",
|
||||
text: t("tabBar.home"),
|
||||
code: "home",
|
||||
},
|
||||
{
|
||||
iconPath: "/static/tabbar/global.png",
|
||||
selectedIconPath: "/static/tabbar/globalHL.png",
|
||||
text: t("tabBar.global"),
|
||||
code: "global",
|
||||
},
|
||||
{
|
||||
iconPath: "/static/tabbar/browse.png",
|
||||
selectedIconPath: "/static/tabbar/browseHL.png",
|
||||
text: t("tabBar.browse"),
|
||||
code: "browse",
|
||||
},
|
||||
{
|
||||
iconPath: "/static/tabbar/order.png",
|
||||
selectedIconPath: "/static/tabbar/orderHL.png",
|
||||
text: t("tabBar.order"),
|
||||
code: "order",
|
||||
},
|
||||
{
|
||||
iconPath: "/static/tabbar/user.png",
|
||||
selectedIconPath: "/static/tabbar/userHL.png",
|
||||
text: t("tabBar.mine"),
|
||||
code: "mine",
|
||||
},
|
||||
]);
|
||||
|
||||
function handleInviteUser() {
|
||||
if (inviteUserRef.value) {
|
||||
inviteUserRef.value.init();
|
||||
}
|
||||
}
|
||||
|
||||
function handleCustomerService() {
|
||||
if (customerServiceRef.value) {
|
||||
customerServiceRef.value.init();
|
||||
}
|
||||
}
|
||||
|
||||
// 组件初始化配置
|
||||
const tabbarInitConfig = {
|
||||
home: {
|
||||
ref: tabbarHomeRef,
|
||||
methods: ["init", "initData"], // init优先执行,快速显示已有内容
|
||||
},
|
||||
browse: {
|
||||
ref: tabbarBrowseRef,
|
||||
methods: ["init", "initData"],
|
||||
},
|
||||
global: {
|
||||
ref: tabbarShopRef,
|
||||
methods: [],
|
||||
// methods: ['initData', 'init']
|
||||
},
|
||||
order: {
|
||||
ref: tabbarOrderRef,
|
||||
methods: ["initData"],
|
||||
},
|
||||
mine: {
|
||||
ref: tabBarMineRef,
|
||||
methods: ["init", "initData"],
|
||||
},
|
||||
};
|
||||
|
||||
// 统一的组件初始化函数
|
||||
function initTabbarComponent(tabType: string) {
|
||||
const config = tabbarInitConfig[tabType as keyof typeof tabbarInitConfig];
|
||||
if (!config) {
|
||||
console.warn(`未找到 ${tabType} 对应的初始化配置`);
|
||||
return;
|
||||
}
|
||||
|
||||
const componentRef = config.ref.value;
|
||||
if (!componentRef) {
|
||||
console.warn(`${tabType} 组件引用不存在`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 对于home组件,优先执行init方法以快速显示内容
|
||||
if (tabType === "home") {
|
||||
// 立即执行init方法,无延迟
|
||||
if (typeof componentRef.init === "function") {
|
||||
componentRef.init();
|
||||
}
|
||||
// 然后执行initData方法
|
||||
if (typeof componentRef.initData === "function") {
|
||||
componentRef.initData();
|
||||
}
|
||||
} else {
|
||||
// 其他组件按原有顺序执行
|
||||
config.methods.forEach((method) => {
|
||||
if (typeof componentRef[method] === "function") {
|
||||
componentRef[method]();
|
||||
} else {
|
||||
console.warn(`${tabType} 组件不存在 ${method} 方法`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleTabbarChange({ value }: { value: string }) {
|
||||
initTabbarComponent(value);
|
||||
}
|
||||
|
||||
const scoreRange = ref(""); // 星级评分
|
||||
const price = ref(""); // 价格范围
|
||||
function toggleScore() {
|
||||
if (scoreRef.value) {
|
||||
scoreRef.value.onOpen();
|
||||
}
|
||||
}
|
||||
function applyScore(value: string) {
|
||||
console.log("applyScore", value);
|
||||
scoreRange.value = value;
|
||||
tabbarHomeRef.value?.init();
|
||||
}
|
||||
function togglePrice() {
|
||||
if (priceChooseRef.value) {
|
||||
priceChooseRef.value.onOpen();
|
||||
}
|
||||
}
|
||||
function applyPrice(value: string) {
|
||||
console.log("applyPrice", value);
|
||||
price.value = value;
|
||||
nextTick(() => {
|
||||
tabbarHomeRef.value?.init();
|
||||
});
|
||||
}
|
||||
|
||||
function toggleNotOpen() {
|
||||
if (notOpenRef.value) {
|
||||
notOpenRef.value.onOpen();
|
||||
}
|
||||
}
|
||||
|
||||
function mineChangeOrder() {
|
||||
console.log("执行了");
|
||||
activeIndex.value = "order";
|
||||
handleTabbarChange({ value: "order" });
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
showCoupon.value = false;
|
||||
}
|
||||
|
||||
function receiveAllCoupon() {
|
||||
if (couponList.value.length === 0) {
|
||||
return handleClose()
|
||||
}
|
||||
receiveAllCouponApi().then(res => {
|
||||
if (res.code === 200) {
|
||||
uni.showToast({
|
||||
title: t('common.prompt.claimCouponSuccessfully'),
|
||||
icon: 'none'
|
||||
})
|
||||
handleClose()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getFontUrl(fontRelativeUrl: string): string {
|
||||
let fontUrl = `url(/static/fonts/${fontRelativeUrl})`;
|
||||
// #ifdef APP-PLUS
|
||||
fontUrl = `url(${plus.io.convertLocalFileSystemURL(`_www/static/fonts/${fontRelativeUrl}`)})`;
|
||||
// #endif
|
||||
return fontUrl;
|
||||
}
|
||||
|
||||
// 优化后的字体加载函数
|
||||
function loadFonts() {
|
||||
// 防止重复加载
|
||||
if (fontLoadingState.value.isLoading || fontLoadingState.value.isLoaded) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
fontLoadingState.value.isLoading = true;
|
||||
|
||||
const fontConfigs = [
|
||||
{ name: "UberMoveRegular", file: "UberMoveRegular.ttf", weight: "normal" },
|
||||
{ name: "UberMoveMedium", file: "UberMoveMedium.otf", weight: "500" },
|
||||
{ name: "UberMoveBold", file: "UberMoveBold.otf", weight: "700" },
|
||||
];
|
||||
|
||||
const loadPromises = fontConfigs.map((config) => loadSingleFont(config));
|
||||
|
||||
return Promise.allSettled(loadPromises)
|
||||
.then((results) => {
|
||||
const successCount = results.filter(
|
||||
(result) => result.status === "fulfilled"
|
||||
).length;
|
||||
const failedCount = results.length - successCount;
|
||||
|
||||
if (successCount > 0) {
|
||||
fontLoadingState.value.isLoaded = true;
|
||||
console.log(`字体加载完成: ${successCount}/${results.length} 成功`);
|
||||
}
|
||||
|
||||
if (
|
||||
failedCount > 0 &&
|
||||
fontLoadingState.value.retryCount < fontLoadingState.value.maxRetries
|
||||
) {
|
||||
fontLoadingState.value.retryCount++;
|
||||
console.log(
|
||||
`字体加载失败 ${failedCount} 个,准备重试 (${fontLoadingState.value.retryCount}/${fontLoadingState.value.maxRetries})`
|
||||
);
|
||||
setTimeout(() => {
|
||||
fontLoadingState.value.isLoading = false;
|
||||
loadFonts();
|
||||
}, 1000 * fontLoadingState.value.retryCount);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
fontLoadingState.value.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 加载单个字体的函数
|
||||
function loadSingleFont(config: {
|
||||
name: string;
|
||||
file: string;
|
||||
weight: string;
|
||||
}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 检查是否已加载
|
||||
if (fontLoadingState.value.loadedFonts.has(config.name)) {
|
||||
resolve(config.name);
|
||||
return;
|
||||
}
|
||||
|
||||
const fontOptions: any = {
|
||||
family: "UberMove",
|
||||
source: getFontUrl(config.file),
|
||||
success: () => {
|
||||
fontLoadingState.value.loadedFonts.add(config.name);
|
||||
console.log(`字体加载成功: ${config.name}`);
|
||||
resolve(config.name);
|
||||
},
|
||||
fail: (error: any) => {
|
||||
console.error(`字体加载失败: ${config.name}`, error);
|
||||
reject(error);
|
||||
},
|
||||
};
|
||||
|
||||
if (config.weight !== "normal") {
|
||||
fontOptions.desc = { weight: config.weight };
|
||||
}
|
||||
|
||||
uni.loadFontFace(fontOptions);
|
||||
});
|
||||
}
|
||||
|
||||
useNetworkStatusChange(() => {
|
||||
if (!userStore.location.location) {
|
||||
userStore.getLocation();
|
||||
}
|
||||
nextTick(() => {
|
||||
// tabbarHomeRef.value?.initData()
|
||||
// tabbarShopRef.value?.initData()
|
||||
// tabbarShopRef.value?.init()
|
||||
// tabbarOrderRef.value?.initData()
|
||||
// tabBarMineRef.value?.init()
|
||||
});
|
||||
});
|
||||
|
||||
onLoad(async () => {
|
||||
uni.hideTabBar();
|
||||
|
||||
// 并行执行初始化任务
|
||||
const initTasks = [loadFonts(), userStore.getLocation(), getInviteInfo()];
|
||||
|
||||
try {
|
||||
await Promise.allSettled(initTasks);
|
||||
console.log("页面初始化完成");
|
||||
} catch (error) {
|
||||
console.error("页面初始化失败:", error);
|
||||
}
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
setTimeout(() => {
|
||||
appUpdate();
|
||||
}, 3000);
|
||||
// #endif
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
if (configStore.tabbarIndex) {
|
||||
activeIndex.value = configStore.tabbarIndex;
|
||||
}
|
||||
nextTick(() => {
|
||||
handleTabbarChange({ value: activeIndex.value });
|
||||
});
|
||||
|
||||
if (userStore.isLogin) {
|
||||
loadUserDataWithDebounce();
|
||||
|
||||
// 获取用户信息
|
||||
userStore.getUserInfo();
|
||||
if(+userStore.userInfo.hasPop !== 2) {
|
||||
getCouponReceiveListApi().then(res=> {
|
||||
if(res.code === 200 && res.data?.length > 0) {
|
||||
showCoupon.value = true;
|
||||
couponList.value = res.data || [];
|
||||
}
|
||||
})
|
||||
}
|
||||
// getCouponReceiveListApi().then(res=> {
|
||||
// showCoupon.value = true;
|
||||
// couponList.value = res.data || [];
|
||||
// })
|
||||
}
|
||||
});
|
||||
|
||||
// 防抖加载用户数据
|
||||
const loadUserDataWithDebounce = debounce(1000, () => {
|
||||
loadUserData();
|
||||
});
|
||||
|
||||
// 合并的用户数据加载函数
|
||||
async function loadUserData() {
|
||||
if (apiLoadingState.value.isLoadingUserData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (
|
||||
now - apiLoadingState.value.lastLoadTime <
|
||||
apiLoadingState.value.debounceDelay
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
apiLoadingState.value.isLoadingUserData = true;
|
||||
apiLoadingState.value.lastLoadTime = now;
|
||||
|
||||
try {
|
||||
// 并行执行多个接口调用
|
||||
const promises = [getUnreadMessageCount(), userStore.getAppointmentTime()];
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
console.log("用户数据加载完成");
|
||||
} catch (error) {
|
||||
console.error("用户数据加载失败:", error);
|
||||
} finally {
|
||||
apiLoadingState.value.isLoadingUserData = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取未读消息数量
|
||||
function getUnreadMessageCount() {
|
||||
return appMessageUnreadCountPost({})
|
||||
.then((res) => {
|
||||
userStore.unreadMessageCount = res.data || 0;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("获取未读消息数量失败:", error);
|
||||
});
|
||||
}
|
||||
|
||||
useEventEmit(EventEnum.CHOOSE_ADDRESS, (data) => {
|
||||
console.log("设置用户的定位地址信息", data);
|
||||
if (data) {
|
||||
userStore.userSetLocation = {
|
||||
location: data.displayName,
|
||||
longitude: data.location.lng,
|
||||
latitude: data.location.lat,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 获取邀请信息配置
|
||||
function getInviteInfo() {
|
||||
if (userStore.isLogin) {
|
||||
getDictFineList({
|
||||
dictType: "invite_poster",
|
||||
}).then((res) => {
|
||||
console.log("获取邀请信息", res);
|
||||
if (res.data.length > 0) {
|
||||
res.data.map((item) => {
|
||||
if (item.dictValue === "bg_image") {
|
||||
Config.shareImage = item.dictLabel;
|
||||
}
|
||||
if (item.dictValue === "invite_url") {
|
||||
Config.shareLink = item.dictLabel;
|
||||
}
|
||||
if (item.dictValue === "desc_text") {
|
||||
Config.shareDesc = item.dictLabel;
|
||||
}
|
||||
if (item.dictValue === "weixinServiceUrl") {
|
||||
Config.weixinServiceUrl = item.dictLabel;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取支付相关配置
|
||||
getDictFineList({
|
||||
dictType: "app_config",
|
||||
}).then((res) => {
|
||||
res.data.map((item) => {
|
||||
if (item.dictValue === "stripeKey") {
|
||||
Config.stripeKey = item.remark;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page">
|
||||
<tabbar-home
|
||||
ref="tabbarHomeRef"
|
||||
:scoreRange="scoreRange"
|
||||
:price="price"
|
||||
@toggleScore="toggleScore"
|
||||
@togglePrice="togglePrice"
|
||||
@toggleNotOpen="toggleNotOpen"
|
||||
v-show="activeIndex === 'home'"
|
||||
/>
|
||||
<tabbar-shop ref="tabbarShopRef" v-show="activeIndex === 'global'" />
|
||||
<tabbar-order ref="tabbarOrderRef" v-show="activeIndex === 'order'" />
|
||||
<tabbar-browse
|
||||
ref="tabbarBrowseRef"
|
||||
@toggleNotOpen="toggleNotOpen"
|
||||
v-show="activeIndex === 'browse'"
|
||||
/>
|
||||
<tabbar-mine
|
||||
ref="tabBarMineRef"
|
||||
v-show="activeIndex === 'mine'"
|
||||
@inviteUser="handleInviteUser"
|
||||
@customerService="handleCustomerService"
|
||||
@changeOrder="mineChangeOrder"
|
||||
/>
|
||||
<wd-tabbar
|
||||
custom-class="bg-#fff shadow-[0rpx_-6rpx_24rpx_rgba(0,0,0,0.1)]"
|
||||
fixed
|
||||
safeAreaInsetBottom
|
||||
placeholder
|
||||
v-model="activeIndex"
|
||||
@change="handleTabbarChange"
|
||||
activeColor="#333"
|
||||
inactiveColor="#999"
|
||||
>
|
||||
<wd-tabbar-item
|
||||
v-for="item in tabBarList"
|
||||
:key="item.code"
|
||||
:name="item.code"
|
||||
:title="item.text"
|
||||
>
|
||||
<template #icon>
|
||||
<view class="relative center">
|
||||
<wd-img
|
||||
class="absolute"
|
||||
custom-class="animate-in fade-in-50 zoom-in-80"
|
||||
height="46rpx"
|
||||
width="46rpx"
|
||||
v-show="activeIndex === item.code"
|
||||
:src="item.selectedIconPath"
|
||||
></wd-img>
|
||||
<wd-img
|
||||
class="absolute"
|
||||
custom-class="animate-in fade-in-50"
|
||||
height="46rpx"
|
||||
width="46rpx"
|
||||
:src="item.iconPath"
|
||||
v-show="activeIndex !== item.code"
|
||||
></wd-img>
|
||||
</view>
|
||||
</template>
|
||||
</wd-tabbar-item>
|
||||
</wd-tabbar>
|
||||
</view>
|
||||
<invite-user ref="inviteUserRef" />
|
||||
<use-code ref="useCodeRef" />
|
||||
<customer-service ref="customerServiceRef" />
|
||||
<!-- 星级筛选弹窗 -->
|
||||
<score @applyScore="applyScore" ref="scoreRef" />
|
||||
<price-choose @applyPrice="applyPrice" ref="priceChooseRef" />
|
||||
<not-open ref="notOpenRef" />
|
||||
<!-- 优惠券弹窗 -->
|
||||
<wd-popup
|
||||
v-model="showCoupon"
|
||||
position="center"
|
||||
custom-style="background:transparent;overflow:visible;padding:0rpx;"
|
||||
@close="handleClose"
|
||||
>
|
||||
<view class="relative w-640rpx h-874rpx">
|
||||
<image
|
||||
class="w-full h-874rpx absolute top-0 left-0"
|
||||
src="/static/images/5005.png"
|
||||
/>
|
||||
<view
|
||||
class="w-full h-874rpx absolute top-0 left-0"
|
||||
>
|
||||
<view
|
||||
class="w-full h-250rpx px-74rpx pt-30rpx text-#6d2d00 text-36rpx font-700 leading-[1.35]"
|
||||
>
|
||||
<view class="">{{ t('pages-store.store.tips-1') }}</view>
|
||||
<view class="mt-4rpx">{{ t('pages-store.store.tips-2') }}</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="w-full"
|
||||
>
|
||||
<view
|
||||
class="text-center text-30rpx text-#b36f2c font-600 tracking-wide flex-center-sb px-70rpx"
|
||||
>
|
||||
<image
|
||||
class="w-92rpx h-5rpx"
|
||||
src="@img/5006.png"
|
||||
/>
|
||||
{{ t('pages-store.store.congratulations') }}
|
||||
<image
|
||||
class="w-92rpx h-5rpx rotate-180"
|
||||
src="@img/5006.png"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="mt-32rpx h-420rpx px-62rpx box-border">
|
||||
<view
|
||||
v-for="item in couponList"
|
||||
:key="item.id"
|
||||
class="relative mb-24rpx rounded-24rpx border-2rpx border-#f5c78c bg-#fff7ed px-32rpx h-136rpx shadow-[0_8rpx_20rpx_rgba(0,0,0,0.08)]"
|
||||
>
|
||||
<image
|
||||
class="w-full h-full absolute top-0 left-0"
|
||||
src="/static/images/5007.png"
|
||||
/>
|
||||
<view class="w-full px-32rpx py-10rpx box-border absolute top-0 left-0">
|
||||
<view class="text-32rpx font-700 text-#c16a1c">
|
||||
<!-- 1-折扣券, 2-满减券-->
|
||||
<text v-if="+item.couponType === 1" class="">
|
||||
{{ Number(item.discountValue * 100).toFixed(0) }}% {{ t('pages-store.store.couponOff') }}
|
||||
</text>
|
||||
<text v-if="+item.couponType === 2" class="">
|
||||
${{ item.discountValue }} {{ t('pages-store.store.couponOff') }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="mt-4rpx text-24rpx text-#a0611d">
|
||||
{{ t('pages-store.store.validDays') }}: {{ item.validDays }} {{ t('pages-store.store.days') }}</view>
|
||||
<view class="mt-8rpx text-24rpx text-#a0611d">
|
||||
{{ item?.merchantVo?.merchantName }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<view v-if="couponList.length === 0" class="pt-60rpx center">
|
||||
<image class="w-250rpx h-250rpx" src="@img/16033@2x.png"></image>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="px-70rpx mt-12rpx">
|
||||
<view
|
||||
class="h-96rpx rounded-full bg-gradient-to-b from-[#7b3f1b] to-[#3f2210] text-#f7e5c6 text-32rpx font-700 center"
|
||||
@click="receiveAllCoupon"
|
||||
>
|
||||
{{ t('pages-store.store.tips-3') }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="absolute left-1/2 -bottom-120rpx -translate-x-1/2 w-88rpx h-88rpx center text-68rpx text-#fff"
|
||||
@click="handleClose"
|
||||
>
|
||||
<view class="i-carbon:close-outline"></view>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
<!-- 优惠券弹窗 -->
|
||||
</template>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<view
|
||||
class="animated-button"
|
||||
:class="[buttonClass, { 'scale-animation': isScaling }]"
|
||||
@click="handleClick"
|
||||
>
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
// 按钮的基础样式类
|
||||
buttonClass?: string
|
||||
// 是否禁用
|
||||
disabled?: boolean
|
||||
// 缩放比例,默认1.1
|
||||
scaleRatio?: number
|
||||
// 缩放动画持续时间,默认200ms
|
||||
scaleDuration?: number
|
||||
// 旋转动画持续时间,默认600ms
|
||||
rotateDuration?: number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'click', event: Event): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
buttonClass: '',
|
||||
disabled: false,
|
||||
scaleRatio: 1.1,
|
||||
scaleDuration: 200,
|
||||
rotateDuration: 600
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 动画状态
|
||||
const isScaling = ref(false)
|
||||
const isRotating = ref(false)
|
||||
const rotationCount = ref(0)
|
||||
|
||||
// 处理点击事件
|
||||
function handleClick(event: Event) {
|
||||
if (props.disabled) return
|
||||
|
||||
// 触发缩放动画
|
||||
isScaling.value = true
|
||||
setTimeout(() => {
|
||||
isScaling.value = false
|
||||
}, props.scaleDuration)
|
||||
|
||||
// 触发旋转动画
|
||||
isRotating.value = true
|
||||
rotationCount.value++
|
||||
setTimeout(() => {
|
||||
isRotating.value = false
|
||||
}, props.rotateDuration)
|
||||
|
||||
// 触发点击事件
|
||||
emit('click', event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.animated-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.scale-animation {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
// 禁用状态
|
||||
.animated-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<view class="">
|
||||
<!-- 筛选工具 -->
|
||||
<view class="filter-section">
|
||||
<view class="filter-list">
|
||||
<view
|
||||
v-for="i in 4"
|
||||
:key="i"
|
||||
class="filter-item-skeleton skeleton-item"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- All Merchants 区域 -->
|
||||
<view class="all-merchants-section">
|
||||
<view class="section-title-skeleton skeleton-item"></view>
|
||||
<view class="merchant-card-skeleton skeleton-item"></view>
|
||||
<view class="w-264rpx h-36rpx rounded-8rpx mt-20rpx skeleton-item"></view>
|
||||
<view class="w-264rpx h-36rpx rounded-8rpx mt-20rpx skeleton-item"></view>
|
||||
|
||||
<view class="merchant-card-skeleton skeleton-item mt-20rpx"></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'FoodSkeleton',
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 通用骨架屏样式
|
||||
.skeleton-item {
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
// 闪烁动画
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选工具
|
||||
.filter-section {
|
||||
padding: 32rpx 30rpx;
|
||||
|
||||
.filter-list {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
|
||||
.filter-item-skeleton {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All Merchants 区域
|
||||
.all-merchants-section {
|
||||
padding: 36rpx 30rpx 0rpx;
|
||||
|
||||
.section-title-skeleton {
|
||||
width: 224rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.merchant-card-skeleton {
|
||||
width: 690rpx;
|
||||
height: 766rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<view class="">
|
||||
<!-- 食品类型分类 -->
|
||||
<view class="food-type-section mt-36rpx">
|
||||
<view class="food-type-list">
|
||||
<view
|
||||
v-for="i in 5"
|
||||
:key="i"
|
||||
class="food-type-item-skeleton skeleton-item"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="flex-center-sb px-30rpx mb-36rpx">
|
||||
<view class="w-200rpx h-36rpx rounded-8rpx skeleton-item"></view>
|
||||
<view class="flex items-center gap-20rpx">
|
||||
<view class="w-200rpx h-64rpx rounded-64rpx px-24rpx skeleton-item"></view>
|
||||
<view class="w-200rpx h-64rpx rounded-64rpx px-24rpx skeleton-item"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="px-30rpx">
|
||||
<template v-for="(item, index) in 5">
|
||||
<view :class="[index === 0 ? '' : 'mt-52rpx']" class="flex items-center">
|
||||
<view class="skeleton-item w-210rpx h-200rpx mr-28rpx shrink-0 rounded-24rpx"></view>
|
||||
<view class="w-full py-10rpx h-200rpx flex flex-col justify-between flex-1">
|
||||
<view class="w-100% h-36rpx rounded-8rpx skeleton-item"></view>
|
||||
<view class="w-50% h-36rpx rounded-8rpx skeleton-item"></view>
|
||||
<view class="w-60% h-36rpx rounded-8rpx skeleton-item"></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'FoodSkeleton',
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 通用骨架屏样式
|
||||
.skeleton-item {
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
// 闪烁动画
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 食品类型分类
|
||||
.food-type-section {
|
||||
padding: 22rpx 30rpx;
|
||||
|
||||
.food-type-list {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
overflow-x: auto;
|
||||
|
||||
.food-type-item-skeleton {
|
||||
flex-shrink: 0;
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 56rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<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>
|
||||
@@ -0,0 +1,128 @@
|
||||
<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>
|
||||
|
||||
<!-- 最近搜索区域 -->
|
||||
<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>
|
||||
</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>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 搜索页面骨架屏组件
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 通用骨架屏样式
|
||||
.skeleton-item {
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
// 闪烁动画
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.search-skeleton {
|
||||
background-color: #fff;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
// 状态栏
|
||||
.status-bar {
|
||||
height: 88rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// 头部区域
|
||||
.header-section {
|
||||
padding: 0 30rpx 40rpx;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 最近搜索区域
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 热门分类区域
|
||||
.popular-section {
|
||||
padding: 40rpx 30rpx 0;
|
||||
|
||||
.section-title-skeleton {
|
||||
width: 300rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 750rpx) {
|
||||
.category-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n();
|
||||
import {useConfigStore} from "@/store";
|
||||
const configStore = useConfigStore();
|
||||
const show = ref(false);
|
||||
const emit = defineEmits(['apply']);
|
||||
|
||||
const sortOptions = [
|
||||
{
|
||||
label: t('components.searchSort.time'),
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: t('components.searchSort.comment'),
|
||||
value: 2
|
||||
},
|
||||
{
|
||||
label: t('components.searchSort.thumbsUp'),
|
||||
value: 3
|
||||
},
|
||||
{
|
||||
label: t('components.searchSort.view'),
|
||||
value: 4
|
||||
}
|
||||
];
|
||||
const currentSort = ref(sortOptions[0].value);
|
||||
function handleClick(item: typeof sortOptions[0]) {
|
||||
// show.value = false;
|
||||
currentSort.value = item.value;
|
||||
}
|
||||
|
||||
function handleClickApply() {
|
||||
emit('apply', currentSort.value);
|
||||
show.value = false;
|
||||
}
|
||||
|
||||
function onOpen() {
|
||||
show.value = true;
|
||||
}
|
||||
function handleClose() {
|
||||
show.value = false;
|
||||
}
|
||||
defineExpose({
|
||||
onOpen,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wd-popup
|
||||
v-model="show"
|
||||
position="bottom"
|
||||
@close="handleClose"
|
||||
>
|
||||
<view class="pl-30rpx pt-50rpx">
|
||||
<view class="text-40rpx lh-40rpx text-#333 font-bold text-center mb-20rpx">{{ t('components.searchSort.title') }}</view>
|
||||
<template v-for="(item, index) in sortOptions">
|
||||
<view @click="handleClick(item)" class="flex items-center h-114rpx">
|
||||
<view class="w-48rpx h-48rpx shrink-0 mr-28rpx">
|
||||
<image
|
||||
:src="
|
||||
item.value === currentSort
|
||||
? '/static/images/chef/133.png'
|
||||
: '/static/images/chef/134.png'
|
||||
"
|
||||
class="w-48rpx h-48rpx"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<view :class="[index === sortOptions.length - 1 ? '' : '']" class="flex-1 text-36rpx lh-36rpx text-#333 font-500 h-114rpx flex items-center">{{ item.label }}</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
<view class="mt-38rpx px-30rpx pb-60rpx">
|
||||
<wd-button @click="handleClickApply" custom-class="!h-108rpx !bg-14181B !text-36rpx !lh-108rpx !text-#fff !rounded-16rpx" block>
|
||||
{{ t('common.apply') }}
|
||||
</wd-button>
|
||||
<view @click="handleClose" class="text-center mt-52rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('common.close') }}</view>
|
||||
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,289 @@
|
||||
<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 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";
|
||||
import FoodSkeleton from "@/pages/search/components/food-skeleton.vue";
|
||||
import RecipeSkeleton from "@/pages/search/components/recipe-skeleton.vue";
|
||||
import {appRecipeCategoryListGet, appSearchMerchantByDishPost, appSearchSearchRecipePost, appCollectCollectPost} from "@/service";
|
||||
import {formatTimestamp} from "@/utils/utils";
|
||||
import {useUserStore} from "@/store";
|
||||
import { debounce } from 'throttle-debounce'
|
||||
import {CollectionType} from "@/constant/enums";
|
||||
const props = defineProps<{
|
||||
activeTab: number;
|
||||
currentIndex: number;
|
||||
keyword?: string;
|
||||
currentSort: number;
|
||||
scoreRange?: string;
|
||||
price?: string;
|
||||
}>();
|
||||
const emit = defineEmits(["toggleScore", "togglePrice", "toggleSort", "reset"]);
|
||||
const { t } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 菜谱数据总条数
|
||||
const recipeTotal = ref(0);
|
||||
// 美食数据总条数
|
||||
const foodTotal = ref(0);
|
||||
// 是否自提
|
||||
const selfPickup = ref<number | null>(null)
|
||||
function togglePickup(value: number) {
|
||||
selfPickup.value = value;
|
||||
refresh()
|
||||
}
|
||||
// 是否有折扣
|
||||
const discount = ref<number | null>(null)
|
||||
function toggleDiscount(value: number) {
|
||||
discount.value = value;
|
||||
refresh()
|
||||
}
|
||||
const { paging, dataList, loading, queryList, firstLoaded } = usePage((pageNum, pageSize) => {
|
||||
if(props.activeTab === 0) {
|
||||
// 搜索美食
|
||||
return new Promise(resolve => {
|
||||
appSearchMerchantByDishPost({
|
||||
body: {
|
||||
keyword: props.keyword || '', // 搜索关键词
|
||||
pageNum: pageNum,
|
||||
pageSize: pageSize,
|
||||
selfPickup: selfPickup.value, // 是否自提
|
||||
discount: discount.value, // 是否有折扣 1是 2 否
|
||||
scoreRange: props.scoreRange || null, // 评分范围 比如 3-4
|
||||
priceRange: props.price || null, // 价格范围 比如 10-30
|
||||
sortType: 1, // 1系统推荐(按照喜好) 2 距离排序 3 评分排序
|
||||
lat: userStore.userLocation.latitude,
|
||||
lng: userStore.userLocation.longitude,
|
||||
}
|
||||
}).then(res => {
|
||||
foodTotal.value = res.total; // 更新美食数据总条数
|
||||
resolve({rows: res.rows})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// 搜索菜谱
|
||||
return new Promise(resolve => {
|
||||
appSearchSearchRecipePost({
|
||||
body: {
|
||||
keyword: props.keyword || '', // 搜索关键词
|
||||
pageNum: pageNum,
|
||||
pageSize: pageSize,
|
||||
recipeCategoryId: recipeCategoryId.value, // 菜谱分类ID
|
||||
sortType: props.currentSort // 排序方式
|
||||
}
|
||||
}).then(res => {
|
||||
recipeTotal.value = res.total; // 更新菜谱数据总条数
|
||||
resolve({rows: res.rows})
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
// 食物加载状态
|
||||
const loadingFoodState = ref(true)
|
||||
// 菜谱加载状态
|
||||
const loadingRecipeState = ref(true)
|
||||
watch(() => props.activeTab, (val) => {
|
||||
if(val === props.currentIndex && props.activeTab === 0) {
|
||||
if(paging.value) {
|
||||
paging.value.refresh()
|
||||
}
|
||||
}
|
||||
if (+val === 0) {
|
||||
setTimeout(() => {
|
||||
loadingFoodState.value = true
|
||||
}, 100)
|
||||
setTimeout(() => {
|
||||
loadingFoodState.value = false
|
||||
}, 400)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
loadingRecipeState.value = true
|
||||
}, 100)
|
||||
setTimeout(() => {
|
||||
loadingRecipeState.value = false
|
||||
}, 400)
|
||||
if(val === props.currentIndex) {
|
||||
getRecipeCategory()
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 获取菜谱分类数据
|
||||
const allRecipeCategoryList = ref([])
|
||||
const recipeCategoryId = ref('')
|
||||
function getRecipeCategory() {
|
||||
appRecipeCategoryListGet({}).then(res => {
|
||||
console.log('菜谱分类数据', res)
|
||||
allRecipeCategoryList.value = res.data;
|
||||
if (res.data.length > 0) {
|
||||
recipeCategoryId.value = res.data[0].id; // 默认选中第一个分类
|
||||
refresh()
|
||||
} else {
|
||||
recipeCategoryId.value = ''; // 没有分类时清空
|
||||
}
|
||||
})
|
||||
}
|
||||
function tabsTypeChange(data){
|
||||
console.log('选中的菜谱分类ID', data)
|
||||
recipeCategoryId.value = data;
|
||||
refresh()
|
||||
}
|
||||
|
||||
// 重置按钮旋转状态
|
||||
const rotationCount = ref(0)
|
||||
|
||||
// 处理重置按钮点击
|
||||
function handleReset() {
|
||||
rotationCount.value++
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
paging.value.refresh()
|
||||
}
|
||||
|
||||
// 收藏菜品
|
||||
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, // 立即触发
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<z-paging
|
||||
ref="paging"
|
||||
v-model="dataList"
|
||||
@query="queryList"
|
||||
:fixed="false"
|
||||
:auto="false"
|
||||
bg-color="#ffffff"
|
||||
>
|
||||
<template v-if="activeTab === 0">
|
||||
<view
|
||||
class="animate-in fade-in animate-ease-out animate-duration-300"
|
||||
v-show="loadingFoodState"
|
||||
>
|
||||
<food-skeleton />
|
||||
</view>
|
||||
<view
|
||||
class="animate-in fade-in animate-ease-in animate-duration-300"
|
||||
v-show="!loadingFoodState"
|
||||
>
|
||||
<!-- 筛选工具 -->
|
||||
<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>
|
||||
</view>
|
||||
</template>
|
||||
<template v-if="activeTab === 1">
|
||||
<view
|
||||
class="animate-in fade-in animate-ease-out animate-duration-300"
|
||||
v-show="loadingRecipeState"
|
||||
>
|
||||
<recipe-skeleton />
|
||||
</view>
|
||||
<view
|
||||
class="animate-in fade-in animate-ease-in animate-duration-300"
|
||||
v-show="!loadingRecipeState"
|
||||
>
|
||||
|
||||
<!-- 分类滚动区域 -->
|
||||
<tabs-type @changeType="tabsTypeChange" :currentId="recipeCategoryId" :list="allRecipeCategoryList" labelKey="categoryName" imgKey="categoryImage" class="my-36rpx" />
|
||||
<!-- 筛选结果 -->
|
||||
<view class="flex-center-sb px-30rpx mb-36rpx">
|
||||
<view class="text-36rpx lh-36rpx text-#333 font-500 tracking-[.04em]"
|
||||
>{{ recipeTotal }} {{ t('pages.search.result.result') }}</view
|
||||
>
|
||||
<view class="flex items-center gap-20rpx">
|
||||
<view
|
||||
@click="emit('toggleSort')"
|
||||
class="bg-#F2F2F2 h-64rpx rounded-64rpx px-24rpx center text-26rpx text-#333 lh-26rpx font-500"
|
||||
>
|
||||
<text v-if="+props.currentSort === 1">{{ t('components.searchSort.time') }}</text>
|
||||
<text v-if="+props.currentSort === 2">{{ t('components.searchSort.comment') }}</text>
|
||||
<text v-if="+props.currentSort === 3">{{ t('components.searchSort.thumbsUp') }}</text>
|
||||
<text v-if="+props.currentSort === 4">{{ t('components.searchSort.view') }}</text>
|
||||
<image
|
||||
src="@img/chef/101.png"
|
||||
class="w-24rpx h-24rpx ml-10rpx"
|
||||
></image
|
||||
>
|
||||
</view>
|
||||
|
||||
<animated-button
|
||||
button-class="bg-#F2F2F2 h-64rpx rounded-64rpx px-24rpx center text-26rpx text-#333 lh-26rpx font-500"
|
||||
@click="handleReset"
|
||||
>
|
||||
{{ t('common.reset') }}
|
||||
<image
|
||||
src="@img/chef/131.png"
|
||||
class="w-24rpx h-24rpx ml-10rpx transition-transform duration-600"
|
||||
:style="{ transform: `rotate(${rotationCount * 360}deg)` }"
|
||||
></image>
|
||||
</animated-button>
|
||||
</view>
|
||||
</view>
|
||||
<template v-for="(item,index) in dataList">
|
||||
<view :class="[index === 0 ? '' : 'mt-52rpx']" class="flex-center-sb px-30rpx">
|
||||
<image
|
||||
:src="item?.recipeImage?.split(',')[0]"
|
||||
mode="aspectFill"
|
||||
class="w-210rpx h-200rpx mr-28rpx shrink-0 rounded-24rpx"
|
||||
></image>
|
||||
<view class="flex-1 h-200rpx">
|
||||
<view class="text-36rpx lh-40rpx text-#333 font-500 mb-40rpx line-clamp-1 tracking-[.04em]">{{ item.recipeName }}</view>
|
||||
<view class="text-30rpx lh-30rpx text-#333 tracking-[.04em]">
|
||||
{{ formatTimestamp(item.createTime) }}
|
||||
</view>
|
||||
<view class="mt-42rpx flex items-center gap-30rpx text-#999 text-30rpx">
|
||||
<view class="flex items-center gap-10rpx">
|
||||
<collection :is-collected="item.isCollect" @collectionChange="handleSubmitCollectRecipe(item)" />
|
||||
{{ item.collectCount }}
|
||||
</view>
|
||||
<view class="flex items-center gap-10rpx">
|
||||
<image
|
||||
src="@img/chef/132.png"
|
||||
class="w-40rpx h-40rpx"
|
||||
></image>
|
||||
{{ item.commentCount }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
</z-paging>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 确保过渡动画平滑
|
||||
.transition-transform {
|
||||
transition: transform 0.6s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<view class="tab-switcher">
|
||||
<view class="tab-container">
|
||||
<view
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
class="tab-item"
|
||||
:class="{ 'active': activeIndex === index }"
|
||||
@click="handleTabClick(index)"
|
||||
>
|
||||
<text class="tab-text" :class="textClass">{{ tab.label }}</text>
|
||||
</view>
|
||||
<!-- 底部跟随横条 -->
|
||||
<view
|
||||
class="active-indicator"
|
||||
:style="{ transform: `translateX(${activeIndex * 100}%)` }"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Tab {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
tabs?: Tab[]
|
||||
modelValue?: number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: number): void
|
||||
(e: 'change', value: number): void
|
||||
}
|
||||
|
||||
// const props = withDefaults(<Props>(), {
|
||||
// tabs: () => [
|
||||
// { label: '美食', value: 'food' },
|
||||
// { label: '菜谱', value: 'recipe' }
|
||||
// ],
|
||||
// modelValue: 0,
|
||||
// textClass: 'text-40rpx'
|
||||
// })
|
||||
const props = defineProps({
|
||||
tabs: {
|
||||
type: Array as () => Tab[],
|
||||
default: () => [
|
||||
{ label: 'Tab 1', value: 'tab1' },
|
||||
{ label: 'Tab 2', value: 'tab2' }
|
||||
]
|
||||
},
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
textClass: {
|
||||
type: String,
|
||||
default: 'text-40rpx'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const activeIndex = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
function handleTabClick(index: number) {
|
||||
activeIndex.value = index
|
||||
emit('change', index)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tab-switcher {
|
||||
@apply w-full bg-white;
|
||||
}
|
||||
|
||||
.tab-container {
|
||||
@apply relative flex items-center border-b-solid border-b-10rpx border-b-#F6F6F6;
|
||||
// @apply px-30rpx py-20rpx;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
@apply relative flex items-center justify-center;
|
||||
@apply w-180rpx h-80rpx pb-24rpx cursor-pointer;
|
||||
@apply transition-all duration-300 ease-out;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
@apply transition-colors duration-300;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.tab-item.active .tab-text {
|
||||
@apply font-bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.active-indicator {
|
||||
@apply absolute bottom--10rpx left-0;
|
||||
@apply w-180rpx h-10rpx;
|
||||
@apply bg-#333;
|
||||
@apply transition-transform duration-300 ease-out;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<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} from '@/service'
|
||||
|
||||
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'})
|
||||
}
|
||||
searchStore.setHistoryList(keyword.value)
|
||||
uni.navigateTo({
|
||||
url: `/pages/search/result?keyword=${keyword.value}`,
|
||||
})
|
||||
keyword.value = ''
|
||||
})
|
||||
}
|
||||
|
||||
function handleHotSearch(item: any) {
|
||||
nextTick(() => {
|
||||
searchStore.setHistoryList(item.name)
|
||||
uni.navigateTo({
|
||||
url: `/pages/search/result?keyword=${item.name}`,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const loading = ref(true)
|
||||
onMounted(() => {
|
||||
loading.value = true
|
||||
// 查询热门搜索词列表
|
||||
getHotSearchList()
|
||||
})
|
||||
|
||||
const hotSearchList = ref([])
|
||||
function getHotSearchList() {
|
||||
appSearchListPost({}).then(res=> {
|
||||
console.log('热门搜索词列表', res)
|
||||
hotSearchList.value = res.data
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="">
|
||||
<view
|
||||
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"
|
||||
>
|
||||
<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.logoUrl" 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.name }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
import { useSearchStore } from "@/store";
|
||||
import TabSwitcher from "./components/tab-switcher.vue";
|
||||
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<{
|
||||
keyword?: string;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const searchStore = useSearchStore();
|
||||
|
||||
const keyword = ref(props.keyword || "");
|
||||
|
||||
function handleSearch() {
|
||||
nextTick(() => {
|
||||
if (keyword.value) {
|
||||
searchStore.setHistoryList(keyword.value);
|
||||
}
|
||||
if (swiperListRef.value) {
|
||||
swiperListRef.value[activeTab.value].refresh()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const swiperListRef = ref<InstanceType<typeof SwiperList>>()
|
||||
const activeTab = ref(0);
|
||||
const tabs = [
|
||||
{ label: t('pages.search.result.food'), value: "food" },
|
||||
{ label: t('pages.search.result.recipe'), value: "recipe" },
|
||||
];
|
||||
function handleTabChange(index: number) {
|
||||
console.log("切换到标签:", index);
|
||||
}
|
||||
|
||||
const scoreRef = ref<InstanceType<typeof Score>>()
|
||||
const priceChooseRef = ref<InstanceType<typeof PriceChoose>>()
|
||||
const sortPopupRef = ref<InstanceType<typeof SortPopup>>()
|
||||
function toggleScore() {
|
||||
if (scoreRef.value) {
|
||||
scoreRef.value.onOpen()
|
||||
}
|
||||
}
|
||||
const scoreRange = ref(''); // 星级评分
|
||||
const price = ref(''); // 价格范围
|
||||
function applyScore(value: string) {
|
||||
console.log('applyScore', value)
|
||||
scoreRange.value = value;
|
||||
nextTick(()=> {
|
||||
if (swiperListRef.value) {
|
||||
swiperListRef.value[0].refresh()
|
||||
}
|
||||
})
|
||||
}
|
||||
function togglePrice() {
|
||||
if (priceChooseRef.value) {
|
||||
priceChooseRef.value.onOpen()
|
||||
}
|
||||
}
|
||||
function applyPrice(value: string) {
|
||||
console.log('applyPrice', value)
|
||||
price.value = value;
|
||||
nextTick(()=> {
|
||||
if (swiperListRef.value) {
|
||||
swiperListRef.value[0].refresh()
|
||||
}
|
||||
})
|
||||
}
|
||||
function toggleSort() {
|
||||
if (sortPopupRef.value) {
|
||||
sortPopupRef.value.onOpen()
|
||||
}
|
||||
}
|
||||
|
||||
const currentSort = ref(1); // 默认排序方式
|
||||
function handleApply(sort: number) {
|
||||
console.log('sort', sort)
|
||||
currentSort.value = sort;
|
||||
nextTick(()=> {
|
||||
if (swiperListRef.value) {
|
||||
swiperListRef.value[1].refresh()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function recipeReset() {
|
||||
handleApply(1)
|
||||
}
|
||||
|
||||
onMounted(()=> {
|
||||
if (swiperListRef.value) {
|
||||
swiperListRef.value[0].refresh()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="">
|
||||
<z-paging-swiper>
|
||||
<template #top>
|
||||
<header-search
|
||||
class="pb-42rpx"
|
||||
v-model="keyword"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<tab-switcher
|
||||
v-model="activeTab"
|
||||
:tabs="tabs"
|
||||
@change="handleTabChange"
|
||||
/>
|
||||
</template>
|
||||
<swiper
|
||||
class="h-full"
|
||||
:current="activeTab"
|
||||
disable-touch
|
||||
>
|
||||
<swiper-item
|
||||
class="swiper-item"
|
||||
v-for="(item, index) in tabs"
|
||||
:key="index"
|
||||
>
|
||||
<swiper-list ref="swiperListRef"
|
||||
:activeTab="activeTab"
|
||||
:currentIndex="index"
|
||||
:currentSort="currentSort"
|
||||
:keyword="keyword"
|
||||
:scoreRange="scoreRange"
|
||||
:price="price"
|
||||
@toggleScore="toggleScore"
|
||||
@togglePrice="togglePrice"
|
||||
@toggleSort="toggleSort"
|
||||
@reset="recipeReset"
|
||||
></swiper-list>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</z-paging-swiper>
|
||||
|
||||
<!-- 星级筛选弹窗 -->
|
||||
<score @applyScore="applyScore" ref="scoreRef" />
|
||||
<price-choose @applyPrice="applyPrice" ref="priceChooseRef" />
|
||||
<!-- 菜谱筛选弹窗 -->
|
||||
<sort-popup ref="sortPopupRef" @apply="handleApply" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.wd-tabs__nav-item) {
|
||||
font-size: 40rpx;
|
||||
color: #666666;
|
||||
}
|
||||
:deep(.wd-tabs) {
|
||||
.wd-tabs__line {
|
||||
bottom: 0 !important;
|
||||
z-index: 99 !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<view>
|
||||
<web-view
|
||||
:src="url"
|
||||
class="webview"
|
||||
:webview-styles="{
|
||||
progress: {
|
||||
color: '#2D2D2D',
|
||||
},
|
||||
}"
|
||||
></web-view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
const url = ref('')
|
||||
|
||||
onLoad((option) => {
|
||||
url.value = decodeURIComponent(option.url)
|
||||
if(option.title){
|
||||
uni.setNavigationBarTitle({
|
||||
title: option.title
|
||||
})
|
||||
}
|
||||
console.log('=>(wx-password.vue:13) url', option)
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="scss"></style>
|
||||
Reference in New Issue
Block a user