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 UI Dialog控件中的表单无法正常提交的解决方法
Dec 19 Javascript
Ext对基本类型的扩展 ext,extjs,format
Dec 25 Javascript
转义字符(\)对JavaScript中JSON.parse的影响概述
Jul 17 Javascript
javascript实现随时变化着的背景颜色
Apr 02 Javascript
jquery简单实现外部链接用新窗口打开的方法
May 30 Javascript
ES6正则表达式扩展笔记
Jul 25 Javascript
JavaScript调试之console.log调试的一个小技巧分享
Aug 07 Javascript
React进阶学习之组件的解耦之道
Aug 07 Javascript
Javascript三种字符串连接方式及性能比较
May 28 Javascript
微信小程序登陆注册功能的实现代码
Dec 10 Javascript
vue仿携程轮播图效果(滑动轮播,下方高度自适应)
Feb 11 Vue.js
vue项目如何打包之项目打包优化(让打包的js文件变小)
Apr 30 Vue.js
微信小程序 扭蛋抽奖机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
雄兵连:天使彦天使彦为爱折翼,彦和炙心同时念动的誓言!
2020/03/02 国漫
php读取mysql中文数据出现乱码的解决方法
2013/08/16 PHP
基于preg_match_all采集后数据处理的一点心得笔记(编码转换和正则匹配)
2014/01/31 PHP
PHP模板引擎smarty详细介绍
2015/05/26 PHP
php如何获取文件的扩展名
2015/10/28 PHP
WordPress主题中添加文章列表页页码导航的PHP代码实例
2015/12/22 PHP
Nginx+php配置文件及原理解析
2020/12/09 PHP
js操作二级联动实现代码
2010/07/27 Javascript
JS实现拖动示例代码
2013/11/01 Javascript
浅析JavaScript中的类型和对象
2013/11/29 Javascript
jQuery获得内容和属性示例代码
2014/01/16 Javascript
用javascript替换URL中的参数值示例代码
2014/01/27 Javascript
jQuery.parseJSON(json)将JSON字符串转换成js对象
2014/07/27 Javascript
html的DOM中document对象anchors集合用法实例
2015/01/21 Javascript
JS获取Table中td值的方法
2015/03/19 Javascript
浅谈JavaScript异步编程
2017/01/20 Javascript
JS实现键值对遍历json数组功能示例
2018/05/30 Javascript
微信小程序自定义select下拉选项框组件的实现代码
2018/08/28 Javascript
vue 解决computed修改data数据的问题
2019/11/06 Javascript
javascript设计模式 ? 状态模式原理与用法实例分析
2020/04/22 Javascript
深入理解Django中内置的用户认证
2017/10/06 Python
Python打包方法Pyinstaller的使用
2018/10/09 Python
对python中的 os.mkdir和os.mkdirs详解
2018/10/16 Python
对Python中DataFrame选择某列值为XX的行实例详解
2019/01/29 Python
Python3安装pip工具的详细步骤
2019/10/14 Python
django 外键创建注意事项说明
2020/05/20 Python
Python脚本实现Zabbix多行日志监控过程解析
2020/08/26 Python
新加坡时尚网上购物:Zalora新加坡
2016/07/26 全球购物
英国著名的小众美容品牌网站:Alyaka
2017/08/08 全球购物
英国最大的经认证的有机超市:Planet Organic
2018/02/02 全球购物
中东地区最大的奢侈品市场:The Luxury Closet
2019/04/09 全球购物
怎样从/向数据文件读/写结构
2014/11/23 面试题
营业员演讲稿
2013/12/30 职场文书
公司市场专员岗位职责
2014/06/29 职场文书
趣味运动会通讯稿
2015/07/18 职场文书
速龙x4-860k处理器相当于i几
2022/04/20 数码科技