2025-07-25 16:42:54 +08:00
|
|
|
|
import { CloseOutlined, ReloadOutlined } from '@ant-design/icons';
|
2025-06-27 16:42:11 +08:00
|
|
|
|
import { PageContainer, PageContainerProps } from '@ant-design/pro-components';
|
2025-07-25 16:42:54 +08:00
|
|
|
|
import { history, useLocation } from '@umijs/max';
|
|
|
|
|
|
import type { MenuProps } from 'antd';
|
|
|
|
|
|
import { Dropdown, message, Space, Tabs } from 'antd';
|
|
|
|
|
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
|
|
import './MyPageContainer.scss';
|
|
|
|
|
|
|
|
|
|
|
|
export interface TabItem {
|
|
|
|
|
|
key: string;
|
|
|
|
|
|
label: string;
|
|
|
|
|
|
path: string;
|
|
|
|
|
|
closable?: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface MyPageContainerProps extends PageContainerProps {
|
|
|
|
|
|
enableTabs?: boolean;
|
|
|
|
|
|
tabKey?: string;
|
|
|
|
|
|
tabLabel?: string;
|
|
|
|
|
|
onTabChange?: (activeKey: string) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 全局标签页状态管理
|
|
|
|
|
|
class TabsManager {
|
|
|
|
|
|
private tabs: TabItem[] = [];
|
|
|
|
|
|
private activeKey: string = '';
|
|
|
|
|
|
private listeners: Set<() => void> = new Set();
|
|
|
|
|
|
|
|
|
|
|
|
subscribe(listener: () => void) {
|
|
|
|
|
|
this.listeners.add(listener);
|
|
|
|
|
|
return () => this.listeners.delete(listener);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private notify() {
|
|
|
|
|
|
this.listeners.forEach((listener) => listener());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getTabs() {
|
|
|
|
|
|
return this.tabs;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getActiveKey() {
|
|
|
|
|
|
return this.activeKey;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
addTab(tab: TabItem) {
|
|
|
|
|
|
const existingIndex = this.tabs.findIndex((t) => t.key === tab.key);
|
|
|
|
|
|
if (existingIndex === -1) {
|
2025-08-08 18:35:02 +08:00
|
|
|
|
// 如果是新标签页,插入到当前激活标签页的右边
|
|
|
|
|
|
const currentActiveIndex = this.tabs.findIndex(
|
|
|
|
|
|
(t) => t.key === this.activeKey,
|
|
|
|
|
|
);
|
|
|
|
|
|
if (currentActiveIndex !== -1) {
|
|
|
|
|
|
// 在当前激活标签页的右边插入新标签页
|
|
|
|
|
|
this.tabs.splice(currentActiveIndex + 1, 0, tab);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果没有当前激活标签页,添加到末尾
|
|
|
|
|
|
this.tabs.push(tab);
|
|
|
|
|
|
}
|
2025-07-25 16:42:54 +08:00
|
|
|
|
} else {
|
2025-08-08 18:35:02 +08:00
|
|
|
|
// 如果标签页已存在,更新其信息
|
2025-07-25 16:42:54 +08:00
|
|
|
|
this.tabs[existingIndex] = { ...this.tabs[existingIndex], ...tab };
|
|
|
|
|
|
}
|
|
|
|
|
|
this.activeKey = tab.key;
|
|
|
|
|
|
this.notify();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-08 18:35:02 +08:00
|
|
|
|
addTabNext(tab: TabItem) {
|
|
|
|
|
|
const existingIndex = this.tabs.findIndex((t) => t.key === tab.key);
|
|
|
|
|
|
if (existingIndex === -1) {
|
|
|
|
|
|
// 强制在当前激活标签页的右边插入新标签页
|
|
|
|
|
|
const currentActiveIndex = this.tabs.findIndex(
|
|
|
|
|
|
(t) => t.key === this.activeKey,
|
|
|
|
|
|
);
|
|
|
|
|
|
if (currentActiveIndex !== -1) {
|
|
|
|
|
|
this.tabs.splice(currentActiveIndex + 1, 0, tab);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.tabs.push(tab);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果标签页已存在,移动到当前激活标签页的右边
|
|
|
|
|
|
const existingTab = this.tabs[existingIndex];
|
|
|
|
|
|
this.tabs.splice(existingIndex, 1); // 先移除原位置的标签页
|
|
|
|
|
|
const currentActiveIndex = this.tabs.findIndex(
|
|
|
|
|
|
(t) => t.key === this.activeKey,
|
|
|
|
|
|
);
|
|
|
|
|
|
if (currentActiveIndex !== -1) {
|
|
|
|
|
|
this.tabs.splice(currentActiveIndex + 1, 0, { ...existingTab, ...tab });
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.tabs.push({ ...existingTab, ...tab });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
this.activeKey = tab.key;
|
|
|
|
|
|
this.notify();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-25 16:42:54 +08:00
|
|
|
|
removeTab(targetKey: string) {
|
|
|
|
|
|
const targetIndex = this.tabs.findIndex((tab) => tab.key === targetKey);
|
|
|
|
|
|
if (targetIndex === -1) return;
|
|
|
|
|
|
|
|
|
|
|
|
const newTabs = this.tabs.filter((tab) => tab.key !== targetKey);
|
|
|
|
|
|
|
|
|
|
|
|
if (newTabs.length === 0) {
|
|
|
|
|
|
this.tabs = [];
|
|
|
|
|
|
this.activeKey = '';
|
|
|
|
|
|
history.push('/');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.tabs = newTabs;
|
|
|
|
|
|
if (this.activeKey === targetKey) {
|
|
|
|
|
|
// 如果关闭的是当前激活的标签,激活相邻的标签
|
|
|
|
|
|
const newActiveKey =
|
|
|
|
|
|
targetIndex > 0 ? newTabs[targetIndex - 1].key : newTabs[0].key;
|
|
|
|
|
|
this.activeKey = newActiveKey;
|
|
|
|
|
|
const targetTab = newTabs.find((tab) => tab.key === newActiveKey);
|
|
|
|
|
|
if (targetTab) {
|
|
|
|
|
|
history.push(targetTab.path);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
this.notify();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setActiveKey(key: string) {
|
|
|
|
|
|
this.activeKey = key;
|
|
|
|
|
|
const targetTab = this.tabs.find((tab) => tab.key === key);
|
|
|
|
|
|
if (targetTab) {
|
|
|
|
|
|
history.push(targetTab.path);
|
|
|
|
|
|
}
|
|
|
|
|
|
this.notify();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
closeOtherTabs(currentKey: string) {
|
|
|
|
|
|
const currentTab = this.tabs.find((tab) => tab.key === currentKey);
|
|
|
|
|
|
if (currentTab) {
|
|
|
|
|
|
this.tabs = [currentTab];
|
|
|
|
|
|
this.activeKey = currentKey;
|
|
|
|
|
|
this.notify();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
closeLeftTabs(currentKey: string) {
|
|
|
|
|
|
const currentIndex = this.tabs.findIndex((tab) => tab.key === currentKey);
|
|
|
|
|
|
if (currentIndex > 0) {
|
|
|
|
|
|
this.tabs = this.tabs.slice(currentIndex);
|
|
|
|
|
|
this.notify();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
closeRightTabs(currentKey: string) {
|
|
|
|
|
|
const currentIndex = this.tabs.findIndex((tab) => tab.key === currentKey);
|
|
|
|
|
|
if (currentIndex !== -1) {
|
|
|
|
|
|
this.tabs = this.tabs.slice(0, currentIndex + 1);
|
|
|
|
|
|
this.notify();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
refreshTab(key: string) {
|
|
|
|
|
|
// 通过路由跳转的方式刷新当前标签页,避免整个页面刷新导致标签页状态丢失
|
|
|
|
|
|
const targetTab = this.tabs.find((tab) => tab.key === key);
|
|
|
|
|
|
if (targetTab) {
|
|
|
|
|
|
const originalPath = targetTab.path;
|
|
|
|
|
|
// 移除可能存在的刷新参数,确保获取干净的原始路径
|
|
|
|
|
|
const cleanPath = originalPath
|
|
|
|
|
|
.replace(/[?&]_refresh=\d+/g, '')
|
|
|
|
|
|
.replace(/\?$/, '');
|
|
|
|
|
|
|
|
|
|
|
|
// 添加时间戳参数强制刷新
|
|
|
|
|
|
const refreshPath = cleanPath.includes('?')
|
|
|
|
|
|
? `${cleanPath}&_refresh=${Date.now()}`
|
|
|
|
|
|
: `${cleanPath}?_refresh=${Date.now()}`;
|
|
|
|
|
|
|
|
|
|
|
|
// 先更新为带刷新参数的路径并跳转
|
|
|
|
|
|
targetTab.path = refreshPath;
|
|
|
|
|
|
this.setActiveKey(key);
|
|
|
|
|
|
|
|
|
|
|
|
// 延迟恢复原始路径,确保路由跳转完成
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
// 恢复为干净的原始路径
|
|
|
|
|
|
targetTab.path = cleanPath;
|
|
|
|
|
|
// 再次跳转到干净路径,移除URL中的刷新参数
|
|
|
|
|
|
history.push(cleanPath);
|
|
|
|
|
|
this.notify();
|
|
|
|
|
|
}, 300);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 全局标签页管理器实例
|
|
|
|
|
|
const tabsManager = new TabsManager();
|
2025-06-27 16:42:11 +08:00
|
|
|
|
|
|
|
|
|
|
export function MyPageContainer({
|
|
|
|
|
|
title,
|
|
|
|
|
|
children,
|
2025-07-25 16:42:54 +08:00
|
|
|
|
enableTabs = true,
|
|
|
|
|
|
tabKey,
|
|
|
|
|
|
tabLabel,
|
|
|
|
|
|
onTabChange,
|
2025-06-27 16:42:11 +08:00
|
|
|
|
...rest
|
2025-07-25 16:42:54 +08:00
|
|
|
|
}: MyPageContainerProps) {
|
|
|
|
|
|
const location = useLocation();
|
|
|
|
|
|
const [tabs, setTabs] = useState<TabItem[]>([]);
|
|
|
|
|
|
const [activeKey, setActiveKey] = useState<string>('');
|
|
|
|
|
|
const contextMenuRef = useRef<{
|
|
|
|
|
|
x: number;
|
|
|
|
|
|
y: number;
|
|
|
|
|
|
targetKey: string;
|
|
|
|
|
|
} | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
// 订阅标签页状态变化
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const unsubscribe = tabsManager.subscribe(() => {
|
|
|
|
|
|
setTabs([...tabsManager.getTabs()]);
|
|
|
|
|
|
setActiveKey(tabsManager.getActiveKey());
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化状态
|
|
|
|
|
|
setTabs([...tabsManager.getTabs()]);
|
|
|
|
|
|
setActiveKey(tabsManager.getActiveKey());
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
unsubscribe();
|
|
|
|
|
|
};
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 当组件挂载时,添加当前页面到标签页
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (enableTabs && tabKey && tabLabel) {
|
|
|
|
|
|
tabsManager.addTab({
|
|
|
|
|
|
key: tabKey,
|
|
|
|
|
|
label: tabLabel,
|
|
|
|
|
|
path: location.pathname + location.search,
|
|
|
|
|
|
closable: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [enableTabs, tabKey, tabLabel, location.pathname, location.search]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleTabChange = useCallback(
|
|
|
|
|
|
(key: string) => {
|
|
|
|
|
|
tabsManager.setActiveKey(key);
|
|
|
|
|
|
onTabChange?.(key);
|
|
|
|
|
|
},
|
|
|
|
|
|
[onTabChange],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const handleTabEdit = useCallback(
|
|
|
|
|
|
(
|
|
|
|
|
|
targetKey: string | React.MouseEvent | React.KeyboardEvent,
|
|
|
|
|
|
action: 'add' | 'remove',
|
|
|
|
|
|
) => {
|
|
|
|
|
|
if (action === 'remove' && typeof targetKey === 'string') {
|
|
|
|
|
|
tabsManager.removeTab(targetKey);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 右键菜单配置
|
|
|
|
|
|
const getContextMenuItems = useCallback(
|
|
|
|
|
|
(targetKey: string): MenuProps['items'] => {
|
|
|
|
|
|
const currentIndex = tabs.findIndex((tab) => tab.key === targetKey);
|
|
|
|
|
|
const hasLeftTabs = currentIndex > 0;
|
|
|
|
|
|
const hasRightTabs = currentIndex < tabs.length - 1;
|
|
|
|
|
|
const hasOtherTabs = tabs.length > 1;
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'refresh',
|
|
|
|
|
|
label: '刷新',
|
|
|
|
|
|
icon: <ReloadOutlined />,
|
|
|
|
|
|
onClick: () => tabsManager.refreshTab(targetKey),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'close',
|
|
|
|
|
|
label: '关闭',
|
|
|
|
|
|
icon: <CloseOutlined />,
|
|
|
|
|
|
onClick: () => tabsManager.removeTab(targetKey),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'divider',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'closeOthers',
|
|
|
|
|
|
label: '关闭其他',
|
|
|
|
|
|
disabled: !hasOtherTabs,
|
|
|
|
|
|
onClick: () => {
|
|
|
|
|
|
tabsManager.closeOtherTabs(targetKey);
|
|
|
|
|
|
message.success('已关闭其他标签页');
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'closeLeft',
|
|
|
|
|
|
label: '关闭左侧',
|
|
|
|
|
|
disabled: !hasLeftTabs,
|
|
|
|
|
|
onClick: () => {
|
|
|
|
|
|
tabsManager.closeLeftTabs(targetKey);
|
|
|
|
|
|
message.success('已关闭左侧标签页');
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'closeRight',
|
|
|
|
|
|
label: '关闭右侧',
|
|
|
|
|
|
disabled: !hasRightTabs,
|
|
|
|
|
|
onClick: () => {
|
|
|
|
|
|
tabsManager.closeRightTabs(targetKey);
|
|
|
|
|
|
message.success('已关闭右侧标签页');
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
},
|
|
|
|
|
|
[tabs],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 自定义标签页渲染
|
|
|
|
|
|
const renderTabBar: React.ComponentProps<typeof Tabs>['renderTabBar'] = (
|
|
|
|
|
|
props,
|
|
|
|
|
|
DefaultTabBar,
|
|
|
|
|
|
) => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<DefaultTabBar {...props}>
|
|
|
|
|
|
{(node) => {
|
|
|
|
|
|
const tabKey = node.key as string;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Dropdown
|
|
|
|
|
|
menu={{ items: getContextMenuItems(tabKey) }}
|
|
|
|
|
|
trigger={['contextMenu']}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div>{node}</div>
|
|
|
|
|
|
</Dropdown>
|
|
|
|
|
|
);
|
|
|
|
|
|
}}
|
|
|
|
|
|
</DefaultTabBar>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (!enableTabs || tabs.length === 0) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<PageContainer
|
|
|
|
|
|
fixedHeader
|
|
|
|
|
|
header={{
|
|
|
|
|
|
title: title,
|
|
|
|
|
|
style: { backgroundColor: '#FFF' },
|
|
|
|
|
|
}}
|
|
|
|
|
|
token={{
|
|
|
|
|
|
paddingBlockPageContainerContent: 0,
|
|
|
|
|
|
paddingInlinePageContainerContent: 0,
|
|
|
|
|
|
}}
|
|
|
|
|
|
{...rest}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
|
|
|
|
{children}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</PageContainer>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-27 16:42:11 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<PageContainer
|
|
|
|
|
|
fixedHeader
|
|
|
|
|
|
header={{
|
2026-03-30 16:53:18 +08:00
|
|
|
|
title: false,
|
|
|
|
|
|
style: { backgroundColor: '#F00' },
|
2025-06-27 16:42:11 +08:00
|
|
|
|
}}
|
2026-03-30 16:53:18 +08:00
|
|
|
|
// header={{
|
|
|
|
|
|
// title: (
|
|
|
|
|
|
// <Tabs
|
|
|
|
|
|
// type="editable-card"
|
|
|
|
|
|
// activeKey={activeKey}
|
|
|
|
|
|
// onChange={handleTabChange}
|
|
|
|
|
|
// onEdit={handleTabEdit}
|
|
|
|
|
|
// hideAdd
|
|
|
|
|
|
// size="middle"
|
|
|
|
|
|
// // 标签size
|
|
|
|
|
|
// renderTabBar={renderTabBar}
|
|
|
|
|
|
// items={tabs.map((tab) => ({
|
|
|
|
|
|
// key: tab.key,
|
|
|
|
|
|
// label: tab.label,
|
|
|
|
|
|
// closable: tab.closable,
|
|
|
|
|
|
// }))}
|
|
|
|
|
|
// style={{
|
|
|
|
|
|
// marginTop: 6,
|
|
|
|
|
|
// marginBottom: 0,
|
|
|
|
|
|
// }}
|
|
|
|
|
|
// className="tabs-header-only"
|
|
|
|
|
|
// />
|
|
|
|
|
|
// ),
|
|
|
|
|
|
// style: { backgroundColor: '#F00', padding: '64px 15px 0 15px' },
|
|
|
|
|
|
// }}
|
2025-06-27 16:42:11 +08:00
|
|
|
|
token={{
|
|
|
|
|
|
paddingBlockPageContainerContent: 0,
|
|
|
|
|
|
paddingInlinePageContainerContent: 0,
|
|
|
|
|
|
}}
|
|
|
|
|
|
{...rest}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
|
|
|
|
{children}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</PageContainer>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-07-25 16:42:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 导出标签页管理器,供其他组件使用
|
|
|
|
|
|
export { tabsManager };
|