diff --git a/src/views/ProductDetail.vue b/src/views/ProductDetail.vue index 7c57f48..e572bef 100644 --- a/src/views/ProductDetail.vue +++ b/src/views/ProductDetail.vue @@ -243,7 +243,105 @@ direction="rtl" size="80%" > - +
+ +
+ +
正在加载购物车数据...
+
+ + +
+ +

购物车是空的

+

快去挑选心仪的商品吧~

+
+ + +
+
+ 共 {{ cartTotalItems }} 件商品 + + 清空购物车 + +
+ +
+
+
+ +
+ +
+

{{ item.name }}

+
+ + {{ item.points }} 积分 +
+
库存:{{ item.stock }}
+
+ +
+
+ + - + + {{ item.quantity }} + + + + +
+ + 删除 + +
+
+
+ + + +
+
@@ -258,7 +356,8 @@ import { ShoppingCart, Coin, ChatDotRound, - User + User, + Loading } from '@element-plus/icons-vue' import api from '@/utils/api' @@ -275,7 +374,9 @@ const quantity = ref(1) const reviews = ref([]) const recommendedProducts = ref([]) const showCart = ref(false) +const cartLoading = ref(false) const userPoints = ref(0) +const cartItems = ref([]) const cartCount = ref(0) // 计算属性 @@ -283,6 +384,28 @@ const totalPoints = computed(() => { return product.value ? product.value.points * quantity.value : 0 }) +// 购物车相关计算属性 +const cartTotalPoints = computed(() => { + return cartItems.value.reduce((total, item) => total + (item.points * item.quantity), 0) +}) + +const cartTotalItems = computed(() => { + return cartItems.value.reduce((total, item) => total + item.quantity, 0) +}) + +// 更新购物车计数 +watch(cartTotalItems, (newCount) => { + cartCount.value = newCount +}, { immediate: true }) + +// 监听购物车抽屉打开状态,打开时从后端加载数据 +watch(showCart, async (newValue) => { + if (newValue) { + // 购物车打开时从后端加载数据 + await loadCartFromBackend() + } +}) + // 方法 const getProductDetail = async () => { try { @@ -309,10 +432,196 @@ const getProductDetail = async () => { } } -const addToCart = () => { - // 添加到购物车逻辑 - ElMessage.success('已添加到购物车') - cartCount.value++ +const addToCart = async () => { + if (!product.value) { + ElMessage.error('商品信息加载中,请稍后再试') + return + } + + if (product.value.stock === 0) { + ElMessage.error('商品已售罄') + return + } + + if (quantity.value <= 0) { + ElMessage.error('请选择有效数量') + return + } + + // 检查是否已存在该商品 + const existingItemIndex = cartItems.value.findIndex(item => item.id === product.value.id) + + if (existingItemIndex !== -1) { + // 商品已存在,增加数量 + const existingItem = cartItems.value[existingItemIndex] + const newQuantity = existingItem.quantity + quantity.value + + if (newQuantity > product.value.stock) { + ElMessage.error(`库存不足,最多只能添加 ${product.value.stock - existingItem.quantity} 个`) + return + } + + existingItem.quantity = newQuantity + ElMessage.success(`已将 ${quantity.value} 个商品添加到购物车`) + } else { + // 新商品,直接添加 + if (quantity.value > product.value.stock) { + ElMessage.error(`库存不足,最多只能添加 ${product.value.stock} 个`) + return + } + + const cartItem = { + id: product.value.id, + name: product.value.name, + image: product.value.images?.[0] || product.value.image, + points: product.value.points, + quantity: quantity.value, + stock: product.value.stock + } + + cartItems.value.push(cartItem) + ElMessage.success(`已将 ${quantity.value} 个商品添加到购物车`) + } + + // 重置数量选择器 + quantity.value = 1 + + // 同步购物车数据到后端 + await syncCartToBackend() +} + +// 购物车商品管理方法 +const updateCartItemQuantity = async (itemId, newQuantity) => { + const item = cartItems.value.find(item => item.id === itemId) + if (!item) return + + if (newQuantity <= 0) { + removeFromCart(itemId) + return + } + + if (newQuantity > item.stock) { + ElMessage.error(`库存不足,最多只能选择 ${item.stock} 个`) + return + } + + item.quantity = newQuantity + + // 同步购物车数据到后端 + await syncCartToBackend() +} + +const removeFromCart = async (itemId) => { + const index = cartItems.value.findIndex(item => item.id === itemId) + if (index !== -1) { + const item = cartItems.value[index] + cartItems.value.splice(index, 1) + ElMessage.success(`已从购物车移除 ${item.name}`) + + // 同步购物车数据到后端 + await syncCartToBackend() + } +} + +const clearCart = () => { + ElMessageBox.confirm( + '确定要清空购物车吗?', + '确认清空', + { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + } + ).then(async () => { + cartItems.value = [] + ElMessage.success('购物车已清空') + + // 同步购物车数据到后端 + await syncCartToBackend() + }).catch(() => {}) + } + +// 购物车数据同步到后端 +const syncCartToBackend = async () => { + try { + const cartData = { + items: cartItems.value.map(item => ({ + productId: item.id, + quantity: item.quantity, + points: item.points, + name: item.name, + image: item.image, + stock: item.stock + })) + } + await api.post('/cart/sync', cartData) + } catch (error) { + console.error('购物车同步失败:', error) + } +} + +// 从后端读取购物车数据 +const loadCartFromBackend = async () => { + cartLoading.value = true + try { + const response = await api.get('/cart') + if (response.data && response.data.items) { + cartItems.value = response.data.items + } + } catch (error) { + console.error('购物车数据加载失败:', error) + ElMessage.error('购物车数据加载失败,请重试') + // 如果加载失败,保持当前购物车状态 + } finally { + cartLoading.value = false + } +} + +// 购物车结算功能 +const checkoutCart = async () => { + if (cartItems.value.length === 0) { + ElMessage.error('购物车是空的') + return + } + + if (cartTotalPoints.value > userPoints.value) { + ElMessage.error('积分不足,无法结算') + return + } + + try { + await ElMessageBox.confirm( + `确定要花费 ${cartTotalPoints.value} 积分购买这些商品吗?`, + '确认结算', + { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + } + ) + + const orderData = { + items: cartItems.value.map(item => ({ + productId: item.id, + quantity: item.quantity, + points: item.points + })), + totalPoints: cartTotalPoints.value + } + + await api.post('/orders', orderData) + + // 清空购物车 + cartItems.value = [] + showCart.value = false + + ElMessage.success('结算成功!') + router.push('/orders') + } catch (error) { + if (error !== 'cancel') { + ElMessage.error('结算失败,请重试') + } + } } const buyNow = async () => { @@ -826,4 +1135,202 @@ watch( grid-template-columns: repeat(2, 1fr); } } + +/* 购物车样式 */ +.cart-content { + height: 100%; + display: flex; + flex-direction: column; +} + +.cart-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 50%; + color: #666; + gap: 12px; +} + +.cart-loading .el-icon { + font-size: 32px; + color: #409eff; +} + +.empty-cart { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 50%; + color: #999; +} + +.empty-icon { + font-size: 64px; + margin-bottom: 16px; + color: #ddd; +} + +.empty-tip { + font-size: 14px; + margin-top: 8px; +} + +.cart-items { + height: 100%; + display: flex; + flex-direction: column; +} + +.cart-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 0; + border-bottom: 1px solid #eee; + font-size: 14px; + color: #666; +} + +.clear-btn { + color: #ff4757; + font-size: 12px; +} + +.cart-list { + flex: 1; + overflow-y: auto; + padding: 16px 0; +} + +.cart-item { + display: flex; + gap: 12px; + padding: 16px 0; + border-bottom: 1px solid #f5f5f5; +} + +.cart-item:last-child { + border-bottom: none; +} + +.item-image { + width: 60px; + height: 60px; + flex-shrink: 0; +} + +.item-image img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 4px; +} + +.item-info { + flex: 1; + min-width: 0; +} + +.item-name { + margin: 0 0 8px 0; + font-size: 14px; + font-weight: 500; + color: #333; + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.item-price { + display: flex; + align-items: center; + gap: 4px; + color: #ff6b35; + font-weight: 600; + font-size: 14px; + margin-bottom: 4px; +} + +.item-stock { + font-size: 12px; + color: #999; +} + +.item-actions { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 8px; +} + +.quantity-control { + display: flex; + align-items: center; + gap: 8px; +} + +.quantity-control .el-button { + width: 24px; + height: 24px; + padding: 0; + min-height: 24px; +} + +.quantity { + font-size: 14px; + font-weight: 500; + min-width: 20px; + text-align: center; +} + +.remove-btn { + color: #ff4757; + font-size: 12px; + padding: 0; +} + +.cart-footer { + border-top: 1px solid #eee; + padding: 16px 0 0 0; + margin-top: auto; +} + +.total-info { + margin-bottom: 16px; +} + +.total-points { + display: flex; + align-items: center; + gap: 4px; + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.total-points .points { + color: #ff6b35; + font-size: 18px; +} + +.user-points { + font-size: 14px; + color: #666; +} + +.checkout-actions { + display: flex; +} + +.checkout-btn { + width: 100%; + height: 44px; +} \ No newline at end of file