Files
jurong_circle_frontdesk/src/views/ProductDetail.vue
2025-08-01 09:33:46 +08:00

741 lines
15 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">
<!-- 导航栏 -->
<nav class="navbar">
<div class="nav-left">
<el-button
type="text"
@click="$router.go(-1)"
class="back-btn"
>
<el-icon><ArrowLeft /></el-icon>
返回
</el-button>
</div>
<div class="nav-center">
<h1 class="nav-title">商品详情</h1>
</div>
<div class="nav-right">
<el-button
type="text"
@click="showCart = true"
class="cart-btn"
>
<el-badge :value="cartCount" :hidden="cartCount === 0">
<el-icon><ShoppingCart /></el-icon>
</el-badge>
</el-button>
</div>
</nav>
<div v-loading="loading" class="product-content">
<div v-if="product" class="product-detail">
<!-- 商品图片 -->
<div class="product-images">
<el-carousel
:interval="4000"
type="card"
height="300px"
indicator-position="outside"
>
<el-carousel-item v-for="(image, index) in product.images" :key="index">
<img :src="image" :alt="product.name" class="product-image" />
</el-carousel-item>
</el-carousel>
</div>
<!-- 商品信息 -->
<div class="product-info">
<div class="product-header">
<h1 class="product-title">{{ product.name }}</h1>
<div class="product-tags">
<el-tag
v-for="tag in product.tags"
:key="tag"
size="small"
class="product-tag"
>
{{ tag }}
</el-tag>
</div>
</div>
<div class="product-price">
<div class="current-price">
<el-icon><Coin /></el-icon>
<span class="price-number">{{ product.points }}</span>
<span class="price-unit">积分</span>
</div>
<div v-if="product.originalPoints" class="original-price">
原价{{ product.originalPoints }}积分
</div>
<div v-if="product.discount" class="discount-info">
<el-tag type="danger" size="small">{{ product.discount }}折优惠</el-tag>
</div>
</div>
<div class="product-stats">
<div class="stat-item">
<span class="stat-label">销量</span>
<span class="stat-value">{{ product.sales }}</span>
</div>
<div class="stat-item">
<span class="stat-label">库存</span>
<span class="stat-value">{{ product.stock }}</span>
</div>
<div class="stat-item">
<span class="stat-label">评分</span>
<span class="stat-value">
<el-rate
v-model="product.rating"
disabled
show-score
text-color="#ff9900"
score-template="{value}"
/>
</span>
</div>
</div>
<!-- 商品描述 -->
<div class="product-description">
<h3>商品描述</h3>
<p>{{ product.description }}</p>
</div>
<!-- 商品详情 -->
<div class="product-details">
<h3>商品详情</h3>
<div class="detail-content">
<p>{{ product.description || '暂无详细描述' }}</p>
</div>
</div>
<!-- 购买选项 -->
<div class="purchase-options">
<div class="quantity-selector">
<span class="option-label">数量</span>
<el-input-number
v-model="quantity"
:min="1"
:max="product.stock"
size="small"
/>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<el-button
size="large"
@click="addToCart"
:disabled="product.stock === 0"
>
加入购物车
</el-button>
<el-button
type="primary"
size="large"
@click="buyNow"
:disabled="product.stock === 0 || totalPoints > userPoints"
>
{{ product.stock === 0 ? '缺货' : totalPoints > userPoints ? '积分不足' : '立即兑换' }}
</el-button>
</div>
<!-- 商品评价 -->
<div class="product-reviews">
<div class="reviews-header">
<h3>用户评价</h3>
<span class="review-count">({{ reviews.length }}条评价)</span>
</div>
<div v-if="reviews.length === 0" class="no-reviews">
<el-icon><ChatDotRound /></el-icon>
<p>暂无评价</p>
</div>
<div v-else class="reviews-list">
<div v-for="review in reviews" :key="review.id" class="review-item">
<div class="review-header">
<div class="reviewer-info">
<el-avatar :size="32" :src="review.user.avatar">
<el-icon><User /></el-icon>
</el-avatar>
<span class="reviewer-name">{{ review.user.name }}</span>
</div>
<div class="review-meta">
<el-rate v-model="review.rating" disabled size="small" />
<span class="review-date">{{ formatDate(review.createdAt) }}</span>
</div>
</div>
<div class="review-content">
<p>{{ review.content }}</p>
<div v-if="review.images" class="review-images">
<img
v-for="(image, index) in review.images"
:key="index"
:src="image"
class="review-image"
@click="previewImage(image)"
/>
</div>
</div>
</div>
</div>
</div>
<!-- 推荐商品 -->
<div class="recommended-products">
<h3>推荐商品</h3>
<div class="recommended-grid">
<div
v-for="item in recommendedProducts"
:key="item.id"
class="recommended-item"
@click="goToProduct(item.id)"
>
<img :src="item.image" :alt="item.name" />
<div class="item-info">
<h4>{{ item.name }}</h4>
<p class="item-price">
<el-icon><Coin /></el-icon>
{{ item.points }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 购物车抽屉 -->
<el-drawer
v-model="showCart"
title="购物车"
direction="rtl"
size="80%"
>
<!-- 购物车内容复用Shop.vue的逻辑 -->
</el-drawer>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
ArrowLeft,
ShoppingCart,
Coin,
ChatDotRound,
User
} from '@element-plus/icons-vue'
import api from '@/utils/api'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
// 响应式数据
const loading = ref(false)
const product = ref(null)
const quantity = ref(1)
const reviews = ref([])
const recommendedProducts = ref([])
const showCart = ref(false)
const userPoints = ref(0)
const cartCount = ref(0)
// 计算属性
const totalPoints = computed(() => {
return product.value ? product.value.points * quantity.value : 0
})
// 方法
const getProductDetail = async () => {
try {
loading.value = true
const productId = route.params.id
const [productRes, reviewsRes, recommendedRes] = await Promise.all([
api.get(`/products/${productId}`),
api.get(`/products/${productId}/reviews`),
api.get(`/products/${productId}/recommended`)
])
console.log(productRes,'productRes');
console.log(reviewsRes,'reviewsRes');
console.log(recommendedRes,'recommendedRes');
product.value = productRes.data.data.product
reviews.value = reviewsRes.data.data.reviews || []
recommendedProducts.value = recommendedRes.data.data.products || []
} catch (error) {
ElMessage.error('获取商品详情失败')
router.go(-1)
} finally {
loading.value = false
}
}
const addToCart = () => {
// 添加到购物车逻辑
ElMessage.success('已添加到购物车')
cartCount.value++
}
const buyNow = async () => {
try {
await ElMessageBox.confirm(
`确定要花费 ${totalPoints.value} 积分兑换 ${quantity.value}${product.value.name} 吗?`,
'确认兑换',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
const orderData = {
items: [{
productId: product.value.id,
quantity: quantity.value,
points: product.value.points
}],
totalPoints: totalPoints.value
}
await api.post('/orders', orderData)
ElMessage.success('兑换成功!')
router.push('/orders')
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('兑换失败,请重试')
}
}
}
const goToProduct = (productId) => {
router.push(`/product/${productId}`)
}
const previewImage = (image) => {
// 图片预览逻辑
}
const formatDate = (date) => {
return new Date(date).toLocaleDateString('zh-CN')
}
const getUserPoints = async () => {
try {
const response = await api.get('/user/points')
userPoints.value = response.data.points
} catch (error) {
console.error('获取用户积分失败:', error)
}
}
// 生命周期
onMounted(() => {
getProductDetail()
getUserPoints()
})
</script>
<style scoped>
.product-detail-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
height: 56px;
background: white;
border-bottom: 1px solid #eee;
position: sticky;
top: 0;
z-index: 100;
}
.nav-left,
.nav-right {
flex: 1;
}
.nav-right {
display: flex;
justify-content: flex-end;
}
.back-btn,
.cart-btn {
color: #409eff;
font-size: 14px;
}
.nav-title {
margin: 0;
font-size: 18px;
font-weight: 500;
color: #333;
}
.product-content {
padding: 0;
}
.product-detail {
background: white;
}
.product-images {
padding: 20px;
background: white;
}
.product-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
.product-info {
padding: 20px;
background: white;
border-bottom: 8px solid #f5f5f5;
}
.product-header {
margin-bottom: 16px;
}
.product-title {
margin: 0 0 8px 0;
font-size: 20px;
font-weight: 600;
color: #333;
line-height: 1.4;
}
.product-tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.product-tag {
margin: 0;
}
.product-price {
margin-bottom: 20px;
}
.current-price {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 8px;
}
.current-price .el-icon {
color: #ff6b35;
font-size: 20px;
}
.price-number {
font-size: 28px;
font-weight: 700;
color: #ff6b35;
}
.price-unit {
font-size: 16px;
color: #ff6b35;
}
.original-price {
color: #999;
font-size: 14px;
text-decoration: line-through;
margin-bottom: 4px;
}
.discount-info {
margin-top: 8px;
}
.product-stats {
display: flex;
gap: 24px;
padding: 16px 0;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
margin-bottom: 20px;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.stat-label {
font-size: 12px;
color: #999;
}
.stat-value {
font-size: 14px;
font-weight: 500;
color: #333;
}
.product-description,
.product-details {
margin-bottom: 20px;
}
.product-description h3,
.product-details h3 {
margin: 0 0 12px 0;
font-size: 16px;
color: #333;
}
.product-description p {
margin: 0;
line-height: 1.6;
color: #666;
}
.detail-item {
display: flex;
padding: 8px 0;
border-bottom: 1px solid #f5f5f5;
}
.detail-label {
width: 80px;
color: #999;
font-size: 14px;
}
.detail-value {
flex: 1;
color: #333;
font-size: 14px;
}
.purchase-options {
margin-bottom: 20px;
}
.quantity-selector {
display: flex;
align-items: center;
gap: 12px;
}
.option-label {
font-size: 14px;
color: #333;
}
.action-buttons {
display: flex;
gap: 12px;
padding: 20px;
background: white;
border-top: 1px solid #eee;
position: sticky;
bottom: 0;
}
.action-buttons .el-button {
flex: 1;
}
.product-reviews {
padding: 20px;
background: white;
border-bottom: 8px solid #f5f5f5;
}
.reviews-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.reviews-header h3 {
margin: 0;
font-size: 16px;
color: #333;
}
.review-count {
font-size: 14px;
color: #999;
}
.no-reviews {
text-align: center;
padding: 40px 20px;
color: #999;
}
.reviews-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.review-item {
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
}
.review-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.reviewer-info {
display: flex;
align-items: center;
gap: 8px;
}
.reviewer-name {
font-size: 14px;
color: #333;
font-weight: 500;
}
.review-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.review-date {
font-size: 12px;
color: #999;
}
.review-content p {
margin: 0 0 8px 0;
line-height: 1.6;
color: #666;
}
.review-images {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.review-image {
width: 60px;
height: 60px;
border-radius: 4px;
object-fit: cover;
cursor: pointer;
}
.recommended-products {
padding: 20px;
background: white;
}
.recommended-products h3 {
margin: 0 0 16px 0;
font-size: 16px;
color: #333;
}
.recommended-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.recommended-item {
display: flex;
flex-direction: column;
background: #f8f9fa;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
}
.recommended-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.recommended-item img {
width: 100%;
height: 100px;
object-fit: cover;
}
.recommended-item .item-info {
padding: 8px;
}
.recommended-item h4 {
margin: 0 0 4px 0;
font-size: 12px;
color: #333;
line-height: 1.4;
}
.recommended-item .item-price {
display: flex;
align-items: center;
gap: 2px;
color: #ff6b35;
font-weight: 600;
font-size: 14px;
margin: 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.product-stats {
gap: 16px;
}
.recommended-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 480px) {
.action-buttons {
padding: 16px;
}
.recommended-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>