{T}

顶部 Tabs 关闭删除与默认路由回退

概述

这一节补齐顶部 Tabs 的关闭删除能力。关键是认识到“关闭 Tabs”不是纯数组删除,而是一次导航状态迁移——它可能同时影响 Tabs 列表、当前激活项、页面路由三处。逻辑的核心分叉点是“删的是不是当前激活项”,极端边界是“删掉最后一个标签后系统回退到哪个默认入口”。主链路补完后,更聪明的回退策略和首页保护属于后续体验增强。

把 Tabs 关闭当成状态迁移来处理,能避免“列表删了、高亮没变、路由没切”这类三者失同步的经典 bug。

学习目标

  • 区分“关闭非当前标签”与“关闭当前标签”两种场景,后者必须重算 current 并切路由。
  • tab-remove 回调拿到被关闭标签的 name,让删除逻辑清晰定位。
  • 保持 removeRoute store action 职责纯粹:只按标识删除,导航回退交给外层。
  • 删除当前标签后必须显式 router.push(),否则 Tabs 高亮变了而页面内容不变。
  • 删除最后一个标签时回退到首页默认入口,并确保首页是真正可导航的完整路由。
  • 认识批量关闭(其他/左/右)可复用纯删除 action,只需参数化过滤
  • 理解常驻(affix)标签应在批量关闭中被保留

一、关闭是一次导航状态迁移,不是纯数组删除

用户点小叉的动作看似只是删掉当前项,实际至少同时影响三件事:Tabs 列表少一项、当前激活项可能发生变化、页面路由可能需要立即切换。所以不能把“关闭 Tabs”理解成纯数组操作,它本质上是一次导航状态迁移。关闭当前标签和非当前标签是两种不同场景,关键不是删得掉,而是删完之后界面状态仍然一致——Tabs、store、route 三者必须保持同一步调。

二、tab-remove 回调拿到的就是被关闭标签的 name

确认点击关闭时 Element Plus Tabs 会把当前 Tab 的标识传出来。el-tab-panename 已统一成 item.name,所以 tab-remove 回调里最有价值的数据就是这个 name。有了它就能去 store 删除对应 route、判断是不是当前激活项、必要时执行路由回退。只要 name 绑定的是稳定路由 name,删除逻辑就很清晰;不要在 remove 时依赖 label 文本做定位。

Vue SFC
<el-tabs @tab-remove="handleTabRemove" />
ts
function handleTabRemove(name: string) {
  emit("tabRemove", name)
}

三、removeRoute 职责纯粹:只删数据,不管回退

tabsStore 新增删除方法,思路是直接对 tabs 做一次 filter(),把匹配项排除掉。它的职责非常纯粹——只负责维护集合本身,不应一上来承担所有 UI 切换、副作用和回退策略。更合理的分工是 store action 删数据、外层布局决定删完后跳到哪里。过滤条件应与当前 Tabs 的 name 标识一致;后续支持“关闭其他/左侧/右侧”时,这类基础删除 action 也更好复用。

ts
function removeRoute(name: string) {
  tabs.value = tabs.value.filter((item) => item.name !== name)
}

四、删非当前只需改列表,删当前必须重算 current

第一个分支判断很关键:关闭的不是当前标签,当前页面没必要跳,只删除列表即可;关闭的是当前激活标签,当前页面已不该继续对应被删的那项,必须至少给 current 赋新值。只删列表但不改 current,Tabs 激活态一定会失真。current 是顶部 Tabs 和当前路由对齐的桥梁,删当前项时必须优先处理它。

ts
if (tabsStore.current === removedName) {
  tabsStore.current = nextName
}

先判断是不是当前项,再决定要不要跳转,是这节逻辑的关键分叉点。

五、删当前后默认切到第一个 Tabs,是简单稳定的回退策略

第一版回退策略很朴素:删除当前标签后,直接把 current 指向剩余 Tabs 的第 0 项。这不是唯一方案,但有两个优点——实现简单、行为稳定,不需要额外维护“上一个访问页”栈。在课程阶段足以支撑可用版本。很多后台系统也会选择“关闭当前后切到相邻页签”,后续可继续优化。课程当前更关注“先能工作”,不是“回退策略最聪明”。

