Vue项目实战——实现一个任务清单(学以致用,两小时带你巩固和强化Vue知识点)

news2025/7/9 11:18:53

Vue2.x 项目实战(一)

内容参考链接
Vue2.x全家桶Vue2.x 全家桶参考链接
Vue2.x项目(一)Vue2.x 实现一个任务清单
Vue2.x项目(二)Vue2.x 实现GitHub搜索案例
Vue3.x项目(三)Vue3.x 实现一个任务清单

文章目录

  • Vue2.x 项目实战(一)
    • Vue2.x 实现 todoList
      • 1、前言
      • 2、项目演示(一睹为快)
      • 3、涉及知识点
      • 4、项目详情(附源码及解析)
      • 5、写在最后的话


Vue2.x 实现 todoList

1、前言

如果你对 vue 的基础知识还很陌生,推荐先去学习一下 vue 基础

  • 如果你 刚学完 vue 基础知识,想检查一下自己的学习成果
  • 如果你 已学完 vue 基础知识,想快速回顾复习
  • 如果你 已精通 vue 基础知识,想做个小案例
  • 那不妨看完这篇文章,我保证你一定会有收获的!

2、项目演示(一睹为快)

todoList 项目演示

在这里插入图片描述

3、涉及知识点

  • Vue基础:插值语法,常用指令,键盘事件,列表渲染,计算属性,事件监听,生命周期
  • Vue进阶:props(父传子),自定义事件(任意组件间通信),自定义事件的解绑,$nextTick 异步
  • 本地存储:任务记录保留在当前浏览器中,长期有效(不手动销毁则一直保留)
  • 第三方库:nonoid(下载导入即可使用)

备注:

  1. 任意组件间的通信方式有很多种(全局事件总线,消息订阅预发布…),熟练掌握一种即可(推荐自定义事件,配置简单,容易理解)
  2. 本文是 vue 基础的练习项目,不涉及 vue 周边(Vuex,Vue-router)

4、项目详情(附源码及解析)

该项目有 五个组件 构成:

(1)App.vue 父组件,以上四个子组件 最终归并的地方,并实现很多功能相关方法

(2)MyHeader.vue 子组件:头部,用于用户文本框 输入添加任务事项

(3)MyList.vue 子组件:躯干,用于 呈现任务的列表

(4)MyItem.vue 子中子组件,Mylist.vue 的子组件,用于 呈现每个任务及编辑删除

(5)MyFooter 子组件,用于 显示所选个数和总个数及删除已完成任务

App.vue 父组件

  • 所有子组件的汇集点
  • 里面定义里很多方法,通过 props 父传子,供子组件们去使用
  • 当然也有自定义事件,供子给父传值,进行页面的渲染更新
<template>
  <!-- 最外层容器 -->
  <div class="todo-container">
    <div class="todo-wrap">
      <!-- 头部子组件,子传父,自定义 addTodo事件,添加一个 todo对象 -->
      <MyHeader @addTodo="addTodo" />
      <!-- 任务列表子组件,父传子,动态绑定对应事件 -->
      <MyList :updateTodo="updateTodo" :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo" />
      <!-- 底部子组件,子传父,全选和全清除 -->
      <MyFooter
        :todos="todos"
        @checkAllTodo="checkAllTodo"
        @clearAllTodo="clearAllTodo"
      />
    </div>
  </div>
</template>

<script>
// 引入所需组件
import MyHeader from "./components/MyHeader.vue";
import MyList from "./components/MyList.vue";
import MyFooter from "./components/MyFooter.vue";

