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 相关文章推荐
jquery中实现简单的tabs插件功能的代码
Mar 02 Javascript
jquery SweetAlert插件实现响应式提示框
Aug 18 Javascript
jQuery文本框得到与失去焦点动态改变样式效果
Sep 08 Javascript
详解JavaScript 中getElementsByName在IE中的注意事项
Feb 21 Javascript
Vue实现选择城市功能
May 27 Javascript
webpack 模块热替换原理
Apr 09 Javascript
详解Vue 动态组件与全局事件绑定总结
Nov 11 Javascript
vue2.0中set添加属性后视图不能更新的解决办法
Feb 22 Javascript
微信小程序实现获取用户信息并存入数据库操作示例
May 07 Javascript
Vue简单封装axios之解决post请求后端接收不到参数问题
Feb 16 Javascript
微信小游戏中three.js离屏画布的示例代码
Oct 12 Javascript
ant design vue导航菜单与路由配置操作
Oct 28 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
先进的自动咖啡技术,真的可以取代咖啡师吗?
2021/03/06 冲泡冲煮
PHP 变量类型的强制转换
2009/10/23 PHP
php set_time_limit(0) 设置程序执行时间的函数
2010/05/26 PHP
Apache服务器无法使用的解决方法
2013/05/08 PHP
php微信公众号开发模式详解
2016/11/28 PHP
PHP时间类完整代码实例
2021/02/26 PHP
jQuery autocomplete插件修改
2009/04/17 Javascript
JavaScript Accessor实现说明
2010/12/06 Javascript
JS加jquery简单实现标签元素的显示或隐藏
2013/09/23 Javascript
关闭ie窗口清除Session的解决方法
2014/01/10 Javascript
使用jQuery不判断浏览器高度解决iframe自适应高度问题
2014/12/16 Javascript
使用 stylelint检查CSS_StyleLint
2016/04/28 Javascript
深入浅析vue组件间事件传递
2017/12/29 Javascript
JS/HTML5游戏常用算法之碰撞检测 包围盒检测算法详解【圆形情况】
2018/12/13 Javascript
Vue.js数字输入框组件使用方法详解
2019/10/19 Javascript
JS如何生成动态列表
2020/09/22 Javascript
[50:02]完美世界DOTA2联赛循环赛 Magma vs IO BO2第一场 11.01
2020/11/02 DOTA
对Python中Iterator和Iterable的区别详解
2018/10/18 Python
python浪漫表白源码
2019/04/05 Python
用python写一个定时提醒程序的实现代码
2019/07/22 Python
python中seaborn包常用图形使用详解
2019/11/25 Python
python 生成任意形状的凸包图代码
2020/04/16 Python
使用已经得到的keras模型识别自己手写的数字方式
2020/06/29 Python
SmartBuyGlasses美国官网:太阳眼镜和眼镜
2017/08/20 全球购物
Black Halo官方网站:购买连衣裙、礼服和连体裤
2018/06/13 全球购物
印尼极简主义和实惠的在线家具店:Fabelio
2019/03/27 全球购物
美国户外服装和装备购物网站:Outland USA
2020/03/22 全球购物
HSRP的含义以及如何工作
2014/09/10 面试题
幼儿园大班教学反思
2014/02/10 职场文书
社会学专业求职信
2014/02/24 职场文书
圣诞节活动策划方案
2014/06/09 职场文书
个人股份转让协议书范本
2015/01/28 职场文书
成本会计岗位职责
2015/02/03 职场文书
百日宴上的祝酒词
2015/08/10 职场文书
SpringBoot项目中控制台日志的保存配置操作
2021/06/18 Java/Android
Vue router配置与使用分析讲解
2022/12/24 Vue.js