今天还是说移动app开发,店铺系列文章,我们经常去超市使用购物车,即一个临时的储物空间,用完清空回收。我大兄弟说,

平时很忙,录入订单的目录很多,临时有事回来要可以继续填写,提交订单后才算结束,这就是一个典型的购物车场景了。那

系统的购物车如何实现?现在就来实战一把,做个如淘宝类的购物车。

作者原创文章,谢绝一切转载!

本文只发表在"公众号"和"博客园",其他均属复制粘贴!如果觉得排版不清晰,请查看公众号文章。

准备:

Idea2019.03/Gradle6.0.1/JDK11.0.4/Lombok0.28/SpringBoot2.2.4RELEASE/mybatisPlus3.3.0/Soul2.1.2/Dubbo2.7.5/Mysql8.0.11

/Vue2.5/OSS/Hbuilder2.6.1

难度: 新手--战士--老兵--大师

目标:

  1. 手机APP前端实现购物车功能
  2. async/await使用

步骤:

为了遇见各种问题,同时保持时效性,我尽量使用最新的软件版本。代码地址:https://github.com/xiexiaobiao/vehicle-shop-mobile.git

1 本套系统大体情况

后端代码量约1.5万,双前端约1.5万,技术还是很具代表性的,不然就不好意思拿出来说事了,详细可看Git库说明,下图是后端代码量分析:

Web管理界面:需要密码的,请公众号留言。

手机端:使用Hbuilder编码,Uniapp框架,再随手捡了几个UI拿来大改了几下,基本形状如下:

2 购物车原理

先说存储,有三套方案;一是直接数据库端存储,与后台交互多,会增加流量和业务复杂度;二是Localstorage存储,持久化到本地浏览器端,除非主动

删除,否则永久存在;三是Session级别存储,使用vuex组件,会话级存储,app关闭即清空。再说vuex组件,是vue框架组件之一,其最常用的功能就是

存储用户登录状态,因为系统很多地方的使用都需要进行登录验证,我们可以在用户登录之后,将登录状态写入vuex,那系统其他地方就可以随用随取,

我这里即说第三套方案,使用vuex做缓存实现购物车。Vuex基础知识,略!请君自查!

思路:建立一个vuex数组,即购物车存储空间,选择商品后,即加入该数组中,如果数量等属性有更新,也同步到该该数组,只要app不关闭就可以打开

购物车继续编辑,直到提交订单时清空该数组。

3 购物车存储

vehicle-shop-app/store/index.js

