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.
Files changed (89) hide show
  1. jettask/constants.py +213 -0
  2. jettask/core/app.py +525 -205
  3. jettask/core/cli.py +193 -185
  4. jettask/core/consumer_manager.py +126 -34
  5. jettask/core/context.py +3 -0
  6. jettask/core/enums.py +137 -0
  7. jettask/core/event_pool.py +501 -168
  8. jettask/core/message.py +147 -0
  9. jettask/core/offline_worker_recovery.py +181 -114
  10. jettask/core/task.py +10 -174
  11. jettask/core/task_batch.py +153 -0
  12. jettask/core/unified_manager_base.py +243 -0
  13. jettask/core/worker_scanner.py +54 -54
  14. jettask/executors/asyncio.py +184 -64
  15. jettask/webui/backend/config.py +51 -0
  16. jettask/webui/backend/data_access.py +2083 -92
  17. jettask/webui/backend/data_api.py +3294 -0
  18. jettask/webui/backend/dependencies.py +261 -0
  19. jettask/webui/backend/init_meta_db.py +158 -0
  20. jettask/webui/backend/main.py +1358 -69
  21. jettask/webui/backend/main_unified.py +78 -0
  22. jettask/webui/backend/main_v2.py +394 -0
  23. jettask/webui/backend/namespace_api.py +295 -0
  24. jettask/webui/backend/namespace_api_old.py +294 -0
  25. jettask/webui/backend/namespace_data_access.py +611 -0
  26. jettask/webui/backend/queue_backlog_api.py +727 -0
  27. jettask/webui/backend/queue_stats_v2.py +521 -0
  28. jettask/webui/backend/redis_monitor_api.py +476 -0
  29. jettask/webui/backend/unified_api_router.py +1601 -0
  30. jettask/webui/db_init.py +204 -32
  31. jettask/webui/frontend/package-lock.json +492 -1
  32. jettask/webui/frontend/package.json +4 -1
  33. jettask/webui/frontend/src/App.css +105 -7
  34. jettask/webui/frontend/src/App.jsx +49 -20
  35. jettask/webui/frontend/src/components/NamespaceSelector.jsx +166 -0
  36. jettask/webui/frontend/src/components/QueueBacklogChart.jsx +298 -0
  37. jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +638 -0
  38. jettask/webui/frontend/src/components/QueueDetailsTable.css +65 -0
  39. jettask/webui/frontend/src/components/QueueDetailsTable.jsx +487 -0
  40. jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +465 -0
  41. jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +423 -0
  42. jettask/webui/frontend/src/components/TaskFilter.jsx +425 -0
  43. jettask/webui/frontend/src/components/TimeRangeSelector.css +21 -0
  44. jettask/webui/frontend/src/components/TimeRangeSelector.jsx +160 -0
  45. jettask/webui/frontend/src/components/layout/AppLayout.css +95 -0
  46. jettask/webui/frontend/src/components/layout/AppLayout.jsx +49 -0
  47. jettask/webui/frontend/src/components/layout/Header.css +34 -10
  48. jettask/webui/frontend/src/components/layout/Header.jsx +31 -23
  49. jettask/webui/frontend/src/components/layout/SideMenu.css +137 -0
  50. jettask/webui/frontend/src/components/layout/SideMenu.jsx +209 -0
  51. jettask/webui/frontend/src/components/layout/TabsNav.css +244 -0
  52. jettask/webui/frontend/src/components/layout/TabsNav.jsx +206 -0
  53. jettask/webui/frontend/src/components/layout/UserInfo.css +197 -0
  54. jettask/webui/frontend/src/components/layout/UserInfo.jsx +197 -0
  55. jettask/webui/frontend/src/contexts/NamespaceContext.jsx +72 -0
  56. jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +245 -0
  57. jettask/webui/frontend/src/main.jsx +1 -0
  58. jettask/webui/frontend/src/pages/Alerts.jsx +684 -0
  59. jettask/webui/frontend/src/pages/Dashboard.jsx +1330 -0
  60. jettask/webui/frontend/src/pages/QueueDetail.jsx +1109 -10
  61. jettask/webui/frontend/src/pages/QueueMonitor.jsx +236 -115
  62. jettask/webui/frontend/src/pages/Queues.jsx +5 -1
  63. jettask/webui/frontend/src/pages/ScheduledTasks.jsx +809 -0
  64. jettask/webui/frontend/src/pages/Settings.jsx +800 -0
  65. jettask/webui/frontend/src/services/api.js +7 -5
  66. jettask/webui/frontend/src/utils/suppressWarnings.js +22 -0
  67. jettask/webui/frontend/src/utils/userPreferences.js +154 -0
  68. jettask/webui/multi_namespace_consumer.py +543 -0
  69. jettask/webui/pg_consumer.py +983 -246
  70. jettask/webui/static/dist/assets/index-7129cfe1.css +1 -0
  71. jettask/webui/static/dist/assets/index-8d1935cc.js +774 -0
  72. jettask/webui/static/dist/index.html +2 -2
  73. jettask/webui/task_center.py +216 -0
  74. jettask/webui/task_center_client.py +150 -0
  75. jettask/webui/unified_consumer_manager.py +193 -0
  76. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/METADATA +1 -1
  77. jettask-0.2.4.dist-info/RECORD +134 -0
  78. jettask/webui/pg_consumer_slow.py +0 -1099
  79. jettask/webui/pg_consumer_test.py +0 -678
  80. jettask/webui/static/dist/assets/index-823408e8.css +0 -1
  81. jettask/webui/static/dist/assets/index-9968b0b8.js +0 -543
  82. jettask/webui/test_pg_consumer_recovery.py +0 -547
  83. jettask/webui/test_recovery_simple.py +0 -492
  84. jettask/webui/test_self_recovery.py +0 -467
  85. jettask-0.2.1.dist-info/RECORD +0 -91
  86. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/WHEEL +0 -0
  87. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/entry_points.txt +0 -0
  88. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/licenses/LICENSE +0 -0
  89. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,244 @@
