diff --git a/frontend/src/pages/Deploy/ScheduleJob/List/index.tsx b/frontend/src/pages/Deploy/ScheduleJob/List/index.tsx index 68b5443d..874a485f 100644 --- a/frontend/src/pages/Deploy/ScheduleJob/List/index.tsx +++ b/frontend/src/pages/Deploy/ScheduleJob/List/index.tsx @@ -1,34 +1,34 @@ -import React, { useState, useEffect, useMemo } from 'react'; -import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; -import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table'; -import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { DataTablePagination } from '@/components/ui/pagination'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import React, {useState, useEffect, useMemo} from 'react'; +import {Card, CardHeader, CardTitle, CardContent} from '@/components/ui/card'; +import {Table, TableHeader, TableBody, TableRow, TableHead, TableCell} from '@/components/ui/table'; +import {Badge} from '@/components/ui/badge'; +import {Button} from '@/components/ui/button'; +import {Input} from '@/components/ui/input'; +import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select'; +import {DataTablePagination} from '@/components/ui/pagination'; +import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs'; import { - Loader2, Plus, Search, Edit, Trash2, Play, Pause, - Clock, Activity, CheckCircle2, XCircle, FolderKanban, PlayCircle, FileText, BarChart3, List + Loader2, Plus, Search, Edit, Trash2, Play, Pause, + Clock, Activity, CheckCircle2, XCircle, FolderKanban, PlayCircle, FileText, BarChart3, List, Ban } from 'lucide-react'; -import { useToast } from '@/components/ui/use-toast'; -import { getScheduleJobs, getJobCategoryList, startJob, pauseJob, resumeJob, stopJob, triggerJob, deleteScheduleJob } from './service'; -import type { ScheduleJobResponse, ScheduleJobQuery, JobCategoryResponse, JobStatus } from './types'; -import type { Page } from '@/types/base'; -import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page'; +import {useToast} from '@/components/ui/use-toast'; +import {getScheduleJobs, getJobCategoryList, startJob, pauseJob, resumeJob, stopJob, triggerJob, disableJob, deleteScheduleJob, enableJob} from './service'; +import type {ScheduleJobResponse, ScheduleJobQuery, JobCategoryResponse, JobStatus} from './types'; +import type {Page} from '@/types/base'; +import {DEFAULT_PAGE_SIZE, DEFAULT_CURRENT} from '@/utils/page'; import CategoryManageDialog from './components/CategoryManageDialog'; import JobLogDialog from './components/JobLogDialog'; import JobEditDialog from './components/JobEditDialog'; import Dashboard from './components/Dashboard'; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, } from '@/components/ui/alert-dialog'; import dayjs from 'dayjs'; @@ -36,598 +36,678 @@ import dayjs from 'dayjs'; * 定时任务列表页 */ const ScheduleJobList: React.FC = () => { - const { toast } = useToast(); - const [loading, setLoading] = useState(false); - const [data, setData] = useState | null>(null); - const [categories, setCategories] = useState([]); - const [categoryDialogOpen, setCategoryDialogOpen] = useState(false); - const [logDialogOpen, setLogDialogOpen] = useState(false); - const [editDialogOpen, setEditDialogOpen] = useState(false); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [selectedJob, setSelectedJob] = useState(null); - const [editJob, setEditJob] = useState(null); - const [deleteJob, setDeleteJob] = useState(null); - const [query, setQuery] = useState({ - pageNum: DEFAULT_CURRENT - 1, - pageSize: DEFAULT_PAGE_SIZE, - jobName: '', - categoryId: undefined, - status: undefined - }); - - // 加载数据 - const loadData = async () => { - setLoading(true); - try { - const result = await getScheduleJobs(query); - setData(result); - } catch (error) { - console.error('加载定时任务失败:', error); - } finally { - setLoading(false); - } - }; - - // 加载分类 - const loadCategories = async () => { - try { - const result = await getJobCategoryList(); - setCategories(result || []); - } catch (error) { - console.error('加载分类失败:', error); - } - }; - - useEffect(() => { - loadCategories(); - }, []); - - useEffect(() => { - loadData(); - }, [query]); - - // 搜索 - const handleSearch = () => { - setQuery(prev => ({ ...prev, pageNum: 0 })); - }; - - // 重置 - const handleReset = () => { - setQuery({ - pageNum: 0, - pageSize: DEFAULT_PAGE_SIZE, - jobName: '', - categoryId: undefined, - status: undefined + const {toast} = useToast(); + const [loading, setLoading] = useState(false); + const [data, setData] = useState | null>(null); + const [categories, setCategories] = useState([]); + const [categoryDialogOpen, setCategoryDialogOpen] = useState(false); + const [logDialogOpen, setLogDialogOpen] = useState(false); + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [selectedJob, setSelectedJob] = useState(null); + const [editJob, setEditJob] = useState(null); + const [deleteJob, setDeleteJob] = useState(null); + const [query, setQuery] = useState({ + pageNum: DEFAULT_CURRENT - 1, + pageSize: DEFAULT_PAGE_SIZE, + jobName: '', + categoryId: undefined, + status: undefined }); - }; - // 新建任务 - const handleCreate = () => { - setEditJob(null); - setEditDialogOpen(true); - }; - - // 编辑任务 - const handleEdit = (record: ScheduleJobResponse) => { - setEditJob(record); - setEditDialogOpen(true); - }; - - // 打开删除确认对话框 - const handleDeleteClick = (record: ScheduleJobResponse) => { - setDeleteJob(record); - setDeleteDialogOpen(true); - }; - - // 确认删除 - const confirmDelete = async () => { - if (!deleteJob) return; - try { - await deleteScheduleJob(deleteJob.id); - toast({ - title: '删除成功', - description: `任务 "${deleteJob.jobName}" 已删除`, - }); - loadData(); - setDeleteDialogOpen(false); - setDeleteJob(null); - } catch (error) { - toast({ - variant: 'destructive', - title: '删除失败', - description: error instanceof Error ? error.message : '未知错误', - }); - } - }; - - // 启动任务 - const handleStart = async (record: ScheduleJobResponse) => { - try { - await startJob(record.id); - toast({ - title: '启动成功', - description: `任务 "${record.jobName}" 已启动`, - }); - loadData(); - } catch (error) { - console.error('启动失败:', error); - toast({ - variant: 'destructive', - title: '启动失败', - description: error instanceof Error ? error.message : '未知错误', - }); - } - }; - - // 暂停任务 - const handlePause = async (record: ScheduleJobResponse) => { - try { - await pauseJob(record.id); - toast({ - title: '暂停成功', - description: `任务 "${record.jobName}" 已暂停`, - }); - loadData(); - } catch (error) { - console.error('暂停失败:', error); - toast({ - variant: 'destructive', - title: '暂停失败', - description: error instanceof Error ? error.message : '未知错误', - }); - } - }; - - // 恢复任务 - const handleResume = async (record: ScheduleJobResponse) => { - try { - await resumeJob(record.id); - toast({ - title: '恢复成功', - description: `任务 "${record.jobName}" 已恢复`, - }); - loadData(); - } catch (error) { - console.error('恢复失败:', error); - toast({ - variant: 'destructive', - title: '恢复失败', - description: error instanceof Error ? error.message : '未知错误', - }); - } - }; - - // 停止任务 - const handleStop = async (record: ScheduleJobResponse) => { - try { - await stopJob(record.id); - toast({ - title: '停止成功', - description: `任务 "${record.jobName}" 已停止`, - }); - loadData(); - } catch (error) { - console.error('停止失败:', error); - toast({ - variant: 'destructive', - title: '停止失败', - description: error instanceof Error ? error.message : '未知错误', - }); - } - }; - - // 立即触发任务 - const handleTrigger = async (record: ScheduleJobResponse) => { - try { - await triggerJob(record.id); - toast({ - title: '触发成功', - description: `任务 "${record.jobName}" 已立即触发执行`, - }); - loadData(); - } catch (error) { - console.error('触发失败:', error); - toast({ - variant: 'destructive', - title: '触发失败', - description: error instanceof Error ? error.message : '未知错误', - }); - } - }; - - // 查看日志 - const handleViewLog = (record: ScheduleJobResponse) => { - setSelectedJob(record); - setLogDialogOpen(true); - }; - - // 状态徽章 - const getStatusBadge = (status: JobStatus) => { - const statusMap: Record = { - ENABLED: { variant: 'default', text: '启用', icon: CheckCircle2 }, - DISABLED: { variant: 'secondary', text: '禁用', icon: XCircle }, - PAUSED: { variant: 'outline', text: '暂停', icon: Pause }, + // 加载数据 + const loadData = async () => { + setLoading(true); + try { + const result = await getScheduleJobs(query); + setData(result); + } catch (error) { + console.error('加载定时任务失败:', error); + } finally { + setLoading(false); + } }; - const statusInfo = statusMap[status] || { variant: 'outline', text: status, icon: Clock }; - const Icon = statusInfo.icon; + + // 加载分类 + const loadCategories = async () => { + try { + const result = await getJobCategoryList(); + setCategories(result || []); + } catch (error) { + console.error('加载分类失败:', error); + } + }; + + useEffect(() => { + loadCategories(); + }, []); + + useEffect(() => { + loadData(); + }, [query]); + + // 搜索 + const handleSearch = () => { + setQuery(prev => ({...prev, pageNum: 0})); + }; + + // 重置 + const handleReset = () => { + setQuery({ + pageNum: 0, + pageSize: DEFAULT_PAGE_SIZE, + jobName: '', + categoryId: undefined, + status: undefined + }); + }; + + // 新建任务 + const handleCreate = () => { + setEditJob(null); + setEditDialogOpen(true); + }; + + // 编辑任务 + const handleEdit = (record: ScheduleJobResponse) => { + setEditJob(record); + setEditDialogOpen(true); + }; + + // 打开删除确认对话框 + const handleDeleteClick = (record: ScheduleJobResponse) => { + setDeleteJob(record); + setDeleteDialogOpen(true); + }; + + // 确认删除 + const confirmDelete = async () => { + if (!deleteJob) return; + try { + await deleteScheduleJob(deleteJob.id); + toast({ + title: '删除成功', + description: `任务 "${deleteJob.jobName}" 已删除`, + }); + loadData(); + setDeleteDialogOpen(false); + setDeleteJob(null); + } catch (error) { + toast({ + variant: 'destructive', + title: '删除失败', + description: error instanceof Error ? error.message : '未知错误', + }); + } + }; + + // 启动任务 + const handleStart = async (record: ScheduleJobResponse) => { + try { + await startJob(record.id); + toast({ + title: '启动成功', + description: `任务 "${record.jobName}" 已启动`, + }); + loadData(); + } catch (error) { + console.error('启动失败:', error); + toast({ + variant: 'destructive', + title: '启动失败', + description: error instanceof Error ? error.message : '未知错误', + }); + } + }; + + // 暂停任务 + const handlePause = async (record: ScheduleJobResponse) => { + try { + await pauseJob(record.id); + toast({ + title: '暂停成功', + description: `任务 "${record.jobName}" 已暂停`, + }); + loadData(); + } catch (error) { + console.error('暂停失败:', error); + toast({ + variant: 'destructive', + title: '暂停失败', + description: error instanceof Error ? error.message : '未知错误', + }); + } + }; + + // 恢复任务 + const handleResume = async (record: ScheduleJobResponse) => { + try { + await resumeJob(record.id); + toast({ + title: '恢复成功', + description: `任务 "${record.jobName}" 已恢复`, + }); + loadData(); + } catch (error) { + console.error('恢复失败:', error); + toast({ + variant: 'destructive', + title: '恢复失败', + description: error instanceof Error ? error.message : '未知错误', + }); + } + }; + + // 停止任务 + const handleStop = async (record: ScheduleJobResponse) => { + try { + await stopJob(record.id); + toast({ + title: '停止成功', + description: `任务 "${record.jobName}" 已停止`, + }); + loadData(); + } catch (error) { + console.error('停止失败:', error); + toast({ + variant: 'destructive', + title: '停止失败', + description: error instanceof Error ? error.message : '未知错误', + }); + } + }; + + // 立即触发任务 + const handleTrigger = async (record: ScheduleJobResponse) => { + try { + await triggerJob(record.id); + toast({ + title: '触发成功', + description: `任务 "${record.jobName}" 已立即触发执行`, + }); + loadData(); + } catch (error) { + console.error('触发失败:', error); + toast({ + variant: 'destructive', + title: '触发失败', + description: error instanceof Error ? error.message : '未知错误', + }); + } + }; + + // 禁用任务 + const handleDisable = async (record: ScheduleJobResponse) => { + try { + await disableJob(record.id); + toast({ + title: '禁用成功', + description: `任务 "${record.jobName}" 已禁用`, + }); + loadData(); + } catch (error) { + console.error('禁用失败:', error); + toast({ + variant: 'destructive', + title: '禁用失败', + description: error instanceof Error ? error.message : '未知错误', + }); + } + }; + + // 启用(解除禁用)任务 + const handleEnable = async (record: ScheduleJobResponse) => { + try { + await enableJob(record.id); + toast({ + title: '启用成功', + description: `任务 "${record.jobName}" 已启用`, + }); + loadData(); + } catch (error) { + console.error('启用失败:', error); + toast({ + variant: 'destructive', + title: '启用失败', + description: error instanceof Error ? error.message : '未知错误', + }); + } + }; + + // 查看日志 + const handleViewLog = (record: ScheduleJobResponse) => { + setSelectedJob(record); + setLogDialogOpen(true); + }; + + // 状态徽章 + const getStatusBadge = (status: JobStatus) => { + const statusMap: Record = { + ENABLED: {variant: 'default', text: '启用', icon: CheckCircle2}, + DISABLED: {variant: 'secondary', text: '禁用', icon: XCircle}, + PAUSED: {variant: 'outline', text: '暂停', icon: Pause}, + }; + const statusInfo = statusMap[status] || {variant: 'outline', text: status, icon: Clock}; + const Icon = statusInfo.icon; + return ( + + + {statusInfo.text} + + ); + }; + + // 统计数据 + const stats = useMemo(() => { + const total = data?.totalElements || 0; + const enabledCount = data?.content?.filter(d => d.status === 'ENABLED').length || 0; + const pausedCount = data?.content?.filter(d => d.status === 'PAUSED').length || 0; + return {total, enabledCount, pausedCount}; + }, [data]); + return ( - - - {statusInfo.text} - +
+
+

定时任务管理

+

+ 创建和管理定时任务,配置执行计划和参数。 +

+
+ + {/* 视图切换 */} + + + + + 列表视图 + + + + 仪表盘 + + + + {/* 列表视图 */} + + {/* 统计卡片 */} +
+ + + 总任务 + + + +
{stats.total}
+

全部定时任务

+
+
+ + + 运行中 + + + +
{stats.enabledCount}
+

正在运行的任务

+
+
+ + + 已暂停 + + + +
{stats.pausedCount}
+

暂停执行的任务

+
+
+
+ + + + 任务列表 +
+ + +
+
+ + {/* 搜索栏 */} +
+
+ setQuery({...query, jobName: e.target.value})} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + /> +
+ + + + +
+ + {/* 任务列表 */} +
+ + + + 任务名称 + 任务分类 + Cron表达式 + Bean名称 + 方法名称 + 状态 + 上次执行 + 下次执行 + 执行次数 + 成功率 + 操作 + + + + {loading ? ( + + +
+ + 加载中... +
+
+
+ ) : data && data.content.length > 0 ? ( + data.content.map((record) => ( + + {record.jobName} + {record.category?.name || '-'} + + {record.cronExpression} + + + {record.beanName} + + + {record.methodName} + + {getStatusBadge(record.status)} + + {record.lastExecuteTime ? dayjs(record.lastExecuteTime).format('YYYY-MM-DD HH:mm:ss') : '-'} + + + {record.nextExecuteTime ? dayjs(record.nextExecuteTime).format('YYYY-MM-DD HH:mm:ss') : '-'} + + + {record.executeCount || 0} + + + {record.executeCount && record.executeCount > 0 ? ( + = 0.9 ? 'default' : + (record.successCount! / record.executeCount) >= 0.7 ? 'outline' : 'destructive' + }> + {((record.successCount! / record.executeCount) * 100).toFixed(1)}% + + ) : ( + - + )} + + +
+ {/* ENABLED 按钮控制 */} + {record.status === 'ENABLED' && ( + <> + {/* 暂停 */} + + {/* 禁用 */} + + {/* 立即执行 */} + + + )} + {/* PAUSED 按钮控制 */} + {record.status === 'PAUSED' && ( + <> + {/* 恢复 */} + + {/* 禁用 */} + + + )} + {/* DISABLED 按钮控制 */} + {record.status === 'DISABLED' && ( + + )} + {/* 删除按钮 所有状态显示 */} + + + +
+
+
+ )) + ) : ( + + + 暂无数据 + + + )} +
+
+
+ + {/* 分页 */} + {data && data.content.length > 0 && ( +
+ setQuery({...query, pageNum: pageIndex})} + /> +
+ )} +
+
+ + {/* 分类管理对话框 */} + + + {/* 任务日志对话框 */} + + + {/* 任务编辑对话框 */} + + + {/* 删除确认对话框 */} + + + + 确认删除定时任务 + +
+

您确定要删除以下定时任务吗?此操作无法撤销。

+ {deleteJob && ( +
+
+ 任务名称: + {deleteJob.jobName} +
+
+ Bean名称: + + {deleteJob.beanName} + +
+
+ 方法名称: + + {deleteJob.methodName} + +
+ {deleteJob.cronExpression && ( +
+ Cron表达式: + + {deleteJob.cronExpression} + +
+ )} +
+ )} +
+
+
+ + 取消 + + 确认删除 + + +
+
+
+ + {/* 仪表盘视图 */} + + + +
+
); - }; - - // 统计数据 - const stats = useMemo(() => { - const total = data?.totalElements || 0; - const enabledCount = data?.content?.filter(d => d.status === 'ENABLED').length || 0; - const pausedCount = data?.content?.filter(d => d.status === 'PAUSED').length || 0; - return { total, enabledCount, pausedCount }; - }, [data]); - - return ( -
-
-

定时任务管理

-

- 创建和管理定时任务,配置执行计划和参数。 -

-
- - {/* 视图切换 */} - - - - - 列表视图 - - - - 仪表盘 - - - - {/* 列表视图 */} - - {/* 统计卡片 */} -
- - - 总任务 - - - -
{stats.total}
-

全部定时任务

-
-
- - - 运行中 - - - -
{stats.enabledCount}
-