import Vue from'vue'
import Vuex from'vuex' Vue.use(Vuex) const store = new Vuex.Store({
state: {
hasLogin: false,
userInfo: {},
// session周期有效
items: [{
idItem: 3,
itemUuid: 'SP100034',
category: '保养',
classification: '',
itemName: '特色全合成机油',
sellPrice: 160.00,
discountPrice: 150.00,
brandName: '丰田',
description: '1.5升塑料瓶装',
shipment: true,
quantity: 3,
remark: '八折优惠5块钱',
alertQuantity: 5,
specification: '1.5升瓶装',
unit: '瓶',
sales: 20 ,
stock: 50,
checked: true, // 是否选中
picAddr:'http://biao-aliyun-oss-pic-bucket.oss-cn-shenzhen.aliyuncs.com/images/2020/03/08/1583628752948gv86t511pi.jpg',
},
],
},
// 同步操作
mutations: {
login(state, provider) {
state.hasLogin = true;
state.userInfo = provider;
// 将数据存储在本地缓存中指定的 key 中,会覆盖掉原来该 key 对应的内容,这是一个异步接口
// 对比vuex,localstorage是永久存储,保存在本地浏览器中
uni.setStorage({//缓存用户登陆状态
key: 'userInfo',
data: provider
})
console.log(state.userInfo);
},
logout(state) {
state.hasLogin = false;
state.userInfo = {};
uni.removeStorage({
key: 'userInfo'
})
},
// 添加进购物车
addCartItems(state,provider){
const cartItem = state.items.find(item => item.itemUuid === provider.itemUuid)
if(!cartItem){
state.items.push(provider);
}else{
cartItem.quantity ++;
} },
// 清空
emptyCart(state){
state.items = [];
},
// 删除一个商品, 形参如果有多个,可使用{}
deleteCartItem(state,idItem){
// 注意es6语法 findIndex 和 find 使用
let index = state.items.findIndex(item => item.idItem === idItem)
state.items.splice(index,1);
},
// 解构
incrementItemQuantity (state, { idItem }) {
const cartItem = state.items.find(item => item.idItem === idItem)
cartItem.quantity++;
},
decrementItemQuantity (state, { idItem }) {
const cartItem = state.items.find(item => item.idItem === idItem)
cartItem.quantity--;
},
setItemQuantity (state, {idItem,quantity }) {
const cartItem = state.items.find(item => item.idItem === idItem)
cartItem.quantity = quantity;
},
},
// 异步
actions: {
//// {commit} 解构 context对象,context与store实例具有相同的属性和方法。这里commit 就是 store.commit
emptyCartAsync({commit}){
setTimeout(()=>{ commit("emptyCart"),3000})
},
addCartAsync: (context,provider) => {
setTimeout(()=>{ context.commit('addCart',addCartItems),3000})
},
/* emptyCartAsync: context => {
return context.commit('emptyCart')
} */
},
getters:{
cartItems: state => {
return state.items;
}
}
}) exportdefault store

以上代码解析:

  1. 文件头引入 import Vuex from 'vuex'
  2. states区是类变量和初始值,定义一个items: []用于存放购物车商品,这里我直接写了一个商品先放里面,可以直观看到数据结构,也方面后面测试,
  3. mutations: {}中属于”同步”方法,包含一些购物车操作的方法,比如addCartItems(state,provider)是添加商品进购物车,我设计成允许重复添加,如果想不重复,直接返回不同代码即可。
  4. actions: {}是属于”异步”方法区,可以调用mutations: {}同步区的方法,也可自己写,
  5. getters和setters属于vuex基础,略!

4 全局声明

vehicle-shop-app/ main.js中:

import Vue from'vue'
import store from'./store'// 全局存储
import App from'./App'
import Request from'./plugins/request/js/index' //测试用数据
import Json from'./Json' import report from'./pages/report/home.vue'
Vue.component('report',report) //这里全局引入,并注册为vue组件,相比单页面js引入,使用更方便
/* import uniNavBar from "./components/uni-nav-bar/uni-nav-bar.vue"
Vue.component('uniNavBar',uniNavBar) */ /* import cuCustom from './colorui/components/cu-custom.vue'
Vue.component('cu-custom',cuCustom) */ import uniIcons from"@/components/uni-icons/uni-icons.vue"
Vue.component('uniIcons',uniIcons) //设置全局的api地址
Vue.prototype.websiteUrl = 'http://10.4.14.132:7000'; const msg = (title, duration=1500, mask=false, icon='none')=>{
//统一提示方便全局修改
if(Boolean(title) === false){
return;
}
uni.showToast({
title,
duration,
mask,
icon
});
} const hidemsg = ()=>{
uni.hideToast()({
});
} const json = type=>{
// 模拟异步请求数据
returnnewPromise(resolve=>{
setTimeout(()=>{
resolve(Json[type]);
}, 500)
})
} const prePage = ()=>{
let pages = getCurrentPages();
let prePage = pages[pages.length - 2];
// #ifdef H5
return prePage;
// #endif
return prePage.$vm;
} Vue.config.productionTip = false
Vue.prototype.$fire = new Vue();
Vue.prototype.$store = store;
Vue.prototype.$api = {msg, hidemsg, json, prePage};
Vue.prototype.$http = Request; App.mpType = 'app' const app = new Vue({
...App
})
app.$mount()

