fix: UI部分完成

This commit is contained in:
2025-05-28 13:22:16 +08:00
parent 72180e9e71
commit b57b43009c
14 changed files with 122 additions and 1640 deletions

View File

@@ -1,5 +1 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
# 奶酪商城 - PC端

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@@ -1,29 +1,88 @@
<template>
<div class="banner-carousel">
<el-carousel
:interval="3000"
height="400px"
indicator-position="outside"
arrow="hover"
<el-carousel
:interval="3000"
height="500px"
indicator-position="outside"
arrow="hover"
>
<el-carousel-item v-for="(banner, index) in banners" :key="index">
<img
:src="banner"
:alt="`轮播图${index + 1}`"
class="w-full h-full object-cover rounded-lg"
>
<a :href="banner.link" target="_blank" class="block w-full h-full relative">
<img
:src="`/demo-img/banner/${banner.image}`"
:alt="banner.title"
class="w-full h-full rounded-lg"
>
<div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-6">
<h3 class="text-white text-2xl font-bold mb-1">{{ banner.title }}</h3>
<p class="text-white/80 text-sm">{{ banner.subtitle }}</p>
</div>
</a>
</el-carousel-item>
</el-carousel>
</div>
</template>
<script setup>
defineProps({
banners: {
type: Array,
default: () => []
}
})
import { ref } from 'vue'
// 轮播图数据
const banners = ref([
// {
// image: 'https://cdn.seovx.com/ha/?mom=302&w=1200&h=400&_=1',
// title: '夏季大促销',
// subtitle: '全场低至5折起',
// link: 'https://example.com/promotion/summer'
// },
{
image: 'vip.png',
title: '会员权益全面更新!',
subtitle: '全新系列震撼登场',
link: 'https://example.com/new-arrivals'
},
{
image: 'hf1.png',
title: '身着汉服,仿佛穿越千年时光,梦回那个诗意盎然的朝代,每一步都踏着历史的回响,每一瞥都藏着古韵今风!',
subtitle: '煌煌华夏,历史绵邈,汉服之美,独具魅力。在多元文化发展的今天,我们更应支持国风汉服,承袭汉服雅韵,让这古老的服饰文化在新时代焕发出新的光彩。',
link: 'https://example.com/new-arrivals'
},
{
image: 'mm1.png',
title: '一片沁润,干涸肌秒变 “水库”!密集补水,让肌肤喝饱水,自带水光滤镜✨',
subtitle: '抗老要趁早!胶原蛋白面膜,提拉紧致,定格肌肤嘭弹少女感🔮',
link: 'https://example.com/new-arrivals'
},
{
image: 'yy1.png',
title: '逃离城市喧嚣,扎进山野怀抱⛺️ 一顶帐篷,一片星空,解锁自由新姿势!',
subtitle: '露营装备天花板来啦!便携炊具、防潮睡垫… 专业级配置,助你轻松拿捏精致露营!',
link: 'https://example.com/new-arrivals'
},
{
image: 'gy1.png',
title: '公益行动,让爱传递!',
subtitle: '爱心汇聚,善意传递。我们用温暖的双手,为孩子们送上知识的礼物,点亮他们心中的希望之光。点滴善意,皆成大爱,共筑美好未来。',
link: 'https://example.com/new-arrivals'
},
{
image: 'gy2.png',
title: '公益服务,助力成长',
subtitle: '以爱之名,陪伴成长。志愿者们耐心守护,为孩子们答疑解惑,在求知路上添砖加瓦。每一次辅导,都是梦想的灌溉,愿他们茁壮成长,绽放光芒。',
link: 'https://example.com/new-arrivals'
},
{
image: 'gy3.png',
title: '公益服务,抗震救灾',
subtitle: '灾难无情,人间有爱。在废墟之上,志愿者们争分夺秒,将物资送到受灾群众手中。我们携手并肩,传递力量与温暖,在困境中重建希望,共克时艰。',
link: 'https://example.com/new-arrivals'
},
{
image: 'gy4.png',
title: '公益服务,关爱老人',
subtitle: '岁月易老,温暖不变。志愿者陪伴在老人身旁,用笑容和行动给予关怀。推轮椅漫步、欢歌相伴,我们用心呵护每一位老人,让他们的晚年充满阳光与爱。',
link: 'https://example.com/new-arrivals'
},
])
</script>
<style scoped>
@@ -31,4 +90,31 @@ defineProps({
border-radius: 8px;
overflow: hidden;
}
.banner-carousel :deep(.el-carousel__item) {
cursor: pointer;
}
.banner-carousel :deep(.el-carousel__arrow) {
background-color: rgba(0, 0, 0, 0.3);
color: white;
width: 40px;
height: 40px;
border-radius: 50%;
font-size: 18px;
}
.banner-carousel :deep(.el-carousel__arrow:hover) {
background-color: rgba(0, 0, 0, 0.6);
}
.banner-carousel :deep(.el-carousel__indicator--horizontal button) {
width: 12px;
height: 12px;
border-radius: 50%;
}
.banner-carousel :deep(.el-carousel__indicator.is-active button) {
background-color: #ff4444;
}
</style>

View File

@@ -21,7 +21,7 @@
@keyup.enter="performSearch"
>
</el-input>
<button class="bg-red-600 text-white px-4 rounded-r-full hover:bg-red-700 transition" @click="performSearch">
<button class="bg-red-600 text-white px-4 rounded-r-full hover:bg-red-700 transition cursor-pointer" @click="performSearch">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>

View File

@@ -4,23 +4,32 @@
<main class="container mx-auto px-4 pt-8 grid grid-cols-12 gap-4">
<!-- 侧边分类导航 -->
<div class="col-span-2 bg-white rounded-lg p-4 hidden md:block">
<div class="col-span-2 bg-white rounded-lg p-4 hidden md:block mb-6">
<h3 class="font-bold text-lg mb-4">所有分类</h3>
<CategoryMenu :categories="categories" />
</div>
<div class="col-span-10">
<!-- 轮播图 -->
<BannerCarousel :banners="banners" class="mb-6" />
</div>
<!-- 侧边分类导航 -->
<div class="col-span-2 bg-white rounded-lg p-4 hidden md:block">
<h3 class="font-bold text-lg mb-4">所有分类</h3>
<CategoryMenu :categories="categories" />
<BannerCarousel />
</div>
<!-- &lt;!&ndash; 侧边分类导航 &ndash;&gt;-->
<!-- <div class="col-span-2 bg-white rounded-lg p-4 hidden md:block">-->
<!-- <h3 class="font-bold text-lg mb-4">所有分类</h3>-->
<!-- <CategoryMenu :categories="categories" />-->
<!-- </div>-->
<!-- &lt;!&ndash; 主内容区 &ndash;&gt;-->
<!-- <div class="col-span-10">-->
<!-- &lt;!&ndash; 限时抢购 &ndash;&gt;-->
<!-- <FlashSaleSection-->
<!-- :products="flashSaleProducts"-->
<!-- :countdown="countdown"-->
<!-- @product-click="handleProductClick"-->
<!-- class="mb-6"-->
<!-- />-->
<!-- </div>-->
<!-- 主内容区 -->
<div class="col-span-10">
<div class="col-span-12">
<!-- 限时抢购 -->
<FlashSaleSection
:products="flashSaleProducts"
@@ -160,13 +169,6 @@ const categories = [
}
]
// 轮播图数据
const banners = [
'https://cdn.seovx.com/ha/?mom=302&w=1200&h=400&_=1',
'https://cdn.seovx.com/ha/?mom=302&w=1200&h=400&_=2',
'https://cdn.seovx.com/ha/?mom=302&w=1200&h=400&_=3'
]
// 商品数据
const flashSaleProducts = ref([])
const hotProducts = ref([])