export default {
  name: "App",
  components: { MyHeader, MyList, MyFooter },
  data() {
    return {
      // 由于 todos 是 MyHeader 组件 和 MyFooter 组件都在用,所以放在APP中(状态提升)
      // 解析 JSON字符串 第一次使用时 null 身上没有 length 属性会报错,所以添加||,前面不能用时,置为空数组
      // localStorage.getItem("xxx") 用于从本地存储中读取 todos
      todos: JSON.parse(localStorage.getItem("todos")) || [],
    };
  },
  methods: {
    // 添加一个 todo
    addTodo(todoObj) {
      this.todos.unshift(todoObj);
    },
    // 勾选 or 取消勾选一个todo
    checkTodo(id) {
      this.todos.forEach((todo) => {
        if (todo.id === id) todo.done = !todo.done;
      });
    },
    // 更新一个 todo
    updateTodo(id, title) {
      this.todos.forEach((todo) => {
        if (todo.id === id) todo.title = title;
      });
      
    },
    // 删除,todo.id !== id 就不会 push 该 todo,即删除
    deleteTodo(id) {
      this.todos = this.todos.filter((todo) => todo.id !== id);
    },
    // 全选 or 取消全选
    checkAllTodo(done) {
      this.todos.forEach((todo) => {
        todo.done = done;
      });
    },
    // 清除所有已经完成的todo
    clearAllTodo() {
      this.todos = this.todos.filter((todo) => {
        return !todo.done;
      });
    },
  },
  watch: {
    todos: {
      // 深度监视 检测到是否被勾选
      deep: true,
      handler(value) {
        // localStorage.setItem("xxx") 用来添加 todo
        // 格式化为 JSON 字符串
        localStorage.setItem("todos", JSON.stringify(value));
      },
    },
  },
  // 销毁前进行自定义事件的解绑
  beforeDestroy() {
    this.$off(['addTodo', 'checkAllTodo', 'clearAllTodo'])
  }
};
</script>

<style>
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 {
  margin-right: 5px;
  background-color: skyblue;
  border: 1px solid rgb(102, 158, 180);
}

.btn-danger:hover {
  color: #fff;
  background-color: #bd362f;
}

.btn:focus {
  outline: none;
}

.todo-container {
  width: 600px;
  margin: 10px auto;
}

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

MyHeader.vue 组件

  • 终端键入 npm i nanoid,安装 nanoid
  • <style> 标签里的 scoped,表示里面定义的样式 仅在当前组件中生效
<template>
  <div class="todo-header">
    <!-- 双向数据绑定 title,绑定键盘 enter 键,点击触发 add 事件,添加 title -->
    <input
      type="text"
      placeholder="请输入你的任务名称,按回车键确认"
      v-model="title"
      @keyup.enter="add"
    />
  </div>
</template>

<script>
import { nanoid } from "nanoid";
export default {
  name: "MyHeader",
  data() {
    return {
      // 要输入的任务事项
      title: "",
    };
  },
  methods: {
    add() {
      // 校验数据
      if (!this.title.trim()) return alert("输入不能为空");
      // 将用户的输入包装成为一个 todo 对象,nanoid() 是随机生成的唯一值,默认为未完成事件
      const todoObj = { id: nanoid(), title: this.title, done: false };
      // 通知 App 组件去添加一个 todo 对象
      this.$emit("addTodo", todoObj);
      // 清空输入
      this.title = "";
    },
  },
};
</script>

<style scoped>
.todo-header input {
  width: 578px;
  height: 28px;
  font-size: 14px;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 4px 7px;
  margin-bottom: 10px;
}

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

MyList.vue 组件

  • 该组件即为 ul 标签包裹着 MyItem.vue 组件的果皮
  • 真正的果肉在 MyItem.vue 组件里面~~
<template>
  <ul class="todo-main">
    <!-- :todo,动态绑定,供 MyItem.vue 使用 -->
    <!-- 自定义 updateTodo 事件,子传父,供子组件编辑更新数据 -->
    <MyItem
      v-for="todoObj in todos"
      :key="todoObj.id"
      :todo="todoObj"
      :checkTodo="checkTodo"
      :deleteTodo="deleteTodo"
      @updateTodo="updateTodo"
    />
  </ul>
</template>

<script>
import MyItem from "./MyItem.vue";
export default {
  name: "MyList",
  components: { MyItem },
  props: ["todos", "checkTodo", "deleteTodo", "updateTodo"],
};
</script>

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

MyItem.vue 组件

  • 获取焦点的时候要用 $nextTick (等 DOM 节点更新后执行),或者用 setTimeout 异步包裹也能达到同样的效果
  • Vue2.x 不能监测对象属性的添加或删除。因为 Vue.js 在 初始化实例时 将属性转为 getter/setter,所以属性必须在 data 对象上才能让 Vue2.x 转换它,才能让它是响应的。
  • 所以,当我们想要在 data 中或者 data 中的对象添加新的属性时,我们需要使用 Vue.set()vm.$set(),否则是无法触发视图更新的。
