Files
users/app/data-platform/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

682 lines
26 KiB
TypeScript

"use client"
import { useState } from "react"
import { Database, Plus, Settings, Play, Pause, RotateCcw, Brain, Zap, CheckCircle, AlertCircle } 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 { Progress } from "@/components/ui/progress"
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 { Textarea } from "@/components/ui/textarea"
import { Switch } from "@/components/ui/switch"
interface DataSource {
id: string
name: string
type: string
status: "connected" | "disconnected" | "syncing"
lastSync: string
recordCount: number
description: string
}
interface AIModel {
id: string
name: string
type: string
status: "training" | "ready" | "error"
accuracy: number
lastTrained: string
parameters: Record<string, any>
}
export default function DataPlatformPage() {
const [dataSources, setDataSources] = useState<DataSource[]>([
{
id: "ds_001",
name: "微信用户数据库",
type: "MySQL",
status: "connected",
lastSync: "2024-01-15T10:30:00Z",
recordCount: 2500000000,
description: "存储微信用户基础信息和行为数据",
},
{
id: "ds_002",
name: "流量关键词库",
type: "PostgreSQL",
status: "connected",
lastSync: "2024-01-15T09:45:00Z",
recordCount: 150000,
description: "搜索引擎关键词和流量数据",
},
{
id: "ds_003",
name: "用户行为日志",
type: "MongoDB",
status: "syncing",
lastSync: "2024-01-15T11:00:00Z",
recordCount: 1500000000,
description: "用户操作行为和交互记录",
},
])
const [aiModels, setAiModels] = useState<AIModel[]>([
{
id: "model_001",
name: "用户价值预测模型",
type: "Classification",
status: "ready",
accuracy: 0.92,
lastTrained: "2024-01-14T15:30:00Z",
parameters: {
algorithm: "RandomForest",
features: 25,
epochs: 100,
learningRate: 0.01,
},
},
{
id: "model_002",
name: "流量趋势分析模型",
type: "Regression",
status: "training",
accuracy: 0.87,
lastTrained: "2024-01-15T08:00:00Z",
parameters: {
algorithm: "LSTM",
features: 15,
epochs: 200,
learningRate: 0.001,
},
},
{
id: "model_003",
name: "用户聚类模型",
type: "Clustering",
status: "ready",
accuracy: 0.89,
lastTrained: "2024-01-13T12:00:00Z",
parameters: {
algorithm: "KMeans",
clusters: 8,
features: 20,
iterations: 300,
},
},
])
const [isAddingDataSource, setIsAddingDataSource] = useState(false)
const [isTrainingModel, setIsTrainingModel] = useState(false)
const [newDataSource, setNewDataSource] = useState({
name: "",
type: "MySQL",
host: "",
port: "",
database: "",
username: "",
password: "",
description: "",
})
const [modelTrainingConfig, setModelTrainingConfig] = useState({
modelId: "",
algorithm: "RandomForest",
features: 25,
epochs: 100,
learningRate: 0.01,
validationSplit: 0.2,
autoTune: true,
})
// 添加数据源
const handleAddDataSource = async () => {
try {
const newSource: DataSource = {
id: `ds_${Date.now()}`,
name: newDataSource.name,
type: newDataSource.type,
status: "connected",
lastSync: new Date().toISOString(),
recordCount: 0,
description: newDataSource.description,
}
setDataSources((prev) => [...prev, newSource])
setIsAddingDataSource(false)
setNewDataSource({
name: "",
type: "MySQL",
host: "",
port: "",
database: "",
username: "",
password: "",
description: "",
})
// 模拟数据导入
setTimeout(() => {
setDataSources((prev) =>
prev.map((ds) =>
ds.id === newSource.id
? { ...ds, recordCount: Math.floor(Math.random() * 1000000) + 10000, status: "connected" as const }
: ds,
),
)
}, 2000)
} catch (error) {
console.error("添加数据源失败:", error)
}
}
// 同步数据源
const handleSyncDataSource = (id: string) => {
setDataSources((prev) =>
prev.map((ds) => (ds.id === id ? { ...ds, status: "syncing" as const, lastSync: new Date().toISOString() } : ds)),
)
// 模拟同步完成
setTimeout(() => {
setDataSources((prev) =>
prev.map((ds) =>
ds.id === id
? {
...ds,
status: "connected" as const,
recordCount: ds.recordCount + Math.floor(Math.random() * 10000),
lastSync: new Date().toISOString(),
}
: ds,
),
)
}, 3000)
}
// 训练AI模型
const handleTrainModel = async () => {
if (!modelTrainingConfig.modelId) return
setIsTrainingModel(true)
// 更新模型状态为训练中
setAiModels((prev) =>
prev.map((model) =>
model.id === modelTrainingConfig.modelId ? { ...model, status: "training" as const } : model,
),
)
// 模拟训练过程
setTimeout(() => {
setAiModels((prev) =>
prev.map((model) =>
model.id === modelTrainingConfig.modelId
? {
...model,
status: "ready" as const,
accuracy: Math.random() * 0.1 + 0.85,
lastTrained: new Date().toISOString(),
parameters: {
algorithm: modelTrainingConfig.algorithm,
features: modelTrainingConfig.features,
epochs: modelTrainingConfig.epochs,
learningRate: modelTrainingConfig.learningRate,
},
}
: model,
),
)
setIsTrainingModel(false)
}, 5000)
}
// 格式化数字
const formatNumber = (num: number): string => {
if (num >= 1000000000) {
return `${(num / 1000000000).toFixed(1)}B`
}
if (num >= 1000000) {
return `${(num / 1000000).toFixed(1)}M`
}
if (num >= 1000) {
return `${(num / 1000).toFixed(1)}K`
}
return num.toString()
}
// 获取状态颜色
const getStatusColor = (status: string) => {
switch (status) {
case "connected":
case "ready":
return "text-green-600 bg-green-50 border-green-200"
case "syncing":
case "training":
return "text-yellow-600 bg-yellow-50 border-yellow-200"
case "disconnected":
case "error":
return "text-red-600 bg-red-50 border-red-200"
default:
return "text-gray-600 bg-gray-50 border-gray-200"
}
}
// 获取状态图标
const getStatusIcon = (status: string) => {
switch (status) {
case "connected":
case "ready":
return <CheckCircle className="w-4 h-4" />
case "syncing":
case "training":
return <Zap className="w-4 h-4 animate-pulse" />
case "disconnected":
case "error":
return <AlertCircle className="w-4 h-4" />
default:
return <Database className="w-4 h-4" />
}
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50">
<div className="container mx-auto px-4 py-8">
{/* 页面标题 */}
<div className="mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-2"></h1>
<p className="text-gray-600">AI模型训练平台</p>
</div>
<Tabs defaultValue="datasources" className="space-y-6">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="datasources" className="flex items-center gap-2">
<Database className="w-4 h-4" />
</TabsTrigger>
<TabsTrigger value="aimodels" className="flex items-center gap-2">
<Brain className="w-4 h-4" />
AI模型
</TabsTrigger>
</TabsList>
{/* 数据源管理 */}
<TabsContent value="datasources" className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-semibold"></h2>
<Dialog open={isAddingDataSource} onOpenChange={setIsAddingDataSource}>
<DialogTrigger asChild>
<Button className="flex items-center gap-2">
<Plus className="w-4 h-4" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="grid grid-cols-2 gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="name"></Label>
<Input
id="name"
value={newDataSource.name}
onChange={(e) => setNewDataSource((prev) => ({ ...prev, name: e.target.value }))}
placeholder="输入数据源名称"
/>
</div>
<div className="space-y-2">
<Label htmlFor="type"></Label>
<Select
value={newDataSource.type}
onValueChange={(value) => setNewDataSource((prev) => ({ ...prev, type: value }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="MySQL">MySQL</SelectItem>
<SelectItem value="PostgreSQL">PostgreSQL</SelectItem>
<SelectItem value="MongoDB">MongoDB</SelectItem>
<SelectItem value="Redis">Redis</SelectItem>
<SelectItem value="ClickHouse">ClickHouse</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="host"></Label>
<Input
id="host"
value={newDataSource.host}
onChange={(e) => setNewDataSource((prev) => ({ ...prev, host: e.target.value }))}
placeholder="localhost"
/>
</div>
<div className="space-y-2">
<Label htmlFor="port"></Label>
<Input
id="port"
value={newDataSource.port}
onChange={(e) => setNewDataSource((prev) => ({ ...prev, port: e.target.value }))}
placeholder="3306"
/>
</div>
<div className="space-y-2">
<Label htmlFor="database"></Label>
<Input
id="database"
value={newDataSource.database}
onChange={(e) => setNewDataSource((prev) => ({ ...prev, database: e.target.value }))}
placeholder="database_name"
/>
</div>
<div className="space-y-2">
<Label htmlFor="username"></Label>
<Input
id="username"
value={newDataSource.username}
onChange={(e) => setNewDataSource((prev) => ({ ...prev, username: e.target.value }))}
placeholder="username"
/>
</div>
<div className="col-span-2 space-y-2">
<Label htmlFor="password"></Label>
<Input
id="password"
type="password"
value={newDataSource.password}
onChange={(e) => setNewDataSource((prev) => ({ ...prev, password: e.target.value }))}
placeholder="password"
/>
</div>
<div className="col-span-2 space-y-2">
<Label htmlFor="description"></Label>
<Textarea
id="description"
value={newDataSource.description}
onChange={(e) => setNewDataSource((prev) => ({ ...prev, description: e.target.value }))}
placeholder="数据源描述信息"
rows={3}
/>
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setIsAddingDataSource(false)}>
</Button>
<Button onClick={handleAddDataSource}></Button>
</div>
</DialogContent>
</Dialog>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
{dataSources.map((source) => (
<Card key={source.id} className="border-2 hover:shadow-lg transition-all duration-200">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-lg">{source.name}</CardTitle>
<Badge className={`${getStatusColor(source.status)} border`}>
{getStatusIcon(source.status)}
<span className="ml-1 capitalize">{source.status}</span>
</Badge>
</div>
<p className="text-sm text-gray-600">{source.description}</p>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-500">:</span>
<span className="ml-2 font-medium">{source.type}</span>
</div>
<div>
<span className="text-gray-500">:</span>
<span className="ml-2 font-medium">{formatNumber(source.recordCount)}</span>
</div>
</div>
<div className="text-sm">
<span className="text-gray-500">:</span>
<span className="ml-2">{new Date(source.lastSync).toLocaleString()}</span>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => handleSyncDataSource(source.id)}
disabled={source.status === "syncing"}
className="flex-1"
>
{source.status === "syncing" ? (
<>
<RotateCcw className="w-3 h-3 mr-1 animate-spin" />
</>
) : (
<>
<RotateCcw className="w-3 h-3 mr-1" />
</>
)}
</Button>
<Button size="sm" variant="outline">
<Settings className="w-3 h-3" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
{/* AI模型管理 */}
<TabsContent value="aimodels" className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-semibold">AI模型管理</h2>
<Dialog open={isTrainingModel} onOpenChange={setIsTrainingModel}>
<DialogTrigger asChild>
<Button className="flex items-center gap-2">
<Brain className="w-4 h-4" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="grid grid-cols-2 gap-4 py-4">
<div className="col-span-2 space-y-2">
<Label htmlFor="model"></Label>
<Select
value={modelTrainingConfig.modelId}
onValueChange={(value) => setModelTrainingConfig((prev) => ({ ...prev, modelId: value }))}
>
<SelectTrigger>
<SelectValue placeholder="选择要训练的模型" />
</SelectTrigger>
<SelectContent>
{aiModels.map((model) => (
<SelectItem key={model.id} value={model.id}>
{model.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="algorithm"></Label>
<Select
value={modelTrainingConfig.algorithm}
onValueChange={(value) => setModelTrainingConfig((prev) => ({ ...prev, algorithm: value }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="RandomForest">Random Forest</SelectItem>
<SelectItem value="XGBoost">XGBoost</SelectItem>
<SelectItem value="LSTM">LSTM</SelectItem>
<SelectItem value="KMeans">K-Means</SelectItem>
<SelectItem value="SVM">SVM</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="features"></Label>
<Input
id="features"
type="number"
value={modelTrainingConfig.features}
onChange={(e) =>
setModelTrainingConfig((prev) => ({ ...prev, features: Number.parseInt(e.target.value) }))
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="epochs"></Label>
<Input
id="epochs"
type="number"
value={modelTrainingConfig.epochs}
onChange={(e) =>
setModelTrainingConfig((prev) => ({ ...prev, epochs: Number.parseInt(e.target.value) }))
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="learningRate"></Label>
<Input
id="learningRate"
type="number"
step="0.001"
value={modelTrainingConfig.learningRate}
onChange={(e) =>
setModelTrainingConfig((prev) => ({
...prev,
learningRate: Number.parseFloat(e.target.value),
}))
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="validationSplit"></Label>
<Input
id="validationSplit"
type="number"
step="0.1"
min="0.1"
max="0.5"
value={modelTrainingConfig.validationSplit}
onChange={(e) =>
setModelTrainingConfig((prev) => ({
...prev,
validationSplit: Number.parseFloat(e.target.value),
}))
}
/>
</div>
<div className="col-span-2 flex items-center space-x-2">
<Switch
id="autoTune"
checked={modelTrainingConfig.autoTune}
onCheckedChange={(checked) =>
setModelTrainingConfig((prev) => ({ ...prev, autoTune: checked }))
}
/>
<Label htmlFor="autoTune"></Label>
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setIsTrainingModel(false)}>
</Button>
<Button onClick={handleTrainModel} disabled={!modelTrainingConfig.modelId}>
</Button>
</div>
</DialogContent>
</Dialog>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
{aiModels.map((model) => (
<Card key={model.id} className="border-2 hover:shadow-lg transition-all duration-200">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-lg">{model.name}</CardTitle>
<Badge className={`${getStatusColor(model.status)} border`}>
{getStatusIcon(model.status)}
<span className="ml-1 capitalize">{model.status}</span>
</Badge>
</div>
<p className="text-sm text-gray-600">{model.type} </p>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span className="font-medium">{(model.accuracy * 100).toFixed(1)}%</span>
</div>
<Progress value={model.accuracy * 100} className="h-2" />
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-500">:</span>
<span className="ml-2 font-medium">{model.parameters.algorithm}</span>
</div>
<div>
<span className="text-gray-500">:</span>
<span className="ml-2 font-medium">{model.parameters.features}</span>
</div>
</div>
<div className="text-sm">
<span className="text-gray-500">:</span>
<span className="ml-2">{new Date(model.lastTrained).toLocaleString()}</span>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
disabled={model.status === "training"}
className="flex-1 bg-transparent"
>
{model.status === "training" ? (
<>
<Pause className="w-3 h-3 mr-1" />
</>
) : (
<>
<Play className="w-3 h-3 mr-1" />
</>
)}
</Button>
<Button size="sm" variant="outline">
<Settings className="w-3 h-3" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
</Tabs>
</div>
</div>
)
}