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


Posted in Javascript onJuly 19, 2019

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

效果展示

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

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

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

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

定义的组件

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 插槽是一块模板,显示不显示,以及怎样显示由父组件来决定, 也就是把你想要滑动的区域插进去,剩下的内容都是官方文档定义好的,复制一遍就好了

固定头部

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

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

侧边栏以及弹出框

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

起初我的想法是用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>

外卖点餐

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

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

商品展示

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

异步问题

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

<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,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

Javascript 相关文章推荐
javascript英文日期(有时间)选择器
May 02 Javascript
Prototype源码浅析 Enumerable部分(二)
Jan 18 Javascript
从面试题学习Javascript 面向对象(创建对象)
Mar 30 Javascript
jquery实现固定顶部导航效果(仿蘑菇街)
Mar 21 Javascript
JavaScript通过prototype给对象定义属性用法实例
Mar 23 Javascript
JavaScript基于setTimeout实现计数的方法
May 08 Javascript
实例解析jQuery插件EasyUI最常用的表单验证规则
Nov 29 Javascript
jquery单击文字或图片内容放大并居中显示
Jun 23 jQuery
vue.js中父组件调用子组件的内部方法示例
Oct 22 Javascript
基于vue打包后字体和图片资源失效问题的解决方法
Mar 06 Javascript
小程序实现发表评论功能
Jul 06 Javascript
全面解析js中的原型,原型对象,原型链
Jan 25 Javascript
微信小程序 扭蛋抽奖机css3动画实现详解
Jul 19 #Javascript
Vue配置marked链接添加target=&quot;_blank&quot;的方法
Jul 19 #Javascript
vue-cli 项目打包完成后运行文件路径报错问题
Jul 19 #Javascript
Smartour 让网页导览变得更简单(推荐)
Jul 19 #Javascript
bootstrap Table实现合并相同行
Jul 19 #Javascript
Element-ui DatePicker显示周数的方法示例
Jul 19 #Javascript
element-ui 中使用upload多文件上传只请求一次接口
Jul 19 #Javascript
You might like
PHP 开发工具
2006/12/06 PHP
php中大括号作用介绍
2012/03/22 PHP
php递归获取目录内文件(包含子目录)封装类分享
2013/12/25 PHP
Thinkphp的volist标签嵌套循环使用教程
2014/07/08 PHP
php简单实现查询数据库返回json数据
2015/04/16 PHP
非常经典的PHP文件上传类分享
2016/05/15 PHP
php redis实现文章发布系统(用户投票系统)
2017/03/04 PHP
PHP pthreads v3下的Volatile简介与使用方法示例
2020/02/21 PHP
jQuery Ajax调用WCF服务详细教程
2015/03/31 Javascript
在HTML中插入JavaScript代码的示例
2015/06/03 Javascript
最全的Javascript编码规范(推荐)
2016/06/22 Javascript
基于Vuejs框架实现翻页组件
2020/06/29 Javascript
从零学习node.js之express入门(六)
2017/02/25 Javascript
微信小程序注册60s倒计时功能 使用JS实现注册60s倒计时功能
2017/08/16 Javascript
React数据传递之组件内部通信的方法
2017/12/31 Javascript
vue.js分页中单击页码更换页面内容的方法(配合spring springmvc)
2018/02/10 Javascript
webpack 最佳配置指北(推荐)
2020/01/07 Javascript
在Python中使用元类的教程
2015/04/28 Python
python实现将文本转换成语音的方法
2015/05/28 Python
Python统计时间内的并发数代码实例
2019/12/28 Python
Python 使用Opencv实现目标检测与识别的示例代码
2020/09/08 Python
python基于pygame实现飞机大作战小游戏
2020/11/19 Python
Python 实现RSA加解密文本文件
2020/12/30 Python
Gibson London官网:以地道的英国男装而著称
2019/12/06 全球购物
教师自荐书
2013/10/08 职场文书
模具设计与制造专业推荐信
2014/02/16 职场文书
活动总结格式范文
2014/04/26 职场文书
青年文明号口号
2014/06/17 职场文书
责任书格式范文
2014/07/28 职场文书
工程部部长岗位职责
2015/02/12 职场文书
2015年党员个人自我评价
2015/03/03 职场文书
公积金贷款承诺书
2015/04/30 职场文书
python实现调用摄像头并拍照发邮箱
2021/04/27 Python
Python中异常处理用法
2021/11/27 Python
vue报错function () { [native code] },无法出现我们想要的内容 Unknown custom element
2022/04/11 Vue.js
php解析非标准json、非规范json的方式实例
2022/05/10 PHP