【vue3入门】实现TodoList简易记事本

6,567 阅读7分钟

前言

本文将带大家用VUE3实现TodoList的一个经典案例,也相当于是一个电子版的简易记事本!

先来看效果图:

52 (1).gif

可以看到需要实现的功能如下:

  1. 新增:输入任务按回车后,就会添加任务到列表。
  2. 删除:点击每个任务后的'X',该任务就会在列表中被删除。
  3. 统计:统计剩余几个任务。
  4. 清空:点击右下角'clear completed',被选中的任务在该列表被清除不显示,并不是真正的删除。
  5. 隐藏:当没有任务时,最后一栏里的内容隐藏。
  6. 状态切换:当点击下面的'Completed Active All'任意按钮时,列表中显示相应的任务。
  7. 全选按钮:当选择此按钮时可以一键选中全部打“√”或者全部不选中。

思路准备

  1. 由于App.vue最终还会被挂载到index.html中,所以在App.vue中编写主要代码。
  2. 在App.vue中先搭建好框架:在template模板中写主要页面代码。首先先写死数据在上面,主要看页面效果是怎样的,好调整。
  3. 模板页面主要分三部分:头部header、主要内容section、尾部footer。在header部分主要写标题和任务输入框;section部分主要写任务列表项;footer部分主要写按钮选择项和计数。
  4. 写JS,并更换模板页面中的数据,用动态的数据代替。在setup()入口函数中写数据源和要实现功能的函数

正文

一、创建项目

1、首先通过vue create xxx创建好项目,其中‘xxx’我们以todolist命名

2、当我们以默认的方式创建项目后,需要删除原先的一些内容,目录结构主要如下:

image.png

其中components和assets文件夹下不要有任何东西。

main.js中初始化为如下:

image.png

App.vue中初始化如下:

image.png

二、编写前端页面

在template模板中编写页面的代码和样式。

1、编写页面主要框架

class="todoapp"的section标签下,主要分头部header、主体section、尾部footer三部分。在header中写标题和任务输入框,输入框前有全选按钮; 在section中分多选框——给选中的项打“√”,ul-li标签下显示任务列表,包括任务名称和删除按钮;在footer标签中写剩余可做任务个数、已做任务按钮、未做任务按钮、全部任务按钮、清空已做任务按钮。

<template>
  <section class="todoapp">
    <header class="header">
      <h1>简易记事本</h1>
      <input type="text" class="new-todo" placeholder="想干的事" @keyup.enter="addTodo" v-model="newTodo">
    </header>

    <section class="main">
      <input type="checkbox" class="toggle-all" id="toggle-all" v-model="allDone">
      <label for="toggle-all">Mark all as complete</label>

      <ul class="todo-list">//循环遍历每个li标签,循环次数取决于数据项的个数
        <li class="todo" v-for="(todo) in filtertodo" :key="todo.id" ><!--for循环要key绑定,保证唯一性 -->
          <div class="view">
            <input type="checkbox" class="toggle" v-model="todo.completed">//多选框
            <label>{{ todo.title }}</label>//任务名
            <button class="destroy" @click="removeTodo(todo)"></button>//删除按钮
          </div>
        </li>

      </ul>
    </section>


    <footer class="footer" v-show="todos.length" v-cloak>//列表中有数据就显示最后一行,没有就不显示
      <span class="todo-count">
        <strong>{{ left }}</strong> left //剩余可做任务个数
      </span>
      <span class="todo-choose">
        <button class="clear-completed todo-do" @click="showAll">All</button> //显示全部任务按钮
        <button class="clear-completed todo-do" @click="showActive">Active</button>//显示可做任务按钮
        <button class="clear-completed todo-do" @click="showCompleted">Completed</button>//显示已完成任务按钮
      </span>
      <button class="clear-completed" @click="removeTo">
        Clear completed  //清空已完成任务按钮
      </button>
    </footer>
  </section>
