fix: 购物车添加库存和资格检查

库存检查:
- 库存为0的商品显示"缺货"状态
- 禁止选择缺货商品
- 禁止修改缺货商品数量
- 背景显示红色半透明样式

资格检查:
- 获取用户purchase_credits
- 检查是否有足够资格购买需要资格的商品
- 资格不足显示"资格不足"状态
- 禁止选择资格不足的商品

结账检查:
- 结账前检查选中商品是否都可购买
- 显示不可购买商品的名称

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 22:12:23 +08:00
parent 5e1ab2cf4c
commit 0df0a83b44
+68 -12
View File
@@ -26,10 +26,19 @@
v-for="item in items" v-for="item in items"
:key="item.id" :key="item.id"
class="cart-item" 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)
}"
> >
<!-- 复选框 --> <!-- 复选框 -->
<el-checkbox v-model="selectedIds" :value="item.id" class="item-check" /> <el-checkbox
v-model="selectedIds"
:value="item.id"
class="item-check"
:disabled="!canSelectItem(item)"
/>
<!-- 商品图片 --> <!-- 商品图片 -->
<div class="item-image-wrap" @click="$router.push(`/products/${item.product?.id}`)"> <div class="item-image-wrap" @click="$router.push(`/products/${item.product?.id}`)">
@@ -64,7 +73,10 @@
+{{ item.product?.credit_reward }} +{{ item.product?.credit_reward }}
</el-tag> </el-tag>
</div> </div>
<!-- 状态提示 -->
<p v-if="!item.product?.is_active" class="item-off">已下架</p> <p v-if="!item.product?.is_active" class="item-off">已下架</p>
<p v-else-if="isOutOfStock(item.product)" class="item-off">缺货</p>
<p v-else-if="!hasEnoughCredit(item.product)" class="item-off">资格不足</p>
<!-- 底部价格 + 数量 --> <!-- 底部价格 + 数量 -->
<div class="item-bottom-row"> <div class="item-bottom-row">
<span class="item-price-val">¥{{ item.product?.price }}</span> <span class="item-price-val">¥{{ item.product?.price }}</span>
@@ -74,6 +86,7 @@
:min="1" :min="1"
:max="getMaxQuantity(item.product)" :max="getMaxQuantity(item.product)"
size="small" size="small"
:disabled="isOutOfStock(item.product)"
@change="(val: number) => updateQuantity(item, val)" @change="(val: number) => updateQuantity(item, val)"
/> />
</div> </div>
@@ -92,9 +105,10 @@
:min="1" :min="1"
:max="getMaxQuantity(item.product)" :max="getMaxQuantity(item.product)"
size="small" size="small"
:disabled="isOutOfStock(item.product)"
@change="(val: number) => updateQuantity(item, val)" @change="(val: number) => updateQuantity(item, val)"
/> />
<span v-if="item.product?.stock !== undefined && item.product?.stock <= 10" class="stock-tip"> <span v-if="item.product?.stock !== undefined && item.product?.stock <= 10 && item.product?.stock > 0" class="stock-tip">
仅剩 {{ item.product?.stock }} 仅剩 {{ item.product?.stock }}
</span> </span>
</div> </div>
@@ -254,7 +268,7 @@ import {
Wallet, Coin, ChatDotRound, Delete Wallet, Coin, ChatDotRound, Delete
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import { useCartStore } from '../../store/cart' 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 { getFirstImage } from '../../utils/image'
import BackNav from '../../components/BackNav.vue' import BackNav from '../../components/BackNav.vue'
@@ -269,6 +283,7 @@ const selectedAddressId = ref<number>()
const selectedPayment = ref('') const selectedPayment = ref('')
const showAddAddress = ref(false) const showAddAddress = ref(false)
const submitting = ref(false) const submitting = ref(false)
const userCredits = ref(0)
const addressForm = reactive({ const addressForm = reactive({
name: '', name: '',
@@ -354,6 +369,13 @@ async function fetchSettings() {
} catch {} } catch {}
} }
async function fetchUserProfile() {
try {
const res: any = await userApi.getProfile()
userCredits.value = res.data?.purchase_credits || 0
} catch {}
}
async function fetchPaymentChannels() { async function fetchPaymentChannels() {
try { try {
const res: any = await paymentChannelApi.list() const res: any = await paymentChannelApi.list()
@@ -393,14 +415,35 @@ async function addAddress() {
function handleSelectAll(val: boolean) { function handleSelectAll(val: boolean) {
if (val) { 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 { } else {
selectedIds.value = [] 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) { function getMaxQuantity(product: any) {
if (!product) return 99 if (!product) return 99
if (isOutOfStock(product)) return 0
let max = 999 let max = 999
if (product.stock !== undefined && product.stock !== null) { if (product.stock !== undefined && product.stock !== null) {
max = product.stock max = product.stock
@@ -442,18 +485,20 @@ async function handleCheckout() {
} }
return 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) { if (unavailableItems.length > 0) {
ElMessage.warning('请移除已下架的商品') const names = unavailableItems.map(item => item.product?.name).join('、')
ElMessage.warning(`以下商品不可购买:${names}`)
return return
} }
submitting.value = true submitting.value = true
try { try {
const res: any = await orderApi.create({ const res: any = await orderApi.create({
shipping_address_id: selectedAddressId.value, shipping_address_id: selectedAddressId.value,
payment_method: selectedPayment.value, payment_method: selectedPayment.value,
cart_item_ids: selectedIds.value cart_item_ids: selectedIds.value
@@ -483,6 +528,7 @@ watch(items, () => {
onMounted(() => { onMounted(() => {
cartStore.fetchCart() cartStore.fetchCart()
fetchSettings() fetchSettings()
fetchUserProfile()
fetchAddresses() fetchAddresses()
fetchPaymentChannels() fetchPaymentChannels()
}) })
@@ -565,6 +611,16 @@ onMounted(() => {
opacity: 0.5; 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-check { width: 60px; flex-shrink: 0; }
> .item-image-wrap { width: 70px; flex-shrink: 0; } > .item-image-wrap { width: 70px; flex-shrink: 0; }