SunnyFarm/frontend/src/views/product/ProductDetail.vue
superlishunqin 364b7acbb7 END
2025-10-10 23:22:52 +08:00

1633 lines
40 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-layout">
<!-- 顶部导航栏 -->
<el-header class="main-header">
<div class="header-content">
<div class="logo-section">
<h1 class="logo" @click="goToHome">🌱 农产品直销平台</h1>
</div>
<div class="search-section">
<el-input
v-model="searchKeyword"
placeholder="搜索农产品...(按下回车键搜索)"
class="search-input"
@keyup.enter="handleSearch"
clearable
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<div class="user-section">
<!-- 购物车图标 -->
<div class="cart-container">
<el-badge :value="cartCount" :hidden="cartCount === 0" class="cart-badge">
<div class="cart-icon" @click="goToCart">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 3H5L5.4 5M7 13H17L21 5H5.4M7 13L5.4 5M7 13L4.7 15.3C4.3 15.7 4.6 16.5 5.1 16.5H17M17 13V16.5M9 19.5C9.8 19.5 10.5 20.2 10.5 21S9.8 22.5 9 22.5 7.5 21.8 7.5 21 8.2 19.5 9 19.5ZM20 19.5C20.8 19.5 21.5 20.2 21.5 21S20.8 22.5 20 22.5 18.5 21.8 18.5 21 19.2 19.5 20 19.5Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"/>
</svg>
<span class="cart-text">购物车</span>
</div>
</el-badge>
</div>
<el-dropdown @command="handleUserAction">
<div class="user-info">
<el-avatar :src="userStore.user?.avatar || defaultAvatar" size="small" />
<span class="username">{{ userStore.user?.nickname || userStore.user?.username }}</span>
<el-icon><ArrowDown /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">个人中心</el-dropdown-item>
<el-dropdown-item command="orders">我的订单</el-dropdown-item>
<el-dropdown-item command="cart">购物车</el-dropdown-item>
<el-dropdown-item command="addresses">收货地址</el-dropdown-item>
<el-dropdown-item command="favorites">我的收藏</el-dropdown-item>
<el-dropdown-item command="announcements">系统公告</el-dropdown-item>
<el-dropdown-item divided command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</el-header>
<!-- 商品详情内容 -->
<div class="product-detail">
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<el-skeleton :rows="10" animated />
</div>
<!-- 商品详情内容 -->
<div v-else-if="product" class="detail-content">
<!-- 面包屑导航 -->
<el-breadcrumb separator="/" class="breadcrumb">
<el-breadcrumb-item>
<router-link to="/">首页</router-link>
</el-breadcrumb-item>
<el-breadcrumb-item>
<span>{{ product.categoryName || '商品分类' }}</span>
</el-breadcrumb-item>
<el-breadcrumb-item>{{ product.name }}</el-breadcrumb-item>
</el-breadcrumb>
<!-- 商品主要信息 -->
<div class="product-main">
<!-- 商品图片 -->
<div class="product-images">
<div class="main-image">
<img :src="currentImage" :alt="product.name" @error="handleImageError" />
</div>
<div class="image-list" v-if="product.imageList && product.imageList.length > 1">
<img
v-for="(image, index) in product.imageList"
:key="index"
:src="image"
:alt="`${product.name}-${index + 1}`"
:class="{ active: currentImage === image }"
@click="currentImage = image"
@error="handleImageError"
/>
</div>
</div>
<!-- 商品信息 -->
<div class="product-info">
<div class="info-header">
<h1 class="product-title">{{ product.name }}</h1>
<div class="favorite-star" @click="toggleFavorite" :class="{ 'is-favorite': isFavorite }">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
</div>
</div>
<!-- 价格信息 -->
<div class="price-info">
<span class="current-price">¥{{ product.price }}</span>
<span v-if="product.originPrice && product.originPrice > product.price" class="origin-price">
¥{{ product.originPrice }}
</span>
</div>
<!-- 商品属性 -->
<div class="product-attrs">
<div class="attr-item" v-if="product.origin">
<span class="attr-label">产地</span>
<span class="attr-value">{{ product.origin }}</span>
</div>
<div class="attr-item" v-if="product.unit">
<span class="attr-label">规格</span>
<span class="attr-value">{{ product.unit }}</span>
</div>
<div class="attr-item" v-if="product.weight">
<span class="attr-label">重量</span>
<span class="attr-value">{{ product.weight }}</span>
</div>
<div class="attr-item" v-if="product.shelfLife">
<span class="attr-label">保质期</span>
<span class="attr-value">{{ product.shelfLife }}</span>
</div>
<div class="attr-item" v-if="product.storageMethod">
<span class="attr-label">储存方式</span>
<span class="attr-value">{{ product.storageMethod }}</span>
</div>
<div class="attr-item">
<span class="attr-label">库存</span>
<span class="attr-value" :class="{ 'low-stock': product.stock < 10 }">
{{ product.stock || 0 }}
</span>
</div>
<div class="attr-item">
<span class="attr-label">销量</span>
<span class="attr-value">{{ product.salesCount || 0 }}</span>
</div>
</div>
<!-- 购买数量 -->
<div class="quantity-selector">
<span class="quantity-label">数量</span>
<el-input-number
v-model="quantity"
:min="1"
:max="product.stock || 1"
size="large"
/>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<div class="main-actions">
<el-button
type="primary"
size="large"
class="cart-btn"
:disabled="!product.stock || product.stock <= 0"
@click="addToCart"
:loading="addingToCart"
>
<el-icon><ShoppingCart /></el-icon>
<span>加入购物车</span>
</el-button>
<el-button
type="danger"
size="large"
class="buy-btn"
:disabled="!product.stock || product.stock <= 0"
@click="buyNow"
:loading="buyingNow"
>
<span>立即购买</span>
</el-button>
</div>
</div>
</div>
</div>
<!-- 商品详情描述 -->
<div class="product-description">
<el-tabs v-model="activeTab">
<el-tab-pane label="商品详情" name="description">
<div class="description-content">
<div v-if="product.description" v-html="product.description"></div>
<div v-else class="no-description">暂无商品详情描述</div>
</div>
</el-tab-pane>
<el-tab-pane label="商品评价" name="reviews">
<div class="reviews-content">
<!-- 评价统计 -->
<div class="review-summary">
<div class="rating-overview">
<div class="avg-rating">
<span class="rating-number">{{ avgRating }}</span>
<el-rate
v-model="avgRating"
disabled
show-score
text-color="#ff9900"
score-template="{value}"
/>
<span class="review-count">{{ reviewCount }}条评价</span>
</div>
</div>
</div>
<!-- 评价列表 -->
<div class="review-list" v-loading="reviewsLoading">
<div v-if="reviews.length === 0" class="no-reviews">
暂无评价
</div>
<div v-else>
<div
v-for="review in reviews"
:key="review.id"
class="review-item"
>
<div class="review-header">
<div class="user-info">
<el-avatar :src="review.user?.avatar || defaultAvatar" size="small" />
<span class="username">{{ review.user?.nickname || review.user?.username || '匿名用户' }}</span>
</div>
<div class="review-rating">
<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>
<div v-if="review.reply" class="merchant-reply">
<div class="reply-header">
<span class="reply-label">商家回复</span>
</div>
<p class="reply-content">{{ review.reply }}</p>
</div>
</div>
</div>
<!-- 分页 -->
<div v-if="reviewCount > 0" class="review-pagination">
<el-pagination
v-model:current-page="reviewPage"
:page-size="reviewPageSize"
:total="reviewCount"
layout="prev, pager, next"
@current-change="handleReviewPageChange"
/>
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- 推荐商品 -->
<div class="recommended-products">
<h3>猜你喜欢</h3>
<div class="products-grid">
<div
v-for="item in recommendedProducts"
:key="item.id"
class="product-card"
@click="goToProduct(item.id)"
>
<img :src="getProductImage(item)" :alt="item.name" @error="handleImageError" />
<div class="product-name">{{ item.name }}</div>
<div class="product-price">¥{{ item.price }}</div>
</div>
</div>
</div>
</div>
<!-- 商品不存在 -->
<div v-else class="not-found">
<el-empty description="商品不存在或已下架" />
<el-button type="primary" @click="goToHome">返回首页</el-button>
</div>
</div>
<!-- 右下角聊天悬浮球 - 使用Teleport挂载到body -->
<Teleport to="body">
<div
v-if="product && product.merchantId"
class="chat-fab"
:style="{
left: fabPos.x + 'px',
top: fabPos.y + 'px'
}"
@mousedown="startDrag"
@touchstart="startDragTouch"
title="联系商家"
@click="openChat"
>
<span class="chat-fab-icon">💬</span>
</div>
</Teleport>
<!-- 聊天窗口 - 使用Teleport挂载到body -->
<Teleport to="body">
<ChatWindow
v-if="product && product.merchantId && showChatWindow"
:visible="showChatWindow"
:merchant-id="product.merchantId"
:merchant-name="product.merchantName || '商家'"
user-type="user"
@close="closeChatWindow"
@minimize="minimizeChatWindow"
:anchor-bottom="anchorBottom"
:anchor-right="anchorRight"
/>
</Teleport>
</div>
</template>
<script>
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { getProductDetail, getHotProducts } from '../../api/product'
import cartApi from '../../api/cart'
import favoriteApi from '../../api/favorite'
import { getProductReviews, getProductRating } from '../../api/review'
import { userStore } from '../../store/user'
import { ShoppingCart, Star, StarFilled, ArrowDown, Search } from '@element-plus/icons-vue'
import ChatWindow from '../../components/ChatWindow.vue'
export default {
name: 'ProductDetail',
components: {
ChatWindow,
ShoppingCart,
Star,
StarFilled,
ArrowDown,
Search
},
setup() {
const router = useRouter()
const route = useRoute()
// 基本数据
const loading = ref(true)
const product = ref(null)
const quantity = ref(1)
const currentImage = ref('')
const activeTab = ref('description')
const addingToCart = ref(false)
const buyingNow = ref(false)
const favoritingLoading = ref(false)
const isFavorite = ref(false)
const recommendedProducts = ref([])
const searchKeyword = ref('')
const cartCount = ref(0)
const showChatWindow = ref(false)
// 评价相关数据
const reviews = ref([])
const avgRating = ref(0)
const reviewCount = ref(0)
const reviewsLoading = ref(false)
const reviewPage = ref(1)
const reviewPageSize = ref(10)
// 拖拽相关
const fabPos = ref({
x: typeof window !== 'undefined' ? window.innerWidth - 80 : 0,
y: typeof window !== 'undefined' ? window.innerHeight - 80 : 0
})
const dragging = ref(false)
const dragOffset = ref({ x: 0, y: 0 })
// 常量
const defaultAvatar = 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
const defaultImage = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjQwMCIgdmlld0JveD0iMCAwIDQwMCA0MDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSI0MDAiIGhlaWdodD0iNDAwIiBmaWxsPSIjRjVGNUY1Ii8+Cjx0ZXh0IHg9IjIwMCIgeT0iMjAwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCIgZmlsbD0iIzk5OTk5OSIgZm9udC1zaXplPSIxNiI+5pqC5peg5Zu+54mHPC90ZXh0Pgo8L3N2Zz4='
// 计算属性
const isLoggedIn = computed(() => userStore.isLoggedIn)
const anchorBottom = computed(() => fabPos.value.y > window.innerHeight / 2)
const anchorRight = computed(() => fabPos.value.x > window.innerWidth / 2)
// 加载商品详情
const loadProductDetail = async () => {
try {
loading.value = true
const productId = route.params.id
const productData = await getProductDetail(productId)
if (productData) {
product.value = productData
if (product.value.imageList && product.value.imageList.length > 0) {
currentImage.value = product.value.imageList[0]
} else {
currentImage.value = defaultImage
}
} else {
ElMessage.error('商品不存在或已下架')
}
} catch (error) {
console.error('获取商品详情失败:', error)
ElMessage.error('获取商品详情失败')
} finally {
loading.value = false
}
}
// 加载推荐商品
const loadRecommendedProducts = async () => {
try {
const hotProductsData = await getHotProducts()
recommendedProducts.value = hotProductsData || []
} catch (error) {
console.error('加载推荐商品失败:', error)
}
}
// 加载购物车数量
const loadCartCount = async () => {
try {
if (userStore.isLoggedIn) {
const count = await cartApi.getCartCount()
cartCount.value = count || 0
}
} catch (error) {
console.error('获取购物车数量失败:', error)
}
}
// 检查收藏状态
const checkFavoriteStatus = async () => {
if (!userStore.isLoggedIn || !product.value) return
try {
const result = await favoriteApi.checkFavorite(product.value.id)
isFavorite.value = result || false
} catch (error) {
console.log('检查收藏状态失败:', error)
isFavorite.value = false
}
}
// 加载商品评价
const loadReviews = async () => {
try {
reviewsLoading.value = true
const productId = route.params.id
try {
const reviewResponse = await getProductReviews(productId, {
page: reviewPage.value,
size: reviewPageSize.value
})
reviews.value = reviewResponse.records || []
reviewCount.value = reviewResponse.total || 0
try {
const ratingResponse = await getProductRating(productId)
avgRating.value = Number(ratingResponse) || 0
} catch (ratingError) {
console.log('评分API暂不可用使用默认值')
avgRating.value = 0
}
} catch (apiError) {
console.log('评价API暂不可用使用默认值')
reviews.value = []
reviewCount.value = 0
avgRating.value = 0
}
} catch (error) {
console.log('评价功能初始化失败:', error)
} finally {
reviewsLoading.value = false
}
}
// 评价分页处理
const handleReviewPageChange = (page) => {
reviewPage.value = page
loadReviews()
}
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return ''
return new Date(dateStr).toLocaleDateString()
}
// 添加到购物车
const handleAddToCart = async () => {
if (!userStore.isLoggedIn) {
ElMessage.warning('请先登录')
router.push('/login')
return
}
try {
addingToCart.value = true
await cartApi.addToCart(product.value.id, quantity.value)
ElMessage.success('已加入购物车')
await loadCartCount()
} catch (error) {
ElMessage.error('添加到购物车失败')
} finally {
addingToCart.value = false
}
}
// 立即购买
const buyNow = () => {
if (!userStore.isLoggedIn) {
ElMessage.warning('请先登录')
router.push('/login')
return
}
router.push({
path: '/checkout',
query: {
type: 'buy-now',
productId: product.value.id,
quantity: quantity.value
}
})
}
// 切换收藏状态
const toggleFavorite = async () => {
if (!userStore.isLoggedIn) {
ElMessage.warning('请先登录')
router.push('/login')
return
}
try {
favoritingLoading.value = true
if (isFavorite.value) {
await favoriteApi.removeFavorite(product.value.id)
isFavorite.value = false
ElMessage.success('取消收藏成功')
} else {
await favoriteApi.addFavorite(product.value.id)
isFavorite.value = true
ElMessage.success('收藏成功')
}
} catch (error) {
console.error('收藏操作失败:', error)
ElMessage.error('操作失败,请稍后重试')
} finally {
favoritingLoading.value = false
}
}
// 聊天相关
const openChat = () => {
if (!isLoggedIn.value) {
ElMessage.warning('请先登录')
router.push('/login')
return
}
showChatWindow.value = true
}
const closeChatWindow = () => {
showChatWindow.value = false
}
const minimizeChatWindow = () => {
showChatWindow.value = false
}
// 拖拽功能
const clampPos = (x, y) => {
const w = window.innerWidth
const h = window.innerHeight
const size = 56
const padding = 8
const nx = Math.min(Math.max(padding, x), w - size - padding)
const ny = Math.min(Math.max(padding, y), h - size - padding)
return { x: nx, y: ny }
}
const startDrag = (e) => {
dragging.value = true
dragOffset.value = { x: e.clientX - fabPos.value.x, y: e.clientY - fabPos.value.y }
const onDragMove = (e) => {
if (!dragging.value) return
const pos = clampPos(e.clientX - dragOffset.value.x, e.clientY - dragOffset.value.y)
fabPos.value = pos
}
const endDrag = () => {
dragging.value = false
window.removeEventListener('mousemove', onDragMove)
window.removeEventListener('mouseup', endDrag)
try {
localStorage.setItem('chatFabPos', JSON.stringify(fabPos.value))
} catch (e) {}
}
window.addEventListener('mousemove', onDragMove)
window.addEventListener('mouseup', endDrag)
}
const startDragTouch = (e) => {
e.preventDefault()
const touch = e.touches[0]
dragging.value = true
dragOffset.value = { x: touch.clientX - fabPos.value.x, y: touch.clientY - fabPos.value.y }
const onDragMoveTouch = (e) => {
if (!dragging.value) return
const touch = e.touches[0]
const pos = clampPos(touch.clientX - dragOffset.value.x, touch.clientY - dragOffset.value.y)
fabPos.value = pos
}
const endDragTouch = () => {
dragging.value = false
window.removeEventListener('touchmove', onDragMoveTouch)
window.removeEventListener('touchend', endDragTouch)
try {
localStorage.setItem('chatFabPos', JSON.stringify(fabPos.value))
} catch (e) {}
}
window.addEventListener('touchmove', onDragMoveTouch, { passive: false })
window.addEventListener('touchend', endDragTouch)
}
// 工具方法
const getProductImage = (product) => {
if (product?.imageList && product.imageList.length > 0) {
return product.imageList[0]
}
return product?.mainImage || product?.imageUrl || defaultImage
}
const handleImageError = (event) => {
event.target.src = defaultImage
}
const goToHome = () => {
router.push('/')
}
const handleSearch = () => {
if (searchKeyword.value.trim()) {
router.push(`/?search=${encodeURIComponent(searchKeyword.value.trim())}`)
}
}
const goToCart = () => {
if (!userStore.isLoggedIn) {
ElMessage.warning('请先登录')
router.push('/login')
return
}
router.push('/cart')
}
const handleUserAction = (command) => {
switch (command) {
case 'profile':
router.push('/profile')
break
case 'orders':
router.push('/orders')
break
case 'cart':
router.push('/cart')
break
case 'addresses':
router.push('/addresses')
break
case 'favorites':
router.push('/favorites')
break
case 'announcements':
router.push('/announcements')
break
case 'logout':
handleLogout()
break
}
}
const handleLogout = async () => {
try {
await userStore.logout()
ElMessage.success('退出登录成功')
router.push('/login')
} catch (error) {
ElMessage.error('退出登录失败')
}
}
const goToProduct = (productId) => {
router.push(`/product/${productId}`)
}
// 监听路由参数变化
watch(() => route.params.id, (newId, oldId) => {
if (newId && newId !== oldId) {
// 重置状态
loading.value = true
product.value = null
currentImage.value = ''
quantity.value = 1
isFavorite.value = false
activeTab.value = 'description'
reviewPage.value = 1
// 加载新商品数据
loadProductDetail().then(() => {
checkFavoriteStatus()
})
loadRecommendedProducts()
loadCartCount()
loadReviews()
// 滚动到顶部
window.scrollTo({ top: 0, behavior: 'smooth' })
}
})
// 页面挂载时加载数据
onMounted(() => {
loadProductDetail().then(() => {
checkFavoriteStatus()
})
loadRecommendedProducts()
loadCartCount()
loadReviews()
// 恢复拖拽位置
try {
const saved = localStorage.getItem('chatFabPos')
if (saved) {
const pos = JSON.parse(saved)
if (pos && typeof pos.x === 'number' && typeof pos.y === 'number') {
fabPos.value = clampPos(pos.x, pos.y)
}
}
} catch (e) {}
// 监听窗口大小变化
const handleResize = () => {
// 如果悬浮球在右侧,保持相对于右边的距离
const rightDistance = window.innerWidth - fabPos.value.x - 56
const bottomDistance = window.innerHeight - fabPos.value.y - 56
if (rightDistance < 100) {
// 在右侧,保持相对右边的距离
fabPos.value.x = window.innerWidth - rightDistance - 56
}
if (bottomDistance < 100) {
// 在底部,保持相对底部的距离
fabPos.value.y = window.innerHeight - bottomDistance - 56
}
// 最后确保不超出边界
fabPos.value = clampPos(fabPos.value.x, fabPos.value.y)
}
window.addEventListener('resize', handleResize)
// 组件卸载时清理
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
})
})
return {
// 基本数据
product,
loading,
quantity,
currentImage,
activeTab,
addingToCart,
buyingNow,
favoritingLoading,
isFavorite,
recommendedProducts,
searchKeyword,
cartCount,
showChatWindow,
// 评价相关数据
reviews,
avgRating,
reviewCount,
reviewsLoading,
reviewPage,
reviewPageSize,
// 拖拽相关
fabPos,
// 计算属性
isLoggedIn,
anchorBottom,
anchorRight,
// 常量
defaultAvatar,
defaultImage,
// 方法
loadReviews,
handleReviewPageChange,
formatDate,
addToCart: handleAddToCart,
buyNow,
toggleFavorite,
openChat,
closeChatWindow,
minimizeChatWindow,
startDrag,
startDragTouch,
getProductImage,
handleImageError,
goToHome,
handleSearch,
goToCart,
handleUserAction,
goToProduct,
// Store
userStore
}
}
}
</script>
<style scoped>
.product-detail-layout {
min-height: 100vh;
background-color: #f5f7fa;
}
/* 头部导航样式 */
.main-header {
background: white;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
padding: 0;
height: 56px;
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
height: 100%;
}
.logo-section .logo {
font-size: 1.5rem;
font-weight: bold;
color: #4CAF50;
margin: 0;
cursor: pointer;
transition: color 0.3s;
}
.logo-section .logo:hover {
color: #45a049;
}
.search-section {
flex: 1;
max-width: 600px;
margin: 0 40px;
}
.search-input {
width: 100%;
}
.user-section {
display: flex;
align-items: center;
gap: 20px;
}
.cart-container {
position: relative;
display: flex;
align-items: center;
}
.cart-badge {
display: flex;
}
.cart-container {
position: relative;
}
.cart-icon {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
cursor: pointer;
transition: all 0.2s ease;
color: #4CAF50;
border-radius: 8px;
}
.cart-icon:hover {
background: rgba(76, 175, 80, 0.08);
color: #45a049;
transform: scale(1.05);
}
.cart-icon svg {
width: 22px;
height: 22px;
margin-bottom: 2px;
}
.cart-text {
font-size: 10px;
font-weight: 500;
line-height: 1;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 8px 12px;
border-radius: 6px;
transition: background-color 0.3s;
}
.user-info:hover {
background-color: #f5f7fa;
}
.username {
font-weight: 500;
color: #333;
}
/* 商品详情样式 */
.product-detail {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.loading-container {
padding: 40px;
}
.breadcrumb {
margin-bottom: 20px;
}
.breadcrumb a {
color: #409EFF;
text-decoration: none;
}
.product-main {
display: flex;
gap: 40px;
margin-bottom: 40px;
}
.product-images {
flex: 1;
max-width: 500px;
}
.main-image {
width: 100%;
height: 490px;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
margin-bottom: 10px;
background: white;
}
.main-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.image-list {
display: flex;
gap: 8px;
overflow-x: auto;
}
.image-list img {
width: 80px;
height: 80px;
object-fit: cover;
border: 2px solid #e0e0e0;
border-radius: 4px;
cursor: pointer;
transition: border-color 0.3s;
}
.image-list img.active,
.image-list img:hover {
border-color: #409EFF;
}
.product-info {
flex: 1;
min-width: 400px;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.product-title {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
line-height: 1.4;
}
/* 商品信息头部样式 */
.info-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
}
.favorite-star {
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 215, 0, 0.1);
border: 2px solid #E0E0E0;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
color: #E0E0E0;
flex-shrink: 0;
}
.favorite-star:hover {
border-color: #FFD700;
background: rgba(255, 215, 0, 0.2);
color: #FFD700;
transform: scale(1.1);
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3);
}
.favorite-star.is-favorite {
border-color: #FFD700;
background: linear-gradient(135deg, #FFD700 0%, #FFC107 100%);
color: white;
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.4);
}
.favorite-star.is-favorite:hover {
background: linear-gradient(135deg, #FFC107 0%, #FF9800 100%);
transform: scale(1.15);
box-shadow: 0 6px 20px rgba(255, 215, 0, 0.5);
}
.favorite-star svg {
transition: transform 0.2s ease;
}
.favorite-star:active svg {
transform: scale(0.9);
}
/* 操作按钮优化样式 */
.action-buttons {
display: flex;
gap: 12px;
}
.main-actions {
display: flex;
gap: 12px;
width: 100%;
}
.main-actions .el-button {
flex: 1;
height: 52px;
font-size: 16px;
font-weight: 600;
border-radius: 8px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.cart-btn {
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
border: none;
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
}
.cart-btn:hover {
box-shadow: 0 6px 16px rgba(76, 175, 80, 0.4);
transform: translateY(-2px);
}
.cart-btn:active {
transform: translateY(0);
}
.cart-btn .el-icon {
margin-right: 8px;
font-size: 18px;
}
.buy-btn {
background: linear-gradient(135deg, #FF6B6B 0%, #FF5252 100%);
border: none;
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3);
}
.buy-btn:hover {
box-shadow: 0 6px 16px rgba(255, 107, 107, 0.4);
transform: translateY(-2px);
}
.buy-btn:active {
transform: translateY(0);
}
/* 禁用状态样式 */
.main-actions .el-button:disabled {
background: #f5f5f5 !important;
border-color: #d9d9d9 !important;
color: #bfbfbf !important;
cursor: not-allowed !important;
transform: none !important;
box-shadow: none !important;
}
.price-info {
margin-bottom: 30px;
}
.current-price {
font-size: 32px;
font-weight: bold;
color: #ff4757;
margin-right: 15px;
}
.origin-price {
font-size: 18px;
color: #999;
text-decoration: line-through;
}
.product-attrs {
margin-bottom: 30px;
}
.attr-item {
display: flex;
margin-bottom: 12px;
font-size: 14px;
}
.attr-label {
width: 80px;
color: #666;
flex-shrink: 0;
}
.attr-value {
color: #333;
}
.attr-value.low-stock {
color: #ff4757;
}
.quantity-selector {
display: flex;
align-items: center;
margin-bottom: 30px;
}
.quantity-label {
margin-right: 15px;
font-size: 16px;
color: #333;
}
/* 操作按钮优化样式 */
.action-buttons {
display: flex;
flex-direction: column;
gap: 16px;
}
.main-actions {
display: flex;
gap: 12px;
}
.secondary-actions {
display: flex;
justify-content: center;
}
.main-actions .el-button {
flex: 1;
height: 52px;
font-size: 16px;
font-weight: 600;
border-radius: 8px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.cart-btn {
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
border: none;
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
}
.cart-btn:hover {
box-shadow: 0 6px 16px rgba(76, 175, 80, 0.4);
transform: translateY(-2px);
}
.cart-btn:active {
transform: translateY(0);
}
.cart-btn .el-icon {
margin-right: 8px;
font-size: 18px;
}
.buy-btn {
background: linear-gradient(135deg, #FF6B6B 0%, #FF5252 100%);
border: none;
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3);
}
.buy-btn:hover {
box-shadow: 0 6px 16px rgba(255, 107, 107, 0.4);
transform: translateY(-2px);
}
.buy-btn:active {
transform: translateY(0);
}
.favorite-btn {
width: 140px;
height: 44px;
border: 2px solid #ddd;
background: white;
color: #666;
border-radius: 22px;
font-weight: 500;
transition: all 0.3s ease;
position: relative;
}
.favorite-btn:hover {
border-color: #FFD700;
color: #FFD700;
background: rgba(255, 215, 0, 0.05);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(255, 215, 0, 0.2);
}
.favorite-btn.is-favorite {
border-color: #FFD700;
background: linear-gradient(135deg, #FFD700 0%, #FFC107 100%);
color: white;
box-shadow: 0 4px 12px rgba(255, 215, 0, 0.3);
}
.favorite-btn.is-favorite:hover {
background: linear-gradient(135deg, #FFC107 0%, #FF9800 100%);
box-shadow: 0 6px 16px rgba(255, 215, 0, 0.4);
}
.favorite-btn .el-icon {
margin-right: 6px;
font-size: 16px;
}
/* 禁用状态样式 */
.main-actions .el-button:disabled {
background: #f5f5f5 !important;
border-color: #d9d9d9 !important;
color: #bfbfbf !important;
cursor: not-allowed !important;
transform: none !important;
box-shadow: none !important;
}
.product-description {
margin-bottom: 40px;
background: white;
border-radius: 8px;
padding: 25px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.description-content {
padding: 20px 0;
line-height: 1.8;
color: #333;
}
.no-description,
.no-reviews {
text-align: center;
color: #999;
padding: 40px;
}
/* 评价统计样式 */
.review-summary {
padding: 20px 0;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 20px;
}
.rating-overview {
display: flex;
align-items: center;
}
.avg-rating {
display: flex;
align-items: center;
gap: 12px;
}
.rating-number {
font-size: 32px;
font-weight: bold;
color: #ff9900;
}
.review-count {
color: #666;
font-size: 14px;
}
/* 评价列表样式 */
.review-list {
min-height: 200px;
}
.review-item {
padding: 20px 0;
border-bottom: 1px solid #f5f5f5;
}
.review-item:last-child {
border-bottom: none;
}
.review-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.review-rating {
display: flex;
align-items: center;
gap: 12px;
}
.review-date {
color: #999;
font-size: 12px;
}
.review-content {
margin: 12px 0;
}
.review-content p {
margin: 0;
line-height: 1.6;
color: #333;
}
.merchant-reply {
background: #f8f9fa;
padding: 12px;
border-radius: 6px;
margin-top: 12px;
}
.reply-header {
margin-bottom: 8px;
}
.reply-label {
color: #666;
font-size: 14px;
font-weight: 500;
}
.reply-content {
margin: 0;
color: #333;
line-height: 1.6;
}
.review-pagination {
display: flex;
justify-content: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #f0f0f0;
}
.recommended-products {
background: white;
border-radius: 8px;
padding: 25px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.recommended-products h3 {
margin-bottom: 20px;
font-size: 20px;
color: #333;
}
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 20px;
}
.product-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: transform 0.3s, box-shadow 0.3s;
background: white;
}
.product-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.product-card img {
width: 100%;
height: 120px;
object-fit: cover;
}
.product-name {
padding: 10px;
font-size: 14px;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-price {
padding: 0 10px 10px;
font-size: 16px;
font-weight: bold;
color: #ff4757;
}
.not-found {
text-align: center;
padding: 80px 20px;
}
.chat-fab {
position: fixed !important;
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%);
box-shadow: 0 10px 24px rgba(64, 158, 255, 0.35);
display: flex;
align-items: center;
justify-content: center;
color: white;
cursor: grab;
z-index: 9999;
transition: box-shadow 0.2s ease, transform 0.1s ease-out;
user-select: none;
will-change: left, top;
pointer-events: auto;
}
.chat-fab:active {
cursor: grabbing;
transform: scale(0.98);
box-shadow: 0 8px 18px rgba(64, 158, 255, 0.3);
}
.chat-fab-icon {
font-size: 22px;
line-height: 1;
}
/* 响应式设计 */
@media (max-width: 768px) {
.header-content {
flex-direction: column;
padding: 10px;
gap: 15px;
}
.search-section {
width: 100%;
margin: 0;
}
.product-main {
flex-direction: column;
gap: 20px;
}
.product-info {
min-width: auto;
}
.action-buttons {
flex-direction: column;
}
.action-buttons .el-button {
flex: none;
}
.products-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 15px;
}
}
/* 搜索框样式 */
.search-input :deep(.el-input__wrapper) {
border-radius: 20px;
border: 1px solid #ddd;
background: #ffffff;
box-shadow: none;
padding: 6px 36px;
height: 36px;
transition: all 0.2s ease;
}
.search-input:hover :deep(.el-input__wrapper) {
border-color: #4CAF50;
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.1);
}
.search-input:focus-within :deep(.el-input__wrapper) {
border-color: #4CAF50;
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.1);
}
.cart-badge :deep(.el-badge__content) {
position: absolute;
top: 0;
right: 0;
transform: translate(25%, -25%);
background: #ff4757;
border: 2px solid white;
font-size: 10px;
font-weight: 700;
min-width: 18px;
height: 18px;
line-height: 14px;
padding: 0 4px;
border-radius: 9px;
box-shadow: 0 2px 4px rgba(255, 71, 87, 0.4);
white-space: nowrap;
display: flex;
align-items: center;
justify-content: center;
}
</style>