<template>
  <li>
    <label>
      <!-- 复选框,:checked 单向绑定 todo 是否已完成,@change 检测复选框的变化 -->
      <input
        type="checkbox"
        :checked="todo.done"
        @change="handleCheck(todo.id)"
      />
      <!-- 非编辑状态下,在 sapn 标签中展示 todo -->
      <span v-show="!todo.isEdit">{{ todo.title }}</span>
      <!-- 绑定失去焦点事件,更新内容。ref 打标识,用于自动获取焦点 -->
      <input
        type="text"
        style="height: 22px"
        v-show="todo.isEdit"
        :value="todo.title"
        @blur="handleBlur(todo, $event)"
        ref="inputTitle"
      />
    </label>
    <!-- 删除 todo -->
    <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
    <!-- 编辑状态下,展示输入框,隐藏编辑按钮。 -->
    <button v-show="!todo.isEdit" class="btn btn-edit" @click="handleEdit(todo)">编辑</button>
  </li>
</template>

<script>
export default {
  name: "MyItem",
  // 声明接收 todo 对象,checkTodo 是否勾选,deleteTodo 删除该 todo
  props: ["todo", "checkTodo", "deleteTodo"],
  methods: {
    // 勾选 or 取消勾选
    handleCheck(id) {
      // 通知 APP 组件 将对应的 todo 对象的 done 值取反
      this.checkTodo(id);
    },
    // 删除 todo
    handleDelete(id) {
      if (confirm("确定删除当前任务吗?")) {
        this.deleteTodo(id);
      }
    },
    // 编辑
    handleEdit(todo) {
      // 如果 todo 身上有 isEdit,则直接修改 isEdit,否则再给 todo 添加新的 isEdit
      // Reflect.has(todo, 'isEdit') 或 todo.hasOwnProperty.call(todo, "isEdit")  
      if (Reflect.has(todo, 'isEdit')) {
        todo.isEdit = true;
      } else {
        this.$set(todo, "isEdit", true);
      }
      // DOM 节点更新后执行   
      this.$nextTick(() => {
        this.$refs.inputTitle.focus()
      })
    },
    // 失去焦点,编辑框隐藏,并判断编辑后的内容是否为空,再呈现编辑后的内容
    handleBlur(todo, e) {
      todo.isEdit = false;
      if(!e.target.value.trim()) return alert('输入内容不能为空!')
      this.$emit('updateTodo', todo.id, e.target.value)
    },
  },
};
</script>

<style scoped>
span {
  color: orange;
}

li {
  list-style: none;
  height: 36px;
  line-height: 36px;
  padding: 0 5px;
  border-bottom: 1px solid #ddd;
}

li label {
  cursor: pointer;
}

input {
  margin-right: 5px;
}

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>

MyFooter.vue 组件

  • 底部的展示,当没有任务时隐藏该组件
  • reduce() 是一个高阶函数,接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值 参考链接
<template>
  <!-- total 不为 0 则显示底部,否则隐藏 -->
  <div class="todo-footer" v-show="total">
    <label>
      <!-- 是否全选,双向绑定 isAll -->
      <input type="checkbox" v-model="isAll" />
    </label>
    <!-- 插值语法呈现数值 -->
    <span class="done">已完成 {{ doneTotal }}</span> /
    <span class="total">全部 {{ total }}</span>
    <button class="btn btn-danger" @click="clearAll()">清除已完成任务</button>
  </div>
</template>

<script>
export default {
  name: "MyFooter",
  props: ["todos"],
  computed: {
    // 返回 todos 的总长度
    total() {
      return this.todos.length;
    },
    // 统计任务已经完成的个数
    doneTotal() {
      // reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值
      // pre 必需:初始值;todo 必需:当前元素;0 可选:传递给函数的初始值
      return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);
    },
    // 是否全选,当被选个数和总个数相同,且总个数大于 0 时,checked 选中
    isAll: {
      get() {
        return this.doneTotal === this.total && this.total > 0;
      },
      set(value) {
        this.$emit("checkAllTodo", value);
      },
    },
  },
  methods: {
    // 清除所有已完成任务
    clearAll() {
      this.$emit("clearAllTodo");
    },
  },
};
</script>

<style scoped>
.done {
  font-weight: bold;
  color: skyblue;
}

.total {
  font-weight: bold;
  color: palevioletred;
}

.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: -3px;
  vertical-align: middle;
  margin-right: -10px;
}

.todo-footer button {
  float: right;
  margin-top: 5px;
}
</style>

5、写在最后的话

如果你是 看完全篇 阅读到了这里,我相信你一定是有收获的!

