今天仍是說移動app開發,店鋪系列文章,咱們常常去超市使用購物車,即一個臨時的儲物空間,用完清空回收。我大兄弟說,前端
平時很忙,錄入訂單的目錄不少,臨時有事回來要能夠繼續填寫,提交訂單後纔算結束,這就是一個典型的購物車場景了。那vue
系統的購物車如何實現?如今就來實戰一把,作個如淘寶類的購物車。git
準備:es6
Idea2019.03/Gradle6.0.1/JDK11.0.4/Lombok0.28/SpringBoot2.2.4RELEASE/mybatisPlus3.3.0/Soul2.1.2/Dubbo2.7.5/Mysql8.0.11github
/Vue2.5/OSS/Hbuilder2.6.1web
難度: 新手--戰士--老兵--大師sql
目標:vuex
步驟:數據庫
爲了碰見各類問題,同時保持時效性,我儘可能使用最新的軟件版本。代碼地址:https://github.com/xiexiaobiao/vehicle-shop-mobile.gitjson
後端代碼量約1.5萬,雙前端約1.5萬,技術仍是很具表明性的,否則就很差意思拿出來講事了,詳細可看Git庫說明,下圖是後端代碼量分析:
Web管理界面:須要密碼的,請公衆號留言。
手機端:使用Hbuilder編碼,Uniapp框架,再隨手撿了幾個UI拿來大改了幾下,基本形狀以下:
先說存儲,有三套方案;一是直接數據庫端存儲,與後臺交互多,會增長流量和業務複雜度;二是Localstorage存儲,持久化到本地瀏覽器端,除非主動
刪除,不然永久存在;三是Session級別存儲,使用vuex組件,會話級存儲,app關閉即清空。再說vuex組件,是vue框架組件之一,其最經常使用的功能就是
存儲用戶登陸狀態,由於系統不少地方的使用都須要進行登陸驗證,咱們能夠在用戶登陸以後,將登陸狀態寫入vuex,那系統其餘地方就能夠隨用隨取,
我這裏即說第三套方案,使用vuex作緩存實現購物車。Vuex基礎知識,略!請君自查!
思路:創建一個vuex數組,即購物車存儲空間,選擇商品後,即加入該數組中,若是數量等屬性有更新,也同步到該該數組,只要app不關閉就能夠打開
購物車繼續編輯,直到提交訂單時清空該數組。
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
以上代碼解析:
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()
以上代碼解析:
若是使用同步方法:this.$store.commit("deleteCartItem",itemIdToDel)
若是使用異步方法:this.$store.dispatch("addCartAsync",itemIdToAdd)
vehicle-shop-app/pages/product/product.vue
商品詳細頁面:
js關鍵代碼:
// 加入購物車 addCartItem(){ // vuex保存 this.$store.commit('addCartItems',this.product); uni.showToast({ title: "加購物車成功!", icon: 'info' }); },
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>
以上代碼解析:
import { mapGetters, mapState,mapActions,mapMutations } from 'vuex'
以後,就能夠直接使用'cartItems'變量了,系統會自動生成,看loadData()中就是let list = this.cartItems;
//數量 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(); },
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,打開購物車頁面,裏面商品就會存在,固然,別忘了,提交訂單時,清空下購物車,
由於出了超市,購物車得還給人家,不能帶回家!
前面一篇,說到後臺請求數據都是異步的,處理很差就是頁面渲染完畢,結果後臺數據纔過來,這就尷尬了。因此這裏我舉個例子解決
下這個問題:
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()語法的現代版本,
全文完!
個人其餘文章:
只寫原創,敬請關注