View File

@@ -1,841 +0,0 @@
<template>
<div class="product-detail-page bg-gray-50 min-h-screen">
<main class="container mx-auto px-4 py-6">
<div v-if="loading" class="flex justify-center items-center h-96">
<el-icon class="is-loading text-4xl text-gray-400">
<Loading />
</el-icon>
</div>
<div v-else-if="product" class="max-w-7xl mx-auto">
<!-- 面包屑导航 -->
<el-breadcrumb separator="/" class="mb-6">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: '/products' }">商品列表</el-breadcrumb-item>
<el-breadcrumb-item>{{ product.title }}</el-breadcrumb-item>
</el-breadcrumb>
<div class="grid grid-cols-12 gap-8">
<!-- 商品图片区域 -->
<div class="col-span-12 lg:col-span-5">
<div class="bg-white rounded-lg p-6 sticky top-40">
<!-- 主图展示 -->
<div class="main-image-container relative mb-4">
<div
class="main-image-wrapper relative overflow-hidden rounded-lg"
@mousemove="handleMouseMove"
@mouseleave="showMagnifier = false"
@mouseenter="showMagnifier = true"
>
<img
ref="mainImageRef"
:src="currentImage"
:alt="product.title"
class="w-full h-96 object-cover cursor-crosshair"
>
<!-- 放大镜遮罩 -->
<div
v-show="showMagnifier"
class="magnifier-mask absolute border-2 border-red-500 bg-opacity-20 pointer-events-none"
:style="maskStyle"
></div>
</div>
</div>
<!-- 缩略图列表 -->
<div class="thumbnail-list">
<div class="flex space-x-2 overflow-x-auto pb-2">
<img
v-for="(image, index) in allImages"
:key="index"
:src="image"
:alt="`商品图片${index + 1}`"
class="thumbnail-item w-16 h-16 object-cover rounded cursor-pointer border-2 transition-all"
:class="{
'border-red-500 ring-2 ring-red-200': currentImage === image,
'border-gray-200 hover:border-red-300': currentImage !== image
}"
@click="currentImage = image"
>
</div>
</div>
<!-- 商品操作按钮 -->
<div class="product-actions mt-6 space-y-3">
<div class="flex space-x-3">
<el-button
type="danger"
size="large"
class="flex-1"
:disabled="!isSpecsSelected"
@click="handleBuyNow"
>
立即购买
</el-button>
<el-button
size="large"
class="flex-1"
:disabled="!isSpecsSelected"
@click="handleAddToCart"
>
加入购物车
</el-button>
</div>
<div class="flex space-x-3">
<el-button
class="flex-1"
@click="toggleFavorite"
:type="isFavorited ? 'danger' : 'default'"
>
<el-icon class="mr-1">
<Star v-if="isFavorited" />
<StarFilled v-else />
</el-icon>
{{ isFavorited ? '已收藏' : '收藏商品' }}
</el-button>
<el-button class="flex-1">
<el-icon class="mr-1">
<Share />
</el-icon>
分享
</el-button>
</div>
</div>
</div>
</div>
<!-- 商品信息区域 -->
<div class="col-span-12 lg:col-span-7">
<div class="bg-white rounded-lg p-6 mb-6">
<!-- 商品标题和价格 -->
<div class="product-header mb-6">
<h1 class="text-2xl font-bold text-gray-800 mb-4 leading-relaxed">{{ product.title }}</h1>
<div class="price-section mb-4">
<div class="flex items-baseline space-x-4 mb-2">
<span class="text-4xl font-bold text-red-600">¥{{ product.price }}</span>
<span class="text-lg text-gray-400 line-through">¥{{
(parseFloat(product.price) * 1.3).toFixed(2)
}}</span>
</div>
<div class="flex items-center space-x-6 text-sm text-gray-600">
<span>已售 {{ product.sold }} 件</span>
<div class="flex items-center">
<el-rate v-model="product.rating" disabled size="small" />
<span class="ml-1">({{ product.reviewCount || 0 }}条评价)</span>
</div>
</div>
</div>
<!-- 促销信息 -->
<div class="promotion-tags flex flex-wrap gap-2 mb-4">
<el-tag type="danger" size="small">限时特价</el-tag>
<el-tag type="warning" size="small">满200减30</el-tag>
<el-tag type="success" size="small">包邮</el-tag>
<el-tag type="info" size="small">7天无理由退货</el-tag>
</div>
</div>
<!-- 商品规格选择 -->
<div class="product-specs mb-8">
<!-- 颜色选择 -->
<div class="spec-group mb-6">
<div class="spec-label mb-3">
<span class="text-sm font-medium text-gray-700">颜色分类</span>
<span v-if="selectedSpecs.color" class="ml-2 text-sm text-gray-500">
已选:{{ selectedSpecs.color }}
</span>
</div>
<div class="spec-options flex flex-wrap gap-3">
<div
v-for="color in colors"
:key="color.name"
class="spec-option color-option relative cursor-pointer"
:class="{ 'selected': selectedSpecs.color === color.name }"
@click="selectedSpecs.color = color.name"
>
<img
:src="color.image"
:alt="color.name"
class="w-16 h-16 object-cover rounded border-2"
:class="{
'border-red-500': selectedSpecs.color === color.name,
'border-gray-200': selectedSpecs.color !== color.name
}"
>
<span class="block text-xs text-center mt-1">{{ color.name }}</span>
<div
v-if="selectedSpecs.color === color.name"
class="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full flex items-center justify-center"
>
<el-icon class="text-white text-xs">
<Check />
</el-icon>
</div>
</div>
</div>
</div>
<!-- 尺寸选择 -->
<div class="spec-group mb-6">
<div class="spec-label mb-3">
<span class="text-sm font-medium text-gray-700">尺码</span>
<span v-if="selectedSpecs.size" class="ml-2 text-sm text-gray-500">
已选:{{ selectedSpecs.size }}
</span>
<el-button text type="primary" size="small" class="ml-2">尺码表</el-button>
</div>
<div class="spec-options flex flex-wrap gap-3">
<button
v-for="size in sizes"
:key="size.name"
class="size-option px-4 py-2 border rounded text-sm font-medium transition-all"
:class="{
'border-red-500 bg-red-50 text-red-600': selectedSpecs.size === size.name,
'border-gray-300 hover:border-red-300': selectedSpecs.size !== size.name,
'opacity-50 cursor-not-allowed': !size.available
}"
:disabled="!size.available"
@click="selectedSpecs.size = size.name"
>
{{ size.name }}
<span v-if="!size.available" class="block text-xs text-gray-400">缺货</span>
</button>
</div>
</div>
<!-- 数量选择 -->
<div class="spec-group mb-6">
<div class="spec-label mb-3">
<span class="text-sm font-medium text-gray-700">数量</span>
<span class="ml-2 text-sm text-gray-500">库存{{ stock }}件</span>
</div>
<div class="quantity-selector flex items-center space-x-4">
<el-input-number
v-model="quantity"
:min="1"
:max="Math.min(stock, 99)"
size="large"
class="quantity-input"
/>
<span class="text-sm text-gray-500">件</span>
</div>
</div>
</div>
<!-- 服务保障 -->
<div class="service-guarantee mb-6">
<h3 class="text-sm font-medium text-gray-700 mb-3">服务保障</h3>
<div class="grid grid-cols-2 gap-4">
<div class="service-item flex items-center space-x-2">
<el-icon class="text-green-500">
<CircleCheckFilled />
</el-icon>
<span class="text-sm">正品保证</span>
</div>
<div class="service-item flex items-center space-x-2">
<el-icon class="text-blue-500">
<Clock />
</el-icon>
<span class="text-sm">快速发货</span>
</div>
<div class="service-item flex items-center space-x-2">
<el-icon class="text-orange-500">
<RefreshRight />
</el-icon>
<span class="text-sm">7天无理由退货</span>
</div>
<div class="service-item flex items-center space-x-2">
<el-icon class="text-purple-500">
<Service />
</el-icon>
<span class="text-sm">24小时客服</span>
</div>
</div>
</div>
</div>
<!-- 商品详情标签页 -->
<div class="bg-white rounded-lg">
<el-tabs
v-model="activeTab"
class="product-detail-tabs"
>
<el-tab-pane label="商品详情" name="detail"
:class="[
{ 'text-red-600 border-b-2 border-red-600': activeTab === 'detail' },
{ 'text-gray-600': activeTab !== 'detail' }
]"
>
<div class="detail-content p-6">
<div class="detail-images space-y-4">
<img
v-for="(image, index) in detailImages"
:key="index"
:src="image"
:alt="`详情图${index + 1}`"
class="w-full rounded-lg"
>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="规格参数" name="specs">
<div class="specs-content p-6 bg-white rounded-lg shadow-sm">
<!-- 标题区域 -->
<div class="mb-6">
<h3 class="text-xl font-semibold text-gray-800 mb-2">产品规格参数</h3>
<p class="text-gray-500 text-sm">详细了解产品的技术参数和性能指标</p>
</div>
<!-- 规格参数表格 -->
<div class="overflow-x-auto">
<table class="w-full">
<tbody>
<tr
v-for="spec in productSpecs"
:key="spec.name"
class="border-b border-gray-200 hover:bg-gray-50 transition-colors"
>
<td class="py-4 pr-8 text-gray-600 font-medium min-w-[120px]">
{{ spec.name }}
</td>
<td class="py-4 text-gray-800">
<!-- 根据参数类型显示不同样式 -->
<span
v-if="spec.type === 'number'"
class="font-mono"
>{{ spec.value }}</span>
<span
v-else-if="spec.type === 'status'"
:class="{
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium': true,
'bg-green-100 text-green-800': spec.value === '支持',
'bg-red-100 text-red-800': spec.value === '不支持',
'bg-blue-100 text-blue-800': spec.value === '可选'
}"
>{{ spec.value }}</span>
<span v-else>{{ spec.value }}</span>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 无数据状态 -->
<div
v-if="productSpecs.length === 0"
class="py-12 text-center"
>
<div class="flex flex-col items-center">
<el-empty description="暂无规格参数数据" />
<el-button
type="primary"
size="small"
class="mt-4"
@click="fetchProductSpecs"
>
刷新数据
</el-button>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane :label="`用户评价(${reviews.length})`" name="reviews">
<div class="reviews-content p-6">
<!-- 评价统计 -->
<div class="review-summary mb-6 p-4 bg-gray-50 rounded-lg">
<div class="flex items-center space-x-8">
<div class="text-center">
<div class="text-3xl font-bold text-red-600 mb-1">{{ averageRating.toFixed(1) }}</div>
<el-rate v-model="averageRating" disabled />
<div class="text-sm text-gray-500 mt-1">综合评分</div>
</div>
<div class="flex-1">
<div class="rating-bars space-y-2">
<div v-for="i in 5" :key="i" class="flex items-center space-x-2">
<span class="text-sm w-8">{{ 6 - i }}星</span>
<div class="flex-1 bg-gray-200 rounded-full h-2">
<div
class="bg-red-500 h-2 rounded-full"
:style="{ width: `${getRatingPercentage(6 - i)}%` }"
></div>
</div>
<span class="text-sm text-gray-500 w-12">{{ getRatingCount(6 - i) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 评价列表 -->
<div class="review-list space-y-6">
<div
v-for="review in reviews"
:key="review.id"
class="review-item pb-6 border-b border-gray-100 last:border-b-0"
>
<div class="flex items-start space-x-4">
<el-avatar :src="review.avatar" :size="40" />
<div class="flex-1">
<div class="flex items-center space-x-4 mb-2">
<span class="font-medium">{{ review.username }}</span>
<el-rate v-model="review.rating" disabled size="small" />
<span class="text-sm text-gray-500">{{ review.date }}</span>
</div>
<p class="text-gray-700 mb-3">{{ review.content }}</p>
<div v-if="review.images" class="review-images flex space-x-2">
<img
v-for="(img, index) in review.images"
:key="index"
:src="img"
:alt="`评价图${index + 1}`"
class="w-20 h-20 object-cover rounded cursor-pointer"
@click="previewImage(img)"
>
</div>
<div class="review-specs mt-2 text-sm text-gray-500">
规格:{{ review.specs }}
</div>
</div>
</div>
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
</div>
<div v-else class="text-center py-12">
<div class="text-6xl mb-4">😕</div>
<h2 class="text-xl font-semibold text-gray-800 mb-2">商品不存在</h2>
<p class="text-gray-600 mb-4">抱歉,您访问的商品可能已下架或不存在</p>
<el-button type="primary" @click="$router.push('/')">返回首页</el-button>
</div>
</main>
<!-- 放大镜显示区域 -->
<div
v-show="showMagnifier"
class="magnifier-viewer fixed border border-gray-300 rounded-lg overflow-hidden bg-white shadow-xl z-50"
:style="{
left: magnifierLeft + 'px',
top: magnifierTop + 'px',
width: '300px',
height: '300px',
backgroundImage: 'url(' + currentImage + ')',
backgroundSize: zoomLevel + '%',
backgroundPosition: backgroundPosition
}"
></div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, inject, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
Loading,
Star,
StarFilled,
Share,
Check,
Clock,
RefreshRight,
CircleCheckFilled,
Service
} from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const route = useRoute()
const router = useRouter()
const globalState = inject('globalState')
const loading = ref(true)
const product = ref(null)
const currentImage = ref('')
const quantity = ref(1)
const activeTab = ref('detail')
const showMagnifier = ref(false)
const mainImageRef = ref(null)
// 放大镜相关
const maskStyle = ref({})
const backgroundPosition = ref('0% 0%')
const zoomLevel = ref(250)
const magnifierLeft = ref(0)
const magnifierTop = ref(0)
const selectedSpecs = reactive({
color: '',
size: ''
})
// 商品规格数据
const colors = [
{ name: '经典黑', image: 'https://cdn.seovx.com/ha/?mom=302&w=100&h=100&_=black' },
{ name: '优雅白', image: 'https://cdn.seovx.com/ha/?mom=302&w=100&h=100&_=white' },
{ name: '时尚红', image: 'https://cdn.seovx.com/ha/?mom=302&w=100&h=100&_=red' },
{ name: '深海蓝', image: 'https://cdn.seovx.com/ha/?mom=302&w=100&h=100&_=blue' }
]
const sizes = [
{ name: 'S', available: true },
{ name: 'M', available: true },
{ name: 'L', available: true },
{ name: 'XL', available: true },
{ name: 'XXL', available: false }
]
const stock = ref(99)
const averageRating = ref(4.5)
const productSpecs = [
{ name: '品牌', value: '时尚品牌' },
{ name: '材质', value: '优质棉质' },
{ name: '产地', value: '中国' },
{ name: '适用季节', value: '四季' },
{ name: '洗涤方式', value: '机洗' },
{ name: '厚度', value: '适中' }
]
const reviews = [
{
id: 1,
username: '用户***1',
avatar: 'https://cdn.seovx.com/ha/?mom=302&w=50&h=50&_=1',
rating: 5,
date: '2023-05-15',
content: '这款产品质量非常好,穿着很舒适,非常满意这次购物体验。',
images: [
'https://cdn.seovx.com/ha/?mom=302&w=200&h=200&_=review1',
'https://cdn.seovx.com/ha/?mom=302&w=200&h=200&_=review2'
],
specs: '经典黑/M'
},
{
id: 2,
username: '用户***2',
avatar: 'https://cdn.seovx.com/ha/?mom=302&w=50&h=50&_=2',
rating: 4,
date: '2023-05-10',
content: '款式很喜欢,尺码也合适,就是物流稍微慢了点。',
specs: '时尚红/L'
},
{
id: 3,
username: '用户***3',
avatar: 'https://cdn.seovx.com/ha/?mom=302&w=50&h=50&_=3',
rating: 5,
date: '2023-05-05',
content: '已经是第二次购买了,质量一如既往的好,客服态度也很棒!',
images: [
'https://cdn.seovx.com/ha/?mom=302&w=200&h=200&_=review3'
],
specs: '深海蓝/XL'
}
]
const allImages = computed(() => {
if (!product.value) return []
return [product.value.cover, ...(product.value.images || [])]
})
const detailImages = computed(() => {
const baseImages = []
for (let i = 1; i <= 6; i++) {
baseImages.push(`https://cdn.seovx.com/ha/?mom=302&w=800&h=600&_=detail${i}`)
}
return baseImages
})
const currentPrice = computed(() => {
if (!product.value) return '0.00'
const basePrice = parseFloat(product.value.price)
const colorPrice = selectedSpecs.color ? 5 : 0
const sizePrice = selectedSpecs.size ? 10 : 0
return (basePrice + colorPrice + sizePrice).toFixed(2)
})
const isSpecsSelected = computed(() => {
return selectedSpecs.color && selectedSpecs.size
})
const isFavorited = computed(() => {
if (!globalState.currentUser || !product.value) return false
return globalState.favorites.some(fav => fav.id === product.value.id)
})
// 处理窗口大小变化和滚动事件
const updateMagnifierPosition = () => {
if (!mainImageRef.value) return
const rect = mainImageRef.value.getBoundingClientRect()
const containerPadding = 20 // 大图容器的内边距
const gap = 20 // 放大镜与大图之间的间距
// 计算放大镜的左侧位置:大图右侧 + 间距
magnifierLeft.value = rect.right + window.scrollX + gap
// 计算放大镜的顶部位置:与大图顶部对齐
magnifierTop.value = rect.top + window.scrollY
}
// 放大镜功能
const handleMouseMove = (event) => {
if (!showMagnifier.value || !mainImageRef.value) return
const rect = mainImageRef.value.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top
const maskSize = 120
// 计算遮罩位置
const maskX = Math.max(0, Math.min(x - maskSize / 2, rect.width - maskSize))
const maskY = Math.max(0, Math.min(y - maskSize / 2, rect.height - maskSize))
maskStyle.value = {
left: `${maskX}px`,
top: `${maskY}px`,
width: `${maskSize}px`,
height: `${maskSize}px`
}
// 计算背景图片位置
const xPercent = (x / rect.width) * 100
const yPercent = (y / rect.height) * 100
backgroundPosition.value = `${xPercent}% ${yPercent}%`
}
const getRatingPercentage = (rating) => {
const count = reviews.filter(r => r.rating === rating).length
return (count / reviews.length) * 100
}
const getRatingCount = (rating) => {
return reviews.filter(r => r.rating === rating).length
}
const toggleFavorite = () => {
if (!globalState.currentUser) {
ElMessage.warning('请先登录')
return
}
const index = globalState.favorites.findIndex(fav => fav.id === product.value.id)
if (index > -1) {
globalState.favorites.splice(index, 1)
ElMessage.success('已取消收藏')
} else {
globalState.favorites.push({
id: product.value.id,
title: product.value.title,
price: product.value.price,
cover: product.value.cover,
addTime: new Date().toISOString()
})
ElMessage.success('已加入收藏')
}
localStorage.setItem('favorites', JSON.stringify(globalState.favorites))
}
const handleAddToCart = () => {
if (!isSpecsSelected.value) {
ElMessage.warning('请选择商品规格')
return
}
const cartItem = {
productId: product.value.id,
title: product.value.title,
price: parseFloat(currentPrice.value),
cover: product.value.cover,
color: selectedSpecs.color,
size: selectedSpecs.size,
quantity: quantity.value
}
const existingItem = globalState.cartItems.find(item =>
item.productId === cartItem.productId &&
item.color === cartItem.color &&
item.size === cartItem.size
)
if (existingItem) {
existingItem.quantity += cartItem.quantity
} else {
globalState.cartItems.push(cartItem)
}
localStorage.setItem('cart', JSON.stringify(globalState.cartItems))
ElMessage.success('已加入购物车')
}
const handleBuyNow = () => {
if (!isSpecsSelected.value) {
ElMessage.warning('请选择商品规格')
return
}
const totalAmount = (parseFloat(currentPrice.value) * quantity.value).toFixed(2)
globalState.showPaymentModal(totalAmount)
}
const previewImage = (imageUrl) => {
// 图片预览功能实现
console.log('预览图片:', imageUrl)
// 可以使用 Element Plus 的 Dialog 组件实现图片预览
}
const loadProduct = async () => {
loading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 500))
const productId = route.params.id
// 生成模拟商品数据
product.value = {
id: productId,
title: `高品质时尚单品 ${productId} - 精选优质材料制作,舒适透气,四季适穿`,
price: (Math.random() * 200 + 50).toFixed(2),
cover: `https://cdn.seovx.com/ha/?mom=302&w=600&h=600&_=${productId}`,
images: [
`https://cdn.seovx.com/ha/?mom=302&w=600&h=600&_=${productId}1`,
`https://cdn.seovx.com/ha/?mom=302&w=600&h=600&_=${productId}2`,
`https://cdn.seovx.com/ha/?mom=302&w=600&h=600&_=${productId}3`,
`https://cdn.seovx.com/ha/?mom=302&w=600&h=600&_=${productId}4`
],
desc: '这是一款优质的时尚单品,采用精选材料制作,工艺精良,品质保证。',
sold: Math.floor(Math.random() * 9999) + 100,
brand: '时尚品牌',
rating: Math.random() * 1 + 4,
reviewCount: Math.floor(Math.random() * 500) + 50
}
currentImage.value = product.value.cover
// 添加到浏览历史
if (globalState.currentUser) {
const historyItem = {
id: product.value.id,
title: product.value.title,
price: product.value.price,
cover: product.value.cover,
viewTime: new Date().toISOString()
}
let history = JSON.parse(localStorage.getItem('browsingHistory') || '[]')
history = history.filter(item => item.id !== product.value.id)
history.unshift(historyItem)
history = history.slice(0, 20)
localStorage.setItem('browsingHistory', JSON.stringify(history))
}
} catch (error) {
console.error('加载商品失败:', error)
product.value = null
} finally {
loading.value = false
}
}
onMounted(() => {
loadProduct()
// 初始化放大镜位置
updateMagnifierPosition()
// 监听窗口大小变化和滚动事件
window.addEventListener('resize', updateMagnifierPosition)
window.addEventListener('scroll', updateMagnifierPosition)
})
onUnmounted(() => {
// 移除事件监听器,防止内存泄漏
window.removeEventListener('resize', updateMagnifierPosition)
window.removeEventListener('scroll', updateMagnifierPosition)
})
</script>
<style lang="scss" scoped>
.main-image-wrapper {
position: relative;
}
.magnifier-mask {
width: 100px;
height: 100px;
}
.magnifier-viewer {
z-index: 1000;
}
.magnifier-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.thumbnail-item:hover {
transform: scale(1.05);
}
.spec-option.selected {
transform: scale(1.05);
}
.color-option {
transition: all 0.2s;
}
.color-option:hover {
transform: translateY(-2px);
}
.size-option:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.quantity-input :deep(.el-input-number) {
width: 120px;
}
.product-detail-tabs :deep(.el-tabs__header) {
margin: 0;
padding: 0 24px;
background: #f8f9fa;
}
.product-detail-tabs :deep(.el-tabs__nav-wrap) {
padding: 16px 0;
}
.review-item:hover {
background: #fafafa;
border-radius: 8px;
padding: 16px;
margin: -16px;
}
.rating-bars {
min-width: 200px;
}
@media (max-width: 1024px) {
.magnifier-viewer {
display: none;
}
}
</style>

