0基础学习VUE3 第 3 课:任务页怎么把列表、筛选、表单、弹窗串起来

第 3 课:任务页怎么把列表、筛选、表单、弹窗串起来

这节课要解决的问题是:

为什么 TasksView.vue 明明只是一个页面,却能同时练到很多 Vue 核心能力?

答案很直接,因为这个页面已经把中后台项目里最高频的一组能力揉在一起了:

  • 数据列表
  • 条件筛选
  • 统计卡片
  • 表单输入
  • 弹窗交互
  • 新增数据
  • 计算属性

如果这一个页面你真正吃透了,后面做大部分业务页都会轻松很多。


这节课主要看哪些文件

  1. src/views/TasksView.vue
  2. src/mock/tasks.ts
  3. src/types/task.ts

先记住任务页的整体结构

你可以先把整个页面压缩成一句话:

假数据 -> 放进响应式列表 -> 根据筛选条件生成新列表 -> 用表格渲染 -> 用弹窗新增任务 -> 新任务回流到列表

如果展开一点,就是:

  1. 先从 mock/tasks.ts 拿到初始任务数据
  2. 把这份数据放进 tasks 这个响应式列表里
  3. 用户输入关键字、选择状态、选择优先级
  4. computed 根据这些条件生成 filteredTasks
  5. 页面用 el-table 渲染 filteredTasks
  6. 用户点击"新增任务"打开弹窗
  7. 填完表单后点击提交
  8. 新任务插入到 tasks
  9. 因为 filteredTasks 依赖 tasks,所以表格自动更新

这条链你先记住,后面所有细节其实都是在服务它。


先看 src/types/task.ts

这个文件的作用非常重要:

先把"任务长什么样"定义清楚

当前定义了这些内容:

  • TaskStatus
    任务状态只能是"待开始、进行中、待评审、已完成"
  • TaskPriority
    优先级只能是"高、中、低"
  • TaskItem
    单个任务对象必须有哪些字段

这里你要建立一个 TypeScript 的核心意识:

类型不是为了麻烦你,而是为了提前限制错误

比如:

  • 你不能把状态写成"马上做"
  • 你不能把优先级写成"非常高"
  • 你不能漏掉 titledueDate

所以类型文件做的事不是显示页面,

而是"先把数据规则说清楚"。


再看 src/mock/tasks.ts

这里放的是任务假数据。

你要把它理解成:

先不用后端接口,先用固定数据练页面逻辑

这对初学者非常重要,因为如果你一开始就把注意力分散到接口请求、跨域、后端返回结构上,Vue 本身反而学不扎实。

当前这份假数据有两个教学价值:

  1. 它让你可以马上练列表渲染
  2. 它让你可以马上练筛选逻辑

所以你可以先把它看成"任务页的数据种子"。


TasksView.vue 的第一层重点:状态到底分成了几类

这个页面的状态其实可以分成 4 类:

1. 原始任务数据

ts 复制代码
const tasks = ref<TaskItem[]>(initialTaskList.map((task) => ({ ...task })))

这是整个页面最核心的数据源。

它表示:

  • 初始值来自假数据
  • 但不是直接拿原数组引用
  • 而是复制出一份新的响应式数组

为什么要复制?

因为这样后面你新增任务时,不会直接改动原始假数据文件里的引用。


2. 筛选条件状态

这几个:

ts 复制代码
const keyword = ref('')
const statusFilter = ref('全部')
const priorityFilter = ref('全部')

它们不是任务本身,而是"用户的筛选条件"。

这就是前端里很常见的一种状态分类方法:

  • 一类状态表示"数据本身"
  • 一类状态表示"用户当前的操作条件"

这两个一定要区分开。


3. 弹窗状态

ts 复制代码
const dialogVisible = ref(false)

这个状态的作用非常单纯:

控制新增任务弹窗是否显示

它提醒你一个重要观念:

UI 是否可见,本身也是状态

很多初学者一开始只盯着数据,却忽略了"界面开关"也是响应式状态的一部分。


