# MyHeader
- components/MyHeader
<template> | |
<div class="todo-header"> | |
<input | |
type="text" | |
placeholder="请输入你的任务名称,按回车键确认" | |
v-model="title" | |
@keyup.enter="add" | |
/> | |
</div> | |
</template> | |
<script> | |
import { nanoid } from "nanoid"; | |
export default { | |
name: "MyHeader", | |
data() { | |
return { | |
// 收集用户输入的 title | |
title: "", | |
}; | |
}, | |
methods: { | |
add() { | |
// 校验数据 | |
if (!this.title.trim()) return alert("输入不能为空"); | |
// 将用户的输入包装成一个 todo 对象 | |
const todo = { id: nanoid(), title: this.title, done: false }; | |
// 通知 App 组件去添加一个 todo 对象 | |
this.$emit("addTodo", 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> |
# MyList
<template> | |
<ul class="todo-main"> | |
<transition-group name="todo" appear> | |
<MyItem v-for="todo in todos" :key="todo.id" :todo="todo" /> | |
</transition-group> | |
</ul> | |
</template> | |
<script> | |
import MyItem from "./MyItem"; | |
export default { | |
name: "MyList", | |
components: { MyItem }, | |
// 声明接收 App 传递过来的数据 | |
props: ["todos"], | |
}; | |
</script> | |
<style scoped> | |
/*main*/ | |
.todo-main { | |
margin-left: 0px; | |
border: 1px solid #ddd; | |
border-radius: 2px; | |
padding: 0px; | |
overflow: hidden; | |
} | |
.todo-empty { | |
height: 40px; | |
line-height: 40px; | |
border: 1px solid #ddd; | |
border-radius: 2px; | |
padding-left: 5px; | |
margin-top: 10px; | |
} | |
.todo-enter-active { | |
animation: td 0.5s linear; | |
} | |
.todo-leave-active { | |
animation: td 0.5s linear reverse; | |
} | |
@keyframes td { | |
from { | |
transform: translateX(100%); | |
} | |
to { | |
transform: translateX(0); | |
} | |
} | |
</style> |
# MyItem
- components/MyItem
<template> | |
<li> | |
<label> | |
<input | |
type="checkbox" | |
:checked="todo.done" | |
@change="handleCheck(todo.id)" | |
/> | |
<!-- 如下代码也能实现功能,但是不太推荐,因为有点违反原则,因为修改了props --> | |
<!-- <input type="checkbox" v-model="todo.done"/> --> | |
<span v-show="!todo.isEdit"><!--swig0--></span> | |
<input | |
type="text" | |
v-show="todo.isEdit" | |
:value="todo.title" | |
@blur="handleBlur(todo, $event)" | |
ref="inputTitle" | |
/> | |
</label> | |
<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 | |
props: ["todo"], | |
methods: { | |
// 勾选 or 取消勾选 | |
handleCheck(id) { | |
// 通知 App 组件将对应的 todo 对象的 done 值取反 | |
// this.checkTodo(id) | |
this.$bus.$emit("checkTodo", id); | |
}, | |
// 删除 | |
handleDelete(id) { | |
if (confirm("确定删除吗?")) { | |
// 通知 App 组件将对应的 todo 对象删除 | |
// this.deleteTodo(id) | |
this.$bus.$emit("deleteTodo", id); | |
} | |
}, | |
handleEdit(todo) { | |
if (Object.prototype.hasOwnProperty.call(todo, "isEdit")) { | |
todo.isEdit = true; | |
} else { | |
this.$set(todo, "isEdit", true); | |
} | |
this.$nextTick(function () { | |
this.$refs.inputTitle.focus(); | |
}); | |
}, | |
handleBlur(todo, e) { | |
this.$bus.$emit("updateTodo", todo.id, e.target.value); | |
todo.isEdit = false; | |
}, | |
}, | |
}; | |
</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> |
# MyFooter
- components/MyHeader
<template>
<div class="todo-footer" v-show="total">
<label>
<!-- <input type="checkbox" :checked="isAll" @change="checkAll"/> -->
<input type="checkbox" v-model="isAll" />
</label>
<span>
<span>已完成{{ doneTotal }}</span> / 全部{{ total }}
</span>
<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: "MyFooter",
props: ["todos"],
computed: {
//总数
total() {
return this.todos.length;
},
//已完成数
doneTotal() {
//此处使用reduce方法做条件统计
/* const x = this.todos.reduce((pre,current)=>{
console.log('@',pre,current)
return pre + (current.done ? 1 : 0)
},0) */
//简写
return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);
},
//控制全选框
isAll: {
//全选框是否勾选
get() {
return this.doneTotal === this.total && this.total > 0;
},
//isAll被修改时set被调用
set(value) {
// this.checkAllTodo(value)
this.$emit("checkAllTodo", value);
},
},
},
methods: {
/* checkAll(e){
this.checkAllTodo(e.target.checked)
} */
//清空所有已完成
clearAll() {
// this.clearAllTodo()
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>
# App
<template> | |
<div id="app"> | |
<div id="root"> | |
<div class="todo-container"> | |
<div class="todo-wrap"> | |
<MyHeader @addTodo="addTodo"></MyHeader> | |
<MyList :todos="todos"> </MyList> | |
<MyFooter | |
:todos="todos" | |
@checkAllTodo="checkAllTodo" | |
@clearAllTodo="clearAllTodo" | |
></MyFooter> | |
</div> | |
</div> | |
</div> | |
</div> | |
</template> | |
<script> | |
import MyHeader from "@/components/MyHeader"; | |
import MyFooter from "./components/MyFooter.vue"; | |
import MyList from "@/components/MyList"; | |
export default { | |
name: "App", | |
components: { | |
MyHeader, | |
MyFooter, | |
MyList, | |
}, | |
data() { | |
return { | |
todos: JSON.parse(localStorage.getItem("todos")) || [], | |
}; | |
}, | |
methods: { | |
addTodo(x) { | |
this.todos.unshift(x); | |
}, | |
checkTodo(id) { | |
this.todos.forEach((todo) => { | |
if (todo.id === id) todo.done = !todo.done; | |
}); | |
}, | |
deleteTodo(id) { | |
this.todos = this.todos.filter((todo) => todo.id !== id); | |
}, | |
checkAllTodo(done) { | |
console.log(done); | |
this.todos.forEach((todo) => (todo.done = done)); | |
}, | |
clearAllTodo() { | |
if (confirm("确定要清除所有已完成的吗?")) { | |
this.todos = this.todos.filter((todo) => !todo.done); | |
} | |
}, | |
updateTodo(id, title) { | |
this.todos.forEach((todo, i) => { | |
if (todo.id === id) { | |
if (!title) { | |
if (confirm("内容为空是否进行删除?")) { | |
this.todos.splice(this.todos[i], 1); | |
} | |
} | |
todo.title = title; | |
} | |
}); | |
}, | |
}, | |
watch: { | |
todos: { | |
deep: true, | |
handler(val) { | |
localStorage.setItem("todos", JSON.stringify(val)); | |
}, | |
}, | |
}, | |
mounted() { | |
this.$bus.$on("checkTodo", this.checkTodo); | |
this.$bus.$on("updateTodo", this.updateTodo); | |
this.$bus.$on("deleteTodo", this.deleteTodo); | |
}, | |
beforeDestroy() { | |
this.$bus.$off("checkTodo"); | |
this.$bus.$off("updateTodo"); | |
this.$bus.$off("deleteTodo"); | |
}, | |
}; | |
</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(103, 159, 180); | |
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> |
# main
import Vue from 'vue' | |
import App from './App.vue' | |
Vue.config.productionTip = false | |
new Vue({ | |
render: h => h(App), | |
beforeCreate() { | |
Vue.prototype.$bus = this | |
} | |
}).$mount('#app') |
# 总结 TodoList 案例
-
组件化编码流程:
(1). 拆分静态组件:组件要按照功能点拆分,命名不要与 html 元素冲突。
(2). 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
1). 一个组件在用:放在组件自身即可。
2). 一些组件在用:放在他们共同的父组件上(<span style="color:red"> 状态提升 </span>)。
(3). 实现交互:从绑定事件开始。
-
props 适用于:
(1). 父组件 ==> 子组件 通信
(2). 子组件 ==> 父组件 通信(要求父先给子一个函数)
-
使用 v-model 时要切记:v-model 绑定的值不能是 props 传过来的值,因为 props 是不可以修改的!
-
props 传过来的若是对象类型的值,修改对象中的属性时 Vue 不会报错,但不推荐这样做。