pay-admin/src/common/components/layout/MyPageContainer.tsx

399 lines
11 KiB
TypeScript
Raw Normal View History

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) {
// 如果是新标签页,插入到当前激活标签页的右边
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-07-25 16:42:54 +08:00
this.tabs[existingIndex] = { ...this.tabs[existingIndex], ...tab };
}
this.activeKey = tab.key;
this.notify();
}
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={{
2025-07-25 16:42:54 +08:00
title: (
<Tabs
type="editable-card"
activeKey={activeKey}
onChange={handleTabChange}
onEdit={handleTabEdit}
hideAdd
size="middle"
// 标签size
2025-07-25 16:42:54 +08:00
renderTabBar={renderTabBar}
items={tabs.map((tab) => ({
key: tab.key,
label: tab.label,
closable: tab.closable,
}))}
style={{
marginTop: 6,
2025-07-25 16:42:54 +08:00
marginBottom: 0,
}}
className="tabs-header-only"
/>
),
2025-06-27 16:42:11 +08:00
style: { backgroundColor: '#FFF' },
}}
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 };