Vue2 --- Todo-List 案例

0. 效果展示

1. 拆分静态组件

以功能为粒度抽取组件,使用组件实现静态页面效果

0. App.vue

<template>
    <div id="app">
        <div class="todo-container">
            <div class="todo-warp">
                <Header/>
                <List/>
                <Footer/>
            </div>
        </div>
    </div>
</template>

<script>

    import Header from "@/components/TodoHeader";
    import List from "@/components/TodoList";
    import Footer from "@/components/TodoFooter";

    export default {
        name: 'App',
        components: {
            Footer,
            List,
            Header
        }
    }
</script>

<style>
    body{
        background: #ffffff;
    }
    .btn{
        display: inline-block;
        padding: 4px 12px;
        margin-bottom: 0;
        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,0.05);
        border-radius: 4px;
    }
    .btn-danger{
        color: #ffffff;
        background-color: #da4f49;
        border: 1px solid #bd362f;
    }
    .btn-danger:hover{
        color: #ffffff;
        background-color: #bd362f;
    }
    .btn:focus{
        outline: none;
    }
    .todo-container{
        width: 600px;
        margin: 0 auto;
    }
    .todo-container .todo-warp{
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>

1. 添加任务

src/components/Header.vue

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

<script>
    export default {
        name: "HeaderInfo"
    }
</script>

<style scoped>
    .todo-header{
        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>

2. 任务列表

src/components/List.vue

<template>
    <div>
        <ul class="todo-main">
            <TodoItem/>
        </ul>
    </div>
</template>

<script>
    import TodoItem from "@/components/TodoItem";

    export default {
        name: "ListInfo",
        components: {
            TodoItem
        }
    }
</script>

<style scoped>
    .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>

3. 具体某项任务

src/components/Item.vue

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

<script>
    export default {
        name: "ItemInfo"
    }
</script>

<style scoped>
    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>

4. 任务页码展示

src/components/Footer.vue

<template>
    <div class="todo-footer">
        <label for="">
            <input type="checkbox">
        </label>
        <span>
            <span>已完成0</span> / 全部2
        </span>
        <button class="btn btn-danger">清除已完成的任务</button>
    </div>
</template>

<script>
    export default {
        name: "FooterInfo"
    }
</script>

<style scoped>
    .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>

2. 动态数据展示

1. 数据的类型,名称是什么

数组中有多个对象的嵌套结构,名字为todos

2. 数据保存在哪个组件

第一种情况: 哪个组件用到了就存在哪里

src/components/List.vue

<template>
    <div>
        <ul class="todo-main">
            <!-- 2. 遍历todos,并将每一个对象传到每一个子组件 -->
            <TodoItem v-for="todo in todos" :key="todo.id" :todo="todo"/>
        </ul>
    </div>
</template>

<script>
    import TodoItem from "@/components/TodoItem";

    export default {
        name: "ListInfo",
        components: {
            TodoItem
        },
        data() {
            return {
                // 1. 定义事件列表
                todos: [
                    {id: "1", title: "吃饭", done: false},
                    {id: "2", title: "睡觉", done: true},
                    {id: "3", title: "打豆豆", done: false},
                ]
            }
        }
    }
</script>

<style scoped>
    .todo-main {
        margin-left: 0;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding: 0;
    }

    .todo-empty {
        height: 40px;
        line-height: 40px;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding-left: 5px;
        margin-top: 10px;
    }
</style>

src/components/Item.vue

<template>
    <li>
        <label for="">
            <!-- 3.radio标签的勾选状态,使用动态属性checked,由父组件中todo对象的done字段决定是否勾选 -->
            <input type="checkbox" :checked="todo.done">
            <span>{{ todo.title}}</span>  <!-- 2. 展示要做事的名称 -->
        </label>
        <button class="btn btn-danger" style="display: none">删除</button>
    </li>
</template>

<script>
    export default {
        name: "ItemInfo",
        props: ["todo"],   // 1. 声明子组件接收的参数
    }
</script>

<style scoped>
    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>

3. 添加一个需要做的事情,默认为未完成

src/components/Header.vue

<template>
    <div class="todo-header">
        <!-- 1. 绑定回车键按下再抬起的事件,并将input中输入的值和title双向绑定 -->
        <input type="text" placeholder="请输入你的任务名称,按回车确认" v-model="title" @keyup.enter="addTodo">
    </div>
</template>

<script>
    // 3.2 导入nanoid包,用来生成唯一随机字符串
    import {nanoid} from 'nanoid'

    export default {
        name: "HeaderInfo",
        data() {
            return {
                title: ""  // 2. 定义input框中输入值的双向绑定
            }
        },
        methods: {
            // 3. 用户输入完成后点击回车,将数据保存到 todos 中
            addTodo() {
                console.log(this.title)
                // 3.1 将用户的输入封装成一个todo对象,id可以用uuid的变种nanoid生成的字符串来表示,npm i nanoid 下载这个包
                const todoObj = {id: nanoid(), title: this.title, done: false}
                console.log(todoObj)
            }
        }
    }
</script>

<style scoped>
    .todo-header {
        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>

写到这里遇到了一个问题,Header组件如何向同级的兄弟组件 List组件 传递参数呢?,以后会有事件总线之类的高级写法,这里还没学到,所以用第二种情况中的方法

第二种情况: 解决第一种情况的问题

所以todos就不能放在List组件中了,需要定义在App组件内,借助App组件,来对每个子组件传递参数

App.vue

<template>
    <div id="app">
        <div class="todo-container">
            <div class="todo-warp">
                <Header/>
                <List :todos="todos"/>
                <Footer/>
            </div>
        </div>
    </div>
</template>

<script>

    import Header from "@/components/TodoHeader";
    import List from "@/components/TodoList";
    import Footer from "@/components/TodoFooter";

    export default {
        name: 'App',
        components: {
            Footer,
            List,
            Header
        },
        data() {
            return {
                // 1. 定义事件列表
                todos: [
                    {id: "1", title: "吃饭", done: false},
                    {id: "2", title: "睡觉", done: true},
                    {id: "3", title: "打豆豆", done: false},
                ]
            }
        }
    }
</script>

<style>
    body {
        background: #ffffff;
    }

    .btn {
        display: inline-block;
        padding: 4px 12px;
        margin-bottom: 0;
        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, 0.05);
        border-radius: 4px;
    }

    .btn-danger {
        color: #ffffff;
        background-color: #da4f49;
        border: 1px solid #bd362f;
    }

    .btn-danger:hover {
        color: #ffffff;
        background-color: #bd362f;
    }

    .btn:focus {
        outline: none;
    }

    .todo-container {
        width: 600px;
        margin: 0 auto;
    }

    .todo-container .todo-warp {
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>

List.vue

<template>
    <div>
        <ul class="todo-main">
            <!-- 2. 遍历appTodos,并将每一个对象传到每一个子组件 -->
            <TodoItem v-for="todo in todos" :key="todo.id" :todo="todo"/>
        </ul>
    </div>
</template>

<script>
    import TodoItem from "@/components/TodoItem";

    export default {
        name: "ListInfo",
        components: {
            TodoItem
        },
        props:["todos"]   // 1.声明从App组件接收的参数名

    }
</script>

<style scoped>
    .todo-main {
        margin-left: 0;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding: 0;
    }

    .todo-empty {
        height: 40px;
        line-height: 40px;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding-left: 5px;
        margin-top: 10px;
    }
</style>

Item.vue不变

Header组件向App组件传参数

App.vue中定义一个自定义函数,并将这个函数传到Header组件内

<template>
    <div id="app">
        <div class="todo-container">
            <div class="todo-warp">
                <!-- 2. 将 receiveTodos 函数传到Header组件内 -->
                <Header :receiveTodos="receiveTodos"/>
                <List :todos="todos"/>
                <Footer/>
            </div>
        </div>
    </div>
</template>

<script>

    import Header from "@/components/TodoHeader";
    import List from "@/components/TodoList";
    import Footer from "@/components/TodoFooter";

    export default {
        name: 'App',
        components: {
            Footer,
            List,
            Header
        },
        data() {
            return {
                todos: [
                    {id: "1", title: "吃饭", done: false},
                    {id: "2", title: "睡觉", done: true},
                    {id: "3", title: "打豆豆", done: false},
                ]
            }
        },
        methods:{
            // 1. 定义自定义函数 receiveTodos(todos)
            receiveTodos(todoObj){
                console.log("我是App组件,我接受到了来自子组件的参数: ",todoObj)
                // 3. 将todo对象加入到todos中
                this.todos.unshift(todoObj)
                
            }
        }
    }
</script>

<style>
    body {
        background: #ffffff;
    }

    .btn {
        display: inline-block;
        padding: 4px 12px;
        margin-bottom: 0;
        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, 0.05);
        border-radius: 4px;
    }

    .btn-danger {
        color: #ffffff;
        background-color: #da4f49;
        border: 1px solid #bd362f;
    }

    .btn-danger:hover {
        color: #ffffff;
        background-color: #bd362f;
    }

    .btn:focus {
        outline: none;
    }

    .todo-container {
        width: 600px;
        margin: 0 auto;
    }

    .todo-container .todo-warp {
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>

Header.vue

<template>
    <div class="todo-header">
        <!-- 1. 绑定回车键按下再抬起的事件,并将input中输入的值和title双向绑定 -->
        <input type="text" placeholder="请输入你的任务名称,按回车确认" v-model="title" @keyup.enter="addTodo">
    </div>
</template>

<script>
    // 3.2 导入nanoid包,用来生成唯一随机字符串
    import {nanoid} from 'nanoid'

    export default {
        name: "HeaderInfo",
        props:["receiveTodos"],  // 1. 声明接收来自App组件的参数名
        data() {
            return {
                title: "" 
            }
        },
        methods: {
            addTodo() {
                if (!this.title.trim()) return alert("输入不能为空")
                const todoObj = {id: nanoid(), title: this.title, done: false}
                // 2. 调用receiveTodos(todObj), 并将用户生成的todo对象传到父组件定义好的函数中
                this.receiveTodos(todoObj)
                // 3. 清空input框中的数据
                this.title = ""
            }
        }
    }
</script>

<style scoped>
    .todo-header {
        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>

3. 交互

1. 勾选和取消勾选

上面代码也可以按照下面写,如果 input 是 checkbox,并且使用v-model绑定了一个布尔值,那么由于v-model是双向绑定,这个checkbox的勾选状态会直接影响源数据中布尔值的变化,但是不建议这样用,因为props是只读的,这样双向数据绑定是修改了props,vue监测props的对象地址的变化,没有更深层的监测其中元素的地址变化

Item.vue

<template>
    <li>
        <label for="">
            <!-- 使用v-model绑定了一个布尔值,那么由于v-model是双向绑定,这个checkbox的勾选状态会直接影响源数据中布尔值的变化 -->
            <input type="checkbox" :checked="todo.done" v-model="todo.done">
            <span>{{ todo.title}}</span>
        </label>
        <button class="btn btn-danger" style="display: none">删除</button>
    </li>
</template>

<script>
    export default {
        name: "ItemInfo",
        props: ["todo"],   // 1. 声明子组件接收的参数
    }
</script>

<style scoped>
    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>

2. 删除

App.vue 中定义删除item的方法

<template>
    <div id="app">
        <div class="todo-container">
            <div class="todo-warp">
                <Header :receiveTodos="receiveTodos"/>
                <List :todos="todos" :changeTodoStatus="changeTodoStatus" :deleteTodoObj="deleteTodoObj"/>
                <Footer/>
            </div>
        </div>
    </div>
</template>

<script>

    import Header from "@/components/TodoHeader";
    import List from "@/components/TodoList";
    import Footer from "@/components/TodoFooter";

    export default {
        name: 'App',
        components: {
            Footer,
            List,
            Header
        },
        data() {
            return {
                todos: [
                    {id: "1", title: "吃饭", done: false},
                    {id: "2", title: "睡觉", done: true},
                    {id: "3", title: "打豆豆", done: false},
                ]
            }
        },
        methods:{
            receiveTodos(todoObj){
                console.log("我是App组件,我接受到了来自子组件的参数: ",todoObj)
                this.todos.unshift(todoObj)
            },
            changeTodoStatus(id){
                this.todos.forEach((todo)=>{
                    if (id === todo.id) todo.done = !todo.done
                })
                console.log(this.todos)
            },
            // 1. 定义删除Item的方法
            deleteTodoObj(id){
                this.todos = this.todos.filter((todo)=>{
                    return todo.id !== id
                })
            }
            // 简写: 只有一个参数
            deleteTodoObj(id) {
                this.todos = this.todos.filter(todo => todo.id !== id)
            }
        }
    }
</script>

<style>
    body {
        background: #ffffff;
    }

    .btn {
        display: inline-block;
        padding: 4px 12px;
        margin-bottom: 0;
        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, 0.05);
        border-radius: 4px;
    }

    .btn-danger {
        color: #ffffff;
        background-color: #da4f49;
        border: 1px solid #bd362f;
    }

    .btn-danger:hover {
        color: #ffffff;
        background-color: #bd362f;
    }

    .btn:focus {
        outline: none;
    }

    .todo-container {
        width: 600px;
        margin: 0 auto;
    }

    .todo-container .todo-warp {
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>

List.vue 接收父组件App的 deleteTodoObj 并下传给子组件Item

<template>
    <div>
        <ul class="todo-main">
            <!-- 2. 将deleteTodoObj方法传给Item组件 -->
            <TodoItem 
                      v-for="todoObj in todos" 
                      :key="todoObj.id" 
                      :todo="todoObj" 
                      :changeTodoStatus="changeTodoStatus" 
                      :deleteTodoObj="deleteTodoObj"  
                      />
        </ul>
    </div>
</template>

<script>
    import TodoItem from "@/components/TodoItem";

    export default {
        name: "ListInfo",
        components: {
            TodoItem
        },
        // 1.声明从App组件接收的deleteTodoObj
        props:["todos","changeTodoStatus","deleteTodoObj"]   

    }
</script>

<style scoped>
    .todo-main {
        margin-left: 0;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding: 0;
    }

    .todo-empty {
        height: 40px;
        line-height: 40px;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding-left: 5px;
        margin-top: 10px;
    }
</style>

Item.vue

<template>
    <!-- 4. 绑定点击事件,得到勾选的某项的ID -->
    <li>
        <label for="">
            <!-- 3.radio标签的勾选状态,使用动态属性checked,由父组件中todo对象的done字段决定是否勾选 -->
            <input type="checkbox" :checked="todo.done" @change="changeTodo(todo.id)">
            <span>{{ todo.title}}</span>  <!-- 2. 展示要做事的名称 -->
        </label>
        <button class="btn btn-danger" @click="deleteTodo(todo.id)">删除</button>
    </li>
</template>

<script>
    export default {
        name: "ItemInfo",
        // 1. 声明接收deleteTodoObj
        props: ["todo", "changeTodoStatus","deleteTodoObj"],   
        methods: {
            changeTodo(id) {
                this.changeTodoStatus(id)
            },
            deleteTodo(id) {
                if (confirm("确定删除吗?")){
                    // 2. 调用deleteTodoObj,将操作的哪条的ID传回给App
                    this.deleteTodoObj(id)
                }
            }
        }
    }
</script>

<style scoped>
    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: #cccccc;
    }

    li:hover button {
        display: block;
    }
</style>

3. 底部统计

使用计算属性,计算出已完成的数量

<template>
    <div class="todo-footer">
        <label for="">
            <input type="checkbox">
        </label>
        <span>
            <!-- 使用计算出来的已完成的数量的计算属性 -->
            <span>已完成{{doneTodototal}}</span> / 全部{{total}}
        </span>
        <button class="btn btn-danger">清除已完成的任务</button>
    </div>
</template>

<script>
    export default {
        name: "FooterInfo",
        props: ["todos"],
        computed: {
            total(){
                return this.todos.length
            },
            // 1. 第一种方法: 计算出已完成的数量
            doneTodototal() {
                let num = 0
                this.todos.forEach(todo => {
                    if (todo.done) {
                        num++
                    }
                })
                return num
            },
            // 第二种方法: reduce是循环整个Array,参数1为自定义函数,参数2为统计条件,
            // pre是每次调用的上次调用reduce的返回值,current是当前的值,最后一次调用函数的返回值是整个reduce的返回值
            doneTodototal() {
                return this.todos.reduce((pre,todo) => pre + (todo.done ? 1 : 0), 0)
            },
        }
    }
</script>

<style scoped>
    .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>

4. 全选

App.vue 下传到Footer组件 全选或全不选的方法

<template>
    <div id="app">
        <div class="todo-container">
            <div class="todo-warp">
                <Header :receiveTodos="receiveTodos"/>
                <List
                        :todos="todos"
                        :changeTodoStatus="changeTodoStatus"
                        :deleteTodoObj="deleteTodoObj"
                />
                <!-- 2. 将checkAllTodo方法传给Footer组件 -->
                <Footer
                        :todos="todos"
                        :checkAllTodo="checkAllTodo"
                />
            </div>
        </div>
    </div>
</template>

<script>

    import Header from "@/components/TodoHeader";
    import List from "@/components/TodoList";
    import Footer from "@/components/TodoFooter";

    export default {
        name: 'App',
        components: {
            Footer,
            List,
            Header
        },
        data() {
            return {
                // 1. 定义事件列表
                todos: [
                    {id: "1", title: "吃饭", done: false},
                    {id: "2", title: "睡觉", done: true},
                    {id: "3", title: "打豆豆", done: false},
                ]
            }
        },
        methods: {
            receiveTodos(todoObj) {
                console.log("我是App组件,我接受到了来自子组件的参数: ", todoObj)
                this.todos.unshift(todoObj)
            },
            changeTodoStatus(id) {
                this.todos.forEach((todo) => {
                    if (id === todo.id) todo.done = !todo.done
                })
                console.log(this.todos)
            },
            deleteTodoObj(id) {
                this.todos = this.todos.filter(todo => todo.id !== id)
            },
            // 1. 定义全选或取消全选
            checkAllTodo(done){
                this.todos.forEach((todo)=>{
                    todo.done = done
                })
            }
        }
    }
</script>

<style>
    body {
        background: #ffffff;
    }

    .btn {
        display: inline-block;
        padding: 4px 12px;
        margin-bottom: 0;
        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, 0.05);
        border-radius: 4px;
    }

    .btn-danger {
        color: #ffffff;
        background-color: #da4f49;
        border: 1px solid #bd362f;
    }

    .btn-danger:hover {
        color: #ffffff;
        background-color: #bd362f;
    }

    .btn:focus {
        outline: none;
    }

    .todo-container {
        width: 600px;
        margin: 0 auto;
    }

    .todo-container .todo-warp {
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>

Footer.vue

<template>
    <div class="todo-footer" v-show="total">
        <label for="">
            <!-- 2. 绑定底部checkbox的点击事件 -->
            <input type="checkbox" :checked="isAll" @change="checkAll">
        </label>
        <span>
            <span>已完成{{doneTodototal}}</span> / 全部{{total}}
        </span>
        <button class="btn btn-danger">清除已完成的任务</button>
    </div>
</template>

<script>
    export default {
        name: "FooterInfo",
        // 1. 接收父组件的 checkAllTodo
        props: ["todos","checkAllTodo"],
        computed: {
            total(){
                return this.todos.length
            },
            doneTodototal() {
                return this.todos.reduce((pre,todo) => pre + (todo.done ? 1 : 0), 0)
            },
            isAll(){
                return this.doneTodototal === this.total && this.total > 0
            }
        },
        methods:{
            // 3. 点击全选或全不选
            checkAll(e){
                // 3.1 e.target.checked 底部的按钮是否被选中
                // 3.2 调用checkAllTodo,将勾选状态传回给App组件
                this.checkAllTodo(e.target.checked)
            }
        }
    }
</script>

<style scoped>
    .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>

对上面版本的优化

Footer.vue

<template>
    <!-- 3. 当总数等于0时,隐藏Footer组件 -->
    <div class="todo-footer" v-show="total">
        <label for="">
            <input type="checkbox" v-model="isAll">
        </label>
        <span>
            <span>已完成{{doneTodototal}}</span> / 全部{{total}}
        </span>
        <button class="btn btn-danger">清除已完成的任务</button>
    </div>
</template>

<script>
    export default {
        name: "FooterInfo",
        props: ["todos","checkAllTodo"],
        computed: {
            total(){
                return this.todos.length
            },
            doneTodototal() {
                return this.todos.reduce((pre,todo) => pre + (todo.done ? 1 : 0), 0)
            },
            isAll:{
                // 下面代码写的是计算属性的简写形式,只读取,修改就会报错
                // return this.doneTodototal === this.total && this.total > 0
                
                // 所以需要写上get()和set()
                get(){
                    return this.doneTodototal === this.total && this.total > 0
                },
                set(value){  // value是 isAll被修改的值
                    this.checkAllTodo(value)
                }
            }
        },
    }
</script>

<style scoped>
    .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>

5. 清除已完成任务

App.vue

<template>
    <div id="app">
        <div class="todo-container">
            <div class="todo-warp">
                <Header :receiveTodos="receiveTodos"/>
                <List
                        :todos="todos"
                        :changeTodoStatus="changeTodoStatus"
                        :deleteTodoObj="deleteTodoObj"
                />
                <!-- 2. 将 clearAllDoneTodo 传给 Footer 组件-->
                <Footer
                        :todos="todos"
                        :checkAllTodo="checkAllTodo"
                        :clearAllDoneTodo="clearAllDoneTodo"
                />
            </div>
        </div>
    </div>
</template>

<script>

    import Header from "@/components/TodoHeader";
    import List from "@/components/TodoList";
    import Footer from "@/components/TodoFooter";

    export default {
        name: 'App',
        components: {
            Footer,
            List,
            Header
        },
        data() {
            return {
                // 1. 定义事件列表
                todos: [
                    {id: "1", title: "吃饭", done: false},
                    {id: "2", title: "睡觉", done: true},
                    {id: "3", title: "打豆豆", done: false},
                ]
            }
        },
        methods: {
            receiveTodos(todoObj) {
                console.log("我是App组件,我接受到了来自子组件的参数: ", todoObj)
                this.todos.unshift(todoObj)
            },
            changeTodoStatus(id) {
                this.todos.forEach((todo) => {
                    if (id === todo.id) todo.done = !todo.done
                })
                console.log(this.todos)
            },
            deleteTodoObj(id) {
                this.todos = this.todos.filter(todo => todo.id !== id)
            },
            checkAllTodo(done) {
                this.todos.forEach((todo) => {
                    todo.done = done
                })
            },
            // 1. 定义 清除已完成任务
            clearAllDoneTodo() {
                if (confirm("确定清除吗?")) {
                    this.todos = this.todos.filter(todo => !todo.done)
                }

            }
        }
    }
</script>

<style>
    body {
        background: #ffffff;
    }

    .btn {
        display: inline-block;
        padding: 4px 12px;
        margin-bottom: 0;
        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, 0.05);
        border-radius: 4px;
    }

    .btn-danger {
        color: #ffffff;
        background-color: #da4f49;
        border: 1px solid #bd362f;
    }

    .btn-danger:hover {
        color: #ffffff;
        background-color: #bd362f;
    }

    .btn:focus {
        outline: none;
    }

    .todo-container {
        width: 600px;
        margin: 0 auto;
    }

    .todo-container .todo-warp {
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>

Footer.vue

<template>
    <div class="todo-footer" v-show="total">
        <label for="">
            <input type="checkbox" v-model="isAll">
        </label>
        <span>
            <span>已完成{{doneTodototal}}</span> / 全部{{total}}
        </span>
        <!-- 2. 绑定点击事件 clearAllDone -->
        <button class="btn btn-danger" @click="clearAllDone">清除已完成的任务</button>
    </div>
</template>

<script>
    export default {
        name: "FooterInfo",
        // 1. 声明接收 clearAllDoneTodo
        props: ["todos","checkAllTodo","clearAllDoneTodo"],
        computed: {
            total(){
                return this.todos.length
            },
            doneTodototal() {
                return this.todos.reduce((pre,todo) => pre + (todo.done ? 1 : 0), 0)
            },
            isAll:{
                get(){
                    return this.doneTodototal === this.total && this.total > 0
                },
                set(done){
                    this.checkAllTodo(done)
                }
            }
        },
        methods:{
            // 3. 调用App组件传过来的clearAllDoneTodo
            clearAllDone(){
                this.clearAllDoneTodo()
            }
        }
    }
</script>

<style scoped>
    .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>

4. 总结

  1. 组件化编程流程
    1. 拆分静态组件: 组件要按照功能点拆分,命名不要与html元素冲突
    2. 实现动态组件: 考虑好数据的存放位置,数据是一个组件在用,还是多个组件同时在用
      1. 一个组件在用: 放在组件自身即可
      2. 多个组件在用: 放在他们共同的父组件上(官方对此行为叫做状态提升)
    3. 实现交互: 从绑定事件开始
  2. props适用于:
    1. 父组件 ==> 子组件 的通信
    2. 子组件 ==> 父组件,需要父组件先下传一个函数,然后在子组件调用
  3. 使用v-model时切记: v-model 绑定的值不能是props传过来的值,因为props是不可以修改的
  4. props 传过来的如果是对象类型的值,修改对象中的属性时Vue不会报错,但是不推荐这样做

5. 加入本地存储机制

使用 watch() 监视todos的变化

App.vue

<template>
    <div id="app">
        <div class="todo-container">
            <div class="todo-warp">
                <Header :receiveTodos="receiveTodos"/>
                <List
                        :todos="todos"
                        :changeTodoStatus="changeTodoStatus"
                        :deleteTodoObj="deleteTodoObj"
                />
                <!-- 2. 将 clearAllDoneTodo 传给 Footer 组件-->
                <Footer
                        :todos="todos"
                        :checkAllTodo="checkAllTodo"
                        :clearAllDoneTodo="clearAllDoneTodo"
                />
            </div>
        </div>
    </div>
</template>

<script>

    import Header from "@/components/TodoHeader";
    import List from "@/components/TodoList";
    import Footer from "@/components/TodoFooter";

    export default {
        name: 'App',
        components: {
            Footer,
            List,
            Header
        },
        data() {
            return {
                // 1. 事件列表从 localstorage 中读取
                todos: JSON.parse(localStorage.getItem("todos"))
            }
        },
        methods: {
            receiveTodos(todoObj) {
                console.log("我是App组件,我接受到了来自子组件的参数: ", todoObj)
                this.todos.unshift(todoObj)
            },
            changeTodoStatus(id) {
                this.todos.forEach((todo) => {
                    if (id === todo.id) todo.done = !todo.done
                })
                console.log(this.todos)
            },
            deleteTodoObj(id) {
                this.todos = this.todos.filter(todo => todo.id !== id)
            },
            checkAllTodo(done) {
                this.todos.forEach((todo) => {
                    todo.done = done
                })
            },
            // 1. 定义 清除已完成任务
            clearAllDoneTodo() {
                if (confirm("确定清除吗?")) {
                    this.todos = this.todos.filter(todo => !todo.done)
                }

            }
        },
        // 2. 如果localstorage发生变化就将新的对象写入localstorage中,覆盖之前的
        watch: {
            todos(value) {
                localStorage.setItem("todos",JSON.stringify(value))
            }
        }
    }
</script>

<style>
    body {
        background: #ffffff;
    }

    .btn {
        display: inline-block;
        padding: 4px 12px;
        margin-bottom: 0;
        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, 0.05);
        border-radius: 4px;
    }

    .btn-danger {
        color: #ffffff;
        background-color: #da4f49;
        border: 1px solid #bd362f;
    }

    .btn-danger:hover {
        color: #ffffff;
        background-color: #bd362f;
    }

    .btn:focus {
        outline: none;
    }

    .todo-container {
        width: 600px;
        margin: 0 auto;
    }

    .todo-container .todo-warp {
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>

上面代码存在的BUG

  1. localStorage中没有todos这个key时,此时的todos为null,Footer组件读null.length时就会报错

    	data() {
                return {
                    // 1. 取值的时候,加个或空数组
                    todos: JSON.parse(localStorage.getItem("todos")) || []
                }
            },
    
  2. 勾选某项任务完成的时候,localStorage中的done并未发生变化

    	// 改为深度监视
        watch: {
            todos: {
                deep:true,
                    handler(value) {
                    localStorage.setItem("todos", JSON.stringify(value))
                }
            },
        }
    

6. 子传父改为自定义事件

App.vue

<template>
    <div id="app">
        <div class="todo-container">
            <div class="todo-warp">
                <!-- 1. 删除自定义函数的方式,改为自定义事件方式 -->
                <!-- <Header :receiveTodos="receiveTodos"/> -->
                <Header @receiveTodos="receiveTodos"/>
                <List
                        :todos="todos"
                        :changeTodoStatus="changeTodoStatus"
                        :deleteTodoObj="deleteTodoObj"
                />
                <!-- 3. 将自定义函数 clearAllDoneTodo ,改为自定义事件方式 -->
                <Footer
                        :todos="todos"
                        :checkAllTodo="checkAllTodo"
                        @clearAllDoneTodo="clearAllDoneTodo"
                />
            </div>
        </div>
    </div>
</template>

<script>

    import Header from "@/components/TodoHeader";
    import List from "@/components/TodoList";
    import Footer from "@/components/TodoFooter";

    export default {
        name: 'App',
        components: {
            Footer,
            List,
            Header
        },
        data() {
            return {
                todos: JSON.parse(localStorage.getItem("todos")) || []
            }
        },
        methods: {
            // 2. 改为 receiveTodos 事件的回调函数
            receiveTodos(todoObj) {
                console.log("我是App组件,我接受到了来自子组件的参数: ", todoObj)
                this.todos.unshift(todoObj)
            },
            changeTodoStatus(id) {
                this.todos.forEach((todo) => {
                    if (id === todo.id) todo.done = !todo.done
                })
                console.log(this.todos)
            },
            deleteTodoObj(id) {
                this.todos = this.todos.filter(todo => todo.id !== id)
            },
            checkAllTodo(done) {
                this.todos.forEach((todo) => {
                    todo.done = done
                })
            },
            clearAllDoneTodo() {
                if (confirm("确定清除吗?")) {
                    this.todos = this.todos.filter(todo => !todo.done)
                }

            }
        },
        watch: {
            todos: {
                deep:true,
                handler(value) {
                    localStorage.setItem("todos", JSON.stringify(value))
                }
            },
        }
    }
</script>

<style>
    body {
        background: #ffffff;
    }

    .btn {
        display: inline-block;
        padding: 4px 12px;
        margin-bottom: 0;
        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, 0.05);
        border-radius: 4px;
    }

    .btn-danger {
        color: #ffffff;
        background-color: #da4f49;
        border: 1px solid #bd362f;
    }

    .btn-danger:hover {
        color: #ffffff;
        background-color: #bd362f;
    }

    .btn:focus {
        outline: none;
    }

    .todo-container {
        width: 600px;
        margin: 0 auto;
    }

    .todo-container .todo-warp {
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>

Header.vue

<template>
    <div class="todo-header">
        <!-- 2. 这里触发App中定义好的 receiveTodos 事件 -->
        <input type="text" placeholder="请输入你的任务名称,按回车确认" v-model="title" @keyup.enter="addTodo">
    </div>
</template>

<script>
    // 3.2 导入nanoid包,用来生成唯一随机字符串
    import {nanoid} from 'nanoid'

    export default {
        name: "HeaderInfo",
        // props:["receiveTodos"], // 1. 不再接收自定义函数
        data() {
            return {
                title: ""
            }
        },
        methods: {
            addTodo() {
                if (!this.title.trim()) return alert("输入不能为空")
                const todoObj = {id: nanoid(), title: this.title, done: false}
                // 3. 将调用自定义函数,改为触发自定义事件
                // this.receiveTodos(todoObj)
                this.$emit("receiveTodos",todoObj)
                this.title = ""
            }
        }
    }
</script>

<style scoped>
    .todo-header {
        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>

Footer.vue

<template>
    <div class="todo-footer" v-show="total">
        <label for="">
            <input type="checkbox" v-model="isAll">
        </label>
        <span>
            <span>已完成{{doneTodototal}}</span> / 全部{{total}}
        </span>
        <!-- 2. 在这里触发自定义事件 clearAllDoneTodo -->
        <button class="btn btn-danger" @click="clearAllDone">清除已完成的任务</button>
    </div>
</template>

<script>
    export default {
        name: "FooterInfo",
        // 1. 删除接收自定义函数 checkAllTodo,clearAllDoneTodo
        props: ["todos"],
        computed: {
            total(){
                return this.todos.length
            },
            doneTodototal() {
                return this.todos.reduce((pre,todo) => pre + (todo.done ? 1 : 0), 0)
            },
            isAll:{
                get(){
                    return this.doneTodototal === this.total && this.total > 0
                },
                set(done){
                    // 4. 删除自定义函数checkAllTodo,修改为触发自定义事件
                    // this.checkAllTodo(done)
                    this.$emit("checkAllTodo",done)
                }
            }
        },
        methods:{
            clearAllDone(){
                // 3. 删除自定义函数,修改为触发自定义事件
                // this.clearAllDoneTodo()
                this.$emit("clearAllDoneTodo")
            }
        }
    }
</script>

<style scoped>
    .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>

7. 加入事件总线(推荐)

main.js

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
    render: h => h(App),
    beforeCreate() {
        Vue.prototype.$bus = this;   // 安装全局事件总线,$bus就是当前应用的vm
    }
}).$mount('#app')

App.vue

<template>
    <div id="app">
        <div class="todo-container">
            <div class="todo-warp">
                <Header @receiveTodos="receiveTodos"/>
                <!-- 1. 删除changeTodoStatus和deleteTodoObj 自定义函数-->
                <List :todos="todos"/>
                <Footer
                        :todos="todos"
                        @checkAllTodo="checkAllTodo"
                        @clearAllDoneTodo="clearAllDoneTodo"
                />
            </div>
        </div>
    </div>
</template>

<script>

    import Header from "@/components/TodoHeader";
    import List from "@/components/TodoList";
    import Footer from "@/components/TodoFooter";

    export default {
        name: 'App',
        components: {
            Footer,
            List,
            Header
        },
        data() {
            return {
                todos: JSON.parse(localStorage.getItem("todos")) || []
            }
        },
        // 2. 在mounted中声明给$bus 事件总线绑定 changeTodoStatus和deleteTodoObj自定义事件
        mounted() {
            this.$bus.$on("changeTodoStatus",this.changeTodoStatus)
            this.$bus.$on("deleteTodoObj",this.deleteTodoObj)
        },
        // 3. 在beforeDestory中解绑自定义事件 changeTodoStatus deleteTodoObj
        beforeDestroy() {
            this.$bus.$off(["changeTodoStatus","deleteTodoObj"])
        },
        methods: {
            receiveTodos(todoObj) {
                console.log("我是App组件,我接受到了来自子组件的参数: ", todoObj)
                this.todos.unshift(todoObj)
            },
            changeTodoStatus(id) {
                this.todos.forEach((todo) => {
                    if (id === todo.id) todo.done = !todo.done
                })
                console.log(this.todos)
            },
            deleteTodoObj(id) {
                this.todos = this.todos.filter(todo => todo.id !== id)
            },
            checkAllTodo(done) {
                this.todos.forEach((todo) => {
                    todo.done = done
                })
            },
            clearAllDoneTodo() {
                if (confirm("确定清除吗?")) {
                    this.todos = this.todos.filter(todo => !todo.done)
                }

            }
        },
        watch: {
            todos: {
                deep:true,
                handler(value) {
                    localStorage.setItem("todos", JSON.stringify(value))
                }
            },
        }
    }
</script>

<style>
    body {
        background: #ffffff;
    }

    .btn {
        display: inline-block;
        padding: 4px 12px;
        margin-bottom: 0;
        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, 0.05);
        border-radius: 4px;
    }

    .btn-danger {
        color: #ffffff;
        background-color: #da4f49;
        border: 1px solid #bd362f;
    }

    .btn-danger:hover {
        color: #ffffff;
        background-color: #bd362f;
    }

    .btn:focus {
        outline: none;
    }

    .todo-container {
        width: 600px;
        margin: 0 auto;
    }

    .todo-container .todo-warp {
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>

List.vue

<template>
    <div>
        <ul class="todo-main">
            <!-- 1. 删除changeTodoStatus和deleteTodoObj 自定义函数 -->
            <TodoItem
                    v-for="todoObj in todos"
                    :key="todoObj.id"
                    :todo="todoObj"
            />
        </ul>
    </div>
</template>

<script>
    import TodoItem from "@/components/TodoItem";

    export default {
        name: "ListInfo",
        components: {
            TodoItem
        },
        props:["todos"]   // 2.删除changeTodoStatus和deleteTodoObj

    }
</script>

<style scoped>
    .todo-main {
        margin-left: 0;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding: 0;
    }

    .todo-empty {
        height: 40px;
        line-height: 40px;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding-left: 5px;
        margin-top: 10px;
    }
</style>

Item.vue

<template>
    <!-- 4. 绑定点击事件,得到勾选的某项的ID -->
    <li>
        <label for="">
            <!-- 3.radio标签的勾选状态,使用动态属性checked,由父组件中todo对象的done字段决定是否勾选 -->
            <input type="checkbox" :checked="todo.done" @change="changeTodo(todo.id)">
            <span>{{ todo.title}}</span>  <!-- 2. 展示要做事的名称 -->
        </label>
        <button class="btn btn-danger" @click="deleteTodo(todo.id)">删除</button>
    </li>
</template>

<script>
    export default {
        name: "ItemInfo",
        props: ["todo"],   // 1. 删除changeTodoStatus和deleteTodoObj
        methods: {
            changeTodo(id) {
                // 2. 删除调用父组件定义的自定义函数,改为事件总线触发 changeTodoStatus 自定义事件
                // this.changeTodoStatus(id)
                this.$bus.$emit("changeTodoStatus",id)
            },
            deleteTodo(id) {
                if (confirm("确定删除吗?")){
                    // 3. 删除调用父组件定义的自定义函数,改为事件总线触发 deleteTodoObj 自定义事件
                    // this.deleteTodoObj(id)
                    this.$bus.$emit("deleteTodoObj",id)

                }
            }
        }
    }
</script>

<style scoped>
    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: #cccccc;
    }

    li:hover button {
        display: block;
    }
</style>

8. 改为消息订阅与发布

下载 pubsub-js

npm i pubsub-js

main.js

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
    render: h => h(App),
    // 1. 卸载全局事件总线
    // beforeCreate() {
        // Vue.prototype.$bus = this;   // 安装全局事件总线,$bus就是当前应用的vm
    // }
}).$mount('#app')

