TODOList案例

创建项目

在这里插入图片描述

组件化编程的一般流程

  • 静态组件:使用组件,实现页面静态下的效果
  • 展示动态数据:页面中的数据,通过data获取,确定数据类型,存储在哪个组件
  • 交互

页面拆分

在这里插入图片描述
在这里插入图片描述
根据页面拆分,创建相关组件

静态页面组件化

将index.html、index.css中的内容迁移到vue工程

  • App.vue

先将html页面和css样式一股脑丢进App.vue

<script>
</script>

<template>
  <div id="app">
    <div class="todo-container">
      <div class="todo-wrap">
        <div class="todo-header">
          <input type="text" placeholder="请输入你的任务名称,按回车键确认" />
        </div>
        <ul class="todo-main">
          <li>
            <label>
              <input type="checkbox" />
              <span>xxxxx</span>
            </label>
            <button class="btn btn-danger" style="display:none">删除</button>
          </li>
          <li>
            <label>
              <input type="checkbox" />
              <span>yyyy</span>
            </label>
            <button class="btn btn-danger" style="display:none">删除</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>
</template>

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

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

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

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

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

拆分页面到各个组件

  • MyHeader.vue
<script>
export default {
  name: 'MyHeader',
  data() {
    return {

    }
  },
  methods: {

  }
}
</script>

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

<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
<script>
export default {
  name: 'MyList',
  data() {
    return {

    }
  },
  methods: {

  }
}
</script>

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

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

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

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

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

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

li:before {
  content: initial;
}

li:last-child {
  border-bottom: none;
}
</style>
  • MyFooter.vue
<script>
export default {
  name: 'MyFooter',
  data() {
    return {

    }
  },
  methods: {

  }
}
</script>

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

<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.vue
<script>
import MyHeader from './components/MyHeader.vue'
import MyFooter from './components/MyFooter.vue'
import MyList from './components/MyList.vue'

export default {
  components: {
    MyHeader,
    MyFooter,
    MyList
  },
  data() {
    return {
      msg: 'Hello Vue 3.0 + Vite'
    }
  }
}
</script>

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

</template>

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

将MyList中的列表展示数据,拆分到MyItem组件中去。拆分完后的MyList和MyItem如下

  • MyList.vue
<script>
import MyItem from './MyItem.vue'
export default {
  name: 'MyList',
  components: {
    MyItem
  },
  data() {
    return {

    }
  },
  methods: {

  }
}
</script>

<template>
  <ul class="todo-main">
    <MyItem></MyItem>
  </ul>
</template>

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

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

</style>
  • MyItem.vue
<script>
export default {
  name: 'MyItem',
  data() {
    return {

    }
  },
  methods: {

  }
}
</script>

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

<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;
}
</style>

页面如下:

至此,完成了页面从html到vue组件的拆分

数据动态展示

目前展示的列表数据,是页面写死的。

现在用vue中的data数据来决定展示哪些列表项目。

那么数据放在哪个组件合适呢?

展示数据的范围属于MyList,每一项数据要传递给MyItem进行展示。

所以,列表数据定义在MyList
在这里插入图片描述

将数据传递个MyItem组件

在这里插入图片描述

完整的MyList与MyItem如下

  • MyList.vue
<script>
import MyItem from './MyItem.vue'
export default {
  name: 'MyList',
  components: {
    MyItem
  },
  data() {
    return {
      todos: [
        { id: '001', todo: '吃饭', done: true },
        { id: '002', todo: '睡觉', done: false },
        { id: '003', todo: '打豆豆', done: true }
      ]
    }
  },
  methods: {

  }
}
</script>

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

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

.todo-empty {
  height: 40px;
  line-height: 40px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding-left: 5px;
  margin-top: 10px;
}
</style>
  • MyItem.vue
<script>
export default {
  name: 'MyItem',
  props: ['todoObj'],
  data() {
    return {

    }
  },
  methods: {

  }
}
</script>

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

