Vue實戰(zhàn)教程之仿肯德基宅急送App
Vue學習有一段時間了,就想著用Vue來寫個項目練練手,弄了半個月,到今天為止也算勉強能看了。
由于不知道怎么拿手機App的接口,并且KFC電腦端官網(wǎng)真的...一言難盡,所以項目所有數(shù)據(jù)都是我截圖然后寫在EasyMock里的,有需要的同學可以自取
技術棧
vue + webpack + vuex + axios
文件目錄
│ App.vue │ main.js │ ├─assets │ logo.png │ ├─components │ │ cartcontrol.vue │ │ code.vue │ │ coupon.vue │ │ mineHeader.vue │ │ scroll.vue │ │ shopHeader.vue │ │ sidebar.vue │ │ submitBar.vue │ │ takeout.vue │ │ wallet.vue │ │ │ └─tabs │ Other.vue │ Outward.vue │ Selfhelp.vue │ Vgold.vue │ ├─pages │ ├─home │ │ home.vue │ │ │ ├─mine │ │ mine.vue │ │ │ ├─order │ │ order.vue │ │ │ └─shop │ shop.vue │ ├─router │ index.js │ └─vuex │ store.js │ types.js │ └─modules com.js cou.js take.js
效果展示
定義的組件
better-scroll
因為每個頁面都需要滑動,所以一開始就把scroll組件封裝好,之后使用的話引入一下就行了
<template> <div ref="wrapper"> <slot></slot> </div> </template> <script> import BScroll from 'better-scroll'; const DIRECTION_H = 'horizontal'; const DIRECTION_V = 'vertical'; export default { name: 'scroll', props: { /** * 1 滾動的時候會派發(fā)scroll事件,會節(jié)流。 * 2 滾動的時候實時派發(fā)scroll事件,不會節(jié)流。 * 3 除了實時派發(fā)scroll事件,在swipe的情況下仍然能實時派發(fā)scroll事件 */ probeType: { type: Number, default: 1 }, /** * 點擊列表是否派發(fā)click事件 */ click: { type: Boolean, default: true }, /** * 是否開啟橫向滾動 */ scrollX: { type: Boolean, default: false }, /** * 是否派發(fā)滾動事件 */ listenScroll: { type: Boolean, default: false }, /** * 列表的數(shù)據(jù) */ data: { type: Array, default: null }, pullup: { type: Boolean, default: false }, pulldown: { type: Boolean, default: false }, beforeScroll: { type: Boolean, default: false }, /** * 當數(shù)據(jù)更新后,刷新scroll的延時。 */ refreshDelay: { type: Number, default: 20 }, direction: { type: String, default: DIRECTION_V } }, methods: { _initScroll() { if(!this.$refs.wrapper) { return } this.scroll = new BScroll(this.$refs.wrapper, { probeType: this.probeType, click: this.click, eventPassthrough: this.direction === DIRECTION_V ? DIRECTION_H : DIRECTION_V }) // 是否派發(fā)滾動事件 if (this.listenScroll) { this.scroll.on('scroll', (pos) => { this.$emit('scroll', pos) }) } // 是否派發(fā)滾動到底部事件,用于上拉加載 if (this.pullup) { this.scroll.on('scrollEnd', () => { if (this.scroll.y <= (this.scroll.maxScrollY + 50)) { this.$emit('scrollToEnd') } }) } // 是否派發(fā)頂部下拉事件,用于下拉刷新 if (this.pulldown) { this.scroll.on('touchend', (pos) => { // 下拉動作 if (pos.y > 50) { this.$emit('pulldown') } }) } // 是否派發(fā)列表滾動開始的事件 if (this.beforeScroll) { this.scroll.on('beforeScrollStart', () => { this.$emit('beforeScroll') }) } }, disable() { // 代理better-scroll的disable方法 this.scroll && this.scroll.disable() }, enable() { // 代理better-scroll的enable方法 this.scroll && this.scroll.enable() }, refresh() { // 代理better-scroll的refresh方法 this.scroll && this.scroll.refresh() }, scrollTo() { // 代理better-scroll的scrollTo方法 this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments) }, scrollToElement() { // 代理better-scroll的scrollToElement方法 this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments) }, }, mounted() { setTimeout(() => { this._initScroll() },20) }, watch: { data () { setTimeout(() => { this.refresh() },this.refreshDelay) } }, } </script> <style> </style>
slot 插槽是一塊模板,顯示不顯示,以及怎樣顯示由父組件來決定, 也就是把你想要滑動的區(qū)域插進去,剩下的內(nèi)容都是官方文檔定義好的,復制一遍就好了
固定頭部
頭部相對頁面是固定的,這里我把頭部都封裝成了組件,在主頁面引入頭部,要滑動的部分放入上面定義好的scroll組件即可
側邊欄以及彈出框
起初我的想法是用router-link
直接跳轉,然后發(fā)現(xiàn)這樣做頁面會自帶導航欄,于是我決定通過CSS動態(tài)綁定來實現(xiàn)它
<template> <div class="sidebar"> <div class="sidebar-con" :class="{showbar: showSidebar}"> <div class="navbar_left" @click="backTo"> <img src="../pages/mine/zuo.png" alt=""> </div> <van-tree-select :height="850" :items="items" :main-active-index="mainActiveIndex" :active-id="activeId" @navclick="onNavClick" @itemclick="onItemClick"/> </div> </div> </template>
樣式用的是Vant UI組件,最外面綁定了一個動態(tài)樣式showbar,然后把整體的初始位置設在屏幕之外,當傳入?yún)?shù)為true時再回來,用Vuex管理它的狀態(tài)
.sidebar-con { position: absolute; top: 0; left: -400px; transform: translateZ(0); opacity: 0; width: 100%; z-index: 1002; height: 100%; overflow: auto; transition: all 0.3s ease; } .showbar { transform: translateX(400px); opacity: 1; }
Vuex狀態(tài)管理
const state = { showSidebar: false } const mutations = { [types.COM_SHOW_SIDE_BAR] (state, status) { state.showSidebar = status } } const actions = { setShowSidebar ({commit}, status) { commit(types.COM_SHOW_SIDE_BAR, status) } } const getters = { showSidebar: state => state.showSidebar }
用mapGetter拿到對象,然后傳給computed屬性,對象可以直接使用
computed: { ...mapGetters([ 'showSidebar' ]) },
當需要顯示的時候使用dispatch將參數(shù)傳入 this.$store.dispatch('setShowSidebar', true)
整體代碼
<template> <div class="sidebar"> <div class="sidebar-con" :class="{showbar: showSidebar}"> <div class="navbar_left" @click="backTo"> <img src="../pages/mine/zuo.png" alt=""> </div> <van-tree-select :height="850" :items="items" :main-active-index="mainActiveIndex" :active-id="activeId" @navclick="onNavClick" @itemclick="onItemClick"/> </div> </div> </template> <script> import { TreeSelect } from 'vant'; import { mapGetters } from 'vuex'; export default { data() { return { }, ], // 左側高亮元素的index mainActiveIndex: 0, // 被選中元素的id activeId: 1 }; }, computed: { ...mapGetters([ 'showSidebar' ]) }, methods: { onNavClick(index) { this.mainActiveIndex = index; }, onItemClick(data) { this.activeId = data.id; this.$emit('active', data.text) this.$store.dispatch('setShowSidebar', false) }, backTo(){ this.$store.dispatch('setShowSidebar', false) }, } } </script> <style scoped> .sidebar-con { position: absolute; top: 0; left: -400px; transform: translateZ(0); opacity: 0; width: 100%; z-index: 1002; height: 100%; overflow: auto; transition: all 0.3s ease; } .showbar { transform: translateX(400px); opacity: 1; } .navbar_left { background-color: #da3a35; } .navbar_left img { width: 25px; height: 25px; margin-left: 3vw; margin-top: 5px; } </style>
外賣點餐
這里參考的是慕課網(wǎng)黃奕大大的課程,課程地址
商品展示
<template> <div class="takeout" :class="{showtakeout: showTakeout}"> <div class="goods"> <div class="header"> <div class="navbar_left" @click="backTo"> <img src="../pages/shop/zuo.png" alt=""> </div> <div class="appointment"> <div class="btn"> <div class="yy">預約</div> <div class="Kcoffee">K咖啡</div> </div> <div class="bag"> <router-link style="color: #000" to="/coupon"> <div class="bagtext"> 卡包<p>3</p>張 </div> </router-link> </div> </div> </div> <div class="goodList"> <div class="menu-wrapper" ref="menuWrapper"> <ul> <li v-for="(item,index) in goods" :key="index" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)" > <span class="text border-1px"> {{item.name}} </span> </li> </ul> </div> <div class="foods-wrapper" ref="foodsWrapper"> <ul> <li v-for="(item,index) in goods" :key="index" class="food-list" ref="foodList"> <h1 class="title">{{item.name}}</h1> <ul> <li v-for="(food,index) in item.foods" :key="index" class="food-item border-1px" @click="selectFood(index, $event)" > <div class="icon"> <img :src="food.image"> </div> <div class="content"> <h2 class="name">{{food.name}}</h2> <div class="price"> <span class="now">¥{{food.price}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol @add="addFood" :food="food"></cartcontrol> </div> </div> </li> </ul> </li> </ul> </div> </div> <submit-bar ref="shopcart" :selectFoods="selectFoods"></submit-bar> </div> </div> </template>
這里通過currentIndex
和index做對比,來確認是否添加current類,通過添加current類來實現(xiàn)當前頁面的區(qū)域的樣式變化,他們之間的對比關系也就是menu區(qū)域和foods區(qū)域的顯示區(qū)域的對比關系
需要注意的是vue傳遞原生事件使用$event
<script> import BScroll from 'better-scroll' import cartcontrol from './cartcontrol' import submitBar from './submitBar' import { mapGetters } from 'vuex' export default { name: 'takeout', data() { return { goods: [], listHeight: [], scrollY: 0 } }, components: { cartcontrol, submitBar }, computed: { ...mapGetters([ 'showTakeout' ]), currentIndex () { for(let i = 0; i < this.listHeight.length; i++) { let height1 = this.listHeight[i - 1] let height2 = this.listHeight[i] if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)) { return i } } return 0 }, selectFoods () { let foods = [] this.goods.forEach(good => { good.foods.forEach(food => { if (food.count) { foods.push(food) } }) }) return foods } }, methods: { backTo () { this.$store.dispatch('setShowTakeout', false) }, selectMenu(index, event) { if (!event._constructed) { return; } let foodList = this.$refs.foodList; let el = foodList[index]; this.foodsScroll.scrollToElement(el, 300); }, selectFood(food, event) { if (!event._constructed) { return; } this.selectedFood = food; }, _initScroll() { this.meunScroll = new BScroll(this.$refs.menuWrapper, { click: true }) this.foodsScroll = new BScroll(this.$refs.foodsWrapper, { click: true, probeType: 3 }) this.foodsScroll.on('scroll', pos => { this.scrollY = Math.abs(Math.round(pos.y)) }) }, _calculateHeight () { let foodList = this.$refs.foodList let height = 0 for (let i = 0; i < foodList.length; i++) { let item = foodList[i] height += item.clientHeight this.listHeight.push(height) } }, }, created () { this.$http.get('https://www.easy-mock.com/mock/5ca49494ea0dc52bf3b67f4e/example/takeout') .then(res => { if (res.data.errno === 0) { this.goods = res.data.data this.$nextTick(() => { this._initScroll() this._calculateHeight() }) } }) } } </script>
購物車
<template> <div class="submitBar"> <van-submit-bar :loading="setloading" :price="totalPrice" button-text="提交訂單" @submit="onSubmit" > <div class="shoppingCart" @click="toggleList"> <img src="../../images/gwc.png" alt=""> <span v-if="selectFoods.length > 0">{{selectFoods.length}}</span> </div> </van-submit-bar> <transition name="fold"> <div class="shopcart-list" v-show="listShow"> <div class="list-header"> <h1 class="title">購物車</h1> <span class="empty" @click="empty">清空</span> </div> <div class="list-content" ref="listContent"> <ul> <li class="food" v-for="(food, index) in selectFoods" :key="index"> <span class="name">{{food.name}}</span> <div class="price"> <span>¥{{food.price*food.count}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol @add="addFood" :food="food"></cartcontrol> </div> </li> </ul> </div> </div> </transition> <transition name="fade"> <div class="list-mask" @click="hideList" v-show="listShow"></div> </transition> </div> </template>
購物車列表的顯示和隱藏以及清空按鈕是通過數(shù)據(jù)fold來決定的,購物車列表是通過計算屬性listshow來實現(xiàn),清空按鈕也是通過設置count屬性來實現(xiàn),這樣都達到了不用操作dom就可以改變dom行為的效果。
<script> import { SubmitBar } from 'vant'; import BScroll from 'better-scroll'; import cartcontrol from './cartcontrol'; export default { props: { selectFoods: { type: Array, default() { return [ { price: 10, count: 1 } ] } }, }, data() { return { setloading: false, fold: true } }, computed: { totalCount () { let count = 0 this.selectFoods.forEach((food) => { count += food.count }) return count }, totalPrice () { let total = 0 this.selectFoods.forEach((food) => { total += food.price * food.count * 100 }) return total }, listShow () { if (!this.totalCount) { this.fold = true return false } let show = !this.fold if (show) { this.$nextTick(() => { if (!this.scroll) { this.scroll = new BScroll(this.$refs.listContent, { click: true }) } else { this.scroll.refresh() } }) } return show } }, methods: { toggleList(){ console.log(this.totalCount) if (!this.totalCount) { return; } this.fold = !this.fold; }, onSubmit() { this.setloading = true }, empty() { this.selectFoods.forEach((food) => { food.count = 0; }); }, hideList() { this.fold = true; }, addFood() {} }, components: { cartcontrol } } </script>
操作按鈕
這個模塊主要通過三個小模塊實現(xiàn),刪除按鈕,顯示數(shù)量塊,增加按鈕
<template> <div class="cartcontrol"> <transition name="move"> <div class="cart-decrease" v-show="food.count > 0" @click="decreaseCart"> <div class="inner"> <img width="15px" height="15px" src="../../images/jian.png" alt=""> </div> </div> </transition> <div class="cart-count" v-show="food.count > 0">{{food.count}}</div> <div class="cart-add" @click="addCart"> <img width="15px" height="15px" src="../../images/add.png" alt=""> </div> </div> </template>
addCart以及decreaseCart方法,默認會傳入event原生dom事件,food數(shù)據(jù)是從父組件傳入的,所以對這個數(shù)據(jù)的修改,也能夠反應到父組件,也因為購物車的數(shù)據(jù)也是從父組件傳入的,使用同一個food數(shù)據(jù),從而關聯(lián)到購物車的購買數(shù)量統(tǒng)計。
<script> export default { name: "cartcontrol", props: { food: { type: Object } }, data() { return { } }, methods: { addCart (event) { console.log(event) if (!event._constructed) { return } if (!this.food.count) { this.$set(this.food, 'count', 1) } else { this.food.count++ } this.$emit('add', event.target) }, decreaseCart (event) { if (!event._constructed) { return } if (this.food.count) { this.food.count-- } } }, } </script>
異步問題
<div class="various" v-for="(item,index) in various" :key="index"> <div class="title"> <div class="strip"></div> <p>{{item[0].name}}</p> <div class="strip"></div> </div> <div class="various_img"> <div class="various_title"> <img :src="item[0].urll" alt=""> </div> <div ref="listwrapper" class="index"> <div class="various_list"> <div class="various_box" v-for="(u,i) in item.slice(1)" :key="i"> <img :src="u.url" alt=""> </div> </div> </div> </div> </div>
這里循環(huán)嵌套,整個DOM結構都是循環(huán)出來的,而better-scroll
需要操作DOM結構,要實現(xiàn)橫向滑動效果,難免會有異步問題。
可是無論我使用.then或者$nextTick
都無法掛載better-scroll
,查閱了大量文檔也無法解決,最后只能使用原生的overflow-X
,若是有解決辦法,歡迎提出,感激不盡!
結語
總的來說這個項目還有很多不足,實現(xiàn)的功能也很少,后續(xù)我會繼續(xù)改進。
如果這篇文章對你有幫助,不妨點個贊吧!
總結
以上所述是小編給大家介紹的Vue實戰(zhàn)教程之仿肯德基宅急送App,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
如果你覺得本文對你有幫助,歡迎轉載,煩請注明出處,謝謝!
相關文章
Element中的Cascader(級聯(lián)列表)動態(tài)加載省\市\(zhòng)區(qū)數(shù)據(jù)的方法
這篇文章主要介紹了Element中的Cascader(級聯(lián)列表)動態(tài)加載省\市\(zhòng)區(qū)數(shù)據(jù)的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-03-03如何使用Vue3實現(xiàn)文章內(nèi)容中多個"關鍵詞"標記高亮顯示
高亮顯示是我們?nèi)粘i_發(fā)中經(jīng)常會遇到的需求,下面這篇文章主要給大家介紹了關于如何使用Vue3實現(xiàn)文章內(nèi)容中多個"關鍵詞"標記高亮顯示的相關資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-11-11vue項目多環(huán)境配置(.env)的實現(xiàn)
最常見的多環(huán)境配置,就是開發(fā)環(huán)境配置,和生產(chǎn)環(huán)境配置,本文主要介紹了vue項目多環(huán)境配置的實現(xiàn),感興趣的可以了解一下2021-07-07