App.vue

<template>
    <div id="app">
        <div class="todo-container">
            <div class="todo-warp">
                <Header @receiveTodos="receiveTodos"/>
                <!-- 1. 删除changeTodoStatus和deleteTodoObj 自定义函数-->
                <List :todos="todos"/>
                <Footer
                        :todos="todos"
                        @checkAllTodo="checkAllTodo"
                        @clearAllDoneTodo="clearAllDoneTodo"
                />
            </div>
        </div>
    </div>
</template>

<script>

    import Header from "@/components/TodoHeader";
    import List from "@/components/TodoList";
    import Footer from "@/components/TodoFooter";
    // 1. 引入pubsub-js
    import pubsub from 'pubsub-js'

    export default {
        name: 'App',
        components: {
            Footer,
            List,
            Header
        },
        data() {
            return {
                todos: JSON.parse(localStorage.getItem("todos")) || []
            }
        },
        // 2. 在mounted() 中定义消息订阅与发布
        mounted() {
            // 2.1 删除事件总线方式,改为消息订阅与发布方式
            // this.$bus.$on("changeTodoStatus",this.changeTodoStatus)
            // this.$bus.$on("deleteTodoObj",this.deleteTodoObj)
            
            this.changPubID = pubsub.subscribe("changeTodoStatus",this.changeTodoStatus)
            this.deletePubID = pubsub.subscribe("deleteTodoObj",this.deleteTodoObj)
        },
        // 3. 在beforeDestory()中取消订阅
        beforeDestroy() {
            // this.$bus.$off(["changeTodoStatus","deleteTodoObj"])
            pubsub.unsubscribe(this.changPubID)
            pubsub.unsubscribe(this.deletePubID)
        },
        methods: {
            receiveTodos(todoObj) {
                console.log("我是App组件,我接受到了来自子组件的参数: ", todoObj)
                this.todos.unshift(todoObj)
            },
            changeTodoStatus(_,id) {
                this.todos.forEach((todo) => {
                    if (id === todo.id) todo.done = !todo.done
                })
                console.log(this.todos)
            },
            deleteTodoObj(_,id) {
                this.todos = this.todos.filter(todo => todo.id !== id)
            },
            checkAllTodo(done) {
                this.todos.forEach((todo) => {
                    todo.done = done
                })
            },
            clearAllDoneTodo() {
                if (confirm("确定清除吗?")) {
                    this.todos = this.todos.filter(todo => !todo.done)
                }

            }
        },
        watch: {
            todos: {
                deep:true,
                handler(value) {
                    localStorage.setItem("todos", JSON.stringify(value))
                }
            },
        }
    }
