听阳光的猫

道阻且长、望你如一

0%

Todo-app

创建项目

npm install -g @vue/cli

vue create todo-app

image-20201028134057459

npm run serve 运行程序

编写HTML

image-20201028140127673

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
<template>
<main>
<div class="container">
<h1>欢迎使用待办事项</h1>
<div class="input-add">
<input type="text" name="todo" />
<button>
<i class="plus"></i>
</button>
</div>
<div class="filters">
<span class="filter active">全部</span>
<span class="filter">已完成</span>
<span class="filter">未完成</span>
</div>
<div class="todo-list">
<div class="todo-item">
<label>
<input type="checkbox" />Todo1
<span class="check-button"></span>
</label>
</div>
<div class="todo-item">
<label>
<input type="checkbox" />Todo2
<span class="check-button"></span>
</label>
</div>
<div class="todo-item">
<label>
<input type="checkbox" />Todo3
<span class="check-button"></span>
</label>
</div>
</div>
</div>
</main>
</template>
  1. cd todo-app
  2. git add
  3. git commit -m "编写HTML"
  4. git remote add origin git@github.com:Derek-Dong/todo-app.git
  5. git push -u origin master

编写CSS

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
144
145
146
147
148
149
150
151
152
153
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: Helvetica, "PingFang SC", "Microsoft Yahei", sans-serif;
}
/* 整个页面 */

main {
width: 100vw;
min-height: 100vh;
display: grid;
align-items: center;
justify-items: center;
background-color: rgb(203, 210, 240);
}

.container {
width: 60%;
max-width: 400px;
box-shadow: 0 0 24px rgba(0, 0, 0, 0.15);
border-radius: 24px;
padding: 48px 28px;
background-color: rgb(245, 246, 252);
}

/* 标题 */
h1 {
margin: 24px 0;
font-size: 28px;
color: #414873;
}

.input-add {
position: relative;
display: flex;
align-items: center;
}

.input-add input {
padding: 16px 52px 16px 18px;
border-radius: 48px;
border: none;
outline: none;
box-shadow: 0 0 24px rgba(0, 0, 0, 0.08);
width: 100%;
font-size: 16px;
color: #626262;
}