以上代码解析:

  1. import store from './store' 引入全局存储
  2. Vue.prototype.$store = store;这样,如果页面需要使用时,举例如下:

    如果使用同步方法:this.$store.commit("deleteCartItem",itemIdToDel)

    如果使用异步方法:this.$store.dispatch("addCartAsync",itemIdToAdd)

5 添加进购物车

vehicle-shop-app/pages/product/product.vue

商品详细页面:

js关键代码:

// 加入购物车
addCartItem(){
// vuex保存
this.$store.commit('addCartItems',this.product);
uni.showToast({
title: "加购物车成功!",
icon: 'info'
});
},

6 购物车管理:

vehicle-shop-app/pages/order/cart.vue

这个物品就是前面vuex购物车默认的一个物品,

展示下JS部分的代码:

<script>
import { mapGetters, mapState,mapActions,mapMutations } from'vuex'
import uniNumberBox from'@/components/uni-number-box.vue'
exportdefault {
components: {
uniNumberBox
},
data() {
return {
total: 0, //总价格
allChecked: false, //全选状态 true|false
empty: false, //空白页现实 true|false
cartList: [],
hasLogin: true,
};
},
activated() {
/* 解决 由订单页返回购物车页,购物车却为空的问题 */
/* 解决 由订单页返回购物车页,购物车却为空的问题 */
// 只要进入该页面就进行刷新,因为onLoad()只加载一次,
// https://blog.csdn.net/qq_27047215/article/details/98943080
this.loadData();
},
onLoad(){
this.loadData();
},
watch:{
//显示空白页
cartList(e){
let empty = e.length === 0 ? true: false;
if(this.empty !== empty){
this.empty = empty;
}
}
},
computed:{
// ...mapState(['hasLogin']),
...mapGetters(['cartItems'])
},
methods: {
// 引入后可直接使用
...mapActions(['emptyCartAsync','addCartAsync']),
...mapMutations(['addCartItems','emptyCart','deleteCartItem']),
//自动计算折扣价
setDiscountPrice:function(item){
// item.discountPrice =
},
//请求数据
loadData(){
// 从vuex中取缓存
// 这里因为cartItems放computed中,自动成为一个data,
let list = this.cartItems;
let cartList = list.map(item=>{
item.checked = true;
return item;
});
this.cartList = cartList;
this.calcTotal(); //计算总价
},
//监听image加载完成
onImageLoad(key, index) {
this.$set(this[key][index], 'loaded', 'loaded');
},
//监听image加载失败
onImageError(key, index) {
this[key][index].image = '/static/errorImage.jpg';
},
navToLogin(){
uni.navigateTo({
url: '/pages/login/login-home'
})
},
//选中状态处理
check(type, index){
if(type === 'item'){
this.cartList[index].checked = !this.cartList[index].checked;
}else{
const checked = !this.allChecked
const list = this.cartList;
list.forEach(item=>{
item.checked = checked;
})
this.allChecked = checked;
}
this.calcTotal(type);
},
//数量
numberChange(data){
console.log(JSON.stringify(data))
// 修改缓存中的数量
this.$store.commit("setItemQuantity",{idItem:this.cartList[data.index].idItem,quantity:data.number })
this.cartList[data.index].quantity = data.number;
this.calcTotal();
},
//删除
deleteCartItem(index){
let list = this.cartList;
let row = list[index];
let id = row.id;
// 删除vuex中对象
let itemIdToDel = this.cartList[index].id;
// this.deleteCartItem(0);
this.$store.commit("deleteCartItem",itemIdToDel)
this.cartList.splice(index, 1);
this.calcTotal();
uni.hideLoading();
},
//清空
clearCart(){
uni.showModal({
content: '清空购物车?',
success: (e)=>{
if(e.confirm){
// vuex使用,引入map辅助函数后,可以直接使用,或者使用$store语法等效
this.emptyCart();
// this.$store.commit("emptyCart")
// this.$store.dispatch("emptyCartAsync");
this.cartList = [];
}
}
})
},
//计算总价
calcTotal(){
let list = this.cartList;
if(list.length === 0){
this.empty = true;
return;
}
let total = 0;
let checked = true;
list.forEach(item=>{
if(item.checked === true){
total += item.discountPrice * Number(item.quantity);
}elseif(checked === true){
checked = false;
}
})
this.allChecked = checked;
this.total = Number(total.toFixed(2));
},
//创建订单
createOrder(paidStatus){
let list = this.cartList;
let goodsData = [];
list.forEach(item=>{
if(item.checked){
goodsData.push({
attr_val: item.attr_val,
number: item.quantity
})
}
}) this.cartList = [];
// this.$api.msg('跳转下一页 sendData');
uni.navigateTo({
url: `/pages/order/createOrder?paidStatus=${JSON.stringify(paidStatus)}`
})
}
}
}
</script>

