Files
jurong_circle_frontdesk/src/views/ProductDetail.vue
2025-09-05 11:40:34 +08:00

977 lines
21 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">
<!-- 购物车按钮已移除 -->
</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 class="small-images">
<img v-for="(image, index) in product.images" :key="index" :src="image" :alt="product.name" class="small-image" />
</div>
</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="price-container">
<div class="main-price">
<img src='/imgs/profile/rongdou.png' alt="融豆" class="rongdou-icon" />
<span class="rongdou-price">{{ product.rongdou_price }}</span>
</div>
<div class="sub-price">
<el-icon class="points-icon"><Coin /></el-icon>
<span class="points-price">{{ product.points_price }}</span>
</div>
</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="detail-group-container">
<!-- 第一组商品详情图标 + 文本 -->
<div class="detail-item-group"> <!-- 每组独立容器 -->
<img src="/imgs/productdetail/shangpingxiangqing.png" alt="商品详情" class="detail-icon">
<!-- <span class="detail-text">{{product.material}}||
<el-tag
v-for="tag in product.tags"
:key="tag"
size="small"
class="product-tag"
>
{{ tag }}
</el-tag>||{{product.brand}}||{{product.origin}}
</span> -->
<span class="detail-text">
<el-tag
v-for="tag in product.tags"
:key="tag"
size="small"
class="product-tag"
>
{{ tag }}
</el-tag>
</span>
</div>
</div>
<!-- 商品详情 -->
<div class="product-details">
<h3 class="section-title">
商品详情
</h3>
<div v-if="showDetails" class="section-content">
<div class="detail-content">
<p>{{ product.description || '暂无详细描述' }}</p>
</div>
</div>
<div v-else class="section-placeholder">
<span class="placeholder-text" @click="toggleDetails">详情</span>
</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>
</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>
<!-- 购物车抽屉已移除 -->
</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,
Loading
} from '@element-plus/icons-vue'
import api from '@/utils/api'
import { getImageUrl } from '@/config'
import { watch } from 'vue'
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([])
// showCart已移除
const userPoints = ref(0)
const showDescription = ref(false)
const showDetails = ref(false)
const selectedCategory = ref(null)
const selectedSize = ref(null)
// 计算属性
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
// 处理商品图片路径
if (product.value.image) {
product.value.image = getImageUrl(product.value.image)
}
if (product.value.images && Array.isArray(product.value.images)) {
product.value.images = product.value.images.map(img => getImageUrl(img))
}
reviews.value = reviewsRes.data.data.reviews || []
recommendedProducts.value = recommendedRes.data.data.products || []
// 处理推荐商品图片路径
recommendedProducts.value.forEach(item => {
if (item.image) {
item.image = getImageUrl(item.image)
}
})
} catch (error) {
ElMessage.error('获取商品详情失败')
router.go(-1)
} finally {
loading.value = false
}
}
const addToCart = async () => {
if (!product.value) {
ElMessage.error('商品信息加载中,请稍后再试')
return
}
if (product.value.stock === 0) {
ElMessage.error('商品已售罄')
return
}
try {
// 检查是否已选择必要的商品属性
if (!selectedCategory.value || !selectedSize.value) {
// 如果没有选择属性跳转到BuyDetails页面进行详细配置
router.push({
path: '/buydetail',
query: {
productId: product.value.id,
quantity: quantity.value
}
})
return
}
// 构建购物车商品数据
const cartItem = {
productId: product.value.id,
quantity: quantity.value,
categoryId: selectedCategory.value.id,
sizeId: selectedSize.value.id,
points: product.value.points,
name: product.value.name,
image: product.value.images?.[0] || product.value.image,
stock: product.value.stock
}
// 添加到购物车
const response = await api.post('/cart/add', cartItem)
if (response.data.success) {
ElMessage.success('商品已加入购物车!')
// 重置选择状态
quantity.value = 1
selectedCategory.value = null
selectedSize.value = null
} else {
throw new Error(response.data.message || '添加到购物车失败')
}
} catch (error) {
ElMessage.error(error.message || '添加到购物车失败,请重试')
}
}
// 购物车商品管理方法已移除
// 购物车数据同步和结算方法已移除
const goToProduct = (productId) => {
router.replace(`/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)
}
}
const toggleDescription = () => {
showDescription.value = !showDescription.value
}
const toggleDetails = () => {
showDetails.value = !showDetails.value
}
// 生命周期
onMounted(() => {
//getProductDetail()
getUserPoints()
})
watch(
() => route.params.id,
(newId) => {
if (newId) {
getProductDetail()
quantity.value = 1 // 重置数量
}
},
{ immediate: true }
)
</script>
<style scoped>
.product-detail-page {
min-height: 100vh;
background: linear-gradient(to bottom, #72c9ffae, #f3f3f3);
}
.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: transparent;
}
.product-images {
padding: 20px;
background: transparent;
}
.product-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
/* 小图容器样式 */
.small-images {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 16px;
flex-wrap: wrap;
}
/* 小图样式 */
.small-image {
width: 64px;
height: 64px;
object-fit: cover;
border-radius: 6px;
border: 2px solid transparent;
cursor: pointer;
transition: all 0.3s ease;
}
.product-info {
padding: 20px;
background: transparent;
}
.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;
}
.price-container {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 6px;
margin-bottom: 8px;
}
.main-price {
display: flex;
align-items: center;
gap: 6px;
}
.rongdou-icon {
width: 24px;
height: 24px;
object-fit: contain;
}
.rongdou-price {
font-size: 28px;
font-weight: 700;
color: #ff6b35;
}
.sub-price {
display: flex;
align-items: center;
gap: 4px;
margin-left: 0;
margin-top: 2px;
}
.points-icon {
width: 16px;
height: 16px;
color: #666;
}
.points-price {
font-size: 16px;
color: #666;
font-weight: normal;
}
.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;
}
.detail-group-container {
display: flex;
flex-direction: column; /* 让两组纵向堆叠(上下排列) */
gap: 12px; /* 两组之间的垂直间距(可根据需求调整,如 16px */
margin: 16px 0; /* 与上下其他内容的间距,保持页面呼吸感 */
}
/* 每组容器:控制图标和文本的横向对齐 */
.detail-item-group {
display: flex; /* 组内图标和文本横向排列 */
align-items: center; /* 图标与文本垂直居中(避免错位) */
gap: 8px; /* 图标和文本之间的横向间距(可调整) */
}
/* 图标样式:保持原大小,避免拉伸 */
.detail-icon {
width: 20px;
height: 20px;
object-fit: contain; /* 保持图片比例,不变形 */
}
/* 文本样式:保持原风格,优化 tag 间距 */
.detail-text {
font-size: 14px;
color: #333;
line-height: 1.5;
}
/* 优化 tag 与文字的间距,避免拥挤 */
.detail-text .product-tag {
margin: 0 4px; /* tag 左右留白,更美观 */
}
.product-details {
margin-bottom: 20px;
}
.section-title {
margin: 0 0 12px 0;
font-size: 16px;
color: #333;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.section-content {
padding: 12px 0;
animation: slideDown 0.3s ease;
}
.section-content p {
margin: 0;
line-height: 1.6;
color: #666;
}
.section-placeholder {
padding: 12px 0;
}
.placeholder-text {
color: #999;
font-size: 14px;
font-style: italic;
cursor: pointer;
transition: color 0.3s ease;
}
.placeholder-text:hover {
color: #409eff;
}
@keyframes slideDown {
from {
opacity: 0;
max-height: 0;
}
to {
opacity: 1;
max-height: 200px;
}
}
.detail-item {
display: flex;
padding: 8px 0;
}
.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;
position: relative;
z-index: 1;
}
.option-label {
font-size: 14px;
color: #333;
}
.quantity-selector :deep(.el-input-number) {
background: transparent;
border: none;
}
.quantity-selector :deep(.el-input-number .el-input__wrapper) {
background: transparent;
border: none;
box-shadow: none;
}
.quantity-selector :deep(.el-input-number .el-input__inner) {
background: transparent;
border: none;
}
.quantity-selector :deep(.el-input-number .el-input-number__decrease),
.quantity-selector :deep(.el-input-number .el-input-number__increase) {
background: transparent;
border: none;
color: #409eff;
}
.quantity-selector :deep(.el-input-number .el-input-number__decrease:hover),
.quantity-selector :deep(.el-input-number .el-input-number__increase:hover) {
background: transparent;
color: #66b1ff;
}
.action-buttons {
display: flex;
gap: 12px;
padding: 20px;
background: transparent;
position: sticky;
bottom: 0;
z-index: 10;
}
.action-buttons .el-button {
flex: 1;
background: #729fff;
border: none;
border-radius: 25px;
color: white;
font-size: 16px;
font-weight: 600;
padding: 14px 24px;
box-shadow: 0 4px 15px rgba(96, 197, 255, 0.3);
transition: all 0.3s ease;
text-transform: none;
}
.action-buttons .el-button:hover {
background: linear-gradient(135deg, #ff5722, #ff7043);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(255, 107, 53, 0.4);
}
.action-buttons .el-button:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(255, 107, 53, 0.3);
}
.action-buttons .el-button:disabled {
background: linear-gradient(135deg, #ccc, #ddd);
color: #999;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.product-reviews {
padding: 20px;
background: transparent;
}
.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: transparent;
}
.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>