正在运行的任务

-
-
- - - 已暂停 - - - -
{stats.pausedCount}
-

暂停执行的任务

-
-
-
- - - - 任务列表 -
- - -
-
- - {/* 搜索栏 */} -
-
- setQuery({ ...query, jobName: e.target.value })} - onKeyDown={(e) => e.key === 'Enter' && handleSearch()} - /> -
- - - - -
- - {/* 任务列表 */} -
- - - - 任务名称 - 任务分类 - Cron表达式 - Bean名称 - 方法名称 - 状态 - 上次执行 - 下次执行 - 执行次数 - 成功率 - 操作 - - - - {loading ? ( - - -
- - 加载中... -
-
-
- ) : data && data.content.length > 0 ? ( - data.content.map((record) => ( - - {record.jobName} - {record.category?.name || '-'} - - {record.cronExpression} - - - {record.beanName} - - - {record.methodName} - - {getStatusBadge(record.status)} - - {record.lastExecuteTime ? dayjs(record.lastExecuteTime).format('YYYY-MM-DD HH:mm:ss') : '-'} - - - {record.nextExecuteTime ? dayjs(record.nextExecuteTime).format('YYYY-MM-DD HH:mm:ss') : '-'} - - - {record.executeCount || 0} - - - {record.executeCount && record.executeCount > 0 ? ( - = 0.9 ? 'default' : - (record.successCount! / record.executeCount) >= 0.7 ? 'outline' : 'destructive' - }> - {((record.successCount! / record.executeCount) * 100).toFixed(1)}% - - ) : ( - - - )} - - -
- - {record.status === 'ENABLED' && ( - - )} - {record.status === 'PAUSED' && ( - - )} - - - -
-
-
- )) - ) : ( - - - 暂无数据 - - - )} -
-
-
- - {/* 分页 */} - {data && data.content.length > 0 && ( -
- setQuery({ ...query, pageNum: pageIndex })} - /> -
- )} -
-
- - {/* 分类管理对话框 */} - - - {/* 任务日志对话框 */} - - - {/* 任务编辑对话框 */} - - - {/* 删除确认对话框 */} - - - - 确认删除定时任务 - -
-