以上代码解析:

  1. activated(){}和onLoad(){}都包含了this.loadData()做页面数据加载,为什么?这是vue生命周期决定的,因为onLoad()只加载一次,系统会自动缓存页面内容,如果你跑到商品页添加商品再返回购物车页,购物车却不显示,activated可以让页面每次进来都刷新一次,这样,购物车里就能实时更新了!
  2. computed:{...mapGetters(['cartItems'])}中,这是vuex语法糖,import { mapGetters, mapState,mapActions,mapMutations } from 'vuex'之后,就可以直接使用'cartItems'变量了,系统会自动生成,看loadData()中就是let list = this.cartItems;
  3. 数量修改:
  4. //数量
    numberChange(data){
    console.log(JSON.stringify(data))
    // 修改缓存中的数量
    this.$store.commit("setItemQuantity",{idItem:this.cartList[data.index].idItem,quantity:data.number })
    this.cartList[data.index].quantity = data.number;
    this.calcTotal();
    },
  5. 清空购物车方法clearCart(),这里演示了三种使用vuex的模式:一是配合import相关的map辅助函数,然后直接使用this.emptyCart(); 二是同步方法this.store.dispatch("emptyCartAsync"); 殊途同归!请君自选!
  6. clearCart(){
    uni.showModal({
    content: '清空购物车?',
    success: (e)=>{
    if(e.confirm){
    // vuex使用,引入map辅助函数后,可以直接使用,或者使用$store语法等效
    this.emptyCart();
    // this.$store.commit("emptyCart")
    // this.$store.dispatch("emptyCartAsync");
    this.cartList = [];
    }
    }
    })
    },

这样,购物车打造完毕!只要用户不关闭app,打开购物车页面,里面商品就会存在,当然,别忘了,提交订单时,清空下购物车,

因为出了超市,购物车得还给人家,不能带回家!

7 async/await化异步为同步

前面一篇,说到后台请求数据都是异步的,处理不好就是页面渲染完毕,结果后台数据才过来,这就尴尬了。所以这里我举个例子解决

下这个问题:

vehicle-shop-app/pages/product/list.vue

async switchChange(item){
item.checked = !item.checked;
// console.log(JSON.stringify(item));
if(item.checked){
// 获取商品详细
let requestItem={};
await Request().request({
url: 'stock/vehicle/stock/item/uid/'+ item.itemUuid,
method: 'get',
header: {},
params: {}
}).then(
res => {
// 返回的对象,多一层data封装,故写为response.data
requestItem = res.data;
}).catch(err => {
console.error('is catch', err)
this.err = err;
}) // 设置数量默认值
requestItem = Object.assign(requestItem,{
discountPrice: requestItem.sellPrice,
})
requestItem.quantity = 1;
//加入vuex缓存,commit是同步方法
// this.$store.commit('addCartItems',requestItem);
this.toAddItemList.push(requestItem);
//修改角标值
this.totalChecked += 1;
this.setStyle(1,true,this.totalChecked);
uni.showToast({
title: "选择商品成功!",
icon: 'info',
duration: 300
});
}else{
// this.$store.commit("deleteCartItem",item)
// 删除临时数组中的值
let index = this.toAddItemList.findIndex(item=>item.itemUuid === requestItem.itemUuid);
this.toAddItemList.splice(index,10);
this.totalChecked -= 1;
this.setStyle(1,true,this.totalChecked);
uni.showToast({
title: "取消商品成功!",
icon: 'info',
duration: 300
});
}
},

