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:
v0
2026-01-31 04:32:36 +00:00
parent 22e725887a
commit b17b488f8e
105 changed files with 20530 additions and 3622 deletions

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

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

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

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

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

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