ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue 3 + Element Plus 实现面包屑与路由标签栏的完整实战指南

Vue 3 + Element Plus 实现面包屑与路由标签栏的完整实战指南 1. 从零到一为什么我们需要面包屑和标签栏在后台管理系统或者中后台应用的开发中导航的清晰度和操作的便捷性直接决定了用户体验和开发效率。想象一下你正在一个复杂的商品管理后台里操作从“商品列表”进入“商品详情”再跳转到“编辑商品规格”如果没有清晰的路径指示你很容易迷失在层层叠叠的页面里不知道“自己从哪来要回哪去”。这就是面包屑导航Breadcrumb的核心价值——它像一串面包屑清晰地标记出你从首页到当前页面的完整路径层级。而路由标签栏Tabs View 或 Tag View则是另一个高频需求。它允许用户在一个浏览器窗口内同时打开多个页面路由并以标签页的形式进行切换和管理。这极大地提升了多任务处理的效率用户无需反复点击浏览器的前进后退按钮或者打开多个浏览器标签页所有操作都在一个应用内完成状态得以保持体验非常流畅。Vue 3 的生态中vue-router负责管理所有路由跳转和状态而element-plus作为一套成熟、优雅的 UI 组件库提供了构建这些导航组件所需的基础“积木”。但官方并没有直接提供一个开箱即用的、集成了路由状态的标签栏组件。因此将两者结合打造一个能自动响应路由变化、支持动态增删、并能与面包屑联动的导航系统就成了一个非常典型的实战需求。这不仅仅是写几个组件那么简单它涉及到路由元信息meta的设计、Vuex/Pinia 状态管理、组件间的通信以及一些边界情况的处理是一个能很好检验前端工程化思维的小项目。2. 项目基石路由设计与状态管理规划在动手写代码之前我们必须先规划好数据的结构和流向。一个健壮的导航系统其核心是路由配置和与之对应的状态管理。2.1 路由配置与元信息Meta设计vue-router的路由配置中的meta字段是我们的“信息仓库”我们可以在这里存放页面标题、图标、是否缓存等当然也包括面包屑和标签栏所需的信息。// router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /, redirect: /dashboard, meta: { hidden: true } // 根路由通常不显示在面包屑和标签栏 }, { path: /dashboard, component: () import(/views/dashboard/index.vue), meta: { title: 控制台, icon: Dashboard, affix: true // 固定标签不可关闭 } }, { path: /system, meta: { title: 系统管理, icon: Setting }, children: [ { path: user, component: () import(/views/system/user/index.vue), meta: { title: 用户管理, // breadcrumb 可以自动从嵌套路由结构生成但有时我们需要自定义显示名称 // 这里我们主要依赖 title } }, { path: role, component: () import(/views/system/role/index.vue), meta: { title: 角色管理 } } ] }, { path: /goods, meta: { title: 商品中心, icon: Goods }, children: [ { path: list, component: () import(/views/goods/list/index.vue), meta: { title: 商品列表 } }, { path: detail/:id, component: () import(/views/goods/detail/index.vue), meta: { title: 商品详情, // 动态路由的标题可能需要从接口获取这里可以先设一个基础标题 dynamicTitle: true } } ] } ] const router createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes }) export default router关键设计点解析title: 这是最重要的字段将同时用于页面文档标题、面包屑显示和标签页名称。icon: 用于侧边栏菜单或面包屑的可选图标与 Element Plus 的图标组件名对应。affix: 这是一个自定义标志用于标记某些标签页如“首页”、“控制台”是固定的用户不能关闭它。这是一个提升体验的细节。hidden: 用于标记某些路由如根路由/、登录页不应出现在面包屑或标签栏中。dynamicTitle: 这是一个预留标志表示该页面的标题可能需要根据路由参数如商品ID从后端接口动态获取。这引出了状态管理的需求。2.2 状态管理使用 Pinia 管理标签列表我们需要一个集中式的状态来管理当前打开的所有标签页visitedViews以及缓存的路由组件cachedViews。这里我们选择 Vue 3 官方推荐的 Pinia。// stores/tagsView.js import { defineStore } from pinia import { ref, computed } from vue export const useTagsViewStore defineStore(tagsView, () { // 已访问的视图标签页列表 const visitedViews ref([]) // 需要缓存的视图 name 列表对应路由配置中的 name 字段 const cachedViews ref([]) // 添加标签 const addView (view) { // 去重判断如果已经存在相同 path 的标签则不再添加但可以更新其信息如动态标题 const index visitedViews.value.findIndex(v v.path view.path) if (index -1) { // 新标签推入数组 visitedViews.value.push({ name: view.name, path: view.path, title: view.meta.title || no-name, affix: !!view.meta.affix }) } else { // 已存在可以在这里更新标题例如从接口获取到动态标题后 // visitedViews.value[index].title newTitle } // 处理缓存如果路由配置了 keepAlive: true (我们通常用 meta.noCache 反向判断) if (view.meta !view.meta.noCache view.name) { if (!cachedViews.value.includes(view.name)) { cachedViews.value.push(view.name) } } } // 删除标签 const delView (view) { return new Promise(resolve { const index visitedViews.value.findIndex(v v.path view.path) if (index -1) { visitedViews.value.splice(index, 1) // 同时从缓存中移除 if (view.name) { const cacheIndex cachedViews.value.indexOf(view.name) if (cacheIndex -1) { cachedViews.value.splice(cacheIndex, 1) } } } resolve({ visitedViews: visitedViews.value }) }) } // 关闭其他标签保留当前和固定标签 const delOthersViews (view) { return new Promise(resolve { visitedViews.value visitedViews.value.filter(v v.affix || v.path view.path) // 清理缓存 cachedViews.value cachedViews.value.filter(name { // 保留当前视图和固定标签对应路由的缓存需要根据路由名判断 // 这里简化处理实际可能需要更复杂的匹配逻辑 const retainedView visitedViews.value.find(v v.name name) return retainedView }) resolve() }) } // 关闭所有标签保留固定标签 const delAllViews () { return new Promise(resolve { visitedViews.value visitedViews.value.filter(v v.affix) cachedViews.value cachedViews.value.filter(name { const retainedView visitedViews.value.find(v v.name name) return retainedView }) resolve() }) } // 更新标签标题用于动态路由 const updateVisitedView (view) { for (let v of visitedViews.value) { if (v.path view.path) { v.title view.meta.title break } } } return { visitedViews, cachedViews, addView, delView, delOthersViews, delAllViews, updateVisitedView } })这个 Store 设计涵盖了标签栏的核心操作增、删单个、其他、全部、更新以及缓存管理。注意所有删除操作都承诺Promise了固定标签affix不会被误删这是用户体验的关键。3. 核心组件实现面包屑导航栏有了数据和状态规划我们就可以开始构建组件了。面包屑组件相对独立它主要依赖当前路由对象$route和路由实例$router来生成路径数组。3.1 组件逻辑与模板我们创建一个Breadcrumb.vue组件。它的核心逻辑是根据当前路由的matched数组过滤掉标记为hidden的路由然后生成一个包含路径和标题的数组用于渲染。template el-breadcrumb classapp-breadcrumb separator/ transition-group namebreadcrumb el-breadcrumb-item v-for(item, index) in levelList :keyitem.path span v-ifitem.redirect noRedirect || index levelList.length - 1 classno-redirect {{ item.meta.title }} /span a v-else click.preventhandleLink(item) {{ item.meta.title }} /a /el-breadcrumb-item /transition-group /el-breadcrumb /template script setup import { ref, watch } from vue import { useRoute, useRouter } from vue-router import { compile } from path-to-regexp const route useRoute() const router useRouter() const levelList ref([]) /** * 生成面包屑数据 */ const getBreadcrumb () { // 过滤掉 meta.hidden 为 true 的路由记录 let matched route.matched.filter(item item.meta item.meta.title !item.meta.hidden) const first matched[0] // 如果第一项不是 dashboard或你的首页可以手动添加首页项 // 这里我们假设首页是 dashboard且已包含在 matched 中所以不需要额外添加 // if (first first.path ! /dashboard) { // matched [{ path: /dashboard, meta: { title: Dashboard }}].concat(matched) // } levelList.value matched.filter(item item.meta item.meta.title item.meta.breadcrumb ! false) } /** * 处理面包屑项点击进行路由跳转 */ const handleLink (item) { const { redirect, path } item if (redirect) { router.push(redirect) return } // 对于动态路由如 /detail/:id需要解析出实际参数 // 这里使用 path-to-regexp 的 compile 方法根据当前路由参数生成真实路径 try { const toPath compile(path)(route.params) router.push(toPath) } catch (err) { console.warn(Breadcrumb link compile error:, err) // 如果编译失败回退到原始 path可能不包含参数 router.push(path) } } // 监听路由变化更新面包屑 watch( () route.path, () { getBreadcrumb() }, { immediate: true } ) /script style scoped .app-breadcrumb { display: inline-block; font-size: 14px; line-height: 50px; margin-left: 8px; } .app-breadcrumb .no-redirect { color: #97a8be; cursor: text; } /style关键点与避坑指南route.matched: 这是vue-router提供的属性是一个数组包含了当前路由的所有嵌套路径片段的路由记录。它是生成面包屑的数据源。过滤逻辑我们通过item.meta.hidden来隐藏某些路由如布局路由、根路由。有时你可能还想通过item.meta.breadcrumb: false来更精细地控制。动态路由处理这是面包屑最容易出问题的地方。当点击一个动态路由如/goods/detail/123的面包屑项时你不能直接跳转到/goods/detail/:id而需要跳转到具体的/goods/detail/123。我们使用path-to-regexp库的compile方法利用当前路由的实际参数route.params来“编译”出真实的路径。务必安装这个库npm install path-to-regexp。首页项处理有些设计希望面包屑始终以“首页”开头。如果matched的第一项不是首页你可以像注释中那样手动拼接一个首页项。但更推荐在路由配置中确保首页路由如/dashboard被正确匹配到。3.2 在布局中集成面包屑通常面包屑位于页面顶部导航栏下方。你需要在你的主布局组件如Layout.vue中引入并使用它。!-- Layout.vue 部分代码 -- template div classapp-wrapper !-- 侧边栏等组件 -- div classmain-container div classnavbar !-- 顶部导航栏 -- /div div classapp-main !-- 这里是 router-view页面内容在此渲染 -- router-view v-slot{ Component } transition namefade-transform modeout-in keep-alive :includecachedViews component :isComponent :keykey / /keep-alive /transition /router-view /div /div /div /template script setup import { computed } from vue import { useRoute } from vue-router import { useTagsViewStore } from /stores/tagsView import Breadcrumb from /components/Breadcrumb/index.vue const route useRoute() const tagsViewStore useTagsViewStore() // 为 router-view 计算 key确保同一路由不同参数时组件能重新渲染 const key computed(() route.path) // 从 store 中获取缓存列表提供给 keep-alive 的 include 属性 const cachedViews computed(() tagsViewStore.cachedViews) /script注意上面代码中router-view的使用我们通过keep-alive的:include属性实现了基于路由name的精确缓存这正是我们在 Pinia Store 中维护cachedViews数组的目的。4. 核心组件实现路由标签栏标签栏组件TagsView.vue是交互更复杂的部分。它需要展示标签列表、高亮当前活动标签、支持鼠标右键菜单关闭、关闭其他、关闭所有等并与路由跳转深度绑定。4.1 监听路由变化并同步状态首先我们需要一个全局的路由守卫或在根组件来监听路由变化并将新的路由信息添加到标签列表和缓存中。// permission.js 或 main.js 中 import router from ./router import { useTagsViewStore } from /stores/tagsView router.beforeEach((to, from, next) { // ... 其他权限校验逻辑 const tagsViewStore useTagsViewStore() // 添加当前路由到标签页 if (to.meta to.meta.title) { tagsViewStore.addView(to) } // 如果需要动态设置页面标题 if (to.meta.title) { document.title ${to.meta.title} - 你的系统名 } next() })4.2 标签栏组件模板与基础样式template div idtags-view-container classtags-view-container scroll-pane refscrollPaneRef classtags-view-wrapper router-link v-fortag in visitedViews reftagRefs :keytag.path :classisActive(tag) ? active : :to{ path: tag.path } classtags-view-item click.middlecloseSelectedTag(tag) contextmenu.preventopenMenu(tag, $event) {{ tag.title }} span v-if!tag.affix classel-icon-close click.prevent.stopcloseSelectedTag(tag) / /router-link /scroll-pane !-- 右键菜单 -- ul v-showvisible :style{left:leftpx,top:toppx} classcontextmenu li clickrefreshSelectedTag(selectedTag)刷新/li li v-if!(selectedTag selectedTag.affix) clickcloseSelectedTag(selectedTag)关闭/li li clickcloseOthersTags关闭其他/li li clickcloseAllTags(selectedTag)关闭所有/li /ul /div /template script setup import { ref, computed, nextTick, watch } from vue import { useRoute, useRouter } from vue-router import ScrollPane from ./ScrollPane.vue // 一个支持横向滚动的容器组件 import { useTagsViewStore } from /stores/tagsView const route useRoute() const router useRouter() const tagsViewStore useTagsViewStore() const visitedViews computed(() tagsViewStore.visitedViews) // 当前活动标签判断 const isActive (tag) { return tag.path route.path } // 关闭标签逻辑 const closeSelectedTag (view) { tagsViewStore.delView(view).then(({ visitedViews }) { // 如果关闭的是当前活动标签需要自动跳转到最后一个标签 if (isActive(view)) { const latestView visitedViews.slice(-1)[0] if (latestView) { router.push(latestView.path) } else { // 如果所有标签都关了跳转到首页或默认页 router.push(/) } } }) } // 右键菜单相关状态 const visible ref(false) const top ref(0) const left ref(0) const selectedTag ref({}) const openMenu (tag, e) { selectedTag.value tag visible.value true left.value e.clientX top.value e.clientY } const closeMenu () { visible.value false } // 右键菜单操作 const refreshSelectedTag (view) { // 刷新当前页的一种常见做法先跳转到一个空路由再跳回来或者使用 router.go(0) // 更优雅的方式是控制 router-view 的 key 变化这里我们简单重载 tagsViewStore.delCachedView(view).then(() { const { fullPath } route router.replace(/redirect fullPath) // 需要配置一个 /redirect 路由用于重定向刷新 }) closeMenu() } const closeOthersTags () { if (selectedTag.value) { tagsViewStore.delOthersViews(selectedTag.value).then(() { // 关闭其他后如果当前活动标签不是选中的标签则跳转到选中的标签 if (!isActive(selectedTag.value)) { router.push(selectedTag.value.path) } }) } closeMenu() } const closeAllTags () { tagsViewStore.delAllViews().then(() { // 关闭所有后跳转到首页或默认页 router.push(/) }) closeMenu() } // 点击页面其他地方关闭右键菜单 onMounted(() { document.addEventListener(click, closeMenu) }) onUnmounted(() { document.removeEventListener(click, closeMenu) }) // 监听路由变化滚动当前活动标签到可视区域 const scrollPaneRef ref() const tagRefs ref([]) watch( () route.path, async () { await nextTick() // 等待 DOM 更新 const activeTag tagRefs.value.find(tagRef isActive(tagRef.to)) if (activeTag scrollPaneRef.value) { scrollPaneRef.value.scrollToCurrentTag(activeTag.$el) } } ) /script style langscss scoped .tags-view-container { height: 34px; width: 100%; background: #fff; border-bottom: 1px solid #d8dce5; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04); .tags-view-wrapper { .tags-view-item { display: inline-block; position: relative; cursor: pointer; height: 26px; line-height: 26px; border: 1px solid #d8dce5; color: #495060; background: #fff; padding: 0 8px; font-size: 12px; margin-left: 5px; margin-top: 4px; text-decoration: none; :first-of-type { margin-left: 15px; } .active { background-color: #409EFF; color: #fff; border-color: #409EFF; ::before { content: ; background: #fff; display: inline-block; width: 8px; height: 8px; border-radius: 50%; position: relative; margin-right: 2px; } } .el-icon-close { width: 16px; height: 16px; vertical-align: middle; border-radius: 50%; text-align: center; transition: all .3s cubic-bezier(.645, .045, .355, 1); transform-origin: 100% 50%; :before { transform: scale(.6); display: inline-block; vertical-align: middle; } :hover { background-color: #b4bccc; color: #fff; } } } } .contextmenu { margin: 0; background: #fff; z-index: 3000; position: absolute; list-style-type: none; padding: 5px 0; border-radius: 4px; font-size: 12px; font-weight: 400; color: #333; box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3); li { margin: 0; padding: 7px 16px; cursor: pointer; :hover { background: #eee; } } } } /style4.3 滚动容器组件ScrollPane由于标签数量可能很多需要一个可以横向滚动的容器。这里我们实现一个简单的ScrollPane.vue。template div refscrollContainerRef classscroll-container wheel.passivehandleScroll div refscrollWrapperRef classscroll-wrapper slot / /div /div /template script setup import { ref, onMounted, onBeforeUnmount } from vue const scrollContainerRef ref() const scrollWrapperRef ref() const handleScroll (e) { const eventDelta e.wheelDelta || -e.deltaY * 40 const $container scrollContainerRef.value const $containerWidth $container.offsetWidth const $wrapper scrollWrapperRef.value const $wrapperWidth $wrapper.offsetWidth if (eventDelta 0) { $container.scrollLeft Math.max(0, $container.scrollLeft - eventDelta) } else { if ($container.scrollLeft $containerWidth $wrapperWidth) { // 已经滚动到最右边 return } $container.scrollLeft Math.min($wrapperWidth, $container.scrollLeft - eventDelta) } } // 滚动到指定标签 const scrollToCurrentTag (currentTag) { const $container scrollContainerRef.value const $containerWidth $container.offsetWidth const $wrapper scrollWrapperRef.value const tagOffsetLeft currentTag.offsetLeft const tagOffsetWidth currentTag.offsetWidth // 如果标签在可视区域左侧之外 if (tagOffsetLeft $container.scrollLeft) { $container.scrollLeft tagOffsetLeft } // 如果标签在可视区域右侧之外 else if (tagOffsetLeft tagOffsetWidth $container.scrollLeft $containerWidth) { $container.scrollLeft tagOffsetLeft tagOffsetWidth - $containerWidth } } // 防止在滚动容器内触发页面滚动 onMounted(() { scrollContainerRef.value.addEventListener(DOMMouseScroll, handleScroll, false) scrollContainerRef.value.addEventListener(mousewheel, handleScroll, false) }) onBeforeUnmount(() { scrollContainerRef.value.removeEventListener(DOMMouseScroll, handleScroll) scrollContainerRef.value.removeEventListener(mousewheel, handleScroll) }) defineExpose({ scrollToCurrentTag }) /script style scoped .scroll-container { white-space: nowrap; position: relative; overflow: hidden; width: 100%; height: 100%; .scroll-wrapper { padding: 0; margin: 0; display: inline-block; } } /style4.4 集成到布局与动态标题更新最后将TagsView组件也放入主布局中并处理动态标题更新的场景。!-- 在 Layout.vue 的 navbar 或 app-main 上方添加 -- template div classapp-wrapper !-- ... -- div classmain-container div classnavbar !-- 顶部导航栏 -- /div tags-view / !-- 添加标签栏 -- div classapp-main !-- ... -- /div /div /div /template script setup import TagsView from /components/TagsView/index.vue // ... 其他导入 /script对于动态标题例如从商品详情接口获取商品名作为标题你需要在页面组件如GoodsDetail.vue中获取数据后更新路由的meta.title并同步到 Store。!-- views/goods/detail/index.vue -- script setup import { useRoute } from vue-router import { useTagsViewStore } from /stores/tagsView const route useRoute() const tagsViewStore useTagsViewStore() // 假设从接口获取数据 const fetchGoodsDetail async (id) { const res await api.getGoodsDetail(id) // 更新当前路由的 meta 信息注意直接修改 route.meta 可能不是响应式的 // 更好的做法是更新 store 中的 visitedView const currentView { path: route.path, meta: { ...route.meta, title: ${res.data.name} - 商品详情 } } tagsViewStore.updateVisitedView(currentView) // 同时更新页面标题 document.title ${res.data.name} - 商品详情 - 你的系统名 } /script5. 进阶优化与常见问题排查实现基本功能后我们还需要考虑一些边界情况和优化点这些都是实战中容易踩坑的地方。5.1 路由缓存keep-alive的精细化控制我们之前通过cachedViews数组和keep-alive的include属性实现了缓存。但有时你希望某些页面永远不缓存如表单填写页或者离开后立即销毁。这可以通过路由的meta.noCache标志来控制。// 路由配置 { path: edit, component: () import(/views/goods/edit.vue), meta: { title: 编辑商品, noCache: true } // 不缓存 }在addView逻辑中我们已经判断了!view.meta.noCache。确保你的页面组件都设置了唯一的name选项与路由配置的name一致这是include匹配的关键。5.2 同一路由不同参数的标签处理这是一个经典问题打开/goods/detail/1和/goods/detail/2我们希望显示两个标签但它们的路由name可能相同如果路由配置了name: GoodsDetailpath不同。我们的设计是以path作为标签的唯一标识这自然支持了同一路由不同参数。但在缓存时如果两个标签对应同一个组件namekeep-alive可能会混淆它们的状态。解决方案一推荐为动态路由组件设置唯一的key。我们在Layout.vue中已经使用了:keyroute.path这确保了不同path的同一组件被视作不同实例从而分别缓存。解决方案二更精细的缓存管理。可以修改 Store将缓存标识从name改为name JSON.stringify(route.params)之类的组合键但这需要更复杂的keep-alive包装器实现成本较高。第一种方案在大多数场景下已足够。5.3 右键菜单“刷新”功能的实现我们上面提到的刷新方式跳转到/redirect路由需要配置一个简单的重定向路由。// router/index.js { path: /redirect, component: Layout, // 你的布局组件 meta: { hidden: true }, children: [ { path: /redirect/:path(.*), component: () import(/views/redirect/index.vue) } ] }!-- views/redirect/index.vue -- script setup import { useRoute, useRouter } from vue-router const route useRoute() const router useRouter() const { params, query } route const { path } params router.replace({ path: / path, query }) /script template div / /template这个组件的作用是作为一个“中转站”接收目标路径然后立即跳转过去从而触发目标组件的重新渲染。5.4 标签滚动与激活状态同步我们已经在TagsView组件中通过watch路由变化并调用scrollToCurrentTag来实现滚动。确保ScrollPane组件的scrollToCurrentTag方法被正确暴露和调用。有时在快速切换或初始加载时DOM 可能未完全渲染导致滚动计算不准使用nextTick确保在 DOM 更新后执行滚动逻辑是关键。5.5 性能考量与内存管理打开的标签页越多visitedViews和cachedViews就越大。虽然keep-alive能提升切换速度但也会占用更多内存。对于内存敏感的应用可以考虑设置标签页上限超过后自动关闭最久未访问的。在beforeRouteLeave钩子中根据业务逻辑判断某些页面离开时是否主动清除其缓存tagsViewStore.delCachedView。使用Pinia的持久化插件如pinia-plugin-persistedstate将visitedViews存储到localStorage这样刷新页面后标签页状态不会丢失但缓存状态cachedViews通常不建议持久化。6. 与 Element Plus Table 等组件的联动实践最新的网络热词提到了element-plus el-table。在实际后台系统中标签栏和表格的联动非常常见。例如在商品列表页一个标签页中点击某行进入详情页新开标签页编辑后返回列表页希望列表页能保留之前的搜索条件、分页状态等。这超出了导航组件本身的范围但我们可以给出设计思路状态持久化将表格的查询参数、分页信息等存储在 Pinia Store 或localStorage/sessionStorage中key 可以与路由的fullPath或name关联。当再次进入该路由时从存储中恢复状态。利用路由查询参数将表格的筛选条件同步到路由的query中如?page1statusactive。这样只要标签页的pathquery组合唯一它就能作为一个独立的状态被保存和恢复。vue-router会复用同一组件实例你可以在组件的watch里监听route.query的变化来重新拉取表格数据。keep-alive的生命周期被缓存的组件会触发activated和deactivated钩子。你可以在activated钩子中检查是否需要刷新数据例如从详情页返回且详情页进行了修改操作从而实现数据同步。一个简单的例子在列表页组件中script setup import { onActivated, watch } from vue import { useRoute } from vue-router const route useRoute() const tableQuery ref({}) // 从路由 query 初始化或恢复查询条件 const initQueryFromRoute () { const { page 1, status } route.query tableQuery.value { page: Number(page), status } fetchTableData() } // 监听路由 query 变化例如浏览器前进后退 watch(() route.query, initQueryFromRoute, { immediate: true }) // 当从缓存中再次激活时可以执行一些逻辑比如检查数据新鲜度 onActivated(() { console.log(列表页被激活) // 可以在这里触发一个数据检查或轻量级刷新 }) /script通过将导航状态标签页、路由状态参数和组件状态表格数据三者有机结合就能构建出体验非常流畅的中后台应用。整个vue-routerelement-plus实现面包屑和标签栏的过程本质上是对路由流、状态流和UI交互的一次深度整合理解了这个脉络你就能应对各种复杂的导航和状态管理需求了。
返回列表