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,144 @@
"use client"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { ScrollArea } from "@/components/ui/scroll-area"
import { FileText, Download, RefreshCw, Brain, CheckCircle, XCircle, Clock } from "lucide-react"
interface AICleaningLogsDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
ruleName?: string
}
const AI_LOGS = [
{
time: "14:35:12",
type: "system",
message: "AI清洗任务启动",
details: "规则: 用户数据智能清洗 | 模型: GPT-4",
},
{
time: "14:35:13",
type: "ai",
message: "分析提示词",
details: "解析清洗意图: 手机号格式化、邮箱校验、地址补全",
},
{
time: "14:35:15",
type: "system",
message: "读取数据批次 1/10",
details: "共 12,895 条记录",
},
{
time: "14:35:18",
type: "ai",
message: "AI分析完成",
details: "发现 156 条需要清洗的记录, 置信度范围: 0.72-0.98",
},
{
time: "14:35:20",
type: "review",
message: "自动审核通过",
details: "142 条记录置信度 >= 0.95, 已自动应用",
},
{
time: "14:35:21",
type: "review",
message: "待人工审核",
details: "14 条记录置信度 < 0.95, 等待审核",
},
{
time: "14:35:25",
type: "system",
message: "批次 1 处理完成",
details: "成功: 12,881 | 待审核: 14 | 跳过: 0",
},
{
time: "14:36:45",
type: "ai",
message: "AI建议",
details: "检测到 '86-' 前缀手机号模式, 建议添加自动化规则",
},
]
export function AICleaningLogsDialog({ open, onOpenChange, ruleName }: AICleaningLogsDialogProps) {
const getTypeIcon = (type: string) => {
switch (type) {
case "ai":
return <Brain className="w-4 h-4 text-violet-500" />
case "review":
return <CheckCircle className="w-4 h-4 text-emerald-500" />
case "error":
return <XCircle className="w-4 h-4 text-red-500" />
default:
return <Clock className="w-4 h-4 text-blue-500" />
}
}
const getTypeBadge = (type: string) => {
switch (type) {
case "ai":
return <Badge className="bg-violet-100 text-violet-700">AI</Badge>
case "review":
return <Badge className="bg-emerald-100 text-emerald-700"></Badge>
case "error":
return <Badge className="bg-red-100 text-red-700"></Badge>
default:
return <Badge className="bg-blue-100 text-blue-700"></Badge>
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[80vh]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FileText className="w-5 h-5" />
AI清洗日志 - {ruleName || "清洗规则"}
</DialogTitle>
<DialogDescription>AI清洗规则的执行日志和AI分析过程</DialogDescription>
</DialogHeader>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-4">
<Badge variant="outline" className="gap-1">
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
</Badge>
<span className="text-sm text-slate-500">已运行: 1分33秒</span>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm">
<RefreshCw className="w-4 h-4 mr-1" />
</Button>
<Button variant="outline" size="sm">
<Download className="w-4 h-4 mr-1" />
</Button>
</div>
</div>
<ScrollArea className="h-[400px] border rounded-lg">
<div className="p-4 space-y-3">
{AI_LOGS.map((log, i) => (
<div key={i} className="flex gap-4 p-3 bg-slate-50 rounded-lg">
<div className="flex-shrink-0 mt-0.5">{getTypeIcon(log.type)}</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs text-slate-500">{log.time}</span>
{getTypeBadge(log.type)}
<span className="font-medium text-slate-800">{log.message}</span>
</div>
<p className="text-sm text-slate-600">{log.details}</p>
</div>
</div>
))}
</div>
</ScrollArea>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,238 @@
"use client"
import { useState } from "react"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Settings, Bell, Users, Clock } from "lucide-react"
import { Checkbox } from "@/components/ui/checkbox"
interface AlertRuleSettingsDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
rule?: {
name: string
condition: string
severity: string
}
}
export function AlertRuleSettingsDialog({ open, onOpenChange, rule }: AlertRuleSettingsDialogProps) {
const [activeTab, setActiveTab] = useState("condition")
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Settings className="w-5 h-5" />
- {rule?.name || "告警规则"}
</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="condition"></TabsTrigger>
<TabsTrigger value="notify"></TabsTrigger>
<TabsTrigger value="suppress"></TabsTrigger>
<TabsTrigger value="action"></TabsTrigger>
</TabsList>
<TabsContent value="condition" className="space-y-4 mt-4">
<div className="space-y-2">
<Label></Label>
<Input defaultValue={rule?.name} />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Select defaultValue="cpu">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="cpu">CPU使用率</SelectItem>
<SelectItem value="memory">使</SelectItem>
<SelectItem value="disk">使</SelectItem>
<SelectItem value="network"></SelectItem>
<SelectItem value="api_latency">API延迟</SelectItem>
<SelectItem value="error_rate"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Select defaultValue="gt">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="gt"></SelectItem>
<SelectItem value="gte"></SelectItem>
<SelectItem value="lt"></SelectItem>
<SelectItem value="lte"></SelectItem>
<SelectItem value="eq"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Input type="number" defaultValue="80" />
</div>
<div className="space-y-2">
<Label></Label>
<Select defaultValue="5">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1</SelectItem>
<SelectItem value="3">3</SelectItem>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="15">15</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label></Label>
<Select defaultValue={rule?.severity || "warning"}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="info"></SelectItem>
<SelectItem value="warning"></SelectItem>
<SelectItem value="critical"></SelectItem>
</SelectContent>
</Select>
</div>
</TabsContent>
<TabsContent value="notify" className="space-y-4 mt-4">
<div className="flex items-center gap-2 mb-2">
<Bell className="w-4 h-4 text-blue-500" />
<span className="font-medium"></span>
</div>
<div className="space-y-3">
{[
{ id: "email", label: "邮件通知", checked: true },
{ id: "sms", label: "短信通知", checked: true },
{ id: "webhook", label: "Webhook", checked: false },
{ id: "dingtalk", label: "钉钉群", checked: true },
{ id: "wechat", label: "企业微信", checked: false },
].map((channel) => (
<div key={channel.id} className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<Checkbox defaultChecked={channel.checked} />
<span className="text-sm font-medium">{channel.label}</span>
</div>
))}
</div>
<div className="space-y-2">
<Label className="flex items-center gap-2">
<Users className="w-4 h-4" />
</Label>
<Select defaultValue="ops">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ops"></SelectItem>
<SelectItem value="dev"></SelectItem>
<SelectItem value="manager"></SelectItem>
<SelectItem value="all"></SelectItem>
</SelectContent>
</Select>
</div>
</TabsContent>
<TabsContent value="suppress" className="space-y-4 mt-4">
<div className="flex items-center gap-2 mb-2">
<Clock className="w-4 h-4 text-amber-500" />
<span className="font-medium"></span>
</div>
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg">
<div>
<p className="font-medium"></p>
<p className="text-sm text-slate-500"></p>
</div>
<Switch defaultChecked />
</div>
<div className="space-y-2">
<Label> ()</Label>
<Input type="number" defaultValue="30" />
</div>
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg">
<div>
<p className="font-medium"></p>
<p className="text-sm text-slate-500"></p>
</div>
<Switch />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Input type="time" defaultValue="22:00" />
</div>
<div className="space-y-2">
<Label></Label>
<Input type="time" defaultValue="08:00" />
</div>
</div>
</TabsContent>
<TabsContent value="action" className="space-y-4 mt-4">
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg">
<div>
<p className="font-medium"></p>
<p className="text-sm text-slate-500"></p>
</div>
<Switch />
</div>
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg">
<div>
<p className="font-medium"></p>
<p className="text-sm text-slate-500"></p>
</div>
<Switch />
</div>
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg">
<div>
<p className="font-medium"></p>
<p className="text-sm text-slate-500"></p>
</div>
<Switch />
</div>
<div className="space-y-2">
<Label></Label>
<Input placeholder="输入告警触发时执行的脚本路径" />
</div>
</TabsContent>
</Tabs>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={() => onOpenChange(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,222 @@
"use client"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Code, Copy, ExternalLink } from "lucide-react"
interface APIDocDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
api?: {
name: string
endpoint: string
method: string
description: string
}
}
export function APIDocDialog({ open, onOpenChange, api }: APIDocDialogProps) {
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[80vh]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Code className="w-5 h-5" />
API文档 - {api?.name || "API服务"}
</DialogTitle>
<DialogDescription>{api?.description}</DialogDescription>
</DialogHeader>
<div className="flex items-center gap-2 p-3 bg-slate-50 rounded-lg">
<Badge className={api?.method === "GET" ? "bg-blue-100 text-blue-700" : "bg-green-100 text-green-700"}>
{api?.method || "GET"}
</Badge>
<code className="flex-1 text-sm">{api?.endpoint || "/api/v1/example"}</code>
<Button variant="ghost" size="sm" onClick={() => copyToClipboard(api?.endpoint || "")}>
<Copy className="w-4 h-4" />
</Button>
</div>
<Tabs defaultValue="request">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="request"></TabsTrigger>
<TabsTrigger value="response"></TabsTrigger>
<TabsTrigger value="example"></TabsTrigger>
<TabsTrigger value="errors"></TabsTrigger>
</TabsList>
<TabsContent value="request" className="mt-4">
<ScrollArea className="h-[300px]">
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="text-left py-2 px-3"></th>
<th className="text-left py-2 px-3"></th>
<th className="text-left py-2 px-3"></th>
<th className="text-left py-2 px-3"></th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="py-2 px-3">
<code>user_id</code>
</td>
<td className="py-2 px-3">string</td>
<td className="py-2 px-3">
<Badge className="bg-red-100 text-red-700"></Badge>
</td>
<td className="py-2 px-3"></td>
</tr>
<tr className="border-b">
<td className="py-2 px-3">
<code>fields</code>
</td>
<td className="py-2 px-3">array</td>
<td className="py-2 px-3">
<Badge variant="outline"></Badge>
</td>
<td className="py-2 px-3"></td>
</tr>
<tr className="border-b">
<td className="py-2 px-3">
<code>include_tags</code>
</td>
<td className="py-2 px-3">boolean</td>
<td className="py-2 px-3">
<Badge variant="outline"></Badge>
</td>
<td className="py-2 px-3">, true</td>
</tr>
</tbody>
</table>
</ScrollArea>
</TabsContent>
<TabsContent value="response" className="mt-4">
<ScrollArea className="h-[300px]">
<pre className="p-4 bg-slate-900 rounded-lg text-sm text-green-400 overflow-x-auto">
{`{
"code": 0,
"message": "success",
"data": {
"user_id": "u_123456",
"name": "张三",
"phone": "138****5678",
"email": "zhang***@example.com",
"tags": ["高价值", "活跃用户", "VIP"],
"portrait": {
"age_group": "25-35",
"gender": "male",
"city": "上海",
"consumption_level": "high"
},
"value_score": {
"rfm_score": 85,
"ltv": 12580,
"churn_risk": 0.12
}
}
}`}
</pre>
</ScrollArea>
</TabsContent>
<TabsContent value="example" className="mt-4">
<ScrollArea className="h-[300px]">
<div className="space-y-4">
<div>
<h4 className="text-sm font-medium mb-2">cURL</h4>
<pre className="p-4 bg-slate-900 rounded-lg text-sm text-green-400 overflow-x-auto">
{`curl -X GET "https://api.shenshoushou.com/api/v1/user/portrait?user_id=u_123456" \\
-H "Authorization: Bearer YOUR_API_KEY" \\
-H "Content-Type: application/json"`}
</pre>
</div>
<div>
<h4 className="text-sm font-medium mb-2">JavaScript</h4>
<pre className="p-4 bg-slate-900 rounded-lg text-sm text-green-400 overflow-x-auto">
{`const response = await fetch(
"https://api.shenshoushou.com/api/v1/user/portrait?user_id=u_123456",
{
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json"
}
}
);
const data = await response.json();`}
</pre>
</div>
</div>
</ScrollArea>
</TabsContent>
<TabsContent value="errors" className="mt-4">
<ScrollArea className="h-[300px]">
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="text-left py-2 px-3"></th>
<th className="text-left py-2 px-3"></th>
<th className="text-left py-2 px-3"></th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="py-2 px-3">
<code>400</code>
</td>
<td className="py-2 px-3"></td>
<td className="py-2 px-3"></td>
</tr>
<tr className="border-b">
<td className="py-2 px-3">
<code>401</code>
</td>
<td className="py-2 px-3"></td>
<td className="py-2 px-3">API Key是否正确</td>
</tr>
<tr className="border-b">
<td className="py-2 px-3">
<code>404</code>
</td>
<td className="py-2 px-3"></td>
<td className="py-2 px-3">ID是否正确</td>
</tr>
<tr className="border-b">
<td className="py-2 px-3">
<code>429</code>
</td>
<td className="py-2 px-3"></td>
<td className="py-2 px-3"></td>
</tr>
<tr className="border-b">
<td className="py-2 px-3">
<code>500</code>
</td>
<td className="py-2 px-3"></td>
<td className="py-2 px-3"></td>
</tr>
</tbody>
</table>
</ScrollArea>
</TabsContent>
</Tabs>
<div className="flex justify-end gap-2">
<Button variant="outline">
<ExternalLink className="w-4 h-4 mr-2" />
线
</Button>
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,164 @@
"use client"
import { useState } from "react"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Badge } from "@/components/ui/badge"
import { Key, Copy, Eye, EyeOff, Plus, Trash2, CheckCircle } from "lucide-react"
interface ApiKeyManagementDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
const MOCK_KEYS = [
{
id: 1,
name: "生产环境密钥",
key: "sk_live_xxxxxxxxxxxx",
created: "2024-12-01",
lastUsed: "刚刚",
status: "active",
},
{
id: 2,
name: "测试环境密钥",
key: "sk_test_xxxxxxxxxxxx",
created: "2024-11-15",
lastUsed: "3天前",
status: "active",
},
{
id: 3,
name: "开发调试密钥",
key: "sk_dev_xxxxxxxxxxxx",
created: "2024-10-20",
lastUsed: "1周前",
status: "inactive",
},
]
export function ApiKeyManagementDialog({ open, onOpenChange }: ApiKeyManagementDialogProps) {
const [keys, setKeys] = useState(MOCK_KEYS)
const [showKey, setShowKey] = useState<number | null>(null)
const [newKeyName, setNewKeyName] = useState("")
const [showNewKeyForm, setShowNewKeyForm] = useState(false)
const [copiedId, setCopiedId] = useState<number | null>(null)
const copyKey = (id: number, key: string) => {
navigator.clipboard.writeText(key)
setCopiedId(id)
setTimeout(() => setCopiedId(null), 2000)
}
const createKey = () => {
if (newKeyName) {
setKeys([
...keys,
{
id: Date.now(),
name: newKeyName,
key: `sk_live_${Math.random().toString(36).substr(2, 12)}`,
created: new Date().toISOString().split("T")[0],
lastUsed: "从未",
status: "active",
},
])
setNewKeyName("")
setShowNewKeyForm(false)
}
}
const deleteKey = (id: number) => {
setKeys(keys.filter((k) => k.id !== id))
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Key className="w-5 h-5" />
API密钥管理
</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="flex justify-end">
<Button size="sm" onClick={() => setShowNewKeyForm(true)}>
<Plus className="w-4 h-4 mr-1" />
</Button>
</div>
{showNewKeyForm && (
<div className="p-4 border rounded-lg bg-gray-50 space-y-3">
<Label></Label>
<div className="flex gap-2">
<Input
placeholder="例如:生产环境密钥"
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
/>
<Button onClick={createKey}></Button>
<Button variant="outline" onClick={() => setShowNewKeyForm(false)}>
</Button>
</div>
</div>
)}
<div className="space-y-3">
{keys.map((item) => (
<div key={item.id} className="p-4 border rounded-lg hover:bg-gray-50">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="font-medium">{item.name}</span>
<Badge
className={item.status === "active" ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-700"}
>
{item.status === "active" ? "启用" : "停用"}
</Badge>
</div>
<Button
variant="ghost"
size="sm"
className="text-red-500 hover:text-red-700"
onClick={() => deleteKey(item.id)}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
<div className="flex items-center gap-2 mb-2">
<code className="flex-1 px-3 py-2 bg-gray-100 rounded text-sm font-mono">
{showKey === item.id ? item.key : item.key.replace(/./g, "•").slice(0, 20) + "..."}
</code>
<Button variant="ghost" size="sm" onClick={() => setShowKey(showKey === item.id ? null : item.id)}>
{showKey === item.id ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</Button>
<Button variant="ghost" size="sm" onClick={() => copyKey(item.id, item.key)}>
{copiedId === item.id ? (
<CheckCircle className="w-4 h-4 text-green-500" />
) : (
<Copy className="w-4 h-4" />
)}
</Button>
</div>
<div className="flex items-center gap-4 text-xs text-gray-500">
<span> {item.created}</span>
<span>使: {item.lastUsed}</span>
</div>
</div>
))}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,168 @@
"use client"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Card, CardContent } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Activity, Clock, Zap, AlertTriangle, CheckCircle } from "lucide-react"
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from "recharts"
interface APIMonitorDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
api?: {
name: string
endpoint: string
}
}
const LATENCY_DATA = [
{ time: "00:00", p50: 28, p95: 45, p99: 68 },
{ time: "04:00", p50: 25, p95: 42, p99: 62 },
{ time: "08:00", p50: 32, p95: 55, p99: 85 },
{ time: "12:00", p50: 45, p95: 78, p99: 120 },
{ time: "16:00", p50: 42, p95: 72, p99: 110 },
{ time: "20:00", p50: 35, p95: 58, p99: 88 },
]
const QPS_DATA = [
{ time: "00:00", qps: 450 },
{ time: "04:00", qps: 280 },
{ time: "08:00", qps: 850 },
{ time: "12:00", qps: 1200 },
{ time: "16:00", qps: 1100 },
{ time: "20:00", qps: 780 },
]
const ERROR_DATA = [
{ time: "00:00", rate: 0.1 },
{ time: "04:00", rate: 0.05 },
{ time: "08:00", rate: 0.15 },
{ time: "12:00", rate: 0.3 },
{ time: "16:00", rate: 0.2 },
{ time: "20:00", rate: 0.12 },
]
export function APIMonitorDialog({ open, onOpenChange, api }: APIMonitorDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Activity className="w-5 h-5" />
API监控 - {api?.name || "API服务"}
</DialogTitle>
<DialogDescription>{api?.endpoint}</DialogDescription>
</DialogHeader>
{/* 实时指标 */}
<div className="grid grid-cols-4 gap-4">
<Card className="bg-slate-50">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500">QPS</p>
<p className="text-2xl font-bold text-slate-800">1,156</p>
</div>
<Zap className="w-8 h-8 text-blue-500" />
</div>
<p className="text-xs text-emerald-600 mt-1">峰值: 1,200</p>
</CardContent>
</Card>
<Card className="bg-slate-50">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500">P99延迟</p>
<p className="text-2xl font-bold text-slate-800">95ms</p>
</div>
<Clock className="w-8 h-8 text-amber-500" />
</div>
<p className="text-xs text-slate-500 mt-1">P50: 32ms</p>
</CardContent>
</Card>
<Card className="bg-slate-50">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500"></p>
<p className="text-2xl font-bold text-emerald-600">99.85%</p>
</div>
<CheckCircle className="w-8 h-8 text-emerald-500" />
</div>
<p className="text-xs text-emerald-600 mt-1">SLA: 99.9%</p>
</CardContent>
</Card>
<Card className="bg-slate-50">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500"></p>
<p className="text-2xl font-bold text-red-600">156</p>
</div>
<AlertTriangle className="w-8 h-8 text-red-500" />
</div>
<p className="text-xs text-slate-500 mt-1"> -23%</p>
</CardContent>
</Card>
</div>
{/* QPS趋势 */}
<div className="space-y-2">
<h4 className="text-sm font-medium">QPS趋势</h4>
<div className="h-48 bg-slate-50 rounded-lg p-4">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={QPS_DATA}>
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
<XAxis dataKey="time" tick={{ fontSize: 12 }} stroke="#94a3b8" />
<YAxis tick={{ fontSize: 12 }} stroke="#94a3b8" />
<Tooltip />
<Area type="monotone" dataKey="qps" stroke="#3b82f6" fill="#93c5fd" fillOpacity={0.5} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
{/* 延迟分布 */}
<div className="space-y-2">
<h4 className="text-sm font-medium"> (ms)</h4>
<div className="h-48 bg-slate-50 rounded-lg p-4">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={LATENCY_DATA}>
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
<XAxis dataKey="time" tick={{ fontSize: 12 }} stroke="#94a3b8" />
<YAxis tick={{ fontSize: 12 }} stroke="#94a3b8" />
<Tooltip />
<Line type="monotone" dataKey="p50" stroke="#22c55e" strokeWidth={2} name="P50" />
<Line type="monotone" dataKey="p95" stroke="#f59e0b" strokeWidth={2} name="P95" />
<Line type="monotone" dataKey="p99" stroke="#ef4444" strokeWidth={2} name="P99" />
</LineChart>
</ResponsiveContainer>
</div>
</div>
{/* 最近错误 */}
<div className="space-y-2">
<h4 className="text-sm font-medium"></h4>
<div className="space-y-2">
{[
{ time: "14:35:12", code: 429, message: "Rate limit exceeded", count: 23 },
{ time: "14:32:08", code: 500, message: "Internal server error", count: 5 },
{ time: "14:28:45", code: 404, message: "User not found", count: 12 },
].map((error, i) => (
<div key={i} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<div className="flex items-center gap-3">
<Badge className="bg-red-100 text-red-700">{error.code}</Badge>
<span className="text-sm text-slate-700">{error.message}</span>
</div>
<div className="flex items-center gap-4 text-sm text-slate-500">
<span>{error.count} </span>
<span>{error.time}</span>
</div>
</div>
))}
</div>
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,186 @@
"use client"
import { useState } from "react"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Settings, Shield, Zap, Bell } from "lucide-react"
interface APISettingsDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
api?: {
name: string
endpoint: string
}
}
export function APISettingsDialog({ open, onOpenChange, api }: APISettingsDialogProps) {
const [activeTab, setActiveTab] = useState("basic")
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Settings className="w-5 h-5" />
API设置 - {api?.name || "API服务"}
</DialogTitle>
<DialogDescription>API服务的基本信息</DialogDescription>
</DialogHeader>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="basic"></TabsTrigger>
<TabsTrigger value="ratelimit"></TabsTrigger>
<TabsTrigger value="auth"></TabsTrigger>
<TabsTrigger value="alert"></TabsTrigger>
</TabsList>
<TabsContent value="basic" className="space-y-4 mt-4">
<div className="space-y-2">
<Label>API名称</Label>
<Input defaultValue={api?.name} />
</div>
<div className="space-y-2">
<Label>API端点</Label>
<Input defaultValue={api?.endpoint} />
</div>
<div className="space-y-2">
<Label></Label>
<Select defaultValue="v1">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="v1">v1</SelectItem>
<SelectItem value="v2">v2 (Beta)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg">
<div>
<p className="font-medium">API</p>
<p className="text-sm text-slate-500">API将返回503错误</p>
</div>
<Switch defaultChecked />
</div>
</TabsContent>
<TabsContent value="ratelimit" className="space-y-4 mt-4">
<div className="flex items-center gap-2 mb-2">
<Zap className="w-4 h-4 text-amber-500" />
<span className="font-medium"></span>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label> (QPS)</Label>
<Input type="number" defaultValue="1000" />
</div>
<div className="space-y-2">
<Label></Label>
<Input type="number" defaultValue="50000" />
</div>
<div className="space-y-2">
<Label></Label>
<Input type="number" defaultValue="1000000" />
</div>
<div className="space-y-2">
<Label></Label>
<Input type="number" defaultValue="100" />
</div>
</div>
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg">
<div>
<p className="font-medium"></p>
<p className="text-sm text-slate-500"></p>
</div>
<Switch defaultChecked />
</div>
</TabsContent>
<TabsContent value="auth" className="space-y-4 mt-4">
<div className="flex items-center gap-2 mb-2">
<Shield className="w-4 h-4 text-blue-500" />
<span className="font-medium"></span>
</div>
<div className="space-y-2">
<Label></Label>
<Select defaultValue="apikey">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="apikey">API Key</SelectItem>
<SelectItem value="oauth2">OAuth 2.0</SelectItem>
<SelectItem value="jwt">JWT Token</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg">
<div>
<p className="font-medium">IP白名单</p>
<p className="text-sm text-slate-500">IP访问</p>
</div>
<Switch />
</div>
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg">
<div>
<p className="font-medium"></p>
<p className="text-sm text-slate-500"></p>
</div>
<Switch defaultChecked />
</div>
</TabsContent>
<TabsContent value="alert" className="space-y-4 mt-4">
<div className="flex items-center gap-2 mb-2">
<Bell className="w-4 h-4 text-red-500" />
<span className="font-medium"></span>
</div>
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg">
<div>
<p className="font-medium"></p>
<p className="text-sm text-slate-500"></p>
</div>
<Switch defaultChecked />
</div>
<div className="space-y-2">
<Label> (%)</Label>
<Input type="number" defaultValue="1" />
</div>
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg">
<div>
<p className="font-medium"></p>
<p className="text-sm text-slate-500">P99延迟超过阈值时告警</p>
</div>
<Switch defaultChecked />
</div>
<div className="space-y-2">
<Label> (ms)</Label>
<Input type="number" defaultValue="200" />
</div>
</TabsContent>
</Tabs>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={() => onOpenChange(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,170 @@
"use client"
import { useState } from "react"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { Checkbox } from "@/components/ui/checkbox"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Progress } from "@/components/ui/progress"
import { RefreshCw, Database, Clock, AlertTriangle } from "lucide-react"
interface BatchRecalculateDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
const DATA_SOURCES = [
{ id: "1", name: "存客宝-用户主库", records: 128956342 },
{ id: "2", name: "触客宝-行为数据", records: 89234567 },
{ id: "3", name: "数智员工-账号API", records: 45678901 },
]
const AI_RULES = [
{ id: 1, name: "用户数据智能清洗", selected: true },
{ id: 2, name: "交易流水异常检测", selected: true },
{ id: 3, name: "聊天记录语义清洗", selected: false },
]
export function BatchRecalculateDialog({ open, onOpenChange }: BatchRecalculateDialogProps) {
const [isProcessing, setIsProcessing] = useState(false)
const [progress, setProgress] = useState(0)
const [selectedRules, setSelectedRules] = useState(AI_RULES.filter((r) => r.selected).map((r) => r.id))
const [scope, setScope] = useState("incremental")
const handleStart = () => {
setIsProcessing(true)
setProgress(0)
const interval = setInterval(() => {
setProgress((prev) => {
if (prev >= 100) {
clearInterval(interval)
setIsProcessing(false)
return 100
}
return prev + 10
})
}, 500)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<RefreshCw className="w-5 h-5" />
</DialogTitle>
<DialogDescription>AI清洗规则</DialogDescription>
</DialogHeader>
{isProcessing ? (
<div className="space-y-4 py-4">
<div className="flex items-center justify-center">
<RefreshCw className="w-12 h-12 text-violet-500 animate-spin" />
</div>
<div className="text-center">
<p className="font-medium text-slate-800">...</p>
<p className="text-sm text-slate-500 mt-1"> {progress}%</p>
</div>
<Progress value={progress} className="h-2" />
<div className="text-center text-sm text-slate-500">
: {Math.ceil(((100 - progress) / 10) * 0.5)}
</div>
</div>
) : (
<div className="space-y-4">
<div className="space-y-2">
<Label>AI清洗规则</Label>
<div className="space-y-2">
{AI_RULES.map((rule) => (
<div key={rule.id} className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<Checkbox
checked={selectedRules.includes(rule.id)}
onCheckedChange={(checked) => {
if (checked) {
setSelectedRules([...selectedRules, rule.id])
} else {
setSelectedRules(selectedRules.filter((id) => id !== rule.id))
}
}}
/>
<span className="text-sm font-medium">{rule.name}</span>
</div>
))}
</div>
</div>
<div className="space-y-2">
<Label></Label>
<Select value={scope} onValueChange={setScope}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="incremental">/</SelectItem>
<SelectItem value="full"></SelectItem>
<SelectItem value="failed"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="p-4 bg-amber-50 rounded-lg border border-amber-200">
<div className="flex items-start gap-2">
<AlertTriangle className="w-5 h-5 text-amber-600 mt-0.5" />
<div>
<p className="font-medium text-amber-800"></p>
<p className="text-sm text-amber-700 mt-1">
{scope === "full"
? "全量重算将处理所有历史数据,预计耗时较长,请确认后执行。"
: "增量重算仅处理新增或变更的数据,速度较快。"}
</p>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="p-3 bg-slate-50 rounded-lg">
<div className="flex items-center gap-2 text-slate-500 mb-1">
<Database className="w-4 h-4" />
</div>
<p className="font-bold text-slate-800">
{scope === "full" ? "128.9M" : scope === "failed" ? "3.4K" : "15.6K"}
</p>
</div>
<div className="p-3 bg-slate-50 rounded-lg">
<div className="flex items-center gap-2 text-slate-500 mb-1">
<Clock className="w-4 h-4" />
</div>
<p className="font-bold text-slate-800">
{scope === "full" ? "约 2 小时" : scope === "failed" ? "约 5 分钟" : "约 15 分钟"}
</p>
</div>
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isProcessing}>
{isProcessing ? "后台运行" : "取消"}
</Button>
{!isProcessing && (
<Button onClick={handleStart} disabled={selectedRules.length === 0}>
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,178 @@
"use client"
import { useState } from "react"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { ScrollArea } from "@/components/ui/scroll-area"
import { FileText, CheckCircle, XCircle, Clock, Download, RefreshCw } from "lucide-react"
interface CleaningRuleLogsDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
ruleName?: string
}
const MOCK_LOGS = [
{
id: 1,
timestamp: "2025-12-16 14:35:12",
level: "INFO",
message: "开始执行清洗任务",
details: "任务ID: task_20251216_143512",
},
{
id: 2,
timestamp: "2025-12-16 14:35:13",
level: "INFO",
message: "连接源数据库成功",
details: "MySQL: 10.88.182.62:3305",
},
{
id: 3,
timestamp: "2025-12-16 14:35:15",
level: "INFO",
message: "读取源数据",
details: "共读取 128,956 条记录",
},
{
id: 4,
timestamp: "2025-12-16 14:35:18",
level: "WARN",
message: "发现异常数据",
details: "25 条记录手机号格式不规范",
},
{
id: 5,
timestamp: "2025-12-16 14:35:22",
level: "INFO",
message: "执行清洗规则",
details: "应用规则: 手机号标准化",
},
{
id: 6,
timestamp: "2025-12-16 14:35:45",
level: "INFO",
message: "数据写入完成",
details: "成功写入 128,931 条, 跳过 25 条",
},
{
id: 7,
timestamp: "2025-12-16 14:35:46",
level: "INFO",
message: "清洗任务完成",
details: "总耗时: 34秒, 错误率: 0.02%",
},
]
const EXECUTION_HISTORY = [
{ id: 1, time: "2025-12-16 14:35", status: "success", processed: 128956, errors: 25, duration: "34秒" },
{ id: 2, time: "2025-12-16 13:35", status: "success", processed: 125832, errors: 18, duration: "32秒" },
{ id: 3, time: "2025-12-16 12:35", status: "success", processed: 130215, errors: 22, duration: "36秒" },
{ id: 4, time: "2025-12-16 11:35", status: "failed", processed: 45000, errors: 1250, duration: "15秒" },
{ id: 5, time: "2025-12-16 10:35", status: "success", processed: 128500, errors: 20, duration: "33秒" },
]
export function CleaningRuleLogsDialog({ open, onOpenChange, ruleName }: CleaningRuleLogsDialogProps) {
const [selectedExecution, setSelectedExecution] = useState(EXECUTION_HISTORY[0])
const getLevelColor = (level: string) => {
switch (level) {
case "INFO":
return "text-blue-600 bg-blue-50"
case "WARN":
return "text-amber-600 bg-amber-50"
case "ERROR":
return "text-red-600 bg-red-50"
default:
return "text-slate-600 bg-slate-50"
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[80vh]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FileText className="w-5 h-5" />
- {ruleName || "清洗规则"}
</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="grid grid-cols-3 gap-4">
{/* 执行历史列表 */}
<div className="col-span-1 border-r pr-4">
<h4 className="text-sm font-medium text-slate-700 mb-3"></h4>
<ScrollArea className="h-[400px]">
<div className="space-y-2">
{EXECUTION_HISTORY.map((exec) => (
<div
key={exec.id}
className={`p-3 rounded-lg cursor-pointer transition-colors ${
selectedExecution.id === exec.id
? "bg-blue-50 border border-blue-200"
: "bg-slate-50 hover:bg-slate-100"
}`}
onClick={() => setSelectedExecution(exec)}
>
<div className="flex items-center justify-between mb-1">
<span className="text-xs text-slate-500">{exec.time}</span>
{exec.status === "success" ? (
<CheckCircle className="w-4 h-4 text-emerald-500" />
) : (
<XCircle className="w-4 h-4 text-red-500" />
)}
</div>
<div className="text-sm">
<span className="font-medium">{exec.processed.toLocaleString()}</span>
<span className="text-slate-500"> / </span>
<span className={exec.status === "failed" ? "text-red-600" : "text-slate-600"}>
{exec.errors}
</span>
</div>
<div className="flex items-center gap-1 text-xs text-slate-500 mt-1">
<Clock className="w-3 h-3" />
{exec.duration}
</div>
</div>
))}
</div>
</ScrollArea>
</div>
{/* 日志详情 */}
<div className="col-span-2">
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-medium text-slate-700"></h4>
<div className="flex gap-2">
<Button variant="outline" size="sm">
<RefreshCw className="w-4 h-4 mr-1" />
</Button>
<Button variant="outline" size="sm">
<Download className="w-4 h-4 mr-1" />
</Button>
</div>
</div>
<ScrollArea className="h-[400px] bg-slate-900 rounded-lg p-4">
<div className="space-y-2 font-mono text-sm">
{MOCK_LOGS.map((log) => (
<div key={log.id} className="flex gap-3">
<span className="text-slate-500 whitespace-nowrap">{log.timestamp}</span>
<Badge className={`${getLevelColor(log.level)} text-xs px-1.5`}>{log.level}</Badge>
<div>
<span className="text-slate-200">{log.message}</span>
<span className="text-slate-500 ml-2">- {log.details}</span>
</div>
</div>
))}
</div>
</ScrollArea>
</div>
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,223 @@
"use client"
import { useState } from "react"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Settings, Code, Clock, Bell } from "lucide-react"
interface CleaningRuleSettingsDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
rule?: {
id: string
name: string
description: string
type: string
sourceTable: string
targetTable: string
status: string
}
}
export function CleaningRuleSettingsDialog({ open, onOpenChange, rule }: CleaningRuleSettingsDialogProps) {
const [activeTab, setActiveTab] = useState("basic")
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Settings className="w-5 h-5" />
- {rule?.name || "清洗规则"}
</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="basic"></TabsTrigger>
<TabsTrigger value="logic"></TabsTrigger>
<TabsTrigger value="schedule"></TabsTrigger>
<TabsTrigger value="alert"></TabsTrigger>
</TabsList>
<TabsContent value="basic" className="space-y-4 mt-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Input defaultValue={rule?.name} />
</div>
<div className="space-y-2">
<Label></Label>
<Select defaultValue={rule?.type}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="format"></SelectItem>
<SelectItem value="dedup"></SelectItem>
<SelectItem value="mapping"></SelectItem>
<SelectItem value="validate"></SelectItem>
<SelectItem value="transform"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label></Label>
<Textarea defaultValue={rule?.description} rows={3} />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Input defaultValue={rule?.sourceTable} />
</div>
<div className="space-y-2">
<Label></Label>
<Input defaultValue={rule?.targetTable} />
</div>
</div>
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg">
<div>
<p className="font-medium"></p>
<p className="text-sm text-slate-500"></p>
</div>
<Switch defaultChecked={rule?.status === "active"} />
</div>
</TabsContent>
<TabsContent value="logic" className="space-y-4 mt-4">
<div className="space-y-2">
<Label className="flex items-center gap-2">
<Code className="w-4 h-4" />
SQL逻辑
</Label>
<div className="bg-slate-900 rounded-lg p-4 font-mono text-sm text-green-400 min-h-[200px]">
<pre>{`-- 清洗逻辑示例
SELECT
user_id,
TRIM(LOWER(email)) as email,
REGEXP_REPLACE(phone, '[^0-9]', '') as phone
FROM ${rule?.sourceTable || "source_table"}
WHERE email IS NOT NULL;`}</pre>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Input type="number" defaultValue="10000" />
</div>
<div className="space-y-2">
<Label>线</Label>
<Input type="number" defaultValue="4" />
</div>
</div>
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg">
<div>
<p className="font-medium"></p>
<p className="text-sm text-slate-500"></p>
</div>
<Switch defaultChecked />
</div>
</TabsContent>
<TabsContent value="schedule" className="space-y-4 mt-4">
<div className="space-y-2">
<Label className="flex items-center gap-2">
<Clock className="w-4 h-4" />
</Label>
<Select defaultValue="cron">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="manual"></SelectItem>
<SelectItem value="interval"></SelectItem>
<SelectItem value="cron">Cron表达式</SelectItem>
<SelectItem value="realtime"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Cron表达式</Label>
<Input defaultValue="0 */1 * * *" placeholder="例: 0 */1 * * * (每小时执行)" />
<p className="text-xs text-slate-500"></p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>()</Label>
<Input type="number" defaultValue="30" />
</div>
<div className="space-y-2">
<Label></Label>
<Input type="number" defaultValue="3" />
</div>
</div>
</TabsContent>
<TabsContent value="alert" className="space-y-4 mt-4">
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg">
<div className="flex items-center gap-2">
<Bell className="w-4 h-4" />
<div>
<p className="font-medium"></p>
<p className="text-sm text-slate-500"></p>
</div>
</div>
<Switch defaultChecked />
</div>
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg">
<div className="flex items-center gap-2">
<Bell className="w-4 h-4" />
<div>
<p className="font-medium"></p>
<p className="text-sm text-slate-500"></p>
</div>
</div>
<Switch defaultChecked />
</div>
<div className="space-y-2">
<Label>(%)</Label>
<Input type="number" defaultValue="5" />
</div>
<div className="space-y-2">
<Label></Label>
<Select defaultValue="all">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="email"></SelectItem>
<SelectItem value="sms"></SelectItem>
<SelectItem value="webhook">Webhook</SelectItem>
<SelectItem value="all"></SelectItem>
</SelectContent>
</Select>
</div>
</TabsContent>
</Tabs>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={() => onOpenChange(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,166 @@
"use client"
import { useState } from "react"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Checkbox } from "@/components/ui/checkbox"
interface CreateAlertRuleDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export function CreateAlertRuleDialog({ open, onOpenChange }: CreateAlertRuleDialogProps) {
const [formData, setFormData] = useState({
name: "",
metric: "",
operator: "gt",
threshold: "",
duration: "5",
severity: "warning",
channels: [] as string[],
})
const toggleChannel = (channel: string) => {
setFormData({
...formData,
channels: formData.channels.includes(channel)
? formData.channels.filter((c) => c !== channel)
: [...formData.channels, channel],
})
}
const handleSubmit = () => {
console.log("创建告警规则:", formData)
onOpenChange(false)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label></Label>
<Input
placeholder="输入规则名称"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Select value={formData.metric} onValueChange={(value) => setFormData({ ...formData, metric: value })}>
<SelectTrigger>
<SelectValue placeholder="选择监控指标" />
</SelectTrigger>
<SelectContent>
<SelectItem value="cpu">CPU使用率</SelectItem>
<SelectItem value="memory">使</SelectItem>
<SelectItem value="disk">使</SelectItem>
<SelectItem value="api_latency">API延迟(P99)</SelectItem>
<SelectItem value="api_error_rate">API错误率</SelectItem>
<SelectItem value="qps">QPS</SelectItem>
<SelectItem value="service_status"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label></Label>
<Select
value={formData.operator}
onValueChange={(value) => setFormData({ ...formData, operator: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="gt"></SelectItem>
<SelectItem value="lt"></SelectItem>
<SelectItem value="eq"></SelectItem>
<SelectItem value="gte"></SelectItem>
<SelectItem value="lte"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Input
placeholder="80"
value={formData.threshold}
onChange={(e) => setFormData({ ...formData, threshold: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label>()</Label>
<Input
type="number"
value={formData.duration}
onChange={(e) => setFormData({ ...formData, duration: e.target.value })}
/>
</div>
</div>
<div className="space-y-2">
<Label></Label>
<RadioGroup
value={formData.severity}
onValueChange={(value) => setFormData({ ...formData, severity: value })}
className="flex gap-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="info" id="info" />
<Label htmlFor="info" className="text-blue-600">
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="warning" id="warning" />
<Label htmlFor="warning" className="text-amber-600">
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="critical" id="critical" />
<Label htmlFor="critical" className="text-red-600">
</Label>
</div>
</RadioGroup>
</div>
<div className="space-y-2">
<Label></Label>
<div className="flex flex-wrap gap-4">
{["邮件", "短信", "企业微信", "钉钉", "Webhook"].map((channel) => (
<div key={channel} className="flex items-center space-x-2">
<Checkbox
id={channel}
checked={formData.channels.includes(channel)}
onCheckedChange={() => toggleChannel(channel)}
/>
<Label htmlFor={channel}>{channel}</Label>
</div>
))}
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={handleSubmit}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,136 @@
"use client"
import { useState } from "react"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Users, Database } from "lucide-react"
interface CreateAssessmentTaskDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export function CreateAssessmentTaskDialog({ open, onOpenChange }: CreateAssessmentTaskDialogProps) {
const [formData, setFormData] = useState({
name: "",
model: "",
targetType: "all",
crowdId: "",
schedule: "once",
scheduleTime: "",
})
const handleSubmit = () => {
console.log("创建评估任务:", formData)
onOpenChange(false)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="space-y-6 py-4">
<div className="space-y-2">
<Label></Label>
<Input
placeholder="输入任务名称"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Select value={formData.model} onValueChange={(value) => setFormData({ ...formData, model: value })}>
<SelectTrigger>
<SelectValue placeholder="选择评估模型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="clv">CLV预测模型 v2.3.1</SelectItem>
<SelectItem value="rfm">RFM评分模型 v1.5.0</SelectItem>
<SelectItem value="churn"> v3.1.2</SelectItem>
<SelectItem value="fraud"> v2.0.0</SelectItem>
<SelectItem value="segment"> v1.0.0</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-3">
<Label></Label>
<RadioGroup
value={formData.targetType}
onValueChange={(value) => setFormData({ ...formData, targetType: value })}
>
<div className="flex items-center space-x-2 p-3 border rounded-lg hover:bg-gray-50">
<RadioGroupItem value="all" id="all" />
<Label htmlFor="all" className="flex items-center gap-2 cursor-pointer flex-1">
<Database className="w-4 h-4 text-blue-500" />
<div>
<p className="font-medium"></p>
<p className="text-xs text-gray-500"></p>
</div>
</Label>
</div>
<div className="flex items-center space-x-2 p-3 border rounded-lg hover:bg-gray-50">
<RadioGroupItem value="crowd" id="crowd" />
<Label htmlFor="crowd" className="flex items-center gap-2 cursor-pointer flex-1">
<Users className="w-4 h-4 text-purple-500" />
<div>
<p className="font-medium"></p>
<p className="text-xs text-gray-500"></p>
</div>
</Label>
</div>
</RadioGroup>
{formData.targetType === "crowd" && (
<Select value={formData.crowdId} onValueChange={(value) => setFormData({ ...formData, crowdId: value })}>
<SelectTrigger>
<SelectValue placeholder="选择人群包" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">-S级 (125,800)</SelectItem>
<SelectItem value="2"> (23,400)</SelectItem>
<SelectItem value="3"> (89,000)</SelectItem>
<SelectItem value="4"> (12,580)</SelectItem>
</SelectContent>
</Select>
)}
</div>
<div className="space-y-3">
<Label></Label>
<RadioGroup
value={formData.schedule}
onValueChange={(value) => setFormData({ ...formData, schedule: value })}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="once" id="once" />
<Label htmlFor="once"></Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="daily" id="daily" />
<Label htmlFor="daily"></Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="weekly" id="weekly" />
<Label htmlFor="weekly"></Label>
</div>
</RadioGroup>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={handleSubmit}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,114 @@
"use client"
import { useState } from "react"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
interface CreateCleaningRuleDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export function CreateCleaningRuleDialog({ open, onOpenChange }: CreateCleaningRuleDialogProps) {
const [formData, setFormData] = useState({
name: "",
description: "",
type: "",
sourceTable: "",
targetTable: "",
})
const handleSubmit = () => {
console.log("创建清洗规则:", formData)
onOpenChange(false)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="name"></Label>
<Input
id="name"
placeholder="输入规则名称"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="description"></Label>
<Textarea
id="description"
placeholder="描述规则的作用"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="type"></Label>
<Select value={formData.type} onValueChange={(value) => setFormData({ ...formData, type: value })}>
<SelectTrigger>
<SelectValue placeholder="选择规则类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="dedup"></SelectItem>
<SelectItem value="format"></SelectItem>
<SelectItem value="mapping"></SelectItem>
<SelectItem value="validate"></SelectItem>
<SelectItem value="transform"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="sourceTable"></Label>
<Select
value={formData.sourceTable}
onValueChange={(value) => setFormData({ ...formData, sourceTable: value })}
>
<SelectTrigger>
<SelectValue placeholder="选择源表" />
</SelectTrigger>
<SelectContent>
<SelectItem value="raw_users">raw_users</SelectItem>
<SelectItem value="raw_transactions">raw_transactions</SelectItem>
<SelectItem value="raw_events">raw_events</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="targetTable"></Label>
<Select
value={formData.targetTable}
onValueChange={(value) => setFormData({ ...formData, targetTable: value })}
>
<SelectTrigger>
<SelectValue placeholder="选择目标表" />
</SelectTrigger>
<SelectContent>
<SelectItem value="clean_users">clean_users</SelectItem>
<SelectItem value="clean_transactions">clean_transactions</SelectItem>
<SelectItem value="clean_events">clean_events</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={handleSubmit}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,184 @@
"use client"
import { useState } from "react"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Badge } from "@/components/ui/badge"
import { X, Plus } from "lucide-react"
interface CreatePackageDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export function CreatePackageDialog({ open, onOpenChange }: CreatePackageDialogProps) {
const [formData, setFormData] = useState({
name: "",
description: "",
conditions: [] as { field: string; operator: string; value: string }[],
tags: [] as string[],
})
const [newTag, setNewTag] = useState("")
const addCondition = () => {
setFormData({
...formData,
conditions: [...formData.conditions, { field: "", operator: "", value: "" }],
})
}
const removeCondition = (index: number) => {
setFormData({
...formData,
conditions: formData.conditions.filter((_, i) => i !== index),
})
}
const addTag = () => {
if (newTag && !formData.tags.includes(newTag)) {
setFormData({ ...formData, tags: [...formData.tags, newTag] })
setNewTag("")
}
}
const removeTag = (tag: string) => {
setFormData({ ...formData, tags: formData.tags.filter((t) => t !== tag) })
}
const handleSubmit = () => {
console.log("创建流量包:", formData)
onOpenChange(false)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4 max-h-[60vh] overflow-y-auto">
<div className="space-y-2">
<Label></Label>
<Input
placeholder="输入名称"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Textarea
placeholder="描述流量包的用途"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label></Label>
<Button variant="outline" size="sm" onClick={addCondition}>
<Plus className="w-3 h-3 mr-1" />
</Button>
</div>
{formData.conditions.length === 0 ? (
<div className="p-4 border border-dashed rounded-lg text-center text-gray-500 text-sm">
"添加条件"
</div>
) : (
<div className="space-y-2">
{formData.conditions.map((condition, index) => (
<div key={index} className="flex items-center gap-2 p-3 bg-gray-50 rounded-lg">
<select
className="flex-1 px-3 py-2 border rounded-md text-sm"
value={condition.field}
onChange={(e) => {
const newConditions = [...formData.conditions]
newConditions[index].field = e.target.value
setFormData({ ...formData, conditions: newConditions })
}}
>
<option value=""></option>
<option value="rfm_score">RFM评分</option>
<option value="clv_score">CLV分数</option>
<option value="churn_prob"></option>
<option value="last_active"></option>
<option value="total_amount"></option>
</select>
<select
className="w-24 px-3 py-2 border rounded-md text-sm"
value={condition.operator}
onChange={(e) => {
const newConditions = [...formData.conditions]
newConditions[index].operator = e.target.value
setFormData({ ...formData, conditions: newConditions })
}}
>
<option value=""></option>
<option value="gt"></option>
<option value="lt"></option>
<option value="eq"></option>
<option value="gte"></option>
<option value="lte"></option>
</select>
<Input
className="w-32"
placeholder="值"
value={condition.value}
onChange={(e) => {
const newConditions = [...formData.conditions]
newConditions[index].value = e.target.value
setFormData({ ...formData, conditions: newConditions })
}}
/>
<Button variant="ghost" size="sm" onClick={() => removeCondition(index)}>
<X className="w-4 h-4" />
</Button>
</div>
))}
</div>
)}
</div>
<div className="space-y-2">
<Label></Label>
<div className="flex items-center gap-2">
<Input
placeholder="输入标签"
value={newTag}
onChange={(e) => setNewTag(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && addTag()}
/>
<Button variant="outline" onClick={addTag}>
</Button>
</div>
{formData.tags.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{formData.tags.map((tag) => (
<Badge key={tag} variant="secondary" className="pr-1">
{tag}
<button onClick={() => removeTag(tag)} className="ml-1 hover:text-red-500">
<X className="w-3 h-3" />
</button>
</Badge>
))}
</div>
)}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={handleSubmit}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,105 @@
"use client"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Download, Users, TrendingUp, Clock } from "lucide-react"
interface PackagePreviewDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
packageData?: {
name: string
description: string
users: number
tags: string[]
}
}
const MOCK_USERS = [
{ id: "U001", name: "张**", phone: "138****1234", rfmScore: 92, clvScore: 85, tags: ["高价值", "活跃"] },
{ id: "U002", name: "李**", phone: "139****5678", rfmScore: 88, clvScore: 79, tags: ["高价值", "VIP"] },
{ id: "U003", name: "王**", phone: "137****9012", rfmScore: 85, clvScore: 82, tags: ["高价值", "活跃"] },
{ id: "U004", name: "赵**", phone: "136****3456", rfmScore: 91, clvScore: 88, tags: ["高价值", "忠诚"] },
{ id: "U005", name: "刘**", phone: "135****7890", rfmScore: 87, clvScore: 76, tags: ["高价值"] },
]
export function PackagePreviewDialog({ open, onOpenChange, packageData }: PackagePreviewDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[700px]">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="space-y-6">
<div className="p-4 bg-gradient-to-r from-indigo-50 to-purple-50 rounded-xl">
<h3 className="font-semibold text-lg">{packageData?.name || "高价值用户包-S级"}</h3>
<p className="text-sm text-gray-600 mt-1">{packageData?.description || "RFM评分S级的高价值用户"}</p>
<div className="flex items-center gap-4 mt-3">
<div className="flex items-center gap-1">
<Users className="w-4 h-4 text-indigo-500" />
<span className="text-sm font-medium">{(packageData?.users || 125800).toLocaleString()} </span>
</div>
<div className="flex items-center gap-1">
<TrendingUp className="w-4 h-4 text-green-500" />
<span className="text-sm font-medium"> ¥2,580</span>
</div>
<div className="flex items-center gap-1">
<Clock className="w-4 h-4 text-gray-500" />
<span className="text-sm"> 2</span>
</div>
</div>
</div>
<div>
<h4 className="font-medium mb-3">5</h4>
<div className="border rounded-lg overflow-hidden">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="text-left py-3 px-4 text-xs font-medium text-gray-500">ID</th>
<th className="text-left py-3 px-4 text-xs font-medium text-gray-500"></th>
<th className="text-left py-3 px-4 text-xs font-medium text-gray-500"></th>
<th className="text-left py-3 px-4 text-xs font-medium text-gray-500">RFM</th>
<th className="text-left py-3 px-4 text-xs font-medium text-gray-500">CLV</th>
<th className="text-left py-3 px-4 text-xs font-medium text-gray-500"></th>
</tr>
</thead>
<tbody>
{MOCK_USERS.map((user) => (
<tr key={user.id} className="border-t">
<td className="py-3 px-4 text-sm font-mono">{user.id}</td>
<td className="py-3 px-4 text-sm">{user.name}</td>
<td className="py-3 px-4 text-sm">{user.phone}</td>
<td className="py-3 px-4 text-sm font-medium text-green-600">{user.rfmScore}</td>
<td className="py-3 px-4 text-sm font-medium text-blue-600">{user.clvScore}</td>
<td className="py-3 px-4">
<div className="flex gap-1">
{user.tags.map((tag) => (
<Badge key={tag} variant="outline" className="text-xs">
{tag}
</Badge>
))}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button>
<Download className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,192 @@
"use client"
import { useState } from "react"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Code, Plus, X } from "lucide-react"
interface PublishApiDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export function PublishApiDialog({ open, onOpenChange }: PublishApiDialogProps) {
const [formData, setFormData] = useState({
name: "",
description: "",
endpoint: "/api/v1/",
method: "GET",
params: [] as { name: string; type: string; required: boolean; desc: string }[],
rateLimit: "1000",
})
const addParam = () => {
setFormData({
...formData,
params: [...formData.params, { name: "", type: "string", required: false, desc: "" }],
})
}
const removeParam = (index: number) => {
setFormData({
...formData,
params: formData.params.filter((_, i) => i !== index),
})
}
const handleSubmit = () => {
console.log("发布API:", formData)
onOpenChange(false)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Code className="w-5 h-5" />
API服务
</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4 max-h-[60vh] overflow-y-auto">
<div className="space-y-2">
<Label>API名称</Label>
<Input
placeholder="例如用户画像查询API"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label>API描述</Label>
<Textarea
placeholder="描述API的功能和用途"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
/>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="col-span-2 space-y-2">
<Label>Endpoint</Label>
<Input
placeholder="/api/v1/..."
value={formData.endpoint}
onChange={(e) => setFormData({ ...formData, endpoint: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label>Method</Label>
<Select value={formData.method} onValueChange={(value) => setFormData({ ...formData, method: value })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="GET">GET</SelectItem>
<SelectItem value="POST">POST</SelectItem>
<SelectItem value="PUT">PUT</SelectItem>
<SelectItem value="DELETE">DELETE</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label></Label>
<Button variant="outline" size="sm" onClick={addParam}>
<Plus className="w-3 h-3 mr-1" />
</Button>
</div>
{formData.params.length === 0 ? (
<div className="p-4 border border-dashed rounded-lg text-center text-gray-500 text-sm">
"添加参数"API参数
</div>
) : (
<div className="space-y-2">
{formData.params.map((param, index) => (
<div key={index} className="flex items-center gap-2 p-3 bg-gray-50 rounded-lg">
<Input
className="w-28"
placeholder="参数名"
value={param.name}
onChange={(e) => {
const newParams = [...formData.params]
newParams[index].name = e.target.value
setFormData({ ...formData, params: newParams })
}}
/>
<Select
value={param.type}
onValueChange={(value) => {
const newParams = [...formData.params]
newParams[index].type = value
setFormData({ ...formData, params: newParams })
}}
>
<SelectTrigger className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="string">string</SelectItem>
<SelectItem value="number">number</SelectItem>
<SelectItem value="boolean">boolean</SelectItem>
<SelectItem value="array">array</SelectItem>
<SelectItem value="object">object</SelectItem>
</SelectContent>
</Select>
<Button
variant={param.required ? "default" : "outline"}
size="sm"
onClick={() => {
const newParams = [...formData.params]
newParams[index].required = !param.required
setFormData({ ...formData, params: newParams })
}}
>
{param.required ? "必填" : "可选"}
</Button>
<Input
className="flex-1"
placeholder="参数说明"
value={param.desc}
onChange={(e) => {
const newParams = [...formData.params]
newParams[index].desc = e.target.value
setFormData({ ...formData, params: newParams })
}}
/>
<Button variant="ghost" size="sm" onClick={() => removeParam(index)}>
<X className="w-4 h-4" />
</Button>
</div>
))}
</div>
)}
</div>
<div className="space-y-2">
<Label>/</Label>
<Input
type="number"
value={formData.rateLimit}
onChange={(e) => setFormData({ ...formData, rateLimit: e.target.value })}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={handleSubmit}>API</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,121 @@
"use client"
import { useState } from "react"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { ChevronLeft, ChevronRight } from "lucide-react"
interface ScheduleCalendarDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
const MOCK_SCHEDULE = [
{
day: 12,
tasks: [
{ name: "数据同步", type: "sync" },
{ name: "CLV训练", type: "model" },
],
},
{ day: 13, tasks: [{ name: "标签计算", type: "tagging" }] },
{
day: 14,
tasks: [
{ name: "数据清洗", type: "cleaning" },
{ name: "RFM评估", type: "model" },
],
},
{ day: 15, tasks: [{ name: "流失预警", type: "model" }] },
{ day: 16, tasks: [{ name: "数据同步", type: "sync" }] },
{ day: 18, tasks: [{ name: "用户分群", type: "tagging" }] },
{ day: 20, tasks: [{ name: "数据清洗", type: "cleaning" }] },
]
const TYPE_COLORS: Record<string, string> = {
sync: "bg-blue-100 text-blue-700",
cleaning: "bg-green-100 text-green-700",
tagging: "bg-purple-100 text-purple-700",
model: "bg-orange-100 text-orange-700",
}
export function ScheduleCalendarDialog({ open, onOpenChange }: ScheduleCalendarDialogProps) {
const [currentMonth, setCurrentMonth] = useState(new Date(2025, 11))
const daysInMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0).getDate()
const firstDayOfMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 1).getDay()
const days = Array.from({ length: daysInMonth }, (_, i) => i + 1)
const emptyDays = Array.from({ length: firstDayOfMonth }, (_, i) => i)
const getTasksForDay = (day: number) => {
return MOCK_SCHEDULE.find((s) => s.day === day)?.tasks || []
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[800px]">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="flex items-center justify-between">
<Button
variant="ghost"
size="sm"
onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1))}
>
<ChevronLeft className="w-4 h-4" />
</Button>
<span className="font-semibold">
{currentMonth.getFullYear()}{currentMonth.getMonth() + 1}
</span>
<Button
variant="ghost"
size="sm"
onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1))}
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
<div className="grid grid-cols-7 gap-1">
{["日", "一", "二", "三", "四", "五", "六"].map((day) => (
<div key={day} className="text-center text-sm font-medium text-gray-500 py-2">
{day}
</div>
))}
{emptyDays.map((i) => (
<div key={`empty-${i}`} className="h-24" />
))}
{days.map((day) => {
const tasks = getTasksForDay(day)
return (
<div key={day} className="h-24 border border-gray-100 rounded-lg p-1 hover:bg-gray-50">
<div className="text-sm font-medium text-gray-700">{day}</div>
<div className="space-y-1 mt-1">
{tasks.slice(0, 2).map((task, i) => (
<Badge key={i} className={`text-xs px-1 py-0 ${TYPE_COLORS[task.type]}`}>
{task.name}
</Badge>
))}
{tasks.length > 2 && <span className="text-xs text-gray-400">+{tasks.length - 2}</span>}
</div>
</div>
)
})}
</div>
<div className="flex items-center gap-4 pt-4 border-t">
<span className="text-sm text-gray-500">:</span>
<Badge className="bg-blue-100 text-blue-700"></Badge>
<Badge className="bg-green-100 text-green-700"></Badge>
<Badge className="bg-purple-100 text-purple-700"></Badge>
<Badge className="bg-orange-100 text-orange-700"></Badge>
</div>
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,228 @@
"use client"
import { useState } from "react"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
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 { ScrollArea } from "@/components/ui/scroll-area"
import { Play, Pause, RefreshCw, Clock, CheckCircle, XCircle } from "lucide-react"
interface TaskDetailDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
task?: {
id: string
name: string
type: string
status: string
progress: number
schedule: string
lastRun: string
nextRun: string
duration: string
}
}
const TASK_LOGS = [
{ time: "14:35:12", level: "INFO", message: "任务开始执行" },
{ time: "14:35:13", level: "INFO", message: "连接数据源成功" },
{ time: "14:35:15", level: "INFO", message: "开始读取数据 (批次 1/10)" },
{ time: "14:35:22", level: "INFO", message: "批次 1 处理完成, 共 12,895 条" },
{ time: "14:35:25", level: "INFO", message: "开始读取数据 (批次 2/10)" },
{ time: "14:35:32", level: "WARN", message: "批次 2 发现 3 条异常数据" },
{ time: "14:35:35", level: "INFO", message: "批次 2 处理完成, 共 12,456 条" },
{ time: "14:36:02", level: "INFO", message: "任务进度: 67%" },
]
const EXECUTION_STATS = [
{ label: "总处理记录", value: "128,956" },
{ label: "成功记录", value: "128,931" },
{ label: "失败记录", value: "25" },
{ label: "平均速度", value: "3,800 条/秒" },
{ label: "内存使用", value: "2.4 GB" },
{ label: "CPU使用", value: "45%" },
]
export function TaskDetailDialog({ open, onOpenChange, task }: TaskDetailDialogProps) {
const [activeTab, setActiveTab] = useState("overview")
const getStatusBadge = (status: string) => {
switch (status) {
case "running":
return (
<Badge className="bg-blue-100 text-blue-700">
<RefreshCw className="w-3 h-3 mr-1 animate-spin" />
</Badge>
)
case "completed":
return (
<Badge className="bg-emerald-100 text-emerald-700">
<CheckCircle className="w-3 h-3 mr-1" />
</Badge>
)
case "failed":
return (
<Badge className="bg-red-100 text-red-700">
<XCircle className="w-3 h-3 mr-1" />
</Badge>
)
case "waiting":
return (
<Badge className="bg-slate-100 text-slate-700">
<Clock className="w-3 h-3 mr-1" />
</Badge>
)
default:
return <Badge variant="secondary">{status}</Badge>
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-3">
{task?.name || "任务详情"}
{getStatusBadge(task?.status || "waiting")}
</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{task?.status === "running" && (
<div className="p-4 bg-blue-50 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-blue-700"></span>
<span className="text-sm font-medium text-blue-700">{task.progress}%</span>
</div>
<Progress value={task.progress} className="h-2" />
</div>
)}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="overview"></TabsTrigger>
<TabsTrigger value="logs"></TabsTrigger>
<TabsTrigger value="config"></TabsTrigger>
</TabsList>
<TabsContent value="overview" className="mt-4 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-slate-50 rounded-lg">
<div className="flex items-center gap-2 text-slate-500 text-sm mb-1">
<Clock className="w-4 h-4" />
</div>
<code className="text-sm bg-white px-2 py-1 rounded">{task?.schedule || "*/5 * * * *"}</code>
</div>
<div className="p-4 bg-slate-50 rounded-lg">
<div className="flex items-center gap-2 text-slate-500 text-sm mb-1">
<Clock className="w-4 h-4" />
</div>
<p className="font-medium">{task?.duration || "2分30秒"}</p>
</div>
<div className="p-4 bg-slate-50 rounded-lg">
<div className="flex items-center gap-2 text-slate-500 text-sm mb-1">
<Clock className="w-4 h-4" />
</div>
<p className="font-medium">{task?.lastRun || "-"}</p>
</div>
<div className="p-4 bg-slate-50 rounded-lg">
<div className="flex items-center gap-2 text-slate-500 text-sm mb-1">
<Clock className="w-4 h-4" />
</div>
<p className="font-medium">{task?.nextRun || "-"}</p>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
{EXECUTION_STATS.map((stat, i) => (
<div key={i} className="text-center p-3 bg-slate-50 rounded-lg">
<p className="text-lg font-bold text-slate-800">{stat.value}</p>
<p className="text-xs text-slate-500">{stat.label}</p>
</div>
))}
</div>
</TabsContent>
<TabsContent value="logs" className="mt-4">
<ScrollArea className="h-[300px] bg-slate-900 rounded-lg p-4">
<div className="space-y-1 font-mono text-sm">
{TASK_LOGS.map((log, i) => (
<div key={i} className="flex gap-3">
<span className="text-slate-500">{log.time}</span>
<span
className={`${
log.level === "WARN"
? "text-amber-400"
: log.level === "ERROR"
? "text-red-400"
: "text-blue-400"
}`}
>
[{log.level}]
</span>
<span className="text-slate-200">{log.message}</span>
</div>
))}
</div>
</ScrollArea>
</TabsContent>
<TabsContent value="config" className="mt-4 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm text-slate-500"></label>
<p className="font-medium">{task?.type || "数据同步"}</p>
</div>
<div className="space-y-2">
<label className="text-sm text-slate-500"></label>
<p className="font-medium">30</p>
</div>
<div className="space-y-2">
<label className="text-sm text-slate-500"></label>
<p className="font-medium">3</p>
</div>
<div className="space-y-2">
<label className="text-sm text-slate-500"></label>
<p className="font-medium">4</p>
</div>
</div>
</TabsContent>
</Tabs>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
{task?.status === "running" ? (
<Button variant="destructive">
<Pause className="w-4 h-4 mr-2" />
</Button>
) : (
<Button>
<Play className="w-4 h-4 mr-2" />
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,193 @@
"use client"
import { useState } from "react"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Upload, FileCode, CheckCircle } from "lucide-react"
interface UploadModelDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export function UploadModelDialog({ open, onOpenChange }: UploadModelDialogProps) {
const [step, setStep] = useState(1)
const [formData, setFormData] = useState({
name: "",
category: "",
description: "",
inputFeatures: "",
outputFormat: "",
updateFrequency: "",
})
const [fileUploaded, setFileUploaded] = useState(false)
const handleFileUpload = () => {
setFileUploaded(true)
}
const handleSubmit = () => {
console.log("上传模型:", formData)
onOpenChange(false)
setStep(1)
setFileUploaded(false)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="flex items-center gap-4 mb-6">
{[1, 2, 3].map((s) => (
<div key={s} className="flex items-center gap-2">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${step >= s ? "bg-blue-500 text-white" : "bg-gray-100 text-gray-500"}`}
>
{step > s ? <CheckCircle className="w-4 h-4" /> : s}
</div>
<span className={`text-sm ${step >= s ? "text-gray-900" : "text-gray-400"}`}>
{s === 1 ? "基本信息" : s === 2 ? "上传文件" : "配置参数"}
</span>
{s < 3 && <div className={`w-12 h-0.5 ${step > s ? "bg-blue-500" : "bg-gray-200"}`} />}
</div>
))}
</div>
{step === 1 && (
<div className="space-y-4">
<div className="space-y-2">
<Label></Label>
<Input
placeholder="输入模型名称"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Select
value={formData.category}
onValueChange={(value) => setFormData({ ...formData, category: value })}
>
<SelectTrigger>
<SelectValue placeholder="选择类别" />
</SelectTrigger>
<SelectContent>
<SelectItem value="clv">CLV模型</SelectItem>
<SelectItem value="rfm">RFM模型</SelectItem>
<SelectItem value="churn"></SelectItem>
<SelectItem value="segment"></SelectItem>
<SelectItem value="other"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Textarea
placeholder="描述模型的功能和用途"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
/>
</div>
</div>
)}
{step === 2 && (
<div className="space-y-4">
<div
className={`border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-colors ${fileUploaded ? "border-green-500 bg-green-50" : "border-gray-300 hover:border-blue-500"}`}
onClick={handleFileUpload}
>
{fileUploaded ? (
<div className="space-y-2">
<CheckCircle className="w-12 h-12 mx-auto text-green-500" />
<p className="text-green-600 font-medium">model_v1.0.pkl</p>
<p className="text-sm text-gray-500">12.5 MB</p>
</div>
) : (
<div className="space-y-2">
<Upload className="w-12 h-12 mx-auto text-gray-400" />
<p className="text-gray-600"></p>
<p className="text-sm text-gray-400"> .pkl, .h5, .pt, .onnx </p>
</div>
)}
</div>
<div className="p-4 bg-blue-50 rounded-xl">
<div className="flex items-start gap-3">
<FileCode className="w-5 h-5 text-blue-500 mt-0.5" />
<div>
<p className="text-sm font-medium text-blue-800"></p>
<ul className="text-xs text-blue-600 mt-1 space-y-1">
<li> pickle, HDF5, PyTorch, ONNX</li>
<li> 500MB</li>
<li> </li>
</ul>
</div>
</div>
</div>
</div>
)}
{step === 3 && (
<div className="space-y-4">
<div className="space-y-2">
<Label></Label>
<Input
placeholder="例如:消费金额,消费频次,最近购买"
value={formData.inputFeatures}
onChange={(e) => setFormData({ ...formData, inputFeatures: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
placeholder="例如:分数(0-100)"
value={formData.outputFormat}
onChange={(e) => setFormData({ ...formData, outputFormat: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Select
value={formData.updateFrequency}
onValueChange={(value) => setFormData({ ...formData, updateFrequency: value })}
>
<SelectTrigger>
<SelectValue placeholder="选择更新频率" />
</SelectTrigger>
<SelectContent>
<SelectItem value="realtime"></SelectItem>
<SelectItem value="daily"></SelectItem>
<SelectItem value="weekly"></SelectItem>
<SelectItem value="monthly"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
)}
<DialogFooter>
{step > 1 && (
<Button variant="outline" onClick={() => setStep(step - 1)}>
</Button>
)}
{step < 3 ? (
<Button onClick={() => setStep(step + 1)} disabled={step === 2 && !fileUploaded}>
</Button>
) : (
<Button onClick={handleSubmit}></Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
)
}