first commit
This commit is contained in:
@@ -0,0 +1,745 @@
|
||||
<script setup lang="ts">
|
||||
import CommonAside from '@/components/CommonAside.vue';
|
||||
import CommonHeader from '@/components/CommonHeader.vue';
|
||||
import { useTabsStore } from '@/stores';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { ref, watch, reactive, nextTick, onMounted, computed } from 'vue';
|
||||
import { More, Close, CircleClose, ArrowUp } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const tabsStore = useTabsStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const defaultDashboardPath = '/home';
|
||||
|
||||
// 根据当前路由恢复 tab(刷新时使用)
|
||||
function restoreTabFromRoute() {
|
||||
const currentPath = route.fullPath;
|
||||
const currentName = route.name;
|
||||
const currentMeta = route.meta || {};
|
||||
|
||||
// 如果当前路径不在 tabList 中,添加它
|
||||
const existTab = tabsStore.tabList.find(t => t.fullPath === currentPath);
|
||||
if (!existTab) {
|
||||
// 从路由 meta 获取标题,如果没有则使用路由 name 或路径
|
||||
const title = currentMeta.title || currentName || '页面';
|
||||
// 使用 addTab 会自动保存到 localStorage
|
||||
tabsStore.addTab({
|
||||
title: title,
|
||||
fullPath: currentPath,
|
||||
name: currentName || title,
|
||||
icon: currentMeta.icon
|
||||
});
|
||||
} else {
|
||||
// 如果已存在,只激活它(不触发路由跳转)
|
||||
tabsStore.setActiveTab(currentPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载后,根据当前路由恢复 tab
|
||||
onMounted(() => {
|
||||
// 等待路由完全加载后再恢复
|
||||
nextTick(() => {
|
||||
// 刷新时,localStorage 中已保存的 tabs 会在 store 初始化时恢复
|
||||
// 这里只需要确保当前路由对应的 tab 存在并激活
|
||||
restoreTabFromRoute();
|
||||
|
||||
// 为 tabs 容器添加右键事件监听(使用事件委托)
|
||||
const tabsWrapper = document.querySelector('.multi-tabs-wrapper');
|
||||
if (tabsWrapper) {
|
||||
tabsWrapper.addEventListener('contextmenu', (e) => {
|
||||
// 排除编辑器相关区域,避免影响编辑器功能
|
||||
// 如果点击在编辑器区域内,完全不做任何处理(优先检查)
|
||||
const editorWrapper = e.target.closest?.('.wang-editor-wrapper');
|
||||
if (editorWrapper) {
|
||||
// 在编辑器区域内,不处理右键菜单,也不阻止事件
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查其他编辑器元素(工具栏、下拉面板等)
|
||||
const editorElements = [
|
||||
'.w-e-toolbar',
|
||||
'.w-e-drop-panel',
|
||||
'.w-e-modal',
|
||||
'.w-e-toolbar-menu',
|
||||
'[data-menu-key]',
|
||||
'[class*="w-e-"]'
|
||||
];
|
||||
|
||||
for (const selector of editorElements) {
|
||||
if (e.target.closest && e.target.closest(selector)) {
|
||||
// 在编辑器元素内,不处理右键菜单
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 只有在非编辑器区域的 tab item 上才处理右键菜单
|
||||
const tabItem = e.target.closest('.el-tabs__item');
|
||||
if (tabItem) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleTabsContextMenu(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 监听路由变化,自动添加/激活对应的 tab
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
(newPath) => {
|
||||
if (newPath) {
|
||||
restoreTabFromRoute();
|
||||
}
|
||||
},
|
||||
{ immediate: false }
|
||||
);
|
||||
|
||||
// 1. 侧栏菜单点击:加入/激活Tab并切换路由
|
||||
const handleAsideMenuClick = async (menuItem) => {
|
||||
const targetPath = menuItem.path;
|
||||
|
||||
if (targetPath === '/home') {
|
||||
tabsStore.closeAll();
|
||||
router.push('/home');
|
||||
return;
|
||||
}
|
||||
|
||||
// 先添加tab
|
||||
tabsStore.addTab({
|
||||
title: menuItem.title,
|
||||
fullPath: targetPath,
|
||||
name: menuItem.title,
|
||||
icon: menuItem.icon
|
||||
});
|
||||
|
||||
// 如果当前路由已经是目标路由,不需要跳转
|
||||
if (router.currentRoute.value.fullPath === targetPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查路由是否存在,如果不存在等待一下再尝试
|
||||
let routeExists = router.resolve(targetPath).matched.length > 0;
|
||||
|
||||
if (!routeExists) {
|
||||
// 等待路由加载(最多等待500ms)
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
routeExists = router.resolve(targetPath).matched.length > 0;
|
||||
if (routeExists) break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果路由存在,直接跳转
|
||||
if (routeExists) {
|
||||
router.push(targetPath).catch(err => {
|
||||
if (err.name !== 'NavigationDuplicated') {
|
||||
console.error('路由跳转失败:', err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 如果路由不存在,尝试刷新页面(最后的手段)
|
||||
console.warn('路由不存在,尝试刷新页面:', targetPath);
|
||||
// 不刷新页面,而是显示错误提示
|
||||
ElMessage.warning(`路由 ${targetPath} 不存在,请刷新缓存后重试`);
|
||||
}
|
||||
};
|
||||
const tabsCloseTab = (targetKey) => {
|
||||
tabsStore.removeTab(targetKey);
|
||||
if (route.fullPath !== tabsStore.activeTab) {
|
||||
router.push(tabsStore.activeTab);
|
||||
}
|
||||
};
|
||||
const closeOthers = () => {
|
||||
tabsStore.closeOthers();
|
||||
if (!tabsStore.tabList.find(tab => tab.fullPath === route.fullPath)) {
|
||||
router.push(tabsStore.activeTab);
|
||||
}
|
||||
};
|
||||
const closeAll = () => {
|
||||
tabsStore.closeAll();
|
||||
router.push(defaultDashboardPath);
|
||||
};
|
||||
// 主动监听tab激活,保证切tab时内容区切换(仅用于tab点击切换,菜单点击由handleAsideMenuClick处理)
|
||||
// 使用 immediate: false 避免初始化时触发,使用 flush: 'post' 确保在 DOM 更新后执行
|
||||
watch(
|
||||
() => tabsStore.activeTab,
|
||||
(newVal, oldVal) => {
|
||||
// 如果新值和当前路由路径不同,且不是初始化(oldVal 不为 undefined),才进行跳转
|
||||
// 注意:这个watch主要用于处理tab点击切换,菜单点击由handleAsideMenuClick直接处理
|
||||
if (newVal && oldVal !== undefined && router.currentRoute.value.fullPath !== newVal) {
|
||||
// 如果切换到首页,关闭其他所有tab
|
||||
if (newVal === '/home') {
|
||||
tabsStore.closeAll();
|
||||
}
|
||||
|
||||
// 检查路由是否存在
|
||||
const routeExists = router.resolve(newVal).matched.length > 0;
|
||||
|
||||
if (routeExists) {
|
||||
// 路由存在,直接跳转
|
||||
nextTick(() => {
|
||||
router.push(newVal).catch(err => {
|
||||
if (err.name !== 'NavigationDuplicated') {
|
||||
console.error('路由跳转失败:', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// 如果路由不存在,等待一下再重试(可能是路由还在加载中)
|
||||
setTimeout(() => {
|
||||
const retryRouteExists = router.resolve(newVal).matched.length > 0;
|
||||
if (retryRouteExists && router.currentRoute.value.fullPath !== newVal) {
|
||||
router.push(newVal).catch(err => {
|
||||
if (err.name !== 'NavigationDuplicated') {
|
||||
console.error('路由跳转失败:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ flush: 'post' }
|
||||
);
|
||||
|
||||
// ========== 右键菜单逻辑 ========== //
|
||||
const contextMenu = reactive({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
tab: null,
|
||||
});
|
||||
const contextDropdownRef = ref(null);
|
||||
|
||||
// 处理 tabs 容器的右键事件
|
||||
const handleTabsContextMenu = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// 找到点击的 tab item 元素(el-tabs__item)
|
||||
let target = event.target;
|
||||
let tabItem = null;
|
||||
|
||||
// 向上查找 el-tabs__item 元素
|
||||
while (target && target !== event.currentTarget) {
|
||||
if (target.classList && target.classList.contains('el-tabs__item')) {
|
||||
tabItem = target;
|
||||
break;
|
||||
}
|
||||
target = target.parentElement;
|
||||
}
|
||||
|
||||
if (!tabItem) return;
|
||||
|
||||
// Element Plus 的 tab item 的 id 格式通常是 "tab-{name}",其中 name 是 tab-pane 的 name 属性
|
||||
const tabId = tabItem.id;
|
||||
if (tabId && tabId.startsWith('tab-')) {
|
||||
const tabName = tabId.replace('tab-', '');
|
||||
const matchedTab = tabsStore.tabList.find(t => t.fullPath === tabName);
|
||||
if (matchedTab) {
|
||||
showContextMenu(event, matchedTab);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果通过 id 找不到,尝试通过 aria-controls 或其他属性
|
||||
const ariaControls = tabItem.getAttribute('aria-controls');
|
||||
if (ariaControls) {
|
||||
const tabName = ariaControls.replace('pane-', '');
|
||||
const matchedTab = tabsStore.tabList.find(t => t.fullPath === tabName);
|
||||
if (matchedTab) {
|
||||
showContextMenu(event, matchedTab);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 显示右键菜单
|
||||
const showContextMenu = (event, tab) => {
|
||||
// 使用 nextTick 确保在下一个事件循环中更新状态,避免在渲染过程中触发更新
|
||||
nextTick(() => {
|
||||
contextMenu.visible = true;
|
||||
contextMenu.x = event.clientX;
|
||||
contextMenu.y = event.clientY;
|
||||
contextMenu.tab = tab;
|
||||
|
||||
// 延迟添加事件监听器,确保菜单已渲染
|
||||
// 关键:完全排除编辑器区域,确保编辑器的事件不被干扰
|
||||
setTimeout(() => {
|
||||
const hideMenuHandler = (e) => {
|
||||
// 如果右键菜单已经隐藏,直接返回
|
||||
if (!contextMenu.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = e.target;
|
||||
if (!target) {
|
||||
hideContextMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查点击是否在右键菜单本身上
|
||||
if (target.closest?.('.context-menu')) {
|
||||
return; // 点击在菜单上,不隐藏
|
||||
}
|
||||
|
||||
// ========== 关键修复:优先检查编辑器区域 ==========
|
||||
// 如果在编辑器区域内,立即返回,不执行任何操作,让编辑器的事件正常处理
|
||||
|
||||
// 先检查是否在编辑器包装器内(最快判断)
|
||||
const wangEditorWrapper = target.closest?.('.wang-editor-wrapper');
|
||||
if (wangEditorWrapper) {
|
||||
// 在编辑器包装器内,完全不做任何处理,让编辑器正常处理
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查所有可能的编辑器元素(包括工具栏、下拉面板、模态框等)
|
||||
const editorSelectors = [
|
||||
'.w-e-toolbar',
|
||||
'.w-e-drop-panel',
|
||||
'.w-e-modal',
|
||||
'.w-e-toolbar-menu',
|
||||
'.toolbar-container',
|
||||
'.editor-container',
|
||||
'.w-e-text-container',
|
||||
'.w-e-text',
|
||||
// 检查是否有 WangEditor 相关的元素
|
||||
'[data-menu-key]',
|
||||
'[class*="w-e-"]'
|
||||
];
|
||||
|
||||
for (const selector of editorSelectors) {
|
||||
if (target.closest?.(selector)) {
|
||||
// 在编辑器区域内,完全不做任何处理,让编辑器的事件正常处理
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 其他区域点击,隐藏菜单
|
||||
hideContextMenu();
|
||||
};
|
||||
|
||||
// 关键修复:使用 capture: false 在冒泡阶段处理,并且延迟注册
|
||||
// 这样可以确保编辑器的监听器(通常在目标元素上)先处理事件
|
||||
// 然后再处理我们的监听器(在 body 上)
|
||||
// 注意:只有在右键菜单显示时才注册,并且排除编辑器区域
|
||||
setTimeout(() => {
|
||||
// 再次检查右键菜单是否仍然可见
|
||||
if (contextMenu.visible) {
|
||||
document.body.addEventListener('click', hideMenuHandler, { once: true, capture: false, passive: true });
|
||||
}
|
||||
}, 100); // 延迟注册,确保编辑器的事件监听器已经注册并可以正常处理
|
||||
|
||||
const hideContextMenuHandler = (e) => {
|
||||
const target = e.target;
|
||||
if (!target) {
|
||||
hideContextMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
// 排除编辑器区域和右键菜单本身
|
||||
if (target.closest?.('.w-e-toolbar') || target.closest?.('.context-menu') || target.closest?.('.wang-editor-wrapper')) {
|
||||
return;
|
||||
}
|
||||
hideContextMenu();
|
||||
};
|
||||
|
||||
document.body.addEventListener('contextmenu', hideContextMenuHandler, { once: true });
|
||||
}, 0);
|
||||
});
|
||||
};
|
||||
function hideContextMenu() {
|
||||
contextMenu.visible = false;
|
||||
contextMenu.tab = null;
|
||||
}
|
||||
function closeTabContextTab() {
|
||||
if (contextMenu.tab && contextMenu.tab.fullPath !== defaultDashboardPath) {
|
||||
tabsStore.removeTab(contextMenu.tab.fullPath);
|
||||
}
|
||||
hideContextMenu();
|
||||
}
|
||||
// 关闭左侧
|
||||
function closeLeftContextTab() {
|
||||
if (contextMenu.tab) {
|
||||
tabsStore.closeLeft(contextMenu.tab.fullPath);
|
||||
// 如果当前路由对应的tab被关闭了,跳转到激活的tab
|
||||
if (!tabsStore.tabList.find(tab => tab.fullPath === route.fullPath)) {
|
||||
router.push(tabsStore.activeTab);
|
||||
}
|
||||
}
|
||||
hideContextMenu();
|
||||
}
|
||||
|
||||
// 关闭右侧
|
||||
function closeRightContextTab() {
|
||||
if (contextMenu.tab) {
|
||||
tabsStore.closeRight(contextMenu.tab.fullPath);
|
||||
// 如果当前路由对应的tab被关闭了,跳转到激活的tab
|
||||
if (!tabsStore.tabList.find(tab => tab.fullPath === route.fullPath)) {
|
||||
router.push(tabsStore.activeTab);
|
||||
}
|
||||
}
|
||||
hideContextMenu();
|
||||
}
|
||||
|
||||
// 关闭其他
|
||||
function closeOthersContextTab() {
|
||||
if (contextMenu.tab) {
|
||||
tabsStore.setActiveTab(contextMenu.tab.fullPath);
|
||||
tabsStore.closeOthers();
|
||||
if (!tabsStore.tabList.find(tab => tab.fullPath === route.fullPath)) {
|
||||
router.push(tabsStore.activeTab);
|
||||
}
|
||||
}
|
||||
hideContextMenu();
|
||||
}
|
||||
|
||||
// 关闭全部
|
||||
function closeAllTabs() {
|
||||
tabsStore.closeAll();
|
||||
hideContextMenu();
|
||||
router.push(defaultDashboardPath);
|
||||
}
|
||||
|
||||
// 计算是否可以关闭左侧/右侧
|
||||
const canCloseLeft = computed(() => {
|
||||
if (!contextMenu.tab) return false;
|
||||
const targetIndex = tabsStore.tabList.findIndex(t => t.fullPath === contextMenu.tab.fullPath);
|
||||
// 至少左侧有一个可关闭的tab(排除首页)
|
||||
return targetIndex > 0 && tabsStore.tabList.slice(0, targetIndex).some(t => t.fullPath !== defaultDashboardPath);
|
||||
});
|
||||
|
||||
const canCloseRight = computed(() => {
|
||||
if (!contextMenu.tab) return false;
|
||||
const targetIndex = tabsStore.tabList.findIndex(t => t.fullPath === contextMenu.tab.fullPath);
|
||||
// 右侧至少有一个tab
|
||||
return targetIndex < tabsStore.tabList.length - 1;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="common-layout">
|
||||
<el-container class="main-container">
|
||||
<common-aside @menu-click="handleAsideMenuClick" />
|
||||
<el-container>
|
||||
<el-header class="main-header">
|
||||
<common-header />
|
||||
</el-header>
|
||||
<el-main class="right-main">
|
||||
<div class="multi-tabs-wrapper">
|
||||
<el-tabs
|
||||
v-model="tabsStore.activeTab"
|
||||
type="card"
|
||||
class="multi-tabs"
|
||||
closable
|
||||
@tab-remove="tabsCloseTab"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="tab in tabsStore.tabList"
|
||||
:key="tab.fullPath"
|
||||
:label="tab.title"
|
||||
:name="tab.fullPath"
|
||||
:closable="tab.fullPath !== defaultDashboardPath"
|
||||
:data-tab-path="tab.fullPath"
|
||||
/>
|
||||
</el-tabs>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<teleport to="body">
|
||||
<div
|
||||
v-if="contextMenu.visible"
|
||||
class="context-menu"
|
||||
:style="{
|
||||
left: contextMenu.x + 'px',
|
||||
top: contextMenu.y + 'px'
|
||||
}"
|
||||
@click.stop
|
||||
>
|
||||
<div class="context-menu-item"
|
||||
:class="{ 'is-disabled': contextMenu.tab && contextMenu.tab.fullPath === defaultDashboardPath }"
|
||||
@click="!((contextMenu.tab && contextMenu.tab.fullPath === defaultDashboardPath)) && closeTabContextTab()">
|
||||
关闭
|
||||
</div>
|
||||
<div class="context-menu-item"
|
||||
:class="{ 'is-disabled': !canCloseLeft }"
|
||||
@click="canCloseLeft && closeLeftContextTab()">
|
||||
关闭左侧
|
||||
</div>
|
||||
<div class="context-menu-item"
|
||||
:class="{ 'is-disabled': !canCloseRight }"
|
||||
@click="canCloseRight && closeRightContextTab()">
|
||||
关闭右侧
|
||||
</div>
|
||||
<div class="context-menu-item"
|
||||
:class="{ 'is-disabled': contextMenu.tab && contextMenu.tab.fullPath === defaultDashboardPath && tabsStore.tabList.length <= 1 }"
|
||||
@click="!((contextMenu.tab && contextMenu.tab.fullPath === defaultDashboardPath && tabsStore.tabList.length <= 1)) && closeOthersContextTab()">
|
||||
关闭其他
|
||||
</div>
|
||||
<div class="context-menu-item"
|
||||
:class="{ 'is-disabled': tabsStore.tabList.length <= 1 }"
|
||||
@click="tabsStore.tabList.length > 1 && closeAllTabs()">
|
||||
关闭全部
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
<!-- 右侧操作按钮 -->
|
||||
<div class="tabs-extra-actions">
|
||||
<el-dropdown trigger="click">
|
||||
<el-button type="primary" link size="small" class="extra-btn">
|
||||
<el-icon><More /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="closeOthers">
|
||||
关闭其他
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="closeAll">
|
||||
关闭全部
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容毛玻璃卡片容器 -->
|
||||
<div class="main-panel glass-card">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :max="10">
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
|
||||
<!-- 回到顶部按钮 -->
|
||||
<el-backtop :target="'.right-main'" :visibility-height="300" :right="30" :bottom="50">
|
||||
<div class="backtop-button">
|
||||
<el-icon :size="20">
|
||||
<ArrowUp />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-backtop>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.common-layout, .main-container {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
background-color: var(--bg-color-page);
|
||||
transition: background-color 0.3s ease;
|
||||
:deep(.el-aside) {
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
height: 100vh;
|
||||
}
|
||||
.main-header {
|
||||
background-color: var(--header-bg-color, #3973ff);
|
||||
transition: background-color 0.3s ease;
|
||||
height: 80px;
|
||||
padding: 0;
|
||||
}
|
||||
.right-main {
|
||||
background-color: var(--el-bg-color-page);
|
||||
color: var(--el-text-color-primary);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
|
||||
.multi-tabs-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.multi-tabs {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
:deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-wrap) {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--el-border-color) transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--el-border-color);
|
||||
border-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-border-color-darker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
margin-right: 8px;
|
||||
padding: 8px 16px;
|
||||
height: 36px;
|
||||
line-height: 20px;
|
||||
color: var(--el-text-color-regular);
|
||||
background: var(--el-fill-color-lighter);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary-light-7);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--el-color-primary);
|
||||
border-color: #4f84ff;
|
||||
background: #4f84ff;
|
||||
color: #fff;
|
||||
|
||||
.el-icon-close {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-icon-close {
|
||||
margin-left: 8px;
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tabs-extra-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.extra-btn {
|
||||
padding: 8px;
|
||||
font-size: 18px;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-menu){
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
// 回到顶部按钮样式
|
||||
.backtop-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--el-color-primary);
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4px 12px rgba(64, 129, 255, 0.4);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-color-primary-light-3);
|
||||
box-shadow: 0 6px 16px rgba(64, 129, 255, 0.5);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
// 右键菜单样式 - 全局样式,因为使用了 teleport 到 body
|
||||
.context-menu {
|
||||
position: fixed !important;
|
||||
z-index: 9999 !important;
|
||||
background: var(--el-bg-color-overlay) !important;
|
||||
border: 1px solid var(--el-border-color-lighter) !important;
|
||||
border-radius: 4px !important;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1) !important;
|
||||
min-width: 120px !important;
|
||||
padding: 4px 0 !important;
|
||||
pointer-events: auto !important; // 确保菜单本身可以接收点击
|
||||
|
||||
.context-menu-item {
|
||||
padding: 8px 16px !important;
|
||||
cursor: pointer !important;
|
||||
color: var(--el-text-color-primary) !important;
|
||||
font-size: 14px !important;
|
||||
transition: background-color 0.2s !important;
|
||||
|
||||
&:hover:not(.is-disabled) {
|
||||
background-color: var(--el-fill-color-light) !important;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
color: var(--el-text-color-disabled) !important;
|
||||
cursor: not-allowed !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 确保编辑器工具栏和下拉面板的 z-index 高于右键菜单
|
||||
:deep(.w-e-toolbar),
|
||||
:deep(.w-e-drop-panel),
|
||||
:deep(.w-e-modal),
|
||||
:deep(.w-e-toolbar-menu) {
|
||||
z-index: 10000 !important; // 高于右键菜单的 9999
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user