Vue實(shí)戰(zhàn)教程之仿肯德基宅急送App
Vue學(xué)習(xí)有一段時(shí)間了,就想著用Vue來(lái)寫(xiě)個(gè)項(xiàng)目練練手,弄了半個(gè)月,到今天為止也算勉強(qiáng)能看了。
由于不知道怎么拿手機(jī)App的接口,并且KFC電腦端官網(wǎng)真的...一言難盡,所以項(xiàng)目所有數(shù)據(jù)都是我截圖然后寫(xiě)在EasyMock里的,有需要的同學(xué)可以自取
技術(shù)棧
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
因?yàn)槊總€(gè)頁(yè)面都需要滑動(dòng),所以一開(kāi)始就把scroll組件封裝好,之后使用的話(huà)引入一下就行了
<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 滾動(dòng)的時(shí)候會(huì)派發(fā)scroll事件,會(huì)節(jié)流。
* 2 滾動(dòng)的時(shí)候?qū)崟r(shí)派發(fā)scroll事件,不會(huì)節(jié)流。
* 3 除了實(shí)時(shí)派發(fā)scroll事件,在swipe的情況下仍然能實(shí)時(shí)派發(fā)scroll事件
*/
probeType: {
type: Number,
default: 1
},
/**
* 點(diǎn)擊列表是否派發(fā)click事件
*/
click: {
type: Boolean,
default: true
},
/**
* 是否開(kāi)啟橫向滾動(dòng)
*/
scrollX: {
type: Boolean,
default: false
},
/**
* 是否派發(fā)滾動(dòng)事件
*/
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
},
/**
* 當(dāng)數(shù)據(jù)更新后,刷新scroll的延時(shí)。
*/
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ā)滾動(dòng)事件
if (this.listenScroll) {
this.scroll.on('scroll', (pos) => {
this.$emit('scroll', pos)
})
}
// 是否派發(fā)滾動(dòng)到底部事件,用于上拉加載
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) => {
// 下拉動(dòng)作
if (pos.y > 50) {
this.$emit('pulldown')
}
})
}
// 是否派發(fā)列表滾動(dòng)開(kāi)始的事件
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 插槽是一塊模板,顯示不顯示,以及怎樣顯示由父組件來(lái)決定, 也就是把你想要滑動(dòng)的區(qū)域插進(jìn)去,剩下的內(nèi)容都是官方文檔定義好的,復(fù)制一遍就好了
固定頭部

頭部相對(duì)頁(yè)面是固定的,這里我把頭部都封裝成了組件,在主頁(yè)面引入頭部,要滑動(dòng)的部分放入上面定義好的scroll組件即可
側(cè)邊欄以及彈出框

起初我的想法是用router-link直接跳轉(zhuǎn),然后發(fā)現(xiàn)這樣做頁(yè)面會(huì)自帶導(dǎo)航欄,于是我決定通過(guò)CSS動(dòng)態(tài)綁定來(lái)實(shí)現(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組件,最外面綁定了一個(gè)動(dòng)態(tài)樣式showbar,然后把整體的初始位置設(shè)在屏幕之外,當(dāng)傳入?yún)?shù)為true時(shí)再回來(lái),用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拿到對(duì)象,然后傳給computed屬性,對(duì)象可以直接使用
computed: {
...mapGetters([
'showSidebar'
])
},
當(dāng)需要顯示的時(shí)候使用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 {
},
],
// 左側(cè)高亮元素的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>
外賣(mài)點(diǎn)餐

