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
@@ -1,11 +1,109 @@
1
- .app-layout {
2
- min-height: 100vh;
3
- background: transparent;
1
+ /* 全局样式重置 */
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ box-sizing: border-box;
4
6
  }
5
7
 
6
- .main-content {
7
- padding: 24px;
8
- max-width: 1440px;
9
- margin: 0 auto;
8
+ html, body, #root {
9
+ height: 100%;
10
10
  width: 100%;
11
+ overflow: hidden;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
16
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
17
+ sans-serif;
18
+ -webkit-font-smoothing: antialiased;
19
+ -moz-osx-font-smoothing: grayscale;
20
+ }
21
+
22
+ /* 页面容器样式 */
23
+ .page-container {
24
+ padding: 24px;
25
+ height: 100%;
26
+ overflow: auto;
27
+ background: #f0f2f5;
28
+ }
29
+
30
+ /* 页面内容包装器 - 用于需要padding的页面 */
31
+ .page-wrapper {
32
+ padding: 24px;
33
+ height: 100%;
34
+ overflow: auto;
35
+ background: #f0f2f5;
36
+ }
37
+
38
+ .page-header {
39
+ background: #fff;
40
+ padding: 16px 24px;
41
+ border-radius: 4px;
42
+ margin-bottom: 16px;
43
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
44
+ }
45
+
46
+ .page-title {
47
+ margin: 0;
48
+ font-size: 20px;
49
+ font-weight: 500;
50
+ color: #262626;
51
+ }
52
+
53
+ /* 卡片样式优化 */
54
+ .ant-card {
55
+ border-radius: 4px;
56
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
57
+ }
58
+
59
+ /* 表格样式优化 */
60
+ .ant-table {
61
+ font-size: 13px;
62
+ }
63
+
64
+ .ant-table-thead > tr > th {
65
+ background: #fafafa;
66
+ font-weight: 600;
67
+ }
68
+
69
+ /* 按钮样式优化 */
70
+ .ant-btn {
71
+ border-radius: 4px;
72
+ font-weight: 400;
73
+ }
74
+
75
+ /* 滚动条统一样式 */
76
+ ::-webkit-scrollbar {
77
+ width: 8px;
78
+ height: 8px;
79
+ }
80
+
81
+ ::-webkit-scrollbar-track {
82
+ background: #f0f0f0;
83
+ border-radius: 4px;
84
+ }
85
+
86
+ ::-webkit-scrollbar-thumb {
87
+ background: #bfbfbf;
88
+ border-radius: 4px;
89
+ }
90
+
91
+ ::-webkit-scrollbar-thumb:hover {
92
+ background: #8c8c8c;
93
+ }
94
+
95
+ /* 动画效果 */
96
+ @keyframes fadeIn {
97
+ from {
98
+ opacity: 0;
99
+ transform: translateY(10px);
100
+ }
101
+ to {
102
+ opacity: 1;
103
+ transform: translateY(0);
104
+ }
105
+ }
106
+
107
+ .fade-in {
108
+ animation: fadeIn 0.3s ease-in-out;
11
109
  }
@@ -1,34 +1,63 @@
1
- import React from 'react'
2
1
  import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
3
- import { Layout, ConfigProvider } from 'antd'
4
- import Header from './components/layout/Header'
2
+ import { ConfigProvider, theme } from 'antd'
3
+ import zhCN from 'antd/locale/zh_CN'
4
+ import AppLayout from './components/layout/AppLayout'
5
5
  import Dashboard from './pages/Dashboard'
6
6
  import Queues from './pages/Queues'
7
7
  import Workers from './pages/Workers'
8
8
  import QueueDetail from './pages/QueueDetail'
9
+ import ScheduledTasks from './pages/ScheduledTasks'
10
+ import Alerts from './pages/Alerts'
11
+ import Settings from './pages/Settings'
9
12
  import { LoadingProvider } from './contexts/LoadingContext'
