vuejs2-5 goods页面
1 goods.vue组件功能(当未使用其他组件时)
1.1 src/App.vue 传递值
<keep-alive> <router-view :seller="seller"></router-view> </keep-alive>
1.2 src/components/goods.vue
<template> <div> <div class="goods"> <!--左侧分类--> <div class="menu-wrapper" ref="menuWrapper"> <ul> <li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="slectMenu(index,$event)"> <span class="text border-1px"> <span class="icon" v-show="item.type>0" :class="classMap[item.type]"></span> {{item.name}} </span> </li> </ul> </div> <!--右侧商品列表--> <div class="foods-wrapper" ref="foodsWrapper"> <ul> <li v-for="item in goods" class="foot-list food-list-hook" ref="foodList"> <h2 class="title">{{item.name}}</h2> <ul> <li v-for="food in item.foods" class="foods-item border-1px" @click="selectFood(food,$event)"> <div class="icon"> <img :src="food.icon" width="57" height="57"> </div> <div class="content"> <h3 class="name">{{food.name}}</h3> <p class="desc">{{food.description}}</p> <div class="extra"> <span class="count">月售{{food.sellCount}}</span> <span>好评率:{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span v-show="food.oldPrice" class="odd">¥{{food.oldPrice}}</span> </div> </div> </li> </ul> </li> </ul> </div> </div> </div> </template> <script type="text/ecmascript-6"> import BScroll from 'better-scroll'; const ERR_OK = 0; // 验证后台数据是否可用 export default{ props: { // 用来接收其他组件的数据 seller: { type: Object } }, data() { // 数据 return { goods: [], listHeight: [], scrollY: 0, selectedFood: {} }; }, computed: { // 计算属性 currentIndex() { // 右侧内容区滚动时左侧对应添加样式 for (let i = 0; i < this.listHeight.length; i++) { let height1 = this.listHeight[i]; let height2 = this.listHeight[i + 1]; if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)) { // 滚动高度大于或等于当前i区块的高度 小于i+1区块的高度 return i; }; }; return 0; // 刚加载进来默认左侧第一个分类添加样式 } }, created() { // dom加载时的操作 this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; // 请求获取goods的数据 this.$http.get('/api/goods') .then((res) => { res = res.body; if (res.errno === ERR_OK) { this.goods = res.data; this.$nextTick(() => { // 保证dom渲染好后 this._initScroll(); this._calcullateHeight(); // 计算区间高度组成的数组 }); } ; }); }, methods: { // 方法 _initScroll() { this.menuScroll = new BScroll(this.$refs.menuWrapper, { click: true // 可以点击 }); this.foodsScroll = new BScroll(this.$refs.foodsWrapper, { probeType: 3, // scroll滚动时监听滚动位置 click: true }); this.foodsScroll.on('scroll', (pos) => { this.scrollY = Math.abs(Math.round(pos.y)); // console.log(this.scrollY); }); }, _calcullateHeight() { // 计算右侧内容各自分区的高度 添加到this.listHeight数组中 // let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let foodList = this.$refs.foodList; // 获得右侧li的dom集合 let height = 0; this.listHeight.push(height); for (let i = 0; i < foodList.length; i++) { let item = foodList[i]; height += item.clientHeight; // 每个区间累加后的高度 this.listHeight.push(height); // 区间高度放到数组里面 }; }, slectMenu(_index, ev) { // 点击左侧分类添加样式,右侧对应内容滚动到顶部 if (!ev._constructed) { // 使用了BS return; }; let foodList = this.$refs.foodList; let els = foodList[_index]; this.foodsScroll.scrollToElement(els, 300); } } }; </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .goods $main_bg = #f3f5f7 $line_bg = #d9dde1 $bottom_bg = rgba(7,17,27,0.1) display:flex overflow:hidden position:absolute top:174px bottom:46px width:100% .menu-wrapper flex:0 0 80px width:80px background:$main_bg .menu-item display:table height:54px width:56px line-height:14px padding:0 12px &.current position:relative z-index:10 margin-top: -1px background:#fff &:before content:'' display:block width:4px height:100% position:absolute top:0 left:0 background-color:#00A0DC .text border-none() font-weight:700 .text display:table-cell width:56px vertical-align:middle font-size:12px border-1px($bottom_bg) .icon display:inline-block width:12px height:12px margin-right:2px background-size:12px background-repeat:no-repeat &.decrease bg-img("decrease_3") &.discount bg-img("discount_3") &.invoice bg-img("invoice_3") &.special bg-img("special_3") &.guarantee bg-img("guarantee_3") .foods-wrapper flex:1 .title height:26px background-color:$main_bg border-left:1px solid $line_bg text-indent:13px font-size:12px color:rgb(147,153,159) line-height:26px .foods-item display:flex margin:18px padding-bottom:18px border-1px($bottom_bg) &:last-child padding:0 border-none() $:first-child .content .cartcontroll-wrapper bottom:5px .icon flex:0 0 57px margin-right:10px img border-radius:3px .content flex:1 position:relative .name margin:2px 0 8px 0 font-size:14px color:rgb(7,17,27) line-height:14px height:14px .desc,txtra font-size:10px color:rgb(147,153,159) line-height:10px .desc margin-bottom:8px line-height:12px .extra font-size:0px .count margin-right:12px .price .now,add line-height:24px font-weight:700 .now font-size:14px color:rgb(240,20,20) margin-right:8px .odd font-size:10px color:rgb(147,153,159) text-decoration:line-through .cartcontroll-wrapper position:absolute bottom:-8px right:0 </style>
2 使用 shopcart.vue组件
2.1 未添加小球下落动画 src/components/goods.vue
<template> <div> <div class="goods"> <!--左侧分类--> <div class="menu-wrapper" ref="menuWrapper"> <ul> <li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="slectMenu(index,$event)"> <span class="text border-1px"> <span class="icon" v-show="item.type>0" :class="classMap[item.type]"></span> {{item.name}} </span> </li> </ul> </div> <!--右侧商品列表--> <div class="foods-wrapper" ref="foodsWrapper"> <ul> <li v-for="item in goods" class="foot-list food-list-hook" ref="foodList"> <h2 class="title">{{item.name}}</h2> <ul> <li v-for="food in item.foods" class="foods-item border-1px" @click="selectFood(food,$event)"> <div class="icon"> <img :src="food.icon" width="57" height="57"> </div> <div class="content"> <h3 class="name">{{food.name}}</h3> <p class="desc">{{food.description}}</p> <div class="extra"> <span class="count">月售{{food.sellCount}}</span> <span>好评率:{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span v-show="food.oldPrice" class="odd">¥{{food.oldPrice}}</span> </div> <div class="cartcontroll-wrapper"> <cart-controll :food="food" ref="food"></cart-controll> </div> </div> </li> </ul> </li> </ul> </div> <!--底部购物车--> <shop-cart ref="shopcart" :select-foods="selectFoods" :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice"></shop-cart> </div> </div> </template> <script type="text/ecmascript-6"> import BScroll from 'better-scroll'; import shopcart from 'components/shopcart/shopcart'; import cartcontroll from 'components/cartcontroll/cartcontroll'; const ERR_OK = 0; export default{ props: { // 用来接收其他组件的数据 seller: { type: Object } }, data() { // 数据 return { goods: [], listHeight: [], scrollY: 0, selectedFood: {} }; }, computed: { // 计算属性 currentIndex() { // 右侧内容区滚动时左侧对应添加样式 for (let i = 0; i < this.listHeight.length; i++) { let height1 = this.listHeight[i]; let height2 = this.listHeight[i + 1]; if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)) { return i; }; }; return 0; }, selectFoods() { // 计算商品数量是否变化(可以监听到被cartcontroll.vue改变的food) // 因为在使用cartcontroll.vue时传给它的就是food // 将变化结果传递给shopcart.vue( :select-foods="selectFoods") let foods = []; this.goods.forEach((good) => { good.foods.forEach((food) => { if (food.count) { // 此时food的数量不为0说明已经被添加或减小更改了 foods.push(food); } }); }); return foods; } }, created() { // dom加载时的操作 this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; // 请求获取goods的数据 this.$http.get('/api/goods') .then((res) => { res = res.body; if (res.errno === ERR_OK) { this.goods = res.data; this.$nextTick(() => { // 保证dom渲染好后 this._initScroll(); this._calcullateHeight(); // 计算区间高度组成的数组 }); } ; }); }, methods: { // 方法 _initScroll() { this.menuScroll = new BScroll(this.$refs.menuWrapper, { click: true // 可以点击 }); this.foodsScroll = new BScroll(this.$refs.foodsWrapper, { probeType: 3, // scroll滚动时监听滚动位置 click: true }); this.foodsScroll.on('scroll', (pos) => { this.scrollY = Math.abs(Math.round(pos.y)); // console.log(this.scrollY); }); }, _calcullateHeight() { // 计算右侧内容各自分区的高度 添加到this.listHeight数组中 // let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let foodList = this.$refs.foodList; // 获得右侧li的dom集合 let height = 0; this.listHeight.push(height); for (let i = 0; i < foodList.length; i++) { let item = foodList[i]; height += item.clientHeight; // 每个区间累加后的高度 this.listHeight.push(height); // 区间高度放到数组里面 }; }, slectMenu(_index, ev) { // 点击左侧分类添加样式,右侧对应内容滚动到顶部 if (!ev._constructed) { // 使用了BS return; }; let foodList = this.$refs.foodList; let els = foodList[_index]; this.foodsScroll.scrollToElement(els, 300); } }, components: { // 注册组件 shopCart: shopcart, // 底部购物车 cartControll: cartcontroll // 增加减少 } }; </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .goods $main_bg = #f3f5f7 $line_bg = #d9dde1 $bottom_bg = rgba(7,17,27,0.1) display:flex overflow:hidden position:absolute top:174px bottom:46px width:100% .menu-wrapper flex:0 0 80px width:80px background:$main_bg .menu-item display:table height:54px width:56px line-height:14px padding:0 12px &.current position:relative z-index:10 margin-top: -1px background:#fff &:before content:'' display:block width:4px height:100% position:absolute top:0 left:0 background-color:#00A0DC .text border-none() font-weight:700 .text display:table-cell width:56px vertical-align:middle font-size:12px border-1px($bottom_bg) .icon display:inline-block width:12px height:12px margin-right:2px background-size:12px background-repeat:no-repeat &.decrease bg-img("decrease_3") &.discount bg-img("discount_3") &.invoice bg-img("invoice_3") &.special bg-img("special_3") &.guarantee bg-img("guarantee_3") .foods-wrapper flex:1 .title height:26px background-color:$main_bg border-left:1px solid $line_bg text-indent:13px font-size:12px color:rgb(147,153,159) line-height:26px .foods-item display:flex margin:18px padding-bottom:18px border-1px($bottom_bg) &:last-child padding:0 border-none() $:first-child .content .cartcontroll-wrapper bottom:5px .icon flex:0 0 57px margin-right:10px img border-radius:3px .content flex:1 position:relative .name margin:2px 0 8px 0 font-size:14px color:rgb(7,17,27) line-height:14px height:14px .desc,txtra font-size:10px color:rgb(147,153,159) line-height:10px .desc margin-bottom:8px line-height:12px .extra font-size:0px .count margin-right:12px .price .now,add line-height:24px font-weight:700 .now font-size:14px color:rgb(240,20,20) margin-right:8px .odd font-size:10px color:rgb(147,153,159) text-decoration:line-through .cartcontroll-wrapper position:absolute bottom:-8px right:0 </style>
2.2 添加小球下落动画
2.2.1 src/components/goods.vue
<div class="cartcontroll-wrapper"> <cart-controll :food="food" @add="addFood"></cart-controll> </div> methods: // 小球相关 addFood(target) { //获得cart-controll.vue 点击的对象 可以获得的原因 1传递了add事件 2 $emit这个对象 this._drop(target); }, _drop(target) { this.$nextTick(() => { // 体验优化,异步执行下落动画 this.$refs.shopcart.drop(target); // 访问子组件shopcart.vue }); }
3 src/components/cartcontroll.vue组件(增加时旋转和平移)
<template> <div class="cartcontroll"> <transition name="move"> <div class="cart-decrease" v-show="food.count>0" @click.stop.prevent="decreaseCart"> <span class="inner icon-remove_circle_outline"></span> </div> </transition> <div class="cart-count" v-show="food.count>0">{{food.count}}</div> <div class="cart-add icon-add_circle" @click.stop.prevent="addCart"></div> </div> </template> <script type="text/ecmascript-6"> import Vue from 'vue'; export default { props: { // 接收其他组件传递过来的数据 food: { type: Object } }, methods: { // 指令方法 addCart: function(ev) { // 增加商品到购物车里 if (!ev._constructed) { // 去掉触发pc的事件--保证pc移动事件相同 return false; }; if (!this.food.count) { // this.food.count = 1; 不生效 Vue.set(this.food, 'count', 1); // 此时这个属性变化就能被观测到 } else { this.food.count++; }; // this.$dispatch('cart.add', ev.target); 2.0- 已被废除 this.$emit('add', event.target); // 触发当前实例上的事件。附加参数都会传给监听器回调。event.target点击的按钮的dom }, decreaseCart: function (ev) { // 减少购物车里面商品的数量 if (!ev._constructed) { return false; }; if (this.food.count) { this.food.count--; }; } } }; </script> <style lang="less" rel="stylesheet/less"> .cartcontroll{ font-size:0px; .cart-decrease{ /*该层负责平移*/ display:inline-block; line-height: 24px; font-size: 24px; transition: all 0.4s linear; opacity: 1; transform: translate3d(0,0,0); .inner{ /*文字层负责旋转*/ display:inline-block; padding: 6px; color:#00A0DC; transition:all 0.4s linear; transform: rotate(0deg); } &.move-enter-active, &.move-leave-active { transition: all 0.4s linear; } &.move-enter, &.move-leave-active{ opacity: 0; transform: translate3d(24px,0,0); .inner{ transform: rotate(180deg); } } } .cart-count{ display:inline-block; vertical-align: top; width:12px; padding-top:6px; line-height: 24px; text-align: center; font-size: 10px; color:rgb(147,153,159); } .cart-add{ display:inline-block; line-height: 24px; font-size: 24px; padding: 6px; color:#00A0DC; } } </style>
4 点击购物车出现列表(购物列表可折叠 增加 减少产品数量) src/components/shopcart.vue
<template> <div> <div class="shop-cart"> <div class="content" @click="toggleList"> <div class="con-left"> <div class="logo-wrapper"> <div class="logo" :class="{'highlight':totalCount>0}"> <i class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></i> </div> <div class="num" v-show="totalCount>0">{{totalCount}}</div> </div> <div class="price" :class="{'highlight':totalCount>0}">¥{{totalPrice}}</div> <div class="desc">另需配送费¥{{minPrice}}元</div> </div> <div class="con-right" :class="{enough:isEnoughPay}" @click.stop.prevent="pay"> <div class="pay">{{payDesc}}</div> </div> </div> <div class="ball-container"> <div v-for="ball in balls"> <transition name="drop" @before-enter="beforeDrop" @enter="dropping" @after-enter="afterDrop"> <div class="ball" v-show="ball.show"> <div class="inner inner-hook"></div> </div> </transition> </div> </div> <transition name="fold"> <div class="shopcart-list" v-show="listShow" transition="fold"> <div class="list-header"> <h2 class="title">购物车</h2> <span class="empty" @click="empty">清空</span> </div> <div class="list-conent" ref="listContent"> <ul> <li class="food border-1px" v-for="food in selectFoods"> <span class="name">{{food.name}}</span> <div class="price"> <span>¥{{food.price*food.count}}</span> </div> <div class="cartcontroll-wrapper"> <cart-controll @add="addFood" :food="food"></cart-controll> </div> </li> </ul> </div> </div> </transition> </div> <!--模糊背景--> <transition name="fade"> <div class="list-mask" v-show="listShow" @click="hideList"></div> </transition> </div> </template> <script type="text/ecmascript-6"> import cartcontroll from 'components/cartcontroll/cartcontroll'; import BScroll from 'better-scroll'; export default { data() { return { isEnoughPay: false, balls: [ // 小球相关 { show: false }, { show: false }, { show: false }, { show: false }, { show: false } ], dropBalls: [], // 小球相关 fold: false // 购物车商品详情页展开还是折叠 }; }, props: { // 接收其他组件传递过来的数据 selectFoods: { type: Array, default: [] }, deliveryPrice: { type: Number, default: 0 }, minPrice: { type: Number, default: 0 } }, computed: { // 计算 totalPrice() { // 选中商品总价格 let total = 0; this.selectFoods.forEach((food) => { // food表示selectFoods数组的每项 total += food.price * food.count; }); return total; }, totalCount() { // 选中的商品总数量 let count = 0; this.selectFoods.forEach((food) => { // food表示selectFoods数组的每项 count += food.count; }); return count; }, payDesc() { // 判断是否满足配送条件 if (this.totalPrice === 0) { this.isEnoughPay = false; return `¥${this.minPrice}元起送`; } else if (this.totalPrice < this.minPrice) { let diff = this.minPrice - this.totalPrice; this.isEnoughPay = false; return `还差¥${diff}元起送`; } else { this.isEnoughPay = true; return '去结算'; } }, listShow: function () { if (!this.totalCount) { // 折叠 this.fold = true; return false; }; let show = !this.fold; // 展开 if (show) { this.$nextTick(() => { if (!this.scroll) { // list-show不断变化,不能变化的时候就初始化 this.scroll = new BScroll(this.$refs.listContent, { // $refs.listContent 要和ref值一样,区分大小写 click: true }); } else { this.scroll.refresh(); }; }); }; return show; } }, methods: { // 小球相关 开始 drop(el) { // 取的父goods.vue组件传来的数据el for (let i = 0; i < this.balls.length; i++) { let ball = this.balls[i]; if (!ball.show) { ball.show = true; ball.el = el; this.dropBalls.push(ball); return; }; }; }, addFood(target) { console.log(1); this.drop(target); }, beforeDrop(el) { let count = this.balls.length; while (count--) { let ball = this.balls[count]; if (ball.show) { let rect = ball.el.getBoundingClientRect(); // 用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置 let x = rect.left - 32; let y = -(window.innerHeight - rect.top - 22); // console.log(x); // console.log(y); el.style.display = ''; el.style.webkitTransform = `translate3d(0,${y}px,0)`; el.style.transform = `translate3d(0,${y}px,0)`; let inner = el.getElementsByClassName('inner-hook')[0]; inner.style.webkitTransform = `translate3d(${x}px,0,0)`; inner.style.transform = `translate3d(${x}px,0,0)`; } } }, dropping(el, done) { /* eslint-disable no-unused-vars */ let rf = el.offsetHeight; this.$nextTick(() => { el.style.webkitTransform = 'translate3d(0,0,0)'; el.style.transform = 'translate3d(0,0,0)'; let inner = el.getElementsByClassName('inner-hook')[0]; inner.style.webkitTransform = 'translate3d(0,0,0)'; inner.style.transform = 'translate3d(0,0,0)'; el.addEventListener('transitionend', done); }); }, afterDrop(el) { let ball = this.dropBalls.shift(); if (ball) { ball.show = false; el.style.display = 'none'; } }, // 小球相关结束 toggleList() { if (!this.totalCount) { return; }; this.fold = !this.fold; }, empty() { // 清空购物车 this.selectFoods.forEach((food) => { food.count = 0; }); }, hideList() { // 购物车商品列表隐藏 this.fold = true; }, pay() { // 满足支付条件点击后操作 if (this.totalPrice < this.minPrice) { return; }; window.alert(`支付${this.totalPrice}元`); } }, components: { cartControll: cartcontroll } }; </script> <!--<style lang="stylus" rel="stylesheet/stylus"> </style>--> <style lang="less" rel="stylesheet/less"> .shop-cart{ position: fixed; height:48px; width:100%; left:0; bottom:0; z-index: 50; .content{ display:flex; background-color: rgb(20,29,39); font-size: 0; .con-left{ flex: 1; font-size:0px; .logo-wrapper, .price, .desc{ display: inline-block; } .logo-wrapper{ position: relative; top:-10px; margin:0 12px; padding: 6px; width:56px; height:56px; box-sizing: border-box; vertical-align: top; border-radius:50%; background-color:#141d27; .logo{ width:100%; height: 100%; border-radius:50%; background-color: rgb(43,52,60); font-size: 24px; line-height: 45px; text-align: center; &.highlight{ background:rgb(0,160,220) } .icon-shopping_cart{ color:rgba(255,255,255,0.4); font-size: 24px; &.highlight{ color:#fff; } } } .num{ position: absolute; top:0; right:0; width:24px; height:16px; line-height: 16px; text-align: center; border-radius: 16px; font-size: 9px; font-weight: 700; color:#fff; background-color: rgb(240,20,20); box-shadow: 0 4px 8px 0 rgba(0,0,0,0.4); } } .price{ display: inline-block; vertical-align: top; margin-top: 12px; line-height: 24px; height:24px; padding-right: 12px; box-sizing: border-box; border-right: 1px solid rgba(255,255,255,0.1); font-size: 16px; font-weight: 700; color:rgb(127,133,138); &.highlight{ color:#fff; } } .desc{ display: inline-block; font-size: 10px; line-height: 24px; height:24px; color:rgb(127,133,138); margin-top:12px; text-indent: 12px; } } .con-right{ flex: 0 0 105px; width: 105px; line-height: 48px; height:48px; padding:0 8px; box-sizing: border-box; color:rgb(127,133,138); background-color: rgb(43,52,60); text-align: center; &.enough{ background-color:#34B413; .pay{ color:#fff; font-size: 16px; } } .pay{ font-size: 12px; font-weight: 700; } } } .ball-container{ .ball{ position: fixed; left:32px; bottom:22px; z-index: 200; &.drop-transition{ transition: all 0.4s cubic-bezier(0.49,-0.29,0.75,0.41); .inner{ width:16px; height: 16px; border-radius: 50%; background-color: rgb(0,160,220); transition:all 0.4s linear; } } } } .shopcart-list{ position: absolute; left:0; top:0; z-index: -1; width:100%; transform: translate3d(0,-100%,0); &.fold-enter-active, &.fold-leave-active{ transition:all 0.5s; } &.fold-enter,&.fold-leave-active{ transform: translate3d(0,0,0); } .list-header{ height:40px; line-height: 40px; padding:0 18px; background-color: #f3f5f7; border-bottom:1px solid rgba(7,17,27,0.1); .title{ float:left; font-size: 14px; color:rgb(7,17,27); } .empty{ float: right; font-size:12px; color:rgb(0,160,220); } } .list-conent{ padding:0 18px; max-height:217px; background-color: #fff; overflow:hidden; .food{ position: relative; padding:12px 0; border-bottom:1px solid rgba(7,17,27,0.1); .name{ line-height: 24px; font-size: 14px; color:rgb(7,17,27); } .price{ position: absolute; right:94px; bottom:12px; line-height: 24px; font-size: 14px; font-weight: 700; color:rgb(240,20,20); } .cartcontroll-wrapper{ position: absolute; right:0; bottom:6px; } } } } } .list-mask{ position:fixed; top:0; left:0; width:100%; height:100%; z-index:40; backdrop-filter:blur(10px); opacity: 1; background: rgba(7, 17, 27, 0.6); &.fade-enter-active, &.fade-leave-active{ transition:0.5s all; } &.fade-enter,&.fade-leave-active{ opacity:0; background:rgba(7,17,27,0); } } </style>
组件间简单图解