Reorganize navigation and module structure based on new requirements. Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
321 lines
12 KiB
TypeScript
321 lines
12 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import {
|
|
BarChart3,
|
|
Plus,
|
|
Play,
|
|
Search,
|
|
Users,
|
|
Clock,
|
|
CheckCircle,
|
|
XCircle,
|
|
RefreshCw,
|
|
Download,
|
|
Filter,
|
|
} from "lucide-react"
|
|
import { Card, CardContent } from "@/components/ui/card"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Progress } from "@/components/ui/progress"
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
import { CreateAssessmentTaskDialog } from "@/components/dialogs/create-assessment-task-dialog"
|
|
|
|
interface AssessmentTask {
|
|
id: string
|
|
name: string
|
|
model: string
|
|
targetCrowd: string
|
|
userCount: number
|
|
status: "running" | "completed" | "failed" | "scheduled"
|
|
progress: number
|
|
createdAt: string
|
|
completedAt: string | null
|
|
schedule: string
|
|
resultSummary?: {
|
|
avgScore: number
|
|
highValue: number
|
|
mediumValue: number
|
|
lowValue: number
|
|
}
|
|
}
|
|
|
|
const MOCK_TASKS: AssessmentTask[] = [
|
|
{
|
|
id: "1",
|
|
name: "全量用户CLV评估",
|
|
model: "CLV预测模型 v2.3.1",
|
|
targetCrowd: "全部用户",
|
|
userCount: 4028567890,
|
|
status: "completed",
|
|
progress: 100,
|
|
createdAt: "2025-12-12 00:00:00",
|
|
completedAt: "2025-12-12 03:45:00",
|
|
schedule: "每周一",
|
|
resultSummary: {
|
|
avgScore: 65.8,
|
|
highValue: 15.2,
|
|
mediumValue: 45.6,
|
|
lowValue: 39.2,
|
|
},
|
|
},
|
|
{
|
|
id: "2",
|
|
name: "活跃用户RFM评分",
|
|
model: "RFM评分模型 v1.5.0",
|
|
targetCrowd: "近30天活跃用户",
|
|
userCount: 1256789012,
|
|
status: "running",
|
|
progress: 67,
|
|
createdAt: "2025-12-12 10:00:00",
|
|
completedAt: null,
|
|
schedule: "每天",
|
|
},
|
|
{
|
|
id: "3",
|
|
name: "流失风险预警",
|
|
model: "流失预警模型 v3.1.2",
|
|
targetCrowd: "沉默用户",
|
|
userCount: 456789012,
|
|
status: "completed",
|
|
progress: 100,
|
|
createdAt: "2025-12-11 22:00:00",
|
|
completedAt: "2025-12-12 00:30:00",
|
|
schedule: "每天",
|
|
resultSummary: {
|
|
avgScore: 0.45,
|
|
highValue: 8.5,
|
|
mediumValue: 25.3,
|
|
lowValue: 66.2,
|
|
},
|
|
},
|
|
{
|
|
id: "4",
|
|
name: "高价值用户细分",
|
|
model: "用户分群模型 v1.0.0",
|
|
targetCrowd: "价值评分>80用户",
|
|
userCount: 125678901,
|
|
status: "scheduled",
|
|
progress: 0,
|
|
createdAt: "2025-12-12 14:00:00",
|
|
completedAt: null,
|
|
schedule: "每周",
|
|
},
|
|
{
|
|
id: "5",
|
|
name: "欺诈风险扫描",
|
|
model: "欺诈检测模型 v2.0.0",
|
|
targetCrowd: "近7天交易用户",
|
|
userCount: 89012345,
|
|
status: "failed",
|
|
progress: 45,
|
|
createdAt: "2025-12-12 08:00:00",
|
|
completedAt: null,
|
|
schedule: "实时",
|
|
},
|
|
]
|
|
|
|
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 },
|
|
scheduled: { label: "已排期", color: "bg-yellow-100 text-yellow-700", icon: Clock },
|
|
}
|
|
|
|
export default function AssessmentServicePage() {
|
|
const [tasks, setTasks] = useState<AssessmentTask[]>(MOCK_TASKS)
|
|
const [searchQuery, setSearchQuery] = useState("")
|
|
const [statusFilter, setStatusFilter] = useState("all")
|
|
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
|
|
|
const formatNumber = (num: number): string => {
|
|
if (num >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`
|
|
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
|
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
|
return num.toString()
|
|
}
|
|
|
|
const filteredTasks = tasks.filter((task) => {
|
|
const matchesSearch = task.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
const matchesStatus = statusFilter === "all" || task.status === statusFilter
|
|
return matchesSearch && matchesStatus
|
|
})
|
|
|
|
const stats = {
|
|
total: tasks.length,
|
|
running: tasks.filter((t) => t.status === "running").length,
|
|
completed: tasks.filter((t) => t.status === "completed").length,
|
|
totalEvaluated: tasks.filter((t) => t.status === "completed").reduce((sum, t) => sum + t.userCount, 0),
|
|
}
|
|
|
|
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>
|
|
<Button
|
|
className="bg-gradient-to-r from-blue-500 to-purple-500 text-white"
|
|
onClick={() => setShowCreateDialog(true)}
|
|
>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
创建评估任务
|
|
</Button>
|
|
</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">
|
|
<BarChart3 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">
|
|
<Users className="w-4 h-4" />
|
|
已评估用户
|
|
</div>
|
|
<div className="text-2xl font-bold text-gray-900">{formatNumber(stats.totalEvaluated)}</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="flex flex-col md:flex-row gap-4">
|
|
<div className="relative flex-1 max-w-md">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
|
<Input
|
|
placeholder="搜索任务..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="pl-10 bg-white/60"
|
|
/>
|
|
</div>
|
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
<SelectTrigger className="w-full md:w-40 bg-white/60">
|
|
<Filter className="w-4 h-4 mr-2" />
|
|
<SelectValue placeholder="状态筛选" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">全部状态</SelectItem>
|
|
<SelectItem value="running">运行中</SelectItem>
|
|
<SelectItem value="completed">已完成</SelectItem>
|
|
<SelectItem value="failed">失败</SelectItem>
|
|
<SelectItem value="scheduled">已排期</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Task List */}
|
|
<div className="space-y-4">
|
|
{filteredTasks.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 mb-4">
|
|
<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={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>
|
|
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-500">
|
|
<span>模型: {task.model}</span>
|
|
<span>目标: {task.targetCrowd}</span>
|
|
<span>用户数: {formatNumber(task.userCount)}</span>
|
|
<span>周期: {task.schedule}</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{task.status === "completed" && (
|
|
<Button variant="outline" size="sm">
|
|
<Download className="w-4 h-4 mr-1" />
|
|
导出
|
|
</Button>
|
|
)}
|
|
{(task.status === "scheduled" || task.status === "failed") && (
|
|
<Button variant="outline" size="sm">
|
|
<Play className="w-4 h-4 mr-1" />
|
|
执行
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{task.status === "running" && (
|
|
<div className="mb-4">
|
|
<div className="flex items-center justify-between text-sm text-gray-500 mb-1">
|
|
<span>执行进度</span>
|
|
<span>{task.progress}%</span>
|
|
</div>
|
|
<Progress value={task.progress} className="h-2" />
|
|
</div>
|
|
)}
|
|
|
|
{task.resultSummary && (
|
|
<div className="grid grid-cols-4 gap-4 p-4 rounded-xl bg-gray-50">
|
|
<div className="text-center">
|
|
<div className="text-xl font-bold text-gray-900">{task.resultSummary.avgScore}</div>
|
|
<div className="text-xs text-gray-500">平均分</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-xl font-bold text-green-600">{task.resultSummary.highValue}%</div>
|
|
<div className="text-xs text-gray-500">高价值</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-xl font-bold text-yellow-600">{task.resultSummary.mediumValue}%</div>
|
|
<div className="text-xs text-gray-500">中价值</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-xl font-bold text-gray-600">{task.resultSummary.lowValue}%</div>
|
|
<div className="text-xs text-gray-500">低价值</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center justify-between text-xs text-gray-500 mt-4 pt-4 border-t border-gray-100">
|
|
<span>创建时间: {task.createdAt}</span>
|
|
{task.completedAt && <span>完成时间: {task.completedAt}</span>}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Create Task Dialog */}
|
|
<CreateAssessmentTaskDialog open={showCreateDialog} onOpenChange={setShowCreateDialog} />
|
|
</div>
|
|
)
|
|
}
|