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:
144
components/dialogs/ai-cleaning-logs-dialog.tsx
Normal file
144
components/dialogs/ai-cleaning-logs-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
238
components/dialogs/alert-rule-settings-dialog.tsx
Normal file
238
components/dialogs/alert-rule-settings-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
222
components/dialogs/api-doc-dialog.tsx
Normal file
222
components/dialogs/api-doc-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
164
components/dialogs/api-key-management-dialog.tsx
Normal file
164
components/dialogs/api-key-management-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
168
components/dialogs/api-monitor-dialog.tsx
Normal file
168
components/dialogs/api-monitor-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
186
components/dialogs/api-settings-dialog.tsx
Normal file
186
components/dialogs/api-settings-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
170
components/dialogs/batch-recalculate-dialog.tsx
Normal file
170
components/dialogs/batch-recalculate-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
178
components/dialogs/cleaning-rule-logs-dialog.tsx
Normal file
178
components/dialogs/cleaning-rule-logs-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
223
components/dialogs/cleaning-rule-settings-dialog.tsx
Normal file
223
components/dialogs/cleaning-rule-settings-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
166
components/dialogs/create-alert-rule-dialog.tsx
Normal file
166
components/dialogs/create-alert-rule-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
136
components/dialogs/create-assessment-task-dialog.tsx
Normal file
136
components/dialogs/create-assessment-task-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
114
components/dialogs/create-cleaning-rule-dialog.tsx
Normal file
114
components/dialogs/create-cleaning-rule-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
184
components/dialogs/create-package-dialog.tsx
Normal file
184
components/dialogs/create-package-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
105
components/dialogs/package-preview-dialog.tsx
Normal file
105
components/dialogs/package-preview-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
192
components/dialogs/publish-api-dialog.tsx
Normal file
192
components/dialogs/publish-api-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
121
components/dialogs/schedule-calendar-dialog.tsx
Normal file
121
components/dialogs/schedule-calendar-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
228
components/dialogs/task-detail-dialog.tsx
Normal file
228
components/dialogs/task-detail-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
193
components/dialogs/upload-model-dialog.tsx
Normal file
193
components/dialogs/upload-model-dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { LayoutDashboard, Database, Users, BarChart3, Bot } from "lucide-react"
|
||||
|
||||
export function MainNav() {
|
||||
const pathname = usePathname()
|
||||
|
||||
const routes = [
|
||||
{
|
||||
href: "/",
|
||||
label: "首页",
|
||||
icon: LayoutDashboard,
|
||||
active: pathname === "/",
|
||||
},
|
||||
{
|
||||
href: "/data-platform",
|
||||
label: "数据中台",
|
||||
icon: Database,
|
||||
active: pathname.startsWith("/data-platform"),
|
||||
},
|
||||
{
|
||||
href: "/user-portrait",
|
||||
label: "用户画像",
|
||||
icon: Users,
|
||||
active: pathname.startsWith("/user-portrait"),
|
||||
},
|
||||
{
|
||||
href: "/value-assessment",
|
||||
label: "价值评估",
|
||||
icon: BarChart3,
|
||||
active: pathname.startsWith("/value-assessment"),
|
||||
},
|
||||
{
|
||||
href: "/ai-assistant",
|
||||
label: "AI助手",
|
||||
icon: Bot,
|
||||
active: pathname.startsWith("/ai-assistant"),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<nav className="flex items-center space-x-4 lg:space-x-6 mx-6">
|
||||
{routes.map((route) => (
|
||||
<Link
|
||||
key={route.href}
|
||||
href={route.href}
|
||||
className={cn(
|
||||
"text-sm font-medium transition-colors hover:text-primary flex items-center gap-2",
|
||||
route.active ? "text-black dark:text-white" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<route.icon className="h-4 w-4" />
|
||||
{route.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from "recharts"
|
||||
|
||||
const data = [
|
||||
{
|
||||
name: "1月",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "2月",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "3月",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "4月",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "5月",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "6月",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "7月",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "8月",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "9月",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "10月",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "11月",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "12月",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
]
|
||||
|
||||
export function Overview() {
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={350}>
|
||||
<BarChart data={data}>
|
||||
<XAxis dataKey="name" stroke="#888888" fontSize={12} tickLine={false} axisLine={false} />
|
||||
<YAxis
|
||||
stroke="#888888"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => `¥${value}`}
|
||||
/>
|
||||
<Bar dataKey="total" fill="#adfa1d" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
|
||||
export function RecentSales() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center">
|
||||
<Avatar className="h-9 w-9">
|
||||
<AvatarImage src="/avatars/01.png" alt="Avatar" />
|
||||
<AvatarFallback>OM</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="ml-4 space-y-1">
|
||||
<p className="text-sm font-medium leading-none">Olivia Martin</p>
|
||||
<p className="text-sm text-muted-foreground">olivia.martin@email.com</p>
|
||||
</div>
|
||||
<div className="ml-auto font-medium">+¥1,999.00</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Avatar className="flex h-9 w-9 items-center justify-center space-y-0 border">
|
||||
<AvatarImage src="/avatars/02.png" alt="Avatar" />
|
||||
<AvatarFallback>JL</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="ml-4 space-y-1">
|
||||
<p className="text-sm font-medium leading-none">Jackson Lee</p>
|
||||
<p className="text-sm text-muted-foreground">jackson.lee@email.com</p>
|
||||
</div>
|
||||
<div className="ml-auto font-medium">+¥39.00</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Avatar className="h-9 w-9">
|
||||
<AvatarImage src="/avatars/03.png" alt="Avatar" />
|
||||
<AvatarFallback>IN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="ml-4 space-y-1">
|
||||
<p className="text-sm font-medium leading-none">Isabella Nguyen</p>
|
||||
<p className="text-sm text-muted-foreground">isabella.nguyen@email.com</p>
|
||||
</div>
|
||||
<div className="ml-auto font-medium">+¥299.00</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Avatar className="h-9 w-9">
|
||||
<AvatarImage src="/avatars/04.png" alt="Avatar" />
|
||||
<AvatarFallback>WK</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="ml-4 space-y-1">
|
||||
<p className="text-sm font-medium leading-none">William Kim</p>
|
||||
<p className="text-sm text-muted-foreground">will@email.com</p>
|
||||
</div>
|
||||
<div className="ml-auto font-medium">+¥99.00</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Avatar className="h-9 w-9">
|
||||
<AvatarImage src="/avatars/05.png" alt="Avatar" />
|
||||
<AvatarFallback>SD</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="ml-4 space-y-1">
|
||||
<p className="text-sm font-medium leading-none">Sofia Davis</p>
|
||||
<p className="text-sm text-muted-foreground">sofia.davis@email.com</p>
|
||||
</div>
|
||||
<div className="ml-auto font-medium">+¥39.00</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
"use client"
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
24
components/ui/slider.tsx
Normal file
24
components/ui/slider.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative flex w-full touch-none select-none items-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
))
|
||||
Slider.displayName = SliderPrimitive.Root.displayName
|
||||
|
||||
export { Slider }
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
|
||||
export function UserNav() {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src="/avatars/01.png" alt="@shadcn" />
|
||||
<AvatarFallback>SC</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="end" forceMount>
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm font-medium leading-none">管理员</p>
|
||||
<p className="text-xs leading-none text-muted-foreground">admin@example.com</p>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
个人资料
|
||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
设置
|
||||
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
退出登录
|
||||
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user