那么下面不妨打开自己的电脑,启动自己的编译器,来跟着做 / 自己做一遍吧!

好吧,我骗了你,真正学会它可能不止两个小时,但再多花点时间,你对 vue 的理解可能会有质的提升,加油~

如果这篇文章对你有些许帮助的话,不妨 三连 + 关注 支持一下~~

下一篇是 github 的搜索 demo,也是使用的 vue2.x 实现的,一起期待一下吧~


在这里插入图片描述


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/406586.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vue 高德地图(@amap/amap-jsapi-loader)的基本使用:添加标记、POI关键字搜索、路线规划...(方法一)

高德地图的基本事件与使用前言&#xff1a; 引入并初始化渲染地图1、初始化地图2、地图鼠标点击事件3、添加标记、 移除标记点4、搜索服务——POI关键字搜索 [AMap.PlaceSearch]5、驾车路线规划服务5.1 可拖拽驾车路线规划 [AMap.DragRoute]5.2 途经点 &#xff08;起点 终点 途…

在vue3+ts项目里使用query和params传参

一 query 传参 &#xff08;类似get请求&#xff09; query 传参方式① 传递方组件 home.vue <template><div classc><p>query传参</p><el-button type"success" click"toList"> to list</el-button> </div>…

LayUI框架的使用步骤实现登录页面

目录 一、LayUI的简介 二、下载安装 三、引入并且测试 四、自定义模块 四、利用LayUI实现一个登录页面 一、LayUI的简介 1.1 什么是LayUI&#xff1f; Layui&#xff08;谐音&#xff1a;类 UI) 是一套开源的 Web UI 解决方案&#xff1b; 由国人开发&#xff08;作者贤心…

Python人脸识别

#头文件&#xff1a;import cv2 as cvimport numpy as npimport osfrom PIL import Imageimport xlsxwriterimport psutilimport time#人脸录入def get_image_name(name):name_map {f.split(.)[1]:int(f.split(.)[0]) for f in os.listdir("./picture")}if not name…

宇宙最强-GPT-4 横空出世:最先进、更安全、更有用

文章目录前言一、准确性提升1.创造力2.视觉输入3.更长的上下文二、相比于ChatGPT有哪些提升1.GPT-4 的高级推理能力超越了 ChatGPT2.GPT-4 在多种测试考试中均优于 ChatGPT。三、研究团队在GPT-4模型都做了哪些改善1.遵循 GPT、GPT-2 和 GPT-3 的研究路径2.我们花了 6 个月的时…

2022年Web前端开发流程和学习路线(详尽版)

前言 前端侧重于人机交互和用户体验&#xff0c;后端侧重于业务逻辑和大规模数据处理。理论上&#xff0c;面向用户的产品里&#xff0c;所有问题&#xff08;包括产品、设计、后端、甚至看不见的问题&#xff09;的表现形式&#xff0c;都会暴露在前端&#xff0c;而只有部分…

JS防抖和节流

前言 在进行窗口的操作或者输入框操作时&#xff0c;如果事件处理函数用的频率无限制&#xff0c;会加重浏览器和服务器的负担&#xff0c;此时我们就可以用防抖&#xff08;debounce&#xff09;和节流&#xff08;throttle&#xff09;的方式来减少调用频率&#xff0c;同时…

简析强制缓存和协商缓存

零、目录 背景介绍 http 缓存机制 使用小结 一、 背景介绍 浏览器和服务器进行交互的过程&#xff0c; 时间开销的瓶颈往往出现在数据的传输的过程之中。 这个场景类似介于 A城 到 B城 之间只有一座 “通道” &#xff0c; 每次想从A城 到 B城 &#xff0c;必须按照人数交付高…

Maven使用教程(IDEA版)

目录 一、Maven简介 1.1 在项目中如何导入jar包&#xff1f; 1.2 传统导入jar包的方式存在什么问题&#xff1f; 1.3 项目生命周期 1.4 Maven简介 二、Maven安装及配置 2.1 Maven下载 2.2 Maven安装 2.3 配置环境变量 三、Maven的项目结构 3.1 Maven的项目结构 3.2…

【CSS】CSS 特性 ③ ( CSS 优先级 | 优先级引入 | 选择器基本权重 )

文章目录一、CSS 优先级1、优先级引入2、选择器基本权重3、完整代码示例一、CSS 优先级 1、优先级引入 定义 CSS 样式时 , 可能出现 多个 类型相同的 规则 定义在 同一个元素上 , 如果 CSS 选择器 相同 , 执行 CSS 层叠性 , 根据 就近原则 选择执行的样式 , 如 : 出现两个 div…

