| | |
| | | import SettingsIcon from '@mui/icons-material/Settings'; |
| | | import ClearAllIcon from '@mui/icons-material/ClearAll'; |
| | | |
| | | // 固定的标签页配置 (不可关闭) |
| | | // 固定标签页配置(不可关闭) |
| | | const FIXED_TABS = [ |
| | | { path: '/dashboard', name: 'menu.dashboard', closable: false } |
| | | ]; |
| | | |
| | | // 特殊页面配置 |
| | | const SPECIAL_PAGES = { |
| | | '/settings': { name: 'menu.settings', closable: true }, |
| | | '/dashboard': { name: 'menu.dashboard', closable: false } |
| | | }; |
| | | |
| | | // 忽略的路径模式 (登录页、根路径等) |
| | | const IGNORED_PATHS = ['/', '/login']; |
| | | |
| | | // 从localStorage获取已保存的标签页 |
| | | const getSavedTabs = () => { |
| | | try { |
| | | const saved = localStorage.getItem('openTabs'); |
| | | if (saved) { |
| | | const parsed = JSON.parse(saved); |
| | | // 确保 dashboard 始终存在且是第一个 |
| | | const hasDashboard = parsed.some(tab => tab.path === '/dashboard'); |
| | | if (!hasDashboard) { |
| | | return [...FIXED_TABS, ...parsed.filter(tab => tab.path !== '/dashboard')]; |
| | | } |
| | | // 确保 dashboard 在第一位 |
| | | const dashboard = parsed.find(tab => tab.path === '/dashboard'); |
| | | const others = parsed.filter(tab => tab.path !== '/dashboard'); |
| | | return [{ ...dashboard, closable: false }, ...others]; |
| | |
| | | } |
| | | }; |
| | | |
| | | // 保存标签页到localStorage |
| | | const saveTabs = (tabs) => { |
| | | try { |
| | | localStorage.setItem('openTabs', JSON.stringify(tabs)); |
| | |
| | | } |
| | | }; |
| | | |
| | | // 规范化路径 - 处理带ID的详情/编辑页面 |
| | | const normalizePath = (path) => { |
| | | if (!path) return path; |
| | | |
| | | // 匹配模式: /resource/123/show 或 /resource/123/edit 或 /resource/123 |
| | | // 将其规范化为基础路径 /resource |
| | | const patterns = [ |
| | | /^(\/[^/]+)\/\d+\/(show|edit)$/, // /task/123/show -> /task |
| | | /^(\/[^/]+)\/\d+$/, // /task/123 -> /task |
| | | /^(\/[^/]+)\/create$/, // /task/create -> /task |
| | | /^(\/[^/]+)\/\d+\/(show|edit)$/, |
| | | /^(\/[^/]+)\/\d+$/, |
| | | /^(\/[^/]+)\/create$/, |
| | | ]; |
| | | |
| | | for (const pattern of patterns) { |
| | | const match = path.match(pattern); |
| | | if (match) { |
| | | return match[1]; |
| | | } |
| | | if (match) return match[1]; |
| | | } |
| | | |
| | | return path; |
| | | }; |
| | | |
| | | // 判断两个路径是否属于同一资源 |
| | | const isSameResource = (path1, path2) => { |
| | | return normalizePath(path1) === normalizePath(path2); |
| | | }; |
| | | |
| | | const LONG_PRESS_MS = 300; |
| | | |
| | | const TabsBar = () => { |
| | | const location = useLocation(); |
| | |
| | | const { permissions } = usePermissions(); |
| | | const [tabs, setTabs] = useState(getSavedTabs); |
| | | const pendingNavigationRef = useRef(null); |
| | | const tabsBarRef = useRef(null); |
| | | const [contextMenu, setContextMenu] = useState(null); |
| | | const [contextMenuTab, setContextMenuTab] = useState(null); |
| | | const contextMenuOpenRef = useRef(false); |
| | | contextMenuOpenRef.current = contextMenu !== null; |
| | | |
| | | // 翻译辅助函数,如果翻译不存在则返回默认值 |
| | | const [draggingIndex, setDraggingIndex] = useState(null); |
| | | const [dropIndicatorIndex, setDropIndicatorIndex] = useState(null); |
| | | const longPressTimerRef = useRef(null); |
| | | const longPressIndexRef = useRef(null); |
| | | const justFinishedDragRef = useRef(false); |
| | | |
| | | // 在标签页右键时阻止浏览器默认菜单 |
| | | useEffect(() => { |
| | | const onDocContextMenu = (e) => { |
| | | const inTabsBarByTarget = tabsBarRef.current && tabsBarRef.current.contains(e.target); |
| | | let inTabsBarByPos = false; |
| | | if (tabsBarRef.current) { |
| | | const rect = tabsBarRef.current.getBoundingClientRect(); |
| | | inTabsBarByPos = e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom; |
| | | } |
| | | const inTabsBar = inTabsBarByTarget || inTabsBarByPos; |
| | | const menuOpen = contextMenuOpenRef.current; |
| | | if (inTabsBar || menuOpen) { |
| | | e.preventDefault(); |
| | | } |
| | | }; |
| | | document.addEventListener('contextmenu', onDocContextMenu, true); |
| | | return () => document.removeEventListener('contextmenu', onDocContextMenu, true); |
| | | }, []); |
| | | |
| | | const t = useCallback((key, defaultValue) => { |
| | | const translated = translate(key); |
| | | return translated === key ? defaultValue : translated; |
| | | }, [translate]); |
| | | |
| | | // 处理待执行的导航 |
| | | useEffect(() => { |
| | | if (pendingNavigationRef.current) { |
| | | navigate(pendingNavigationRef.current); |
| | |
| | | } |
| | | }); |
| | | |
| | | // 根据路径查找菜单名称 |
| | | const findMenuName = useCallback((path, menus) => { |
| | | if (!menus || !path) return null; |
| | | const cleanPath = path.replace(/^\//, ''); |
| | |
| | | return searchMenu(menus); |
| | | }, []); |
| | | |
| | | // 获取标签页显示名称 |
| | | const getTabLabel = useCallback((tab) => { |
| | | // 固定标签页直接返回翻译后的名称 |
| | | if (tab.name) { |
| | | return translate(tab.name); |
| | | } |
| | | |
| | | // 检查特殊页面 |
| | | if (tab.name) return translate(tab.name); |
| | | const normalizedPath = normalizePath(tab.path); |
| | | if (SPECIAL_PAGES[normalizedPath]) { |
| | | return translate(SPECIAL_PAGES[normalizedPath].name); |
| | | } |
| | | |
| | | // 动态标签页尝试从权限中获取名称 |
| | | if (permissions) { |
| | | const menuName = findMenuName(tab.path, permissions); |
| | | if (menuName) { |
| | | return translate(menuName); |
| | | } |
| | | if (menuName) return translate(menuName); |
| | | } |
| | | // 默认返回路径 |
| | | return tab.path.replace(/^\//, '').split('/')[0] || tab.path; |
| | | }, [translate, permissions, findMenuName]); |
| | | |
| | | // 监听路由变化,添加新标签页 |
| | | useEffect(() => { |
| | | const currentPath = location.pathname; |
| | | |
| | | // 忽略特定路径 |
| | | if (IGNORED_PATHS.includes(currentPath)) { |
| | | return; |
| | | } |
| | | if (IGNORED_PATHS.includes(currentPath)) return; |
| | | |
| | | setTabs(prevTabs => { |
| | | // 检查是否已有相同资源的标签页 |
| | | const existingTabIndex = prevTabs.findIndex(tab => |
| | | tab.path === currentPath || isSameResource(tab.path, currentPath) |
| | | ); |
| | | |
| | | if (existingTabIndex >= 0) { |
| | | // 如果路径完全相同,不需要更新 |
| | | if (prevTabs[existingTabIndex].path === currentPath) { |
| | | return prevTabs; |
| | | } |
| | | // 更新现有标签页的路径(保持标签位置) |
| | | if (prevTabs[existingTabIndex].path === currentPath) return prevTabs; |
| | | const updatedTabs = [...prevTabs]; |
| | | updatedTabs[existingTabIndex] = { |
| | | ...updatedTabs[existingTabIndex], |
| | |
| | | return updatedTabs; |
| | | } |
| | | |
| | | // 查找菜单名称 |
| | | let menuName = null; |
| | | if (permissions) { |
| | | menuName = findMenuName(currentPath, permissions); |
| | | } |
| | | |
| | | // 检查特殊页面 |
| | | if (permissions) menuName = findMenuName(currentPath, permissions); |
| | | const normalizedPath = normalizePath(currentPath); |
| | | if (SPECIAL_PAGES[normalizedPath] && !menuName) { |
| | | menuName = SPECIAL_PAGES[normalizedPath].name; |
| | | } |
| | | |
| | | // 添加新标签页 |
| | | const newTab = { |
| | | path: currentPath, |
| | | name: menuName, |
| | |
| | | }); |
| | | }, [location.pathname, permissions, findMenuName]); |
| | | |
| | | // 切换标签页 |
| | | const handleTabChange = (event, newValue) => { |
| | | if (justFinishedDragRef.current) { |
| | | justFinishedDragRef.current = false; |
| | | return; |
| | | } |
| | | const targetTab = tabs[newValue]; |
| | | if (targetTab && targetTab.path !== location.pathname) { |
| | | navigate(targetTab.path); |
| | | } |
| | | }; |
| | | |
| | | // 关闭标签页 |
| | | const handleCloseTab = (event, tabPath) => { |
| | | event.stopPropagation(); |
| | | event.preventDefault(); |
| | |
| | | const tabIndex = tabs.findIndex(tab => tab.path === tabPath); |
| | | const newTabs = tabs.filter(tab => tab.path !== tabPath); |
| | | |
| | | // 如果关闭的是当前标签页,需要导航到其他标签页 |
| | | if (location.pathname === tabPath || isSameResource(location.pathname, tabPath)) { |
| | | // 优先导航到左边的标签页,否则导航到右边的 |
| | | const newIndex = Math.min(tabIndex, newTabs.length - 1); |
| | | if (newIndex >= 0 && newTabs[newIndex]) { |
| | | pendingNavigationRef.current = newTabs[newIndex].path; |
| | |
| | | setTabs(newTabs); |
| | | }; |
| | | |
| | | |
| | | |
| | | // 关闭所有标签页(除了dashboard) |
| | | const handleCloseAll = () => { |
| | | const dashboardTab = tabs.find(tab => tab.path === '/dashboard'); |
| | | const newTabs = [dashboardTab || { ...FIXED_TABS[0] }]; |
| | | |
| | | saveTabs(newTabs); |
| | | setTabs(newTabs); |
| | | |
| | | if (location.pathname !== '/dashboard') { |
| | | navigate('/dashboard'); |
| | | } |
| | | }; |
| | | |
| | | // 关闭其他标签页(保留指定标签页) |
| | | const handleCloseOthers = (keepTabPath) => { |
| | | const keepTab = tabs.find(tab => tab.path === keepTabPath); |
| | | if (!keepTab) { |
| | | return; |
| | | } |
| | | |
| | | // 保留指定标签页和dashboard(如果dashboard不是指定标签页) |
| | | if (!keepTab) return; |
| | | const dashboardTab = tabs.find(tab => tab.path === '/dashboard'); |
| | | const newTabs = []; |
| | | |
| | | // 如果指定标签页不是dashboard,确保dashboard也在列表中 |
| | | if (keepTab.path !== '/dashboard' && dashboardTab) { |
| | | newTabs.push(dashboardTab); |
| | | } |
| | | |
| | | // 添加指定标签页(如果还没有添加) |
| | | if (!newTabs.some(tab => tab.path === keepTab.path)) { |
| | | newTabs.push(keepTab); |
| | | } |
| | | |
| | | saveTabs(newTabs); |
| | | setTabs(newTabs); |
| | | setContextMenu(null); |
| | | setContextMenuTab(null); |
| | | }; |
| | | |
| | | //关闭左侧标签页(保留指定标签页及其右侧的所有标签页) |
| | | const handleCloseLeft = (keepTabPath) => { |
| | | const keepTabIndex = tabs.findIndex(tab => tab.path === keepTabPath); |
| | | if (keepTabIndex < 0) { |
| | | return; |
| | | } |
| | | |
| | | // 保留指定标签页及其右侧的所有标签页 |
| | | const newTabs = tabs.slice(keepTabIndex); |
| | | |
| | | // 确保dashboard始终在第一位(如果存在) |
| | | if (keepTabIndex < 0) return; |
| | | let newTabs = tabs.slice(keepTabIndex); |
| | | const dashboardTab = newTabs.find(tab => tab.path === '/dashboard'); |
| | | if (dashboardTab && newTabs[0].path !== '/dashboard') { |
| | | const dashboardIndex = newTabs.findIndex(tab => tab.path === '/dashboard'); |
| | |
| | | newTabs.unshift(dashboardTab); |
| | | } |
| | | } |
| | | |
| | | saveTabs(newTabs); |
| | | setTabs(newTabs); |
| | | setContextMenu(null); |
| | | setContextMenuTab(null); |
| | | }; |
| | | |
| | | // 关闭右侧标签页(保留指定标签页及其左侧的所有标签页) |
| | | const handleCloseRight = (keepTabPath) => { |
| | | const keepTabIndex = tabs.findIndex(tab => tab.path === keepTabPath); |
| | | if (keepTabIndex < 0) { |
| | | return; |
| | | } |
| | | |
| | | // 保留指定标签页及其左侧的所有标签页 |
| | | if (keepTabIndex < 0) return; |
| | | const newTabs = tabs.slice(0, keepTabIndex + 1); |
| | | |
| | | saveTabs(newTabs); |
| | | setTabs(newTabs); |
| | | setContextMenu(null); |
| | | setContextMenuTab(null); |
| | | }; |
| | | |
| | | // 处理右键菜单 |
| | | const handleContextMenu = (event, tab) => { |
| | | event.preventDefault(); |
| | | event.stopPropagation(); |
| | |
| | | canCloseRightForTab(tab.path) || |
| | | canCloseOthersForTab(tab.path); |
| | | if (!hasItems) return; |
| | | setContextMenu({ |
| | | mouseX: event.clientX + 2, |
| | | mouseY: event.clientY - 6, |
| | | }); |
| | | setContextMenu({ mouseX: event.clientX + 2, mouseY: event.clientY - 6 }); |
| | | setContextMenuTab(tab); |
| | | }; |
| | | |
| | | // 关闭右键菜单 |
| | | const handleCloseContextMenu = () => { |
| | | setContextMenu(null); |
| | | setContextMenuTab(null); |
| | | }; |
| | | |
| | | // 右键菜单:关闭当前标签 |
| | | const handleCloseCurrentTab = () => { |
| | | if (!contextMenuTab) { |
| | | handleCloseContextMenu(); |
| | | return; |
| | | } |
| | | |
| | | const tabPath = contextMenuTab.path; |
| | | const tabIndex = tabs.findIndex(tab => tab.path === tabPath); |
| | | const newTabs = tabs.filter(tab => tab.path !== tabPath); |
| | | |
| | | // 如果关闭的是当前标签页,需要导航到其他标签页 |
| | | if (location.pathname === tabPath || isSameResource(location.pathname, tabPath)) { |
| | | // 优先导航到左边的标签页,否则导航到右边的 |
| | | const newIndex = Math.min(tabIndex, newTabs.length - 1); |
| | | if (newIndex >= 0 && newTabs[newIndex]) { |
| | | pendingNavigationRef.current = newTabs[newIndex].path; |
| | |
| | | pendingNavigationRef.current = '/dashboard'; |
| | | } |
| | | } |
| | | |
| | | saveTabs(newTabs); |
| | | setTabs(newTabs); |
| | | handleCloseContextMenu(); |
| | | }; |
| | | |
| | | // 右键菜单:关闭其他标签 |
| | | const handleCloseOtherTabs = () => { |
| | | if (contextMenuTab) { |
| | | handleCloseOthers(contextMenuTab.path); |
| | | } |
| | | if (contextMenuTab) handleCloseOthers(contextMenuTab.path); |
| | | }; |
| | | |
| | | // 右键菜单:关闭左侧标签 |
| | | const handleCloseLeftTabs = () => { |
| | | if (contextMenuTab) { |
| | | handleCloseLeft(contextMenuTab.path); |
| | | } |
| | | if (contextMenuTab) handleCloseLeft(contextMenuTab.path); |
| | | }; |
| | | |
| | | // 右键菜单:关闭右侧标签 |
| | | const handleCloseRightTabs = () => { |
| | | if (contextMenuTab) { |
| | | handleCloseRight(contextMenuTab.path); |
| | | } |
| | | if (contextMenuTab) handleCloseRight(contextMenuTab.path); |
| | | }; |
| | | |
| | | const canCloseOthersForTab = (tabPath) => { |
| | | return tabs.filter(tab => { |
| | | const isTarget = tab.path === tabPath || isSameResource(tab.path, tabPath); |
| | | return !isTarget && tab.closable; |
| | | }).length > 0; |
| | | }; |
| | | const canCloseLeftForTab = (tabPath) => { |
| | | const tabIndex = tabs.findIndex(tab => tab.path === tabPath || isSameResource(tab.path, tabPath)); |
| | | if (tabIndex <= 0) return false; |
| | | return tabs.slice(0, tabIndex).some(tab => tab.closable); |
| | | }; |
| | | const canCloseRightForTab = (tabPath) => { |
| | | const tabIndex = tabs.findIndex(tab => tab.path === tabPath || isSameResource(tab.path, tabPath)); |
| | | if (tabIndex < 0 || tabIndex >= tabs.length - 1) return false; |
| | | return tabs.slice(tabIndex + 1).some(tab => tab.closable); |
| | | }; |
| | | |
| | | // 获取当前标签页索引 |
| | | const currentTabIndex = tabs.findIndex(tab => |
| | | tab.path === location.pathname || isSameResource(tab.path, location.pathname) |
| | | ); |
| | | const activeIndex = currentTabIndex >= 0 ? currentTabIndex : 0; |
| | | |
| | | // 判断是否有可关闭的标签 |
| | | const hasClosableTabs = tabs.some(tab => tab.closable); |
| | | |
| | | // 判断指定标签页是否可以关闭其他标签 |
| | | const canCloseOthersForTab = (tabPath) => { |
| | | return tabs.filter(tab => { |
| | | const isTargetTab = tab.path === tabPath || isSameResource(tab.path, tabPath); |
| | | return !isTargetTab && tab.closable; |
| | | }).length > 0; |
| | | }; |
| | | // 右键打开时,点击菜单和标签栏外关闭 |
| | | const menuPaperRef = useRef(null); |
| | | useEffect(() => { |
| | | if (contextMenu === null) return; |
| | | const onDocClick = (e) => { |
| | | const inMenu = menuPaperRef.current && menuPaperRef.current.contains(e.target); |
| | | const inTabsBar = tabsBarRef.current && tabsBarRef.current.contains(e.target); |
| | | if (!inMenu && !inTabsBar) { |
| | | setContextMenu(null); |
| | | setContextMenuTab(null); |
| | | } |
| | | }; |
| | | document.addEventListener('mousedown', onDocClick, true); |
| | | return () => document.removeEventListener('mousedown', onDocClick, true); |
| | | }, [contextMenu]); |
| | | |
| | | // 判断指定标签页是否可以关闭左侧标签 |
| | | const canCloseLeftForTab = (tabPath) => { |
| | | const tabIndex = tabs.findIndex(tab => tab.path === tabPath || isSameResource(tab.path, tabPath)); |
| | | if (tabIndex <= 0) { |
| | | return false; // 没有左侧标签或这是第一个标签 |
| | | const clearLongPressTimer = useCallback(() => { |
| | | if (longPressTimerRef.current) { |
| | | clearTimeout(longPressTimerRef.current); |
| | | longPressTimerRef.current = null; |
| | | } |
| | | // 检查左侧是否有可关闭的标签(排除dashboard) |
| | | return tabs.slice(0, tabIndex).some(tab => tab.closable); |
| | | }; |
| | | longPressIndexRef.current = null; |
| | | }, []); |
| | | |
| | | // 判断指定标签页是否可以关闭右侧标签 |
| | | const canCloseRightForTab = (tabPath) => { |
| | | const tabIndex = tabs.findIndex(tab => tab.path === tabPath || isSameResource(tab.path, tabPath)); |
| | | if (tabIndex < 0 || tabIndex >= tabs.length - 1) { |
| | | return false; // 没有右侧标签或这是最后一个标签 |
| | | } |
| | | // 检查右侧是否有可关闭的标签 |
| | | return tabs.slice(tabIndex + 1).some(tab => tab.closable); |
| | | }; |
| | | const handleTabPointerDown = useCallback((e, index) => { |
| | | if (index < 0) return; |
| | | longPressIndexRef.current = index; |
| | | longPressTimerRef.current = setTimeout(() => { |
| | | longPressTimerRef.current = null; |
| | | setDraggingIndex(index); |
| | | setDropIndicatorIndex(index); |
| | | }, LONG_PRESS_MS); |
| | | }, []); |
| | | |
| | | const handleTabPointerUp = useCallback(() => { |
| | | clearLongPressTimer(); |
| | | }, [clearLongPressTimer]); |
| | | |
| | | useEffect(() => { |
| | | if (draggingIndex === null) return; |
| | | const getDropIndex = (clientX) => { |
| | | const nodes = tabsBarRef.current?.querySelectorAll('[data-tab-index]'); |
| | | if (!nodes?.length) return draggingIndex; |
| | | for (let i = 0; i < nodes.length; i++) { |
| | | const rect = nodes[i].getBoundingClientRect(); |
| | | const mid = rect.left + rect.width / 2; |
| | | if (clientX <= mid) { |
| | | const drop = i; |
| | | if (draggingIndex === 0) return drop <= 0 ? 0 : draggingIndex; |
| | | return drop <= 0 ? 1 : drop; |
| | | } |
| | | } |
| | | const drop = nodes.length; |
| | | return draggingIndex === 0 ? 0 : drop; |
| | | }; |
| | | const clientXFromEvent = (e) => e.clientX ?? e.touches?.[0]?.clientX; |
| | | const onMove = (e) => { |
| | | const x = clientXFromEvent(e); |
| | | if (x != null) setDropIndicatorIndex(getDropIndex(x)); |
| | | }; |
| | | const onTouchMove = (e) => { |
| | | e.preventDefault(); |
| | | onMove(e); |
| | | }; |
| | | const onUp = () => { |
| | | justFinishedDragRef.current = true; |
| | | setDraggingIndex((di) => { |
| | | setDropIndicatorIndex((dropIdx) => { |
| | | if (di !== null && dropIdx !== null && di !== dropIdx) { |
| | | const newTabs = [...tabs]; |
| | | const [item] = newTabs.splice(di, 1); |
| | | const insertAt = dropIdx > di ? dropIdx - 1 : dropIdx; |
| | | newTabs.splice(insertAt, 0, item); |
| | | const dashboard = newTabs.find((t) => t.path === '/dashboard'); |
| | | if (dashboard && newTabs[0].path !== '/dashboard') { |
| | | const idx = newTabs.indexOf(dashboard); |
| | | newTabs.splice(idx, 1); |
| | | newTabs.unshift(dashboard); |
| | | } |
| | | saveTabs(newTabs); |
| | | setTabs(newTabs); |
| | | } |
| | | return null; |
| | | }); |
| | | return null; |
| | | }); |
| | | }; |
| | | document.addEventListener('mousemove', onMove, true); |
| | | document.addEventListener('mouseup', onUp, true); |
| | | document.addEventListener('touchmove', onTouchMove, { passive: false, capture: true }); |
| | | document.addEventListener('touchend', onUp, true); |
| | | document.addEventListener('touchcancel', onUp, true); |
| | | return () => { |
| | | document.removeEventListener('mousemove', onMove, true); |
| | | document.removeEventListener('mouseup', onUp, true); |
| | | document.removeEventListener('touchmove', onTouchMove, true); |
| | | document.removeEventListener('touchend', onUp, true); |
| | | document.removeEventListener('touchcancel', onUp, true); |
| | | }; |
| | | }, [draggingIndex, tabs]); |
| | | |
| | | return ( |
| | | <Box |
| | | ref={tabsBarRef} |
| | | sx={{ |
| | | width: '100%', |
| | | backgroundColor: '#fff', |
| | |
| | | key={tab.path} |
| | | label={ |
| | | <Box |
| | | data-tab-index={index} |
| | | onContextMenu={(e) => handleContextMenu(e, tab)} |
| | | onMouseDown={(e) => handleTabPointerDown(e, index)} |
| | | onMouseUp={handleTabPointerUp} |
| | | onMouseLeave={handleTabPointerUp} |
| | | onTouchStart={(e) => handleTabPointerDown(e, index)} |
| | | onTouchEnd={handleTabPointerUp} |
| | | onTouchCancel={handleTabPointerUp} |
| | | sx={{ |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | gap: 0.5, |
| | | width: '100%', |
| | | ...(draggingIndex === index && { opacity: 0.7 }), |
| | | }} |
| | | > |
| | | {tab.path === '/dashboard' && ( |
| | |
| | | display: 'inline-flex', |
| | | alignItems: 'center', |
| | | justifyContent: 'center', |
| | | p: 0.35, |
| | | p: 0.25, |
| | | ml: 0.5, |
| | | borderRadius: '50%', |
| | | cursor: 'pointer', |
| | | color: 'inherit', |
| | | '&:hover': { |
| | | backgroundColor: 'rgba(0, 0, 0, 0.1)', |
| | | color: '#d32f2f', |
| | | }, |
| | | }} |
| | | > |
| | | <CloseIcon sx={{ fontSize: 16 }} /> |
| | | <CloseIcon sx={{ fontSize: 14 }} /> |
| | | </Box> |
| | | </Tooltip> |
| | | )} |
| | |
| | | /> |
| | | ))} |
| | | </Tabs> |
| | | {/* 清除所有标签按钮 */} |
| | | {hasClosableTabs && ( |
| | | <Box sx={{ display: 'flex', alignItems: 'center', pr: 1 }}> |
| | | <Divider orientation="vertical" flexItem sx={{ mx: 0.5, height: 20, alignSelf: 'center' }} /> |
| | |
| | | </Tooltip> |
| | | </Box> |
| | | )} |
| | | {/* 右键菜单 */} |
| | | <Menu |
| | | open={contextMenu !== null} |
| | | onClose={handleCloseContextMenu} |
| | |
| | | : undefined |
| | | } |
| | | disableScrollLock |
| | | ModalProps={{ disablePortal: true }} |
| | | ModalProps={{ |
| | | disablePortal: true, |
| | | BackdropProps: { sx: { pointerEvents: 'none' } }, |
| | | }} |
| | | PaperProps={{ |
| | | ref: menuPaperRef, |
| | | sx: { |
| | | minWidth: 120, |
| | | borderRadius: '8px', |
| | |
| | | py: 0.5, |
| | | }, |
| | | }} |
| | | MenuListProps={{ |
| | | sx: { py: 0 }, |
| | | }} |
| | | MenuListProps={{ sx: { py: 0 } }} |
| | | > |
| | | {contextMenuTab && contextMenuTab.closable && ( |
| | | <MenuItem |
| | | onClick={handleCloseCurrentTab} |
| | | sx={{ |
| | | fontSize: '0.8125rem', |
| | | py: 0.75, |
| | | px: 1.5, |
| | | minHeight: 'auto', |
| | | }} |
| | | > |
| | | {t('ra.action.close', '关闭当前标签')} |
| | | </MenuItem> |
| | | )} |
| | | {contextMenuTab && canCloseLeftForTab(contextMenuTab.path) && ( |
| | | <MenuItem |
| | | onClick={handleCloseLeftTabs} |
| | | sx={{ |
| | | fontSize: '0.8125rem', |
| | | py: 0.75, |
| | | px: 1.5, |
| | | minHeight: 'auto', |
| | | }} |
| | | > |
| | | <MenuItem onClick={handleCloseLeftTabs} sx={{ fontSize: '0.8125rem', py: 0.75, px: 1.5, minHeight: 'auto' }}> |
| | | {t('ra.action.closeLeft', '关闭左侧标签')} |
| | | </MenuItem> |
| | | )} |
| | | {contextMenuTab && canCloseRightForTab(contextMenuTab.path) && ( |
| | | <MenuItem |
| | | onClick={handleCloseRightTabs} |
| | | sx={{ |
| | | fontSize: '0.8125rem', |
| | | py: 0.75, |
| | | px: 1.5, |
| | | minHeight: 'auto', |
| | | }} |
| | | > |
| | | <MenuItem onClick={handleCloseRightTabs} sx={{ fontSize: '0.8125rem', py: 0.75, px: 1.5, minHeight: 'auto' }}> |
| | | {t('ra.action.closeRight', '关闭右侧标签')} |
| | | </MenuItem> |
| | | )} |
| | | {contextMenuTab && canCloseOthersForTab(contextMenuTab.path) && ( |
| | | <MenuItem |
| | | onClick={handleCloseOtherTabs} |
| | | sx={{ |
| | | fontSize: '0.8125rem', |
| | | py: 0.75, |
| | | px: 1.5, |
| | | minHeight: 'auto', |
| | | }} |
| | | > |
| | | <MenuItem onClick={handleCloseOtherTabs} sx={{ fontSize: '0.8125rem', py: 0.75, px: 1.5, minHeight: 'auto' }}> |
| | | {t('ra.action.closeOthers', '关闭其他标签')} |
| | | </MenuItem> |
| | | )} |