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

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

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容