【16.Vue的过渡与动画
】
1.点击切换按钮:实现某一元素的显示/隐藏
Test.vue文件中
<template>
<div>
<!-- 点击事件触发后,isShow取反 -->
<!-- 通过点击按钮让h1标签出现或者消失 -->
<button @click="isShow = !isShow">切换按钮:显示/隐藏</button>
<h1 v-show="isShow">你好啊,梅小姐!</h1>
</div>
</template>
<script>
export default {
name: 'Test',
data() {
return {
isShow: true,
};
},
};
</script>
<style scoped>
button {
background-color: aquamarine;
}
h1 {
background-color: orange;
}
</style>
2.切换太生硬了:利用Vue的 transition
标签,来处理过渡以及动画
-
1、准备样式
① 过度样式
- 元素进入的样式
- v-enter:进入的起点
- v-enter-active:进入过程中
- v-enter-to:进入的终点
- 元素离开的样式
- v-leave:离开的起点
- v-leave-active:离开过程中
- v-leave-to:离开的终点
② 动画样式
-
定义动画
- @keyframes 动画名 { … }
-
使用动画
-
v-enter-active:进入过程中的动画
-
v-leave-active:离开过程中的动画
-
-
2、使用
<transition>
包裹要过度的元素<transition>要过添加过度或动画的元素</transiton>
知识补充:
-
关键帧 @keyframes 动画名 { … }
-
让
<transition>
包裹的元素一开始就有出场动画【有以下两种方式,but如果写成了appear="true"
,没有冒号,那等式右边的"true"
就成一个字符串,所以必须要有冒号】<transition name="haha" :appear="true">
<transition name="haha" appear>
完整Test.vue
代码
<template>
<div>
<!-- 点击事件触发后,isShow取反 -->
<!-- 通过点击按钮让h1标签出现或者消失 -->
<button @click="isShow = !isShow">切换按钮:显示/隐藏</button>
<transition name="haha" appear>
<h1 v-show="isShow">你好啊,梅小姐!</h1>
</transition>
</div>
</template>
<script>
export default {
name: 'Test',
data() {
return {
isShow: true,
};
},
};
</script>
<style scoped>
button {
background-color: aquamarine;
}
h1 {
background-color: orange;
}
/* 针对name为haha的 <transition></transition>设置的样式动画*/
.haha-enter-active {
/* 进入过程中 :动画名XYG,持续时间 1s 匀速*/
animation: XYG 1s linear ;
}
.haha-leave-active {
/* 离开过程中 :动画名XYG,持续时间 1s 匀速 翻转XYG动画*/
animation: XYG 1s linear reverse;
}
/* 自定义动画 XYG 从from...到to...*/
@keyframes XYG{
from{
/*XYG动画从左边看不见 */
transform: translateX(-100%);
}
to{
/*到完全显示出来 */
transform: translateX(0);
}
}
</style>
3.如果Vue的 <transition>
标签中想放入多个类似于list的标签,请用<transition-group>
标签,但是在<transition-group>
标签中包裹的元素必须写key值。
<transition-group name="haha" appear>
<h1 v-show="isShow" key="1">你好啊,梅小姐!</h1>
<h1 v-show="!isShow" key="2">梅小姐,伍六七想约你去海边!</h1>
</transition-group>
- 样式css分析
/* 进入的起点、离开的终点 */
.haha-enter,.haha-leave-to{
transform: translateX(-100%);
}
/* 进入的过程、离开的过程 */
.haha-enter-active,.haha-leave-active{
transition: 0.5s linear;
}
/* 进入的终点、离开的起点 */
.haha-enter-to,.haha-leave{
transform: translateX(0);
}
【此处的v指<transition-group name="haha">中的name:haha】
进入、离开的起点
-
.v-leave
:在离开过渡被触发时立刻生效,并在下一帧被移除。 -
.v-enter
:在进入过渡被触发时立刻生效,并在下一帧被移除。
进入、离开的过程
-
.v-leave-active
:在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。 -
.v-enter-active
:在整个进入过渡的阶段中应用,在进入过渡被触发时立刻生效,在过渡/动画完成之后移除。
进入、离开的终点
-
.v-leave-to
:在离开过渡被触发之后下一帧生效 (与此同时 v-leave 被删除),在过渡/动画完成之后移除。 -
.v-enter-to
:在进入过渡被触发之后下一帧生效 (与此同时 v-enter 被删除),在过渡/动画完成之后移除。 - .v-leave-active和**.v-leave是同时被添加到标签中的,但是.v-leave被添加的时间只有一帧**;.v-leave-to是最后一个添加到标签中的
<template>
<div>
<!-- 点击事件触发后,isShow取反 -->
<!-- 通过点击按钮让h1标签出现或者消失 -->
<button @click="isShow = !isShow">切换按钮:显示/隐藏</button>
<transition-group name="haha" appear>
<h1 v-show="isShow" key="1">你好啊,梅小姐!</h1>
<h1 v-show="!isShow" key="2">梅小姐,伍六七想约你去海边!</h1>
</transition-group>
</div>
</template>
<script>
export default {
name: 'Test2',
data() {
return {
isShow: true,
};
},
};
</script>
<style scoped>
button {
background-color: aquamarine;
}
h1 {
background-color: orange;
}
/* 进入的起点、离开的终点 */
.haha-enter,.haha-leave-to{
transform: translateX(-100%);
}
/* 进入的过程、离开的过程 */
.haha-enter-active,.haha-leave-active{
transition: 0.5s linear;
}
/* 进入的终点、离开的起点 */
.haha-enter-to,.haha-leave{
transform: translateX(0);
}
</style>
Vue使用第三方库实现动画效果:animate.css
1、安装animate.css库
npm i animate.css
2、引入animate.css库
import 'animate.css';
3、基础用法
name="animate__animated animate__bounce"
必须配置
<transition name="animate__animated animate__bounce">
4、配置样式
通过enter-active-class
和leave-active-class
实现不同样式配置
<transition-group
appear
name="animate__animated animate__bounce"
enter-active-class="animate__backInRight"
leave-active-class="animate__backOutUp"
>
...
</transition-group>
案例代码:
<template>
<div>
<!-- 点击事件触发后,isShow取反 -->
<!-- 通过点击按钮让h1标签出现或者消失 -->
<button @click="isShow = !isShow">切换按钮:显示/隐藏</button>
<transition-group
appear
name="animate__animated animate__bounce"
enter-active-class="animate__backInRight"
leave-active-class="animate__backOutUp"
>
<h1 v-show="isShow" key="1">你好啊,梅小姐!</h1>
<h1 v-show="!isShow" key="2">梅小姐,伍六七想约你去海边!</h1>
</transition-group>
</div>
</template>
<script>
// animate依赖包在node_modules
import 'animate.css';
export default {
name: 'Test3',
data() {
return {
isShow: true,
};
},
};
</script>
<style scoped>
button {
background-color: aquamarine;
}
h1 {
background-color: orange;
}
</style>
动画效果展示:
【17.TodoList案例动画
】
TodoList.vue文件中需要修改的代码
<template>
<ul class="todo-main">
<transition-group
appear
name="animate__animated animate__bounce"
enter-active-class="animate__backInRight"
leave-active-class="animate__backOutUp"
>
<TodoItem
v-for="todoObj in todos"
:key="todoObj.id"
:todo="todoObj"
/>
</transition-group>
</ul>
</template>
import 'animate.css';
动画效果展示:
完整代码:
main.js
//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false
//创建vm
new Vue({
el:'#app',
render: h => h(App),
// 生命周期钩子beforeCreate中模板未解析,且this是vm
beforeCreate() {
// this:指的是vm
Vue.prototype.$bus = this //安装全局事件总线$bus
}
})
App.vue
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<TodoHeader @addTodo="addTodo"/>
<TodoList
:todos="todos"
/>
<TodoFooter
:todos="todos"
@checkAllTodo="checkAllTodo"
@clearAllTodo="clearAllTodo"
/>
</div>
</div>
</div>
</template>
<script>
import pubsub from "pubsub-js";
//引入App的子组件
import TodoHeader from './components/TodoHeader'
import TodoFooter from './components/TodoFooter'
import TodoList from './components/TodoList'
export default {
name:'App',
components: { TodoHeader,TodoFooter,TodoList },
data() {
return {
//由于todos是TodoHeader组件和TodoFooter组件都在使用,所以放在App中(状态提升)
todos:[
{id:'001',title:'练功夫',done:true},
{id:'002',title:'睡觉',done:false},
{id:'003',title:'打豆豆',done:true}
],
}
},
methods: {
//添加一个todo
addTodo(todoObj){
// 在数组的开头添加一个数据
this.todos.unshift(todoObj)
},
//全选or取消全选
checkAllTodo(done){
this.todos.forEach(todo => todo.done = done)
},
// 清除所有已经完成的todo
clearAllTodo(){
this.todos= this.todos.filter(todo =>{
return todo.done == false
// 或者换成 return !todo.done
}
)
},
},
mounted() {
// 利用数据总线去通信
//勾选or取消勾选一个todo
this.$bus.$on('checkTodo',(id)=>{
this.todos.forEach((todo) => {
if(todo.id === id) todo.done = !todo.done
})
})
// 利用消息的订阅与发布去通信
// 订阅deleteTodo
this.pubId = pubsub.subscribe('deleteTodo',(_,id)=>{
this.todos=this.todos.filter(
(todo)=>{
return todo.id != id
}
)
})
},
beforeDestroy() {
// this.$bus.$off(['deleteTodo','checkTodo'])
this.$bus.$off(['checkTodo'])
// 取消订阅
pubsub.unsubscribe(this.pubId)
},
}
</script>
<!-- style没有scoped属性:【全局样式】 -->
<!-- style有scoped属性:样式设置只在本组件里起作用【局部样式】 -->
<style>
/*base*/
body {
background: #fff;
}
.btn {
/* 行内的块级元素 */
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
/* 文本内容居中 */
text-align: center;
/* 垂直居中 */
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
/* 字体颜色设置:白色 */
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
/* 鼠标移动到删除按钮时 */
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
/* 上下外边距为0,左右自动,实际效果为左右居中*/
margin: 0 auto;
}
/* 后代选择器(包含选择器),选择到的是todo-container下面的所有后代todo-wrap */
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #67dbd1;
border-radius: 5px;
}
</style>
TodoHeader.vue
<template>
<div class="todo-header">
<!-- @keyup.enter="add" :按下回车按键,调用add方法 -->
<input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add" v-model="title"/>
</div>
</template>
<script>
// 引入nanoid库生成ID号
import { nanoid } from 'nanoid'
export default {
name: 'TodoHeader',
/* //接收从App组件【父组件】传递过来的addTodo方法
props:['addTodo'], */
data() {
return {
title: '',
}
},
methods: {
add(){
// 如果输入框里为空,就跳过下面的代码,并弹窗
if (!this.title.trim()) return alert('请输入值')
//将用户的输入包装成一个todo对象
const todoObj={id:nanoid(),title:this.title,done:false}
//通知App组件去添加一个todo对象
//触发自定义事件addTodo,并把子组件中的参数todoObj传给父组件
this.$emit('addTodo',todoObj)
//清空输入
this.title = ''
}
},
};
</script>
<style scoped>
/* 头部样式设置 */
/* 后代选择器(包含选择器),选择到的是todo-header下面的所有后代input */
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
/* 内边距:上下4px,左右7px */
padding: 4px 7px;
}
/* :focus获得焦点,并设置其新样式:例如:用户单击一个input输入框获取焦点,然后这个input输入框的边框样式就会发生改变,和其他的输入框区别开来,表明已被选中。 */
.todo-header input:focus {
/* outline 与 border 相似,不同之处在于 outline 在整个元素周围画了一条线;它不能像 border 那样,指定在元素的一个面上设置轮廓,也就是不能单独设置顶部轮廓、右侧轮廓、底部轮廓或左侧轮廓。 */
outline: none;
/* 定义边框的颜色 */
border-color: rgba(82, 168, 236, 0.8);
/* boxShadow 属性把一个或多个下拉阴影添加到框上 */
/* 设置inset:内部阴影,不设置inset:外部阴影 */
/* 【0 0】:不设置X轴与Y轴偏移量 */
/* 第三个值【如10px,8px】:设置值阴影模糊半径为15px */
box-shadow: inset 0 0 10px rgba(124, 56, 207, 0.075), 0 0 8px rgba(224, 58, 17, 0.6);
background-color: bisque;
}
</style>
TodoList.vue
<template>
<ul class="todo-main">
<transition-group
appear
name="animate__animated animate__bounce"
enter-active-class="animate__backInRight"
leave-active-class="animate__backOutUp"
>
<TodoItem
v-for="todoObj in todos"
:key="todoObj.id"
:todo="todoObj"
/>
</transition-group>
</ul>
</template>
<script>
import 'animate.css';
import TodoItem from './TodoItem';
export default {
name: 'TodoList',
components: { TodoItem },
//声明接收App传递过来的数据,其中todos是自己用的,checkTodo和deleteTodo是给子组件TodoItem用的
props: ['todos'],
};
</script>
<style scoped>
/*main*/
.todo-main {
/* 左外边距:0px 【盒子贴着盒子】*/
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>
TodoItem.vue
<template>
<li>
<label>
<input type="checkbox"
:checked="todo.done"
@change="handleCheck()"
/>
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" @click="handleDelete()">删除</button>
</li>
</template>
<script>
import pubsub from "pubsub-js";
export default {
name: 'TodoItem',
// 声明接受从别的组件中的todoObj对象,todo
props: ['todo'],
methods: {
//勾选or取消勾选【别弄混了:这里的id其实就是上面change事件中的todo.id】
handleCheck(){
//change事件触发后,通知App组件将对应的todo对象的done值取反
// this.checkTodo(id)
this.$bus.$emit('checkTodo',this.todo.id)
// console.log(this.todo.id);
},
//删除
handleDelete(){
if (confirm('Are you sure you want to delete?')) {
//点击后,发布订阅后通知App组件将对应的todo对象删除,参数
pubsub.publish('deleteTodo',this.todo.id)
}
}
},
};
</script>
<style scoped>
/*item*/
li {
/* ul无序列表 ol有序列表*/
/* 列表前面无标记 */
list-style: none;
/* height定义了一个li盒子的高度 */
height: 36px;
/* 行高:指的是文字占有的实际高度 */
line-height: 36px;
/* 当height和line-height相等时,即盒子的高度和行高一样,内容上下居中 */
padding: 0 5px;
/* 边框底部:1px的实心线 颜色*/
border-bottom: 1px solid #c0abc3;
}
/* 后代选择器(包含选择器),选择到的是li下面的所有后代label */
li label {
/* 左对齐浮动【元素一旦浮动就会脱离文档流(不占位,漂浮)】 */
float: left;
/* 鼠标放在label元素上时变成小小手 */
cursor: pointer;
}
li label input {
/* 垂直居中 */
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
/* 后代选择器(包含选择器),选择到的是li下面的所有后代button */
li button {
/* 向右浮动 */
float: right;
/* 不为被隐藏的对象保留其物理空间,即该对象在页面上彻底消失,通俗来说就是看不见也摸不到。 */
display: none;
/* 上边距为3px */
margin-top: 3px;
}
li:before {
/* initial:它将属性设置为其默认值。 */
content: initial;
}
/* 结构伪类选择器 选择最后一个li元素 */
li:last-child {
/* 边框底部没有线 */
border-bottom: none;
}
li:hover{
background-color: #ddd;
}
/* 鼠标移动到该元素上时,将button按钮显示出来 */
li:hover button{
/* display:block将元素显示为块级元素 */
display: block;
}
</style>
TodoFooter.vue
<template>
<div class="todo-footer" v-show="total">
<label>
<!-- <input type="checkbox" :checked="isAll" @change="checkAll"/>
可以用下面这行代码代替-->
<!--对于type="checkbox",v-model绑定的就是一个布尔值,isAll为false or true -->
<input type="checkbox" v-model="isAll"/>
</label>
<span>
<span>已完成{{ doneTotal }}</span> / 全部{{ total }}
</span>
<button class="btn btn-danger" @click="clearAllDone">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: 'TodoFooter',
props: ['todos'],
computed:{
//总数
total(){
return this.todos.length
},
// 已完成数
doneTotal(){
//此处使用reduce方法做条件统计
/* return this.todos.reduce(
// todo:遍历数组todos中的每一个元素
// 比如:数组遍历时,把数组todos中的每一个元素分别赋值给todo【包含id='001'】
(pre,todo)=>{
// console.log('@',pre,todo)
return pre + (todo.done ? 1 : 0)
}
,0) */
//简写
return this.todos.reduce((pre,todo)=>pre + (todo.done ? 1 : 0),0)
},
//控制全选框
/* isAll(){
//计算属性简写:isAll属性,只能被读取,不能被修改
return this.total === this.doneTotal && this.total>0
} */
isAll:{
//get有什么作用?当有人读取isAll时,get就会被调用,且返回值就作为isAll的值
//get什么时候调用?1.初次读取isAll时。2.所依赖的数据发生变化时。
get(){
//全选框是否勾选 【&&:且】
return this.total === this.doneTotal && this.total>0
},
//set什么时候调用? 当isAll被修改时。
// value就是:v-model绑定的值false【未勾选】 or true【勾选】
set(value){
console.log(value)
this.$emit('checkAllTodo',value)
}
},
},
methods: {
/* checkAll(e){
console.log(e.target.checked);
// 拿到的是全选或者全不选的布尔值
this.checkAllTodo(e.target.checked)
} */
// 清空所有已完成
clearAllDone(){
// this.clearAllTodo()
this.$emit('clearAllTodo')
}
},
};
</script>
<style scoped>
/*footer*/
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}
.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}
.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}
.todo-footer button {
float: right;
margin-top: 5px;
}
</style>