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"
: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)
}"
>
<!-- 复选框 -->
<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}`)">
@@ -64,7 +73,10 @@
+{{ item.product?.credit_reward }}
</el-tag>
</div>
<!-- 状态提示 -->
<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">
<span class="item-price-val">¥{{ item.product?.price }}</span>
@@ -74,6 +86,7 @@
:min="1"
:max="getMaxQuantity(item.product)"
size="small"
:disabled="isOutOfStock(item.product)"
@change="(val: number) => updateQuantity(item, val)"
/>
</div>
@@ -92,9 +105,10 @@
:min="1"
:max="getMaxQuantity(item.product)"
size="small"
:disabled="isOutOfStock(item.product)"
@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 }}
</span>
</div>
@@ -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<number>()
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; }