Vue快速上手:八、小案例练手

0 阅读8分钟

案例介绍

这章节主要是讲解一个小案例用于加深并更深入理解前面所学的基础知识,这是一个事务管理的案例,虽然简单但是新手入门练习特别典型,这里面使用一些ES6的数组方法,如:forEach、filter、splice等方法,如果不了解可以先自行了解一下数组的各种方法以及ES6的箭头函数的用法,界面如下:

微信截图_20241020143754.png

微信截图_20241020144507.png

思路分析

功能

  • 录入事务到列表中
  • 输出整个事务列表
  • 在列表中更改事务的完成状态,已完成的事务项为其加上删除线
  • 在列表中删除某项
  • 全选/全不选
  • 删除选中的事务
  • 统计已完成/未完成的事务数量

实现思路

根据上述的页面结构,可以分成三个子组件:录入部分、列表部分、底下按钮部分。

  • 根组件(app.vue)

在根组件中创建数据列表遵循单身数据流的思路,将处理增、删、改等方法的处理过程都放在根组件中,通过将事件传递到子组件中,创建计算属性得到已完成、未完成的事件数量。

  • 录入组件(components/Entering.vue)

使用表单的submit事件录入事件名称,虽然同样也可以采用键盘的事件然后判断是否为回车键也可以实现,但是会有一些小BUG,比如在中文输入法状态时想输入英文就得使用回车键,此时这个回车键就会和结束录入冲突。

生成单个事务的对象,并且将这个对象通过调用父组件的录入事件将其加入事件列表中。

  • 列表组件(components/TaskList.vue)

列表输出,并且接收并调用父组件的删除和更改状态的事件

  • 底部按钮部分组件(components/BtnBox.vue)

接收并调用父组件的全选/全不选、删除事件,并接收已完成、未完成数量。

代码实现

  • 父组件(app.vue)
<script setup>
    import { ref, reactive, computed } from "vue";
    import Entering from "@/components/Entering.vue";
    import TaskList from "@/components/TaskList.vue";
    import BtnBox from "@/components/BtnBox.vue";

    const taskList = reactive([]); // 存储事件列表的数组

    // 处理事务录入: 参数为一个事务对象
    const handleEntering = obj => {
        if(taskList.some(item => item.workName === obj.workName)) {
            alert('事务已存在,请勿重复添加!');
            return;
        }
        taskList.push(obj);
    };

    // 处理事务删除: 参数为一个数字id时删除id项的事务,为字符串'selected'时删除所有选中的事务
    const handleDelete = playload => {
        for(let i = 0; i < taskList.length; i++) {
            if(playload === "selected" && taskList[i].selected) {
                taskList.splice(i--, 1);
            } else if(playload === taskList[i].id) {
                taskList.splice(i--, 1); // 使用数组的splice方法删除元素
            }
        }
    };

    // 更改事务完成状态:参数为一个事务id时更改id项的事务完成状态
    // 箭头函数只有一个表达式时可省略大括号,并且这个表达式的值就是函数的返回值
    const handleFinishStatus = id => taskList.forEach(item => id === item.id && (item.finished = !item.finished));

    // 更改列表选中状态:参数为一个状态,将所有列表的选中状态都设置为status
    const handleSelectedStatus = status => taskList.forEach(item => item.selected = status);

    // 创建计算属性computed,计算待办事务数量
    const unfinishedCount = computed(() => taskList.filter(item => !item.finished).length);

    // 创建计算属性computed,计算已完成事务数量
    const finishedCount = computed(() => taskList.filter(item => item.finished).length);
</script>

<template>
    <div class="wrapper">
        <!-- 录入 -->
        <Entering @entering="handleEntering" />

        <!-- 事务列表 -->
        <TaskList :data="taskList" @delete="handleDelete" @updatefinished="handleFinishStatus" />

        <!-- 按钮区 -->
        <BtnBox
            @delete="handleDelete"
            @updatefinished="handleFinishStatus"
            @updateselected="handleSelectedStatus"
            :finishedCount="finishedCount"
            :unfinishedCount="unfinishedCount"
        />
    </div>
