first commit
This commit is contained in:
@@ -0,0 +1,445 @@
|
||||
<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";
|
||||
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)
|
||||
}
|
||||
|
||||
function normalizeInputNumberValue(payload: any): number {
|
||||
if (typeof payload === 'number') {
|
||||
return payload
|
||||
}
|
||||
if (payload && typeof payload === 'object') {
|
||||
if (typeof payload.detail?.value === 'number') {
|
||||
return payload.detail.value
|
||||
}
|
||||
if (typeof payload.value === 'number') {
|
||||
return payload.value
|
||||
}
|
||||
}
|
||||
const numeric = Number(payload)
|
||||
return Number.isNaN(numeric) ? 0 : numeric
|
||||
}
|
||||
|
||||
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)
|
||||
cartDataList.value = res.data
|
||||
syncItemCountCache(res.data || [])
|
||||
|
||||
// 购物车有菜品,查询菜品会员折扣价
|
||||
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 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"
|
||||
>${{ item.merchantDishVo.discountPrice }}</text
|
||||
>
|
||||
<text
|
||||
v-if="item.merchantDishVo.originalPrice"
|
||||
class="original-price text-[#7D7D7D] text-24rpx font-normal line-through ml-10rpx"
|
||||
>${{ item.merchantDishVo.originalPrice }}</text
|
||||
>
|
||||
|
||||
<!-- 会员价标签 -->
|
||||
<view
|
||||
v-if="item.merchantDishVo.memberPrice"
|
||||
class="member-price-tag ml-10rpx center pl-16rpx pr-6rpx pb-2rpx"
|
||||
>
|
||||
<text class="text-[#FBE3C3] text-20rpx"
|
||||
>{{ t('pages-store.store.members') }}: ${{ item.merchantDishVo.memberPrice }}</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="@img/chef/1285.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>
|
||||
Reference in New Issue
Block a user