Files
sale/frontend/src/views/user/ProductDetail.vue
T
admin 51edee4d92
Build and Push Docker Image / build-and-push (push) Successful in 1m5s
Build and Push Docker Image / deploy (push) Successful in 7s
fix: 修复游客购物车商品显示和添加地址问题
- ProductDetail.vue: 添加购物车时传入完整商品信息
- Cart.vue: 未登录点击添加地址跳转登录页
- 游客购物车正确显示商品状态
2026-07-13 03:55:59 +00:00

274 lines
8.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="product-detail-page" v-if="product">
<BackNav />
<div class="detail-card">
<el-row :gutter="32">
<el-col :span="12">
<div class="product-images">
<el-carousel v-if="productImages.length > 1" height="400px" :autoplay="false" indicator-position="outside">
<el-carousel-item v-for="(img, index) in productImages" :key="index">
<el-image :src="img" fit="scale-down" class="carousel-image" :preview-src-list="productImages" :initial-index="index">
<template #error>
<div class="image-placeholder">
<el-icon size="80"><Goods /></el-icon>
</div>
</template>
</el-image>
</el-carousel-item>
</el-carousel>
<el-image v-else-if="productImages.length === 1" :src="productImages[0]" fit="scale-down" class="single-image" :preview-src-list="productImages">
<template #error>
<div class="image-placeholder">
<el-icon size="80"><Goods /></el-icon>
</div>
</template>
</el-image>
<div v-else class="image-placeholder empty">
<el-icon size="80"><Goods /></el-icon>
</div>
</div>
</el-col>
<el-col :span="12">
<h1>{{ product.name }}</h1>
<p class="price">¥{{ product.price }}</p>
<p class="description">{{ product.description }}</p>
<div class="meta">
<el-tag v-if="product.require_credit" :type="hasEnoughCredit() ? 'warning' : 'danger'">
{{ $t('product.requireCredit') }}: 1
<span v-if="!hasEnoughCredit()"> (资格不足)</span>
</el-tag>
<el-tag v-if="product.credit_reward > 0" type="success">+{{ product.credit_reward * quantity }} {{ $t('product.creditReward') }}</el-tag>
</div>
<div class="user-credits" v-if="product.require_credit">
<span class="credits-label">当前资格</span>
<span class="credits-value" :class="{ 'insufficient': !hasEnoughCredit() }">{{ userCredits }}</span>
</div>
<div class="custom-fields" v-if="product.custom_fields?.length">
<h4>{{ $t('product.customFields') }}</h4>
<div v-for="f in product.custom_fields" :key="f.id" class="field-item">
<span class="field-name">{{ f.field_name }}:</span>
<span>{{ f.field_value }}</span>
</div>
</div>
<div class="purchase-section">
<el-input-number v-model="quantity" :min="product.min_purchase || 1" :max="product.max_purchase || 999" />
<el-button type="primary" size="large" @click="addToCart">{{ $t('product.addToCart') }}</el-button>
<el-button type="success" size="large" @click="buyNow">{{ $t('product.buyNow') }}</el-button>
</div>
</el-col>
</el-row>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Goods } from '@element-plus/icons-vue'
import { productApi, userApi } from '../../api'
import { useCartStore } from '../../store/cart'
import { useI18n } from 'vue-i18n'
import { getImageUrl } from '../../utils/image'
import BackNav from '../../components/BackNav.vue'
const route = useRoute()
const router = useRouter()
const { t } = useI18n()
const cartStore = useCartStore()
const product = ref<any>(null)
const quantity = ref(1)
const userCredits = ref(0)
const productImages = computed(() => {
if (!product.value?.images) return []
return product.value.images.split(',').map((s: string) => s.trim()).filter(Boolean).map((url: string) => getImageUrl(url))
})
// 检查用户是否有足够资格购买
function hasEnoughCredit(): boolean {
if (!product.value || !product.value.require_credit) return true
return userCredits.value >= 1
}
// 获取用户资格信息
async function fetchUserCredits() {
// 未登录时静默跳过,不发起请求
const token = localStorage.getItem('token')
if (!token) return
try {
const res: any = await userApi.getProfile()
userCredits.value = res.data?.purchase_credits || 0
} catch {
// 静默处理错误
}
}
onMounted(async () => {
const res: any = await productApi.getById(Number(route.params.id))
product.value = res.data
// 设置默认数量为最小购买量
if (product.value?.min_purchase && product.value.min_purchase > 1) {
quantity.value = product.value.min_purchase
}
// 获取用户资格信息
await fetchUserCredits()
})
async function addToCart() {
// 检查购物资格
if (!hasEnoughCredit()) {
ElMessage.warning(`资格不足!该商品需要1个资格,您当前只有 ${userCredits.value} 资格`)
return
}
try {
// 传入商品信息,支持游客购物车显示
await cartStore.addItem(product.value.id, quantity.value, product.value)
ElMessage.success(t('product.addToCart') + ' ✓')
} catch (err: any) {
// 处理后端返回的错误
const errorMsg = err.response?.data?.error || '添加购物车失败'
ElMessage.error(errorMsg)
}
}
function buyNow() {
addToCart().then(() => {
// 只有成功添加后才跳转
router.push('/cart')
})
}
</script>
<style scoped lang="scss">
.product-detail-page { padding: 0; }
.detail-card { background: #2d2d44; border: 1px solid rgba(255, 255, 255, 0.06); border-radius: 12px; padding: 32px; }
.product-images {
height: 400px;
background: rgba(255, 255, 255, 0.04);
border-radius: 12px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
.carousel-image, .single-image {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
// 约束 el-image 组件
:deep(.el-image) {
max-width: 100%;
max-height: 100%;
}
// 确保图片按比例缩放显示并居中
:deep(.el-image__inner) {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
.image-placeholder {
width: 100%;
height: 400px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.04);
color: rgba(255, 255, 255, 0.2);
}
.image-placeholder.empty {
height: 400px;
}
}
h1 { font-size: 28px; color: #fff; margin-bottom: 16px; }
.price { font-size: 32px; font-weight: 700; color: #4e6ef2; margin-bottom: 16px; }
.description { color: rgba(255, 255, 255, 0.5); margin-bottom: 16px; line-height: 1.6; }
.meta { display: flex; gap: 8px; margin-bottom: 16px; }
.user-credits {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.04);
border-radius: 6px;
.credits-label {
color: rgba(255, 255, 255, 0.6);
font-size: 14px;
}
.credits-value {
color: #10b981;
font-size: 16px;
font-weight: 600;
&.insufficient {
color: #f56c6c;
}
}
}
.custom-fields { margin-bottom: 24px; h4 { color: #fff; margin-bottom: 8px; } .field-item { color: rgba(255, 255, 255, 0.7); margin-bottom: 4px; .field-name { color: rgba(255, 255, 255, 0.5); margin-right: 8px; } } }
.purchase-section { display: flex; gap: 12px; align-items: center; margin-top: 24px; }
@media (max-width: 768px) {
.detail-card {
padding: 16px;
}
.el-row {
flex-direction: column;
}
.el-col {
max-width: 100%;
flex: 0 0 100%;
}
.product-images {
height: 300px;
.image-placeholder {
height: 300px;
}
}
h1 {
font-size: 18px;
margin-top: 12px;
margin-bottom: 12px;
line-height: 1.3;
}
.price {
font-size: 20px;
margin-bottom: 12px;
}
.description {
font-size: 14px;
}
.purchase-section {
flex-wrap: wrap;
:deep(.el-input-number) {
width: 100%;
}
:deep(.el-button) {
flex: 1;
}
}
}
</style>