增加团队管理页面
This commit is contained in:
parent
98cef3f68f
commit
945d827eb8
@ -477,6 +477,12 @@ const GitManager: React.FC = () => {
|
|||||||
{group.name}
|
{group.name}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
{group.projectCount !== undefined && group.projectCount > 0 && (
|
||||||
|
<Badge variant="secondary" className="text-xs px-1.5 py-0 h-5">
|
||||||
|
{group.projectCount}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
{group.webUrl && (
|
{group.webUrl && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@ -739,6 +745,13 @@ const GitManager: React.FC = () => {
|
|||||||
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{project.branchCount !== undefined && project.branchCount > 0 && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<GitBranch className="h-3 w-3" />
|
||||||
|
{project.branchCount} 分支
|
||||||
|
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{project.lastActivityAt && (
|
{project.lastActivityAt && (
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Calendar className="h-3 w-3" />
|
<Calendar className="h-3 w-3" />
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,441 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import {
|
||||||
|
Activity,
|
||||||
|
CheckCircle2,
|
||||||
|
XCircle,
|
||||||
|
Pause,
|
||||||
|
Clock,
|
||||||
|
Loader2,
|
||||||
|
TrendingUp,
|
||||||
|
AlertCircle,
|
||||||
|
Calendar,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { getDashboard, getScheduleJobs } from '../service';
|
||||||
|
import type { DashboardResponse, ScheduleJobResponse, LogStatus } from '../types';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
import 'dayjs/locale/zh-cn';
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
|
const Dashboard: React.FC = () => {
|
||||||
|
const [data, setData] = useState<DashboardResponse | null>(null);
|
||||||
|
const [allJobs, setAllJobs] = useState<ScheduleJobResponse[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// 加载仪表盘数据
|
||||||
|
const loadData = async () => {
|
||||||
|
try {
|
||||||
|
const [dashboardData, jobsData] = await Promise.all([
|
||||||
|
getDashboard(),
|
||||||
|
getScheduleJobs({ pageNum: 0, pageSize: 100 })
|
||||||
|
]);
|
||||||
|
setData(dashboardData);
|
||||||
|
setAllJobs(jobsData?.content || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载仪表盘数据失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始加载和5秒轮询
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
const interval = setInterval(loadData, 5000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 获取日志状态徽章
|
||||||
|
const getLogStatusBadge = (status: LogStatus) => {
|
||||||
|
const statusMap: Record<LogStatus, {
|
||||||
|
variant: 'default' | 'secondary' | 'destructive' | 'success';
|
||||||
|
text: string;
|
||||||
|
icon: React.ElementType;
|
||||||
|
}> = {
|
||||||
|
SUCCESS: { variant: 'success', text: '成功', icon: CheckCircle2 },
|
||||||
|
FAIL: { variant: 'destructive', text: '失败', icon: XCircle },
|
||||||
|
TIMEOUT: { variant: 'secondary', text: '超时', icon: Clock },
|
||||||
|
};
|
||||||
|
const info = statusMap[status];
|
||||||
|
const Icon = info.icon;
|
||||||
|
return (
|
||||||
|
<Badge variant={info.variant} className="inline-flex items-center gap-1">
|
||||||
|
<Icon className="h-3 w-3" />
|
||||||
|
{info.text}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 按状态分组任务
|
||||||
|
const groupedJobs = React.useMemo(() => {
|
||||||
|
return {
|
||||||
|
running: allJobs.filter(j => data?.runningJobs.some(r => r.jobId === j.id)),
|
||||||
|
paused: allJobs.filter(j => j.status === 'PAUSED'),
|
||||||
|
idle: allJobs.filter(j => j.status === 'ENABLED' && !data?.runningJobs.some(r => r.jobId === j.id)),
|
||||||
|
disabled: allJobs.filter(j => j.status === 'DISABLED'),
|
||||||
|
};
|
||||||
|
}, [allJobs, data]);
|
||||||
|
|
||||||
|
// 获取即将执行的任务(按下次执行时间排序)
|
||||||
|
const upcomingJobs = React.useMemo(() => {
|
||||||
|
return allJobs
|
||||||
|
.filter(j => j.nextExecuteTime && j.status === 'ENABLED')
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (!a.nextExecuteTime || !b.nextExecuteTime) return 0;
|
||||||
|
return dayjs(a.nextExecuteTime).valueOf() - dayjs(b.nextExecuteTime).valueOf();
|
||||||
|
})
|
||||||
|
.slice(0, 10);
|
||||||
|
}, [allJobs]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-96">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 任务概览统计卡片 */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-5">
|
||||||
|
<Card className="border-l-4 border-l-blue-500">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">任务总数</CardTitle>
|
||||||
|
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{data?.summary.total || 0}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">所有定时任务</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-l-4 border-l-orange-500">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">正在执行</CardTitle>
|
||||||
|
<Loader2 className="h-4 w-4 text-muted-foreground animate-spin" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{data?.summary.running || 0}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">运行中的任务</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-l-4 border-l-green-500">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">已启用</CardTitle>
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{data?.summary.enabled || 0}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">活跃的任务</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-l-4 border-l-yellow-500">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">已暂停</CardTitle>
|
||||||
|
<Pause className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{data?.summary.paused || 0}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">暂停的任务</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-l-4 border-l-gray-500">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">已禁用</CardTitle>
|
||||||
|
<XCircle className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{data?.summary.disabled || 0}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">停用的任务</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 正在执行的任务 */}
|
||||||
|
{data && data.runningJobs.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Loader2 className="h-5 w-5 animate-spin text-orange-500" />
|
||||||
|
正在执行的任务 ({data.runningJobs.length})
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{data.runningJobs.map((job) => (
|
||||||
|
<div key={job.jobId} className="p-4 border rounded-lg space-y-3">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-lg">{job.jobName}</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
开始时间: {dayjs(job.startTime).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" className="text-orange-600 border-orange-600">
|
||||||
|
{job.progress}%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<Progress value={job.progress} className="h-3" />
|
||||||
|
{job.message && (
|
||||||
|
<p className="text-sm text-muted-foreground flex items-center gap-2">
|
||||||
|
<Activity className="h-4 w-4" />
|
||||||
|
{job.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 多视图 Tabs */}
|
||||||
|
<Tabs defaultValue="timeline" className="space-y-4">
|
||||||
|
<TabsList className="grid w-full grid-cols-3">
|
||||||
|
<TabsTrigger value="timeline">
|
||||||
|
<Calendar className="h-4 w-4 mr-2" />
|
||||||
|
时间线视图
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="status">
|
||||||
|
<TrendingUp className="h-4 w-4 mr-2" />
|
||||||
|
状态分组
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">
|
||||||
|
<AlertCircle className="h-4 w-4 mr-2" />
|
||||||
|
最近日志
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{/* 时间线视图 */}
|
||||||
|
<TabsContent value="timeline">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>任务执行时间线</CardTitle>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
即将执行的任务按时间顺序排列
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{upcomingJobs.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
<Clock className="h-12 w-12 mx-auto mb-2 opacity-20" />
|
||||||
|
<p>暂无即将执行的任务</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="relative pl-8">
|
||||||
|
{/* 时间线 */}
|
||||||
|
<div className="absolute left-3 top-0 bottom-0 w-0.5 bg-border" />
|
||||||
|
|
||||||
|
{upcomingJobs.map((job, index) => (
|
||||||
|
<div key={job.id} className="relative mb-6 last:mb-0">
|
||||||
|
{/* 时间点 */}
|
||||||
|
<div className="absolute -left-[1.65rem] top-1.5 w-3 h-3 rounded-full bg-primary border-2 border-background" />
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="min-w-[80px] text-sm font-medium text-muted-foreground">
|
||||||
|
{dayjs(job.nextExecuteTime).format('HH:mm')}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 p-3 border rounded-lg hover:bg-accent transition-colors">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{job.jobName}</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
{dayjs(job.nextExecuteTime).fromNow()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline">
|
||||||
|
{index === 0 && data?.runningJobs.some(r => r.jobId === job.id) ? (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
|
执行中
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
待执行
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 状态分组视图 */}
|
||||||
|
<TabsContent value="status">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 运行中 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
|
<div className="h-2 w-2 rounded-full bg-orange-500 animate-pulse" />
|
||||||
|
运行中 ({groupedJobs.running.length})
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{groupedJobs.running.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">无正在运行的任务</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{groupedJobs.running.map((job) => {
|
||||||
|
const runningJob = data?.runningJobs.find(r => r.jobId === job.id);
|
||||||
|
return (
|
||||||
|
<div key={job.id} className="flex items-center gap-3 p-2 border rounded">
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="font-medium">{job.jobName}</p>
|
||||||
|
{runningJob && (
|
||||||
|
<Progress value={runningJob.progress} className="h-2 mt-2" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{runningJob && (
|
||||||
|
<Badge variant="outline">{runningJob.progress}%</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 已暂停 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
|
<Pause className="h-4 w-4 text-yellow-600" />
|
||||||
|
已暂停 ({groupedJobs.paused.length})
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{groupedJobs.paused.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">无暂停的任务</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{groupedJobs.paused.map((job) => (
|
||||||
|
<div key={job.id} className="flex items-center justify-between p-2 border rounded">
|
||||||
|
<p className="font-medium">{job.jobName}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
上次: {job.lastExecuteTime ? dayjs(job.lastExecuteTime).fromNow() : '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 空闲中 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
||||||
|
空闲中 ({groupedJobs.idle.length})
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{groupedJobs.idle.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">无空闲任务</p>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-2 md:grid-cols-2">
|
||||||
|
{groupedJobs.idle.map((job) => (
|
||||||
|
<div key={job.id} className="p-2 border rounded">
|
||||||
|
<p className="font-medium truncate">{job.jobName}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
下次: {job.nextExecuteTime ? dayjs(job.nextExecuteTime).format('MM-DD HH:mm') : '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 已禁用 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
|
<XCircle className="h-4 w-4 text-gray-500" />
|
||||||
|
已禁用 ({groupedJobs.disabled.length})
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{groupedJobs.disabled.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">无禁用的任务</p>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-2 md:grid-cols-2">
|
||||||
|
{groupedJobs.disabled.map((job) => (
|
||||||
|
<div key={job.id} className="p-2 border rounded opacity-60">
|
||||||
|
<p className="font-medium truncate">{job.jobName}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">已停用</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 最近日志 */}
|
||||||
|
<TabsContent value="logs">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>最近执行日志 (10条)</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{!data || data.recentLogs.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
<AlertCircle className="h-12 w-12 mx-auto mb-2 opacity-20" />
|
||||||
|
<p>暂无执行日志</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{data.recentLogs.map((log) => (
|
||||||
|
<div key={log.id} className="p-3 border rounded-lg hover:bg-accent transition-colors">
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{log.jobName}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{dayjs(log.executeTime).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{getLogStatusBadge(log.status)}
|
||||||
|
</div>
|
||||||
|
{log.resultMessage && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{log.resultMessage}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{log.exceptionInfo && (
|
||||||
|
<p className="text-sm text-red-600 mt-1">
|
||||||
|
{log.exceptionInfo}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
|
|
||||||
@ -4,37 +4,37 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
DialogBody,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import {
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from '@/components/ui/table';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { DataTablePagination } from '@/components/ui/pagination';
|
import { DataTablePagination } from '@/components/ui/pagination';
|
||||||
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
import {
|
import {
|
||||||
FileText,
|
FileText,
|
||||||
Loader2,
|
Loader2,
|
||||||
Search,
|
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
XCircle,
|
XCircle,
|
||||||
Clock,
|
Clock,
|
||||||
Server,
|
Server,
|
||||||
Calendar,
|
Calendar,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
|
Timer,
|
||||||
|
Code,
|
||||||
|
Info,
|
||||||
|
Activity,
|
||||||
|
RefreshCw,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { getJobLogs } from '../service';
|
import { getJobLogs } from '../service';
|
||||||
import type { ScheduleJobResponse, ScheduleJobLogResponse, ScheduleJobLogQuery, LogStatus } from '../types';
|
import type { ScheduleJobResponse, ScheduleJobLogResponse, ScheduleJobLogQuery, LogStatus } from '../types';
|
||||||
import type { Page } from '@/types/base';
|
import type { Page } from '@/types/base';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
interface JobLogDialogProps {
|
interface JobLogDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -131,193 +131,260 @@ const JobLogDialog: React.FC<JobLogDialogProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-7xl max-h-[90vh] overflow-hidden flex flex-col">
|
<DialogContent className="max-w-7xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<FileText className="h-5 w-5" />
|
<FileText className="h-5 w-5 text-primary" />
|
||||||
执行日志 - {job?.jobName}
|
执行日志 - {job?.jobName}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex-1 overflow-hidden flex flex-col space-y-4">
|
<DialogBody className="space-y-4">
|
||||||
{/* 筛选栏 */}
|
{/* 筛选栏 */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center justify-between">
|
||||||
<Select
|
<div className="flex items-center gap-3">
|
||||||
value={query.status || 'all'}
|
<Select
|
||||||
onValueChange={(value) => setQuery({ ...query, status: value !== 'all' ? value as LogStatus : undefined, pageNum: 0 })}
|
value={query.status || 'all'}
|
||||||
>
|
onValueChange={(value) => setQuery({ ...query, status: value !== 'all' ? value as LogStatus : undefined, pageNum: 0 })}
|
||||||
<SelectTrigger className="w-[150px]">
|
>
|
||||||
<SelectValue placeholder="全部状态" />
|
<SelectTrigger className="w-[160px]">
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="全部状态" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
<SelectItem value="all">全部状态</SelectItem>
|
<SelectContent>
|
||||||
<SelectItem value="SUCCESS">成功</SelectItem>
|
<SelectItem value="all">全部状态</SelectItem>
|
||||||
<SelectItem value="FAIL">失败</SelectItem>
|
<SelectItem value="SUCCESS">✓ 成功</SelectItem>
|
||||||
<SelectItem value="TIMEOUT">超时</SelectItem>
|
<SelectItem value="FAIL">✗ 失败</SelectItem>
|
||||||
</SelectContent>
|
<SelectItem value="TIMEOUT">⏱ 超时</SelectItem>
|
||||||
</Select>
|
</SelectContent>
|
||||||
<Button variant="outline" size="sm" onClick={loadLogs}>
|
</Select>
|
||||||
<Search className="h-4 w-4 mr-2" />
|
</div>
|
||||||
|
<Button variant="outline" size="sm" onClick={loadLogs} disabled={loading}>
|
||||||
|
<RefreshCw className={cn("h-4 w-4 mr-2", loading && "animate-spin")} />
|
||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 日志列表 */}
|
{/* 日志列表 */}
|
||||||
<div className="flex-1 overflow-auto rounded-md border">
|
{loading ? (
|
||||||
<Table>
|
<div className="flex items-center justify-center h-64">
|
||||||
<TableHeader>
|
<div className="text-center">
|
||||||
<TableRow>
|
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-2 text-primary" />
|
||||||
<TableHead className="w-[180px]">执行时间</TableHead>
|
<p className="text-sm text-muted-foreground">加载中...</p>
|
||||||
<TableHead className="w-[180px]">完成时间</TableHead>
|
</div>
|
||||||
<TableHead className="w-[100px]">耗时</TableHead>
|
</div>
|
||||||
<TableHead className="w-[100px]">状态</TableHead>
|
) : data && data.content.length > 0 ? (
|
||||||
<TableHead className="w-[120px]">服务器IP</TableHead>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<TableHead>结果消息</TableHead>
|
{/* 左侧:日志列表 */}
|
||||||
<TableHead className="w-[80px]">操作</TableHead>
|
<div className="space-y-2">
|
||||||
</TableRow>
|
<ScrollArea className="h-[500px] pr-4">
|
||||||
</TableHeader>
|
{data.content.map((log) => (
|
||||||
<TableBody>
|
<Card
|
||||||
{loading ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={7} className="h-32 text-center">
|
|
||||||
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : data && data.content.length > 0 ? (
|
|
||||||
data.content.map((log) => (
|
|
||||||
<TableRow
|
|
||||||
key={log.id}
|
key={log.id}
|
||||||
className={selectedLog?.id === log.id ? 'bg-accent' : ''}
|
className={cn(
|
||||||
|
"mb-3 cursor-pointer transition-all hover:shadow-md",
|
||||||
|
selectedLog?.id === log.id
|
||||||
|
? "border-primary bg-accent"
|
||||||
|
: "hover:border-muted-foreground/30"
|
||||||
|
)}
|
||||||
|
onClick={() => setSelectedLog(log)}
|
||||||
>
|
>
|
||||||
<TableCell className="text-xs">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-start justify-between mb-3">
|
||||||
<Calendar className="h-3 w-3 text-muted-foreground" />
|
<div className="flex items-center gap-2">
|
||||||
{dayjs(log.executeTime).format('YYYY-MM-DD HH:mm:ss')}
|
{getStatusBadge(log.status)}
|
||||||
</div>
|
<span className="text-xs text-muted-foreground font-mono">
|
||||||
</TableCell>
|
{formatDuration(log.duration)}
|
||||||
<TableCell className="text-xs">
|
</span>
|
||||||
{log.finishTime ? dayjs(log.finishTime).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-xs font-mono">
|
|
||||||
{formatDuration(log.duration)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{getStatusBadge(log.status)}</TableCell>
|
|
||||||
<TableCell className="text-xs">
|
|
||||||
{log.serverIp ? (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Server className="h-3 w-3 text-muted-foreground" />
|
|
||||||
{log.serverIp}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||||
'-'
|
<Calendar className="h-3 w-3" />
|
||||||
)}
|
{dayjs(log.executeTime).format('MM-DD HH:mm:ss')}
|
||||||
</TableCell>
|
</div>
|
||||||
<TableCell className="max-w-[300px] truncate text-xs" title={log.resultMessage}>
|
</div>
|
||||||
{log.resultMessage || '-'}
|
|
||||||
</TableCell>
|
<div className="space-y-2">
|
||||||
<TableCell>
|
{log.serverIp && (
|
||||||
<Button
|
<div className="flex items-center gap-2 text-xs">
|
||||||
variant="ghost"
|
<Server className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
size="sm"
|
<span className="font-mono">{log.serverIp}</span>
|
||||||
className="h-7 text-xs"
|
</div>
|
||||||
onClick={() => setSelectedLog(log)}
|
)}
|
||||||
>
|
{log.resultMessage && (
|
||||||
详情
|
<p className="text-xs text-muted-foreground line-clamp-2">
|
||||||
</Button>
|
{log.resultMessage}
|
||||||
</TableCell>
|
</p>
|
||||||
</TableRow>
|
)}
|
||||||
))
|
</div>
|
||||||
) : (
|
</CardContent>
|
||||||
<TableRow>
|
</Card>
|
||||||
<TableCell colSpan={7} className="h-32 text-center text-muted-foreground">
|
))}
|
||||||
暂无执行日志
|
</ScrollArea>
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
{/* 分页 */}
|
||||||
|
{data && data.content.length > 0 && (
|
||||||
|
<DataTablePagination
|
||||||
|
pageIndex={query.pageNum || 0}
|
||||||
|
pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
|
||||||
|
pageCount={data.totalPages}
|
||||||
|
onPageChange={(pageIndex) => setQuery({ ...query, pageNum: pageIndex })}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 分页 */}
|
|
||||||
{data && data.content.length > 0 && (
|
|
||||||
<DataTablePagination
|
|
||||||
pageIndex={query.pageNum || 0}
|
|
||||||
pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
|
|
||||||
pageCount={data.totalPages}
|
|
||||||
onPageChange={(pageIndex) => setQuery({ ...query, pageNum: pageIndex })}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 日志详情 */}
|
|
||||||
{selectedLog && (
|
|
||||||
<div className="border-t pt-4 space-y-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h3 className="text-sm font-semibold flex items-center gap-2">
|
|
||||||
<AlertCircle className="h-4 w-4" />
|
|
||||||
执行详情
|
|
||||||
</h3>
|
|
||||||
<Button variant="ghost" size="sm" onClick={() => setSelectedLog(null)}>
|
|
||||||
关闭
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
{/* 右侧:详情面板 */}
|
||||||
<div>
|
<div className="border rounded-lg">
|
||||||
<span className="text-muted-foreground">Bean名称:</span>
|
{selectedLog ? (
|
||||||
<code className="ml-2 text-xs bg-muted px-1 py-0.5 rounded">{selectedLog.beanName}</code>
|
<ScrollArea className="h-[556px]">
|
||||||
</div>
|
<div className="p-5 space-y-4">
|
||||||
<div>
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-muted-foreground">方法名称:</span>
|
<h3 className="text-base font-semibold flex items-center gap-2">
|
||||||
<code className="ml-2 text-xs bg-muted px-1 py-0.5 rounded">{selectedLog.methodName}</code>
|
<Info className="h-4 w-4 text-primary" />
|
||||||
</div>
|
执行详情
|
||||||
<div>
|
</h3>
|
||||||
<span className="text-muted-foreground">服务器主机:</span>
|
{getStatusBadge(selectedLog.status)}
|
||||||
<span className="ml-2">{selectedLog.serverHost || '-'}</span>
|
</div>
|
||||||
</div>
|
|
||||||
<div>
|
<Separator />
|
||||||
<span className="text-muted-foreground">服务器IP:</span>
|
|
||||||
<span className="ml-2">{selectedLog.serverIp || '-'}</span>
|
{/* 基本信息 */}
|
||||||
</div>
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Calendar className="h-4 w-4 text-muted-foreground mt-0.5" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">执行时间</p>
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
{dayjs(selectedLog.executeTime).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedLog.finishTime && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-muted-foreground mt-0.5" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">完成时间</p>
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
{dayjs(selectedLog.finishTime).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Timer className="h-4 w-4 text-muted-foreground mt-0.5" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">执行耗时</p>
|
||||||
|
<p className="text-sm font-medium font-mono">
|
||||||
|
{formatDuration(selectedLog.duration)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedLog.serverIp && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Server className="h-4 w-4 text-muted-foreground mt-0.5" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">服务器信息</p>
|
||||||
|
<p className="text-sm font-medium font-mono">{selectedLog.serverIp}</p>
|
||||||
|
{selectedLog.serverHost && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">{selectedLog.serverHost}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* 技术信息 */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Code className="h-4 w-4 text-muted-foreground mt-0.5" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">Bean名称</p>
|
||||||
|
<code className="text-xs bg-muted px-2 py-1 rounded block overflow-x-auto">
|
||||||
|
{selectedLog.beanName}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Activity className="h-4 w-4 text-muted-foreground mt-0.5" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">方法名称</p>
|
||||||
|
<code className="text-xs bg-muted px-2 py-1 rounded block overflow-x-auto">
|
||||||
|
{selectedLog.methodName}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedLog.methodParams && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground mb-2 flex items-center gap-1">
|
||||||
|
<Code className="h-3.5 w-3.5" />
|
||||||
|
方法参数
|
||||||
|
</p>
|
||||||
|
<pre className="text-xs bg-muted p-3 rounded overflow-x-auto font-mono">
|
||||||
|
{selectedLog.methodParams}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 结果信息 */}
|
||||||
|
{selectedLog.resultMessage && (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground mb-2 flex items-center gap-1">
|
||||||
|
<CheckCircle2 className="h-3.5 w-3.5" />
|
||||||
|
结果消息
|
||||||
|
</p>
|
||||||
|
<div className="text-sm bg-muted/50 p-3 rounded border">
|
||||||
|
{selectedLog.resultMessage}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 异常信息 */}
|
||||||
|
{selectedLog.exceptionInfo && (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-destructive mb-2 flex items-center gap-1 font-medium">
|
||||||
|
<XCircle className="h-3.5 w-3.5" />
|
||||||
|
异常信息
|
||||||
|
</p>
|
||||||
|
<pre className="text-xs bg-destructive/5 border border-destructive/20 p-3 rounded overflow-x-auto font-mono text-destructive max-h-60">
|
||||||
|
{selectedLog.exceptionInfo}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-[556px] text-center">
|
||||||
|
<div>
|
||||||
|
<Info className="h-12 w-12 mx-auto mb-3 text-muted-foreground/30" />
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
选择左侧日志查看详情
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-64 border rounded-lg">
|
||||||
|
<div className="text-center">
|
||||||
|
<AlertCircle className="h-12 w-12 mx-auto mb-3 text-muted-foreground/30" />
|
||||||
|
<p className="text-sm text-muted-foreground">暂无执行日志</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedLog.methodParams && (
|
|
||||||
<div>
|
|
||||||
<label className="text-sm text-muted-foreground mb-1 block">方法参数:</label>
|
|
||||||
<Textarea
|
|
||||||
value={selectedLog.methodParams}
|
|
||||||
readOnly
|
|
||||||
className="font-mono text-xs h-20"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedLog.resultMessage && (
|
|
||||||
<div>
|
|
||||||
<label className="text-sm text-muted-foreground mb-1 block">结果消息:</label>
|
|
||||||
<Textarea
|
|
||||||
value={selectedLog.resultMessage}
|
|
||||||
readOnly
|
|
||||||
className="text-xs h-20"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedLog.exceptionInfo && (
|
|
||||||
<div>
|
|
||||||
<label className="text-sm text-destructive mb-1 block flex items-center gap-1">
|
|
||||||
<XCircle className="h-4 w-4" />
|
|
||||||
异常信息:
|
|
||||||
</label>
|
|
||||||
<Textarea
|
|
||||||
value={selectedLog.exceptionInfo}
|
|
||||||
readOnly
|
|
||||||
className="font-mono text-xs h-40 text-destructive"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</DialogBody>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,18 +6,20 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { DataTablePagination } from '@/components/ui/pagination';
|
import { DataTablePagination } from '@/components/ui/pagination';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import {
|
import {
|
||||||
Loader2, Plus, Search, Edit, Trash2, Play, Pause,
|
Loader2, Plus, Search, Edit, Trash2, Play, Pause,
|
||||||
Clock, Activity, CheckCircle2, XCircle, FolderKanban, PlayCircle, FileText
|
Clock, Activity, CheckCircle2, XCircle, FolderKanban, PlayCircle, FileText, BarChart3, List
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { getScheduleJobs, getJobCategoryList, executeJobNow, pauseJob, resumeJob, deleteScheduleJob } from './service';
|
import { getScheduleJobs, getJobCategoryList, startJob, pauseJob, resumeJob, stopJob, triggerJob, deleteScheduleJob } from './service';
|
||||||
import type { ScheduleJobResponse, ScheduleJobQuery, JobCategoryResponse, JobStatus } from './types';
|
import type { ScheduleJobResponse, ScheduleJobQuery, JobCategoryResponse, JobStatus } from './types';
|
||||||
import type { Page } from '@/types/base';
|
import type { Page } from '@/types/base';
|
||||||
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
||||||
import CategoryManageDialog from './components/CategoryManageDialog';
|
import CategoryManageDialog from './components/CategoryManageDialog';
|
||||||
import JobLogDialog from './components/JobLogDialog';
|
import JobLogDialog from './components/JobLogDialog';
|
||||||
import JobEditDialog from './components/JobEditDialog';
|
import JobEditDialog from './components/JobEditDialog';
|
||||||
|
import Dashboard from './components/Dashboard';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -139,20 +141,20 @@ const ScheduleJobList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 立即执行
|
// 启动任务
|
||||||
const handleExecute = async (record: ScheduleJobResponse) => {
|
const handleStart = async (record: ScheduleJobResponse) => {
|
||||||
try {
|
try {
|
||||||
await executeJobNow(record.id);
|
await startJob(record.id);
|
||||||
toast({
|
toast({
|
||||||
title: '执行成功',
|
title: '启动成功',
|
||||||
description: `任务 "${record.jobName}" 已开始执行`,
|
description: `任务 "${record.jobName}" 已启动`,
|
||||||
});
|
});
|
||||||
loadData();
|
loadData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('执行失败:', error);
|
console.error('启动失败:', error);
|
||||||
toast({
|
toast({
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
title: '执行失败',
|
title: '启动失败',
|
||||||
description: error instanceof Error ? error.message : '未知错误',
|
description: error instanceof Error ? error.message : '未知错误',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -196,6 +198,44 @@ const ScheduleJobList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 停止任务
|
||||||
|
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) => {
|
const handleViewLog = (record: ScheduleJobResponse) => {
|
||||||
setSelectedJob(record);
|
setSelectedJob(record);
|
||||||
@ -240,8 +280,23 @@ const ScheduleJobList: React.FC = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 视图切换 */}
|
||||||
|
<Tabs defaultValue="list" className="space-y-6">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="list" className="gap-2">
|
||||||
|
<List className="h-4 w-4" />
|
||||||
|
列表视图
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="dashboard" className="gap-2">
|
||||||
|
<BarChart3 className="h-4 w-4" />
|
||||||
|
仪表盘
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{/* 列表视图 */}
|
||||||
|
<TabsContent value="list" className="space-y-6">
|
||||||
{/* 统计卡片 */}
|
{/* 统计卡片 */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
<Card className="bg-gradient-to-br from-blue-500/10 to-blue-500/5 border-blue-500/20">
|
<Card className="bg-gradient-to-br from-blue-500/10 to-blue-500/5 border-blue-500/20">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-blue-700">总任务</CardTitle>
|
<CardTitle className="text-sm font-medium text-blue-700">总任务</CardTitle>
|
||||||
@ -408,8 +463,8 @@ const ScheduleJobList: React.FC = () => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
onClick={() => handleExecute(record)}
|
onClick={() => handleTrigger(record)}
|
||||||
title="立即执行"
|
title="立即触发"
|
||||||
>
|
>
|
||||||
<PlayCircle className="h-4 w-4" />
|
<PlayCircle className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -564,6 +619,13 @@ const ScheduleJobList: React.FC = () => {
|
|||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 仪表盘视图 */}
|
||||||
|
<TabsContent value="dashboard">
|
||||||
|
<Dashboard />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import type {
|
|||||||
ScheduleJobQuery,
|
ScheduleJobQuery,
|
||||||
ScheduleJobLogResponse,
|
ScheduleJobLogResponse,
|
||||||
ScheduleJobLogQuery,
|
ScheduleJobLogQuery,
|
||||||
|
DashboardResponse,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
const JOB_CATEGORY_URL = '/api/v1/schedule/job-categories';
|
const JOB_CATEGORY_URL = '/api/v1/schedule/job-categories';
|
||||||
@ -92,10 +93,10 @@ export const batchDeleteScheduleJobs = (ids: number[]) =>
|
|||||||
request.post(`${SCHEDULE_JOB_URL}/batch-delete`, { ids });
|
request.post(`${SCHEDULE_JOB_URL}/batch-delete`, { ids });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 立即执行任务
|
* 启动任务
|
||||||
*/
|
*/
|
||||||
export const executeJobNow = (id: number) =>
|
export const startJob = (id: number) =>
|
||||||
request.post<void>(`${SCHEDULE_JOB_URL}/${id}/execute`);
|
request.post<void>(`${SCHEDULE_JOB_URL}/${id}/start`);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 暂停任务
|
* 暂停任务
|
||||||
@ -109,6 +110,26 @@ export const pauseJob = (id: number) =>
|
|||||||
export const resumeJob = (id: number) =>
|
export const resumeJob = (id: number) =>
|
||||||
request.post<void>(`${SCHEDULE_JOB_URL}/${id}/resume`);
|
request.post<void>(`${SCHEDULE_JOB_URL}/${id}/resume`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止任务
|
||||||
|
*/
|
||||||
|
export const stopJob = (id: number) =>
|
||||||
|
request.post<void>(`${SCHEDULE_JOB_URL}/${id}/stop`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 立即触发任务
|
||||||
|
*/
|
||||||
|
export const triggerJob = (id: number) =>
|
||||||
|
request.post<void>(`${SCHEDULE_JOB_URL}/${id}/trigger`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新Cron表达式
|
||||||
|
*/
|
||||||
|
export const updateJobCron = (id: number, cronExpression: string) =>
|
||||||
|
request.put<void>(`${SCHEDULE_JOB_URL}/${id}/cron`, null, {
|
||||||
|
params: { cronExpression }
|
||||||
|
});
|
||||||
|
|
||||||
// ==================== 任务日志 ====================
|
// ==================== 任务日志 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -117,3 +138,11 @@ export const resumeJob = (id: number) =>
|
|||||||
export const getJobLogs = (params?: ScheduleJobLogQuery) =>
|
export const getJobLogs = (params?: ScheduleJobLogQuery) =>
|
||||||
request.get<Page<ScheduleJobLogResponse>>(`${JOB_LOG_URL}/page`, { params });
|
request.get<Page<ScheduleJobLogResponse>>(`${JOB_LOG_URL}/page`, { params });
|
||||||
|
|
||||||
|
// ==================== 仪表盘 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取仪表盘数据
|
||||||
|
*/
|
||||||
|
export const getDashboard = () =>
|
||||||
|
request.get<DashboardResponse>(`${SCHEDULE_JOB_URL}/dashboard`);
|
||||||
|
|
||||||
|
|||||||
@ -148,3 +148,35 @@ export interface ScheduleJobLogQuery extends BaseQuery {
|
|||||||
executeTimeEnd?: string;
|
executeTimeEnd?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仪表盘概览统计
|
||||||
|
*/
|
||||||
|
export interface DashboardSummary {
|
||||||
|
total: number;
|
||||||
|
enabled: number;
|
||||||
|
disabled: number;
|
||||||
|
paused: number;
|
||||||
|
running: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 正在运行的任务
|
||||||
|
*/
|
||||||
|
export interface RunningJob {
|
||||||
|
jobId: number;
|
||||||
|
jobName: string;
|
||||||
|
startTime: string;
|
||||||
|
progress: number;
|
||||||
|
message?: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仪表盘响应
|
||||||
|
*/
|
||||||
|
export interface DashboardResponse {
|
||||||
|
summary: DashboardSummary;
|
||||||
|
runningJobs: RunningJob[];
|
||||||
|
recentLogs: ScheduleJobLogResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user