当前位置: 代码迷 >> 综合 >> Vue 学习笔记07:Todo-list 案例
  详细解决方案

Vue 学习笔记07:Todo-list 案例

热度:91   发布时间:2023-12-12 03:24:35.0

Todo-list 案例

  • 一、组件化编码流程
  • 二、props 扩展
  • 三、webStorage
  • 四、自定义事件
  • 五、全局事件总线
  • 六、消息订阅与发布
  • 七、nextTick
  • 八、动画与过度
    • 1. 动画 animation
    • 2. 过渡 transition
    • 3. 第三方样式库 animate.css
  • 九、案例代码
    • 1. Header
    • 2. List
    • 3. Item
    • 4. Footer
    • 5. App.vue
    • 6. main.js

一、组件化编码流程

  1. 拆分静态组件:组件要按照功能点拆分,命名不要与 html 元素冲突。
  2. 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件再用:
    (1)一个组件在用:放在组件自身即可。
    (2)一些组件再用:放在他们共同的父组件上(状态提示)。
  3. 交互——从绑定事件监听开始。

二、props 扩展

  • props 适用于:
    (1)父组件 ===> 子组件 通信
    (2)子组件 ===> 父组件 通信(要求父先给子一个函数)

  • 使用 v-model 时要切记:
    v-model 绑定的值不能是 props 传过来的值,因为 props 是不可修改的!

  • props 传过来的若是对象类型的值,修改对象中的属性时 Vue 不会报错,但不推荐这样做。

三、webStorage

  • 存储内容大小一般支持 5MB 左右(不同浏览器可能不一样)

  • 浏览器通过 Window.sessionStorage 和 Window.localStorage 属性实现本地存储机制。

  • 相关 API:
    (1)xxxxxStorage.setItem('key', 'value');
    该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值。
    (2)xxxxxStorage.getItem('person');
    该方法接受一个键名作为参数,返回键名对应的值。
    (3)xxxxxStorage.removeItem('key');
    该方法接受一个键名作为参数,并把该键名从存储中删除。
    (4)xxxxxStorage.clear();
    该方法会清空存储中的所有数据。

  • 备注:
    (1)SessionStorage 存储的内容会随着浏览器窗口关闭而消失。
    (2)LocalStorage 存储的内容,需要手动清除才会消失。
    (3)xxxxxStorage.getItem(xxx); 如果 xxx 对应的 value 获取不到,那么 getItem 的返回值是 null。
    (4)JSON.parse(null) 的结果依然是 null。

四、自定义事件

  • 自定义事件是一种组件间通信的方式,适用于:【子组件 ===> 父组件】

  • 使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的回调在A中)

  • 绑定自定义事件:
    (1)第一种方式,通过 v-on,在父组件中:Demo @atguigu="test"Demo v-on:atguigu="test"
    (2)第二种方式,通过 ref,在父组件中:

    <demo ref="demo">methods:{test(){...}
    },
    mounted(){this.$refs.demo.$on('ayguigu', this.test)
    }
    

    (3)若想让自定义事件只能触发一次,可以用 .once 修饰符,或 $once 方法。

  • 触发自定义事件:this.$emit('atguigu', 传给父的参数数据)

  • 解绑自定义事件:解绑一个 this.$off('atguigu')、解绑多个 this.$off(['atguigu', 'demo', ...])、解绑全部 this.$off()

  • 组件上也可以绑定原生 DOM 事件,需要使用 native 修饰符。<student @click.native="show"></student>

  • 注意:通过 this.$refs.demo.$on('ayguigu', 回调) 绑定自定义时间时,回调要么配置在 methods 中,要么用箭头函数,否则回调中的 this 会指向子组件。

五、全局事件总线

  • 全局事件总线是一种组件间通信的方式,适用于任意组件间的通信。

  • 安装全局事件总线:
    在这里插入图片描述

  • 使用全局事件总线:
    (1)接收数据:A组件想接收数据,则在A组件中给 $bus 绑定自定义事件,事件的回调留在A组件自身。

    methods() {
          demo(data){
          ......}
    }
    ......
    mounted() {
          this.$bus.$on('xxx', this.demo)
    }
    

    (2)提供数据:this.$bus.$emit('xxx', 数据)

  • 最好在 beforeDestroy 钩子中,用 $off 去解绑当前组件所用到的事件。

