0
点赞
收藏
分享

微信扫一扫

petite-vue实战点餐app(二):购物车、支付页

Jonescy 2022-10-19 阅读 26

接着上一次内容继续写,这次主要完成:

  • 购物车逻辑控制
  • 加购及小红点功能
  • 商品详情
  • 购物车面板
  • 支付页面

购物车逻辑

购物车逻辑控制就是维护一个购物车数据模型​​cart​​,包含多个商品和数量,类似这样:

{
1: {
prod: {id:1, name:'油条', price:3},
amount: 2
},
2:{
prod: {id:2, name:'豆浆', price:4},
amount: 1
},
}

加购

用户点​​+​​按钮添加商品数量,自动计算总额:

petite-vue实战点餐app(二):购物车、支付页_jsonimage-20210902181849321

<div class="btn-add" @click="addToCart(prod)">+</div>

createApp({
// 购物车逻辑
cart: {},
addToCart(prod) {
// 添加过数量加1,否则设置为1
if (this.cart[prod.id]) {
this.cart[prod.id].amount += 1
} else {
this.cart[prod.id] = { prod, amount: 1 }
}
},
get total() {
// 计算总额
return Object.keys(this.cart).reduce((total, curr) => {
const cartItem = this.cart[curr]
return total + cartItem.prod.price * cartItem.amount
}, 0)
}
}).mount('#app')

小红点

小红点用来显示用户买的商品的数量总和,可以用​​组件​​定义红点,这样不仅能在购物车里面用上,在分类中也可以用上:

petite-vue实战点餐app(二):购物车、支付页_ide_02

定义红点组件:

function Counter({ cate, cart }) {
return {
$template: `
<span class="red-dot" v-if="count > 0">{{count}}</span>
`,
get count() {
return Object.keys(cart)
.map(key cart[key])
.filter(item (cate ? item.cate === cate : true))
.reduce((total, item) => total + item.amount, 0)
},
}
}

引入​​Counter​​组件:

createApp({
// 红点
Counter
})

模板中使用​​Counter​​组件:

<div class="cart">
<!-- ... -->
<!-- 红点 -->
<span class="red-dot-box" v-scope="Counter({cart})"></span>
</div>

红点样式:

span.red-dot {
font-size: 2rem;
background: red;
border-radius: 50%;
display: inline-block;
width: 3rem;
height: 3rem;
color: white;
text-align: center;
}

span.red-dot-box {
position: absolute;
left: 5.4rem;
}

在分类中复用​​Counter​​:

<div class="cate-item" :class="{selected: cate === currentCate}" v-for="cate in categories" :key="cate" @click="selectCate(cate)">
{{cate}}
<!-- 红点 -->
<span class="red-dot-container" v-scope="Counter({cart, cate})"></span>
</div>

span.red-dot-container {
position: relative;
top: -0.3rem;
line-height: 3rem;
}

ordering-food-step4

商品详情

详情这里再次遇坑,​​v-if​​​设置一个要显示的购物车项目,有则显示内容,无则隐藏内容,多么简单的需求,竟然发现​​pvue​​​在我关闭详情窗口置空​​currItem​​​之后还会访问下面的数据,这和​​vue​​​的行为可不一致。无奈只能在里面所有访问​​currItem​​​的地方加了一个​​?​​:

<div class="detail-box" v-if="currItem">
<div class="detail">
<img :src="currItem?.prod.img">
<p>{{currItem?.prod.name}}</p>
<div class="container">
<div class="price">
¥{{currItem?.prod.price}}/{{currItem?.prod.unit}}
</div>
<div class="detail-btns">
<span class="btn-add" @click="currItem.amount--">-</span>
<span>{{currItem?.amount}}</span>
<span class="btn-add" @click="currItem.amount++">+</span>
</div>
</div>
<div class="btn-box">
<div class="btn-confirm" @click="confirm()">确定</div>
<div class="btn-cancel" @click="cancel()">返回</div>
</div>
</div>
</div>
</div>

.detail-box {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
}

.detail {
width: 90%;
height: 94%;
margin: 5%;
background: white;
border-radius: 0.8rem;
font-size: 3rem;
position: absolute;
}

.detail>img {
width: 94%;
margin-left: 3%;
margin-top: 1rem;
}

.detail p {
padding-left: 3rem;
padding-top: 2rem;
border-top: 1px solid silver;
}

.detail>.container {
float: left;
}

.detail>.container {
width: 100%;
padding-left: 3rem;
box-sizing: border-box;
}

.detail .price {
float: left;
color: #00bcd4;
}