</template>

2、编写template模板样式

为了方便,在此项目我在npm库中引入了todomvc-app-css样式,链接为todomvc-app-css - npm (npmjs.com),在终端输入npm i todomvc-app-css进行下载,详细见于此网页进行查看。或者你们喜欢用自己的样式也行,不过用了这个下载的样式后,类名不能乱写,要用里面已经写好的类名样式才有效,并在main.js中引入该css

image.png

最后下载后在node_modules文件夹下有todomvc-app-css文件夹,在此文件夹下index.css是主要实现样式。除此之外,在此css中我加了一些额外样式:

span.todo-choose{
	display: inline-block;
	margin-top: -10px;
}
.todo-choose button.todo-do{
	padding: 10px;
	/* width: 100%;
	height: 100%; */
}
button.clear-completed.todo-do{
	
	width: 90px;
}

三、编写JS实现

页面框架搭建好后,就是JS实现了!

主要实现的功能,编写的函数主要如下:

1. 新增:输入任务按回车后,就会添加任务到列表。

思路: 首先在输入框中绑定键盘输入事件@keyup.enter="addTodo",以及双向绑定事件 v-model="newTodo"

const addTodo = () => {
      if (!state.newTodo) return  //若输入框输入的值为空不予加入列表中
      state.filtertodo.push({ //加数据到筛选的数组中
        id: state.filtertodo.length + 1,   //每加入的数据加到列表末尾
        title: state.newTodo, //加的内容为为输入的值
        completed: false  //默认新增的不勾选 
      })
      state.todos.push({  //加数据到todos数组中 todos数组和filtertodo数组同步变化 额外加todos数组是为了后面状态切换可以还原原数据,不必更改原数组
        id: state.todos.length + 1,
        title: state.newTodo,
        completed: false
      })
      state.newTodo = '' //每次回车加完后重新使输入框的值为空
    }

2.全选按钮:点击全选按钮列表全部选中打√或者全部不选中

思路: 在全选按钮中双向绑定 v-model="allDone"

const allDone = computed({ // 当计算属性传入一个对象时,可以对该值进行修改
      get: function () { // 默认值 获得值
        return remaining.value === 0 //全选中则返回true
      },
      set: function (value) {  //修改值
        state.filtertodo.forEach((todo) => { //遍历每个列表项把全选按钮的值赋值给每个列表项
          todo.completed = value
        })
      }
    })

3. 删除:点击每个任务后的'X',该任务就会在列表中被删除。

思路: 给"X"按钮绑定点击事件@click="removeTodo(todo)"

const removeTodo = (item) => {
      // state.todos.splice(idx, 1)
      state.filtertodo = state.todos.filter(todo => todo.id !== item.id) //把点击的该项过滤出去
      state.todos = state.todos.filter(todo => todo.id !== item.id)
    }

4. 统计:统计剩余几个任务。

思路:computed()函数计算没有被选中的任务的个数

//template
<strong>{{ left }}</strong> left


//js
const remaining = computed(
      () => state.todos.filter(todo => !todo.completed).length
    )
 const left=computed(()=>{
      return remaining.value
    })

5. 清空:点击右下角'clear completed',被选中的任务在该列表被清除不显示,并不是真正的删除。

思路: 给此按钮绑定点击事件@click="removeTo"

const removeTo=()=>{
      state.filtertodo = state.todos.filter(todo=>!todo.completed)
    }//过滤掉已经完成的,返回还没有完成的

6. 隐藏:当没有任务时,最后一栏里的内容隐藏。

思路:footer标签绑定v-show

<footer class="footer" v-show="filtertodo.length" > //数组filtertodo有值就显示,即显示的列表任务有就显示,没有则隐藏

7. 状态切换:当点击下面的'Completed Active All'任意按钮时,列表中显示相应的任务。

