第 37 课:任务详情抽屉上一条 / 下一条切换
第 37 课:任务详情抽屉上一条 / 下一条切换
这一课我们继续沿着“任务管理页主线”往后推进,把上一课已经做好的“任务详情抽屉”再往真实后台系统推进一步。
这次的目标很明确:
- 在任务详情抽屉里增加“上一条 / 下一条”切换
- 不离开当前页面,不跳转详情页
- 切换时继续保持
taskId地址栏同步 - 刷新后仍然能恢复到当前详情任务
- 补上单元测试、E2E 测试和课程文档
这一课一句话在做什么?
这一课我们让“任务详情抽屉”从“只能看当前一条任务”升级成“可以沿着当前工作上下文继续向前 / 向后处理任务”。
更准确地说,是这句话:
详情抽屉里的上一条 / 下一条,不应该按原始数组乱跳,也不应该只按当前页切片跳,而应该按“当前筛选 + 当前排序后的结果”来跳。
这句话是这节课最重要的工程结论。
为什么后台系统里很常见这个能力?
很多后台页面里,用户并不是只看一条任务,然后就结束。
更常见的是这种流程:
- 在列表里筛出一批任务
- 打开其中一条看详情
- 看完后顺手切到下一条继续处理
- 一直保持在同一个工作上下文里
这比“看完一条就关抽屉,再回列表,再点下一条”更顺。
所以这节课本质上不是多加两个按钮,而是在训练你理解:
- 什么叫“当前工作上下文”
- 什么叫“上下文内导航”
- 为什么导航顺序必须和用户当前看到的任务顺序一致
这节课最关键的设计结论
1. 详情导航要基于sortedTasks
这次我们没有把详情抽屉导航基于:
- 原始
tasks - 当前页
paginatedTasks
而是基于:
sortedTasks
原因很重要:
tasks只是原始数据顺序,不一定等于用户当前看到的顺序paginatedTasks只是当前页切片,太窄了,翻页后的任务就不在导航上下文里sortedTasks才是“当前筛选 + 当前排序后的完整结果”
也就是说:
详情抽屉切换的是当前结果集里的相邻任务,不是当前分页切片里的相邻任务。
这才更符合后台系统里“保持工作上下文连续”的体验。
2. 详情抽屉仍然只是展示层
这次TaskDetailDrawer.vue只做两件事:
- 显示当前位置摘要
- 抛出“上一条 / 下一条”的事件
它不自己决定要切到哪条任务。
真正的详情上下文仍然在页面级useTasksPage.ts里管理。
这说明一个很重要的分层原则:
子组件表达用户意图,页面级 composable 管理真实业务状态。
3. 新入口继续复用旧逻辑
这次新增了:
openPreviousTaskDetail()openNextTaskDetail()
但它们最后仍然复用已有的:
openTaskDetail(task)
这说明我们没有重新发明一套详情打开逻辑,而是在已有页面级状态基础上扩展入口。
这就是健康的工程演进方式。
这次主要改了哪些文件?
这节课主要改了 7 个地方:
src/composables/useTasksPage.tssrc/views/TasksView.vuesrc/components/tasks/TaskDetailDrawer.vuesrc/composables/__tests__/useTasksPage.spec.tse2e/pages/TasksPage.tse2e/app.spec.tsdocs/README.md
另外新增了本节文档:
docs/37-task-detail-drawer-prev-next-navigation.md
在useTasksPage.ts里学什么?
文件:
src/composables/useTasksPage.ts
这次在页面级 composable 里新增了 6 个和详情导航直接相关的状态 / 动作:
taskDetailNavigationTasksactiveTaskDetailPositionactiveTaskDetailTotalCounthasPreviousTaskDetailhasNextTaskDetailopenPreviousTaskDetail()openNextTaskDetail()
其中最关键的是这句思路:
先把当前可切换任务集合定义清楚,再谈上一条 / 下一条。
1.taskDetailNavigationTasks
它直接复用了sortedTasks。
意思是:
- 先筛选
- 再排序
- 然后把这份结果当成详情抽屉的线性导航上下文
2.activeTaskDetailPosition
它把当前详情任务在导航结果里的索引转成更适合展示的“第几条”。
这里有一个细节很值得学:
- 找得到任务时:返回
1 ~ N - 找不到任务时:返回
0
这让 UI 层可以明确地区分:
- 当前任务正常位于结果里
- 当前任务已经不在当前筛选结果里
3.hasPreviousTaskDetail/hasNextTaskDetail
这两个计算属性负责让抽屉里的按钮天然知道:
- 什么时候该启用
- 什么时候该禁用
这样模板层就不需要自己算边界。
4.openPreviousTaskDetail()/openNextTaskDetail()
这两个函数没有自己手写一套“切换详情状态”的新逻辑。
它们做的事很简单:
- 根据当前位置拿到目标任务
- 校验目标任务存在
- 复用
openTaskDetail(task)
这说明:
新功能优先扩展旧路径,而不是复制旧路径。
在TaskDetailDrawer.vue里学什么?
文件:
src/components/tasks/TaskDetailDrawer.vue
这一课里,详情抽屉主要新增了两类能力。
1. 新增导航相关 props
这次抽屉接收了:
currentPositiontotalCounthasPrevioushasNext
这些都不是抽屉自己算的,而是父层传进来的。
这能说明一个非常重要的组件设计原则:
展示组件尽量消费外部状态,而不是自己偷偷推导业务上下文。
2. 新增导航相关 emits
这次抽屉新增了:
previousnext
用户点击按钮后,抽屉只负责告诉父组件:
- 我想切到上一条
- 我想切到下一条
它不直接改activeTaskDetailId。
这说明:
子组件负责“发意图”,父组件和 composable 负责“改状态”。
3. 新增当前位置摘要
这次抽屉顶部增加了一块位置摘要,例如:
第 2 / 3 条
这块 UI 看起来很小,但表达的是很重要的上下文信息:
- 当前不是孤立的一条任务
- 当前任务是整套结果里的第几条
- 用户能预期自己还能往前还是往后处理
这类信息密度很符合后台系统风格。
在TasksView.vue里学什么?
文件:
src/views/TasksView.vue
页面层这次做的事情不复杂,但很关键:
- 从
useTasksPage()里拿出导航相关状态 - 把这些状态传给
TaskDetailDrawer - 把
@previous/@next事件接回页面级动作
注意这里一个很值得你形成习惯的点:
如果页面层不需要额外提示、不需要额外分支,它可以直接把 composable 动作挂到模板事件上。
比如这次:
@previous="openPreviousTaskDetail"@next="openNextTaskDetail"
这说明页面层不一定每次都要包一层函数。
什么时候要包?
- 需要
ElMessage - 需要确认框
- 需要做额外校验
什么时候可以直接挂?
- 已有动作已经足够表达这次意图
单元测试这次测了什么?
文件:
src/composables/__tests__/useTasksPage.spec.ts
这次新增了两组重点测试。
1. 验证导航遵循当前排序顺序
这组测试先把任务排序切成:
截止日期从近到远
然后打开排序结果里的中间任务,再验证:
- 上一条切到的是排序后的上一条
- 下一条切到的是排序后的下一条
这很重要,因为它确保我们不是按原始数组乱跳。
2. 验证导航限制在当前筛选子集里
这组测试把状态筛选切到:
待开始
让详情导航上下文收敛成:
4 / 5 / 6
然后验证:
- 第一条不能再往前
- 中间项可以前后切换
- 最后一条不能再往后
这说明我们真的把“详情导航上下文”约束在当前结果集里了。
E2E 这次测了什么?
文件:
e2e/pages/TasksPage.tse2e/app.spec.ts
这次新增了详情导航相关 Page Object 方法:
expectTaskDetailPosition()openPreviousTaskDetail()openNextTaskDetail()
然后补了一条完整 E2E 测试链路:
- 先筛到
待开始 - 打开第 5 条任务详情
- 断言当前位置是
第 2 / 3 条 - 切到下一条,变成第 6 条
- 再切回上一条,回到第 5 条
- 再切到上一条,回到第 4 条
- 验证每一步
taskId都会跟着改 - 刷新页面
- 验证详情抽屉、位置摘要、查询参数都能恢复
这条测试链路同时覆盖了:
- 真实按钮点击
- 详情切换
- 地址栏同步
- 刷新恢复
- 当前筛选上下文保持稳定
你现在真正应该学会什么?
如果你学完这一课,只记住“加了两个按钮”,其实远远不够。
你更应该真正学会下面这 4 件事:
1. 上一条 / 下一条本质上是“上下文导航”
它不是普通按钮,也不是单纯的 UI 特效。
它表达的是:
用户正在沿着当前结果集继续工作。
2. 导航顺序必须和用户当前视角一致
这也是为什么这次必须基于:
- 当前筛选
- 当前排序
而不是原始数组。
3. 分页切片不等于导航上下文
这节课很适合你建立一个高级一点的前端意识:
paginatedTasks是页面显示切片sortedTasks是当前完整结果集- 详情导航应该更接近结果集,而不是更接近页面切片
这是很典型的“不同状态有不同职责”。
4. 新功能优先复用旧动作
这次上一条 / 下一条最后都复用了:
openTaskDetail(task)
说明真正好的代码结构,不是每来一个功能就多写一套逻辑,而是能自然扩展已有路径。
这一课完成后,任务页又更像真实后台了吗?
是的,而且这一步很关键。
现在这个任务页已经不只是:
- 能筛选
- 能排序
- 能分页
- 能看详情
而是开始具备一种真实后台常见的“连续处理体验”:
- 打开一条
- 看完继续切下一条
- 不中断上下文
这已经非常接近真实业务里的审核台、工单台、任务处理台体验了。
