@qlover/create-app 0.3.2 → 0.3.4

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. package/CHANGELOG.md +139 -0
  2. package/package.json +4 -4
  3. package/templates/react-app/README.md +311 -120
  4. package/templates/react-app/config/Identifier.I18n.ts +1048 -0
  5. package/templates/react-app/config/app.router.json +7 -7
  6. package/templates/react-app/config/common.ts +13 -0
  7. package/templates/react-app/config/theme.json +7 -88
  8. package/templates/react-app/package.json +11 -5
  9. package/templates/react-app/postcss.config.js +1 -2
  10. package/templates/react-app/public/locales/en/common.json +142 -1
  11. package/templates/react-app/public/locales/zh/common.json +142 -1
  12. package/templates/react-app/src/App.tsx +16 -3
  13. package/templates/react-app/src/base/apis/AiApi.ts +4 -4
  14. package/templates/react-app/src/base/apis/feApi/FeApiAdapter.ts +2 -2
  15. package/templates/react-app/src/base/apis/userApi/UserApiAdapter.ts +2 -2
  16. package/templates/react-app/src/base/cases/AppConfig.ts +103 -0
  17. package/templates/react-app/src/base/cases/{appError/AppError.ts → AppError.ts} +0 -3
  18. package/templates/react-app/src/base/cases/DialogHandler.ts +86 -0
  19. package/templates/react-app/src/base/cases/RequestLogger.ts +1 -1
  20. package/templates/react-app/src/base/cases/RouterLoader.ts +166 -0
  21. package/templates/react-app/src/base/port/InteractionHubInterface.ts +94 -0
  22. package/templates/react-app/src/base/services/I18nService.ts +50 -3
  23. package/templates/react-app/src/base/services/ProcesserService.ts +0 -1
  24. package/templates/react-app/src/base/types/deprecated-antd.d.ts +60 -0
  25. package/templates/react-app/src/core/IOC.ts +18 -8
  26. package/templates/react-app/src/core/bootstrap.ts +41 -62
  27. package/templates/react-app/src/core/bootstraps/PrintBootstrap.ts +14 -0
  28. package/templates/react-app/src/core/bootstraps/index.ts +36 -7
  29. package/templates/react-app/src/core/globals.ts +8 -1
  30. package/templates/react-app/src/core/registers/RegisterApi.ts +2 -5
  31. package/templates/react-app/src/core/registers/RegisterCommon.ts +38 -29
  32. package/templates/react-app/src/core/registers/RegisterControllers.ts +5 -10
  33. package/templates/react-app/src/core/registers/RegisterGlobals.ts +21 -17
  34. package/templates/react-app/src/core/registers/index.ts +27 -13
  35. package/templates/react-app/src/main.tsx +1 -1
  36. package/templates/react-app/src/pages/404.tsx +1 -1
  37. package/templates/react-app/src/pages/500.tsx +1 -1
  38. package/templates/react-app/src/pages/auth/Login.tsx +128 -36
  39. package/templates/react-app/src/pages/base/About.tsx +118 -2
  40. package/templates/react-app/src/pages/base/ErrorIdentifier.tsx +38 -19
  41. package/templates/react-app/src/pages/base/Executor.tsx +442 -29
  42. package/templates/react-app/src/pages/base/Home.tsx +99 -93
  43. package/templates/react-app/src/pages/base/JSONStorage.tsx +47 -38
  44. package/templates/react-app/src/pages/base/Layout.tsx +5 -2
  45. package/templates/react-app/src/pages/base/Request.tsx +90 -208
  46. package/templates/react-app/src/pages/base/components/BaseHeader.tsx +13 -5
  47. package/templates/react-app/src/styles/css/antd-themes/_default.css +239 -0
  48. package/templates/react-app/src/styles/css/antd-themes/dark.css +176 -0
  49. package/templates/react-app/src/styles/css/antd-themes/index.css +3 -0
  50. package/templates/react-app/src/styles/css/antd-themes/no-context.css +34 -0
  51. package/templates/react-app/src/styles/css/antd-themes/pink.css +199 -0
  52. package/templates/react-app/src/{uikit/styles → styles}/css/index.css +3 -0
  53. package/templates/react-app/src/styles/css/page.css +11 -0
  54. package/templates/react-app/src/styles/css/tailwind.css +5 -0
  55. package/templates/react-app/src/styles/css/themes/_default.css +29 -0
  56. package/templates/react-app/src/styles/css/themes/dark.css +29 -0
  57. package/templates/react-app/src/styles/css/themes/index.css +3 -0
  58. package/templates/react-app/src/styles/css/themes/pink.css +29 -0
  59. package/templates/react-app/src/uikit/components/LanguageSwitcher.tsx +56 -0
  60. package/templates/react-app/src/uikit/components/Loading.tsx +27 -21
  61. package/templates/react-app/src/uikit/components/RouterRenderComponent.tsx +1 -1
  62. package/templates/react-app/src/uikit/components/ThemeSwitcher.tsx +63 -13
  63. package/templates/react-app/src/uikit/contexts/BaseRouteContext.ts +1 -1
  64. package/templates/react-app/src/uikit/controllers/RouterController.ts +1 -1
  65. package/templates/react-app/src/uikit/controllers/UserController.ts +2 -2
  66. package/templates/react-app/tailwind.config.js +1 -15
  67. package/templates/react-app/tsconfig.json +3 -2
  68. package/templates/react-app/tsconfig.node.json +2 -1
  69. package/templates/react-app/vite.config.ts +15 -3
  70. package/templates/react-app/lib/tailwind/root10px.js +0 -178
  71. package/templates/react-app/lib/tailwind/theme-generator.js +0 -238
  72. package/templates/react-app/public/locales/en/about.json +0 -3
  73. package/templates/react-app/public/locales/en/executor.json +0 -6
  74. package/templates/react-app/public/locales/en/home.json +0 -10
  75. package/templates/react-app/public/locales/en/jsonStorage.json +0 -11
  76. package/templates/react-app/public/locales/en/login.json +0 -7
  77. package/templates/react-app/public/locales/en/request.json +0 -15
  78. package/templates/react-app/public/locales/zh/about.json +0 -3
  79. package/templates/react-app/public/locales/zh/executor.json +0 -6
  80. package/templates/react-app/public/locales/zh/home.json +0 -10
  81. package/templates/react-app/public/locales/zh/jsonStorage.json +0 -11
  82. package/templates/react-app/public/locales/zh/login.json +0 -7
  83. package/templates/react-app/public/locales/zh/request.json +0 -15
  84. package/templates/react-app/src/base/cases/router-loader/index.ts +0 -90
  85. package/templates/react-app/src/base/port/InversifyIocInterface.ts +0 -9
  86. package/templates/react-app/src/core/AppConfig.ts +0 -36
  87. package/templates/react-app/src/uikit/styles/css/page.css +0 -3
  88. package/templates/react-app/src/uikit/styles/css/tailwind.css +0 -3
  89. /package/templates/react-app/config/{ErrorIdentifier.ts → Identifier.Error.ts} +0 -0
