jettask 0.2.6__py3-none-any.whl → 0.2.8__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.
Files changed (66) hide show
  1. jettask/core/cli.py +152 -0
  2. jettask/pg_consumer/sql/add_execution_time_field.sql +29 -0
  3. jettask/pg_consumer/sql/create_new_tables.sql +137 -0
  4. jettask/pg_consumer/sql/create_tables_v3.sql +175 -0
  5. jettask/pg_consumer/sql/migrate_to_new_structure.sql +179 -0
  6. jettask/pg_consumer/sql/modify_time_fields.sql +69 -0
  7. jettask/webui/frontend/package.json +30 -0
  8. jettask/webui/frontend/src/App.css +109 -0
  9. jettask/webui/frontend/src/App.jsx +66 -0
  10. jettask/webui/frontend/src/components/NamespaceSelector.jsx +166 -0
  11. jettask/webui/frontend/src/components/QueueBacklogChart.jsx +298 -0
  12. jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +638 -0
  13. jettask/webui/frontend/src/components/QueueDetailsTable.css +65 -0
  14. jettask/webui/frontend/src/components/QueueDetailsTable.jsx +487 -0
  15. jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +465 -0
  16. jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +423 -0
  17. jettask/webui/frontend/src/components/TaskFilter.jsx +425 -0
  18. jettask/webui/frontend/src/components/TimeRangeSelector.css +21 -0
  19. jettask/webui/frontend/src/components/TimeRangeSelector.jsx +160 -0
  20. jettask/webui/frontend/src/components/charts/QueueChart.jsx +111 -0
  21. jettask/webui/frontend/src/components/charts/QueueTrendChart.jsx +115 -0
  22. jettask/webui/frontend/src/components/charts/WorkerChart.jsx +40 -0
  23. jettask/webui/frontend/src/components/common/StatsCard.jsx +18 -0
  24. jettask/webui/frontend/src/components/layout/AppLayout.css +95 -0
  25. jettask/webui/frontend/src/components/layout/AppLayout.jsx +49 -0
  26. jettask/webui/frontend/src/components/layout/Header.css +106 -0
  27. jettask/webui/frontend/src/components/layout/Header.jsx +106 -0
  28. jettask/webui/frontend/src/components/layout/SideMenu.css +137 -0
  29. jettask/webui/frontend/src/components/layout/SideMenu.jsx +209 -0
  30. jettask/webui/frontend/src/components/layout/TabsNav.css +244 -0
  31. jettask/webui/frontend/src/components/layout/TabsNav.jsx +206 -0
  32. jettask/webui/frontend/src/components/layout/UserInfo.css +197 -0
  33. jettask/webui/frontend/src/components/layout/UserInfo.jsx +197 -0
  34. jettask/webui/frontend/src/contexts/LoadingContext.jsx +27 -0
  35. jettask/webui/frontend/src/contexts/NamespaceContext.jsx +72 -0
  36. jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +245 -0
  37. jettask/webui/frontend/src/index.css +114 -0
  38. jettask/webui/frontend/src/main.jsx +20 -0
  39. jettask/webui/frontend/src/pages/Alerts.jsx +684 -0
  40. jettask/webui/frontend/src/pages/Dashboard/index.css +35 -0
  41. jettask/webui/frontend/src/pages/Dashboard/index.jsx +281 -0
  42. jettask/webui/frontend/src/pages/Dashboard.jsx +1330 -0
  43. jettask/webui/frontend/src/pages/QueueDetail.jsx +1117 -0
  44. jettask/webui/frontend/src/pages/QueueMonitor.jsx +527 -0
  45. jettask/webui/frontend/src/pages/Queues.jsx +12 -0
  46. jettask/webui/frontend/src/pages/ScheduledTasks.jsx +809 -0
  47. jettask/webui/frontend/src/pages/Settings.jsx +800 -0
  48. jettask/webui/frontend/src/pages/Workers.jsx +12 -0
  49. jettask/webui/frontend/src/services/api.js +114 -0
  50. jettask/webui/frontend/src/services/queueTrend.js +152 -0
  51. jettask/webui/frontend/src/utils/suppressWarnings.js +22 -0
  52. jettask/webui/frontend/src/utils/userPreferences.js +154 -0
  53. jettask/webui/frontend/vite.config.js +26 -0
  54. {jettask-0.2.6.dist-info → jettask-0.2.8.dist-info}/METADATA +70 -2
  55. {jettask-0.2.6.dist-info → jettask-0.2.8.dist-info}/RECORD +59 -14
  56. jettask/webui/static/dist/assets/index-7129cfe1.css +0 -1
  57. jettask/webui/static/dist/assets/index-8d1935cc.js +0 -774
  58. jettask/webui/static/dist/index.html +0 -15
  59. jettask/webui/static/index.html +0 -1734
  60. jettask/webui/static/queue.html +0 -981
  61. jettask/webui/static/queues.html +0 -549
  62. jettask/webui/static/workers.html +0 -734
  63. {jettask-0.2.6.dist-info → jettask-0.2.8.dist-info}/WHEEL +0 -0
  64. {jettask-0.2.6.dist-info → jettask-0.2.8.dist-info}/entry_points.txt +0 -0
  65. {jettask-0.2.6.dist-info → jettask-0.2.8.dist-info}/licenses/LICENSE +0 -0
  66. {jettask-0.2.6.dist-info → jettask-0.2.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,206 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Tabs, Dropdown, Menu } from 'antd';