</script>

<style>
    body {
        background: #ffffff;
    }

    .btn {
        display: inline-block;
        padding: 4px 12px;
        margin-bottom: 0;
        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, 0.05);
        border-radius: 4px;
    }

    .btn-danger {
        color: #ffffff;
        background-color: #da4f49;
        border: 1px solid #bd362f;
    }

    .btn-danger:hover {
        color: #ffffff;
        background-color: #bd362f;
    }

    .btn:focus {
        outline: none;
    }

    .todo-container {
        width: 600px;
        margin: 0 auto;
    }

    .todo-container .todo-warp {
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>

Item.vue

<template>
    <li>
        <label for="">
            <input type="checkbox" :checked="todo.done" @change="changeTodo(todo.id)">
            <span>{{ todo.title}}</span>  <!-- 2. 展示要做事的名称 -->
        </label>
        <button class="btn btn-danger" @click="deleteTodo(todo.id)">删除</button>
    </li>
</template>

<script>
    // 1. 引入pubsub-js
    import pubsub from 'pubsub-js'
    
    export default {
        name: "ItemInfo",
        props: ["todo"],
        methods: {
            // 2. changeTodo 和 deleteTodo改为发布消息方式
            changeTodo(id) {
                // this.$bus.$emit("changeTodoStatus",id)
                pubsub.publish("changeTodoStatus",id)
            },
            deleteTodo(id) {
                if (confirm("确定删除吗?")){
                    // this.$bus.$emit("deleteTodoObj",id)
                    pubsub.publish("deleteTodoObj",id)
                }
            }
        }
    }
</script>

<style scoped>
    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: #cccccc;
    }

    li:hover button {
        display: block;
    }
