介绍

组件层次关系
在这里插入图片描述
页面效果

在这里插入图片描述

遇到的问题

  • ESLint 规则: eslint-disable vue/no-mutating-props
    源代码
<input type="checkbox" v-model="todo.isCompleted" />

按照规则修改

<input type="checkbox" :checked="todo.isCompleted" @input="$emit('input', $event.target.todo.isCompleted)"  />

base.css

  • 在index.html 引入的页面基本样式 base.css
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;
}

存储工具类

  • localStorageUtil.ts
// 保存数据到浏览器的缓存中
export function saveArray(key: string,value: []) {
  localStorage.setItem(key,JSON.stringify(value))
}
export function readArray(key: string){
  return JSON.parse(localStorage.getItem(key)|| '[]')
}

定义类型

  • todo.ts
// 定义一个接口,约束state的数据类型
export interface Todo {
  id: number,
  title: string,
  isCompleted: boolean
}

app.vue

<template>
  <div class="todo-container">
    <div class="todo-wrap">
      <Header :addTodo="addTodo" />
      <List :todos = "todos" :delTodo = "delTodo" :updateTodo = "updateTodo" />
      <Footer :todos = "todos" :checkAll ="checkAll" :clear = "clearAllCompletedTodos"/>
    </div>
  </div>
</template>
<script lang='ts'>
import { defineComponent, onMounted, reactive, toRefs, watch } from "vue";
import Header from './components/Header.vue'
import List from './components/List.vue'
import Footer from './components/Footer.vue'
import {Todo} from './type/todo'
import { saveArray, readArray } from './utils/localStorageUtil'
export default defineComponent({
  name: "App",
  components: {
    Header,
    List,
    Footer
  },
  setup() {
    // const state = reactive<{todos: Todo[]}>({
    //   todos: [
    //     {id:1,title: '记单词',isCompleted: false},
    //     {id:2,title: '编程',isCompleted: true}
    //   ]
    // })
    const state = reactive<{todos: Todo[]}>({
      todos: []
    })
    const key = 'todos_key'
    // 界面加载完毕后过一会后再读取数据
    onMounted(()=>{
      setTimeout(()=>{
        state.todos = readArray(key)
      },1000)
    })
    const addTodo = (todo: Todo) => {
      state.todos.unshift(todo)
    } // 添加数组的方法,放在数组头部
    const delTodo = (index: number) => {
      state.todos.splice(index,1)
    } // 删除数据
    const updateTodo = (todo: Todo, isComplete: boolean) => {
      todo.isCompleted = isComplete
    } // 修改todo的isCompleted属性的状态,属性的修改应该让父组件来决定
    const checkAll = (isComplete: boolean)=>{
      state.todos.forEach(todo => {
        todo.isCompleted = isComplete
      })
    }
    const clearAllCompletedTodos = ()=> {
      state.todos = state.todos.filter(todo=>!todo.isCompleted)
    }
    // 监视操作: 如果todos数组的数据变化了,直接存储到浏览器的缓存中
    // watch(()=>state.todos,(value)=>{
    //   localStorage.setItem('todos_key',JSON.stringify(value))
    // },{deep:true}) 
    watch(()=>state.todos,(value: Todo [])=>{
      saveArray(key, value as [])
    },{deep:true}) 
    return {
      ...toRefs(state),
      addTodo,
      delTodo,
      updateTodo,
      checkAll,
      clearAllCompletedTodos
    }
  }
});
</script>
<style  scoped>
.todo-container {
  width: 600px;
  margin: 0 auto;
}
.todo-container .todo-wrap {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 5px;
}
</style>

Header.vue

<template>
  <div class="todo-header">
    <input type="text" placeholder="请输入你的任务名称,按回车键确认" @keydown.enter="add" v-model="title">
  </div>
</template>
<script lang='ts'>
import {defineComponent, ref} from 'vue'
export default defineComponent({
  name: 'Header',
  props: {
    addTodo: {
      type: Function,
      require: true // 必须要传
    }
  },
  setup(props) {
    const title = ref('')
    const add = () => {``
      if (!title.value.trim()) return;
      props.addTodo?.({
        id: Date.now(),
        title: title.value,
        isCompleted: false
      })
      title.value = ''
    }
    return {
      title,
      add,
    }
  }
})
</script>
<style scoped>
.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>

