首页 > 编程 > JavaScript > 正文

Vue实战教程之仿肯德基宅急送App

2019-11-19 11:09:44
字体:
来源:转载
供稿:网友

Vue学习有一段时间了,就想着用Vue来写个项目练练手,弄了半个月,到今天为止也算勉强能看了。

由于不知道怎么拿手机App的接口,并且KFC电脑端官网真的...一言难尽,所以项目所有数据都是我截图然后写在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 滚动的时候会派发scroll事件,会节流。   * 2 滚动的时候实时派发scroll事件,不会节流。   * 3 除了实时派发scroll事件,在swipe的情况下仍然能实时派发scroll事件   */  probeType: {   type: Number,   default: 1  },  /**   * 点击列表是否派发click事件   */  click: {   type: Boolean,   default: true  },  /**   * 是否开启横向滚动   */  scrollX: {   type: Boolean,   default: false  },  /**   * 是否派发滚动事件   */  listenScroll: {   type: Boolean,   default: false  },  /**   * 列表的数据   */  data: {   type: Array,   default: null  },  pullup: {   type: Boolean,   default: false  },  pulldown: {   type: Boolean,   default: false  },  beforeScroll: {   type: Boolean,   default: false  },  /**   * 当数据更新后,刷新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   })   // 是否派发滚动事件   if (this.listenScroll) {    this.scroll.on('scroll', (pos) => {     this.$emit('scroll', pos)    })   }   // 是否派发滚动到底部事件,用于上拉加载   if (this.pullup) {    this.scroll.on('scrollEnd', () => {     if (this.scroll.y <= (this.scroll.maxScrollY + 50)) {      this.$emit('scrollToEnd')     }    })   }   // 是否派发顶部下拉事件,用于下拉刷新   if (this.pulldown) {    this.scroll.on('touchend', (pos) => {     // 下拉动作     if (pos.y > 50) {      this.$emit('pulldown')     }    })   }   // 是否派发列表滚动开始的事件   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 插槽是一块模板,显示不显示,以及怎样显示由父组件来决定, 也就是把你想要滑动的区域插进去,剩下的内容都是官方文档定义好的,复制一遍就好了

固定头部

 

头部相对页面是固定的,这里我把头部都封装成了组件,在主页面引入头部,要滑动的部分放入上面定义好的scroll组件即可

侧边栏以及弹出框

起初我的想法是用router-link直接跳转,然后发现这样做页面会自带导航栏,于是我决定通过CSS动态绑定来实现它

<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组件,最外面绑定了一个动态样式showbar,然后把整体的初始位置设在屏幕之外,当传入参数为true时再回来,用Vuex管理它的状态

.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状态管理

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将参数传入 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>

外卖点餐


这里参考的是慕课网黄奕大大的课程,课程地址

商品展示

<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类来实现当前页面的区域的样式变化,他们之间的对比关系也就是menu区域和foods区域的显示区域的对比关系

需要注意的是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>

购物车列表的显示和隐藏以及清空按钮是通过数据fold来决定的,购物车列表是通过计算属性listshow来实现,清空按钮也是通过设置count属性来实现,这样都达到了不用操作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>

操作按钮

这个模块主要通过三个小模块实现,删除按钮,显示数量块,增加按钮

<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数据是从父组件传入的,所以对这个数据的修改,也能够反应到父组件,也因为购物车的数据也是从父组件传入的,使用同一个food数据,从而关联到购物车的购买数量统计。

<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>

这里循环嵌套,整个DOM结构都是循环出来的,而better-scroll需要操作DOM结构,要实现横向滑动效果,难免会有异步问题。

可是无论我使用.then或者$nextTick都无法挂载better-scroll,查阅了大量文档也无法解决,最后只能使用原生的overflow-X,若是有解决办法,欢迎提出,感激不尽!

结语

总的来说这个项目还有很多不足,实现的功能也很少,后续我会继续改进。

如果这篇文章对你有帮助,不妨点个赞吧!

GitHub地址

总结

以上所述是小编给大家介绍的Vue实战教程之仿肯德基宅急送App,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对武林网网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表