@@ -1,10 +1,27 @@
1
- import { IOC } from '@/core/IOC';
1
+ import { Button, Progress, Tag, Space, Card, Input, Select } from 'antd';
2
2
  import { useBaseRoutePage } from '@/uikit/contexts/BaseRouteContext';
3
+ import { useState, useEffect } from 'react';
4
+ import { IOC } from '@/core/IOC';
3
5
  import { JSONStorageController } from '@/uikit/controllers/JSONStorageController';
4
6
  import { ExecutorController } from '@/uikit/controllers/ExecutorController';
5
7
  import { useSliceStore } from '@qlover/slice-store-react';
8
+ import * as i18nKeys from '@config/Identifier.I18n';
9
+
10
+ interface Task {
11
+ id: string;
12
+ name: string;
13
+ status: 'pending' | 'running' | 'completed' | 'failed';
14
+ progress: number;
15
+ type: 'data-sync' | 'report-generation' | 'system-maintenance' | 'backup';
16
+ startTime?: Date;
17
+ endTime?: Date;
18
+ responseType?: 'text' | 'json' | 'blob';
19
+ url?: string;
20
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
21
+ }
6
22
 
7
23
  export default function Executor() {
24
+ const { t } = useBaseRoutePage();
8
25
  const executorController = IOC(ExecutorController);
9
26
  const jSONStorageController = IOC(JSONStorageController);
10
27
  const requestTimeout = useSliceStore(
@@ -17,46 +34,442 @@ export default function Executor() {
17
34
  executorController.selector.helloState
18
35
  );
19
36
 
20
- const { t } = useBaseRoutePage();
37
+ const [tasks, setTasks] = useState<Task[]>([
38
+ {
39
+ id: '1',
40
+ name: t(i18nKeys.PAGE_EXECUTOR_TEST_PLUGIN_TITLE),
41
+ status: 'pending',
42
+ progress: 0,
43
+ type: 'data-sync',
44
+ responseType: 'text',
45
+ url: '/api/v1/executor/test',
46
+ method: 'GET'
47
+ },
48
+ {
49
+ id: '2',
50
+ name: t(i18nKeys.PAGE_EXECUTOR_TASK_TYPE_DATA_SYNC),
51
+ status: 'pending',
52
+ progress: 0,
53
+ type: 'data-sync',
54
+ responseType: 'json',
55
+ url: '/api/v1/data/sync',
56
+ method: 'POST'
57
+ },
58
+ {
59
+ id: '3',
60
+ name: t(i18nKeys.PAGE_EXECUTOR_TASK_TYPE_MAINTENANCE),
61
+ status: 'pending',
62
+ progress: 0,
63
+ type: 'system-maintenance',
64
+ responseType: 'json',
65
+ url: '/api/v1/system/maintenance',
66
+ method: 'PUT'
67
+ },
68
+ {
69
+ id: '4',
70
+ name: t(i18nKeys.PAGE_EXECUTOR_TASK_TYPE_BACKUP),
71
+ status: 'pending',
72
+ progress: 0,
73
+ type: 'backup',
74
+ responseType: 'blob',
75
+ url: '/api/v1/data/backup',
76
+ method: 'GET'
77
+ }
78
+ ]);
79
+
80
+ const [customUrl, setCustomUrl] = useState('');
81
+ const [customMethod, setCustomMethod] = useState<
82
+ 'GET' | 'POST' | 'PUT' | 'DELETE'
83
+ >('GET');
84
+ const [customResponseType, setCustomResponseType] = useState<
85
+ 'text' | 'json' | 'blob'
86
+ >('text');
87
+
88
+ // 监听 helloState 变化,更新任务状态
89
+ useEffect(() => {
90
+ if (helloState.result) {
91
+ IOC('DialogHandler').success(
92
+ t(i18nKeys.PAGE_EXECUTOR_PLUGIN_TEST_SUCCESS)
93
+ );
94
+ // 更新任务状态
95
+ setTasks((prevTasks) =>
96
+ prevTasks.map((task) =>
97
+ task.id === '1'
98
+ ? {
99
+ ...task,
100
+ status: 'completed',
101
+ progress: 100,
102
+ endTime: new Date()
103
+ }
104
+ : task
105
+ )
106
+ );
107
+ } else if (helloState.error) {
108
+ IOC('DialogHandler').error(t(i18nKeys.PAGE_EXECUTOR_PLUGIN_TEST_FAILURE));
109
+ // 更新任务状态
110
+ setTasks((prevTasks) =>
111
+ prevTasks.map((task) =>
112
+ task.id === '1'
113
+ ? { ...task, status: 'failed', endTime: new Date() }
114
+ : task
115
+ )
116
+ );
117
+ }
118
+ }, [helloState.result, helloState.error, t]);
119
+
120
+ const getStatusColor = (status: Task['status']) => {
121
+ switch (status) {
122
+ case 'running':
123
+ return 'active';
124
+ case 'completed':
125
+ return 'success';
126
+ case 'failed':
127
+ return 'exception';
128
+ default:
129
+ return 'normal';
130
+ }
131
+ };
132
+
133
+ const getTypeColor = (type: Task['type']) => {
134
+ switch (type) {
135
+ case 'data-sync':
136
+ return 'blue';
137
+ case 'report-generation':
138
+ return 'green';
139
+ case 'system-maintenance':
140
+ return 'orange';
141
+ case 'backup':
142
+ return 'purple';
143
+ default:
144
+ return 'default';
145
+ }
146
+ };
147
+
148
+ const formatDuration = (startTime?: Date, endTime?: Date) => {
149
+ if (!startTime) return '-';
150
+ const end = endTime || new Date();
151
+ const duration = end.getTime() - startTime.getTime();
152
+ const minutes = Math.floor(duration / (1000 * 60));
153
+ return `${minutes} ${t(i18nKeys.PAGE_EXECUTOR_TASK_DURATION_UNIT)}`;
154
+ };
155
+
156
+ const handleStartTask = async (taskId: string) => {
157
+ const task = tasks.find((t) => t.id === taskId);
158
+ if (!task) return;
159
+
160
+ setTasks((prevTasks) =>
161
+ prevTasks.map((t) =>
162
+ t.id === taskId ? { ...t, status: 'running', startTime: new Date() } : t
163
+ )
164
+ );
165
+
166
+ try {
167
+ await executorController.onTestPlugins();
168
+
169
+ setTasks((prevTasks) =>
170
+ prevTasks.map((t) =>
171
+ t.id === taskId
172
+ ? { ...t, status: 'completed', progress: 100, endTime: new Date() }
173
+ : t
174
+ )
175
+ );
176
+
177
+ IOC('DialogHandler').success(
178
+ t(i18nKeys.PAGE_EXECUTOR_TASK_SUCCESS, { name: task.name })
179
+ );
180
+ } catch {
181
+ setTasks((prevTasks) =>
182
+ prevTasks.map((t) =>
183
+ t.id === taskId ? { ...t, status: 'failed', endTime: new Date() } : t
184
+ )
185
+ );
186
+
187
+ IOC('DialogHandler').error(
188
+ t(i18nKeys.PAGE_EXECUTOR_TASK_FAILURE, { name: task.name })
189
+ );
190
+ }
191
+ };
192
+
193
+ const handleStopTask = (taskId: string) => {
194
+ setTasks((prevTasks) =>
195
+ prevTasks.map((task) =>
196
+ task.id === taskId
197
+ ? { ...task, status: 'failed', endTime: new Date() }
198
+ : task
199
+ )
200
+ );
201
+ };
202
+
203
+ const handleCreateTask = () => {
204
+ if (!customUrl) {
205
+ IOC('DialogHandler').error(
206
+ t(i18nKeys.PAGE_EXECUTOR_CUSTOM_TASK_URL_REQUIRED)
207
+ );
208
+ return;
209
+ }
210
+
211
+ const newTask: Task = {
212
+ id: Date.now().toString(),
213
+ name: t(i18nKeys.PAGE_EXECUTOR_CUSTOM_TASK_NAME, {
214
+ method: customMethod,
215
+ url: customUrl
216
+ }),
217
+ status: 'pending',
218
+ progress: 0,
219
+ type: 'data-sync',
220
+ responseType: customResponseType,
221
+ url: customUrl,
222
+ method: customMethod
223
+ };
224
+
225
+ setTasks((prev) => [...prev, newTask]);
226
+ setCustomUrl('');
227
+ setCustomMethod('GET');
228
+ setCustomResponseType('text');
229
+ };
21
230
 
22
231
  return (
23
- <div className="min-h-screen bg-gray-100 py-6 flex flex-col justify-center sm:py-12">
24
- <div className="relative py-3 sm:max-w-xl sm:mx-auto">
25
- <div className="bg-white shadow-lg rounded-lg px-8 py-6">
26
- <h1 className="text-3xl font-bold text-center text-gray-800 mb-8">
27
- {t('executorDemo')}
28
- </h1>
29
- </div>
30
-
31
- <div className="bg-white shadow-lg rounded-lg px-8 py-6">
32
- {t('requestTimeout')}: {requestTimeout}
33
- </div>
34
- <div className="space-y-6">
35
- <div className="p-6 bg-gray-50 rounded-lg">
36
- <h2 className="text-xl font-semibold text-gray-800 mb-4">
37
- {t('executorTestPlugin')}
38
- </h2>
39
-
40
- <div className="text-gray-800">
232
+ <div className="min-h-screen bg-primary py-8 px-4 sm:px-6 lg:px-8">
233
+ <div className="max-w-6xl mx-auto space-y-6">
234
+ {/* Header Section */}
235
+ <section className="py-8">
236
+ <div className="text-center">
237
+ <h1 className="text-4xl md:text-5xl font-bold mb-6 text-text">
238
+ {t(i18nKeys.PAGE_EXECUTOR_MAIN_TITLE)}
239
+ </h1>
240
+ <p className="text-xl text-text-secondary mb-8">
241
+ {t(i18nKeys.PAGE_EXECUTOR_DESCRIPTION)}
242
+ </p>
243
+ </div>
244
+ </section>
245
+
246
+ {/* Test Plugin Section */}
247
+ <section className="bg-secondary shadow sm:rounded-lg p-6 border border-border">
248
+ <h2 className="text-xl font-medium text-text mb-4">
249
+ {t(i18nKeys.PAGE_EXECUTOR_TEST_PLUGIN_TITLE)}
250
+ </h2>
251
+ <div className="space-y-4">
252
+ <div className="text-text-secondary">
253
+ {t(i18nKeys.PAGE_REQUEST_TIMEOUT)}: {requestTimeout}
254
+ </div>
255
+ <div>
41
256
  {helloState.loading ? (
42
- <div>Loading...</div>
257
+ <div className="text-text-secondary">Loading...</div>
43
258
  ) : (
44
- <button
45
- className="bg-blue-500 px-4 py-2 rounded-md"
259
+ <Button
260
+ type="primary"
46
261
  onClick={executorController.onTestPlugins}
47
262
  >
48
- {t('testPlugin')}
49
- </button>
263
+ {t(i18nKeys.PAGE_EXECUTOR_TEST_PLUGIN_TITLE)}
264
+ </Button>
50
265
  )}
51
-
266
+ </div>
267
+ <div className="bg-elevated p-4 rounded-lg">
52
268
  {helloState.error instanceof Error ? (
53
- <div>{helloState.error.message}</div>
269
+ <div className="text-red-500">{helloState.error.message}</div>
54
270
  ) : (
55
- <div>{IOC('JSON').stringify(helloState.result?.data)}</div>
271
+ <pre className="text-text-secondary">
272
+ {IOC('JSON').stringify(helloState.result?.data)}
273
+ </pre>
56
274
  )}
57
275
  </div>
58
276
  </div>
59
- </div>
277
+ </section>
278
+
279
+ {/* Create Task Section */}
280
+ <section className="bg-secondary shadow sm:rounded-lg p-6 border border-border">
281
+ <h2 className="text-xl font-medium text-text mb-4">
282
+ {t(i18nKeys.PAGE_EXECUTOR_CREATE_TASK_TITLE)}
283
+ </h2>
284
+ <div className="space-y-4">
285
+ <div className="flex items-center space-x-4">
286
+ <Input
287
+ placeholder={t(i18nKeys.PAGE_EXECUTOR_ENTER_URL)}
288
+ value={customUrl}
289
+ onChange={(e) => setCustomUrl(e.target.value)}
290
+ className="flex-1"
291
+ />
292
+ <Select
293
+ value={customMethod}
294
+ onChange={setCustomMethod}
295
+ style={{ width: 120 }}
296
+ >
297
+ <Select.Option value="GET">GET</Select.Option>
298
+ <Select.Option value="POST">POST</Select.Option>
299
+ <Select.Option value="PUT">PUT</Select.Option>
300
+ <Select.Option value="DELETE">DELETE</Select.Option>
301
+ </Select>
302
+ <Select
303
+ value={customResponseType}
304
+ onChange={setCustomResponseType}
305
+ style={{ width: 120 }}
306
+ >
307
+ <Select.Option value="text">Text</Select.Option>
308
+ <Select.Option value="json">JSON</Select.Option>
309
+ <Select.Option value="blob">Blob</Select.Option>
310
+ </Select>
311
+ <Button type="primary" onClick={handleCreateTask}>
312
+ {t(i18nKeys.PAGE_EXECUTOR_CREATE_BUTTON)}
313
+ </Button>
314
+ </div>
315
+ </div>
316
+ </section>
317
+
318
+ {/* Task Statistics */}
319
+ <section className="grid grid-cols-1 md:grid-cols-4 gap-4">
320
+ <Card className="bg-elevated border-border">
321
+ <div className="text-center">
322
+ <div className="text-2xl font-bold text-text">{tasks.length}</div>
323
+ <div className="text-sm text-text-secondary">
324
+ {t(i18nKeys.PAGE_EXECUTOR_TASK_STATS_TOTAL)}
325
+ </div>
326
+ </div>
327
+ </Card>
328
+ <Card className="bg-elevated border-border">
329
+ <div className="text-center">
330
+ <div className="text-2xl font-bold text-text">
331
+ {tasks.filter((t) => t.status === 'running').length}
332
+ </div>
333
+ <div className="text-sm text-text-secondary">
334
+ {t(i18nKeys.PAGE_EXECUTOR_TASK_STATS_RUNNING)}
335
+ </div>
336
+ </div>
337
+ </Card>
338
+ <Card className="bg-elevated border-border">
339
+ <div className="text-center">
340
+ <div className="text-2xl font-bold text-text">
341
+ {tasks.filter((t) => t.status === 'completed').length}
342
+ </div>
343
+ <div className="text-sm text-text-secondary">
344
+ {t(i18nKeys.PAGE_EXECUTOR_TASK_STATS_COMPLETED)}
345
+ </div>
346
+ </div>
347
+ </Card>
348
+ <Card className="bg-elevated border-border">
349
+ <div className="text-center">
350
+ <div className="text-2xl font-bold text-text">
351
+ {tasks.filter((t) => t.status === 'failed').length}
352
+ </div>
353
+ <div className="text-sm text-text-secondary">
354
+ {t(i18nKeys.PAGE_EXECUTOR_TASK_STATS_FAILED)}
355
+ </div>
356
+ </div>
357
+ </Card>
358
+ </section>
359
+
360
+ {/* Task List Section */}
361
+ <section className="bg-secondary shadow sm:rounded-lg p-6 border border-border">
362
+ <h2 className="text-xl font-medium text-text mb-4">
363
+ {t(i18nKeys.PAGE_EXECUTOR_TASK_LIST_TITLE)}
364
+ </h2>
365
+ <div className="space-y-4">
366
+ {tasks.map((task) => (
367
+ <Card
368
+ key={task.id}
369
+ className="bg-elevated border-border"
370
+ title={
371
+ <div className="flex items-center justify-between">
372
+ <span className="text-text">{task.name}</span>
373
+ <Space>
374
+ <Tag color={getTypeColor(task.type)}>{task.type}</Tag>
375
+ <Tag color={getStatusColor(task.status)}>
376
+ {task.status}
377
+ </Tag>
378
+ </Space>
379
+ </div>
380
+ }
381
+ >
382
+ <div className="space-y-4">
383
+ <div className="grid grid-cols-2 gap-4 text-text-secondary">
384
+ <div>
385
+ <span className="font-medium">URL:</span> {task.url}
386
+ </div>
387
+ <div>
388
+ <span className="font-medium">Method:</span> {task.method}
389
+ </div>
390
+ <div>
391
+ <span className="font-medium">Response Type:</span>{' '}
392
+ {task.responseType}
393
+ </div>
394
+ <div>
395
+ <span className="font-medium">Duration:</span>{' '}
396
+ {formatDuration(task.startTime, task.endTime)}
397
+ </div>
398
+ </div>
399
+ <Progress
400
+ percent={task.progress}
401
+ status={getStatusColor(task.status)}
402
+ />
403
+ <div className="flex justify-end space-x-2">
404
+ {task.status === 'pending' && (
405
+ <Button
406
+ type="primary"
407
+ onClick={() => handleStartTask(task.id)}
408
+ >
409
+ {t(i18nKeys.PAGE_EXECUTOR_TASK_START)}
410
+ </Button>
411
+ )}
412
+ {task.status === 'running' && (
413
+ <Button
414
+ type="primary"
415
+ danger
416
+ onClick={() => handleStopTask(task.id)}
417
+ >
418
+ {t(i18nKeys.PAGE_EXECUTOR_TASK_STOP)}
419
+ </Button>
420
+ )}
421
+ </div>
422
+ </div>
423
+ </Card>
424
+ ))}
425
+ </div>
426
+ </section>
427
+
428
+ {/* Task History */}
429
+ <section className="bg-secondary shadow sm:rounded-lg p-6 border border-border">
430
+ <h2 className="text-xl font-medium text-text mb-4">
431
+ {t(i18nKeys.PAGE_EXECUTOR_TASK_HISTORY_TITLE)}
432
+ </h2>
433
+ <div className="space-y-2">
434
+ {tasks
435
+ .filter(
436
+ (task) =>
437
+ task.status === 'completed' || task.status === 'failed'
438
+ )
439
+ .map((task) => (
440
+ <div
441
+ key={task.id}
442
+ className="flex items-center justify-between p-2 bg-elevated rounded"
443
+ >
444
+ <div className="flex items-center space-x-2">
445
+ <Tag color={getStatusColor(task.status)}>{task.status}</Tag>
446
+ <span className="text-text">{task.name}</span>
447
+ </div>
448
+ <span className="text-sm text-text-secondary">
449
+ {task.endTime?.toLocaleString()}
450
+ </span>
451
+ </div>
452
+ ))}
453
+ </div>
454
+ </section>
455
+
456
+ {/* Call to Action Section */}
457
+ <section className="py-8 text-center">
458
+ <h2 className="text-2xl font-bold mb-4 text-text">
459
+ {t(i18nKeys.PAGE_EXECUTOR_HELP_TITLE)}
460
+ </h2>
461
+ <p className="text-lg text-text-secondary mb-6">
462
+ {t(i18nKeys.PAGE_EXECUTOR_HELP_DESCRIPTION)}
463
+ </p>
464
+ <Space>
465
+ <Button type="primary" size="large">
466
+ {t(i18nKeys.PAGE_EXECUTOR_VIEW_GUIDE)}
467
+ </Button>
468
+ <Button size="large">
469
+ {t(i18nKeys.PAGE_EXECUTOR_CONTACT_SUPPORT)}
470
+ </Button>
471
+ </Space>
472
+ </section>
60
473
  </div>
61
474
  </div>
62
475
  );