srcTodoList动画
文章发布较早,内容可能过时,阅读注意甄别。
# 代码
# 代码路径
$ tree -N
.
├── App.vue
├── components
│ ├── MyFooter.vue
│ ├── MyHeader.vue
│ ├── MyItem.vue
│ └── MyList.vue
└── main.js
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# App.vue
<template>
<div id="app">
<div class="todo-container">
<div class="todo-wrap">
<!-- header -->
<MyHeader @addTodo="addTodo"></MyHeader>
<MyList :todos="todos"></MyList>
<MyFooter
:todos="todos"
@checkAllTodo="checkAllTodo"
@clearAllTodo="clearAllTodo"
>
</MyFooter>
</div>
</div>
</div>
</template>
<script>
import MyFooter from "./components/MyFooter.vue";
import MyHeader from "./components/MyHeader.vue";
import MyList from "./components/MyList.vue";
import pubsub from "pubsub-js";
export default {
name: "App",
components: { MyFooter, MyHeader, MyList },
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,我接收到了数据:',x);
this.todos.unshift(todoObj);
},
// 勾选or取消勾选一个todo
checkTodo(id) {
this.todos.forEach((todo) => {
if (todo.id === id) {
todo.done = !todo.done;
}
});
},
// 删除一个todo
deleteTodo(_, id) {
this.todos = this.todos.filter((todo) => {
return todo.id !== id;
});
},
// 全选or取消全选
checkAllTodo(done) {
this.todos.forEach((todo) => {
todo.done = done;
});
},
// 清除所有已完成的todo
clearAllTodo() {
this.todos = this.todos.filter((todo) => {
return !todo.done;
});
},
// 更新一个todo
updateTodo(id, title) {
this.todos.forEach((todo) => {
if (todo.id === id) {
todo.title = title;
}
});
},
},
mounted() {
this.$bus.$on("checkTodo", this.checkTodo);
this.$bus.$on("updateTodo", this.updateTodo);
// this.$bus.$on('deleteTodo',this.deleteTodo)
this.pid = pubsub.subscribe("deleteTodo", this.deleteTodo);
},
beforeDestroy() {
this.$bus.$off("checkTodo");
this.$bus.$off("updateTodo");
// this.$bus.$off('deleteTodo')
pubsub.unsubcribe(this.pid);
},
};
</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(119, 181, 206);
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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# MyFooter.vue
<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() {
// 第一种:使用遍历的方式
// let i = 0
// this.todos.forEach(todo => {
// if(todo.done){
// i++
// }
// });
// return i
// 第二种:使用 reduce 处理
// return this.todos.reduce((pre, todo) => pre+(todo.done ? 1 : 0), 0);
// 第三种:使用filter处理
const a = this.todos.filter((todo) => {
return todo.done;
});
return a.length;
},
isAll: {
get() {
return this.total === this.doneTotal && this.total > 0;
},
set(value) {
// this.checkAllTodo(value)
this.$emit("checkAllTodo", value);
},
},
},
methods: {
clearAll() {
console.log("aaaa");
// this.clearAllTodo()
this.$emit("clearAllTodo");
},
// checkAll(e){
// this.checkAllTodo(e.target.checked)
// }
},
};
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# MyHeader.vue
<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",
// props:['addTodo'],
data() {
return {
title: "",
};
},
methods: {
add() {
// 校验数据
if (!this.title.trim()) {
return alert("输入不能为空");
}
// 将用户的输入包装成一个todo对象
const todoObj = { id: nanoid(), title: this.title, done: false };
// 通知APP组件添加一个todo对象
this.$emit("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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# MyItem.vue
<template>
<li>
<label>
<input
type="checkbox"
:checked="todo.done"
@change="changeTodo(todo.id)"
/>
<span v-show="!todo.isEdit">{{ todo.title }}</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>
import pubsub from "pubsub-js";
export default {
name: "MyItem",
// 声明接收todo对象
props: ["todo"],
methods: {
// 勾选or取消勾选
changeTodo(id) {
// 通知App组件将对应的todo对象的done值取反
// this.checkTodo(id)
this.$bus.$emit("checkTodo", id);
},
// 删除todo--通过事件总线的方式
// handleDelete(id){
// if(confirm('确定删除吗?')){
// this.$bus.$emit('deleteTodo',id)
// }
// }
// 删除todo--通过订阅发布
handleDelete(id) {
if (confirm("确定删除吗?")) {
pubsub.publish("deleteTodo", id);
}
},
handleEdit(todo) {
if (todo.hasOwnProperty("isEdit")) {
todo.isEdit = true;
} else {
this.$set(todo, "isEdit", true);
}
this.$nextTick(function () {
this.$refs.inputTitle.focus();
});
},
// 失去焦点回调 (真正执行修改逻辑)
handleBlur(todo, e) {
todo.isEdit = false;
if (!e.target.value.trim()) return alert("内容不能为空");
this.$bus.$emit("updateTodo", todo.id, e.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: gray;
}
li:hover button {
display: block;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# MyList.vue
<template>
<ul class="todo-main">
<transition-group appear name="todo">
<MyItem v-for="t in todos" :key="t.id" :todo="t" />
</transition-group>
</ul>
</template>
<script>
import MyItem from "./MyItem.vue";
export default {
name: "MyList",
components: { MyItem },
props: ["todos"],
};
</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: qwe 0.5s linear;
}
.todo-leave-active {
animation: qwe 0.5s linear reverse;
}
@keyframes qwe {
from {
transform: translateX(100%);
}
to {
transform: translateX(0px);
}
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# main.js
import Vue from "vue";
import App from "./App.vue";
Vue.config.productionTip = false;
new Vue({
el: "#app",
components: { App },
render: (h) => h(App),
beforeCreate() {
Vue.prototype.$bus = this;
},
});
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
上次更新: 2024/06/13, 22:13:45