第 35 课:任务详情抽屉与地址栏同步

张开发
2026/4/22 1:33:24 15 分钟阅读

分享文章

第 35 课:任务详情抽屉与地址栏同步
第 35 课任务详情抽屉与地址栏同步这一课我们继续沿着“任务管理页主线能力增强”往下推进。前面几课我们已经让任务页具备了表格视图、卡片视图、看板视图筛选、排序、分页用户偏好持久化看板拖拽改状态这一课继续把它做得更像真实后台让用户可以在不离开任务列表的前提下打开单条任务详情抽屉并把当前详情状态同步到地址栏这样刷新、分享、前进后退时都能恢复同一条任务。这节课一句话在做什么这一课我们完成了 7 件事给任务页新增了“任务详情抽屉”交互。让表格、卡片、看板三种视图都能进入详情抽屉。在页面级状态里新增“当前正在查看哪条任务”的上下文。把详情状态同步到地址栏里的taskId查询参数。刷新页面后详情抽屉可以根据taskId自动恢复。关闭抽屉后会自动把taskId从 URL 里移除。补上了单元测试、E2E 测试和课程文档。这一课最重要的设计结论这一课最重要的结论是任务详情抽屉不是一个“纯 UI 开关”而是一个“页面级上下文状态”。也就是说真正要被管理的不是抽屉开了没开而是当前正在查看哪一条任务只要你把这个核心状态想清楚后面的很多设计都会自然很多打开抽屉本质是设置当前详情任务 id关闭抽屉本质是清空当前详情任务 idURL 同步本质是把当前详情任务 id 写进taskId刷新恢复本质是从taskId反推页面上下文为什么后台系统很适合“列表页 详情抽屉”很多初学者第一次做详情时最容易默认想到的是点击列表跳转详情页看完再返回列表这当然可以做但后台系统里还有一种非常常见的模式保留当前列表在右侧打开详情抽屉用户继续在同一上下文里查看、编辑、删除、关闭这种设计很适合后台原因有 3 个。1. 它能保留列表上下文当你在任务列表里筛选、排序、翻页以后用户已经处在一个明确上下文里我现在看的是哪一批任务这批任务按什么顺序展示当前我关注的是哪条记录如果这时直接跳转到单独详情页用户就暂时离开了当前上下文。而详情抽屉的优势是当前列表还在当前筛选还在当前滚动位置大致还在用户看完可以直接继续处理下一条这对后台效率很重要。2. 它特别适合“先看一眼再决定下一步”很多后台场景里用户打开详情并不是为了做深度浏览而是为了快速确认这条任务是谁负责的截止日期是什么说明内容是什么我要不要马上编辑要不要删除这种“轻量查看 快速决策”的场景用抽屉比整页跳转更顺手。3. 它让列表页更像一个工作台你要慢慢建立一个意识真实后台页面不只是“展示数据”更像“处理事务的工作台”。工作台的特点是当前上下文尽量不丢关键动作尽量就地完成用户尽量少跳页所以这一课的详情抽屉不是一个“装饰组件”而是任务页继续进化成后台工作台的重要一步。这一课在useTasksPage里做了什么文件src/composables/useTasksPage.ts这一课的核心还是先落在页面级 composable而不是先写 UI。1. 新增了页面级详情状态这一课在任务页里新增了 3 个关键状态activeTaskDetailIdactiveTaskDetailisTaskDetailVisible它们分别表示当前详情上下文指向哪条任务当前详情上下文对应的完整任务对象当前抽屉是否应该打开这里最重要的是第一个activeTaskDetailId因为它才是真正的“单一事实来源”。剩下两个都只是从它推导出来有 id抽屉就开能根据 id 找到任务对象就展示这条任务详情这就是很典型的 Vue 状态设计思路尽量把核心状态缩成最小再通过 computed 推导出其他展示状态。2. 新增了openTaskDetail()和closeTaskDetail()这一课页面级 composable 新增了两个动作openTaskDetail(task)closeTaskDetail()它们做的事很简单打开时把当前任务 id 写进页面状态关闭时把当前任务 id 清空但你要注意一个工程点这里打开详情的入口不是写成isDrawerOpen true而是写成activeTaskDetailId task.id这说明我们真正管理的是当前详情上下文而不是某个弹层显示标记3. 详情目标失效时会自动清理上下文这一课还补了一个很重要的边界如果当前详情抽屉指向的任务已经不存在了就自动把activeTaskDetailId清空这个场景很真实比如你正打开着某条任务详情这条任务被删了如果不做清理就会出现抽屉还开着但上下文已经失效所以我们在任务 id 列表的监听里顺手把这类脏上下文清掉。这一步非常符合后台页面的真实需求页面状态不只要能打开还要能在数据失效时正确收尾。这一课为什么要把详情状态同步到 URL如果详情抽屉只存在于组件内部状态里会有 3 个明显问题。1. 刷新页面后详情会丢如果你只是本地写了activeTaskDetailId 3但没有把它同步到地址栏那么一刷新页面这个状态就没了。用户会遇到这种体验刚刚明明在看第 3 条任务一刷新就回到“没有打开详情”这不利于真实工作流。2. 当前页面不可分享如果地址栏里没有taskId你就没法把“我现在看到的这条任务”分享给别人。而一旦地址栏里有了?taskId3这个链接就具备了“可恢复当前上下文”的能力。3. 浏览器前进后退失去意义后台系统里把状态写进 URL 的一个重要原因是浏览器的历史记录才有业务意义如果详情状态不进 URL那么用户打开一条任务详情再关闭再前进后退浏览器很难帮你恢复这个上下文。所以这一课把详情状态写进地址栏不是为了“炫技”而是为了让页面状态真正具备刷新恢复能力链接分享能力浏览器历史协同能力这一课在useTaskDetailQuerySync里做了什么文件src/composables/useTaskDetailQuerySync.ts这是这一课新增的 query 同步 composable。它只负责一件事在页面详情状态和 URL 的taskId之间做双向同步1. 为什么要单独新建一个 composable因为我们之前已经有一个useTaskFilterQuerySync它负责的是关键字状态筛选优先级筛选排序分页而现在新增的详情抽屉状态虽然也要同步到 URL但它并不是“筛选状态”的一部分。所以最清晰的做法不是把它强塞进原来的 composable而是单独拆出useTaskDetailQuerySync这样职责就会非常清楚一个 composable 管列表筛选 query一个 composable 管详情上下文 query这就是很典型的“按领域拆分 side effect”的方式。2. 它负责从 URL 读取taskId当页面首次进入或者浏览器地址发生变化时它会读取route.query.taskId解析成合法的正整数 id写回页面级taskDetailId这样如果用户直接访问/tasks?taskId3页面就能自动进入“第 3 条任务详情已打开”这就是深链接恢复能力。3. 它负责把页面状态写回 URL当页面里当前详情任务 id 改变时它会打开详情时把taskId写进 query关闭详情时把taskId从 query 里移除而且这里使用的是router.replace不是router.push为什么因为详情抽屉的开合更像“同一页面内的上下文变化”不是“完整页面跳转”。如果每一次开关详情都 push 一条历史记录浏览器历史会很吵。所以这里用 replace 更符合这个场景。4. 它会保留其他 query 参数这一步特别重要。这一课里任务页已经同时有两类 query筛选相关keyword、status、priority、sort、page详情相关taskId如果详情同步时不小心直接覆盖整个 query就会把筛选条件冲掉。所以useTaskDetailQuerySync在写回地址栏时会先复制当前已有 query再只改动taskId也就是说它只拥有taskId而不是拥有整个 query 对象。这是非常重要的前端工程意识一个副作用模块只应该改自己负责的那部分状态。这一课还修正了一个很关键的 query 竞态问题文件src/composables/useTaskFilterQuerySync.ts这一课不只是新增了详情 query 同步还顺手修掉了一个更细的边界问题。场景是这样的用户正在输入关键字关键字 query 还在防抖等待用户这时先点开了某条任务详情地址栏新增了taskId如果筛选 query 同步逻辑写得不严谨就可能出现两个问题还没落地的关键字被冲掉或者关键字后面落地时把刚加上的taskId覆盖掉这一课做了两个关键修正1. 关键字防抖落地时总是基于“最新地址栏”重新构建 query这样即使后面新加了taskId关键字真正同步时也不会把它覆盖掉。2. 筛选 query 的路由监听只监听自己关心的那些 key也就是只监听keywordstatusprioritysortpage这样当地址栏只是新增了taskId筛选 composable 就不会误以为“筛选条件变了”也不会把本地关键字状态回滚掉。这一步非常值得你记住因为它背后体现的是一个通用原则多个 composable 共同操作同一个 URL 时一定要先划清每个人拥有哪一段 query。这一课在视图组件上做了什么1.TaskDetailDrawer.vue文件src/components/tasks/TaskDetailDrawer.vue这是这一课新增的详情抽屉组件。它负责展示任务标题状态和优先级标签负责人、截止日期、任务编号任务说明学习提示它的特点是自己不维护任务数据源只接收visible、loading、task通过update:visible、edit、delete把动作抛给父层这说明这个组件是一个很标准的展示型 交互事件上抛型组件2.TaskTable.vue文件src/components/tasks/TaskTable.vue这一课把表格里的任务标题改成了可点击按钮并新增view-detail事件。这样点击表格标题时页面层就能进入详情抽屉流程。3.TaskCardList.vue文件src/components/tasks/TaskCardList.vue卡片视图也做了同样的事标题可点击抛出view-detail这一步很重要因为详情能力不应该只属于表格视图。如果一个后台页面支持多种主视图那么“查看详情”这种核心动作通常应该在多种视图里都保持一致。4.TaskKanbanBoard.vue文件src/components/tasks/TaskKanbanBoard.vue看板视图同样新增了标题按钮view-detail而且在按钮上用了click.stop这样点击标题时只会进入详情不会和看板卡片上的其他交互互相干扰。5.TasksView.vue文件src/views/TasksView.vue这一课页面层新增了 3 类事情接入useTaskDetailQuerySync把三种视图里的view-detail统一接到页面层把TaskDetailDrawer正式挂到页面模板里这里你要继续强化一个非常重要的分层意识子组件只负责抛事件页面层负责接线composable 负责页面级状态query composable 负责副作用同步这就是我们前面一直在练的组件层页面层状态层副作用层继续分层协作。这一课最值得你学会的前端思想1. 详情抽屉本质上是页面上下文不只是显隐开关以后你做这类能力时优先先想当前页面到底在“查看谁”而不是只想某个抽屉是不是打开只要把“当前上下文对象”想清楚很多状态会自然变简单。2. URL 不是只给路由跳转用的它也是页面状态容器前面我们已经把筛选条件排序分页写进过 URL。这一课再往前一步你要开始真正理解URL 不只是页面路径也是页面状态的外部表达。能被写进 URL 的状态通常意味着它值得刷新恢复值得分享值得和浏览器历史协作3. 多个 query 同步模块可以并存但必须有边界这一课其实是很典型的真实工程场景一个页面不止一种 query 状态只要出现多个同步模块就一定要明确谁负责哪些参数谁不应该碰哪些参数否则最后很容易出现互相覆盖状态回滚竞态 bug这也是为什么我们这次专门修了关键字防抖和taskId共存的问题。这一课补了哪些测试1. 单元测试文件src/composables/__tests__/useTaskDetailQuerySync.spec.tssrc/composables/__tests__/useTaskFilterQuerySync.spec.tssrc/composables/__tests__/useTasksPage.spec.ts新增覆盖了首次进入时是否能从taskId回填详情状态打开和关闭详情时是否会正确写回taskId是否能保留无关 query 参数非法taskId是否会被清理当关键字防抖挂起时新增taskId是否不会冲掉关键字状态当前详情任务被删除后详情上下文是否会自动清空这一层主要验证query 同步规则是否正确多个 query 模块是否能和平共处页面级详情状态收尾是否正确2. E2E 测试文件e2e/pages/TasksPage.tse2e/app.spec.ts新增覆盖了从任务表打开详情抽屉断言taskId已经进入 URL刷新页面断言详情抽屉能根据 URL 自动恢复关闭详情抽屉断言taskId从 URL 清掉这一层主要验证不是只有 composable 自测通过而是真实浏览器页面里的标题点击、抽屉显示、地址栏更新、刷新恢复整条链路都打通了这一课改了哪些文件src/composables/useTaskFilterQuerySync.tssrc/composables/useTaskDetailQuerySync.tssrc/composables/useTasksPage.tssrc/views/TasksView.vuesrc/components/tasks/TaskDetailDrawer.vuesrc/components/tasks/TaskTable.vuesrc/components/tasks/TaskCardList.vuesrc/components/tasks/TaskKanbanBoard.vuesrc/composables/__tests__/useTaskFilterQuerySync.spec.tssrc/composables/__tests__/useTaskDetailQuerySync.spec.tssrc/composables/__tests__/useTasksPage.spec.tse2e/pages/TasksPage.tse2e/app.spec.tsdocs/35-task-detail-drawer-and-query-sync.mddocs/README.md这一课最值得你真正记住什么如果你只记住“现在任务可以打开详情抽屉了”那还不够。你更应该记住下面这 6 点详情抽屉真正管理的不是开关而是“当前详情任务上下文”。页面级状态应该尽量收缩成最小核心值比如activeTaskDetailId。值得刷新恢复、分享和历史协同的状态通常应该进入 URL。多个 query 同步模块可以共存但必须只改自己负责的那部分参数。无关 query 的变化不应该误伤本地状态特别是防抖场景。当详情目标失效时页面要主动清理失效上下文。这一课的验证命令完成后至少应该验证npmrun type-checknpmrun test:unit ----runnpmrun lintnpmrun test:e2e ----projectchromium一句话总结这一课真正要学会的是任务详情抽屉不只是一个右侧弹层而是“列表页上下文 页面级状态 URL 同步 刷新恢复”这一整套后台页面能力的组合。

更多文章