From 01ea11a8e7a217b51208ae31ca7bd4c7cd13f7fa Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期四, 08 一月 2026 12:37:49 +0800
Subject: [PATCH] #tabsBar
---
rsf-admin/src/layout/TabsBar.jsx | 364 +++++++++++++++++++++++++++++++++++++++++++++
rsf-admin/src/layout/index.jsx | 59 ++++++-
2 files changed, 413 insertions(+), 10 deletions(-)
diff --git a/rsf-admin/src/layout/TabsBar.jsx b/rsf-admin/src/layout/TabsBar.jsx
new file mode 100644
index 0000000..bd77113
--- /dev/null
+++ b/rsf-admin/src/layout/TabsBar.jsx
@@ -0,0 +1,364 @@
+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'];
+
+// 浠巐ocalStorage鑾峰彇宸蹭繚瀛樼殑鏍囩椤�
+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);
+ }
+};
+
+// 瑙勮寖鍖栬矾寰� - 澶勭悊甯D鐨勮鎯�/缂栬緫椤甸潰
+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);
+ };
+
+
+
+ // 鍏抽棴鎵�鏈夋爣绛鹃〉锛堥櫎浜哾ashboard锛�
+ 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="鍏抽棴">
+ <IconButton
+ size="small"
+ onClick={(e) => handleCloseTab(e, tab.path)}
+ sx={{
+ p: 0.25,
+ ml: 0.5,
+ '&:hover': {
+ backgroundColor: 'rgba(0, 0, 0, 0.1)',
+ },
+ }}
+ >
+ <CloseIcon sx={{ fontSize: 14 }} />
+ </IconButton>
+ </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;
diff --git a/rsf-admin/src/layout/index.jsx b/rsf-admin/src/layout/index.jsx
index 9d105ce..293ae54 100644
--- a/rsf-admin/src/layout/index.jsx
+++ b/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';
+
+const LayoutContent = ({ children }) => {
+ const [sidebarIsOpen] = useSidebarState();
+ const sidebarWidth = sidebarIsOpen ? 200 : 50;
+
+ return (
+ <RALayout
+ appBar={AppBar}
+ menu={MyMenu}
+ 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,
+ 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 }) => (
- <RALayout
- appBar={AppBar}
- menu={MyMenu}
- sx={{ '& .RaLayout-content': { position: 'absolute', left: '200px', overflowY: 'auto', width: 'calc(100% - 200px)', height: 'calc(100% - 50px)' } }}
- >
- {children}
- <CheckForApplicationUpdate />
- </RALayout>
+ <LayoutContent>{children}</LayoutContent>
);
--
Gitblit v1.9.1