标签页方法 (useTabs)
useTabs
是基于 useTabStore
封装的便捷 Hook,提供了完整的标签页状态管理功能。
功能特点
- 📑 标签管理 - 添加、删除、切换标签页
- 💾 状态同步 - 自动同步路由和标签页状态
- 🔒 标签锁定 - 支持锁定重要标签防止关闭
- 🎯 智能导航 - 提供便捷的标签页导航方法
- 🔄 批量操作 - 支持批量关闭标签页操作
- 📊 状态查询 - 提供丰富的标签页状态查询
基础用法
获取标签页 Hook
js
import { useTabStore } from '@duxweb/dvha-core'
// 获取当前管理端的标签页Store
const tabStore = useTabStore()
// 或者指定管理端名称
const adminTabStore = useTabStore('admin')
Hook 返回值
js
const {
// 状态数据
current, // 当前激活的标签页路径
tabs, // 所有标签页数组
// 查询方法
isTab, // 检查是否为标签页
// 操作方法
addTab, // 添加标签页
delTab, // 删除标签页
changeTab, // 切换标签页
lockTab, // 锁定/解锁标签页
// 批量操作
delOther, // 关闭其他标签页
delLeft, // 关闭左侧标签页
delRight, // 关闭右侧标签页
clearTab, // 清除所有标签页
} = useTabStore()
标签页操作
添加标签页
js
import { useTabStore } from '@duxweb/dvha-core'
import { useRouter } from 'vue-router'
const tabStore = useTabStore()
const router = useRouter()
// 基础添加
const addNewTab = () => {
tabStore.addTab({
name: 'users',
label: '用户管理',
path: '/admin/users'
})
}
// 添加并导航
const addAndNavigate = () => {
tabStore.addTab({
name: 'dashboard',
label: '仪表盘',
path: '/admin/dashboard'
}, (tab) => {
// 添加成功后的回调
router.push(tab.path)
})
}
// 添加带元数据的标签
const addTabWithMeta = () => {
tabStore.addTab({
name: 'important',
label: '重要页面',
path: '/admin/important',
meta: {
lock: true, // 锁定标签
icon: 'star', // 图标
closable: false // 不可关闭
}
})
}
删除标签页
js
// 删除指定标签页
const deleteTab = (path) => {
tabStore.delTab(path, (nextTab) => {
// 删除后的回调,自动导航到下一个标签
if (nextTab) {
router.push(nextTab.path)
}
})
}
// 删除当前标签页
const deleteCurrentTab = () => {
if (tabStore.current) {
tabStore.delTab(tabStore.current, (nextTab) => {
router.push(nextTab.path)
})
}
}
// 带确认的删除
const deleteTabWithConfirm = (path) => {
if (confirm('确定要关闭此标签页吗?')) {
tabStore.delTab(path)
}
}
切换标签页
js
// 切换到指定标签页
const switchTab = (path) => {
tabStore.changeTab(path, (tab) => {
// 切换成功后的回调
router.push(tab.path)
console.log('已切换到:', tab.label)
})
}
// 切换到下一个标签页
const switchToNext = () => {
const currentIndex = tabStore.tabs.findIndex(
tab => tab.path === tabStore.current
)
const nextIndex = (currentIndex + 1) % tabStore.tabs.length
const nextTab = tabStore.tabs[nextIndex]
if (nextTab) {
switchTab(nextTab.path)
}
}
// 切换到上一个标签页
const switchToPrev = () => {
const currentIndex = tabStore.tabs.findIndex(
tab => tab.path === tabStore.current
)
const prevIndex = currentIndex === 0
? tabStore.tabs.length - 1
: currentIndex - 1
const prevTab = tabStore.tabs[prevIndex]
if (prevTab) {
switchTab(prevTab.path)
}
}
锁定标签页
js
// 锁定/解锁标签页
const toggleLock = (path) => {
tabStore.lockTab(path)
console.log('标签页锁定状态已切换')
}
// 检查标签页是否锁定
const isTabLocked = (path) => {
const tab = tabStore.tabs.find(t => t.path === path)
return tab?.meta?.lock || false
}
// 锁定所有标签页
const lockAllTabs = () => {
tabStore.tabs.forEach(tab => {
if (!tab.meta?.lock) {
tabStore.lockTab(tab.path)
}
})
}
批量操作
关闭其他标签页
js
// 关闭除指定标签外的所有标签页
const closeOtherTabs = (keepPath) => {
tabStore.delOther(keepPath, () => {
console.log('已关闭其他标签页')
router.push(keepPath)
})
}
// 关闭除当前标签外的所有标签页
const closeOtherCurrentTabs = () => {
if (tabStore.current) {
closeOtherTabs(tabStore.current)
}
}
关闭左侧/右侧标签页
js
// 关闭左侧标签页
const closeLeftTabs = (path) => {
tabStore.delLeft(path, () => {
console.log('已关闭左侧标签页')
})
}
// 关闭右侧标签页
const closeRightTabs = (path) => {
tabStore.delRight(path, () => {
console.log('已关闭右侧标签页')
})
}
// 关闭当前标签页左侧的所有标签页
const closeCurrentLeftTabs = () => {
if (tabStore.current) {
closeLeftTabs(tabStore.current)
}
}
清除所有标签页
js
// 清除所有标签页
const clearAllTabs = () => {
if (confirm('确定要关闭所有标签页吗?')) {
tabStore.clearTab()
// 导航到首页
router.push('/admin')
}
}
// 清除除锁定标签外的所有标签页
const clearUnlockedTabs = () => {
const lockedTabs = tabStore.tabs.filter(tab => tab.meta?.lock)
tabStore.clearTab()
// 重新添加锁定的标签页
lockedTabs.forEach(tab => {
tabStore.addTab(tab)
})
}
状态查询
检查标签页状态
js
// 检查路径是否为标签页
const checkIsTab = (path) => {
return tabStore.isTab(path)
}
// 获取标签页数量
const getTabCount = () => {
return tabStore.tabs.length
}
// 获取当前标签页信息
const getCurrentTab = () => {
return tabStore.tabs.find(tab => tab.path === tabStore.current)
}
// 获取指定标签页信息
const getTabByPath = (path) => {
return tabStore.tabs.find(tab => tab.path === path)
}
// 检查是否有未保存的标签页
const hasUnsavedTabs = () => {
return tabStore.tabs.some(tab => tab.meta?.unsaved)
}
标签页过滤和排序
js
import { computed } from 'vue'
// 获取可见的标签页
const visibleTabs = computed(() => {
return tabStore.tabs.filter(tab => !tab.meta?.hidden)
})
// 获取锁定的标签页
const lockedTabs = computed(() => {
return tabStore.tabs.filter(tab => tab.meta?.lock)
})
// 获取可关闭的标签页
const closableTabs = computed(() => {
return tabStore.tabs.filter(tab => !tab.meta?.lock)
})
// 按重要性排序的标签页
const sortedTabs = computed(() => {
return [...tabStore.tabs].sort((a, b) => {
// 锁定的标签页优先
if (a.meta?.lock && !b.meta?.lock) return -1
if (!a.meta?.lock && b.meta?.lock) return 1
// 按添加时间排序
return (a.meta?.addTime || 0) - (b.meta?.addTime || 0)
})
})
实际应用示例
标签页组件
vue
<template>
<div class="tabs-container">
<!-- 标签栏 -->
<div class="tabs-bar">
<div
v-for="tab in visibleTabs"
:key="tab.path"
:class="getTabClass(tab)"
@click="switchToTab(tab.path)"
@contextmenu.prevent="showContextMenu($event, tab)"
>
<!-- 标签图标 -->
<i v-if="tab.meta?.icon" :class="tab.meta.icon"></i>
<!-- 标签标题 -->
<span class="tab-title">{{ tab.label }}</span>
<!-- 未保存标识 -->
<span v-if="tab.meta?.unsaved" class="unsaved-dot">●</span>
<!-- 锁定图标 -->
<i v-if="tab.meta?.lock" class="lock-icon">🔒</i>
<!-- 关闭按钮 -->
<button
v-else-if="tab.meta?.closable !== false"
class="close-btn"
@click.stop="closeTab(tab.path)"
>×</button>
</div>
<!-- 添加按钮 -->
<button class="add-tab-btn" @click="showAddTabDialog">+</button>
</div>
<!-- 标签页内容 -->
<div class="tab-content">
<router-view />
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useTabStore } from '@duxweb/dvha-core'
import { useRouter } from 'vue-router'
const tabStore = useTabStore()
const router = useRouter()
// 可见标签页
const visibleTabs = computed(() => {
return tabStore.tabs.filter(tab => !tab.meta?.hidden)
})
// 获取标签页样式类
const getTabClass = (tab) => {
return [
'tab-item',
{
'active': tab.path === tabStore.current,
'locked': tab.meta?.lock,
'unsaved': tab.meta?.unsaved
}
]
}
// 切换标签页
const switchToTab = (path) => {
tabStore.changeTab(path, (tab) => {
router.push(tab.path)
})
}
// 关闭标签页
const closeTab = (path) => {
const tab = tabStore.tabs.find(t => t.path === path)
// 检查是否有未保存的内容
if (tab?.meta?.unsaved) {
if (!confirm('标签页有未保存的内容,确定要关闭吗?')) {
return
}
}
tabStore.delTab(path, (nextTab) => {
if (nextTab) {
router.push(nextTab.path)
}
})
}
</script>
标签页导航组件
vue
<template>
<div class="tab-navigation">
<!-- 标签页控制按钮 -->
<div class="tab-controls">
<button @click="refreshCurrentTab" title="刷新">
🔄
</button>
<button @click="closeCurrentTab" title="关闭当前">
×
</button>
<button @click="closeOthers" title="关闭其他">
⋯
</button>
<button @click="closeAll" title="关闭所有">
🗙
</button>
</div>
<!-- 标签页列表 -->
<div class="tab-list">
<select v-model="currentPath" @change="switchTab">
<option
v-for="tab in tabStore.tabs"
:key="tab.path"
:value="tab.path"
>
{{ tab.label }}
{{ tab.meta?.lock ? '🔒' : '' }}
{{ tab.meta?.unsaved ? '●' : '' }}
</option>
</select>
</div>
<!-- 标签页统计 -->
<div class="tab-stats">
总计: {{ tabStore.tabs.length }} 个标签页
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useTabStore } from '@duxweb/dvha-core'
import { useRouter } from 'vue-router'
const tabStore = useTabStore()
const router = useRouter()
const currentPath = computed({
get: () => tabStore.current,
set: (value) => switchTab(value)
})
// 刷新当前标签页
const refreshCurrentTab = () => {
router.go(0)
}
// 关闭当前标签页
const closeCurrentTab = () => {
if (tabStore.current) {
tabStore.delTab(tabStore.current, (nextTab) => {
if (nextTab) {
router.push(nextTab.path)
}
})
}
}
// 关闭其他标签页
const closeOthers = () => {
if (tabStore.current) {
tabStore.delOther(tabStore.current)
}
}
// 关闭所有标签页
const closeAll = () => {
if (confirm('确定要关闭所有标签页吗?')) {
tabStore.clearTab()
router.push('/admin')
}
}
// 切换标签页
const switchTab = (path) => {
tabStore.changeTab(path, (tab) => {
router.push(tab.path)
})
}
</script>
标签页持久化
js
// composables/useTabPersistence.js
import { watch } from 'vue'
import { useTabStore } from '@duxweb/dvha-core'
export function useTabPersistence(manageName) {
const tabStore = useTabStore(manageName)
const storageKey = `tabs-${manageName}`
// 保存标签页状态
const saveState = () => {
const state = {
tabs: tabStore.tabs.map(tab => ({
...tab,
// 移除不需要持久化的字段
component: undefined
})),
current: tabStore.current,
timestamp: Date.now()
}
try {
localStorage.setItem(storageKey, JSON.stringify(state))
} catch (error) {
console.warn('保存标签页状态失败:', error)
}
}
// 恢复标签页状态
const restoreState = () => {
try {
const saved = localStorage.getItem(storageKey)
if (!saved) return
const state = JSON.parse(saved)
// 检查状态是否过期(24小时)
if (Date.now() - state.timestamp > 24 * 60 * 60 * 1000) {
localStorage.removeItem(storageKey)
return
}
// 恢复标签页
state.tabs.forEach(tab => {
tabStore.addTab(tab)
})
// 恢复当前标签页
if (state.current) {
tabStore.changeTab(state.current)
}
} catch (error) {
console.warn('恢复标签页状态失败:', error)
localStorage.removeItem(storageKey)
}
}
// 监听状态变化并保存
watch([
() => tabStore.tabs,
() => tabStore.current
], saveState, { deep: true })
// 清除状态
const clearState = () => {
localStorage.removeItem(storageKey)
}
return {
saveState,
restoreState,
clearState
}
}
键盘快捷键
js
// composables/useTabShortcuts.js
import { onMounted, onUnmounted } from 'vue'
import { useTabStore } from '@duxweb/dvha-core'
import { useRouter } from 'vue-router'
export function useTabShortcuts() {
const tabStore = useTabStore()
const router = useRouter()
const handleKeydown = (event) => {
// Ctrl/Cmd + W: 关闭当前标签页
if ((event.ctrlKey || event.metaKey) && event.key === 'w') {
event.preventDefault()
if (tabStore.current) {
tabStore.delTab(tabStore.current, (nextTab) => {
if (nextTab) {
router.push(nextTab.path)
}
})
}
}
// Ctrl/Cmd + Tab: 切换到下一个标签页
if ((event.ctrlKey || event.metaKey) && event.key === 'Tab') {
event.preventDefault()
const currentIndex = tabStore.tabs.findIndex(
tab => tab.path === tabStore.current
)
const nextIndex = (currentIndex + 1) % tabStore.tabs.length
const nextTab = tabStore.tabs[nextIndex]
if (nextTab) {
tabStore.changeTab(nextTab.path, (tab) => {
router.push(tab.path)
})
}
}
// Ctrl/Cmd + Shift + Tab: 切换到上一个标签页
if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === 'Tab') {
event.preventDefault()
const currentIndex = tabStore.tabs.findIndex(
tab => tab.path === tabStore.current
)
const prevIndex = currentIndex === 0
? tabStore.tabs.length - 1
: currentIndex - 1
const prevTab = tabStore.tabs[prevIndex]
if (prevTab) {
tabStore.changeTab(prevTab.path, (tab) => {
router.push(tab.path)
})
}
}
// Ctrl/Cmd + 数字键: 切换到指定位置的标签页
if ((event.ctrlKey || event.metaKey) && /^[1-9]$/.test(event.key)) {
event.preventDefault()
const index = parseInt(event.key) - 1
const tab = tabStore.tabs[index]
if (tab) {
tabStore.changeTab(tab.path, (tab) => {
router.push(tab.path)
})
}
}
}
onMounted(() => {
document.addEventListener('keydown', handleKeydown)
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
})
}
最佳实践
- 合理使用锁定: 重要页面如首页应该锁定
- 状态持久化: 适当保存用户的标签页状态
- 性能优化: 限制最大标签页数量,及时清理缓存
- 用户体验: 提供键盘快捷键和右键菜单
- 错误处理: 处理标签页加载失败的情况
- 内存管理: 监控标签页的内存使用情况
注意事项
- 管理端隔离: 不同管理端的标签页是完全隔离的
- 路由同步: 确保标签页状态与路由状态同步
- 权限控制: 检查用户是否有权限访问标签页
- 数据一致性: 注意标签页间数据状态的同步
- 兼容性: 考虑不同浏览器的localStorage支持
- 清理机制: 定期清理过期的标签页状态