feat: 商品提交列表

This commit is contained in:
2026-07-28 10:08:15 +08:00
parent 45489cd0c4
commit 78bca3b3d5
24 changed files with 1849 additions and 234 deletions
+6
View File
@@ -0,0 +1,6 @@
{
"name": "points-mall",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+7
View File
@@ -8,6 +8,7 @@
"name": "supplied-h5",
"version": "1.0.0",
"dependencies": {
"@vant/area-data": "^1.5.2",
"core-js": "^3.37.1",
"pinia": "^2.3.1",
"vant": "^2.13.9",
@@ -1851,6 +1852,12 @@
"@types/node": "*"
}
},
"node_modules/@vant/area-data": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vant/area-data/-/area-data-1.5.2.tgz",
"integrity": "sha512-Gtxgt6Rjgopt6234ANpO0bBsSwtjZ23lBlVDHIy8Mi2NJqyoj1vgVWY0dri8/2LCZAWzQ6EnwRrUVViUZ0cvMA==",
"license": "MIT"
},
"node_modules/@vant/icons": {
"version": "3.0.2",
"license": "MIT"
+1
View File
@@ -7,6 +7,7 @@
"build": "vue-cli-service build"
},
"dependencies": {
"@vant/area-data": "^1.5.2",
"core-js": "^3.37.1",
"pinia": "^2.3.1",
"vant": "^2.13.9",
+4
View File
@@ -11,6 +11,8 @@ import JoyfulSelectionCheckout from '@/view/joyful-selection/cart/checkout/index
import JoyfulSelectionPayment from '@/view/joyful-selection/cart/payment/index.vue'
import JoyfulSelectionPaymentConfirm from '@/view/joyful-selection/cart/payment-confirm/index.vue'
import JoyfulSelectionPaymentSuccess from '@/view/joyful-selection/cart/payment-success/index.vue'
import JoyfulSelectionAddressList from '@/view/joyful-selection/address/index.vue'
import JoyfulSelectionAddressEdit from '@/view/joyful-selection/address/edit/index.vue'
import JoyfulSelectionOrders from '@/view/joyful-selection/profile/orders/index.vue'
import JoyfulSelectionProfile from '@/view/joyful-selection/profile/index.vue'
import JoyfulSelectionPoints from '@/view/joyful-selection/profile/points/index.vue'
@@ -38,6 +40,8 @@ export default new Router({
{ path: 'payment', component: JoyfulSelectionPayment },
{ path: 'payment-confirm', component: JoyfulSelectionPaymentConfirm },
{ path: 'payment-success', component: JoyfulSelectionPaymentSuccess },
{ path: 'address', component: JoyfulSelectionAddressList },
{ path: 'address/edit', component: JoyfulSelectionAddressEdit },
{ path: 'orders', component: JoyfulSelectionOrders },
{ path: 'profile', component: JoyfulSelectionProfile },
{ path: 'points', component: JoyfulSelectionPoints }
+8
View File
@@ -45,6 +45,14 @@ export const useCartStore = defineStore('joyfulSelectionCart', {
this.items.unshift(item)
}
this.persistCart()
},
removeItems(cartIds) {
if (!this.loaded) {
this.loadCart()
}
this.items = this.items.filter(item => !cartIds.includes(item.cartId))
this.persistCart()
}
}
@@ -0,0 +1,48 @@
const ADDRESS_KEY = 'joyful-selection-addresses'
const CHOSEN_ADDRESS_KEY = 'joyful-selection-chosen-address-id'
const defaultAddresses = [
{
id: 'addr-1',
name: '张三',
tel: '13888888888',
province: '浙江省',
city: '杭州市',
county: '余杭区',
addressDetail: '文一西路 888 号未来科技城海创园5幢1203室',
areaCode: '330110',
postalCode: '310000',
isDefault: true
}
]
export const readAddresses = () => {
try {
const addresses = JSON.parse(localStorage.getItem(ADDRESS_KEY) || '[]')
return addresses.length ? addresses : defaultAddresses
} catch (error) {
return defaultAddresses
}
}
export const saveAddresses = addresses => {
localStorage.setItem(ADDRESS_KEY, JSON.stringify(addresses))
}
export const readChosenAddressId = () => {
return localStorage.getItem(CHOSEN_ADDRESS_KEY) || readAddresses().find(item => item.isDefault)?.id || readAddresses()[0]?.id || ''
}
export const saveChosenAddressId = id => {
localStorage.setItem(CHOSEN_ADDRESS_KEY, id)
}
export const formatAddress = address => {
if (!address) {
return ''
}
return `${address.province}${address.city}${address.county}${address.addressDetail}`
}
export const createAddressId = () => `addr-${Date.now()}`
@@ -0,0 +1,132 @@
<template>
<main class="address-edit-page">
<van-nav-bar :title="pageTitle" left-arrow @click-left="$router.back()" />
<van-address-edit
:area-list="areaList"
:address-info="addressInfo"
show-postal
show-delete
show-set-default
show-search-result
:search-result="searchResult"
:area-columns-placeholder="['请选择', '请选择', '请选择']"
@save="onSave"
@delete="onDelete"
@change-detail="onChangeDetail"
/>
</main>
</template>
<script>
import { Toast } from 'vant'
import { areaList } from '@vant/area-data'
import { createAddressId, readAddresses, saveAddresses, saveChosenAddressId } from '../address-data'
export default {
name: 'JoyfulSelectionAddressEdit',
data: () => ({
areaList,
searchResult: [],
addressInfo: {}
}),
computed: {
editId() {
return this.$route.query.id
},
pageTitle() {
return this.editId ? '编辑地址' : '新增地址'
}
},
created() {
const address = readAddresses().find(item => item.id === this.editId)
if (address) {
this.addressInfo = { ...address }
}
},
methods: {
onSave(content) {
const addresses = readAddresses()
const nextAddress = {
...content,
id: this.editId || content.id || createAddressId()
}
const nextAddresses = addresses.filter(item => item.id !== nextAddress.id)
if (nextAddress.isDefault) {
nextAddresses.forEach(item => { item.isDefault = false })
}
if (!nextAddresses.length && !nextAddress.isDefault) {
nextAddress.isDefault = true
}
saveAddresses([nextAddress, ...nextAddresses])
saveChosenAddressId(nextAddress.id)
Toast('保存成功')
this.$router.back()
},
onDelete() {
if (!this.editId) {
this.$router.back()
return
}
const addresses = readAddresses().filter(item => item.id !== this.editId)
if (addresses.length && !addresses.some(item => item.isDefault)) {
addresses[0].isDefault = true
}
saveAddresses(addresses)
saveChosenAddressId(addresses[0]?.id || '')
Toast('删除成功')
this.$router.back()
},
onChangeDetail(value) {
this.searchResult = value
? [
{
name: '未来科技城',
address: '浙江省杭州市余杭区文一西路'
},
{
name: '海创园',
address: '浙江省杭州市余杭区文一西路998号'
}
]
: []
}
}
}
</script>
<style scoped lang="less">
.address-edit-page {
min-height: 100vh;
padding-top: 46px;
background: #f7f7f7;
}
.address-edit-page ::v-deep .van-nav-bar {
position: fixed;
z-index: 20;
top: 0;
right: 0;
left: 0;
box-shadow: 0 1px 0 rgba(0, 0, 0, .04);
}
.address-edit-page ::v-deep .van-address-edit {
padding: 12px;
}
.address-edit-page ::v-deep .van-button--danger {
background: #ff5b20;
border-color: #ff5b20;
}
.address-edit-page ::v-deep .van-switch--on {
background: #ff5b20;
}
</style>
@@ -0,0 +1,94 @@
<template>
<main class="address-page">
<van-nav-bar title="地址管理" left-arrow @click-left="$router.back()" />
<van-address-list
v-model="chosenAddressId"
:list="addressList"
:disabled-list="disabledList"
disabled-text="以下地址超出配送范围"
default-tag-text="默认"
@add="onAdd"
@edit="onEdit"
@select="onSelect"
/>
</main>
</template>
<script>
import { formatAddress, readAddresses, readChosenAddressId, saveChosenAddressId } from './address-data'
export default {
name: 'JoyfulSelectionAddressList',
data: () => ({
chosenAddressId: '',
addresses: [],
disabledList: []
}),
computed: {
addressList() {
return this.addresses.map(item => ({
id: item.id,
name: item.name,
tel: item.tel,
address: formatAddress(item),
isDefault: item.isDefault
}))
}
},
created() {
this.addresses = readAddresses()
this.chosenAddressId = readChosenAddressId()
},
methods: {
onSelect(item) {
this.chosenAddressId = item.id
saveChosenAddressId(item.id)
if (this.$route.query.from === 'checkout') {
this.$router.back()
}
},
onAdd() {
this.$router.push('/joyful-selection/address/edit')
},
onEdit(item) {
this.$router.push({ path: '/joyful-selection/address/edit', query: { id: item.id } })
}
}
}
</script>
<style scoped lang="less">
.address-page {
min-height: 100vh;
padding-top: 46px;
background: #f7f7f7;
}
.address-page ::v-deep .van-nav-bar {
position: fixed;
z-index: 20;
top: 0;
right: 0;
left: 0;
box-shadow: 0 1px 0 rgba(0, 0, 0, .04);
}
.address-page ::v-deep .van-address-list {
padding: 12px 12px 86px;
}
.address-page ::v-deep .van-address-item {
border-radius: 8px;
}
.address-page ::v-deep .van-address-item .van-radio__icon--checked .van-icon,
.address-page ::v-deep .van-address-list__add {
border-color: #ff5b20;
background: #ff5b20;
}
.address-page ::v-deep .van-tag--danger {
background: #ff5b20;
}
</style>
@@ -1,50 +1,393 @@
<template>
<main class="checkout-page">
<van-nav-bar title="确认订单" left-arrow @click-left="$router.back()" />
<van-cell-group inset>
<van-cell title="收货人:张三 138****8888" label="浙江省杭州市余杭区文一西路 888 号 1203 室" is-link />
</van-cell-group>
<van-cell-group inset title="配送方式">
<van-cell title="预计送达:5月20日(周二)" value="快速配送 免邮" />
</van-cell-group>
<van-cell-group inset>
<van-card
v-for="item in goods"
:key="item.id"
:title="item.name"
:desc="item.desc"
:price="item.price"
:thumb="item.image"
num="1"
/>
</van-cell-group>
<van-field v-model="remark" label="订单备注" placeholder="选填,给商家留言" />
<section class="amount">
<p>商品金额 <b>¥297.90</b></p>
<p>运费 <b>¥0.00</b></p>
<p>优惠券 <b>暂无优惠券</b></p>
<section class="address-card" @click="$router.push({ path: '/joyful-selection/address', query: { from: 'checkout' } })">
<van-icon class="address-icon" name="location" />
<div class="address-info">
<h3>收货人:{{ chosenAddress.name }} <span>{{ maskTel(chosenAddress.tel) }}</span></h3>
<p>{{ chosenAddress.province }}{{ chosenAddress.city }}{{ chosenAddress.county }}</p>
<p>{{ chosenAddress.addressDetail }}</p>
</div>
<van-icon class="arrow-icon" name="arrow" />
</section>
<van-submit-bar :price="29790" button-text="提交订单" @submit="$router.push('/joyful-selection/payment')" />
<section class="delivery-card">
<div class="delivery-row">
<span>配送方式</span>
<b>快速配送&nbsp;&nbsp;免邮</b>
</div>
<div class="delivery-row delivery-date">
<span>预计送达:</span>
<em>5月20日(周二)</em>
</div>
</section>
<section class="goods-card">
<article v-for="item in visibleGoods" :key="item.cartId || item.id" class="order-item">
<van-image class="order-thumb" :src="item.image" fit="cover" />
<div class="order-info">
<h3>{{ item.name }}</h3>
<p>¥{{ item.price }}</p>
</div>
<span class="order-count">x{{ item.count || 1 }}</span>
</article>
<button v-if="showLoadMore" type="button" class="load-more" @click="showAllGoods = true">
加载更多
<van-icon name="arrow-down" />
</button>
</section>
<section class="remark-card">
<span>订单备注</span>
<input v-model="remark" type="text" placeholder="选填,给商家留言" />
</section>
<section class="amount-card">
<p><span>商品金额</span><b>¥{{ formatYuan(goodsAmount) }}</b></p>
<p><span>运费</span><b>¥0.00</b></p>
<p><span>优惠券</span><b class="muted">选择优惠券</b></p>
</section>
<van-submit-bar :price="goodsAmount" button-text="提交订单" @submit="onSubmit" />
</main>
</template>
<script>
import { joyfulProducts } from '../../product-data'
import { mapActions, mapState } from 'pinia'
import { useCartStore } from '@/store/cart'
import { readAddresses, readChosenAddressId } from '../../address/address-data'
import { createOrderNo, savePaymentInfo } from '../payment/payment-data'
const CHECKOUT_ITEMS_KEY = 'joyful-selection-checkout-items'
export default {
name: 'JoyfulSelectionCheckout',
data: () => ({
remark: '',
goods: joyfulProducts.slice(0, 3)
})
goods: [],
showAllGoods: false,
chosenAddress: {}
}),
computed: {
...mapState(useCartStore, ['items']),
visibleGoods() {
return this.showAllGoods ? this.goods : this.goods.slice(0, 3)
},
showLoadMore() {
return !this.showAllGoods && this.goods.length > 3
},
goodsAmount() {
return this.goods.reduce((total, item) => total + Math.round(Number(item.price) * 100) * Number(item.count || 1), 0)
}
},
created() {
this.loadCart()
this.goods = this.getCheckoutGoods()
this.loadAddress()
},
activated() {
this.loadAddress()
},
methods: {
...mapActions(useCartStore, ['loadCart']),
getCheckoutGoods() {
const storedItems = this.readCheckoutItems()
if (storedItems.length) {
return storedItems
}
if (this.$route.query.source === 'cart') {
return this.items
}
return []
},
readCheckoutItems() {
try {
return JSON.parse(sessionStorage.getItem(CHECKOUT_ITEMS_KEY) || '[]')
} catch (error) {
return []
}
},
formatYuan(price) {
return (Number(price || 0) / 100).toFixed(2)
},
loadAddress() {
const addresses = readAddresses()
const chosenId = readChosenAddressId()
this.chosenAddress = addresses.find(item => item.id === chosenId) || addresses[0] || {}
},
maskTel(tel = '') {
return tel.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2')
},
onSubmit() {
savePaymentInfo({
orderNo: createOrderNo(),
amount: this.goodsAmount,
receiverName: this.chosenAddress.name || '',
receiverTel: this.maskTel(this.chosenAddress.tel || ''),
goods: this.goods
})
this.$router.push('/joyful-selection/payment')
}
}
}
</script>
<style scoped>
.checkout-page { min-height: 100vh; padding-bottom: 60px; background: #f7f7f7; }
.van-cell-group { margin: 10px 12px; }
.amount { margin: 10px 12px; padding: 10px 16px; border-radius: 10px; background: #fff; font-size: 13px; }
.amount p { display: flex; justify-content: space-between; }
.amount b { font-weight: normal; }
.van-submit-bar { bottom: 0; }
<style scoped lang="less">
.checkout-page {
min-height: 100vh;
padding: 46px 0 62px;
background: #f7f7f7;
}
.checkout-page ::v-deep .van-nav-bar {
position: fixed;
z-index: 20;
top: 0;
right: 0;
left: 0;
background: #fff;
box-shadow: 0 1px 0 rgba(0, 0, 0, .04);
}
.address-card,
.delivery-card,
.goods-card,
.remark-card,
.amount-card {
margin: 10px 12px 0;
border-radius: 8px;
background: #fff;
}
.address-card {
display: flex;
align-items: center;
padding: 14px 12px;
}
.address-icon {
flex: none;
margin-right: 10px;
color: #ff5b20;
font-size: 20px;
}
.address-info {
min-width: 0;
flex: 1;
}
.address-info h3 {
margin: 0 0 5px;
color: #222;
font-size: 14px;
font-weight: 700;
}
.address-info h3 span {
margin-left: 12px;
font-weight: 700;
}
.address-info p {
overflow: hidden;
margin: 2px 0 0;
color: #555;
font-size: 12px;
line-height: 17px;
text-overflow: ellipsis;
white-space: nowrap;
}
.arrow-icon {
flex: none;
color: #999;
font-size: 16px;
}
.delivery-card {
padding: 13px 14px;
}
.delivery-row {
display: flex;
align-items: center;
justify-content: space-between;
color: #333;
font-size: 13px;
line-height: 22px;
}
.delivery-row b {
color: #333;
font-weight: 700;
}
.delivery-date {
justify-content: flex-start;
color: #999;
}
.delivery-date em {
margin-left: 8px;
color: #999;
font-style: normal;
}
.goods-card {
padding: 2px 14px;
}
.order-item {
display: flex;
align-items: center;
min-height: 86px;
padding: 12px 0;
}
.order-item + .order-item {
border-top: 1px solid #f4f4f4;
}
.load-more {
display: flex;
width: 100%;
height: 44px;
align-items: center;
justify-content: center;
gap: 4px;
border: 0;
border-top: 1px solid #f4f4f4;
color: #ff5b20;
font-size: 13px;
background: #fff;
}
.load-more .van-icon {
font-size: 14px;
}
.order-thumb {
width: 64px;
height: 64px;
flex: none;
overflow: hidden;
border-radius: 4px;
background: #f7f7f7;
}
.order-info {
min-width: 0;
flex: 1;
padding: 0 10px;
}
.order-info h3 {
display: -webkit-box;
overflow: hidden;
margin: 0 0 8px;
color: #222;
font-size: 13px;
font-weight: 700;
line-height: 18px;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.order-info p {
margin: 0;
color: #333;
font-size: 13px;
font-weight: 700;
}
.order-count {
flex: none;
color: #222;
font-size: 12px;
}
.remark-card {
display: flex;
align-items: center;
height: 46px;
padding: 0 14px;
}
.remark-card span {
flex: none;
color: #333;
font-size: 13px;
}
.remark-card input {
min-width: 0;
flex: 1;
margin-left: 16px;
border: 0;
outline: none;
color: #333;
font-size: 13px;
text-align: right;
background: transparent;
}
.remark-card input::placeholder {
color: #999;
}
.amount-card {
padding: 12px 14px;
}
.amount-card p {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0;
color: #777;
font-size: 13px;
line-height: 28px;
}
.amount-card b {
color: #333;
font-weight: 700;
}
.amount-card .muted {
color: #777;
font-weight: 400;
}
.checkout-page ::v-deep .van-submit-bar {
bottom: 0;
height: 58px;
border-top: 1px solid #eee;
}
.checkout-page ::v-deep .van-submit-bar__bar {
height: 58px;
padding: 0 16px;
}
.checkout-page ::v-deep .van-submit-bar__price {
color: #ff5b20;
font-size: 17px;
font-weight: 700;
}
.checkout-page ::v-deep .van-submit-bar__button {
width: 112px;
height: 40px;
border-radius: 22px;
background: #ff5b20;
border-color: #ff5b20;
font-weight: 700;
}
</style>
@@ -0,0 +1,324 @@
<template>
<section class="cart-panel" :class="{ 'cart-panel--page': pageMode }">
<van-notice-bar v-if="showNotice" text="满 199 元包邮,快去凑单吧~" left-icon="info-o" />
<section class="cart-list">
<van-empty v-if="!items.length" description="购物车还是空的" />
<template v-else>
<article
v-for="item in items"
:key="item.cartId"
class="cart-item"
>
<van-checkbox
:value="isItemChecked(item)"
class="item-check"
checked-color="#ff5b20"
@input="toggleItem(item, $event)"
/>
<van-image class="item-thumb" :src="item.image" fit="cover" />
<div class="item-info">
<h3>{{ item.name }}</h3>
<p class="item-desc">{{ item.desc }}</p>
<van-tag class="item-tag" plain type="danger">{{ item.tag }}</van-tag>
<div class="item-bottom">
<b>¥{{ item.price }}</b>
<van-stepper
v-model="item.count"
class="quantity-stepper"
integer
min="0"
button-size="30"
input-width="36"
@change="onCountChange(item, $event)"
/>
</div>
</div>
</article>
</template>
</section>
<van-submit-bar
:fixed="fixedSubmit"
:price="selectedTotalPrice"
:button-text="submitText"
@submit="$emit('checkout', selectedItems)"
>
<van-checkbox
:value="isAllChecked"
:indeterminate="isPartChecked"
checked-color="#ff5b20"
@input="toggleAll"
>全选</van-checkbox>
</van-submit-bar>
</section>
</template>
<script>
import { mapActions, mapState } from 'pinia'
import { useCartStore } from '@/store/cart'
export default {
name: 'JoyfulCartPanel',
props: {
fixedSubmit: {
type: Boolean,
default: false
},
pageMode: {
type: Boolean,
default: false
},
showNotice: {
type: Boolean,
default: true
}
},
data: () => ({
selectedIds: []
}),
computed: {
...mapState(useCartStore, ['items']),
cartIds() {
return this.items.map(item => item.cartId)
},
selectedItems() {
return this.items.filter(item => this.selectedIds.includes(item.cartId))
},
selectedTotalCount() {
return this.selectedItems.reduce((total, item) => total + Number(item.count || 0), 0)
},
selectedTotalPrice() {
return this.selectedItems.reduce((total, item) => total + Math.round(Number(item.price) * 100) * Number(item.count || 0), 0)
},
isAllChecked() {
return this.items.length > 0 && this.selectedIds.length === this.items.length
},
isPartChecked() {
return this.selectedIds.length > 0 && this.selectedIds.length < this.items.length
},
submitText() {
return `去结算(${this.selectedTotalCount})`
}
},
watch: {
cartIds(newIds, oldIds) {
const wasAllChecked = oldIds.length > 0 && oldIds.every(cartId => this.selectedIds.includes(cartId))
const validSelectedIds = this.selectedIds.filter(cartId => newIds.includes(cartId))
this.selectedIds = wasAllChecked ? newIds : validSelectedIds
}
},
created() {
this.loadCart()
this.selectAllItems()
},
methods: {
...mapActions(useCartStore, ['loadCart', 'persistCart', 'removeItems']),
selectAllItems() {
this.selectedIds = this.items.map(item => item.cartId)
},
isItemChecked(item) {
return this.selectedIds.includes(item.cartId)
},
toggleItem(item, checked) {
if (checked && !this.isItemChecked(item)) {
this.selectedIds = [...this.selectedIds, item.cartId]
return
}
if (!checked) {
this.selectedIds = this.selectedIds.filter(cartId => cartId !== item.cartId)
}
},
toggleAll(checked) {
this.selectedIds = checked ? this.items.map(item => item.cartId) : []
},
onCountChange(item, count) {
if (Number(count) < 1) {
this.removeItems([item.cartId])
return
}
this.persistCart()
}
}
}
</script>
<style scoped lang="less">
.cart-panel {
display: flex;
min-height: 0;
flex-direction: column;
background: #fff;
}
.cart-panel--page {
min-height: calc(100vh - 46px);
padding-bottom: 108px;
}
.cart-panel ::v-deep .van-notice-bar {
height: 34px;
flex: none;
color: #ff7a38;
background: #fff3ea;
font-size: 13px;
}
.cart-list {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 0 16px 12px;
background: #fff;
}
.cart-item {
display: flex;
min-height: 128px;
align-items: center;
padding: 16px 0;
background: #fff;
}
.cart-item + .cart-item {
border-top: 1px solid #f5f5f5;
}
.item-check {
flex: none;
margin-right: 12px;
}
.cart-panel ::v-deep .van-checkbox__icon--checked .van-icon {
border-color: #ff5b20;
background-color: #ff5b20;
}
.item-thumb {
width: 94px;
height: 94px;
flex: none;
overflow: hidden;
border-radius: 5px;
background: #f7f7f7;
}
.item-info {
display: flex;
min-width: 0;
min-height: 94px;
flex: 1;
flex-direction: column;
padding-left: 12px;
}
.item-info h3 {
display: -webkit-box;
overflow: hidden;
margin: 0 0 4px;
color: #222;
font-size: 15px;
font-weight: 700;
line-height: 20px;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.item-desc {
overflow: hidden;
margin: 0 0 6px;
color: #999;
font-size: 12px;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-tag {
width: fit-content;
color: #ff5b20;
border-color: #ff9b73;
font-size: 11px;
line-height: 14px;
}
.item-bottom {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: auto;
}
.item-bottom b {
color: #ff5b20;
font-size: 16px;
font-weight: 700;
}
.quantity-stepper {
flex: none;
}
.cart-panel ::v-deep .quantity-stepper .van-stepper__minus,
.cart-panel ::v-deep .quantity-stepper .van-stepper__plus,
.cart-panel ::v-deep .quantity-stepper .van-stepper__input {
height: 30px;
margin: 0;
border: 1px solid #edf0f4;
background: #fff;
color: #222;
}
.cart-panel ::v-deep .quantity-stepper .van-stepper__minus {
width: 32px;
border-radius: 6px 0 0 6px;
color: #9aa0a6;
}
.cart-panel ::v-deep .quantity-stepper .van-stepper__input {
border-right: 0;
border-left: 0;
font-size: 14px;
font-weight: 700;
}
.cart-panel ::v-deep .quantity-stepper .van-stepper__plus {
width: 32px;
border-radius: 0 6px 6px 0;
}
.cart-panel ::v-deep .van-submit-bar {
position: relative;
height: 58px;
flex: none;
border-top: 1px solid #f0f0f0;
background: #fff;
}
.cart-panel--page ::v-deep .van-submit-bar {
position: fixed;
bottom: 50px;
}
.cart-panel ::v-deep .van-submit-bar__bar {
height: 58px;
padding: 0 18px;
}
.cart-panel ::v-deep .van-submit-bar__price {
color: #ff5b20;
font-size: 17px;
font-weight: 700;
}
.cart-panel ::v-deep .van-submit-bar__button {
width: 104px;
height: 40px;
border-radius: 22px;
background: #ff5b20;
border-color: #ff5b20;
font-weight: 700;
}
</style>
@@ -1,42 +1,227 @@
<template>
<main class="edit-page">
<van-nav-bar title="购物车" right-text="完成" @click-right="$router.push('/joyful-selection/cart')" />
<section>
<van-card
v-for="item in goods"
:key="item.id"
:title="item.name"
:desc="item.desc"
:price="item.price"
:thumb="item.image"
>
<template #footer><van-stepper v-model="item.count" theme="round" button-size="22" /></template>
</van-card>
<section class="edit-list">
<van-empty v-if="!items.length" description="购物车还是空的" />
<article v-for="item in items" :key="item.cartId" class="edit-item">
<van-checkbox
:value="isItemChecked(item)"
class="item-check"
checked-color="#ff5b20"
@input="toggleItem(item, $event)"
/>
<van-image class="item-thumb" :src="item.image" fit="cover" />
<div class="item-info">
<h3>{{ item.name }}</h3>
<p>{{ item.desc }}</p>
<van-tag class="item-tag" plain type="danger">{{ item.tag }}</van-tag>
<div class="item-bottom">
<b>¥{{ item.price }}</b>
<span>x{{ item.count }}</span>
</div>
</div>
</article>
</section>
<van-submit-bar button-text="删除" @submit="remove">
<van-checkbox v-model="checked">全选</van-checkbox>
<van-submit-bar class="edit-submit" button-text="删除" @submit="remove">
<van-checkbox
:value="isAllChecked"
:indeterminate="isPartChecked"
checked-color="#ff5b20"
@input="toggleAll"
>全选</van-checkbox>
</van-submit-bar>
</main>
</template>
<script>
import { Toast } from 'vant'
import { joyfulProducts } from '../../product-data'
import { mapActions, mapState } from 'pinia'
import { useCartStore } from '@/store/cart'
export default {
name: 'JoyfulSelectionCartEdit',
data: () => ({
checked: false,
goods: joyfulProducts.map(item => ({ ...item, count: 1 }))
selectedIds: []
}),
computed: {
...mapState(useCartStore, ['items']),
isAllChecked() {
return this.items.length > 0 && this.selectedIds.length === this.items.length
},
isPartChecked() {
return this.selectedIds.length > 0 && this.selectedIds.length < this.items.length
}
},
created() {
this.loadCart()
this.selectedIds = this.items.map(item => item.cartId)
},
methods: {
remove() { Toast('请选择要删除的商品') }
...mapActions(useCartStore, ['loadCart', 'removeItems']),
isItemChecked(item) {
return this.selectedIds.includes(item.cartId)
},
toggleItem(item, checked) {
if (checked && !this.isItemChecked(item)) {
this.selectedIds = [...this.selectedIds, item.cartId]
return
}
if (!checked) {
this.selectedIds = this.selectedIds.filter(cartId => cartId !== item.cartId)
}
},
toggleAll(checked) {
this.selectedIds = checked ? this.items.map(item => item.cartId) : []
},
remove() {
if (!this.selectedIds.length) {
Toast('请选择要删除的商品')
return
}
this.removeItems(this.selectedIds)
this.selectedIds = []
Toast('已删除')
}
}
}
</script>
<style scoped>
.edit-page { min-height: 100vh; padding-bottom: 60px; background: #f7f7f7; }
.edit-page section { padding: 0 12px; }
.edit-page .van-card { margin-top: 10px; border-radius: 10px; background: #fff; }
<style scoped lang="less">
.edit-page {
min-height: 100vh;
padding: 46px 0 58px;
background: #fff;
}
.edit-page ::v-deep .van-nav-bar {
position: fixed;
z-index: 20;
top: 0;
right: 0;
left: 0;
box-shadow: 0 1px 0 rgba(0, 0, 0, .04);
}
.edit-list {
padding: 0 16px 14px;
background: #fff;
}
.edit-item {
display: flex;
min-height: 128px;
align-items: center;
padding: 16px 0;
background: #fff;
}
.edit-item + .edit-item {
border-top: 1px solid #f5f5f5;
}
.item-check {
flex: none;
margin-right: 12px;
}
.edit-page ::v-deep .van-checkbox__icon--checked .van-icon {
border-color: #ff5b20;
background-color: #ff5b20;
}
.item-thumb {
width: 94px;
height: 94px;
flex: none;
overflow: hidden;
border-radius: 5px;
background: #f7f7f7;
}
.item-info {
display: flex;
min-width: 0;
min-height: 94px;
flex: 1;
flex-direction: column;
padding-left: 12px;
}
.item-info h3 {
display: -webkit-box;
overflow: hidden;
margin: 0 0 4px;
color: #222;
font-size: 15px;
font-weight: 700;
line-height: 20px;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.item-info p {
overflow: hidden;
margin: 0 0 6px;
color: #999;
font-size: 12px;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-tag {
width: fit-content;
color: #ff5b20;
border-color: #ff9b73;
font-size: 11px;
line-height: 14px;
}
.item-bottom {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: auto;
}
.item-bottom b {
color: #ff5b20;
font-size: 16px;
font-weight: 700;
}
.item-bottom span {
color: #777;
font-size: 13px;
}
.edit-page ::v-deep .van-submit-bar {
bottom: 0;
height: 58px;
border-top: 1px solid #f0f0f0;
background: #fff;
}
.edit-page ::v-deep .van-submit-bar__bar {
height: 58px;
justify-content: space-between;
padding: 0 18px;
}
.edit-page ::v-deep .van-submit-bar__text {
flex: 1;
text-align: left;
}
.edit-page ::v-deep .van-submit-bar__button {
width: 104px;
height: 40px;
border-radius: 22px;
background: #ff5b20;
border-color: #ff5b20;
font-weight: 700;
}
</style>
@@ -1,57 +1,40 @@
<template>
<main class="cart-page">
<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-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="totalPrice" :button-text="submitText" @submit="$router.push('/joyful-selection/checkout')">
<van-checkbox v-model="checked">全选</van-checkbox>
</van-submit-bar>
<joyful-cart-panel fixed-submit page-mode @checkout="goCheckout" />
</main>
</template>
<script>
import { mapActions, mapState } from 'pinia'
import { useCartStore } from '@/store/cart'
import JoyfulCartPanel from './components/JoyfulCartPanel.vue'
export default {
name: 'JoyfulSelectionCart',
data: () => ({
checked: true
}),
computed: {
...mapState(useCartStore, ['items', 'totalCount', 'totalPrice']),
submitText() {
return `去结算(${this.totalCount})`
}
},
created() {
this.loadCart()
components: {
JoyfulCartPanel
},
methods: {
...mapActions(useCartStore, ['loadCart', 'persistCart'])
goCheckout(items) {
sessionStorage.setItem('joyful-selection-checkout-items', JSON.stringify(items))
this.$router.push({ path: '/joyful-selection/checkout', query: { source: 'cart' } })
}
}
}
</script>
<style scoped lang="less">
.cart-page { min-height: 100vh; padding-bottom: 106px; background: #f7f7f7; }
.cart-list { padding: 0 12px; }
.cart-list .van-card { margin-top: 10px; border-radius: 10px; background: #fff; }
.cart-list ::v-deep .van-card__price { color: #ff5b20; font-weight: bold; }
.van-submit-bar { bottom: 50px; }
.cart-page {
min-height: 100vh;
padding-top: 46px;
background: #fff;
}
.cart-page ::v-deep .van-nav-bar {
position: fixed;
z-index: 20;
top: 0;
right: 0;
left: 0;
box-shadow: 0 1px 0 rgba(0, 0, 0, .04);
}
</style>
@@ -1,3 +1,53 @@
<template><main class="confirm-page"><van-nav-bar title="确认支付" left-arrow @click-left="$router.back()"/><section><p>支付金额</p><b>¥ 297.90</b></section><van-popup v-model="show" round position="bottom" :style="{height:'42%'}" :close-on-click-overlay="false"><article><h2>确认支付</h2><p>您即将使用微信支付</p><p>支付金额 <b>¥297.90</b></p><div><van-button round @click="$router.back()">取消</van-button><van-button round type="danger" @click="$router.push('/joyful-selection/payment-success')">确认支付</van-button></div></article></van-popup></main></template>
<script>export default{name:'JoyfulSelectionPaymentConfirm',data:()=>({show:true})}</script>
<style scoped>.confirm-page{min-height:100vh;background:#f7f7f7;text-align:center}.confirm-page section{margin:15px;padding:30px;border-radius:12px;background:#fff}.confirm-page section b{font-size:26px}.confirm-page article{padding:20px;text-align:center}.confirm-page article p{color:#666;font-size:13px}.confirm-page article div{display:flex;gap:10px;margin-top:40px}.confirm-page article .van-button{flex:1}</style>
<template>
<main class="confirm-page">
<van-nav-bar title="确认支付" left-arrow @click-left="$router.back()" />
<section>
<p>支付金额</p>
<b>¥ {{ amountText }}</b>
</section>
<van-popup v-model="show" round position="bottom" :style="{ height: '42%' }" :close-on-click-overlay="false">
<article>
<h2>确认支付</h2>
<p>您即将使用{{ payTypeText }}</p>
<p>支付金额 <b>¥{{ amountText }}</b></p>
<div>
<van-button round @click="$router.back()">取消</van-button>
<van-button round type="danger" @click="$router.push({ path: '/joyful-selection/payment-success', query: { payType: $route.query.payType } })">确认支付</van-button>
</div>
</article>
</van-popup>
</main>
</template>
<script>
import { readPaymentInfo, formatYuan } from '../payment/payment-data'
export default {
name: 'JoyfulSelectionPaymentConfirm',
data: () => ({
show: true,
paymentInfo: {}
}),
computed: {
amountText() {
return formatYuan(this.paymentInfo.amount)
},
payTypeText() {
const map = {
points: '积分支付',
wechat: '微信支付',
alipay: '支付宝支付'
}
return map[this.$route.query.payType] || '积分支付'
}
},
created() {
this.paymentInfo = readPaymentInfo()
}
}
</script>
<style scoped>
.confirm-page{min-height:100vh;padding-top:46px;background:#f7f7f7;text-align:center}.confirm-page ::v-deep .van-nav-bar{position:fixed;z-index:20;top:0;right:0;left:0;box-shadow:0 1px 0 rgba(0,0,0,.04)}.confirm-page section{margin:15px;padding:30px;border-radius:12px;background:#fff}.confirm-page section b{font-size:26px}.confirm-page article{padding:20px;text-align:center}.confirm-page article p{color:#666;font-size:13px}.confirm-page article div{display:flex;gap:10px;margin-top:40px}.confirm-page article .van-button{flex:1}.confirm-page ::v-deep .van-button--danger{background:#ff5b20;border-color:#ff5b20}
</style>
@@ -1,4 +1,57 @@
<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>
<template>
<main class="success-page">
<van-icon name="checked" color="#20bd58" size="68" />
<h2>支付成功</h2>
<b>¥ {{ amountText }}</b>
<section>
<p>订单编号 <span>{{ paymentInfo.orderNo || '-' }}</span></p>
<p>支付方式 <span>{{ payTypeText }}</span></p>
<p>支付时间 <span>{{ paidAt }}</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>
import { readPaymentInfo, formatYuan } from '../payment/payment-data'
export default {
name: 'JoyfulSelectionPaymentSuccess',
data: () => ({
paymentInfo: {},
paidAt: ''
}),
computed: {
amountText() {
return formatYuan(this.paymentInfo.amount)
},
payTypeText() {
const map = {
points: '积分支付',
wechat: '微信支付',
alipay: '支付宝支付'
}
return map[this.$route.query.payType] || '积分支付'
}
},
created() {
this.paymentInfo = readPaymentInfo()
this.paidAt = this.formatTime(new Date())
},
methods: {
formatTime(date) {
const pad = value => String(value).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
}
}
}
</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}.success-page .van-button--danger{background:#ff5b20;border-color:#ff5b20}
</style>
@@ -1,3 +1,220 @@
<template><main class="payment-page"><van-nav-bar title="确认支付" left-arrow @click-left="$router.back()"/><section class="pay-total"><p>支付金额</p><b>¥ 297.90</b></section><van-cell-group inset title="订单信息"><van-cell title="订单编号" value="YX2024051812345678"/><van-cell title="收货人" value="张三 138****8888"/></van-cell-group><van-radio-group v-model="payType"><van-cell-group inset title="支付方式"><van-cell title="微信支付" icon="wechat-pay" clickable @click="payType='wechat'"><template #right-icon><van-radio name="wechat"/></template></van-cell><van-cell title="支付宝支付" icon="alipay" clickable @click="payType='alipay'"><template #right-icon><van-radio name="alipay"/></template></van-cell><van-cell title="银联支付" icon="credit-pay" clickable @click="payType='union'"><template #right-icon><van-radio name="union"/></template></van-cell><van-cell title="余额支付(余额:¥32.00)" icon="gold-coin-o" clickable @click="payType='balance'"><template #right-icon><van-radio name="balance"/></template></van-cell></van-cell-group></van-radio-group><van-button class="pay-button" round type="danger" block @click="$router.push('/joyful-selection/payment-confirm')">立即支付 ¥297.90</van-button></main></template>
<script>export default{name:'JoyfulSelectionPayment',data:()=>({payType:'wechat'})}</script>
<style scoped>.payment-page{min-height:100vh;background:#f7f7f7}.pay-total{padding:25px;text-align:center;background:#fff}.pay-total p{margin:0;color:#666;font-size:13px}.pay-total b{display:block;margin-top:10px;font-size:26px}.van-cell-group{margin:10px 12px}.pay-button{position:fixed;right:16px;bottom:20px;left:16px;width:calc(100% - 32px)}</style>
<template>
<main class="payment-page">
<van-nav-bar title="确认支付" left-arrow @click-left="$router.back()" />
<section class="amount-card">
<p>支付金额</p>
<b>¥ {{ amountText }}</b>
</section>
<section class="info-card">
<h3>订单信息</h3>
<p><span>订单编号</span><b>{{ paymentInfo.orderNo || '-' }}</b></p>
<p><span>收货人</span><b>{{ receiverText }}</b></p>
</section>
<section class="pay-card">
<h3>支付方式</h3>
<van-radio-group v-model="payType">
<van-cell
v-for="item in payMethods"
:key="item.name"
clickable
@click="payType = item.name"
>
<template #title>
<div class="pay-method">
<span class="pay-icon" :class="item.name">
<van-icon :name="item.icon" />
</span>
<b>{{ item.label }}</b>
</div>
</template>
<template #right-icon>
<van-radio :name="item.name" checked-color="#ff5b20" />
</template>
</van-cell>
</van-radio-group>
</section>
<van-button class="pay-button" round block @click="onPay">立即支付 ¥{{ amountText }}</van-button>
</main>
</template>
<script>
import { readPaymentInfo, formatYuan } from './payment-data'
export default {
name: 'JoyfulSelectionPayment',
data: () => ({
payType: 'points',
paymentInfo: {},
pointsBalance: '32.00'
}),
computed: {
amountText() {
return formatYuan(this.paymentInfo.amount)
},
receiverText() {
const name = this.paymentInfo.receiverName || '-'
const tel = this.paymentInfo.receiverTel || ''
return tel ? `${name} ${tel}` : name
},
payMethods() {
return [
{ name: 'points', label: `积分支付(余额:¥${this.pointsBalance})`, icon: 'gold-coin-o' },
{ name: 'wechat', label: '微信支付', icon: 'wechat-pay' },
{ name: 'alipay', label: '支付宝支付', icon: 'alipay' }
]
}
},
created() {
this.paymentInfo = readPaymentInfo()
},
methods: {
onPay() {
this.$router.push({ path: '/joyful-selection/payment-confirm', query: { payType: this.payType } })
}
}
}
</script>
<style scoped lang="less">
.payment-page {
min-height: 100vh;
padding: 46px 8px 84px;
background: #f7f7f7;
}
.payment-page ::v-deep .van-nav-bar {
position: fixed;
z-index: 20;
top: 0;
right: 0;
left: 0;
box-shadow: 0 1px 0 rgba(0, 0, 0, .04);
}
.amount-card,
.info-card,
.pay-card {
margin-top: 10px;
border-radius: 8px;
background: #fff;
}
.amount-card {
padding: 24px 16px 27px;
text-align: center;
}
.amount-card p {
margin: 0 0 14px;
color: #333;
font-size: 12px;
}
.amount-card b {
color: #222;
font-size: 28px;
font-weight: 700;
line-height: 1;
}
.info-card {
padding: 14px 12px 12px;
}
.info-card h3,
.pay-card h3 {
margin: 0 0 12px;
color: #222;
font-size: 13px;
font-weight: 700;
}
.info-card p {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0;
color: #777;
font-size: 12px;
line-height: 26px;
}
.info-card b {
color: #333;
font-weight: 400;
}
.pay-card {
overflow: hidden;
padding: 14px 0 4px;
}
.pay-card h3 {
padding: 0 12px;
}
.pay-card ::v-deep .van-cell {
padding: 10px 12px;
}
.pay-card ::v-deep .van-cell::after {
display: none;
}
.pay-method {
display: flex;
align-items: center;
}
.pay-method b {
color: #333;
font-size: 13px;
font-weight: 500;
}
.pay-icon {
display: flex;
width: 18px;
height: 18px;
align-items: center;
justify-content: center;
margin-right: 8px;
border-radius: 4px;
color: #fff;
}
.pay-icon .van-icon {
font-size: 14px;
}
.pay-icon.points {
background: #ffb000;
}
.pay-icon.wechat {
background: #18c764;
}
.pay-icon.alipay {
background: #1989fa;
}
.pay-button {
position: fixed;
right: 8px;
bottom: 18px;
left: 8px;
width: calc(100% - 16px);
height: 44px;
border: 0;
color: #fff;
background: #ff5b20;
font-size: 15px;
font-weight: 700;
}
</style>
@@ -0,0 +1,23 @@
const PAYMENT_KEY = 'joyful-selection-payment-info'
export const createOrderNo = () => {
const now = new Date()
const pad = value => String(value).padStart(2, '0')
const timestamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
return `YX${timestamp}`
}
export const savePaymentInfo = info => {
sessionStorage.setItem(PAYMENT_KEY, JSON.stringify(info))
}
export const readPaymentInfo = () => {
try {
return JSON.parse(sessionStorage.getItem(PAYMENT_KEY) || '{}')
} catch (error) {
return {}
}
}
export const formatYuan = price => (Number(price || 0) / 100).toFixed(2)
@@ -68,7 +68,7 @@ export default {
{ name: '超值专区', icon: 'label', color: '#ec8c3d', background: '#fff4e9' },
{ name: '全部分类', icon: 'apps-o', color: '#989898', background: '#f5f5f5' }
],
flashProducts: joyfulProducts,
flashProducts: joyfulProducts.slice(0, 4),
hotProducts: [joyfulProducts[2], joyfulProducts[0], joyfulProducts[1], joyfulProducts[3]]
}
},
@@ -4,19 +4,32 @@
<van-tabbar v-if="showTabbar" route active-color="#ff5b20" inactive-color="#777">
<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="cart-o" :badge="cartBadge" to="/joyful-selection/cart">购物车</van-tabbar-item>
<van-tabbar-item icon="user-o" to="/joyful-selection/profile">我的</van-tabbar-item>
</van-tabbar>
</main>
</template>
<script>
import { mapActions, mapState } from 'pinia'
import { useCartStore } from '@/store/cart'
export default {
name: 'JoyfulSelectionLayout',
computed: {
...mapState(useCartStore, ['totalCount']),
cartBadge() {
return this.totalCount > 0 ? this.totalCount : ''
},
showTabbar() {
return ['/joyful-selection/home', '/joyful-selection/products', '/joyful-selection/cart', '/joyful-selection/profile'].includes(this.$route.path)
}
},
created() {
this.loadCart()
},
methods: {
...mapActions(useCartStore, ['loadCart'])
}
}
</script>
@@ -3,6 +3,87 @@ import miniOvenImage from '@/assets/joyful-selection/products/mini-oven.png'
import smartSpeakerImage from '@/assets/joyful-selection/products/smart-speaker.png'
import giftBoxImage from '@/assets/joyful-selection/products/gift-box.png'
const productImages = {
bluetoothEarbuds: bluetoothEarbudsImage,
miniOven: miniOvenImage,
smartSpeaker: smartSpeakerImage,
giftBox: giftBoxImage
}
const createProduct = ({ id, name, desc, detail, price, sales, image, categoryId, category, tag = '赠品' }) => ({
id,
name,
desc,
detail: detail || desc,
price,
originPrice: (Number(price) * 1.25).toFixed(2),
sales,
image,
categoryId,
category,
tag
})
export const joyfulCategories = [
{ id: 'recommend', name: '推荐' },
{ id: 'homeLiving', name: '居家生活' },
{ id: 'digitalAppliance', name: '数码家电' },
{ id: 'beautyCare', name: '美妆个护' },
{ id: 'foodDrink', name: '食品饮料' },
{ id: 'motherBaby', name: '母婴玩具' },
{ id: 'fashionBag', name: '服饰鞋包' },
{ id: 'sportsOutdoor', name: '运动户外' }
]
const categoryNameMap = joyfulCategories.reduce((map, category) => {
map[category.id] = category.name
return map
}, {})
const rawProducts = [
['cream-oven', '奶油白家用迷你烤箱', '小巧不占地,早餐烘焙好帮手', '129.00', '1.2万件', productImages.miniOven, 'homeLiving'],
['cotton-bedding', '100%纯棉舒适四件套', '柔软亲肤,安心睡眠', '179.00', '6453件', productImages.miniOven, 'homeLiving'],
['aroma-gift', '橙意香薰礼盒套装', '淡雅留香,装点居家空间', '59.90', '8321件', productImages.giftBox, 'homeLiving'],
['desktop-vase', '简约北欧陶瓷花瓶', '清新配色,点亮桌面角落', '39.90', '3180件', productImages.giftBox, 'homeLiving'],
['earbuds-air2', '小米蓝牙耳机 Air2 SE', '轻盈半入耳,通勤运动都好听', '69.90', '1.2万件', productImages.bluetoothEarbuds, 'digitalAppliance'],
['smart-speaker-pro', '黑曜智能音箱 Pro', '语音唤醒,沉浸听感', '99.00', '2.1万件', productImages.smartSpeaker, 'digitalAppliance'],
['electric-toothbrush', '智能声波电动牙刷', '高频洁净,呵护口腔', '129.00', '2.1万件', productImages.bluetoothEarbuds, 'digitalAppliance'],
['dual-charger', '双口快充充电器 30W', '小巧便携,多设备快充', '69.00', '5632件', productImages.smartSpeaker, 'digitalAppliance'],
['skin-care-box', '植萃保湿护肤礼盒', '水润修护,清爽不粘腻', '99.90', '4820件', productImages.giftBox, 'beautyCare'],
['aroma-wash', '香氛洗护旅行套装', '温和清洁,清新留香', '59.90', '7932件', productImages.giftBox, 'beautyCare'],
['makeup-mirror', '便携补光化妆镜', '高清镜面,随时补妆', '49.90', '3651件', productImages.smartSpeaker, 'beautyCare'],
['hand-cream', '滋润护手霜三支装', '细腻滋养,轻柔香气', '39.90', '5280件', productImages.giftBox, 'beautyCare'],
['orange-gift', '橙意惊喜零食礼盒', '精致包装,送礼更有心意', '59.90', '6324件', productImages.giftBox, 'foodDrink'],
['coffee-cup', '咖啡随行杯组合', '锁温防漏,醇香随行', '49.90', '3520件', productImages.bluetoothEarbuds, 'foodDrink'],
['tea-snack', '下午茶点心礼盒', '多种风味,一盒满足', '69.90', '4891件', productImages.giftBox, 'foodDrink'],
['fruit-drink', '轻享果饮礼盒', '清爽酸甜,聚会常备', '79.90', '2976件', productImages.giftBox, 'foodDrink'],
['baby-bottle', '安心防摔儿童水杯', '轻巧易握,出门好携带', '39.90', '4250件', productImages.bluetoothEarbuds, 'motherBaby'],
['toy-blocks', '益智拼搭积木套装', '色彩柔和,陪伴成长', '89.00', '2186件', productImages.giftBox, 'motherBaby'],
['kids-night-light', '柔光儿童陪伴夜灯', '温和光线,安心入睡', '69.90', '3548件', productImages.smartSpeaker, 'motherBaby'],
['baby-towel', '纯棉柔软浴巾两件装', '亲肤吸水,日常必备', '59.90', '2761件', productImages.miniOven, 'motherBaby'],
['laptop-backpack', '大容量双肩电脑背包', '分区收纳,通勤出行轻松装', '149.00', '1.6万件', productImages.giftBox, 'fashionBag'],
['mini-bag', '轻巧通勤斜挎小包', '简洁百搭,日常实用', '79.90', '5321件', productImages.giftBox, 'fashionBag'],
['silk-scarf', '柔软撞色丝巾礼盒', '细腻亲肤,点亮穿搭', '69.90', '2680件', productImages.giftBox, 'fashionBag'],
['canvas-tote', '简约帆布托特包', '大容量开口,随手能装', '49.90', '6412件', productImages.giftBox, 'fashionBag'],
['sport-bottle', '运动便携保温水杯', '长效锁温,户外随行', '49.90', '7342件', productImages.bluetoothEarbuds, 'sportsOutdoor'],
['neck-massager', '筋膜放松按摩仪 Mini', '轻巧便携,舒缓酸胀', '99.00', '3921件', productImages.smartSpeaker, 'sportsOutdoor'],
['camping-light', '户外便携露营灯', '多档照明,夜晚更安心', '79.00', '2568件', productImages.smartSpeaker, 'sportsOutdoor'],
['fitness-band', '弹力健身拉力带套装', '居家训练,轻松塑形', '29.90', '4396件', productImages.miniOven, 'sportsOutdoor']
]
export const joyfulProductList = rawProducts.map(([id, name, desc, price, sales, image, categoryId]) =>
createProduct({
id,
name,
desc,
price,
sales,
image,
categoryId,
category: categoryNameMap[categoryId]
})
)
export const joyfulProducts = [
{
id: 'bluetooth-earbuds',
@@ -47,7 +128,8 @@ export const joyfulProducts = [
category: '食品饮料',
tag: '品质精选',
image: giftBoxImage
}
},
...joyfulProductList
]
export const getJoyfulProduct = id => joyfulProducts.find(item => item.id === id) || joyfulProducts[0]
@@ -59,13 +59,24 @@
<van-goods-action>
<van-goods-action-icon icon="chat-o" text="客服" dot />
<van-goods-action-icon icon="cart-o" text="购物车" :badge="cartBadge" @click="$router.push('/joyful-selection/cart')" />
<van-goods-action-icon icon="cart-o" text="购物车" :badge="cartBadge" @click="openCartPopup" />
<van-goods-action-icon icon="star" text="已收藏" color="#ff5000" />
<van-goods-action-button type="warning" text="加入购物车" @click="openSku('cart')" />
<van-goods-action-button type="danger" text="立即购买" @click="openSku('buy')" />
</van-goods-action>
<joyful-sku-popup ref="skuPopup" @add-cart="loadCart" />
<van-popup v-model="showCartPopup" class="cart-popup" position="bottom" round>
<header class="cart-popup-head">
<b>购物车</b>
<van-icon name="cross" @click="showCartPopup = false" />
</header>
<joyful-cart-panel
ref="cartPanel"
class="cart-popup-panel"
@checkout="goCheckout"
/>
</van-popup>
</main>
</template>
@@ -73,14 +84,17 @@
import { mapActions, mapState } from 'pinia'
import { useCartStore } from '@/store/cart'
import { getJoyfulProduct } from '../product-data'
import JoyfulSkuPopup from '../components/JoyfulSkuPopup.vue'
import JoyfulSkuPopup from '../products/components/JoyfulSkuPopup.vue'
import JoyfulCartPanel from '../cart/components/JoyfulCartPanel.vue'
export default {
name: 'JoyfulSelectionProductDetail',
components: {
JoyfulSkuPopup
JoyfulSkuPopup,
JoyfulCartPanel
},
data: () => ({
showCartPopup: false,
count: 1,
features: [
{ icon: 'music-o', title: '舒适半入耳设计', desc: '长时间佩戴更舒适' },
@@ -101,6 +115,20 @@ export default {
},
methods: {
...mapActions(useCartStore, ['loadCart']),
openCartPopup() {
this.loadCart()
this.showCartPopup = true
this.$nextTick(() => {
if (this.$refs.cartPanel) {
this.$refs.cartPanel.selectAllItems()
}
})
},
goCheckout(items) {
sessionStorage.setItem('joyful-selection-checkout-items', JSON.stringify(items))
this.showCartPopup = false
this.$router.push({ path: '/joyful-selection/checkout', query: { source: 'cart' } })
},
openSku(mode) {
this.$refs.skuPopup.open(this.product, mode)
}
@@ -357,4 +385,39 @@ export default {
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.cart-popup {
display: flex;
height: 76vh;
flex-direction: column;
overflow: hidden;
}
.cart-popup-head {
display: flex;
height: 48px;
flex: none;
align-items: center;
justify-content: center;
border-bottom: 1px solid #f2f2f2;
background: #fff;
}
.cart-popup-head b {
color: #222;
font-size: 16px;
font-weight: 700;
}
.cart-popup-head .van-icon {
position: absolute;
right: 16px;
color: #999;
font-size: 20px;
}
.cart-popup-panel {
flex: 1;
min-height: 0;
}
</style>
@@ -1,13 +1,15 @@
<template>
<van-sku
v-model="show"
:class="`sku-popup--${mode}`"
:sku="sku"
:goods="skuGoods"
:goods-id="currentGoods.id"
:initial-sku="initialSku"
:hide-stock="sku.hide_stock"
:show-add-cart-btn="mode === 'cart'"
:show-buy-btn="mode === 'buy'"
:show-stepper="false"
:show-add-cart-btn="mode !== 'buy'"
:show-buy-btn="true"
:preview-on-click-image="true"
add-cart-text="加入购物车"
buy-text="立即购买"
@@ -61,13 +63,19 @@ export default {
},
sku() {
const price = Math.round(Number(this.currentGoods.price) * 100)
const imageOptions = colorOptions.map(color => ({
...color,
imgUrl: this.currentGoods.image,
previewImgUrl: this.currentGoods.image
}))
return {
tree: [
{
k: '颜色',
k_s: 's1',
v: colorOptions
v: imageOptions,
largeImageMode: true
},
{
k: '尺寸',
@@ -81,13 +89,13 @@ export default {
s1: color.id,
s2: size.id,
price,
stock_num: 99
stock_num: 227
}))
),
price: this.currentGoods.price,
stock_num: 99,
stock_num: 227,
none_sku: false,
hide_stock: true
hide_stock: false
}
}
},
@@ -129,50 +137,115 @@ export default {
this.$emit('add-cart')
},
onBuyClicked(data) {
this.addCart(data)
const item = this.getCartPayload(data)
sessionStorage.setItem('joyful-selection-checkout-items', JSON.stringify([item]))
this.show = false
this.$emit('buy')
this.$router.push('/joyful-selection/checkout')
this.$router.push({ path: '/joyful-selection/checkout', query: { source: 'buy' } })
}
}
}
</script>
<style scoped lang="less">
::v-deep .van-sku-container {
overflow: hidden;
border-radius: 14px 14px 0 0;
}
::v-deep .van-sku-header {
min-height: 118px;
min-height: 96px;
padding: 18px 42px 12px 16px;
}
::v-deep .van-sku-header__img-wrap {
width: 110px;
height: 110px;
border-radius: 8px;
display: none;
}
::v-deep .van-sku-header__goods-info {
min-height: 110px;
padding-top: 2px;
min-height: 66px;
padding: 0;
}
::v-deep .van-sku__goods-price {
margin-top: 0;
}
::v-deep .van-sku__goods-name {
color: #111;
font-size: 15px;
font-weight: 700;
line-height: 20px;
margin-top: 7px;
color: #999;
font-size: 13px;
font-weight: 400;
line-height: 18px;
}
::v-deep .van-sku__price-symbol,
::v-deep .van-sku__price-num {
color: #ff5b20;
color: #f0182d;
}
::v-deep .van-sku__price-symbol {
font-size: 17px;
}
::v-deep .van-sku__price-num {
font-size: 22px;
font-weight: 400;
}
::v-deep .van-sku__stock {
color: #999;
font-size: 13px;
}
::v-deep .van-sku__selected-name {
margin-top: 8px;
color: #999;
font-size: 12px;
}
::v-deep .van-sku-body {
padding: 0 16px;
}
::v-deep .van-sku-group-container {
margin-left: 0;
}
::v-deep .van-sku-row {
margin: 0;
padding: 0 0 12px;
}
::v-deep .van-sku-row__title {
padding-bottom: 12px;
color: #333;
font-size: 14px;
}
::v-deep .van-sku-row__row {
display: flex;
justify-content: space-between;
align-items: center;
}
::v-deep .van-sku-row:not(:first-child) .van-sku-row__item {
height: 31px;
margin: 0 10px 8px 0;
padding: 0 14px;
border: 0;
border-radius: 4px;
background: #f6f7f9;
color: #333;
font-size: 13px;
line-height: 31px;
}
::v-deep .van-sku-row__item--active {
color: #ff5b20;
border-color: #ff5b20;
color: #ff5b20 !important;
border-color: #ff5b20 !important;
background: #fff8f4 !important;
}
::v-deep .van-sku-actions .van-button--danger {
background: #ff5b20;
border-color: #ff5b20;
}
</style>
@@ -52,103 +52,8 @@
</template>
<script>
import { joyfulProducts } from '../product-data'
import JoyfulSkuPopup from '../components/JoyfulSkuPopup.vue'
const productImages = joyfulProducts.map(item => item.image)
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 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: '赠品'
}))
)
import { joyfulCategories, joyfulProductList } from '../product-data'
import JoyfulSkuPopup from './components/JoyfulSkuPopup.vue'
export default {
name: 'JoyfulSelectionProductList',
@@ -158,8 +63,8 @@ export default {
data: () => ({
selectedCategoryId: 'recommend',
keyword: '',
categories,
goods
categories: joyfulCategories,
goods: joyfulProductList
}),
computed: {
visibleGoods() {
@@ -37,7 +37,8 @@ export default {
</script>
<style scoped>
.orders-page { min-height: 100vh; background: #f7f7f7; }
.orders-page { min-height: 100vh; padding-top: 46px; background: #f7f7f7; }
.orders-page ::v-deep .van-nav-bar { position: fixed; z-index: 20; top: 0; right: 0; left: 0; box-shadow: 0 1px 0 rgba(0, 0, 0, .04); }
.orders-page section { padding: 0 12px; }
.orders-page article { margin-top: 10px; border-radius: 10px; background: #fff; }
.orders-page header, .orders-page footer { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; color: #999; font-size: 11px; }
@@ -1,3 +1,3 @@
<template><main class="points-page"><van-nav-bar title="我的积分" left-arrow @click-left="$router.back()"/><section class="score"><small>可用积分</small><b>2368</b><van-button size="small" round @click="$router.push('/joyful-selection/products')">积分说明</van-button></section><van-grid :column-num="4" :border="false"><van-grid-item icon="gift-o" text="积分商城"/><van-grid-item icon="gift-card-o" text="积分抽奖"/><van-grid-item icon="balance-list-o" text="积分兑换"/><van-grid-item icon="todo-list-o" text="积分签到"/></van-grid><section class="details"><h3>积分明细</h3><van-cell v-for="item in items" :key="item.title" :title="item.title" :label="item.time" :value="item.value" :value-class="item.value[0] === '+' ? 'income' : 'cost'"/><p>没有更多了~</p></section></main></template>
<script>export default{name:'JoyfulSelectionPoints',data:()=>({items:[{title:'签到奖励',time:'2024-05-18 09:30',value:'+10'},{title:'订单完成奖励',time:'2024-05-14 10:20',value:'+88'},{title:'积分兑换',time:'2024-05-08 15:11',value:'-200'},{title:'晒单奖励',time:'2024-05-02 08:30',value:'+20'},{title:'评价奖励',time:'2024-04-28 12:08',value:'+5'}]})}</script>
<style scoped>.points-page{min-height:100vh;background:#f7f7f7}.score{position:relative;margin:10px 12px;padding:16px 18px;border-radius:10px;background:linear-gradient(135deg,#ffe0ac,#ffc66e)}.score small,.score b{display:block}.score small{font-size:11px}.score b{margin-top:6px;font-size:22px}.score .van-button{position:absolute;right:14px;top:24px;background:rgba(255,255,255,.6)}.points-page>.van-grid{margin:0 12px;border-radius:10px;background:#fff}.details{margin:10px 12px;padding:2px 0;border-radius:10px;background:#fff}.details h3{margin:14px;font-size:14px}.details p{text-align:center;color:#aaa;font-size:11px}.income{color:#14a44d}.cost{color:#f15b45}</style>
<style scoped>.points-page{min-height:100vh;padding-top:46px;background:#f7f7f7}.points-page ::v-deep .van-nav-bar{position:fixed;z-index:20;top:0;right:0;left:0;box-shadow:0 1px 0 rgba(0,0,0,.04)}.score{position:relative;margin:10px 12px;padding:16px 18px;border-radius:10px;background:linear-gradient(135deg,#ffe0ac,#ffc66e)}.score small,.score b{display:block}.score small{font-size:11px}.score b{margin-top:6px;font-size:22px}.score .van-button{position:absolute;right:14px;top:24px;background:rgba(255,255,255,.6)}.points-page>.van-grid{margin:0 12px;border-radius:10px;background:#fff}.details{margin:10px 12px;padding:2px 0;border-radius:10px;background:#fff}.details h3{margin:14px;font-size:14px}.details p{text-align:center;color:#aaa;font-size:11px}.income{color:#14a44d}.cost{color:#f15b45}</style>