0
点赞
收藏
分享

微信扫一扫

Vue TodoList案例


文章目录

  • ​​界面完成​​
  • ​​添加功能实现​​
  • ​​勾选​​
  • ​​实现方法一​​
  • ​​实现方法二​​
  • ​​删除​​
  • ​​底部统计功能实现​​
  • ​​底部清除已完成任务​​
  • ​​总结​​

界面完成

Vue TodoList案例_数组
components 文件夹下新建 Header.vue来完成头部组件

<template>
<div class="todo-header">
<input type="text" placeholder="请输入任务名,按回车确认">
</div>
</template>

<script>
export default {
name: "Header"
}
</script>

<style scoped>
.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.8);
}

</style>

Footer.vue 来完成底部组件

<template>
<div class="todo-footer">
<label>
<input type="checkbox">
</label>
<span>
<span>已完成0</span>/全部2
</span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>

<script>
export default {
name: "Footer"
}
</script>

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

List.vue 来完成中间内容组件

<template>
<div>
<ul class="todo-main">
<Item v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj"/>
</ul>
</div>
</template>

<script>
import Item from "./Item"

export default {
name: "List",
components: {
Item
},
data(){
return{
todos:[
{id:'001',title:'抽烟',done:true},
{id:'002',title:'喝酒',done:false},
{id:'003',title:'开车',done:true},
]
}
}
}
</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>

Item.vue 来完成每一项的组件

<template>
<li>
<label>
<input type="checkbox" :checked="todo.done">
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" style="display:none">删除</button>
</li>
</template>

<script>
export default {
name: "Item",
props:['todo']
}
</script>

<style scoped>
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;
}

</style>

App.vue 中引入并注册组件,同时写一些公共样式

<template>
<div class="root">
<div class="todo-container">
<div class="todo-wrap">
<Header/>
<List/>
<Footer/>
</div>
</div>
</div>
</template>

<script>
//引入组件
import Header from "./components/Header";
import Footer from "./components/Footer";
import List from "./components/List";

export default {
name: 'App',
components: {
Header,
Footer,
List
}
}
</script>

<style>
body {
background: #ffffff;
}

.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertica1-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgb(255, 255, 255, 0.2), 0 1px 2px rgb(255, 255, 255, 0.5);
border-radius: 4px;
}

.btn-danger {
color: #ffffff;
background-color: #da4f49;
border: 1px solid #bd362f;
}

.btn-danger:hover {
color: #ffffff;
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.js

//引入Vue
import Vue from 'vue';
//引入App
import App from './App';

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

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

添加功能实现

Vue TodoList案例_vue_02

我们需要在 Header.vue 编写代码,首先给 input 中增加键盘抬起时的事件 add,当按下回车时获取输入内容,然后包装成一个对象,也就是 List.vue 中相同的数据类型​​{id:'001',title:'抽烟',done:true}​

其中 id 需要是唯一标识,可以使用 ​​npm i nanoid​​​ 来安装 nanoid,用来生成唯一 id
其中 title 就是输入的内容,通 add(e) 中的参数 e.target.value 就可以拿到
最后 done 默认 false

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

<script>
import {nanoid} from 'nanoid'
export default {
name: "Header",
methods: {
add(e) {
const todoObj = {id: nanoid(), title: e.target.value, done: false}
console.log(todoObj);
}
//或者在input中增加双向绑定v-model="title"
/*add(){
console.log(this.title);
}*/
}
}
</script>

<style scoped>
.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.8);
}

</style>

在头部输入123,然后按回车,查看打印的内容
Vue TodoList案例_功能实现_03
好了现在需要把这个对象添加给 List.vue 中的 todos 中。我们之前学过父组件向子组件中传值,但目前没有学兄弟组件之间的传值,所以,我们可以把 List.vue 中的数据 todos 剪切走放到 App.vue 中(后边学习其他方法)

App.vue 中

......
<List :todos="todos"/>
......

<script>
//引入组件
......

