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 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);
|
|
// 处理待执行的导航
|
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 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);
|
|
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
|
sx={{
|
display: 'flex',
|
alignItems: 'center',
|
gap: 0.5,
|
}}
|
>
|
{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>
|
)}
|
</Box>
|
);
|
};
|
|
export default TabsBar;
|