</template>

<style scoped>
    .wrapper {
        margin: 20px auto 0;
        width: 800px;
    }
</style>
  • 录入组件(components/Entering.vue)
<script setup>
    import { ref } from "vue";

    const str = ref("");

    // 接收并声明父组件传递过来的事件
    const emits = defineEmits(["entering"]);

    // 处理表单提交事件,使用回车键可触发
    const handleSubmit = (e) => {
        e.preventDefault(); // 取消表单提交默认行为,使其不会重新刷新页面
        if(str.value === "") {
            return;
        }
        const obj = {
            workName: str.value, // 事务名
            finished: false, // 事务的完成状态
            id: new Date().getTime(), // 生成时间戳的id
            selected: false // 是否被选中,用于在checkbox中使用
        }
        emits("entering", obj); // 调用父组件中的录入事件
        str.value = ""; // 清空输入框内容
    }
</script>

<template>
    <div class="form">
        <form @submit="handleSubmit">
            <input type="text" v-model="str" placeholder="输入要做的事务,按回车结束">
        </form>
    </div>
</template>

<style scoped>
    .form {
        width: 100%;
    }
    .form input {
        width: 100%;
        height: 35px;
        border-radius: 5px;
        text-indent: 1em;
    }
</style>
  • 列表组件(components/TaskList.vue)
<script setup>
    const props = defineProps({
        data: Array
    });
    const emits = defineEmits(["delete", "updatefinished"]);

    // 点击完成/回退按钮
    const handleFinishStatus = id => emits("updatefinished", id);

    // 点击删除按钮
    const handleDelete = id => emits("delete", id);

</script>

<template>
    <ul class="list">
        <li v-if="!props.data.length">暂无事务,请添加!</li>

        <li v-for="(task, index) in props.data" :key="task.id">
            <span class="cb">
                <input type="checkbox" v-model="task.selected">
            </span>
            <span class="work-name" :class=" task.finished ? 'finished' : '' ">{{ task.workName }}</span>
            <span class="btn">
                <button @click="handleFinishStatus(task.id)">{{ task.finished ? '回退' : '完成' }}</button>
                <button @click="handleDelete(task.id)">删除</button>
            </span>
        </li>
    </ul>
</template>

<style scoped>
    .list {
        margin: 30px auto 0;
        padding: 10px;
        width: 100%;
        border: 1px solid #ccc;
    }
    .list li {
        display: flex;
        justify-content: space-between;
        margin: 20px 0 0;
        width: 100%;
    }
    .list li:first-child {
        margin-top: 0;
    }
    .cb {
        width: 30px;
    }
    .cb input {
        width: 100%;
        height: 20px;
    }
    .work-name {
        flex: 1;
        padding: 0 10px;
    }
    .work-name.finished {
        text-decoration: line-through;
    }
    .btn {
        display: flex;
        justify-content: space-between;
        width: 100px;
    }
    .btn button {
        width: 48px;
    }
</style>
  • 底部按钮部分组件(components/BtnBox.vue)
<script setup>
    import { ref } from 'vue';

    const props = defineProps({
        finishedCount: Number,
        unfinishedCount: Number
    });
    const emits = defineEmits(['delete', 'updatefinished', 'updateselected']);
    const selected = ref(false);

    // 点击删除
    const handleDelete = () => emits("delete", "selected");

    // 全选按钮事件
    const handleCb = () => emits('updateselected', selected.value);
</script>

<template>
    <div class="btn-box">
        <div class="left">
            <span class="cb">
                <input type="checkbox" v-model="selected" @change="handleCb"> 全选
            </span>
            <button @click="handleDelete">删除</button>
        </div>

        <div class="right">
            <span>已完成:{{ props.finishedCount ? props.finishedCount : 0 }}</span>
            <span>未完成:{{ props.unfinishedCount ? props.unfinishedCount : 0 }}</span>
        </div>
    </div>
