在 v3 + ts 中不使用 pinia 简单写一个记事本

138 阅读1分钟

无接口纯本地操作
记事本分为三个组件组成:

  • 头部是一个输入框和全选框,用户回车可以创建一个待办事项
  • 身体是循环渲染的代办事项列表,有单选框与删除按钮,单选框控制该代办事项是否完成
  • 尾部是代办事项的总数、任务状态切换(完成、未完成、已完成)、清除已完成事项
// 父组件中的代码
<script setup lang="ts">
import "./styles/base.css"
import "./styles/index.css"

import TodoHeader from "./components/TodoHeader.vue"
import TodoMain from "./components/TodoMain.vue"
import TodoFooter from "./components/TodoFooter.vue"

import { computed, ref, watch } from "vue"
// todo.d.ts 文件引入
import { List } from "./todo"
// 本地持久化
const taskList = ref<List[] | undefined>(JSON.parse(localStorage.getItem('list') || '[]'))
// 监听子组件回车事件的回调函数
const hAddList = (val:string) => {
  taskList.value!.push({
    id: (+new Date()).toString(),
    name: val,
    isDone: false
  })
}
// 待办事项的类型
const type = ref(1)
// 使用计算属性判断当前任务状态 1:全部,2:未完成,3:已完成
const newList = computed({
  get():List[]|undefined{
      if(type.value===1){
      return taskList.value
    }else if(type.value===2){
      // 未完成
      return taskList.value!.filter(item => !item.isDone)
    }else if(type.value===3){
      return taskList.value!.filter(item => item.isDone)
    }
  },
  set(){
  }
})
const hType = (val: number) =>{
  type.value = val
}
const upDataCheckbox = (id: string) => {
  taskList.value!.forEach(item => {
    if(item.id === id){
      item.isDone = !item.isDone
    }
  })
}
// 使用计算属性控制小选框和全选框是否勾选
const isAll = computed({
  get(){
    return newList.value!.every(item => item.isDone)
  },
  set(val){
    // isAll 值被修改时执行的方法,val 是修改后的值
    newList.value!.map(item => item.isDone = val)
  }
})
// 监听子组件修改全选框是否选中事件的回调
const upDataIsAll = (val: boolean) => {
  isAll.value = val
}
// 监听子组件删除事件的回调
const delList = (id:string) => {
  taskList.value = taskList.value!.filter(item => item.id !== id)
}
// 监听子组件清除已完成事件的回调
const delListDone = () => {
  taskList.value = taskList.value!.filter(item => !item.isDone)
}
// 侦听数组的变化,变化的数组重新存入本地
watch(()=> taskList, () => {
  console.log(taskList.value)
  localStorage.setItem('list',JSON.stringify(taskList.value))
}, {deep: true})
</script>

<template>
  <section class="todoapp">
    <TodoHeader @upDataIsAll="upDataIsAll" :isAll="isAll" @addList="hAddList"></TodoHeader>
    <TodoMain @delList="delList" @upDataCheckbox="upDataCheckbox" :taskList="newList"></TodoMain>
    <TodoFooter @delListDone="delListDone" :taskList="taskList" :type="type" @upDataType="hType"></TodoFooter>
  </section>
</template>

类型声明文件:todo.d.ts

type List = {
    id: string;
    isDone: boolean;
    name: string;
}
export {
    List
}

头部子组件

<template>
    <header class="header">
      <h1>todos</h1>
      <input 
      id="toggle-all" 
      :checked="isAll"
      @click="upDataIsAll"
      class="toggle-all" 
      type="checkbox" >
      <label for="toggle-all"></label>
      <input
        class="new-todo"
        v-model.trim="todoInp"
        placeholder="输入任务名称-回车确认"
        autofocus
        @keydown.enter="haddEnter"
      />
    </header>
  </template>
<script setup lang="ts">
import { ref } from 'vue';
const prop = defineProps<{ isAll: boolean }>()
const emit = defineEmits<{
  (e: 'addList', todoInp: string):void,
  (e: 'upDataIsAll', boo: boolean):void
}>()
// 输入的内容
const todoInp = ref<string>('')
const haddEnter = () => {
  // 传递到父组件添加数据
  emit('addList', todoInp.value)
  // 清空input
  todoInp.value = ''
}
const upDataIsAll = () => {
  emit('upDataIsAll', !prop.isAll)
}
</script>

身体

<script setup lang="ts">
import { List } from '../todo'

const emit = defineEmits<{ 
  (e: 'upDataCheckbox', id: string):void 
  (e: 'delList', id: string):void 
}>()

defineProps<{taskList: List[]|undefined}>()

// 点击时让当前这一项的 isDone 取反, 也就是通知父组件修改当前项的完成状态
const checkIsDone = (id: string)=>{
  emit('upDataCheckbox', id)
}

// 通知父组件删除当前项
const delList = (id: string) => {
  emit('delList', id)
}

</script>
<template>
    <ul class="todo-list">
      <!-- completed: 完成的类名 -->
      <li :class="{'completed': item.isDone}" v-for="item in taskList" :key="item!.id">
        <div class="view">
          <input @click="checkIsDone(item.id)" :checked="item.isDone" class="toggle" type="checkbox"/>
          <label>{{item.name}}</label>
          <button @click="delList(item.id)" class="destroy"></button>
        </div>
      </li>
    </ul>
</template>

底部组件

<script setup lang="ts">
import { List } from '../todo';

// 使用计算属性,判断点击的是哪个按钮,给父组件传递对应的数据,父组件中计算属性返回对应的数组
const emit = defineEmits<{
  (e:'upDataType', num: number):void
  (e:'delListDone'):void
}>()
// 通知父组件修改当前点击的底部完成状态
const hisDone = (num:number) => {
  emit('upDataType', num)
}

defineProps<{type:number,taskList: List[] | undefined }>()
const delList = () => {
  // 通知父组件清除已完成的数据
  emit('delListDone')
}
</script>
<template>
    <footer class="footer">
      <span class="todo-count">剩余<strong>{{taskList!.length}}</strong></span>
      <ul class="filters">
        <li>
          <a @click="hisDone(1)" :class="{'selected': type===1}" href="javascript:;">全部</a>
        </li>
        <li>
          <a @click="hisDone(2)" :class="{'selected': type===2}" href="javascript:;">未完成</a>
        </li>
        <li>
          <a @click="hisDone(3)" :class="{'selected': type===3}" href="javascript:;">已完成</a>
        </li>
      </ul>
      <button @click="delList" class="clear-completed" >清除已完成</button>
    </footer>
  </template>