1
13 小时以前 b8a67d701995d8b578e7c87617a5b8cd9b13a9db
rsf-admin/src/layout/TabsBar.jsx
@@ -1,7 +1,7 @@
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';
@@ -86,6 +86,36 @@
    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(() => {
@@ -240,6 +270,126 @@
        }
    };
    // 关闭其他标签
    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)
@@ -249,8 +399,25 @@
    // 判断是否有可关闭的标签
    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',
@@ -282,10 +449,12 @@
                        key={tab.path}
                        label={
                            <Box
                                onContextMenu={(e) => handleContextMenu(e, tab)}
                                sx={{
                                    display: 'flex',
                                    alignItems: 'center',
                                    gap: 0.5,
                                    width: '100%',
                                }}
                            >
                                {tab.path === '/dashboard' && (
@@ -362,6 +531,54 @@
                    </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>
    );
};