</style>

9. 添加编辑按钮

Item.vue

<template>
    <li>
        <label for="">
            <input type="checkbox" :checked="todo.done" @change="changeTodo(todo.id)">
            <!-- 4. 和span标签相反出现,用!isEdit控制-->
            <span v-show="!todo.isEdit">{{ todo.title}}</span>
            <!-- 3. 准备input框,绑定失去焦点事件,失去焦点的瞬间保存数据 添加v-fbind自定义指令,默认获取焦点-->
            <input type="text" v-show="todo.isEdit" :value="todo.title" v-fbind @blur="confirmEdit(todo,$event)">
        </label>
        <button class="btn btn-danger" @click="deleteTodo(todo.id)">删除</button>
        <!-- 1. 添加编辑按钮,并绑定事件,传的是todo和event事件对象,用todo.isEdit控制编辑按钮是否展示 -->
        <button v-show="!todo.isEdit" class="btn btn-edit" @click="editTodo(todo,$event)">编辑</button>
    </li>
</template>

<script>
    export default {
        name: "ItemInfo",
        props: ["todo"],
        directives: {
            // fbind自定义指令,默认获取焦点
            fbind(element){
                element.focus()  // 获取焦点
            }

        },
        methods: {
            changeTodo(id) {
                this.$bus.$emit("changeTodoStatus", id)
            },
            deleteTodo(id) {
                if (confirm("确定删除吗?")) {
                    this.$bus.$emit("deleteTodoObj", id)

                }
            },
            // 2. 编辑Todo
            editTodo(todo, e) {
                console.log("e>>>>>", e)
                // 这样是不会触发Vue对todo的监视的,需要用this.$set()来设置属性
                // todo.isEdit = true

                // todo身上有isEdit,用todo.isEdit设置
                // eslint-disable-next-line no-prototype-builtins
                if (todo.hasOwnProperty("isEdit")) {
                    // 如果有isEdit,就直接修改即可
                    todo.isEdit = true
                } else {
                    // 如果没有isEdit,需要用this.$set()来设置属性
                    this.$set(todo, "isEdit", true)

                }
            },
            confirmEdit(todo, e) {
                // 3. 先将input隐藏,改为文本框
                todo.isEdit = false
                if (!e.target.value.trim()) return alert("输入不能为空") // 判断不为空
                this.$bus.$emit("updateTodo", todo.id, e.target.value) // 传需要改哪条和修改后的input值
            }
        }
    }
