Reorganize navigation and module structure based on new requirements. Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
315 lines
12 KiB
TypeScript
315 lines
12 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { Clock, Play, Pause, CheckCircle, XCircle, RefreshCw, Calendar, Settings, MoreHorizontal } from "lucide-react"
|
|
import { Card, CardContent } from "@/components/ui/card"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Progress } from "@/components/ui/progress"
|
|
import { ScheduleCalendarDialog } from "@/components/dialogs/schedule-calendar-dialog"
|
|
import { TaskDetailDialog } from "@/components/dialogs/task-detail-dialog"
|
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
|
|
|
interface ScheduledTask {
|
|
id: string
|
|
name: string
|
|
type: "sync" | "cleaning" | "tagging" | "model"
|
|
schedule: string
|
|
status: "running" | "completed" | "failed" | "waiting" | "paused"
|
|
progress: number
|
|
nextRun: string
|
|
lastRun: string
|
|
duration: string
|
|
logs: string[]
|
|
}
|
|
|
|
const MOCK_TASKS: ScheduledTask[] = [
|
|
{
|
|
id: "1",
|
|
name: "用户数据同步",
|
|
type: "sync",
|
|
schedule: "*/5 * * * *",
|
|
status: "running",
|
|
progress: 67,
|
|
nextRun: "-",
|
|
lastRun: "2025-12-12 14:30:00",
|
|
duration: "2分30秒",
|
|
logs: ["正在同步数据...", "已处理 67%"],
|
|
},
|
|
{
|
|
id: "2",
|
|
name: "交易数据清洗",
|
|
type: "cleaning",
|
|
schedule: "0 */1 * * *",
|
|
status: "completed",
|
|
progress: 100,
|
|
nextRun: "2025-12-12 15:00:00",
|
|
lastRun: "2025-12-12 14:00:00",
|
|
duration: "5分12秒",
|
|
logs: ["清洗完成,处理 89234567 条记录"],
|
|
},
|
|
{
|
|
id: "3",
|
|
name: "用户标签计算",
|
|
type: "tagging",
|
|
schedule: "0 2 * * *",
|
|
status: "waiting",
|
|
progress: 0,
|
|
nextRun: "2025-12-13 02:00:00",
|
|
lastRun: "2025-12-12 02:00:00",
|
|
duration: "15分38秒",
|
|
logs: ["等待执行"],
|
|
},
|
|
{
|
|
id: "4",
|
|
name: "CLV模型训练",
|
|
type: "model",
|
|
schedule: "0 3 * * 0",
|
|
status: "failed",
|
|
progress: 45,
|
|
nextRun: "2025-12-15 03:00:00",
|
|
lastRun: "2025-12-08 03:00:00",
|
|
duration: "-",
|
|
logs: ["错误:内存不足", "任务中断于 45%"],
|
|
},
|
|
{
|
|
id: "5",
|
|
name: "行为数据同步",
|
|
type: "sync",
|
|
schedule: "实时",
|
|
status: "running",
|
|
progress: 100,
|
|
nextRun: "-",
|
|
lastRun: "2025-12-12 14:35:12",
|
|
duration: "持续运行",
|
|
logs: ["Kafka消费正常", "当前lag: 125"],
|
|
},
|
|
]
|
|
|
|
const TYPE_CONFIG = {
|
|
sync: { label: "数据同步", color: "bg-blue-100 text-blue-700" },
|
|
cleaning: { label: "数据清洗", color: "bg-green-100 text-green-700" },
|
|
tagging: { label: "标签计算", color: "bg-purple-100 text-purple-700" },
|
|
model: { label: "模型训练", color: "bg-orange-100 text-orange-700" },
|
|
}
|
|
|
|
const STATUS_CONFIG = {
|
|
running: { label: "运行中", color: "bg-blue-100 text-blue-700", icon: RefreshCw },
|
|
completed: { label: "已完成", color: "bg-green-100 text-green-700", icon: CheckCircle },
|
|
failed: { label: "失败", color: "bg-red-100 text-red-700", icon: XCircle },
|
|
waiting: { label: "等待中", color: "bg-gray-100 text-gray-700", icon: Clock },
|
|
paused: { label: "已暂停", color: "bg-yellow-100 text-yellow-700", icon: Pause },
|
|
}
|
|
|
|
export default function TaskSchedulePage() {
|
|
const [tasks, setTasks] = useState<ScheduledTask[]>(MOCK_TASKS)
|
|
const [currentTime, setCurrentTime] = useState(new Date())
|
|
const [showCalendarDialog, setShowCalendarDialog] = useState(false)
|
|
const [showDetailDialog, setShowDetailDialog] = useState(false)
|
|
const [selectedTask, setSelectedTask] = useState<ScheduledTask | null>(null)
|
|
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
setCurrentTime(new Date())
|
|
// 模拟进度更新
|
|
setTasks((prev) =>
|
|
prev.map((task) => {
|
|
if (task.status === "running" && task.progress < 100) {
|
|
return { ...task, progress: Math.min(task.progress + Math.random() * 2, 100) }
|
|
}
|
|
return task
|
|
}),
|
|
)
|
|
}, 2000)
|
|
return () => clearInterval(interval)
|
|
}, [])
|
|
|
|
const stats = {
|
|
total: tasks.length,
|
|
running: tasks.filter((t) => t.status === "running").length,
|
|
completed: tasks.filter((t) => t.status === "completed").length,
|
|
failed: tasks.filter((t) => t.status === "failed").length,
|
|
}
|
|
|
|
const handleViewDetail = (task: ScheduledTask) => {
|
|
setSelectedTask(task)
|
|
setShowDetailDialog(true)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6 p-6">
|
|
{/* Header */}
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">任务调度</h1>
|
|
<p className="text-gray-500 mt-1">管理与监控数据处理任务</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Badge variant="outline" className="px-3 py-1">
|
|
<Clock className="w-3 h-3 mr-1" />
|
|
{currentTime.toLocaleTimeString()}
|
|
</Badge>
|
|
<Button variant="outline" onClick={() => setShowCalendarDialog(true)}>
|
|
<Calendar className="w-4 h-4 mr-2" />
|
|
调度日历
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
|
<Settings className="w-4 h-4" />
|
|
总任务数
|
|
</div>
|
|
<div className="text-2xl font-bold text-gray-900">{stats.total}</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
|
<RefreshCw className="w-4 h-4 text-blue-500" />
|
|
运行中
|
|
</div>
|
|
<div className="text-2xl font-bold text-blue-600">{stats.running}</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
|
<CheckCircle className="w-4 h-4 text-green-500" />
|
|
已完成
|
|
</div>
|
|
<div className="text-2xl font-bold text-green-600">{stats.completed}</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
|
<XCircle className="w-4 h-4 text-red-500" />
|
|
失败
|
|
</div>
|
|
<div className="text-2xl font-bold text-red-600">{stats.failed}</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Task List */}
|
|
<div className="space-y-4">
|
|
{tasks.map((task) => {
|
|
const StatusIcon = STATUS_CONFIG[task.status].icon
|
|
|
|
return (
|
|
<Card key={task.id} className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<h3 className="font-semibold text-gray-900">{task.name}</h3>
|
|
<Badge className={TYPE_CONFIG[task.type].color}>{TYPE_CONFIG[task.type].label}</Badge>
|
|
<Badge className={STATUS_CONFIG[task.status].color}>
|
|
<StatusIcon className={`w-3 h-3 mr-1 ${task.status === "running" ? "animate-spin" : ""}`} />
|
|
{STATUS_CONFIG[task.status].label}
|
|
</Badge>
|
|
</div>
|
|
|
|
{task.status === "running" && (
|
|
<div className="mb-3">
|
|
<div className="flex items-center justify-between text-sm text-gray-500 mb-1">
|
|
<span>执行进度</span>
|
|
<span>{Math.round(task.progress)}%</span>
|
|
</div>
|
|
<Progress value={task.progress} className="h-2" />
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
<div>
|
|
<span className="text-gray-500">调度规则:</span>
|
|
<code className="ml-1 text-xs bg-gray-100 px-1 rounded">{task.schedule}</code>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-500">下次执行:</span>
|
|
<span className="ml-1 text-gray-900">{task.nextRun}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-500">上次执行:</span>
|
|
<span className="ml-1 text-gray-900">{task.lastRun}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-500">耗时:</span>
|
|
<span className="ml-1 text-gray-900">{task.duration}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{task.logs.length > 0 && (
|
|
<div className="mt-3 p-2 bg-gray-50 rounded-lg">
|
|
<div className="text-xs text-gray-500 mb-1">最新日志:</div>
|
|
{task.logs.slice(-2).map((log, i) => (
|
|
<div key={i} className="text-xs text-gray-600">
|
|
{log}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 ml-4">
|
|
{task.status === "running" ? (
|
|
<Button variant="outline" size="sm">
|
|
<Pause className="w-4 h-4" />
|
|
</Button>
|
|
) : (
|
|
<Button variant="outline" size="sm">
|
|
<Play className="w-4 h-4" />
|
|
</Button>
|
|
)}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="sm">
|
|
<MoreHorizontal className="w-4 h-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={() => handleViewDetail(task)}>查看详情</DropdownMenuItem>
|
|
<DropdownMenuItem>编辑任务</DropdownMenuItem>
|
|
<DropdownMenuItem>查看日志</DropdownMenuItem>
|
|
<DropdownMenuItem className="text-red-600">删除任务</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Schedule Calendar Dialog */}
|
|
<ScheduleCalendarDialog open={showCalendarDialog} onOpenChange={setShowCalendarDialog} />
|
|
|
|
<TaskDetailDialog
|
|
open={showDetailDialog}
|
|
onOpenChange={setShowDetailDialog}
|
|
task={
|
|
selectedTask
|
|
? {
|
|
id: selectedTask.id,
|
|
name: selectedTask.name,
|
|
type: selectedTask.type,
|
|
status: selectedTask.status,
|
|
progress: selectedTask.progress,
|
|
schedule: selectedTask.schedule,
|
|
lastRun: selectedTask.lastRun,
|
|
nextRun: selectedTask.nextRun,
|
|
duration: selectedTask.duration,
|
|
}
|
|
: undefined
|
|
}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|