Optimize user detail page for asset assessment and tag info. #VERCEL_SKIP Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
452 lines
17 KiB
TypeScript
452 lines
17 KiB
TypeScript
"use client"
|
||
|
||
import { useState } from "react"
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Label } from "@/components/ui/label"
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||
import { Database, Plus, RefreshCw, Settings, Brain, Play } from "lucide-react"
|
||
import BottomTabs from "@/components/nav/bottom-tabs"
|
||
|
||
interface DataSource {
|
||
id: string
|
||
name: string
|
||
description: string
|
||
type: string
|
||
records: string
|
||
lastSync: string
|
||
status: "connected" | "disconnected" | "syncing"
|
||
}
|
||
|
||
interface AIModel {
|
||
id: string
|
||
name: string
|
||
type: string
|
||
accuracy: string
|
||
algorithm: string
|
||
features: number
|
||
lastTrained: string
|
||
status: "ready" | "training" | "error"
|
||
}
|
||
|
||
const mockDataSources: DataSource[] = [
|
||
{
|
||
id: "wechat-db",
|
||
name: "微信用户数据库",
|
||
description: "存储微信用户基础信息和行为数据",
|
||
type: "MySQL",
|
||
records: "2.5B",
|
||
lastSync: "2024/1/15 18:30:00",
|
||
status: "connected",
|
||
},
|
||
{
|
||
id: "traffic-keywords",
|
||
name: "流量关键词库",
|
||
description: "搜索引擎关键词和流量数据",
|
||
type: "PostgreSQL",
|
||
records: "150.0K",
|
||
lastSync: "2024/1/15 17:45:00",
|
||
status: "connected",
|
||
},
|
||
{
|
||
id: "user-behavior",
|
||
name: "用户行为日志",
|
||
description: "用户操作行为和交互记录",
|
||
type: "MongoDB",
|
||
records: "1.5B",
|
||
lastSync: "2024/1/15 19:00:00",
|
||
status: "syncing",
|
||
},
|
||
]
|
||
|
||
const mockAIModels: AIModel[] = [
|
||
{
|
||
id: "user-value-prediction",
|
||
name: "用户价值预测模型",
|
||
type: "Classification",
|
||
accuracy: "92.0%",
|
||
algorithm: "RandomForest",
|
||
features: 25,
|
||
lastTrained: "2024/1/14 23:30:00",
|
||
status: "ready",
|
||
},
|
||
{
|
||
id: "traffic-trend-analysis",
|
||
name: "流量趋势分析模型",
|
||
type: "Regression",
|
||
accuracy: "87.0%",
|
||
algorithm: "LSTM",
|
||
features: 15,
|
||
lastTrained: "2024/1/15 16:00:00",
|
||
status: "training",
|
||
},
|
||
{
|
||
id: "user-clustering",
|
||
name: "用户聚类模型",
|
||
type: "Clustering",
|
||
accuracy: "89.0%",
|
||
algorithm: "KMeans",
|
||
features: 20,
|
||
lastTrained: "2024/1/13 20:00:00",
|
||
status: "ready",
|
||
},
|
||
]
|
||
|
||
export default function DataPlatformPage() {
|
||
const [activeTab, setActiveTab] = useState("datasource")
|
||
const [dataSources, setDataSources] = useState<DataSource[]>(mockDataSources)
|
||
const [aiModels, setAIModels] = useState<AIModel[]>(mockAIModels)
|
||
const [syncing, setSyncing] = useState<string | null>(null)
|
||
const [training, setTraining] = useState<string | null>(null)
|
||
const [showAddDialog, setShowAddDialog] = useState(false)
|
||
const [newDataSource, setNewDataSource] = useState({
|
||
name: "",
|
||
description: "",
|
||
type: "MySQL",
|
||
host: "",
|
||
port: "",
|
||
database: "",
|
||
username: "",
|
||
password: "",
|
||
})
|
||
|
||
const handleSync = async (sourceId: string) => {
|
||
setSyncing(sourceId)
|
||
setTimeout(() => {
|
||
setSyncing(null)
|
||
setDataSources((prev) =>
|
||
prev.map((source) =>
|
||
source.id === sourceId ? { ...source, lastSync: new Date().toLocaleString("zh-CN") } : source,
|
||
),
|
||
)
|
||
}, 2000)
|
||
}
|
||
|
||
const handleRetrain = async (modelId: string) => {
|
||
setTraining(modelId)
|
||
setTimeout(() => {
|
||
setTraining(null)
|
||
setAIModels((prev) =>
|
||
prev.map((model) =>
|
||
model.id === modelId ? { ...model, lastTrained: new Date().toLocaleString("zh-CN") } : model,
|
||
),
|
||
)
|
||
}, 3000)
|
||
}
|
||
|
||
const handleAddDataSource = () => {
|
||
const newSource: DataSource = {
|
||
id: `datasource-${Date.now()}`,
|
||
name: newDataSource.name,
|
||
description: newDataSource.description,
|
||
type: newDataSource.type,
|
||
records: "0",
|
||
lastSync: "从未同步",
|
||
status: "disconnected",
|
||
}
|
||
setDataSources((prev) => [...prev, newSource])
|
||
setNewDataSource({
|
||
name: "",
|
||
description: "",
|
||
type: "MySQL",
|
||
host: "",
|
||
port: "",
|
||
database: "",
|
||
username: "",
|
||
password: "",
|
||
})
|
||
setShowAddDialog(false)
|
||
}
|
||
|
||
const getStatusBadge = (status: DataSource["status"]) => {
|
||
switch (status) {
|
||
case "connected":
|
||
return <Badge className="bg-green-100 text-green-800 border-green-200">Connected</Badge>
|
||
case "disconnected":
|
||
return <Badge variant="destructive">Disconnected</Badge>
|
||
case "syncing":
|
||
return <Badge className="bg-blue-100 text-blue-800 border-blue-200">Syncing</Badge>
|
||
default:
|
||
return <Badge variant="secondary">Unknown</Badge>
|
||
}
|
||
}
|
||
|
||
const getModelStatusBadge = (status: AIModel["status"]) => {
|
||
switch (status) {
|
||
case "ready":
|
||
return <Badge className="bg-green-100 text-green-800 border-green-200">Ready</Badge>
|
||
case "training":
|
||
return <Badge className="bg-yellow-100 text-yellow-800 border-yellow-200">Training</Badge>
|
||
case "error":
|
||
return <Badge variant="destructive">Error</Badge>
|
||
default:
|
||
return <Badge variant="secondary">Unknown</Badge>
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50">
|
||
<div className="bg-white border-b">
|
||
<div className="container mx-auto px-4 py-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-gray-900">数据中台</h1>
|
||
<p className="text-gray-600 mt-1">数据源管理与AI模型训练平台</p>
|
||
</div>
|
||
{activeTab === "datasource" ? (
|
||
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
|
||
<DialogTrigger asChild>
|
||
<Button className="flex items-center gap-2">
|
||
<Plus className="w-4 h-4" />
|
||
添加数据源
|
||
</Button>
|
||
</DialogTrigger>
|
||
<DialogContent className="sm:max-w-[425px]">
|
||
<DialogHeader>
|
||
<DialogTitle>添加新数据源</DialogTitle>
|
||
</DialogHeader>
|
||
<div className="grid gap-4 py-4">
|
||
<div className="grid grid-cols-4 items-center gap-4">
|
||
<Label htmlFor="name" className="text-right">
|
||
名称
|
||
</Label>
|
||
<Input
|
||
id="name"
|
||
value={newDataSource.name}
|
||
onChange={(e) => setNewDataSource({ ...newDataSource, name: e.target.value })}
|
||
className="col-span-3"
|
||
/>
|
||
</div>
|
||
<div className="grid grid-cols-4 items-center gap-4">
|
||
<Label htmlFor="description" className="text-right">
|
||
描述
|
||
</Label>
|
||
<Input
|
||
id="description"
|
||
value={newDataSource.description}
|
||
onChange={(e) => setNewDataSource({ ...newDataSource, description: e.target.value })}
|
||
className="col-span-3"
|
||
/>
|
||
</div>
|
||
<div className="grid grid-cols-4 items-center gap-4">
|
||
<Label htmlFor="type" className="text-right">
|
||
类型
|
||
</Label>
|
||
<Select
|
||
value={newDataSource.type}
|
||
onValueChange={(value) => setNewDataSource({ ...newDataSource, type: value })}
|
||
>
|
||
<SelectTrigger className="col-span-3">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="MySQL">MySQL</SelectItem>
|
||
<SelectItem value="PostgreSQL">PostgreSQL</SelectItem>
|
||
<SelectItem value="MongoDB">MongoDB</SelectItem>
|
||
<SelectItem value="Redis">Redis</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="grid grid-cols-4 items-center gap-4">
|
||
<Label htmlFor="host" className="text-right">
|
||
主机
|
||
</Label>
|
||
<Input
|
||
id="host"
|
||
value={newDataSource.host}
|
||
onChange={(e) => setNewDataSource({ ...newDataSource, host: e.target.value })}
|
||
className="col-span-3"
|
||
placeholder="localhost"
|
||
/>
|
||
</div>
|
||
<div className="grid grid-cols-4 items-center gap-4">
|
||
<Label htmlFor="port" className="text-right">
|
||
端口
|
||
</Label>
|
||
<Input
|
||
id="port"
|
||
value={newDataSource.port}
|
||
onChange={(e) => setNewDataSource({ ...newDataSource, port: e.target.value })}
|
||
className="col-span-3"
|
||
placeholder="3306"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="flex justify-end gap-2">
|
||
<Button variant="outline" onClick={() => setShowAddDialog(false)}>
|
||
取消
|
||
</Button>
|
||
<Button onClick={handleAddDataSource} disabled={!newDataSource.name || !newDataSource.type}>
|
||
添加
|
||
</Button>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
) : (
|
||
<Button className="flex items-center gap-2" onClick={() => handleRetrain("all")}>
|
||
<RefreshCw className="w-4 h-4" />
|
||
重新训练模型
|
||
</Button>
|
||
)}
|
||
</div>
|
||
|
||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||
<TabsList className="grid w-full grid-cols-2">
|
||
<TabsTrigger value="datasource" className="flex items-center gap-2">
|
||
<Database className="w-4 h-4" />
|
||
数据源管理
|
||
</TabsTrigger>
|
||
<TabsTrigger value="aimodel" className="flex items-center gap-2">
|
||
<Brain className="w-4 h-4" />
|
||
AI模型
|
||
</TabsTrigger>
|
||
</TabsList>
|
||
</Tabs>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="container mx-auto px-4 py-6">
|
||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||
<TabsContent value="datasource" className="space-y-4">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<h2 className="text-xl font-semibold">数据源管理</h2>
|
||
</div>
|
||
|
||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||
{dataSources.map((source) => (
|
||
<Card key={source.id} className="bg-white">
|
||
<CardHeader className="pb-4">
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex-1">
|
||
<CardTitle className="text-lg font-semibold mb-2">{source.name}</CardTitle>
|
||
<p className="text-gray-600 text-sm mb-3">{source.description}</p>
|
||
<div className="space-y-1 text-sm text-gray-500">
|
||
<div>
|
||
类型:<span className="font-medium">{source.type}</span>
|
||
</div>
|
||
<div>
|
||
记录数:<span className="font-medium">{source.records}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
{getStatusBadge(syncing === source.id ? "syncing" : source.status)}
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="pt-0">
|
||
<div className="space-y-3">
|
||
<div className="text-sm text-gray-500">最后同步:{source.lastSync}</div>
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handleSync(source.id)}
|
||
disabled={syncing === source.id}
|
||
className="flex items-center gap-2 flex-1"
|
||
>
|
||
<RefreshCw className={`w-4 h-4 ${syncing === source.id ? "animate-spin" : ""}`} />
|
||
{syncing === source.id ? "同步中" : "同步数据"}
|
||
</Button>
|
||
<Button variant="ghost" size="sm">
|
||
<Settings className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="aimodel" className="space-y-4">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<h2 className="text-xl font-semibold">AI模型管理</h2>
|
||
</div>
|
||
|
||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||
{aiModels.map((model) => (
|
||
<Card key={model.id} className="bg-white">
|
||
<CardHeader className="pb-4">
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex-1">
|
||
<CardTitle className="text-lg font-semibold mb-2">{model.name}</CardTitle>
|
||
<p className="text-gray-600 text-sm mb-3">{model.type} 模型</p>
|
||
|
||
<div className="mb-3">
|
||
<div className="flex justify-between text-sm mb-1">
|
||
<span>准确率</span>
|
||
<span className="font-medium">{model.accuracy}</span>
|
||
</div>
|
||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||
<div className="bg-gray-900 h-2 rounded-full" style={{ width: model.accuracy }}></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-1 text-sm text-gray-500">
|
||
<div>
|
||
算法:<span className="font-medium">{model.algorithm}</span>
|
||
</div>
|
||
<div>
|
||
特征数:<span className="font-medium">{model.features}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
{getModelStatusBadge(training === model.id ? "training" : model.status)}
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="pt-0">
|
||
<div className="space-y-3">
|
||
<div className="text-sm text-gray-500">最后训练:{model.lastTrained}</div>
|
||
<div className="flex items-center gap-2">
|
||
{model.status === "ready" ? (
|
||
<Button variant="outline" size="sm" className="flex items-center gap-2 flex-1 bg-transparent">
|
||
<Play className="w-4 h-4" />
|
||
预测
|
||
</Button>
|
||
) : model.status === "training" ? (
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
disabled
|
||
className="flex items-center gap-2 flex-1 bg-transparent"
|
||
>
|
||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||
训练中
|
||
</Button>
|
||
) : (
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handleRetrain(model.id)}
|
||
disabled={training === model.id}
|
||
className="flex items-center gap-2 flex-1"
|
||
>
|
||
<RefreshCw className={`w-4 h-4 ${training === model.id ? "animate-spin" : ""}`} />
|
||
重新训练
|
||
</Button>
|
||
)}
|
||
<Button variant="ghost" size="sm">
|
||
<Settings className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
</TabsContent>
|
||
</Tabs>
|
||
</div>
|
||
|
||
<BottomTabs />
|
||
</div>
|
||
)
|
||
}
|