</script>

<style scoped>
    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: #cccccc;
    }

    li:hover button {
        display: block;
    }
</style>

App.vue

<template>
    <div id="app">
        <div class="todo-container">
            <div class="todo-warp">
                <Header @receiveTodos="receiveTodos"/>
                <List :todos="todos"/>
                <Footer
                        :todos="todos"
                        @checkAllTodo="checkAllTodo"
                        @clearAllDoneTodo="clearAllDoneTodo"
                />
            </div>
        </div>
    </div>
</template>

<script>

    import Header from "@/components/TodoHeader";
    import List from "@/components/TodoList";
    import Footer from "@/components/TodoFooter";

    export default {
        name: 'App',
        components: {
            Footer,
            List,
            Header
        },
        data() {
            return {
                todos: JSON.parse(localStorage.getItem("todos")) || []
            }
        },
        mounted() {
            this.$bus.$on("changeTodoStatus", this.changeTodoStatus)
            this.$bus.$on("deleteTodoObj", this.deleteTodoObj)
            // 1. 给全局事件总线添加 updateTodo 自定义事件
            this.$bus.$on("updateTodo", this.updateTodo)
        },
        beforeDestroy() {
            // 3. 解绑updateTodo
            this.$bus.$off(["changeTodoStatus", "deleteTodoObj","updateTodo"])
        },
        methods: {
            receiveTodos(todoObj) {
                console.log("我是App组件,我接受到了来自子组件的参数: ", todoObj)
                this.todos.unshift(todoObj)
            },
            changeTodoStatus(id) {
                this.todos.forEach((todo) => {
                    if (id === todo.id) todo.done = !todo.done
                })
                console.log(this.todos)
            },
            deleteTodoObj(id) {
                this.todos = this.todos.filter(todo => todo.id !== id)
            },
            checkAllTodo(done) {
                this.todos.forEach((todo) => {
                    todo.done = done
                })
            },
            clearAllDoneTodo() {
                if (confirm("确定清除吗?")) {
                    this.todos = this.todos.filter(todo => !todo.done)
                }

            },
            // 2. 修改todos中的元素title
            updateTodo(id, title) {
                this.todos.forEach((todo) => {
                    if (id === todo.id) todo.title = title
                })
            }
        },
        watch: {
            todos: {
                deep: true,
                handler(value) {
                    localStorage.setItem("todos", JSON.stringify(value))
                }
            },
        }
    }
