| | |
| | | import React, { useState, useEffect, useCallback, useRef } from 'react'; |
| | | import { useLocation, useNavigate } from 'react-router-dom'; |
| | | import { useTranslate, usePermissions } from 'react-admin'; |
| | | import { Box, Tab, Tabs, IconButton, Tooltip, Divider } from '@mui/material'; |
| | | import { Box, Tab, Tabs, IconButton, Tooltip, Divider, Menu, MenuItem } from '@mui/material'; |
| | | import CloseIcon from '@mui/icons-material/Close'; |
| | | import HomeIcon from '@mui/icons-material/Home'; |
| | | import SettingsIcon from '@mui/icons-material/Settings'; |
| | |
| | | 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; |
| | | |
| | | // 在标签页右键,阻止浏览器默认菜单 |
| | | 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); |
| | | }, []); // 用 ref 读最新状态,避免快速点击闭包滞后 |
| | | |
| | | // 翻译辅助,无翻译 key 时返回默认值 |
| | | const t = useCallback((key, defaultValue) => { |
| | | const translated = translate(key); |
| | | return translated === key ? defaultValue : translated; |
| | | }, [translate]); |
| | | |
| | | // 处理待执行的导航 |
| | | useEffect(() => { |
| | |
| | | } |
| | | }; |
| | | |
| | | // 关闭其他标签 |
| | | const handleCloseOthers = (keepTabPath) => { |
| | | const keepTab = tabs.find(tab => tab.path === keepTabPath); |
| | | if (!keepTab) return; |
| | | const dashboardTab = tabs.find(tab => tab.path === '/dashboard'); |
| | | const newTabs = []; |
| | | 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; |
| | | 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'); |
| | | if (dashboardIndex > 0) { |
| | | newTabs.splice(dashboardIndex, 1); |
| | | 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; |
| | | const newTabs = tabs.slice(0, keepTabIndex + 1); |
| | | saveTabs(newTabs); |
| | | setTabs(newTabs); |
| | | setContextMenu(null); |
| | | setContextMenuTab(null); |
| | | }; |
| | | |
| | | // 右键菜单:打开 |
| | | const handleContextMenu = (event, tab) => { |
| | | event.preventDefault(); |
| | | event.stopPropagation(); |
| | | if (!tab) return; |
| | | const tabObj = tabs.find(t => t.path === tab.path || isSameResource(t.path, tab.path)); |
| | | if (!tabObj) return; |
| | | const hasItems = tabObj.closable || |
| | | canCloseLeftForTab(tab.path) || |
| | | canCloseRightForTab(tab.path) || |
| | | canCloseOthersForTab(tab.path); |
| | | if (!hasItems) return; |
| | | 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; |
| | | } else { |
| | | pendingNavigationRef.current = '/dashboard'; |
| | | } |
| | | } |
| | | saveTabs(newTabs); |
| | | setTabs(newTabs); |
| | | handleCloseContextMenu(); |
| | | }; |
| | | |
| | | // 右键菜单:关闭其他/左侧/右侧 |
| | | const handleCloseOtherTabs = () => { |
| | | if (contextMenuTab) handleCloseOthers(contextMenuTab.path); |
| | | }; |
| | | const handleCloseLeftTabs = () => { |
| | | if (contextMenuTab) handleCloseLeft(contextMenuTab.path); |
| | | }; |
| | | const handleCloseRightTabs = () => { |
| | | 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 hasClosableTabs = tabs.some(tab => tab.closable); |
| | | |
| | | // 右键打开时遮罩处理 |
| | | 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]); |
| | | |
| | | return ( |
| | | <Box |
| | | ref={tabsBarRef} |
| | | sx={{ |
| | | width: '100%', |
| | | backgroundColor: '#fff', |
| | |
| | | key={tab.path} |
| | | label={ |
| | | <Box |
| | | onContextMenu={(e) => handleContextMenu(e, tab)} |
| | | sx={{ |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | gap: 0.5, |
| | | width: '100%', |
| | | }} |
| | | > |
| | | {tab.path === '/dashboard' && ( |
| | |
| | | </Tooltip> |
| | | </Box> |
| | | )} |
| | | {/* 右键菜单 */} |
| | | <Menu |
| | | open={contextMenu !== null} |
| | | onClose={handleCloseContextMenu} |
| | | anchorReference="anchorPosition" |
| | | anchorPosition={ |
| | | contextMenu !== null |
| | | ? { top: contextMenu.mouseY, left: contextMenu.mouseX } |
| | | : undefined |
| | | } |
| | | disableScrollLock |
| | | ModalProps={{ |
| | | disablePortal: true, |
| | | BackdropProps: { sx: { pointerEvents: 'none' } }, // 不拦右键,便于再右键另一个标签 |
| | | }} |
| | | PaperProps={{ |
| | | ref: menuPaperRef, |
| | | sx: { |
| | | minWidth: 120, |
| | | borderRadius: '8px', |
| | | boxShadow: '0 4px 20px rgba(0,0,0,0.08)', |
| | | mt: 0.5, |
| | | py: 0.5, |
| | | }, |
| | | }} |
| | | 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' }}> |
| | | {t('ra.action.closeLeft', '关闭左侧标签')} |
| | | </MenuItem> |
| | | )} |
| | | {contextMenuTab && canCloseRightForTab(contextMenuTab.path) && ( |
| | | <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' }}> |
| | | {t('ra.action.closeOthers', '关闭其他标签')} |
| | | </MenuItem> |
| | | )} |
| | | </Menu> |
| | | </Box> |
| | | ); |
| | | }; |