import { useState, useEffect, useCallback } from 'react'; import { Card, CardContent } from '@/components/ui/card'; import { StatStrip } from '@/components/ui/stat-strip'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Checkbox } from '@/components/ui/checkbox'; import { Play, Plus, Search, Clock, Pause, Edit, Trash2, History, Loader2, Terminal, FileCode, RefreshCw, PackageOpen, } from 'lucide-react'; import { toast } from 'sonner'; import { cronJobApi } from '@/services/api'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { getStatusBadge, parseCron } from './cronJobHelpers'; import { CronJobFormDialog } from './CronJobFormDialog'; import { ExecutionHistoryDialog } from './ExecutionHistoryDialog'; export function CronJobManager() { const [searchQuery, setSearchQuery] = useState(''); const [jobs, setJobs] = useState([]); const [initialLoading, setInitialLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [isFormDialogOpen, setIsFormDialogOpen] = useState(false); const [isLogDialogOpen, setIsLogDialogOpen] = useState(false); const [executingId, setExecutingId] = useState(null); // editJob=null 表示新建模式;非 null 表示编辑已有任务 // Logan.Li const [editJob, setEditJob] = useState(null); // 批量选择(批量删除) // Logan.Li const [selectedIds, setSelectedIds] = useState([]); const _confirm = useConfirm(); const openCreate = () => { setEditJob(null); setIsFormDialogOpen(true); }; const openEdit = ( job: any) => { setEditJob(job); setIsFormDialogOpen(true); }; // 加载任务列表(双 loading state:首次全屏 spinner,手动刷新只 spin icon) // Logan.Li const refreshData = useCallback(async () => { setRefreshing(true); try { const data = await cronJobApi.getJobs(); setJobs(data.list || []); } catch (error) { console.error('Failed to load cron jobs:', error); toast.error('加载失败'); } finally { setRefreshing(false); setInitialLoading(false); } }, []); useEffect(() => { refreshData(); }, [refreshData]); const filteredJobs = jobs.filter( (job) => (job.name || '').toLowerCase().includes(searchQuery.toLowerCase()) || (job.description || '').toLowerCase().includes(searchQuery.toLowerCase()) ); const handleToggleJob = async ( id: any) => { try { await cronJobApi.toggleJob(id); toast.success('状态已切换'); refreshData(); } catch (error) { console.error('Failed to toggle job:', error); toast.error('操作失败'); } }; const handleExecuteJob = async ( id: any) => { setExecutingId(id); try { const result = await cronJobApi.executeJob(id); const status = result?.status || 'success'; if (status === 'success') { toast.success('执行成功', { description: `耗时 ${result?.duration || 0}ms` }); } else { toast.error('执行失败', { description: (result?.output || '').slice(0, 100) }); } refreshData(); } catch (error) { console.error('Failed to execute job:', error); toast.error('执行失败'); } finally { setExecutingId(null); } }; // 删除任务(带确认框) // Logan.Li const handleDeleteJob = async ( job: any) => { const ok = await _confirm({ title: '删除定时任务', description: `确定删除「${job.name}」吗?此操作不可恢复。`, variant: 'destructive', }); if (!ok) return; try { await cronJobApi.deleteJob(job.id); toast.success('任务已删除'); refreshData(); } catch (error) { console.error('Failed to delete job:', error); toast.error('删除失败'); } }; // 批量选择相关 // Logan.Li const toggleSelect = (id: number) => { setSelectedIds((prev) => prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id] ); }; const toggleSelectAll = () => { if (selectedIds.length === filteredJobs.length && filteredJobs.length > 0) { setSelectedIds([]); } else { setSelectedIds(filteredJobs.map((j) => j.id)); } }; // 批量删除任务(带确认框) // Logan.Li const handleBatchDelete = async () => { if (selectedIds.length === 0) return; const ok = await _confirm({ title: '批量删除定时任务', description: `确定删除选中的 ${selectedIds.length} 个任务吗?此操作不可恢复。`, confirmText: '批量删除', variant: 'destructive', }); if (!ok) return; try { await cronJobApi.batchDelete(selectedIds); toast.success(`已删除 ${selectedIds.length} 个任务`); setSelectedIds([]); refreshData(); } catch (error) { console.error('Failed to batch delete jobs:', error); toast.error('批量删除失败'); } }; return (
{/* Header */}

定时任务

Cron 任务管理与执行

{/* Stats */} j.enabled).length, color: 'emerald' }, { label: '累计成功', value: jobs.reduce((sum, j) => sum + (j.success_count || 0), 0), color: 'emerald' }, { label: '累计失败', value: jobs.reduce((sum, j) => sum + (j.fail_count || 0), 0), color: 'red' }, ]} /> {/* Filters */}
setSearchQuery(e.target.value)} className="pl-10 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
{filteredJobs.length > 0 && ( )}
{selectedIds.length > 0 && (
已选择 {selectedIds.length} 项
)}
{initialLoading ? (
) : filteredJobs.length === 0 ? (

暂无定时任务

) : null} {/* Job List */}
{filteredJobs.map((job) => (
toggleSelect(job.id)} className="border-[var(--dg-border)]" />

{job.name}

{job.enabled ? '已启用' : '已禁用'} {getStatusBadge(job.last_status)} {job.type === 'shell' ? 'Shell' : job.type === 'python' ? 'Python' : '命令'}

{job.description || '—'}

{parseCron(job.cron_expr)} {(job.type === 'shell' || job.type === 'python') && job.script?.name ? ( {job.script.name} ) : job.content && ( {job.content} )}
上次执行: {job.last_run_at ? new Date(job.last_run_at).toLocaleString() : '--'} 成功: {job.success_count || 0} / 失败: {job.fail_count || 0} 累计: {job.run_count || 0} 次
))}
{/* 新建/编辑定时任务弹窗 */} {/* 执行历史弹窗 */}
); }