feat: enhance user profile with detailed tags and asset evaluation
Optimize user detail page for asset assessment and tag info. #VERCEL_SKIP Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
@@ -1,340 +1,246 @@
|
||||
"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"
|
||||
import { Database, Plus, RefreshCw, Settings, Brain, Play } from "lucide-react"
|
||||
import BottomTabs from "@/components/nav/bottom-tabs"
|
||||
|
||||
interface DataSource {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
status: "connected" | "disconnected" | "syncing"
|
||||
lastSync: string
|
||||
recordCount: number
|
||||
description: string
|
||||
type: string
|
||||
records: string
|
||||
lastSync: string
|
||||
status: "connected" | "disconnected" | "syncing"
|
||||
}
|
||||
|
||||
interface AIModel {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
status: "training" | "ready" | "error"
|
||||
accuracy: number
|
||||
accuracy: string
|
||||
algorithm: string
|
||||
features: number
|
||||
lastTrained: string
|
||||
parameters: Record<string, any>
|
||||
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 [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 [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: "",
|
||||
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 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 handleSyncDataSource = (id: string) => {
|
||||
setDataSources((prev) =>
|
||||
prev.map((ds) => (ds.id === id ? { ...ds, status: "syncing" as const, lastSync: new Date().toISOString() } : ds)),
|
||||
)
|
||||
|
||||
// 模拟同步完成
|
||||
const handleRetrain = async (modelId: string) => {
|
||||
setTraining(modelId)
|
||||
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,
|
||||
setTraining(null)
|
||||
setAIModels((prev) =>
|
||||
prev.map((model) =>
|
||||
model.id === modelId ? { ...model, lastTrained: new Date().toLocaleString("zh-CN") } : model,
|
||||
),
|
||||
)
|
||||
}, 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 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 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) => {
|
||||
const getStatusBadge = (status: DataSource["status"]) => {
|
||||
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"
|
||||
return <Badge className="bg-green-100 text-green-800 border-green-200">Connected</Badge>
|
||||
case "disconnected":
|
||||
case "error":
|
||||
return "text-red-600 bg-red-50 border-red-200"
|
||||
return <Badge variant="destructive">Disconnected</Badge>
|
||||
case "syncing":
|
||||
return <Badge className="bg-blue-100 text-blue-800 border-blue-200">Syncing</Badge>
|
||||
default:
|
||||
return "text-gray-600 bg-gray-50 border-gray-200"
|
||||
return <Badge variant="secondary">Unknown</Badge>
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态图标
|
||||
const getStatusIcon = (status: string) => {
|
||||
const getModelStatusBadge = (status: AIModel["status"]) => {
|
||||
switch (status) {
|
||||
case "connected":
|
||||
case "ready":
|
||||
return <CheckCircle className="w-4 h-4" />
|
||||
case "syncing":
|
||||
return <Badge className="bg-green-100 text-green-800 border-green-200">Ready</Badge>
|
||||
case "training":
|
||||
return <Zap className="w-4 h-4 animate-pulse" />
|
||||
case "disconnected":
|
||||
return <Badge className="bg-yellow-100 text-yellow-800 border-yellow-200">Training</Badge>
|
||||
case "error":
|
||||
return <AlertCircle className="w-4 h-4" />
|
||||
return <Badge variant="destructive">Error</Badge>
|
||||
default:
|
||||
return <Database className="w-4 h-4" />
|
||||
return <Badge variant="secondary">Unknown</Badge>
|
||||
}
|
||||
}
|
||||
|
||||
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}>
|
||||
<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="max-w-2xl">
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>添加新数据源</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid grid-cols-2 gap-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">数据源名称</Label>
|
||||
<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((prev) => ({ ...prev, name: e.target.value }))}
|
||||
placeholder="输入数据源名称"
|
||||
onChange={(e) => setNewDataSource({ ...newDataSource, name: e.target.value })}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="type">数据库类型</Label>
|
||||
<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((prev) => ({ ...prev, type: value }))}
|
||||
onValueChange={(value) => setNewDataSource({ ...newDataSource, type: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -342,130 +248,114 @@ export default function DataPlatformPage() {
|
||||
<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>
|
||||
<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((prev) => ({ ...prev, host: e.target.value }))}
|
||||
onChange={(e) => setNewDataSource({ ...newDataSource, host: e.target.value })}
|
||||
className="col-span-3"
|
||||
placeholder="localhost"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="port">端口</Label>
|
||||
<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((prev) => ({ ...prev, port: e.target.value }))}
|
||||
onChange={(e) => setNewDataSource({ ...newDataSource, port: e.target.value })}
|
||||
className="col-span-3"
|
||||
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 variant="outline" onClick={() => setShowAddDialog(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleAddDataSource}>连接并导入数据</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 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
@@ -473,201 +363,79 @@ export default function DataPlatformPage() {
|
||||
</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>
|
||||
<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 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{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>
|
||||
<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>
|
||||
<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" />
|
||||
<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>
|
||||
<Button size="sm" variant="outline">
|
||||
<Settings className="w-3 h-3" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Settings className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -676,6 +444,8 @@ export default function DataPlatformPage() {
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<BottomTabs />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user