Vue:实现TodoList案例(尚硅谷)

Vue核心:Vue核心:组件化编程(脚手架)

一、静态页面

app.vue
注: MyItem.vue不直接在app.vue中引入,而在MyList.vue中引入

<template>
   <div id="root">
     <div class="todo-container">
   	<div class="todo-wrap">
   	  <MyHeader/>
        <MyList/>
        <MyFooter/>
   	</div>
     </div>
   </div>

</template>


   import MyHeader from './components/MyHeader'
   import MyList from './components/MyList'
   import MyFooter from './components/MyFooter.vue'

   export default {
    
    
   	name:'App',
   	components:{
    
    MyHeader,MyList,MyFooter},
   }
</script>

<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;
     margin: 0 auto;
   }
   .todo-container .todo-wrap {
    
    
     padding: 10px;
     border: 1px solid #ddd;
     border-radius: 5px;
   }
</style> 

MyHeader.vue

<template>
	<div class="todo-header">
		<input type="text" placeholder="请输入你的任务名称,按回车键确认"/>
	 </div>
</template>

<style>
   /*header*/
	.todo-header input {
    
    
	  width: 560px;
	  height: 28px;
	  font-size: 14px;
	  border: 1px solid #ccc;
	  border-radius: 4px;
	  padding: 4px 7px;
	}

	.todo-header input:focus {
    
    
	  outline: none;
	  border-color: rgba(82, 168, 236, 0.8);
	  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
	}
</style>    

MyList.vue

<template>
	<ul class="todo-main">
		<li>
			<label>
				<input type="checkbox"/>
				<span>xxxxx</span>
			</label>
			<button class="btn btn-danger" style="display:none">删除</button>
		</li>
	</ul>
</template>

// 在拆 到 MyItem中
<template>
	<ul class="todo-main">
		<MyItem/>
       // 想要数据多 就继续引入 <MyItem/> 组件
	</ul>
</template>