13
+ import { NamespaceProvider } from './contexts/NamespaceContext'
10
14
  import './App.css'
11
15
 
12
- const { Content } = Layout
13
-
14
16
  function App() {
17
+ // 自定义主题
18
+ const customTheme = {
19
+ algorithm: theme.defaultAlgorithm,
20
+ token: {
21
+ colorPrimary: '#1890ff',
22
+ borderRadius: 4,
23
+ colorBgContainer: '#ffffff',
24
+ },
25
+ components: {
26
+ Layout: {
27
+ siderBg: '#1a1d21',
28
+ headerBg: '#2a2d31',
29
+ },
30
+ Menu: {
31
+ darkItemBg: 'transparent',
32
+ darkItemSelectedBg: '#1890ff',
33
+ darkItemHoverBg: 'rgba(24, 144, 255, 0.1)',
34
+ },
35
+ },
36
+ }
37
+
15
38
  return (
16
- <ConfigProvider>
39
+ <ConfigProvider theme={customTheme} locale={zhCN}>
17
40
  <LoadingProvider>
18
- <Router>
19
- <Layout className="app-layout">
20
- <Header />
21
- <Content className="main-content">
22
- <Routes>
23
- <Route path="/" element={<Navigate to="/dashboard" replace />} />
24
- <Route path="/dashboard" element={<Dashboard />} />
25
- <Route path="/queues" element={<Queues />} />
26
- <Route path="/queues/:queueName" element={<QueueDetail />} />
27
- <Route path="/workers" element={<Workers />} />
28
- </Routes>
29
- </Content>
30
- </Layout>
31
- </Router>
41
+ <NamespaceProvider>
42
+ <Router>
43
+ <Routes>
44
+ <Route path="/" element={<AppLayout />}>
45
+ <Route index element={<Navigate to="/dashboard" replace />} />
46
+ <Route path="dashboard" element={<Dashboard />} />
47
+ <Route path="queues" element={<Queues />} />
48
+ <Route path="queue/:queueName" element={<QueueDetail />} />
49
+ <Route path="scheduled-tasks" element={<ScheduledTasks />} />
50
+ <Route path="alerts" element={<Alerts />} />
51
+ {/* 新增路由 */}
52
+ <Route path="analytics" element={<div style={{ padding: 24 }}>数据分析页面开发中...</div>} />
53
+ <Route path="performance" element={<div style={{ padding: 24 }}>性能监控页面开发中...</div>} />
54
+ <Route path="logs" element={<div style={{ padding: 24 }}>日志查询页面开发中...</div>} />
55
+ <Route path="api-docs" element={<div style={{ padding: 24 }}>API文档页面开发中...</div>} />
56
+ <Route path="settings" element={<Settings />} />
57
+ </Route>
58
+ </Routes>
59
+ </Router>
60
+ </NamespaceProvider>
32
61
  </LoadingProvider>
33
62
  </ConfigProvider>
34
63
  )
@@ -0,0 +1,166 @@
1
+ /**
2
+ * 命名空间选择器组件
3
+ * 放置在页面顶部,用于切换不同的命名空间
4
+ */
5
+ import React, { useState, useEffect } from 'react';
6
+ import { Select, Space, Tag, Tooltip, message, Button, Dropdown, Modal } from 'antd';
7
+ import { DatabaseOutlined, CloudServerOutlined, SettingOutlined, PlusOutlined, EditOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
8
+ import { useNavigate } from 'react-router-dom';
9
+ import { useNamespace } from '../contexts/NamespaceContext';
10
+
11
+ const { Option } = Select;
12
+
13
+ const NamespaceSelector = ({ value, onChange, style }) => {
14
+ const [namespaces, setNamespaces] = useState([]);
15
+ const [loading, setLoading] = useState(false);
16
+ const navigate = useNavigate();
17
+ const { refreshTrigger } = useNamespace(); // 获取刷新触发器
18
+
19
+ // 获取命名空间列表
20
+ const fetchNamespaces = async () => {
21
+ setLoading(true);
22
+ try {
23
+ const response = await fetch('/api/data/namespaces');
24
+ if (response.ok) {
25
+ const data = await response.json();
26
+ setNamespaces(data);
27
+
28
+ // 如果没有选中的命名空间,默认选中第一个
29
+ if (!value && data.length > 0) {
30
+ const firstNamespace = data[0].name;
31
+ if (onChange) {
32
+ onChange(firstNamespace);
33
+ }
34
+ } else if (data.length === 0) {
35
+ // 如果没有任何命名空间,弹出提示并引导到管理页面
36
+ showNoNamespaceModal();
37
+ }
38
+ } else {
39
+ message.error('获取命名空间列表失败');
40
+ }
41
+ } catch (error) {
42
+ console.error('获取命名空间失败:', error);
43
+ message.error('连接任务中心失败');
44
+ } finally {
45
+ setLoading(false);
46
+ }
47
+ };
48
+
49
+ // 组件挂载时获取命名空间列表,以及当refreshTrigger变化时刷新
50
+ useEffect(() => {
51
+ fetchNamespaces();
52
+ }, [refreshTrigger]); // 监听刷新触发器而不是value
53
+
54
+ // 处理命名空间切换
55
+ const handleNamespaceChange = (namespaceName) => {
56
+ console.log('🔧 NamespaceSelector切换命名空间:', namespaceName);
57
+ console.log('🔧 当前props.value:', value);
58
+ console.log('🔧 onChange函数存在:', !!onChange);
59
+
60
+ if (onChange) {
61
+ onChange(namespaceName);
62
+ console.log('🔧 已调用onChange函数');
63
+ }
64
+ message.success(`已切换到命名空间: ${namespaceName}`);
65
+ };
66
+
67
+ // 显示无命名空间提示弹窗
68
+ const showNoNamespaceModal = () => {
69
+ Modal.confirm({
70
+ title: '暂无可用命名空间',
71
+ icon: <ExclamationCircleOutlined />,
72
+ content: '当前系统中没有配置任何命名空间,请先创建一个命名空间。',
73
+ okText: '去管理命名空间',
74
+ cancelText: '取消',
75
+ onOk: () => {
76
+ navigate('/settings');
77
+ },
78
+ });
79
+ };
80
+
81
+ // 获取命名空间标签颜色
82
+ const getNamespaceTagColor = (name) => {
83
+ if (name === 'default') return 'blue';
84
+ if (name.includes('test')) return 'orange';
85
+ if (name.includes('prod')) return 'green';
86
+ if (name.includes('dev')) return 'purple';
87
+ return 'default';
88
+ };
89
+
90
+
91
+ return (
92
+ <Space style={{ ...style }}>
93
+ <span style={{ color: 'rgba(255, 255, 255, 0.65)', fontSize: '14px' }}>
94
+ <DatabaseOutlined style={{ marginRight: '6px' }} />
95
+ 命名空间:
96
+ </span>
97
+
98
+ <Select
99
+ value={value}
100
+ onChange={handleNamespaceChange}
101
+ loading={loading}
102
+ style={{ minWidth: 200 }}
103
+ placeholder="请选择命名空间"
104
+ showSearch
105
+ optionFilterProp="children"
106
+ filterOption={(input, option) =>
107
+ option.children.props.children[1]?.toLowerCase().includes(input.toLowerCase())
108
+ }
109
+ dropdownRender={(menu) => (
110
+ <>
111
+ {menu}
112
+ <div
113
+ style={{
114
+ padding: '8px 12px',
115
+ textAlign: 'center',
116
+ borderTop: '1px solid #f0f0f0',
117
+ cursor: 'pointer'
118
+ }}
119
+ onClick={(e) => {
120
+ e.stopPropagation();
121
+ e.preventDefault();
122
+ console.log('🔧 点击管理命名空间区域');
123
+ navigate('/settings');
124
+ }}
125
+ >
126
+ <SettingOutlined style={{ marginRight: 4 }} />
127
+ 管理命名空间
128
+ </div>
129
+ </>
130
+ )}
131
+ >
132
+ {namespaces.map(ns => (
133
+ <Option key={ns.name} value={ns.name}>
134
+ <Space>
135
+ <Tag color={getNamespaceTagColor(ns.name)} style={{ margin: 0 }}>
136
+ {ns.name}
137
+ </Tag>
138
+ {ns.description && (
139
+ <Tooltip title={ns.description}>
140
+ <span style={{ color: '#999', fontSize: '12px' }}>
141
+ {ns.description.length > 20
142
+ ? `${ns.description.substring(0, 20)}...`
143
+ : ns.description}
144
+ </span>
145
+ </Tooltip>
146
+ )}
147
+ </Space>
148
+ </Option>
149
+ ))}
150
+ </Select>
151
+
152
+ {value && (
153
+ <Tooltip title={`当前命名空间: ${value}`}>
154
+ <CloudServerOutlined
155
+ style={{
156
+ color: '#52c41a',
157
+ fontSize: '16px'
158
+ }}
159
+ />
160
+ </Tooltip>
161
+ )}
162
+ </Space>
163
+ );
164
+ };
165
+
166
+ export default NamespaceSelector;
@@ -0,0 +1,298 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { Card, Select, Spin, Empty, Space, Button, message } from 'antd';
3
+ import { Line } from '@ant-design/plots';
4
+ import { ReloadOutlined, ExpandOutlined } from '@ant-design/icons';
5
+ import { useNamespace } from '../contexts/NamespaceContext';
6
+ import dayjs from 'dayjs';
7
+ import axios from 'axios';
8
+
9
+ const { Option } = Select;
10
+
11
+ // 时间范围选项
12
+ const TIME_RANGES = {
13
+ '15m': { label: '15分钟', minutes: 15 },
14
+ '30m': { label: '30分钟', minutes: 30 },
15
+ '1h': { label: '1小时', minutes: 60 },
16
+ '3h': { label: '3小时', minutes: 180 },
17
+ '6h': { label: '6小时', minutes: 360 },
18
+ '12h': { label: '12小时', minutes: 720 },
19
+ '24h': { label: '24小时', minutes: 1440 },
20
+ };
21
+
22
+ function QueueBacklogChart({
23
+ height = 300,
24
+ showTitle = true,
25
+ defaultTimeRange = '1h',
26
+ autoRefresh = true,
27
+ refreshInterval = 60000, // 默认60秒刷新一次
28
+ onExpand = null
29
+ }) {
30
+ const { currentNamespace } = useNamespace();
31
+ const [loading, setLoading] = useState(false);
32
+ const [timeRange, setTimeRange] = useState(defaultTimeRange);
33
+ const [selectedQueues, setSelectedQueues] = useState([]);
34
+ const [availableQueues, setAvailableQueues] = useState([]);
35
+ const [chartData, setChartData] = useState([]);
36
+ const [lastUpdateTime, setLastUpdateTime] = useState(null);
37
+
38
+ // 获取可用队列列表
39
+ const fetchAvailableQueues = useCallback(async () => {
40
+ try {
41
+ const namespace = currentNamespace || 'default';
42
+ const response = await axios.get(`/api/queues/${namespace}`);
43
+ if (response.data.success) {
44
+ const queues = response.data.data.map(q => q.name);
45
+ setAvailableQueues(queues);
46
+
47
+ // 默认选择前5个队列
48
+ if (selectedQueues.length === 0 && queues.length > 0) {
49
+ setSelectedQueues(queues.slice(0, 5));
50
+ }
51
+ }
52
+ } catch (error) {
53
+ console.error('Failed to fetch queues:', error);
54
+ }
55
+ }, [currentNamespace, selectedQueues.length]);
56
+
57
+ // 获取队列积压数据
58
+ const fetchBacklogData = useCallback(async () => {
59
+ if (selectedQueues.length === 0) return;
60
+
61
+ setLoading(true);
62
+ try {
63
+ const namespace = currentNamespace || 'default';
64
+ const endTime = dayjs();
65
+ const startTime = endTime.subtract(TIME_RANGES[timeRange].minutes, 'minute');
66
+
67
+ const params = {
68
+ namespace,
69
+ queues: selectedQueues,
70
+ time_range: timeRange,
71
+ start_time: startTime.toISOString(),
72
+ end_time: endTime.toISOString(),
73
+ filters: [] // 不需要额外筛选
74
+ };
75
+
76
+ // 使用队列流量接口获取积压数据
77
+ const response = await axios.post(`/api/data/queue-flow-rates/${namespace}`, params);
78
+
79
+ if (response.data.success) {
80
+ const { data } = response.data;
81
+
82
+ // 转换数据格式,只保留pending数据作为积压量
83
+ const backlogData = data
84
+ .filter(item => item.metric === 'pending')
85
+ .map(item => ({
86
+ time: dayjs(item.time).format('HH:mm'),
87
+ timestamp: item.time,
88
+ queue: item.queue,
89
+ value: item.value || 0,
90
+ type: '积压量'
91
+ }));
92
+
93
+ setChartData(backlogData);
94
+ setLastUpdateTime(dayjs());
95
+ }
96
+ } catch (error) {
97
+ message.error('获取队列积压数据失败');
98
+ console.error('Failed to fetch backlog data:', error);
99
+ } finally {
100
+ setLoading(false);
101
+ }
102
+ }, [currentNamespace, selectedQueues, timeRange]);
103
+
104
+ // 初始化
105
+ useEffect(() => {
106
+ fetchAvailableQueues();
107
+ }, [fetchAvailableQueues]);
108
+
109
+ // 当选中队列变化时,获取数据
110
+ useEffect(() => {
111
+ if (selectedQueues.length > 0) {
112
+ fetchBacklogData();
113
+ }
114
+ }, [selectedQueues, fetchBacklogData]);
115
+
116
+ // 自动刷新
117
+ useEffect(() => {
118
+ if (!autoRefresh || selectedQueues.length === 0) return;
119
+
120
+ const timer = setInterval(() => {
121
+ fetchBacklogData();
122
+ }, refreshInterval);
123
+
124
+ return () => clearInterval(timer);
125
+ }, [autoRefresh, refreshInterval, fetchBacklogData, selectedQueues.length]);
126
+
127
+ // 图表配置
128
+ const config = {
129
+ data: chartData,
130
+ xField: 'time',
131
+ yField: 'value',
132
+ seriesField: 'queue',
133
+ height: height,
134
+ smooth: true,
135
+ animation: {
136
+ appear: {
137
+ animation: 'wave-in',
138
+ duration: 1000,
139
+ },
140
+ },
141
+ xAxis: {
142
+ title: {
143
+ text: '时间',
144
+ style: { fontSize: 12 },
145
+ },
146
+ label: {
147
+ autoRotate: true,
148
+ autoHide: true,
149
+ },
150
+ },
151
+ yAxis: {
152
+ title: {
153
+ text: '积压任务数',
154
+ style: { fontSize: 12 },
155
+ },
156
+ min: 0,
157
+ nice: true,
158
+ },
159
+ legend: {
160
+ position: 'top-right',
161
+ itemSpacing: 10,
162
+ },
163
+ tooltip: {
164
+ shared: true,
165
+ showCrosshairs: true,
166
+ formatter: (datum) => {
167
+ return {
168
+ name: datum.queue,
169
+ value: `${datum.value.toLocaleString()} 个任务`,
170
+ };
171
+ },
172
+ },
173
+ theme: {
174
+ colors10: [
175
+ '#5B8FF9',
176
+ '#5AD8A6',
177
+ '#5D7092',
178
+ '#F6BD16',
179
+ '#E8684A',
180
+ '#6DC8EC',
181
+ '#9270CA',
182
+ '#FF9D4D',
183
+ '#269A99',
184
+ '#FF99C3',
185
+ ],
186
+ },
187
+ // 添加告警线(可选)
188
+ annotations: [
189
+ {
190
+ type: 'line',
191
+ start: ['min', 1000],
192
+ end: ['max', 1000],
193
+ style: {
194
+ stroke: '#ff9800',
195
+ lineDash: [4, 4],
196
+ lineWidth: 1,
197
+ },
198
+ text: {
199
+ content: '警告线 (1000)',
200
+ position: 'end',
201
+ style: {
202
+ textAlign: 'end',
203
+ fontSize: 10,
204
+ fill: '#ff9800',
205
+ },
206
+ offsetY: -5,
207
+ },
208
+ },
209
+ {
210
+ type: 'line',
211
+ start: ['min', 5000],
212
+ end: ['max', 5000],
213
+ style: {
214
+ stroke: '#f44336',
215
+ lineDash: [4, 4],
216
+ lineWidth: 1,
217
+ },
218
+ text: {
219
+ content: '危险线 (5000)',
220
+ position: 'end',
221
+ style: {
222
+ textAlign: 'end',
223
+ fontSize: 10,
224
+ fill: '#f44336',
225
+ },
226
+ offsetY: -5,
227
+ },
228
+ },
229
+ ],
230
+ };
231
+
232
+ return (
233
+ <Card
234
+ title={showTitle ? "队列积压趋势" : null}
235
+ size="small"
236
+ extra={
237
+ <Space>
238
+ {lastUpdateTime && (
239
+ <span style={{ fontSize: 12, color: '#999' }}>
240
+ 更新于 {lastUpdateTime.format('HH:mm:ss')}
241
+ </span>
242
+ )}
243
+ <Select
244
+ value={timeRange}
245
+ onChange={setTimeRange}
246
+ style={{ width: 100 }}
247
+ size="small"
248
+ >
249
+ {Object.entries(TIME_RANGES).map(([key, { label }]) => (
250
+ <Option key={key} value={key}>{label}</Option>
251
+ ))}
252
+ </Select>
253
+ <Select
254
+ mode="multiple"
255
+ placeholder="选择队列"
256
+ value={selectedQueues}
257
+ onChange={setSelectedQueues}
258
+ style={{ minWidth: 200, maxWidth: 400 }}
259
+ size="small"
260
+ maxTagCount={2}
261
+ maxTagTextLength={10}
262
+ >
263
+ {availableQueues.map(queue => (
264
+ <Option key={queue} value={queue}>{queue}</Option>
265
+ ))}
266
+ </Select>
267
+ <Button
268
+ icon={<ReloadOutlined />}
269
+ size="small"
270
+ onClick={fetchBacklogData}
271
+ loading={loading}
272
+ />
273
+ {onExpand && (
274
+ <Button
275
+ icon={<ExpandOutlined />}
276
+ size="small"
277
+ onClick={onExpand}
278
+ title="展开详情"
279
+ />
280
+ )}
281
+ </Space>
282
+ }
283
+ >
284
+ <Spin spinning={loading}>
285
+ {chartData.length > 0 ? (
286
+ <Line {...config} />
287
+ ) : (
288
+ <Empty
289
+ description={selectedQueues.length === 0 ? "请选择要监控的队列" : "暂无数据"}
290
+ style={{ height: height, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column' }}
291
+ />
292
+ )}
293
+ </Spin>
294
+ </Card>
295
+ );
296
+ }
297
+
298
+ export default QueueBacklogChart;