六、消息订阅与发布

  • 消息订阅与发布是一种组件间通信的方式,适用于任意组件间通信。

  • 使用步骤:
    (1)安装第三方库 pubsub:npm i pubsub-js
    (2)引入:import pubsub from "pubsub-js";
    (3)接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身。

    methods() {
          demo(data){
          ......}
    }
    ......
    mounted() {
          this.pid = pubsub.subscribe('xxx', this.demo) // 订阅消息
    }
    

    (4)提供数据:pubsub.publish('xxx', 数据)

  • 最好在 beforeDestroy 钩子中,用 pubsub.unsubscribe(this.pid); 去取消订阅。

七、nextTick

  • 语法:this.$nextTick(回调函数)
    在这里插入图片描述

  • 作用:nextTick 是一个生命周期钩子,在下一次 DOM 更新结束后执行其指定的回调。

  • 什么时候用:当改变数据后,要基于更新后的新 DOM 进行某些操作时,要在 nextTick 所指定的回调函数中执行。

八、动画与过度

  • 作用:在插入、更新或移除 DOM 元素时,在合适的时候给元素添加样式类名。

  • 图示:
    在这里插入图片描述

  • 写法:
    (1)准备好样式:
             元素进入的样式:
                  ① v-enter:进入的起点
                  ② v-enter-active:进入过程中
                  ① v-enter-to:进入的终点
             元素离开的样式:
                  ① v-leave:离开的起点
                  ② v-leave-active:离开过程中
                  ① v-leave-to:离开的终点

  • 使用 <transition> 包裹要过度的元素,并配置 name 属性:

    <transition><h1 v-show="isShow">你好啊!</h1>
    </transition>
    
  • 备注:若有多个元素需要过度,则需要使用:<transition-group>,且每个元素都要指定 key 值。

1. 动画 animation

<template><div><transition-group name="hello" appear><h2 v-show="show" key="1">学校名称:{
   { name }}</h2><h2 v-show="show" key="2">学校地址:{
   { address }}</h2></transition-group></div>
</template><style lang="css" scoped>h2 {
      background-color: pink;}.hello-enter-active {
      animation: identifier 1s;}.hello-leave-active {
      animation: identifier 1s reverse;}@keyframes identifier {
      from {
      transform: translateX(-100%);}to {
      transform: translateX(0);}} </style>

2. 过渡 transition

<template><div><transition-group name="hello" appear><h2 v-show="show" key="1">学校名称:{
   { name }}</h2><h2 v-show="show" key="2">学校地址:{
   { address }}</h2></transition-group></div>
</template><style lang="css" scoped>h2 {
      background-color: pink;}.hello-enter,.hello-leave-to {
      transform: translateX(-100%);}.hello-enter-to,.hello-leave {
      transform: translateX(0);}.hello-enter-active,.hello-leave-active {
      transition: 1s;} </style>

3. 第三方样式库 animate.css

  • 安装

    npm i animate.css
    
  • 引入,在 <script> 中引入

    <script>import 'animate.css'
    </script>
    
  • 使用

    <template><div><transition-groupname="animate__animated animate__bounce"enter-active-class="animate__jello"leave-active-class="animate__backOutUp"appear><h2 v-show="!show" key="1">学校名称:{
         { name }}</h2><h2 v-show="show" key="2">学校地址:{
         { address }}</h2></transition-group></div>
    </template><script> import "animate.css"; </script><style lang="css" scoped>h2 {
            background-color: pink;} </style>
    

九、案例代码

1. Header

<template><div class="todo-header"><inputtype="text"placeholder="请输入你的任务名称,按回车键确认"v-model="title"@keyup.enter="add"/></div>
</template><script> import {
       nanoid } from "nanoid"; export default {
      name: "AddHeader",data() {
      return {
      title: "",};},methods: {
      add() {
      if (!this.title.trim()) return alert("输入不能为空");// 包装成对象const todo = {
      id: nanoid(),title: this.title,done: false,};this.$emit('headerAddTodo',todo)this.title = "";},}, }; </script><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>

2. List

<template><ul class="todo-main" v-if="todo.length"><transition-groupname="animate__animated animate__bounce"enter-active-class="animate__bounceInRight"leave-active-class="animate__backOutRight"appear><todo-item v-for="item in todo" :key="item.id" :todo="item" /></transition-group></ul>
</template><script> import "animate.css"; import TodoItem from "./TodoItem.vue"; export default {
      components: {
       TodoItem },name: "TodoList",props: ["todo"], }; </script><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>

3. Item

