chore: 以本地为准,上传全部并替换 GitHub
This commit is contained in:
414
app/data-ingestion/tasks/page.tsx
Normal file
414
app/data-ingestion/tasks/page.tsx
Normal file
@@ -0,0 +1,414 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import {
|
||||
Calendar,
|
||||
Plus,
|
||||
Play,
|
||||
Pause,
|
||||
RefreshCw,
|
||||
Clock,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Loader2,
|
||||
Settings,
|
||||
Database,
|
||||
Zap,
|
||||
Target,
|
||||
} from "lucide-react"
|
||||
|
||||
// 任务接口
|
||||
interface Task {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
type: 'sync' | 'clean' | 'tag' | 'export'
|
||||
source: string
|
||||
target: string
|
||||
schedule: string
|
||||
enabled: boolean
|
||||
lastRun?: string
|
||||
lastStatus?: 'success' | 'failed' | 'running'
|
||||
nextRun?: string
|
||||
processedCount?: number
|
||||
}
|
||||
|
||||
// 预定义任务
|
||||
const DEFAULT_TASKS: Task[] = [
|
||||
{
|
||||
id: 'task_1',
|
||||
name: '腾讯QQ数据同步',
|
||||
description: '从KR_腾讯同步QQ手机关联数据到用户估值表',
|
||||
type: 'sync',
|
||||
source: 'KR_腾讯',
|
||||
target: 'KR.用户估值',
|
||||
schedule: '每小时',
|
||||
enabled: true,
|
||||
lastRun: '10分钟前',
|
||||
lastStatus: 'success',
|
||||
nextRun: '50分钟后',
|
||||
processedCount: 1250000
|
||||
},
|
||||
{
|
||||
id: 'task_2',
|
||||
name: '微博UID关联',
|
||||
description: '从KR_微博同步UID与手机号关联',
|
||||
type: 'sync',
|
||||
source: 'KR_微博',
|
||||
target: 'KR.用户估值',
|
||||
schedule: '每天凌晨',
|
||||
enabled: true,
|
||||
lastRun: '昨天 02:00',
|
||||
lastStatus: 'success',
|
||||
nextRun: '明天 02:00',
|
||||
processedCount: 5800000
|
||||
},
|
||||
{
|
||||
id: 'task_3',
|
||||
name: '存客宝CRM同步',
|
||||
description: '从MySQL同步存客宝用户数据',
|
||||
type: 'sync',
|
||||
source: 'cunkebao_v3',
|
||||
target: 'KR_存客宝.用户资产统一视图',
|
||||
schedule: '每5分钟',
|
||||
enabled: true,
|
||||
lastRun: '2分钟前',
|
||||
lastStatus: 'success',
|
||||
nextRun: '3分钟后',
|
||||
processedCount: 350
|
||||
},
|
||||
{
|
||||
id: 'task_4',
|
||||
name: '手机号格式化清洗',
|
||||
description: '清洗手机号格式,补充归属地信息',
|
||||
type: 'clean',
|
||||
source: 'KR.用户估值',
|
||||
target: 'KR.用户估值',
|
||||
schedule: '每小时',
|
||||
enabled: true,
|
||||
lastRun: '30分钟前',
|
||||
lastStatus: 'success',
|
||||
processedCount: 95000
|
||||
},
|
||||
{
|
||||
id: 'task_5',
|
||||
name: 'RFM评分计算',
|
||||
description: '计算用户RFM评分并更新用户等级',
|
||||
type: 'tag',
|
||||
source: 'KR.用户估值',
|
||||
target: 'KR.用户估值',
|
||||
schedule: '每天凌晨',
|
||||
enabled: true,
|
||||
lastRun: '昨天 03:00',
|
||||
lastStatus: 'success',
|
||||
processedCount: 20000000
|
||||
},
|
||||
{
|
||||
id: 'task_6',
|
||||
name: '流量包导出',
|
||||
description: '导出高价值用户流量包',
|
||||
type: 'export',
|
||||
source: 'KR.用户估值',
|
||||
target: '文件系统',
|
||||
schedule: '按需',
|
||||
enabled: false,
|
||||
lastRun: '3天前',
|
||||
lastStatus: 'success',
|
||||
processedCount: 50000
|
||||
},
|
||||
]
|
||||
|
||||
export default function TaskSchedulePage() {
|
||||
const [tasks, setTasks] = useState<Task[]>(DEFAULT_TASKS)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showAddDialog, setShowAddDialog] = useState(false)
|
||||
const [runningTask, setRunningTask] = useState<string | null>(null)
|
||||
|
||||
// 任务类型配置
|
||||
const TASK_TYPES = {
|
||||
sync: { label: '数据同步', color: 'bg-blue-100 text-blue-700', icon: RefreshCw },
|
||||
clean: { label: '数据清洗', color: 'bg-green-100 text-green-700', icon: Zap },
|
||||
tag: { label: '标签计算', color: 'bg-purple-100 text-purple-700', icon: Target },
|
||||
export: { label: '数据导出', color: 'bg-orange-100 text-orange-700', icon: Database },
|
||||
}
|
||||
|
||||
// 执行任务
|
||||
const runTask = async (taskId: string) => {
|
||||
setRunningTask(taskId)
|
||||
// 模拟执行
|
||||
await new Promise(r => setTimeout(r, 2000))
|
||||
setTasks(tasks.map(t => t.id === taskId ? {
|
||||
...t,
|
||||
lastRun: '刚刚',
|
||||
lastStatus: 'success' as const
|
||||
} : t))
|
||||
setRunningTask(null)
|
||||
}
|
||||
|
||||
// 切换任务状态
|
||||
const toggleTask = (taskId: string) => {
|
||||
setTasks(tasks.map(t => t.id === taskId ? { ...t, enabled: !t.enabled } : t))
|
||||
}
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`
|
||||
return num.toLocaleString()
|
||||
}
|
||||
|
||||
const getStatusIcon = (status?: string) => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return <CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
case 'failed':
|
||||
return <XCircle className="h-4 w-4 text-red-500" />
|
||||
case 'running':
|
||||
return <Loader2 className="h-4 w-4 text-blue-500 animate-spin" />
|
||||
default:
|
||||
return <Clock className="h-4 w-4 text-gray-400" />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
|
||||
<div className="p-6 space-y-6">
|
||||
{/* 顶部标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">任务调度</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">管理数据同步、清洗、计算等定时任务</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline" onClick={() => setLoading(true)}>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
</Button>
|
||||
<Button onClick={() => setShowAddDialog(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建任务
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="border-0 shadow-sm bg-white/80">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">任务总数</p>
|
||||
<p className="text-2xl font-bold">{tasks.length}</p>
|
||||
</div>
|
||||
<Calendar className="h-8 w-8 text-blue-500 opacity-50" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-0 shadow-sm bg-white/80">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">运行中</p>
|
||||
<p className="text-2xl font-bold text-green-600">{tasks.filter(t => t.enabled).length}</p>
|
||||
</div>
|
||||
<Play className="h-8 w-8 text-green-500 opacity-50" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-0 shadow-sm bg-white/80">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">今日执行</p>
|
||||
<p className="text-2xl font-bold">128</p>
|
||||
</div>
|
||||
<CheckCircle2 className="h-8 w-8 text-purple-500 opacity-50" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-0 shadow-sm bg-white/80">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">处理数据</p>
|
||||
<p className="text-2xl font-bold">{formatNumber(tasks.reduce((sum, t) => sum + (t.processedCount || 0), 0))}</p>
|
||||
</div>
|
||||
<Database className="h-8 w-8 text-orange-500 opacity-50" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 任务列表 */}
|
||||
<div className="space-y-3">
|
||||
{tasks.map((task) => {
|
||||
const typeConfig = TASK_TYPES[task.type]
|
||||
const TypeIcon = typeConfig.icon
|
||||
const isRunning = runningTask === task.id
|
||||
|
||||
return (
|
||||
<Card key={task.id} className="border-0 shadow-sm bg-white/80">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`p-2.5 rounded-xl ${typeConfig.color.replace('text-', 'bg-').replace('-700', '-100')}`}>
|
||||
<TypeIcon className={`h-5 w-5 ${typeConfig.color.split(' ')[1]}`} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-gray-900">{task.name}</h3>
|
||||
<Badge className={typeConfig.color}>{typeConfig.label}</Badge>
|
||||
{!task.enabled && <Badge variant="outline" className="text-gray-500">已暂停</Badge>}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-0.5">{task.description}</p>
|
||||
<div className="flex items-center gap-4 mt-2 text-xs text-gray-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<Database className="h-3 w-3" />
|
||||
{task.source} → {task.target}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
{task.schedule}
|
||||
</span>
|
||||
{task.lastRun && (
|
||||
<span className="flex items-center gap-1">
|
||||
{getStatusIcon(task.lastStatus)}
|
||||
上次: {task.lastRun}
|
||||
</span>
|
||||
)}
|
||||
{task.processedCount && (
|
||||
<span>处理: {formatNumber(task.processedCount)} 条</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch
|
||||
checked={task.enabled}
|
||||
onCheckedChange={() => toggleTask(task.id)}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => runTask(task.id)}
|
||||
disabled={isRunning}
|
||||
>
|
||||
{isRunning ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Play className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 新建任务弹窗 */}
|
||||
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>新建任务</DialogTitle>
|
||||
<DialogDescription>创建新的数据处理任务</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>任务名称</Label>
|
||||
<Input placeholder="输入任务名称" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>任务类型</Label>
|
||||
<Select defaultValue="sync">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="sync">数据同步</SelectItem>
|
||||
<SelectItem value="clean">数据清洗</SelectItem>
|
||||
<SelectItem value="tag">标签计算</SelectItem>
|
||||
<SelectItem value="export">数据导出</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>执行频率</Label>
|
||||
<Select defaultValue="hourly">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="5min">每5分钟</SelectItem>
|
||||
<SelectItem value="hourly">每小时</SelectItem>
|
||||
<SelectItem value="daily">每天</SelectItem>
|
||||
<SelectItem value="manual">手动触发</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>数据源</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择数据源" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="kr_tencent">KR_腾讯</SelectItem>
|
||||
<SelectItem value="kr_weibo">KR_微博</SelectItem>
|
||||
<SelectItem value="kr_ckb">KR_存客宝</SelectItem>
|
||||
<SelectItem value="kr_valuation">KR.用户估值</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>目标</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择目标" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="kr_valuation">KR.用户估值</SelectItem>
|
||||
<SelectItem value="kr_ckb_view">KR_存客宝.用户资产统一视图</SelectItem>
|
||||
<SelectItem value="file">文件导出</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowAddDialog(false)}>取消</Button>
|
||||
<Button onClick={() => setShowAddDialog(false)}>创建任务</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user