接着上一次内容继续写,这次主要完成:
- 购物车逻辑控制
- 加购及小红点功能
- 商品详情
- 购物车面板
- 支付页面
购物车逻辑
购物车逻辑控制就是维护一个购物车数据模型cart
,包含多个商品和数量,类似这样:
{
1: {
prod: {id:1, name:'油条', price:3},
amount: 2
},
2:{
prod: {id:2, name:'豆浆', price:4},
amount: 1
},
}
加购
用户点+
按钮添加商品数量,自动计算总额:
image-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')
小红点
小红点用来显示用户买的商品的数量总和,可以用组件
定义红点,这样不仅能在购物车里面用上,在分类中也可以用上:
定义红点组件:
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
购物车
购物车能够列表显示所有购物项,有没有发现这个购物项和详情的购物项可以统一?
所以又是一个使用组件的场景,我们重构之前代码,提取一个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
写在最后
这样主流程就完成了,还有几个功能:历史账单
、多人点餐
和支付
历史账单
主要是上次账单信息展示:
- 桌台信息可以随二维码带过来
- 上次支付信息是跟客户关联的,可以存入客户端
localStorage
中
这个功能没什么用,就留给大家做着玩了!
多人点餐
、支付
功能不是pvue内容,另外pvue也太坑了,写完也没价值,接下来我会重构一版vue3.2的实现,这些功能尽量都实现了。
大家想看的话,还不多多`点赞+分享`支持一下村长!