标签组件
DVHA 框架提供了标签页系统,支持多标签页浏览,提升用户在复杂管理界面中的操作体验。
功能特点
- 📑 多标签浏览 - 支持在多个页面间快速切换
- 💾 状态保持 - 自动保持页面状态和滚动位置
- 🔒 锁定标签 - 支持锁定重要标签防止误关闭
- 🎯 智能缓存 - 基于 KeepAlive 的智能组件缓存
- 🔄 动态管理 - 支持添加、删除、切换标签页
- 🎨 自定义样式 - 完全可定制的标签页外观
- ⚡ 高性能 - 优化的渲染和内存管理
核心组件
DuxTabRouterView
标签页路由视图组件,负责标签页的渲染和状态管理:
vue
<template>
<div class="layout">
<!-- 标签栏 -->
<div class="tabs-bar">
<tab-bar />
</div>
<!-- 标签页内容 -->
<div class="tabs-content">
<DuxTabRouterView />
</div>
</div>
</template>
<script setup>
import { DuxTabRouterView } from '@duxweb/dvha-core'
</script>
工作原理
DuxTabRouterView
组件的核心特性:
- 自动标签管理: 监听路由变化,自动添加新标签
- 组件缓存: 使用 KeepAlive 缓存组件实例
- 内存优化: 自动清理已关闭标签的缓存
- 过渡动画: 内置标签切换的过渡效果
标签栏组件
基础标签栏
vue
<template>
<div class="tab-bar">
<div
v-for="tab in tabs"
:key="tab.path"
:class="['tab-item', { active: tab.path === current }]"
@click="changeTab(tab.path)"
>
<span class="tab-label">{{ tab.label }}</span>
<!-- 锁定图标 -->
<i
v-if="tab.meta?.lock"
class="tab-lock"
@click.stop="lockTab(tab.path)"
>🔒</i>
<!-- 关闭按钮 -->
<button
v-else
class="tab-close"
@click.stop="delTab(tab.path)"
>×</button>
</div>
</div>
</template>
<script setup>
import { useTabStore } from '@duxweb/dvha-core'
import { useRouter } from 'vue-router'
const router = useRouter()
const tabStore = useTabStore()
const { tabs, current, delTab, changeTab, lockTab } = tabStore
const handleTabClick = (path) => {
changeTab(path, (tab) => {
router.push(tab.path)
})
}
</script>
<style scoped>
.tab-bar {
display: flex;
background: #fff;
border-bottom: 1px solid #e8e8e8;
}
.tab-item {
display: flex;
align-items: center;
padding: 8px 16px;
cursor: pointer;
border-right: 1px solid #e8e8e8;
transition: background-color 0.2s;
}
.tab-item:hover {
background-color: #f5f5f5;
}
.tab-item.active {
background-color: #1890ff;
color: white;
}
.tab-close {
margin-left: 8px;
background: none;
border: none;
cursor: pointer;
font-size: 16px;
line-height: 1;
}
</style>
带右键菜单的标签栏
vue
<template>
<div class="tab-bar">
<div
v-for="tab in tabs"
:key="tab.path"
:class="['tab-item', { active: tab.path === current }]"
@click="changeTab(tab.path)"
@contextmenu.prevent="showContextMenu($event, tab)"
>
<span class="tab-label">{{ tab.label }}</span>
<button
v-if="!tab.meta?.lock"
class="tab-close"
@click.stop="delTab(tab.path)"
>×</button>
</div>
<!-- 右键菜单 -->
<div
v-if="contextMenu.visible"
:style="contextMenuStyle"
class="context-menu"
>
<div @click="refreshTab">刷新</div>
<div @click="closeTab">关闭</div>
<div @click="closeOthers">关闭其他</div>
<div @click="closeLeft">关闭左侧</div>
<div @click="closeRight">关闭右侧</div>
<div @click="toggleLock">
{{ contextMenu.tab?.meta?.lock ? '解锁' : '锁定' }}
</div>
</div>
</div>
<!-- 点击遮罩关闭菜单 -->
<div
v-if="contextMenu.visible"
class="context-overlay"
@click="hideContextMenu"
/>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useTabStore } from '@duxweb/dvha-core'
import { useRouter } from 'vue-router'
const router = useRouter()
const tabStore = useTabStore()
const contextMenu = ref({
visible: false,
x: 0,
y: 0,
tab: null
})
const contextMenuStyle = computed(() => ({
left: `${contextMenu.value.x}px`,
top: `${contextMenu.value.y}px`
}))
const showContextMenu = (event, tab) => {
contextMenu.value = {
visible: true,
x: event.clientX,
y: event.clientY,
tab
}
}
const hideContextMenu = () => {
contextMenu.value.visible = false
}
const refreshTab = () => {
const tab = contextMenu.value.tab
router.go(0) // 刷新当前页面
hideContextMenu()
}
const closeTab = () => {
const tab = contextMenu.value.tab
tabStore.delTab(tab.path, (nextTab) => {
router.push(nextTab.path)
})
hideContextMenu()
}
const closeOthers = () => {
const tab = contextMenu.value.tab
tabStore.delOther(tab.path, () => {
router.push(tab.path)
})
hideContextMenu()
}
const closeLeft = () => {
const tab = contextMenu.value.tab
tabStore.delLeft(tab.path)
hideContextMenu()
}
const closeRight = () => {
const tab = contextMenu.value.tab
tabStore.delRight(tab.path)
hideContextMenu()
}
const toggleLock = () => {
const tab = contextMenu.value.tab
tabStore.lockTab(tab.path)
hideContextMenu()
}
</script>
标签页布局
完整布局示例
vue
<template>
<div class="admin-layout">
<!-- 顶部导航 -->
<header class="header">
<div class="header-left">
<img src="/logo.png" class="logo" />
<h1>管理后台</h1>
</div>
<div class="header-right">
<user-dropdown />
</div>
</header>
<!-- 主体区域 -->
<div class="main">
<!-- 侧边菜单 -->
<aside class="sidebar">
<main-menu />
</aside>
<!-- 内容区域 -->
<div class="content">
<!-- 标签栏 -->
<div class="tabs-bar">
<tab-bar />
</div>
<!-- 页面内容 -->
<div class="page-content">
<DuxTabRouterView />
</div>
</div>
</div>
</div>
</template>
<script setup>
import { DuxTabRouterView } from '@duxweb/dvha-core'
import TabBar from './components/TabBar.vue'
import MainMenu from './components/MainMenu.vue'
import UserDropdown from './components/UserDropdown.vue'
</script>
<style scoped>
.admin-layout {
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
height: 60px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
background: #001529;
color: white;
}
.main {
flex: 1;
display: flex;
overflow: hidden;
}
.sidebar {
width: 200px;
background: #001529;
overflow-y: auto;
}
.content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.tabs-bar {
height: 40px;
border-bottom: 1px solid #e8e8e8;
}
.page-content {
flex: 1;
overflow: hidden;
}
</style>
响应式布局
vue
<template>
<div class="responsive-layout">
<!-- 移动端顶部栏 -->
<header class="mobile-header" v-if="isMobile">
<button @click="toggleSidebar">☰</button>
<h1>{{ currentPageTitle }}</h1>
</header>
<!-- 桌面端布局 -->
<div class="desktop-layout" v-else>
<!-- 正常的桌面端布局 -->
</div>
<!-- 移动端标签切换器 -->
<div class="mobile-tabs" v-if="isMobile">
<select v-model="currentTab" @change="switchTab">
<option
v-for="tab in tabs"
:key="tab.path"
:value="tab.path"
>
{{ tab.label }}
</option>
</select>
</div>
<!-- 页面内容 -->
<div class="content">
<DuxTabRouterView />
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useTabStore } from '@duxweb/dvha-core'
import { useRouter } from 'vue-router'
const router = useRouter()
const tabStore = useTabStore()
const isMobile = ref(window.innerWidth < 768)
const currentTab = computed({
get: () => tabStore.current,
set: (value) => {
tabStore.changeTab(value, (tab) => {
router.push(tab.path)
})
}
})
const switchTab = () => {
const tab = tabStore.tabs.find(t => t.path === currentTab.value)
if (tab) {
router.push(tab.path)
}
}
</script>
标签页状态管理
自定义标签标题
vue
<script setup>
import { onMounted } from 'vue'
import { useTabStore, useRouteStore } from '@duxweb/dvha-core'
const tabStore = useTabStore()
const routeStore = useRouteStore()
onMounted(() => {
// 更新当前标签页标题
const currentRoute = routeStore.searchRouteName(route.name)
if (currentRoute) {
tabStore.addTab({
...currentRoute,
label: '自定义标题', // 覆盖默认标题
path: route.path
})
}
})
</script>
条件显示标签
vue
<script setup>
import { watch } from 'vue'
import { useRoute } from 'vue-router'
import { useTabStore } from '@duxweb/dvha-core'
const route = useRoute()
const tabStore = useTabStore()
watch(() => route.meta, (meta) => {
// 根据路由meta决定是否显示在标签中
if (meta.hideInTabs) {
// 不添加到标签页
return
}
// 正常添加标签页
tabStore.addTab({
name: route.name,
label: meta.title || route.name,
path: route.path
})
}, { immediate: true })
</script>
高级功能
标签页拖拽排序
vue
<template>
<div class="draggable-tabs">
<draggable
v-model="sortedTabs"
item-key="path"
@end="onDragEnd"
>
<template #item="{ element: tab }">
<div
:class="['tab-item', { active: tab.path === current }]"
@click="changeTab(tab.path)"
>
<span>{{ tab.label }}</span>
<button @click.stop="delTab(tab.path)">×</button>
</div>
</template>
</draggable>
</div>
</template>
<script setup>
import { computed } from 'vue'
import draggable from 'vuedraggable'
import { useTabStore } from '@duxweb/dvha-core'
const tabStore = useTabStore()
const sortedTabs = computed({
get: () => tabStore.tabs,
set: (value) => {
// 更新标签页顺序
tabStore.tabs = value
}
})
const onDragEnd = () => {
// 拖拽结束后的处理
console.log('标签页顺序已更新')
}
</script>
标签页分组
vue
<template>
<div class="grouped-tabs">
<div
v-for="group in groupedTabs"
:key="group.name"
class="tab-group"
>
<div class="group-header">{{ group.label }}</div>
<div class="group-tabs">
<div
v-for="tab in group.tabs"
:key="tab.path"
:class="['tab-item', { active: tab.path === current }]"
@click="changeTab(tab.path)"
>
{{ tab.label }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useTabStore } from '@duxweb/dvha-core'
const tabStore = useTabStore()
const groupedTabs = computed(() => {
const groups = {}
tabStore.tabs.forEach(tab => {
const groupName = tab.meta?.group || 'default'
if (!groups[groupName]) {
groups[groupName] = {
name: groupName,
label: tab.meta?.groupLabel || '默认',
tabs: []
}
}
groups[groupName].tabs.push(tab)
})
return Object.values(groups)
})
</script>
标签页持久化
js
// 标签页状态持久化
import { useTabStore } from '@duxweb/dvha-core'
const tabStore = useTabStore()
// 保存标签页状态
const saveTabsState = () => {
const state = {
tabs: tabStore.tabs,
current: tabStore.current
}
localStorage.setItem('tabs-state', JSON.stringify(state))
}
// 恢复标签页状态
const restoreTabsState = () => {
const saved = localStorage.getItem('tabs-state')
if (saved) {
const state = JSON.parse(saved)
state.tabs.forEach(tab => {
tabStore.addTab(tab)
})
if (state.current) {
tabStore.changeTab(state.current)
}
}
}
// 监听标签页变化并保存
tabStore.$subscribe(() => {
saveTabsState()
})
性能优化
组件缓存策略
vue
<script>
// 在 DuxTabRouterView 中实现的缓存策略
export default {
name: 'DuxTabRouterView',
setup() {
const cacheMap = new Map()
const maxCacheSize = 10 // 最大缓存数量
const wrap = (name, component) => {
// 缓存大小控制
if (cacheMap.size >= maxCacheSize) {
const firstKey = cacheMap.keys().next().value
cacheMap.delete(firstKey)
}
let cache = cacheMap.get(name)
if (!cache) {
cache = {
name,
render() { return component }
}
cacheMap.set(name, cache)
}
return cache
}
return { wrap }
}
}
</script>
内存清理
js
// 自动清理不再使用的标签页缓存
tabStore.$subscribe((mutation, state) => {
const activePaths = new Set(state.tabs.map(t => t.path))
// 清理已关闭标签页的缓存
cacheMap.forEach((cache, path) => {
if (!activePaths.has(path)) {
cacheMap.delete(path)
}
})
})
最佳实践
- 合理设置缓存: 不要缓存太多标签页,影响性能
- 锁定重要标签: 首页等重要页面应该锁定
- 清晰的标签标题: 确保标签标题简洁明了
- 响应式设计: 在移动端提供适合的标签页体验
- 状态持久化: 合理保存用户的标签页状态
- 性能监控: 监控标签页的内存使用情况
注意事项
- 内存管理: 及时清理不需要的组件缓存
- 用户体验: 避免标签页过多导致界面混乱
- 数据一致性: 注意标签页间数据状态的同步
- 权限控制: 确保用户只能访问有权限的标签页
- 错误处理: 处理标签页加载失败的情况
- 兼容性: 考虑不同浏览器的兼容性问题