.detail-btns {
float: right;
}

.detail-btns>span {
float: left;
margin-right: 2rem;
}

.btn-box {
position: absolute;
bottom: 0;
width: 100%;
border-top: 1px solid silver;
}

.btn-confirm,
.btn-cancel {
width: 50%;
float: left;
text-align: center;
padding: 3rem 0;
box-sizing: border-box;
color: #00bcd4;
}

.btn-confirm {
border-right: 1px solid silver;
}

逻辑控制部分:

createApp({ 
// 详情
currItem: null,
currAmount: 0,
selectProd(prod) {
// 若还未加购则创建一个新项
if (!this.cart[prod.id]) {
this.cart[prod.id] = { prod, amount: 0, cate: this.currentCate }
}
// 保存当前数量,如果用户取消则还原
this.currAmount = this.cart[prod.id].amount
this.currItem = this.cart[prod.id]
},
confirm() {
// 若数量为0则删除购物项
if (this.currItem.amount === 0) {
delete this.cart[this.currItem.prod.id]
}
this.currItem = null
},
cancel() {
// 取消需要还原原先的数量
this.currItem.amount = this.currAmount
this.currItem = null
},
})

ordering-food-step5

购物车

购物车能够列表显示所有购物项,有没有发现这个购物项和详情的购物项可以统一?

petite-vue实战点餐app(二):购物车、支付页_ide_03

所以又是一个使用组件的场景,我们重构之前代码,提取一个​​CartItem​​组件:

<!--模板部分-->
<template id="cart-item-template">
<p>{{item?.prod.name}}</p>
<div class="container">
<div class="price">
¥{{item?.prod.price}}/{{item?.prod.unit}}
</div>
<div class="detail-btns">
<span class="btn-add" @click="item.amount--">-</span>
<span>{{item?.amount}}</span>
<span class="btn-add" @click="item.amount++">+</span>
</div>
</div>
</template>

function CartItem(item) {
return {
$template: '#cart-item-template',
item
}
}

下面创建购物车面板并使用前面的组件:

<!-- 购物车 -->
<div class="panel-cart" v-if="showCartPanel" @click.self="hideCartPanel()">
<div class="panel-cart-content">
<h3>
<span>点菜单</span>
<span class="btn-clear-all">清空全部</span>
</h3>
<div class="cart-item-list">
<div class="cart-item" v-for="item in cart" :key="item.prod.id"
v-scope="CartItem(item)"></div>
</div>
</div>
</div>

样式

.panel-cart {
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
right: 0;
left: 0;
bottom: 9rem;
top: 0;
}

.panel-cart-content {
background-color: white;
position: absolute;
width: 100%;
height: 38%;
bottom: 0;
display: flex;
flex-direction: column;
}

.panel-cart-content > h3 {
background-color: #f5f5f5;
margin: 0;
padding: 1.8rem 2rem;
font-size: 2.4rem;
font-weight: normal;
color: #9e9e9e;
display: flex;
justify-content: space-between;
}
.cart-item {
font-size: 3rem;
border-bottom: 1px solid silver;
}

.container {
overflow: auto;
padding-bottom: 1.4rem;
}
.cart-item-list {
overflow: auto;
flex: 1;
}

控制逻辑

createApp({  
CartItem,
showCartPanel: false,
toggleCartPanel() {
this.showCartPanel = !this.showCartPanel
},
hideCartPanel() {
this.showCartPanel = false
},
clearAll() {
this.cart = {}
localStorage.removeItem('cart')
},
})

<div class="toolbar">
<div class="cart" @click="toggleCartPanel">

<!-- 购物车 -->
<div class="panel-cart" v-if="showCartPanel" @click.self="hideCartPanel()">

<span>点菜单</span>
<span class="btn-clear-all" @click="clearAll">清空全部</span>

ordering-food-step6

提交

提交功能会进入一个全新页面,一开始我还受单页面开发思维影响,想着加入一个路由,不过仔细想想,按pvue理念完全可以变成一个多页面应用,观察这个应用发现它也是这么处理的:

  • 将购物车信息存入​​localStorage​
  • 跳转至支付页面​​payment.html​​,读取并还原购物车信息

将购物车信息存入​​localStorage​

<div v-cloak v-scope v-effect="save()">

createApp({
save() {
// 自动保存
if (Object.keys(this.cart).length > 0) {
localStorage.setItem('cart', JSON.stringify(this.cart))
}
},
})

跳转至支付页面​​payment.html​​:

<link rel="stylesheet" href="./assets/payment.css">

