VueCLI核心知识综合案例TodoList

目录

[1 拿到一个功能模块首先需要拆分组件:](#1 拿到一个功能模块首先需要拆分组件:)

[2 使用组件实现静态页面的效果](#2 使用组件实现静态页面的效果)

[3 分析数据保存在哪个组件](#3 分析数据保存在哪个组件)

[4 实现添加数据](#4 实现添加数据)

[5 实现复选框勾选](#5 实现复选框勾选)

[6 实现数据的删除](#6 实现数据的删除)

[7 实现底部组件中数据的统计](#7 实现底部组件中数据的统计)

[8 实现勾选全部的小复选框来实现大复选框的勾选](#8 实现勾选全部的小复选框来实现大复选框的勾选)

[9 实现勾选大复选框来实现所有的小复选框都被勾选](#9 实现勾选大复选框来实现所有的小复选框都被勾选)

[10 清空所有数据](#10 清空所有数据)

[11 实现案例中的数据存入本地存储](#11 实现案例中的数据存入本地存储)

[12 案例中使用自定义事件完成组件间的数据通信](#12 案例中使用自定义事件完成组件间的数据通信)

[13 案例中实现数据的编辑](#13 案例中实现数据的编辑)

[14 实现数据进出的动画效果](#14 实现数据进出的动画效果)


【分析】组件化编码的流程

  1. 实现静态组件:抽取组件,使用组件实现静态页面效果

2.展示动态数据:

2.1 数据的类型、名称是什么?

2.2 数据保存在哪个组件?

3.交互---从绑定事件监听开始


1 拿到一个功能模块首先需要拆分组件:


2 使用组件实现静态页面的效果

【main.js】

javascript 复制代码
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  el: '#app',
  render: h => h(App)
})

【MyHeader】

html 复制代码
<template>
    <div class="todo-header">
       <input type="text"/>
    </div>
</template>

<script>
    export default {
        name: 'MyHeader',
    }
</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>

【Item】

html 复制代码
<template> 
    <li>
        <label>
            <input type="checkbox"/>
            <span v-for="todo in todos" :key="todo.id">{{todo.title}}</span>
        </label>
        <button class="btn btn-danger">删除</button>
    </li>
</template>

<script>
    export default {
        name: 'Item',
        data() {
            return {
                todos: [
                    {id: '001', title: '吃饭', done: true},
                    {id: '002', title: '学习', done: false},
                    {id: '003', title: '追剧', done: true},
                ]
            }
        },
    }
</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: rgb(196, 195, 195);
    }
    li:hover button{
        display: block;
    }
</style>

【List】

html 复制代码
<template>
    <ul class="todo-main">
        <Item></Item>
        <Item></Item>
    </ul>
</template>

<script>
    import Item from './Item.vue'
    export default {
        name: 'List',
        components:{
            Item
        },
        
    }
</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>

【MyFooter】

html 复制代码
<template>
    <div class="todo-footer">
        <label>
            <input type="checkbox"/>
        </label>

        <span>
            <span>已完成 0</span> / 3
        </span>
        <button class="btn btn-danger">清除已完成任务</button>
    </div>
</template>

<script>
    export default {
        name: 'MyFooter',
    }
</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>

【App】

html 复制代码
<template>
    <div id="root">
        <div class="todo-container">
            <div class="todo-wrap">
                <MyHeader></MyHeader>
                <List></List>
                <MyFooter></MyFooter>
            </div>
        </div>
    </div>
</template>

<script>
    import MyHeader from './components/MyHeader.vue'
    import List from './components/List.vue'
    import MyFooter from './components/MyFooter.vue'
    
    export default {
        name:'App',
        components:{
            MyHeader,
            List,
            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>

通过以上代码就可以实现静态页面的效果了!!!

3 分析数据保存在哪个组件

在上述代码中数据是保存在Item组件中的,但是如果想要在后续实现一系列交互效果:在MyHeader组件中需要添加数据,而MyHeader组件和Item组件没有直接的关系, 就当前学习阶段的知识而言,并不能实现这两个组件之间的通信(后续会有解决方案),同理MyFooter也一样。


【分析】因为App组件是所有组件的父组件,所以数据放在App组件中,再使用props配置,所有的子组件就都可以访问到。

【App】(同时需要将数据传递到Item组件中,在当前阶段只能通过props配置一层一层往下传,所以是 App-->List,List-->Item)

1. 实现 App-->List 传递todos数据

html 复制代码
<template>
    <div id="root">
        <div class="todo-container">
            <div class="todo-wrap">
                <MyHeader></MyHeader>
                <List :todos="todos"></List>
                <MyFooter></MyFooter>
            </div>
        </div>
    </div>
</template>

<script>
    import MyHeader from './components/MyHeader.vue'
    import List from './components/List.vue'
    import MyFooter from './components/MyFooter.vue'
    
    export default {
        name:'App',
        components:{
            MyHeader,
            List,
            MyFooter
        },
        data() {
            return {
                todos: [
                    {id: '001', title: '吃饭', done: true},
                    {id: '002', title: '学习', done: false},
                    {id: '003', title: '追剧', done: true},
                ]
            }
        },
    }
</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>

2. List接受todos数据

html 复制代码
<template>
    <ul class="todo-main">
        <Item v-for="todo in todos" 
        :key="todo.id"></Item>
    </ul>
</template>

<script>
    import Item from './Item.vue'
    export default {
        name: 'List',
        components:{
            Item
        },
        props: ['todos']
        
    }
</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>

通过上述的代码便可以根据数据的数量来渲染出几个Item了,但是此时Item里面是没有内容的,所以需要 List-->Item 再次传递每条数据


3. 实现 List-->Item 传递todo数据

html 复制代码
<template>
    <ul class="todo-main">
        <Item v-for="todo in todos" 
        :key="todo.id" :todo="todo"></Item>
    </ul>
</template>

<script>
    import Item from './Item.vue'
    export default {
        name: 'List',
        components:{
            Item
        },
        props: ['todos']
        
    }
</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>

4. Item接受todo数据

html 复制代码
<template> 
    <li>
        <label>
            <input type="checkbox"/>
          
            <span>{{todo.title}}</span>
        </label>
        <button class="btn btn-danger">删除</button>
    </li>
</template>

<script>
    export default {
        name: 'Item',
        // 声明接收todo对象
        props:['todo'],
    }
</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: rgb(196, 195, 195);
    }
    li:hover button{
        display: block;
    }
</style>

4 实现添加数据

【App】定义接收数据的回调函数

html 复制代码
<template>
    <div id="root">
        <div class="todo-container">
            <div class="todo-wrap">
                <MyHeader :addTodo="addTodo"></MyHeader>
                <List :todos="todos"></List>
                <MyFooter></MyFooter>
            </div>
        </div>
    </div>
</template>

<script>
    import MyHeader from './components/MyHeader.vue'
    import List from './components/List.vue'
    import MyFooter from './components/MyFooter.vue'
    
    export default {
        name:'App',
        components:{
            MyHeader,
            List,
            MyFooter
        },
        data() {
            return {
                todos: [
                    {id: '001', title: '吃饭', done: true},
                    {id: '002', title: '学习', done: false},
                    {id: '003', title: '追剧', done: true},
                ]
            }
        },
        methods: {
            // 添加一个todo
            addTodo(todoObj) {
                this.todos.unshift(todoObj)
            }
        }
    }
</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】实现添加数据的方法

html 复制代码
<template>
    <div class="todo-header">
        <!-- 绑定键盘回车事件 -->
       <input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add" v-model="title"/>
    </div>
</template>

<script>
    import {nanoid} from 'nanoid'  // 生成id
    export default {
        name: 'MyHeader',
        data() {
            return {
                title: ''
            }
        },
        props: ['addTodo'],  // 接收父组件传过来的addTodo函数
        methods: {
            add(e) {
                // 校验数据
                if (!this.title.trim()) return alert('输入不能为空')

                // 将用户的输入包装成为一个todo对象
                const todoObj = {
                    id: nanoid(),
                    /* title: e.target.value, */
                    title: this.title,
                    done: false
                }
                console.log(todoObj)
                // console.log(e.target.value)
                // console.log(this.title)
                // 通知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>

5 实现复选框勾选

【App】也是要逐层传递

html 复制代码
<template>
    <div id="root">
        <div class="todo-container">
            <div class="todo-wrap">
                <MyHeader :addTodo="addTodo"></MyHeader>
                <List :todos="todos" :changeTodo="changeTodo"></List>
                <MyFooter></MyFooter>
            </div>
        </div>
    </div>
</template>

<script>
    import MyHeader from './components/MyHeader.vue'
    import List from './components/List.vue'
    import MyFooter from './components/MyFooter.vue'
    
    export default {
        name:'App',
        components:{
            MyHeader,
            List,
            MyFooter
        },
        data() {
            return {
                todos: [
                    {id: '001', title: '吃饭', done: true},
                    {id: '002', title: '学习', done: false},
                    {id: '003', title: '追剧', done: true},
                ]
            }
        },
        methods: {
            // 添加一个todo
            addTodo(todoObj) {
                this.todos.unshift(todoObj)
            },
            // 勾选或者取消勾选一个todo
            changeTodo(id) {
                this.todos.forEach((todo) => {
                  if (todo.id === id) todo.done = !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>

【List】

html 复制代码
<template>
    <ul class="todo-main">
        <Item v-for="todo in todos" 
        :key="todo.id" :todo="todo"
        :changeTodo="changeTodo">
        </Item>
    </ul>
</template>

<script>
    import Item from './Item.vue'
    export default {
        name: 'List',
        components:{
            Item
        },
        props: ['todos', 'changeTodo']
        
    }
</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>

【Item】

html 复制代码
<template> 
    <li>
        <label>
            <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
          
            <span>{{todo.title}}</span>
        </label>
        <button class="btn btn-danger">删除</button>
    </li>
</template>

<script>
    export default {
        name: 'Item',
        // 声明接收todo对象
        props:['todo', 'changeTodo'],
        methods: {
            // 勾选 or 取消勾选
            handleCheck(id) {
                // 通知 App组件将对应的todo对象的状态改变
                this.changeTodo(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: rgb(196, 195, 195);
    }
    li:hover button{
        display: block;
    }
</style>

6 实现数据的删除

【App】

html 复制代码
<template>
    <div id="root">
        <div class="todo-container">
            <div class="todo-wrap">
                <MyHeader :addTodo="addTodo"></MyHeader>
                <List 
                    :todos="todos" 
                    :changeTodo="changeTodo" 
                    :deleteTodo="deleteTodo">
                </List>
                <MyFooter></MyFooter>
            </div>
        </div>
    </div>
</template>

<script>
    import MyHeader from './components/MyHeader.vue'
    import List from './components/List.vue'
    import MyFooter from './components/MyFooter.vue'
    
    export default {
        name:'App',
        components:{
            MyHeader,
            List,
            MyFooter
        },
        data() {
            return {
                todos: [
                    {id: '001', title: '吃饭', done: true},
                    {id: '002', title: '学习', done: false},
                    {id: '003', title: '追剧', done: true},
                ]
            }
        },
        methods: {
            // 添加一个todo
            addTodo(todoObj) {
                this.todos.unshift(todoObj)
            },

            // 勾选或者取消勾选一个todo
            changeTodo(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)
            },
        }
    }
</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>

【List】

html 复制代码
<template>
    <ul class="todo-main">
        <Item v-for="todo in todos" 
            :key="todo.id" 
            :todo="todo"
            :changeTodo="changeTodo"
            :deleteTodo="deleteTodo">
        </Item>
    </ul>
</template>

<script>
    import Item from './Item.vue'
    export default {
        name: 'List',
        components:{
            Item
        },
        props: ['todos', 'changeTodo', '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>

【Item】

html 复制代码
<template> 
    <li>
        <label>
            <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
          
            <span>{{todo.title}}</span>
        </label>
        <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
    </li>
</template>

<script>
    export default {
        name: 'Item',
        // 声明接收todo对象
        props:['todo', 'changeTodo', 'deleteTodo'],
        methods: {
            // 勾选 or 取消勾选
            handleCheck(id) {
                // 通知 App组件将对应的todo对象的状态改变
                this.changeTodo(id)
                
            },

            // 删除操作
            handleDelete(id) {
                // console.log(id)
                if (confirm("确定删除吗?")) {
                    // 通知App删除
                    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: rgb(196, 195, 195);
    }
    li:hover button{
        display: block;
    }
</style>

7 实现底部组件中数据的统计

**【分析】**如果想要统计数据的数量,就需要将数据传递到MyFooter组件中

【App】

html 复制代码
<template>
    <div id="root">
        <div class="todo-container">
            <div class="todo-wrap">
                <MyHeader :addTodo="addTodo"></MyHeader>
                <List 
                    :todos="todos" 
                    :changeTodo="changeTodo" 
                    :deleteTodo="deleteTodo">
                </List>
                <MyFooter
                    :todos="todos"
                >
                </MyFooter>
            </div>
        </div>
    </div>
</template>

<script>
    import MyHeader from './components/MyHeader.vue'
    import List from './components/List.vue'
    import MyFooter from './components/MyFooter.vue'
    
    export default {
        name:'App',
        components:{
            MyHeader,
            List,
            MyFooter
        },
        data() {
            return {
                todos: [
                    {id: '001', title: '吃饭', done: true},
                    {id: '002', title: '学习', done: false},
                    {id: '003', title: '追剧', done: true},
                ]
            }
        },
        methods: {
            // 添加一个todo
            addTodo(todoObj) {
                this.todos.unshift(todoObj)
            },

            // 勾选或者取消勾选一个todo
            changeTodo(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)
            },
        }
    }
</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>

【MyFooter】

在这个组件中可以使用计算属性实现数据的总长度和被勾选的数据的计算

html 复制代码
<template>
    <div class="todo-footer" v-if="todosLength">
        <label>
            <input type="checkbox"/>
        </label>
        <span>
            <span>已完成 {{doneTotal}}</span> / {{todosLength}}
        </span>
        <button class="btn btn-danger">清除已完成任务</button>
    </div>
</template>

<script>
export default {
    name: 'MyFooter',
    props: ['todos'],
    computed: {
        todosLength() {
            return this.todos.length
        },
        doneTotal() {
            return this.todos.filter(todo => todo.done).length

            // 也可以使用下面求和来实现
            // return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
        }
    }
}
</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>

8 实现勾选全部的小复选框来实现大复选框的勾选

:checked="isAll"

isAll也是通过计算属性计算得来的

【MyFooter】

html 复制代码
<template>
    <div class="todo-footer" v-if="todosLength">
        <label>
            <input type="checkbox" :checked="isAll"/>
        </label>
        <span>
            <span>已完成 {{doneTotal}}</span> / {{todosLength}}
        </span>
        <button class="btn btn-danger">清除已完成任务</button>
    </div>
</template>

<script>
export default {
    name: 'MyFooter',
    props: ['todos'],
    computed: {
        todosLength() {
            return this.todos.length
        },
        doneTotal() {
            return this.todos.filter(todo => todo.done).length

            // 也可以使用下面求和来实现
            // return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
        },

        isAll() {
            return this.doneTotal === this.todosLength && this.todosLength > 0
        }, 
    }
}
</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>

9 实现勾选大复选框来实现所有的小复选框都被勾选

【App】

html 复制代码
<template>
    <div id="root">
        <div class="todo-container">
            <div class="todo-wrap">
                <MyHeader :addTodo="addTodo"></MyHeader>
                <List 
                    :todos="todos" 
                    :changeTodo="changeTodo" 
                    :deleteTodo="deleteTodo">
                </List>
                <MyFooter
                    :todos="todos"
                    :checkAllTodo="checkAllTodo"
                >
                </MyFooter>
            </div>
        </div>
    </div>
</template>

<script>
    import MyHeader from './components/MyHeader.vue'
    import List from './components/List.vue'
    import MyFooter from './components/MyFooter.vue'
    
    export default {
        name:'App',
        components:{
            MyHeader,
            List,
            MyFooter
        },
        data() {
            return {
                todos: [
                    {id: '001', title: '吃饭', done: true},
                    {id: '002', title: '学习', done: false},
                    {id: '003', title: '追剧', done: true},
                ]
            }
        },
        methods: {
            // 添加一个todo
            addTodo(todoObj) {
                this.todos.unshift(todoObj)
            },

            // 勾选或者取消勾选一个todo
            changeTodo(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
                })
            },
        }
    }
</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>

【MyFooter】

html 复制代码
<template>
    <div class="todo-footer" v-if="todosLength">
        <label>
            <input type="checkbox" :checked="isAll" @change="checkAll"/>
        </label>
        <span>
            <span>已完成 {{doneTotal}}</span> / {{todosLength}}
        </span>
        <button class="btn btn-danger">清除已完成任务</button>
    </div>
</template>

<script>
export default {
    name: 'MyFooter',
    props: ['todos', 'checkAllTodo'],
    computed: {
        todosLength() {
            return this.todos.length
        },
        doneTotal() {
            return this.todos.filter(todo => todo.done).length

            // 也可以使用下面求和来实现
            // return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
        },

        isAll() {
            return this.doneTotal === this.todosLength && this.todosLength > 0
        }, 
    },
    methods: {
        checkAll(e) {
            this.checkAllTodo(e.target.checked)
        }
    }
}
</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>

10 清空所有数据

【App】

html 复制代码
<template>
    <div id="root">
        <div class="todo-container">
            <div class="todo-wrap">
                <MyHeader :addTodo="addTodo"></MyHeader>
                <List 
                    :todos="todos" 
                    :changeTodo="changeTodo" 
                    :deleteTodo="deleteTodo">
                </List>
                <MyFooter
                    :todos="todos"
                    :checkAllTodo="checkAllTodo"
                    :clearAllTodo="clearAllTodo"
                >
                </MyFooter>
            </div>
        </div>
    </div>
</template>

<script>
    import MyHeader from './components/MyHeader.vue'
    import List from './components/List.vue'
    import MyFooter from './components/MyFooter.vue'
    
    export default {
        name:'App',
        components:{
            MyHeader,
            List,
            MyFooter
        },
        data() {
            return {
                todos: [
                    {id: '001', title: '吃饭', done: true},
                    {id: '002', title: '学习', done: false},
                    {id: '003', title: '追剧', done: true},
                ]
            }
        },
        methods: {
            // 添加一个todo
            addTodo(todoObj) {
                this.todos.unshift(todoObj)
            },

            // 勾选或者取消勾选一个todo
            changeTodo(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) => !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>

【MyFooter】

html 复制代码
<template>
    <div class="todo-footer" v-if="todosLength">
        <label>
            <input type="checkbox" :checked="isAll" @change="checkAll"/>
        </label>
        <span>
            <span>已完成 {{doneTotal}}</span> / {{todosLength}}
        </span>
        <button class="btn btn-danger"  @click="clearTodo">清除已完成任务</button>
    </div>
</template>

<script>
export default {
    name: 'MyFooter',
    props: ['todos', 'checkAllTodo', 'clearAllTodo'],
    computed: {
        todosLength() {
            return this.todos.length
        },
        doneTotal() {
            return this.todos.filter(todo => todo.done).length

            // 也可以使用下面求和来实现
            // return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
        },

        isAll() {
            return this.doneTotal === this.todosLength && this.todosLength > 0
        }, 
    },
    methods: {
        checkAll(e) {
            this.checkAllTodo(e.target.checked)
        },

        clearTodo() {
            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>

11 实现案例中的数据存入本地存储

【分析】首先我们要知道什么时候需要将数据存入本地存储?所以这就用到了watch监听,当todos的值发生变化时,将新的值存入本地存储。

又因为当我们勾选复选框时,我们发现本地存储中的 done 值并没有发生变化?这主要是因为监听默认只会监听第一层,如果想要监听对象中某个数据发生变化时,就需要深度监视了。

【App】

这里使用 || 运算可以防止一开始本地存储中没有数据而报错

12 案例中使用自定义事件完成组件间的数据通信

这边以添加数据为例

【App】

给发送数据的组件绑定自定义事件

javascript 复制代码
<MyHeader @addTodo="addTodo"></MyHeader>

...
 methods: {
      // 添加一个todo
      addTodo(todoObj) {
            this.todos.unshift(todoObj)
      },
}

【MyHeader】

13 案例中实现数据的编辑

需求分析:当点击编辑按钮时,变成input表单修改数据,此时编辑按钮隐藏,当失去焦点时,编辑完成,显示编辑后的数据,同时编辑按钮显示。

这边使用全局事件总线来实现通信

【App】

javascript 复制代码
        methods: {
            ...
            // 更改
            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')
        }

【Item】



因为如果想要失去焦点时实现数据的修改,那么你必须提前获取焦点,但是由于Vue的执行机制,当Vue底层监视到数据发生改变时,它并不会立即去重新渲染模板,而是继续执行后面的代码,所以如果不加以处理的话,直接获取焦点,肯定会报错,因为页面中的元素还没有加载解析出,找不到获取焦点的input元素,所以可以通过以下的代码实现

javascript 复制代码
this.$nextTick(function() {  // 告诉Vue,DOM渲染完毕后,再执行focus()方法
      this.$refs.inputTiltle.focus()
})

14 实现数据进出的动画效果

【Item】

使用<transtion></transtion>标签包裹


相关推荐
燃先生._.4 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖5 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
black^sugar6 小时前
纯前端实现更新检测
开发语言·前端·javascript
2401_857600957 小时前
SSM 与 Vue 共筑电脑测评系统:精准洞察电脑世界
前端·javascript·vue.js
2401_857600957 小时前
数字时代的医疗挂号变革:SSM+Vue 系统设计与实现之道
前端·javascript·vue.js
GDAL7 小时前
vue入门教程:组件透传 Attributes
前端·javascript·vue.js
小白学大数据7 小时前
如何使用Selenium处理JavaScript动态加载的内容?
大数据·javascript·爬虫·selenium·测试工具
轻口味7 小时前
Vue.js 核心概念:模板、指令、数据绑定
vue.js
2402_857583497 小时前
基于 SSM 框架的 Vue 电脑测评系统:照亮电脑品质之路
前端·javascript·vue.js