3
+ import { useNavigate, useLocation } from 'react-router-dom';
4
+ import {
5
+ CloseOutlined,
6
+ ReloadOutlined,
7
+ CloseCircleOutlined,
8
+ VerticalRightOutlined,
9
+ VerticalLeftOutlined,
10
+ HomeOutlined
11
+ } from '@ant-design/icons';
12
+ import './TabsNav.css';
13
+
14
+ const TabsNav = () => {
15
+ const navigate = useNavigate();
16
+ const location = useLocation();
17
+ const [activeKey, setActiveKey] = useState('/dashboard');
18
+ const [tabs, setTabs] = useState([
19
+ {
20
+ key: '/dashboard',
21
+ label: '概览',
22
+ closable: false,
23
+ }
24
+ ]);
25
+
26
+ // 路由与标签映射
27
+ const routeToTab = {
28
+ '/dashboard': { label: '概览', closable: false },
29
+ '/queues': { label: '任务队列', closable: true },
30
+ '/scheduled-tasks': { label: '定时任务', closable: true },
31
+ '/alerts': { label: '监控告警', closable: true },
32
+ '/analytics': { label: '数据分析', closable: true },
33
+ '/performance': { label: '性能监控', closable: true },
34
+ '/logs': { label: '日志查询', closable: true },
35
+ '/api-docs': { label: 'API文档', closable: true },
36
+ '/settings': { label: '系统设置', closable: true },
37
+ };
38
+
39
+ // 动态路由处理(如队列详情页)
40
+ const getDynamicTabInfo = (pathname) => {
41
+ if (pathname.startsWith('/queue/')) {
42
+ const queueName = pathname.split('/queue/')[1];
43
+ return {
44
+ label: `队列: ${decodeURIComponent(queueName)}`,
45
+ closable: true,
46
+ };
47
+ }
48
+ return null;
49
+ };
50
+
51
+ useEffect(() => {
52
+ const pathname = location.pathname;
53
+
54
+ // 使用函数式更新来避免依赖 tabs 状态
55
+ setTabs(prevTabs => {
56
+ // 检查是否已存在该标签
57
+ const existingTab = prevTabs.find(tab => tab.key === pathname);
58
+
59
+ if (!existingTab) {
60
+ // 获取标签信息
61
+ let tabInfo = routeToTab[pathname] || getDynamicTabInfo(pathname);
62
+
63
+ if (tabInfo) {
64
+ const newTab = {
65
+ key: pathname,
66
+ ...tabInfo,
67
+ };
68
+ return [...prevTabs, newTab];
69
+ }
70
+ }
71
+
72
+ return prevTabs;
73
+ });
74
+
75
+ setActiveKey(pathname);
76
+ }, [location.pathname]);
77
+
78
+ const handleTabChange = (key) => {
79
+ setActiveKey(key);
80
+ navigate(key);
81
+ };
82
+
83
+ const handleTabEdit = (targetKey, action) => {
84
+ if (action === 'remove') {
85
+ removeTab(targetKey);
86
+ }
87
+ };
88
+
89
+ const removeTab = (targetKey) => {
90
+ const targetIndex = tabs.findIndex(tab => tab.key === targetKey);
91
+ const newTabs = tabs.filter(tab => tab.key !== targetKey);
92
+
93
+ if (newTabs.length && targetKey === activeKey) {
94
+ // 如果关闭的是当前标签,切换到相邻标签
95
+ const newActiveKey = targetIndex === 0
96
+ ? newTabs[0].key
97
+ : newTabs[targetIndex - 1].key;
98
+ setActiveKey(newActiveKey);
99
+ navigate(newActiveKey);
100
+ }
101
+
102
+ setTabs(newTabs);
103
+ };
104
+
105
+ const handleRefresh = () => {
106
+ window.location.reload();
107
+ };
108
+
109
+ const handleCloseAll = () => {
110
+ const homeTabs = tabs.filter(tab => !tab.closable);
111
+ setTabs(homeTabs);
112
+ setActiveKey('/dashboard');
113
+ navigate('/dashboard');
114
+ };
115
+
116
+ const handleCloseOthers = () => {
117
+ const currentTab = tabs.find(tab => tab.key === activeKey);
118
+ const homeTabs = tabs.filter(tab => !tab.closable);
119
+ const newTabs = currentTab?.closable
120
+ ? [...homeTabs, currentTab]
121
+ : homeTabs;
122
+ setTabs(newTabs);
123
+ };
124
+
125
+ const handleCloseLeft = () => {
126
+ const currentIndex = tabs.findIndex(tab => tab.key === activeKey);
127
+ const rightTabs = tabs.slice(currentIndex);
128
+ const homeTabs = tabs.filter(tab => !tab.closable && tabs.indexOf(tab) < currentIndex);
129
+ setTabs([...homeTabs, ...rightTabs]);
130
+ };
131
+
132
+ const handleCloseRight = () => {
133
+ const currentIndex = tabs.findIndex(tab => tab.key === activeKey);
134
+ const leftTabs = tabs.slice(0, currentIndex + 1);
135
+ setTabs(leftTabs);
136
+ };
137
+
138
+ const contextMenu = (
139
+ <Menu
140
+ items={[
141
+ {
142
+ key: 'refresh',
143
+ icon: <ReloadOutlined />,
144
+ label: '刷新当前',
145
+ onClick: handleRefresh,
146
+ },
147
+ {
148
+ type: 'divider',
149
+ },
150
+ {
151
+ key: 'closeOthers',
152
+ icon: <CloseCircleOutlined />,
153
+ label: '关闭其他',
154
+ onClick: handleCloseOthers,
155
+ },
156
+ {
157
+ key: 'closeLeft',
158
+ icon: <VerticalRightOutlined />,
159
+ label: '关闭左侧',
160
+ onClick: handleCloseLeft,
161
+ },
162
+ {
163
+ key: 'closeRight',
164
+ icon: <VerticalLeftOutlined />,
165
+ label: '关闭右侧',
166
+ onClick: handleCloseRight,
167
+ },
168
+ {
169
+ key: 'closeAll',
170
+ icon: <CloseOutlined />,
171
+ label: '关闭所有',
172
+ onClick: handleCloseAll,
173
+ },
174
+ ]}
175
+ />
176
+ );
177
+
178
+ const tabItems = tabs.map(tab => ({
179
+ key: tab.key,
180
+ label: (
181
+ <Dropdown overlay={contextMenu} trigger={['contextMenu']} >
182
+ <span className={`tab-label ${tab.key === '/dashboard' ? 'tab-label-center' : ''}`} >
183
+ {tab.label}
184
+ </span>
185
+ </Dropdown>
186
+ ),
187
+ closable: tab.closable,
188
+ }));
189
+
190
+ return (
191
+ <div className="tabs-nav-container">
192
+ <Tabs
193
+ type="editable-card"
194
+ hideAdd
195
+ activeKey={activeKey}
196
+ onChange={handleTabChange}
197
+ onEdit={handleTabEdit}
198
+ items={tabItems}
199
+ className="tabs-nav"
200
+ size="small"
201
+ />
202
+ </div>
203
+ );
204
+ };
205
+
206
+ export default TabsNav;
@@ -0,0 +1,197 @@
1
+ /* 用户信息容器 */
2
+ .user-info-container {
3
+ display: flex;
4
+ align-items: center;
5
+ height: 100%;
6
+ padding: 0 16px;
7
+ }
8
+
9
+ /* 图标按钮样式 */
10
+ .header-icon-btn {
11
+ color: rgba(255, 255, 255, 0.85);
12
+ font-size: 16px;
13
+ padding: 4px 8px;
14
+ height: 32px;
15
+ width: 32px;
16
+ display: flex;
17
+ align-items: center;
18
+ justify-content: center;
19
+ transition: all 0.3s;
20
+ }
21
+
22
+ .header-icon-btn:hover {
23
+ color: #fff;
24
+ background: rgba(255, 255, 255, 0.1);
25
+ }
26
+
27
+ /* 用户头像容器 */
28
+ .user-avatar-container {
29
+ display: flex;
30
+ align-items: center;
31
+ gap: 8px;
32
+ cursor: pointer;
33
+ padding: 4px 8px;
34
+ border-radius: 4px;
35
+ transition: all 0.3s;
36
+ }
37
+
38
+ .user-avatar-container:hover {
39
+ background: rgba(255, 255, 255, 0.1);
40
+ }
41
+
42
+ .username {
43
+ color: rgba(255, 255, 255, 0.85);
44
+ font-size: 14px;
45
+ max-width: 100px;
46
+ overflow: hidden;
47
+ text-overflow: ellipsis;
48
+ white-space: nowrap;
49
+ }
50
+
51
+ /* 用户下拉菜单 */
52
+ .user-dropdown-menu {
53
+ min-width: 180px;
54
+ }
55
+
56
+ .user-dropdown-menu .ant-menu-item {
57
+ height: 40px;
58
+ line-height: 40px;
59
+ }
60
+
61
+ .theme-badge,
62
+ .language-badge {
63
+ background: #f0f0f0;
64
+ padding: 2px 8px;
65
+ border-radius: 4px;
66
+ font-size: 12px;
67
+ color: #666;
68
+ }
69
+
70
+ /* 通知下拉框容器 */
71
+ .notification-dropdown-container {
72
+ width: 360px;
73
+ }
74
+
75
+ .notification-dropdown {
76
+ background: #fff;
77
+ border-radius: 4px;
78
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
79
+ }
80
+
81
+ /* 通知头部 */
82
+ .notification-header {
83
+ display: flex;
84
+ justify-content: space-between;
85
+ align-items: center;
86
+ padding: 12px 16px;
87
+ border-bottom: 1px solid #f0f0f0;
88
+ }
89
+
90
+ .notification-header span {
91
+ font-size: 16px;
92
+ font-weight: 500;
93
+ }
94
+
95
+ /* 通知菜单 */
96
+ .notification-menu {
97
+ max-height: 400px;
98
+ overflow-y: auto;
99
+ border: none;
100
+ }
101
+
102
+ .notification-menu .ant-menu-item {
103
+ height: auto;
104
+ padding: 12px 16px;
105
+ border-bottom: 1px solid #f0f0f0;
106
+ }
107
+
108
+ .notification-menu .ant-menu-item:last-child {
109
+ border-bottom: none;
110
+ }
111
+
112
+ .notification-menu .ant-menu-item.unread {
113
+ background: #e6f7ff;
114
+ }
115
+
116
+ .notification-menu .ant-menu-item:hover {
117
+ background: #f5f5f5;
118
+ }
119
+
120
+ /* 通知项 */
121
+ .notification-item {
122
+ display: flex;
123
+ flex-direction: column;
124
+ gap: 4px;
125
+ }
126
+
127
+ .notification-title {
128
+ font-size: 14px;
129
+ font-weight: 500;
130
+ color: #262626;
131
+ }
132
+
133
+ .notification-description {
134
+ font-size: 12px;
135
+ color: #8c8c8c;
136
+ line-height: 1.5;
137
+ }
138
+
139
+ .notification-time {
140
+ font-size: 11px;
141
+ color: #bfbfbf;
142
+ }
143
+
144
+ /* 空通知 */
145
+ .empty-notification {
146
+ padding: 32px;
147
+ text-align: center;
148
+ color: #8c8c8c;
149
+ font-size: 14px;
150
+ }
151
+
152
+ /* 通知底部 */
153
+ .notification-footer {
154
+ padding: 8px;
155
+ text-align: center;
156
+ border-top: 1px solid #f0f0f0;
157
+ }
158
+
159
+ /* 帮助下拉菜单 */
160
+ .help-dropdown-menu {
161
+ min-width: 140px;
162
+ }
163
+
164
+ .help-dropdown-menu .ant-menu-item {
165
+ height: 36px;
166
+ line-height: 36px;
167
+ }
168
+
169
+ /* Badge样式覆盖 */
170
+ .ant-badge-count {
171
+ height: 16px;
172
+ min-width: 16px;
173
+ line-height: 16px;
174
+ font-size: 10px;
175
+ padding: 0 4px;
176
+ right: -6px;
177
+ top: -6px;
178
+ }
179
+
180
+ /* 滚动条样式 */
181
+ .notification-menu::-webkit-scrollbar {
182
+ width: 6px;
183
+ }
184
+
185
+ .notification-menu::-webkit-scrollbar-track {
186
+ background: #f0f0f0;
187
+ border-radius: 3px;
188
+ }
189
+
190
+ .notification-menu::-webkit-scrollbar-thumb {
191
+ background: #bfbfbf;
192
+ border-radius: 3px;
193
+ }
194
+
195
+ .notification-menu::-webkit-scrollbar-thumb:hover {
196
+ background: #8c8c8c;
197
+ }
@@ -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,27 @@
1
+ import React, { createContext, useState, useContext } from 'react';
2
+
3
+ const LoadingContext = createContext();
4
+
5
+ export const LoadingProvider = ({ children }) => {
6
+ const [isLoading, setIsLoading] = useState(false);
7
+ const [loadingText, setLoadingText] = useState('');
8
+
9
+ const setLoading = (loading, text = '') => {
10
+ setIsLoading(loading);
11
+ setLoadingText(text);
12
+ };
13
+
14
+ return (
15
+ <LoadingContext.Provider value={{ isLoading, loadingText, setLoading }}>
16
+ {children}
17
+ </LoadingContext.Provider>
18
+ );
19
+ };
20
+
21
+ export const useLoading = () => {
22
+ const context = useContext(LoadingContext);
23
+ if (!context) {
24
+ throw new Error('useLoading must be used within a LoadingProvider');
25
+ }
26
+ return context;
27
+ };
@@ -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
+ };