829 lines
23 KiB
Vue
829 lines
23 KiB
Vue
<template>
|
||
<div class="tabs-container">
|
||
<!-- 左侧滚动按钮 -->
|
||
<button class="scroll-btn scroll-btn-left" @click="scrollLeft" :class="{ 'is-disabled': !canScrollLeft }">
|
||
<el-icon><ArrowLeft /></el-icon>
|
||
</button>
|
||
|
||
<!-- 标签列表区域 -->
|
||
<div class="tabs-scroll-area" ref="tabsContainerRef">
|
||
<draggable
|
||
v-model="tabsList"
|
||
item-key="path"
|
||
class="tabs-list"
|
||
:animation="300"
|
||
ghost-class="ghost"
|
||
chosen-class="chosen"
|
||
drag-class="drag"
|
||
handle=".tab-label"
|
||
:move="checkDragMove"
|
||
@start="handleDragStart"
|
||
@end="handleDragEnd"
|
||
>
|
||
<template #item="{ element: tab }">
|
||
<div
|
||
class="tab-item"
|
||
:class="{ 'is-active': tabsStore.activeTab === tab.path }"
|
||
@click="handleTabClick(tab)"
|
||
@contextmenu.prevent="handleContextMenu($event, tab.path)"
|
||
>
|
||
<div class="tab-label">
|
||
<span class="tab-text">{{ tab.title }}</span>
|
||
</div>
|
||
<span
|
||
v-if="tab.path !== '/dashboard'"
|
||
class="tab-close"
|
||
@click.stop="handleTabRemove(tab.path)"
|
||
>
|
||
<el-icon><Close /></el-icon>
|
||
</span>
|
||
</div>
|
||
</template>
|
||
</draggable>
|
||
</div>
|
||
|
||
<!-- 右侧滚动按钮 -->
|
||
<button class="scroll-btn scroll-btn-right" @click="scrollRight" :class="{ 'is-disabled': !canScrollRight }">
|
||
<el-icon><ArrowRight /></el-icon>
|
||
</button>
|
||
|
||
<!-- 当前页面下拉菜单 -->
|
||
<!-- <el-dropdown trigger="click" @command="handleDropdownCommand" placement="bottom-end" ref="pageDropdownRef">
|
||
<button class="current-page-btn" @click.stop="togglePageDropdown">
|
||
<el-icon><MoreFilled /></el-icon>
|
||
</button>
|
||
<template #dropdown>
|
||
<div class="dropdown-header">已打开的页面</div>
|
||
<el-dropdown-menu class="page-dropdown-menu">
|
||
<el-dropdown-item
|
||
v-for="tab in tabsStore.tabs"
|
||
:key="tab.path"
|
||
:command="tab.path"
|
||
:class="{ 'is-active-item': tabsStore.activeTab === tab.path }"
|
||
>
|
||
<span class="dropdown-item-text">{{ tab.title }}</span>
|
||
<el-icon
|
||
v-if="tab.path !== '/dashboard'"
|
||
class="dropdown-item-close"
|
||
@click.stop="handleTabRemove(tab.path)"
|
||
>
|
||
<Close />
|
||
</el-icon>
|
||
</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown> -->
|
||
|
||
<!-- 右键菜单 -->
|
||
<teleport to="body">
|
||
<div
|
||
v-if="contextMenuVisible"
|
||
class="context-menu-dropdown"
|
||
:style="contextMenuStyle"
|
||
@click.stop
|
||
>
|
||
<div
|
||
class="context-menu-item"
|
||
:class="{ 'is-disabled': !canRefresh }"
|
||
@click="canRefresh && handleContextMenuCommand('refresh')"
|
||
>
|
||
<el-icon><Refresh /></el-icon>
|
||
<span>刷新当前页</span>
|
||
</div>
|
||
<div
|
||
class="context-menu-item"
|
||
:class="{ 'is-disabled': !canCloseCurrent }"
|
||
@click="canCloseCurrent && handleContextMenuCommand('close')"
|
||
>
|
||
<el-icon><Close /></el-icon>
|
||
<span>关闭当前</span>
|
||
</div>
|
||
<div
|
||
class="context-menu-item"
|
||
:class="{ 'is-disabled': !canCloseOther }"
|
||
@click="canCloseOther && handleContextMenuCommand('closeOther')"
|
||
>
|
||
<el-icon><Switch /></el-icon>
|
||
<span>关闭其他</span>
|
||
</div>
|
||
<div
|
||
class="context-menu-item"
|
||
:class="{ 'is-disabled': !canCloseLeft }"
|
||
@click="canCloseLeft && handleContextMenuCommand('closeLeft')"
|
||
>
|
||
<el-icon><ArrowLeft /></el-icon>
|
||
<span>关闭左侧</span>
|
||
</div>
|
||
<div
|
||
class="context-menu-item"
|
||
:class="{ 'is-disabled': !canCloseRight }"
|
||
@click="canCloseRight && handleContextMenuCommand('closeRight')"
|
||
>
|
||
<el-icon><ArrowRight /></el-icon>
|
||
<span>关闭右侧</span>
|
||
</div>
|
||
<div class="context-menu-divider"></div>
|
||
<div class="context-menu-item" @click="handleContextMenuCommand('closeAll')">
|
||
<el-icon><FolderRemove /></el-icon>
|
||
<span>关闭全部</span>
|
||
</div>
|
||
</div>
|
||
</teleport>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { useTabsStore, fixedTabs, isFixedTab } from '@/stores/modules/tabs'
|
||
import type { TabItem } from '@/stores/modules/tabs'
|
||
import draggable from 'vuedraggable'
|
||
import { Close, Refresh, Switch, ArrowLeft, ArrowRight, FolderRemove, MoreFilled } from '@element-plus/icons-vue'
|
||
|
||
// 能否关闭当前(右键点击的不是固定标签页)
|
||
const canCloseCurrent = computed(() => {
|
||
const path = contextMenuPath.value || tabsStore.activeTab || ''
|
||
return !isFixedTab(path)
|
||
})
|
||
|
||
// 能否刷新(右键点击的 tab 是当前打开的页面)
|
||
const canRefresh = computed(() => {
|
||
const path = contextMenuPath.value || ''
|
||
return path === route.path
|
||
})
|
||
|
||
// 能否关闭其他(除了固定标签和右键点击的标签外还有其他标签)
|
||
const canCloseOther = computed(() => {
|
||
const path = contextMenuPath.value || tabsStore.activeTab || ''
|
||
// 标签总数 - 固定标签数 - 右键点击的标签(如果是固定标签则不减)> 0
|
||
const fixedCount = tabsStore.tabs.filter(t => isFixedTab(t.path)).length
|
||
const otherCount = tabsStore.tabs.length - fixedCount - (isFixedTab(path) ? 0 : 1)
|
||
return otherCount > 0
|
||
})
|
||
|
||
// 能否关闭左侧(左侧有非固定标签页)
|
||
const canCloseLeft = computed(() => {
|
||
const path = contextMenuPath.value || tabsStore.activeTab || ''
|
||
const rightClickIndex = tabsStore.tabs.findIndex(t => t.path === path)
|
||
if (rightClickIndex <= 0) return false
|
||
// 检查左侧是否有非固定标签
|
||
for (let i = 0; i < rightClickIndex; i++) {
|
||
const tab = tabsStore.tabs[i]
|
||
if (tab && !isFixedTab(tab.path)) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
})
|
||
|
||
// 能否关闭右侧(右侧有非固定标签页)
|
||
const canCloseRight = computed(() => {
|
||
const path = contextMenuPath.value || tabsStore.activeTab || ''
|
||
const rightClickIndex = tabsStore.tabs.findIndex(t => t.path === path)
|
||
if (rightClickIndex === -1 || rightClickIndex >= tabsStore.tabs.length - 1) return false
|
||
// 检查右侧是否有非固定标签
|
||
for (let i = rightClickIndex + 1; i < tabsStore.tabs.length; i++) {
|
||
const tab = tabsStore.tabs[i]
|
||
if (tab && !isFixedTab(tab.path)) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
})
|
||
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
const tabsStore = useTabsStore()
|
||
const tabsContainerRef = ref<HTMLElement>()
|
||
const pageDropdownRef = ref()
|
||
|
||
// 右键菜单状态
|
||
const contextMenuVisible = ref(false)
|
||
const contextMenuX = ref(0)
|
||
const contextMenuY = ref(0)
|
||
const contextMenuPath = ref('')
|
||
|
||
// 滚动状态
|
||
const canScrollLeft = ref(false)
|
||
const canScrollRight = ref(false)
|
||
|
||
// 计算右键菜单样式
|
||
const contextMenuStyle = computed(() => ({
|
||
left: contextMenuX.value + 'px',
|
||
top: contextMenuY.value + 'px'
|
||
}))
|
||
|
||
// 双向绑定tabs列表用于拖拽
|
||
const tabsList = computed({
|
||
get: () => tabsStore.tabs,
|
||
set: (val) => tabsStore.setTabs(val)
|
||
})
|
||
|
||
// 检查滚动状态
|
||
const checkScroll = () => {
|
||
if (tabsContainerRef.value) {
|
||
const { scrollLeft, scrollWidth, clientWidth } = tabsContainerRef.value
|
||
canScrollLeft.value = scrollLeft > 0
|
||
canScrollRight.value = scrollLeft + clientWidth < scrollWidth - 1
|
||
}
|
||
}
|
||
|
||
// 向左滚动
|
||
const scrollLeft = () => {
|
||
if (tabsContainerRef.value && canScrollLeft.value) {
|
||
tabsContainerRef.value.scrollBy({ left: -200, behavior: 'smooth' })
|
||
setTimeout(checkScroll, 300)
|
||
}
|
||
}
|
||
|
||
// 向右滚动
|
||
const scrollRight = () => {
|
||
if (tabsContainerRef.value && canScrollRight.value) {
|
||
tabsContainerRef.value.scrollBy({ left: 200, behavior: 'smooth' })
|
||
setTimeout(checkScroll, 300)
|
||
}
|
||
}
|
||
|
||
// 切换当前页面下拉菜单
|
||
const togglePageDropdown = () => {
|
||
if (pageDropdownRef.value) {
|
||
pageDropdownRef.value.handleOpen()
|
||
}
|
||
}
|
||
|
||
// 组件挂载后,检查是否需要跳转到保存的激活页面
|
||
onMounted(() => {
|
||
// 如果当前路由不是保存的激活页面,则跳转
|
||
if (tabsStore.activeTab && tabsStore.activeTab !== route.path) {
|
||
const targetTab = tabsStore.tabs.find(item => item.path === tabsStore.activeTab)
|
||
if (targetTab) {
|
||
router.replace({ path: targetTab.path, query: targetTab.query })
|
||
}
|
||
}
|
||
|
||
// 初始检查滚动状态
|
||
nextTick(checkScroll)
|
||
|
||
// 添加滚动监听
|
||
tabsContainerRef.value?.addEventListener('scroll', checkScroll)
|
||
|
||
// 全局左键点击关闭右键菜单
|
||
document.addEventListener('click', handleGlobalClick)
|
||
// 全局右键监听,如果右键在其他tab上,关闭当前右键菜单
|
||
document.addEventListener('contextmenu', handleGlobalContextMenu)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
tabsContainerRef.value?.removeEventListener('scroll', checkScroll)
|
||
document.removeEventListener('click', handleGlobalClick)
|
||
document.removeEventListener('contextmenu', handleGlobalContextMenu)
|
||
})
|
||
|
||
// 全局左键点击处理 - 关闭右键菜单
|
||
const handleGlobalClick = (e: MouseEvent) => {
|
||
closeContextMenu()
|
||
closePageDropdown()
|
||
}
|
||
|
||
// 全局右键菜单处理 - 如果右键在其他位置,关闭当前右键菜单
|
||
const handleGlobalContextMenu = (e: MouseEvent) => {
|
||
// 检查右键点击是否在tabs区域内
|
||
const target = e.target as HTMLElement
|
||
const tabItem = target.closest('.tab-item')
|
||
|
||
// 如果右键不在任何一个tab上,关闭右键菜单
|
||
if (!tabItem) {
|
||
closeContextMenu()
|
||
}
|
||
}
|
||
|
||
// 监听路由变化,自动添加页签
|
||
watch(
|
||
() => route.fullPath,
|
||
() => {
|
||
if (route.meta?.title && !route.meta?.isAuthPage) {
|
||
tabsStore.addTab({
|
||
title: route.meta.title as string,
|
||
path: route.path,
|
||
query: route.query
|
||
})
|
||
// 路由变化后检查滚动
|
||
nextTick(checkScroll)
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
// 页签点击
|
||
const handleTabClick = (tab: TabItem) => {
|
||
// 点击时关闭右键菜单
|
||
closeContextMenu()
|
||
|
||
tabsStore.setActiveTab(tab.path)
|
||
if (tab.path !== route.path) {
|
||
router.push({ path: tab.path, query: tab.query })
|
||
}
|
||
}
|
||
|
||
// 页签关闭
|
||
const handleTabRemove = (path: any) => {
|
||
tabsStore.removeTab(path)
|
||
// 如果关闭的是当前页签,跳转到新的激活页签
|
||
if (path === route.path && tabsStore.activeTab) {
|
||
const targetTab = tabsStore.tabs.find(item => item.path === tabsStore.activeTab)
|
||
if (targetTab) {
|
||
router.push({ path: targetTab.path, query: targetTab.query })
|
||
}
|
||
}
|
||
// 关闭后检查滚动状态
|
||
nextTick(checkScroll)
|
||
}
|
||
|
||
// 下拉菜单命令处理
|
||
const handleDropdownCommand = (path: string) => {
|
||
handleTabClick({ path, title: '', query: {} })
|
||
}
|
||
|
||
// 关闭当前页面下拉菜单
|
||
const closePageDropdown = () => {
|
||
if (pageDropdownRef.value) {
|
||
pageDropdownRef.value.handleClose()
|
||
}
|
||
}
|
||
|
||
// 拖拽开始时关闭右键菜单
|
||
const handleDragStart = () => {
|
||
closeContextMenu()
|
||
}
|
||
|
||
// 检查拖拽移动是否允许(固定标签页不能被拖动到左侧)
|
||
const checkDragMove = (evt: any) => {
|
||
const draggedPath = evt.draggedContext.element.path
|
||
// 固定标签页本身不能移动
|
||
if (isFixedTab(draggedPath)) {
|
||
return false
|
||
}
|
||
|
||
// 获取目标位置的索引
|
||
const toIndex = evt.draggedContext.toIndex
|
||
// 检查目标位置左侧是否有固定标签页
|
||
for (let i = 0; i < toIndex; i++) {
|
||
const tab = tabsStore.tabs[i]
|
||
if (tab && isFixedTab(tab.path)) {
|
||
// 如果目标位置左边有固定标签页,不允许移动到这里
|
||
return false
|
||
}
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
// 拖拽结束
|
||
const handleDragEnd = () => {
|
||
// 确保固定标签页始终在最左边
|
||
const fixedTabsList: TabItem[] = []
|
||
const otherTabsList: TabItem[] = []
|
||
|
||
for (const tab of tabsStore.tabs) {
|
||
if (isFixedTab(tab.path)) {
|
||
fixedTabsList.push(tab)
|
||
} else {
|
||
otherTabsList.push(tab)
|
||
}
|
||
}
|
||
|
||
// 只有当固定标签页不在最前面时才需要重新排序
|
||
const firstTab = tabsStore.tabs[0]
|
||
if (firstTab && !isFixedTab(firstTab.path)) {
|
||
tabsStore.setTabs([...fixedTabsList, ...otherTabsList])
|
||
}
|
||
}
|
||
|
||
|
||
|
||
// 右键菜单命令处理
|
||
const handleContextMenuCommand = (command: string) => {
|
||
// 使用右键选中的路径,如果没有则用当前激活的标签
|
||
const currentPath = contextMenuPath.value || tabsStore.activeTab
|
||
|
||
// 保存右键点击的路径(在关闭菜单前保存)
|
||
const rightClickedPath = contextMenuPath.value
|
||
|
||
closeContextMenu()
|
||
|
||
switch (command) {
|
||
case 'refresh':
|
||
// 获取当前右键点击的 tab 或当前激活的 tab
|
||
const refreshPath = rightClickedPath || tabsStore.activeTab
|
||
if (refreshPath) {
|
||
const targetTab = tabsStore.tabs.find(item => item.path === refreshPath)
|
||
// 如果不是当前页面,先跳转过去并更新 activeTab
|
||
if (refreshPath !== route.path) {
|
||
tabsStore.setActiveTab(refreshPath)
|
||
router.push({ path: refreshPath, query: targetTab?.query }).then(() => {
|
||
// 等待路由切换完成后再刷新
|
||
nextTick(() => {
|
||
tabsStore.refreshTab(refreshPath)
|
||
})
|
||
})
|
||
} else {
|
||
// 当前页面直接刷新
|
||
tabsStore.refreshTab(refreshPath)
|
||
}
|
||
}
|
||
break
|
||
case 'close':
|
||
if (currentPath) {
|
||
// 检查关闭后当前浏览的页面是否还存在
|
||
const willCloseCurrentPage = currentPath === route.path
|
||
handleTabRemove(currentPath)
|
||
// 如果当前页面被关闭了,跳转到右键选中的页面
|
||
if (willCloseCurrentPage && tabsStore.activeTab) {
|
||
const newTab = tabsStore.tabs.find(item => item.path === tabsStore.activeTab)
|
||
if (newTab) {
|
||
router.push({ path: newTab.path, query: newTab.query })
|
||
}
|
||
}
|
||
}
|
||
break
|
||
case 'closeOther':
|
||
if (currentPath) {
|
||
// 检查关闭后当前浏览的页面是否还存在(关闭其他时只有右键点击的页面保留)
|
||
const willCloseCurrentPage = route.path !== currentPath
|
||
tabsStore.closeOtherTabs(currentPath)
|
||
// 如果当前页面被关闭了,跳转到右键选中的页面
|
||
if (willCloseCurrentPage) {
|
||
const targetTab = tabsStore.tabs.find(item => item.path === currentPath)
|
||
if (targetTab) {
|
||
router.push({ path: targetTab.path, query: targetTab.query })
|
||
}
|
||
}
|
||
}
|
||
break
|
||
case 'closeLeft':
|
||
if (currentPath) {
|
||
// 获取要关闭的标签列表(左侧所有标签,排除固定标签页)
|
||
const rightClickIndex = tabsStore.tabs.findIndex(item => item.path === currentPath)
|
||
const tabsToClose = tabsStore.tabs.slice(0, rightClickIndex).filter(item => item.path !== '/dashboard')
|
||
// 检查当前页面是否在要关闭的列表中
|
||
const currentTab = tabsToClose.find(item => item.path === route.path)
|
||
|
||
tabsStore.closeLeftTabs(currentPath)
|
||
|
||
// 如果当前页面在要关闭的列表中,跳转到右键选中的页面
|
||
if (currentTab) {
|
||
const targetTab = tabsStore.tabs.find(item => item.path === currentPath)
|
||
if (targetTab) {
|
||
router.push({ path: targetTab.path, query: targetTab.query })
|
||
}
|
||
}
|
||
}
|
||
break
|
||
case 'closeRight':
|
||
if (currentPath) {
|
||
// 获取要关闭的标签列表(右侧所有标签,排除固定标签页)
|
||
const rightClickIndex = tabsStore.tabs.findIndex(item => item.path === currentPath)
|
||
const tabsToClose = tabsStore.tabs.slice(rightClickIndex + 1).filter(item => item.path !== '/dashboard')
|
||
// 检查当前页面是否在要关闭的列表中
|
||
const currentTab = tabsToClose.find(item => item.path === route.path)
|
||
|
||
tabsStore.closeRightTabs(currentPath)
|
||
|
||
// 如果当前页面在要关闭的列表中,跳转到右键选中的页面
|
||
if (currentTab) {
|
||
const targetTab = tabsStore.tabs.find(item => item.path === currentPath)
|
||
if (targetTab) {
|
||
router.push({ path: targetTab.path, query: targetTab.query })
|
||
}
|
||
}
|
||
}
|
||
break
|
||
case 'closeAll':
|
||
tabsStore.closeAllTabs()
|
||
router.push('/dashboard')
|
||
break
|
||
}
|
||
}
|
||
|
||
// 处理右键菜单显示 - 不再激活标签,只记录右键点击的路径
|
||
const handleContextMenu = (e: MouseEvent, path: string) => {
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
|
||
// 先关闭可能打开的当前页面下拉菜单
|
||
closePageDropdown()
|
||
|
||
// 记录右键点击的路径用于后续操作
|
||
contextMenuPath.value = path
|
||
|
||
// 计算菜单位置,确保不超出屏幕
|
||
const menuWidth = 160
|
||
const menuHeight = 240
|
||
let x = e.clientX
|
||
let y = e.clientY
|
||
|
||
// 如果右边空间不够,往左显示
|
||
if (x + menuWidth > window.innerWidth) {
|
||
x = window.innerWidth - menuWidth - 10
|
||
}
|
||
|
||
// 如果下面空间不够,往上显示
|
||
if (y + menuHeight > window.innerHeight) {
|
||
y = window.innerHeight - menuHeight - 10
|
||
}
|
||
|
||
// 确保最小值
|
||
x = Math.max(10, x)
|
||
y = Math.max(10, y)
|
||
|
||
contextMenuX.value = x
|
||
contextMenuY.value = y
|
||
contextMenuVisible.value = true
|
||
}
|
||
|
||
// 关闭右键菜单
|
||
const closeContextMenu = () => {
|
||
contextMenuVisible.value = false
|
||
contextMenuPath.value = ''
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.tabs-container {
|
||
height: 42px;
|
||
box-sizing: content-box;
|
||
background: var(--el-bg-color);
|
||
// border-bottom: 1px solid var(--el-border-color-light);
|
||
padding: 0 40px;
|
||
position: relative;
|
||
z-index: 10;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.scroll-btn {
|
||
position: absolute;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 28px;
|
||
height: 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: var(--el-fill-color-light);
|
||
border: 1px solid var(--el-border-color-light);
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
color: var(--el-text-color-secondary);
|
||
transition: all 0.2s ease;
|
||
z-index: 5;
|
||
|
||
&:hover:not(.is-disabled) {
|
||
background: var(--el-fill-color);
|
||
color: var(--el-color-primary);
|
||
border-color: var(--el-color-primary-light-5);
|
||
}
|
||
|
||
&.is-disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
&.scroll-btn-left {
|
||
left: 6px;
|
||
}
|
||
|
||
&.scroll-btn-right {
|
||
right: 6px;
|
||
}
|
||
}
|
||
|
||
.tabs-scroll-area {
|
||
flex: 1;
|
||
height: 100%;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
scrollbar-width: none;
|
||
-ms-overflow-style: none;
|
||
|
||
&::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
.tabs-list {
|
||
display: flex;
|
||
align-items: center;
|
||
height: 100%;
|
||
gap: 2px;
|
||
min-width: max-content;
|
||
}
|
||
|
||
.tab-item {
|
||
display: flex;
|
||
align-items: center;
|
||
height: 32px;
|
||
padding: 0 12px;
|
||
background: var(--el-fill-color-light);
|
||
border: 1px solid var(--el-border-color-light);
|
||
// border-bottom: none;
|
||
border-radius: 4px 4px 0 0;
|
||
color: var(--el-text-color-regular);
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
transition: all 0.2s ease;
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
|
||
.tab-label {
|
||
display: flex;
|
||
align-items: center;
|
||
cursor: grab;
|
||
|
||
&:active {
|
||
cursor: grabbing;
|
||
}
|
||
}
|
||
|
||
.tab-text {
|
||
max-width: 120px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.tab-close {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-left: 6px;
|
||
width: 16px;
|
||
height: 16px;
|
||
border-radius: 50%;
|
||
font-size: 12px;
|
||
opacity: 0.6;
|
||
transition: all 0.2s ease;
|
||
|
||
&:hover {
|
||
opacity: 1;
|
||
background: var(--el-color-danger-light-9);
|
||
color: var(--el-color-danger);
|
||
}
|
||
}
|
||
|
||
&:hover {
|
||
background: var(--el-fill-color);
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
&.is-active {
|
||
background: #fff;
|
||
color: var(--el-color-primary);
|
||
border-color: var(--el-border-color);
|
||
font-weight: 500;
|
||
position: relative;
|
||
|
||
&::after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: -1px;
|
||
left: 0;
|
||
right: 0;
|
||
height: 2px;
|
||
background: #fff;
|
||
}
|
||
}
|
||
|
||
// 拖拽动画样式
|
||
&.ghost {
|
||
opacity: 0.5;
|
||
background: var(--el-color-primary-light-9);
|
||
border: 1px dashed var(--el-color-primary);
|
||
}
|
||
|
||
&.chosen {
|
||
opacity: 0.8;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
&.drag {
|
||
opacity: 1;
|
||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
||
transform: translateY(-4px) scale(1.02);
|
||
}
|
||
}
|
||
|
||
.current-page-btn {
|
||
position: absolute;
|
||
right: 6px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 28px;
|
||
height: 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: var(--el-fill-color-light);
|
||
border: 1px solid var(--el-border-color-light);
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
color: var(--el-text-color-secondary);
|
||
transition: all 0.2s ease;
|
||
padding: 0;
|
||
|
||
&:hover {
|
||
background: var(--el-fill-color);
|
||
color: var(--el-color-primary);
|
||
border-color: var(--el-color-primary-light-5);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 右键菜单下拉
|
||
.context-menu-dropdown {
|
||
position: fixed;
|
||
z-index: 999999;
|
||
background: var(--el-bg-color);
|
||
border: 1px solid var(--el-border-color-light);
|
||
border-radius: 4px;
|
||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||
padding: 4px 0;
|
||
min-width: 160px;
|
||
|
||
.context-menu-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 16px;
|
||
font-size: 13px;
|
||
color: var(--el-text-color-regular);
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
|
||
&:hover:not(.is-disabled) {
|
||
background: var(--el-fill-color-light);
|
||
color: var(--el-color-primary);
|
||
}
|
||
|
||
&.is-disabled {
|
||
color: var(--el-text-color-placeholder);
|
||
cursor: not-allowed;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.el-icon {
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
.context-menu-divider {
|
||
height: 1px;
|
||
background: var(--el-border-color-light);
|
||
margin: 4px 0;
|
||
}
|
||
}
|
||
|
||
// 当前页面下拉菜单
|
||
.dropdown-header {
|
||
padding: 8px 12px;
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
// border-bottom: 1px solid var(--el-border-color-light);
|
||
background: var(--el-fill-color-lighter);
|
||
}
|
||
|
||
.page-dropdown-menu {
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
|
||
:deep(.el-dropdown-menu__item) {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding-right: 12px;
|
||
|
||
&.is-active-item {
|
||
background: var(--el-color-primary-light-9);
|
||
color: var(--el-color-primary);
|
||
}
|
||
|
||
.dropdown-item-text {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.dropdown-item-close {
|
||
margin-left: 8px;
|
||
font-size: 12px;
|
||
opacity: 0.6;
|
||
|
||
&:hover {
|
||
opacity: 1;
|
||
color: var(--el-color-danger);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|