代码解析:以上代码中switchChange()方法,用于响应商品勾选发生变化的,得先去后台找到这个数据,然后做处理,先使用 async 修饰,

说明这个方法是个异步的方法,然后对异步的部分使用await 修饰,这样,系统发起阻塞,只有await后面的部分运行完毕,才会继续运行后面的代码!

重点就是: await后面必须一定是返回Promise对象,不管你是封装的函数还是代码块,否则写了await无效果!如果君想试试效果,建议多写几个

console.log(“A/B/C”)放不同位置,打印下,看谁先打印,就有印象了,其实async/await就是早期promise.then()语法的现代版本,

补充:

  1. 实际代码和页面很可能和我上面说到不一样,因为需求在变,我代码也一直在更新,我尽量保留代码痕迹。

全文完!


我的其他文章:

  只写原创,敬请关注

移动应用APP购物车(店铺系列二)的更多相关文章

  1. Web 开发人员和设计师必读文章推荐【系列二十九】

    <Web 前端开发精华文章推荐>2014年第8期(总第29期)和大家见面了.梦想天空博客关注 前端开发 技术,分享各类能够提升网站用户体验的优秀 jQuery 插件,展示前沿的 HTML5 ...

  2. Web 开发精华文章集锦(jQuery、HTML5、CSS3)【系列二十七】

    <Web 前端开发精华文章推荐>2014年第6期(总第27期)和大家见面了.梦想天空博客关注 前端开发 技术,分享各类能够提升网站用户体验的优秀 jQuery 插件,展示前沿的 HTML5 ...

  3. Web 前端开发人员和设计师必读精华文章【系列二十六】

    <Web 前端开发精华文章推荐>2014年第5期(总第26期)和大家见面了.梦想天空博客关注 前端开发 技术,分享各类能够提升网站用户体验的优秀 jQuery 插件,展示前沿的 HTML5 ...

  4. Web 前端开发精华文章推荐(HTML5、CSS3、jQuery)【系列二十三】

    <Web 前端开发精华文章推荐>2014年第2期(总第23期)和大家见面了.梦想天空博客关注 前端开发 技术,分享各类能够提升网站用户体验的优秀 jQuery 插件,展示前沿的 HTML5 ...

  5. Web 前端开发精华文章推荐(HTML5、CSS3、jQuery)【系列二十二】

    <Web 前端开发精华文章推荐>2014年第一期(总第二十二期)和大家见面了.梦想天空博客关注 前端开发 技术,分享各类能够提升网站用户体验的优秀 jQuery 插件,展示前沿的 HTML ...

  6. 【圣诞特献】Web 前端开发精华文章推荐【系列二十一】

    <Web 前端开发精华文章推荐>2013年第九期(总第二十一期)和大家见面了.梦想天空博客关注 前端开发 技术,分享各种增强网站用户体验的 jQuery 插件,展示前沿的 HTML5 和  ...

  7. Web 前端开发精华文章集锦(jQuery、HTML5、CSS3)【系列二十】

    <Web 前端开发精华文章推荐>2013年第八期(总第二十期)和大家见面了.梦想天空博客关注 前端开发 技术,分享各种增强网站用户体验的 jQuery 插件,展示前沿的 HTML5 和 C ...

  8. [知识库分享系列] 二、.NET(ASP.NET)

    最近时间又有了新的想法,当我用新的眼光在整理一些很老的知识库时,发现很多东西都已经过时,或者是很基础很零碎的知识点.如果分享出去大家不看倒好,更担心的是会误人子弟,但为了保证此系列的完整,还是选择分享 ...

  9. highcharts 结合phantomjs纯后台生成图片系列二之php2

    上篇文章中介绍了phantomjs的使用场景,方法. 本篇文章详细介绍使用php,highcharts 结合phantomjs纯后台生成图片.包含一步步详细的php代码 一.highcharts 结合 ...

  10. WCF编程系列(二)了解WCF

    WCF编程系列(二)了解WCF   面向服务     服务是复用进化的结果,起初的复用是函数,面向对象编程的出现使复用从函数上升到对象,随后面向组件编程又将复用从对象上升到组件,现在面向服务编程将复用 ...