<template><li><label><input type="checkbox" :checked="todo.done" @change="handle(todo.id)" /><span v-show="!todo.isEdit"> {
   { todo.title }}</span></label><button class="btn btn-danger" @click="ListDelete(todo.id)">删除</button><button v-show="!todo.isEdit" class="btn btn-edit" @click="editItem(todo)">编辑</button><inputv-show="todo.isEdit"type="text":value="todo.title"ref="itemInput"@keyup.enter="inputBlur(todo, $event)"@blur="inputBlur(todo, $event)"/></li>
</template><script> export default {
      name: "TodoItem",props: ["todo"],methods: {
      handle(id) {
      this.$bus.$emit("checkTodo", id);},ListDelete(id) {
      if (confirm("确定删除吗?")) {
      this.$bus.$emit("deleteTodo", id);}},editItem(todo) {
      if (todo.hasOwnProperty("isEdit")) {
      todo.isEdit = true;} else {
      this.$set(todo, "isEdit", true);}this.$nextTick(function () {
      this.$refs.itemInput.focus();});},inputBlur(todo, e) {
      todo.isEdit = false;// console.log(todo.id, e.target.value);if (e.target.value.trim() === "") return alert("你没有输入值");this.$bus.$emit("todotitle", todo.id, e.target.value);},}, }; </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: #ddd; } li:hover button {
      display: block; } </style>

4. Footer

<template><div class="todo-footer" v-if="todo.length"><label><input type="checkbox" v-model="checkAll" /></label><span><span>已完成{
   { doneTotal }}</span> / 全部{
   { todo.length }}</span><button class="btn btn-danger" @click="clearAll">清除已完成任务</button></div>
</template><script> export default {
      name: "SelectFooter",props: ["todo"],computed: {
      doneTotal() {
      return this.todo.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);},checkAll: {
      get() {
      return this.todo.length === this.doneTotal;},set(value) {
      this.$emit("checkAllTodo", value);},},},methods: {
      clearAll() {
      this.$emit("clearAllTodo")},}, }; </script><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>

5. App.vue

<template><div id="app"><div class="todo-container"><div class="todo-wrap"><add-header @headerAddTodo="addTodo" /><todo-list :todo="todo" /><select-footer:todo="todo"@checkAllTodo="checkAllTodo"@clearAllTodo="clearAllTodo"/></div></div></div>
</template><script> import AddHeader from "./components/AddHeader"; import SelectFooter from "./components/SelectFooter"; import TodoList from "./components/TodoList";export default {
      name: "App",components: {
      AddHeader,SelectFooter,TodoList,},data() {
      return {
      todo: JSON.parse(localStorage.getItem("todo")) || [],};},methods: {
      // 添加一个todoaddTodo(todo) {
      this.todo.unshift(todo);},// 勾选or取消勾选checkTodo(id) {
      this.todo.forEach((todo) => {
      if (id === todo.id) todo.done = !todo.done;});},// 修改editTodo(id, val) {
      this.todo.forEach((todo) => {
      if (id === todo.id) todo.title = val.trim();});},// 删除deleteTodo(id) {
      this.todo = this.todo.filter((todo) => todo.id !== id);},checkAllTodo(check) {
      this.todo.forEach((todo) => (todo.done = check));},clearAllTodo() {
      const x = this.todo.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);if (x <= 0) {
      alert("没有勾选任何TODO");} else {
      if (confirm("确定删除所有已完成吗?")) {
      this.todo = this.todo.filter((todo) => !todo.done);}}},},watch: {
      todo: {
      deep: true,handler(value) {
      localStorage.setItem("todo", JSON.stringify(value));},},},created() {
      this.todo.forEach((todo) => {
      if (todo.hasOwnProperty("isEdit")) {
      todo.isEdit = false;}});},mounted() {
      this.$bus.$on("checkTodo", this.checkTodo);this.$bus.$on("deleteTodo", this.deleteTodo);this.$bus.$on("todotitle", this.editTodo);},beforeDestroy() {
      this.$bus.$off(["checkTodo", "deleteTodo", "todotitle"]);}, }; </script><style> /*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-edit {
      color: #fff;background-color: skyblue;border: 1px solid rgb(106, 160, 182);margin-right: 5px; } .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>

6. main.js

import Vue from 'vue'
import App from './App.vue'Vue.config.productionTip = falsenew Vue({
    render: h => h(App),beforeCreate() {
    Vue.prototype.$bus = this;},
}).$mount('#app')
  相关解决方案