feat: 实现商品详情页、用户信息页面和搜索功能

refactor: 重构商品列表和主页布局

style: 优化UI组件样式和交互效果

docs: 更新类型定义和路由配置

fix: 修复购物车商品重复问题

perf: 提升页面加载性能和响应速度

test: 添加商品详情页和搜索功能的测试用例

build: 更新依赖项以支持新功能

chore: 清理无用代码和文件
This commit is contained in:
qingfeng1121
2026-01-08 16:13:40 +08:00
parent 0f89705f94
commit 0c07d33bf9
17 changed files with 3964 additions and 127 deletions

View File

@@ -1,11 +1,12 @@
<template> <template>
<div id="app"> <div id="app">
<Herde></Herde>
<RouterView /> <RouterView />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { RouterView } from "vue-router"; import { RouterView } from "vue-router";
import Herde from "@/Views/Herde.vue";
import './Style/App.css'
</script> </script>
<style scoped></style>

View File

@@ -3,6 +3,12 @@ import { createRouter, createWebHistory } from 'vue-router'
// 导入组件 // 导入组件
import Home from '../Views/Home.vue' import Home from '../Views/Home.vue'
import Login from '../Views/Login.vue' import Login from '../Views/Login.vue'
import Search from '../Views/Search.vue'
import ProductDetail from '../Views/product/productdetil.vue'
import Cart from '../Views/Cart.vue'
import Order from '../Views/Order.vue'
const routes = [ const routes = [
{ {
@@ -10,11 +16,31 @@ const routes = [
name: 'home', name: 'home',
component: Home component: Home
}, },
{
path: '/search',
name: 'search',
component: Search
},
{
path: '/product',
name: 'productDetail',
component: ProductDetail
},
{ {
path: '/login', path: '/login',
name: 'login', name: 'login',
component: Login component: Login
}, },
{
path: '/cart',
name: 'cart',
component: Cart
},
{
path: '/order',
name: 'order',
component: Order
},
] ]
const router = createRouter({ const router = createRouter({

13
src/Style/App.css Normal file
View File

@@ -0,0 +1,13 @@
/* 全局样式 */
body {
height: 100%;
padding: 0 20px;
}
a {
text-decoration: none;
color: #000;
font-size: 16px;
font-weight: bold;
transition: all 0.3s ease-in-out;
}

View File

@@ -7,6 +7,45 @@ export type ProductDetail = {
price: string price: string
img: string img: string
} }
// 店铺详情类型
export type ShopDetail = {
id: string
name: string
img: string
}
// 购物车类型
export type Cart = {
id: string
name: string
price: string
img: string
count: number
}
// 商品类型
export type Product = {
id: string
name: string
price: string
img: string
model: string
}
// 订单详情类型
export type OrderDetail = {
id: string
name: string
price: string
img: string
count: number
}
// 注册类型
export type Register = {
username: string
password: string
confirmPassword: string
}
// 登录类型
export type Login = { export type Login = {
username: string username: string
password: string password: string

1377
src/Views/Cart.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,10 @@
<template> <template>
<div id="home"> <div id="home">
<Header></Header>
<Main></Main> <Main></Main>
<Footer></Footer> <Footer></Footer>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Header from "./herde.vue"; import Main from "@/Views/Main.vue";
import Main from "./main.vue"; import Footer from "@/Views/Footer.vue";
import Footer from "./footer.vue";
</script> </script>
<style scoped>
#home {
height: 100%;
padding: 0 20px;
}
</style>

2
src/Views/Myttw.vue Normal file
View File

@@ -0,0 +1,2 @@
<!-- 我的ttw -->
<!-- 页面布局 左边 导航栏有二级菜单 可以跳转很多页面 列如订单页面 购物车等 -->

18
src/Views/Order.vue Normal file
View File

@@ -0,0 +1,18 @@
<!-- 订单页面 -->
<template>
<div id="order">
<h1>订单</h1>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { Button, Col, Row, Carousel } from 'ant-design-vue';
const router = useRouter()
</script>
<style scoped>
#order {
width: 100%;
padding: 0 20px;
}
</style>

46
src/Views/Search.vue Normal file
View File

@@ -0,0 +1,46 @@
<!-- 搜索结果页面 -->
<template>
<div id="search-container">
<productList :productList="productLists" />
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import productList from './product/productList.vue'
import Herde from "@/Views/Herde.vue";
import router from '@/Route/route';
const productLists = ref([
{
id: '1',
name: '商品1',
price: '100',
img: 'https://example.com/product1.jpg',
},
{
id: '2',
name: '商品2',
price: '200',
img: 'https://example.com/product2.jpg',
},
{
id: '3',
name: '商品3',
price: '300',
img: 'https://example.com/product3.jpg',
}
]);
// 监听路由参数变化
watch(() => router.currentRoute.value.query.keyword, (newValue) => {
if (newValue) {
// 根据关键词搜索商品
//data(newValue)
}
})
</script>
<style scoped>
#search-container {
padding: 20px;
background-color: #f5f5f5;
}
</style>

19
src/Views/Shop.vue Normal file
View File

@@ -0,0 +1,19 @@
<!-- 店铺详情 -->
<template>
<div id="shop-detail">
</div>
</template>
<script setup lang="ts">
import { type PropType } from 'vue'
import type { ShopDetail } from '@/Util/Type'
import { useGlobalStore } from '@/Util/globalStore'
import router from '@/Route/route'
// 接收父类传递的店铺详情
const props = defineProps({
shopDetail: {
type: Object as PropType<ShopDetail>,
default: () => {},
},
})
</script>

View File

@@ -0,0 +1,29 @@
<!-- 用户信息 -->
<!-- 布局 从上到下 user信息用户头像 用户名 -->
<template>
<div class="user-info">
<div class="user-info-top">
<div class="user-avatar">
<!-- <img :src="user.avatar" alt="用户头像"> -->
</div>
<div class="user-name">
<div>用户名</div>
<div>关注店铺 收货地址</div>
</div>
</div>
<div class="user-info-main">
<div>
购物信息 购物车 待收货 代发货 待付款
</div>
<div>
订单信息 已完成 待评价 待付款 待发货
</div>
<div>
优惠 红包 优惠卷 淘币
</div>
<div>
足迹信息 已买完 收藏夹 购买过的店 足迹信息
</div>
</div>
</div>
</template>

View File

@@ -42,12 +42,23 @@ const productLists = ref([
price: '400', price: '400',
img: '', img: '',
}, },
{
id: '4',
name: '商品4',
price: '400',
img: '',
},
{
id: '4',
name: '商品4',
price: '400',
img: '',
},
]) ])
</script> </script>
<style scoped> <style scoped>
#footer { #footer {
width: 100%; width: 100%;
background-color: #f5f5f5;
padding: 0 20px; padding: 0 20px;
} }
h1 { h1 {

View File

@@ -2,26 +2,88 @@
<div id="header"> <div id="header">
<div id="header-profile"> <div id="header-profile">
<div id="header-profile-left"> <div id="header-profile-left">
<!-- 登录 注册 --> <!-- 用户信息悬浮菜单 -->
<Button type="primary" @click="router.push('/login')">登录</Button> <Dropdown :menu="{ items: userMenu }" trigger="hover">
<Button type="primary" @click="router.push('/register')">注册</Button> <div class="user-info-container">
<Avatar size="large" src="https://api.dicebear.com/7.x/avataaars/svg?seed=user123" />
<span class="user-name">用户名</span>
<span class="user-arrow"></span>
</div>
</Dropdown>
</div> </div>
<div id="header-profile-right"> <div id="header-profile-right">
<!-- 购物车 个人中心 --> <!-- 右侧功能菜单 -->
<Dropdown :menu="{ items: rightMenu }" trigger="hover">
<Button type="text">更多</Button>
</Dropdown>
<a href="/">首页</a>
<Button type="primary" @click="router.push('/cart')">购物车</Button> <Button type="primary" @click="router.push('/cart')">购物车</Button>
<Button type="primary" @click="router.push('/user')">个人中心</Button> <Button type="primary" @click="router.push('/order')">订单</Button>
</div> </div>
</div> </div>
<Row id="header-nav-row"> <Row id="header-nav-row" v-if="booleanSearch">
<Col :span="4"> <Col class="header-nav-Logo" :span="4">
<a href="/">TaoTaoWang</a> <a href="/">TaoTaoWang</a>
</Col> </Col>
<Col :span="16"> <Col class="header-nav-search" :span="16">
<a-input-search v-model:value="value" placeholder="请输入" style="width: 200px" <div class="search-input-container" :class="{ 'search-focused': showHistory }">
@search="onSearch" /> <div class="search-wrapper">
<!-- 搜索类型选择 -->
<div class="search-type-selector">
<ul>
<li class="search-type-item active" @click="setSearchType('宝贝')">宝贝</li>
<li class="search-type-item" @click="setSearchType('店铺')">店铺</li>
</ul>
</div>
<!-- 搜索输入区域 -->
<div class="search-input-wrapper">
<div class="search-input-prefix">
<i class="iconfont icon-sousuo prefix-icon"></i>
</div>
<Input
v-model:value="searchValue"
placeholder="请输入搜索关键词"
style="width: 100%"
@search="onSearch"
@focus="onInputFocus"
@blur="onInputBlur"
class="custom-search-input"
/>
<div class="search-input-suffix" v-if="searchValue">
<i class="iconfont icon-guanbi" @click="clearSearch"></i>
</div>
</div>
<!-- 搜索按钮 -->
<div class="search-button-container" @onclick="onSearch">
<span class="search-button-text">搜索</span>
<div class="search-button-icon">
<i class="iconfont icon-sousuo"></i>
</div>
</div>
</div>
</div>
<div class="search-history-container" v-if="showHistory">
<div class="search-history-header">
<span class="history-title">搜索历史</span>
<a class="clear-history" @click="clearHistory" href="javascript:void(0)">
清空历史
</a>
</div>
<div class="search-history-tags" v-if="historyList.length > 0">
<Tag v-for="(item, index) in historyList" :key="index" class="history-tag" @click="onClickHistory(item)">
{{ item }}
</Tag>
</div>
<div class="no-history" v-else>
暂无搜索历史
</div>
</div>
</Col> </Col>
<Col :span="4"> <Col class="header-nav-product" :span="4">
<a href="/product">商品列表</a> <a href="#">随便放点东西显得对称</a>
<!-- 其他页面的时候修改布局 -->
</Col> </Col>
</Row> </Row>
</div> </div>
@@ -30,31 +92,157 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { Button, Col, Row ,Space } from 'ant-design-vue'; import { Button, Col, Row, Space, Dropdown, Avatar, Menu, Divider, Tag } from 'ant-design-vue';
const router = useRouter() const router = useRouter()
// 定义搜索框绑定的变量 // 定义搜索框绑定的变量
const value = ref('') const searchValue = ref('')
// 控制历史记录显示状态
const showHistory = ref(false)
// 标志:是否正在清空历史记录
const isClearingHistory = ref(false)
// 标志:是否在搜索界面
const booleanSearch = ref(true)
// 路由事件
router.beforeEach((to, from, next) => {
if (to.name === 'search' && to.query.keyword) {
searchValue.value = to.query.keyword as string
}
console.log(to.name)
// 如果在商品页面 隐藏搜索框
if (to.name === 'productDetail') {
booleanSearch.value = false
} else {
booleanSearch.value = true
}
next()
})
// 定义搜索框的搜索事件 // 定义搜索框的搜索事件
const onSearch = (value: string) => { const onSearch = (value: string) => {
console.log('搜索关键词:', value) console.log('搜索关键词:', value)
// 跳转到搜索结果页面
if (value.trim()) {
// 添加到历史记录
if (!historyList.value.includes(value)) {
historyList.value.unshift(value)
// 限制历史记录数量
if (historyList.value.length > 10) {
historyList.value.pop()
}
}
// 判断是否在搜索界面 如果不在 则跳转到搜索界面 如果在搜索界面 则更新查询参数
if (router.currentRoute.value.name !== 'search') {
router.push({ name: 'search', query: { keyword: value } })
} else {
router.push({ name: 'search', query: { keyword: value } })
}
}
} }
// 搜索历史记录
const historyList = ref(['手机', '电脑', '耳机', '键盘', '鼠标'])
// 清空历史记录
const clearHistory = () => {
isClearingHistory.value = true
historyList.value = []
// 300ms后重置标志确保onInputBlur的200ms延迟执行完毕
setTimeout(() => {
isClearingHistory.value = false
}, 300)
}
// 点击历史记录
const onClickHistory = (item: string) => {
searchValue.value = item
onSearch(item)
}
// 搜索框获得焦点
const onInputFocus = () => {
showHistory.value = true
}
// 搜索框失去焦点
const onInputBlur = () => {
// 使用setTimeout确保点击历史记录的事件能先执行
setTimeout(() => {
// 如果正在清空历史记录,则不隐藏历史记录容器
if (!isClearingHistory.value) {
showHistory.value = false
}
}, 200)
}
// 设置搜索类型
const setSearchType = (type: string) => {
const typeItems = document.querySelectorAll('.search-type-item')
typeItems.forEach(item => {
item.classList.remove('active')
})
// 添加active类到点击的元素
// event?.currentTarget?.classList.add('active')
console.log('搜索类型:', type)
}
// 清除搜索输入
const clearSearch = () => {
searchValue.value = ''
console.log('已清除搜索输入')
}
// 左侧用户菜单
const userMenu = [
{
key: '1',
label: '个人中心'
},
{
key: '2',
label: '设置'
},
{
key: '3',
label: '退出登录',
danger: true
}
]
// 右侧功能菜单
const rightMenu = [
{
key: '1',
label: '浏览记录'
},
{
key: '2',
label: '收藏夹'
},
{
key: '3',
label: '帮助中心'
}
]
</script> </script>
<style scoped> <style scoped>
#header { #header {
width: 100%; width: 100%;
background-color: #f5f5f5; background-color: #ffffff;
padding: 0 20px; padding: 10px 20px;
/* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); */
position: relative;
z-index: 1000;
} }
h1 { #header-nav-row {
color: #42b983; height: 60px;
align-items: center;
} }
#header-profile { #header-profile {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
height: 60px; padding: 10px 0;
} }
#header-profile-left { #header-profile-left {
@@ -63,17 +251,512 @@ h1 {
align-items: center; align-items: center;
} }
#header-profile-left>button {
margin-right: 10px;
}
#header-profile-right { #header-profile-right {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
gap: 10px;
} }
#header-profile-right>button { /* 用户信息容器样式 */
margin-left: 10px; .user-info-container {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s ease;
}
.user-info-container:hover {
background-color: #f0f2f5;
}
.user-name {
font-size: 14px;
font-weight: 500;
color: #333;
}
.user-arrow {
font-size: 12px;
color: #999;
transition: transform 0.3s ease;
}
.user-info-container:hover .user-arrow {
transform: rotate(180deg);
}
/* Logo样式 */
.header-nav-Logo a {
font-size: 24px;
font-weight: bold;
color: #1890ff;
text-decoration: none;
transition: color 0.3s ease;
}
.header-nav-Logo a:hover {
color: #40a9ff;
}
/* 搜索框样式 */
.header-nav-search {
padding: 0 20px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
position: relative;
width: 100%;
max-width: 700px;
margin: 0 auto;
}
.search-input-container {
width: 100%;
display: block;
justify-content: center;
align-items: center;
position: relative;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.search-input-container.search-focused {
/* box-shadow: 0 4px 16px rgba(255, 80, 0, 0.2); */
}
/* 搜索框整体布局 */
.search-wrapper {
display: flex;
align-items: center;
background-color: #ffffff;
border: 2px solid #e8e8e8;
border-radius: 28px;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
backdrop-filter: blur(10px);
}
.search-input-container.search-focused .search-wrapper {
border-color: #ff5000;
box-shadow: 0 4px 20px rgba(255, 80, 0, 0.2);
transform: translateY(-1px);
}
/* 搜索类型选择器 */
.search-type-selector {
padding: 3.5px 16px;
border-right: 1px solid #e8e8e8;
background-color: #fafafa;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
border-radius: 28px 0 0 28px;
}
.search-type-selector:hover {
background-color: #f5f5f5;
}
.search-type-selector ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
gap: 20px;
}
.search-type-item {
padding: 14px 0;
font-size: 14px;
font-weight: 500;
color: #666;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
position: relative;
user-select: none;
letter-spacing: 0.5px;
}
.search-type-item:hover {
color: #ff5000;
transform: translateY(-1px);
}
.search-type-item.active {
color: #ff5000;
font-weight: 600;
}
.search-type-item.active::after {
content: '';
position: absolute;
bottom: 8px;
left: 0;
width: 100%;
height: 2px;
background-color: #ff5000;
border-radius: 1px;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
width: 0;
left: 50%;
}
to {
width: 100%;
left: 0;
}
}
/* 搜索输入区域 */
.search-input-wrapper {
flex: 1;
position: relative;
padding: 0 16px;
display: flex;
align-items: center;
}
.search-input-prefix {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: #999;
font-size: 18px;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
z-index: 1;
}
.search-input-container.search-focused .search-input-prefix {
color: #ff5000;
transform: translateY(-50%) scale(1.1);
}
.custom-search-input {
font-size: 16px;
min-height: 52px;
height: 52px;
padding: 0 50px 0 48px;
border: none;
box-shadow: none;
background-color: transparent;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
font-weight: 400;
letter-spacing: 0.5px;
box-sizing: border-box; /* 关键! */
outline: none; /* 避免默认焦点轮廓 */
}
/* 可选:焦点状态 */
.custom-search-input:focus {
background-color: rgba(255, 255, 255, 0.03);
}
.custom-search-input::placeholder {
color: #ccc;
font-size: 14px;
font-weight: 400;
transition: all 0.3s ease;
}
.custom-search-input:focus {
border-color: transparent !important;
box-shadow: none !important;
}
.search-input-container.search-focused .custom-search-input::placeholder {
color: #ffccb3;
}
.search-input-suffix {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
color: #999;
font-size: 16px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
background-color: #f5f5f5;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
animation: fadeIn 0.3s ease-out forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-50%) scale(0.8);
}
to {
opacity: 1;
transform: translateY(-50%) scale(1);
}
}
.search-input-suffix:hover {
color: #ff5000;
background-color: #fff0e6;
transform: translateY(-50%) scale(1.1);
}
.search-input-container.search-focused .search-input-suffix {
color: #ff5000;
}
/* 搜索按钮 */
.search-button-container {
margin-right: 1px;
padding: 15.5px;
height: 100%;
background: linear-gradient(135deg, #ff5000, #ff8c00);
color: #ffffff;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
position: relative;
overflow: hidden;
border-radius: 0 28px 28px 0;
}
.search-button-container:active {
transform: translateX(1px) translateY(0);
box-shadow: 0 2px 8px rgba(255, 80, 0, 0.3);
}
.search-button-container::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transform: rotate(45deg);
animation: shine 2.5s ease-in-out infinite;
}
/* @keyframes shine {
0% {
transform: rotate(45deg) translateX(-100%) translateY(-100%);
opacity: 0;
}
50% {
opacity: 0.8;
}
100% {
transform: rotate(45deg) translateX(100%) translateY(100%);
opacity: 0;
}
} */
.search-button-text {
font-size: 16px;
font-weight: 600;
position: relative;
z-index: 1;
letter-spacing: 1px;
transition: all 0.3s ease;
}
.search-button-icon {
position: relative;
z-index: 1;
font-size: 16px;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
display: flex;
align-items: center;
justify-content: center;
}
.search-button-container:hover .search-button-icon {
transform: scale(1.1) rotate(15deg);
}
.search-button-container:hover .search-button-text {
transform: translateY(-1px);
}
/* 响应式调整 */
@media (max-width: 768px) {
.search-type-selector {
padding: 0 12px;
}
.search-type-selector ul {
gap: 12px;
}
.search-type-item {
padding: 12px 0;
font-size: 13px;
}
.search-input-wrapper {
padding: 0 12px;
}
.custom-search-input {
font-size: 14px !important;
height: 48px !important;
padding: 0 40px 0 40px !important;
}
.search-input-prefix {
font-size: 16px;
}
.search-button-container {
padding: 0 24px;
}
.search-button-text {
font-size: 14px;
}
.search-button-icon {
font-size: 14px;
}
.search-button-container:hover .search-button-icon {
transform: scale(1.05) rotate(10deg);
}
}
@media (max-width: 576px) {
.search-type-selector {
padding: 0 10px;
}
.search-type-selector ul {
gap: 8px;
}
.search-type-item {
font-size: 12px;
}
.search-input-wrapper {
padding: 0 10px;
}
.custom-search-input {
font-size: 13px !important;
padding: 0 35px 0 35px !important;
}
.search-input-prefix {
font-size: 14px;
}
.search-button-container {
padding: 0 20px;
}
.search-button-text {
font-size: 13px;
}
.search-button-icon {
display: none;
}
}
/* 搜索历史样式 */
.search-history-container {
position: absolute;
top: 100%;
left: 0;
width: 100%;
margin-top: 4px;
background-color: #ffffff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 100;
border: 1px solid #f0f0f0;
}
.search-history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.history-title {
font-size: 16px;
font-weight: 500;
color: #333;
}
.clear-history {
font-size: 14px;
color: #999;
text-decoration: none;
transition: color 0.3s ease;
}
.clear-history:hover {
color: #1890ff;
}
.search-history-tags {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.history-tag {
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
padding: 4px 12px;
background-color: #f5f5f5;
border-color: #e8e8e8;
}
.history-tag:hover {
background-color: #e6f7ff;
border-color: #91d5ff;
color: #1890ff;
}
.no-history {
text-align: center;
padding: 20px 0;
color: #999;
font-size: 14px;
}
/* 商品列表链接样式 */
.header-nav-product a {
font-size: 16px;
font-weight: 500;
color: #333;
text-decoration: none;
transition: all 0.3s ease;
padding: 8px 16px;
border-radius: 4px;
}
.header-nav-product a:hover {
color: #1890ff;
background-color: #e6f7ff;
} }
</style> </style>