</script>

<style>
    body {
        background: #ffffff;
    }

    .btn {
        display: inline-block;
        padding: 4px 12px;
        margin-bottom: 0;
        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, 0.05);
        border-radius: 4px;
    }

    .btn-danger {
        color: #ffffff;
        background-color: #da4f49;
        border: 1px solid #bd362f;
    }

    .btn-edit {
        color: #ffffff;
        background-color: deepskyblue;
        border: 1px solid #bd362f;
        margin-right: 5px;
    }

    .btn-danger:hover {
        color: #ffffff;
        background-color: #bd362f;
    }

    .btn:focus {
        outline: none;
    }

    .todo-container {
        width: 600px;
        margin: 0 auto;
    }

    .todo-container .todo-warp {
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>

对于获取焦点,除了使用自定义命令外,也可以用ref属性来的到DOM元素

Item.vue

<template>
    <li>
        <label for="">
            <input type="checkbox" :checked="todo.done" @change="changeTodo(todo.id)">
            <span v-show="!todo.isEdit">{{ todo.title}}</span>
            <!-- 使用ref绑定DOM元素 -->
            <input type="text" v-show="todo.isEdit" :value="todo.title" ref="updateElement" @blur="confirmEdit(todo,$event)">
        </label>
        <button class="btn btn-danger" @click="deleteTodo(todo.id)">删除</button>
        <button v-show="!todo.isEdit" class="btn btn-edit" @click="editTodo(todo,$event)">编辑</button>
    </li>
</template>

<script>
    export default {
        name: "ItemInfo",
        props: ["todo"],

        methods: {
            changeTodo(id) {
                this.$bus.$emit("changeTodoStatus", id)
            },
            deleteTodo(id) {
                if (confirm("确定删除吗?")) {
                    this.$bus.$emit("deleteTodoObj", id)

                }
            },
            editTodo(todo, e) {
                if (todo.hasOwnProperty("isEdit")) {
                    todo.isEdit = true
                } else {
                    this.$set(todo, "isEdit", true)

                }
                // 执行这句话的时候,Vue还没有渲染出input框,Vue是等所有的代码执行完毕后,再统一渲染页面,所以页面没有效果
                // this.$refs.updateElement.focus()  
                
                // 使用$nextTick()解决,它指定的回调会在Dom元素更新完毕之后再执行
                this.$nextTick(function(){
                    this.$refs.updateElement.focus()
                }) 
            },
            confirmEdit(todo, e) {
                todo.isEdit = false
                if (!e.target.value.trim()) return alert("输入不能为空")
                this.$bus.$emit("updateTodo", todo.id, e.target.value)
            }
        }
    }
</script>

<style scoped>
    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: #cccccc;
    }

    li:hover button {
        display: block;
    }