随机推荐

  1. CDS标准视图:设备 I_Equipment

    视图名称:I_Equipment 视图类型:基础视图 视图内容: 设备编码和设备内容 设备来源及详细信息 有效期 事务代码: IE03,IH08 视图代码 点击查看代码 @EndUserText.la ...

  2. 并发编程 - 线程同步(四)之原子操作Interlocked详解一

    上一章我们了解了原子操作Interlocked类的设计原理及简单介绍,今天我们将对Interlocked的使用进行详细讲解. 在此之前我们先学习一个概念--原子操作. 01.Read方法 该方法用于原 ...

  3. ThreeJs-14HTML混合3D渲染

    一.css渲染器基本使用 使用场景:经常在一些3D物体左右还会跟随一些文字等,实际都是标签,应用场景非常广泛 先完成基本的3D,月球围绕地球转,这里就是给月球加了sin函数 然后现在就先创建一个标签体 ...

  4. pkill 踢出某个终端

    是ps命令和kill命令的结合,按照进程名来杀死指定进程 选项 -o:仅向找到的最小(起始)进程号发送信号: -n:仅向找到的最大(结束)进程号发送信号: -P:指定父进程号发送信号: -g:指定进程 ...

  5. linux mint安装Idea

    一.前言 这一节我们介绍在Linux下如何安装与破解Intellij idea2017.现在有很多公司开发环境都是Linux,所以掌握在Linux环境下使用Idea办公也是咱们必须得掌握的技能. 记住 ...

  6. Flink内存解释

    一.JobManager内存 JobManager 是 Flink 集群的控制单元. 它由三种不同的组件组成:ResourceManager.Dispatcher 和每个正在运行作业的 JobMast ...

  7. element vue 动态单选_VUE 动态构建混合数据Treeselect选择树,同时解决巨树问题

    今天在项目中需要通过行政区域选择,然后选择该行政区域下面的景区,也就是要构建行政区划.景区两表数据表的树.全国的行政区域到县已经3500多了,再加上景区会有几万个点,这棵选择树不论是在后台还是在前台构 ...

  8. APEX实战第1篇:本地部署拥有AI能力的APEX

    学会部署APEX是为了更好构建企业级AI应用打基础,比如企业级的知识平台.智能报表等. 先前在<手把手教你喂养 DeepSeek 本地模型>,使用AnythingLLM方式,虽然操作上已经 ...

  9. springboot+vue项目:工具箱

    常用账号管理:工作相关账号.游戏账号.各平台账号 加班调休管理:公司没有对应的系统,需要自己记录加班调休情况. 待办事项:方便记录待办,以提醒还有哪些事情没有办理. 待实现功能: 1.点击侧边栏菜单, ...

  10. Linux - 配置IP&主机名的快捷操作

    nmtui 执行以下命令可以进入一个可视化界面,进行IP的可视化配置.以及网络服务的重启(注意,这个重启是停止然后启动, 如果使用xshell进行操作会失去ssh连接,直连服务器时可这直接操作).主机 ...