<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;
}
</style>

在这里插入图片描述

功能1:添加待办

  • 添加nanoid
pnpm add nanoid

用于新增数据的id生成

  • 分析

    1. 目前列表数据的展示定义在MyList组件

    2. 列表项的新增在MyHeader组件

    3. 怎么将MyHeader中新增的列表项传递到MyList中呢?

      将todos放在App.vue

    4. MyHeader的新增动作写在哪里呢?

      App.vue,数据在哪里,方法就在哪里

  • App.vue中定义todos数据,并将数据传递给MyList.vue
    在这里插入图片描述
  • MyList.vue中接收todos
    在这里插入图片描述
  • App.vue中添加新增列表项数据的方法
    在这里插入图片描述

这个方法是在MyHeader输入数据后,回车键触发的

MyHeader生成todo,并调用App中的addTodo方法

在这里插入图片描述

重点:props中的addTodo,是App中传递过来的方法。
在这里插入图片描述
我们通过传递参数的方式,这里传递方法。实现了将组件A的方法,被组件B中的事件触发并调用的效果。
在这里插入图片描述

功能2:勾选

  • 功能描述:列表项勾选或取消勾选后,数据变更到todos中

  • 分析

    1. 事件触发在MyItem上
    2. todos数据在App上
    3. 所以,变更的方法写在App中,但要将这个方法传递到MyItem上进行调用
    4. App和MyItem中间隔了一个MyList,MyList起到一个中间商的作用
  • App.vue中的变更状态的方法
    在这里插入图片描述

  • MyList做个二道贩子
    在这里插入图片描述
    在这里插入图片描述

功能3:删除

  • 功能描述:点击删除后,数据从列表中清除出去

  • 分析

    1. 删除的数据在App组件,所以删除的方法也在App组件上
    2. 删除的动作在MyItem中触发,所以需要将App中的删除方法,通过属性绑定传递给MyItem
  • App
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

功能4:底部统计

  • 功能说明:统计底部已完成和全部的数量

  • 分析

    统计的数据依赖于todos,所以需要将todos的数据传递给MyFooter

  • App
    在这里插入图片描述
    在这里插入图片描述

功能5:全选、清除已完成

在这里插入图片描述
分析

  1. 数据在App,所以两个方法写到App
  2. 操作在MyFooter,所以App将方法传递给MyFooter
  • App
<script>
import MyHeader from './components/MyHeader.vue'
import MyFooter from './components/MyFooter.vue'
import MyList from './components/MyList.vue'

export default {
  components: {
    MyHeader,
    MyFooter,
    MyList
  },
  data() {
    return {
      todos: [
        { id: '001', todo: '吃饭', done: true },
        { id: '002', todo: '睡觉', done: false },
        { id: '003', todo: '打豆豆', done: true }
      ]
    }
  },
  methods: {
    // 将新的待做事项添加到列表中
    addTodo(todo) {
      this.todos.unshift(todo)
    },
    //修改todo的done值
    changeDone(id) {
      this.todos.forEach(item => {
        if (item.id === id) item.done = !item.done
      })
    },
    // 删除todo
    deleteTodo(id) {
      this.todos = this.todos.filter((todo) => {
        return todo.id !== id
      })
    },
    // 全选 全不选
    checkAll(done) {
      this.todos.forEach((todo) => {
        todo.done = done
      })
    },
    // 清除已完成
    deleteDone() {
      this.todos = this.todos.filter(todo => !todo.done)
    }
  }

}
</script>

<template>
  <div class="todo-container">
    <div class="todo-wrap">
      <MyHeader :addTodo="addTodo"></MyHeader>
      <MyList :todos="todos" :changeDone="changeDone" :deleteTodo="deleteTodo"></MyList>
      <MyFooter :todos="todos" :checkAll="checkAll" :deleteDone="deleteDone"></MyFooter>
    </div>
  </div>

</template>