List.vue

<template>
  <ul class="todo-main">
    <Item v-for="(todo, index) in todos " :key="todo.id" :todo="todo" :delTodo="delTodo" :index="index" :updateTodo="updateTodo"/>
  </ul>
</template>
<script lang='ts'>
import {defineComponent} from 'vue'
import Item from './Item.vue'
export default defineComponent({
  name: 'List',
  components: {
    Item
  },
  props: ['todos', 'delTodo', 'updateTodo']
})
</script>
<style scoped>
.todo-main {
  margin-left: 0;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding: 0;
}
.todo-empty{
  height: 40px;
  line-height: 20px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding-left: 5px;
  margin-top: 10px;
}
</style>

Item.vue

/* eslint-disable vue/no-mutating-props */
<template>
  <li @mouseenter="mouseHandler(true)" @mouseleave="mouseHandler(false)"
  :style="{backgroundColor:bgColor,color:myColor}"
  >
    <label >
      <input type="checkbox" v-model="isComplete"  />
      <span>&nbsp;{{todo.title}}</span>
    </label>
    <button class="btn btn-danger" v-if="isShow" @click="del">删除</button>
  </li>
</template>
<script lang='ts'>
import { computed, defineComponent, ref } from "vue";
import { Todo } from "../type/todo"
export default defineComponent({
  name: "Item",
  props: {
    todo: {
      type: Object as () => Todo,
      required: true
    },
    delTodo: {
      type: Function,
      required : true
    },
    index: {
      type: Number,
      required: true
    },
    updateTodo: {
      type: Function,
      required: true
    }
  },
  setup(props) {
    const bgColor = ref('white') // 背景色
    const myColor = ref('black') // 字体颜色
    const isShow = ref(false) // 按钮默认不显示
    const mouseHandler = (flag: boolean)=> {
      if (flag) {
        bgColor.value = 'pink'
        myColor.value = 'green'
        isShow.value = true
      } else {
        bgColor.value = 'white'
        myColor.value = 'black'
        isShow.value = false
      }
    }
    const del = () => {
      if (window.confirm("是否输出该任务")) {
        props.delTodo?.(props.index)
      }
    }
    const isComplete = computed({
      get() {
        return props.todo.isCompleted
      },
      set(val: boolean) {
        props.updateTodo?.(props.todo, val)
      }
    }) // 利用计算属性的方式 来让当前的复选框选中/不选中
    return {
      bgColor,
      myColor,
      isShow,
      mouseHandler,
      del,
      isComplete
    }    
  }
});
</script>
<style scoped>
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;
  margin-top: 3px;
}
li::before {
  content: initial;
}
li:last-child {
  border-bottom: none;
}
</style>

Footer.vue

<template>
  <div class="todo-footer">
    <label>
      <input type="checkbox" v-model="isCheckAll"/>
    </label>
    <span>
      <span>已完成{{count}}</span> / 全部{{todos.length}}
    </span>
    <button class="btn btn-danger" @click="delAll">清除已完成的任务</button>
  </div>
</template>
<script lang='ts'>
import { Todo } from "@/type/todo";
import { computed, defineComponent } from "vue";
export default defineComponent({
  name: "Footer",
  props: {
    todos: {
      type: Array as () => Todo[],
      required: true
    },
    checkAll: {
      type: Function,
      required: true
    },
    clear: {
      type: Function,
      required: true
    }
  },
  setup(props) {
    const count = computed(()=>{
      return props.todos?.reduce((pre,todo)=>pre+(todo.isCompleted?1:0),0)
    })
    const isCheckAll = computed({
      get() {
        return count.value>0 && props.todos.length === count.value
      },
      set(val) {
        props.checkAll(val)
      }
    })
    const delAll = ()=>{
      if (window.confirm("是否清除已完成的任务")) {
        props.clear()
      }
    }
    return {
      count,
      isCheckAll,
      delAll
    }
  }
});
</script>
<style scoped>
.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>
Logo

前往低代码交流专区

更多推荐