ts
tabsStore.current = tabsStore.tabs[0]?.name as string

六、仅改 current 不够,删当前后必须显式 router.push()

一个典型问题:Tabs 高亮切过去了,但页面内容没切过去。因为 current 只是 Tabs 当前激活项状态,不会自动替你完成路由切换。所以删除当前标签并重设 current 之后,还必须 router.push({ name: tabsStore.current }),顶部状态和页面内容才能同时一致。视图激活态和真实路由是两回事,不能默认前者会自动驱动后者;删除当前标签后的路由切换是明确副作用,应显式触发。

七、删最后一个标签:回退到系统默认入口而非剩余项

把逻辑往极端推进:用户把所有 Tabs 都删光时,没有“剩余第 0 项”可拿,逻辑进入新分支——回到系统默认菜单入口。在当前项目结构里通常是首页(menuData.menupath === "/" 的那一项)。删除最后一个标签是单独边界条件,不能混在普通删除逻辑里想当然处理;这类“系统默认入口”最好有统一来源,而不是在多处各写硬编码。

八、首页兜底能否稳定工作,取决于它是不是完整可导航路由

如果首页只是一个父级壳子、下面还有子菜单,光跳到 / 还不一定够。要保证首页真正可用,往往需要首页路由本身就能承载内容,或已配置 redirect。这与前面“点击顶层面包屑时需要 redirect”同一类问题——兜底入口必须是真正可落地的页面。这类默认入口若带子菜单,提早配好 redirect 会省很多状态回退问题;Tabs 删除逻辑最终依赖路由体系本身的完整性。

九、主链路已补齐,剩下是更聪明的删除策略

到收尾时顶部 Tabs 主链路已成型:可新增、可持久化、可点击切换、可关闭删除、可在极端情况下回到默认页。还没做的是体验增强项:删除当前页后切前一个还是后一个、首页是否允许关闭、关闭最后一个是否总回首页、菜单新增时的更多规则。这些属于“更聪明的策略层”,建立在主链路稳定前提上。这类导航系统越往后越像产品策略题,而非纯技术实现题;当前阶段以可用优先,策略精细化可后续迭代。

十、从单删到批量关闭:关闭其他/左侧/右侧

主链路稳定后,最常见的体验增强是“批量关闭”:右键菜单或按钮提供“关闭其他”“关闭左侧”“关闭右侧”。实现上复用 removeRoute 的纯删除职责——传入一个待保留或待删除的 name 集合,对 tabs 做对应过滤即可,外层再对“是否涉及当前项”做一次统一回退判断。批量关闭不引入新状态模型,只是把单删的分支逻辑参数化,这也是当初把 removeRoute 设计得纯粹的价值所在。

ts
function closeOthers(current: string) {
  tabs.value = tabs.value.filter((item) => item.name === current || item.affix)
}

常见问题

问题原因解决方案
点击小叉没反应基础组件没透传 tab-removeHeaderTabs 监听 @tab-removeemit 给外层
关当前后 Tabs 变了但页面没切只更新 current,没执行路由跳转重设 current 后同步 router.push()
删最后一个时报错或越界直接读 tabs[0],没处理空数组tabs.length === 0 单独做首页回退兜底
回首页后仍是空白页首页只是壳路由,缺 redirect确保首页可展示或提前配好 redirect
关非当前项当前页被意外切走删除逻辑没先判断 current !== removedName区分“删当前项”和“删其他项”两条分支
Tabs 删后状态不稳定列表、current、route 没一起更新删除逻辑同时维护三者
关闭其他后当前页乱跳批量删除没统一回退判断复用单删分支,对当前项做回退
固定的首页标签被关掉没区分常驻标签批量关闭保留 affix 项
关闭当前后想回相邻页只切到第 0 项记录访问栈取相邻项
关闭最后一个总回首页业务希望留空回退策略按需定制
removeRoute 里写了跳转职责不纯难复用只删数据,回退交外层
Tabs 高亮和内容不同步忘了 router.push删当前后显式 push
首页壳路由跳过去空白缺 redirect配好首页 redirect
用 label 定位被删项label 会变用稳定 name 做标识

延伸阅读