您确定要删除以下定时任务吗?此操作无法撤销。

- {deleteJob && ( -
-
- 任务名称: - {deleteJob.jobName} -
-
- Bean名称: - - {deleteJob.beanName} - -
-
- 方法名称: - - {deleteJob.methodName} - -
- {deleteJob.cronExpression && ( -
- Cron表达式: - - {deleteJob.cronExpression} - -
- )} -
- )} -
-
-
- - 取消 - - 确认删除 - - -
-
-
- - {/* 仪表盘视图 */} - - - -
-
- ); }; export default ScheduleJobList; diff --git a/frontend/src/pages/Deploy/ScheduleJob/List/service.ts b/frontend/src/pages/Deploy/ScheduleJob/List/service.ts index c81035f5..905708c9 100644 --- a/frontend/src/pages/Deploy/ScheduleJob/List/service.ts +++ b/frontend/src/pages/Deploy/ScheduleJob/List/service.ts @@ -122,6 +122,17 @@ export const stopJob = (id: number) => export const triggerJob = (id: number) => request.post(`${SCHEDULE_JOB_URL}/${id}/trigger`); +/** + * 禁用任务 + */ +export const disableJob = (id: number) => + request.post(`${SCHEDULE_JOB_URL}/${id}/disable`); + +/** + * 启用任务(解除禁用) + */ +export const enableJob = (id: number) => request.post(`${SCHEDULE_JOB_URL}/${id}/enable`); + /** * 更新Cron表达式 */