4. 表单状态

ts 复制代码
const taskForm = reactive({...})

这表示:

  • 新增任务表单里的所有字段
  • 都集中放在一个对象里管理

你可以把它理解成:

这个对象就是弹窗里那张表单在 JavaScript 里的镜像

输入框改什么,taskForm 就跟着变。


为什么这里同时用了 refreactive

这是初学者最容易混淆的一点。

你先这样记:

ref 的情况

适合单个值:

  • 字符串
  • 布尔值
  • 数字
  • 当前选中值
  • 弹窗开关

比如:

  • keyword
  • dialogVisible
  • nextTaskId

reactive 的情况

适合一整组相关字段组成的对象。

比如当前页面的 taskForm

  • title
  • assignee
  • dueDate
  • status
  • priority
  • description

这些字段天然属于同一张表单,

所以放到一个 reactive 对象里最自然。


页面里真正的核心是 computed

这页最值得反复看的,是这个思路:

不要手动维护"筛选后的列表",而是用计算属性自动推导

也就是这个:

ts 复制代码
const filteredTasks = computed(() => ...)

这行代码的意义非常大。

它说明:

  • 原始数据只有一份 tasks
  • 筛选条件也只有一份
  • 最终展示结果不需要单独再存一份
  • 直接由原始数据 + 条件自动算出来

这就是 Vue 和响应式系统非常重要的一个思想:

能推导出来的数据,就尽量不要重复存储


filteredTasks 到底做了什么

它的过滤逻辑分成 3 步:

1. 关键字匹配

ts 复制代码
task.title.includes(keyword.value.trim()) ||
task.description.includes(keyword.value.trim())

表示:

  • 只要标题里包含关键字
  • 或者描述里包含关键字
  • 就算命中

2. 状态匹配

ts 复制代码
statusFilter.value === '全部' || task.status === statusFilter.value

表示:

  • 如果当前筛选是"全部",那就直接通过
  • 否则必须和任务状态完全相等

这是很常见的"全部 / 指定值"筛选写法。


3. 优先级匹配

逻辑和状态一样:

ts 复制代码
priorityFilter.value === '全部' || task.priority === priorityFilter.value

4. 三个条件一起成立才保留

最后:

ts 复制代码
return matchesKeyword && matchesStatus && matchesPriority

这表示:

必须同时满足关键字、状态、优先级这三组条件

所以这段代码本质上就是:

多条件组合筛选

这是后台项目里非常非常常见的一类业务逻辑。


为什么 taskStats 也用 computed

你会发现统计卡片不是写死的,也是算出来的:

ts 复制代码
const taskStats = computed(() => [...])

这是因为统计卡片也属于"派生数据"。

比如:

  • 全部任务数,来自 tasks.length
  • 高优先级数量,来自 tasks.filter(...)
  • 待评审数量,也来自 tasks.filter(...)
  • 筛选结果数量,来自 filteredTasks.length

也就是说:

卡片本身不是独立数据,它只是对已有数据的再表达

这就是为什么它非常适合用 computed


新增任务流程是怎么跑通的

新增任务其实是一个标准的小型业务闭环:

  1. 点击按钮
  2. 弹窗打开
  3. 用户填写表单
  4. 点击提交
  5. 校验数据
  6. 插入列表
  7. 关闭弹窗
  8. 重置表单
  9. 页面自动刷新显示

这几步你以后做任何"新增数据"页面,都会反复遇到。


第一步:打开弹窗

ts 复制代码
function openCreateDialog() {
  dialogVisible.value = true
}

这段代码非常简单,但它代表一个关键思路:

点击行为不直接改 DOM,而是改状态

然后 Vue 根据状态变化,自动更新界面。

这就是响应式开发和传统手动操作 DOM 的根本区别。


第二步:填写表单

模板里用了大量 v-model,比如:

vue 复制代码
<el-input v-model="taskForm.title" />

你要把 v-model 理解成:

输入框里的值 和 JavaScript 里的状态 双向同步

也就是说:

  • 用户打字,taskForm.title 会变
  • 如果代码里改了 taskForm.title,输入框显示也会跟着变

这是表单页面最核心的一个机制。


第三步:提交时先做校验

ts 复制代码
if (!taskForm.title.trim()) {
  ElMessage.warning('请先输入任务标题。')
  return
}

这一步很重要,因为它说明:

不是用户点了提交就一定创建成功,应该先校验输入是否合法

这是表单逻辑的基本意识。

你以后会继续学到更完整的校验:

  • 必填校验
  • 长度校验
  • 格式校验
  • 异步校验

现在先把"先判断,再提交"的节奏吃透。


第四步:为什么要 unshift

提交成功后,当前代码用了:

ts 复制代码
tasks.value.unshift({...})

unshift 的意思是:

把新任务插入到数组最前面

这样做的体验是:

  • 新增后立刻能在表格最上方看到结果
  • 不需要滚到最底部找新数据

这其实是一个很实用的交互细节。


第五步:为什么新增后表格会自动刷新

因为表格绑定的是:

vue 复制代码
<el-table :data="filteredTasks" />

filteredTasks 又依赖:

  • tasks
  • keyword
  • statusFilter
  • priorityFilter

所以只要 tasks 变了,
filteredTasks 就会重新计算,

然后表格就会自动重新渲染。

这就是响应式系统最有价值的地方:

你不用手动"通知表格刷新",数据变了,界面自己会更新


为什么提交后还要重置表单

当前代码里调用了:

ts 复制代码
resetTaskForm()

它的作用是:

  • 清空上一次输入的标题
  • 恢复默认负责人
  • 恢复默认日期
  • 恢复默认状态
  • 恢复默认优先级
  • 清空描述

如果不重置,下一次打开弹窗时就会残留上次的输入内容。

这就是很典型的"表单脏状态"问题。

所以你要记住:

表单提交成功后,通常要考虑是否需要重置


为什么弹窗关闭时也要重置

模板里写了:

vue 复制代码
@closed="resetTaskForm"

这表示:

不只是提交成功要重置,

连用户直接关闭弹窗,也要把表单恢复干净。

这是一种很好的"状态收尾"习惯。


表格这部分你要重点学什么

任务表格里包含了几种非常常见的页面能力:

1. 普通列渲染

比如:

vue 复制代码
<el-table-column prop="title" label="任务标题" />

这表示:

  • 这一列显示 row.title
  • 表头名称叫"任务标题"

2. 自定义插槽列

比如状态列:

vue 复制代码
<template #default="{ row }">
  <el-tag :type="getStatusType(row.status)" round>{{ row.status }}</el-tag>
</template>

这表示:

  • 默认列显示已经不够了
  • 需要根据状态动态渲染不同颜色标签

这里你要理解:

当简单数据显示不够用时,就用插槽自定义单元格内容

这会是你以后做表格时的高频技能。


3. 辅助函数负责"翻译展示规则"

当前页面有两个函数:

  • getStatusType
  • getPriorityType

它们本质上不是在处理业务数据,

而是在做一件事:

把业务值翻译成 UI 展示规则

比如:

  • "已完成" -> success
  • "高" -> danger

这是很常见的页面层辅助函数写法。


任务页最值得你学的设计思维

这页除了语法,更重要的是它体现了一种很标准的页面拆解思路:

1. 先分清原始数据和派生数据

  • 原始数据是 tasks
  • 派生数据是 filteredTaskstaskStats

2. 先分清业务状态和界面状态

  • 业务状态:任务列表、表单内容
  • 界面状态:弹窗是否打开

3. 把"输入"和"展示"分开

  • 输入在表单里
  • 展示在表格和统计卡片里

4. 把"数据规则"和"显示规则"分开

  • 数据规则:状态值、优先级值、字段结构
  • 显示规则:标签颜色、表格列、卡片文案

如果你开始能这样拆页面,说明你已经不再是"看到什么写什么"的阶段了。


这节课最容易混淆的点

1. tasksfilteredTasks 到底谁才是主数据

正确理解:

  • tasks 是主数据
  • filteredTasks 是根据主数据算出来的结果

所以新增、删除、编辑都应该改 tasks

而不是直接改 filteredTasks


2. 为什么统计卡片不直接写死

因为统计结果本来就依赖任务列表变化。

如果写死,新增任务后卡片就不准了。


3. 为什么弹窗开关也要放进响应式状态

因为界面显示与隐藏本身就是页面的一部分状态。

它不是"附属品",而是业务流程的一环。


4. 为什么表单要重置两次思考

你要分别考虑两种场景:

  • 提交成功后
  • 用户手动关闭后

只处理一种,通常都不够完整。


你现在应该能回答的 12 个问题

  1. TaskItem 这个类型文件解决了什么问题?
  2. 为什么任务页先从假数据开始,而不是直接接接口?
  3. tasks 为什么是主数据源?
  4. keywordstatusFilterpriorityFilter 属于哪一类状态?
  5. taskForm 为什么更适合用 reactive
  6. filteredTasks 为什么要用 computed
  7. 为什么"筛选后的列表"不应该单独存一份?
  8. taskStats 为什么也适合用 computed
  9. v-model 在表单里到底做了什么?
  10. 为什么新增任务后表格会自动刷新?
  11. 为什么关闭弹窗时也要重置表单?
  12. 表格自定义插槽和普通列分别适合什么场景?

这节课的动手练习

练习 1

打开 src/views/TasksView.vue,给筛选逻辑再加一个条件:

  • 只显示负责人是当前用户的任务

目的:

练习在现有 computed 里继续叠加筛选条件。


练习 2

把新增任务时的默认优先级从:

ts 复制代码
priority: '中'

改成:

ts 复制代码
priority: '高'

然后运行项目,看看弹窗默认选项是否变化。

目的:

理解表单默认值来自哪里。


练习 3

src/mock/tasks.ts 里多加两条任务数据,然后看看:

  • 统计卡片会不会变化
  • 表格会不会变化
  • 筛选结果会不会变化

目的:

理解"一个主数据源变化,会驱动多个展示区域一起变化"。


练习 4

把关键字筛选从只搜:

  • 标题
  • 描述

扩展成还能搜负责人。

目的:

练习自己扩展筛选逻辑,而不是只会照着写。


这节课的复习结论

最后把任务页压缩成 8 句话:

  1. types/task.ts 先定义了任务数据规则。
  2. mock/tasks.ts 提供了前端练习用的初始任务数据。
  3. tasks 是页面的主数据源。
  4. keyword、状态筛选、优先级筛选属于条件状态。
  5. filteredTaskstaskStats 都是派生数据,所以适合用 computed
  6. 弹窗和表单也是状态,不能只盯着列表本身。
  7. 新增任务的本质是"校验 -> 插入主数据 -> 重置状态"。
  8. 这个页面已经覆盖了后台业务页最常见的一组 Vue 能力。
相关推荐
试试勇气2 小时前
Linux学习笔记(十九)--生产消费模型与线程安全
java·笔记·学习
蜡台2 小时前
Monorepo 架构管理多个子项目实现
前端·javascript·vue.js·pnpm·monorepo
guojb8242 小时前
从0开始设计一个树和扁平数组的双向同步方案
前端·数据结构·vue.js
前端小趴菜052 小时前
Vue项目,前端如何来做登录密码加密传输?
前端·javascript·vue.js
tangdou3690986552 小时前
图文并茂安装Claude Code 以及配置 Coding Plan 教程
前端·人工智能·后端
arvin_xiaoting2 小时前
OpenClaw学习总结_II_频道系统_4:Slack集成详解
前端·学习·自动化·llm·ai agent·飞书机器人·openclaw
qq_389600132 小时前
pads-logic 学习笔记
笔记·嵌入式硬件·学习·硬件工程·pcb工艺
CHU7290352 小时前
让知识传递更顺畅:在线教学课堂APP的功能设计
前端·人工智能·小程序