实现效果
代码展示
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>