</style>

8. 加入动画效果

8.1 每项Todo加动画效果

Item.vue

<template>
    <!-- 1. 整个外层套上transition -->
    <transition name="leftSlide" appear>
        <li>
            <label for="">
                <input type="checkbox" :checked="todo.done" @change="changeTodo(todo.id)">
                <span v-show="!todo.isEdit">{{ todo.title}}</span>
                <input type="text" v-show="todo.isEdit" :value="todo.title" ref="updateElement"
                       @blur="confirmEdit(todo,$event)">
            </label>
            <button class="btn btn-danger" @click="deleteTodo(todo.id)">删除</button>
            <button v-show="!todo.isEdit" class="btn btn-edit" @click="editTodo(todo,$event)">编辑</button>
        </li>
    </transition>

</template>

<script>
    export default {
        name: "ItemInfo",
        props: ["todo"],

        methods: {
            changeTodo(id) {
                this.$bus.$emit("changeTodoStatus", id)
            },
            deleteTodo(id) {
                if (confirm("确定删除吗?")) {
                    this.$bus.$emit("deleteTodoObj", id)

                }
            },
            // eslint-disable-next-line no-unused-vars
            editTodo(todo, e) {
                // eslint-disable-next-line no-prototype-builtins
                if (todo.hasOwnProperty("isEdit")) {
                    todo.isEdit = true
                } else {
                    this.$set(todo, "isEdit", true)

                }
                this.$nextTick(function () {
                    this.$refs.updateElement.focus()  
                })

            },
            confirmEdit(todo, e) {
                todo.isEdit = false
                if (!e.target.value.trim()) return alert("输入不能为空")
                this.$bus.$emit("updateTodo", todo.id, e.target.value)
            }
        }
    }