export default {
name: 'App',
components: {
......
},
data(){
return{
todos:[
{id:'001',title:'抽烟',done:true},
{id:'002',title:'喝酒',done:false},
{id:'003',title:'开车',done:true},
]
}
}
}
</script>

然后 List.vue 中接收一下就可以了

......
<script>
import Item from "./Item"

export default {
name: "List",
props:["todos"],
components: {
Item
}
}
</script>

接着,当 Header.vue 中组装了一个对象后,传给 App.vue,需要这样做:首先 App.vue 中需要先传一个方法 addTodo 给 Header.vue,然后 Header.vue 接收到这个方法调用即可

修改 App.vue

<Header :addTodo="addTodo"/>

<script>
......
export default {
......

methods:{
addTodo(todoObj){
this.todos.unshift(todoObj)
}
}
}
</script>

修改 Header.vue

......
<script>
......
export default {
......
props:["addTodo"],
methods: {
add(e) {
if(!e.target.value.trim()) return alert("输入不能为空")
const todoObj = {id: nanoid(), title: e.target.value, done: false}
this.addTodo(todoObj)
e.target.value = ""
}
//或者在input中增加双向绑定v-model="title"
/*add(){
console.log(this.title);
}*/
}
}
</script>
......

勾选

实现方法一

思路:勾选后需要改变数据中的 done 的值,任务列表 todos 在 App.vue 中,所以在 App.vue 中增加 checkTodo 方法。由于 App.vue - List.vue - Item.vue 是嵌套关系,我们需要逐层传递,因此 先把这个方法传给 List.vue

App.vue

<List :todos="todos" :checkTodo="checkTodo"/>

<script>
......
export default {
......
methods:{
......
//勾选或取消勾选一个todo
checkTodo(id){
this.todos.forEach((todo)=>{
if(todo.id === id) todo.done = !todo.done
})
}
}
}
</script>

List.vue 中接收并再传递给 Item.vue

<Item v-for="todoObj in todos" 
:key="todoObj.id"
:todo="todoObj"
:checkTodo="checkTodo"
/>

<script>
......

export default {
......
props:["todos","checkTodo"],
......
}
</script>
......

Item.vue 中接收调用,在 checkbox 中增加 change 事件,获取选中或取消勾选的 id

<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)">

<script>
export default {
name: "Item",
props:['todo','checkTodo'],
methods:{
handleCheck(id){
this.checkTodo(id)
}
}
}
</script>

到此为止我们就实现了勾选改变值的功能,运行看下效果:
Vue TodoList案例_功能实现_04

实现方法二

我们可以换另一种写法,​​v-model​​​对应的 data 属性是 Boolean 时,返回的是​​checkbox​​​的​​checked​​的 boolean 值,所以第一种方法中的 传方法之类的全都不写了,直接修改 Item.vue

<input type="checkbox" v-model="todo.done">

虽然同样可以实现上边的效果,但是这种方法是不建议的,因为我们之前说过,传过来的 props 是不能修改的,为什么 vue 还没报错呢?

我们看着段代码

let obj = {a:1,b:2}
obj.a = 2//第一种修改方式
obj = {x:11,b:22}//第二种修改方式

vue 能检测到的是第二种修改方式

不建议这种写法哦

删除

Vue TodoList案例_功能实现_05
先修改 App.vue,增加一个删除方法 deleteTodo,传给 List.vue

<template>
<div class="root">
<div class="todo-container">
<div class="todo-wrap">
<Header :addTodo="addTodo"/>
<List :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
<Footer/>
</div>
</div>
</div>
</template>

<script>
......

export default {
......
methods: {
......
//删除todo
deleteTodo(id) {
//过滤不改变原数组
/*this.todos = this.todos.filter((todo)=>{
return todo.id !== id
})*/
//可以精简为
this.todos = this.todos.filter(todo => todo.id !== id)
}
}
}
</script>
......

List.vue 接收,并传给 Item.vue

<template>
<div>
<ul class="todo-main">
<Item ......
:deleteTodo="deleteTodo"
/>
</ul>
</div>
</template>