這里參考的是慕課網(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">預(yù)約</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>
這里通過(guò)currentIndex和index做對(duì)比,來(lái)確認(rèn)是否添加current類(lèi),通過(guò)添加current類(lèi)來(lái)實(shí)現(xiàn)當(dāng)前頁(yè)面的區(qū)域的樣式變化,他們之間的對(duì)比關(guān)系也就是menu區(qū)域和foods區(qū)域的顯示區(qū)域的對(duì)比關(guān)系
需要注意的是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>
購(gòu)物車(chē)
<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">購(gòu)物車(chē)</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>
購(gòu)物車(chē)列表的顯示和隱藏以及清空按鈕是通過(guò)數(shù)據(jù)fold來(lái)決定的,購(gòu)物車(chē)列表是通過(guò)計(jì)算屬性listshow來(lái)實(shí)現(xiàn),清空按鈕也是通過(guò)設(shè)置count屬性來(lái)實(shí)現(xiàn),這樣都達(dá)到了不用操作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>
操作按鈕
這個(gè)模塊主要通過(guò)三個(gè)小模塊實(shí)現(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方法,默認(rèn)會(huì)傳入event原生dom事件,food數(shù)據(jù)是從父組件傳入的,所以對(duì)這個(gè)數(shù)據(jù)的修改,也能夠反應(yīng)到父組件,也因?yàn)橘?gòu)物車(chē)的數(shù)據(jù)也是從父組件傳入的,使用同一個(gè)food數(shù)據(jù),從而關(guān)聯(lián)到購(gòu)物車(chē)的購(gòu)買(mǎi)數(shù)量統(tǒng)計(jì)。
<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>
異步問(wèn)題

<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)嵌套,整個(gè)DOM結(jié)構(gòu)都是循環(huán)出來(lái)的,而better-scroll需要操作DOM結(jié)構(gòu),要實(shí)現(xiàn)橫向滑動(dòng)效果,難免會(huì)有異步問(wèn)題。
可是無(wú)論我使用.then或者$nextTick都無(wú)法掛載better-scroll,查閱了大量文檔也無(wú)法解決,最后只能使用原生的overflow-X,若是有解決辦法,歡迎提出,感激不盡!
結(jié)語(yǔ)
總的來(lái)說(shuō)這個(gè)項(xiàng)目還有很多不足,實(shí)現(xiàn)的功能也很少,后續(xù)我會(huì)繼續(xù)改進(jìn)。
如果這篇文章對(duì)你有幫助,不妨點(diǎn)個(gè)贊吧!
總結(jié)
以上所述是小編給大家介紹的Vue實(shí)戰(zhàn)教程之仿肯德基宅急送App,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
如果你覺(jué)得本文對(duì)你有幫助,歡迎轉(zhuǎn)載,煩請(qǐng)注明出處,謝謝!
相關(guān)文章
詳解vantUI框架在vue項(xiàng)目中的應(yīng)用踩坑
這篇文章主要介紹了詳解vantUI框架在vue項(xiàng)目中的應(yīng)用踩坑,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-12-12
Vue中img的src是動(dòng)態(tài)渲染時(shí)不顯示的解決
今天小編就為大家分享一篇Vue中img的src是動(dòng)態(tài)渲染時(shí)不顯示的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-11-11
Element中的Cascader(級(jí)聯(lián)列表)動(dòng)態(tài)加載省\市\(zhòng)區(qū)數(shù)據(jù)的方法
這篇文章主要介紹了Element中的Cascader(級(jí)聯(lián)列表)動(dòng)態(tài)加載省\市\(zhòng)區(qū)數(shù)據(jù)的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-03-03
如何使用Vue3實(shí)現(xiàn)文章內(nèi)容中多個(gè)"關(guān)鍵詞"標(biāo)記高亮顯示
高亮顯示是我們?nèi)粘i_(kāi)發(fā)中經(jīng)常會(huì)遇到的需求,下面這篇文章主要給大家介紹了關(guān)于如何使用Vue3實(shí)現(xiàn)文章內(nèi)容中多個(gè)"關(guān)鍵詞"標(biāo)記高亮顯示的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-11-11
Vue+Openlayers實(shí)現(xiàn)實(shí)時(shí)坐標(biāo)點(diǎn)展示
這篇文章主要為大家詳細(xì)介紹了Vue+Openlayers實(shí)現(xiàn)實(shí)時(shí)坐標(biāo)點(diǎn)展示,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
vue項(xiàng)目多環(huán)境配置(.env)的實(shí)現(xiàn)
最常見(jiàn)的多環(huán)境配置,就是開(kāi)發(fā)環(huán)境配置,和生產(chǎn)環(huán)境配置,本文主要介紹了vue項(xiàng)目多環(huán)境配置的實(shí)現(xiàn),感興趣的可以了解一下2021-07-07
Vue打包部署到Nginx時(shí),css樣式不生效的解決方式
這篇文章主要介紹了Vue打包部署到Nginx時(shí),css樣式不生效的解決方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08