</script>

<style scoped>
    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: #cccccc;
    }

    li:hover button {
        display: block;
    }
    /* 2. 编写动画效果 */ 
    /* 进入的起点 */
    .leftSlide-enter, .leftSlide-leave-to {
        transform: translateX(100%);
    }

    /* 进入和离开过程中,需要激活的动画效果 */
    .leftSlide-enter-active, .leftSlide-leave-active {
        transition: 0.7s linear;
    }

    /* 进入的终点 */
    .leftSlide-enter-to, .leftSlide-leave {
        transform: translateX(0);
    }
</style>

8.2 Item组件加动画效果

这里必须用 transition-group 标签,因为是多个Item组件

List.vue

<template>
    <div>
        <ul class="todo-main">
            <!-- 1. Item组件外层套 transition-group 标签 -->
            <transition-group name="leftSlide" appear>

                <TodoItem
                        v-for="todoObj in todos"
                        :key="todoObj.id"
                        :todo="todoObj"
                />
            </transition-group>
        </ul>
    </div>
</template>

<script>
    import TodoItem from "@/components/TodoItem";

    export default {
        name: "ListInfo",
        components: {
            TodoItem
        },
        props: ["todos"]  

    }
</script>

<style scoped>
    .todo-main {
        margin-left: 0;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding: 0;
    }

    .todo-empty {
        height: 40px;
        line-height: 40px;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding-left: 5px;
        margin-top: 10px;
    }

    /* 2. 编写动画效果 */
    /* 进入的起点 */
    .leftSlide-enter, .leftSlide-leave-to {
        transform: translateX(100%);
    }

    /* 进入和离开过程中,需要激活的动画效果 */
    .leftSlide-enter-active, .leftSlide-leave-active {
        transition: 0.7s linear;
    }

    /* 进入的终点 */
    .leftSlide-enter-to, .leftSlide-leave {
        transform: translateX(0);
    }
</style>
posted @ 2024-03-20 15:12  河图s  阅读(33)  评论(0)    收藏  举报