1
+ /* 标签导航容器 */
2
+ .tabs-nav-container {
3
+ flex: 1;
4
+ height: 100%;
5
+ display: flex;
6
+ align-items: center;
7
+ padding: 0;
8
+ padding-right: 8px;
9
+ overflow: hidden;
10
+ position: relative;
11
+ }
12
+
13
+ /* 标签样式 - 扁平化设计 */
14
+ .tabs-nav {
15
+ height: 100%;
16
+ width: 100%;
17
+ }
18
+
19
+ .tabs-nav .ant-tabs-nav {
20
+ height: 100%;
21
+ margin: 0;
22
+ background: transparent;
23
+ border: none;
24
+ display: flex;
25
+ align-items: center;
26
+ }
27
+
28
+ .tabs-nav .ant-tabs-nav::before {
29
+ border: none;
30
+ }
31
+
32
+ .tabs-nav .ant-tabs-nav-wrap {
33
+ display: flex;
34
+ align-items: center;
35
+ height: auto;
36
+ }
37
+
38
+ .tabs-nav .ant-tabs-nav-list {
39
+ height: 48px;
40
+ display: flex;
41
+ align-items: flex-end;
42
+ gap: 2px;
43
+ padding-top: 8px;
44
+ }
45
+
46
+ .tabs-nav .ant-tabs-tab {
47
+ background: transparent;
48
+ border: none;
49
+ border-radius: 4px 4px 0 0;
50
+ margin: 0;
51
+ padding: 0 12px;
52
+ height: 40px;
53
+ line-height: 40px;
54
+ min-width: 90px;
55
+ color: rgba(255, 255, 255, 0.65);
56
+ transition: all 0.2s;
57
+ }
58
+
59
+ /* 第一个标签特殊处理 - 左侧无圆角 */
60
+ .tabs-nav .ant-tabs-tab:first-child {
61
+ border-top-left-radius: 0;
62
+ padding-left: 16px;
63
+ }
64
+
65
+ .tabs-nav .ant-tabs-tab:hover {
66
+ background: rgba(255, 255, 255, 0.08);
67
+ color: rgba(255, 255, 255, 0.85);
68
+ }
69
+
70
+ .tabs-nav .ant-tabs-tab-active {
71
+ background: #f0f2f5;
72
+ color: #262626;
73
+ position: relative;
74
+ border-left: 1px solid #d9d9d9;
75
+ border-top: 1px solid #d9d9d9;
76
+ border-right: 1px solid #d9d9d9;
77
+ border-bottom: 1px solid #f0f2f5;
78
+ font-weight: 500;
79
+ z-index: 20;
80
+ height: 41px;
81
+ padding: 0 12px;
82
+ margin-bottom: -1px;
83
+ }
84
+
85
+ /* 第一个激活标签左侧无边框 */
86
+ .tabs-nav .ant-tabs-tab-active:first-child {
87
+ padding-left: 16px;
88
+ border-left: none;
89
+ }
90
+
91
+ .tabs-nav .ant-tabs-tab-active:hover {
92
+ background: #f0f2f5;
93
+ color: #262626;
94
+ }
95
+
96
+ .tabs-nav .ant-tabs-tab-btn {
97
+ color: inherit;
98
+ }
99
+
100
+ /* 标签内容样式 */
101
+ .tab-label {
102
+ display: inline-flex;
103
+ align-items: center;
104
+ gap: 4px;
105
+ user-select: none;
106
+ font-size: 13px;
107
+ white-space: nowrap;
108
+ line-height: 1;
109
+ }
110
+
111
+ /* 概览标签特殊样式 - 居中显示 */
112
+ .tab-label-center {
113
+ justify-content: center;
114
+ width: 100%;
115
+ text-align: center;
116
+ }
117
+
118
+ .tab-icon {
119
+ display: inline-flex;
120
+ align-items: center;
121
+ font-size: 12px;
122
+ opacity: 0.8;
123
+ }
124
+
125
+ /* 关闭按钮样式 */
126
+ .tabs-nav .ant-tabs-tab-remove {
127
+ margin-left: 8px;
128
+ margin-right: -6px;
129
+ color: rgba(255, 255, 255, 0.45);
130
+ font-size: 10px;
131
+ transition: all 0.2s;
132
+ padding: 0;
133
+ display: inline-flex;
134
+ align-items: center;
135
+ justify-content: center;
136
+ width: 12px;
137
+ height: 12px;
138
+ opacity: 0;
139
+ visibility: hidden;
140
+ }
141
+
142
+ /* 鼠标悬停在标签上时显示关闭按钮 */
143
+ .tabs-nav .ant-tabs-tab:hover .ant-tabs-tab-remove {
144
+ opacity: 1;
145
+ visibility: visible;
146
+ }
147
+
148
+ .tabs-nav .ant-tabs-tab-remove:hover {
149
+ color: rgba(255, 255, 255, 0.85);
150
+ background: rgba(255, 255, 255, 0.1);
151
+ border-radius: 2px;
152
+ }
153
+
154
+ /* 激活标签的关闭按钮 - 始终显示 */
155
+ .tabs-nav .ant-tabs-tab-active .ant-tabs-tab-remove {
156
+ color: rgba(0, 0, 0, 0.45);
157
+ opacity: 1;
158
+ visibility: visible;
159
+ }
160
+
161
+ .tabs-nav .ant-tabs-tab-active .ant-tabs-tab-remove:hover {
162
+ color: rgba(0, 0, 0, 0.65);
163
+ background: rgba(0, 0, 0, 0.06);
164
+ }
165
+
166
+ /* 禁用Ant Design的卡片样式 */
167
+ .tabs-nav.ant-tabs-card > .ant-tabs-nav .ant-tabs-tab,
168
+ .tabs-nav.ant-tabs-editable-card > .ant-tabs-nav .ant-tabs-tab {
169
+ background: transparent;
170
+ border: none;
171
+ }
172
+
173
+ .tabs-nav.ant-tabs-card > .ant-tabs-nav .ant-tabs-tab-active,
174
+ .tabs-nav.ant-tabs-editable-card > .ant-tabs-nav .ant-tabs-tab-active {
175
+ background: #f0f2f5;
176
+ border-left: 1px solid #d9d9d9;
177
+ border-top: 1px solid #d9d9d9;
178
+ border-right: 1px solid #d9d9d9;
179
+ border-bottom: 1px solid #f0f2f5;
180
+ margin-bottom: -1px;
181
+ }
182
+
183
+ /* 导航操作按钮 */
184
+ .tabs-nav .ant-tabs-nav-operations {
185
+ display: none;
186
+ }
187
+
188
+ /* 标签ink bar隐藏 */
189
+ .tabs-nav .ant-tabs-ink-bar {
190
+ display: none;
191
+ }
192
+
193
+ /* 滚动条样式 */
194
+ .tabs-nav .ant-tabs-nav-wrap {
195
+ overflow: hidden;
196
+ }
197
+
198
+ .tabs-nav .ant-tabs-nav-list {
199
+ transition: transform 0.3s;
200
+ }
201
+
202
+ /* 上下文菜单样式 */
203
+ .ant-dropdown-menu {
204
+ min-width: 120px;
205
+ }
206
+
207
+ .ant-dropdown-menu-item {
208
+ padding: 8px 12px;
209
+ }
210
+
211
+ .ant-dropdown-menu-item .anticon {
212
+ margin-right: 8px;
213
+ }
214
+
215
+ /* 移除多余的边距和填充 */
216
+ .tabs-nav.ant-tabs {
217
+ line-height: 1;
218
+ }
219
+
220
+ .tabs-nav .ant-tabs-content-holder {
221
+ display: none;
222
+ }
223
+
224
+ /* 标签分隔线 - 仅非激活标签显示 */
225
+ .tabs-nav .ant-tabs-tab {
226
+ position: relative;
227
+ }
228
+
229
+ .tabs-nav .ant-tabs-tab:not(.ant-tabs-tab-active)::after {
230
+ content: '';
231
+ position: absolute;
232
+ top: 50%;
233
+ right: 0;
234
+ transform: translateY(-50%);
235
+ width: 1px;
236
+ height: 14px;
237
+ background: rgba(255, 255, 255, 0.08);
238
+ }
239
+
240
+ .tabs-nav .ant-tabs-tab:last-child::after,
241
+ .tabs-nav .ant-tabs-tab-active::after,
242
+ .tabs-nav .ant-tabs-tab-active + .ant-tabs-tab::after {
243
+ display: none;
244
+ }
@@ -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
+ }