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/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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user