(Vue案例)TodoList

这篇博客详细介绍了如何使用Vue.js构建一个TodoList应用,从基本的组件结构到本地存储、自定义事件、事件总线以及消息订阅和发布的实现。同时,还涵盖了Vue.nextTick的用法以及在TodoList中添加动画效果的技巧。通过一步步的代码展示,帮助读者深入理解Vue.js的实用技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

实现效果

 代码展示

main.js

//引入vue
import Vue from "vue";
//引入组件App
import App from "./App.vue";

//关闭vue的生产提示
Vue.config.productionTip = false;

//创建vm
new Vue({
  el: "#app",
  render: (h) => h(App),
});

App.vue

<template>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <todo-head :addTodo="addTodo" />
        <todo-list
          :todoList="todoList"
          :deleteTodo="deleteTodo"
          :updateFlag="updateFlag"
        />
        <todo-fotter
          :todoList="todoList"
          :updateFlagAll="updateFlagAll"
          :deleteFlagAll="deleteFlagAll"
        />
      </div>
    </div>
  </div>
</template>

<script>
import TodoList from "./components/todo-list.vue";
import TodoHead from "./components/todo-head.vue";
import TodoFotter from "./components/todo-fotter.vue";
//引入组件

export default {
  components: { TodoHead, TodoList, TodoFotter },
  name: "App",
  data() {
    return {
      todoList: [
        { id: "001", name: "yb", flag: true },
        { id: "002", name: "yg", flag: false },
        { id: "003", name: "yf", flag: true },
      ],
    };
  },
  methods: {
    //添加
    addTodo(todo) {
      this.todoList.unshift(todo); //在数组最前方添加
    },
    //删除
    deleteTodo(id) {
      //过滤出 不是要删除id的 所有值
      this.todoList = this.todoList.filter((todo) => todo.id != id);
    },
    //删除全部已选的
    deleteFlagAll() {
      this.todoList = this.todoList.filter((todo) => todo.flag === false);
    },
    //修改指定事 是否完成
    updateFlag(id) {
      this.todoList.forEach((todo) => {
        if (todo.id === id) todo.flag = !todo.flag;
      });
    },
    //修改全部的 是否完成
    updateFlagAll(flag) {
      this.todoList.forEach((todo) => {
        todo.flag = flag;
      });
    },
    //删除指定的
  },
};
</script>

<style>
/*base*/
body {
  background: plum;
}
.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;
}

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

components里的 todo-head.vue

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

<script scoped>
//引入nanoid
import { nanoid } from "nanoid";
export default {
  props: ["addTodo"],
  data() {
    return {
      todoName: "",
    };
  },
  methods: {
    add() {
      //去除前后空格判断不为空 进行添加
      if (this.todoName.trim() != "" && this.todoName.trim() != null) {
        const todo = { id: nanoid(), name: this.todoName, flag: false };
        this.addTodo(todo);
        this.todoName = "";
      }
    },
  },
};
</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>

components里的todo-list.vue

<template>
  <ul class="todo-main">
    <todo-item
      v-for="todo in todoList"
      :key="todo.id"
      :todo="todo"
      :deleteTodo="deleteTodo"
      :updateFlag="updateFlag"
    />
  </ul>
</template>

<script>
import todoItem from "./todo-item.vue";
export default {
  components: { todoItem },
  props: ["todoList", "deleteTodo", "updateFlag"],
};
</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>

components里的 todo-Item.vue

<template>
  <li>
    <label>
      <!-- change:元素发生修改时调用 -->
      <input type="checkbox" :checked="todo.flag" @change="upd(todo.id)" />
      <span>{{ todo.name }}</span>
    </label>
    <button class="btn btn-danger" @click="del(todo.id)">删除</button>
  </li>
</template>

<script>
export default {
  props: ["todo", "deleteTodo", "updateFlag"],
  methods: {
    del(id) {
      this.deleteTodo(id);
    },
    upd(id) {
      this.updateFlag(id);
    },
  },
};
</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;
}
li:hover button {
  display: block;
}
</style>

components 里的 todo-fotter.vue

<template>
  <div class="todo-footer" v-show="allNUm">
    <label>
      <input type="checkbox" v-model="flag" />
    </label>
    <span>
      <span>已完成{{ trueNum }}</span> / 全部{{ allNUm }}
    </span>
    <button class="btn btn-danger" @click="delFlagAll">清除已完成任务</button>
  </div>
</template>

<script>
export default {
  props: ["todoList", "updateFlagAll", "deleteFlagAll"],
  methods: {
    delFlagAll() {
      this.deleteFlagAll();
    },
  },
  computed: {
    allNUm() {
      return this.todoList.length;
    },
    trueNum() {
      /* reduce 根据数组的长度 执行几次
      returnValue 每次的返回值
      todo 每次的实例
      0 初始值 */
      return this.todoList.reduce(
        (returnValue, todo) => returnValue + (todo.flag ? 1 : 0),
        0
      );
    },
    flag: {
      get() {
        //如果 已完成的数量 === 全部数量 默认全选
        return this.trueNum === this.allNUm && this.trueNum > 0;
      },
      set(value) {
        //value获取当前多选框的状态  勾选 true 不勾选 false
        this.updateFlagAll(value);
      },
    },
  },
};
</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>

