454 lines
14 KiB
Vue
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>
|