All checks were successful
Build and Push Docker Image / build (push) Successful in 4m14s
438 lines
12 KiB
TypeScript
438 lines
12 KiB
TypeScript
import { useMyState } from '@/common';
|
||
import { CloseOutlined, ReloadOutlined } from '@ant-design/icons';
|
||
import { PageContainer, PageContainerProps } from '@ant-design/pro-components';
|
||
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);
|
||
}
|
||
} else {
|
||
// 如果标签页已存在,更新其信息
|
||
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();
|
||
}
|
||
|
||
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();
|
||
|
||
export function MyPageContainer({
|
||
title,
|
||
children,
|
||
enableTabs = true,
|
||
tabKey,
|
||
tabLabel,
|
||
onTabChange,
|
||
...rest
|
||
}: MyPageContainerProps) {
|
||
const location = useLocation();
|
||
const { snap } = useMyState();
|
||
const [tabs, setTabs] = useState<TabItem[]>([]);
|
||
const [activeKey, setActiveKey] = useState<string>('');
|
||
const contextMenuRef = useRef<{
|
||
x: number;
|
||
y: number;
|
||
targetKey: string;
|
||
} | null>(null);
|
||
|
||
// 用户下拉菜单配置
|
||
const userMenuItems: MenuProps['items'] = [
|
||
{
|
||
key: 'profile',
|
||
label: '个人资料',
|
||
icon: <span>👤</span>,
|
||
},
|
||
{
|
||
key: 'password',
|
||
label: '修改密码',
|
||
icon: <span>🔒</span>,
|
||
},
|
||
{
|
||
type: 'divider',
|
||
},
|
||
{
|
||
key: 'logout',
|
||
label: '退出登录',
|
||
icon: <span>🚪</span>,
|
||
danger: true,
|
||
onClick: () => {
|
||
localStorage.removeItem('token');
|
||
window.location.href = '/login';
|
||
},
|
||
},
|
||
];
|
||
|
||
// 订阅标签页状态变化
|
||
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>
|
||
// );
|
||
// }
|
||
|
||
return (
|
||
<PageContainer
|
||
fixedHeader
|
||
header={{
|
||
title: (
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
width: '100%',
|
||
}}
|
||
>
|
||
{/* 左侧:Tabs标签页 */}
|
||
<div style={{ flex: 1 }}>
|
||
<Tabs
|
||
type="editable-card"
|
||
activeKey={activeKey}
|
||
onChange={handleTabChange}
|
||
onEdit={handleTabEdit}
|
||
hideAdd
|
||
size="middle"
|
||
renderTabBar={renderTabBar}
|
||
items={tabs.map((tab) => ({
|
||
key: tab.key,
|
||
label: tab.label,
|
||
closable: tab.closable,
|
||
}))}
|
||
style={{
|
||
marginTop: 6,
|
||
marginBottom: 0,
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
),
|
||
style: { backgroundColor: '#FFF' },
|
||
}}
|
||
token={{
|
||
paddingBlockPageContainerContent: 0,
|
||
paddingInlinePageContainerContent: 0,
|
||
}}
|
||
{...rest}
|
||
>
|
||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||
{children}
|
||
</Space>
|
||
</PageContainer>
|
||
);
|
||
}
|
||
|
||
// 导出标签页管理器,供其他组件使用
|
||
export { tabsManager };
|