.input-add button {
width: 46px;
height: 46px;
border-radius: 50%;
background: linear-gradient(#c0a5f3, #7f95f7);
border: none;
outline: none;

color: #fff;
position: absolute;
right: 0;

cursor: pointer;
}

.input-add .plus {
display: block;
width: 100%;
height: 100%;
background: linear-gradient(#fff, #fff), linear-gradient(#fff, #fff);
background-size: 50% 2px, 2px 50%;
background-position: center;
background-repeat: no-repeat;
}

.filters {
display: flex;
margin: 24px 2px;
color: #c0c2ce;
font-size: 14px;
}

.filters .filter {
margin-right: 14px;
transition: 0.8s;
cursor: pointer;
}

.filters .filter.active {
color: #6b729c;
transform: scale(1.2);
}

.todo-list {
display: grid;
row-gap: 14px;
}

.todo-item {
background: #fff;
padding: 16px;
border-radius: 8px;
color: #626262;
}

.todo-item label {
position: relative;
display: flex;
align-items: center;
}

.todo-item.done label {
text-decoration: line-through;
font-style: italic;
}

.todo-item label span.check-button {
position: absolute;
top: 0;
}

.todo-item label span.check-button::before,
.todo-item label span.check-button::after {
content: "";
display: block;
position: absolute;
width: 18px;
height: 18px;
border-radius: 50%;
}

.todo-item label span.check-button::before {
border: 1px solid #b382f9;
}

.todo-item label span.check-button::after {
transition: 0.4s;
background: #b382f9;
transform: translate(1px, 1px) scale(0.8);
opacity: 0;
}

.todo-item input {
margin-right: 16px;
opacity: 0;
}

.todo-item input:checked + span.check-button::after {
opacity: 1;
}
</style>

抽离组件

App.vue

image-20201028144027238

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
<template>
<main>
<div class="container">
<h1>欢迎使用待办事项</h1>
<todo-add />
<todo-filter />
<todo-list />
</div>
</main>
</template>

<script>
import TodoAdd from "./components/TodoAdd";
import TodoFilter from "./components/TodoFilter";
import TodoList from "./components/TodoList";

export default {
name: "App",
components: { TodoAdd, TodoFilter, TodoList },
};
</script>

<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: Helvetica, "PingFang SC", "Microsoft Yahei", sans-serif;
}
/* 整个页面 */

main {
width: 100vw;
min-height: 100vh;
display: grid;
align-items: center;
justify-items: center;
background-color: rgb(203, 210, 240);
}

.container {
width: 60%;
max-width: 400px;
box-shadow: 0 0 24px rgba(0, 0, 0, 0.15);
border-radius: 24px;
padding: 48px 28px;
background-color: rgb(245, 246, 252);
}

/* 标题 */
h1 {
margin: 24px 0;
font-size: 28px;
color: #414873;
}
</style>

TodoAdd.vue

image-20201028144107096

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
<template>
<div class="input-add">
<input type="text" name="todo" />
<button>
<i class="plus"></i>
</button>
</div>
</template>

<script>
export default {
name: "TodoAdd",
};
</script>

<style>
.input-add {
position: relative;
display: flex;
align-items: center;
}

.input-add input {
padding: 16px 52px 16px 18px;
border-radius: 48px;
border: none;
outline: none;
box-shadow: 0 0 24px rgba(0, 0, 0, 0.08);
width: 100%;
font-size: 16px;
color: #626262;
}

.input-add button {
width: 46px;
height: 46px;
border-radius: 50%;
background: linear-gradient(#c0a5f3, #7f95f7);
border: none;
outline: none;

color: #fff;
position: absolute;
right: 0;

cursor: pointer;
}

.input-add .plus {
display: block;
width: 100%;
height: 100%;
background: linear-gradient(#fff, #fff), linear-gradient(#fff, #fff);
background-size: 50% 2px, 2px 50%;
background-position: center;
background-repeat: no-repeat;
}
</style>

TodoFilter.vue

image-20201028144132553

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
<template>
<div class="filters">
<span class="filter active">全部</span>
<span class="filter">已完成</span>
<span class="filter">未完成</span>
</div>
</template>

<script>
export default {
name: "TodoFilter",
};
</script>

<style>
.filters {
display: flex;
margin: 24px 2px;
color: #c0c2ce;
font-size: 14px;
}

.filters .filter {
margin-right: 14px;
transition: 0.8s;
cursor: pointer;
}

.filters .filter.active {
color: #6b729c;
transform: scale(1.2);
}
</style>

TodoList.vue

image-20201028144208453

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="todo-list">
<todo-list-item v-for="n in 3" :key="n"> </todo-list-item>
</div>
</template>

<script>
import TodoListItem from "./TodoListItem";

export default {
name: "TodoList",
components: { TodoListItem },
};
</script>

<style>
.todo-list {
display: grid;
row-gap: 14px;
}
</style>

TodoListItem.vue

image-20201028144231979

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
<template>
<div class="todo-item">
<label>
<input type="checkbox" />Todo1
<span class="check-button"></span>
</label>
</div>
</template>

<script>
export default {
name: "TodoListItem",
};
</script>

<style>
.todo-item {
background: #fff;
padding: 16px;
border-radius: 8px;
color: #626262;
}

.todo-item label {
position: relative;
display: flex;
align-items: center;
}

.todo-item.done label {
text-decoration: line-through;
font-style: italic;
}

.todo-item label span.check-button {
position: absolute;
top: 0;
}

.todo-item label span.check-button::before,
.todo-item label span.check-button::after {
content: "";
display: block;
position: absolute;
width: 18px;
height: 18px;
border-radius: 50%;
}

.todo-item label span.check-button::before {
border: 1px solid #b382f9;
}

.todo-item label span.check-button::after {
transition: 0.4s;
background: #b382f9;
transform: translate(1px, 1px) scale(0.8);
opacity: 0;
}

.todo-item input {
margin-right: 16px;
opacity: 0;
}

.todo-item input:checked + span.check-button::after {
opacity: 1;
}
</style>

事件&数据

image-20201017104653567

App.vue

image-20201028145751263

image-20201028145835691

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
<template>
<main>
<div class="container">
<h1>欢迎使用待办事项</h1>
<!-- 通过add-todo监听todoAdd里面的事件 -->
<todo-add :tid="todos.length" @add-todo="addTodo" />
<todo-filter :selected="filter" @change-filter="filter = $event" />
<!-- 使用ref包装的数据需要通过value才能访问数据,但是因为在template中会自动解析 -->
<todo-list :todos="filteredTodos" />
</div>
</main>
</template>

<script>
import { computed, ref } from "vue";

import TodoAdd from "./components/TodoAdd";
import TodoFilter from "./components/TodoFilter";
import TodoList from "./components/TodoList";

export default {
name: "App",
components: { TodoAdd, TodoFilter, TodoList },
setup() {
// 使用ref保存空数组作为默认todo列表的数据
const todos = ref([]);
// 定义一个添加todo的函数:通过事件接收一个todo参数,保存todo信息添加到列表中
const addTodo = (todo) => todos.value.push(todo);
// 保持当前选中的过滤选项
const filter = ref("all");
// 根据filter的值过滤todos列表
const filteredTodos = computed(() => {
switch (filter.value) {
case "done":
return todos.value.filter((todo) => todo.completed);
case "todo":
return todos.value.filter((todo) => !todo.completed);
default:
return todos.value;
}
});
// 为了在template中使用数据,使用return以对象的形式返回数据和函数
return {
todos,
addTodo,
filter,
filteredTodos,
};
},
};
</script>

<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: Helvetica, "PingFang SC", "Microsoft Yahei", sans-serif;
}
/* 整个页面 */

main {
width: 100vw;
min-height: 100vh;
display: grid;
align-items: center;
justify-items: center;
background-color: rgb(203, 210, 240);
}

.container {
width: 60%;
max-width: 400px;
box-shadow: 0 0 24px rgba(0, 0, 0, 0.15);
border-radius: 24px;
padding: 48px 28px;
background-color: rgb(245, 246, 252);
}

/* 标题 */
h1 {
margin: 24px 0;
font-size: 28px;
color: #414873;
}
</style>

TodoAdd.vue

image-20201028145927917

image-20201028150003199

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
<template>
<div class="input-add">
<!--
v-model 同步用户输入的内容
@keyup.enter给回车键添加函数
-->
<input
type="text"
name="todo"
v-model="todoContent"
@keyup.enter="emitAddTodo"
/>
<button @click="emitAddTodo">
<i class="plus"></i>
</button>
</div>
</template>

<script>
import { ref } from "vue";

export default {
name: "TodoAdd",
// props可以访问传递进来的属性,context保留了vue上下文的信息
setup(props, context) {
const todoContent = ref("");

const emitAddTodo = () => {
const todo = {
id: props.tid,
// 这里需要使用value才能使用ref中的值
content: todoContent.value,
completed: false,
};
context.emit("add-todo", todo);
todoContent.value = "";
};

return {
todoContent,
emitAddTodo,
};
},
};
</script>

<style>
.input-add {
position: relative;
display: flex;
align-items: center;
}

.input-add input {
padding: 16px 52px 16px 18px;
border-radius: 48px;
border: none;
outline: none;
box-shadow: 0 0 24px rgba(0, 0, 0, 0.08);
width: 100%;
font-size: 16px;
color: #626262;
}

.input-add button {
width: 46px;
height: 46px;
border-radius: 50%;
background: linear-gradient(#c0a5f3, #7f95f7);
border: none;
outline: none;

color: #fff;
position: absolute;
right: 0;

cursor: pointer;
}

.input-add .plus {
display: block;
width: 100%;
height: 100%;
background: linear-gradient(#fff, #fff), linear-gradient(#fff, #fff);
background-size: 50% 2px, 2px 50%;
background-position: center;
background-repeat: no-repeat;
}
</style>

TodoFilter.vue

image-20201028150050350

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
<template>
<div class="filters">
<span
class="filter"
v-for="filter in filters"
:key="filter.value"
@click="$emit('change-filter', filter.value)"
:class="{ active: selected === filter.value }"
>
{{ filter.label }}
</span>
</div>
</template>

<script>
export default {
name: "TodoFilter",
props: ["selected"],
setup() {
const filters = [
{ label: "全部", value: "all" },
{ label: "已完成", value: "done" },
{ label: "未完成", value: "todo" },
];

return {
filters,
};
},
};
</script>

<style>
.filters {
display: flex;
margin: 24px 2px;
color: #c0c2ce;
font-size: 14px;
}

.filters .filter {
margin-right: 14px;
transition: 0.8s;
cursor: pointer;
}

.filters .filter.active {
color: #6b729c;
transform: scale(1.2);
}
</style>

TodoList.vue

image-20201028150133247

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
<template>
<div class="todo-list">
<!-- 通过todo-item将todo信息传递给todo-list-item子组件 -->
<todo-list-item
v-for="todo in todos"
:key="todo.id"
:todo-item="todo"
@change-state="todo.completed = $event.target.checked"
></todo-list-item>
</div>
</template>

<script>
import TodoListItem from "./TodoListItem";

export default {
name: "TodoList",
components: { TodoListItem },
// 接收父组件传递的todos列表数据
props: ["todos"],
};
</script>

<style>
.todo-list {
display: grid;
row-gap: 14px;
}
</style>

TodoListItem.vue

image-20201028150211258

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
<template>
<div class="todo-item" :class="{ done: todoItem.completed }">
<label>
<!-- :checked根据todo中的完成状态显示为选中还是未选中状态 -->
<input
type="checkbox"
:checked="todoItem.completed"
@click="$emit('change-state', $event)"
/>
<!-- 显示todo-item的内容 -->
{{ todoItem.content }}
<span class="check-button"></span>
</label>
</div>
</template>

<script>
export default {
name: "TodoListItem",
// 接收todo-item属性
props: ["todoItem"],
};
</script>

<style>
.todo-item {
background: #fff;
padding: 16px;
border-radius: 8px;
color: #626262;
}

.todo-item label {
position: relative;
display: flex;
align-items: center;
}

.todo-item.done label {
text-decoration: line-through;
font-style: italic;
}

.todo-item label span.check-button {
position: absolute;
top: 0;
}

.todo-item label span.check-button::before,
.todo-item label span.check-button::after {
content: "";
display: block;
position: absolute;
width: 18px;
height: 18px;
border-radius: 50%;
}

.todo-item label span.check-button::before {
border: 1px solid #b382f9;
}

.todo-item label span.check-button::after {
transition: 0.4s;
background: #b382f9;
transform: translate(1px, 1px) scale(0.8);
opacity: 0;
}

.todo-item input {
margin-right: 16px;
opacity: 0;
}

.todo-item input:checked + span.check-button::after {
opacity: 1;
}
</style>

composables

image-20201029123057624

image-20201029123152301

image-20201029123230375

useTodos.js

image-20201029123817002

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ref } from "vue";

export default function useTodos() {
// 使用ref保存空数组作为默认todo列表的数据
const todos = ref([]);
// 定义一个添加todo的函数:通过事件接收一个todo参数,保存todo信息添加到列表中
const addTodo = (todo) => todos.value.push(todo);

// 为了在template中使用数据,使用return以对象的形式返回数据和函数
return {
todos,
addTodo,
};
}

image-20201029124100376

useFilteredTodos.js

image-20201029125038576

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { computed, ref } from "vue";

export default function useFilteredTodos(todos) {
// 保持当前选中的过滤选项
const filter = ref("all");
// 根据filter的值过滤todos列表
const filteredTodos = computed(() => {
switch (filter.value) {
case "done":
return todos.value.filter((todo) => todo.completed);
case "todo":
return todos.value.filter((todo) => !todo.completed);
default:
return todos.value;
}
});
return {
filter,
filteredTodos,
};
}

image-20201029125118694

App.vue

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
<template>
<main>
<div class="container">
<h1>欢迎使用待办事项</h1>
<!-- 通过add-todo监听todoAdd里面的事件 -->
<todo-add :tid="todos.length" @add-todo="addTodo" />
<todo-filter :selected="filter" @change-filter="filter = $event" />
<!-- 使用ref包装的数据需要通过value才能访问数据,但是因为在template中会自动解析 -->
<todo-list :todos="filteredTodos" />
</div>
</main>
</template>

<script>
import TodoAdd from "./components/TodoAdd";
import TodoFilter from "./components/TodoFilter";
import TodoList from "./components/TodoList";
import useTodos from "@/composables/useTodos.js";
import useFilteredTodos from "@/composables/useFilteredTodos.js";

export default {
name: "App",
components: { TodoAdd, TodoFilter, TodoList },
setup() {
const { todos, addTodo } = useTodos();
const { filter, filteredTodos } = useFilteredTodos(todos);

// 为了在template中使用数据,使用return以对象的形式返回数据和函数
return {
todos,
addTodo,
filter,
filteredTodos,
};
},
};
</script>

<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: Helvetica, "PingFang SC", "Microsoft Yahei", sans-serif;
}
/* 整个页面 */

main {
width: 100vw;
min-height: 100vh;
display: grid;
align-items: center;
justify-items: center;
background-color: rgb(203, 210, 240);
}

.container {
width: 60%;
max-width: 400px;
box-shadow: 0 0 24px rgba(0, 0, 0, 0.15);
border-radius: 24px;
padding: 48px 28px;
background-color: rgb(245, 246, 252);
}

/* 标题 */
h1 {
margin: 24px 0;
font-size: 28px;
color: #414873;
}
</style>

TodoAdd.vue

image-20201029130026895

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
<template>
<div class="input-add">
<!--
v-model 同步用户输入的内容
@keyup.enter给回车键添加函数
-->
<input
type="text"
name="todo"
v-model="todoContent"
@keyup.enter="emitAddTodo"
/>
<button @click="emitAddTodo">
<i class="plus"></i>
</button>
</div>
</template>

<script>
import { ref } from "vue";

export default {
name: "TodoAdd",
// props可以访问传递进来的属性,context保留了vue上下文的信息
setup(props, context) {
return useEmitAddTodo(props.tid, context.emit);
},
};

function useEmitAddTodo(tid, emit) {
const todoContent = ref("");

const emitAddTodo = () => {
const todo = {
id: tid,
// 这里需要使用value才能使用ref中的值
content: todoContent.value,
completed: false,
};
emit("add-todo", todo);
todoContent.value = "";
};

return {
todoContent,
emitAddTodo,
};
}
</script>

<style>
.input-add {
position: relative;
display: flex;
align-items: center;
}

.input-add input {
padding: 16px 52px 16px 18px;
border-radius: 48px;
border: none;
outline: none;
box-shadow: 0 0 24px rgba(0, 0, 0, 0.08);
width: 100%;
font-size: 16px;
color: #626262;
}

.input-add button {
width: 46px;
height: 46px;
border-radius: 50%;
background: linear-gradient(#c0a5f3, #7f95f7);
border: none;
outline: none;

color: #fff;
position: absolute;
right: 0;

cursor: pointer;
}

.input-add .plus {
display: block;
width: 100%;
height: 100%;
background: linear-gradient(#fff, #fff), linear-gradient(#fff, #fff);
background-size: 50% 2px, 2px 50%;
background-position: center;
background-repeat: no-repeat;
}
</style>