diff --git a/frontend/src/views/user/Cart.vue b/frontend/src/views/user/Cart.vue index 7bf9194..f8478da 100644 --- a/frontend/src/views/user/Cart.vue +++ b/frontend/src/views/user/Cart.vue @@ -26,10 +26,19 @@ v-for="item in items" :key="item.id" class="cart-item" - :class="{ 'item-disabled': !item.product?.is_active }" + :class="{ + 'item-disabled': !item.product?.is_active, + 'item-out-of-stock': isOutOfStock(item.product), + 'item-no-credit': !hasEnoughCredit(item.product) + }" > - +
@@ -64,7 +73,10 @@ +{{ item.product?.credit_reward }}
+

已下架

+

缺货

+

资格不足

¥{{ item.product?.price }} @@ -74,6 +86,7 @@ :min="1" :max="getMaxQuantity(item.product)" size="small" + :disabled="isOutOfStock(item.product)" @change="(val: number) => updateQuantity(item, val)" />
@@ -92,9 +105,10 @@ :min="1" :max="getMaxQuantity(item.product)" size="small" + :disabled="isOutOfStock(item.product)" @change="(val: number) => updateQuantity(item, val)" /> - + 仅剩 {{ item.product?.stock }} 件 @@ -254,7 +268,7 @@ import { Wallet, Coin, ChatDotRound, Delete } from '@element-plus/icons-vue' import { useCartStore } from '../../store/cart' -import { systemApi, addressApi, orderApi, paymentChannelApi } from '../../api' +import { systemApi, addressApi, orderApi, paymentChannelApi, userApi } from '../../api' import { getFirstImage } from '../../utils/image' import BackNav from '../../components/BackNav.vue' @@ -269,6 +283,7 @@ const selectedAddressId = ref() const selectedPayment = ref('') const showAddAddress = ref(false) const submitting = ref(false) +const userCredits = ref(0) const addressForm = reactive({ name: '', @@ -354,6 +369,13 @@ async function fetchSettings() { } catch {} } +async function fetchUserProfile() { + try { + const res: any = await userApi.getProfile() + userCredits.value = res.data?.purchase_credits || 0 + } catch {} +} + async function fetchPaymentChannels() { try { const res: any = await paymentChannelApi.list() @@ -393,14 +415,35 @@ async function addAddress() { function handleSelectAll(val: boolean) { if (val) { - selectedIds.value = items.value.filter(i => i.product?.is_active).map(i => i.id) + selectedIds.value = items.value.filter(i => canSelectItem(i)).map(i => i.id) } else { selectedIds.value = [] } } +// 检查商品是否缺货 +function isOutOfStock(product: any): boolean { + if (!product) return false + return product.stock !== undefined && product.stock !== null && product.stock <= 0 +} + +// 检查用户是否有足够资格购买 +function hasEnoughCredit(product: any): boolean { + if (!product || !product.require_credit) return true + return userCredits.value >= (product.credit_cost || 0) +} + +// 检查商品是否可以选择 +function canSelectItem(item: any): boolean { + if (!item.product?.is_active) return false + if (isOutOfStock(item.product)) return false + if (!hasEnoughCredit(item.product)) return false + return true +} + function getMaxQuantity(product: any) { if (!product) return 99 + if (isOutOfStock(product)) return 0 let max = 999 if (product.stock !== undefined && product.stock !== null) { max = product.stock @@ -442,18 +485,20 @@ async function handleCheckout() { } return } - - const unavailableItems = items.value - .filter(item => selectedIds.value.includes(item.id) && !item.product?.is_active) - + + // 检查选中商品是否有不可购买的 + const selectedItems = items.value.filter(item => selectedIds.value.includes(item.id)) + const unavailableItems = selectedItems.filter(item => !canSelectItem(item)) + if (unavailableItems.length > 0) { - ElMessage.warning('请移除已下架的商品') + const names = unavailableItems.map(item => item.product?.name).join('、') + ElMessage.warning(`以下商品不可购买:${names}`) return } - + submitting.value = true try { - const res: any = await orderApi.create({ + const res: any = await orderApi.create({ shipping_address_id: selectedAddressId.value, payment_method: selectedPayment.value, cart_item_ids: selectedIds.value @@ -483,6 +528,7 @@ watch(items, () => { onMounted(() => { cartStore.fetchCart() fetchSettings() + fetchUserProfile() fetchAddresses() fetchPaymentChannels() }) @@ -565,6 +611,16 @@ onMounted(() => { opacity: 0.5; } + &.item-out-of-stock { + opacity: 0.6; + background: rgba(245, 108, 108, 0.1) !important; + } + + &.item-no-credit { + opacity: 0.6; + background: rgba(245, 158, 11, 0.1) !important; + } + /* 桌面端布局 */ > .item-check { width: 60px; flex-shrink: 0; } > .item-image-wrap { width: 70px; flex-shrink: 0; }