feat: 商品列表、商品详情优化

This commit is contained in:
2026-07-27 18:12:29 +08:00
parent eed5c3fe03
commit 8542b437ad
12 changed files with 794 additions and 156 deletions
+55
View File
@@ -9,6 +9,7 @@
"version": "1.0.0",
"dependencies": {
"core-js": "^3.37.1",
"pinia": "^2.3.1",
"vant": "^2.13.9",
"vue": "^2.7.16",
"vue-router": "^3.6.5"
@@ -2372,6 +2373,12 @@
"dev": true,
"license": "ISC"
},
"node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/@vue/shared": {
"version": "3.5.39",
"dev": true,
@@ -6934,6 +6941,54 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pinia": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
"integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.3",
"vue-demi": "^0.14.10"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.4.4",
"vue": "^2.7.0 || ^3.5.11"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/pkg-dir": {
"version": "4.2.0",
"dev": true,
+1
View File
@@ -8,6 +8,7 @@
},
"dependencies": {
"core-js": "^3.37.1",
"pinia": "^2.3.1",
"vant": "^2.13.9",
"vue": "^2.7.16",
"vue-router": "^3.6.5"
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<meta name="theme-color" content="#ffffff">
<title>供货端</title>
<title>积分商城</title>
</head>
<body>
<noscript>请启用 JavaScript 后访问本应用。</noscript>
+5
View File
@@ -1,13 +1,18 @@
import Vue from 'vue'
import Vant from 'vant'
import { createPinia, PiniaVuePlugin } from 'pinia'
import 'vant/lib/index.css'
import App from './App.vue'
import router from './router'
Vue.use(Vant)
Vue.use(PiniaVuePlugin)
Vue.config.productionTip = false
const pinia = createPinia()
new Vue({
router,
pinia,
render: h => h(App)
}).$mount('#app')
+2 -1
View File
@@ -28,7 +28,8 @@ export default new Router({
path: '/joyful-selection',
component: JoyfulSelectionLayout,
children: [
{ path: '', component: JoyfulSelectionHome },
{ path: '', redirect: 'home' },
{ path: 'home', component: JoyfulSelectionHome },
{ path: 'products', component: JoyfulSelectionProductList },
{ path: 'product-detail', component: JoyfulSelectionProductDetail },
{ path: 'cart', component: JoyfulSelectionCart },
+44
View File
@@ -0,0 +1,44 @@
import { defineStore } from 'pinia'
const CART_KEY = 'joyful-selection-cart'
const readCartItems = () => {
try {
return JSON.parse(localStorage.getItem(CART_KEY) || '[]')
} catch (error) {
return []
}
}
const saveCartItems = items => {
localStorage.setItem(CART_KEY, JSON.stringify(items))
}
export const useCartStore = defineStore('joyfulSelectionCart', {
state: () => ({
items: []
}),
getters: {
totalCount: state => state.items.reduce((total, item) => total + Number(item.count || 0), 0),
totalPrice: state => state.items.reduce((total, item) => total + Math.round(Number(item.price) * 100) * Number(item.count || 0), 0)
},
actions: {
loadCart() {
this.items = readCartItems()
},
persistCart() {
saveCartItems(this.items)
},
addItem(item) {
const index = this.items.findIndex(cartItem => cartItem.cartId === item.cartId)
if (index > -1) {
this.items[index].count += item.count
} else {
this.items.unshift(item)
}
this.persistCart()
}
}
})
+1 -1
View File
@@ -68,7 +68,7 @@ export default {
{ id: 'daily', image: bannerDaily, alt: '精选商品每日推荐' }
],
suppliers: [
{ id: 'joy', name: '悦享优选', image: supplierJoy, description: '精选好物,悦享品质生活', tags: ['正品保障', '极速发货', '售后无忧'], productCount: 6800, sales: '18万+', backgroundColor: '#f4f7ff', tagColor: '#4e83ef', tagBackground: '#edf3ff', buttonColor: '#ff651d', path: '/joyful-selection' },
{ id: 'joy', name: '悦享优选', image: supplierJoy, description: '精选好物,悦享品质生活', tags: ['正品保障', '极速发货', '售后无忧'], productCount: 6800, sales: '18万+', backgroundColor: '#f4f7ff', tagColor: '#4e83ef', tagBackground: '#edf3ff', buttonColor: '#ff651d', path: '/joyful-selection/home' },
{ id: 'home', name: '美家生活馆', image: supplierHome, description: '家居好物,点亮品质生活', tags: ['家居日用', '品质精选', '售后保障'], productCount: 5200, sales: '12万+', backgroundColor: '#f1fbf9', tagColor: '#18bfae', tagBackground: '#e9faf7', buttonColor: '#ff651d', path: '/home-living' },
{ id: 'happy', name: '乐购甄选', image: supplierHappy, description: '天天低价,乐享购物', tags: ['低价好物', '限时优惠', '极速配送'], productCount: 7600, sales: '22万+', backgroundColor: '#f7f4ff', tagColor: '#7b61ee', tagBackground: '#f0edff', buttonColor: '#ff651d', path: '/happy-shopping' },
{ id: 'fruit', name: '鲜果优品', image: supplierFruit, description: '新鲜直达,优选好果', tags: ['产地直采', '新鲜直达', '坏果包赔'], productCount: 3500, sales: '9万+', backgroundColor: '#fff9ed', tagColor: '#f39d16', tagBackground: '#fff5df', buttonColor: '#ff651d', path: '/fresh-fruit' },
@@ -3,33 +3,48 @@
<van-nav-bar title="购物车" right-text="编辑" @click-right="$router.push('/joyful-selection/cart/edit')" />
<van-notice-bar text="满 199 元包邮,快去凑单吧~" left-icon="info-o" />
<section class="cart-list">
<van-card
v-for="item in goods"
:key="item.id"
:title="item.name"
:desc="item.desc"
:price="item.price"
:thumb="item.image"
>
<template #tags><van-tag plain type="danger">{{ item.tag }}</van-tag></template>
<template #footer><van-stepper v-model="item.count" theme="round" button-size="22" /></template>
</van-card>
<van-empty v-if="!items.length" description="购物车还是空的" />
<template v-else>
<van-card
v-for="item in items"
:key="item.cartId"
:title="item.name"
:desc="item.desc"
:price="item.price"
:thumb="item.image"
>
<template #tags><van-tag plain type="danger">{{ item.tag }}</van-tag></template>
<template #footer><van-stepper v-model="item.count" theme="round" button-size="22" @change="persistCart" /></template>
</van-card>
</template>
</section>
<van-submit-bar :price="29790" button-text="去结算(3)" @submit="$router.push('/joyful-selection/checkout')">
<van-submit-bar :price="totalPrice" :button-text="submitText" @submit="$router.push('/joyful-selection/checkout')">
<van-checkbox v-model="checked">全选</van-checkbox>
</van-submit-bar>
</main>
</template>
<script>
import { joyfulProducts } from '../product-data'
import { mapActions, mapState } from 'pinia'
import { useCartStore } from '@/store/cart'
export default {
name: 'JoyfulSelectionCart',
data: () => ({
checked: true,
goods: joyfulProducts.slice(0, 3).map(item => ({ ...item, count: 1 }))
})
checked: true
}),
computed: {
...mapState(useCartStore, ['items', 'totalCount', 'totalPrice']),
submitText() {
return `去结算(${this.totalCount})`
}
},
created() {
this.loadCart()
},
methods: {
...mapActions(useCartStore, ['loadCart', 'persistCart'])
}
}
</script>
@@ -1,3 +1,4 @@
<template><main class="success-page"><van-icon name="checked" color="#20bd58" size="68"/><h2>支付成功</h2><b>¥ 297.90</b><section><p>订单编号 <span>YX2024051812345678</span></p><p>支付方式 <span>微信支付</span></p><p>支付时间 <span>2024-05-18 14:35:28</span></p></section><div><van-button round @click="$router.push('/joyful-selection/orders')">查看订单</van-button><van-button round type="danger" @click="$router.push('/joyful-selection')">继续购物</van-button></div></main></template>
<template><main class="success-page"><van-icon name="checked" color="#20bd58" size="68"/><h2>支付成功</h2><b>¥ 297.90</b><section><p>订单编号 <span>YX2024051812345678</span></p><p>支付方式 <span>微信支付</span></p><p>支付时间 <span>2024-05-18 14:35:28</span></p></section><div><van-button round @click="$router.push('/joyful-selection/orders')">查看订单</van-button><van-button round type="danger" @click="$router.push('/joyful-selection/home')">继续购物</van-button></div></main></template>
<script>export default{name:'JoyfulSelectionPaymentSuccess'}</script>
<style scoped>.success-page{min-height:100vh;padding:80px 30px;text-align:center;background:#fff}.success-page h2{margin:15px 0 10px;font-size:17px}.success-page>b{font-size:22px}.success-page section{margin-top:42px;text-align:left;color:#888;font-size:12px}.success-page section p{display:flex;justify-content:space-between}.success-page section span{color:#333}.success-page>div{display:flex;gap:10px;margin-top:35px}.success-page .van-button{flex:1}</style>
@@ -2,7 +2,7 @@
<main class="joyful-layout">
<router-view />
<van-tabbar v-if="showTabbar" route active-color="#ff5b20" inactive-color="#777">
<van-tabbar-item icon="wap-home-o" to="/joyful-selection">首页</van-tabbar-item>
<van-tabbar-item icon="wap-home-o" to="/joyful-selection/home">首页</van-tabbar-item>
<van-tabbar-item icon="apps-o" to="/joyful-selection/products">分类</van-tabbar-item>
<van-tabbar-item icon="cart-o" to="/joyful-selection/cart">购物车</van-tabbar-item>
<van-tabbar-item icon="user-o" to="/joyful-selection/profile">我的</van-tabbar-item>
@@ -15,7 +15,7 @@ export default {
name: 'JoyfulSelectionLayout',
computed: {
showTabbar() {
return ['/joyful-selection', '/joyful-selection/products', '/joyful-selection/cart', '/joyful-selection/profile'].includes(this.$route.path)
return ['/joyful-selection/home', '/joyful-selection/products', '/joyful-selection/cart', '/joyful-selection/profile'].includes(this.$route.path)
}
}
}
@@ -1,20 +1,62 @@
<template>
<main class="detail-page">
<van-nav-bar title="商品详情" left-arrow @click-left="$router.back()" />
<van-image class="product-image" :src="product.image" fit="cover" />
<header class="detail-top">
<van-icon name="arrow-left" @click="$router.back()" />
<h1>商品详情</h1>
<div>
<van-icon name="star-o" />
<van-icon name="share-o" />
</div>
</header>
<section class="image-section">
<van-image class="product-image" :src="product.image" fit="cover" />
<span class="image-index">1/5</span>
</section>
<section class="product-info">
<h1>{{ product.name }}</h1>
<h2>{{ product.name }}</h2>
<p>{{ product.detail }}</p>
<div><b>¥{{ product.price }}</b><del>¥{{ product.originPrice }}</del><van-tag type="danger" plain>{{ product.tag }}</van-tag></div>
<div class="price-row">
<b>¥{{ product.price }}</b>
<del>¥{{ product.originPrice }}</del>
<van-tag class="point-tag" plain type="danger">{{ product.tag }}</van-tag>
</div>
</section>
<van-cell-group inset title="商品服务">
<van-cell title="正品保障" value="极速发货 · 售后无忧" />
<van-cell title="配送" value="满 99 元免配送费" />
</van-cell-group>
<section class="option-card">
<van-cell title="选择规格" value="默认规格" is-link />
<van-cell title="商品数量">
<template #right-icon>
<div class="quantity-box">
<van-stepper v-model="count" integer min="1" button-size="28" input-width="42" />
<span>库存充足</span>
</div>
</template>
</van-cell>
</section>
<section class="service-card">
<h3>商品服务</h3>
<van-cell title="正品保障" value="品质保证,安心兑换" is-link>
<template #icon><van-icon class="service-icon" name="shield-o" /></template>
</van-cell>
<van-cell title="配送" value="满 99 元免配送费" is-link>
<template #icon><van-icon class="service-icon" name="logistics" /></template>
</van-cell>
</section>
<section class="detail-content">
<h2>商品详情</h2>
<p>{{ product.desc }}</p>
<h3>商品详情</h3>
<div class="feature-grid">
<article v-for="item in features" :key="item.title">
<span><van-icon :name="item.icon" /></span>
<b>{{ item.title }}</b>
<p>{{ item.desc }}</p>
</article>
</div>
</section>
<van-goods-action>
<van-goods-action-icon icon="chat-o" text="客服" dot />
<van-goods-action-icon icon="cart-o" text="购物车" badge="5" @click="$router.push('/joyful-selection/cart')" />
@@ -30,19 +72,268 @@ import { getJoyfulProduct } from '../product-data'
export default {
name: 'JoyfulSelectionProductDetail',
data: () => ({
count: 1,
features: [
{ icon: 'music-o', title: '舒适半入耳设计', desc: '长时间佩戴更舒适' },
{ icon: 'bar-chart-o', title: '高清音质', desc: '清晰稳定的聆听体验' },
{ icon: 'battery-o', title: '长续航', desc: '单次可用约 5 小时' },
{ icon: 'cluster-o', title: '蓝牙 5.0', desc: '稳定连接,低延迟' }
]
}),
computed: {
product() { return getJoyfulProduct(this.$route.query.id) }
}
}
</script>
<style scoped>
.detail-page { min-height: 100vh; padding-bottom: 56px; background: #f7f7f7; }
.product-image { display: block; width: 100%; height: 310px; background: #fff; }
.product-info, .detail-content { margin: 10px 12px; padding: 14px; border-radius: 10px; background: #fff; }
.product-info h1 { margin: 0 0 8px; font-size: 17px; }
.product-info p, .detail-content p { color: #777; font-size: 12px; line-height: 1.6; }
.product-info b { margin-right: 8px; color: #ff5b20; font-size: 21px; }
.product-info del { margin-right: 9px; color: #aaa; font-size: 11px; }
.detail-content h2 { margin: 0; font-size: 15px; }
<style scoped lang="less">
.detail-page {
min-height: 100vh;
padding-bottom: 60px;
background: #f5f5f5;
}
.detail-top {
position: fixed;
z-index: 10;
top: 0;
right: 0;
left: 0;
display: flex;
height: 54px;
align-items: center;
justify-content: space-between;
padding: 0 18px;
color: #111;
background: rgba(247, 248, 250, .88);
backdrop-filter: blur(8px);
}
.detail-top > .van-icon {
font-size: 26px;
}
.detail-top h1 {
margin: 0;
font-size: 18px;
font-weight: 700;
}
.detail-top div {
display: flex;
gap: 20px;
}
.detail-top div .van-icon {
font-size: 24px;
}
.image-section {
position: relative;
padding-top: 54px;
background: #eef0f4;
}
.product-image {
display: block;
width: 100%;
height: 365px;
background: #fff;
}
.image-index {
position: absolute;
right: 18px;
bottom: 26px;
min-width: 44px;
padding: 5px 10px;
border-radius: 16px;
color: #fff;
font-size: 14px;
line-height: 1;
text-align: center;
background: rgba(0, 0, 0, .36);
}
.product-info,
.option-card,
.service-card,
.detail-content {
margin: 10px 12px 0;
border-radius: 12px;
background: #fff;
}
.product-info {
margin-top: -4px;
padding: 18px 16px 16px;
}
.product-info h2 {
margin: 0 0 12px;
color: #111;
font-size: 20px;
font-weight: 700;
line-height: 1.25;
}
.product-info p {
margin: 0;
color: #777;
font-size: 14px;
line-height: 1.7;
}
.price-row {
display: flex;
align-items: center;
gap: 12px;
margin-top: 17px;
}
.price-row b {
color: #ff5b20;
font-size: 24px;
font-weight: 700;
line-height: 1;
}
.price-row del {
color: #999;
font-size: 13px;
}
.point-tag {
color: #ff2f3a;
border-color: #ff2f3a;
font-size: 13px;
}
.option-card {
overflow: hidden;
padding: 4px 16px;
}
.option-card ::v-deep .van-cell {
padding: 15px 0;
}
.option-card ::v-deep .van-cell::after {
right: 0;
left: 0;
}
.option-card ::v-deep .van-cell__title {
color: #222;
font-size: 15px;
font-weight: 700;
}
.option-card ::v-deep .van-cell__value {
color: #888;
font-size: 14px;
}
.quantity-box {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 6px;
}
.quantity-box span {
color: #999;
font-size: 12px;
}
.service-card {
overflow: hidden;
padding: 16px 16px 9px;
}
.service-card h3,
.detail-content h3 {
margin: 0 0 13px;
color: #111;
font-size: 16px;
font-weight: 700;
}
.service-card ::v-deep .van-cell {
padding: 11px 0;
}
.service-card ::v-deep .van-cell::after {
display: none;
}
.service-card ::v-deep .van-cell__title {
color: #222;
font-size: 15px;
}
.service-card ::v-deep .van-cell__value {
color: #888;
font-size: 14px;
}
.service-icon {
margin-right: 10px;
color: #ff5b20;
font-size: 20px;
}
.detail-content {
padding: 16px;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.feature-grid article {
min-width: 0;
text-align: center;
}
.feature-grid span {
display: flex;
width: 48px;
height: 48px;
align-items: center;
justify-content: center;
margin: 0 auto 10px;
border-radius: 50%;
color: #2d558c;
background: #f0f2f6;
}
.feature-grid span .van-icon {
font-size: 25px;
}
.feature-grid b {
display: block;
overflow: hidden;
color: #222;
font-size: 12px;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.feature-grid p {
display: -webkit-box;
overflow: hidden;
margin: 4px 0 0;
color: #888;
font-size: 11px;
line-height: 1.35;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
</style>
@@ -2,33 +2,43 @@
<main class="product-page">
<div class="fixed-top">
<van-nav-bar title="商品列表" left-arrow @click-left="$router.back()" />
<van-tabs v-model="active" color="#ff5b20" title-active-color="#ff5b20" title-inactive-color="#333">
<van-tab v-for="tab in tabs" :key="tab" :title="tab" />
</van-tabs>
<div class="search-row">
<van-search v-model="keyword" shape="round" placeholder="搜索商品名称" />
<button type="button" class="filter-button">
<van-icon name="filter-o" />
<span>筛选</span>
</button>
</div>
</div>
<section class="layout">
<aside>
<span
<div class="layout-aside">
<span
v-for="category in categories"
:key="category"
:class="{ active: category === selected }"
@click="selected = category"
:key="category.id"
:class="{ active: category.id === selectedCategoryId }"
@click="selectedCategoryId = category.id"
>
{{ category }}
</span>
{{ category.name }}
</span>
</div>
</aside>
<div class="goods">
<article v-for="item in visibleGoods" :key="item.id" class="goods-item" @click="goProductDetail(item)">
<van-image class="goods-thumb" :src="item.image" fit="cover" />
<div class="goods-info">
<h3>{{ item.name }}</h3>
<van-tag class="goods-tag" plain type="danger">{{ item.tag }}</van-tag>
<div class="goods-title">
<van-tag class="goods-tag" plain type="danger">{{ item.tag }}</van-tag>
<h3>{{ item.name }}</h3>
</div>
<div class="goods-bottom">
<b>¥{{ item.price }}</b>
<span>已售{{ item.sales }}</span>
<button type="button" class="cart-button" @click.stop="$router.push('/joyful-selection/cart')">
<div class="price-stack">
<div><b>¥{{ item.price }}</b></div>
<div><span>已售{{ item.sales }}</span></div>
</div>
<button type="button" class="cart-button" @click.stop="openSku(item)">
<van-icon name="cart-o" />
</button>
</div>
@@ -37,91 +47,232 @@
</div>
</section>
<van-sku
v-model="showSku"
:sku="sku"
:goods="skuGoods"
:goods-id="currentGoods.id"
:initial-sku="initialSku"
:hide-stock="sku.hide_stock"
buy-text="立即购买"
add-cart-text="加入购物车"
@buy-clicked="onBuyClicked"
@add-cart="onAddCartClicked"
/>
</main>
</template>
<script>
import { Toast } from 'vant'
import { joyfulProducts } from '../product-data'
import { useCartStore } from '@/store/cart'
const productImages = joyfulProducts.map(item => item.image)
const categorySeeds = {
居家生活: [
const categories = [
{
id: 'recommend',
name: '推荐',
products: []
},
{
id: 'homeLiving',
name: '居家生活',
products: [
['cream-oven', '奶油白家用迷你烤箱', '小巧不占地,早餐烘焙好帮手', '129.00', '1.2万件', productImages[1]],
['cotton-bedding', '100%纯棉舒适四件套', '柔软亲肤,安心睡眠', '179.00', '6453件', productImages[1]],
['aroma-gift', '橙意香薰礼盒套装', '淡雅留香,装点居家空间', '59.90', '8321件', productImages[3]],
['desktop-vase', '简约北欧陶瓷花瓶', '清新配色,点亮桌面角落', '39.90', '3180件', productImages[3]]
],
数码家电: [
]
},
{
id: 'digitalAppliance',
name: '数码家电',
products: [
['earbuds-air2', '小米蓝牙耳机 Air2 SE', '轻盈半入耳,通勤运动都好听', '69.90', '1.2万件', productImages[0]],
['smart-speaker-pro', '黑曜智能音箱 Pro', '语音唤醒,沉浸听感', '99.00', '2.1万件', productImages[2]],
['electric-toothbrush', '智能声波电动牙刷', '高频洁净,呵护口腔', '129.00', '2.1万件', productImages[0]],
['dual-charger', '双口快充充电器 30W', '小巧便携,多设备快充', '69.00', '5632件', productImages[2]]
],
美妆个护: [
]
},
{
id: 'beautyCare',
name: '美妆个护',
products: [
['skin-care-box', '植萃保湿护肤礼盒', '水润修护,清爽不粘腻', '99.90', '4820件', productImages[3]],
['aroma-wash', '香氛洗护旅行套装', '温和清洁,清新留香', '59.90', '7932件', productImages[3]],
['makeup-mirror', '便携补光化妆镜', '高清镜面,随时补妆', '49.90', '3651件', productImages[2]],
['hand-cream', '滋润护手霜三支装', '细腻滋养,轻柔香气', '39.90', '5280件', productImages[3]]
],
食品饮料: [
]
},
{
id: 'foodDrink',
name: '食品饮料',
products: [
['orange-gift', '橙意惊喜零食礼盒', '精致包装,送礼更有心意', '59.90', '6324件', productImages[3]],
['coffee-cup', '咖啡随行杯组合', '锁温防漏,醇香随行', '49.90', '3520件', productImages[0]],
['tea-snack', '下午茶点心礼盒', '多种风味,一盒满足', '69.90', '4891件', productImages[3]],
['fruit-drink', '轻享果饮礼盒', '清爽酸甜,聚会常备', '79.90', '2976件', productImages[3]]
],
母婴玩具: [
]
},
{
id: 'motherBaby',
name: '母婴玩具',
products: [
['baby-bottle', '安心防摔儿童水杯', '轻巧易握,出门好携带', '39.90', '4250件', productImages[0]],
['toy-blocks', '益智拼搭积木套装', '色彩柔和,陪伴成长', '89.00', '2186件', productImages[3]],
['kids-night-light', '柔光儿童陪伴夜灯', '温和光线,安心入睡', '69.90', '3548件', productImages[2]],
['baby-towel', '纯棉柔软浴巾两件装', '亲肤吸水,日常必备', '59.90', '2761件', productImages[1]]
],
服饰鞋包: [
]
},
{
id: 'fashionBag',
name: '服饰鞋包',
products: [
['laptop-backpack', '大容量双肩电脑背包', '分区收纳,通勤出行轻松装', '149.00', '1.6万件', productImages[3]],
['mini-bag', '轻巧通勤斜挎小包', '简洁百搭,日常实用', '79.90', '5321件', productImages[3]],
['silk-scarf', '柔软撞色丝巾礼盒', '细腻亲肤,点亮穿搭', '69.90', '2680件', productImages[3]],
['canvas-tote', '简约帆布托特包', '大容量开口,随手能装', '49.90', '6412件', productImages[3]]
],
运动户外: [
]
},
{
id: 'sportsOutdoor',
name: '运动户外',
products: [
['sport-bottle', '运动便携保温水杯', '长效锁温,户外随行', '49.90', '7342件', productImages[0]],
['neck-massager', '筋膜放松按摩仪 Mini', '轻巧便携,舒缓酸胀', '99.00', '3921件', productImages[2]],
['camping-light', '户外便携露营灯', '多档照明,夜晚更安心', '79.00', '2568件', productImages[2]],
['fitness-band', '弹力健身拉力带套装', '居家训练,轻松塑形', '29.90', '4396件', productImages[1]]
]
}
]
}
]
const goods = Object.entries(categorySeeds).flatMap(([category, items]) =>
items.map(([id, name, desc, price, sales, image]) => ({
id,
name,
desc,
price,
sales,
image,
category,
tag: '赠品'
}))
const productCategories = categories.filter(category => category.id !== 'recommend')
const goods = productCategories.flatMap(category =>
category.products.map(([id, name, desc, price, sales, image]) => ({
id,
name,
desc,
price,
sales,
image,
categoryId: category.id,
tag: '赠品'
}))
)
const colorOptions = [
{ id: 'cream', name: '奶油白' },
{ id: 'blue', name: '雾霾蓝' },
{ id: 'orange', name: '暖橙色' }
]
const sizeOptions = [
{ id: 'standard', name: '标准款' },
{ id: 'plus', name: '升级款' }
]
const defaultGoods = goods[0]
export default {
name: 'JoyfulSelectionProductList',
data: () => ({
active: 0,
selected: '推荐',
tabs: ['综合', '销量', '价格↕', '筛选⌄'],
categories: ['推荐', '居家生活', '数码家电', '美妆个护', '食品饮料', '母婴玩具', '服饰鞋包', '运动户外'],
selectedCategoryId: 'recommend',
showSku: false,
keyword: '',
currentGoods: defaultGoods,
categories,
goods
}),
computed: {
visibleGoods() {
return this.selected === '推荐'
? this.goods.slice(0, 10)
: this.goods.filter(item => item.category === this.selected)
const categoryGoods = this.selectedCategoryId === 'recommend'
? this.goods.slice(0, 10)
: this.goods.filter(item => item.categoryId === this.selectedCategoryId)
return this.keyword
? categoryGoods.filter(item => item.name.includes(this.keyword))
: categoryGoods
},
skuGoods() {
return {
title: this.currentGoods.name,
picture: this.currentGoods.image
}
},
initialSku() {
return {
s1: colorOptions[0].id,
s2: sizeOptions[0].id
}
},
sku() {
const price = Math.round(Number(this.currentGoods.price) * 100)
return {
tree: [
{
k: '颜色',
k_s: 's1',
v: colorOptions
},
{
k: '尺寸',
k_s: 's2',
v: sizeOptions
}
],
list: colorOptions.flatMap(color =>
sizeOptions.map(size => ({
id: `${this.currentGoods.id}-${color.id}-${size.id}`,
s1: color.id,
s2: size.id,
price,
stock_num: 99
}))
),
price: this.currentGoods.price,
stock_num: 99,
none_sku: false,
hide_stock: true
}
}
},
methods: {
goProductDetail(item) {
this.$router.push({ path: '/joyful-selection/product-detail', query: { id: item.id } })
},
openSku(item) {
this.currentGoods = item
this.showSku = true
},
onBuyClicked() {
Toast('请选择规格后购买')
},
onAddCartClicked(data) {
const selectedSku = data.selectedSkuComb || {}
const selectedColor = colorOptions.find(item => item.id === selectedSku.s1) || colorOptions[0]
const selectedSize = sizeOptions.find(item => item.id === selectedSku.s2) || sizeOptions[0]
const count = data.selectedNum || 1
const cartStore = useCartStore()
cartStore.addItem({
cartId: selectedSku.id || `${this.currentGoods.id}-${selectedColor.id}-${selectedSize.id}`,
id: this.currentGoods.id,
name: this.currentGoods.name,
desc: `${selectedColor.name} / ${selectedSize.name}`,
price: this.currentGoods.price,
image: this.currentGoods.image,
tag: this.currentGoods.tag,
count,
sku: {
color: selectedColor.name,
size: selectedSize.name
}
})
Toast('已加入购物车')
this.showSku = false
}
}
}
@@ -130,8 +281,8 @@ export default {
<style scoped lang="less">
.product-page {
min-height: 100vh;
padding: 90px 0 56px;
background: #fff;
padding: 110px 0 58px;
background: #f8f8f8;
}
.fixed-top {
@@ -144,34 +295,68 @@ export default {
}
.fixed-top ::v-deep .van-nav-bar {
height: 46px;
height: 50px;
box-shadow: none;
}
.fixed-top ::v-deep .van-nav-bar__title {
font-size: 15px;
color: #111;
font-size: 18px;
font-weight: 700;
}
.fixed-top ::v-deep .van-nav-bar .van-icon {
color: #111;
font-size: 23px;
}
.search-row {
display: flex;
align-items: center;
gap: 11px;
padding: 12px 18px 10px;
}
.search-row ::v-deep .van-search {
flex: 1;
padding: 0;
background: transparent;
}
.search-row ::v-deep .van-search__content {
height: 38px;
align-items: center;
padding-left: 14px;
border-radius: 22px;
background: #f5f6f8;
}
.search-row ::v-deep .van-field__left-icon {
margin-right: 7px;
color: #646971;
font-size: 18px;
}
.search-row ::v-deep .van-field__control {
color: #222;
font-size: 14px;
}
.filter-button {
display: flex;
height: 38px;
flex: none;
align-items: center;
gap: 5px;
border: 0;
color: #111;
font-size: 14px;
font-weight: 600;
background: transparent;
}
.fixed-top ::v-deep .van-tabs__wrap {
height: 44px;
}
.fixed-top ::v-deep .van-tab {
color: #333;
font-size: 12px;
}
.fixed-top ::v-deep .van-tab--active {
color: #ff5b20;
font-weight: 600;
}
.fixed-top ::v-deep .van-tabs__line {
width: 18px;
height: 2px;
border-radius: 2px;
background: #ff5b20;
.filter-button .van-icon {
font-size: 18px;
}
.layout {
@@ -181,30 +366,38 @@ export default {
.layout aside {
position: sticky;
top: 90px;
width: 78px;
height: calc(100vh - 146px);
top: 110px;
width: 90px;
height: calc(100vh - 168px);
padding: 10px 10px 0 0;
flex: none;
overflow-y: auto;
background: #f8f8f8;
background: #ffffff;
}
.layout-aside {
background-color: #f5f6f8;
border-top-right-radius: 15px;
overflow: hidden;
}
.layout aside span {
display: flex;
min-height: 48px;
min-height: 50px;
align-items: center;
justify-content: center;
padding: 0 8px;
color: #777;
font-size: 12px;
line-height: 1.2;
padding: 0 10px;
color: #252525;
font-size: 14px;
line-height: 1.25;
text-align: center;
}
.layout aside .active {
position: relative;
background: #fff;
color: #222;
background: #ededed;;
color: #111;
font-size: 15px;
font-weight: 700;
}
@@ -212,9 +405,9 @@ export default {
position: absolute;
top: 50%;
left: 0;
width: 3px;
height: 18px;
border-radius: 0 3px 3px 0;
width: 4px;
height: 28px;
border-radius: 0 4px 4px 0;
background: #ff5b20;
content: '';
transform: translateY(-50%);
@@ -223,19 +416,22 @@ export default {
.goods {
flex: 1;
min-width: 0;
padding: 8px 10px 12px;
padding: 10px 10px 14px 12px;
}
.goods-item {
display: flex;
min-height: 96px;
gap: 10px;
padding: 0 0 13px;
min-height: 116px;
align-items: center;
gap: 12px;
padding: 10px 12px 10px 10px;
border-radius: 14px;
background: #fff;
box-shadow: 0 4px 18px rgba(0, 0, 0, .045);
}
.goods-item + .goods-item {
margin-top: 13px;
margin-top: 10px;
}
.goods-thumb {
@@ -243,7 +439,7 @@ export default {
height: 88px;
flex: none;
overflow: hidden;
border-radius: 4px;
border-radius: 6px;
background: #f6f6f6;
}
@@ -252,68 +448,97 @@ export default {
min-width: 0;
flex: 1;
flex-direction: column;
padding-top: 2px;
align-self: stretch;
justify-content: space-between;
padding: 4px 0;
}
.goods-info h3 {
.goods-title {
display: flex;
min-width: 0;
align-items: flex-start;
gap: 5px;
}
.goods-title h3 {
display: -webkit-box;
overflow: hidden;
min-height: 34px;
margin: 0 0 5px;
color: #222;
font-size: 13px;
flex: 1;
margin: 0;
color: #111;
font-size: 15px;
font-weight: 700;
line-height: 17px;
line-height: 20px;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.goods-tag {
flex: none;
width: fit-content;
transform: scale(.86);
transform-origin: left center;
margin-top: 1px;
color: #ff4b4b;
border-color: #ff4b4b;
font-size: 11px;
line-height: 16px;
}
.goods-bottom {
display: flex;
align-items: center;
margin-top: auto;
}
.goods-bottom b {
.price-stack {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.price-stack b {
color: #ff5b20;
font-size: 14px;
font-size: 18px;
font-weight: 700;
}
.goods-bottom span {
.price-stack span {
min-width: 0;
flex: 1;
overflow: hidden;
margin-left: 9px;
color: #aaa;
font-size: 10px;
color: #9b9da3;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.cart-button {
display: flex;
width: 26px;
height: 26px;
width: 36px;
height: 36px;
flex: none;
align-items: center;
justify-content: center;
border: 1px solid #ff7a3c;
border: 0;
border-radius: 50%;
color: #ff5b20;
background: #fff;
box-shadow: 0 2px 6px rgba(255, 91, 32, .12);
color: #fff;
background: #ff5b20;
box-shadow: 0 4px 10px rgba(255, 91, 32, .22);
}
.cart-button .van-icon {
font-size: 17px;
font-size: 20px;
}
.product-page ::v-deep .van-sku-actions .van-button--danger {
background: #ff5b20;
border-color: #ff5b20;
}
.product-page ::v-deep .van-sku-row__item--active {
color: #ff5b20;
border-color: #ff5b20;
}
</style>