</template>

<style scoped>
    .btn-box {
        display: flex;
        justify-content: space-between;
        margin: 30px 0 0;
    }
    input {
        width: 30px;
        height: 20px;
        vertical-align: middle;
    }
    button {
        margin-left: 15px;
        width: 48px;
    }
    .btn-box .right span {
        margin-left: 15px;
    }
</style>

使用hooks重构代码

Vue3和Vue2在使用上最大的区别就是组合式函数的使用,可以随意导入自己想要的函数使用,而不是一团臃肿的都挤在一个文件中。

在Vue3中像refreactivewatch等这些方法都是Vue内置的函数方法,所以我们也可以将组件中一些功能或将一些可复用处理或状态抽离出来,单独放到一个js文件中导出使用。这种方式一般称为hooks函数(钩子函数)。

src目录下新建一个hooks目录专门用来存储hooks文件,我将新建一个useTaskData.js的文件用来处理根组件的增、册、改、查等功能逻辑。

代码实现

  • hooks/useTaskData.js
import {computed, reactive} from "vue";

// 导入数据列表
export let taskList = null;

export const useTaskData = () => {
    if(!taskList) {
        taskList = reactive([]);
    }
    return taskList;
};

// 处理事务录入: 参数为一个事务对象
export const handleEntering = obj => {
    if(taskList.some(item => item.workName === obj.workName)) {
        alert('事务已存在,请勿重复添加!');
        return;
    }
    taskList.push(obj);
};

// 处理事务删除: 参数为一个数字id时删除id项的事务,为字符串'selected'时删除所有选中的事务
export const handleDelete = playload => {
    for(let i = 0; i < taskList.length; i++) {
        if(playload === "selected" && taskList[i].selected) {
            taskList.splice(i--, 1);
        } else if(playload === taskList[i].id) {
            taskList.splice(i--, 1); // 使用数组的splice方法删除元素
        }
    }
};

// 更改事务完成状态:参数为一个事务id时更改id项的事务完成状态
export const handleFinishStatus = id => taskList.forEach(item => id === item.id && (item.finished = !item.finished));

// 更改列表选中状态:参数为一个状态,将所有列表的选中状态都设置为status
export const handleSelectedStatus = status => taskList.forEach(item => item.selected = status);

// 创建计算属性computed,计算待办事务数量
export const unfinishedCount = computed(() => taskList.filter(item => !item.finished).length);

// 创建计算属性computed,计算已完成事务数量
export const finishedCount = computed(() => taskList.filter(item => item.finished).length);
  • app.vue
<script setup>
    import { ref, reactive, computed } from "vue";
    import Entering from "@/components/Entering.vue";
    import TaskList from "@/components/TaskList.vue";
    import BtnBox from "@/components/BtnBox.vue";

</script>

<template>
    <div class="wrapper">
        <!-- 录入 -->
        <Entering />

        <!-- 事务列表 -->
        <TaskList />

        <!-- 按钮区 -->
        <BtnBox/>
    </div>
</template>

<style scoped>
    .wrapper {
        margin: 20px auto 0;
        width: 800px;
    }
</style>
  • Entering.vue
<script setup>
    import { handleEntering } from "@/hooks/useTaskData.js";
    import { ref } from "vue";

    const str = ref("");

    // 处理表单提交事件,使用回车键可触发
    const handleSubmit = (e) => {
        e.preventDefault(); // 取消表单提交默认行为,使其不会重新刷新页面
        if(str.value === "") {
            return;
        }
        const obj = {
            workName: str.value, // 事务名
            finished: false, // 事务的完成状态
            id: new Date().getTime(), // 生成时间戳的id
            selected: false // 是否被选中,用于在checkbox中使用
        }
        handleEntering(obj); // 调用父组件中的录入事件
        str.value = ""; // 清空输入框内容
    }