<script>
export default {
......
props:["todos","checkTodo","deleteTodo"],
......
}
</script>

Item.vue 接收调用,并把要删除的 id 传过去

<template>
<li>
<label>
<input type="checkbox" :checked="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 {
......
props:['todo','checkTodo','deleteTodo'],
methods:{
......
//删除
handleDelete(id){
if(confirm("确认删除吗?")){
this.deleteTodo(id)
}
}
}
}
</script>

<style scoped>
......

li:hover{
background-color: #dddddd;
}

li:hover button{
display: block;
}
</style>

底部统计功能实现

Vue TodoList案例_功能实现_06

底部要实现统计功能,必须知道 todos 的长度和选中个数,所以需要将 todos 传过来

App.vue 将 todos 传给 Footer.vue

<Footer :todos="todos"/>

Footer.vue 接收并计算,使用 reduce 函数计算选中的个数,​​关于reduce的用法​​

如果选中个数和数组个数相等,那么前边的 checkbox 将被选中。使用计算属性返回选中个数和数组个数是否相等即可

如果把项目全部删除,那么底部将整体不展示,可以给 Footer.vue 增加一个 v-show 即可

点击 checkbox 将把项目全部选中,可以给 checkbox 增加 change 事件来全选或全部选,所以需要在 App.vue 中增加方法

App.vue

<Footer :todos="todos" :checkAllTodo="checkAllTodo"/>
<script>
......

export default {
......
methods: {
......
//全选 or 取消全选
checkAllTodo(done){
this.todos.forEach((todo)=>{
todo.done = done
})
}
}
}
</script>

Footer.vue

<template>
<div class="todo-footer" v-show="total">
<label>
<input type="checkbox" :checked="isAll" @change="checkAll">
</label>
<span>
<span>已完成{{ doneTotal }}</span>/全部{{ total }}
</span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>

<script>
export default {
name: "Footer",
props: ["todos","checkAllTodo"],
computed: {
total(){
return this.todos.length
},
doneTotal() {
/*return this.todos.reduce((pre, current) => {
return pre + (current.done ? 1 : 0)
}, 0)*/
//精简为
return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
},
isAll(){
return this.doneTotal === this.total
}
},
methods:{
checkAll(e){
this.checkAllTodo(e.target.checked)
}
}
}
</script>

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

我们还可以优化一点,其中 Footer.vue 的 checkbox 可以使用 v-model 来双向绑定数据

<input type="checkbox" v-model="isAll">

<script>
export default {
name: "Footer",
props: ["todos","checkAllTodo"],
computed: {
......
isAll:{
get(){
return this.doneTotal === this.total
},
set(value){
this.checkAllTodo(value)
}
}
}
}
</script>

底部清除已完成任务

Vue TodoList案例_vue_07

App.vue中增加 clearAllTodo 方法,返回未被选中的项目,然后把这个方法传给 Footer.vue

App.vue中

<Footer :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"/>

<script>
......
export default {
......
methods: {
......
//清除全部选中的任务
clearAllTodo(){
this.todos = this.todos.filter((todo)=>{
return !todo.done
})
}
}
}
</script>

Footer.vue接收并使用

<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>

<script>
export default {
......
methods:{
clearAll(){
this.clearAllTodo()
}
}
}
</script>

总结

1、组件化编码流程:
(1).拆分静态组件:组件要按照功能点拆分,命名不要与 html 元素冲突
(2).实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
------①一个组件在用:放在组件自身即可
------②一些组件在用:放在他们共同的父组件上(状态提升)
(3).实现交互:从绑定事件开始
2、props适用于:
(1).父组件=>子组件通信
(2).子组件=>父组件通信(要求父先给子一个函数)
3、使用​​​v-model​​​时要切记:​​v-model​​​绑定的值不能是 props 传过来的值,因为 props 是不可以修改的!!
4、props 传过来的若是对象类型的值,修改对象中的属性时 Vue 不会报错。但不推荐这样做

举报

相关推荐

0 条评论