一, shopCart 组件
(1) goods 父组件和 子组件 shopCart 传参
deliveryPrice:{ // 单价 从 json seller 对象数据中获取
type:Number,
default:0
},
minPrice:{ // 最低起送价 从 json seller 对象数据中获取
type:Number,
default:20
}
其中 deliveryPrice 和 minPrice 的数据都是从 data.json 数据 中 seller 对象下 获得. 所以在 goods 组件中还要 获取到 seller 对象 的数据, 否则会报错:
[vue warn]: Error in render: "TypeError: Cannot read property'deliveryPrice'of undefined"
解决方法: 根组件 App.vue 中 router-view 组件获取 seller 数据, 传到 goods 组件中
1-1.app.vue (根组件 也是 goods 的父组件)
<keep-alive>
<router-view :sell="sellerObj"></router-view>
</keep-alive>
注意: sellerObj 是 data 定义 的 对象里用来接收 data.json 数据, 相当于 实参
1-2.goods.vue (相对于跟组件的子组件 且 shopCart 的父组件)
通过 props 属性 进行组件之间的通信
props: {
sell: Object // 相当于 形参
},
1-3.shopCart.vue (goods 的子组件)
<shopCart :delivery-price="sell.deliveryPrice" :min-price="sell.minPrice"></shopCart>
(2) 选中商品 的 计算功能
1-1. 传入用户选中商品的集合
说明: 从父组件会 传入一个用户选中商品的 数组, 数组里会存放着 n 个对象, 每个对象里存放着该 商品的 价格 和 数量.
props:{ // 通过父组件传过来的 ( 相当于形参 )
selefoodsArr:{ // 用户选中的商品存放在一个数组里 接收的是 data.json 数据的 goods(数组)
type:Array, // 当父组件传过来的 类型是对象或者 是数组时, default 就是一个函数
default (){
return [] // 返回数组 存放着选中 商品 对应的 goods 下的 foods 数组 (由 父组件 的 实参 决定的返回值)
}
}
1-2. 利用计算属性 选中商品数量的变化, 商品总价, 动态改变描述等功能
computed: {
totalPrice() { // 计算总价, 超过起送额度后提示可付款
let total = 0 // 定义一个返回值
this.selefoodsArr.forEach((rfoods) = >{ // 遍历 这个 goods 数组 取到 价格 和 数量 (当然在这里数据库没有 count 这个属性, 稍后 我们会利用 vue.set() 新建一个 count 属性)
total += rfoods.price * rfoods.count // 形参 rfoods 实参 是 foods
});
return total;
},
totalCount() { // // 计算选中的 food 数量, 在购物车图标处显示, 采用绝对定位, top:0;right:0; 显示在购物车图标右上角
let count = 0 this.selefoodsArr.forEach((rfoods) = >{ // 形参 rfoods 实参 是 foods
count += rfoods.count
});
return count;
},
payDesc() { // 控制底部右边内容随 food 的变化而变化, payDesc() 控制显示内容, enough 添加类调整显示样式
let diff = this.minPrice - this.totalPrice
if (!this.totalPrice) {
return `¥${this.minPrice} 起送 `
} else if (diff > 0) {
return ` 还差¥${diff} 元 `
} else {
return '去结算'
}
}
}
这样就渲染到 template 里了
<div class="shopCart">
<div class="content">
<div class="content-left">
<div class="logo-wrapper">
<!-- 徽章 展示选中商品的个数 -->
<div class="badge" v-show="totalCount">
{{totalCount}}
</div>
<!-- 购物车 图标 选择商品和未选择商品 时 动态改变 样式 条件: 只要选择了商品即总价不为 0 , 样式变 -->
<div class="logo" :class="{'active':totalCount}">
<i class="icon-shopping_cart"></i>
</div>
</div>
<!-- 同理: 总价 不为 0 字体高亮 -->
<div class="price" :class="{'active':totalPrice}">
¥{{totalPrice}}
</div>
<!-- 配送费 data.json 提供 -->
<div class="desc">
另需要配送费¥{{deliveryPrice}} 元
</div>
</div>
<!-- 根据条件 动态 改变样式 -->
<div class="content-right" :class="{'enough':totalPrice>=minPrice}">
{{payDesc}}
</div>
</div>
</div>
相关样式
&.active
color white
&.enough
background #00b43c
color white
总结: 通过以上学习我们能发现, selectFoods() 的变化起着关键作用, 它的变化会引起 DOM 的变化, 并最终体现到界面上, 而我们不用关注 DOM 内部的具体实现, 这就是 vue 的一大好处. 如果采用 jQuery 完成这些功能会略显繁杂.
二, cartControl 组件
说明: 这个组件是控制购物车小球的. 其中涉及到小球的动画
(1) 新增属性 count
说明:
在 goods 下的 foods 添加一个属性 count, 用来存储用户选中的商品个数, 计算商品总价 以及 关联徽章 (显示用户选择商品的个数) 的变化
方法: 通过 import Vue from 'vue'; 使用 set 接口, 通过 vue.set() 添加属性, 当它变化时就能被检测到, 从而父组件能获取到 count 值 (遍历选中的商品时使用)
methods: {
addCart(event) { // 点击 count 加,
//console.log(event.target);
if (!event._constructed) { // 去掉自带 click 事件的点击
return;
}
if (!this.foodsele.count) {
Vue.set(this.foodsele, 'count', 1)
} else {
this.foodsele.count++
}
},
decreaseCart(event) { // 点击减少
if (!event._constructed) { // 去掉自带 click 事件的点击
return;
}
if (this.foodsele.count) {
this.foodsele.count--
}
}
}
(2) 添加按钮 实现 transtion 过渡
我们要实现的效果是: 当点击添加按钮时, 减少按钮出现 并伴随着 旋转, 平移以及透明度变化的 一些 动画效果
<transition name='move'> <!-- 平移动画 -->
<div class="cart-decrease" v-show="foodsele.count" @click='decreaseCart($event)'>
<span class="icon-remove_circle_outline inner"></span><!-- 旋转, 透明度动画 -->
</div>
</transition>
.cart-decrease
display inline-block
padding 6px
transition: all .4s linear /* 过渡效果的 CSS 属性的名称, 过渡效果需要多少时间, 速度效果的速度曲线 */
.inner
line-height 24px
font-size 24px
color rgb(0,160,220)
transition all 0.4s linear
&.move-enter-active, &.move-leave-active
transform translate3d(0,0,0) /* 这样可以开启硬件加速, 动画更流畅, 3D 旋转, X 轴位移 24px */
.inner
display inline-block /* 设置成 inline-block 才有高度, 才能有动画 */
transform rotate(0)
&.move-enter, &.move-leave-active
opacity: 0
transform translate3d(24px,0,0)
.inner
transform rotate(180deg)
三, 抛物线小球动画
通过两个层来控制小球, 外层控制一个方向的变化, 内层控制另外一个方向的变化 (写两层才会有抛物线的效果), 采用 fixed 布局 (是相对于视口的动画)
事件发射和接收
组件之间传值 - 1 组件之间传值 - 2 扩展
Vue1.0 组件间传递
使用 $on() 监听事件;
使用 $emit() 在它上面触发事件;
使用 $dispatch() 派发事件, 事件沿着父链冒泡;
使用 $broadcast() 广播事件, 事件向下传导给所有的后代
(1) Vue2.0 组件之间传递数据
1-1. 当点击 添加数量时 在 cartControl 组件里的 addCount 方法里 通过 $emit 属性 派发一个事件 , 传入点击的对象
addCart(event) { // 点击 count 加,
// console.log(event.target);
if (!event._constructed) { // 去掉自带 click 事件的点击
return;
}
if (!this.foodsele.count) {
Vue.set(this.foodsele, 'count', 1)
} else {
this.foodsele.count++
}
// 当点击 添加数量时 通过 $emit 属性 提交一个名为 add 给父组件
// 子组件通过 $emit 触发 add 事件 , 将参数传递给父组件
this.$emit('add', event.target);
}
1-2. 操作 goods 组件
购物车组件如果提交了 addCart 事件就调用 add 函数
<cart-control :foodsele='food' @add="addFood"></cart-control>
父组件使用 @add="addFood" 监听由子组件 vm.$emit 触发的事件, 通过 addFood() 接受从子组件传递过来的数据, 通知父组件数据改变了.
addFood(target) {
this._drop(target);
}
1-3. 父组件访问子组件 vue 提供了接口 ref
复制代码 代码如下:
_drop(target) {
// 体验优化, 异步执行下落动画
this.$nextTick(() = >{
this.$refs.shopCart.balldrop(target); // 将 target 传入 shopCart 子组件中的 balldrop 方法, 所以 drop 方法能获得用户点击按钮的元素, 即能获取点击按钮的位置
});
}
区别 访问 DOM 变量
1-3. 操作 shopCart 组件
data (){ // 定义一个数组 来 控制小球的状态 定义多个对象, 表示页面中做多同时运动的小球
return{ // 定义 5 个 小球
balls:[{show:false},{show:false},{show:false},{show:false},{show:false}],
dropBalls:[] // 接收下落小球
}
}
methods:{
balldrop(ele) {
// console.log(el) 取到点击 对象
for(var i=0;i<this.balls.length;i++){
let ball=this.balls[i]
if(!ball.show){
ball.show=true
ball.ele=ele
this.dropBalls.push(ball)
return;
}
}
}
}
动画过程开始, 利用 vue 提供的钩子函数
beforeEnter (el){ // 找到所以设为 true 的小球
let count=this.balls.length
while(count--){
let ball = this.balls[count];
if(ball.show){
let pos=ball.el.getBoundingClientRect() // 返回元素相对于视口偏移的位置
let x=pos.left-32 // 点击的按钮与小球 (fixed) 之间 x 方向的差值
let y=-(window.innerHeight-pos.top-22)
el.style.display = ''; // 设置初始位置前, 手动置空, 覆盖之前的 display:none, 使其显示
el.style.webkitTransform = `translate3d(0,${y}px,0)`; // 外层元素做纵向的动画, y 是变量
el.style.transform = `translate3d(0,${y}px,0)`;
let inner = el.getElementsByClassName('inner_hook')[0];// 内层元素做横向动画, inner-hook(用于 js 选择的样式名加上 - hook, 表明只是用 // 于 js 选择的, 没有真实的样式含义)
inner.style.webkitTransform = `translate3d(${x}px,0,0)`;
inner.style.transform = `translate3d(${x}px,0,0)`;
}
}
},
enter(el) {
/* 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)';
});
},
afterEnter(el) {
let ball = this.dropBalls.shift(); // 取到做完动画的球, 再置为 false, 即重置, 它还可以接着被利用
if (ball) {
ball.show = false;
el.style.display = 'none';
}
}
<div class="ball-container">
<div v-for="ball in balls">
<transition name="drop" @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter">
<div class="ball" v-show="ball.show">
<div class="inner inner_hook"></div>
</div>
</transition>
</div>
</div>
&.drop-enter,&.drop-enter-active
transition all 0.4s cubic-bezier(0.49,-0.29,0.75,0.41)
.inner
width 16px
height 16px
border-radius 50%
background rgb(0,160,220)
transition all 0.4s linear
来源: http://www.jb51.net/article/133739.htm