<div v-scope @mounted="onmounted" v-effect="save()">
<div class="cart-item-list">
<div class="cart-item" v-for="item in cart" :key="item.prod.id" v-scope="CartItem(item)"></div>
</div>

<div class="check-out">
<p>总计:¥<span>{{total}}</span></p>
<div class="btns">
<div class="btn-add-veg" @click="addVeg">加菜</div>
<div class="btn-payment" @click="pay">下单</div>
</div>
</div>
</div>

<!-- 购物车项目模板 -->
<template id="cart-item-template">
<p>{{item?.prod.name}}</p>
<div class="container">
<div class="price">
¥{{item?.prod.price}}/{{item?.prod.unit}}
</div>
<div class="detail-btns">
<span class="btn-add" @click="item.amount--">-</span>
<span>{{item?.amount}}</span>
<span class="btn-add" @click="item.amount++">+</span>
</div>
</div>
</template>

<script type="module">
import { createApp } from 'https://unpkg.com/petite-vue?module'

function CartItem(item) {
return {
$template: '#cart-item-template',
item
}
}

createApp({
cart: {},
onmounted() {
this.cart = JSON.parse(localStorage.getItem('cart'))
},
CartItem,
save() {
// 自动保存
localStorage.setItem('cart', JSON.stringify(this.cart))
},
get total() {
return Object.keys(this.cart).reduce((total, curr) => {
const cartItem = this.cart[curr]
return total + cartItem.prod.price * cartItem.amount
}, 0)
},
addVeg() {
window.location.href = './ordering-food.html'
}
}).mount()
</script>

.cart-item-list {
font-size: 3rem;
}

.cart-item {
border-bottom: 1px solid #bdbdbd;
padding: 1rem;
}

.price {
color: #00bcd4;
}
span.btn-add {
border: 1px solid #e0e0e0;
border-radius: 50%;
padding: 0px 1.2rem;
color: #00bcd4;
}

.container {
display: flex;
justify-content: space-between;
}

p {
margin: 0;
}

.check-out {
border-top: 1px solid #757575;
position: fixed;
bottom: 0;
width: 100%;
font-size: 2.6rem;
color: #00bcd4;
display: flex;
justify-content: space-between;
height: 9rem;
align-items: center;
}

.btns {
display: flex;
}

.btn-add-veg {
background-color: orange;
color: white;
font-size: 2.2rem;
padding: 1rem 2rem;
border-radius: 0.5rem;
margin-right: 1rem;
}

.btn-payment {
background-color: red;
color: white;
font-size: 2.2rem;
padding: 1rem 2rem;
border-radius: 0.5rem;
}

.btns {
margin-right: 3rem;
}

加菜

点击加菜再回到主页,还原购物车信息。

<div v-cloak v-scope @mounted="onmounted">

createApp({    
onmounted() {
this.cart = JSON.parse(localStorage.getItem('cart'))
},
})

此时发现购物车信息在,但小红点的还原失败了,原因如下:

  • 原先​​Counter​​将购物车传入,然后计算总数,现在重新设置了新的​​this.cart​​,组件内部不能再次触发更新。这可能是个设计,也可能是个bug。
  • 我们解决方案也很简单:我们不再传入​​cart​​,直接使用全局作用域中的​​cart​

function Counter(props) {
return {
$template: `
<span class="red-dot" v-if="count > 0">{{count}}</span>
`,
get count() {
return Object.keys(this.cart)
.map(key this.cart[key])
.filter(item (props?.cate ? item.cate === props.cate : true))
.reduce((total, item) => total + item.amount, 0)
},
}
}

这样使用也变了:

<span class="red-dot-box" v-scope="Counter()"></span>

<span class="red-dot-container" v-scope="Counter({cate})"></span>

这样红点功能又恢复了!

ordering-step-7

写在最后

这样主流程就完成了,还有几个功能:​​历史账单​​​、​​多人点餐​​​和​​支付​

petite-vue实战点餐app(二):购物车、支付页_json_04

​历史账单​​主要是上次账单信息展示:

  • 桌台信息可以随二维码带过来
  • 上次支付信息是跟客户关联的,可以存入客户端​​localStorage​​中

这个功能没什么用,就留给大家做着玩了!

​多人点餐​​​、​​支付​​功能不是pvue内容,另外pvue也太坑了,写完也没价值,接下来我会重构一版vue3.2的实现,这些功能尽量都实现了。

大家想看的话,还不多多`点赞+分享`支持一下村长!


举报

相关推荐

0 条评论