jettask 0.2.1__py3-none-any.whl → 0.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jettask/constants.py +213 -0
- jettask/core/app.py +525 -205
- jettask/core/cli.py +193 -185
- jettask/core/consumer_manager.py +126 -34
- jettask/core/context.py +3 -0
- jettask/core/enums.py +137 -0
- jettask/core/event_pool.py +501 -168
- jettask/core/message.py +147 -0
- jettask/core/offline_worker_recovery.py +181 -114
- jettask/core/task.py +10 -174
- jettask/core/task_batch.py +153 -0
- jettask/core/unified_manager_base.py +243 -0
- jettask/core/worker_scanner.py +54 -54
- jettask/executors/asyncio.py +184 -64
- jettask/webui/backend/config.py +51 -0
- jettask/webui/backend/data_access.py +2083 -92
- jettask/webui/backend/data_api.py +3294 -0
- jettask/webui/backend/dependencies.py +261 -0
- jettask/webui/backend/init_meta_db.py +158 -0
- jettask/webui/backend/main.py +1358 -69
- jettask/webui/backend/main_unified.py +78 -0
- jettask/webui/backend/main_v2.py +394 -0
- jettask/webui/backend/namespace_api.py +295 -0
- jettask/webui/backend/namespace_api_old.py +294 -0
- jettask/webui/backend/namespace_data_access.py +611 -0
- jettask/webui/backend/queue_backlog_api.py +727 -0
- jettask/webui/backend/queue_stats_v2.py +521 -0
- jettask/webui/backend/redis_monitor_api.py +476 -0
- jettask/webui/backend/unified_api_router.py +1601 -0
- jettask/webui/db_init.py +204 -32
- jettask/webui/frontend/package-lock.json +492 -1
- jettask/webui/frontend/package.json +4 -1
- jettask/webui/frontend/src/App.css +105 -7
- jettask/webui/frontend/src/App.jsx +49 -20
- jettask/webui/frontend/src/components/NamespaceSelector.jsx +166 -0
- jettask/webui/frontend/src/components/QueueBacklogChart.jsx +298 -0
- jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +638 -0
- jettask/webui/frontend/src/components/QueueDetailsTable.css +65 -0
- jettask/webui/frontend/src/components/QueueDetailsTable.jsx +487 -0
- jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +465 -0
- jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +423 -0
- jettask/webui/frontend/src/components/TaskFilter.jsx +425 -0
- jettask/webui/frontend/src/components/TimeRangeSelector.css +21 -0
- jettask/webui/frontend/src/components/TimeRangeSelector.jsx +160 -0
- jettask/webui/frontend/src/components/layout/AppLayout.css +95 -0
- jettask/webui/frontend/src/components/layout/AppLayout.jsx +49 -0
- jettask/webui/frontend/src/components/layout/Header.css +34 -10
- jettask/webui/frontend/src/components/layout/Header.jsx +31 -23
- jettask/webui/frontend/src/components/layout/SideMenu.css +137 -0
- jettask/webui/frontend/src/components/layout/SideMenu.jsx +209 -0
- jettask/webui/frontend/src/components/layout/TabsNav.css +244 -0
- jettask/webui/frontend/src/components/layout/TabsNav.jsx +206 -0
- jettask/webui/frontend/src/components/layout/UserInfo.css +197 -0
- jettask/webui/frontend/src/components/layout/UserInfo.jsx +197 -0
- jettask/webui/frontend/src/contexts/NamespaceContext.jsx +72 -0
- jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +245 -0
- jettask/webui/frontend/src/main.jsx +1 -0
- jettask/webui/frontend/src/pages/Alerts.jsx +684 -0
- jettask/webui/frontend/src/pages/Dashboard.jsx +1330 -0
- jettask/webui/frontend/src/pages/QueueDetail.jsx +1109 -10
- jettask/webui/frontend/src/pages/QueueMonitor.jsx +236 -115
- jettask/webui/frontend/src/pages/Queues.jsx +5 -1
- jettask/webui/frontend/src/pages/ScheduledTasks.jsx +809 -0
- jettask/webui/frontend/src/pages/Settings.jsx +800 -0
- jettask/webui/frontend/src/services/api.js +7 -5
- jettask/webui/frontend/src/utils/suppressWarnings.js +22 -0
- jettask/webui/frontend/src/utils/userPreferences.js +154 -0
- jettask/webui/multi_namespace_consumer.py +543 -0
- jettask/webui/pg_consumer.py +983 -246
- jettask/webui/static/dist/assets/index-7129cfe1.css +1 -0
- jettask/webui/static/dist/assets/index-8d1935cc.js +774 -0
- jettask/webui/static/dist/index.html +2 -2
- jettask/webui/task_center.py +216 -0
- jettask/webui/task_center_client.py +150 -0
- jettask/webui/unified_consumer_manager.py +193 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/METADATA +1 -1
- jettask-0.2.4.dist-info/RECORD +134 -0
- jettask/webui/pg_consumer_slow.py +0 -1099
- jettask/webui/pg_consumer_test.py +0 -678
- jettask/webui/static/dist/assets/index-823408e8.css +0 -1
- jettask/webui/static/dist/assets/index-9968b0b8.js +0 -543
- jettask/webui/test_pg_consumer_recovery.py +0 -547
- jettask/webui/test_recovery_simple.py +0 -492
- jettask/webui/test_self_recovery.py +0 -467
- jettask-0.2.1.dist-info/RECORD +0 -91
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/WHEEL +0 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,197 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import { Dropdown, Menu, Avatar, Badge, Space, Button, Modal, Tooltip } from 'antd';
|
3
|
+
import {
|
4
|
+
UserOutlined,
|
5
|
+
BellOutlined,
|
6
|
+
SettingOutlined,
|
7
|
+
LogoutOutlined,
|
8
|
+
QuestionCircleOutlined,
|
9
|
+
InfoCircleOutlined,
|
10
|
+
LockOutlined,
|
11
|
+
GlobalOutlined,
|
12
|
+
SkinOutlined,
|
13
|
+
FullscreenOutlined,
|
14
|
+
FullscreenExitOutlined,
|
15
|
+
GithubOutlined,
|
16
|
+
FileTextOutlined,
|
17
|
+
} from '@ant-design/icons';
|
18
|
+
import './UserInfo.css';
|
19
|
+
|
20
|
+
const UserInfo = () => {
|
21
|
+
const [fullscreen, setFullscreen] = useState(false);
|
22
|
+
const [notifications, setNotifications] = useState([
|
23
|
+
{ id: 1, title: '任务执行失败', description: '队列 payment_queue 有5个任务执行失败', time: '5分钟前', read: false },
|
24
|
+
{ id: 2, title: '系统性能警告', description: 'CPU使用率超过80%', time: '10分钟前', read: false },
|
25
|
+
{ id: 3, title: '定时任务完成', description: '数据备份任务已完成', time: '1小时前', read: true },
|
26
|
+
]);
|
27
|
+
|
28
|
+
const unreadCount = notifications.filter(n => !n.read).length;
|
29
|
+
|
30
|
+
const handleFullscreen = () => {
|
31
|
+
if (!fullscreen) {
|
32
|
+
document.documentElement.requestFullscreen();
|
33
|
+
} else {
|
34
|
+
document.exitFullscreen();
|
35
|
+
}
|
36
|
+
setFullscreen(!fullscreen);
|
37
|
+
};
|
38
|
+
|
39
|
+
const handleLogout = () => {
|
40
|
+
Modal.confirm({
|
41
|
+
title: '确认退出',
|
42
|
+
content: '您确定要退出系统吗?',
|
43
|
+
okText: '确定',
|
44
|
+
cancelText: '取消',
|
45
|
+
onOk: () => {
|
46
|
+
// 这里处理退出逻辑
|
47
|
+
console.log('Logout');
|
48
|
+
},
|
49
|
+
});
|
50
|
+
};
|
51
|
+
|
52
|
+
const handleMarkAllRead = () => {
|
53
|
+
setNotifications(notifications.map(n => ({ ...n, read: true })));
|
54
|
+
};
|
55
|
+
|
56
|
+
const handleClearAll = () => {
|
57
|
+
setNotifications([]);
|
58
|
+
};
|
59
|
+
|
60
|
+
const userMenu = (
|
61
|
+
<Menu className="user-dropdown-menu">
|
62
|
+
<Menu.Item key="profile" icon={<UserOutlined />}>
|
63
|
+
个人中心
|
64
|
+
</Menu.Item>
|
65
|
+
<Menu.Item key="password" icon={<LockOutlined />}>
|
66
|
+
修改密码
|
67
|
+
</Menu.Item>
|
68
|
+
<Menu.Item key="settings" icon={<SettingOutlined />}>
|
69
|
+
个人设置
|
70
|
+
</Menu.Item>
|
71
|
+
<Menu.Divider />
|
72
|
+
<Menu.Item key="theme" icon={<SkinOutlined />}>
|
73
|
+
<Space>
|
74
|
+
主题设置
|
75
|
+
<span className="theme-badge">深色</span>
|
76
|
+
</Space>
|
77
|
+
</Menu.Item>
|
78
|
+
<Menu.Item key="language" icon={<GlobalOutlined />}>
|
79
|
+
<Space>
|
80
|
+
语言
|
81
|
+
<span className="language-badge">中文</span>
|
82
|
+
</Space>
|
83
|
+
</Menu.Item>
|
84
|
+
<Menu.Divider />
|
85
|
+
<Menu.Item key="logout" icon={<LogoutOutlined />} onClick={handleLogout}>
|
86
|
+
退出登录
|
87
|
+
</Menu.Item>
|
88
|
+
</Menu>
|
89
|
+
);
|
90
|
+
|
91
|
+
const notificationMenu = (
|
92
|
+
<div className="notification-dropdown">
|
93
|
+
<div className="notification-header">
|
94
|
+
<span>通知</span>
|
95
|
+
<Space>
|
96
|
+
<Button type="link" size="small" onClick={handleMarkAllRead}>
|
97
|
+
全部已读
|
98
|
+
</Button>
|
99
|
+
<Button type="link" size="small" onClick={handleClearAll}>
|
100
|
+
清空
|
101
|
+
</Button>
|
102
|
+
</Space>
|
103
|
+
</div>
|
104
|
+
<Menu className="notification-menu">
|
105
|
+
{notifications.length > 0 ? (
|
106
|
+
notifications.map(notification => (
|
107
|
+
<Menu.Item key={notification.id} className={notification.read ? 'read' : 'unread'}>
|
108
|
+
<div className="notification-item">
|
109
|
+
<div className="notification-title">{notification.title}</div>
|
110
|
+
<div className="notification-description">{notification.description}</div>
|
111
|
+
<div className="notification-time">{notification.time}</div>
|
112
|
+
</div>
|
113
|
+
</Menu.Item>
|
114
|
+
))
|
115
|
+
) : (
|
116
|
+
<div className="empty-notification">暂无通知</div>
|
117
|
+
)}
|
118
|
+
</Menu>
|
119
|
+
<div className="notification-footer">
|
120
|
+
<Button type="link" size="small">查看更多</Button>
|
121
|
+
</div>
|
122
|
+
</div>
|
123
|
+
);
|
124
|
+
|
125
|
+
const helpMenu = (
|
126
|
+
<Menu className="help-dropdown-menu">
|
127
|
+
<Menu.Item key="docs" icon={<FileTextOutlined />}>
|
128
|
+
使用文档
|
129
|
+
</Menu.Item>
|
130
|
+
<Menu.Item key="api" icon={<InfoCircleOutlined />}>
|
131
|
+
API文档
|
132
|
+
</Menu.Item>
|
133
|
+
<Menu.Item key="github" icon={<GithubOutlined />}>
|
134
|
+
GitHub
|
135
|
+
</Menu.Item>
|
136
|
+
<Menu.Divider />
|
137
|
+
<Menu.Item key="about" icon={<InfoCircleOutlined />}>
|
138
|
+
关于系统
|
139
|
+
</Menu.Item>
|
140
|
+
</Menu>
|
141
|
+
);
|
142
|
+
|
143
|
+
return (
|
144
|
+
<div className="user-info-container">
|
145
|
+
<Space size={16}>
|
146
|
+
{/* 全屏按钮 */}
|
147
|
+
<Tooltip title={fullscreen ? '退出全屏' : '全屏'}>
|
148
|
+
<Button
|
149
|
+
type="text"
|
150
|
+
icon={fullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
151
|
+
onClick={handleFullscreen}
|
152
|
+
className="header-icon-btn"
|
153
|
+
/>
|
154
|
+
</Tooltip>
|
155
|
+
|
156
|
+
{/* 帮助菜单 */}
|
157
|
+
<Dropdown overlay={helpMenu} placement="bottomRight" arrow>
|
158
|
+
<Button
|
159
|
+
type="text"
|
160
|
+
icon={<QuestionCircleOutlined />}
|
161
|
+
className="header-icon-btn"
|
162
|
+
/>
|
163
|
+
</Dropdown>
|
164
|
+
|
165
|
+
{/* 通知中心 */}
|
166
|
+
<Dropdown
|
167
|
+
overlay={notificationMenu}
|
168
|
+
placement="bottomRight"
|
169
|
+
trigger={['click']}
|
170
|
+
overlayClassName="notification-dropdown-container"
|
171
|
+
>
|
172
|
+
<Badge count={unreadCount} size="small">
|
173
|
+
<Button
|
174
|
+
type="text"
|
175
|
+
icon={<BellOutlined />}
|
176
|
+
className="header-icon-btn"
|
177
|
+
/>
|
178
|
+
</Badge>
|
179
|
+
</Dropdown>
|
180
|
+
|
181
|
+
{/* 用户信息 */}
|
182
|
+
<Dropdown overlay={userMenu} placement="bottomRight" arrow>
|
183
|
+
<div className="user-avatar-container">
|
184
|
+
<Avatar
|
185
|
+
size={32}
|
186
|
+
icon={<UserOutlined />}
|
187
|
+
style={{ backgroundColor: '#1890ff' }}
|
188
|
+
/>
|
189
|
+
<span className="username">管理员</span>
|
190
|
+
</div>
|
191
|
+
</Dropdown>
|
192
|
+
</Space>
|
193
|
+
</div>
|
194
|
+
);
|
195
|
+
};
|
196
|
+
|
197
|
+
export default UserInfo;
|
@@ -0,0 +1,72 @@
|
|
1
|
+
/**
|
2
|
+
* 命名空间上下文
|
3
|
+
* 用于全局管理当前选中的命名空间
|
4
|
+
*/
|
5
|
+
import React, { createContext, useState, useContext, useEffect } from 'react';
|
6
|
+
|
7
|
+
const NamespaceContext = createContext();
|
8
|
+
|
9
|
+
export const useNamespace = () => {
|
10
|
+
const context = useContext(NamespaceContext);
|
11
|
+
if (!context) {
|
12
|
+
throw new Error('useNamespace must be used within NamespaceProvider');
|
13
|
+
}
|
14
|
+
return context;
|
15
|
+
};
|
16
|
+
|
17
|
+
export const NamespaceProvider = ({ children }) => {
|
18
|
+
// 从localStorage读取上次选择的命名空间
|
19
|
+
const [currentNamespace, setCurrentNamespaceState] = useState(() => {
|
20
|
+
const saved = localStorage.getItem('selectedNamespace');
|
21
|
+
console.log('🔧 NamespaceContext初始化,从localStorage读取:', saved);
|
22
|
+
return saved || 'default'; // 默认使用default命名空间
|
23
|
+
});
|
24
|
+
|
25
|
+
// 添加一个刷新触发器状态
|
26
|
+
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
27
|
+
|
28
|
+
// 当命名空间改变时,保存到localStorage
|
29
|
+
useEffect(() => {
|
30
|
+
console.log('🔧 NamespaceContext命名空间变化:', currentNamespace);
|
31
|
+
if (currentNamespace) {
|
32
|
+
localStorage.setItem('selectedNamespace', currentNamespace);
|
33
|
+
}
|
34
|
+
}, [currentNamespace]);
|
35
|
+
|
36
|
+
const setCurrentNamespace = (namespace) => {
|
37
|
+
console.log('🔧 NamespaceContext.setCurrentNamespace被调用:', namespace);
|
38
|
+
console.log('🔧 当前值:', currentNamespace);
|
39
|
+
setCurrentNamespaceState(namespace);
|
40
|
+
};
|
41
|
+
|
42
|
+
// 添加刷新命名空间列表的方法
|
43
|
+
const refreshNamespaceList = () => {
|
44
|
+
console.log('🔧 NamespaceContext.refreshNamespaceList被调用');
|
45
|
+
setRefreshTrigger(prev => prev + 1);
|
46
|
+
};
|
47
|
+
|
48
|
+
const value = {
|
49
|
+
currentNamespace,
|
50
|
+
setCurrentNamespace,
|
51
|
+
refreshTrigger, // 暴露刷新触发器
|
52
|
+
refreshNamespaceList, // 暴露刷新方法
|
53
|
+
// 辅助方法:构建带命名空间的API URL
|
54
|
+
getApiUrl: (path) => {
|
55
|
+
if (!currentNamespace) {
|
56
|
+
throw new Error('No namespace selected');
|
57
|
+
}
|
58
|
+
// 如果路径中包含{namespace}占位符,替换它
|
59
|
+
if (path.includes('{namespace}')) {
|
60
|
+
return path.replace('{namespace}', currentNamespace);
|
61
|
+
}
|
62
|
+
// 否则在路径前添加命名空间
|
63
|
+
return `/api/data/${currentNamespace}${path}`;
|
64
|
+
}
|
65
|
+
};
|
66
|
+
|
67
|
+
return (
|
68
|
+
<NamespaceContext.Provider value={value}>
|
69
|
+
{children}
|
70
|
+
</NamespaceContext.Provider>
|
71
|
+
);
|
72
|
+
};
|
@@ -0,0 +1,245 @@
|
|
1
|
+
import { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react';
|
2
|
+
import { useLocation, useNavigate } from 'react-router-dom';
|
3
|
+
|
4
|
+
const TabsContext = createContext();
|
5
|
+
|
6
|
+
export const useTabs = () => {
|
7
|
+
const context = useContext(TabsContext);
|
8
|
+
if (!context) {
|
9
|
+
throw new Error('useTabs must be used within TabsProvider');
|
10
|
+
}
|
11
|
+
return context;
|
12
|
+
};
|
13
|
+
|
14
|
+
// 生成标签页的唯一ID
|
15
|
+
const generateTabId = (path, params = {}) => {
|
16
|
+
// 对于队列详情页等需要区分不同参数的页面,将参数包含在ID中
|
17
|
+
const paramStr = Object.keys(params).sort().map(key => `${key}=${params[key]}`).join('&');
|
18
|
+
return paramStr ? `${path}?${paramStr}` : path;
|
19
|
+
};
|
20
|
+
|
21
|
+
// 获取页面标题
|
22
|
+
const getPageTitle = (path, params = {}) => {
|
23
|
+
if (path === '/dashboard') return '概览';
|
24
|
+
if (path === '/queues') return '队列';
|
25
|
+
if (path === '/workers') return 'Workers';
|
26
|
+
if (path === '/scheduled-tasks') return '定时任务';
|
27
|
+
if (path === '/alerts') return '监控告警';
|
28
|
+
if (path.startsWith('/queue/')) {
|
29
|
+
const queueName = path.replace('/queue/', '');
|
30
|
+
// 如果有scheduled_task_id参数,说明是从定时任务页面跳转过来的历史记录
|
31
|
+
if (params.scheduled_task_id) {
|
32
|
+
return `历史记录 (ID:${params.scheduled_task_id})`;
|
33
|
+
}
|
34
|
+
return `队列: ${decodeURIComponent(queueName)}`;
|
35
|
+
}
|
36
|
+
return '未知页面';
|
37
|
+
};
|
38
|
+
|
39
|
+
export const TabsProvider = ({ children }) => {
|
40
|
+
const [tabs, setTabs] = useState([]);
|
41
|
+
const [activeTabId, setActiveTabId] = useState(null);
|
42
|
+
const [tabStates, setTabStates] = useState({}); // 保存每个标签页的状态
|
43
|
+
const location = useLocation();
|
44
|
+
const navigate = useNavigate();
|
45
|
+
const isNavigatingRef = useRef(false); // 标记是否正在通过addOrActivateTab导航
|
46
|
+
|
47
|
+
// 添加或激活标签页
|
48
|
+
const addOrActivateTab = useCallback((path, title, params = {}, state = {}) => {
|
49
|
+
const tabId = generateTabId(path, params);
|
50
|
+
|
51
|
+
// 设置导航标记,防止useEffect重复创建标签页
|
52
|
+
isNavigatingRef.current = true;
|
53
|
+
|
54
|
+
setTabs(prevTabs => {
|
55
|
+
const existingTab = prevTabs.find(tab => tab.id === tabId);
|
56
|
+
|
57
|
+
if (existingTab) {
|
58
|
+
// 标签页已存在,不需要创建新的
|
59
|
+
return prevTabs;
|
60
|
+
}
|
61
|
+
|
62
|
+
// 创建新标签页
|
63
|
+
const newTab = {
|
64
|
+
id: tabId,
|
65
|
+
path,
|
66
|
+
title: title || getPageTitle(path, params),
|
67
|
+
params,
|
68
|
+
closable: true, // 除了某些页面外都可以关闭
|
69
|
+
};
|
70
|
+
|
71
|
+
// 限制最大标签页数量
|
72
|
+
const maxTabs = 10;
|
73
|
+
if (prevTabs.length >= maxTabs) {
|
74
|
+
// 关闭最早的可关闭标签页
|
75
|
+
const closableTab = prevTabs.find(tab => tab.closable);
|
76
|
+
if (closableTab) {
|
77
|
+
return [...prevTabs.filter(tab => tab.id !== closableTab.id), newTab];
|
78
|
+
}
|
79
|
+
}
|
80
|
+
return [...prevTabs, newTab];
|
81
|
+
});
|
82
|
+
|
83
|
+
// 保存标签页状态
|
84
|
+
if (state && Object.keys(state).length > 0) {
|
85
|
+
setTabStates(prev => ({
|
86
|
+
...prev,
|
87
|
+
[tabId]: state
|
88
|
+
}));
|
89
|
+
}
|
90
|
+
// 无论标签页是否已存在,都激活并导航
|
91
|
+
setActiveTabId(tabId);
|
92
|
+
|
93
|
+
// 导航到新标签页的路径
|
94
|
+
const fullPath = params && Object.keys(params).length > 0
|
95
|
+
? `${path}?${new URLSearchParams(params).toString()}`
|
96
|
+
: path;
|
97
|
+
navigate(fullPath);
|
98
|
+
|
99
|
+
// 导航完成后重置标记
|
100
|
+
setTimeout(() => {
|
101
|
+
isNavigatingRef.current = false;
|
102
|
+
}, 100);
|
103
|
+
}, [navigate]);
|
104
|
+
|
105
|
+
// 关闭标签页
|
106
|
+
const closeTab = useCallback((tabId, event) => {
|
107
|
+
if (event) {
|
108
|
+
event.stopPropagation();
|
109
|
+
}
|
110
|
+
|
111
|
+
setTabs(prevTabs => {
|
112
|
+
const tabIndex = prevTabs.findIndex(tab => tab.id === tabId);
|
113
|
+
const newTabs = prevTabs.filter(tab => tab.id !== tabId);
|
114
|
+
|
115
|
+
// 如果关闭的是当前激活的标签页,需要切换到其他标签页
|
116
|
+
if (tabId === activeTabId && newTabs.length > 0) {
|
117
|
+
// 优先切换到右边的标签页,如果没有则切换到左边
|
118
|
+
const newActiveTab = newTabs[Math.min(tabIndex, newTabs.length - 1)];
|
119
|
+
setActiveTabId(newActiveTab.id);
|
120
|
+
navigate(newActiveTab.path);
|
121
|
+
}
|
122
|
+
|
123
|
+
return newTabs;
|
124
|
+
});
|
125
|
+
|
126
|
+
// 清除标签页状态
|
127
|
+
setTabStates(prev => {
|
128
|
+
const newStates = { ...prev };
|
129
|
+
delete newStates[tabId];
|
130
|
+
return newStates;
|
131
|
+
});
|
132
|
+
}, [activeTabId, navigate]);
|
133
|
+
|
134
|
+
// 切换标签页
|
135
|
+
const switchTab = useCallback((tabId) => {
|
136
|
+
const tab = tabs.find(t => t.id === tabId);
|
137
|
+
if (tab) {
|
138
|
+
setActiveTabId(tabId);
|
139
|
+
navigate(tab.path);
|
140
|
+
}
|
141
|
+
}, [tabs, navigate]);
|
142
|
+
|
143
|
+
// 保存当前标签页状态
|
144
|
+
const saveTabState = useCallback((state) => {
|
145
|
+
if (activeTabId) {
|
146
|
+
setTabStates(prev => ({
|
147
|
+
...prev,
|
148
|
+
[activeTabId]: {
|
149
|
+
...prev[activeTabId],
|
150
|
+
...state
|
151
|
+
}
|
152
|
+
}));
|
153
|
+
}
|
154
|
+
}, [activeTabId]);
|
155
|
+
|
156
|
+
// 获取当前标签页状态
|
157
|
+
const getTabState = useCallback(() => {
|
158
|
+
return activeTabId ? (tabStates[activeTabId] || {}) : {};
|
159
|
+
}, [activeTabId, tabStates]);
|
160
|
+
|
161
|
+
// 关闭所有标签页
|
162
|
+
const closeAllTabs = useCallback(() => {
|
163
|
+
setTabs([]);
|
164
|
+
setTabStates({});
|
165
|
+
setActiveTabId(null);
|
166
|
+
}, []);
|
167
|
+
|
168
|
+
// 关闭其他标签页
|
169
|
+
const closeOtherTabs = useCallback((tabId) => {
|
170
|
+
const tab = tabs.find(t => t.id === tabId);
|
171
|
+
if (tab) {
|
172
|
+
setTabs([tab]);
|
173
|
+
setTabStates(prev => ({
|
174
|
+
[tabId]: prev[tabId]
|
175
|
+
}));
|
176
|
+
setActiveTabId(tabId);
|
177
|
+
}
|
178
|
+
}, [tabs]);
|
179
|
+
|
180
|
+
// 监听路由变化
|
181
|
+
useEffect(() => {
|
182
|
+
// 如果是通过addOrActivateTab导航的,跳过处理
|
183
|
+
if (isNavigatingRef.current) {
|
184
|
+
return;
|
185
|
+
}
|
186
|
+
|
187
|
+
const path = location.pathname;
|
188
|
+
const search = location.search;
|
189
|
+
const params = Object.fromEntries(new URLSearchParams(search));
|
190
|
+
|
191
|
+
// 生成标签页ID
|
192
|
+
const tabId = generateTabId(path, params);
|
193
|
+
const existingTab = tabs.find(tab => tab.id === tabId);
|
194
|
+
|
195
|
+
if (!existingTab) {
|
196
|
+
// 如果标签页不存在,创建新的(但不再调用navigate,因为已经在这个路径了)
|
197
|
+
setTabs(prevTabs => {
|
198
|
+
// 再次检查避免重复
|
199
|
+
if (prevTabs.find(tab => tab.id === tabId)) {
|
200
|
+
return prevTabs;
|
201
|
+
}
|
202
|
+
|
203
|
+
const newTab = {
|
204
|
+
id: tabId,
|
205
|
+
path,
|
206
|
+
title: getPageTitle(path, params),
|
207
|
+
params,
|
208
|
+
closable: true,
|
209
|
+
};
|
210
|
+
|
211
|
+
const maxTabs = 10;
|
212
|
+
if (prevTabs.length >= maxTabs) {
|
213
|
+
const closableTab = prevTabs.find(tab => tab.closable);
|
214
|
+
if (closableTab) {
|
215
|
+
return [...prevTabs.filter(tab => tab.id !== closableTab.id), newTab];
|
216
|
+
}
|
217
|
+
}
|
218
|
+
|
219
|
+
return [...prevTabs, newTab];
|
220
|
+
});
|
221
|
+
setActiveTabId(tabId);
|
222
|
+
} else {
|
223
|
+
// 如果标签页存在,激活它
|
224
|
+
setActiveTabId(tabId);
|
225
|
+
}
|
226
|
+
}, [location, tabs]);
|
227
|
+
|
228
|
+
const value = {
|
229
|
+
tabs,
|
230
|
+
activeTabId,
|
231
|
+
addOrActivateTab,
|
232
|
+
closeTab,
|
233
|
+
switchTab,
|
234
|
+
saveTabState,
|
235
|
+
getTabState,
|
236
|
+
closeAllTabs,
|
237
|
+
closeOtherTabs,
|
238
|
+
};
|
239
|
+
|
240
|
+
return (
|
241
|
+
<TabsContext.Provider value={value}>
|
242
|
+
{children}
|
243
|
+
</TabsContext.Provider>
|
244
|
+
);
|
245
|
+
};
|