初学Vue(全家桶):toDolist案例入门版(props配置实现组件间交互)

初学Vue

toDoLlist案例入门版–props实现组件数据交互

前言

实现步骤中的代码会有重复的部分,代码是根据功能,按照思路一点一点添加上的,同时我也在代码之外添加了部分注释,帮助理解,完整代码在最后。

页面最终效果展示

请添加图片描述

实现步骤

1、脚手架搭建项目

vue create todolist
在这里插入图片描述

2、写出一个静态页面,包含html结构和css样式

在这里插入图片描述

  • test.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>做出基本模板</title>
</head>

<body>
    <div id="root">
        <div class="todo-container">
            <div class="todo-wrap">
                <!-- 输入框部分 -->
                <div class="todo-header">
                    <input type="text" placeholder="请输入你的任务名称,回车键确认">
                </div>
                <!-- /输入框部分 -->

                <!-- 列表部分 -->
                <ul ul="todo-main">
                    <li>
                        <label>
                            <input type="checkbox">
                            <span>xxxxx</span>
                        </label>
                        <button class="btn btn-danger">删除</button>
                    </li>
                    <li>
                        <label>
                            <input type="checkbox">
                            <span>xxxxx</span>
                        </label>
                        <button class="btn btn-danger">删除</button>
                    </li>
                </ul>

                <!-- /列表部分 -->

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

<style>
    /* 整体样式 */
    body {
      
      
        background: rgba(230, 241, 245, 0.816)
    }

    .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;
    }

    /*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);
    }

    /*main*/
    .todo-main {
      
      
        margin-left: 0px;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding: 0px;
    }

    /*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:hover{
      
      
        background-color: grey;
        cursur: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:hover button{
      
      
 		display:block
	}
	
    li:before {
      
      
        content: initial;
    }

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

    /*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>

</html>

3、将静态页面拆分,放入到各个组件中

在这里插入图片描述

  • App.vue组件
<template>
  <div id="app">
    <div class="todo-container">
      <div class="todo-wrap">
        <!-- 输入框部分 -->
        <MyHeader></MyHeader>
        <!-- /输入框部分 -->

        <!-- 列表部分 -->
        <MyList></MyList>
        <!-- /列表部分 -->

        <!-- 底部部分 -->
        <MyFooter></MyFooter>
        <!-- /底部部分 -->
      </div>
    </div>
  </div>
</template>

<script>
// 引入各个子组件
import MyHeader from "./components/MyHeader.vue";
import MyList from "./components/MyList.vue";
import MyFooter from "./components/MyFooter.vue";

export default {
      
      
  name: "App",
  // 注册组件
  components: {
      
      
    MyHeader,
    MyFooter,
    MyList,
  },
};
</script>

<style>
/* 整体样式 */
body {
      
      
  background: rgba(230, 241, 245, 0.816);
}

.btn {
      
      
  display: inline-block;
  padding: 4px 12px;
  margin-bottom: 0;
  font-size: 14px;
  line-height: 20px;
  text-align: center;
  vertical-align: middle;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
    0 1px 2px rgba(0, 0, 0, 0.05);
  border-radius: 4px;
}

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

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

.btn:focus {
      
      
  outline: none;
}

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

.todo-container .todo-wrap {
      
      
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 5px;
}
</style>
  • MyHeader.vue组件
<template>
  <div>
    <div class="todo-header">
      <input type="text" placeholder="请输入你的任务名称,回车键确认" />
    </div>
  </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>
  • MyList.vue组件
<template>
  <div>
    <ul ul="todo-main">
      <MyItem></MyItem>
      <MyItem></MyItem>
      <MyItem></MyItem>
    </ul>
  </div>
</template>

<script>
import MyItem from "./MyItem.vue";

export default {
      
      
  name: "MyList",
  components: {
      
       MyItem },
};
</script>

<style scoped>
/*main*/
.todo-main {
      
      
  margin-left: 0px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding: 0px;
}
</style>
  • MyItem.vue组件
<template>
  <div>
    <li>
      <label>
        <input type="checkbox" />
        <span>xxxxx</span>
      </label>
      <button class="btn btn-danger">删除</button>
    </li>
  </div>
</template>

