Files
cheflinkuser/src/pages-store/pages/store/index.vue
T
2026-04-11 11:55:03 +08:00

1091 lines
31 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { debounce } from 'throttle-debounce'
import dayjs from 'dayjs'
import Config from '@/config/index'
const { t, locale } = useI18n();
import StoreSkeleton from './components/store-skeleton.vue';
import { useScrollThreshold } from '@/hooks/useScrollThreshold'
import {
appCollectCollectPost, appMerchantCartCalculateSavingsPost, appMerchantCartListByMerchantIdPost,
appMerchantDetailMerchantIdGet,
appMerchantDishDishIdGet,
appMerchantMenuMenuListByUserPost, type MerchantCartVo,
type MerchantVo
} from "@/service";
import {CollectionType} from "@/constant/enums";
import {useUserStore} from "@/store";
import { parseBusinessHoursUtils, getDistanceInMiles } from "@/utils/utils";
import CouponPopup from './components/coupon-popup.vue'
import {getMerchantCouponReceiveListApi} from "@/pages-user/service";
// import type { MerchantVo } from '@/service/types'
// 页面加载状态
const loading = ref(true);
const userStore = useUserStore();
const storeID = ref('')
onLoad((options: any)=> {
console.log(options)
if(options.id) {
storeID.value = options.id
getStoreDetail()
// getMenuList()
}
})
// 定时器用于更新闭店提示
let closingTimer: NodeJS.Timeout | null = null
// 启动闭店提示定时器
function startClosingTimer() {
if (closingTimer) {
clearInterval(closingTimer)
}
closingTimer = setInterval(() => {
if (storeDetail.value.businessHours) {
closingInfo.value = parseBusinessHours(storeDetail.value.businessHours)
// 如果不再显示提示,清除定时器
if (!closingInfo.value.show) {
clearInterval(closingTimer!)
closingTimer = null
}
}
}, 60000) // 每分钟更新一次
}
// 停止闭店提示定时器
function stopClosingTimer() {
if (closingTimer) {
clearInterval(closingTimer)
closingTimer = null
}
}
onShow(()=> {
nextTick(()=> {
if(storeID.value) {
// 查询当前店铺购物车信息
getCartInfo()
}
})
})
// 页面卸载时清除定时器
onUnmounted(() => {
stopClosingTimer()
})
// 获取商家详情信息
const storeDetail = ref<MerchantVo>({})
// 闭店提示信息
const closingInfo = ref({ show: false, minutes: 0 })
// 解析营业时间并判断是否在闭店前30分钟
function parseBusinessHours(businessHours: string) {
if (!businessHours) return { show: false, minutes: 0 }
const now = dayjs()
const currentDay = now.day() // 0=Sunday, 1=Monday, ..., 6=Saturday
const dayNames = [t('pages-store.store.weekdays.sunday'), t('pages-store.store.weekdays.monday'), t('pages-store.store.weekdays.tuesday'), t('pages-store.store.weekdays.wednesday'), t('pages-store.store.weekdays.thursday'), t('pages-store.store.weekdays.friday'), t('pages-store.store.weekdays.saturday')]
const todayName = dayNames[currentDay]
// 检查今天是否营业
if (!businessHours.includes(todayName)) {
return { show: false, minutes: 0 }
}
// 提取时间范围,格式如:05:00-16:00
const timeMatch = businessHours.match(/(\d{2}):(\d{2})-(\d{2}):(\d{2})/)
if (!timeMatch) {
return { show: false, minutes: 0 }
}
const [, startHour, startMin, endHour, endMin] = timeMatch
// 使用dayjs创建今天的开店和闭店时间
const openTime = now.hour(parseInt(startHour)).minute(parseInt(startMin)).second(0)
const closeTime = now.hour(parseInt(endHour)).minute(parseInt(endMin)).second(0)
// 如果闭店时间小于开店时间,说明跨天营业,闭店时间应该是明天
const actualCloseTime = closeTime.isBefore(openTime) ? closeTime.add(1, 'day') : closeTime
// 检查是否在营业时间内
const isOpen = now.isAfter(openTime) && now.isBefore(actualCloseTime)
if (!isOpen) {
return { show: false, minutes: 0 }
}
// 计算闭店前30分钟的时间点
const thirtyMinsBefore = actualCloseTime.subtract(30, 'minute')
// 判断是否在闭店前30分钟内
if (now.isAfter(thirtyMinsBefore) && now.isBefore(actualCloseTime)) {
const minutesLeft = actualCloseTime.diff(now, 'minute')
return { show: true, minutes: minutesLeft }
}
return { show: false, minutes: 0 }
}
const storeDistance = ref(null)
function getStoreDetail() {
loading.value = true
appMerchantDetailMerchantIdGet({
params: {
merchantId: storeID.value,
}
}).then((res: any) => {
console.log('商家详情', res)
storeDetail.value = res.data as MerchantVo
getMerchantCouponReceiveList()
// 解析营业时间并判断闭店提示
if (res.data.businessHours) {
closingInfo.value = parseBusinessHours(res.data.businessHours)
// 如果需要显示闭店提示,启动定时器
if (closingInfo.value.show) {
startClosingTimer()
}
}
if(res.data.merchantCategoryIds && res.data.merchantCategoryIds.length > 0) {
tabs.value = [
{
title: t('pages.store.all'),
key: ''
},
...res.data.merchantCategoryIds.map((item) => {
return {
title: item.menuName,
key: item.id
}
})
]
}
// 用详情中的首屏菜品初始化列表,并让下一次触底从第 2 页开始,避免仍请求第 1 页导致与详情数据重复
const firstRecords = res.data?.dishPage?.records
if (firstRecords && firstRecords.length > 0) {
dishListByQuery.value = [...firstRecords]
const gotFullPage = firstRecords.length >= pageSize.value
hasMore.value = gotFullPage
pageNum.value = gotFullPage ? 2 : 1
} else if (tabs.value.length > 0) {
dishListByQuery.value = []
hasMore.value = true
pageNum.value = 1
nextTick(() => loadDishList(false))
}
// 商户的经纬度存在,并且用户的经纬度也存在
if(res.data.latitude && res.data.longitude && userStore.userLocation.latitude && userStore.userLocation.longitude) {
let distance = getDistanceInMiles(res.data.latitude, res.data.longitude, userStore.userLocation.latitude, userStore.userLocation.longitude)
console.log('距离商家距离', distance)
storeDistance.value = distance
}
// 判断配送和自取的开通状态
const hasDelivery = +storeDetail.value.deliveryService === 1
const hasPickup = +storeDetail.value.selfPickup === 1
if (!hasDelivery && !hasPickup) {
// 两个都没开通,不显示切换组件
showDeliverySwitch.value = false
} else if (!hasDelivery && hasPickup) {
// 只开通自取,默认选中自取
deliveryMethod.value = 1
showDeliverySwitch.value = true
} else if (hasDelivery && !hasPickup) {
// 只开通配送,默认选中配送
deliveryMethod.value = 0
showDeliverySwitch.value = true
} else {
// 两个都开通,默认选中配送
deliveryMethod.value = 0
showDeliverySwitch.value = true
}
}).catch((error) => {
console.error('获取商家详情失败:', error);
}).finally(()=> {
loading.value = false
})
}
const storeCouponList = ref([])
function getMerchantCouponReceiveList() {
getMerchantCouponReceiveListApi(storeID.value).then((res: any)=> {
console.log('商家优惠券列表', res)
storeCouponList.value = res.data
})
}
const cartDataList = ref<MerchantCartVo[]>([])
function getCartInfo() {
if(!userStore.isLogin) return
appMerchantCartListByMerchantIdPost({
params: {
merchantId: storeID.value,
}
}).then((res: any)=> {
console.log('购物车列表', res)
cartDataList.value = res.data
// 购物车有菜品,查询菜品会员折扣价
if(cartDataList.value.length > 0) {
appMerchantCartCalculateSavings()
}
})
}
// 查询菜品会员折扣价
const cartSavingsData = ref({})
function appMerchantCartCalculateSavings() {
appMerchantCartCalculateSavingsPost({
body: cartDataList.value.map(item => item.id)
}).then(res=> {
console.log('菜品会员折扣价', res)
cartSavingsData.value = res.data
})
}
// 优惠券列表(静态数据)
const couponList = ref([
{ id: 1, text: '20% Off' },
{ id: 2, text: '30% Off' },
{ id: 3, text: '$5 Off' },
{ id: 4, text: '40% Off' },
])
// 跳转到优惠券页面
const couponPopupRef = ref()
function handleClaimNow() {
couponPopupRef.value.init()
}
// 用户查询菜单列表
function getMenuList() {
appMerchantMenuMenuListByUserPost({
params: {
merchantId: storeID.value,
}
}).then(res=> {
console.log('商家菜单列表', res)
// 这里可以处理商家菜单列表数据
}).catch(error => {
console.error('获取商家菜单列表失败:', error);
})
}
// 配送方式
const deliveryMethod = ref(0);
const deliveryMethodOptions = [t('pages-store.store.delivery'), t('pages-store.store.pickup')];
const showDeliverySwitch = ref(true); // 是否显示配送方式切换组件
function handleClickSegmented(index: number) {
console.log("切换配送方式:", index);
if(+storeDetail.value.deliveryService !== 1 && index === 0) {
uni.showToast({
title: t('pages-store.store.toast.deliveryService'),
icon: 'none'
})
return
}
if(+storeDetail.value.selfPickup !== 1 && index === 1) {
uni.showToast({
title: t('pages-store.store.toast.selfPickup'),
icon: 'none'
})
return
}
deliveryMethod.value = index
}
const activeTab = ref(0);
const tabs = ref<any[]>([]);
function daySuffix(value: unknown) {
const n = Number(value)
const isEn = String(locale.value || '').toLowerCase().startsWith('en')
if (isEn) {
return n === 1 ? ` ${t('pages-store.store.day')}` : ` ${t('pages-store.store.days')}`
}
return t('pages-store.store.days')
}
// 分页相关状态
const pageNum = ref(1);
const pageSize = ref(10);
const hasMore = ref(true);
const isLoadingMore = ref(false);
const dishListByQuery = ref<any[]>([]);
// 计算当前显示的商品列表(统一走 dishListByQuery,避免「全部」首屏用详情、加载更多再拼第 1 页造成重复)
const currentDishList = computed(() => dishListByQuery.value || [])
// 加载菜品列表
async function loadDishList(isLoadMore = false) {
if (isLoadingMore.value) return;
const currentTab = tabs.value[activeTab.value];
if (!currentTab) return;
try {
isLoadingMore.value = true;
// 非加载更多场景下,固定先请求第 1 页,并将内部 pageNum 重置为 1
if (!isLoadMore) {
pageNum.value = 1;
}
// 构建请求参数
const body: any = {
merchantId: storeID.value,
pageNum: isLoadMore ? pageNum.value : 1,
pageSize: pageSize.value
};
// 如果选中的 tab 的 key 不为空,则传递 menuId
if (currentTab.key !== '') {
body.menuId = currentTab.key;
}
console.log('加载菜品列表参数', body);
const res = await appMerchantDishDishIdGet({ body });
if (res.data && res.data.rows) {
if (isLoadMore) {
// 加载更多:按 id 去重,防止接口分页重叠或重复请求时的重复项
const existingIds = new Set(dishListByQuery.value.map((r: any) => r.id))
const nextRows = res.data.rows.filter((r: any) => r != null && !existingIds.has(r.id))
dishListByQuery.value = [...dishListByQuery.value, ...nextRows]
} else {
// 首次加载或刷新
dishListByQuery.value = res.data.rows;
}
const gotFullPage = res.data.rows.length >= pageSize.value;
// 更新分页信息
hasMore.value = gotFullPage;
if (isLoadMore) {
// 下一次请求再使用下一页页码
if (gotFullPage) {
pageNum.value++;
}
} else {
// 首次/刷新后,下一次「加载更多」应该从第 2 页开始
pageNum.value = gotFullPage ? 2 : 1;
}
} else {
hasMore.value = false;
}
} catch (error) {
console.error('加载菜品列表失败:', error);
} finally {
isLoadingMore.value = false;
}
}
// 监听 tab 切换,重置数据并加载当前 tab 的第一页
watch(activeTab, () => {
hasMore.value = true;
dishListByQuery.value = [];
loadDishList(false);
});
// 触底加载更多
onReachBottom(() => {
if (hasMore.value && !isLoadingMore.value) {
loadDishList(true);
}
});
const showStatusBar = useScrollThreshold()
onPageScroll((e) => {
uni.$emit('page-scroll', e)
})
function navigateToDishes(item: any) {
uni.navigateTo({
url: '/pages-store/pages/store/dishes?id=' + item.id + '&storeId=' + storeID.value,
})
}
function navigateBack() {
uni.navigateBack({
delta: 1,
})
}
function navigateToCart() {
uni.navigateTo({
url:
'/pages-user/pages/cart/store-cart'
+ '?storeId=' + storeID.value
+ '&storeName=' + encodeURIComponent(storeDetail.value.merchantName),
})
}
// 收藏店铺
function handleCollectionClick() {
debouncedEmit(storeDetail.value.isCollect, storeDetail.value.id, CollectionType.STORE, ()=> {
storeDetail.value.isCollect = !storeDetail.value.isCollect
})
}
// 收藏菜品
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, // 立即触发
})
function navigateTo(url: string) {
uni.navigateTo({ url })
}
function isSoldOutStock(stockLike: unknown) {
const n = Number(stockLike)
return !Number.isNaN(n) && n <= 0
}
function getDishPromoLabel(item: any): string {
const raw = item?.marketingLabel ?? item?.hotSaleTag ?? item?.rankTag ?? item?.promotionLabel
return typeof raw === 'string' && raw.trim() ? raw.trim() : ''
}
// 分享商家
function handleShare() {
uni.shareWithSystem({
summary: '',
href: `${Config.shareLink}pages-store/pages/store/index?id=${storeID.value}`,
success(){
// 分享完成,请注意此时不一定是成功分享
},
fail(){
// 分享失败
}
})
}
</script>
<template>
<view
class="animate-in fade-in animate-ease-out animate-duration-300"
v-show="loading"
>
<StoreSkeleton/>
</view>
<view
class="animate-in fade-in animate-ease-in animate-duration-300"
v-if="!loading"
>
<view class="store-box">
<!-- 顶部导航栏 -->
<view class="fixed top-0 left-0 z-9 w-full transition-all pt-6rpx" :class="[showStatusBar ? 'bg-#fff' : '']">
<status-bar />
<view class="flex-center-sb px-30rpx h-88rpx">
<image
@click="navigateBack"
src="@img/chef/1327.png"
mode="aspectFill"
class="w-48rpx h-48rpx relative z-1"
/>
<image
@click="handleShare"
src="@img-store/1335.png"
mode="aspectFill"
class="w-48rpx h-48rpx relative z-1"
/>
</view>
</view>
<!-- 占位 -->
<status-bar />
<view class="h-44rpx"></view>
<!-- 店铺 Logo -->
<view class="flex justify-center ">
<image
:src="storeDetail?.shopImages?.split(',')[0]"
mode="aspectFill"
class="w-128rpx h-128rpx rounded-24rpx bg-common"
/>
</view>
<!-- 店铺信息区域 -->
<view class="px-30rpx pt-24rpx pb-30rpx">
<view class="text-center">
<!-- 店铺名称 -->
<view class="text-36rpx lh-44rpx text-#333 font-bold">
{{ storeDetail?.merchantName }}
</view>
<!-- 评分 + CHEFLINK -->
<view class="center text-24rpx lh-24rpx mt-16rpx">
<view class="flex items-center">
<text class="text-#333 font-500">{{ storeDetail?.rating }}</text>
<image
src="@img/chef/124.png"
class="w-24rpx h-24rpx mx-4rpx"
></image>
<text class="text-#7D7D7D">({{ storeDetail?.commentCount }})</text>
</view>
<view class="flex items-center text-#CE7138 px-10rpx">
<image
src="@img-store/1339.png"
class="w-24rpx h-24rpx mr-4rpx"
></image>
CHEFLINK
</view>
</view>
<!-- 总销量 -->
<view class="text-24rpx lh-24rpx text-#7D7D7D text-center mt-12rpx">
{{ t('common.sales') }}{{ storeDetail?.totalOrderCount }}
</view>
</view>
<!-- 配送信息卡片 -->
<view v-if="showDeliverySwitch" class="delivery-info-card">
<template v-if="+storeDetail?.deliveryService === 1 && deliveryMethod === 0">
<view class="delivery-info-left">
<view>{{ t('pages-store.store.tips4') }} $ {{ storeDetail?.minOrderPrice }}</view>
<view>{{ t('pages-store.store.tips5') }} $ {{ storeDetail?.deliveryFee }}{{ t('pages-store.store.start') }}</view>
</view>
<view class="delivery-info-divider"></view>
<view class="delivery-info-right">
<view class="text-32rpx lh-40rpx text-#333 font-500">
{{ storeDetail?.deliveryTime }}{{ daySuffix(storeDetail?.deliveryTime) }}
</view>
<view class="text-24rpx lh-24rpx text-#7D7D7D mt-8rpx">{{ t('pages-store.store.earTime') }}</view>
</view>
</template>
<template v-if="+storeDetail?.selfPickup === 1 && deliveryMethod === 1">
<view class="delivery-info-left">
<view v-if="cartDataList.length > 0 && cartSavingsData && cartSavingsData.savings > 0">
{{ t('pages-store.store.use') }} {{ Config.appName }} {{ t('pages-store.store.tips3') }} ${{ cartSavingsData.savings }} {{ t('pages-store.store.discount') }}
</view>
<view v-else>--</view>
</view>
<view class="delivery-info-divider"></view>
<view class="delivery-info-right">
<view class="text-32rpx lh-40rpx text-#333 font-500">{{ storeDetail?.pickupTime }}{{ t('common.minutes') }}</view>
<view class="text-24rpx lh-24rpx text-#7D7D7D mt-8rpx">{{ t('pages-store.store.earTime') }}</view>
</view>
</template>
</view>
<!-- 优惠券标签行 -->
<view class="mt-24rpx flex items-center" v-if="storeCouponList.length">
<scroll-view
scroll-x
class="coupon-scroll flex-1"
:show-scrollbar="false"
enable-flex
>
<view class="coupon-container">
<view
v-for="(coupon, index) in storeCouponList"
:key="coupon.id || index"
class="coupon-item"
>
<view class="coupon-tag">
<text class="coupon-text">{{ coupon.nameZh }}</text>
</view>
</view>
</view>
</scroll-view>
<view @click="handleClaimNow" class="flex items-center shrink-0 ml-16rpx">
<text class="text-28rpx lh-28rpx font-500 text-#CE7138 mr-4rpx">{{ t('pages-store.store.claimNow') }}</text>
<i class="i-carbon:chevron-right text-24rpx text-#CE7138"></i>
</view>
</view>
</view>
<!-- 分类标签胶囊样式 -->
<scroll-view scroll-x class="w-full" :show-scrollbar="false" enable-flex>
<view class="flex items-center px-30rpx pb-24rpx">
<view
v-for="(item, index) in tabs"
:key="item.key"
@click="activeTab = index"
class="tab-chip"
:class="activeTab === index ? 'tab-chip--active' : ''"
>
{{ item.title }}
</view>
</view>
</scroll-view>
<!-- 商品列表 -->
<view v-if="tabs.length > 0" class="px-30rpx pb-180rpx">
<view v-if="currentDishList.length > 0" class="grid grid-cols-2 gap-24rpx items-start">
<template v-for="item in currentDishList" :key="item.id">
<view @click="navigateToDishes(item)" class="dish-card" :class="{ 'dish-card--soldout': isSoldOutStock(item?.stock) }">
<!-- 商品图片 -->
<view class="dish-card-image">
<!-- NEW 绑带标签 -->
<view v-if="item.isNew == 1" class="dish-new-ribbon">
<text class="dish-new-ribbon__text">NEW</text>
</view>
<view @click.stop="handleDishCollectionClick(item)" class="w-56rpx h-56rpx absolute z-4 top-12rpx right-12rpx center">
<image
v-if="!item.isCollect"
src="@img-store/1334.png"
mode="aspectFill"
class="w-44rpx h-44rpx"
style="filter: drop-shadow(0 2rpx 6rpx rgba(0,0,0,0.18))"
/>
<image
v-else
src="@img-store/1337.png"
mode="aspectFill"
class="w-44rpx h-44rpx"
style="filter: drop-shadow(0 2rpx 6rpx rgba(0,0,0,0.18))"
/>
</view>
<!-- 已售完遮罩 -->
<view v-if="isSoldOutStock(item?.stock)" class="dish-sold-dim"></view>
<!-- 已售完标签 -->
<view v-if="isSoldOutStock(item?.stock)" class="dish-sold-tag">{{ t('common.prompt.soldOut') }}</view>
<image
:src="item?.dishImage?.split(',')[0]"
mode="aspectFill"
class="dish-card-img"
/>
</view>
<!-- 卡片信息区 -->
<view class="dish-card-body">
<!-- 价格 + 销量 -->
<view class="flex items-start justify-between gap-12rpx mb-14rpx">
<view class="min-w-0 flex-1">
<text class="dish-price">$ {{ item.discountPrice }}</text>
<text
v-if="Number(item?.originalPrice) > Number(item?.discountPrice)"
class="dish-original-price"
>$ {{ item.originalPrice }}</text>
</view>
<text class="dish-sales shrink-0">{{ t('pages-store.store.sales') }}{{ item.salesCount }}</text>
</view>
<!-- 商品名称 -->
<view class="dish-title line-clamp-2 mb-16rpx">
{{ item.dishName }}
</view>
<!-- 会员价 + 加购按钮 -->
<view class="flex items-center justify-between gap-12rpx">
<view
v-if="Number(item.memberPrice) > 0"
class="dish-member shrink min-w-0"
>
<text class="dish-member-inner">{{ t('pages-store.store.members') }}: $ {{ item.memberPrice }}</text>
</view>
<view v-else class="flex-1 min-w-0"></view>
<view class="dish-add-btn center shrink-0">
<image
src="@img/chef/1285.png"
class="w-28rpx h-28rpx"
></image>
</view>
</view>
<!-- 营销标签 -->
<view
v-if="getDishPromoLabel(item)"
class="dish-promo mt-16rpx"
>
<text class="dish-promo-text">{{ getDishPromoLabel(item) }}</text>
</view>
</view>
</view>
</template>
</view>
<template v-else>
<view class="py-100rpx center">
<image class="w-250rpx h-250rpx" src="@img/chef/100.png"></image>
</view>
</template>
</view>
<!-- 底部购物车浮窗 -->
<view
v-if="userStore.isLogin && cartDataList.length > 0"
class="store-cart-float"
@click="navigateToCart"
>
<view class="store-cart-float-inner">
<view class="relative mr-16rpx">
<view class="i-carbon:shopping-cart text-44rpx text-#333"></view>
<view class="store-cart-badge">{{ cartDataList.length > 99 ? '99+' : cartDataList.length }}</view>
</view>
<text class="text-28rpx lh-28rpx text-#333 font-500">{{ t('pages-user.cart.viewCart') }}</text>
<view class="text-28rpx text-#999 ml-55rpx"></view>
</view>
</view>
<!-- 会员省钱提示条 -->
<view
@click="navigateTo('/pages-user/pages/member/index')"
v-if="cartDataList.length > 0 && cartSavingsData && cartSavingsData.savings > 0"
class="store-savings-bar"
>
<image
src="@img/chef/1289.png"
class="mr-10rpx w-32rpx h-32rpx shrink-0"
></image>
<text class="text-[#fff] text-24rpx lh-24rpx">
{{ t('pages-store.store.use') }} {{ Config.appName }} {{ t('pages-store.store.tips3') }} ${{ cartSavingsData.savings }} {{ t('pages-store.store.discount') }}
</text>
</view>
</view>
</view>
<coupon-popup
ref="couponPopupRef"
:coupon-list="storeCouponList"
@confirm="getMerchantCouponReceiveList"
/>
</template>
<style>
page {
background-color: #F6F6F6;
}
</style>
<style scoped lang="scss">
/* ====== 配送信息卡片 ====== */
.delivery-info-card {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 30rpx;
padding: 24rpx 40rpx;
border: 2rpx solid #D8D8D8;
border-radius: 20rpx;
min-height: 140rpx;
}
.delivery-info-left {
flex: 1;
text-align: center;
font-size: 24rpx;
line-height: 36rpx;
color: #CE7138;
padding-right: 30rpx;
}
.delivery-info-divider {
width: 2rpx;
height: 100rpx;
background: #D8D8D8;
flex-shrink: 0;
}
.delivery-info-right {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding-left: 30rpx;
}
/* ====== 优惠券标签 ====== */
.coupon-scroll {
width: 100%;
}
.coupon-container {
display: flex;
align-items: center;
white-space: nowrap;
}
.coupon-item {
margin-right: 15rpx;
flex-shrink: 0;
&:last-child {
margin-right: 0;
}
}
.coupon-tag {
min-width: 110rpx;
height: 52rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 0 18rpx;
position: relative;
background-image: url("/static/images/5008.png");
background-size: 100% 100%;
background-repeat: no-repeat;
}
.coupon-text {
font-size: 24rpx;
line-height: 24rpx;
color: #CE7138;
font-weight: 400;
white-space: nowrap;
}
/* ====== 胶囊标签 ====== */
.tab-chip {
height: 60rpx;
line-height: 60rpx;
padding: 0 32rpx;
border-radius: 30rpx;
font-size: 26rpx;
color: #333;
border: 2rpx solid #E0E0E0;
white-space: nowrap;
flex-shrink: 0;
margin-right: 20rpx;
text-align: center;
&:last-child {
margin-right: 0;
}
&--active {
background: #333;
color: #fff;
border-color: #333;
}
}
/* ====== 商品卡片 ====== */
.dish-card {
width: 100%;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
background: #fff;
&--soldout {
opacity: 0.7;
}
}
.dish-card-image {
position: relative;
width: 100%;
height: 330rpx;
background: #f0f0f0;
overflow: hidden;
}
.dish-card-img {
width: 100%;
height: 100%;
display: block;
z-index: 1;
}
.dish-card-body {
padding: 20rpx 20rpx 22rpx;
}
.dish-price {
color: #e02e24;
font-size: 32rpx;
font-weight: 600;
line-height: 1.2;
}
.dish-original-price {
margin-left: 10rpx;
color: #b3b3b3;
font-size: 22rpx;
font-weight: 400;
text-decoration: line-through;
vertical-align: baseline;
}
.dish-sales {
color: #999;
font-size: 24rpx;
line-height: 1.35;
max-width: 48%;
text-align: right;
}
.dish-title {
color: #1a1a1a;
font-size: 28rpx;
font-weight: 500;
line-height: 1.45;
}
.dish-member {
display: inline-flex;
align-items: center;
max-width: calc(100% - 88rpx);
}
.dish-member-inner {
display: inline-block;
padding: 6rpx 18rpx;
border-radius: 999rpx;
background: linear-gradient(180deg, #fff5eb 0%, #ffe8d6 100%);
color: #c45c1a;
font-size: 24rpx;
font-weight: 500;
line-height: 1.35;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dish-add-btn {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: #14181b;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
}
.dish-promo {
padding: 12rpx 16rpx;
border-radius: 12rpx;
background: #fff0f0;
}
.dish-promo-text {
color: #e02e24;
font-size: 22rpx;
font-weight: 500;
line-height: 1.4;
}
/* NEW 绑带标签 */
.dish-new-ribbon {
position: absolute;
top: 0;
left: 0;
width: 184rpx;
height: 184rpx;
overflow: hidden;
z-index: 5;
pointer-events: none;
&__text {
position: absolute;
top: 26rpx;
left: -66rpx;
width: 240rpx;
text-align: center;
font-size: 22rpx;
font-weight: 700;
color: #fff;
letter-spacing: 2rpx;
line-height: 44rpx;
background: #E23636;
transform: rotate(-45deg);
}
}
/* 已售完遮罩 */
.dish-sold-dim {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 2;
background: rgba(20, 24, 27, 0.42);
pointer-events: none;
}
/* 已售完标签 */
.dish-sold-tag {
position: absolute;
z-index: 3;
left: 16rpx;
top: 16rpx;
padding: 0 14rpx;
height: 48rpx;
border-radius: 24rpx;
background: rgba(20, 24, 27, 0.75);
color: #fff;
font-size: 24rpx;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
/* ====== 底部购物车浮窗 ====== */
.store-cart-float {
position: fixed;
bottom: calc(40rpx + env(safe-area-inset-bottom));
left: 50%;
width: 90%;
transform: translateX(-50%);
z-index: 9;
}
.store-cart-float-inner {
justify-content: space-between;
display: flex;
align-items: center;
height: 96rpx;
padding: 0 36rpx;
border-radius: 48rpx;
background: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.12);
border: 1rpx solid rgba(255, 255, 255, 0.6);
}
.store-cart-badge {
position: absolute;
top: -8rpx;
right: -12rpx;
min-width: 32rpx;
height: 32rpx;
padding: 0 8rpx;
font-size: 20rpx;
line-height: 32rpx;
font-weight: 600;
color: #fff;
text-align: center;
background: #e23636;
border-radius: 999rpx;
}
/* ====== 会员省钱提示条 ====== */
.store-savings-bar {
position: fixed;
bottom: calc(160rpx + env(safe-area-inset-bottom));
left: 50%;
width: 90%;
transform: translateX(-50%);
z-index: 8;
height: 64rpx;
border-radius: 32rpx;
background: #CE7138;
display: flex;
align-items: center;
padding: 0 32rpx;
white-space: nowrap;
}
</style>