fix: UI部分完成
@@ -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端
|
||||
|
||||
BIN
public/demo-img/banner/gy1.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
public/demo-img/banner/gy2.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
public/demo-img/banner/gy3.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
public/demo-img/banner/gy4.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
public/demo-img/banner/hf1.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
public/demo-img/banner/mm1.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
public/demo-img/banner/vip.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
public/demo-img/banner/yy1.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
<!-- <!– 侧边分类导航 –>-->
|
||||
<!-- <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>-->
|
||||
|
||||
<!-- <!– 主内容区 –>-->
|
||||
<!-- <div class="col-span-10">-->
|
||||
<!-- <!– 限时抢购 –>-->
|
||||
<!-- <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([])
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||