<script>
export default {
      
      
  name: "MyItem",
};
</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:hover {
      
      
  background-color: rgb(226, 219, 219);
  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:hover button{
      
      
  display:block
}

li:before {
      
      
  content: initial;
}

li:last-child {
      
      
  border-bottom: none;
}
</style>
  • MyFooter.vue组件
<template>
  <div>
    <div class="todo-footer">
      <label>
        <input type="checkbox" />
      </label>
      <span> <span>已完成0</span>/ 全部2 </span>
      <button class="btn btn-danger">清除已完成任务</button>
    </div>
  </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>

(1)在子组件的样式中使用了scoped属性,用来避免子组件中样式因出现重名而出现问题。
(2)MyItem组件是MyList的子组件,所以MyItem是在MyList中导入并注册,而不是直接在App.vue中导入注册。

4、动态展示列表中的数据

需要注意数据的类型、名称,数据保存在哪个组件中

以下代码均省略样式部分

更改后的MyList.vue

<template>
  <div>
    <ul ul="todo-main">
        <!-- 显示todos数组中所有的事项,用id作为key -->
      <MyItem 
      v-for="todoObj in todos" 
      :key="todoObj.id"
      :todo="todoObj"
      ></MyItem>
    </ul>
  </div>
</template>

<script>
import MyItem from "./MyItem.vue";

export default {
      
      
  name: "MyList",
  components: {
      
       MyItem },
  data() {
      
      
    return {
      
      
    // 自己设置了三条数据
      todos: [
          {
      
       id: "001", title: "唱歌", done: true },
          {
      
       id: "002", title: "跳舞", done: false },
          {
      
       id: "003", title: "打代码", done: true }
      ],
    };
  },
};
</script>

更改后的MyItem.vue

<template>
  <div>
    <li>
      <label>
        <input type="checkbox" :checked="todo.done"/>
        <span>{
   
   {todo.title}}</span>
      </label>
      <button class="btn btn-danger">删除</button>
    </li>
  </div>
</template>

<script>
export default {
      
      
  name: "MyItem",
  props:['todo'] // 通过props配置项接收来自MyList.vue组件标签组件< MyItem>传过来的值(todo),todo中存储了id、title、done属性
};
</script>

1、使用一个数组来存储整个列表,因为列表有多个事项,而一个事项由多个属性组成,例如id(唯一标识),title(事项名称),done(是否完成),所以单个事项用一个对象表示。也就是说数组中存储的是对象。
2、有n个事项就显示n个事项,使用v-for属性借助id遍历出所有事项(此时只遍历了n个事项出来,并且不能显示事项中的具体内容)
3、在MyList.vue中的组件标签MyItem中的 :todo="todoObj"用于将从todos数组中获取到的单个对象传给MyItem.vue接收。在MyItem中通过配置项props接收todo。
4、MyItem.vue接收到todo后,通过插值语法{ {todo.title}}可以获取到事项内容,通过:checked="todo.done"来勾选是否已完成。

5、输入框输入数据添加事项

  • 这里有一个坑需要注意下

在项目目录下,MyList.vue和Myheader.vue两个组件是同级的,
在这里插入图片描述
需要注意的是props配置项支持父组件中的数据传给子组件,但如果想要在兄弟级组件间传递数据则稍微麻烦点

添加事项是在MyHeader.vue中完成的,输入框输入值后将生成的对象传递出去,并且在另一个组件中通过props配置项接收数据,并使用todos数组存储传进来的对象,而todos数组此时是在MyList.vue组件中的,这两个组件是同级关系,因此不能通过props来传值。
在这里插入图片描述
也就是说上图中中间椭圆传值部分并不能在兄弟级的组件下使用props配置项实现。

  • 解决措施
    这里我们可以将data中的todos数组交给父组件App.vue管理,让它来起到桥梁的作用。如图:
    在这里插入图片描述

todos数组指向MyList组件很好理解,就是在MyList.vue中将数据传递给MyList.vue组件,MyList用props接收数据,主要是todoObj指向todos数组这段该怎么实现,子组件怎么传数据给父组件?

可以这么实现,在父组件也就是App.vue中添加一个方法,这个方法的功能就是用来添加一个事项,并且通过组件标签将这个方法传到MyHeader.vue中去,MyHeader.vue用props接收这个方法,紧接着调用即可即可给父组件中添加数据。

简单来说就是在父组件中定义一个方法,将这个方法传给子组件,子组件用props接收后调用,更新父组件中的数据,实现兄弟级组件间的数据传递
在这里插入图片描述
代码实现

  • App.vue
<template>
  <div id="app">
    <div class="todo-container">
      <div class="todo-wrap">
        <!-- 输入框部分 -->
        <!-- 将addTodo方法传到MyHeader组件去 -->
        <MyHeader :addTodo="addTodo"></MyHeader>
        <!-- /输入框部分 -->

        <!-- 列表部分 -->
        <!-- 将todos数组传递到MyList组件中 -->
        <MyList :todos="todos"></MyList>
        <!-- /列表部分 -->

        <!-- 底部部分 -->
        <MyFooter></MyFooter>
        <!-- /底部部分 -->
      </div>
    </div>
  </div>
</template>

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

export default {
      
      
  name: "App",
  components: {
      
      
    MyHeader,
    MyFooter,
    MyList,
  },
  data() {
      
      
    return {
      
      
      todos: [
        {
      
       id: "001", title: "唱歌", done: true },
        {
      
       id: "002", title: "跳舞", done: false },
        {
      
       id: "003", title: "打代码", done: true },
      ],
    };
  },
  methods:{
      
      
    addTodo(e){
      
      
      // console.log("我是App组件,我收到了数据",x);
      this.todos.unshift(e)
    }
  }
};
</script>

  • MyList.vue
template>
  <div>
    <ul ul="todo-main">
        <!-- 显示todos数组中所有的事项,用id作为key -->
      <MyItem 
      v-for="todoObj in todos" 
      :key="todoObj.id"
      :todo="todoObj"
      ></MyItem>
    </ul>
  </div>
</template>

<script>
import MyItem from "./MyItem.vue";

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

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

<script>
import {
      
      nanoid} from "nanoid"
export default {
      
      
  name: "MyHeader",
  methods:{
      
      
    add(event){
      
      
      // 如果输入为空,则结束方法,不添加任何数据
      if(!this.value) return alert("输入不能为空")
      // 使用能生成唯一标识的库nanoid或者uuid生成id,这里用nanoid(轻量级的uuid)
      // 将用户的输入包装成一个todo对象
      const todoObj = {
      
      id:nanoid(),title:event.target.value,done:false}
      this.addTodo(todoObj) // 调用传递过来的方法,添加数据
      event.target.value='' // 回车后清空输入框中的内容
    }
  },
  props:["addTodo"], // 使用props接收到传递过来的方法
};
</script>

6、实现列表中勾选数据交互

主线是拿到单个事项的id值,然后根据id值修改done属性的值,支线一是如何拿到id值,支线二是如何根据id值修改done属性。

支线一:MyItem组件中,点击勾选与取消勾选时触发@change事件,调用对应的方法,该方法参数为id,并且该方法会调用从App传过来的方法。

支线二:在App组件中定义一个方法,该方法参数为id,作用时可以获取列表中每一项,然后比较id,修改对应id中done属性的值,然后将该方法传递给MyItem组件。

需要注意的是:(这里未使用组件通信的方法)
MyList是MyItem的父组件,而App是MyList的父组件,因此传递方法时要一层一层的传。

代码实现:

  • App.vue
<template>
  <div id="app">
    <div class="todo-container">
      <div class="todo-wrap">
        <!-- 输入框部分 -->
        <!-- 将addTodo方法传到MyHeader组件去 -->
        <MyHeader :addTodo="addTodo"></MyHeader>
        <!-- /输入框部分 -->

        <!-- 列表部分 -->
        <MyList :todos="todos" :checkTodo="checkTodo"></MyList>
        <!-- /列表部分 -->

        <!-- 底部部分 -->
        <MyFooter></MyFooter>
        <!-- /底部部分 -->
      </div>
    </div>
  </div>
</template>

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

export default {
      
      
  name: "App",
  components: {
      
      
    MyHeader,
    MyFooter,
    MyList,
  },
  data() {
      
      
    return {
      
      
      todos: [
        {
      
       id: "001", title: "唱歌", done: true },
        {
      
       id: "002", title: "跳舞", done: false },
        {
      
       id: "003", title: "打代码", done: true },
      ],
    };
  },
  methods:{
      
      
    // 添加一个列表事项
    addTodo(e){
      
      
      // console.log("我是App组件,我收到了数据",x);
      this.todos.unshift(e)
    },
    // 勾选or取消勾选todo事项
    checkTodo(id){
      
      
      // 拿到todos中的每一项,找到对应id的那一项
      this.todos.forEach((todo)=>{
      
      
        if(todo.id === id)
          todo.done = !todo.done
      })
    }
  }
}
</script>
  • MyList.vue
<template>
  <div>
    <ul ul="todo-main">
        <!-- 显示todos数组中所有的事项,用id作为key -->
      <MyItem 
      v-for="todoObj in todos" 
      :key="todoObj.id"
      :todo="todoObj"
      :checkTodo="checkTodo"
      ></MyItem>
    </ul>
  </div>
</template>

<script>
import MyItem from "./MyItem.vue";

export default {
      
      
  name: "MyList",
  components: {
      
       MyItem },
  props:['todos','checkTodo']
};
</script>
  • MyItem.vue
<template>
  <div>
    <li>
      <label>
        <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
        <span>{
   
   {todo.title}}</span>
      </label>
      <button class="btn btn-danger">删除</button>
    </li>
  </div>
</template>

<script>
export default {
      
      
  name: "MyItem",
  props:['todo','checkTodo'], // 这里todo是个对象,由MyList传过来的,存储了id、title、done属性
  methods:{
      
      
    handleCheck(id){
      
      
      // 通知App组件将对应todo对象的done值取反
      this.checkTodo(id)
    }
  }
};
</script>

7、实现列表删除按钮的功能

思路:拿id,删除对应事项

  • App.vue
<template>
  <div id="app">
    <div class="todo-container">
      <div class="todo-wrap">
        <!-- 输入框部分 -->
        <!-- 将addTodo方法传到MyHeader组件去 -->
        <MyHeader :addTodo="addTodo"></MyHeader>
        <!-- /输入框部分 -->

        <!-- 列表部分 -->
        <MyList 
          :todos="todos" 
          :checkTodo="checkTodo" 
          :deleteTodo="deleteTodo"
        ></MyList>
        <!-- /列表部分 -->

        <!-- 底部部分 -->
        <MyFooter></MyFooter>
        <!-- /底部部分 -->
      </div>
    </div>
  </div>
</template>

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

export default {
      
      
  name: "App",
  components: {
      
      
    MyHeader,
    MyFooter,
    MyList,
  },
  data() {
      
      
    return {
      
      
      todos: [
        {
      
       id: "001", title: "唱歌", done: true },
        {
      
       id: "002", title: "跳舞", done: false },
        {
      
       id: "003", title: "打代码", done: true },
      ],
    };
  },
  methods:{
      
      
    // 添加一个列表事项
    addTodo(e){
      
      
      // console.log("我是App组件,我收到了数据",x);
      this.todos.unshift(e)
    },
    // 勾选or取消勾选todo事项
    checkTodo(id){
      
      
      // 拿到todos中的每一项,找到指定id的那一项
      this.todos.forEach((todo)=>{
      
      
        if(todo.id === id)
          todo.done = !todo.done
      })
    },
    // 删除一个todo
    deleteTodo(id){
      
      
      this.todos = this.todos.filter((todo)=>{
      
      
        return todo.id !== id
      })
    }

  }
}
</script>
  • MyList.vue
<template>
  <div>
    <ul ul="todo-main">
        <!-- 显示todos数组中所有的事项,用id作为key -->
      <MyItem 
      v-for="todoObj in todos" 
      :key="todoObj.id"
      :todo="todoObj"
      :checkTodo="checkTodo" 
      :deleteTodo="deleteTodo"
      ></MyItem>
    </ul>
  </div>
</template>

<script>
import MyItem from "./MyItem.vue";

export default {
      
      
  name: "MyList",
  components: {
      
       MyItem },
  props:['todos','checkTodo','deleteTodo']
};
</script>
  • MyItem.vue
<template>
  <div>
    <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>
  </div>
</template>

<script>
export default {
      
      
  name: "MyItem",
  props:['todo','checkTodo','deleteTodo'], // 这里todo是个对象,由MyList传过来的,存储了id、title、done属性
  methods:{
      
      
    handleCheck(id){
      
      
      // 通知App组件将对应todo对象的done值取反
      this.checkTodo(id)
    },
    // 删除事项
    handleDelete(id){
      
      
      if(confirm("确定删除吗?")){
      
      
        // 通知App删除数据
        this.deleteTodo(id)
      }
    }
  }
};
</script>

8、底部统计功能实现

  • App.vue
<template>
  <div id="app">
    <div class="todo-container">
      <div class="todo-wrap">
        <!-- 输入框部分 -->
        <!-- 将addTodo方法传到MyHeader组件去 -->
        <MyHeader :addTodo="addTodo"></MyHeader>
        <!-- /输入框部分 -->

        <!-- 列表部分 -->
        <MyList 
          :todos="todos" 
          :checkTodo="checkTodo" 
          :deleteTodo="deleteTodo"
        ></MyList>
        <!-- /列表部分 -->

        <!-- 底部部分 -->
        <MyFooter
          :todos="todos"
        ></MyFooter>
        <!-- /底部部分 -->
      </div>
    </div>
  </div>
</template>

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

export default {
      
      
  name: "App",
  components: {
      
      
    MyHeader,
    MyFooter,
    MyList,
  },
  data() {
      
      
    return {
      
      
      todos: [
        {
      
       id: "001", title: "唱歌", done: true },
        {
      
       id: "002", title: "跳舞", done: false },
        {
      
       id: "003", title: "打代码", done: true },
      ],
    };
  },
  methods:{
      
      
    // 添加一个列表事项
    addTodo(e){
      
      
      // console.log("我是App组件,我收到了数据",x);
      this.todos.unshift(e)
    },
    // 勾选or取消勾选todo事项
    checkTodo(id){
      
      
      // 拿到todos中的每一项,找到对应id的那一项
      this.todos.forEach((todo)=>{
      
      
        if(todo.id === id)
          todo.done = !todo.done
      })
    },
    // 点击删除一个todo
    deleteTodo(id){
      
      
      this.todos = this.todos.filter((todo)=>{
      
      
        return todo.id !== id
      })
    }

  }
}
</script>
  • MyFooter.vue
<template>
  <div>
    <div class="todo-footer">
      <label>
        <input type="checkbox" />
      </label>
      <span> <span>已完成{
   
   {doneTotal}}</span>/ 全部{
   
   {todos.length}}</span>
      <button class="btn btn-danger">清除已完成任务</button>
    </div>
  </div>
</template>

<script>
export default {
      
      
  name: "MyFooter",
  props:['todos'],
  computed:{
      
      
    doneTotal(){
      
      
      // 高端一点的方法
      // return this.todos.reduce((pre,current)=>{
      
      
      //   return pre + (current.done ? 1 : 0)
      // },0)

      // 普通的方法
      let i=0
      this.todos.forEach((todo)=>{
      
      
        if(todo.done) i++
      })
      return i
    }
  },
}
</script>

9、底部删除\勾选功能实现

  • App.vue
<template>
  <div id="app">
    <div class="todo-container">
      <div class="todo-wrap">
        <!-- 输入框部分 -->
        <!-- 将addTodo方法传到MyHeader组件去 -->
        <MyHeader :addTodo="addTodo"></MyHeader>
        <!-- /输入框部分 -->

        <!-- 列表部分 -->
        <MyList 
          :todos="todos" 
          :checkTodo="checkTodo" 
          :deleteTodo="deleteTodo"
        ></MyList>
        <!-- /列表部分 -->

        <!-- 底部部分 -->
        <MyFooter
          :todos="todos"
          :checkAllTodo="checkAllTodo"
          :clearAllTodo="clearAllTodo"
        ></MyFooter>
        <!-- /底部部分 -->
      </div>
    </div>
  </div>
</template>

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

export default {
      
      
  name: "App",
  components: {
      
      
    MyHeader,
    MyFooter,
    MyList,
  },
  data() {
      
      
    return {
      
      
      todos: [
        {
      
       id: "001", title: "唱歌", done: true },
        {
      
       id: "002", title: "跳舞", done: false },
        {
      
       id: "003", title: "打代码", done: true },
      ],
    };
  },
  methods:{
      
      
    // 添加一个列表事项
    addTodo(e){
      
      
      // console.log("我是App组件,我收到了数据",x)
      this.todos.unshift(e)
    },
    // 勾选or取消勾选todo事项
    checkTodo(id){
      
      
      // 拿到todos中的每一项,找到对应id的那一项
      this.todos.forEach((todo)=>{
      
      
        if(todo.id === id)
          todo.done = !todo.done
      })
    },
    // 点击删除一个todo
    deleteTodo(id){
      
      
      this.todos = this.todos.filter((todo)=>{
      
      
        return todo.id !== id
      })
    },
    // 控制全选或全不选
    checkAllTodo(done){
      
      
      this.todos.forEach((todo)=>{
      
      
        todo.done = done;
      })
    },
    // 清除已经完成的事项
    clearAllTodo(){
      
      
      this.todos = this.todos.filter((todo)=>{
      
      
        return !todo.done
      })
    }
  }
}
</script>
  • MyFooter.vue
<template>
  <div>
      <!--当列表中没有事项时,v-show用于隐藏Footer部分-->
    <div class="todo-footer" v-show="total"> 
      <label>
        <input type="checkbox" :checked="isAll" @change="checkAll"/>
      </label>
      <span> <span>已完成{
   
   {doneTotal}}</span>/ 全部{
   
   {total}}</span>
      <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
    </div>
  </div>
</template>

<script>
export default {
      
      
  name: "MyFooter",
  props:['todos','checkAllTodo','clearAllTodo'],
  computed:{
      
      
    total(){
      
      
      return this.todos.length
    },
    doneTotal(){
      
      
      // 高端一点的方法
      // return this.todos.reduce((pre,current)=>{
      
      
      //   return pre + (current.done ? 1 : 0)
      // },0)

      // 普通的方法
      let i=0
      this.todos.forEach((todo)=>{
      
      
        if(todo.done) i++
      })
      return i
    },
	isAll:{
      
      
	   // 看全选框是否勾选
	   get(){
      
      
	     return this.doneTotal === this.total && this.total>0
	   },
	   // isAll被修改时set被调用
	   set(value){
      
      
	     this.checkAllTodo(value)
	   }
    }
  },
  methods:{
      
      
    // 全选或取消全选
    checkAll(event){
      
      
      this.checkAllTodo(event.target.checked)
    },
    // 清除已完成事项
    clearAll(){
      
      
      this.clearAllTodo()
    }
  }
}
</script>

完整代码

  • index.html
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>
  • App.vue
<template>
  <div id="app">
    <div class="todo-container">
      <div class="todo-wrap">
        <!-- 输入框部分 -->
        <!-- 将addTodo方法传到MyHeader组件去 -->
        <MyHeader :addTodo="addTodo"></MyHeader>
        <!-- /输入框部分 -->

        <!-- 列表部分 -->
        <MyList 
          :todos="todos" 
          :checkTodo="checkTodo" 
          :deleteTodo="deleteTodo"
        ></MyList>
        <!-- /列表部分 -->

        <!-- 底部部分 -->
        <MyFooter
          :todos="todos"
          :checkAllTodo="checkAllTodo"
        ></MyFooter>
        <!-- /底部部分 -->
      </div>
    </div>
  </div>
</template>

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

export default {
      
      
  name: "App",
  components: {
      
      
    MyHeader,
    MyFooter,
    MyList,
  },
  data() {
      
      
    return {
      
      
      todos: [
        {
      
       id: "001", title: "唱歌", done: true },
        {
      
       id: "002", title: "跳舞", done: false },
        {
      
       id: "003", title: "打代码", done: true },
      ],
    };
  },
  methods:{
      
      
    // 添加一个列表事项
    addTodo(e){
      
      
      // console.log("我是App组件,我收到了数据",x)
      this.todos.unshift(e)
    },
    // 勾选or取消勾选todo事项
    checkTodo(id){
      
      
      // 拿到todos中的每一项,找到对应id的那一项
      this.todos.forEach((todo)=>{
      
      
        if(todo.id === id)
          todo.done = !todo.done
      })
    },
    // 点击删除一个todo
    deleteTodo(id){
      
      
      this.todos = this.todos.filter((todo)=>{
      
      
        return todo.id !== id
      })
    },
    // 控制全选或全不选
    checkAllTodo(done){
      
      
      this.todos.forEach((todo)=>{
      
      
        todo.done = done;
      })
    }
  }
}
</script>

<style>
/* 整体样式 */
body {
      
      
  background: rgba(230, 241, 245, 0.816);
}

.btn {
      
      
  display: inline-block;
  padding: 4px 12px;
  margin-bottom: 0;
  font-size: 14px;
  line-height: 20px;
  text-align: center;
  vertical-align: middle;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
    0 1px 2px rgba(0, 0, 0, 0.05);
  border-radius: 4px;
}

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

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

.btn:focus {
      
      
  outline: none;
}

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

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

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

<script>
import {
      
      nanoid} from "nanoid"
export default {
      
      
  name: "MyHeader",
  methods:{
      
      
    add(event){
      
      
      // 如果输入为空,则结束方法,不添加任何数据
      if(!event.target.value){
      
      
        return alert("输入不能为空")
      }
      // 使用能生成唯一标识的库nanoid或者uuid生成id,这里用nanoid(轻量级的uuid)
      // 将用户的输入包装成一个todo对象
      const todoObj = {
      
      id:nanoid(),title:event.target.value,done:false}
      this.addTodo(todoObj) // 调用传递过来的方法,添加数据
      event.target.value='' // 回车后清空输入框中的内容
    }
  },
  props:["addTodo"], // 使用props接收到传递过来的方法
};
</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>
  • MyList.vue
<template>
  <div>
    <ul ul="todo-main">
        <!-- 显示todos数组中所有的事项,用id作为key -->
      <MyItem 
      v-for="todoObj in todos" 
      :key="todoObj.id"
      :todo="todoObj"
      :checkTodo="checkTodo" 
      :deleteTodo="deleteTodo"
      ></MyItem>
    </ul>
  </div>
</template>

<script>
import MyItem from "./MyItem.vue";

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

<style scoped>
/*main*/
.todo-main {
      
      
  margin-left: 0px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding: 0px;
}
</style>
  • MyItem.vue
<template>
  <div>
    <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>
  </div>
</template>

<script>
export default {
      
      
  name: "MyItem",
  props:['todo','checkTodo','deleteTodo'], // 这里todo是个对象,由MyList传过来的,存储了id、title、done属性
  methods:{
      
      
    handleCheck(id){
      
      
      // 通知App组件将对应todo对象的done值取反
      this.checkTodo(id)
    },
    // 删除事项
    handleDelete(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:hover {
      
      
  background-color: rgb(226, 219, 219);
  cursor: pointer;
}

li:hover button{
      
      
  display:block
}

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>
  • MyFooter.vue
<template>
  <div>
      <!--当列表中没有事项时,v-show用于隐藏Footer部分-->
    <div class="todo-footer" v-show="total"> 
      <label>
        <input type="checkbox" :checked="isAll" @change="checkAll"/>
      </label>
      <span> <span>已完成{
   
   {doneTotal}}</span>/ 全部{
   
   {total}}</span>
      <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
    </div>
  </div>
</template>

<script>
export default {
      
      
  name: "MyFooter",
  props:['todos','checkAllTodo','clearAllTodo'],
  computed:{
      
      
    total(){
      
      
      return this.todos.length
    },
    doneTotal(){
      
      
      // 高端一点的方法
      // return this.todos.reduce((pre,current)=>{
      
      
      //   return pre + (current.done ? 1 : 0)
      // },0)

      // 普通的方法
      let i=0
      this.todos.forEach((todo)=>{
      
      
        if(todo.done) i++
      })
      return i
    },
    isAll:{
      
      
      // 看全选框是否勾选
      get(){
      
      
        return this.doneTotal === this.total && this.total>0
      },
      // isAll被修改时set被调用
      set(value){
      
      
        this.checkAllTodo(value)
      }
    }
  },
  methods:{
      
      
    // 全选或取消全选
    checkAll(event){
      
      
      this.checkAllTodo(event.target.checked)
    },
    // 清除已完成事项
    clearAll(){
      
      
      this.clearAllTodo()
    }
  }
}
</script>

<style scoped>
/*footer*/
.todo-footer {
      
      
  height: 40px;
  line-height: 40px;
  padding-left: 6px;
  margin-top: 5px;
}

.todo-footer label {
      
      
  display: inline-block;
  margin-right: 20px;
  cursor: pointer;
}

.todo-footer label input {
      
      
  position: relative;
  top: -1px;
  vertical-align: middle;
  margin-right: 5px;
}

.todo-footer button {
      
      
  float: right;
  margin-top: 5px;
}
</style>

猜你喜欢

转载自blog.csdn.net/lalala_dxf/article/details/125087564