一、实现样式

二、文档结构

三、核心点

  1. todos分为外面整体的App.vue,里面包含todoHeader.vue、List.vue>Item.vue、TodoFooter.vue

  2. 采用localStorage本地存储,对数据todos进行深度监视watch,只要数据一改变,就对localStorage本地存储数据进行修改

watch:{
    todos:{
      deep:true,
      handler(value){
        localStorage.setItem('todoList',JSON.stringify(value))
      }
  }

  1. 整体数据存放在App.vue里面,使用props方法将数据对子组件进行传递,使用全局事件总线来实现子对父的通信,

 // 安装事件总线
    beforeCreate() {
        Vue.prototype.$bus = this
    }
  1. 安装nanoid,用来生成唯一标识id

 // 生成一个对象
 const todo = {id:nanoid(),title:this.$refs.todotext.value,done:false};

四、附录

4.1 main.js

import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
​
new Vue({
    el: '#root',
    render: h => h(App),
    // 安装事件总线
    beforeCreate() {
        Vue.prototype.$bus = this
    }
​
})

4.2 App.vue

<template>
  <div class="main">
    <h1>TODOS</h1>
    <TodoHeader/>
    <List 
    :todos="todos" 
    :deletetodo="deletetodo"
    />
    <TodoFooter
    :todos="todos" 
    />
​
​
  </div>
</template>
​
<script>
  import TodoHeader from './components/TodoHeader'
  import List from './components/List.vue'
  import TodoFooter from './components/TodoFooter.vue'
​
​
    export default {
    name:'App',
    components:{
       TodoHeader,
       List,
       TodoFooter,
    },
    data(){
      return{
       todos:JSON.parse(localStorage.getItem('todoList'))||[]
    }
},
  watch:{
    todos:{
      deep:true,
      handler(value){
        localStorage.setItem('todoList',JSON.stringify(value))
      }
  }
  
},
​
methods:{
    // 添加数据到todos
    addtodo(todo){
          this.todos.unshift(todo)
    },
​
    // 删除todos
    deletetodo(id){
      // console.log(id);
    //过滤器 过滤id
    if(confirm('你确定删除吗?')){
      this.todos = this.todos.filter(todo=>todo.id !==id)
    }
     
    },
​
    // 全选  取消全选
    checkAlltodo(done){
      this.todos.forEach(todo=>{
          todo.done = done
      })
      // console.log(done);
     
    },
    // 删除已完成的事情
    clearDonetodo(){
        this.todos = this.todos.filter(todo=>{
        return !todo.done
      })
    },
    // 编辑
    edittodo(id,value){
      // console.log(id,value);
      this.todos.forEach(todo=>{
        if(todo.id === id)
          todo.title = value
      })
    }
},
mounted(){
  this.$bus.$on('addtodo',this.addtodo)
  this.$bus.$on('checkAlltodo',this.checkAlltodo)
  this.$bus.$on('deletetodo',this.deletetodo)
  this.$bus.$on('clearDonetodo',this.clearDonetodo)
  this.$bus.$on('edittodo',this.edittodo)
},
beforeDestroy(){
  this.$bus.$off('addtodo')
  this.$bus.$off('checkAlltodo')
  this.$bus.$off('deletetodo')
  this.$bus.$off('clearDonetodo')
   this.$bus.$off('edittodo')
}
   
  }
​
</script>
​
<style>
.main{
  /* text-align: center; */
  width: 600px;
  padding: 20px 8px;
  border: 1px solid #DDDDDD;
  border-radius: 8px;
  background-color:  rgba(255, 255, 255, 0.95);;
  /* margin-top: 120px; */
  box-shadow: 0 2px 4px 0 rgb(0 0 0 / 5%);
 
}
*{
  margin: 0 auto;
  box-sizing: border-box;
 
}
body{
  width: 100%;
            height: 100vh;  /* 重点一 */
            margin: 0 auto;
            background-image: url(./assets/back.jpg);
            background-repeat: no-repeat;
            background-size: cover; /* 重点二 */
            overflow: auto;
        padding-top: 120px;
}
h1{
  text-align: center;
  margin-bottom: 20px;
}
​
</style>

4.3 todoHeader.vue

<template>
    <div class="todoHeader">
        <input type="text" maxlength="30" ref="todotext" placeholder="请输入你的任务" @keydown.enter="inputtodo()">
    </div>
  
</template>
​
<script>
   import {nanoid} from 'nanoid'
​
export default {
    name: 'TodoHeader',
    methods:{
        // 输入框输入
        inputtodo(){
            // 判断是否为空
            if(this.$refs.todotext.value.trim()){
​
                // 生成一个对象
                const todo = {id:nanoid(),title:this.$refs.todotext.value,done:false};
                // this.addtodo(todo)
                this.$bus.$emit('addtodo',todo)
                // console.log(todo);
                this.$refs.todotext.value = ''
                
            }else{
                alert('输入内容不能为空!!!')
                this.$refs.todotext.value = ''
            }   
        }
    }
​
}
</script>
​
<style scoped>
.todoHeader input{
    height: 40px;
    width: 580px;
    margin-bottom: 20px;
    padding: 0 8px;
    font-size: 16px;
    border: 1px solid #ddd;
    outline: none;
    border-radius: 4px;
}
​
</style>

4.4 List.vue

<template>
<div class="todoList">
    <ul>
     <Item 
     v-for="(todo) in todos" 
     :key="todo.id" 
     :todo="todo"
     /> 
    </ul>
​
</div>
</template>
​
<script>
import Item from './Item.vue'
export default {
    name:'List',
    components:{
        Item
        },
props:['todos']
        
}
</script>
​
<style>
ul{
    padding: 0;
    margin: 0;
}
.todoList{
    margin-bottom: 30px;
}
​
​
</style>

4.5 Item.vue

<template> 
<li class="todoItem" :class="{'tododone':todo.done}">
    <input type="checkbox" v-model="todo.done">
       <span v-show="!todo.isEdit">{{todo.title}}</span>
       <input  type="text" maxlength="30"
        v-show="todo.isEdit" 
        :value="todo.title"
        ref="inputtext"
        @blur="inputblur"
        class="editItem"
        >
    <a href="javascript:;" >
        <img src="../assets/edit.png" @click="edittodo(todo)">
        <img src="../assets/delete.png" @click="deletetodo(todo.id)">  
    </a>
   
</li>
​
</template>
​
<script>
​
export default {
name:'Item', 
methods:{
    // 删除
    deletetodo(id){
        this.$bus.$emit('deletetodo',id)
    },
    // 编辑
    edittodo(todo){
        if(todo.hasOwnProperty('isEdit')){
            todo.isEdit = true;
        }else{
            this.$set(todo,'isEdit',true)
​
        }
        // 节点渲染完  获取焦点
        this.$nextTick(function(){
            this.$refs.inputtext.focus()
        } )
    },
    //失去焦点
    inputblur(todo){
        this.todo.isEdit = false
        // console.log(this.todo.id,this.$refs.inputtext.value);
        if(this.$refs.inputtext.value.trim()){
            this.$bus.$emit('edittodo',this.todo.id,this.$refs.inputtext.value)
        }else{
            alert('编辑内容不能为空!!!')
        } 
    }
},
props:['todo']
}
</script>
​
<style scoped>
.todoItem{
    list-style: none;
    height: 48px;
    width: 578px;
    border: 1px solid #ddd;
    line-height: 48px;
    padding: 0 10px;
    margin-bottom:-1px;
    position: relative;
}
.tododone{
    background-color: #eee;
    text-decoration: line-through;
    color: #ccc;
}
.todoItem a{
    color: #ccc;
    text-decoration: none;
    position:absolute;
    right:6px;
    top: 4px;
    display: none;
}
.todoItem img{
    width: 20px;
    height: 20px;
    margin-right: 8px;
}
.todoItem input{
    margin-right: 6px;
}
.editItem{
    outline: none;
    font-size: 16px;
    border: none;
    height: 30px;
    width: 400px;
    margin-left: -2px;
    background-color:transparent;
}
​
.todoItem:hover{
    background-color: #eee;
}
.todoItem:hover a{
    display: inline;
}
​
</style>
​

4.6 todoFooter.vue

<template>
    <div class="todoFooter" v-show="total>0">
        <input type="checkbox" v-model="isAll">
        <span>已完成{{donetotal}}/全部{{total}}</span>
        <button @click="cleardone">清除已完成任务</button>
    </div>
</template>
<script>
export default {
name:'TodoFooter',
data(){
    return{
        done:10,
        all:15
    }
},
computed:{
    // 已完成
        donetotal(){
            let i = 0
            this.todos.forEach(todo => {
                if(todo.done){
                i++
            }     
            });
            return i
        },
        // 总数
        total(){
            return this.todos.length
        },
        // 是否全选
        isAll:{
           get(){
            return this.donetotal == this.total && this.donetotal>0
           },
           set(value){
            // this.checkAlltodo(value)
            this.$bus.$emit('checkAlltodo',value)
           }   
        }
},
methods:{
    cleardone(){
        if(confirm('请确认是否删除所有已完成事项?'))
        // this.clearDonetodo()
        this.$bus.$emit('clearDonetodo') 
    }
},
props:['todos']
}
</script>

<style scoped>
.todoFooter{
    margin-left: 12px;
    position: relative;
    height: 34px;
    line-height: 34px;
}
.todoFooter input{
    margin-right: 6px;
}
.todoFooter button{
    width: 120px;
    height: 34px;
    font-size: 14px;
    background-color: #1A73E8;
    color: white;
    border: none;
    border-radius: 6px;
    position: absolute;
    right: 10px;
    /* bottom: 0px; */
    cursor: pointer;
}

</style>