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>
This commit is contained in:
3
app/value-model/assessment/loading.tsx
Normal file
3
app/value-model/assessment/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
320
app/value-model/assessment/page.tsx
Normal file
320
app/value-model/assessment/page.tsx
Normal file
@@ -0,0 +1,320 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
3
app/value-model/models/loading.tsx
Normal file
3
app/value-model/models/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
358
app/value-model/models/page.tsx
Normal file
358
app/value-model/models/page.tsx
Normal file
@@ -0,0 +1,358 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Boxes,
|
||||
Plus,
|
||||
Search,
|
||||
Play,
|
||||
Settings,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
AlertTriangle,
|
||||
GitBranch,
|
||||
BarChart3,
|
||||
Cpu,
|
||||
TrendingUp,
|
||||
} from "lucide-react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } 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 { UploadModelDialog } from "@/components/dialogs/upload-model-dialog"
|
||||
|
||||
interface Model {
|
||||
id: string
|
||||
name: string
|
||||
type: "preset" | "custom"
|
||||
category: "clv" | "rfm" | "churn" | "fraud" | "segment"
|
||||
version: string
|
||||
status: "online" | "training" | "testing" | "offline"
|
||||
accuracy: number
|
||||
precision: number
|
||||
recall: number
|
||||
f1Score: number
|
||||
lastTrained: string
|
||||
inputFeatures: string[]
|
||||
outputFormat: string
|
||||
updateFrequency: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const MOCK_MODELS: Model[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "CLV预测模型",
|
||||
type: "preset",
|
||||
category: "clv",
|
||||
version: "v2.3.1",
|
||||
status: "online",
|
||||
accuracy: 87.5,
|
||||
precision: 85.2,
|
||||
recall: 89.1,
|
||||
f1Score: 87.1,
|
||||
lastTrained: "2025-12-10",
|
||||
inputFeatures: ["历史消费金额", "消费频次", "注册时长", "活跃度"],
|
||||
outputFormat: "CLV分数 (0-100)",
|
||||
updateFrequency: "每周",
|
||||
description: "基于用户历史行为预测客户终身价值",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "RFM评分模型",
|
||||
type: "preset",
|
||||
category: "rfm",
|
||||
version: "v1.5.0",
|
||||
status: "online",
|
||||
accuracy: 92.3,
|
||||
precision: 91.5,
|
||||
recall: 93.2,
|
||||
f1Score: 92.3,
|
||||
lastTrained: "2025-12-11",
|
||||
inputFeatures: ["最近消费时间", "消费频率", "消费金额"],
|
||||
outputFormat: "R/F/M各维度评分 (0-100)",
|
||||
updateFrequency: "每天",
|
||||
description: "基于RFM模型的用户价值分层",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "流失预警模型",
|
||||
type: "preset",
|
||||
category: "churn",
|
||||
version: "v3.1.2",
|
||||
status: "online",
|
||||
accuracy: 85.8,
|
||||
precision: 87.3,
|
||||
recall: 84.2,
|
||||
f1Score: 85.7,
|
||||
lastTrained: "2025-12-09",
|
||||
inputFeatures: ["活跃度变化", "消费趋势", "投诉记录", "登录频次"],
|
||||
outputFormat: "流失概率 (0-1)",
|
||||
updateFrequency: "每天",
|
||||
description: "预测用户未来30天流失概率",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "欺诈检测模型",
|
||||
type: "preset",
|
||||
category: "fraud",
|
||||
version: "v2.0.0",
|
||||
status: "online",
|
||||
accuracy: 96.2,
|
||||
precision: 94.8,
|
||||
recall: 97.5,
|
||||
f1Score: 96.1,
|
||||
lastTrained: "2025-12-08",
|
||||
inputFeatures: ["交易金额", "交易频率", "设备信息", "地理位置"],
|
||||
outputFormat: "欺诈风险等级 (低/中/高)",
|
||||
updateFrequency: "实时",
|
||||
description: "实时检测可疑交易行为",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "用户分群模型",
|
||||
type: "custom",
|
||||
category: "segment",
|
||||
version: "v1.0.0",
|
||||
status: "training",
|
||||
accuracy: 78.5,
|
||||
precision: 76.2,
|
||||
recall: 80.1,
|
||||
f1Score: 78.1,
|
||||
lastTrained: "2025-12-12",
|
||||
inputFeatures: ["消费行为", "浏览偏好", "互动记录", "人口属性"],
|
||||
outputFormat: "用户群体标签",
|
||||
updateFrequency: "每周",
|
||||
description: "基于多维特征的用户自动分群",
|
||||
},
|
||||
]
|
||||
|
||||
const CATEGORY_CONFIG = {
|
||||
clv: { label: "CLV模型", color: "bg-blue-100 text-blue-700" },
|
||||
rfm: { label: "RFM模型", color: "bg-green-100 text-green-700" },
|
||||
churn: { label: "流失预警", color: "bg-orange-100 text-orange-700" },
|
||||
fraud: { label: "欺诈检测", color: "bg-red-100 text-red-700" },
|
||||
segment: { label: "用户分群", color: "bg-purple-100 text-purple-700" },
|
||||
}
|
||||
|
||||
const STATUS_CONFIG = {
|
||||
online: { label: "已上线", color: "bg-green-100 text-green-700", icon: CheckCircle },
|
||||
training: { label: "训练中", color: "bg-blue-100 text-blue-700", icon: Cpu },
|
||||
testing: { label: "测试中", color: "bg-yellow-100 text-yellow-700", icon: Clock },
|
||||
offline: { label: "已下线", color: "bg-gray-100 text-gray-700", icon: AlertTriangle },
|
||||
}
|
||||
|
||||
export default function ModelManagementPage() {
|
||||
const [models, setModels] = useState<Model[]>(MOCK_MODELS)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [selectedModel, setSelectedModel] = useState<Model | null>(null)
|
||||
const [showUploadDialog, setShowUploadDialog] = useState(false)
|
||||
|
||||
const stats = {
|
||||
total: models.length,
|
||||
online: models.filter((m) => m.status === "online").length,
|
||||
avgAccuracy: (models.reduce((sum, m) => sum + m.accuracy, 0) / models.length).toFixed(1),
|
||||
presetModels: models.filter((m) => m.type === "preset").length,
|
||||
}
|
||||
|
||||
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={() => setShowUploadDialog(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">
|
||||
<Boxes 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">
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
已上线
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-green-600">{stats.online}</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">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
平均准确率
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.avgAccuracy}%</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">
|
||||
<GitBranch className="w-4 h-4" />
|
||||
预置模型
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.presetModels}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative 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>
|
||||
|
||||
{/* Model List */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{models
|
||||
.filter((m) => m.name.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
.map((model) => {
|
||||
const StatusIcon = STATUS_CONFIG[model.status].icon
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={model.id}
|
||||
className={`border-none shadow-sm bg-white/60 backdrop-blur-sm hover:shadow-md transition-shadow cursor-pointer ${selectedModel?.id === model.id ? "ring-2 ring-blue-500" : ""}`}
|
||||
onClick={() => setSelectedModel(model)}
|
||||
>
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold text-gray-900">{model.name}</h3>
|
||||
<Badge className={CATEGORY_CONFIG[model.category].color}>
|
||||
{CATEGORY_CONFIG[model.category].label}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">{model.description}</p>
|
||||
</div>
|
||||
<Badge className={STATUS_CONFIG[model.status].color}>
|
||||
<StatusIcon className={`w-3 h-3 mr-1 ${model.status === "training" ? "animate-spin" : ""}`} />
|
||||
{STATUS_CONFIG[model.status].label}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4 mb-4">
|
||||
<div className="text-center p-2 rounded-lg bg-gray-50">
|
||||
<div className="text-lg font-bold text-gray-900">{model.accuracy}%</div>
|
||||
<div className="text-xs text-gray-500">准确率</div>
|
||||
</div>
|
||||
<div className="text-center p-2 rounded-lg bg-gray-50">
|
||||
<div className="text-lg font-bold text-gray-900">{model.precision}%</div>
|
||||
<div className="text-xs text-gray-500">精确率</div>
|
||||
</div>
|
||||
<div className="text-center p-2 rounded-lg bg-gray-50">
|
||||
<div className="text-lg font-bold text-gray-900">{model.recall}%</div>
|
||||
<div className="text-xs text-gray-500">召回率</div>
|
||||
</div>
|
||||
<div className="text-center p-2 rounded-lg bg-gray-50">
|
||||
<div className="text-lg font-bold text-gray-900">{model.f1Score}%</div>
|
||||
<div className="text-xs text-gray-500">F1分数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-sm text-gray-500 pt-4 border-t border-gray-100">
|
||||
<div className="flex items-center gap-4">
|
||||
<span>版本: {model.version}</span>
|
||||
<span>更新: {model.updateFrequency}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Play className="w-4 h-4 mr-1" />
|
||||
测试
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Settings className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Model Detail Panel */}
|
||||
{selectedModel && (
|
||||
<Card className="border-none shadow-md bg-white/80 backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<BarChart3 className="w-5 h-5" />
|
||||
模型详情: {selectedModel.name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 mb-3">输入特征</h4>
|
||||
<div className="space-y-2">
|
||||
{selectedModel.inputFeatures.map((feature, i) => (
|
||||
<div key={i} className="flex items-center gap-2 p-2 rounded-lg bg-gray-50">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-500" />
|
||||
<span className="text-sm text-gray-700">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 mb-3">输出格式</h4>
|
||||
<div className="p-3 rounded-lg bg-gray-50">
|
||||
<code className="text-sm text-gray-700">{selectedModel.outputFormat}</code>
|
||||
</div>
|
||||
|
||||
<h4 className="font-medium text-gray-900 mb-3 mt-6">模型指标</h4>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>准确率</span>
|
||||
<span>{selectedModel.accuracy}%</span>
|
||||
</div>
|
||||
<Progress value={selectedModel.accuracy} className="h-2" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>精确率</span>
|
||||
<span>{selectedModel.precision}%</span>
|
||||
</div>
|
||||
<Progress value={selectedModel.precision} className="h-2" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>召回率</span>
|
||||
<span>{selectedModel.recall}%</span>
|
||||
</div>
|
||||
<Progress value={selectedModel.recall} className="h-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<UploadModelDialog open={showUploadDialog} onOpenChange={setShowUploadDialog} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
12
app/value-model/page.tsx
Normal file
12
app/value-model/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
"use client"
|
||||
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect } from "react"
|
||||
|
||||
export default function ValueModelPage() {
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
router.replace("/value-model/models")
|
||||
}, [router])
|
||||
return null
|
||||
}
|
||||
3
app/value-model/reports/loading.tsx
Normal file
3
app/value-model/reports/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
237
app/value-model/reports/page.tsx
Normal file
237
app/value-model/reports/page.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Download, Share2, Eye, Calendar, TrendingUp, TrendingDown, BarChart3, PieChart } from "lucide-react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
|
||||
interface Report {
|
||||
id: string
|
||||
name: string
|
||||
type: "distribution" | "trend" | "comparison"
|
||||
period: string
|
||||
generatedAt: string
|
||||
metrics: {
|
||||
label: string
|
||||
value: string
|
||||
change?: number
|
||||
}[]
|
||||
chartData?: any
|
||||
}
|
||||
|
||||
const MOCK_REPORTS: Report[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "用户价值分布报告",
|
||||
type: "distribution",
|
||||
period: "2025年12月",
|
||||
generatedAt: "2025-12-12 06:00:00",
|
||||
metrics: [
|
||||
{ label: "S级用户", value: "5.2%", change: 0.3 },
|
||||
{ label: "A级用户", value: "12.8%", change: 0.5 },
|
||||
{ label: "B级用户", value: "28.5%", change: -0.2 },
|
||||
{ label: "C级用户", value: "35.2%", change: -0.4 },
|
||||
{ label: "D级用户", value: "18.3%", change: -0.2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "价值趋势对比报告",
|
||||
type: "trend",
|
||||
period: "近6个月",
|
||||
generatedAt: "2025-12-12 06:00:00",
|
||||
metrics: [
|
||||
{ label: "平均CLV", value: "¥2,856", change: 12.5 },
|
||||
{ label: "高价值用户占比", value: "18.0%", change: 2.3 },
|
||||
{ label: "用户价值总量", value: "¥125.6B", change: 8.7 },
|
||||
{ label: "人均贡献", value: "¥31.2", change: 5.2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "项目价值排名报告",
|
||||
type: "comparison",
|
||||
period: "2025年12月",
|
||||
generatedAt: "2025-12-11 22:00:00",
|
||||
metrics: [
|
||||
{ label: "#1 华东区项目", value: "¥28.5B", change: 15.2 },
|
||||
{ label: "#2 华南区项目", value: "¥22.3B", change: 12.8 },
|
||||
{ label: "#3 华北区项目", value: "¥18.9B", change: 8.5 },
|
||||
{ label: "#4 西南区项目", value: "¥15.6B", change: 10.2 },
|
||||
{ label: "#5 华中区项目", value: "¥12.8B", change: 6.3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "流失风险分析报告",
|
||||
type: "distribution",
|
||||
period: "2025年12月",
|
||||
generatedAt: "2025-12-12 00:00:00",
|
||||
metrics: [
|
||||
{ label: "高风险用户", value: "8.5%", change: -1.2 },
|
||||
{ label: "中风险用户", value: "25.3%", change: 0.5 },
|
||||
{ label: "低风险用户", value: "66.2%", change: 0.7 },
|
||||
{ label: "预计流失数", value: "34.2M", change: -5.8 },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const TYPE_CONFIG = {
|
||||
distribution: { label: "分布报告", color: "bg-blue-100 text-blue-700", icon: PieChart },
|
||||
trend: { label: "趋势报告", color: "bg-green-100 text-green-700", icon: TrendingUp },
|
||||
comparison: { label: "对比报告", color: "bg-purple-100 text-purple-700", icon: BarChart3 },
|
||||
}
|
||||
|
||||
export default function AssessmentReportsPage() {
|
||||
const [reports, setReports] = useState<Report[]>(MOCK_REPORTS)
|
||||
const [selectedPeriod, setSelectedPeriod] = useState("month")
|
||||
|
||||
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">
|
||||
<Select value={selectedPeriod} onValueChange={setSelectedPeriod}>
|
||||
<SelectTrigger className="w-32 bg-white/60">
|
||||
<Calendar className="w-4 h-4 mr-2" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="week">本周</SelectItem>
|
||||
<SelectItem value="month">本月</SelectItem>
|
||||
<SelectItem value="quarter">本季度</SelectItem>
|
||||
<SelectItem value="year">本年</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
批量导出
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Card className="border-none shadow-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-2 text-blue-100 mb-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
<span>用户价值总量</span>
|
||||
</div>
|
||||
<div className="text-4xl font-bold mb-2">¥125.6B</div>
|
||||
<div className="flex items-center gap-1 text-green-300 text-sm">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
<span>较上月增长 8.7%</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-none shadow-lg bg-gradient-to-br from-green-500 to-emerald-600 text-white">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-2 text-green-100 mb-2">
|
||||
<BarChart3 className="w-5 h-5" />
|
||||
<span>高价值用户占比</span>
|
||||
</div>
|
||||
<div className="text-4xl font-bold mb-2">18.0%</div>
|
||||
<div className="flex items-center gap-1 text-green-300 text-sm">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
<span>较上月提升 2.3%</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-none shadow-lg bg-gradient-to-br from-purple-500 to-violet-600 text-white">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-2 text-purple-100 mb-2">
|
||||
<PieChart className="w-5 h-5" />
|
||||
<span>平均CLV</span>
|
||||
</div>
|
||||
<div className="text-4xl font-bold mb-2">¥2,856</div>
|
||||
<div className="flex items-center gap-1 text-green-300 text-sm">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
<span>较上月增长 12.5%</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Report List */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{reports.map((report) => {
|
||||
const TypeIcon = TYPE_CONFIG[report.type].icon
|
||||
|
||||
return (
|
||||
<Card key={report.id} className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<CardTitle className="text-lg">{report.name}</CardTitle>
|
||||
<Badge className={TYPE_CONFIG[report.type].color}>
|
||||
<TypeIcon className="w-3 h-3 mr-1" />
|
||||
{TYPE_CONFIG[report.type].label}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">周期: {report.period}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
<Download className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
<Share2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{report.metrics.map((metric, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-3 rounded-xl bg-gray-50">
|
||||
<span className="text-gray-600">{metric.label}</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-bold text-gray-900">{metric.value}</span>
|
||||
{metric.change !== undefined && (
|
||||
<Badge
|
||||
className={
|
||||
metric.change > 0
|
||||
? "bg-green-100 text-green-700"
|
||||
: metric.change < 0
|
||||
? "bg-red-100 text-red-700"
|
||||
: "bg-gray-100 text-gray-700"
|
||||
}
|
||||
>
|
||||
{metric.change > 0 ? (
|
||||
<TrendingUp className="w-3 h-3 mr-1" />
|
||||
) : metric.change < 0 ? (
|
||||
<TrendingDown className="w-3 h-3 mr-1" />
|
||||
) : null}
|
||||
{metric.change > 0 ? "+" : ""}
|
||||
{metric.change}%
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-gray-100 text-xs text-gray-500">
|
||||
生成时间: {report.generatedAt}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user