vue3_todolist项目

输出效果

在这里插入图片描述

总结

  1. 完成一个简易版的todo项目
  2. 使用简单的ts语法
  3. 使用vue3中的composition API
  4. 对于父子组件的通信有了一定的了解

App.vue

<template>
  <div class="todo-container">
    <div class="todo-wrap">
      <add-bar :addTodo='addTodo'></add-bar>
      <display 
        :todos='todos' 
        :deleteTodo='deleteTodo'
        :updateTodo='updateTodo'
      ></display>
      <panel
        :todos='todos'
        :checkAllItem = 'checkAllItem'
        :clearAllCompletedTodos='clearAllCompletedTodos'
      ></panel>
    </div>
  </div>
</template>

<script lang="ts">
import {Todo} from './types/todo'

import { defineComponent,reactive, toRefs, watch,onMounted } from "vue";

import AddBar from "./components/AddBar.vue";
import Display from "./components/Display.vue";
import Panel from "./components/Panel.vue";

import {saveTodos,readTodos} from './utils/localStorage'
export default defineComponent({
  name: "App",
  components: {
    AddBar,
    Display,
    Panel,
  },
  setup() {
    // const state = reactive<{todos:Todo []}>({
    //   todos:[
    //     {id:1,title:'Alice',isCompleted:true},
    //     {id:2,title:'Bruce',isCompleted:false},
    //     {id:3,title:'Celina',isCompleted:true},
    //     {id:4,title:'Celina',isCompleted:true},
    //   ]
    // })
    // 页面加载完成之后在读取数据
    const state = reactive<{todos:Todo[]}>({
      todos:[]
    })
    onMounted(() => {
      setTimeout(()=>{
        state.todos = readTodos()
      },1000)
    })
    // 添加新的数据
    const addTodo = (todo:Todo) =>{
      state.todos.unshift(todo)
    }
    //删除数据项
    const deleteTodo = (index:number) =>{
      state.todos.splice(index,1)
    }
    //修改todo的状态
    const updateTodo = (todo:Todo,isCompleted:boolean) => {
      todo.isCompleted = isCompleted
    }
    //修改所有todo的状态
    const checkAllItem = (isCompleted:boolean)=>{
      state.todos.forEach((todo)=>{
        todo.isCompleted = isCompleted
      })
    }
    //清理所有已完成的todo
    const clearAllCompletedTodos = () =>{
      state.todos = state.todos.filter(todo => !todo.isCompleted)
    }
    //监听todos的变化并保存到本地
    watch(()=>state.todos,saveTodos,{deep:true})
    return {
      ...toRefs(state),
      addTodo,
      deleteTodo,
      updateTodo,
      checkAllItem,
      clearAllCompletedTodos
    };
  },
});
</script>

<style scpoed>
/*base*/
body {
  background: #fff;
}
/*app*/
.todo-container {
  width: 600px;
  margin: 0 auto;
}
.todo-container .todo-wrap {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 5px;
}
</style>

AddBar.vue

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

<script lang="ts">
import {Todo} from '../types/todo'
import { defineComponent,ref } from "vue";

export default defineComponent({
  name: "AddBar",
  props:{
    addTodo:{
      type:Function,
      required:true
    }
  },
  setup(props) {
    const title = ref('')
    const addItem = () => {
      const text = title.value
      if(!text.trim())return
      const todo:Todo = {
        id:Date.now(),
        title:text,
        isCompleted:false
      }
      props.addTodo(todo)
      title.value=''
    }
    return {
      title,
      addItem
    };
  },
});
</script>

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

Display.vue

<template>
  <ul class="todo-main">
    <display-item 
      v-for="todo,index in todos" 
      :key="todo.id"
      :todo='todo'
      :index = index
      :deleteTodo='deleteTodo'
      :updateTodo='updateTodo'>
    </display-item>
  </ul>
</template>

<script lang="ts">
import {Todo} from '../types/todo'

import { defineComponent } from "vue";
import DisplayItem from "./DisplayItem.vue";

export default defineComponent({
  name: "Display",
  components: {
    DisplayItem,
  },
  props:{
    todos:{
      type:Object as () => Todo[],
      required:true
    },
    deleteTodo:{
      type:Function,
      required:true
    },
    updateTodo:{
      type:Function,
      required:true
    }
  }

  
});
</script>

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

DisplayItem.vue

<template>
  <li @mouseenter="mouseHandler(true)" @mouseleave="mouseHandler(false)">
    <label :class="{ 'delete-line': isCompleted }">
      <input type="checkbox" v-model="isCompleted" />
      <span>{{ todo.title }}</span>
    </label>
    <button class="btn btn-danger" v-show="isShow" @click="delTodo">
      删除
    </button>
  </li>
</template>

<script lang='ts'>
import { Todo } from "@/types/todo";
import { computed, defineComponent, ref } from "vue";

export default defineComponent({
  name: "DisplayItem",
  props: {
    todo: {
      type: Object as () => Todo,
      required: true,
    },
    index: {
      type: Number,
      required: true,
    },
    deleteTodo: {
      type: Function,
      required: true,
    },
    updateTodo: {
      type: Function,
      required: true,
    },
  },
  setup(props) {
    //按钮是否显示
    const isShow = ref(false);
    const mouseHandler = (flag: boolean) => {
      flag ? (isShow.value = true) : (isShow.value = false);
    };
    // 删除todo条目
    const delTodo = () => {
      if(window.confirm('delete?')){
        props.deleteTodo(props.index);
      }
    };
    const isCompleted = computed({
      get() {
        return props.todo.isCompleted;
      },
      set(val) {
        props.updateTodo(props.todo, val);
      },
    });
    return {
      isShow,
      mouseHandler,
      delTodo,
      isCompleted,
    };
  },
});
</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: pink;
}

.delete-line {
  text-decoration-line: line-through;
}

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

types>todo.ts

export  interface Todo{
    id:number,
    title:string,
    isCompleted:boolean
}

utils>localStorage.ts

import {Todo} from '../types/todo'
//保存数据到浏览器的缓存中
export function saveTodos(todos:Todo[]):void{
    localStorage.setItem('todos_key',JSON.stringify(todos))
}

export function readTodos():Todo[]{
    return JSON.parse(localStorage.getItem('todos_key')||'[]')
}

下载

下载地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值