32-Vue脚手架-Todo-list 案例
Todo-list 案例
组件化编码流程(通用)
1.拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突,如下所示
2.实现动态组件:要考虑好数据的存放位置,数据是要一个组件在用,还是一些组件在用
- 一个组件在用:放在组件自身即可
- 一些组件在用:放在他们共同的父组件上
3.实现交互:从绑定事件开始
props 适用于
1)父组件 ==> 子组件通信
2)子组件 ==> 负责见通信(要求父组件先给子组件一个函数)
使用 v-model 要切记:v-model 绑定的值不能是 props 传递过来的值,因为 props是不可以修改的。
props 传过来的若是对象类型的值,修改对象中的属性时,Vue 不会报错,但不推荐这样做
扩展:这里介绍一个ID生成器库,NanoID
安装命令:npm i nanoid
// 导入nanoid库
import { nanoid } from 'nanoid';
案例:Todo-list 实现
相关代码源码:
链接:https://pan.baidu.com/s/1YT9ZhvQEu_BGBs0WBMa-Eg
提取码:46p1
脚手架文件结构
├── node_modules
├── public
│ ├── favicon.ico: 页签图标
│ └── index.html: 主页面
├── src
│ ├── assets: 存放静态资源
│ │ └── logo.png
│ │── component: 存放组件
│ │ └── UserHeader.vue
│ │ └── UserList.vue
│ │ └── UserItem.vue
│ │ └── UserFooter.vue
│ │── App.vue: 汇总所有组件
│ │── main.js: 入口文件
├── .gitignore: git版本管制忽略的配置
├── babel.config.js: babel的配置文件
├── package.json: 应用包配置文件
├── README.md: 应用描述文件
├── package-lock.json:包版本控制文件
src/components/UserHeader.vue
<template> <div class="todo-header"> <input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add" /> </div> </template> <script> // ID 生成器库 import { nanoid } from "nanoid"; export default { name: "UserHeader", data() { return { title: "", }; }, // 接收来自App的addTodo方法 props: ["addTodo"], methods: { add() { // 校验数据 if (!this.title.trim()) return alert("输入不能为空"); // 将用户的输入包装成一个对象 const todoObj = { id: nanoid(), title: this.title, done: false }; console.log(todoObj) // 调用来自App的addTodo方法,通知App组件去添加一个对象 this.addTodo(todoObj); // 清空输入 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>
src/components/UserList.vue
<template> <ul class="todo-main"> <!--根据todos数据,进行遍历--> <UserItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj" :checkTodo="checkTodo" :deleteTodo="deleteTodo" ></UserItem> </ul> </template> <script> import UserItem from "./UserItem.vue"; export default { name: "UserList", components: { UserItem, }, // 接收来自App的数据 props: ["todos", "checkTodo",'deleteTodo'], }; </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>
src/components/UserItem.vue
<template> <li> <label> <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/> <!--如下代码也能实现功能,但不推荐,违反修改props的原则--> <!--<input type="checkbox" v-model="todo.done" @change="handleCheck(todo.id)"/>--> <span>{{todo.title}}</span> </label> <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button> </li> </template> <script> export default { name: "UserItem", //声明接收对象 props: ["todo", "checkTodo", "deleteTodo"], // mounted() { // console.log(this.todo) // } methods: { //勾选or取消勾选 handleCheck(id) { console.log("勾选或取消勾选,id:",id) this.checkTodo(id); }, //删除 handleDelete(id) { if (confirm("确定删除吗")) { console.log("删除,id:",id) this.deleteTodo(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: #ddd; } li:hover button { display: block; } </style>
src/components/UserFooter.vue
<template> <!-- v-show="total" 当total为0时,返回false,页面不显示,当total为非0时,返回true,页面显示--> <div class="todo-footer" v-show="total"> <label> <!--<input type="checkbox" :checked="isAll" @change="checkAll" />--> <!--为什么可以用v-model,这是因为绑定的是计算属性isAll,没有修改props的值--> <input type="checkbox" v-model="isAll" @change="checkAll" /> </label> <span> <span>已完成{{ doneTotal }}</span> / 全部{{ total }} </span> <button class="btn btn-danger" @click="clearAll">清除已完成任务</button> </div> </template> <script> // import { computed } from "vue"; export default { name: "UserFooter", props: ["todos", "checkAllTodo", "clearAllTodo"], methods: { checkAll(e) { console.log("@checkAll",e.target.checked) this.checkAllTodo(e.target.checked); }, clearAll() { this.clearAllTodo(); }, }, computed: { total() { return this.todos.length; }, doneTotal() { // 1.遍历列表的方法 // let i = 0 // this.todos.forEach((todo)=>{ // if(todo.done === true) i++ // }); // return i // 2.reduce()方法,对数组中的每个元素按序执行一个提供的 reducer 函数,将其结果汇总为单个返回值 // const x = this.todos.reduce((pre, current) => { // console.log("@",pre) // return pre + (current.done ? 1 : 0); // }, 0); // return x; return this.todos.reduce( (pre, current) => pre + (current.done ? 1 : 0), 0 ); }, // isAll() { // // 判断已完成 / 全部 前面的复选框够不够,取决于已完成的个数是否等于全部的个数 // return this.doneTotal === this.total && this.total > 0; // }, // 计算属性 isAll 的完整写法 isAll:{ get(){ console.log("@isAll 的 get",this.doneTotal === this.total && this.total > 0) return this.doneTotal === this.total && this.total > 0; }, set(value){ console.log("@isAll 的 set",value) this.checkAllTodo(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>
src/App.vue
<template> <div id="root"> <div class="todo-container"> <div class="todo-wrap"> <!--动态绑定函数,在父组件定义一个函数,子组件调用该函数,父组件就可以收到子组件传过来的参数 :addTodo="addTodo"--> <UserHeader :addTodo="addTodo"></UserHeader> <UserList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo" ></UserList> <UserFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"></UserFooter> </div> </div> </div> </template> <script> import UserHeader from "./components/UserHeader.vue"; import UserList from "./components/UserList.vue"; import UserFooter from "./components/UserFooter.vue"; export default { name: "App", components: { UserHeader, UserList, UserFooter, }, data() { return { todos: [ { id: "001", title: "起床", done: true }, { id: "002", title: "洗漱", done: false }, { id: "003", title: "睡觉", done: true }, ], }; }, methods: { // 添加一个todo addTodo(todoObj) { console.log("我是APP组件,我收到了数据:",todoObj) // unshift() 在数组头部添加元素 this.todos.unshift(todoObj); }, // 勾选or取消勾选一个todo checkTodo(id) { // forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数 this.todos.forEach((todo) => { // 将tudo的done属性进行取反 if (todo.id === id) todo.done = !todo.done; }); }, // 删除一个 deleteTodo(id) { // 数组.filter() 实现数组的过滤,创建一个新数组, 其包含通过所提供函数实现的测试的所有元素 // 过滤出,todo.id 不是 id 的数据 this.todos = this.todos.filter((todo) => todo.id !== id); }, // 全选or全不选 checkAllTodo(done) { // forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数 this.todos.forEach((todo) => { todo.done = done; }); }, //清除所有已经完成的任务 clearAllTodo() { if(confirm("确认删除所有已完成的任务吗")){ // 数组.filter() 实现数组的过滤,创建一个新数组, 其包含通过所提供函数实现的测试的所有元素 // 过滤出,todo.done 还没完成 的数据 // this.todos = this.todos.filter((todo) => todo.done == false); this.todos = this.todos.filter((todo) => !todo.done); } }, }, }; </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-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>
src/main.js
import Vue from "vue"
import App from "./App.vue"
// 阻止 vue 在启动时生成生产提示
Vue.config.productionTip = false
new Vue({
el:"#app",
render:h => h(App)
})
public/index.html
<!DOCTYPE html> <html lang=""> <head> <meta charset="utf-8"> <!-- 针对IE浏览器的一个特殊配置,含义是仍IE浏览器以最高的渲染级别渲染页面 --> <!-- Vue不支持IE8及以下的版本,因为Vue使用了IE8无法模拟的ECMAScript 5特性。但它支持所有兼容ECMAScript 5的浏览器 --> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <!-- 开启移动端的理想视口 --> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <!-- 配置页签的图标 --> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <!-- 配置网页的标题 --> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <!--当浏览器不支持js时,noscript中的元素就会被渲染--> <noscript> <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> <!-- built files will be auto injected --> </body> </html>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix