第 3 课:任务页怎么把列表、筛选、表单、弹窗串起来
这节课要解决的问题是:
为什么 TasksView.vue 明明只是一个页面,却能同时练到很多 Vue 核心能力?
答案很直接,因为这个页面已经把中后台项目里最高频的一组能力揉在一起了:
- 数据列表
- 条件筛选
- 统计卡片
- 表单输入
- 弹窗交互
- 新增数据
- 计算属性
如果这一个页面你真正吃透了,后面做大部分业务页都会轻松很多。
这节课主要看哪些文件
src/views/TasksView.vuesrc/mock/tasks.tssrc/types/task.ts
先记住任务页的整体结构
你可以先把整个页面压缩成一句话:
假数据 -> 放进响应式列表 -> 根据筛选条件生成新列表 -> 用表格渲染 -> 用弹窗新增任务 -> 新任务回流到列表
如果展开一点,就是:
- 先从
mock/tasks.ts拿到初始任务数据 - 把这份数据放进
tasks这个响应式列表里 - 用户输入关键字、选择状态、选择优先级
computed根据这些条件生成filteredTasks- 页面用
el-table渲染filteredTasks - 用户点击"新增任务"打开弹窗
- 填完表单后点击提交
- 新任务插入到
tasks里 - 因为
filteredTasks依赖tasks,所以表格自动更新
这条链你先记住,后面所有细节其实都是在服务它。
先看 src/types/task.ts
这个文件的作用非常重要:
先把"任务长什么样"定义清楚
当前定义了这些内容:
TaskStatus
任务状态只能是"待开始、进行中、待评审、已完成"TaskPriority
优先级只能是"高、中、低"TaskItem
单个任务对象必须有哪些字段
这里你要建立一个 TypeScript 的核心意识:
类型不是为了麻烦你,而是为了提前限制错误
比如:
- 你不能把状态写成"马上做"
- 你不能把优先级写成"非常高"
- 你不能漏掉
title或dueDate
所以类型文件做的事不是显示页面,
而是"先把数据规则说清楚"。
再看 src/mock/tasks.ts
这里放的是任务假数据。
你要把它理解成:
先不用后端接口,先用固定数据练页面逻辑
这对初学者非常重要,因为如果你一开始就把注意力分散到接口请求、跨域、后端返回结构上,Vue 本身反而学不扎实。
当前这份假数据有两个教学价值:
- 它让你可以马上练列表渲染
- 它让你可以马上练筛选逻辑
所以你可以先把它看成"任务页的数据种子"。
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 就跟着变。
为什么这里同时用了 ref 和 reactive
这是初学者最容易混淆的一点。
你先这样记:
用 ref 的情况
适合单个值:
- 字符串
- 布尔值
- 数字
- 当前选中值
- 弹窗开关
比如:
keyworddialogVisiblenextTaskId
用 reactive 的情况
适合一整组相关字段组成的对象。
比如当前页面的 taskForm:
titleassigneedueDatestatusprioritydescription
这些字段天然属于同一张表单,
所以放到一个 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。
新增任务流程是怎么跑通的
新增任务其实是一个标准的小型业务闭环:
- 点击按钮
- 弹窗打开
- 用户填写表单
- 点击提交
- 校验数据
- 插入列表
- 关闭弹窗
- 重置表单
- 页面自动刷新显示
这几步你以后做任何"新增数据"页面,都会反复遇到。
第一步:打开弹窗
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 又依赖:
taskskeywordstatusFilterpriorityFilter
所以只要 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. 辅助函数负责"翻译展示规则"
当前页面有两个函数:
getStatusTypegetPriorityType
它们本质上不是在处理业务数据,
而是在做一件事:
把业务值翻译成 UI 展示规则
比如:
- "已完成" ->
success - "高" ->
danger
这是很常见的页面层辅助函数写法。
任务页最值得你学的设计思维
这页除了语法,更重要的是它体现了一种很标准的页面拆解思路:
1. 先分清原始数据和派生数据
- 原始数据是
tasks - 派生数据是
filteredTasks和taskStats
2. 先分清业务状态和界面状态
- 业务状态:任务列表、表单内容
- 界面状态:弹窗是否打开
3. 把"输入"和"展示"分开
- 输入在表单里
- 展示在表格和统计卡片里
4. 把"数据规则"和"显示规则"分开
- 数据规则:状态值、优先级值、字段结构
- 显示规则:标签颜色、表格列、卡片文案
如果你开始能这样拆页面,说明你已经不再是"看到什么写什么"的阶段了。
这节课最容易混淆的点
1. tasks 和 filteredTasks 到底谁才是主数据
正确理解:
tasks是主数据filteredTasks是根据主数据算出来的结果
所以新增、删除、编辑都应该改 tasks,
而不是直接改 filteredTasks。
2. 为什么统计卡片不直接写死
因为统计结果本来就依赖任务列表变化。
如果写死,新增任务后卡片就不准了。
3. 为什么弹窗开关也要放进响应式状态
因为界面显示与隐藏本身就是页面的一部分状态。
它不是"附属品",而是业务流程的一环。
4. 为什么表单要重置两次思考
你要分别考虑两种场景:
- 提交成功后
- 用户手动关闭后
只处理一种,通常都不够完整。
你现在应该能回答的 12 个问题
TaskItem这个类型文件解决了什么问题?- 为什么任务页先从假数据开始,而不是直接接接口?
tasks为什么是主数据源?keyword、statusFilter、priorityFilter属于哪一类状态?taskForm为什么更适合用reactive?filteredTasks为什么要用computed?- 为什么"筛选后的列表"不应该单独存一份?
taskStats为什么也适合用computed?v-model在表单里到底做了什么?- 为什么新增任务后表格会自动刷新?
- 为什么关闭弹窗时也要重置表单?
- 表格自定义插槽和普通列分别适合什么场景?
这节课的动手练习
练习 1
打开 src/views/TasksView.vue,给筛选逻辑再加一个条件:
- 只显示负责人是当前用户的任务
目的:
练习在现有 computed 里继续叠加筛选条件。
练习 2
把新增任务时的默认优先级从:
ts
priority: '中'
改成:
ts
priority: '高'
然后运行项目,看看弹窗默认选项是否变化。
目的:
理解表单默认值来自哪里。
练习 3
在 src/mock/tasks.ts 里多加两条任务数据,然后看看:
- 统计卡片会不会变化
- 表格会不会变化
- 筛选结果会不会变化
目的:
理解"一个主数据源变化,会驱动多个展示区域一起变化"。
练习 4
把关键字筛选从只搜:
- 标题
- 描述
扩展成还能搜负责人。
目的:
练习自己扩展筛选逻辑,而不是只会照着写。
这节课的复习结论
最后把任务页压缩成 8 句话:
types/task.ts先定义了任务数据规则。mock/tasks.ts提供了前端练习用的初始任务数据。tasks是页面的主数据源。keyword、状态筛选、优先级筛选属于条件状态。filteredTasks和taskStats都是派生数据,所以适合用computed。- 弹窗和表单也是状态,不能只盯着列表本身。
- 新增任务的本质是"校验 -> 插入主数据 -> 重置状态"。
- 这个页面已经覆盖了后台业务页最常见的一组 Vue 能力。