</script>

<template>
    <div class="form">
        <form @submit="handleSubmit">
            <input type="text" v-model="str" placeholder="输入要做的事务,按回车结束">
        </form>
    </div>
</template>

<style scoped>
    .form {
        width: 100%;
    }
    .form input {
        width: 100%;
        height: 35px;
        border-radius: 5px;
        text-indent: 1em;
    }
</style>
  • TaskList.vue
<script setup>
    import { useTaskData, handleDelete, handleFinishStatus } from "@/hooks/useTaskData.js";

    // 使用useTaskData钩子获取数据列表
    const taskList = useTaskData();

    // 删除
    const delTask = id => handleDelete(id);

    // 更改状态
    const updateFinished = id => handleFinishStatus(id);
</script>

<template>
    <ul class="list">
        <li v-if="!taskList.length">暂无事务,请添加!</li>

        <li v-for="(task, index) in taskList" :key="task.id">
            <span class="cb">
                <input type="checkbox" v-model="task.selected">
            </span>
            <span class="work-name" :class=" task.finished ? 'finished' : '' ">{{ task.workName }}</span>
            <span class="btn">
                <button @click="updateFinished(task.id)">{{ task.finished ? '回退' : '完成' }}</button>
                <button @click="delTask(task.id)">删除</button>
            </span>
        </li>
    </ul>
</template>

<style scoped>
    .list {
        margin: 30px auto 0;
        padding: 10px;
        width: 100%;
        border: 1px solid #ccc;
    }
    .list li {
        display: flex;
        justify-content: space-between;
        margin: 20px 0 0;
        width: 100%;
    }
    .list li:first-child {
        margin-top: 0;
    }
    .cb {
        width: 30px;
    }
    .cb input {
        width: 100%;
        height: 20px;
    }
    .work-name {
        flex: 1;
        padding: 0 10px;
    }
    .work-name.finished {
        text-decoration: line-through;
    }
    .btn {
        display: flex;
        justify-content: space-between;
        width: 100px;
    }
    .btn button {
        width: 48px;
    }
</style>
  • BtnBox.vue
<script setup>
    import { ref } from "vue";
    import { handleDelete, handleSelectedStatus, unfinishedCount, finishedCount } from "@/hooks/useTaskData.js";

    const selected = ref(false);

    // 点击删除
    const delTasks = () => handleDelete("selected");

    // 全选按钮事件
    const handleCb = () => handleSelectedStatus(selected.value);
</script>

<template>
    <div class="btn-box">
        <div class="left">
            <span class="cb">
                <input type="checkbox" v-model="selected" @change="handleCb"> 全选
            </span>
            <button @click="delTasks">删除</button>
        </div>

        <div class="right">
            <span>已完成:{{ finishedCount ? finishedCount : 0 }}</span>
            <span>未完成:{{ unfinishedCount ? unfinishedCount : 0 }}</span>
        </div>
    </div>
</template>

<style scoped>
    .btn-box {
        display: flex;
        justify-content: space-between;
        margin: 30px 0 0;
    }
    input {
        width: 30px;
        height: 20px;
        vertical-align: middle;
    }
    button {
        margin-left: 15px;
        width: 48px;
    }
    .btn-box .right span {
        margin-left: 15px;
    }
</style>

使用hooks不仅可以加强项目的可维护性,同时在某一定程度上在小项目或者某些功能中实现全局状态跨组件互相通信.

上面的taskList这个响应式状态在这个小案例中就是一个跨全局状态, 然后将操作这个响应式状态的方法以export导出。

结束

到此Vue快速入门专栏系列的基本篇已经写完,接下来写的就是Vue路由篇了。基础篇很多详细的知识其实并没讲到,更详细的知识考虑到要对Vue有一定的理解和概念才好说明,会在整个专栏的最后将来补充。