Files
cheflinkuser/src/pages-user/pages/cart/store-cart.vue
T
2026-06-17 23:29:04 +08:00

454 lines
14 KiB
Vue

<script setup lang="ts">
import {
appMerchantCartAddCartByIdPost,
appMerchantCartCalculateSavingsPost, appMerchantCartDeleteCartPost,
appMerchantCartListByMerchantIdPost,
type MerchantCartVo
} from "@/service";
const { t } = useI18n()
import Config from '@/config/index'
import RemoveStore from "./components/remove-store.vue";
import StoreCartSkeleton from "./components/store-cart-skeleton.vue";
import { onBeforeUnmount, ref } from "vue";
import {useConfigStore} from "@/store";
import { parseMerchantCartPayload } from "@/utils/utils";
const configStore = useConfigStore();
// 骨架屏加载状态
const loading = ref(true);
// 是否需要餐具
const needUtensils = ref(false);
const removeStoreRef = ref<InstanceType<typeof RemoveStore>>()
function goBack() {
if(type.value && type.value === 'index') {
uni.redirectTo({
url: `/pages-store/pages/store/index?id=${storeId.value}`,
})
} else {
uni.navigateBack();
}
}
// 数量缓存与更新定时器
const itemCountCache = new Map<string | number, number>()
const pendingUpdateTargets = new Map<string | number, number>()
const pendingUpdateTimers = new Map<string | number, ReturnType<typeof setTimeout>>()
const updateDelay = 100
function syncItemCountCache(list: MerchantCartVo[] = []) {
itemCountCache.clear()
list.forEach(item => {
if (item?.id != null) {
itemCountCache.set(item.id, item.count ?? 0)
}
})
}
function clearPendingTimers() {
pendingUpdateTimers.forEach(timer => clearTimeout(timer))
pendingUpdateTimers.clear()
pendingUpdateTargets.clear()
}
onBeforeUnmount(() => {
clearPendingTimers()
})
function addCartCount(item: MerchantCartVo, count: number) {
if (!item?.id || count <= 0) return
appMerchantCartAddCartByIdPost({
body: {
id: item.id,
dishId: item.dishId,
merchantId: item.merchantId,
count,
}
}).then(() => {
getCartInfo()
}).catch(() => {
getCartInfo()
})
}
function deleteCartCount(item: MerchantCartVo, count: number) {
if (!item?.id || count <= 0) return
appMerchantCartDeleteCartPost({
body: {
id: item.id,
count,
}
}).then(() => {
getCartInfo()
}).catch(() => {
getCartInfo()
})
}
function scheduleCartUpdate(item: MerchantCartVo, targetValue: number) {
if (!item?.id) return
const key = item.id
pendingUpdateTargets.set(key, targetValue)
const existTimer = pendingUpdateTimers.get(key)
if (existTimer) {
clearTimeout(existTimer)
}
const timer = setTimeout(() => {
pendingUpdateTimers.delete(key)
const latestTarget = pendingUpdateTargets.get(key)
pendingUpdateTargets.delete(key)
if (typeof latestTarget !== 'number') {
return
}
const prev = itemCountCache.get(key) ?? 0
const diff = latestTarget - prev
if (diff === 0) {
return
}
const absDiff = Math.abs(diff)
if (diff > 0) {
addCartCount(item, absDiff)
} else {
deleteCartCount(item, absDiff)
}
itemCountCache.set(key, latestTarget)
}, updateDelay)
pendingUpdateTimers.set(key, timer)
}
/** wd-input-number change 为 { value },兼容 detail.value */
function normalizeInputNumberValue(payload: unknown): number {
const raw =
typeof payload === "number"
? payload
: (payload as any)?.value ?? (payload as any)?.detail?.value ?? payload;
const n = Number(raw);
return Number.isFinite(n) ? n : 0;
}
const delItemData = ref<MerchantCartVo | null>(null)
function handleQuantityChange(payload: any, item: MerchantCartVo) {
if (!item) return
const nextValue = normalizeInputNumberValue(payload)
const key = item.id
const prev = itemCountCache.get(key) ?? item.count ?? 0
if (nextValue === prev) {
return
}
if (nextValue < 0) {
item.count = prev
return
}
if (prev === 1 && nextValue === 0) {
// item.count = prev
delItemData.value = item
removeStoreRef.value?.onOpen(item.merchantDishVo.dishName || '')
return
}
scheduleCartUpdate(item, nextValue)
}
function handleRemoveClose() {
if (!delItemData.value) {
return
}
delItemData.value.count = 1
}
function handleRemove() {
if (!delItemData.value) {
return
}
deleteCartCount(delItemData.value, 1)
delItemData.value = null
}
function handleRemovePopupClose() {
if (!delItemData.value) {
return
}
const item = delItemData.value
const key = item.id
const fallback = itemCountCache.get(key) ?? 1
item.count = fallback || 1
delItemData.value = null
}
const orderRemark = ref('')
function goToCheckout() {
// 检查库存是否充足
const outOfStockItem = cartDataList.value.find(item => {
const stock = Number(item.merchantDishVo?.stock)
return !Number.isNaN(stock) && stock < item.count
})
if (outOfStockItem) {
uni.showToast({
title: `${outOfStockItem.merchantDishVo?.dishName || ''} ${t('common.prompt.stockInsufficient')}`,
icon: 'none'
})
return
}
uni.navigateTo({
url: "/pages-store/pages/order/checkout?storeId=" + storeId.value + "&needTableware=" + needUtensils.value + "&orderRemark=" + orderRemark.value,
});
}
const storeId = ref('')
const storeName = ref('')
const type = ref(null)
onLoad((options: any)=> {
loading.value = true
if(options.storeId) {
storeId.value = options.storeId as string
// storeName.value = options.storeName as string
storeName.value = decodeURIComponent(options.storeName || '')
if(options.type) {
type.value = options.type
}
// 查询当前店铺购物车详情
getCartInfo()
}
})
const cartDataList = ref<MerchantCartVo[]>([])
function getCartInfo() {
appMerchantCartListByMerchantIdPost({
params: {
merchantId: storeId.value,
}
}).then((res: any)=> {
console.log('购物车列表', res)
const items = parseMerchantCartPayload(res?.data).items as MerchantCartVo[]
cartDataList.value = items
syncItemCountCache(items)
// 购物车有菜品,查询菜品会员折扣价
if(cartDataList.value.length > 0) {
appMerchantCartCalculateSavings()
}
}).finally(()=> {
loading.value = false
})
}
// 查询菜品会员折扣价
const cartSavingsData = ref({})
function appMerchantCartCalculateSavings() {
appMerchantCartCalculateSavingsPost({
body: cartDataList.value.map(item => item.id)
}).then(res=> {
console.log('菜品会员折扣价', res)
cartSavingsData.value = res.data
})
}
function getCartItemDiscountPrice(item: MerchantCartVo) {
return item.discountPrice ?? item.merchantDishVo?.discountPrice
}
function getCartItemOriginalPrice(item: MerchantCartVo) {
return item.originalPrice ?? item.merchantDishVo?.originalPrice
}
function getCartItemMemberPrice(item: MerchantCartVo) {
return item.memberPrice ?? item.merchantDishVo?.memberPrice
}
function navigateTo(url: string) {
uni.navigateTo({ url })
}
</script>
<template>
<view class="">
<navbar />
<!-- 骨架屏 -->
<view
class="animate-in fade-in animate-ease-out animate-duration-300"
v-show="loading"
>
<store-cart-skeleton/>
</view>
<view
class="animate-in fade-in animate-ease-in animate-duration-300"
v-show="!loading"
>
<!-- 标题 -->
<view
class="px-30rpx mt-20rpx mb-22rpx text-46rpx lh-46rpx font-bold text-#333"
>
{{ storeName }}
</view>
<!-- 商品列表 -->
<view
v-for="(item, index) in cartDataList"
:key="index"
class="px-30rpx py-32rpx flex items-center border-bottom"
>
<!-- 商品图片 -->
<image
:src="item.merchantDishVo.dishImage?.split(',')[0]"
mode="aspectFill"
class="w-136rpx h-136rpx shrink-0 rounded-16rpx"
></image>
<!-- 商品信息 -->
<view class="item-info ml-20rpx flex-1">
<view class="text-[#333333] text-30rpx lh-30rpx font-500">{{
item.merchantDishVo.dishName
}}</view>
<view class="text-[#7D7D7D] text-24rpx lh-24rpx mt-20rpx" v-if="item.sideDishList?.length > 0">
<template v-for="dish in item.sideDishList">
<text class="mr-6rpx">
{{ dish?.merchantSideDishItemVo?.name || '' }}
</text>
<text v-if="dish?.merchantSideDishItemVo?.price" class="text-#00A76D mr-10rpx">+${{ dish?.merchantSideDishItemVo?.price }}</text>
</template>
</view>
<view class="price-row flex items-center mt-18rpx">
<text class="current-price text-[#333333] text-30rpx font-normal"
>${{ getCartItemDiscountPrice(item) }}</text
>
<text
v-if="getCartItemOriginalPrice(item)"
class="original-price text-[#7D7D7D] text-24rpx font-normal line-through ml-10rpx"
>${{ getCartItemOriginalPrice(item) }}</text
>
<!-- 会员价标签 -->
<view
v-if="Number(getCartItemMemberPrice(item)) > 0"
class="member-price-tag ml-10rpx center pl-16rpx pr-6rpx pb-2rpx"
>
<text class="text-[#FBE3C3] text-20rpx"
>{{ t('pages-store.store.members') }}: ${{ getCartItemMemberPrice(item) }}</text
>
</view>
</view>
</view>
<!-- 数量控制 -->
<wd-input-number
v-model="item.count"
long-press
:min="0"
:step="1"
:input-width="80"
button-size="56"
custom-class="!bg-transparent"
@change="(value) => handleQuantityChange(value, item)"
/>
</view>
<!-- 添加商品 -->
<view @click="goBack" class="h-148rpx flex items-center justify-end pr-30rpx">
<view class="w-212rpx h-64rpx bg-#F2F2F2 rounded-64rpx center">
<image
src="/static/app/images/add_cart.png"
class="mr-16rpx w-24rpx h-24rpx shrink-0"
></image>
<text class="text-#333 text-28rpx lh-28rpx font-500">{{ t('pages-user.cart.addItems') }}</text>
</view>
</view>
<view class="h-10rpx bg-#F6F6F6"></view>
<!-- 选项备注工具 -->
<view class="px-30rpx pt-52rpx pb-72rpx">
<view @click="needUtensils = !needUtensils" class="pb-52rpx flex-center-sb">
<view class="flex items-center">
<image
src="@img/chef/1283.png"
class="mr-28rpx w-44rpx h-44rpx shrink-0"
></image>
<text class="text-[#333333] text-32rpx lh-32rpx font-500">
{{ t('pages-user.cart.requestTableware') }}
</text>
</view>
<image
:src="
needUtensils
? '/static/images/chef/133.png'
: '/static/images/chef/134.png'
"
class="w-44rpx h-44rpx shrink-0"
mode="aspectFit"
/>
</view>
<view class="">
<view class="flex items-center mb-24rpx">
<image
src="@img/chef/1284.png"
class="mr-28rpx w-44rpx h-44rpx shrink-0"
></image>
<text class="text-[#333333] text-32rpx lh-32rpx font-500">
{{ t('pages-user.cart.addNote') }}
</text>
</view>
<view class="pl-72rpx">
<view
class="min-h-180rpx box-border bg-#F6F6F6 rounded-20rpx overflow-hidden px-20rpx"
>
<wd-textarea
:maxlength="150"
v-model="orderRemark"
custom-class="!bg-#F6F6F6"
custom-textarea-container-class="!bg-#F6F6F6"
no-border
auto-height
:placeholder="t('pages-user.cart.addNote')"
/>
</view>
</view>
</view>
</view>
<view v-show="!loading && cartDataList.length > 0" class="h-204rpx"></view>
</view>
<!-- 底部结算栏 -->
<view v-show="!loading && cartDataList.length > 0" class="fixed z-9 bottom-0 left-0 right-0 h-204rpx bg-white">
<view @click="navigateTo('/pages-user/pages/member/index')" v-if="cartDataList.length > 0 && cartSavingsData && cartSavingsData.savings > 0" class="h-76rpx bg-#CE7138 pl-56rpx flex items-center">
<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 class="bg-white flex-center-sb px-30rpx pt-18rpx">
<view class="text-30rpx font-500">
<view class="lh-30rpx mb-8rpx">{{ t('pages-user.cart.totalPrice') }}</view>
<text class="text-[#EE2916] text-36rpx"
>${{ cartSavingsData?.totalPayPrice }}</text
>
</view>
<view
class="w-360rpx h-92rpx bg-[#14181B] rounded-16rpx center"
@click="goToCheckout"
>
<text class="text-white text-30rpx font-500">{{ t("pages-store.checkout.title") }}</text>
</view>
</view>
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
</view>
<remove-store ref="removeStoreRef" @confirm="handleRemove" @close="handleRemoveClose"/>
</view>
</template>
<style>
page {
background-color: #fff;
}
</style>
<style lang="scss" scoped>
.member-price-tag {
height: 28rpx;
background-image: url("/static/images/chef/1282.png");
background-size: 100% 100%;
background-repeat: no-repeat;
}
</style>