From b8a67d701995d8b578e7c87617a5b8cd9b13a9db Mon Sep 17 00:00:00 2001
From: 1 <1@123>
Date: 星期六, 07 三月 2026 10:00:02 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/devlop-phyz' into devlop-phyz
---
rsf-admin/src/layout/TabsBar.jsx | 219 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 218 insertions(+), 1 deletions(-)
diff --git a/rsf-admin/src/layout/TabsBar.jsx b/rsf-admin/src/layout/TabsBar.jsx
index c346104..ab4f171 100644
--- a/rsf-admin/src/layout/TabsBar.jsx
+++ b/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>
);
};
--
Gitblit v1.9.1