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 parseInt与Number函数的区别
Jan 21 Javascript
Google AJAX 搜索 API实现代码
Nov 17 Javascript
Javascript 自适应高度的Tab选项卡
Apr 05 Javascript
javascript面向对象特性代码实例
Jun 12 Javascript
js实现交换运动效果的方法
Apr 10 Javascript
js实现精美的图片跟随鼠标效果实例
May 16 Javascript
第四章之BootStrap表单与图片
Apr 25 Javascript
仿iframe效果Aajx文件上传实例
Nov 18 Javascript
js实现关闭网页出现是否离开提示
Dec 07 Javascript
Vue用v-for给src属性赋值的方法
Mar 03 Javascript
vue数组对象排序的实现代码
Jun 20 Javascript
Angular5中提取公共组件之radio list的实例代码
Jul 10 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
在windows平台上构建自己的PHP实现方法(仅适用于php5.2)
2013/07/05 PHP
PHP实现生成唯一编号(36进制的不重复编号)
2014/07/01 PHP
PHP之图片上传类实例代码(加了缩略图)
2016/06/30 PHP
浅谈php中的循环while、do...while、for、foreach四种循环
2016/11/05 PHP
超棒的javascript页面顶部卷动广告效果
2007/12/01 Javascript
JavaScript Sort 表格排序
2009/10/31 Javascript
JSON JQUERY模板实现说明
2010/07/03 Javascript
jquery nth-child()选择器的简单应用
2010/07/10 Javascript
js的alert样式如何更改如背景颜色
2014/01/22 Javascript
JQuery实现防止退格键返回的方法
2015/02/12 Javascript
JS实现控制表格行文本对齐的方法
2015/03/30 Javascript
jQuery动态效果显示人物结构关系图的方法
2015/05/07 Javascript
JavaScript实现DIV层拖动及动态增加新层的方法
2015/05/12 Javascript
javascript如何实现360度全景照片问题汇总
2016/04/04 Javascript
Vue中封装input组件的实例详解
2017/10/17 Javascript
实现div内部滚动条滚动到底部和顶部的代码
2017/11/15 Javascript
jQuery基于闭包实现的显示与隐藏div功能示例
2018/06/09 jQuery
4 种滚动吸顶实现方式的比较
2019/04/09 Javascript
Vue 指令实现按钮级别权限管理功能
2019/04/23 Javascript
Js Snowflake(雪花算法)生成随机ID的实现方法
2020/08/26 Javascript
python开发之for循环操作实例详解
2015/11/12 Python
Python队列的定义与使用方法示例
2017/06/24 Python
Python中生成器和迭代器的区别详解
2018/02/10 Python
pandas对指定列进行填充的方法
2018/04/11 Python
Python分布式进程中你会遇到的问题解析
2019/05/28 Python
python简单鼠标自动点击某区域的实例
2019/06/25 Python
python 字典套字典或列表的示例
2019/12/16 Python
Python图像处理库PIL的ImageDraw模块介绍详解
2020/02/26 Python
Python使用Matlab命令过程解析
2020/06/04 Python
科茨沃尔德家居商店:Scotts of Stow
2018/06/29 全球购物
Skyscanner台湾:全球知名的旅行比价引擎
2018/07/01 全球购物
Whistles官网:英国女装品牌
2020/08/14 全球购物
举例说明类变量和实例变量的区别
2016/06/30 面试题
yy生日主持词
2014/03/20 职场文书
灵山大佛导游词
2015/02/04 职场文书
IDEA 链接Mysql数据库并执行查询操作的完整代码
2021/05/20 MySQL