VueX使用

vuex基本概念 vuex官方文档 vuex是vue的状态管理工具&#xff0c;状态即数据。 状态管理就是集中管理vue中 通用的 一些数据 注意&#xff08;官方原文&#xff09;&#xff1a; 不是所有的场景都适用于vuex&#xff0c;只有在必要的时候才使用vuex 使用了vuex之后&#xf…

手机解锁方法:8个顶级的 Android 手机解锁软件

一般来说&#xff0c;太简单的密码是不安全的&#xff0c;所以我们设置一个安全的密码&#xff0c;可能会稍微复杂一点。然而&#xff0c;我们可能经常会忘记复杂的密码并锁定我们的 Android 智能手机。 8个顶级的 Android 手机解锁软件 如果您遇到过这种情况并且正在寻找一种…

【vue2】vue全家桶介绍,学习vue必备

​ &#x1f973;博 主&#xff1a;初映CY的前说(前端领域) &#x1f31e;个人信条&#xff1a;想要变成得到&#xff0c;中间还有做到&#xff01; &#x1f918;本文核心&#xff1a;vue全家桶介绍&#xff0c;学习vue必备&#xff01;&#xff01;&#xff01; 【前言…

Vue开发实例(11)之el-menu实现左侧菜单导航

作者简介 作者名&#xff1a;编程界明世隐 简介&#xff1a;CSDN博客专家&#xff0c;从事软件开发多年&#xff0c;精通Java、JavaScript&#xff0c;博主也是从零开始一步步把学习成长、深知学习和积累的重要性&#xff0c;喜欢跟广大ADC一起打野升级&#xff0c;欢迎您关注&…

【Vue从入门到进阶】Node.js安装与配置

✅作者简介&#xff1a;CSDN一位小博主&#xff0c;正在学习前端&#xff0c;欢迎大家一起来交流学习&#x1f3c6; &#x1f4c3;个人主页&#xff1a;白月光777的CSDN博客 &#x1f525;系列专栏&#xff1a;Vue从入门到进阶 &#x1f4ac;个人格言&#xff1a;但行好事&…

OpenAI 发布GPT-4——全网抢先体验

OpenAI 发布GPT-4 最近 OpenAI 犹如开挂一般&#xff0c;上周才刚刚推出GPT-3.5-Turbo API&#xff0c;今天凌晨再次祭出GPT-4这个目前最先进的多模态预训练大模型。与上一代GPT3.5相比&#xff0c;GPT-4最大的飞跃是增加了识图能力&#xff0c;并且回答准确性也得到显著提高。…

使用SpringBoot一小时快速搭建一个简单后台管理(后端篇)

不好意思让大家久等啦&#xff0c;最近也是因为学期末了&#xff0c;事情多了一点&#xff0c;所以更新的比较慢&#xff0c;请大家谅解下~ 好了话不多说&#xff0c;进入今天的教程环节 本次案例一共两篇文章教学&#xff1a; &#xff08;第一篇&#xff09;&#xff1a;数据…

聊聊vue3的defineProps、defineEmits、defineExpose

最近在开发中用到了vue3的defineProps、defineEmits和defineExpose&#xff0c;感觉发现新大陆一般&#xff0c;所以利用闲碎时间对这三个方法做个总结。 defineProps const props defineProps<{foo: String,bar?: Number }>()defineProps 是vue3的写法并且是一个仅 …

Video.js 使用教程 - 手把手教你基于 Vue 搭建 HTML 5 视频播放器

本文首发&#xff1a;《Video.js 使用教程 - 手把手教你基于 Vue 搭建 HTML 5 视频播放器》 Video.js 是最强大的网页嵌入式 HTML 5 视频播放器的组件库之一&#xff0c;也是大多数人首选的网页视频播放解决方案。复杂的网页视频渲染&#xff0c;在引入 Video.js 后&#xff0…

idea连接SQL Server数据库

数据库配置 1.安装数据库&#xff1a;自行安装 我的SQL Server版本为2019 2.登录数据库&#xff0c;登陆方式有两种&#xff0c;连接数据库选择SQLServer身份验证 1.windows登录&#xff0c;直接登录 2.SQLServer登录&#xff0c;需要输入用户名和密码&#xff0c;默…