Files
shensheshou/app/value-model/assessment/page.tsx
v0 b17b488f8e refactor: restructure navigation and module layout
Reorganize navigation and module structure based on new requirements.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
2026-01-31 04:32:36 +00:00

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