vue3 实现简单的todolist,
运用到的知识点: 组件传值props, ref, onMounted, reactive, toRefs, watch
实现功能如下图。
在追加一个全部删除按钮
在app里添加
// 全部删除
const delAll = (id) => {
state.todos = state.todos.filter((val) => {
console.log(val.id, "val");
val.id !== id;
});
};
记得传值过去就行
介绍下,我这里进行了封装和父子组件传值。
App文件(父组件)
<template>
<div class="todo-container">
<div class="todo-wrap">
<Herder :addTodo="addtodo" />
<List :todos="todos" :deleteTodo="deleteTodo" :updataTodo="updataTodo" />
<Footer :todos="todos" :checkAll="checkAll" :deleteAll="deleteAll" :delAll="delAll" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, reactive, toRefs, watch } from "vue";
import Herder from "./components/com/headTo.vue";
import List from "./components/com/listTo.vue";
import Footer from "./components/com/foterTo.vue";
// 定义接口,约束state数据类型
import Todo from "./components/type/type";
import { readTods, saveTodos } from "./components/utils/index";
export default defineComponent({
name: "App",
components: { Herder, List, Footer },
setup() {
// 初始化数据是一个数组形式,数组中的每个数据都是对象
const state = reactive<{ todos: Todo[] }>({
todos: [],// 初始化为空数组
});
// 加载完成后在读取数据
onMounted(() => {
setTimeout(() => {
state.todos = readTods(); // 此处进行了本地存储的封装,
}, 1000);
});
// 添加数据事件,是在输入框中执行,就在head 头部结构中进行
const addtodo = (todo: Todo) => {
state.todos.unshift(todo); //头部增加
// state.todos.push(todo); 尾部增加
};
// 删除数据, list 表格中进行传值
const deleteTodo = (index: number) => {
state.todos.splice(index, 1);
};
// 修改todo的isComputed属性的状态 , list 表格中进行传值
const updataTodo = (todo: Todo, isCompleted: boolean) => {
todo.isCompleted = isCompleted;
console.log(todo, "todo");
};
// 全选,全不选。 在底部的footer中执行
const checkAll = (isCompleted: boolean) => {
state.todos.forEach((todo) => {
todo.isCompleted = isCompleted;
});
};
// 删除所有选中, 在底部的footer中执行
const deleteAll = () => {
state.todos = state.todos.filter((todo) => !todo.isCompleted);
};
// 全部删除
const delAll = (id) => {
state.todos = state.todos.filter((val) => {
console.log(val.id, "val");
val.id !== id;
});
};
// 监听操作,若todos数组变化直接存储到缓存中
// watch(
// () => state.todos,
// (value) => {
// localStorage.setItem("todos——key", JSON.stringify(value));
// },
// { deep: true }
// );
watch(() => state.todos, saveTodos, { deep: true }); // 设置缓存
return {
...toRefs(state),
addtodo,
deleteTodo,
updataTodo,
checkAll,
deleteAll,
delAll
};
},
});
</script>
<style scoped>
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px, solid #000;
border-radius: 5px;
}
</style>
type封装就一点不在上代码,看图就行
utils 存储封装也很少
head 头部代码
<template>
<div class="todo-header">
<input
type="text"
placeholder="输入,回车结束"
v-model="title"
@keyup.enter="add"
/>
<!-- add事件,是在输入框中进行的,
就要推送给父组件 app ,
我没有运用context上下文去emit 推送(context.emit),
直接用了 setup(props,context)中的第一个参数props 操作的 -->
</div>
</template>
<script>
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "headTo",
props: {
// 接收父组件传递的参数
addTodo: {
type: Function,
required: true,
},
},
setup(props) {
const title = ref("");
const add = () => {
// 当获取的值为非空在添加
const text = title.value;
if (!text.trim()) return; // 没有数据
// 有数据,就添加信息值
const todo = {
id: Date.now(),
title: text,
isCompleted: false, // 新数据是默认不选中的
};
// 调用addTodo方法
props.addTodo(todo);
// 清空文本
title.value = "";
};
return {
title,
add,
};
},
});
</script>
<style scoped>
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px salmon #ccc;
border-radius: 4px;
padding: 4px 7px;
}
.todo-header input:focus {
outline: none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: rgba(82, 168, 236, 0.6);
}
</style>
此时头部操作结束。
list文件
<template>
<ul class="todo-main">
<Item
v-for="(todo, index) in todos"
:key="todo.id"
:todo="todo"
:deleteTodo="deleteTodo"
:index="index"
:updataTodo="updataTodo"
/>
</ul>
</template>
<script>
import { defineComponent } from "vue";
import Item from "./itemTo.vue"; // 因为要循环遍历,就把每一项单独拉出去做成组件 item
export default defineComponent({
name: "listTo",
components: { Item },
props: ["todos", "deleteTodo", "updataTodo"], // 接收父组件app传递来的数据,在通过 list传递给item
setup() {
return {};
},
});
</script>
<style scoped>
.todo-mian {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border-radius: 2px;
border: 1px solid #ddd;
padding-left: 5px;
margin-top: 10px;
}
</style>
Item 文件
<template>
<!-- 鼠标移入移除时事件和颜色改变 -->
<li
@mouseenter="mouseHander(true)"
@mouseleave="mouseHander(false)"
:style="{ backgroundColor: bgColor, color: whColor }"
>
<label>
<input type="checkbox" v-model="isCompleted" />
<span>{{ todo.title }}</span>
</label>
<!-- 删除按钮操作 -->
<button
class="btn btn-danger"
style="display: none"
v-show="isShow"
@click="delTodo"
>
删除
</button>
</li>
</template>
<script>
import { computed, defineComponent, ref } from "vue";
export default defineComponent({
name: "itemTo",
props: {
// 父组件list传递的
todo: {
type: Object,
required: true,
},
deleteTodo: {
type: Function,
required: true,
},
index: {
type: Number,
required: true,
},
updataTodo: {
type: Function,
required: true,
},
},
setup(props) {
const bgColor = ref("white");
const whColor = ref("black");
const isShow = ref(false);
// 鼠标进入和离开的函数,flag true/false
const mouseHander = (flag) => {
if (flag) {
bgColor.value = "pink";
whColor.value = "green";
isShow.value = true;
} else {
bgColor.value = "white";
whColor.value = "black";
isShow.value = false;
}
};
// 删除数据方法
const delTodo = () => {
if (window.confirm("删除吗")) {
props.deleteTodo(props.index);
}
};
// 计算属性的方式,操作选中状态
const isCompleted = computed({
get() {
// 查询到传递过来的todo对象里面的数据的状态
return props.todo.isCompleted;
},
set(val) {
console.log(val);
props.updataTodo(props.todo, val);
},
});
return {
mouseHander,
bgColor,
whColor,
isShow,
delTodo,
isCompleted,
};
},
});
</script>
<style scoped>
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
</style>
底部的 footer 文件
<template>
<div class="todo-footer">
<label>
<input type="checkbox" v-model="isCkeck" />
</label>
<span>
<span>已完成{{ count }}</span> / 全部{{ todos.length }}
</span >
<button class="btn btn-danger" @click="deleteAll">清除完成任务项</button>
</div>
</template>
<script>
import { defineComponent, computed } from "vue";
export default defineComponent({
name: "foterTo",
props: {
todos: {
type: Array,
required: true,
},
checkAll: {
type: Boolean,
required: true,
},
deleteAll: {
type: Function,
required: true,
},
},
setup(props) {
console.log(props);
// 已完的成计算属性
const count = computed(() => {
return props.todos.reduce(
(pre, todo, index) => pre + (todo.isCompleted ? 1 : 0),
0
);
});
// 全选、全不选计算属性
const isCkeck = computed({
get() {
return count.value > 0 && props.todos.length === count.value;
},
set(val) {
props.checkAll(val);
return;
},
});
return {
count,
isCkeck,
};
},
});
</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>
就这样结束了。
如果不想这样来回的组件封装。在一个文件中直接展示,可以看下个文件