<style>
  	/*list*/
	.todo-main {
    
    
	  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>    

MyItem.vue

<template>
	<li>
		 <label>
				<input type="checkbox"/>
				<span>xxxxx</span>
		 </label>
			  <button class="btn btn-danger" style="display:none">删除</button>
	</li>
</template>

<style>
	/*item*/
	li {
    
    
	  list-style: none;
	  height: 36px;
	  line-height: 36px;
	  padding: 0 5px;
	  border-bottom: 1px solid #ddd;
	}

	li label {
    
    
	  float: left;
	  cursor: pointer;
	}

	li label li input {
    
    
	  vertical-align: middle;
	  margin-right: 6px;
	  position: relative;
	  top: -1px;
	}

	li button {
    
    
	  float: right;
	  display: none;
	  margin-top: 3px;
	}

	li:before {
    
    
	  content: initial;
	}

	li:last-child {
    
    
	  border-bottom: none;
	}   
</style>  

在这里插入图片描述

二、展示动态的数据在这里插入图片描述

数据的类型、名称是什么

  • 一堆要做的事情是一个数组,一个个要做的事情是对象,对象里面的内容=={id,name,done(标识,完成)}==

数据保存在哪个组件

  • List组件展示就将数据保存在List中

MyList.vue

  • 根据数据决定使用多少次 MyItem
  • 把每一条的具体信息对象传递给 MyItem
<template>
	<ul class="todo-main">
		<MyItem v-for:"todoObj in todos" :key="todoObj.key" :todo="todoObj"/>
     
	</ul>
</template>

<script>
	import MyItem from './MyItem'

	export default {
    
    
		name:'MyList',
		components:{
    
    MyItem},
       data() {
    
    
           return {
    
    
               todos:[
                   {
    
    id:'001',title:'抽烟',done:true},
                   {
    
    id:'002',title:'喝酒',done:false},
                   {
    
    id:'003',title:'开车',done:true}
               ]
           }
       }
	}
</script>   

MyItem.vue

  • 接收
  • 动态决定是否勾选
<template>
	<li>
		 <label>
            	<!--动态决定是否勾选-->
				<input type="checkbox" :checked="todo.done"/>
				<span>{
    
    {
    
    todo.title}}</span>
		 </label>
			  <button class="btn btn-danger" style="display:none">删除</button>
	</li>
</template>

<script>
	export default {
    
    
		name:'MyItem',
		//声明接收todo
		props:['todo'],
	}
</script>

在这里插入图片描述

三、交互

组件之间的通信(兄弟、子传父、爷传孙),后面有更好的方式实现

3.1 添加

MyHeader.vue

  • 绑定个键盘事件

  • 把用户的输入打印

  • 获取用户的输入

    • 方式一:event 事件对象
    add(event){
          
          
       consloe.log(event.target.value) // 获得发生事件对象的元素
    }
    
    • 方式二:v-model
    <input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model='title' @keyup.enter="add"/>
    
    	data() {
          
          
            return {
          
          
                title:''
            }
        }
    	menthod: {
          
          
            add(event){
          
          
        		consloe.log(this.target) // 获得发生事件对象的元素
            }
        }
    
  • 把获取到的数据包装成一个todo对象 id使用uuid 的压缩版本 nanoid (单机版本) npm i nanoid

  • 把对象放到数组的前民(unshift),在List组件中保存数据的todos ,在Header组件输出

  • 两个兄弟组件之间直接进行数据传递——暂时实现不了

  • 原始间接传递

    • 把List中的todos[] 给 App,让App通过 props 方式传递给list
    • 让Header 把todoObj 给App
      在这里插入图片描述

具体案例实现:

  • 在App里定义一个addTodo方法,通过父传子的形式传给MyHeader
  • MyHeader调用了addTodo方法,并对App.vue在data.todos中添加一个todo
  • App.vue向MyList中传todos,即可达到插入新的事件的效果

App.vue

<template>
	<div id="root">
		<div class="todo-container">
			<div class="todo-wrap">
				<MyHeader :addTodo="addTodo"/>
				<MyList :todos="todos"/>
				<MyFoote/>
			</div>
		</div>
	</div>
</template>

<script>
	import MyHeader from './components/MyHeader'
	import MyList from './components/MyList'
	import MyFooter from './components/MyFooter.vue'

	export default {
    
    
		name:'App',
		components:{
    
    MyHeader,MyList,MyFooter},
		data() {
    
    
			return {
    
    
				//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
				todos:[
					{
    
    id:'001',title:'抽烟',done:true},
					{
    
    id:'002',title:'喝酒',done:false},
					{
    
    id:'003',title:'开车',done:true}
				]
			}
		},
        methods: {
    
    
			//在data.todos中添加一个todo
			addTodo(todoObj){
    
    
				this.todos.unshift(todoObj)
			}
		},

	}
</script>

MyHeader.vue

	<template>
	<div class="todo-header">
		<input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add"/>
	 </div>
</template>

<script>
   // 引入 nanoid 
   import {
    
    nanoid} from 'nanoid'
	export default {
    
    
		name:'MyHeader',
		props:['addTodo'],
		menthod: {
    
    
           add(event){
    
    
               	consloe.log(event.target.value) // 获得发生事件对象的元素
				//将用户的输入包装成一个todo对象
				const todoObj = {
    
    id:nanoid(),title:event.target.value,done:false}
               	consloe.log(todoObj)
               	// 方式一:实现 清空数据时操作了dom
               	this.addTodo(todoObj)
               	//清空输入
				event.target.value = ''
           }
       },
       // 方式二:v-model
       data() {
    
    
			return {
    
    
				//收集用户输入的title
				title:''
			}
		},
		methods: {
    
    
			add(){
    
    
				//校验数据
				if(!this.title.trim()) return alert('输入不能为空')
				//将用户的输入包装成一个todo对象
				const todoObj = {
    
    id:nanoid(),title:this.title,done:false}
				//通知App组件去添加一个todo对象
				this.addTodo(todoObj)
				//清空输入
				this.title = ''
			}
		},
       
	}
</script>

MyList.vue

<template>
	<ul class="todo-main">
		<MyItem v-for:"todoObj in todos" :key="todoObj.key" :todo="todoObj"/>
	</ul>
</template>

<script>
	import MyItem from './MyItem'

	export default {
    
    
		name:'MyList',
       	components:{
    
    MyItem},
		props:['todos'],
	}
</script>   

3.2 勾选

MyItem.vue

  • 拿到勾选的id,去todos中找到具体的某个人的 done 属性取反
  • todos数据在App (数据在哪里操作数据的方法就在哪里)
<template>
	<li>
		 <label>
           	<!--动态决定是否勾选-->
           	<!--change 改变就会触发-->
				<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/> 
			<!-- 	
				如下代码也能实现功能,但是不太推荐,因为有点违反原则,因为修改了props 
				v-model 绑定的是传递过来的数据 props 不建议
			-->
			<!-- <input type="checkbox" v-model="todo.done"/> -->
				<span>{
    
    {
    
    todo.title}}</span>
		 </label>
		 <button class="btn btn-danger" style="display:none">删除</button>
	</li>
</template>

<script>
	export default {
    
    
		name:'MyItem',
		//声明接收todo
		props:['todo'],
      	methods: {
    
    
			//勾选or取消勾选
			handleCheck(id){
    
    
				//通知App组件将对应的todo对象的done值取反
				//checkTodo为App.vue定义的方法
				this.checkTodo(id)
			}
		},
	}
</script>

App.vue

<template>
	<div id="root">
		<div class="todo-container">
			<div class="todo-wrap">
				<MyHeader :addTodo="addTodo" :checkTodo="checkTodo"/>
				<MyList :todos="todos"/>
				<MyFoote/>
			</div>
		</div>
	</div>
</template>

<script>
   
	export default {
    
    
		name:'App',
		components:{
    
    MyHeader,MyList,MyFooter},
		data() {
    
    
			return {
    
    
				//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在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取消勾选一个todo
			checkTodo(id){
    
    
				//通过Item传回的id参数,对todos做遍历,找到对应id的对象,将其done取反
				this.todos.forEach((todo)=>{
    
    
					if(todo.id === id) todo.done = !todo.done
				})
			}
		}
	}
</script>

在这里插入图片描述
MyList.vue
补充下列代码

<MyItem v-for:"todoObj in todos" :key="todoObj.key" :todo="todoObj" :checkTodo="checkTodo"/>	

props:['todos','checkTodo']

3.3 删除

  • 鼠标悬浮有高亮效果,并出现删除按钮
  • 获取id,根据id删除

MyItem.vue 通知app删除对应项 同样是 爷 传 孙

<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
	...	
	//声明接收todo、checkTodo、deleteTodo
	props:['todo','checkTodo','deleteTodo'],
	methods: {
    
    
			//删除
			handleDelete(id){
    
    
				//confirm会跳出个弹框让用户选择 确定 或 取消,并返回bool值
				if(confirm('确定删除吗?')){
    
    
					//通知App组件将对应的todo对象删除
					this.deleteTodo(id)
				}
			}
		},
	...		
<style scoped>
	li:hover{
    
    
		background-color: #ddd;
	} 
	li:hover button{
    
    
		display: block; // 鼠标滑过显示 删除按钮
	}
</style>

App.vue 传 list

<MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
...
methods: {
    
    
	//删除一个todo
	deleteTodo(id){
    
    
		// filter 不改变原数组 
		this.todos = this.todos.filter( todo => todo.id !== id )
	}
}
...

list 接收

<MyItem 
	v-for="todoObj in todos"
	:key="todoObj.id" 
	:todo="todoObj" 
	:checkTodo="checkTodo"
	:deleteTodo="deleteTodo"
/>
...
props:['todos','checkTodo','deleteTodo']

在这里插入图片描述

3.4 底部统计

  • 统计全部和已完成 MyFooter –> todos 数组的长度 done 为真的数量

App.vue 给 footer 传递todos数组

<MyFooter :todos="todos" />

MyFooter.vue 声明接收

// 1
<span>已完成{
    
    {
    
    todos.???}}</span> / 全部{
    
    {
    
    todos.length}}

props:['todos'],

//2
// 等于0 时不展示
<div class="todo-footer" v-show="total">
   
<span>已完成{
    
    {
    
    doneTotal}}</span> / 全部{
    
    {
    
    total}}

		computed: {
    
    
			//总数
			total(){
    
    
				return this.todos.length
			},
			//已完成数
			// 方式一: 数组中的方法 reduce 推荐
			doneTotal(){
    
    
				//此处使用reduce方法做条件统计
				//reduce以todos中的个数作为循环次数,第一次循环以程序员写的0作为pre,current是现在的todos[i]对象
				//第二次循环以第一次循环的返回值为pre,以此类推
				//最后一次循环的返回值作为整个函数的返回值,即返回给x
				
				/* const x = this.todos.reduce((pre,current)=>{
					console.log('@',pre,current)
					return pre + (current.done ? 1 : 0)
				},0) */
				
				//简写
				return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0) ,0)
			}
			// 方式二:常规遍历
			doneTotal(){
    
    
				let i = 0
				this.todos.forEach((todo)=>){
    
    
					if(todo.done) i++
				}
				return i
			}
		},

