chore: 以本地为准,上传全部并替换 GitHub

This commit is contained in:
卡若
2026-02-03 11:36:53 +08:00
parent 1219166526
commit b404bf546e
131 changed files with 37618 additions and 3930 deletions

View 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>
)
}