chen.lin
昨天 faa124062a9f3c1ec459caaaf16059243510de5d
标签
1个文件已添加
2个文件已修改
634 ■■■■■ 已修改文件
rsf-admin/src/i18n/zh.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/layout/TabsBar.jsx 578 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/layout/index.jsx 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/zh.js
@@ -1386,6 +1386,15 @@
        modiftySite: '修改库口',
        selectWave: '波次规则',
    },
    ra: {
        action: {
            close: '关闭当前标签',
            closeLeft: '关闭左侧标签',
            closeRight: '关闭右侧标签',
            closeOthers: '关闭其他标签',
            closeAll: '关闭所有标签',
        }
    },
    request: {
        error: {
            stock: "库存不足,无法提交!!",
rsf-admin/src/layout/TabsBar.jsx
New file
@@ -0,0 +1,578 @@
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, 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';
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];
        }
        return [...FIXED_TABS];
    } catch {
        return [...FIXED_TABS];
    }
};
// 保存标签页到localStorage
const saveTabs = (tabs) => {
    try {
        localStorage.setItem('openTabs', JSON.stringify(tabs));
    } catch (e) {
        console.error('Failed to save tabs to localStorage:', e);
    }
};
// 规范化路径 - 处理带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
    ];
    for (const pattern of patterns) {
        const match = path.match(pattern);
        if (match) {
            return match[1];
        }
    }
    return path;
};
// 判断两个路径是否属于同一资源
const isSameResource = (path1, path2) => {
    return normalizePath(path1) === normalizePath(path2);
};
const TabsBar = () => {
    const location = useLocation();
    const navigate = useNavigate();
    const translate = useTranslate();
    const { permissions } = usePermissions();
    const [tabs, setTabs] = useState(getSavedTabs);
    const pendingNavigationRef = useRef(null);
    const [contextMenu, setContextMenu] = useState(null);
    const [contextMenuTab, setContextMenuTab] = useState(null);
    // 翻译辅助函数,如果翻译不存在则返回默认值
    const t = useCallback((key, defaultValue) => {
        const translated = translate(key);
        return translated === key ? defaultValue : translated;
    }, [translate]);
    // 处理待执行的导航
    useEffect(() => {
        if (pendingNavigationRef.current) {
            navigate(pendingNavigationRef.current);
            pendingNavigationRef.current = null;
        }
    });
    // 根据路径查找菜单名称
    const findMenuName = useCallback((path, menus) => {
        if (!menus || !path) return null;
        const cleanPath = path.replace(/^\//, '');
        const normalizedCleanPath = normalizePath('/' + cleanPath).replace(/^\//, '');
        const searchMenu = (items) => {
            for (const item of items) {
                if (item.component === cleanPath || item.component === normalizedCleanPath) {
                    return item.name;
                }
                if (item.children) {
                    const found = searchMenu(item.children);
                    if (found) return found;
                }
            }
            return null;
        };
        return searchMenu(menus);
    }, []);
    // 获取标签页显示名称
    const getTabLabel = useCallback((tab) => {
        // 固定标签页直接返回翻译后的名称
        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);
            }
        }
        // 默认返回路径
        return tab.path.replace(/^\//, '').split('/')[0] || tab.path;
    }, [translate, permissions, findMenuName]);
    // 监听路由变化,添加新标签页
    useEffect(() => {
        const currentPath = location.pathname;
        // 忽略特定路径
        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;
                }
                // 更新现有标签页的路径(保持标签位置)
                const updatedTabs = [...prevTabs];
                updatedTabs[existingTabIndex] = {
                    ...updatedTabs[existingTabIndex],
                    path: currentPath
                };
                saveTabs(updatedTabs);
                return updatedTabs;
            }
            // 查找菜单名称
            let menuName = null;
            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,
                closable: SPECIAL_PAGES[normalizedPath]?.closable !== false
            };
            const newTabs = [...prevTabs, newTab];
            saveTabs(newTabs);
            return newTabs;
        });
    }, [location.pathname, permissions, findMenuName]);
    // 切换标签页
    const handleTabChange = (event, newValue) => {
        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;
            } else {
                pendingNavigationRef.current = '/dashboard';
            }
        }
        saveTabs(newTabs);
        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不是指定标签页)
        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始终在第一位(如果存在)
    //     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();
        setContextMenu(
            contextMenu === null
                ? {
                      mouseX: event.clientX + 2,
                      mouseY: event.clientY - 6,
                  }
                : null
        );
        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 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 canCloseLeftForTab = (tabPath) => {
    //     const tabIndex = tabs.findIndex(tab => tab.path === tabPath);
    //     if (tabIndex <= 0) {
    //         return false; // 没有左侧标签或这是第一个标签
    //     }
    //     // 检查左侧是否有可关闭的标签(排除dashboard)
    //     return tabs.slice(0, tabIndex).some(tab => tab.closable);
    // };
    // 判断指定标签页是否可以关闭右侧标签
    // const canCloseRightForTab = (tabPath) => {
    //     const tabIndex = tabs.findIndex(tab => tab.path === tabPath);
    //     if (tabIndex < 0 || tabIndex >= tabs.length - 1) {
    //         return false; // 没有右侧标签或这是最后一个标签
    //     }
    //     // 检查右侧是否有可关闭的标签
    //     return tabs.slice(tabIndex + 1).some(tab => tab.closable);
    // };
    return (
        <Box
            sx={{
                width: '100%',
                backgroundColor: '#fff',
                borderBottom: '1px solid #e0e0e0',
                boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)',
                display: 'flex',
                alignItems: 'center',
            }}
        >
            <Tabs
                value={activeIndex}
                onChange={handleTabChange}
                variant="scrollable"
                scrollButtons="auto"
                sx={{
                    flex: 1,
                    minHeight: 36,
                    '& .MuiTabs-indicator': {
                        backgroundColor: '#1976d2',
                        height: 2,
                    },
                    '& .MuiTabs-scrollButtons': {
                        width: 28,
                    },
                }}
            >
                {tabs.map((tab, index) => (
                    <Tab
                        key={tab.path}
                        label={
                            <Box
                                onContextMenu={(e) => handleContextMenu(e, tab)}
                                sx={{
                                    display: 'flex',
                                    alignItems: 'center',
                                    gap: 0.5,
                                    width: '100%',
                                }}
                            >
                                {tab.path === '/dashboard' && (
                                    <HomeIcon sx={{ fontSize: 16 }} />
                                )}
                                {normalizePath(tab.path) === '/settings' && (
                                    <SettingsIcon sx={{ fontSize: 16 }} />
                                )}
                                <span>{getTabLabel(tab)}</span>
                                {tab.closable && (
                                    <Tooltip title="关闭">
                                        <Box
                                            component="span"
                                            onClick={(e) => handleCloseTab(e, tab.path)}
                                            sx={{
                                                display: 'inline-flex',
                                                alignItems: 'center',
                                                justifyContent: 'center',
                                                p: 0.25,
                                                ml: 0.5,
                                                borderRadius: '50%',
                                                cursor: 'pointer',
                                                '&:hover': {
                                                    backgroundColor: 'rgba(0, 0, 0, 0.1)',
                                                },
                                            }}
                                        >
                                            <CloseIcon sx={{ fontSize: 14 }} />
                                        </Box>
                                    </Tooltip>
                                )}
                            </Box>
                        }
                        sx={{
                            minHeight: 36,
                            py: 0.5,
                            px: 1.5,
                            textTransform: 'none',
                            fontSize: '0.8125rem',
                            fontWeight: activeIndex === index ? 600 : 400,
                            color: activeIndex === index ? '#1976d2' : 'text.secondary',
                            backgroundColor: activeIndex === index ? 'rgba(25, 118, 210, 0.08)' : 'transparent',
                            borderRadius: '4px 4px 0 0',
                            minWidth: 'auto',
                            '&:hover': {
                                backgroundColor: 'rgba(25, 118, 210, 0.12)',
                            },
                            '&.Mui-selected': {
                                color: '#1976d2',
                            },
                        }}
                    />
                ))}
            </Tabs>
            {/* 清除所有标签按钮 */}
            {hasClosableTabs && (
                <Box sx={{ display: 'flex', alignItems: 'center', pr: 1 }}>
                    <Divider orientation="vertical" flexItem sx={{ mx: 0.5, height: 20, alignSelf: 'center' }} />
                    <Tooltip title="关闭所有标签">
                        <IconButton
                            size="small"
                            onClick={handleCloseAll}
                            sx={{
                                p: 0.5,
                                color: 'text.secondary',
                                '&:hover': {
                                    backgroundColor: 'rgba(0, 0, 0, 0.08)',
                                    color: '#d32f2f',
                                },
                            }}
                        >
                            <ClearAllIcon sx={{ fontSize: 18 }} />
                        </IconButton>
                    </Tooltip>
                </Box>
            )}
            {/* 右键菜单 */}
            <Menu
                open={contextMenu !== null}
                onClose={handleCloseContextMenu}
                anchorReference="anchorPosition"
                anchorPosition={
                    contextMenu !== null
                        ? { top: contextMenu.mouseY, left: contextMenu.mouseX }
                        : undefined
                }
            >
                {contextMenuTab && contextMenuTab.closable && (
                    <MenuItem onClick={handleCloseCurrentTab}>
                        {t('ra.action.close', '关闭当前标签')}
                    </MenuItem>
                )}
                {/* {contextMenuTab && canCloseLeftForTab(contextMenuTab.path) && (
                    <MenuItem onClick={handleCloseLeftTabs}>
                        {t('ra.action.closeLeft', '关闭左侧标签')}
                    </MenuItem>
                )}
                {contextMenuTab && canCloseRightForTab(contextMenuTab.path) && (
                    <MenuItem onClick={handleCloseRightTabs}>
                        {t('ra.action.closeRight', '关闭右侧标签')}
                    </MenuItem>
                )} */}
                {contextMenuTab && canCloseOthersForTab(contextMenuTab.path) && (
                    <MenuItem onClick={handleCloseOtherTabs}>
                        {t('ra.action.closeOthers', '关闭其他标签')}
                    </MenuItem>
                )}
            </Menu>
        </Box>
    );
};
export default TabsBar;
rsf-admin/src/layout/index.jsx
@@ -1,14 +1,53 @@
import { Layout as RALayout, CheckForApplicationUpdate } from "react-admin";
import { Layout as RALayout, CheckForApplicationUpdate, useSidebarState } from "react-admin";
import AppBar from './AppBar';
import { MyMenu } from './MyMenu'
import { MyMenu } from './MyMenu';
import TabsBar from './TabsBar';
import { Box } from '@mui/material';
export const Layout = ({ children }) => (
const LayoutContent = ({ children }) => {
  const [sidebarIsOpen] = useSidebarState();
  const sidebarWidth = sidebarIsOpen ? 200 : 50;
  return (
  <RALayout
    appBar={AppBar}
    menu={MyMenu}
    sx={{ '& .RaLayout-content': { position: 'absolute', left: '200px', overflowY: 'auto', width: 'calc(100% - 200px)', height: 'calc(100% - 50px)' } }}
      sx={{
        '& .RaLayout-content': {
          position: 'absolute',
          left: `${sidebarWidth}px`,
          overflowY: 'auto',
          width: `calc(100% - ${sidebarWidth}px)`,
          height: 'calc(100% - 86px)', // 减去TabsBar的高度 (50px AppBar + 36px TabsBar)
          top: '86px',
          transition: (theme) =>
            theme.transitions.create(['left', 'width'], {
              easing: theme.transitions.easing.sharp,
              duration: theme.transitions.duration.leavingScreen,
            }),
        }
      }}
  >
      <Box sx={{
        position: 'fixed',
        top: 48,
        left: sidebarWidth + 5,
        right: 0,
        zIndex: 1100,
        transition: (theme) =>
          theme.transitions.create('left', {
            easing: theme.transitions.easing.sharp,
            duration: theme.transitions.duration.leavingScreen,
          }),
      }}>
        <TabsBar />
      </Box>
    {children}
    <CheckForApplicationUpdate />
  </RALayout>
);
};
export const Layout = ({ children }) => (
  <LayoutContent>{children}</LayoutContent>
);