在这里插入图片描述

3.5 底部交互

  • 全选 / 全不选,取决于 已完成 和 全部 是否相等
  • 如果没有数据时,不应该勾选,且不应该展示下面整个框

3.5.1 MyFooter.vue 已完成 / 完成数量的动态变化

MyFooter.vue

//1.复杂写法
//<input type="checkbox" :checked="doneTotal === tatal"/>

//2.vue简便写法
//total = 0即没有添加事件时,该模块不显示
<div v-show="total">
	<input type="checkbox" :checked="isAll" @change="checkAll"/>
</div>
</script>
export default {
    
    
		name:'MyFooter',
		props:['todos','checkAllTodo','clearAllTodo'],
		computed: {
    
    
			//总数
			total(){
    
    
				return this.todos.length
			},
			//已完成数
			doneTotal(){
    
    
				return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0) ,0)
			},
			
			// 简写方式,没有setter 方法  只能被读取不能被修改才可以  后面需要修改
			//控制全选框
			// 一个计算属性可以通过其他的计算属性 在进行计算 
			isAll(){
    
     
				//已完成事件等于全部事件 且 全部事件大于0  才返回真
				return this.doneTotal === this.total && this.total > 0
			}
			
			
			
		},
	}
</script>

在这里插入图片描述

3.5.2 MyFooter.vue 全选 和 局部选 的动态绑定

  • this.checkAllTodo(e.target.checked) // true false 全选 或者 全不选
  • 告诉存储 todos 的人全选全不选

