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
效果展示
定义的组件
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 插槽是一块模板,显示不显示,以及怎样显示由父组件来决定, 也就是把你想要滑动的区域插进去,剩下的内容都是官方文档定义好的,复制一遍就好了
固定头部
头部相对页面是固定的,这里我把头部都封装成了组件,在主页面引入头部,要滑动的部分放入上面定义好的scroll组件即可
侧边栏以及弹出框
起初我的想法是用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>
外卖点餐
这里参考的是慕课网黄奕大大的课程,课程地址
商品展示
<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>
异步问题
<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
,若是有解决办法,欢迎提出,感激不尽!
结语
总的来说这个项目还有很多不足,实现的功能也很少,后续我会继续改进。
如果这篇文章对你有帮助,不妨点个赞吧!
以上所述是小编给大家介绍的Vue实战教程之仿肯德基宅急送App,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!
暂无评论内容