<style scoped>
/*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
<script>
export default {
  name: 'MyFooter',
  props: ['todos', 'checkAll', 'deleteDone'],
  data() {
    return {

    }
  },
  methods: {

  },
  computed: {
    doneTotal() {
      return this.todos.filter(item => item.done).length
    },
    // 是否全部勾选
    isAll: {
      get() {
        return this.total === this.doneTotal && this.total > 0
      },
      set(value) {
        this.checkAll(value)
      }
    }
  }

}
</script>

<template>
  <div class="todo-footer">
    <label>
      <input type="checkbox" v-model="isAll" />
    </label>
    <span>
      <span>已完成 {{ doneTotal }} </span> / 全部 {{ todos.length }}
    </span>
    <button class="btn btn-danger" @click="deleteDone">清除已完成任务</button>
  </div>
</template>

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

功能6:本地存储改造

目前所有的操作,一旦属性,就会恢复原样,所以,我们需要将数据持久化存储

在这里插入图片描述

在这里插入图片描述

  • App.vue完整代码
<script>
import MyHeader from './components/MyHeader.vue'
import MyFooter from './components/MyFooter.vue'
import MyList from './components/MyList.vue'

export default {
  components: {
    MyHeader,
    MyFooter,
    MyList
  },
  data() {
    return {
      // todos的值从本地存储中进行获取
      // 如果本地存储不存在todos会返回null,此时todos的值为空数组
      // 当读取本地存储返回null,null为假,会返回 []
      // 由于存储的为json字符串,需要进行类型转换
      todos: JSON.parse(localStorage.getItem('todos')) || []
    }
  },
  methods: {
    // 将新的待做事项添加到列表中
    addTodo(todo) {
      this.todos.unshift(todo)
    },
    //修改todo的done值
    changeDone(id) {
      this.todos.forEach(item => {
        if (item.id === id) item.done = !item.done
      })
    },
    // 删除todo
    deleteTodo(id) {
      this.todos = this.todos.filter((todo) => {
        return todo.id !== id
      })
    },
    // 全选 全不选
    checkAll(done) {
      this.todos.forEach((todo) => {
        todo.done = done
      })
    },
    // 清除已完成
    deleteDone() {
      this.todos = this.todos.filter(todo => !todo.done)
    }
  },
  watch: {
    // 使用监视属性监视todos的改变
    // 只要todos发生了改变,就将新的todos进行本地存储
    todos: {
      // 由于默认只会监视一层,监视数组内对象中是否完成的变化需要进行深度监视
      deep: true,
      handler(newVal) {
        // 由于todos是数组对象类型数据,进行本地存储需要转换为json
        localStorage.setItem('todos', JSON.stringify(newVal))
      }
    }
  }

}
</script>

<template>
  <div class="todo-container">
    <div class="todo-wrap">
      <MyHeader :addTodo="addTodo"></MyHeader>
      <MyList :todos="todos" :changeDone="changeDone" :deleteTodo="deleteTodo"></MyList>
      <MyFooter :todos="todos" :checkAll="checkAll" :deleteDone="deleteDone"></MyFooter>
    </div>
  </div>

</template>

<style scoped>
/*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 :todos="todos" :checkAll="checkAll" :deleteDone="deleteDone"></MyFooter>
这种属性绑定传递参数的方式,改为如下绑定事件
<MyFooter :todos="todos" @checkAll="checkAll" @deleteDone="deleteDone"></MyFooter>

子组件中

      set(value) {
        // this.checkAll(value)
        // 触发自定义事件 选择所有
        this.$emit('checkAll', value)
      }

原先通过props接收父组件方法,再进行调用的方式,改为发送事件,并传递值
  • App
<script>
import MyHeader from './components/MyHeader.vue'
import MyFooter from './components/MyFooter.vue'
import MyList from './components/MyList.vue'

export default {
  components: {
    MyHeader,
    MyFooter,
    MyList
  },
  data() {
    return {
      // todos的值从本地存储中进行获取
      // 如果本地存储不存在todos会返回null,此时todos的值为空数组
      // 当读取本地存储返回null,null为假,会返回 []
      // 由于存储的为json字符串,需要进行类型转换
      todos: JSON.parse(localStorage.getItem('todos')) || []
    }
  },
  methods: {
    // 将新的待做事项添加到列表中
    addTodo(todo) {
      this.todos.unshift(todo)
    },
    //修改todo的done值
    changeDone(id) {
      this.todos.forEach(item => {
        if (item.id === id) item.done = !item.done
      })
    },
    // 删除todo
    deleteTodo(id) {
      this.todos = this.todos.filter((todo) => {
        return todo.id !== id
      })
    },
    // 全选 全不选
    checkAll(done) {
      this.todos.forEach((todo) => {
        todo.done = done
      })
    },
    // 清除已完成
    deleteDone() {
      this.todos = this.todos.filter(todo => !todo.done)
    }
  },
  watch: {
    // 使用监视属性监视todos的改变
    // 只要todos发生了改变,就将新的todos进行本地存储
    todos: {
      // 由于默认只会监视一层,监视数组内对象中是否完成的变化需要进行深度监视
      deep: true,
      handler(newVal) {
        // 由于todos是数组对象类型数据,进行本地存储需要转换为json
        localStorage.setItem('todos', JSON.stringify(newVal))
      }
    }
  }

}
</script>

<template>
  <div class="todo-container">
    <div class="todo-wrap">
      <MyHeader  @addTodo="addTodo"></MyHeader>
      <MyList :todos="todos" :changeDone="changeDone" :deleteTodo="deleteTodo"></MyList>
      <MyFooter :todos="todos" @checkAll="checkAll" @deleteDone="deleteDone"></MyFooter>
    </div>
  </div>

</template>

<style scoped>
/*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.vue
<script>
export default {
  name: 'MyFooter',
  props: ['todos'],
  data() {
    return {

    }
  },
  methods: {
    // 清除已完成
    clearDone() {
      // this.deleteDone()
      // 触发自定义事件 清除已完成
      this.$emit('deleteDone')
    }
  },
  computed: {
    doneTotal() {
      return this.todos.filter(item => item.done).length
    },
    // 是否全部勾选
    isAll: {
      get() {
        return this.total === this.doneTotal && this.total > 0
      },
      set(value) {
        // this.checkAll(value)
        // 触发自定义事件 选择所有
        this.$emit('checkAll', value)
      }
    }
  }

}
</script>

<template>
  <div class="todo-footer">
    <label>
      <input type="checkbox" v-model="isAll" />
    </label>
    <span>
      <span>已完成 {{ doneTotal }} </span> / 全部 {{ todos.length }}
    </span>
    <button class="btn btn-danger" @click="clearDone">清除已完成任务</button>
  </div>
</template>

<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
<script>
import MyHeader from './components/MyHeader.vue'
import MyFooter from './components/MyFooter.vue'
import MyList from './components/MyList.vue'

export default {
  components: {
    MyHeader,
    MyFooter,
    MyList
  },
  data() {
    return {
      // todos的值从本地存储中进行获取
      // 如果本地存储不存在todos会返回null,此时todos的值为空数组
      // 当读取本地存储返回null,null为假,会返回 []
      // 由于存储的为json字符串,需要进行类型转换
      todos: JSON.parse(localStorage.getItem('todos')) || []
    }
  },
  methods: {
    // 将新的待做事项添加到列表中
    addTodo(todo) {
      this.todos.unshift(todo)
    },
    //修改todo的done值
    changeDone(id) {
      this.todos.forEach(item => {
        if (item.id === id) item.done = !item.done
      })
    },
    // 删除todo
    deleteTodo(id) {
      this.todos = this.todos.filter((todo) => {
        return todo.id !== id
      })
    },
    // 全选 全不选
    checkAll(done) {
      this.todos.forEach((todo) => {
        todo.done = done
      })
    },
    // 清除已完成
    deleteDone() {
      this.todos = this.todos.filter(todo => !todo.done)
    }
  },
  watch: {
    // 使用监视属性监视todos的改变
    // 只要todos发生了改变,就将新的todos进行本地存储
    todos: {
      // 由于默认只会监视一层,监视数组内对象中是否完成的变化需要进行深度监视
      deep: true,
      handler(newVal) {
        // 由于todos是数组对象类型数据,进行本地存储需要转换为json
        localStorage.setItem('todos', JSON.stringify(newVal))
      }
    }
  }

}
</script>

<template>
  <div class="todo-container">
    <div class="todo-wrap">
      <MyHeader  @addTodo="addTodo"></MyHeader>
      <MyList :todos="todos" :changeDone="changeDone" :deleteTodo="deleteTodo"></MyList>
      <MyFooter :todos="todos" @checkAll="checkAll" @deleteDone="deleteDone"></MyFooter>
    </div>
  </div>

</template>

<style scoped>
/*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
<script>
// 导入nanoid 
import { nanoid } from 'nanoid'
export default {
  name: 'MyHeader',
  data() {
    return {
      task: ''
    }
  },
  methods: {
    addTask() {
      // 没有输入不进行添加
      if (!this.task) return
      // console.log(this.task)
      // 新的待做事项
      const todo = {
        id: nanoid(),
        todo: this.task,
        done: false
      }
      // 添加待做事项
      // 触发自定义事件 添加待做事项
      // this.addTodo(todo)
      this.$emit('addTodo', todo)
      // 输入框清空
      this.task = ''
    }
  }
}
</script>

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

<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
<script>
import MyItem from './MyItem.vue'
export default {
  name: 'MyList',
  props: ['todos','changeDone','deleteTodo'],
  components: {
    MyItem
  },
  data() {
    return {
    
    }
  },
  methods: {

  }
}
</script>

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

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

.todo-empty {
  height: 40px;
  line-height: 40px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding-left: 5px;
  margin-top: 10px;
}
</style>
  • MyItem
<script>
export default {
  name: 'MyItem',
  props: ['todoObj', 'changeDone', 'deleteTodo'],
  data() {
    return {

    }
  },
  methods: {
    handlerDelete(id) {
      if (confirm('确定删除吗?')) {
        this.deleteTodo(this.todoObj.id)
      }
    }
  }
}
</script>

<template>
  <li>
    <label>
      <input type="checkbox" :checked="todoObj.done" @click="changeDone(todoObj.id)" />
      <span>{{ todoObj.todo }}</span>
    </label>
    <button class="btn btn-danger" @click="handlerDelete(todoObj.id)">删除</button>
  </li>
</template>

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

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

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

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

li:before {
  content: initial;
}

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

li:hover {
  background-color: #ddd;
}

li:hover button {
  display: inline-block;
}


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

</style>
  • MyFooter
<script>
export default {
  name: 'MyFooter',
  props: ['todos'],
  data() {
    return {

    }
  },
  methods: {
    // 清除已完成
    clearDone() {
      // this.deleteDone()
      // 触发自定义事件 清除已完成
      this.$emit('deleteDone')
    }
  },
  computed: {
    doneTotal() {
      return this.todos.filter(item => item.done).length
    },
    // 是否全部勾选
    isAll: {
      get() {
        return this.total === this.doneTotal && this.total > 0
      },
      set(value) {
        // this.checkAll(value)
        // 触发自定义事件 选择所有
        this.$emit('checkAll', value)
      }
    }
  }

}
</script>

<template>
  <div class="todo-footer">
    <label>
      <input type="checkbox" v-model="isAll" />
    </label>
    <span>
      <span>已完成 {{ doneTotal }} </span> / 全部 {{ todos.length }}
    </span>
    <button class="btn btn-danger" @click="clearDone">清除已完成任务</button>
  </div>
</template>

<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个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值