MyFooter.vue

// 全选按钮

//方法一:普通方法
<input type="checkbox" :checked="isAll" @change="checkAll"/>
...


methods: {
    
    
	checkAll(e){
    
    
		// true,false表示全选 或 全不选,传给app.vue中checkAllTodo方法
		this.checkAllTodo(e.target.checked) 
	} 		
},


// 方法二: v-model(推荐)
//注意这里修改的不是props,而是直接修改的todos,所以可以用v-model
<input type="checkbox" v-model="isAll"/>
...
	//非简写方式 可读可写
	computed: {
    
    
		//控制全选框
		isAll:{
    
    
			//全选框是否勾选
			get(){
    
    
				return this.doneTotal === this.total && this.total > 0
			},
			//isAll被修改时set被调用
			set(value){
    
    
				this.checkAllTodo(value)
			}
		}
	},

App.vue

<MyFooter :todos="todos" :checkAllTodo="checkAllTodo" />
methods: {
    
    
	//全选or取消全选
	//这个done就是全选框的true或false
	checkAllTodo(done){
    
    
		//遍历每一个小框,将小框的true或false和全选框的选择状态同步
		this.todos.forEach((todo)=>{
    
    
			todo.done = done
		})
	},
}

3.5.3 批量删除已完成事件

<div class="todo-footer" v-show="total">
	<label>
		<!-- <input type="checkbox" :checked="isAll" @change="checkAll"/> -->
		<input type="checkbox" v-model="isAll"/>
	</label>
	<span>
		<span>已完成{
    
    {
    
    doneTotal}}</span> / 全部{
    
    {
    
    total}}
	</span>
	<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
</div>
...
	props:['todos','checkAllTodo',,'clearAllTodo'],
	methods: {
    
    
		//批量删除已完成事件  
		clearAll(){
    
    
			this.clearAllTodo()
		}
	},

App.vue

//清除所有已经完成的todo
	clearAllTodo(){
    
    
		this.todos = this.todos.filter((todo)=>{
    
    
			return !todo.done
		})
	}

四、todoList案例总结

在这里插入图片描述

五、完整代码

App.vue

<template>
   <div id="root">
   	<div class="todo-container">
   		<div class="todo-wrap">
   			<MyHeader :addTodo="addTodo"/>
   			<MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
   			<MyFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"/>
   		</div>
   	</div>
   </div>
</template>

<script>
   import MyHeader from './components/MyHeader'
   import MyList from './components/MyList'
   import MyFooter from './components/MyFooter.vue'

   export default {
    
    
   	name:'App',
   	components:{
    
    MyHeader,MyList,MyFooter},
   	data() {
    
    
   		return {
    
    
   			//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在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取消勾选一个todo
   		checkTodo(id){
    
    
   			this.todos.forEach((todo)=>{
    
    
   				if(todo.id === id) todo.done = !todo.done
   			})
   		},
   		//删除一个todo
   		deleteTodo(id){
    
    
   			this.todos = this.todos.filter( todo => todo.id !== id )
   		},
   		//全选or取消全选
   		checkAllTodo(done){
    
    
   			this.todos.forEach((todo)=>{
    
    
   				todo.done = done
   			})
   		},
   		//清除所有已经完成的todo
   		clearAllTodo(){
    
    
   			this.todos = this.todos.filter((todo)=>{
    
    
   				return !todo.done
   			})
   		}
   	}
   }
</script>

<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;
   	margin: 0 auto;
   }
   .todo-container .todo-wrap {
    
    
   	padding: 10px;
   	border: 1px solid #ddd;
   	border-radius: 5px;
   }