思路: 点击All按钮则显示全部任务,点击Active按钮则显示还没完成的任务,点击Completed按钮则显示已完成的任务。都给各个按钮绑定点击事件。

 const showAll=()=>{
     return state.filtertodo=state.todos.filter(todo=>!todo.completed||todo.completed)
    }
    
const showActive=()=>{
      state.filtertodo = state.todos.filter(todo=>!todo.completed)
    }
   
const showCompleted=()=>{
      state.filtertodo = state.todos.filter(todo=>todo.completed)
    }

tips:

  • ...toRefs(state):将state上的每个属性,都转化为ref形式的响应式数据
  • computed():接受一个参数为函数,计算结果readonly;接受一个对象作为参数,对象中会有get,set函数,get用于返回值,set用于修改值
  • computed和带有ref返回的值都要加value
  • filter:数组.filter(function(形参1,形参2,形参3){})——形参1:必选,可以理解为过滤数组的每一项item;形参2:可选,当前元素索引值;形参3:当前元素的数组对象。return出来的使条件成立的项,不成立的过滤掉;filter()返回的是一个新数组,不会改变原数组。

具体实现代码如下:

<script>
import { reactive, toRefs, computed } from 'vue'
export default {
  setup() { // 入口函数

    const state = reactive({ //响应式
      newTodo: '',//输入框的值
      todos: [ //初始化数据
        { id: '1', title: '吃饭', completed: true },
        { id: '2', title: '睡觉', completed: false }
      ],
      filtertodo:[ //
      { id: '1', title: '吃饭', completed: true },
        { id: '2', title: '睡觉', completed: false }
      ]
    })

    const addTodo = () => {
      if (!state.newTodo) return
      state.filtertodo.push({
        id: state.filtertodo.length + 1,
        title: state.newTodo,
        completed: false
      })
      state.todos.push({
        id: state.todos.length + 1,
        title: state.newTodo,
        completed: false
      })
      state.newTodo = ''
    }

    const removeTodo = (item) => {
      // state.todos.splice(idx, 1)
      state.filtertodo = state.todos.filter(todo => todo.id !== item.id)
      state.todos = state.todos.filter(todo => todo.id !== item.id)
    }
    const removeTo=()=>{
      state.filtertodo = state.todos.filter(todo=>!todo.completed)
    }

    const remaining = computed(
      () => state.todos.filter(todo => !todo.completed).length
    )

    const allDone = computed({ // 当计算属性传入一个对象时
      get: function () { // 默认值
        return remaining.value === 0
      },
      set: function (value) {
        state.filtertodo.forEach((todo) => {
          todo.completed = value
        })
      }
    })

    const left=computed(()=>{
      return remaining.value
    })

    const showAll=()=>{
     return state.filtertodo=state.todos.filter(todo=>!todo.completed||todo.completed)
    }

    const showActive=()=>{
      state.filtertodo = state.todos.filter(todo=>!todo.completed)
    }
   
    const showCompleted=()=>{
      state.filtertodo = state.todos.filter(todo=>todo.completed)
    }

    // console.log(allDone.value);

    return {
      ...toRefs(state), //解构 newTodo  todos
      addTodo,
      removeTodo,
      allDone,
      left,
      removeTo,
      showAll,
      showActive,
      showCompleted,
      
    }

  }
}
</script>

注意: 在这里我用了比较累赘的方法但是好理解——用两个一样数据的数组(todos、filtertodo)装同样的对象,这样做是为了让点击下面的切换按钮后可以回到原数据模样,filtertodo主要是将挑选出的数据显示,每次切换时filtertodo数组会改变,而todos数组的数据在切换时将符合条件的赋值给filtertodo并显示出来。

总结

本篇文章就到此为止啦,由于本人经验水平有限,难免会有纰漏,对此欢迎指正。如觉得本文对你有帮助的话,欢迎点赞收藏❤❤❤,您的点赞是持续写作的动力,感谢支持。要是您觉得有更好的方法,欢迎评论,提出建议!