View File

@@ -1,761 +0,0 @@
<template>
<div class="product-detail-page bg-gray-50 min-h-screen">
<main class="container mx-auto px-4 py-6">
<div v-if="loading" class="flex justify-center items-center h-96">
<el-icon class="is-loading text-4xl text-gray-400">
<Loading/>
</el-icon>
</div>
<div v-else-if="product" class="max-w-7xl mx-auto">
<!-- 面包屑导航 -->
<el-breadcrumb separator="/" class="mb-6">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: '/products' }">商品列表</el-breadcrumb-item>
<el-breadcrumb-item>{{ product.title }}</el-breadcrumb-item>
</el-breadcrumb>
<div class="grid grid-cols-12 gap-8">
<!-- 商品图片区域 -->
<div class="col-span-12 lg:col-span-5">
<div class="bg-white rounded-lg p-6 sticky top-40">
<!-- 主图展示 -->
<div class="main-image-container relative mb-4">
<div
class="main-image-wrapper relative overflow-hidden rounded-lg"
@mousemove="handleMouseMove"
@mouseleave="showMagnifier = false"
@mouseenter="showMagnifier = true"
>
<img
ref="mainImageRef"
:src="currentImage"
:alt="product.title"
class="w-full h-96 object-cover cursor-crosshair"
>
<!-- 放大镜遮罩 -->
<div
v-show="showMagnifier"
class="magnifier-mask absolute border-2 border-red-500 bg-opacity-20 pointer-events-none"
:style="maskStyle"
></div>
</div>
<!-- 放大镜显示区域 -->
<div
v-show="showMagnifier"
class="magnifier-viewer absolute top-0 left-full ml-4 w-96 h-96 border border-gray-300 rounded-lg overflow-hidden bg-white shadow-lg"
>
<img
:src="currentImage"
class="magnifier-image"
:style="magnifierStyle"
style="z-index: 999999;"
>
</div>
</div>
<!-- 缩略图列表 -->
<div class="thumbnail-list">
<div class="flex space-x-2 overflow-x-auto pb-2">
<img
v-for="(image, index) in allImages"
:key="index"
:src="image"
:alt="`商品图片${index + 1}`"
class="thumbnail-item w-16 h-16 object-cover rounded cursor-pointer border-2 transition-all"
:class="{
'border-red-500 ring-2 ring-red-200': currentImage === image,
'border-gray-200 hover:border-red-300': currentImage !== image
}"
@click="currentImage = image"
>
</div>
</div>
<!-- 商品操作按钮 -->
<div class="product-actions mt-6 space-y-3">
<div class="flex space-x-3">
<el-button
type="danger"
size="large"
class="flex-1"
:disabled="!isSpecsSelected"
@click="handleBuyNow"
>
立即购买
</el-button>
<el-button
size="large"
class="flex-1"
:disabled="!isSpecsSelected"
@click="handleAddToCart"
>
加入购物车
</el-button>
</div>
<div class="flex space-x-3">
<el-button
class="flex-1"
@click="toggleFavorite"
:type="isFavorited ? 'danger' : 'default'"
>
<el-icon class="mr-1">
<Star v-if="isFavorited"/>
<StarFilled v-else/>
</el-icon>
{{ isFavorited ? '已收藏' : '收藏商品' }}
</el-button>
<el-button class="flex-1">
<el-icon class="mr-1">
<Share/>
</el-icon>
分享
</el-button>
</div>
</div>
</div>
</div>
<!-- 商品信息区域 -->
<div class="col-span-12 lg:col-span-7">
<div class="bg-white rounded-lg p-6 mb-6">
<!-- 商品标题和价格 -->
<div class="product-header mb-6">
<h1 class="text-2xl font-bold text-gray-800 mb-4 leading-relaxed">{{ product.title }}</h1>
<div class="price-section mb-4">
<div class="flex items-baseline space-x-4 mb-2">
<span class="text-4xl font-bold text-red-600">¥{{ product.price }}</span>
<span class="text-lg text-gray-400 line-through">¥{{
(parseFloat(product.price) * 1.3).toFixed(2)
}}</span>
</div>
<div class="flex items-center space-x-6 text-sm text-gray-600">
<span>已售 {{ product.sold }} 件</span>
<div class="flex items-center">
<el-rate v-model="product.rating" disabled size="small"/>
<span class="ml-1">({{ product.reviewCount || 0 }}条评价)</span>
</div>
</div>
</div>
<!-- 促销信息 -->
<div class="promotion-tags flex flex-wrap gap-2 mb-4">
<el-tag type="danger" size="small">限时特价</el-tag>
<el-tag type="warning" size="small">满200减30</el-tag>
<el-tag type="success" size="small">包邮</el-tag>
<el-tag type="info" size="small">7天无理由退货</el-tag>
</div>
</div>
<!-- 商品规格选择 -->
<div class="product-specs mb-8">
<!-- 颜色选择 -->
<div class="spec-group mb-6">
<div class="spec-label mb-3">
<span class="text-sm font-medium text-gray-700">颜色分类</span>
<span v-if="selectedSpecs.color" class="ml-2 text-sm text-gray-500">
已选:{{ selectedSpecs.color }}
</span>
</div>
<div class="spec-options flex flex-wrap gap-3">
<div
v-for="color in colors"
:key="color.name"
class="spec-option color-option relative cursor-pointer"
:class="{ 'selected': selectedSpecs.color === color.name }"
@click="selectedSpecs.color = color.name"
>
<img
:src="color.image"
:alt="color.name"
class="w-16 h-16 object-cover rounded border-2"
:class="{
'border-red-500': selectedSpecs.color === color.name,
'border-gray-200': selectedSpecs.color !== color.name
}"
>
<span class="block text-xs text-center mt-1">{{ color.name }}</span>
<div
v-if="selectedSpecs.color === color.name"
class="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full flex items-center justify-center"
>
<el-icon class="text-white text-xs">
<Check/>
</el-icon>
</div>
</div>
</div>
</div>
<!-- 尺寸选择 -->
<div class="spec-group mb-6">
<div class="spec-label mb-3">
<span class="text-sm font-medium text-gray-700">尺码</span>
<span v-if="selectedSpecs.size" class="ml-2 text-sm text-gray-500">
已选:{{ selectedSpecs.size }}
</span>
<el-button text type="primary" size="small" class="ml-2">尺码表</el-button>
</div>
<div class="spec-options flex flex-wrap gap-3">
<button
v-for="size in sizes"
:key="size.name"
class="size-option px-4 py-2 border rounded text-sm font-medium transition-all"
:class="{
'border-red-500 bg-red-50 text-red-600': selectedSpecs.size === size.name,
'border-gray-300 hover:border-red-300': selectedSpecs.size !== size.name,
'opacity-50 cursor-not-allowed': !size.available
}"
:disabled="!size.available"
@click="selectedSpecs.size = size.name"
>
{{ size.name }}
<span v-if="!size.available" class="block text-xs text-gray-400">缺货</span>
</button>
</div>
</div>
<!-- 数量选择 -->
<div class="spec-group mb-6">
<div class="spec-label mb-3">
<span class="text-sm font-medium text-gray-700">数量</span>
<span class="ml-2 text-sm text-gray-500">库存{{ stock }}件</span>
</div>
<div class="quantity-selector flex items-center space-x-4">
<el-input-number
v-model="quantity"
:min="1"
:max="Math.min(stock, 99)"
size="large"
class="quantity-input"
/>
<span class="text-sm text-gray-500">件</span>
</div>
</div>
</div>
<!-- 服务保障 -->
<div class="service-guarantee mb-6">
<h3 class="text-sm font-medium text-gray-700 mb-3">服务保障</h3>
<div class="grid grid-cols-2 gap-4">
<div class="service-item flex items-center space-x-2">
<el-icon class="text-green-500">
<CircleCheckFilled/>
</el-icon>
<span class="text-sm">正品保证</span>
</div>
<div class="service-item flex items-center space-x-2">
<el-icon class="text-blue-500">
<Clock/>
</el-icon>
<span class="text-sm">快速发货</span>
</div>
<div class="service-item flex items-center space-x-2">
<el-icon class="text-orange-500">
<RefreshRight/>
</el-icon>
<span class="text-sm">7天无理由退货</span>
</div>
<div class="service-item flex items-center space-x-2">
<el-icon class="text-purple-500">
<Service/>
</el-icon>
<span class="text-sm">24小时客服</span>
</div>
</div>
</div>
</div>
<!-- 商品详情标签页 -->
<div class="bg-white rounded-lg">
<el-tabs
v-model="activeTab"
class="product-detail-tabs"
>
<el-tab-pane label="商品详情" name="detail"
:class="[
{ 'text-red-600 border-b-2 border-red-600': activeTab === 'detail' },
{ 'text-gray-600': activeTab !== 'detail' }
]"
>
<div class="detail-content p-6">
<div class="detail-images space-y-4">
<img
v-for="(image, index) in detailImages"
:key="index"
:src="image"
:alt="`详情图${index + 1}`"
class="w-full rounded-lg"
>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="规格参数" name="specs">
<div class="specs-content p-6">
<table class="w-full">
<tbody>
<tr v-for="spec in productSpecs" :key="spec.name" class="border-b">
<td class="py-3 pr-8 text-gray-600 font-medium">{{ spec.name }}</td>
<td class="py-3 text-gray-800">{{ spec.value }}</td>
</tr>
</tbody>
</table>
</div>
</el-tab-pane>
<el-tab-pane :label="`用户评价(${reviews.length})`" name="reviews">
<div class="reviews-content p-6">
<!-- 评价统计 -->
<div class="review-summary mb-6 p-4 bg-gray-50 rounded-lg">
<div class="flex items-center space-x-8">
<div class="text-center">
<div class="text-3xl font-bold text-red-600 mb-1">{{ averageRating.toFixed(1) }}</div>
<el-rate v-model="averageRating" disabled/>
<div class="text-sm text-gray-500 mt-1">综合评分</div>
</div>
<div class="flex-1">
<div class="rating-bars space-y-2">
<div v-for="i in 5" :key="i" class="flex items-center space-x-2">
<span class="text-sm w-8">{{ 6 - i }}星</span>
<div class="flex-1 bg-gray-200 rounded-full h-2">
<div
class="bg-red-500 h-2 rounded-full"
:style="{ width: `${getRatingPercentage(6 - i)}%` }"
></div>
</div>
<span class="text-sm text-gray-500 w-12">{{ getRatingCount(6 - i) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 评价列表 -->
<div class="review-list space-y-6">
<div
v-for="review in reviews"
:key="review.id"
class="review-item pb-6 border-b border-gray-100 last:border-b-0"
>
<div class="flex items-start space-x-4">
<el-avatar :src="review.avatar" :size="40"/>
<div class="flex-1">
<div class="flex items-center space-x-4 mb-2">
<span class="font-medium">{{ review.username }}</span>
<el-rate v-model="review.rating" disabled size="small"/>
<span class="text-sm text-gray-500">{{ review.date }}</span>
</div>
<p class="text-gray-700 mb-3">{{ review.content }}</p>
<div v-if="review.images" class="review-images flex space-x-2">
<img
v-for="(img, index) in review.images"
:key="index"
:src="img"
:alt="`评价图${index + 1}`"
class="w-20 h-20 object-cover rounded cursor-pointer"
@click="previewImage(img)"
>
</div>
<div class="review-specs mt-2 text-sm text-gray-500">
规格:{{ review.specs }}
</div>
</div>
</div>
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
</div>
<div v-else class="text-center py-12">
<div class="text-6xl mb-4">😕</div>
<h2 class="text-xl font-semibold text-gray-800 mb-2">商品不存在</h2>
<p class="text-gray-600 mb-4">抱歉,您访问的商品可能已下架或不存在</p>
<el-button type="primary" @click="$router.push('/')">返回首页</el-button>
</div>
</main>
</div>
</template>
<script setup>
import {ref, reactive, computed, onMounted, inject} from 'vue'
import {useRoute, useRouter} from 'vue-router'
import {
Loading,
Star,
StarFilled,
Share,
Check,
Clock,
RefreshRight,
CircleCheckFilled,
Service
} from '@element-plus/icons-vue'
import {ElMessage} from 'element-plus'
const route = useRoute()
const router = useRouter()
const globalState = inject('globalState')
const loading = ref(true)
const product = ref(null)
const currentImage = ref('')
const quantity = ref(1)
const activeTab = ref('detail')
const showMagnifier = ref(false)
const mainImageRef = ref(null)
// 放大镜相关
const maskStyle = ref({})
const magnifierStyle = ref({})
const selectedSpecs = reactive({
color: '',
size: ''
})
// 商品规格数据
const colors = [
{name: '经典黑', image: 'https://cdn.seovx.com/ha/?mom=302&w=100&h=100&_=black'},
{name: '优雅白', image: 'https://cdn.seovx.com/ha/?mom=302&w=100&h=100&_=white'},
{name: '时尚红', image: 'https://cdn.seovx.com/ha/?mom=302&w=100&h=100&_=red'},
{name: '深海蓝', image: 'https://cdn.seovx.com/ha/?mom=302&w=100&h=100&_=blue'}
]
const sizes = [
{name: 'S', available: true},
{name: 'M', available: true},
{name: 'L', available: true},
{name: 'XL', available: true},
{name: 'XXL', available: false}
]
const stock = ref(99)
const averageRating = ref(4.5)
const productSpecs = [
{name: '品牌', value: '时尚品牌'},
{name: '材质', value: '优质棉质'},
{name: '产地', value: '中国'},
{name: '适用季节', value: '四季'},
{name: '洗涤方式', value: '机洗'},
{name: '厚度', value: '适中'}
]
const reviews = [
{
id: 1,
username: '用户***1',
avatar: 'https://cdn.seovx.com/ha/?mom=302&w=50&h=50&_=1',
rating: 5,
content: '质量很好,物流很快,非常满意!面料舒适,做工精细,值得推荐。',
date: '2023-12-01',
images: [
'https://cdn.seovx.com/ha/?mom=302&w=200&h=200&_=r1',
'https://cdn.seovx.com/ha/?mom=302&w=200&h=200&_=r2'
],
specs: '颜色:经典黑 尺码:M'
},
{
id: 2,
username: '用户***2',
avatar: 'https://cdn.seovx.com/ha/?mom=302&w=50&h=50&_=2',
rating: 4,
content: '款式不错,尺码合适,推荐购买。发货速度很快,包装也很好。',
date: '2023-11-28',
specs: '颜色:优雅白 尺码:L'
},
{
id: 3,
username: '用户***3',
avatar: 'https://cdn.seovx.com/ha/?mom=302&w=50&h=50&_=3',
rating: 5,
content: '超级喜欢这个颜色,质感很好,穿着很舒服,会回购的!',
date: '2023-11-25',
images: [
'https://cdn.seovx.com/ha/?mom=302&w=200&h=200&_=r3'
],
specs: '颜色:时尚红 尺码:S'
}
]
const allImages = computed(() => {
if (!product.value) return []
return [product.value.cover, ...(product.value.images || [])]
})
const detailImages = computed(() => {
const baseImages = []
for (let i = 1; i <= 6; i++) {
baseImages.push(`https://cdn.seovx.com/ha/?mom=302&w=800&h=600&_=detail${i}`)
}
return baseImages
})
const isSpecsSelected = computed(() => {
return selectedSpecs.color && selectedSpecs.size
})
const isFavorited = computed(() => {
if (!globalState.currentUser || !product.value) return false
return globalState.favorites.some(fav => fav.id === product.value.id)
})
// 放大镜功能
const handleMouseMove = (event) => {
if (!showMagnifier.value || !mainImageRef.value) return
const rect = mainImageRef.value.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top
const maskSize = 100
const scale = 2
// 计算遮罩位置
const maskX = Math.max(0, Math.min(x - maskSize / 2, rect.width - maskSize))
const maskY = Math.max(0, Math.min(y - maskSize / 2, rect.height - maskSize))
maskStyle.value = {
left: `${maskX}px`,
top: `${maskY}px`,
width: `${maskSize}px`,
height: `${maskSize}px`
}
// 计算放大图片位置
const bgX = -(maskX * scale)
const bgY = -(maskY * scale)
magnifierStyle.value = {
transform: `translate(${bgX}px, ${bgY}px) scale(${scale})`,
transformOrigin: 'top left'
}
}
const getRatingPercentage = (rating) => {
const count = reviews.filter(r => r.rating === rating).length
return (count / reviews.length) * 100
}
const getRatingCount = (rating) => {
return reviews.filter(r => r.rating === rating).length
}
const toggleFavorite = () => {
if (!globalState.currentUser) {
ElMessage.warning('请先登录')
return
}
const index = globalState.favorites.findIndex(fav => fav.id === product.value.id)
if (index > -1) {
globalState.favorites.splice(index, 1)
ElMessage.success('已取消收藏')
} else {
globalState.favorites.push({
id: product.value.id,
title: product.value.title,
price: product.value.price,
cover: product.value.cover,
addTime: new Date().toISOString()
})
ElMessage.success('已加入收藏')
}
localStorage.setItem('favorites', JSON.stringify(globalState.favorites))
}
const handleAddToCart = () => {
if (!isSpecsSelected.value) {
ElMessage.warning('请选择商品规格')
return
}
const cartItem = {
productId: product.value.id,
title: product.value.title,
price: parseFloat(product.value.price),
cover: product.value.cover,
color: selectedSpecs.color,
size: selectedSpecs.size,
quantity: quantity.value
}
const existingItem = globalState.cartItems.find(item =>
item.productId === cartItem.productId &&
item.color === cartItem.color &&
item.size === cartItem.size
)
if (existingItem) {
existingItem.quantity += cartItem.quantity
} else {
globalState.cartItems.push(cartItem)
}
localStorage.setItem('cart', JSON.stringify(globalState.cartItems))
ElMessage.success('已加入购物车')
}
const handleBuyNow = () => {
if (!isSpecsSelected.value) {
ElMessage.warning('请选择商品规格')
return
}
const totalAmount = (parseFloat(product.value.price) * quantity.value).toFixed(2)
globalState.showPaymentModal(totalAmount)
}
const previewImage = (image) => {
// 图片预览功能
ElMessage.info('图片预览功能开发中...')
}
const loadProduct = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500))
const productId = route.params.id
// 生成模拟商品数据
product.value = {
id: productId,
title: `高品质时尚单品 ${productId} - 精选优质材料制作,舒适透气,四季适穿`,
price: (Math.random() * 200 + 50).toFixed(2),
cover: `https://cdn.seovx.com/ha/?mom=302&w=600&h=600&_=${productId}`,
images: [
`https://cdn.seovx.com/ha/?mom=302&w=600&h=600&_=${productId}1`,
`https://cdn.seovx.com/ha/?mom=302&w=600&h=600&_=${productId}2`,
`https://cdn.seovx.com/ha/?mom=302&w=600&h=600&_=${productId}3`,
`https://cdn.seovx.com/ha/?mom=302&w=600&h=600&_=${productId}4`
],
desc: '这是一款优质的时尚单品,采用精选材料制作,工艺精良,品质保证。',
sold: Math.floor(Math.random() * 9999) + 100,
brand: '时尚品牌',
rating: Math.random() * 1 + 4,
reviewCount: Math.floor(Math.random() * 500) + 50
}
currentImage.value = product.value.cover
// 添加到浏览历史
if (globalState.currentUser) {
const historyItem = {
id: product.value.id,
title: product.value.title,
price: product.value.price,
cover: product.value.cover,
viewTime: new Date().toISOString()
}
let history = JSON.parse(localStorage.getItem('browsingHistory') || '[]')
history = history.filter(item => item.id !== product.value.id)
history.unshift(historyItem)
history = history.slice(0, 20) // 只保留最近20条
localStorage.setItem('browsingHistory', JSON.stringify(history))
}
} catch (error) {
console.error('加载商品失败:', error)
product.value = null
} finally {
loading.value = false
}
}
onMounted(() => {
loadProduct()
})
</script>
<style lang="scss" scoped>
.main-image-wrapper {
position: relative;
}
.magnifier-mask {
width: 100px;
height: 100px;
}
.magnifier-viewer {
z-index: 1000;
}
.magnifier-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.thumbnail-item:hover {
transform: scale(1.05);
}
.spec-option.selected {
transform: scale(1.05);
}
.color-option {
transition: all 0.2s;
}
.color-option:hover {
transform: translateY(-2px);
}
.size-option:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.quantity-input :deep(.el-input-number) {
width: 120px;
}
.product-detail-tabs :deep(.el-tabs__header) {
margin: 0;
padding: 0 24px;
background: #f8f9fa;
}
.product-detail-tabs :deep(.el-tabs__nav-wrap) {
padding: 16px 0;
}
.review-item:hover {
background: #fafafa;
border-radius: 8px;
padding: 16px;
margin: -16px;
}
.rating-bars {
min-width: 200px;
}
@media (max-width: 1024px) {
.magnifier-viewer {
display: none;
}
}
</style>