总结 

1、组件化编码流程:
        (1)、拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突
        (2)、实现动态组件:考虑好数据的存放位置,数据是一个在用,还是一堆组件再用
            1)、一个组件在用:放在组件自身即可。
            2)、一些组件在用:放在他们共同的父组件上(状态提升)
        (3)、实现交互:从绑定事件开始。
    2、props适用于:
        (1)、父组件 ===> 子组件 通信   (父组件创建一个函数 写点方法 让子组件传入参数调用实现功能)
        (2)、子组件 ===> 父组件 通信 (要求父组件传过来一个函数 再通过上方的说明实现)

    3、使用v-model 时要切记:v-model 绑定的值不能是 props传过来的值,因为props是 不可以修改的!
    4、props传过来的若是对象类型的值,修改对象的属性时Vue不会报错 会提示破浪线 可以运行,但不推荐使用

更新

TodoList_本地存储

App.vue

  data() {
    return {
      //获取存储
      todoList: localStorage.getItem("todos") || [],
    };
  },


  watch: {
    todoList: {
      deep: true, //开启深度监视
      handler(value) {
        //这里获取的 value 是todoList 被修改后的值
        localStorage.setItem("todos", JSON.stringify(value)); //把值转为json格式字符串
      },
    },
  },

TodoList_自定义事件

App.vue

<template>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <todo-head @addTodo="addTodo" />
        <todo-list
          :todoList="todoList"
          :deleteTodo="deleteTodo"
          :updateFlag="updateFlag"
        />
        <todo-fotter
          :todoList="todoList"
          @updateFlagAll="updateFlagAll"
          @deleteFlagAll="deleteFlagAll"
        />
      </div>
    </div>
  </div>
</template>

//给底部组件标签绑定两个自定义事件    @updateFlagAll     @deleteFlagAll

 todo-fotter.vue

<template>
  <div class="todo-footer" v-show="allNUm">
    <label>
      <input type="checkbox" v-model="flag" />
    </label>
    <span>
      <span>已完成{{ trueNum }}</span> / 全部{{ allNUm }}
    </span>
    <button class="btn btn-danger" @click="delFlagAll">清除已完成任务</button>
  </div>
</template>

<script>
export default {
  props: ["todoList"],
  methods: {
    delFlagAll() {
      //this.deleteFlagAll();
      this.$emit("deleteFlagAll");
    },
  },
  computed: {
    allNUm() {
      return this.todoList.length;
    },
    trueNum() {
      /* reduce 根据数组的长度 执行几次
      returnValue 每次的返回值
      todo 每次的实例
      0 初始值 */
      return this.todoList.reduce(
        (returnValue, todo) => returnValue + (todo.flag ? 1 : 0),
        0
      );
    },
    flag: {
      get() {
        //如果 已完成的数量 === 全部数量 默认全选
        return this.trueNum === this.allNUm && this.trueNum > 0;
      },
      set(value) {
        //value获取当前多选框的状态  勾选 true 不勾选 false
        //this.updateFlagAll(value);
        this.$emit("updateFlagAll", value);
      },
    },
  },
};
</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>

TodoList_事件总线

main.js

//引入vue
import Vue from "vue";
//引入组件App
import App from "./App.vue";

//关闭vue的生产提示
Vue.config.productionTip = false;

//创建vm
new Vue({
  el: "#app",
  render: (h) => h(App),
  beforeCreate() {
    Vue.prototype.$bus = this; //设置全局事件总线
  },
});

App.vue

<template>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <todo-head @addTodo="addTodo" />
        <todo-list :todoList="todoList" />
        <todo-fotter
          :todoList="todoList"
          @updateFlagAll="updateFlagAll"
          @deleteFlagAll="deleteFlagAll"
        />
      </div>
    </div>
  </div>
</template>

<script>
import TodoList from "./components/todo-list.vue";
import TodoHead from "./components/todo-head.vue";
import TodoFotter from "./components/todo-fotter.vue";
//引入组件

export default {
  components: { TodoHead, TodoList, TodoFotter },
  name: "App",
  data() {
    return {
      //获取存储 使用JSON把字符解析成对象
      todoList: JSON.parse(localStorage.getItem("todos")) || [],
    };
  },
  methods: {
    //添加
    addTodo(todo) {
      this.todoList.unshift(todo); //在数组最前方添加u
    },
    //删除
    deleteTodo(id) {
      //过滤出 不是要删除id的 所有值
      this.todoList = this.todoList.filter((todo) => todo.id != id);
    },
    //删除全部已选的
    deleteFlagAll() {
      this.todoList = this.todoList.filter((todo) => todo.flag === false);
    },
    //修改指定事 是否完成
    updateFlag(id) {
      this.todoList.forEach((todo) => {
        if (todo.id === id) todo.flag = !todo.flag;
      });
    },
    //修改全部的 是否完成
    updateFlagAll(flag) {
      this.todoList.forEach((todo) => {
        todo.flag = flag;
      });
    },
    //删除指定的g
  },
  mounted() {
    this.$bus.$on("deleteTodo", this.deleteTodo); //gei$bus设置自定义事件
    this.$bus.$on("updateFlag", this.updateFlag);
  },
  beforeDestroy() {
    this.$bus.$off("deleteTodo", "updateFlag");
  },
  watch: {
    todoList: {
      deep: true, //开启深度监视
      handler(value) {
        //这里获取的 value 是todoList 被修改后的值
        localStorage.setItem("todos", JSON.stringify(value)); //把值转为json格式字符串
      },
    },
  },
};
</script>