</style>

MyHeader.vue

<template>
   <div class="todo-header">
   	<input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
   </div>
</template>

<script>
   import {
    
    nanoid} from 'nanoid'
   export default {
    
    
   	name:'MyHeader',
   	//接收从App传递过来的addTodo
   	props:['addTodo'],
   	data() {
    
    
   		return {
    
    
   			//收集用户输入的title
   			title:''
   		}
   	},
   	methods: {
    
    
   		add(){
    
    
   			//校验数据
   			if(!this.title.trim()) return alert('输入不能为空')
   			//将用户的输入包装成一个todo对象
   			const todoObj = {
    
    id:nanoid(),title:this.title,done:false}
   			//通知App组件去添加一个todo对象
   			this.addTodo(todoObj)
   			//清空输入
   			this.title = ''
   		}
   	},
   }
</script>

<style scoped>
   /*header*/
   .todo-header input {
    
    
   	width: 560px;
   	height: 28px;
   	font-size: 14px;
   	border: 1px solid #ccc;
   	border-radius: 4px;
   	padding: 4px 7px;
   }

   .todo-header input:focus {
    
    
   	outline: none;
   	border-color: rgba(82, 168, 236, 0.8);
   	box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
   }
</style>

MyFooter.vue

<template>
   <div class="todo-footer" v-show="total">
   	<label>
   		<!-- <input type="checkbox" :checked="isAll" @change="checkAll"/> -->
   		<input type="checkbox" v-model="isAll"/>
   	</label>
   	<span>
   		<span>已完成{
    
    {
    
    doneTotal}}</span> / 全部{
    
    {
    
    total}}
   	</span>
   	<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
   </div>
</template>