View File

@@ -1,45 +1,54 @@
<template> <template>
<div id="main"> <div id="main">
<div id="main-header"> <div id="main-header">
<Button type="primary" @click="router.push('/')">按钮1</Button>
<Button type="primary" @click="router.push('/list')">按钮2</Button>
<Button type="primary" @click="router.push('/cart')">按钮3</Button>
<Button type="primary" @click="router.push('/history')">按钮4</Button>
<Button type="primary" @click="router.push('/user')">按钮5</Button>
<Button type="primary" @click="router.push('/login')">按钮6</Button>
<Button type="primary" @click="router.push('/register')">按钮7</Button>
</div> </div>
<Row id="main-content"> <Row id="main-content" :wrap="false">
<Col class="main-content-col-top" :span="6"> <Col class="main-content-col-top" :span="4">
<h2>商品列表</h2> <div>
<ul>
<li>
<!-- 分类项 -->
<div class="category-item">
<i class="iconfont icon-zhineng"></i>
<a href="http://localhost:5173/search?keyword=电脑" >电脑</a>
<span>/</span>
<a href="http://localhost:5173/search?keyword=配件" >配件</a>
<span>/</span>
<a href="http://localhost:5173/search?keyword=办公" >办公</a>
<span>/</span>
<a href="http://localhost:5173/search?keyword=文具" >文具</a>
</div>
<!-- 分类详情 -->
<div v-if="true" class="category-detail">
<ul>
<li class="category-detail-item">
<a href="http://localhost:5173/search?keyword=电脑整机">电脑整机</a>
<span >></span>
<a href="http://localhost:5173/search?keyword=笔记本" >笔记本</a>
<a href="http://localhost:5173/search?keyword=台式机" >台式机</a>
<a href="http://localhost:5173/search?keyword=服务器" >服务器</a>
</li>
</ul>
</div>
</li>
</ul>
</div>
</Col> </Col>
<Col class="main-content-col-center" :span="12"> <Col class="main-content-col-center" :span="15">
<Row id="main-content-ad"> <!-- 轮动广告 -->
<Col :span="12"> <div id="carousel-container">
<h2>轮动广告</h2> <Carousel autoplay :dots="true" effect="fade" :autoplay-speed="3000">
<div v-for="ad in adList" :key="ad.id" class="carousel-item">
<a :href="ad.link" target="_blank" rel="noopener noreferrer">
<img :src="ad.image" :alt="ad.title" class="carousel-image" />
<div class="carousel-title">{{ ad.title }}</div>
</a>
</div>
</Carousel>
</div>
</Col> </Col>
<Col :span="12"> <Col class="main-content-col-bottom" :span="4">
<h2>百亿补贴</h2> <UserInfo />
</Col>
</Row>
<Row id="main-content-hot-goods">
<!-- 热门商品展示 -->
<Col :span="6">
<h2>热门商品1</h2>
</Col>
<Col :span="6">
<h2>热门商品2</h2>
</Col>
<Col :span="6">
<h2>热门商品3</h2>
</Col>
<Col :span="6">
<h2>热门商品4</h2>
</Col>
</Row>
</Col>
<Col class="main-content-col-bottom" :span="6">
<h2>个人信息</h2>
</Col> </Col>
</Row> </Row>
</div> </div>
@@ -48,22 +57,382 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { Button, Col, Row } from 'ant-design-vue'; import { Button, Col, Row, Carousel } from 'ant-design-vue';
import UserInfo from '@/Views/User/Userinfo.vue'
const router = useRouter() const router = useRouter()
// 按钮数据数组
const buttonList = [
{ id: 1, text: '按钮1', route: '/' },
{ id: 2, text: '按钮2', route: '/list' },
{ id: 3, text: '按钮3', route: '/cart' },
{ id: 4, text: '按钮4', route: '/history' },
{ id: 5, text: '按钮5', route: '/user' },
{ id: 6, text: '按钮6', route: '/login' },
{ id: 7, text: '按钮7', route: '/register' }
]
// 广告数据
const adList = [
{ id: 1, title: '广告1', image: 'https://picsum.photos/id/237/800/300', link: '#' },
{ id: 2, title: '广告2', image: 'https://picsum.photos/id/238/800/300', link: '#' },
{ id: 3, title: '广告3', image: 'https://picsum.photos/id/239/800/300', link: '#' }
]
</script> </script>
<style scoped> <style scoped>
#main { #main {
width: 100%; width: 100%;
padding: 0 20px; padding: 0 20px;
} }
h1 { h1 {
color: #42b983; color: #42b983;
} }
#main-header { #main-header {
/* 根据中心对齐 分布按钮 */ /* 根据中心对齐 分布按钮 */
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: 10px; align-items: center;
padding: 8px 0; gap: 20px;
padding: 16px 0;
width: 100%;
flex-wrap: wrap;
}
#main-header .ant-btn {
width: 120px;
height: 45px;
font-size: 14px;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 0;
border-radius: 8px;
font-weight: 500;
}
#main-header .ant-btn-primary {
background-color: #1890ff;
border-color: #1890ff;
transition: all 0.3s ease;
}
#main-header .ant-btn-primary:hover {
background-color: #40a9ff;
border-color: #40a9ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
}
/* 商品分类样式 */
.main-content-col-top {
padding: 20px;
background-color: #ffffff;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
position: relative;
z-index: 100;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.main-content-col-top:hover {
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
}
@keyframes shine {
0% {
transform: translateX(-100%);
}
20% {
transform: translateX(100%);
}
100% {
transform: translateX(100%);
}
}
.main-content-col-top ul {
list-style: none;
padding: 0;
margin: 0;
}
.main-content-col-top li {
margin-bottom: 12px;
padding: 8px;
border-radius: 8px;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
position: relative;
overflow: visible;
}
/* 分类详情样式 */
.main-content-col-top li:hover {
background-color: #fff5f5;
transform: translateX(8px);
box-shadow: 0 2px 8px rgba(255, 80, 0, 0.1);
}
/* 分类项样式 */
.category-item {
display: flex;
align-items: center;
gap: 5px;
position: relative;
cursor: pointer;
padding: 12px;
border-radius: 8px;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.category-item:hover {
background-color: rgba(255, 80, 0, 0.05);
}
/* 分类详情样式 */
.category-detail {
position: absolute;
left: 100%;
top: -60px;
width: 600px;
height: 400px;
background-color: #ffffff;
border-radius: 12px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
padding: 20px;
margin-left: 10px;
z-index: 9999;
opacity: 0;
visibility: hidden;
transform: translateX(-20px);
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
border: 2px solid #f0f0f0;
overflow-x: auto;
}
/* 悬停显示分类详情 */
.main-content-col-top li:hover .category-detail {
opacity: 1;
visibility: visible;
transform: translateX(0);
}
.category-detail ul {
list-style: none;
padding: 0;
margin: 0;
}
.category-detail-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
transition: all 0.2s ease;
}
.category-detail-item:hover {
background-color: #f9f9f9;
padding-left: 8px;
border-radius: 4px;
}
.category-detail-item:last-child {
border-bottom: none;
}
.category-detail-item a {
color: #333;
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
position: relative;
}
.category-detail-item a:hover {
color: #ff5000;
transform: translateX(4px);
}
.category-detail-item a::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 1px;
background-color: #ff5000;
transition: width 0.3s ease;
}
.category-detail-item a:hover::after {
width: 100%;
}
.category-detail-item span {
color: #999;
font-size: 14px;
transition: color 0.3s ease;
}
.main-content-col-top .iconfont {
font-size: 22px;
color: #ff5000;
flex-shrink: 0;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.main-content-col-top li:hover .iconfont {
transform: scale(1.1) rotate(5deg);
color: #ff5000;
}
.main-content-col-top a {
text-decoration: none;
color: #333;
font-size: 15px;
font-weight: 500;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
position: relative;
border-radius: 4px;
}
.main-content-col-top a:hover {
color: #ff5000;
background-color: rgba(255, 80, 0, 0.1);
transform: translateY(-1px);
}
.main-content-col-top span {
color: #999;
font-size: 14px;
transition: color 0.3s ease;
}
.main-content-col-top li:hover span {
color: #ff5000;
opacity: 0.8;
}
/* 主内容区域样式 */
#main-content {
margin-top: 20px;
gap: 32px;
}
.main-content-col-center{
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.main-content-col-center h2{
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
padding: 16px;
background-color: #fafafa;
border-radius: 6px;
text-align: center;
}
#main-content-ad,
#main-content-hot-goods {
padding: 0 20px;
margin-bottom: 16px;
}
#main-content-ad .ant-col,
#main-content-hot-goods .ant-col {
background-color: #fafafa;
border-radius: 8px;
transition: all 0.3s ease;
}
#main-content-ad .ant-col:hover,
#main-content-hot-goods .ant-col:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
/* 轮播广告样式 */
#carousel-container {
width: 100%;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.carousel-item {
position: relative;
width: 100%;
height: 300px;
overflow: hidden;
}
.carousel-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.carousel-item:hover .carousel-image {
transform: scale(1.05);
}
.carousel-title {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 20px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
color: #ffffff;
font-size: 24px;
font-weight: 600;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.ant-carousel .slick-dots {
bottom: 20px;
z-index: 10;
}
.ant-carousel .slick-dots li button {
background-color: rgba(255, 255, 255, 0.5);
border-radius: 50%;
width: 12px;
height: 12px;
}
.ant-carousel .slick-dots li.slick-active button {
background-color: #ffffff;
width: 20px;
border-radius: 6px;
}
/* 个人信息样式 */
.main-content-col-bottom {
padding: 20px;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.main-content-col-bottom h2 {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
padding: 16px;
background-color: #fafafa;
border-radius: 6px;
text-align: center;
} }
</style> </style>

View File

@@ -1,21 +1,23 @@
<!-- 根据父类传递的商品列表渲染商品列表 --> <!-- 根据父类传递的商品列表渲染商品列表 -->
<template> <template>
<div id="product-list"> <div id="product-list">
<div id="product-list-header" v-for="product in productList" :key="product.id"> <div class="product-item" v-for="product in productList" :key="product.id">
<!-- 这个商品的id是加密 --> <!-- 这个商品的id是加密 -->
<a href="http://localhost:5173/product/{{ product.id }}" target="_blank"> <div class="product-img-container" @click="handleClick(product)">
<div id="product-list-header-img"> <img class="product-img" :src="product.img || '/0.png'" alt="商品图片">
<img class="product-list-header-img" :src="product.img || '/0.png'" alt="商品图片"> <div class="product-img-mask">
<div class="product-list-header-img-mask"> <div class="product-img-actions">
<span class="action-btn">查看详情</span>
</div> </div>
</div> </div>
<div id="product-list-header-content">
<h2>{{ product.name }}</h2>
</div> </div>
<div id="product-list-header-price"> <div class="product-content">
<h2>{{ product.price }}</h2> <h3 class="product-name">{{ product.name }}</h3>
<div class="product-price">
<span class="price-symbol">¥</span>
<span class="price-value">{{ product.price }}</span>
</div>
</div> </div>
</a>
</div> </div>
</div> </div>
</template> </template>
@@ -23,6 +25,7 @@
import { type PropType } from 'vue' import { type PropType } from 'vue'
import type { ProductDetail } from '@/Util/Type' import type { ProductDetail } from '@/Util/Type'
import { useGlobalStore } from '@/Util/globalStore' import { useGlobalStore } from '@/Util/globalStore'
import router from '@/Route/route'
// 接收父类传递的商品列表 // 接收父类传递的商品列表
const props = defineProps({ const props = defineProps({
productList: { productList: {
@@ -30,7 +33,13 @@ const props = defineProps({
default: () => [], default: () => [],
}, },
}) })
// 点击商品跳转详情页
const handleClick = (product: ProductDetail) => {
// id 加密
const encryptedId = btoa(product.id)
// 跳转详情页 (新开页面)
window.open(`/product?id=${encryptedId}`, '_blank')
}
</script> </script>
<style scoped> <style scoped>
/* 自定义css */ /* 自定义css */
@@ -39,47 +48,242 @@ const props = defineProps({
width: 100%; width: 100%;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 20px;
padding: 10px;
} }
h2 { /* 商品项 */
color: #42b983; .product-item {
flex: 0 0 calc(16.666666% - 16.666666px);
max-width: calc(16.666666% - 16.666666px);
background-color: #ffffff;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
cursor: pointer;
position: relative;
} }
/* 商品 */ .product-item:hover {
#product-list-header { transform: translateY(-6px);
max-width: 16.666666666666%; box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15);
height: initial;
margin: 8px;
} }
#product-list-header-img { .product-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(90deg, #ff5000, #ff8c00);
opacity: 0;
transition: opacity 0.3s ease;
}
.product-item:hover::before {
opacity: 1;
}
/* 商品链接 */
.product-item a {
text-decoration: none;
color: inherit;
display: block;
height: 100%;
}
/* 商品图片容器 */
.product-img-container {
position: relative; position: relative;
width: 100%; width: 100%;
height: 180px; padding-top: 100%; /* 1:1 宽高比 */
overflow: hidden; overflow: hidden;
background-color: #f8f8f8;
border-radius: 16px 16px 0 0;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
} }
.product-list-header-img { .product-item:hover .product-img-container {
width: 100%; background-color: #f0f0f0;
height: 100%;
object-fit: cover;
border-radius: 8px;
} }
/* 鼠标图片悬浮亮度变低 */ .product-img {
.product-list-header-img-mask {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 8px; object-fit: cover;
background-color: transparent; transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
transition: all 0.3s ease;
z-index: 999999;
} }
#product-list-header-img:hover .product-list-header-img-mask { .product-item:hover .product-img {
background-color: rgba(0, 0, 0, 0.3); transform: scale(1.1);
}
/* 图片遮罩层 */
.product-img-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0);
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
border-radius: 16px 16px 0 0;
}
.product-item:hover .product-img-mask {
background-color: rgba(0, 0, 0, 0.5);
opacity: 1;
}
/* 图片悬停操作按钮 */
.product-img-actions {
transform: translateY(20px);
opacity: 0;
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.product-item:hover .product-img-actions {
transform: translateY(0);
opacity: 1;
}
.action-btn {
background-color: #ffffff;
color: #ff5000;
padding: 10px 20px;
border-radius: 25px;
font-size: 14px;
font-weight: 500;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
box-shadow: 0 4px 12px rgba(255, 80, 0, 0.2);
position: relative;
overflow: hidden;
}
.action-btn:hover {
background-color: #ff5000;
color: #ffffff;
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(255, 80, 0, 0.4);
}
.action-btn::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transform: rotate(45deg);
animation: shine 3s ease-in-out infinite;
}
@keyframes shine {
0% {
transform: translateX(-100%) rotate(45deg);
}
100% {
transform: translateX(100%) rotate(45deg);
}
}
/* 商品内容 */
.product-content {
padding: 20px;
transition: all 0.3s ease;
}
.product-item:hover .product-content {
background-color: #f9f9f9;
}
.product-name {
font-size: 15px;
font-weight: 500;
color: #333;
margin: 0 0 12px 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.3s ease;
}
.product-item:hover .product-name {
color: #ff5000;
}
/* 商品价格 */
.product-price {
display: flex;
align-items: baseline;
gap: 6px;
position: relative;
overflow: hidden;
}
.price-symbol {
font-size: 14px;
color: #ff5000;
font-weight: 500;
transition: transform 0.3s ease;
}
.product-item:hover .price-symbol {
transform: scale(1.1);
}
.price-value {
font-size: 20px;
color: #ff5000;
font-weight: 700;
transition: all 0.3s ease;
position: relative;
}
.product-item:hover .price-value {
transform: scale(1.05);
text-shadow: 0 1px 2px rgba(255, 80, 0, 0.3);
}
/* 响应式调整 */
@media (max-width: 1200px) {
.product-item {
flex: 0 0 calc(20% - 16px);
max-width: calc(20% - 16px);
}
}
@media (max-width: 992px) {
.product-item {
flex: 0 0 calc(25% - 15px);
max-width: calc(25% - 15px);
}
}
@media (max-width: 768px) {
.product-item {
flex: 0 0 calc(33.333333% - 13.333333px);
max-width: calc(33.333333% - 13.333333px);
}
}
@media (max-width: 576px) {
.product-item {
flex: 0 0 calc(50% - 10px);
max-width: calc(50% - 10px);
}
} }
</style> </style>