<style>
/*base*/
body {
  background: plum;
}
.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;
}

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

todo-list.vue

<template>
  <ul class="todo-main">
    <todo-item v-for="todo in todoList" :key="todo.id" :todo="todo" />
  </ul>
</template>

props: ["todoList"],

todo-item.vue

<template>
  <li>
    <label>
      <!-- change:元素发生修改时调用 -->
      <input type="checkbox" :checked="todo.flag" @change="upd(todo.id)" />
      <span>{{ todo.name }}</span>
    </label>
    <button class="btn btn-danger" @click="del(todo.id)">删除</button>
  </li>
</template>

<script>
export default {
  props: ["todo"],
  methods: {
    del(id) {
      //this.deleteTodo(id);
      if (confirm("确认删除")) {
        this.$bus.$emit("deleteTodo", id);
      }
    },
    upd(id) {
      //this.updateFlag(id);
      this.$bus.$emit("updateFlag", id);
    },
  },
};
</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;
}
li:hover button {
  display: block;
}
</style>

TodoList_消息订阅于发布

       下载包

        全装 pubsub.js(消息发布与订阅库) 

 npm i pubsub-js

引入包 

//引入订阅消息与发布库
import pubsub from "pubsub-js";

App.vue

  mounted() {
    this.pubId = pubsub.subscribe("deleteTodo", this.deleteTodo); //设置消息订阅
    this.$bus.$on("updateFlag", this.updateFlag);
  },

todo-item.vue

    del(id) {
      if (confirm("确认删除")) {
        //this.deleteTodo(id);
        //this.$bus.$emit("deleteTodo", id);
        pubsub.publish("deleteTodo", id); //发送消息
      }
    },

TodoList_nextTick

todo-item.vue

<template>
  <li>
    <label>
      <!-- change:元素发生修改时调用 -->
      <input type="checkbox" :checked="todo.flag" @change="upd(todo.id)" />
      <span v-show="!todo.isEdit">{{ todo.name }}</span>
      <input
        type="text"
        :value="todo.name"
        v-show="todo.isEdit"
        @blur="nameBlur($event, todo)"
        ref="inputFocus"
      />
    </label>
    <button class="btn btn-danger" @click="del(todo.id)">删除</button>
    <button v-show="!todo.isEdit" class="btn btn-edit" @click="updName(todo)">
      编辑
    </button>
  </li>
</template>

<script>
//引入订阅消息与发布库
import pubsub from "pubsub-js";
export default {
  props: ["todo"],
  methods: {
    del(id) {
      //this.deleteTodo(id);
      if (confirm("确认删除")) {
        //this.$bus.$emit("deleteTodo", id);
        pubsub.publish("deleteTodo", id);
      }
    },
    upd(id) {
      //this.updateFlag(id);
      this.$bus.$emit("updateFlag", id);
    },
    updName(todo) {
      if ("isEdit" in todo) {
        //判断该对象 有没有 isEdit属性
        todo.isEdit = true;
      } else {
        console.log("@");
        this.$set(todo, "isEdit", true);
      }
      // $nextTick() 会在模板解析下一次执行里面的函数
      this.$nextTick(() => this.$refs.inputFocus.focus());
    },
    nameBlur(event, todo) {
      todo.isEdit = false;
      if (!event.target.value.trim()) return alert("不能为空");
      //字符  有值的时候 返回的是 true 没有返回 false
      this.$bus.$emit("updateName", todo.id, event.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: pink;
}
li:hover button {
  display: block;
}
</style>

TodoList_动画

todo-list.vue

<template>
  <!-- 普通的动画 -->
  <!-- <ul class="todo-main">
    <transition-group name="todo" appear>
      <todo-item v-for="todo in todoList" :key="todo.id" :todo="todo" />
    </transition-group>
  </ul> -->
  <!-- 第三方库动画 -->
  <ul class="todo-main">
    <transition-group
      name="animate__animated animate__bounce"
      appear
      enter-active-class="animate__bounce"
      leave-active-class="animate__backOutDown"
    >
      <todo-item v-for="todo in todoList" :key="todo.id" :todo="todo" />
    </transition-group>
  </ul>
</template>

<script>
//引入
import "animate.css";
import todoItem from "./todo-item.vue";
export default {
  components: { todoItem },
  props: ["todoList"],
};
</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;
}

/* 普通的动画 */
/* .todo-enter-active {
  animation: goto 1s linear;
}
.todo-leave-active {
  animation: goto 1s linear reverse;
}

@keyframes goto {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(0%);
  }
} */
</style>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

XinZeBig

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值