<script>
   export default {
    
    
   	name:'MyFooter',
   	props:['todos','checkAllTodo','clearAllTodo'],
   	computed: {
    
    
   		//总数
   		total(){
    
    
   			return this.todos.length
   		},
   		//已完成数
   		doneTotal(){
    
    
   			//此处使用reduce方法做条件统计
   			/* const x = this.todos.reduce((pre,current)=>{
   				console.log('@',pre,current)
   				return pre + (current.done ? 1 : 0)
   			},0) */
   			//简写
   			return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0) ,0)
   		},
   		//控制全选框
   		isAll:{
    
    
   			//全选框是否勾选
   			get(){
    
    
   				return this.doneTotal === this.total && this.total > 0
   			},
   			//isAll被修改时set被调用
   			set(value){
    
    
   				this.checkAllTodo(value)
   			}
   		}
   	},
   	methods: {
    
    
   		/* checkAll(e){
   			this.checkAllTodo(e.target.checked)
   		} */
   		//清空所有已完成
   		clearAll(){
    
    
   			this.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>

MyList.vue

<template>
   <ul class="todo-main">
   	<MyItem 
   		v-for="todoObj in todos"
   		:key="todoObj.id" 
   		:todo="todoObj" 
   		:checkTodo="checkTodo"
   		:deleteTodo="deleteTodo"
   	/>
   </ul>
</template>

<script>
   import MyItem from './MyItem'

   export default {
    
    
   	name:'MyList',
   	components:{
    
    MyItem},
   	//声明接收App传递过来的数据,其中todos是自己用的,checkTodo和deleteTodo是给子组件MyItem用的
   	props:['todos','checkTodo','deleteTodo']
   }
</script>

<style scoped>
   /*main*/
   .todo-main {
    
    
   	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>

MyItem.vue

<template>
   <li>
   	<label>
   		<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
   		<!-- 
   			
   			如下代码也能实现功能,但是不太推荐,因为有点违反原则,因为修改了props 
   			v-model 绑定的是传递过来的数据
   		-->
   		<!-- <input type="checkbox" v-model="todo.done"/> -->
   		<span>{
    
    {
    
    todo.title}}</span>
   	</label>
   	<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
   </li>
</template>

<script>
   export default {
    
    
   	name:'MyItem',
   	//声明接收todo、checkTodo、deleteTodo
   	props:['todo','checkTodo','deleteTodo'],
   	methods: {
    
    
   		//勾选or取消勾选
   		handleCheck(id){
    
    
   			//通知App组件将对应的todo对象的done值取反
   			this.checkTodo(id)
   		},
   		//删除
   		handleDelete(id){
    
    
   			if(confirm('确定删除吗?')){
    
    
   				//通知App组件将对应的todo对象删除
   				this.deleteTodo(id)
   			}
   		}
   	},
   }
</script>

<style scoped>
   /*item*/
   li {
    
    
   	list-style: none;
   	height: 36px;
   	line-height: 36px;
   	padding: 0 5px;
   	border-bottom: 1px solid #ddd;
   }

   li label {
    
    
   	float: left;
   	cursor: pointer;
   }

   li label li input {
    
    
   	vertical-align: middle;
   	margin-right: 6px;
   	position: relative;
   	top: -1px;
   }

   li button {
    
    
   	float: right;
   	display: none;
   	margin-top: 3px;
   }

   li:before {
    
    
   	content: initial;
   }

   li:last-child {
    
    
   	border-bottom: none;
   }

   li:hover{
    
    
   	background-color: #ddd;
   }
   
   li:hover button{
    
    
   	display: block;
   }
</style>

六、TodoList本地监视

关于浏览器本地存储不熟悉的可以看回这篇博客:

使用监视switch,监视数据todos的变化,变化后拿最新的数据存储

  • 第一次使用时,没有数据,JSON.parse 读取为空会报错,应该给一个空数组
  • 有勾选,监视的是todos下的done属性,所以应该是深度监视
    -完整版 deep:true

app.vue

<template>
	<div id="root">
		<div class="todo-container">
			<div class="todo-wrap">
				<MyHeader :addTodo="addTodo"/>
				<MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
				<MyFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"/>
			</div>
		</div>
	</div>
</template>

<script>
	import MyHeader from './components/MyHeader'
	import MyList from './components/MyList'
	import MyFooter from './components/MyFooter.vue'

	export default {
    
    
		name:'App',
		components:{
    
    MyHeader,MyList,MyFooter},
		data() {
    
    
			return {
    
    
				//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
               // 第一次使用时,没有数据,JSON.parse 读取为空会报错,应该给一个空数组
               //不为空则返回JSON对象
				todos:JSON.parse(localStorage.getItem('todos')) || []
			}
		},
		watch: {
    
    
			todos:{
    
    
				deep:true,
				handler(value){
    
    
					localStorage.setItem('todos',JSON.stringify(value))
				}
			}
		},
	}
</script>

在这里插入图片描述

七、TodoList自定义事件

app.vue对MyHeader.vue

<MyHeader @addTodo="addTodo"/>

MyHeader.vue

<template>
   <div class="todo-header">
   	<input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
   </div>
</template>

<script>
   import {
    
    nanoid} from 'nanoid'
   export default {
    
    
   	name:'MyHeader',
      // 接收App传递的过来的addTodo
      // props:['addTodo'],   不需要接收了
   	data() {
    
    
   		return {
    
    
   			title:''
   		}
   	},
   	methods: {
    
    
   		add(){
    
    
   			if(!this.title.trim()) return alert('输入不能为空')
   			const todoObj = {
    
    id:nanoid(),title:this.title,done:false}
   			//通知App组件去添加一个todo对象
              // this.addTodo(todoObj)
   			this.$emit('addTodo',todoObj,1,2,3) // 触发事件
   			this.title = ''
   		}
   	},
   }
</script>

在这里插入图片描述
app.vue对MyFooter.vue

// :todos="todos" 是传的数据,不用改
<MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>  

MyFooter.vue

<template>
	<div class="todo-footer" v-show="total">
		<label>
			<!-- <input type="checkbox" :checked="isAll" @change="checkAll"/> -->
			<input type="checkbox" v-model="isAll"/>
		</label>
		<span>
			<span>已完成{
    
    {
    
    doneTotal}}</span> / 全部{
    
    {
    
    total}}
		</span>
		<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
	</div>
</template>

<script>
	export default {
    
    
		name:'MyFooter',
       //props:['todos','checkAllTodo','clearAllTodo'],
		props:['todos'],
		computed: {
    
    
			total(){
    
    
				return this.todos.length
			},
			doneTotal(){
    
    
				return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0) ,0)
			},
			isAll:{
    
    
				get(){
    
    
					return this.doneTotal === this.total && this.total > 0
				},
				set(value){
    
    
					// this.checkAllTodo(value)
					this.$emit('checkAllTodo',value)
				}
			}
		},
		methods: {
    
    
			//清空所有已完成
			clearAll(){
    
    
				// this.clearAllTodo()
				this.$emit('clearAllTodo')
			}
		},
	}
</script>

在这里插入图片描述

八、 TodoList事件总线![

原本是App –> Mylist –>MyItem 逐层传递

main.js

// 安装全局事件总线
//创建vm
new Vue({
    
    
	el:'#app',
	render: h => h(App),
	beforeCreate() {
    
    
		Vue.prototype.$bus = this
	},
})

App.vue

<template>
	<div id="root">
		<div class="todo-container">
			<div class="todo-wrap">
				<!-- 1.1<MyHeader @addTodo="addTodo" :checkTodo="checkTodo":deleteTodo="deleteTodo"/> -->
				<MyList :todos="todos"/> // 不给list传
				<MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
			</div>
		</div>
	</div>
</template>

<script>
	export default {
    
    
        //收数据绑定事件总线,身上的自定义事件
		mounted() {
    
    
			this.$bus.$on('checkTodo',this.checkTodo) // 2.1
			this.$bus.$on('deleteTodo',this.deleteTodo) // 2.1
		},
		beforeDestroy() {
    
    
			this.$bus.$off('checkTodo') // 2.1
			this.$bus.$off('deleteTodo') // 2.1 
		},
	}
</script>

MyList.vue

<template>
	<ul class="todo-main">
		<MyItem 
			v-for="todoObj in todos"
			:key="todoObj.id" 
			:todo="todoObj" 
            <!--1.3:checkTodo="checkTodo" -->
            <!--1.4:deleteTodo="deleteTodo" -->  
		/>
	</ul>
</template>

<script>
	import MyItem from './MyItem'

	export default {
    
    
		name:'MyList',
		components:{
    
    MyItem},
		//声明接收App传递过来的数据
        // 1.2 props:['todos','checkTodo','clearAllTodo'] // List也不接收
		props:['todos']
	}
</script>

MyItem.vue

<template>
	<li>
		<label>
			<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
			<!-- 如下代码也能实现功能,但是不太推荐,因为有点违反原则,因为修改了props -->
			<!-- <input type="checkbox" v-model="todo.done"/> -->
			<span>{
    
    {
    
    todo.title}}</span>
		</label>
		<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
	</li>
</template>

<script>
	export default {
    
    
		name:'MyItem',
		//声明接收todo
		// 1.5 props:['todo','checkTodo','deleteTodo'], // Item 也接收不到了
        props:['todo'],
		methods: {
    
    
			//勾选or取消勾选
			handleCheck(id){
    
    
				// this.checkTodo(id)
				this.$bus.$emit('checkTodo',id)// tem里面触发,绑定事件 2.2
			},
			//删除
			handleDelete(id){
    
    
					// this.deleteTodo(id)
					this.$bus.$emit('deleteTodo',id) // 2.2
				}
			}
		},
	}
</script> 

在这里插入图片描述

九、TodoList消息订阅与发布

9.1 删除功能

App.vue 订阅 Item 发布
App.vue

<template>
  <div id="root">
  	<div class="todo-container">
  		<div class="todo-wrap">
  			<MyHeader @addTodo="addTodo"/>
  			<MyList :todos="todos"/>
  			<MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
  		</div>
  	</div>
  </div>
</template>

<script>
  import pubsub from 'pubsub-js'

  export default {
    
    
  	methods: {
    
    
  		//删除一个todo
  		//下划线占位,第一个参数是方法名
  		deleteTodo(_,id){
    
    
  			this.todos = this.todos.filter( todo => todo.id !== id )
  		}
  	},
  	mounted() {
    
    
  		this.$bus.$on('checkTodo',this.checkTodo)
  		this.pubId = pubsub.subscribe('deleteTodo',this.deleteTodo)
  	},
  	beforeDestroy() {
    
    
  		this.$bus.$off('checkTodo')
  		pubsub.unsubscribe(this.pubId)
  	},
  }
</script>

MyItem.vue

<script>
	import pubsub from 'pubsub-js'
	export default {
    
    
		methods: {
    
    
			//删除
			handleDelete(id){
    
    
				if(confirm('确定删除吗?')){
    
    
					// this.$bus.$emit('deleteTodo',id)
					pubsub.publish('deleteTodo',id)
				}
			}
		},
	}
</script>

9.2 TodoList编辑功能

  • 新增编辑按钮,点击编辑按钮,变成input框
  • 需要修改完后input变回文字,但由于在浏览器中存储了数据,所以刷新还是input,所以需要使用失去焦点事件
  • 数据校验输入不能为空
  • 点击编辑按钮时,新出现的输入框自动获取焦点

MyItem.vue

<template>
	<li>
		<label>
			<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
			<span v-show="!todo.isEdit">{
    
    {
    
    todo.title}}</span>
			<input 
				type="text" 
				v-show="todo.isEdit" 
				:value="todo.title" 
				@blur="handleBlur(todo,$event)"
				ref="inputTitle"
			>
		</label>
		<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
		<button v-show="!todo.isEdit" class="btn btn-edit" @click="handleEdit(todo)">编辑</button>
	</li>
</template>

<script>
	import pubsub from 'pubsub-js'
	export default {
    
    
		name:'MyItem',
		//声明接收todo
		props:['todo'],
		methods: {
    
    
			//勾选or取消勾选
			handleCheck(id){
    
    
				//通知App组件将对应的todo对象的done值取反
				// this.checkTodo(id)
				this.$bus.$emit('checkTodo',id)
			},
			//删除
			handleDelete(id){
    
    
				if(confirm('确定删除吗?')){
    
    
					//通知App组件将对应的todo对象删除
					// this.deleteTodo(id)
					// this.$bus.$emit('deleteTodo',id)
					pubsub.publish('deleteTodo',id)
				}
			},
			//编辑
			handleEdit(todo){
    
    
               // 判断 todo 身上是否有 isEdit 属性(正在修改的状态)
				if(todo.hasOwnProperty('isEdit')){
    
     
				// 有就直接修改
					todo.isEdit = true 
				}else{
    
    
					// console.log('@')
					// 没有添加 $set 添加数据(响应式)
					this.$set(todo,'isEdit',true) 
                   	console.log(todo)
				}
				//1.直接写focus会出现一个问题:系统在执行完整个代码才会重载Vue,
				//而在这过程中input还没有显示,即往一个不存在的input上挂focus,则无法实现
               //2. 解决方法一:简单实现-使用定时器setTimeout,可不给时间。因为定时器会在该区域代码执行完后再调用
               //3. 解决方法二(官方写法):$nextTick会在下一次DOM更新结束后执行其指定的回调
				this.$nextTick(function(){
    
     // $nextTick 下一轮 
					this.$refs.inputTitle.focus() // 拿到输入框获取焦点 focus获取焦点
				})
			},
			
			//失去焦点回调(真正执行修改逻辑)
			//e是输入框事件
			handleBlur(todo,e){
    
    
				todo.isEdit = false
				if(!e.target.value.trim()) return alert('输入不能为空!')
				this.$bus.$emit('updateTodo',todo.id,e.target.value)
			}
		},
	}
</script>

app.vue

<script>
	export default {
    
    

		methods: {
    
    
			//更新一个todo
			updateTodo(id,title){
    
    
				this.todos.forEach((todo)=>{
    
    
					if(todo.id === id) todo.title = title
				})
			}
		},
		mounted() {
    
    
			this.$bus.$on('updateTodo',this.updateTodo)
		},
		beforeDestroy() {
    
    
			this.$bus.$off('updateTodo')
		},
	}
</script>

十、TodoList过度与动画

给每件todoThing添加和删除添加动画效果

  • 方式一:给todo —>Item
  • 方式二:List

方式一:

<template>
	<transition name="todo" apper>
   	<li>
			<label>
			<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
			<!-- 如下代码也能实现功能,但是不太推荐,因为有点违反原则,因为修改了props -->
			<!-- <input type="checkbox" v-model="todo.done"/> -->
			<span v-show="!todo.isEdit">{
    
    {
    
    todo.title}}</span>
				<input 
					type="text" 
					v-show="todo.isEdit" 
					:value="todo.title" 
					@blur="handleBlur(todo,$event)"
					ref="inputTitle"
				>
			</label>
			<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
			<button v-show="!todo.isEdit" class="btn btn-edit" @click="handleEdit(todo)">编辑</button>
		</li>
   </transition>
</template>

<style scoped>
	.todo-enter-active{
    
    
		animation: atguigu 0.5s linear;
	}

	.todo-leave-active{
    
    
		animation: atguigu 0.5s linear reverse;
	}

	@keyframes atguigu {
    
    
		from{
    
    
			transform: translateX(100%);
		}
		to{
    
    
			transform: translateX(0px);
		}
	}
</style>

方式二:List

<template>
	<ul class="todo-main">
		<transition-group name="todo" appear>
			<!--使用一次,就是一次todo-->
			<MyItem 
				v-for="todoObj in todos"
				:key="todoObj.id" 
				:todo="todoObj" 
			/>
		</transition-group>
	</ul>
</template>

<style scoped>
	.todo-enter-active{
    
    
		animation: atguigu 0.5s linear;
	}

	.todo-leave-active{
    
    
		animation: atguigu 0.5s linear reverse;
	}

	@keyframes atguigu {
    
    
		from{
    
    
			transform: translateX(100%);
		}
		to{
    
    
			transform: translateX(0px);
		}
	}
</style>

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_51487301/article/details/126159930