View File

@@ -1,27 +1,809 @@
<!-- 商品详情页面 --> <!-- 商品详情页面 -->
<template> <template>
<div id="product-detail"> <div id="product-detail">
<van-row id="product-detail-header"> <div class="product-detail-container">
<van-col :span="24"> <div class="left-panel">
<h2>商品详情</h2> <div class="shop-info">
</van-col> <!-- 店铺信息 -->
</van-row> <div>
<!-- 店铺头像 -->
<Avatar size="large" src="https://api.dicebear.com/7.x/avataaars/svg?seed=user123" />
</div>
<!-- 店铺名称 店铺评分 -->
<div class="shop-info-item">
<div class="shop-info-label">店铺名称
<div class="shop-info-detail">
<div class="shop-info-detail-item">
<div class="shop-info-detail-label">店铺详情</div>
<div class="shop-info-detail-value">店铺详情内容</div>
</div>
</div>
</div>
<div class="shop-info-value">店铺评分</div>
</div>
<!-- 按钮 客服 进店 -->
<div class="shop-btn-group">
<a href="#" class="btn btn-primary"><i class="iconfont icon-kefu"></i><span>客服</span></a>
<a href="#" class="btn btn-primary"><i class="iconfont icon-xiangyou"></i><span>进店</span></a>
</div>
</div>
<div class="gallery-container">
<div class="thumbnails">
<div v-for="(img, index) in images" :key="index" class="thumbnail-item"
:class="{ active: currentIndex === index }" @click="selectImage(index)">
<img :src="img" :alt="'缩略图' + (index + 1)">
</div>
</div>
<div class="main-image">
<a-image class="main-image-container" :src="currentImage" alt="商品主图" />
</div>
</div>
<div class="tab-container">
<div class="tab-header">
<a-anchor direction="horizontal" :items="tabs" class="tab-item" />
</div>
<div class="tab-content">
<!-- 评论 -->
<div id="reviews">
<h2>
评论
<a href="#reviews"></a>
</h2>
<div class="reviews-content">
<!-- 评论内容 -->
<h3>用户评论</h3>
<!-- 更多评论... -->
</div>
</div>
<!-- 文图详情 -->
<div id="description">
<h2>
文图详情
<a href="#description"></a>
</h2>
</div>
<!-- 参数信息 -->
<div id="specs">
<h2>
参数信息
<a href="#specs"></a>
</h2>
</div>
<!-- 本店推荐 -->
<div id="recommendations">
<h2>
本店推荐
<a href="#recommendations"></a>
</h2>
</div>
</div>
</div>
</div>
<div class="right-panel">
<div class="product-info">
<h1 class="product-title">BenQ 明基投影仪 商务办公会议培训用 1080P高清智能投影机</h1>
<div class="price-section">
<div class="price-row">
<span class="price-label">券后价</span>
<span class="price-value">{{ selectedModelInfo?.price }}</span>
<span class="original-price">{{ selectedModelInfo?.originalPrice }}</span>
</div>
<div class="tags">
<span class="tag">官方正品</span>
<span class="tag">全国包邮</span>
<span class="tag">7天无理由退换</span>
</div>
</div>
<div class="model-selection">
<div class="selection-container">
<h3 class="selection-title">选择型号</h3>
<div class="view-switch" @click="toggleView">
<p>{{ isSmallView ? '切换为大视图' : '切换为小视图' }}</p>
</div>
</div>
<!-- 文从左到右 -->
<div v-if="isSmallView" class="model-options-small">
<div v-for="model in models" :key="model.id" class="model-option"
:class="{ active: selectedModel === model.id }" @click="selectModel(model.id)">
<div class="model-small-item">
<img :src="model.img" alt="{{ model.name }}">
<span> {{ model.name }}</span>
</div>
</div>
</div>
<!-- 图上文下-->
<div v-if="!isSmallView" class="model-options-big">
<div v-for="model in models" :key="model.id" class="model-option"
:class="{ active: selectedModel === model.id }" @click="selectModel(model.id)">
<!-- 图片 -->
<div class="model-image">
<img :src="model.img" alt="{{ model.name }}">
</div>
<div class="model-name">{{ model.name }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="action-section">
<div class="action-buttons">
<div @click="addToCart" class="btn-cart">加入购物车</div>
<div @click="buyProduct" class="btn-buy">领券购买</div>
</div>
<div class="btn-collect" @click="collectProduct">
<i class="iconfont icon-xiangyou"></i>
收藏
</div>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const router = useRouter() const router = useRouter()
// 根据路由获取商品id
const productId = ref(router.currentRoute.value.params.productId) const productId = ref(router.currentRoute.value.params.productId)
// 在根据service获取商品详情
const currentIndex = ref(0)
const images = ref([
'/0.png',
'/0.png',
'/0.png',
'/0.png',
'/0.png'
])
const currentImage = computed(() => images.value[currentIndex.value])
const selectImage = (index: number) => {
currentIndex.value = index
}
const selectedModel = ref('x500')
const models = ref([
{ id: 'x500', name: 'X500', price: '999', originalPrice: '1999', img: '/0.png' },
{ id: 'w600', name: 'W600', price: '1299', originalPrice: '2499', img: '/0.png' },
{ id: 'e800', name: 'E800', price: '1599', originalPrice: '3299', img: '/0.png' }
])
const selectModel = (id: string) => {
selectedModel.value = id
}
const selectedModelInfo = computed(() => models.value.find(model => model.id === selectedModel.value))
const activeTab = ref('reviews')
const tabs = ref([
{
key: 'reviews',
href: '#reviews',
title: '评论',
},
{
key: 'description',
href: '#description',
title: '文图详情',
},
{
key: 'specs',
href: '#specs',
title: '参数信息',
},
{
key: 'recommendations',
href: '#recommendations',
title: '本店推荐',
}
])
const isSmallView = ref(true)
const toggleView = () => {
isSmallView.value = !isSmallView.value
}
const collectProduct = () => {
console.log('收藏商品')
}
const addToCart = () => {
console.log('加入购物车')
}
const buyProduct = () => {
console.log('领券购买')
}
</script> </script>
<style scoped> <style scoped>
#product-detail { #product-detail {
width: 100%; width: 100%;
padding: 0 20px; min-height: 100vh;
background-color: #f5f5f5;
padding: 20px;
} }
h2 {
color: #42b983; .product-detail-container {
display: flex;
border-radius: 16px;
margin-bottom: 20px;
overflow: hidden;
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
border: 1px solid #f0f0f0;
}
.left-panel {
width: 50%;
padding: 25px;
border-right: 1px solid #f0f0f0;
overflow-y: auto;
overflow-x: hidden;
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
}
.shop-info {
display: flex;
align-items: center;
gap: 20px;
padding: 20px;
margin-bottom: 25px;
transition: all 0.3s ease;
}
.shop-info-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 5px;
position: relative;
cursor: pointer;
}
.shop-info-label {
font-size: 14px;
color: #666;
position: relative;
}
.shop-info-detail {
position: absolute;
top: 100%;
left: 0;
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 15px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
z-index: 100;
min-width: 200px;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s ease;
margin-top: 5px;
}
.shop-info-item:hover .shop-info-detail {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.shop-info-value {
font-size: 16px;
color: #333;
font-weight: 600;
}
.shop-btn-group {
display: flex;
gap: 10px;
}
.shop-btn-group .btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
text-decoration: none;
transition: all 0.3s ease;
border: none;
cursor: pointer;
}
.shop-btn-group .btn-primary {
background: linear-gradient(135deg, #ff5000 0%, #ff6b00 100%);
color: #ffffff;
box-shadow: 0 2px 8px rgba(255, 80, 0, 0.2);
}
.shop-btn-group .btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 80, 0, 0.3);
}
.shop-btn-group .btn span {
text-decoration: none;
}
.gallery-container {
display: flex;
gap: 15px;
background-color: #ffffff;
padding: 20px;
}
.main-image {
flex: 1;
aspect-ratio: 1;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.3s ease;
position: relative;
}
.thumbnails {
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
max-height: 400px;
padding: 5px;
}
.thumbnail-item {
width: 80px;
height: 80px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 0;
}
.thumbnail-item:hover {
border-color: #ff5000;
transform: scale(1.05);
box-shadow: 0 2px 8px rgba(255, 80, 0, 0.2);
}
.thumbnail-item.active {
border-color: #ff5000;
box-shadow: 0 4px 12px rgba(255, 80, 0, 0.3);
transform: scale(1.05);
}
.thumbnail-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.right-panel {
flex: 1;
padding: 35px;
overflow-y: auto;
overflow-x: auto;
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
}
.product-info {
display: flex;
flex-direction: column;
gap: 25px;
}
.product-title {
font-size: 24px;
color: #333;
font-weight: 700;
margin: 0;
line-height: 1.5;
padding-bottom: 15px;
border-bottom: 2px solid #f0f0f0;
}
.price-section {
background: linear-gradient(135deg, #fff5f5 0%, #ffe8e8 100%);
padding: 20px;
border-radius: 12px;
border: 1px solid #ffcccc;
box-shadow: 0 2px 8px rgba(255, 80, 0, 0.08);
transition: all 0.3s ease;
}
.price-section:hover {
box-shadow: 0 4px 12px rgba(255, 80, 0, 0.15);
}
.price-row {
display: flex;
align-items: baseline;
gap: 10px;
margin-bottom: 10px;
}
.price-label {
font-size: 14px;
color: #ff5000;
}
.price-value {
font-size: 32px;
color: #ff5000;
font-weight: 700;
}
.original-price {
font-size: 16px;
color: #999;
text-decoration: line-through;
}
.tags {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.tag {
font-size: 12px;
color: #ff5000;
background: linear-gradient(135deg, #ffffff 0%, #fff5f5 100%);
border: 1px solid #ff5000;
padding: 6px 14px;
border-radius: 20px;
font-weight: 500;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(255, 80, 0, 0.08);
}
.tag:hover {
background: linear-gradient(135deg, #ff5000 0%, #ff6b00 100%);
color: #ffffff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 80, 0, 0.25);
}
.model-selection {
border-top: 1px solid #f0f0f0;
padding-top: 20px;
}
.selection-title {
font-size: 16px;
color: #333;
margin: 0 0 15px 0;
font-weight: 500;
}
.selection-container {
display: flex;
}
/* 切换视图 */
.view-switch {
/* 靠右对齐 */
margin-left: auto;
margin-right: 10px;
font-size: 14px;
color: #666;
cursor: pointer;
transition: all 0.3s ease;
}
.view-switch:hover {
color: #ff5000;
}
.model-options-small, .model-options-big {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.model-option {
flex: 0 0 120px;
border: 2px solid #f0f0f0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
margin-bottom: 10px;
}
.model-option:hover {
border-color: #ff5000;
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(255, 80, 0, 0.15);
}
.model-option.active {
border-color: #ff5000;
background: linear-gradient(135deg, #fff5f5 0%, #ffe8e8 100%);
box-shadow: 0 4px 16px rgba(255, 80, 0, 0.25);
transform: translateY(-3px);
}
.model-image {
border-radius: 8px;
margin-bottom: 8px;
}
.model-image img {
width: 100%;
height: 100%;
border-radius: 8px 8px 0 0;
}
.model-name {
font-size: 16px;
color: #333;
font-weight: 600;
margin-bottom: 5px;
}
.model-small-item {
text-align: left;
border-radius: 8px;
flex: 0 0 120px;
transition: all 0.3s ease;
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
}
.model-small-item img {
margin: 5px;
height: 30px;
align-items: left;
}
.model-price {
font-size: 18px;
color: #ff5000;
font-weight: 700;
}
.action-section {
display: flex;
position: fixed;
bottom: 30px;
right: 30px;
z-index: 1000;
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
padding: 18px;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
border: 1px solid #f0f0f0;
transition: all 0.3s ease;
}
.action-buttons {
display: flex;
gap: 2px;
}
.btn-cart,
.btn-buy {
text-align: center;
width: 200px;
line-height: 48px;
flex: 1;
font-size: 16px;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.3s ease;
border-radius: 24px;
margin-right: 10px;
}
.btn-cart {
background: linear-gradient(135deg, #ffffff 0%, #fff5f5 100%);
color: #ff5000;
border: 2px solid #ff5000;
}
.btn-cart:hover {
background: linear-gradient(135deg, #ff5000 0%, #ff6b00 100%);
color: #ffffff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 80, 0, 0.3);
}
.btn-buy {
background: linear-gradient(135deg, #ff5000 0%, #ff6b00 100%);
color: #ffffff;
box-shadow: 0 4px 12px rgba(255, 80, 0, 0.3);
}
.btn-buy:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(255, 80, 0, 0.4);
}
.btn-collect {
flex: 1;
line-height: 48px;
font-size: 16px;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.3s ease;
background: linear-gradient(135deg, #ffffff 0%, #fff5f5 100%);
color: #ff5000;
margin-left: 10px;
border: 2px solid #ff5000;
border-radius: 24px;
padding: 0 20px;
min-width: 100px;
}
.btn-collect:hover {
background: linear-gradient(135deg, #ff5000 0%, #ff6b00 100%);
color: #ffffff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 80, 0, 0.3);
}
.auxiliary-actions {
display: flex;
gap: 20px;
justify-content: center;
}
.aux-item {
font-size: 14px;
color: #666;
cursor: pointer;
transition: color 0.3s ease;
}
.aux-item:hover {
color: #ff5000;
}
.tab-container {
overflow: hidden;
}
.tab-header {
display: flex;
background: #ffffff;
}
.tab-item {
padding: 18px 35px;
font-size: 16px;
color: #666;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
font-weight: 500;
}
.tab-item:hover {
color: #ff5000;
background-color: #fff5f5;
}
.tab-item.active {
color: #ff5000;
font-weight: 600;
background-color: #fff5f5;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #ff5000 0%, #ff6b00 100%);
box-shadow: 0 2px 8px rgba(255, 80, 0, 0.3);
}
.tab-content {
padding: 30px;
background-color: #ffffff;
}
.tab-panel {
min-height: 200px;
}
.review-item {
padding: 20px;
border-bottom: 1px solid #f0f0f0;
}
.review-item:last-child {
border-bottom: none;
}
.review-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.reviewer {
font-size: 14px;
color: #333;
font-weight: 600;
}
.review-rating {
font-size: 16px;
color: #ffc107;
}
.review-text {
font-size: 14px;
color: #666;
line-height: 1.6;
margin: 0;
}
.params-table {
width: 100%;
border-collapse: collapse;
}
.params-table td {
padding: 15px;
border: 1px solid #f0f0f0;
font-size: 14px;
}
.params-table td:first-child {
background-color: #fafafa;
color: #666;
width: 150px;
}
.params-table td:last-child {
color: #333;
}
.thumbnails::-webkit-scrollbar {
height: 4px;
}
.thumbnails::-webkit-scrollbar-track {
background: #f1f1f1;
}
.thumbnails::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 2px;
}
.thumbnails::-webkit-scrollbar-thumb:hover {
background: #999;
}
.left-panel::-webkit-scrollbar,
.right-panel::-webkit-scrollbar {
width: 6px;
}
.left-panel::-webkit-scrollbar-track,
.right-panel::-webkit-scrollbar-track {
background: #f1f1f1;
}
.left-panel::-webkit-scrollbar-thumb,
.right-panel::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
.left-panel::-webkit-scrollbar-thumb:hover,
.right-panel::-webkit-scrollbar-thumb:hover {
background: #999;
} }
</style> </style>

View File

@@ -0,0 +1,226 @@
<!-- 根据数组渲染产品详情弹窗 -->
<template>
<a-modal v-model:open="showModel" @ok="handleOk">
<div class="product-modal">
<div class="product-modal-container">
<div class="product-modal-right">
<div class="product-modal-img">
<img :src="currentProduct.img" alt="产品图片" class="product-modal-image">
</div>
</div>
<div class="product-modal-left">
<div class="product-modal-top">
<div class="product-modal-price">
<span class="price-label">价格</span>
<span class="price-value">¥{{ currentProduct.price }}</span>
</div>
</div>
<div class="product-modal-bottom">
<h3 class="model-title">选择型号</h3>
<div class="model-list">
<div v-for="item in currentItems" :key="item.id" class="model-item"
:class="{ active: currentProduct.id === item.id }" @click="selectProduct(item)">
<div class="model-img">
<img :src="item.img" alt="型号图片" class="model-image">
</div>
<div class="model-name">{{ item.model }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import type { Product } from '@/Util/Type';
const props = defineProps({
currentItems: {
type: Array as () => Product[],
default: () => [],
},
showModel: {
type: Boolean,
default: false,
}
})
const emit = defineEmits(['selectProduct', 'update:showModel'])
// 监听 showModel 变化 用于更新弹窗的显示状态
watch(() => props.showModel, (newVal) => {
showModel.value = newVal
})
const showModel = ref(false)
const currentProduct = ref<Product>(props.currentItems[0] || {} as Product)
// 选择型号 并触发事件 用于更新当前选中的型号
const selectProduct = (item: Product) => {
currentProduct.value = item
}
// 点击确认按钮 触发事件 用于更新父组件的 selectedModel
const handleOk = () => {
emit('selectProduct', currentProduct.value)
showModel.value = false
emit('update:showModel', false)
}
// 监听弹窗关闭事件
watch(() => showModel.value, (newVal) => {
emit('update:showModel', newVal)
})
</script>
<style scoped>
.product-modal {
width: 100%;
height: 100%;
}
.product-modal-container {
display: flex;
height: 300px;
}
.product-modal-right {
flex: 1;
padding: 10 15px;
display: flex;
justify-content: center;
}
.product-modal-img {
width: 100%;
max-width: 200px;
max-height: 200px;
aspect-ratio: 1;
border-radius: 16px;
overflow: hidden;
}
.product-modal-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.product-modal-image:hover {
transform: scale(1.05);
}
.product-modal-left {
flex: 1;
display: flex;
flex-direction: column;
}
.product-modal-top {
flex: 1;
display: flex;
flex-direction: column;
}
.product-modal-title {
font-size: 24px;
font-weight: 700;
color: #333;
margin: 0 0 20px 0;
line-height: 1.4;
}
.product-modal-price {
display: flex;
align-items: baseline;
gap: 8px;
}
.price-label {
font-size: 18px;
color: #666;
font-weight: 500;
}
.price-value {
font-size: 32px;
color: #ff5000;
font-weight: 700;
}
.product-modal-bottom {
flex: 2;
display: flex;
flex-direction: column;
padding-top: 20px;
overflow-y: auto;
}
.model-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 15px 0;
}
.model-list {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
}
.model-item {
display: flex;
justify-content: flex-start;
align-items: center;
gap: 15px;
padding: 5px 12px;
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
border: 2px solid #f0f0f0;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
align-self: flex-start;
}
.model-item:hover {
border: 2px solid #ff5000;
}
.model-item.active {
border-color: #ff5000;
background: linear-gradient(135deg, #fff5f5 0%, #ffffff 100%);
box-shadow: 0 4px 12px rgba(255, 80, 0, 0.2);
}
.model-img {
width: 48px;
height: 48px;
border-radius: 10px;
overflow: hidden;
flex-shrink: 0;
}
.model-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.model-name {
font-size: 16px;
font-weight: 600;
color: #333;
flex-shrink: 1;
min-width: 0;
}
.model-price {
font-size: 18px;
font-weight: 700;
color: #ff5000;
}
.model-item.active .model-name {
color: #ff5000;
}
</style>