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:
@@ -1,14 +1,14 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { MainNav } from "@/components/main-nav"
|
||||
import { UserNav } from "@/components/user-nav"
|
||||
import { ThemeToggle } from "@/components/theme-toggle"
|
||||
import { Search } from "lucide-react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Inter } from "next/font/google"
|
||||
import "./globals.css"
|
||||
import { Inter } from "next/font/google"
|
||||
import { useState, useEffect } from "react"
|
||||
import { usePathname } from "next/navigation"
|
||||
import Sidebar from "./components/Sidebar"
|
||||
import MobileHeader from "./components/MobileHeader"
|
||||
import MobileSidebar from "./components/MobileSidebar"
|
||||
import BottomNav from "./components/BottomNav"
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] })
|
||||
|
||||
@@ -17,37 +17,56 @@ export default function ClientLayout({
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
const pathname = usePathname()
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth < 768)
|
||||
}
|
||||
|
||||
checkMobile()
|
||||
window.addEventListener("resize", checkMobile)
|
||||
|
||||
return () => window.removeEventListener("resize", checkMobile)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<title>神射手数据资产中台</title>
|
||||
<meta name="description" content="基于IMEI、手机号的用户数据资产管理平台" />
|
||||
<title>用户数据资产中台</title>
|
||||
<meta name="description" content="基于苹果毛玻璃设计的用户数据资产中台" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
</head>
|
||||
<body className={inter.className}>
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container flex h-16 items-center">
|
||||
<div className="mr-4 hidden md:flex">
|
||||
<a className="mr-6 flex items-center space-x-2" href="/">
|
||||
<span className="hidden font-bold sm:inline-block">神射手数据中台</span>
|
||||
</a>
|
||||
<MainNav />
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-between space-x-2 md:justify-end">
|
||||
<div className="w-full flex-1 md:w-auto md:flex-none">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input placeholder="搜索..." className="pl-8 h-9 md:w-[300px] lg:w-[300px]" />
|
||||
</div>
|
||||
</div>
|
||||
<ThemeToggle />
|
||||
<UserNav />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex-1 bg-background">{children}</main>
|
||||
{/* 背景装饰 */}
|
||||
<div className="fixed inset-0 -z-10">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-50 via-white to-purple-50" />
|
||||
<div className="absolute top-0 left-0 w-72 h-72 md:w-96 md:h-96 bg-blue-200 rounded-full mix-blend-multiply filter blur-xl opacity-30 animate-pulse" />
|
||||
<div className="absolute top-0 right-0 w-72 h-72 md:w-96 md:h-96 bg-purple-200 rounded-full mix-blend-multiply filter blur-xl opacity-30 animate-pulse animation-delay-2000" />
|
||||
<div className="absolute bottom-0 left-1/2 w-72 h-72 md:w-96 md:h-96 bg-pink-200 rounded-full mix-blend-multiply filter blur-xl opacity-30 animate-pulse animation-delay-4000" />
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-screen">
|
||||
{/* 桌面端侧边栏 */}
|
||||
{!isMobile && <Sidebar />}
|
||||
|
||||
{/* 移动端侧边栏 */}
|
||||
{isMobile && <MobileSidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />}
|
||||
|
||||
{/* 主内容区域 */}
|
||||
<main className={`flex-1 ${isMobile ? "pb-20" : "p-6"}`}>
|
||||
{/* 移动端头部 */}
|
||||
{isMobile && <MobileHeader onMenuToggle={() => setSidebarOpen(true)} />}
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className={`glass-card min-h-full ${isMobile ? "mx-2 mb-4" : ""}`}>{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* 移动端底部导航 */}
|
||||
{isMobile && <BottomNav />}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
432
app/ai-agent/chat/page.tsx
Normal file
432
app/ai-agent/chat/page.tsx
Normal file
@@ -0,0 +1,432 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useRef, useEffect } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import {
|
||||
Bot,
|
||||
Send,
|
||||
User,
|
||||
Sparkles,
|
||||
Database,
|
||||
Tags,
|
||||
FileText,
|
||||
Download,
|
||||
Copy,
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
Loader2,
|
||||
Zap,
|
||||
Search,
|
||||
BarChart3,
|
||||
Table,
|
||||
RefreshCw,
|
||||
} from "lucide-react"
|
||||
|
||||
// 第四部分:AI Agent智能系统 - 智能对话
|
||||
export default function AIAgentChatPage() {
|
||||
const [messages, setMessages] = useState<any[]>([
|
||||
{
|
||||
id: "1",
|
||||
role: "assistant",
|
||||
content: "您好!我是神射手AI Agent,可以帮您完成以下任务:\n\n- **自然语言查询**:用自然语言查询用户数据\n- **智能打标**:AI自动为用户打标签\n- **数据清洗**:AI识别并清洗异常数据\n- **智能报告**:自动生成数据分析报告\n\n请问有什么可以帮您?",
|
||||
timestamp: new Date(),
|
||||
},
|
||||
])
|
||||
const [inputValue, setInputValue] = useState("")
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 快捷指令
|
||||
const quickCommands = [
|
||||
{ icon: Search, label: "查询高价值用户", command: "帮我找出最近30天内消费超过1万元的高价值用户" },
|
||||
{ icon: Tags, label: "AI打标签", command: "为最近7天未登录的用户打上流失风险标签" },
|
||||
{ icon: Zap, label: "数据清洗", command: "检查用户表中的手机号格式,清洗异常数据" },
|
||||
{ icon: FileText, label: "生成报告", command: "生成本月用户增长分析报告" },
|
||||
]
|
||||
|
||||
// 支持的指令类型
|
||||
const supportedTasks = [
|
||||
{ type: "NLQ", label: "自然语言查询", desc: "用自然语言查询数据库", icon: Search },
|
||||
{ type: "TAG", label: "AI打标", desc: "智能为用户打标签", icon: Tags },
|
||||
{ type: "CLEAN", label: "数据清洗", desc: "AI识别清洗异常数据", icon: Zap },
|
||||
{ type: "REPORT", label: "智能报告", desc: "自动生成分析报告", icon: FileText },
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
|
||||
}
|
||||
}, [messages])
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!inputValue.trim() || isLoading) return
|
||||
|
||||
const userMessage = {
|
||||
id: Date.now().toString(),
|
||||
role: "user",
|
||||
content: inputValue,
|
||||
timestamp: new Date(),
|
||||
}
|
||||
setMessages((prev) => [...prev, userMessage])
|
||||
setInputValue("")
|
||||
setIsLoading(true)
|
||||
|
||||
// 模拟AI响应
|
||||
setTimeout(() => {
|
||||
let response: any = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: "assistant",
|
||||
timestamp: new Date(),
|
||||
}
|
||||
|
||||
// 根据输入内容生成不同类型的响应
|
||||
if (inputValue.includes("高价值") || inputValue.includes("消费") || inputValue.includes("查询")) {
|
||||
response.content = "我已理解您的需求。正在为您查询符合条件的用户..."
|
||||
response.taskType = "NLQ"
|
||||
response.result = {
|
||||
type: "table",
|
||||
title: "高价值用户查询结果",
|
||||
sql: "SELECT * FROM users WHERE total_spending > 10000 AND last_active_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)",
|
||||
data: [
|
||||
{ id: "U001", name: "张三", phone: "138****1234", spending: "¥15,680", lastActive: "2天前" },
|
||||
{ id: "U002", name: "李四", phone: "139****5678", spending: "¥12,340", lastActive: "1天前" },
|
||||
{ id: "U003", name: "王五", phone: "137****9012", spending: "¥11,890", lastActive: "3天前" },
|
||||
],
|
||||
total: 823,
|
||||
}
|
||||
} else if (inputValue.includes("打标") || inputValue.includes("标签")) {
|
||||
response.content = "我将为符合条件的用户打上「流失风险」标签。这是一个AI打标任务,需要您确认后执行。"
|
||||
response.taskType = "TAG"
|
||||
response.result = {
|
||||
type: "confirmation",
|
||||
title: "AI打标任务确认",
|
||||
description: "为最近7天未登录的用户打上「流失风险」标签",
|
||||
affectedCount: 12456,
|
||||
confidence: 0.87,
|
||||
needReview: true,
|
||||
}
|
||||
} else if (inputValue.includes("清洗") || inputValue.includes("异常")) {
|
||||
response.content = "我已扫描用户表,发现以下数据质量问题:"
|
||||
response.taskType = "CLEAN"
|
||||
response.result = {
|
||||
type: "cleaning",
|
||||
title: "数据清洗建议",
|
||||
issues: [
|
||||
{ field: "phone", issue: "格式不规范", count: 234, example: "1381234567 → 138****4567" },
|
||||
{ field: "email", issue: "缺失值", count: 1256, suggestion: "填充默认值或标记" },
|
||||
{ field: "birth_date", issue: "超出范围", count: 45, example: "2099-01-01" },
|
||||
],
|
||||
needReview: true,
|
||||
}
|
||||
} else if (inputValue.includes("报告") || inputValue.includes("分析")) {
|
||||
response.content = "正在为您生成用户增长分析报告..."
|
||||
response.taskType = "REPORT"
|
||||
response.result = {
|
||||
type: "report",
|
||||
title: "用户增长分析报告(2026年1月)",
|
||||
summary: "本月新增用户23,456人,同比增长15.2%;活跃用户856,234人,DAU/MAU比值0.32。",
|
||||
highlights: [
|
||||
"新用户转化率提升至18.5%,环比增长3.2%",
|
||||
"高价值用户占比8.7%,贡献65%的交易额",
|
||||
"用户流失率下降至2.1%,创历史新低",
|
||||
],
|
||||
}
|
||||
} else {
|
||||
response.content = "我理解您的需求。请问您想要:\n1. 查询用户数据\n2. AI打标签\n3. 数据清洗\n4. 生成报告\n\n请选择或详细描述您的需求。"
|
||||
}
|
||||
|
||||
setMessages((prev) => [...prev, response])
|
||||
setIsLoading(false)
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
const handleQuickCommand = (command: string) => {
|
||||
setInputValue(command)
|
||||
}
|
||||
|
||||
const renderMessageContent = (message: any) => {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<p className="whitespace-pre-wrap">{message.content}</p>
|
||||
|
||||
{message.result?.type === "table" && (
|
||||
<Card className="bg-gray-50 border-0">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="font-medium text-gray-900">{message.result.title}</h4>
|
||||
<Badge variant="secondary">共 {message.result.total} 条</Badge>
|
||||
</div>
|
||||
<div className="bg-gray-100 rounded p-2 mb-3 font-mono text-xs text-gray-600 overflow-x-auto">
|
||||
{message.result.sql}
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
<th className="text-left py-2 px-3">ID</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>
|
||||
<th className="text-left py-2 px-3">最近活跃</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{message.result.data.map((row: any, index: number) => (
|
||||
<tr key={index} className="border-b border-gray-100">
|
||||
<td className="py-2 px-3">{row.id}</td>
|
||||
<td className="py-2 px-3">{row.name}</td>
|
||||
<td className="py-2 px-3">{row.phone}</td>
|
||||
<td className="py-2 px-3 text-green-600 font-medium">{row.spending}</td>
|
||||
<td className="py-2 px-3">{row.lastActive}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<Button size="sm" variant="outline">
|
||||
<Download className="h-4 w-4 mr-1" />
|
||||
导出Excel
|
||||
</Button>
|
||||
<Button size="sm" variant="outline">
|
||||
<Copy className="h-4 w-4 mr-1" />
|
||||
复制SQL
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{message.result?.type === "confirmation" && (
|
||||
<Card className="bg-blue-50 border-blue-200">
|
||||
<CardContent className="p-4">
|
||||
<h4 className="font-medium text-gray-900 mb-2">{message.result.title}</h4>
|
||||
<p className="text-sm text-gray-600 mb-3">{message.result.description}</p>
|
||||
<div className="flex items-center gap-4 text-sm mb-3">
|
||||
<span>影响用户数:<strong>{message.result.affectedCount.toLocaleString()}</strong></span>
|
||||
<span>置信度:<strong>{(message.result.confidence * 100).toFixed(0)}%</strong></span>
|
||||
</div>
|
||||
{message.result.needReview && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm">确认执行</Button>
|
||||
<Button size="sm" variant="outline">查看详情</Button>
|
||||
<Button size="sm" variant="ghost">取消</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{message.result?.type === "cleaning" && (
|
||||
<Card className="bg-yellow-50 border-yellow-200">
|
||||
<CardContent className="p-4">
|
||||
<h4 className="font-medium text-gray-900 mb-3">{message.result.title}</h4>
|
||||
<div className="space-y-3">
|
||||
{message.result.issues.map((issue: any, index: number) => (
|
||||
<div key={index} className="flex items-start justify-between p-3 bg-white rounded-lg">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary">{issue.field}</Badge>
|
||||
<span className="text-sm font-medium">{issue.issue}</span>
|
||||
</div>
|
||||
{issue.example && (
|
||||
<p className="text-xs text-gray-500 mt-1">示例:{issue.example}</p>
|
||||
)}
|
||||
{issue.suggestion && (
|
||||
<p className="text-xs text-gray-500 mt-1">建议:{issue.suggestion}</p>
|
||||
)}
|
||||
</div>
|
||||
<Badge className="bg-red-100 text-red-700">{issue.count} 条</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<Button size="sm">一键清洗</Button>
|
||||
<Button size="sm" variant="outline">预览修改</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{message.result?.type === "report" && (
|
||||
<Card className="bg-green-50 border-green-200">
|
||||
<CardContent className="p-4">
|
||||
<h4 className="font-medium text-gray-900 mb-2">{message.result.title}</h4>
|
||||
<p className="text-sm text-gray-600 mb-3">{message.result.summary}</p>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-gray-700">核心发现:</p>
|
||||
{message.result.highlights.map((highlight: string, index: number) => (
|
||||
<div key={index} className="flex items-start gap-2 text-sm text-gray-600">
|
||||
<span className="text-green-500">•</span>
|
||||
{highlight}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<Button size="sm">
|
||||
<Download className="h-4 w-4 mr-1" />
|
||||
下载完整报告
|
||||
</Button>
|
||||
<Button size="sm" variant="outline">
|
||||
<BarChart3 className="h-4 w-4 mr-1" />
|
||||
查看图表
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
|
||||
<div className="flex h-[calc(100vh-64px)]">
|
||||
{/* 左侧:功能面板 */}
|
||||
<div className="w-64 border-r bg-white/80 backdrop-blur p-4 hidden lg:block">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">AI能力</h3>
|
||||
<div className="space-y-2">
|
||||
{supportedTasks.map((task) => (
|
||||
<div
|
||||
key={task.type}
|
||||
className="flex items-start gap-3 p-3 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors"
|
||||
>
|
||||
<div className="p-2 rounded-lg bg-blue-50">
|
||||
<task.icon className="h-4 w-4 text-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">{task.label}</p>
|
||||
<p className="text-xs text-gray-500">{task.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">快捷指令</h3>
|
||||
<div className="space-y-2">
|
||||
{quickCommands.map((cmd, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleQuickCommand(cmd.command)}
|
||||
className="w-full flex items-center gap-2 p-2 rounded-lg text-left text-sm text-gray-600 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<cmd.icon className="h-4 w-4 text-gray-400" />
|
||||
{cmd.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 中间:对话区域 */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* 消息列表 */}
|
||||
<ScrollArea className="flex-1 p-6" ref={scrollRef}>
|
||||
<div className="max-w-3xl mx-auto space-y-6">
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`flex gap-4 ${message.role === "user" ? "flex-row-reverse" : ""}`}
|
||||
>
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center shrink-0 ${
|
||||
message.role === "user"
|
||||
? "bg-blue-500"
|
||||
: "bg-gradient-to-br from-purple-500 to-blue-500"
|
||||
}`}
|
||||
>
|
||||
{message.role === "user" ? (
|
||||
<User className="h-5 w-5 text-white" />
|
||||
) : (
|
||||
<Bot className="h-5 w-5 text-white" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`flex-1 ${message.role === "user" ? "text-right" : ""}`}
|
||||
>
|
||||
<div
|
||||
className={`inline-block max-w-full text-left rounded-2xl px-4 py-3 ${
|
||||
message.role === "user"
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-white shadow-sm"
|
||||
}`}
|
||||
>
|
||||
{message.role === "user" ? (
|
||||
<p>{message.content}</p>
|
||||
) : (
|
||||
renderMessageContent(message)
|
||||
)}
|
||||
</div>
|
||||
{message.role === "assistant" && (
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2">
|
||||
<ThumbsUp className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2">
|
||||
<ThumbsDown className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2">
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2">
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center">
|
||||
<Bot className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div className="bg-white rounded-2xl px-4 py-3 shadow-sm">
|
||||
<div className="flex items-center gap-2 text-gray-500">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span>AI正在思考...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* 输入区域 */}
|
||||
<div className="border-t bg-white/80 backdrop-blur p-4">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 relative">
|
||||
<Input
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && handleSend()}
|
||||
placeholder="输入您的问题,例如:帮我找出最近30天的高价值用户..."
|
||||
className="pr-12 py-6 bg-white"
|
||||
/>
|
||||
<Button
|
||||
size="icon"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2"
|
||||
onClick={handleSend}
|
||||
disabled={isLoading || !inputValue.trim()}
|
||||
>
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-2 text-center">
|
||||
AI生成的内容可能存在误差,重要操作请人工确认
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
336
app/ai-agent/nlq/page.tsx
Normal file
336
app/ai-agent/nlq/page.tsx
Normal file
@@ -0,0 +1,336 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import {
|
||||
Search,
|
||||
Play,
|
||||
Download,
|
||||
Copy,
|
||||
History,
|
||||
Star,
|
||||
StarOff,
|
||||
Clock,
|
||||
Database,
|
||||
Loader2,
|
||||
ChevronRight,
|
||||
Table,
|
||||
BarChart3,
|
||||
} from "lucide-react"
|
||||
|
||||
// 第四部分:AI Agent - 自然语言查询 (NLQ)
|
||||
export default function NLQPage() {
|
||||
const [query, setQuery] = useState("")
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState("query")
|
||||
const [showResult, setShowResult] = useState(false)
|
||||
const [queryResult, setQueryResult] = useState<any>(null)
|
||||
|
||||
// 示例查询
|
||||
const exampleQueries = [
|
||||
{ label: "高价值用户", query: "查找最近30天消费超过1万元的用户" },
|
||||
{ label: "流失用户分析", query: "统计最近7天未登录的活跃用户数量" },
|
||||
{ label: "地域分布", query: "按省份统计用户数量,按数量降序排列" },
|
||||
{ label: "消费趋势", query: "统计每天的交易金额,最近30天" },
|
||||
]
|
||||
|
||||
// 历史查询
|
||||
const historyQueries = [
|
||||
{ id: "1", query: "查找最近30天消费超过1万元的用户", time: "2分钟前", starred: true },
|
||||
{ id: "2", query: "统计各渠道用户转化率", time: "1小时前", starred: false },
|
||||
{ id: "3", query: "获取VIP用户的平均消费金额", time: "2小时前", starred: true },
|
||||
{ id: "4", query: "按年龄段统计用户数量", time: "昨天", starred: false },
|
||||
{ id: "5", query: "查询高活跃度但低消费的用户群体", time: "昨天", starred: false },
|
||||
]
|
||||
|
||||
const handleExecuteQuery = () => {
|
||||
if (!query.trim()) return
|
||||
setIsLoading(true)
|
||||
setShowResult(false)
|
||||
|
||||
// 模拟AI解析和查询
|
||||
setTimeout(() => {
|
||||
setQueryResult({
|
||||
sql: `SELECT u.id, u.name, u.phone, SUM(t.amount) as total_spending
|
||||
FROM users u
|
||||
JOIN transactions t ON u.id = t.user_id
|
||||
WHERE t.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||||
GROUP BY u.id, u.name, u.phone
|
||||
HAVING total_spending > 10000
|
||||
ORDER BY total_spending DESC
|
||||
LIMIT 100`,
|
||||
data: [
|
||||
{ id: "U001", name: "张三", phone: "138****1234", total_spending: 25680 },
|
||||
{ id: "U002", name: "李四", phone: "139****5678", total_spending: 18920 },
|
||||
{ id: "U003", name: "王五", phone: "137****9012", total_spending: 15340 },
|
||||
{ id: "U004", name: "赵六", phone: "136****3456", total_spending: 13280 },
|
||||
{ id: "U005", name: "钱七", phone: "135****7890", total_spending: 12150 },
|
||||
],
|
||||
totalCount: 823,
|
||||
executionTime: "0.23s",
|
||||
explanation: "我将您的查询解析为:从用户表和交易表中联合查询,筛选最近30天内消费总额超过10000元的用户,按消费金额降序排列。",
|
||||
})
|
||||
setShowResult(true)
|
||||
setIsLoading(false)
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
const handleExampleClick = (exampleQuery: string) => {
|
||||
setQuery(exampleQuery)
|
||||
}
|
||||
|
||||
const handleHistoryClick = (historyQuery: string) => {
|
||||
setQuery(historyQuery)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
|
||||
<div className="p-6 space-y-6">
|
||||
{/* 顶部标题 */}
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">自然语言查询</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">用自然语言描述您的数据需求,AI自动转换为SQL并执行查询</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
{/* 左侧:历史记录 */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base font-semibold flex items-center gap-2">
|
||||
<History className="h-4 w-4" />
|
||||
历史查询
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{historyQueries.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => handleHistoryClick(item.query)}
|
||||
className="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors group"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<p className="text-sm text-gray-700 line-clamp-2 group-hover:text-blue-600">
|
||||
{item.query}
|
||||
</p>
|
||||
{item.starred ? (
|
||||
<Star className="h-4 w-4 text-yellow-500 shrink-0" />
|
||||
) : (
|
||||
<StarOff className="h-4 w-4 text-gray-300 shrink-0 opacity-0 group-hover:opacity-100" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 mt-1 text-xs text-gray-400">
|
||||
<Clock className="h-3 w-3" />
|
||||
{item.time}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 右侧:查询区域 */}
|
||||
<div className="lg:col-span-3 space-y-6">
|
||||
{/* 查询输入 */}
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleExecuteQuery()}
|
||||
placeholder="输入您的查询,例如:查找最近30天消费超过1万元的用户"
|
||||
className="pl-12 py-6 text-lg bg-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-500">示例查询:</span>
|
||||
{exampleQueries.map((example, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleExampleClick(example.query)}
|
||||
className="text-sm text-blue-600 hover:text-blue-700 hover:underline"
|
||||
>
|
||||
{example.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Button onClick={handleExecuteQuery} disabled={isLoading || !query.trim()}>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
解析中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
执行查询
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 查询结果 */}
|
||||
{showResult && queryResult && (
|
||||
<div className="space-y-4">
|
||||
{/* AI解释 */}
|
||||
<Card className="bg-blue-50 border-blue-200">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-2 rounded-lg bg-blue-100">
|
||||
<Database className="h-4 w-4 text-blue-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-blue-800">{queryResult.explanation}</p>
|
||||
<div className="flex items-center gap-4 mt-2 text-xs text-blue-600">
|
||||
<span>执行时间:{queryResult.executionTime}</span>
|
||||
<span>结果数量:{queryResult.totalCount} 条</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* SQL和结果 */}
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="bg-gray-100">
|
||||
<TabsTrigger value="query">
|
||||
<Table className="h-4 w-4 mr-1" />
|
||||
查询结果
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="sql">
|
||||
<Database className="h-4 w-4 mr-1" />
|
||||
SQL语句
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="chart">
|
||||
<BarChart3 className="h-4 w-4 mr-1" />
|
||||
图表
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Copy className="h-4 w-4 mr-1" />
|
||||
复制
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Download className="h-4 w-4 mr-1" />
|
||||
导出
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TabsContent value="query" className="mt-0">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-gray-50">
|
||||
<th className="text-left py-3 px-4 font-medium">用户ID</th>
|
||||
<th className="text-left py-3 px-4 font-medium">姓名</th>
|
||||
<th className="text-left py-3 px-4 font-medium">手机号</th>
|
||||
<th className="text-right py-3 px-4 font-medium">消费总额</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{queryResult.data.map((row: any, index: number) => (
|
||||
<tr key={index} className="border-b border-gray-100 hover:bg-gray-50">
|
||||
<td className="py-3 px-4 font-mono text-gray-600">{row.id}</td>
|
||||
<td className="py-3 px-4">{row.name}</td>
|
||||
<td className="py-3 px-4 text-gray-500">{row.phone}</td>
|
||||
<td className="py-3 px-4 text-right text-green-600 font-semibold">
|
||||
¥{row.total_spending.toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-4 pt-4 border-t">
|
||||
<span className="text-sm text-gray-500">
|
||||
显示 1-{queryResult.data.length} 条,共 {queryResult.totalCount} 条
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" disabled>
|
||||
上一页
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
下一页
|
||||
<ChevronRight className="h-4 w-4 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="sql" className="mt-0">
|
||||
<div className="bg-gray-900 rounded-lg p-4 font-mono text-sm text-gray-100 overflow-x-auto">
|
||||
<pre className="whitespace-pre-wrap">{queryResult.sql}</pre>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-4">
|
||||
<Button variant="outline" size="sm">
|
||||
<Copy className="h-4 w-4 mr-1" />
|
||||
复制SQL
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
在SQL编辑器中打开
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="chart" className="mt-0">
|
||||
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-lg">
|
||||
<div className="text-center text-gray-500">
|
||||
<BarChart3 className="h-12 w-12 mx-auto mb-2 text-gray-300" />
|
||||
<p>图表视图即将支持</p>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 无结果时的提示 */}
|
||||
{!showResult && !isLoading && (
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-12 text-center">
|
||||
<Search className="h-12 w-12 mx-auto mb-4 text-gray-300" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">开始您的数据探索</h3>
|
||||
<p className="text-sm text-gray-500 mb-6">
|
||||
输入自然语言描述您想要查询的数据,AI将自动转换为SQL并执行
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
{exampleQueries.map((example, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleExampleClick(example.query)}
|
||||
className="px-4 py-2 rounded-full bg-gray-100 text-sm text-gray-600 hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
{example.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
550
app/ai-agent/smart-tag/page.tsx
Normal file
550
app/ai-agent/smart-tag/page.tsx
Normal file
@@ -0,0 +1,550 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import {
|
||||
Tags,
|
||||
Plus,
|
||||
Search,
|
||||
Play,
|
||||
Pause,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
AlertCircle,
|
||||
Settings,
|
||||
FileText,
|
||||
RefreshCw,
|
||||
Sparkles,
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
Eye,
|
||||
MoreVertical,
|
||||
Bot,
|
||||
Zap,
|
||||
} from "lucide-react"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
|
||||
// 第四部分:AI Agent - AI打标
|
||||
export default function AISmartTagPage() {
|
||||
const [activeTab, setActiveTab] = useState("tasks")
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
||||
const [showReviewDialog, setShowReviewDialog] = useState(false)
|
||||
const [selectedTask, setSelectedTask] = useState<any>(null)
|
||||
const [selectedItems, setSelectedItems] = useState<string[]>([])
|
||||
|
||||
// AI打标任务列表
|
||||
const tagTasks = [
|
||||
{
|
||||
id: "1",
|
||||
name: "高价值用户识别",
|
||||
prompt: "根据用户的消费金额、频次、最近活跃时间,识别高价值用户",
|
||||
status: "completed",
|
||||
targetTag: "高价值用户",
|
||||
dataSource: "用户表+交易表",
|
||||
affectedCount: 45678,
|
||||
confidence: 0.92,
|
||||
createdAt: "2026-01-30 10:00",
|
||||
completedAt: "2026-01-30 10:15",
|
||||
needReview: false,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "流失风险预警",
|
||||
prompt: "识别最近30天未登录且之前活跃的用户,标记为流失风险",
|
||||
status: "pending_review",
|
||||
targetTag: "流失风险",
|
||||
dataSource: "用户行为表",
|
||||
affectedCount: 12456,
|
||||
confidence: 0.87,
|
||||
createdAt: "2026-01-31 09:00",
|
||||
completedAt: "2026-01-31 09:12",
|
||||
needReview: true,
|
||||
reviewStats: { approved: 0, rejected: 0, pending: 12456 },
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "兴趣标签推断",
|
||||
prompt: "根据用户浏览和购买记录,推断用户兴趣偏好",
|
||||
status: "running",
|
||||
targetTag: "兴趣标签",
|
||||
dataSource: "行为日志表",
|
||||
affectedCount: 0,
|
||||
confidence: 0,
|
||||
createdAt: "2026-01-31 14:00",
|
||||
progress: 67,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "地域特征识别",
|
||||
prompt: "根据IP地址和收货地址,识别用户地域特征",
|
||||
status: "failed",
|
||||
targetTag: "地域标签",
|
||||
dataSource: "用户表+订单表",
|
||||
affectedCount: 0,
|
||||
confidence: 0,
|
||||
createdAt: "2026-01-29 15:00",
|
||||
error: "数据源连接超时",
|
||||
},
|
||||
]
|
||||
|
||||
// 待审核的标签结果
|
||||
const pendingReviews = [
|
||||
{ id: "r1", userId: "U10001", userName: "张三", currentTags: ["活跃用户"], newTag: "流失风险", confidence: 0.92, reason: "30天未登录" },
|
||||
{ id: "r2", userId: "U10002", userName: "李四", currentTags: ["普通用户"], newTag: "流失风险", confidence: 0.88, reason: "25天未登录" },
|
||||
{ id: "r3", userId: "U10003", userName: "王五", currentTags: ["新用户"], newTag: "流失风险", confidence: 0.75, reason: "注册后未活跃" },
|
||||
{ id: "r4", userId: "U10004", userName: "赵六", currentTags: ["VIP用户"], newTag: "流失风险", confidence: 0.65, reason: "消费频次下降" },
|
||||
{ id: "r5", userId: "U10005", userName: "钱七", currentTags: ["活跃用户"], newTag: "流失风险", confidence: 0.58, reason: "互动减少" },
|
||||
]
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
return <Badge className="bg-green-100 text-green-700">已完成</Badge>
|
||||
case "running":
|
||||
return <Badge className="bg-blue-100 text-blue-700">执行中</Badge>
|
||||
case "pending_review":
|
||||
return <Badge className="bg-yellow-100 text-yellow-700">待审核</Badge>
|
||||
case "failed":
|
||||
return <Badge className="bg-red-100 text-red-700">失败</Badge>
|
||||
default:
|
||||
return <Badge variant="secondary">未知</Badge>
|
||||
}
|
||||
}
|
||||
|
||||
const getConfidenceColor = (confidence: number) => {
|
||||
if (confidence >= 0.8) return "text-green-600"
|
||||
if (confidence >= 0.6) return "text-yellow-600"
|
||||
return "text-red-600"
|
||||
}
|
||||
|
||||
const handleOpenReview = (task: any) => {
|
||||
setSelectedTask(task)
|
||||
setShowReviewDialog(true)
|
||||
}
|
||||
|
||||
const handleBatchApprove = () => {
|
||||
console.log("批量通过", selectedItems)
|
||||
setSelectedItems([])
|
||||
}
|
||||
|
||||
const handleBatchReject = () => {
|
||||
console.log("批量拒绝", selectedItems)
|
||||
setSelectedItems([])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
|
||||
<div className="p-6 space-y-6">
|
||||
{/* 顶部标题 */}
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">AI智能打标</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">使用AI自动识别用户特征并打标签,支持人工审核确认</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button onClick={() => setShowCreateDialog(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
创建打标任务
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">任务总数</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{tagTasks.length}</p>
|
||||
</div>
|
||||
<Bot className="h-8 w-8 text-purple-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">待审核</p>
|
||||
<p className="text-2xl font-bold text-yellow-600">
|
||||
{tagTasks.filter((t) => t.status === "pending_review").length}
|
||||
</p>
|
||||
</div>
|
||||
<Clock className="h-8 w-8 text-yellow-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">已打标用户</p>
|
||||
<p className="text-2xl font-bold text-green-600">58,134</p>
|
||||
</div>
|
||||
<Tags className="h-8 w-8 text-green-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">平均置信度</p>
|
||||
<p className="text-2xl font-bold text-blue-600">87.5%</p>
|
||||
</div>
|
||||
<Sparkles className="h-8 w-8 text-blue-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<div className="flex items-center justify-between">
|
||||
<TabsList className="bg-white">
|
||||
<TabsTrigger value="tasks">打标任务</TabsTrigger>
|
||||
<TabsTrigger value="review">审核中心</TabsTrigger>
|
||||
<TabsTrigger value="history">历史记录</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索任务..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 w-64 bg-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabsContent value="tasks" className="mt-4">
|
||||
<div className="space-y-4">
|
||||
{tagTasks.map((task) => (
|
||||
<Card key={task.id} className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="font-semibold text-gray-900">{task.name}</h3>
|
||||
{getStatusBadge(task.status)}
|
||||
<Badge variant="outline">{task.targetTag}</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mb-3">{task.prompt}</p>
|
||||
<div className="flex items-center gap-6 text-sm text-gray-500">
|
||||
<span>数据源:{task.dataSource}</span>
|
||||
{task.affectedCount > 0 && (
|
||||
<span>影响用户:{task.affectedCount.toLocaleString()}</span>
|
||||
)}
|
||||
{task.confidence > 0 && (
|
||||
<span className={getConfidenceColor(task.confidence)}>
|
||||
置信度:{(task.confidence * 100).toFixed(0)}%
|
||||
</span>
|
||||
)}
|
||||
<span>创建时间:{task.createdAt}</span>
|
||||
</div>
|
||||
{task.status === "running" && (
|
||||
<div className="mt-3">
|
||||
<div className="flex items-center justify-between text-sm mb-1">
|
||||
<span className="text-gray-500">执行进度</span>
|
||||
<span className="text-blue-600">{task.progress}%</span>
|
||||
</div>
|
||||
<div className="w-full h-2 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-blue-500 rounded-full transition-all"
|
||||
style={{ width: `${task.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{task.status === "failed" && (
|
||||
<div className="mt-3 p-3 bg-red-50 rounded-lg text-sm text-red-600">
|
||||
错误信息:{task.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{task.status === "pending_review" && (
|
||||
<Button size="sm" onClick={() => handleOpenReview(task)}>
|
||||
<Eye className="h-4 w-4 mr-1" />
|
||||
审核
|
||||
</Button>
|
||||
)}
|
||||
{task.status === "failed" && (
|
||||
<Button size="sm" variant="outline">
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
重试
|
||||
</Button>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
查看日志
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
编辑配置
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="review" className="mt-4">
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base font-semibold">待审核标签(共 {pendingReviews.length} 条)</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedItems.length > 0 && (
|
||||
<>
|
||||
<span className="text-sm text-gray-500">已选 {selectedItems.length} 项</span>
|
||||
<Button size="sm" onClick={handleBatchApprove}>
|
||||
<ThumbsUp className="h-4 w-4 mr-1" />
|
||||
批量通过
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={handleBatchReject}>
|
||||
<ThumbsDown className="h-4 w-4 mr-1" />
|
||||
批量拒绝
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{pendingReviews.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`flex items-center justify-between p-4 rounded-lg border transition-colors ${
|
||||
selectedItems.includes(item.id) ? "bg-blue-50 border-blue-200" : "bg-gray-50 border-transparent"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<Checkbox
|
||||
checked={selectedItems.includes(item.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedItems([...selectedItems, item.id])
|
||||
} else {
|
||||
setSelectedItems(selectedItems.filter((id) => id !== item.id))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-900">{item.userName}</span>
|
||||
<span className="text-sm text-gray-500">({item.userId})</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-sm text-gray-500">现有标签:</span>
|
||||
{item.currentTags.map((tag, index) => (
|
||||
<Badge key={index} variant="secondary" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
<span className="text-gray-400">→</span>
|
||||
<Badge className="bg-yellow-100 text-yellow-700 text-xs">
|
||||
+ {item.newTag}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-1">原因:{item.reason}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-gray-500">置信度</p>
|
||||
<p className={`font-semibold ${getConfidenceColor(item.confidence)}`}>
|
||||
{(item.confidence * 100).toFixed(0)}%
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" variant="outline" className="text-green-600 border-green-200 hover:bg-green-50 bg-transparent">
|
||||
<ThumbsUp className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="text-red-600 border-red-200 hover:bg-red-50 bg-transparent">
|
||||
<ThumbsDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="history" className="mt-4">
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-6 text-center text-gray-500">
|
||||
历史记录将显示所有已完成的打标任务及其审核结果
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* 创建任务弹窗 */}
|
||||
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>创建AI打标任务</DialogTitle>
|
||||
<DialogDescription>配置AI打标规则,AI将自动识别符合条件的用户并打标签</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>任务名称</Label>
|
||||
<Input placeholder="例如:高价值用户识别" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>AI提示词</Label>
|
||||
<Textarea
|
||||
placeholder="描述你希望AI如何识别用户,例如:根据用户的消费金额、频次、最近活跃时间,识别高价值用户"
|
||||
rows={4}
|
||||
/>
|
||||
<p className="text-xs text-gray-400">
|
||||
提示:清晰描述识别条件,AI将自动分析数据并打标签
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>目标标签</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择或创建标签" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="high-value">高价值用户</SelectItem>
|
||||
<SelectItem value="churn-risk">流失风险</SelectItem>
|
||||
<SelectItem value="new-user">新用户</SelectItem>
|
||||
<SelectItem value="vip">VIP用户</SelectItem>
|
||||
<SelectItem value="custom">创建新标签...</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>数据源</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择数据源" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="users">用户表</SelectItem>
|
||||
<SelectItem value="transactions">交易表</SelectItem>
|
||||
<SelectItem value="behaviors">行为表</SelectItem>
|
||||
<SelectItem value="all">全部数据</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>置信度阈值</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Input type="number" defaultValue="0.7" min="0" max="1" step="0.1" className="w-24" />
|
||||
<span className="text-sm text-gray-500">低于此阈值的结果需要人工审核</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="auto-approve" />
|
||||
<Label htmlFor="auto-approve" className="text-sm font-normal">
|
||||
高置信度结果自动通过(置信度 ≥ 90%)
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowCreateDialog(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={() => setShowCreateDialog(false)}>
|
||||
<Zap className="h-4 w-4 mr-2" />
|
||||
开始执行
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 审核弹窗 */}
|
||||
<Dialog open={showReviewDialog} onOpenChange={setShowReviewDialog}>
|
||||
<DialogContent className="max-w-4xl max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>审核 - {selectedTask?.name}</DialogTitle>
|
||||
<DialogDescription>
|
||||
共 {selectedTask?.affectedCount.toLocaleString()} 条待审核,请确认AI打标结果
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4 max-h-96 overflow-y-auto">
|
||||
<div className="space-y-3">
|
||||
{pendingReviews.map((item) => (
|
||||
<div key={item.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<span className="font-medium">{item.userName}</span>
|
||||
<span className="text-gray-500 ml-2">({item.userId})</span>
|
||||
<p className="text-sm text-gray-500 mt-1">{item.reason}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={getConfidenceColor(item.confidence)}>
|
||||
{(item.confidence * 100).toFixed(0)}%
|
||||
</span>
|
||||
<Button size="sm" variant="outline" className="text-green-600 bg-transparent">
|
||||
<ThumbsUp className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="text-red-600 bg-transparent">
|
||||
<ThumbsDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowReviewDialog(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
<Button variant="outline">全部拒绝</Button>
|
||||
<Button>全部通过</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,175 +1,61 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { BarChart3, TrendingUp, Users, Download, RefreshCw } from 'lucide-react'
|
||||
import { BarChart, Bar, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'
|
||||
|
||||
export default function DataAnalysis() {
|
||||
const userBehaviorData = [
|
||||
{ hour: '00:00', active: 1250, purchases: 85, interactions: 4200 },
|
||||
{ hour: '04:00', active: 850, purchases: 35, interactions: 1800 },
|
||||
{ hour: '08:00', active: 5600, purchases: 420, interactions: 18500 },
|
||||
{ hour: '12:00', active: 8900, purchases: 680, interactions: 28600 },
|
||||
{ hour: '16:00', active: 7200, purchases: 520, interactions: 22400 },
|
||||
{ hour: '20:00', active: 9500, purchases: 750, interactions: 32800 },
|
||||
]
|
||||
|
||||
const channelData = [
|
||||
{ name: '微信朋友圈', value: 35, color: '#8b5cf6' },
|
||||
{ name: '微信群', value: 28, color: '#3b82f6' },
|
||||
{ name: '一对一私聊', value: 22, color: '#10b981' },
|
||||
{ name: '小程序', value: 15, color: '#f59e0b' },
|
||||
]
|
||||
|
||||
const conversionData = [
|
||||
{ stage: '浏览', users: 100000, rate: 100 },
|
||||
{ stage: '兴趣', users: 45000, rate: 45 },
|
||||
{ stage: '咨询', users: 18000, rate: 18 },
|
||||
{ stage: '下单', users: 5400, rate: 5.4 },
|
||||
{ stage: '复购', users: 1620, rate: 1.6 },
|
||||
]
|
||||
import { Users, Clock, Share2 } from "lucide-react"
|
||||
|
||||
export default function DataAnalysisPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">数据分析</h1>
|
||||
<p className="text-gray-600">用户行为分析、渠道分析与转化漏斗</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
刷新数据
|
||||
</Button>
|
||||
<Button>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出报告
|
||||
</Button>
|
||||
<h1 className="text-3xl font-bold text-gray-900">数据分析</h1>
|
||||
<p className="text-gray-500 mt-1">用户行为深度洞察与渠道分析</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 关键指标 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 mb-6">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">今日活跃用户</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">285.6K</div>
|
||||
<Badge variant="outline" className="border-green-500 text-green-600 bg-green-50">+12.5%</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">转化率</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">5.4%</div>
|
||||
<Badge variant="outline" className="border-blue-500 text-blue-600 bg-blue-50">+0.8%</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">人均互动次数</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">8.6</div>
|
||||
<Badge variant="outline" className="border-purple-500 text-purple-600 bg-purple-50">+1.2</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">客单价</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">¥328</div>
|
||||
<Badge variant="outline" className="border-orange-500 text-orange-600 bg-orange-50">+15.2%</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 用户行为趋势 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<Card>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Users className="w-5 h-5" />
|
||||
<CardTitle className="flex items-center gap-2 text-gray-900">
|
||||
<Clock className="w-5 h-5 text-blue-600" />
|
||||
用户活跃时段分布
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={userBehaviorData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||
<XAxis dataKey="hour" stroke="#6b7280" />
|
||||
<YAxis stroke="#6b7280" />
|
||||
<Tooltip />
|
||||
<Bar dataKey="active" fill="#8b5cf6" name="活跃用户" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-xl border border-dashed border-gray-200 text-gray-400">
|
||||
活跃时段热力图
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<BarChart3 className="w-5 h-5" />
|
||||
渠道分布
|
||||
<CardTitle className="flex items-center gap-2 text-gray-900">
|
||||
<Share2 className="w-5 h-5 text-purple-600" />
|
||||
渠道来源分布
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={channelData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={({ name, value }) => `${name} ${value}%`}
|
||||
outerRadius={100}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
>
|
||||
{channelData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-xl border border-dashed border-gray-200 text-gray-400">
|
||||
渠道占比饼图
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="lg:col-span-2 bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-gray-900">
|
||||
<Users className="w-5 h-5 text-green-600" />
|
||||
用户转化漏斗
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-xl border border-dashed border-gray-200 text-gray-400">
|
||||
转化漏斗图表
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 转化漏斗 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
用户转化漏斗
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{conversionData.map((stage, index) => (
|
||||
<div key={index}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-gray-700 font-medium">{stage.stage}</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-gray-900 font-semibold">{stage.users.toLocaleString()}人</span>
|
||||
<span className="text-blue-600 font-semibold w-16 text-right">{stage.rate}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-12 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg flex items-center px-4 text-white font-semibold" style={{ width: `${stage.rate}%`, minWidth: '10%' }}>
|
||||
{stage.rate}%
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Cpu, BarChart3, TrendingUp, Users, Target, Brain, ArrowRight, Send, Sparkles } from 'lucide-react'
|
||||
import { Cpu, BarChart3, TrendingUp, Users, Target, Brain, ArrowRight, Send, Sparkles } from "lucide-react"
|
||||
|
||||
export default function AIAssistant() {
|
||||
const [message, setMessage] = useState("")
|
||||
@@ -18,53 +18,45 @@ export default function AIAssistant() {
|
||||
},
|
||||
])
|
||||
|
||||
// AI功能卡片
|
||||
const aiFeatures = [
|
||||
{
|
||||
icon: <BarChart3 className="w-8 h-8" />,
|
||||
title: "数据分析",
|
||||
description: "自动分析用户行为,生成洞察报告",
|
||||
color: "from-blue-500 to-cyan-500",
|
||||
href: "/platform/ai-assistant/data-analysis",
|
||||
stats: { reports: 1258, insights: 856 }
|
||||
href: "/ai-assistant/data-analysis",
|
||||
stats: { reports: 1258, insights: 856 },
|
||||
},
|
||||
{
|
||||
icon: <TrendingUp className="w-8 h-8" />,
|
||||
title: "趋势预测",
|
||||
description: "预测用户行为和价值变化趋势",
|
||||
color: "from-purple-500 to-pink-500",
|
||||
href: "/platform/ai-assistant/trend-prediction",
|
||||
stats: { accuracy: 94.2, predictions: 2450 }
|
||||
href: "/ai-assistant/trend-prediction",
|
||||
stats: { accuracy: 94.2, predictions: 2450 },
|
||||
},
|
||||
{
|
||||
icon: <Users className="w-8 h-8" />,
|
||||
title: "用户画像",
|
||||
description: "智能生成用户群体画像",
|
||||
color: "from-green-500 to-emerald-500",
|
||||
href: "/platform/ai-assistant/user-profiling",
|
||||
stats: { profiles: 4285, segments: 128 }
|
||||
href: "/ai-assistant/user-profiling",
|
||||
stats: { profiles: 4285, segments: 128 },
|
||||
},
|
||||
{
|
||||
icon: <Target className="w-8 h-8" />,
|
||||
title: "精准推荐",
|
||||
description: "推荐最佳营销策略和用户群",
|
||||
color: "from-orange-500 to-red-500",
|
||||
href: "/platform/ai-assistant/recommendation",
|
||||
stats: { campaigns: 586, conversion: 18.5 }
|
||||
href: "/ai-assistant/recommendation",
|
||||
stats: { campaigns: 586, conversion: 18.5 },
|
||||
},
|
||||
]
|
||||
|
||||
// 快捷问题
|
||||
const quickQuestions = [
|
||||
"分析今日新增用户特征",
|
||||
"预测本月用户增长趋势",
|
||||
"高价值用户流失预警",
|
||||
"生成本周运营报告",
|
||||
]
|
||||
const quickQuestions = ["分析今日新增用户特征", "预测本月用户增长趋势", "高价值用户流失预警", "生成本周运营报告"]
|
||||
|
||||
const handleSend = () => {
|
||||
if (!message.trim()) return
|
||||
|
||||
setChatHistory([
|
||||
...chatHistory,
|
||||
{
|
||||
@@ -82,23 +74,19 @@ export default function AIAssistant() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center">
|
||||
<Brain className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-gray-900">AI智能助手</h1>
|
||||
<p className="text-gray-600">数据分析、报告生成与智能预测</p>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center shadow-lg">
|
||||
<Brain className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">AI智能助手</h1>
|
||||
<p className="text-gray-500">数据分析、报告生成与智能预测</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* AI状态 */}
|
||||
<Card>
|
||||
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Cpu className="w-5 h-5 text-green-600" />
|
||||
@@ -109,7 +97,7 @@ export default function AIAssistant() {
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">系统状态</span>
|
||||
<Badge variant="outline" className="border-green-500 text-green-600 bg-green-50">
|
||||
<Badge variant="outline" className="border-green-200 text-green-600 bg-green-50">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse mr-2"></div>
|
||||
运行中
|
||||
</Badge>
|
||||
@@ -130,30 +118,47 @@ export default function AIAssistant() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* AI功能卡片 */}
|
||||
<div className="lg:col-span-2 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{aiFeatures.map((feature, index) => (
|
||||
<Link key={index} href={feature.href}>
|
||||
<Card className="hover:shadow-lg transition-shadow cursor-pointer h-full">
|
||||
<Card className="hover:shadow-lg transition-all cursor-pointer h-full bg-white/60 backdrop-blur-md border-gray-200 shadow-sm group">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
<div className={`w-16 h-16 rounded-xl bg-gradient-to-br ${feature.color} flex items-center justify-center text-white`}>
|
||||
<div
|
||||
className={`w-16 h-16 rounded-xl bg-gradient-to-br ${feature.color} flex items-center justify-center text-white shadow-md group-hover:scale-105 transition-transform`}
|
||||
>
|
||||
{feature.icon}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-xl font-semibold text-gray-900">{feature.title}</h3>
|
||||
<ArrowRight className="w-5 h-5 text-gray-400" />
|
||||
<h3 className="text-xl font-bold text-gray-900">{feature.title}</h3>
|
||||
<ArrowRight className="w-5 h-5 text-gray-400 group-hover:text-blue-500 transition-colors" />
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm">{feature.description}</p>
|
||||
<p className="text-gray-500 text-sm">{feature.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-gray-200">
|
||||
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-gray-100">
|
||||
{Object.entries(feature.stats).map(([key, value], i) => (
|
||||
<div key={i}>
|
||||
<div className="text-xs text-gray-600 mb-1">{key === 'reports' ? '报告数' : key === 'insights' ? '洞察数' : key === 'accuracy' ? '准确率' : key === 'predictions' ? '预测数' : key === 'profiles' ? '画像数' : key === 'segments' ? '分组数' : key === 'campaigns' ? '活动数' : '转化率'}</div>
|
||||
<div className="text-lg font-semibold text-gray-900">
|
||||
{typeof value === 'number' && value % 1 !== 0 ? `${value}%` : value}
|
||||
<div className="text-xs text-gray-500 mb-1">
|
||||
{key === "reports"
|
||||
? "报告数"
|
||||
: key === "insights"
|
||||
? "洞察数"
|
||||
: key === "accuracy"
|
||||
? "准确率"
|
||||
: key === "predictions"
|
||||
? "预测数"
|
||||
: key === "profiles"
|
||||
? "画像数"
|
||||
: key === "segments"
|
||||
? "分组数"
|
||||
: key === "campaigns"
|
||||
? "活动数"
|
||||
: "转化率"}
|
||||
</div>
|
||||
<div className="text-lg font-bold text-gray-900">
|
||||
{typeof value === "number" && value % 1 !== 0 ? `${value}%` : value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -164,35 +169,30 @@ export default function AIAssistant() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 对话区域 */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card className="bg-white/5 border-white/10 h-[calc(100vh-200px)]">
|
||||
<CardHeader className="border-b border-white/10">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<BarChart3 className="w-5 h-5" />
|
||||
<div className="lg:col-span-3">
|
||||
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm h-[600px] flex flex-col">
|
||||
<CardHeader className="border-b border-gray-100">
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<BarChart3 className="w-5 h-5 text-blue-600" />
|
||||
AI对话
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6 flex flex-col h-[calc(100%-80px)]">
|
||||
{/* 聊天历史 */}
|
||||
<div className="flex-1 overflow-y-auto space-y-4 mb-6">
|
||||
<CardContent className="p-6 flex flex-col flex-1 overflow-hidden">
|
||||
<div className="flex-1 overflow-y-auto space-y-4 mb-6 pr-2">
|
||||
{chatHistory.map((chat, index) => (
|
||||
<div key={index} className={`flex ${chat.role === "user" ? "justify-end" : "justify-start"}`}>
|
||||
<div
|
||||
className={`max-w-[80%] rounded-2xl p-4 ${
|
||||
chat.role === "user"
|
||||
? "bg-gradient-to-br from-blue-500 to-purple-500 text-white"
|
||||
: "bg-white/10 text-white"
|
||||
}`}
|
||||
className={`max-w-[80%] rounded-2xl p-4 shadow-sm ${chat.role === "user" ? "bg-gradient-to-br from-blue-500 to-purple-500 text-white" : "bg-white border border-gray-100 text-gray-800"}`}
|
||||
>
|
||||
<div className="mb-1">{chat.content}</div>
|
||||
<div className="text-xs opacity-70">{chat.timestamp}</div>
|
||||
<div className={`text-xs ${chat.role === "user" ? "text-blue-100" : "text-gray-400"}`}>
|
||||
{chat.timestamp}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 快捷问题 */}
|
||||
<div className="mb-4 flex flex-wrap gap-2">
|
||||
{quickQuestions.map((question, index) => (
|
||||
<Button
|
||||
@@ -200,15 +200,14 @@ export default function AIAssistant() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setMessage(question)}
|
||||
className="bg-white/5 border-white/10 hover:bg-white/10 text-xs"
|
||||
className="bg-white hover:bg-gray-50 text-xs border-gray-200 text-gray-600"
|
||||
>
|
||||
<Sparkles className="w-3 h-3 mr-1" />
|
||||
<Sparkles className="w-3 h-3 mr-1 text-yellow-500" />
|
||||
{question}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 输入框 */}
|
||||
<div className="flex items-end gap-3">
|
||||
<Textarea
|
||||
value={message}
|
||||
@@ -220,11 +219,14 @@ export default function AIAssistant() {
|
||||
}
|
||||
}}
|
||||
placeholder="输入您的问题,按Enter发送..."
|
||||
className="bg-white/5 border-white/10 text-white resize-none"
|
||||
className="bg-white border-gray-200 text-gray-900 resize-none focus:ring-blue-500"
|
||||
rows={3}
|
||||
/>
|
||||
<Button onClick={handleSend} className="bg-gradient-to-r from-blue-500 to-purple-500 h-[88px]">
|
||||
<Send className="w-5 h-5" />
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
className="bg-gradient-to-r from-blue-500 to-purple-500 h-[88px] w-20 shadow-md hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<Send className="w-6 h-6" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,313 +1,75 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Target, Users, TrendingUp, Zap, Download, RefreshCw, ArrowRight } from 'lucide-react'
|
||||
|
||||
export default function Recommendation() {
|
||||
const recommendedCampaigns = [
|
||||
{
|
||||
id: 1,
|
||||
title: '美妆新品推广',
|
||||
targetGroup: '高净值女性用户',
|
||||
userCount: 856780,
|
||||
expectedRevenue: 12850000,
|
||||
probability: 82,
|
||||
roi: 3.5,
|
||||
timeframe: '未来7天',
|
||||
actions: ['朋友圈广告', '私聊推送', '限时优惠'],
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '会员升级计划',
|
||||
targetGroup: '活跃普通会员',
|
||||
userCount: 1285670,
|
||||
expectedRevenue: 8560000,
|
||||
probability: 75,
|
||||
roi: 4.2,
|
||||
timeframe: '未来15天',
|
||||
actions: ['会员权益推广', '专属优惠', 'VIP体验券'],
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '沉默用户唤醒',
|
||||
targetGroup: '30天未活跃用户',
|
||||
userCount: 2856780,
|
||||
expectedRevenue: 5680000,
|
||||
probability: 58,
|
||||
roi: 2.8,
|
||||
timeframe: '未来30天',
|
||||
actions: ['优惠券发放', '新品预告', '个性化推荐'],
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '交叉销售',
|
||||
targetGroup: '单品类购买用户',
|
||||
userCount: 1714283,
|
||||
expectedRevenue: 9280000,
|
||||
probability: 68,
|
||||
roi: 3.1,
|
||||
timeframe: '未来14天',
|
||||
actions: ['关联商品推荐', '搭配优惠', '场景营销'],
|
||||
priority: 'medium'
|
||||
},
|
||||
]
|
||||
|
||||
const recommendedSegments = [
|
||||
{
|
||||
name: '高转化潜力群体',
|
||||
count: 428567,
|
||||
conversion: 35.8,
|
||||
value: 580,
|
||||
characteristics: ['近期浏览频繁', '价格接受度高', '品牌认知强']
|
||||
},
|
||||
{
|
||||
name: '复购价值用户',
|
||||
count: 856780,
|
||||
conversion: 28.5,
|
||||
value: 420,
|
||||
characteristics: ['首购满意度高', '客单价适中', '活跃度稳定']
|
||||
},
|
||||
{
|
||||
name: '裂变传播节点',
|
||||
count: 285670,
|
||||
conversion: 22.3,
|
||||
value: 320,
|
||||
characteristics: ['社交影响力大', '分享意愿强', '粉丝基数高']
|
||||
},
|
||||
]
|
||||
|
||||
const contentRecommendations = [
|
||||
{
|
||||
type: '朋友圈内容',
|
||||
theme: '新品上市+限时优惠',
|
||||
targetUsers: 2856780,
|
||||
expectedEngagement: 18.5,
|
||||
bestTime: '20:00-22:00'
|
||||
},
|
||||
{
|
||||
type: '私聊话术',
|
||||
theme: '专属VIP权益介绍',
|
||||
targetUsers: 856780,
|
||||
expectedEngagement: 42.8,
|
||||
bestTime: '10:00-12:00'
|
||||
},
|
||||
{
|
||||
type: '社群活动',
|
||||
theme: '用户互动+抽奖',
|
||||
targetUsers: 1285670,
|
||||
expectedEngagement: 32.5,
|
||||
bestTime: '周末 15:00-17:00'
|
||||
},
|
||||
]
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
return priority === 'high' ? 'border-red-500 text-red-600 bg-red-50' : 'border-yellow-500 text-yellow-600 bg-yellow-50'
|
||||
}
|
||||
import { Target, Zap, MessageSquare, Gift } from "lucide-react"
|
||||
|
||||
export default function RecommendationPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">精准推荐</h1>
|
||||
<p className="text-gray-600">AI智能推荐营销策略、目标用户群与内容方案</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
刷新推荐
|
||||
</Button>
|
||||
<Button>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出方案
|
||||
</Button>
|
||||
<h1 className="text-3xl font-bold text-gray-900">精准推荐</h1>
|
||||
<p className="text-gray-500 mt-1">智能营销策略与内容推荐</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 核心指标 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 mb-6">
|
||||
<Card className="bg-gradient-to-br from-purple-50 to-purple-100 border-purple-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">推荐方案数</CardTitle>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-gray-900">
|
||||
<Zap className="w-5 h-5 text-yellow-500" />
|
||||
营销方案推荐
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-purple-600">586</div>
|
||||
<div className="text-sm text-gray-600">本月生成</div>
|
||||
<div className="h-48 flex items-center justify-center bg-gray-50 rounded-xl border border-dashed border-gray-200 text-gray-400">
|
||||
最佳活动方案
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-blue-50 to-blue-100 border-blue-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">平均转化率</CardTitle>
|
||||
|
||||
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-gray-900">
|
||||
<Target className="w-5 h-5 text-red-500" />
|
||||
目标群体推荐
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-blue-600">18.5%</div>
|
||||
<Badge variant="outline" className="border-green-500 text-green-600 bg-green-50">+3.2%</Badge>
|
||||
<div className="h-48 flex items-center justify-center bg-gray-50 rounded-xl border border-dashed border-gray-200 text-gray-400">
|
||||
高转化潜力人群
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-green-50 to-green-100 border-green-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">预计收益</CardTitle>
|
||||
|
||||
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-gray-900">
|
||||
<MessageSquare className="w-5 h-5 text-blue-500" />
|
||||
话术内容推荐
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-green-600">¥36.4M</div>
|
||||
<div className="text-sm text-gray-600">近期方案</div>
|
||||
<div className="h-48 flex items-center justify-center bg-gray-50 rounded-xl border border-dashed border-gray-200 text-gray-400">
|
||||
智能生成营销文案
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-orange-50 to-orange-100 border-orange-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">平均ROI</CardTitle>
|
||||
|
||||
<Card className="lg:col-span-3 bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-gray-900">
|
||||
<Gift className="w-5 h-5 text-purple-600" />
|
||||
权益匹配建议
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-orange-600">3.4x</div>
|
||||
<div className="text-sm text-gray-600">投入产出比</div>
|
||||
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-xl border border-dashed border-gray-200 text-gray-400">
|
||||
用户权益最优匹配矩阵
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 推荐营销方案 */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Target className="w-5 h-5" />
|
||||
推荐营销方案
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{recommendedCampaigns.map((campaign) => (
|
||||
<div key={campaign.id} className="p-6 bg-white border border-gray-200 rounded-lg hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-xl font-bold text-gray-900">{campaign.title}</h3>
|
||||
<Badge variant="outline" className={getPriorityColor(campaign.priority)}>
|
||||
{campaign.priority === 'high' ? '高优先级' : '中优先级'}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-gray-300 text-gray-600">
|
||||
{campaign.timeframe}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-gray-600">目标群体: {campaign.targetGroup}</p>
|
||||
</div>
|
||||
<Button>
|
||||
执行方案
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-6 mb-4">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-1">目标用户数</div>
|
||||
<div className="text-lg font-semibold text-gray-900">{campaign.userCount.toLocaleString()}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-1">预期收益</div>
|
||||
<div className="text-lg font-semibold text-green-600">¥{(campaign.expectedRevenue / 10000).toFixed(1)}万</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-1">成功概率</div>
|
||||
<div className="text-lg font-semibold text-blue-600">{campaign.probability}%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-1">预期ROI</div>
|
||||
<div className="text-lg font-semibold text-purple-600">{campaign.roi}x</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-2">推荐动作</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{campaign.actions.map((action, index) => (
|
||||
<Badge key={index} variant="outline" className="border-blue-300 text-blue-600 bg-blue-50">
|
||||
<Zap className="w-3 h-3 mr-1" />
|
||||
{action}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 推荐目标群体 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
{recommendedSegments.map((segment, index) => (
|
||||
<Card key={index}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 text-lg">{segment.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 mb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">用户数</span>
|
||||
<span className="text-lg font-semibold text-gray-900">{segment.count.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">预期转化率</span>
|
||||
<span className="text-lg font-semibold text-green-600">{segment.conversion}%</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">人均价值</span>
|
||||
<span className="text-lg font-semibold text-blue-600">¥{segment.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-3 border-t border-gray-200">
|
||||
<div className="text-sm text-gray-600 mb-2">群体特征</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{segment.characteristics.map((char, i) => (
|
||||
<Badge key={i} variant="outline" className="border-gray-300 text-gray-700 text-xs">
|
||||
{char}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 内容推荐 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
内容推荐
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{contentRecommendations.map((content, index) => (
|
||||
<div key={index} className="p-4 bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg border border-blue-200">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold text-gray-900 mb-1">{content.type}</h4>
|
||||
<p className="text-gray-600">{content.theme}</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="border-purple-500 text-purple-600 bg-purple-50">
|
||||
最佳时间: {content.bestTime}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-600">目标触达: </span>
|
||||
<span className="font-semibold text-gray-900">{content.targetUsers.toLocaleString()} 人</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">预期互动率: </span>
|
||||
<span className="font-semibold text-green-600">{content.expectedEngagement}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,222 +1,75 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { TrendingUp, TrendingDown, AlertCircle, Download, RefreshCw } from 'lucide-react'
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts'
|
||||
|
||||
export default function TrendPrediction() {
|
||||
const userGrowthPrediction = [
|
||||
{ date: '2024-06', actual: 428, predicted: null },
|
||||
{ date: '2024-07', actual: null, predicted: 445 },
|
||||
{ date: '2024-08', actual: null, predicted: 468 },
|
||||
{ date: '2024-09', actual: null, predicted: 495 },
|
||||
{ date: '2024-10', actual: null, predicted: 528 },
|
||||
{ date: '2024-11', actual: null, predicted: 565 },
|
||||
{ date: '2024-12', actual: null, predicted: 608 },
|
||||
]
|
||||
|
||||
const valuePrediction = [
|
||||
{ date: '2024-06', actual: 125.8, predicted: null },
|
||||
{ date: '2024-07', actual: null, predicted: 132.5 },
|
||||
{ date: '2024-08', actual: null, predicted: 141.2 },
|
||||
{ date: '2024-09', actual: null, predicted: 151.8 },
|
||||
{ date: '2024-10', actual: null, predicted: 164.5 },
|
||||
{ date: '2024-11', actual: null, predicted: 179.2 },
|
||||
{ date: '2024-12', actual: null, predicted: 196.5 },
|
||||
]
|
||||
|
||||
const churnRiskData = [
|
||||
{ segment: '高价值用户', risk: 8.5, count: 1285, trend: 'down' },
|
||||
{ segment: 'VIP用户', risk: 12.3, count: 856, trend: 'stable' },
|
||||
{ segment: '活跃用户', risk: 18.6, count: 12856, trend: 'up' },
|
||||
{ segment: '沉默用户', risk: 45.8, count: 28560, trend: 'up' },
|
||||
]
|
||||
|
||||
const opportunityData = [
|
||||
{ name: '新品推广机会', potential: '¥85.6M', probability: 78, timeframe: '未来30天' },
|
||||
{ name: '复购唤醒', potential: '¥56.2M', probability: 65, timeframe: '未来15天' },
|
||||
{ name: '会员升级', potential: '¥38.5M', probability: 82, timeframe: '未来7天' },
|
||||
{ name: '交叉销售', potential: '¥92.8M', probability: 58, timeframe: '未来45天' },
|
||||
]
|
||||
import { TrendingUp, AlertTriangle, DollarSign, Activity } from "lucide-react"
|
||||
|
||||
export default function TrendPredictionPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">趋势预测</h1>
|
||||
<p className="text-gray-600">用户增长预测、价值预测与流失预警</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
重新预测
|
||||
</Button>
|
||||
<Button>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出报告
|
||||
</Button>
|
||||
<h1 className="text-3xl font-bold text-gray-900">趋势预测</h1>
|
||||
<p className="text-gray-500 mt-1">AI驱动的未来趋势与风险预警</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 预测指标 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 mb-6">
|
||||
<Card className="bg-gradient-to-br from-blue-50 to-blue-100 border-blue-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">下月预测用户数</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-blue-600 mb-1">445M</div>
|
||||
<div className="text-sm text-gray-600">+3.9% 增长</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-green-50 to-green-100 border-green-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">下月预测价值</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-green-600 mb-1">¥132.5亿</div>
|
||||
<div className="text-sm text-gray-600">+5.3% 增长</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-orange-50 to-orange-100 border-orange-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">流失风险用户</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-orange-600 mb-1">42.5K</div>
|
||||
<div className="text-sm text-gray-600">需重点关注</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-purple-50 to-purple-100 border-purple-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">预测准确率</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-purple-600 mb-1">94.2%</div>
|
||||
<div className="text-sm text-gray-600">基于历史数据</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 趋势图表 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<Card>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<CardTitle className="flex items-center gap-2 text-gray-900">
|
||||
<TrendingUp className="w-5 h-5 text-blue-600" />
|
||||
用户增长预测
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={userGrowthPrediction}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||
<XAxis dataKey="date" stroke="#6b7280" />
|
||||
<YAxis stroke="#6b7280" />
|
||||
<Tooltip />
|
||||
<Line type="monotone" dataKey="actual" stroke="#3b82f6" strokeWidth={2} name="实际值" />
|
||||
<Line type="monotone" dataKey="predicted" stroke="#8b5cf6" strokeWidth={2} strokeDasharray="5 5" name="预测值" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-xl border border-dashed border-gray-200 text-gray-400">
|
||||
未来30天增长曲线预测
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-green-600" />
|
||||
资产价值预测
|
||||
<CardTitle className="flex items-center gap-2 text-gray-900">
|
||||
<DollarSign className="w-5 h-5 text-green-600" />
|
||||
价值提升潜力
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={valuePrediction}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||
<XAxis dataKey="date" stroke="#6b7280" />
|
||||
<YAxis stroke="#6b7280" />
|
||||
<Tooltip />
|
||||
<Area type="monotone" dataKey="actual" stroke="#10b981" fill="#10b981" fillOpacity={0.3} name="实际值" />
|
||||
<Area type="monotone" dataKey="predicted" stroke="#8b5cf6" fill="#8b5cf6" fillOpacity={0.2} strokeDasharray="5 5" name="预测值" />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-xl border border-dashed border-gray-200 text-gray-400">
|
||||
LTV价值预测模型
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-gray-900">
|
||||
<AlertTriangle className="w-5 h-5 text-orange-500" />
|
||||
流失风险预警
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-xl border border-dashed border-gray-200 text-gray-400">
|
||||
高风险用户群分布
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-gray-900">
|
||||
<Activity className="w-5 h-5 text-purple-600" />
|
||||
商机预测
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-xl border border-dashed border-gray-200 text-gray-400">
|
||||
潜在高价值转化机会
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 流失风险预警 */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<AlertCircle className="w-5 h-5 text-orange-600" />
|
||||
流失风险预警
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{churnRiskData.map((item, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center gap-4">
|
||||
<div>
|
||||
<div className="text-lg font-semibold text-gray-900">{item.segment}</div>
|
||||
<div className="text-sm text-gray-600">{item.count.toLocaleString()} 用户</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-orange-600">{item.risk}%</div>
|
||||
<div className="text-xs text-gray-600">流失风险</div>
|
||||
</div>
|
||||
{item.trend === 'down' ? (
|
||||
<TrendingDown className="w-6 h-6 text-green-600" />
|
||||
) : item.trend === 'up' ? (
|
||||
<TrendingUp className="w-6 h-6 text-red-600" />
|
||||
) : (
|
||||
<div className="w-6 h-6 flex items-center justify-center">
|
||||
<div className="w-4 h-0.5 bg-gray-400"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 商机预测 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-purple-600" />
|
||||
商机预测
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{opportunityData.map((item, index) => (
|
||||
<div key={index} className="p-4 bg-gradient-to-br from-purple-50 to-pink-50 rounded-lg border border-purple-200">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-lg font-semibold text-gray-900">{item.name}</h3>
|
||||
<Badge variant="outline" className="border-purple-500 text-purple-600 bg-purple-50">
|
||||
{item.timeframe}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-end justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-1">预测收益</div>
|
||||
<div className="text-2xl font-bold text-purple-600">{item.potential}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-gray-600 mb-1">成功概率</div>
|
||||
<div className="text-2xl font-bold text-green-600">{item.probability}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,239 +1,75 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Users, Tag, Download, RefreshCw, TrendingUp } from 'lucide-react'
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar } from 'recharts'
|
||||
|
||||
export default function UserProfiling() {
|
||||
const ageDistribution = [
|
||||
{ range: '18-25', count: 856780, percentage: 20 },
|
||||
{ range: '26-35', count: 1500000, percentage: 35 },
|
||||
{ range: '36-45', count: 1285670, percentage: 30 },
|
||||
{ range: '46-55', count: 428567, percentage: 10 },
|
||||
{ range: '55+', count: 214284, percentage: 5 },
|
||||
]
|
||||
|
||||
const behaviorProfile = [
|
||||
{ aspect: '消费能力', value: 85 },
|
||||
{ aspect: '活跃度', value: 72 },
|
||||
{ aspect: '互动频率', value: 68 },
|
||||
{ aspect: '忠诚度', value: 78 },
|
||||
{ aspect: '推荐意愿', value: 62 },
|
||||
{ aspect: '复购率', value: 75 },
|
||||
]
|
||||
|
||||
const segments = [
|
||||
{
|
||||
name: '高净值群体',
|
||||
count: 1284567,
|
||||
characteristics: ['消费能力强', '忠诚度高', '品牌认知强'],
|
||||
value: 580,
|
||||
color: 'purple'
|
||||
},
|
||||
{
|
||||
name: '年轻潮流族',
|
||||
count: 2856780,
|
||||
characteristics: ['追求时尚', '社交活跃', '冲动消费'],
|
||||
value: 280,
|
||||
color: 'blue'
|
||||
},
|
||||
{
|
||||
name: '理性消费者',
|
||||
count: 4285670,
|
||||
characteristics: ['价格敏感', '重视品质', '对比研究'],
|
||||
value: 180,
|
||||
color: 'green'
|
||||
},
|
||||
{
|
||||
name: '价格敏感型',
|
||||
count: 1714283,
|
||||
characteristics: ['促销驱动', '低频复购', '优惠敏感'],
|
||||
value: 85,
|
||||
color: 'yellow'
|
||||
},
|
||||
]
|
||||
|
||||
const interestTags = [
|
||||
{ tag: '美妆护肤', count: 2856780, heat: 95 },
|
||||
{ tag: '时尚穿搭', count: 2145680, heat: 88 },
|
||||
{ tag: '健康养生', count: 1856780, heat: 76 },
|
||||
{ tag: '数码科技', count: 1428567, heat: 72 },
|
||||
{ tag: '母婴育儿', count: 1285670, heat: 68 },
|
||||
{ tag: '美食烹饪', count: 985670, heat: 58 },
|
||||
{ tag: '运动健身', count: 856780, heat: 52 },
|
||||
{ tag: '旅游出行', count: 714283, heat: 45 },
|
||||
]
|
||||
import { Users, Fingerprint, Layers, Tag } from "lucide-react"
|
||||
|
||||
export default function UserProfilingPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">用户画像</h1>
|
||||
<p className="text-gray-600">智能分析用户群体特征与行为偏好</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
重新分析
|
||||
</Button>
|
||||
<Button>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出画像
|
||||
</Button>
|
||||
<h1 className="text-3xl font-bold text-gray-900">用户画像</h1>
|
||||
<p className="text-gray-500 mt-1">全维度用户特征与群体细分</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 核心指标 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 mb-6">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">用户群体数</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">128</div>
|
||||
<div className="text-sm text-gray-600">覆盖42.8亿用户</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">标签覆盖率</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">87.5%</div>
|
||||
<Badge variant="outline" className="border-green-500 text-green-600 bg-green-50">优秀</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">画像完整度</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">92.3%</div>
|
||||
<Badge variant="outline" className="border-blue-500 text-blue-600 bg-blue-50">优秀</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">更新频率</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">实时</div>
|
||||
<div className="text-sm text-gray-600">每小时更新</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 年龄分布与行为画像 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<Card>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Users className="w-5 h-5" />
|
||||
年龄分布
|
||||
<CardTitle className="flex items-center gap-2 text-gray-900">
|
||||
<Users className="w-5 h-5 text-blue-600" />
|
||||
年龄与性别分布
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={ageDistribution}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||
<XAxis dataKey="range" stroke="#6b7280" />
|
||||
<YAxis stroke="#6b7280" />
|
||||
<Tooltip />
|
||||
<Bar dataKey="count" fill="#8b5cf6" name="用户数" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-xl border border-dashed border-gray-200 text-gray-400">
|
||||
人口属性统计图
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
行为画像
|
||||
<CardTitle className="flex items-center gap-2 text-gray-900">
|
||||
<Fingerprint className="w-5 h-5 text-purple-600" />
|
||||
行为特征雷达图
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<RadarChart data={behaviorProfile}>
|
||||
<PolarGrid stroke="#e5e7eb" />
|
||||
<PolarAngleAxis dataKey="aspect" stroke="#6b7280" />
|
||||
<PolarRadiusAxis stroke="#6b7280" />
|
||||
<Radar name="行为指数" dataKey="value" stroke="#8b5cf6" fill="#8b5cf6" fillOpacity={0.5} />
|
||||
<Tooltip />
|
||||
</RadarChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-xl border border-dashed border-gray-200 text-gray-400">
|
||||
用户行为偏好雷达
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-gray-900">
|
||||
<Layers className="w-5 h-5 text-green-600" />
|
||||
群体细分
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-xl border border-dashed border-gray-200 text-gray-400">
|
||||
用户聚类分析结果
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-gray-900">
|
||||
<Tag className="w-5 h-5 text-orange-500" />
|
||||
兴趣标签热度
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-xl border border-dashed border-gray-200 text-gray-400">
|
||||
标签云展示
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 用户群体细分 */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Users className="w-5 h-5" />
|
||||
用户群体细分
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{segments.map((segment, index) => (
|
||||
<div key={index} className={`p-6 bg-gradient-to-br from-${segment.color}-50 to-${segment.color}-100 rounded-lg border border-${segment.color}-200`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-xl font-bold text-gray-900">{segment.name}</h3>
|
||||
<Badge variant="outline" className={`border-${segment.color}-500 text-${segment.color}-600 bg-${segment.color}-50`}>
|
||||
{segment.count.toLocaleString()} 人
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-2 mb-4">
|
||||
{segment.characteristics.map((char, i) => (
|
||||
<Badge key={i} variant="outline" className="mr-2 border-gray-300 text-gray-700">
|
||||
{char}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
人均价值: <span className="text-lg font-semibold text-gray-900">¥{segment.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 兴趣标签热度 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Tag className="w-5 h-5" />
|
||||
兴趣标签热度
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{interestTags.map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-gray-700 font-medium">{item.tag}</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-gray-600">{item.count.toLocaleString()} 人</span>
|
||||
<span className="text-sm font-semibold text-blue-600">热度 {item.heat}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"
|
||||
style={{ width: `${item.heat}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
3
app/ai-insight/data-cleaning/loading.tsx
Normal file
3
app/ai-insight/data-cleaning/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
881
app/ai-insight/data-cleaning/page.tsx
Normal file
881
app/ai-insight/data-cleaning/page.tsx
Normal file
@@ -0,0 +1,881 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Slider } from "@/components/ui/slider"
|
||||
import {
|
||||
ArrowLeft,
|
||||
Sparkles,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
Play,
|
||||
Settings,
|
||||
Eye,
|
||||
RefreshCw,
|
||||
Database,
|
||||
Zap,
|
||||
Plus,
|
||||
Brain,
|
||||
MessageSquare,
|
||||
Shield,
|
||||
History,
|
||||
Check,
|
||||
X,
|
||||
Pause,
|
||||
FileText,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
const availableDataSources = [
|
||||
{ id: "1", name: "存客宝-用户主库", type: "mysql", records: 128956342, tables: ["users", "orders", "profiles"] },
|
||||
{ id: "2", name: "触客宝-行为数据", type: "kafka", records: 89234567, tables: ["events", "sessions"] },
|
||||
{ id: "3", name: "数智员工-账号API", type: "restapi", records: 45678901, tables: ["accounts", "activities"] },
|
||||
{ id: "7", name: "微信消息-Webhook", type: "webhook", records: 34567890, tables: ["messages", "contacts"] },
|
||||
]
|
||||
|
||||
const aiCleaningRules = [
|
||||
{
|
||||
id: 1,
|
||||
name: "用户数据智能清洗",
|
||||
description: "自动识别并修正用户数据中的格式错误、缺失值和异常值",
|
||||
dataSource: "存客宝-用户主库",
|
||||
targetTable: "users",
|
||||
prompt:
|
||||
"分析用户数据表,识别以下问题并提供修正建议:1) 手机号格式不规范 2) 邮箱格式错误 3) 姓名包含特殊字符 4) 地址信息不完整 5) 日期格式不统一。对于每个问题,给出具体的修正规则。",
|
||||
model: "GPT-4",
|
||||
status: "running",
|
||||
confidenceThreshold: 0.8,
|
||||
autoApproveThreshold: 0.95,
|
||||
processed: 125800,
|
||||
pending: 3400,
|
||||
approved: 120000,
|
||||
rejected: 2400,
|
||||
lastRun: "10分钟前",
|
||||
accuracy: 96.2,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "交易流水异常检测",
|
||||
description: "识别交易数据中的异常记录和潜在错误",
|
||||
dataSource: "存客宝-用户主库",
|
||||
targetTable: "orders",
|
||||
prompt:
|
||||
"检查交易流水数据,识别以下异常:1) 金额为负或超出合理范围 2) 时间戳异常 3) 重复交易记录 4) 用户ID与订单不匹配 5) 状态字段值异常。标记问题记录并建议处理方式。",
|
||||
model: "GPT-4",
|
||||
status: "running",
|
||||
confidenceThreshold: 0.7,
|
||||
autoApproveThreshold: 0.9,
|
||||
processed: 89000,
|
||||
pending: 1200,
|
||||
approved: 85000,
|
||||
rejected: 2800,
|
||||
lastRun: "30分钟前",
|
||||
accuracy: 94.5,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "聊天记录语义清洗",
|
||||
description: "清洗聊天记录中的敏感信息和无效内容",
|
||||
dataSource: "微信消息-Webhook",
|
||||
targetTable: "messages",
|
||||
prompt:
|
||||
"处理聊天记录数据:1) 识别并脱敏个人敏感信息(身份证、银行卡号等)2) 过滤垃圾信息和广告 3) 标准化时间格式 4) 提取并分类用户意图 5) 标记需要人工处理的复杂内容。",
|
||||
model: "Claude-3",
|
||||
status: "paused",
|
||||
confidenceThreshold: 0.6,
|
||||
autoApproveThreshold: 0.85,
|
||||
processed: 456000,
|
||||
pending: 15000,
|
||||
approved: 420000,
|
||||
rejected: 21000,
|
||||
lastRun: "2小时前",
|
||||
accuracy: 91.8,
|
||||
},
|
||||
]
|
||||
|
||||
const pendingCleaningSuggestions = [
|
||||
{
|
||||
id: 1,
|
||||
ruleId: 1,
|
||||
ruleName: "用户数据智能清洗",
|
||||
field: "phone",
|
||||
originalValue: "86-138-1234-5678",
|
||||
suggestedValue: "13812345678",
|
||||
confidence: 0.95,
|
||||
reason: "去除国际区号前缀和分隔符,保留纯数字手机号",
|
||||
affectedRows: 2350,
|
||||
createdAt: "2025-12-12 14:30",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
ruleId: 1,
|
||||
ruleName: "用户数据智能清洗",
|
||||
field: "email",
|
||||
originalValue: "user@example,com",
|
||||
suggestedValue: "user@example.com",
|
||||
confidence: 0.88,
|
||||
reason: "修正邮箱格式,将逗号替换为点号",
|
||||
affectedRows: 156,
|
||||
createdAt: "2025-12-12 14:28",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
ruleId: 2,
|
||||
ruleName: "交易流水异常检测",
|
||||
field: "amount",
|
||||
originalValue: "-500.00",
|
||||
suggestedValue: "500.00",
|
||||
confidence: 0.72,
|
||||
reason: "检测到负数金额,建议修正为正数(可能是退款记录录入错误)",
|
||||
affectedRows: 45,
|
||||
createdAt: "2025-12-12 14:25",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
ruleId: 3,
|
||||
ruleName: "聊天记录语义清洗",
|
||||
field: "content",
|
||||
originalValue: "我的身份证号是310101199001011234",
|
||||
suggestedValue: "我的身份证号是310***********1234",
|
||||
confidence: 0.92,
|
||||
reason: "检测到身份证号,进行脱敏处理保护用户隐私",
|
||||
affectedRows: 890,
|
||||
createdAt: "2025-12-12 14:22",
|
||||
},
|
||||
]
|
||||
|
||||
// 清洗历史
|
||||
const cleaningHistory = [
|
||||
{ id: 1, rule: "用户数据智能清洗", action: "approved", operator: "系统自动", count: 5000, time: "2025-12-12 14:00" },
|
||||
{ id: 2, rule: "交易流水异常检测", action: "approved", operator: "张管理员", count: 120, time: "2025-12-12 13:45" },
|
||||
{
|
||||
id: 3,
|
||||
rule: "聊天记录语义清洗",
|
||||
action: "rejected",
|
||||
operator: "李管理员",
|
||||
count: 35,
|
||||
time: "2025-12-12 13:30",
|
||||
reason: "置信度过低",
|
||||
},
|
||||
]
|
||||
|
||||
export default function AIDataCleaningPage() {
|
||||
const [activeTab, setActiveTab] = useState("rules")
|
||||
const [createRuleDialogOpen, setCreateRuleDialogOpen] = useState(false)
|
||||
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false)
|
||||
const [previewDialogOpen, setPreviewDialogOpen] = useState(false)
|
||||
const [selectedRule, setSelectedRule] = useState<(typeof aiCleaningRules)[0] | null>(null)
|
||||
const [selectedSuggestion, setSelectedSuggestion] = useState<(typeof pendingCleaningSuggestions)[0] | null>(null)
|
||||
const [selectedSuggestions, setSelectedSuggestions] = useState<number[]>([])
|
||||
const [confidenceFilter, setConfidenceFilter] = useState([0.5])
|
||||
|
||||
const [newRule, setNewRule] = useState({
|
||||
name: "",
|
||||
dataSource: "",
|
||||
targetTable: "",
|
||||
prompt: "",
|
||||
model: "gpt-4",
|
||||
confidenceThreshold: 0.7,
|
||||
autoApproveThreshold: 0.9,
|
||||
})
|
||||
|
||||
const totalPending = aiCleaningRules.reduce((sum, r) => sum + r.pending, 0)
|
||||
const totalApproved = aiCleaningRules.reduce((sum, r) => sum + r.approved, 0)
|
||||
const totalProcessed = aiCleaningRules.reduce((sum, r) => sum + r.processed, 0)
|
||||
|
||||
const filteredSuggestions = pendingCleaningSuggestions.filter((s) => s.confidence >= confidenceFilter[0])
|
||||
|
||||
// 获取选中数据源的表列表
|
||||
const getTablesForDataSource = (dsName: string) => {
|
||||
const ds = availableDataSources.find((d) => d.name === dsName)
|
||||
return ds?.tables || []
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-violet-50/30 p-6">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/ai-insight">
|
||||
<Button variant="ghost" size="icon" className="rounded-full">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">AI数据清洗</h1>
|
||||
<p className="text-slate-500 mt-1">基于大模型智能识别数据质量问题,自动生成清洗规则,支持审核机制</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
批量重算
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-gradient-to-r from-violet-500 to-purple-600 hover:from-violet-600 hover:to-purple-700"
|
||||
onClick={() => setCreateRuleDialogOpen(true)}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
创建AI规则
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计概览 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-slate-800">{(totalProcessed / 1000).toFixed(0)}K</p>
|
||||
</div>
|
||||
<Database className="w-8 h-8 text-blue-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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">
|
||||
{((totalApproved / totalProcessed) * 100).toFixed(1)}%
|
||||
</p>
|
||||
</div>
|
||||
<CheckCircle className="w-8 h-8 text-emerald-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-amber-600">{totalPending.toLocaleString()}</p>
|
||||
</div>
|
||||
<AlertTriangle className="w-8 h-8 text-amber-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-slate-800">
|
||||
{aiCleaningRules.filter((r) => r.status === "running").length} / {aiCleaningRules.length}
|
||||
</p>
|
||||
</div>
|
||||
<RefreshCw className="w-8 h-8 text-blue-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-violet-600">0.89</p>
|
||||
</div>
|
||||
<Brain className="w-8 h-8 text-violet-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="bg-white/60 backdrop-blur">
|
||||
<TabsTrigger value="rules" className="gap-2">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
AI清洗规则
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="review" className="gap-2">
|
||||
<Shield className="w-4 h-4" />
|
||||
审核中心
|
||||
{totalPending > 0 && <Badge className="ml-1 bg-amber-100 text-amber-700">{totalPending}</Badge>}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="history" className="gap-2">
|
||||
<History className="w-4 h-4" />
|
||||
操作历史
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* AI清洗规则列表 */}
|
||||
<TabsContent value="rules" className="mt-4">
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">AI清洗规则配置</CardTitle>
|
||||
<p className="text-sm text-slate-500">数据源来自数据治理模块,无需单独上传</p>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{aiCleaningRules.map((rule) => (
|
||||
<div key={rule.id} className="p-4 bg-slate-50/80 rounded-lg border border-slate-200/60">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-cyan-600 flex items-center justify-center">
|
||||
<Zap className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-medium text-slate-800">{rule.name}</h4>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{rule.model}
|
||||
</Badge>
|
||||
{rule.status === "running" ? (
|
||||
<Badge className="bg-emerald-100 text-emerald-700">运行中</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">已暂停</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 mt-0.5">{rule.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{rule.status === "running" ? (
|
||||
<Button variant="outline" size="sm">
|
||||
<Pause className="w-4 h-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="outline" size="sm">
|
||||
<Play className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedRule(rule)
|
||||
setSettingsDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
<FileText className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 数据源和目标表 */}
|
||||
<div className="grid grid-cols-2 gap-4 mb-3">
|
||||
<div className="p-3 bg-white rounded-lg">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-500 mb-1">
|
||||
<Database className="w-3 h-3" />
|
||||
数据源
|
||||
</div>
|
||||
<p className="text-sm font-medium text-slate-700">{rule.dataSource}</p>
|
||||
<p className="text-xs text-slate-500 mt-1">目标表: {rule.targetTable}</p>
|
||||
</div>
|
||||
<div className="p-3 bg-white rounded-lg">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-500 mb-1">
|
||||
<Zap className="w-3 h-3" />
|
||||
置信度阈值
|
||||
</div>
|
||||
<p className="text-sm font-medium text-slate-700">
|
||||
审核: {">="}
|
||||
{rule.confidenceThreshold} | 自动: {">="}
|
||||
{rule.autoApproveThreshold}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 提示词 */}
|
||||
<div className="p-3 bg-white rounded-lg mb-3">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-500 mb-1">
|
||||
<MessageSquare className="w-3 h-3" />
|
||||
AI提示词
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 line-clamp-2">{rule.prompt}</p>
|
||||
</div>
|
||||
|
||||
{/* 统计数据 */}
|
||||
<div className="grid grid-cols-5 gap-4 text-sm">
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold text-slate-800">{rule.accuracy}%</p>
|
||||
<p className="text-xs text-slate-500">准确率</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold text-slate-800">{(rule.processed / 1000).toFixed(0)}K</p>
|
||||
<p className="text-xs text-slate-500">已处理</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold text-amber-600">{rule.pending.toLocaleString()}</p>
|
||||
<p className="text-xs text-slate-500">待审核</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold text-emerald-600">{(rule.approved / 1000).toFixed(0)}K</p>
|
||||
<p className="text-xs text-slate-500">已通过</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold text-red-600">{rule.rejected.toLocaleString()}</p>
|
||||
<p className="text-xs text-slate-500">已拒绝</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 pt-3 border-t border-slate-200/60 flex items-center justify-between text-xs text-slate-500">
|
||||
<span>上次运行: {rule.lastRun}</span>
|
||||
<Button variant="link" size="sm" className="h-auto p-0 text-violet-600">
|
||||
立即执行
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 审核中心 */}
|
||||
<TabsContent value="review" className="mt-4">
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-base">清洗建议审核</CardTitle>
|
||||
<p className="text-sm text-slate-500 mt-1">AI生成的数据修正建议,需人工确认后执行</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-slate-500">置信度:</span>
|
||||
<div className="w-32">
|
||||
<Slider
|
||||
value={confidenceFilter}
|
||||
onValueChange={setConfidenceFilter}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.1}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
{">="}
|
||||
{confidenceFilter[0]}
|
||||
</span>
|
||||
</div>
|
||||
{selectedSuggestions.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-slate-500">已选 {selectedSuggestions.length} 项</span>
|
||||
<Button size="sm" variant="outline" className="text-emerald-600 bg-transparent">
|
||||
<Check className="w-4 h-4 mr-1" />
|
||||
批量通过
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="text-red-600 bg-transparent">
|
||||
<X className="w-4 h-4 mr-1" />
|
||||
批量拒绝
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{filteredSuggestions.map((suggestion) => (
|
||||
<div key={suggestion.id} className="p-4 bg-slate-50/80 rounded-lg border border-slate-200/60">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-1 rounded"
|
||||
checked={selectedSuggestions.includes(suggestion.id)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedSuggestions([...selectedSuggestions, suggestion.id])
|
||||
} else {
|
||||
setSelectedSuggestions(selectedSuggestions.filter((id) => id !== suggestion.id))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Badge variant="outline">{suggestion.ruleName}</Badge>
|
||||
<Badge className="bg-blue-100 text-blue-700">字段: {suggestion.field}</Badge>
|
||||
<Badge
|
||||
className={`${
|
||||
suggestion.confidence >= 0.9
|
||||
? "bg-emerald-100 text-emerald-700"
|
||||
: suggestion.confidence >= 0.7
|
||||
? "bg-amber-100 text-amber-700"
|
||||
: "bg-red-100 text-red-700"
|
||||
}`}
|
||||
>
|
||||
置信度: {(suggestion.confidence * 100).toFixed(0)}%
|
||||
</Badge>
|
||||
<span className="text-xs text-slate-500">影响 {suggestion.affectedRows} 行</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 p-3 bg-white rounded-lg mb-2">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">原始值</p>
|
||||
<code className="text-sm text-red-600 bg-red-50 px-2 py-1 rounded">
|
||||
{suggestion.originalValue}
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">建议值</p>
|
||||
<code className="text-sm text-emerald-600 bg-emerald-50 px-2 py-1 rounded">
|
||||
{suggestion.suggestedValue}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-white rounded-lg">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-500 mb-1">
|
||||
<Brain className="w-3 h-3" />
|
||||
AI分析理由
|
||||
</div>
|
||||
<p className="text-sm text-slate-600">{suggestion.reason}</p>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-2">创建时间: {suggestion.createdAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" variant="outline" className="text-emerald-600 bg-transparent">
|
||||
<Check className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="text-red-600 bg-transparent">
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setSelectedSuggestion(suggestion)
|
||||
setPreviewDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 操作历史 */}
|
||||
<TabsContent value="history" className="mt-4">
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base">清洗操作历史</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{cleaningHistory.map((item) => (
|
||||
<div key={item.id} className="flex items-center justify-between p-3 bg-slate-50/80 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="outline">{item.rule}</Badge>
|
||||
<Badge
|
||||
className={`${
|
||||
item.action === "approved" ? "bg-emerald-100 text-emerald-700" : "bg-red-100 text-red-700"
|
||||
}`}
|
||||
>
|
||||
{item.action === "approved" ? "通过" : "拒绝"}
|
||||
</Badge>
|
||||
<span className="text-sm text-slate-600">{item.count} 条记录</span>
|
||||
{item.reason && <span className="text-sm text-slate-500">原因: {item.reason}</span>}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-slate-500">
|
||||
<span>{item.operator}</span>
|
||||
<span>{item.time}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* 创建AI规则弹窗 */}
|
||||
<Dialog open={createRuleDialogOpen} onOpenChange={setCreateRuleDialogOpen}>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>创建AI清洗规则</DialogTitle>
|
||||
<DialogDescription>配置AI模型、选择数据源和目标表,定义清洗提示词</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>规则名称</Label>
|
||||
<Input
|
||||
placeholder="例如:用户数据智能清洗"
|
||||
value={newRule.name}
|
||||
onChange={(e) => setNewRule({ ...newRule, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>AI模型</Label>
|
||||
<Select value={newRule.model} onValueChange={(v) => setNewRule({ ...newRule, model: v })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="gpt-4">GPT-4</SelectItem>
|
||||
<SelectItem value="gpt-4-turbo">GPT-4 Turbo</SelectItem>
|
||||
<SelectItem value="claude-3">Claude 3</SelectItem>
|
||||
<SelectItem value="qwen-max">通义千问 Max</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>选择数据源</Label>
|
||||
<Select
|
||||
value={newRule.dataSource}
|
||||
onValueChange={(v) => setNewRule({ ...newRule, dataSource: v, targetTable: "" })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="从数据治理模块选择" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableDataSources.map((ds) => (
|
||||
<SelectItem key={ds.id} value={ds.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="w-4 h-4" />
|
||||
<span>{ds.name}</span>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{ds.type}
|
||||
</Badge>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>目标表</Label>
|
||||
<Select
|
||||
value={newRule.targetTable}
|
||||
onValueChange={(v) => setNewRule({ ...newRule, targetTable: v })}
|
||||
disabled={!newRule.dataSource}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择要清洗的表" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{getTablesForDataSource(newRule.dataSource).map((table) => (
|
||||
<SelectItem key={table} value={table}>
|
||||
{table}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>AI提示词</Label>
|
||||
<Textarea
|
||||
placeholder="描述你希望AI如何清洗数据。例如:分析用户数据表,识别以下问题并提供修正建议..."
|
||||
value={newRule.prompt}
|
||||
onChange={(e) => setNewRule({ ...newRule, prompt: e.target.value })}
|
||||
rows={4}
|
||||
/>
|
||||
<p className="text-xs text-slate-500">提示:明确说明要检测的问题类型和期望的修正方式</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>置信度阈值 (需审核)</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Slider
|
||||
value={[newRule.confidenceThreshold]}
|
||||
onValueChange={(v) => setNewRule({ ...newRule, confidenceThreshold: v[0] })}
|
||||
min={0.1}
|
||||
max={1}
|
||||
step={0.1}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="w-12 text-sm font-medium">{newRule.confidenceThreshold}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>自动通过阈值</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Slider
|
||||
value={[newRule.autoApproveThreshold]}
|
||||
onValueChange={(v) => setNewRule({ ...newRule, autoApproveThreshold: v[0] })}
|
||||
min={0.1}
|
||||
max={1}
|
||||
step={0.1}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="w-12 text-sm font-medium">{newRule.autoApproveThreshold}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setCreateRuleDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={() => setCreateRuleDialogOpen(false)}>
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
创建并测试
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 规则设置弹窗 */}
|
||||
<Dialog open={settingsDialogOpen} onOpenChange={setSettingsDialogOpen}>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>规则设置 - {selectedRule?.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{selectedRule && (
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>规则名称</Label>
|
||||
<Input defaultValue={selectedRule.name} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>AI模型</Label>
|
||||
<Select defaultValue={selectedRule.model.toLowerCase().replace(" ", "-")}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="gpt-4">GPT-4</SelectItem>
|
||||
<SelectItem value="claude-3">Claude 3</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>数据源</Label>
|
||||
<Input defaultValue={selectedRule.dataSource} disabled />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>AI提示词</Label>
|
||||
<Textarea defaultValue={selectedRule.prompt} rows={4} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>置信度阈值</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Slider
|
||||
defaultValue={[selectedRule.confidenceThreshold]}
|
||||
min={0.1}
|
||||
max={1}
|
||||
step={0.1}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="w-12 text-sm">{selectedRule.confidenceThreshold}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>自动通过阈值</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Slider
|
||||
defaultValue={[selectedRule.autoApproveThreshold]}
|
||||
min={0.1}
|
||||
max={1}
|
||||
step={0.1}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="w-12 text-sm">{selectedRule.autoApproveThreshold}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setSettingsDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={() => setSettingsDialogOpen(false)}>保存</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 预览弹窗 */}
|
||||
<Dialog open={previewDialogOpen} onOpenChange={setPreviewDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>清洗建议详情</DialogTitle>
|
||||
</DialogHeader>
|
||||
{selectedSuggestion && (
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-3 bg-slate-50 rounded-lg">
|
||||
<p className="text-xs text-slate-500">规则</p>
|
||||
<p className="font-medium">{selectedSuggestion.ruleName}</p>
|
||||
</div>
|
||||
<div className="p-3 bg-slate-50 rounded-lg">
|
||||
<p className="text-xs text-slate-500">字段</p>
|
||||
<p className="font-medium">{selectedSuggestion.field}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-slate-50 rounded-lg">
|
||||
<p className="text-xs text-slate-500 mb-2">数据变更</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<code className="flex-1 p-2 bg-red-50 text-red-600 rounded text-sm">
|
||||
{selectedSuggestion.originalValue}
|
||||
</code>
|
||||
<span className="text-slate-400">→</span>
|
||||
<code className="flex-1 p-2 bg-emerald-50 text-emerald-600 rounded text-sm">
|
||||
{selectedSuggestion.suggestedValue}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-slate-50 rounded-lg">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-500 mb-1">
|
||||
<Brain className="w-3 h-3" />
|
||||
AI分析理由
|
||||
</div>
|
||||
<p className="text-sm">{selectedSuggestion.reason}</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm text-slate-500">
|
||||
<span>影响行数: {selectedSuggestion.affectedRows}</span>
|
||||
<span>置信度: {(selectedSuggestion.confidence * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-red-600 bg-transparent"
|
||||
onClick={() => setPreviewDialogOpen(false)}
|
||||
>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
拒绝
|
||||
</Button>
|
||||
<Button className="bg-emerald-600 hover:bg-emerald-700" onClick={() => setPreviewDialogOpen(false)}>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
通过并执行
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
21
app/ai-insight/loading.tsx
Normal file
21
app/ai-insight/loading.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50/30 p-6">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
<Skeleton className="h-10 w-64" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<Skeleton key={i} className="h-28 rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<Skeleton key={i} className="h-40 rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
251
app/ai-insight/nlq/page.tsx
Normal file
251
app/ai-insight/nlq/page.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { MessageSquare, ArrowLeft, Sparkles, Code, Copy, Play, Clock, CheckCircle2, Table } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
const exampleQueries = [
|
||||
"查询最近7天新增的高价值用户",
|
||||
"统计各项目的用户流失率",
|
||||
"找出消费金额前100的用户",
|
||||
"分析用户活跃时段分布",
|
||||
]
|
||||
|
||||
const queryHistory = [
|
||||
{
|
||||
id: 1,
|
||||
query: "查询最近30天流失风险大于80的用户数量",
|
||||
sql: "SELECT COUNT(*) FROM users WHERE churn_risk > 80 AND last_active >= DATE_SUB(NOW(), INTERVAL 30 DAY)",
|
||||
result: "共 2,340 位用户",
|
||||
time: "10分钟前",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
query: "统计各省份的用户分布",
|
||||
sql: "SELECT province, COUNT(*) as count FROM users GROUP BY province ORDER BY count DESC",
|
||||
result: "返回 31 条记录",
|
||||
time: "1小时前",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
query: "找出RFM评分S级的用户",
|
||||
sql: "SELECT * FROM users WHERE rfm_level = 'S' ORDER BY total_value DESC LIMIT 100",
|
||||
result: "返回 100 条记录",
|
||||
time: "2小时前",
|
||||
},
|
||||
]
|
||||
|
||||
export default function NLQPage() {
|
||||
const [query, setQuery] = useState("")
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
const [result, setResult] = useState<{
|
||||
sql: string
|
||||
data: { columns: string[]; rows: string[][] }
|
||||
summary: string
|
||||
} | null>(null)
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!query.trim()) return
|
||||
setIsProcessing(true)
|
||||
// 模拟AI处理
|
||||
setTimeout(() => {
|
||||
setResult({
|
||||
sql: `SELECT user_id, name, phone, rfm_score, total_value
|
||||
FROM users
|
||||
WHERE rfm_level = 'S'
|
||||
AND last_active >= DATE_SUB(NOW(), INTERVAL 7 DAY)
|
||||
ORDER BY total_value DESC
|
||||
LIMIT 10`,
|
||||
data: {
|
||||
columns: ["用户ID", "姓名", "手机号", "RFM评分", "总价值"],
|
||||
rows: [
|
||||
["U001", "张伟", "138****8888", "95", "¥125,800"],
|
||||
["U002", "李娜", "139****6666", "92", "¥98,500"],
|
||||
["U003", "王芳", "137****5555", "90", "¥87,200"],
|
||||
["U004", "刘洋", "136****4444", "88", "¥76,800"],
|
||||
["U005", "陈静", "135****3333", "86", "¥65,400"],
|
||||
],
|
||||
},
|
||||
summary: "找到 5 位符合条件的S级高价值用户,总价值 ¥453,700",
|
||||
})
|
||||
setIsProcessing(false)
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-emerald-50/30 p-6">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/ai-insight">
|
||||
<Button variant="ghost" size="icon" className="rounded-full">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">自然语言查询</h1>
|
||||
<p className="text-slate-500 mt-1">用自然语言描述需求,AI自动生成SQL并执行</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 查询输入区 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 relative">
|
||||
<MessageSquare className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||||
<Input
|
||||
placeholder="用自然语言描述你想查询的数据,例如:查询最近7天新增的高价值用户..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
|
||||
className="pl-12 pr-4 py-6 text-base"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={isProcessing || !query.trim()}
|
||||
className="bg-gradient-to-r from-emerald-500 to-teal-600 hover:from-emerald-600 hover:to-teal-700 px-6 py-6"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="w-5 h-5 mr-2" />
|
||||
AI查询
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 示例查询 */}
|
||||
<div className="flex items-center gap-2 mt-4 flex-wrap">
|
||||
<span className="text-sm text-slate-500">示例:</span>
|
||||
{exampleQueries.map((eq, i) => (
|
||||
<Button
|
||||
key={i}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs bg-transparent"
|
||||
onClick={() => setQuery(eq)}
|
||||
>
|
||||
{eq}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 查询结果 */}
|
||||
{result && (
|
||||
<div className="space-y-4">
|
||||
{/* 生成的SQL */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Code className="w-4 h-4 text-slate-500" />
|
||||
生成的SQL
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Copy className="w-4 h-4 mr-1" />
|
||||
复制
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Play className="w-4 h-4 mr-1" />
|
||||
重新执行
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="bg-slate-900 text-slate-100 p-4 rounded-lg text-sm overflow-x-auto">
|
||||
<code>{result.sql}</code>
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 查询结果表格 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Table className="w-4 h-4 text-slate-500" />
|
||||
查询结果
|
||||
</CardTitle>
|
||||
<Badge className="bg-emerald-100 text-emerald-700">
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
{result.summary}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200">
|
||||
{result.data.columns.map((col, i) => (
|
||||
<th key={i} className="text-left py-3 px-4 text-xs font-medium text-slate-500">
|
||||
{col}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{result.data.rows.map((row, i) => (
|
||||
<tr key={i} className="border-b border-slate-100 hover:bg-slate-50/50">
|
||||
{row.map((cell, j) => (
|
||||
<td key={j} className="py-3 px-4 text-sm text-slate-700">
|
||||
{cell}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 查询历史 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Clock className="w-4 h-4 text-slate-500" />
|
||||
查询历史
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{queryHistory.map((item) => (
|
||||
<div key={item.id} className="p-4 bg-slate-50/80 rounded-lg border border-slate-200/60">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-slate-800">{item.query}</p>
|
||||
<pre className="mt-2 text-xs text-slate-500 bg-slate-100 p-2 rounded overflow-x-auto">
|
||||
{item.sql}
|
||||
</pre>
|
||||
</div>
|
||||
<div className="text-right ml-4">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{item.result}
|
||||
</Badge>
|
||||
<p className="text-xs text-slate-500 mt-1">{item.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
292
app/ai-insight/page.tsx
Normal file
292
app/ai-insight/page.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import {
|
||||
Brain,
|
||||
Sparkles,
|
||||
TrendingUp,
|
||||
MessageSquare,
|
||||
FileText,
|
||||
Tag,
|
||||
Target,
|
||||
Zap,
|
||||
ArrowRight,
|
||||
Activity,
|
||||
Clock,
|
||||
CheckCircle2,
|
||||
AlertTriangle,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
// 模拟数据
|
||||
const aiModules = [
|
||||
{
|
||||
id: "smart-tag",
|
||||
name: "智能打标",
|
||||
description: "基于NLP和行为分析,自动为用户生成标签",
|
||||
icon: Tag,
|
||||
color: "from-violet-500 to-purple-600",
|
||||
status: "running",
|
||||
stats: { today: 12580, accuracy: 94.2 },
|
||||
href: "/ai-insight/smart-tag",
|
||||
},
|
||||
{
|
||||
id: "prediction",
|
||||
name: "智能预测",
|
||||
description: "预测用户流失、转化、价值变化趋势",
|
||||
icon: TrendingUp,
|
||||
color: "from-blue-500 to-cyan-600",
|
||||
status: "running",
|
||||
stats: { today: 8420, accuracy: 91.8 },
|
||||
href: "/ai-insight/prediction",
|
||||
},
|
||||
{
|
||||
id: "nlq",
|
||||
name: "自然语言查询",
|
||||
description: "用自然语言查询数据,AI自动生成SQL",
|
||||
icon: MessageSquare,
|
||||
color: "from-emerald-500 to-teal-600",
|
||||
status: "running",
|
||||
stats: { today: 256, accuracy: 88.5 },
|
||||
href: "/ai-insight/nlq",
|
||||
},
|
||||
{
|
||||
id: "report",
|
||||
name: "智能报告生成",
|
||||
description: "自动生成数据分析报告和洞察摘要",
|
||||
icon: FileText,
|
||||
color: "from-orange-500 to-amber-600",
|
||||
status: "running",
|
||||
stats: { today: 48, accuracy: 92.1 },
|
||||
href: "/ai-insight/report",
|
||||
},
|
||||
]
|
||||
|
||||
const recentTasks = [
|
||||
{ id: 1, type: "智能打标", target: "新增用户批次", status: "completed", count: 2580, time: "2分钟前" },
|
||||
{ id: 2, type: "流失预测", target: "全量用户扫描", status: "running", progress: 67, time: "进行中" },
|
||||
{ id: 3, type: "NLQ查询", target: "高价值用户分布", status: "completed", count: 1, time: "5分钟前" },
|
||||
{ id: 4, type: "报告生成", target: "周度运营报告", status: "completed", count: 1, time: "1小时前" },
|
||||
{ id: 5, type: "智能打标", target: "行为标签更新", status: "warning", count: 156, time: "2小时前" },
|
||||
]
|
||||
|
||||
const aiInsights = [
|
||||
{ id: 1, type: "预警", content: "检测到 2,340 位用户有流失风险,建议启动挽留计划", priority: "high" },
|
||||
{ id: 2, type: "机会", content: "发现 890 位高潜力用户,转化概率 > 75%", priority: "medium" },
|
||||
{ id: 3, type: "趋势", content: "近7天用户活跃度环比上升 12.5%", priority: "low" },
|
||||
]
|
||||
|
||||
export default function AIInsightPage() {
|
||||
const [gpuUsage, setGpuUsage] = useState(68)
|
||||
const [apiCalls, setApiCalls] = useState(125800)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setGpuUsage((prev) => Math.min(95, Math.max(50, prev + (Math.random() - 0.5) * 10)))
|
||||
setApiCalls((prev) => prev + Math.floor(Math.random() * 100))
|
||||
}, 3000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50/30 p-6">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">AI赋能与洞察中心</h1>
|
||||
<p className="text-slate-500 mt-1">智能标签、预测分析、自然语言查询、报告生成</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-emerald-50 rounded-full">
|
||||
<span className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse" />
|
||||
<span className="text-sm text-emerald-700">AI引擎运行中</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI 资源状态 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500">GPU使用率</p>
|
||||
<p className="text-2xl font-bold text-slate-800">{gpuUsage.toFixed(1)}%</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center">
|
||||
<Brain className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={gpuUsage} className="mt-3 h-1.5" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500">今日API调用</p>
|
||||
<p className="text-2xl font-bold text-slate-800">{apiCalls.toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-cyan-600 flex items-center justify-center">
|
||||
<Zap className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-emerald-600 mt-3">较昨日 +18.2%</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-slate-800">91.6%</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-emerald-500 to-teal-600 flex items-center justify-center">
|
||||
<Target className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-emerald-600 mt-3">超过基准 +3.2%</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-slate-800">12</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-orange-500 to-amber-600 flex items-center justify-center">
|
||||
<Activity className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-3">全部正常运行</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* AI 功能模块入口 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{aiModules.map((module) => (
|
||||
<Link key={module.id} href={module.href}>
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60 hover:shadow-lg hover:border-slate-300 transition-all cursor-pointer group h-full">
|
||||
<CardContent className="p-5">
|
||||
<div
|
||||
className={`w-12 h-12 rounded-xl bg-gradient-to-br ${module.color} flex items-center justify-center mb-4`}
|
||||
>
|
||||
<module.icon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-slate-800 mb-1">{module.name}</h3>
|
||||
<p className="text-sm text-slate-500 mb-4">{module.description}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs text-slate-500">
|
||||
今日处理:{" "}
|
||||
<span className="font-medium text-slate-700">{module.stats.today.toLocaleString()}</span>
|
||||
</div>
|
||||
<ArrowRight className="w-4 h-4 text-slate-400 group-hover:text-slate-600 group-hover:translate-x-1 transition-all" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* AI 洞察 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-amber-500" />
|
||||
AI 智能洞察
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{aiInsights.map((insight) => (
|
||||
<div
|
||||
key={insight.id}
|
||||
className={`p-3 rounded-lg border ${
|
||||
insight.priority === "high"
|
||||
? "bg-red-50 border-red-200"
|
||||
: insight.priority === "medium"
|
||||
? "bg-amber-50 border-amber-200"
|
||||
: "bg-blue-50 border-blue-200"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
insight.priority === "high"
|
||||
? "bg-red-100 text-red-700 border-red-300"
|
||||
: insight.priority === "medium"
|
||||
? "bg-amber-100 text-amber-700 border-amber-300"
|
||||
: "bg-blue-100 text-blue-700 border-blue-300"
|
||||
}
|
||||
>
|
||||
{insight.type}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 mt-2">{insight.content}</p>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" className="w-full mt-2 bg-transparent" size="sm">
|
||||
查看全部洞察
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 最近任务 */}
|
||||
<Card className="lg:col-span-2 bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Clock className="w-4 h-4 text-slate-500" />
|
||||
最近AI任务
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{recentTasks.map((task) => (
|
||||
<div key={task.id} className="flex items-center justify-between p-3 bg-slate-50/80 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
{task.status === "completed" ? (
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
|
||||
) : task.status === "running" ? (
|
||||
<div className="w-5 h-5 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||
) : (
|
||||
<AlertTriangle className="w-5 h-5 text-amber-500" />
|
||||
)}
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-800">{task.type}</p>
|
||||
<p className="text-xs text-slate-500">{task.target}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
{task.status === "running" ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Progress value={task.progress} className="w-20 h-1.5" />
|
||||
<span className="text-xs text-slate-500">{task.progress}%</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm font-medium text-slate-700">{task.count?.toLocaleString()}</p>
|
||||
<p className="text-xs text-slate-500">{task.time}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
331
app/ai-insight/prediction/page.tsx
Normal file
331
app/ai-insight/prediction/page.tsx
Normal file
@@ -0,0 +1,331 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import {
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Users,
|
||||
AlertTriangle,
|
||||
Target,
|
||||
ArrowLeft,
|
||||
Play,
|
||||
Calendar,
|
||||
BarChart3,
|
||||
PieChart,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from "recharts"
|
||||
|
||||
// 模拟预测数据
|
||||
const churnPrediction = [
|
||||
{ date: "12-06", actual: 120, predicted: 125 },
|
||||
{ date: "12-07", actual: 135, predicted: 130 },
|
||||
{ date: "12-08", actual: 128, predicted: 132 },
|
||||
{ date: "12-09", actual: 142, predicted: 138 },
|
||||
{ date: "12-10", actual: 138, predicted: 145 },
|
||||
{ date: "12-11", actual: 155, predicted: 150 },
|
||||
{ date: "12-12", actual: null, predicted: 162 },
|
||||
{ date: "12-13", actual: null, predicted: 158 },
|
||||
{ date: "12-14", actual: null, predicted: 165 },
|
||||
]
|
||||
|
||||
const conversionPrediction = [
|
||||
{ date: "12-06", rate: 3.2 },
|
||||
{ date: "12-07", rate: 3.5 },
|
||||
{ date: "12-08", rate: 3.3 },
|
||||
{ date: "12-09", rate: 3.8 },
|
||||
{ date: "12-10", rate: 4.1 },
|
||||
{ date: "12-11", rate: 4.0 },
|
||||
{ date: "12-12", rate: 4.3 },
|
||||
{ date: "12-13", rate: 4.5 },
|
||||
{ date: "12-14", rate: 4.8 },
|
||||
]
|
||||
|
||||
const riskUsers = [
|
||||
{ id: 1, name: "用户A****8", risk: 92, lastActive: "7天前", value: "高", reason: "活跃度骤降" },
|
||||
{ id: 2, name: "用户B****2", risk: 88, lastActive: "5天前", value: "高", reason: "消费频次下降" },
|
||||
{ id: 3, name: "用户C****6", risk: 85, lastActive: "6天前", value: "中", reason: "互动减少" },
|
||||
{ id: 4, name: "用户D****1", risk: 82, lastActive: "4天前", value: "高", reason: "投诉增加" },
|
||||
{ id: 5, name: "用户E****9", risk: 78, lastActive: "8天前", value: "中", reason: "登录减少" },
|
||||
]
|
||||
|
||||
export default function PredictionPage() {
|
||||
const [timeRange, setTimeRange] = useState("7d")
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50/30 p-6">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/ai-insight">
|
||||
<Button variant="ghost" size="icon" className="rounded-full">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">智能预测</h1>
|
||||
<p className="text-slate-500 mt-1">流失预警、转化预测、价值变化趋势分析</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setTimeRange("7d")}
|
||||
className={timeRange === "7d" ? "bg-slate-100" : ""}
|
||||
>
|
||||
<Calendar className="w-4 h-4 mr-1" />
|
||||
7天
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setTimeRange("30d")}
|
||||
className={timeRange === "30d" ? "bg-slate-100" : ""}
|
||||
>
|
||||
30天
|
||||
</Button>
|
||||
<Button className="bg-gradient-to-r from-blue-500 to-cyan-600 hover:from-blue-600 hover:to-cyan-700">
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
执行预测
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 预测概览 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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">2,340</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-lg bg-red-100 flex items-center justify-center">
|
||||
<TrendingDown className="w-5 h-5 text-red-600" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-red-600 mt-2">未来7天预警</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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">890</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-lg bg-emerald-100 flex items-center justify-center">
|
||||
<TrendingUp className="w-5 h-5 text-emerald-600" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-emerald-600 mt-2">转化概率 {"> "}75%</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-slate-800">91.8%</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||
<Target className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-emerald-600 mt-2">较上月 +1.2%</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-amber-600">156</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-lg bg-amber-100 flex items-center justify-center">
|
||||
<AlertTriangle className="w-5 h-5 text-amber-600" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-amber-600 mt-2">风险值 {"> "}80</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* 流失预测趋势 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<BarChart3 className="w-4 h-4 text-slate-500" />
|
||||
流失预测趋势
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={churnPrediction}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
|
||||
<XAxis dataKey="date" tick={{ fontSize: 12 }} stroke="#94a3b8" />
|
||||
<YAxis tick={{ fontSize: 12 }} stroke="#94a3b8" />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: "white",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="actual"
|
||||
stroke="#3b82f6"
|
||||
strokeWidth={2}
|
||||
dot={{ r: 4 }}
|
||||
name="实际值"
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="predicted"
|
||||
stroke="#f97316"
|
||||
strokeWidth={2}
|
||||
strokeDasharray="5 5"
|
||||
dot={{ r: 4 }}
|
||||
name="预测值"
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-6 mt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-blue-500" />
|
||||
<span className="text-xs text-slate-600">实际流失</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-0.5 bg-orange-500" style={{ borderStyle: "dashed" }} />
|
||||
<span className="text-xs text-slate-600">预测流失</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 转化率预测 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<PieChart className="w-4 h-4 text-slate-500" />
|
||||
转化率预测
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={conversionPrediction}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
|
||||
<XAxis dataKey="date" tick={{ fontSize: 12 }} stroke="#94a3b8" />
|
||||
<YAxis tick={{ fontSize: 12 }} stroke="#94a3b8" tickFormatter={(v) => `${v}%`} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: "white",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
formatter={(value: number) => [`${value}%`, "转化率"]}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient id="colorRate" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area type="monotone" dataKey="rate" stroke="#10b981" strokeWidth={2} fill="url(#colorRate)" />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="text-center mt-4">
|
||||
<p className="text-sm text-slate-600">
|
||||
预测未来7天转化率将提升至 <span className="font-bold text-emerald-600">4.8%</span>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 高风险用户列表 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<AlertTriangle className="w-4 h-4 text-amber-500" />
|
||||
高风险用户预警
|
||||
</CardTitle>
|
||||
<Button variant="outline" size="sm">
|
||||
导出名单
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200">
|
||||
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500">用户</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500">风险值</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500">用户价值</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500">最后活跃</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500">风险原因</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{riskUsers.map((user) => (
|
||||
<tr key={user.id} className="border-b border-slate-100 hover:bg-slate-50/50">
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-slate-200 to-slate-300 flex items-center justify-center">
|
||||
<Users className="w-4 h-4 text-slate-600" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-800">{user.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Progress value={user.risk} className="w-16 h-2" />
|
||||
<span className="text-sm font-medium text-red-600">{user.risk}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<Badge
|
||||
className={
|
||||
user.value === "高" ? "bg-amber-100 text-amber-700" : "bg-slate-100 text-slate-700"
|
||||
}
|
||||
>
|
||||
{user.value}价值
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-slate-600">{user.lastActive}</td>
|
||||
<td className="py-3 px-4 text-sm text-slate-600">{user.reason}</td>
|
||||
<td className="py-3 px-4">
|
||||
<Button variant="outline" size="sm">
|
||||
挽留策略
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
app/ai-insight/report/loading.tsx
Normal file
3
app/ai-insight/report/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
228
app/ai-insight/report/page.tsx
Normal file
228
app/ai-insight/report/page.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
FileText,
|
||||
ArrowLeft,
|
||||
Plus,
|
||||
Download,
|
||||
Eye,
|
||||
Calendar,
|
||||
Clock,
|
||||
CheckCircle2,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Filter,
|
||||
BarChart3,
|
||||
PieChart,
|
||||
TrendingUp,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
const reportTemplates = [
|
||||
{
|
||||
id: 1,
|
||||
name: "日度运营报告",
|
||||
description: "每日用户活跃、增长、转化数据汇总",
|
||||
schedule: "每日 09:00",
|
||||
icon: BarChart3,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "周度分析报告",
|
||||
description: "每周关键指标趋势分析和洞察",
|
||||
schedule: "每周一 10:00",
|
||||
icon: TrendingUp,
|
||||
},
|
||||
{ id: 3, name: "月度价值报告", description: "月度用户价值评估和排名变化", schedule: "每月1日 09:00", icon: PieChart },
|
||||
{ id: 4, name: "自定义报告", description: "根据需求自定义报告内容和维度", schedule: "手动触发", icon: FileText },
|
||||
]
|
||||
|
||||
const generatedReports = [
|
||||
{
|
||||
id: 1,
|
||||
name: "2024年12月第2周运营分析报告",
|
||||
type: "周度分析报告",
|
||||
status: "completed",
|
||||
pages: 12,
|
||||
generatedAt: "2024-12-12 10:00",
|
||||
summary: "本周新增用户 12,580,环比增长 15.3%;活跃用户 89,200,DAU/MAU 提升至 32.5%",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "2024年12月11日运营日报",
|
||||
type: "日度运营报告",
|
||||
status: "completed",
|
||||
pages: 6,
|
||||
generatedAt: "2024-12-11 09:00",
|
||||
summary: "日活 28,500,新增 1,890,转化率 3.8%,各项指标均达预期",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "2024年12月12日运营日报",
|
||||
type: "日度运营报告",
|
||||
status: "generating",
|
||||
pages: 0,
|
||||
generatedAt: "生成中...",
|
||||
summary: "",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "2024年11月用户价值评估报告",
|
||||
type: "月度价值报告",
|
||||
status: "completed",
|
||||
pages: 28,
|
||||
generatedAt: "2024-12-01 09:00",
|
||||
summary: "S级用户占比提升至 8.2%,高价值用户总价值突破 125 亿元",
|
||||
},
|
||||
]
|
||||
|
||||
export default function ReportPage() {
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-orange-50/30 p-6">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/ai-insight">
|
||||
<Button variant="ghost" size="icon" className="rounded-full">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">智能报告生成</h1>
|
||||
<p className="text-slate-500 mt-1">自动生成数据分析报告和运营洞察</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="bg-gradient-to-r from-orange-500 to-amber-600 hover:from-orange-600 hover:to-amber-700">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
生成新报告
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 报告模板 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
{reportTemplates.map((template) => (
|
||||
<Card
|
||||
key={template.id}
|
||||
className="bg-white/70 backdrop-blur border-slate-200/60 hover:shadow-lg hover:border-slate-300 transition-all cursor-pointer"
|
||||
>
|
||||
<CardContent className="p-5">
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-orange-500 to-amber-600 flex items-center justify-center mb-3">
|
||||
<template.icon className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-slate-800 mb-1">{template.name}</h3>
|
||||
<p className="text-xs text-slate-500 mb-3">{template.description}</p>
|
||||
<div className="flex items-center gap-1 text-xs text-slate-400">
|
||||
<Calendar className="w-3 h-3" />
|
||||
{template.schedule}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 已生成报告列表 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">已生成报告</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<Input
|
||||
placeholder="搜索报告..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-9 w-64"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
筛选
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{generatedReports.map((report) => (
|
||||
<div
|
||||
key={report.id}
|
||||
className="flex items-center justify-between p-4 bg-slate-50/80 rounded-lg border border-slate-200/60"
|
||||
>
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-orange-100 to-amber-100 flex items-center justify-center">
|
||||
<FileText className="w-5 h-5 text-orange-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-medium text-slate-800">{report.name}</h4>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{report.type}
|
||||
</Badge>
|
||||
{report.status === "completed" ? (
|
||||
<Badge className="bg-emerald-100 text-emerald-700 border-emerald-300">
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
已完成
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge className="bg-blue-100 text-blue-700 border-blue-300">
|
||||
<RefreshCw className="w-3 h-3 mr-1 animate-spin" />
|
||||
生成中
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{report.summary && <p className="text-sm text-slate-500 mt-1 line-clamp-1">{report.summary}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="text-right">
|
||||
{report.status === "completed" && (
|
||||
<p className="text-sm font-medium text-slate-700">{report.pages} 页</p>
|
||||
)}
|
||||
<p className="text-xs text-slate-500 flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{report.generatedAt}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" disabled={report.status !== "completed"}>
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
查看
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled={report.status !== "completed"}>
|
||||
<Download className="w-4 h-4 mr-1" />
|
||||
下载
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 报告预览区域 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base">报告预览</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="bg-slate-100 rounded-lg p-8 text-center">
|
||||
<FileText className="w-16 h-16 text-slate-300 mx-auto mb-4" />
|
||||
<p className="text-slate-500">选择一份报告进行预览</p>
|
||||
<p className="text-sm text-slate-400 mt-1">支持在线预览和 PDF 下载</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
app/ai-insight/smart-tag/loading.tsx
Normal file
3
app/ai-insight/smart-tag/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
3
app/ai-insight/smart-tag/logs/[id]/loading.tsx
Normal file
3
app/ai-insight/smart-tag/logs/[id]/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
127
app/ai-insight/smart-tag/logs/[id]/page.tsx
Normal file
127
app/ai-insight/smart-tag/logs/[id]/page.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { ArrowLeft, Search, Download, RefreshCw, CheckCircle, XCircle, AlertTriangle, Clock } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
const logs = [
|
||||
{ id: 1, time: "2025-12-12 14:35:12", level: "info", message: "开始执行打标任务 BATCH-20241212-003", users: 3200 },
|
||||
{ id: 2, time: "2025-12-12 14:35:15", level: "info", message: "加载用户数据完成,共 3200 条", users: null },
|
||||
{ id: 3, time: "2025-12-12 14:35:20", level: "info", message: "调用AI模型 XGBoost 进行预测...", users: null },
|
||||
{ id: 4, time: "2025-12-12 14:36:45", level: "success", message: "生成候选标签 2856 个,置信度 >= 0.7", users: 2856 },
|
||||
{ id: 5, time: "2025-12-12 14:36:48", level: "warning", message: "低置信度标签 344 个被过滤 (< 0.4)", users: 344 },
|
||||
{ id: 6, time: "2025-12-12 14:36:50", level: "info", message: "写入候选标签库完成", users: null },
|
||||
{ id: 7, time: "2025-12-12 14:36:52", level: "success", message: "任务完成,耗时 1m 40s", users: null },
|
||||
{ id: 8, time: "2025-12-12 14:30:00", level: "info", message: "开始执行打标任务 BATCH-20241212-002", users: 1890 },
|
||||
{ id: 9, time: "2025-12-12 14:32:56", level: "success", message: "任务完成,生成标签 1654 个", users: 1654 },
|
||||
{ id: 10, time: "2025-12-12 14:00:00", level: "error", message: "AI模型调用超时,任务中断", users: null },
|
||||
{ id: 11, time: "2025-12-12 14:00:30", level: "info", message: "自动重试任务...", users: null },
|
||||
{ id: 12, time: "2025-12-12 14:03:24", level: "success", message: "重试成功,任务完成", users: null },
|
||||
]
|
||||
|
||||
const levelConfig = {
|
||||
info: { color: "bg-blue-100 text-blue-700", icon: Clock },
|
||||
success: { color: "bg-emerald-100 text-emerald-700", icon: CheckCircle },
|
||||
warning: { color: "bg-amber-100 text-amber-700", icon: AlertTriangle },
|
||||
error: { color: "bg-red-100 text-red-700", icon: XCircle },
|
||||
}
|
||||
|
||||
export default function TagLogsPage() {
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [levelFilter, setLevelFilter] = useState("all")
|
||||
|
||||
const filteredLogs = logs.filter((log) => {
|
||||
const matchesSearch = log.message.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
const matchesLevel = levelFilter === "all" || log.level === levelFilter
|
||||
return matchesSearch && matchesLevel
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-violet-50/30 p-6">
|
||||
<div className="max-w-5xl mx-auto space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/ai-insight/smart-tag">
|
||||
<Button variant="ghost" size="icon" className="rounded-full">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">执行日志</h1>
|
||||
<p className="text-slate-500 mt-1">高消费潜力标签 - 运行日志</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出日志
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">运行日志</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<Input
|
||||
placeholder="搜索日志..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-9 w-64"
|
||||
/>
|
||||
</div>
|
||||
<Select value={levelFilter} onValueChange={setLevelFilter}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue placeholder="日志级别" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部</SelectItem>
|
||||
<SelectItem value="info">信息</SelectItem>
|
||||
<SelectItem value="success">成功</SelectItem>
|
||||
<SelectItem value="warning">警告</SelectItem>
|
||||
<SelectItem value="error">错误</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{filteredLogs.map((log) => {
|
||||
const config = levelConfig[log.level as keyof typeof levelConfig]
|
||||
const Icon = config.icon
|
||||
return (
|
||||
<div key={log.id} className="flex items-start gap-3 p-3 bg-slate-50/80 rounded-lg font-mono text-sm">
|
||||
<Badge className={`${config.color} text-xs shrink-0`}>
|
||||
<Icon className="w-3 h-3 mr-1" />
|
||||
{log.level.toUpperCase()}
|
||||
</Badge>
|
||||
<span className="text-slate-500 shrink-0">{log.time}</span>
|
||||
<span className="text-slate-800 flex-1">{log.message}</span>
|
||||
{log.users && (
|
||||
<Badge variant="outline" className="shrink-0">
|
||||
{log.users.toLocaleString()}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
980
app/ai-insight/smart-tag/page.tsx
Normal file
980
app/ai-insight/smart-tag/page.tsx
Normal file
@@ -0,0 +1,980 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Slider } from "@/components/ui/slider"
|
||||
import {
|
||||
Tag,
|
||||
Play,
|
||||
Pause,
|
||||
Settings,
|
||||
RefreshCw,
|
||||
CheckCircle2,
|
||||
Users,
|
||||
ArrowLeft,
|
||||
Search,
|
||||
Eye,
|
||||
Check,
|
||||
X,
|
||||
AlertTriangle,
|
||||
FileText,
|
||||
Brain,
|
||||
History,
|
||||
Shield,
|
||||
Sparkles,
|
||||
Database,
|
||||
MessageSquare,
|
||||
Zap,
|
||||
Plus,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
const availableDataSources = [
|
||||
{ id: "1", name: "存客宝-用户主库", type: "mysql", records: 128956342 },
|
||||
{ id: "2", name: "触客宝-行为数据", type: "kafka", records: 89234567 },
|
||||
{ id: "3", name: "数智员工-账号API", type: "restapi", records: 45678901 },
|
||||
{ id: "7", name: "微信消息-Webhook", type: "webhook", records: 34567890 },
|
||||
]
|
||||
|
||||
const tagRules = [
|
||||
{
|
||||
id: 1,
|
||||
name: "高消费潜力识别",
|
||||
description: "基于消费行为和浏览历史识别高消费潜力用户",
|
||||
dataSource: "存客宝-用户主库",
|
||||
prompt:
|
||||
"分析用户的消费记录和浏览行为,识别具有高消费潜力的用户。考虑因素包括:消费频次、客单价、浏览高端商品频率、收藏夹内容等。",
|
||||
model: "GPT-4",
|
||||
status: "running",
|
||||
confidenceThreshold: 0.7,
|
||||
autoApproveThreshold: 0.9,
|
||||
tagged: 125800,
|
||||
pending: 2340,
|
||||
approved: 120000,
|
||||
rejected: 3460,
|
||||
lastRun: "10分钟前",
|
||||
accuracy: 94.2,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "流失风险预警",
|
||||
description: "基于活跃度衰减和行为变化预测流失风险",
|
||||
dataSource: "触客宝-行为数据",
|
||||
prompt:
|
||||
"分析用户近期的活跃度变化趋势,识别可能流失的用户。重点关注:登录频次下降、互动减少、浏览时长缩短、取消关注等负面信号。",
|
||||
model: "GPT-4",
|
||||
status: "running",
|
||||
confidenceThreshold: 0.6,
|
||||
autoApproveThreshold: 0.85,
|
||||
tagged: 23400,
|
||||
pending: 890,
|
||||
approved: 21000,
|
||||
rejected: 1510,
|
||||
lastRun: "30分钟前",
|
||||
accuracy: 91.5,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "兴趣偏好分析",
|
||||
description: "基于聊天内容和互动行为分析用户兴趣",
|
||||
dataSource: "微信消息-Webhook",
|
||||
prompt:
|
||||
"分析用户的聊天记录和互动内容,提取用户的兴趣偏好标签。包括但不限于:产品类目偏好、品牌偏好、价格敏感度、购买意向等。",
|
||||
model: "Claude-3",
|
||||
status: "paused",
|
||||
confidenceThreshold: 0.5,
|
||||
autoApproveThreshold: 0.8,
|
||||
tagged: 456000,
|
||||
pending: 12500,
|
||||
approved: 420000,
|
||||
rejected: 23500,
|
||||
lastRun: "2小时前",
|
||||
accuracy: 88.7,
|
||||
},
|
||||
]
|
||||
|
||||
const candidateTags = [
|
||||
{
|
||||
id: 1,
|
||||
userId: "U10086",
|
||||
userName: "张三",
|
||||
tag: "高消费潜力",
|
||||
confidence: 0.92,
|
||||
source: "消费行为分析",
|
||||
createdAt: "2025-12-12 14:30",
|
||||
reason: "近30天消费3次,客单价680元",
|
||||
aiAnalysis:
|
||||
"该用户近30天内有3次消费记录,平均客单价680元,高于平台均值45%。同时多次浏览高端商品页面,收藏夹中有5件单价超过1000元的商品。综合判断具有高消费潜力。",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
userId: "U10087",
|
||||
userName: "李四",
|
||||
tag: "流失风险-高",
|
||||
confidence: 0.85,
|
||||
source: "活跃度衰减",
|
||||
createdAt: "2025-12-12 14:28",
|
||||
reason: "连续7天未登录,上月活跃度下降60%",
|
||||
aiAnalysis:
|
||||
"该用户从高活跃状态(日均登录2次)下降到连续7天未登录。上月互动量较前月下降60%,且最后一次登录时取消了3个商品收藏。行为模式符合典型流失前兆。",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
userId: "U10088",
|
||||
userName: "王五",
|
||||
tag: "兴趣偏好-美妆",
|
||||
confidence: 0.78,
|
||||
source: "NLP语义分析",
|
||||
createdAt: "2025-12-12 14:25",
|
||||
reason: "聊天记录多次提及护肤、化妆品关键词",
|
||||
aiAnalysis:
|
||||
"在最近20条聊天记录中,用户10次提及护肤相关词汇(补水、抗皱、精华),5次询问化妆品推荐。结合其浏览记录中70%为美妆类目,判断其主要兴趣为美妆护肤。",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
userId: "U10089",
|
||||
userName: "赵六",
|
||||
tag: "社交影响力-KOC",
|
||||
confidence: 0.71,
|
||||
source: "社交网络分析",
|
||||
createdAt: "2025-12-12 14:22",
|
||||
reason: "好友数500+,朋友圈互动率12%",
|
||||
aiAnalysis:
|
||||
"该用户好友数量达到523人,朋友圈发布频率为每周3次,平均互动率12%(点赞+评论/好友数),高于平台平均3.5%。具备一定的社交影响力,可作为KOC培养对象。",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
userId: "U10090",
|
||||
userName: "钱七",
|
||||
tag: "价格敏感型",
|
||||
confidence: 0.68,
|
||||
source: "购买行为分析",
|
||||
createdAt: "2025-12-12 14:20",
|
||||
reason: "多次使用优惠券,偏好促销商品",
|
||||
aiAnalysis:
|
||||
'该用户过去6个月的12次购买中,11次使用了优惠券,平均折扣率达到28%。浏览记录显示经常筛选"促销"标签,且多次在大促期间集中购买。符合价格敏感型用户特征。',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
userId: "U10091",
|
||||
userName: "孙八",
|
||||
tag: "高消费潜力",
|
||||
confidence: 0.45,
|
||||
source: "消费行为分析",
|
||||
createdAt: "2025-12-12 14:18",
|
||||
reason: "单次消费较高但频次低",
|
||||
aiAnalysis:
|
||||
"该用户仅有2次消费记录,但单次消费金额较高(1200元、980元)。由于样本量不足,对其消费潜力的判断置信度较低,建议人工审核确认。",
|
||||
},
|
||||
]
|
||||
|
||||
const tagHistory = [
|
||||
{ id: 1, tag: "高消费潜力", action: "approved", operator: "张管理员", count: 150, time: "2025-12-12 14:00" },
|
||||
{
|
||||
id: 2,
|
||||
tag: "流失风险",
|
||||
action: "rejected",
|
||||
operator: "李管理员",
|
||||
count: 23,
|
||||
time: "2025-12-12 13:45",
|
||||
reason: "置信度过低",
|
||||
},
|
||||
{ id: 3, tag: "兴趣偏好", action: "approved", operator: "系统自动", count: 500, time: "2025-12-12 13:30" },
|
||||
{
|
||||
id: 4,
|
||||
tag: "社交影响力",
|
||||
action: "modified",
|
||||
operator: "王管理员",
|
||||
count: 12,
|
||||
time: "2025-12-12 13:15",
|
||||
reason: "调整为KOL级别",
|
||||
},
|
||||
]
|
||||
|
||||
export default function SmartTagPage() {
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [activeTab, setActiveTab] = useState("rules")
|
||||
const [selectedTags, setSelectedTags] = useState<number[]>([])
|
||||
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false)
|
||||
const [reviewDialogOpen, setReviewDialogOpen] = useState(false)
|
||||
const [selectedRule, setSelectedRule] = useState<(typeof tagRules)[0] | null>(null)
|
||||
const [selectedCandidate, setSelectedCandidate] = useState<(typeof candidateTags)[0] | null>(null)
|
||||
const [confidenceFilter, setConfidenceFilter] = useState([0.4])
|
||||
const [batchRunDialogOpen, setBatchRunDialogOpen] = useState(false)
|
||||
const [createRuleDialogOpen, setCreateRuleDialogOpen] = useState(false)
|
||||
const [newRule, setNewRule] = useState({
|
||||
name: "",
|
||||
dataSource: "",
|
||||
prompt: "",
|
||||
model: "gpt-4",
|
||||
confidenceThreshold: 0.7,
|
||||
autoApproveThreshold: 0.9,
|
||||
})
|
||||
|
||||
const totalPending = tagRules.reduce((sum, r) => sum + r.pending, 0)
|
||||
const totalApproved = tagRules.reduce((sum, r) => sum + r.approved, 0)
|
||||
const totalRejected = tagRules.reduce((sum, r) => sum + r.rejected, 0)
|
||||
|
||||
const handleApprove = (id: number) => {
|
||||
setSelectedTags((prev) => prev.filter((t) => t !== id))
|
||||
}
|
||||
|
||||
const handleReject = (id: number) => {
|
||||
setSelectedTags((prev) => prev.filter((t) => t !== id))
|
||||
}
|
||||
|
||||
const handleBatchApprove = () => {
|
||||
setSelectedTags([])
|
||||
}
|
||||
|
||||
const filteredCandidates = candidateTags.filter((c) => c.confidence >= confidenceFilter[0])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-violet-50/30 p-6">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/ai-insight">
|
||||
<Button variant="ghost" size="icon" className="rounded-full">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">AI智能打标</h1>
|
||||
<p className="text-slate-500 mt-1">基于大模型分析用户数据,自动生成标签,支持置信度审核</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => setBatchRunDialogOpen(true)}>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
批量重算
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-gradient-to-r from-violet-500 to-purple-600 hover:from-violet-600 hover:to-purple-700"
|
||||
onClick={() => setCreateRuleDialogOpen(true)}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
创建AI规则
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-slate-800">12,580</p>
|
||||
</div>
|
||||
<Users className="w-8 h-8 text-violet-500" />
|
||||
</div>
|
||||
<p className="text-xs text-emerald-600 mt-2">较昨日 +15.3%</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-slate-800">45,200</p>
|
||||
</div>
|
||||
<Tag className="w-8 h-8 text-blue-500" />
|
||||
</div>
|
||||
<p className="text-xs text-emerald-600 mt-2">较昨日 +22.1%</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-amber-600">{totalPending.toLocaleString()}</p>
|
||||
</div>
|
||||
<AlertTriangle className="w-8 h-8 text-amber-500" />
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-2">需人工确认</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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">{totalApproved.toLocaleString()}</p>
|
||||
</div>
|
||||
<CheckCircle2 className="w-8 h-8 text-emerald-500" />
|
||||
</div>
|
||||
<p className="text-xs text-emerald-600 mt-2">
|
||||
审核通过率 {((totalApproved / (totalApproved + totalRejected)) * 100).toFixed(1)}%
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-slate-800">0.82</p>
|
||||
</div>
|
||||
<Brain className="w-8 h-8 text-purple-500" />
|
||||
</div>
|
||||
<p className="text-xs text-emerald-600 mt-2">超过基准 +5.2%</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-slate-800">2 / 3</p>
|
||||
</div>
|
||||
<RefreshCw className="w-8 h-8 text-emerald-500" />
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-2">1个已暂停</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="bg-white/60 backdrop-blur">
|
||||
<TabsTrigger value="rules" className="gap-2">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
AI打标规则
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="review" className="gap-2">
|
||||
<Shield className="w-4 h-4" />
|
||||
审核中心
|
||||
{totalPending > 0 && <Badge className="ml-1 bg-amber-100 text-amber-700">{totalPending}</Badge>}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="history" className="gap-2">
|
||||
<History className="w-4 h-4" />
|
||||
操作历史
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* AI打标规则Tab - 重构展示提示词和数据源 */}
|
||||
<TabsContent value="rules" className="mt-4">
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">AI打标规则配置</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<Input
|
||||
placeholder="搜索规则..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-9 w-64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{tagRules.map((rule) => (
|
||||
<div key={rule.id} className="p-4 bg-slate-50/80 rounded-lg border border-slate-200/60">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center">
|
||||
<Brain className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-medium text-slate-800">{rule.name}</h4>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{rule.model}
|
||||
</Badge>
|
||||
{rule.status === "running" ? (
|
||||
<Badge className="bg-emerald-100 text-emerald-700 border-emerald-300">运行中</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">已暂停</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 mt-0.5">{rule.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{rule.status === "running" ? (
|
||||
<Button variant="outline" size="sm">
|
||||
<Pause className="w-4 h-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="outline" size="sm">
|
||||
<Play className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedRule(rule)
|
||||
setSettingsDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href={`/ai-insight/smart-tag/logs/${rule.id}`}>
|
||||
<FileText className="w-4 h-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-3">
|
||||
<div className="p-3 bg-white rounded-lg">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-500 mb-1">
|
||||
<Database className="w-3 h-3" />
|
||||
数据源
|
||||
</div>
|
||||
<p className="text-sm font-medium text-slate-700">{rule.dataSource}</p>
|
||||
</div>
|
||||
<div className="p-3 bg-white rounded-lg">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-500 mb-1">
|
||||
<Zap className="w-3 h-3" />
|
||||
置信度阈值
|
||||
</div>
|
||||
<p className="text-sm font-medium text-slate-700">
|
||||
审核: {">="}
|
||||
{rule.confidenceThreshold} | 自动通过: {">="}
|
||||
{rule.autoApproveThreshold}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-white rounded-lg mb-3">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-500 mb-1">
|
||||
<MessageSquare className="w-3 h-3" />
|
||||
AI提示词
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 line-clamp-2">{rule.prompt}</p>
|
||||
</div>
|
||||
|
||||
{/* 统计数据 */}
|
||||
<div className="grid grid-cols-5 gap-4 text-sm">
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold text-slate-800">{rule.accuracy}%</p>
|
||||
<p className="text-xs text-slate-500">准确率</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold text-slate-800">{rule.tagged.toLocaleString()}</p>
|
||||
<p className="text-xs text-slate-500">已打标</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold text-amber-600">{rule.pending.toLocaleString()}</p>
|
||||
<p className="text-xs text-slate-500">待审核</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold text-emerald-600">{rule.approved.toLocaleString()}</p>
|
||||
<p className="text-xs text-slate-500">已通过</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold text-red-600">{rule.rejected.toLocaleString()}</p>
|
||||
<p className="text-xs text-slate-500">已拒绝</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 pt-3 border-t border-slate-200/60 flex items-center justify-between text-xs text-slate-500">
|
||||
<span>上次运行: {rule.lastRun}</span>
|
||||
<Button variant="link" size="sm" className="h-auto p-0 text-violet-600">
|
||||
立即执行
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 审核中心Tab - 增强审核详情展示AI分析依据 */}
|
||||
<TabsContent value="review" className="mt-4">
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-base">候选标签审核</CardTitle>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
AI生成的标签需要人工确认后才能入库,置信度高于自动通过阈值的标签将自动生效
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-slate-500">置信度过滤:</span>
|
||||
<div className="w-32">
|
||||
<Slider
|
||||
value={confidenceFilter}
|
||||
onValueChange={setConfidenceFilter}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.1}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
{">="}
|
||||
{confidenceFilter[0]}
|
||||
</span>
|
||||
</div>
|
||||
{selectedTags.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-slate-500">已选 {selectedTags.length} 项</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-emerald-600 bg-transparent"
|
||||
onClick={handleBatchApprove}
|
||||
>
|
||||
<Check className="w-4 h-4 mr-1" />
|
||||
批量通过
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="text-red-600 bg-transparent">
|
||||
<X className="w-4 h-4 mr-1" />
|
||||
批量拒绝
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{filteredCandidates.map((candidate) => (
|
||||
<div key={candidate.id} className="p-4 bg-slate-50/80 rounded-lg border border-slate-200/60">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-1 rounded"
|
||||
checked={selectedTags.includes(candidate.id)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedTags((prev) => [...prev, candidate.id])
|
||||
} else {
|
||||
setSelectedTags((prev) => prev.filter((t) => t !== candidate.id))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="font-medium text-slate-800">{candidate.userName}</span>
|
||||
<span className="text-xs text-slate-400">{candidate.userId}</span>
|
||||
<Badge className="bg-violet-100 text-violet-700">{candidate.tag}</Badge>
|
||||
<Badge
|
||||
className={`${
|
||||
candidate.confidence >= 0.8
|
||||
? "bg-emerald-100 text-emerald-700"
|
||||
: candidate.confidence >= 0.6
|
||||
? "bg-amber-100 text-amber-700"
|
||||
: "bg-red-100 text-red-700"
|
||||
}`}
|
||||
>
|
||||
置信度: {(candidate.confidence * 100).toFixed(0)}%
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 mb-2">{candidate.reason}</p>
|
||||
<div className="p-3 bg-white rounded-lg border border-slate-200">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-500 mb-1">
|
||||
<Brain className="w-3 h-3" />
|
||||
AI分析依据
|
||||
</div>
|
||||
<p className="text-sm text-slate-600">{candidate.aiAnalysis}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-2 text-xs text-slate-500">
|
||||
<span>来源: {candidate.source}</span>
|
||||
<span>时间: {candidate.createdAt}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-emerald-600 bg-transparent"
|
||||
onClick={() => handleApprove(candidate.id)}
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-red-600 bg-transparent"
|
||||
onClick={() => handleReject(candidate.id)}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setSelectedCandidate(candidate)
|
||||
setReviewDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 操作历史Tab */}
|
||||
<TabsContent value="history" className="mt-4">
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base">审核操作历史</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{tagHistory.map((item) => (
|
||||
<div key={item.id} className="flex items-center justify-between p-3 bg-slate-50/80 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge className="bg-violet-100 text-violet-700">{item.tag}</Badge>
|
||||
<Badge
|
||||
className={`${
|
||||
item.action === "approved"
|
||||
? "bg-emerald-100 text-emerald-700"
|
||||
: item.action === "rejected"
|
||||
? "bg-red-100 text-red-700"
|
||||
: "bg-blue-100 text-blue-700"
|
||||
}`}
|
||||
>
|
||||
{item.action === "approved" ? "通过" : item.action === "rejected" ? "拒绝" : "修改"}
|
||||
</Badge>
|
||||
<span className="text-sm text-slate-600">{item.count} 条记录</span>
|
||||
{item.reason && <span className="text-sm text-slate-500">原因: {item.reason}</span>}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-slate-500">
|
||||
<span>{item.operator}</span>
|
||||
<span>{item.time}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<Dialog open={createRuleDialogOpen} onOpenChange={setCreateRuleDialogOpen}>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>创建AI打标规则</DialogTitle>
|
||||
<DialogDescription>配置AI模型、数据源和提示词,自动为用户生成标签</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>规则名称</Label>
|
||||
<Input
|
||||
placeholder="例如:高消费潜力识别"
|
||||
value={newRule.name}
|
||||
onChange={(e) => setNewRule({ ...newRule, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>AI模型</Label>
|
||||
<Select value={newRule.model} onValueChange={(v) => setNewRule({ ...newRule, model: v })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="gpt-4">GPT-4</SelectItem>
|
||||
<SelectItem value="gpt-4-turbo">GPT-4 Turbo</SelectItem>
|
||||
<SelectItem value="claude-3">Claude 3</SelectItem>
|
||||
<SelectItem value="qwen-max">通义千问 Max</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>选择数据源 (从数据治理模块获取)</Label>
|
||||
<Select value={newRule.dataSource} onValueChange={(v) => setNewRule({ ...newRule, dataSource: v })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择要分析的数据源" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableDataSources.map((ds) => (
|
||||
<SelectItem key={ds.id} value={ds.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="w-4 h-4" />
|
||||
<span>{ds.name}</span>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{ds.type}
|
||||
</Badge>
|
||||
<span className="text-xs text-slate-500">{(ds.records / 1000000).toFixed(1)}M 条</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>AI提示词</Label>
|
||||
<Textarea
|
||||
placeholder="描述你希望AI如何分析用户数据并生成标签。例如:分析用户的消费记录和浏览行为,识别具有高消费潜力的用户..."
|
||||
value={newRule.prompt}
|
||||
onChange={(e) => setNewRule({ ...newRule, prompt: e.target.value })}
|
||||
rows={4}
|
||||
/>
|
||||
<p className="text-xs text-slate-500">提示:清晰描述分析目标、考虑因素和输出标签格式</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>置信度阈值 (需审核)</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Slider
|
||||
value={[newRule.confidenceThreshold]}
|
||||
onValueChange={(v) => setNewRule({ ...newRule, confidenceThreshold: v[0] })}
|
||||
min={0.1}
|
||||
max={1}
|
||||
step={0.1}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="w-12 text-sm font-medium">{newRule.confidenceThreshold}</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">低于此阈值的标签将被自动拒绝</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>自动通过阈值</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Slider
|
||||
value={[newRule.autoApproveThreshold]}
|
||||
onValueChange={(v) => setNewRule({ ...newRule, autoApproveThreshold: v[0] })}
|
||||
min={0.1}
|
||||
max={1}
|
||||
step={0.1}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="w-12 text-sm font-medium">{newRule.autoApproveThreshold}</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">高于此阈值的标签将自动通过,无需审核</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setCreateRuleDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={() => setCreateRuleDialogOpen(false)}>
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
创建并测试
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 规则设置弹窗 */}
|
||||
<Dialog open={settingsDialogOpen} onOpenChange={setSettingsDialogOpen}>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>规则设置 - {selectedRule?.name}</DialogTitle>
|
||||
<DialogDescription>配置AI打标规则参数</DialogDescription>
|
||||
</DialogHeader>
|
||||
{selectedRule && (
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>规则名称</Label>
|
||||
<Input defaultValue={selectedRule.name} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>AI模型</Label>
|
||||
<Select defaultValue={selectedRule.model.toLowerCase().replace(" ", "-")}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="gpt-4">GPT-4</SelectItem>
|
||||
<SelectItem value="claude-3">Claude 3</SelectItem>
|
||||
<SelectItem value="qwen-max">通义千问 Max</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>数据源</Label>
|
||||
<Select defaultValue={selectedRule.dataSource}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableDataSources.map((ds) => (
|
||||
<SelectItem key={ds.id} value={ds.name}>
|
||||
{ds.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>AI提示词</Label>
|
||||
<Textarea defaultValue={selectedRule.prompt} rows={4} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>置信度阈值</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Slider
|
||||
defaultValue={[selectedRule.confidenceThreshold]}
|
||||
min={0.1}
|
||||
max={1}
|
||||
step={0.1}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="w-12 text-sm">{selectedRule.confidenceThreshold}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>自动通过阈值</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Slider
|
||||
defaultValue={[selectedRule.autoApproveThreshold]}
|
||||
min={0.1}
|
||||
max={1}
|
||||
step={0.1}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="w-12 text-sm">{selectedRule.autoApproveThreshold}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setSettingsDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={() => setSettingsDialogOpen(false)}>保存</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 审核详情弹窗 */}
|
||||
<Dialog open={reviewDialogOpen} onOpenChange={setReviewDialogOpen}>
|
||||
<DialogContent className="sm:max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>标签审核详情</DialogTitle>
|
||||
</DialogHeader>
|
||||
{selectedCandidate && (
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="flex items-center gap-4 p-4 bg-slate-50 rounded-lg">
|
||||
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center text-white font-bold">
|
||||
{selectedCandidate.userName[0]}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium">{selectedCandidate.userName}</h4>
|
||||
<p className="text-sm text-slate-500">{selectedCandidate.userId}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-slate-500">候选标签</span>
|
||||
<Badge className="bg-violet-100 text-violet-700">{selectedCandidate.tag}</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-slate-500">置信度</span>
|
||||
<span
|
||||
className={`font-medium ${selectedCandidate.confidence >= 0.7 ? "text-emerald-600" : "text-amber-600"}`}
|
||||
>
|
||||
{(selectedCandidate.confidence * 100).toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-slate-500">数据来源</span>
|
||||
<span>{selectedCandidate.source}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-slate-50 rounded-lg">
|
||||
<h5 className="font-medium mb-2 flex items-center gap-2">
|
||||
<Brain className="w-4 h-4 text-violet-500" />
|
||||
AI分析依据
|
||||
</h5>
|
||||
<p className="text-sm text-slate-600">{selectedCandidate.aiAnalysis}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-red-600 bg-transparent"
|
||||
onClick={() => setReviewDialogOpen(false)}
|
||||
>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
拒绝
|
||||
</Button>
|
||||
<Button className="bg-emerald-600 hover:bg-emerald-700" onClick={() => setReviewDialogOpen(false)}>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
通过
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 批量重算弹窗 */}
|
||||
<Dialog open={batchRunDialogOpen} onOpenChange={setBatchRunDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>批量重新计算</DialogTitle>
|
||||
<DialogDescription>选择要重新运行的AI打标规则</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 py-4">
|
||||
{tagRules.map((rule) => (
|
||||
<label
|
||||
key={rule.id}
|
||||
className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg cursor-pointer hover:bg-slate-100"
|
||||
>
|
||||
<input type="checkbox" className="rounded" defaultChecked={rule.status === "running"} />
|
||||
<div>
|
||||
<p className="font-medium">{rule.name}</p>
|
||||
<p className="text-sm text-slate-500">数据源: {rule.dataSource}</p>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setBatchRunDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={() => setBatchRunDialogOpen(false)}>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
开始执行
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
'use client'
|
||||
"use client"
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { Home, Database, Users, Bot } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { LayoutDashboard, Database, Tags, Brain, Monitor } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ href: '/', label: '首页', icon: Home },
|
||||
{ href: '/data-platform', label: '数据中台', icon: Database },
|
||||
{ href: '/user-portrait', label: '画像', icon: Users },
|
||||
{ href: '/ai-assistant', label: 'AI智能助手', icon: Bot },
|
||||
{ href: "/", label: "概览", icon: LayoutDashboard },
|
||||
{ href: "/data-governance", label: "数据治理", icon: Database },
|
||||
{ href: "/tag-portrait", label: "标签画像", icon: Tags },
|
||||
{ href: "/ai-insight", label: "AI洞察", icon: Brain },
|
||||
{ href: "/monitoring", label: "监控", icon: Monitor },
|
||||
] as const
|
||||
|
||||
export default function BottomNav() {
|
||||
@@ -17,20 +18,20 @@ export default function BottomNav() {
|
||||
|
||||
return (
|
||||
<nav className="fixed bottom-0 left-0 right-0 z-40 border-t bg-white/95 backdrop-blur md:hidden">
|
||||
<ul className="grid grid-cols-4">
|
||||
<ul className="grid grid-cols-5">
|
||||
{NAV_ITEMS.map(({ href, label, icon: Icon }) => {
|
||||
const active = pathname === href || pathname.startsWith(`${href}/`)
|
||||
const active = pathname === href || (href !== "/" && pathname.startsWith(`${href}/`))
|
||||
return (
|
||||
<li key={href}>
|
||||
<Link
|
||||
href={href}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
aria-current={active ? "page" : undefined}
|
||||
className={cn(
|
||||
'flex flex-col items-center justify-center gap-1 py-2 text-xs',
|
||||
active ? 'text-gray-900' : 'text-gray-500',
|
||||
"flex flex-col items-center justify-center gap-1 py-2 text-xs",
|
||||
active ? "text-blue-600" : "text-gray-500",
|
||||
)}
|
||||
>
|
||||
<Icon className={cn('h-5 w-5', active && 'fill-gray-900')} />
|
||||
<Icon className={cn("h-5 w-5", active && "text-blue-600")} />
|
||||
<span>{label}</span>
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import { X } from "lucide-react"
|
||||
import { X, LayoutDashboard, Database, Tags, LineChart, Brain, Package, Monitor, ChevronRight } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
|
||||
interface MobileSidebarProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ href: "/", label: "概览", icon: LayoutDashboard },
|
||||
{ href: "/data-governance", label: "数据治理", icon: Database },
|
||||
{ href: "/tag-portrait", label: "标签画像", icon: Tags },
|
||||
{ href: "/value-model", label: "价值模型", icon: LineChart },
|
||||
{ href: "/ai-insight", label: "AI洞察", icon: Brain },
|
||||
{ href: "/data-asset", label: "数据资产", icon: Package },
|
||||
{ href: "/monitoring", label: "系统监控", icon: Monitor },
|
||||
]
|
||||
|
||||
export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
|
||||
const pathname = usePathname()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -25,26 +38,37 @@ export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
|
||||
<aside className="relative ml-auto h-full w-64 bg-white shadow-lg">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b">
|
||||
<h2 className="text-lg font-semibold">导航</h2>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-gray-900">神射手</h2>
|
||||
<p className="text-xs text-gray-500">私域银行数据中台</p>
|
||||
</div>
|
||||
<button onClick={onClose} aria-label="Close menu" className="rounded p-1 hover:bg-gray-100">
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Navigation links — keep in sync with Sidebar.tsx */}
|
||||
{/* Navigation links */}
|
||||
<nav className="flex flex-col gap-1 p-4">
|
||||
<Link href="/" className="rounded px-3 py-2 text-sm hover:bg-gray-100" onClick={onClose}>
|
||||
数据概览
|
||||
</Link>
|
||||
<Link href="/data-platform" className="rounded px-3 py-2 text-sm hover:bg-gray-100" onClick={onClose}>
|
||||
数据中台
|
||||
</Link>
|
||||
<Link href="/user-portrait" className="rounded px-3 py-2 text-sm hover:bg-gray-100" onClick={onClose}>
|
||||
用户画像
|
||||
</Link>
|
||||
<Link href="/ai-assistant" className="rounded px-3 py-2 text-sm hover:bg-gray-100" onClick={onClose}>
|
||||
AI 智能助手
|
||||
</Link>
|
||||
{NAV_ITEMS.map(({ href, label, icon: Icon }) => {
|
||||
const active = pathname === href || (href !== "/" && pathname.startsWith(href))
|
||||
return (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
className={cn(
|
||||
"flex items-center justify-between rounded-lg px-3 py-3 text-sm transition-colors",
|
||||
active ? "bg-blue-50 text-blue-600" : "text-gray-600 hover:bg-gray-100",
|
||||
)}
|
||||
onClick={onClose}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon className="h-5 w-5" />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
<ChevronRight className="h-4 w-4 text-gray-400" />
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -1,46 +1,208 @@
|
||||
'use client'
|
||||
"use client"
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { Home, Database, Users, Bot } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Database,
|
||||
Tags,
|
||||
Brain,
|
||||
Package,
|
||||
Monitor,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
FileText,
|
||||
Users,
|
||||
Zap,
|
||||
Target,
|
||||
BarChart3,
|
||||
Shield,
|
||||
Bell,
|
||||
Server,
|
||||
MessageSquare,
|
||||
Sparkles,
|
||||
GitBranch,
|
||||
Calendar,
|
||||
Bot,
|
||||
Search,
|
||||
FileOutput,
|
||||
Webhook,
|
||||
Activity,
|
||||
ScrollText,
|
||||
} from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useState } from "react"
|
||||
|
||||
// 按照5个HTML文档重构的导航结构
|
||||
const NAV_ITEMS = [
|
||||
{ href: '/', label: '首页', icon: Home },
|
||||
{ href: '/data-platform', label: '数据中台', icon: Database },
|
||||
{ href: '/user-portrait', label: '画像', icon: Users },
|
||||
{ href: '/ai-assistant', label: 'AI智能助手', icon: Bot },
|
||||
// 第一部分:数据概览
|
||||
{
|
||||
href: "/",
|
||||
label: "数据概览",
|
||||
icon: LayoutDashboard,
|
||||
description: "用户洞察仪表板"
|
||||
},
|
||||
// 第二部分:数据接入
|
||||
{
|
||||
href: "/data-ingestion",
|
||||
label: "数据接入",
|
||||
icon: Database,
|
||||
children: [
|
||||
{ href: "/data-ingestion/sources", label: "数据源管理", icon: Database },
|
||||
{ href: "/data-ingestion/cleaning", label: "清洗规则", icon: Zap },
|
||||
{ href: "/data-ingestion/tasks", label: "任务调度", icon: Calendar },
|
||||
{ href: "/data-ingestion/lineage", label: "数据血缘", icon: GitBranch },
|
||||
{ href: "/data-ingestion/quality", label: "质量监控", icon: Shield },
|
||||
],
|
||||
},
|
||||
// 第三部分:标签画像
|
||||
{
|
||||
href: "/tag-portrait",
|
||||
label: "标签画像",
|
||||
icon: Tags,
|
||||
children: [
|
||||
{ href: "/tag-portrait/tags", label: "标签体系", icon: Tags },
|
||||
{ href: "/tag-portrait/portrait", label: "用户画像", icon: Users },
|
||||
{ href: "/tag-portrait/crowd", label: "人群圈选", icon: Target },
|
||||
],
|
||||
},
|
||||
// 第四部分:AI Agent智能系统
|
||||
{
|
||||
href: "/ai-agent",
|
||||
label: "AI Agent",
|
||||
icon: Bot,
|
||||
children: [
|
||||
{ href: "/ai-agent/chat", label: "智能对话", icon: MessageSquare },
|
||||
{ href: "/ai-agent/smart-tag", label: "AI打标", icon: Sparkles },
|
||||
{ href: "/ai-agent/data-cleaning", label: "AI清洗", icon: Zap },
|
||||
{ href: "/ai-agent/nlq", label: "自然语言查询", icon: Search },
|
||||
{ href: "/ai-agent/report", label: "智能报告", icon: FileText },
|
||||
],
|
||||
},
|
||||
// 第五部分:数据输出
|
||||
{
|
||||
href: "/data-output",
|
||||
label: "数据输出",
|
||||
icon: FileOutput,
|
||||
children: [
|
||||
{ href: "/data-output/packages", label: "流量包", icon: Package },
|
||||
{ href: "/data-output/api-market", label: "API市场", icon: Server },
|
||||
{ href: "/data-output/subscription", label: "数据订阅", icon: Webhook },
|
||||
],
|
||||
},
|
||||
// 系统监控(独立模块)
|
||||
{
|
||||
href: "/system",
|
||||
label: "系统监控",
|
||||
icon: Monitor,
|
||||
children: [
|
||||
{ href: "/system/health", label: "系统健康", icon: Activity },
|
||||
{ href: "/system/alerts", label: "告警中心", icon: Bell },
|
||||
{ href: "/system/logs", label: "操作日志", icon: ScrollText },
|
||||
{ href: "/system/metrics", label: "业务指标", icon: BarChart3 },
|
||||
],
|
||||
},
|
||||
] as const
|
||||
|
||||
export default function Sidebar() {
|
||||
type NavItem = (typeof NAV_ITEMS)[number]
|
||||
|
||||
function NavItemComponent({ item, level = 0 }: { item: NavItem; level?: number }) {
|
||||
const pathname = usePathname()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const hasChildren = "children" in item && item.children && item.children.length > 0
|
||||
const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href))
|
||||
|
||||
// Auto expand if child is active
|
||||
const childActive =
|
||||
hasChildren && item.children?.some((child) => pathname === child.href || pathname.startsWith(child.href))
|
||||
|
||||
return (
|
||||
<aside className="hidden md:block w-56 shrink-0 border-r bg-white">
|
||||
<nav className="p-3">
|
||||
<li>
|
||||
{hasChildren ? (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={cn(
|
||||
"w-full flex items-center justify-between gap-3 rounded-xl px-4 py-3 text-sm font-medium transition-all duration-200",
|
||||
isActive || childActive
|
||||
? "bg-blue-50 text-blue-600"
|
||||
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<item.icon className="h-5 w-5" />
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
{isOpen || childActive ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
</button>
|
||||
{(isOpen || childActive) && (
|
||||
<ul className="mt-1 ml-4 space-y-1 border-l-2 border-gray-100 pl-4">
|
||||
{item.children?.map((child) => {
|
||||
const childIsActive = pathname === child.href
|
||||
return (
|
||||
<li key={child.href}>
|
||||
<Link
|
||||
href={child.href}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-lg px-3 py-2 text-sm transition-all duration-200",
|
||||
childIsActive
|
||||
? "bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-md"
|
||||
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900",
|
||||
)}
|
||||
>
|
||||
<child.icon className="h-4 w-4" />
|
||||
<span>{child.label}</span>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
href={item.href}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-xl px-4 py-3 text-sm font-medium transition-all duration-200",
|
||||
isActive
|
||||
? "bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-md"
|
||||
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900",
|
||||
)}
|
||||
>
|
||||
<item.icon className="h-5 w-5" />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Sidebar() {
|
||||
return (
|
||||
<aside className="hidden md:flex flex-col w-64 shrink-0 border-r bg-white/80 backdrop-blur-md h-screen sticky top-0">
|
||||
<div className="p-6">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-1">神射手</h2>
|
||||
<p className="text-sm text-gray-600">私域银行数据中台</p>
|
||||
</div>
|
||||
<nav className="flex-1 px-4 pb-4 overflow-y-auto">
|
||||
<ul className="space-y-1">
|
||||
{NAV_ITEMS.map(({ href, label, icon: Icon }) => {
|
||||
const active = pathname === href || pathname.startsWith(`${href}/`)
|
||||
return (
|
||||
<li key={href}>
|
||||
<Link
|
||||
href={href}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors',
|
||||
active
|
||||
? 'bg-gray-900 text-white'
|
||||
: 'text-gray-700 hover:bg-gray-100',
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span>{label}</span>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<NavItemComponent key={item.href} item={item} />
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="p-4 border-t border-gray-100">
|
||||
<div className="flex items-center gap-3 px-2">
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center text-white text-sm font-bold">
|
||||
管
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">平台管理员</p>
|
||||
<p className="text-xs text-gray-500 truncate">admin@archer.com</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
3
app/data-asset/api-market/loading.tsx
Normal file
3
app/data-asset/api-market/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
326
app/data-asset/api-market/page.tsx
Normal file
326
app/data-asset/api-market/page.tsx
Normal file
@@ -0,0 +1,326 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Server,
|
||||
ArrowLeft,
|
||||
Plus,
|
||||
Search,
|
||||
Filter,
|
||||
Activity,
|
||||
Zap,
|
||||
Clock,
|
||||
Code,
|
||||
Key,
|
||||
Copy,
|
||||
Eye,
|
||||
Settings,
|
||||
BarChart3,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"
|
||||
import { ApiKeyManagementDialog } from "@/components/dialogs/api-key-management-dialog"
|
||||
import { PublishApiDialog } from "@/components/dialogs/publish-api-dialog"
|
||||
|
||||
const apiServices = [
|
||||
{
|
||||
id: 1,
|
||||
name: "用户画像查询API",
|
||||
description: "根据用户ID查询完整画像信息",
|
||||
endpoint: "/api/v1/user/portrait",
|
||||
method: "GET",
|
||||
totalCalls: 2580000,
|
||||
todayCalls: 125800,
|
||||
qps: 1200,
|
||||
latency: 32,
|
||||
status: "running",
|
||||
subscribers: 45,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "价值评估API",
|
||||
description: "实时评估用户价值等级",
|
||||
endpoint: "/api/v1/user/value",
|
||||
method: "POST",
|
||||
totalCalls: 1890000,
|
||||
todayCalls: 89500,
|
||||
qps: 800,
|
||||
latency: 45,
|
||||
status: "running",
|
||||
subscribers: 38,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "标签推荐API",
|
||||
description: "智能推荐用户标签",
|
||||
endpoint: "/api/v1/tag/recommend",
|
||||
method: "POST",
|
||||
totalCalls: 980000,
|
||||
todayCalls: 45600,
|
||||
qps: 500,
|
||||
latency: 28,
|
||||
status: "running",
|
||||
subscribers: 23,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "流失预测API",
|
||||
description: "预测用户流失概率",
|
||||
endpoint: "/api/v1/predict/churn",
|
||||
method: "POST",
|
||||
totalCalls: 560000,
|
||||
todayCalls: 28900,
|
||||
qps: 300,
|
||||
latency: 120,
|
||||
status: "running",
|
||||
subscribers: 18,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "人群圈选API",
|
||||
description: "根据条件筛选用户人群",
|
||||
endpoint: "/api/v1/crowd/select",
|
||||
method: "POST",
|
||||
totalCalls: 320000,
|
||||
todayCalls: 15600,
|
||||
qps: 200,
|
||||
latency: 250,
|
||||
status: "running",
|
||||
subscribers: 12,
|
||||
},
|
||||
]
|
||||
|
||||
const apiCallTrend = [
|
||||
{ time: "00:00", calls: 8500 },
|
||||
{ time: "04:00", calls: 3200 },
|
||||
{ time: "08:00", calls: 15800 },
|
||||
{ time: "12:00", calls: 28500 },
|
||||
{ time: "16:00", calls: 32000 },
|
||||
{ time: "20:00", calls: 25600 },
|
||||
{ time: "24:00", calls: 18900 },
|
||||
]
|
||||
|
||||
export default function APIMarketPage() {
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [totalCalls, setTotalCalls] = useState(305400)
|
||||
const [showKeyDialog, setShowKeyDialog] = useState(false)
|
||||
const [showPublishDialog, setShowPublishDialog] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setTotalCalls((prev) => prev + Math.floor(Math.random() * 500))
|
||||
}, 2000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50/30 p-6">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/data-asset">
|
||||
<Button variant="ghost" size="icon" className="rounded-full">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">API服务市场</h1>
|
||||
<p className="text-slate-500 mt-1">API服务发布、订阅与监控</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={() => setShowKeyDialog(true)}>
|
||||
<Key className="w-4 h-4 mr-2" />
|
||||
管理密钥
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-gradient-to-r from-blue-500 to-cyan-600 hover:from-blue-600 hover:to-cyan-700"
|
||||
onClick={() => setShowPublishDialog(true)}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
发布API
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API调用概览 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-slate-800">{totalCalls.toLocaleString()}</p>
|
||||
</div>
|
||||
<Zap className="w-8 h-8 text-blue-500" />
|
||||
</div>
|
||||
<p className="text-xs text-emerald-600 mt-2">较昨日 +15.2%</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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">600</p>
|
||||
</div>
|
||||
<Activity className="w-8 h-8 text-emerald-500" />
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-2">峰值: 1,200</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-slate-800">95ms</p>
|
||||
</div>
|
||||
<Clock className="w-8 h-8 text-amber-500" />
|
||||
</div>
|
||||
<p className="text-xs text-emerald-600 mt-2">较上周 -8%</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-slate-800">99.8%</p>
|
||||
</div>
|
||||
<BarChart3 className="w-8 h-8 text-violet-500" />
|
||||
</div>
|
||||
<p className="text-xs text-emerald-600 mt-2">稳定运行</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 调用趋势图 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">今日API调用趋势</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={apiCallTrend}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
|
||||
<XAxis dataKey="time" tick={{ fontSize: 12 }} stroke="#94a3b8" />
|
||||
<YAxis tick={{ fontSize: 12 }} stroke="#94a3b8" />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: "white",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
/>
|
||||
<Line type="monotone" dataKey="calls" stroke="#3b82f6" strokeWidth={2} dot={{ r: 4 }} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 筛选工具栏 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<Input
|
||||
placeholder="搜索API服务..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-9 bg-white/70"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
筛选
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* API服务列表 */}
|
||||
<div className="space-y-4">
|
||||
{apiServices.map((api) => (
|
||||
<Card key={api.id} className="bg-white/70 backdrop-blur border-slate-200/60 hover:shadow-md transition-all">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-cyan-600 flex items-center justify-center">
|
||||
<Server className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-slate-800">{api.name}</h3>
|
||||
<Badge className="bg-emerald-100 text-emerald-700">
|
||||
<Activity className="w-3 h-3 mr-1" />
|
||||
运行中
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 mt-0.5">{api.description}</p>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={api.method === "GET" ? "bg-blue-50 text-blue-600" : "bg-green-50 text-green-600"}
|
||||
>
|
||||
{api.method}
|
||||
</Badge>
|
||||
<code className="text-xs bg-slate-100 px-2 py-0.5 rounded">{api.endpoint}</code>
|
||||
<Button variant="ghost" size="sm" className="h-6 px-2">
|
||||
<Copy className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold text-slate-800">{api.todayCalls.toLocaleString()}</p>
|
||||
<p className="text-xs text-slate-500">今日调用</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-semibold text-slate-700">{api.qps}</p>
|
||||
<p className="text-xs text-slate-500">QPS</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-semibold text-slate-700">{api.latency}ms</p>
|
||||
<p className="text-xs text-slate-500">延迟</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-semibold text-slate-700">{api.subscribers}</p>
|
||||
<p className="text-xs text-slate-500">订阅方</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Code className="w-4 h-4 mr-1" />
|
||||
文档
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
监控
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Settings className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ApiKeyManagementDialog open={showKeyDialog} onOpenChange={setShowKeyDialog} />
|
||||
<PublishApiDialog open={showPublishDialog} onOpenChange={setShowPublishDialog} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
app/data-asset/loading.tsx
Normal file
3
app/data-asset/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
3
app/data-asset/packages/loading.tsx
Normal file
3
app/data-asset/packages/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
261
app/data-asset/packages/page.tsx
Normal file
261
app/data-asset/packages/page.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Package,
|
||||
ArrowLeft,
|
||||
Plus,
|
||||
Download,
|
||||
Trash2,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Filter,
|
||||
Clock,
|
||||
CheckCircle2,
|
||||
Eye,
|
||||
Copy,
|
||||
MoreHorizontal,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { CreatePackageDialog } from "@/components/dialogs/create-package-dialog"
|
||||
import { PackagePreviewDialog } from "@/components/dialogs/package-preview-dialog"
|
||||
|
||||
const packages = [
|
||||
{
|
||||
id: 1,
|
||||
name: "高价值用户包-S级",
|
||||
description: "RFM评分S级的高价值用户",
|
||||
users: 125800,
|
||||
status: "active",
|
||||
createdAt: "2024-12-10",
|
||||
updatedAt: "2小时前",
|
||||
downloads: 23,
|
||||
tags: ["高价值", "S级", "活跃"],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "流失预警用户包",
|
||||
description: "流失风险值大于80的用户",
|
||||
users: 23400,
|
||||
status: "active",
|
||||
createdAt: "2024-12-11",
|
||||
updatedAt: "1小时前",
|
||||
downloads: 15,
|
||||
tags: ["流失风险", "预警"],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "潜力转化用户包",
|
||||
description: "转化概率大于60%的潜力用户",
|
||||
users: 89000,
|
||||
status: "active",
|
||||
createdAt: "2024-12-08",
|
||||
updatedAt: "30分钟前",
|
||||
downloads: 45,
|
||||
tags: ["潜力用户", "转化"],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "沉默用户唤醒包",
|
||||
description: "30天内未活跃的沉默用户",
|
||||
users: 156000,
|
||||
status: "generating",
|
||||
createdAt: "2024-12-12",
|
||||
updatedAt: "生成中",
|
||||
downloads: 0,
|
||||
tags: ["沉默用户", "唤醒"],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "新用户引导包",
|
||||
description: "7天内注册的新用户",
|
||||
users: 12580,
|
||||
status: "active",
|
||||
createdAt: "2024-12-05",
|
||||
updatedAt: "4小时前",
|
||||
downloads: 18,
|
||||
tags: ["新用户", "引导"],
|
||||
},
|
||||
]
|
||||
|
||||
export default function PackagesPage() {
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
||||
const [showPreviewDialog, setShowPreviewDialog] = useState(false)
|
||||
const [selectedPackage, setSelectedPackage] = useState<(typeof packages)[0] | null>(null)
|
||||
|
||||
const handlePreview = (pkg: (typeof packages)[0]) => {
|
||||
setSelectedPackage(pkg)
|
||||
setShowPreviewDialog(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-indigo-50/30 p-6">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/data-asset">
|
||||
<Button variant="ghost" size="icon" className="rounded-full">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">流量包管理</h1>
|
||||
<p className="text-slate-500 mt-1">创建、管理和导出用户流量包</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700"
|
||||
onClick={() => setShowCreateDialog(true)}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
创建流量包
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 筛选工具栏 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<Input
|
||||
placeholder="搜索流量包名称或标签..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
筛选
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 流量包列表 */}
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{packages.map((pkg) => (
|
||||
<Card key={pkg.id} className="bg-white/70 backdrop-blur border-slate-200/60 hover:shadow-md transition-all">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center">
|
||||
<Package className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-slate-800">{pkg.name}</h3>
|
||||
{pkg.status === "active" ? (
|
||||
<Badge className="bg-emerald-100 text-emerald-700">
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
可用
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge className="bg-blue-100 text-blue-700">
|
||||
<RefreshCw className="w-3 h-3 mr-1 animate-spin" />
|
||||
生成中
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 mt-0.5">{pkg.description}</p>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
{pkg.tags.map((tag, i) => (
|
||||
<Badge key={i} variant="outline" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-slate-800">{pkg.users.toLocaleString()}</p>
|
||||
<p className="text-xs text-slate-500">用户数</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-semibold text-slate-700">{pkg.downloads}</p>
|
||||
<p className="text-xs text-slate-500">下载次数</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-slate-600 flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{pkg.updatedAt}
|
||||
</p>
|
||||
<p className="text-xs text-slate-400">创建于 {pkg.createdAt}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={pkg.status !== "active"}
|
||||
onClick={() => handlePreview(pkg)}
|
||||
>
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
预览
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled={pkg.status !== "active"}>
|
||||
<Download className="w-4 h-4 mr-1" />
|
||||
导出
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
复制
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
更新
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-red-600">
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CreatePackageDialog open={showCreateDialog} onOpenChange={setShowCreateDialog} />
|
||||
<PackagePreviewDialog
|
||||
open={showPreviewDialog}
|
||||
onOpenChange={setShowPreviewDialog}
|
||||
packageData={
|
||||
selectedPackage
|
||||
? {
|
||||
name: selectedPackage.name,
|
||||
description: selectedPackage.description,
|
||||
users: selectedPackage.users,
|
||||
tags: selectedPackage.tags,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
274
app/data-asset/page.tsx
Normal file
274
app/data-asset/page.tsx
Normal file
@@ -0,0 +1,274 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Package, Server, Download, Upload, Users, Activity, ArrowRight, CheckCircle2, Zap } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
const assetOverview = {
|
||||
totalPackages: 156,
|
||||
apiServices: 28,
|
||||
totalCalls: 12580000,
|
||||
activeSubscribers: 89,
|
||||
}
|
||||
|
||||
const trafficPackages = [
|
||||
{ id: 1, name: "高价值用户包", users: 125800, status: "active", updated: "2小时前", downloads: 23 },
|
||||
{ id: 2, name: "流失预警用户包", users: 23400, status: "active", updated: "1小时前", downloads: 15 },
|
||||
{ id: 3, name: "潜力转化用户包", users: 89000, status: "active", updated: "30分钟前", downloads: 45 },
|
||||
{ id: 4, name: "沉默用户唤醒包", users: 156000, status: "pending", updated: "生成中", downloads: 0 },
|
||||
]
|
||||
|
||||
const apiServices = [
|
||||
{ id: 1, name: "用户画像查询API", calls: 2580000, qps: 1200, status: "running", latency: "32ms" },
|
||||
{ id: 2, name: "价值评估API", calls: 1890000, qps: 800, status: "running", latency: "45ms" },
|
||||
{ id: 3, name: "标签推荐API", calls: 980000, qps: 500, status: "running", latency: "28ms" },
|
||||
{ id: 4, name: "流失预测API", calls: 560000, qps: 300, status: "running", latency: "120ms" },
|
||||
]
|
||||
|
||||
export default function DataAssetPage() {
|
||||
const [todayCalls, setTodayCalls] = useState(258000)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setTodayCalls((prev) => prev + Math.floor(Math.random() * 1000))
|
||||
}, 2000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-indigo-50/30 p-6">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">数据资产与输出中心</h1>
|
||||
<p className="text-slate-500 mt-1">流量包管理、API服务市场、数据输出</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline">
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
导入数据
|
||||
</Button>
|
||||
<Button className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700">
|
||||
<Package className="w-4 h-4 mr-2" />
|
||||
创建流量包
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 资产概览 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-slate-800">{assetOverview.totalPackages}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center">
|
||||
<Package className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-emerald-600 mt-2">本月新增 12</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500">API服务数</p>
|
||||
<p className="text-2xl font-bold text-slate-800">{assetOverview.apiServices}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-cyan-600 flex items-center justify-center">
|
||||
<Server className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-emerald-600 mt-2">全部运行中</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500">今日API调用</p>
|
||||
<p className="text-2xl font-bold text-slate-800">{todayCalls.toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-emerald-500 to-teal-600 flex items-center justify-center">
|
||||
<Zap className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-emerald-600 mt-2">较昨日 +15.2%</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-slate-800">{assetOverview.activeSubscribers}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-orange-500 to-amber-600 flex items-center justify-center">
|
||||
<Users className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-emerald-600 mt-2">本月新增 8</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 功能入口 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Link href="/data-asset/packages">
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60 hover:shadow-lg hover:border-slate-300 transition-all cursor-pointer group h-full">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center">
|
||||
<Package className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-800">流量包管理</h3>
|
||||
<p className="text-sm text-slate-500">创建、管理、导出用户流量包</p>
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight className="w-5 h-5 text-slate-400 group-hover:text-slate-600 group-hover:translate-x-1 transition-all" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
<Link href="/data-asset/api-market">
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60 hover:shadow-lg hover:border-slate-300 transition-all cursor-pointer group h-full">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-blue-500 to-cyan-600 flex items-center justify-center">
|
||||
<Server className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-800">API服务市场</h3>
|
||||
<p className="text-sm text-slate-500">API服务发布、订阅、监控</p>
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight className="w-5 h-5 text-slate-400 group-hover:text-slate-600 group-hover:translate-x-1 transition-all" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* 热门流量包 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Package className="w-4 h-4 text-indigo-500" />
|
||||
热门流量包
|
||||
</CardTitle>
|
||||
<Link href="/data-asset/packages">
|
||||
<Button variant="ghost" size="sm">
|
||||
查看全部
|
||||
<ArrowRight className="w-4 h-4 ml-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{trafficPackages.map((pkg) => (
|
||||
<div key={pkg.id} className="flex items-center justify-between p-3 bg-slate-50/80 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-indigo-100 to-purple-100 flex items-center justify-center">
|
||||
<Users className="w-5 h-5 text-indigo-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-800">{pkg.name}</p>
|
||||
<p className="text-xs text-slate-500">{pkg.users.toLocaleString()} 用户</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{pkg.status === "active" ? (
|
||||
<Badge className="bg-emerald-100 text-emerald-700">
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
可用
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge className="bg-blue-100 text-blue-700">
|
||||
<Activity className="w-3 h-3 mr-1 animate-pulse" />
|
||||
生成中
|
||||
</Badge>
|
||||
)}
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-slate-500">{pkg.updated}</p>
|
||||
{pkg.downloads > 0 && (
|
||||
<p className="text-xs text-slate-400">
|
||||
<Download className="w-3 h-3 inline mr-1" />
|
||||
{pkg.downloads}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* API服务状态 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Server className="w-4 h-4 text-blue-500" />
|
||||
API服务状态
|
||||
</CardTitle>
|
||||
<Link href="/data-asset/api-market">
|
||||
<Button variant="ghost" size="sm">
|
||||
查看全部
|
||||
<ArrowRight className="w-4 h-4 ml-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{apiServices.map((api) => (
|
||||
<div key={api.id} className="flex items-center justify-between p-3 bg-slate-50/80 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-100 to-cyan-100 flex items-center justify-center">
|
||||
<Zap className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-800">{api.name}</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
QPS: {api.qps} | 延迟: {api.latency}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Badge className="bg-emerald-100 text-emerald-700">
|
||||
<Activity className="w-3 h-3 mr-1" />
|
||||
运行中
|
||||
</Badge>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium text-slate-700">{(api.calls / 1000000).toFixed(1)}M</p>
|
||||
<p className="text-xs text-slate-500">总调用</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
348
app/data-governance/cleaning/page.tsx
Normal file
348
app/data-governance/cleaning/page.tsx
Normal file
@@ -0,0 +1,348 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Zap, Plus, Play, CheckCircle, Clock, AlertTriangle, FileText, Settings, Code, ArrowRight } from "lucide-react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { CreateCleaningRuleDialog } from "@/components/dialogs/create-cleaning-rule-dialog"
|
||||
import { CleaningRuleSettingsDialog } from "@/components/dialogs/cleaning-rule-settings-dialog"
|
||||
import { CleaningRuleLogsDialog } from "@/components/dialogs/cleaning-rule-logs-dialog"
|
||||
|
||||
interface CleaningRule {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
type: "dedup" | "format" | "mapping" | "validate" | "transform"
|
||||
sourceTable: string
|
||||
targetTable: string
|
||||
status: "active" | "inactive" | "testing"
|
||||
lastRun: string
|
||||
processedRecords: number
|
||||
errorRate: number
|
||||
}
|
||||
|
||||
const MOCK_RULES: CleaningRule[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "用户手机号标准化",
|
||||
description: "统一手机号格式,去除+86前缀和空格",
|
||||
type: "format",
|
||||
sourceTable: "raw_users",
|
||||
targetTable: "clean_users",
|
||||
status: "active",
|
||||
lastRun: "2025-12-12 14:30:00",
|
||||
processedRecords: 12895634,
|
||||
errorRate: 0.02,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "交易记录去重",
|
||||
description: "基于交易ID和时间戳去除重复记录",
|
||||
type: "dedup",
|
||||
sourceTable: "raw_transactions",
|
||||
targetTable: "clean_transactions",
|
||||
status: "active",
|
||||
lastRun: "2025-12-12 14:25:00",
|
||||
processedRecords: 89234567,
|
||||
errorRate: 0.01,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "城市编码映射",
|
||||
description: "将城市名称映射为标准行政区划代码",
|
||||
type: "mapping",
|
||||
sourceTable: "raw_users",
|
||||
targetTable: "clean_users",
|
||||
status: "active",
|
||||
lastRun: "2025-12-12 14:20:00",
|
||||
processedRecords: 45678901,
|
||||
errorRate: 0.05,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "金额字段校验",
|
||||
description: "校验金额字段非负且在合理范围内",
|
||||
type: "validate",
|
||||
sourceTable: "raw_transactions",
|
||||
targetTable: "clean_transactions",
|
||||
status: "testing",
|
||||
lastRun: "2025-12-12 10:00:00",
|
||||
processedRecords: 1000000,
|
||||
errorRate: 0.12,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "时间戳转换",
|
||||
description: "将各种时间格式统一转换为UTC时间戳",
|
||||
type: "transform",
|
||||
sourceTable: "raw_events",
|
||||
targetTable: "clean_events",
|
||||
status: "active",
|
||||
lastRun: "2025-12-12 14:35:00",
|
||||
processedRecords: 256789012,
|
||||
errorRate: 0.001,
|
||||
},
|
||||
]
|
||||
|
||||
const TYPE_CONFIG = {
|
||||
dedup: { label: "去重", color: "bg-blue-100 text-blue-700" },
|
||||
format: { label: "格式化", color: "bg-green-100 text-green-700" },
|
||||
mapping: { label: "映射", color: "bg-purple-100 text-purple-700" },
|
||||
validate: { label: "校验", color: "bg-yellow-100 text-yellow-700" },
|
||||
transform: { label: "转换", color: "bg-orange-100 text-orange-700" },
|
||||
}
|
||||
|
||||
const PRESETS = [
|
||||
{ name: "去除首尾空格", code: "TRIM(column)" },
|
||||
{ name: "转大写", code: "UPPER(column)" },
|
||||
{ name: "转小写", code: "LOWER(column)" },
|
||||
{ name: "日期格式化", code: "DATE_FORMAT(column, '%Y-%m-%d')" },
|
||||
{ name: "空值填充", code: "COALESCE(column, default_value)" },
|
||||
{ name: "正则替换", code: "REGEXP_REPLACE(column, pattern, replacement)" },
|
||||
]
|
||||
|
||||
export default function DataCleaningPage() {
|
||||
const [rules, setRules] = useState<CleaningRule[]>(MOCK_RULES)
|
||||
const [selectedTab, setSelectedTab] = useState("rules")
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
||||
const [showSettingsDialog, setShowSettingsDialog] = useState(false)
|
||||
const [showLogsDialog, setShowLogsDialog] = useState(false)
|
||||
const [selectedRule, setSelectedRule] = useState<CleaningRule | null>(null)
|
||||
|
||||
const stats = {
|
||||
totalRules: rules.length,
|
||||
activeRules: rules.filter((r) => r.status === "active").length,
|
||||
totalProcessed: rules.reduce((sum, r) => sum + r.processedRecords, 0),
|
||||
avgErrorRate: (rules.reduce((sum, r) => sum + r.errorRate, 0) / rules.length).toFixed(3),
|
||||
}
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
const handleSettings = (rule: CleaningRule) => {
|
||||
setSelectedRule(rule)
|
||||
setShowSettingsDialog(true)
|
||||
}
|
||||
|
||||
const handleViewLogs = (rule: CleaningRule) => {
|
||||
setSelectedRule(rule)
|
||||
setShowLogsDialog(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">数据清洗</h1>
|
||||
<p className="text-gray-500 mt-1">配置清洗规则,保障数据质量</p>
|
||||
</div>
|
||||
<Button
|
||||
className="bg-gradient-to-r from-blue-500 to-purple-500 text-white"
|
||||
onClick={() => setShowCreateDialog(true)}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
新增清洗规则
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<FileText className="w-4 h-4" />
|
||||
清洗规则
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.totalRules}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
启用中
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-green-600">{stats.activeRules}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<Zap className="w-4 h-4" />
|
||||
已处理记录
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{formatNumber(stats.totalProcessed)}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-500" />
|
||||
平均错误率
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.avgErrorRate}%</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
|
||||
<TabsList className="bg-white/60 backdrop-blur-sm">
|
||||
<TabsTrigger value="rules">清洗规则</TabsTrigger>
|
||||
<TabsTrigger value="presets">预置函数</TabsTrigger>
|
||||
<TabsTrigger value="sql">SQL编辑器</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="rules" className="mt-4">
|
||||
<div className="space-y-4">
|
||||
{rules.map((rule) => (
|
||||
<Card key={rule.id} className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="font-semibold text-gray-900">{rule.name}</h3>
|
||||
<Badge className={TYPE_CONFIG[rule.type].color}>{TYPE_CONFIG[rule.type].label}</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
rule.status === "active"
|
||||
? "border-green-500 text-green-600"
|
||||
: rule.status === "testing"
|
||||
? "border-yellow-500 text-yellow-600"
|
||||
: "border-gray-500 text-gray-600"
|
||||
}
|
||||
>
|
||||
{rule.status === "active" ? "启用" : rule.status === "testing" ? "测试中" : "停用"}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mb-3">{rule.description}</p>
|
||||
<div className="flex items-center gap-6 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-gray-500">数据流向:</span>
|
||||
<code className="bg-gray-100 px-2 py-0.5 rounded text-xs">{rule.sourceTable}</code>
|
||||
<ArrowRight className="w-3 h-3 text-gray-400" />
|
||||
<code className="bg-gray-100 px-2 py-0.5 rounded text-xs">{rule.targetTable}</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">处理量:</span>
|
||||
<span className="ml-1 font-medium">{formatNumber(rule.processedRecords)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">错误率:</span>
|
||||
<span
|
||||
className={`ml-1 font-medium ${rule.errorRate > 0.1 ? "text-red-600" : "text-green-600"}`}
|
||||
>
|
||||
{rule.errorRate}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Play className="w-4 h-4 mr-1" />
|
||||
执行
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleSettings(rule)}>
|
||||
<Settings className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 pt-4 border-t border-gray-100 flex items-center justify-between text-xs text-gray-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
最后执行: {rule.lastRun}
|
||||
</span>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="h-auto p-0 text-blue-500"
|
||||
onClick={() => handleViewLogs(rule)}
|
||||
>
|
||||
查看执行日志
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="presets" className="mt-4">
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">预置清洗函数</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{PRESETS.map((preset, i) => (
|
||||
<div key={i} className="p-4 rounded-xl bg-gray-50 hover:bg-gray-100 transition-colors cursor-pointer">
|
||||
<h4 className="font-medium text-gray-900 mb-2">{preset.name}</h4>
|
||||
<code className="text-xs text-gray-600 bg-white px-2 py-1 rounded block">{preset.code}</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="sql" className="mt-4">
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Code className="w-5 h-5" />
|
||||
SQL清洗规则编辑器
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="bg-gray-900 rounded-xl 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,
|
||||
COALESCE(city, '未知') as city,
|
||||
DATE_FORMAT(created_at, '%Y-%m-%d') as created_date
|
||||
FROM raw_users
|
||||
WHERE email IS NOT NULL
|
||||
AND LENGTH(phone) >= 11;`}</pre>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 mt-4">
|
||||
<Button variant="outline">测试执行</Button>
|
||||
<Button>保存规则</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Create Cleaning Rule Dialog */}
|
||||
<CreateCleaningRuleDialog open={showCreateDialog} onOpenChange={setShowCreateDialog} />
|
||||
|
||||
<CleaningRuleSettingsDialog
|
||||
open={showSettingsDialog}
|
||||
onOpenChange={setShowSettingsDialog}
|
||||
rule={
|
||||
selectedRule
|
||||
? {
|
||||
id: selectedRule.id,
|
||||
name: selectedRule.name,
|
||||
description: selectedRule.description,
|
||||
type: selectedRule.type,
|
||||
sourceTable: selectedRule.sourceTable,
|
||||
targetTable: selectedRule.targetTable,
|
||||
status: selectedRule.status,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<CleaningRuleLogsDialog open={showLogsDialog} onOpenChange={setShowLogsDialog} ruleName={selectedRule?.name} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
12
app/data-governance/page.tsx
Normal file
12
app/data-governance/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
"use client"
|
||||
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect } from "react"
|
||||
|
||||
export default function DataGovernancePage() {
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
router.replace("/data-governance/sources")
|
||||
}, [router])
|
||||
return null
|
||||
}
|
||||
265
app/data-governance/quality/page.tsx
Normal file
265
app/data-governance/quality/page.tsx
Normal file
@@ -0,0 +1,265 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Shield, TrendingUp, TrendingDown, RefreshCw, Download } from "lucide-react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
|
||||
interface QualityMetric {
|
||||
name: string
|
||||
score: number
|
||||
trend: "up" | "down" | "stable"
|
||||
change: number
|
||||
description: string
|
||||
}
|
||||
|
||||
interface TableQuality {
|
||||
tableName: string
|
||||
completeness: number
|
||||
uniqueness: number
|
||||
timeliness: number
|
||||
accuracy: number
|
||||
overallScore: number
|
||||
issues: number
|
||||
lastCheck: string
|
||||
}
|
||||
|
||||
const QUALITY_METRICS: QualityMetric[] = [
|
||||
{ name: "完整性", score: 96.8, trend: "up", change: 0.5, description: "字段非空率" },
|
||||
{ name: "唯一性", score: 99.2, trend: "stable", change: 0, description: "主键重复率" },
|
||||
{ name: "及时性", score: 94.5, trend: "down", change: -1.2, description: "数据更新延迟" },
|
||||
{ name: "准确性", score: 97.3, trend: "up", change: 0.8, description: "格式校验通过率" },
|
||||
]
|
||||
|
||||
const TABLE_QUALITY: TableQuality[] = [
|
||||
{
|
||||
tableName: "users",
|
||||
completeness: 98.5,
|
||||
uniqueness: 100,
|
||||
timeliness: 96.2,
|
||||
accuracy: 99.1,
|
||||
overallScore: 98.5,
|
||||
issues: 2,
|
||||
lastCheck: "2025-12-12 14:30:00",
|
||||
},
|
||||
{
|
||||
tableName: "transactions",
|
||||
completeness: 99.8,
|
||||
uniqueness: 99.9,
|
||||
timeliness: 98.5,
|
||||
accuracy: 99.5,
|
||||
overallScore: 99.4,
|
||||
issues: 0,
|
||||
lastCheck: "2025-12-12 14:30:00",
|
||||
},
|
||||
{
|
||||
tableName: "user_behaviors",
|
||||
completeness: 92.3,
|
||||
uniqueness: 98.5,
|
||||
timeliness: 89.2,
|
||||
accuracy: 95.6,
|
||||
overallScore: 93.9,
|
||||
issues: 8,
|
||||
lastCheck: "2025-12-12 14:25:00",
|
||||
},
|
||||
{
|
||||
tableName: "accounts",
|
||||
completeness: 97.2,
|
||||
uniqueness: 100,
|
||||
timeliness: 95.8,
|
||||
accuracy: 98.3,
|
||||
overallScore: 97.8,
|
||||
issues: 1,
|
||||
lastCheck: "2025-12-12 14:30:00",
|
||||
},
|
||||
{
|
||||
tableName: "tags",
|
||||
completeness: 100,
|
||||
uniqueness: 100,
|
||||
timeliness: 100,
|
||||
accuracy: 100,
|
||||
overallScore: 100,
|
||||
issues: 0,
|
||||
lastCheck: "2025-12-12 14:30:00",
|
||||
},
|
||||
]
|
||||
|
||||
export default function DataQualityPage() {
|
||||
const [selectedPeriod, setSelectedPeriod] = useState("today")
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [overallScore, setOverallScore] = useState(96.95)
|
||||
|
||||
const getScoreColor = (score: number) => {
|
||||
if (score >= 95) return "text-green-600"
|
||||
if (score >= 80) return "text-yellow-600"
|
||||
return "text-red-600"
|
||||
}
|
||||
|
||||
const getScoreBg = (score: number) => {
|
||||
if (score >= 95) return "bg-green-100"
|
||||
if (score >= 80) return "bg-yellow-100"
|
||||
return "bg-red-100"
|
||||
}
|
||||
|
||||
const refreshData = async () => {
|
||||
setIsRefreshing(true)
|
||||
await new Promise((r) => setTimeout(r, 1500))
|
||||
setOverallScore((prev) => prev + (Math.random() - 0.5) * 0.5)
|
||||
setIsRefreshing(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">质量监控</h1>
|
||||
<p className="text-gray-500 mt-1">实时监控数据质量指标</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Select value={selectedPeriod} onValueChange={setSelectedPeriod}>
|
||||
<SelectTrigger className="w-32 bg-white/60">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="today">今日</SelectItem>
|
||||
<SelectItem value="week">本周</SelectItem>
|
||||
<SelectItem value="month">本月</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" onClick={refreshData} disabled={isRefreshing}>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||
刷新
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出报告
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overall Score */}
|
||||
<Card className="border-none shadow-lg bg-gradient-to-br from-green-500 to-emerald-600 text-white">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-green-100 mb-2">
|
||||
<Shield className="w-5 h-5" />
|
||||
<span>数据质量总分</span>
|
||||
</div>
|
||||
<div className="text-6xl font-bold">{overallScore.toFixed(1)}</div>
|
||||
<p className="text-green-100 mt-2">满分100分,当前处于优秀水平</p>
|
||||
</div>
|
||||
<div className="hidden md:block">
|
||||
<div className="w-32 h-32 rounded-full border-8 border-white/20 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold">A+</div>
|
||||
<div className="text-xs text-green-100">质量等级</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Quality Metrics */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{QUALITY_METRICS.map((metric, i) => (
|
||||
<Card key={i} className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-gray-500">{metric.name}</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{metric.trend === "up" ? (
|
||||
<TrendingUp className="w-4 h-4 text-green-500" />
|
||||
) : metric.trend === "down" ? (
|
||||
<TrendingDown className="w-4 h-4 text-red-500" />
|
||||
) : null}
|
||||
<span
|
||||
className={`text-xs ${metric.change > 0 ? "text-green-600" : metric.change < 0 ? "text-red-600" : "text-gray-500"}`}
|
||||
>
|
||||
{metric.change > 0 ? "+" : ""}
|
||||
{metric.change}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`text-3xl font-bold ${getScoreColor(metric.score)}`}>{metric.score}%</div>
|
||||
<p className="text-xs text-gray-400 mt-1">{metric.description}</p>
|
||||
<Progress value={metric.score} className="h-2 mt-2" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Table Quality Details */}
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">数据表质量详情</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200">
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-500">数据表</th>
|
||||
<th className="text-center py-3 px-4 text-sm font-medium text-gray-500">完整性</th>
|
||||
<th className="text-center py-3 px-4 text-sm font-medium text-gray-500">唯一性</th>
|
||||
<th className="text-center py-3 px-4 text-sm font-medium text-gray-500">及时性</th>
|
||||
<th className="text-center py-3 px-4 text-sm font-medium text-gray-500">准确性</th>
|
||||
<th className="text-center py-3 px-4 text-sm font-medium text-gray-500">总分</th>
|
||||
<th className="text-center py-3 px-4 text-sm font-medium text-gray-500">问题数</th>
|
||||
<th className="text-right py-3 px-4 text-sm font-medium text-gray-500">最后检查</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{TABLE_QUALITY.map((table, i) => (
|
||||
<tr key={i} className="border-b border-gray-100 hover:bg-gray-50">
|
||||
<td className="py-3 px-4">
|
||||
<code className="text-sm font-medium text-gray-900">{table.tableName}</code>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-center">
|
||||
<span className={`text-sm font-medium ${getScoreColor(table.completeness)}`}>
|
||||
{table.completeness}%
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-center">
|
||||
<span className={`text-sm font-medium ${getScoreColor(table.uniqueness)}`}>
|
||||
{table.uniqueness}%
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-center">
|
||||
<span className={`text-sm font-medium ${getScoreColor(table.timeliness)}`}>
|
||||
{table.timeliness}%
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-center">
|
||||
<span className={`text-sm font-medium ${getScoreColor(table.accuracy)}`}>{table.accuracy}%</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-center">
|
||||
<Badge className={`${getScoreBg(table.overallScore)} ${getScoreColor(table.overallScore)}`}>
|
||||
{table.overallScore}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-center">
|
||||
{table.issues > 0 ? (
|
||||
<Badge variant="destructive">{table.issues}</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary" className="bg-green-100 text-green-700">
|
||||
0
|
||||
</Badge>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-right text-xs text-gray-500">{table.lastCheck}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
app/data-governance/sources/loading.tsx
Normal file
3
app/data-governance/sources/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
933
app/data-governance/sources/page.tsx
Normal file
933
app/data-governance/sources/page.tsx
Normal file
@@ -0,0 +1,933 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Database,
|
||||
Plus,
|
||||
Search,
|
||||
Play,
|
||||
Pause,
|
||||
RefreshCw,
|
||||
Settings,
|
||||
Trash2,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
Server,
|
||||
FileText,
|
||||
Globe,
|
||||
AlertTriangle,
|
||||
Link2,
|
||||
} from "lucide-react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
|
||||
interface DataSource {
|
||||
id: string
|
||||
name: string
|
||||
type: "mysql" | "postgresql" | "restapi" | "webhook" | "sftp" | "kafka"
|
||||
status: "running" | "stopped" | "error" | "syncing"
|
||||
syncMode: "full" | "incremental" | "realtime"
|
||||
frequency: string
|
||||
lastSync: string
|
||||
records: number
|
||||
errorCount: number
|
||||
config: Record<string, string>
|
||||
}
|
||||
|
||||
const MOCK_SOURCES: DataSource[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "存客宝-用户主库",
|
||||
type: "mysql",
|
||||
status: "running",
|
||||
syncMode: "incremental",
|
||||
frequency: "每5分钟",
|
||||
lastSync: "2025-12-12 14:30:00",
|
||||
records: 128956342,
|
||||
errorCount: 0,
|
||||
config: { host: "db.cunkebao.com", port: "3306", database: "user_main" },
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "触客宝-行为数据",
|
||||
type: "kafka",
|
||||
status: "running",
|
||||
syncMode: "realtime",
|
||||
frequency: "实时",
|
||||
lastSync: "2025-12-12 14:35:12",
|
||||
records: 89234567,
|
||||
errorCount: 2,
|
||||
config: { brokers: "kafka.chukebao.com:9092", topic: "user_behavior" },
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "数智员工-账号API",
|
||||
type: "restapi",
|
||||
status: "running",
|
||||
syncMode: "incremental",
|
||||
frequency: "每小时",
|
||||
lastSync: "2025-12-12 14:00:00",
|
||||
records: 45678901,
|
||||
errorCount: 0,
|
||||
config: { endpoint: "https://api.shuzhi.com/v1/accounts", method: "GET", auth: "Bearer Token" },
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "征信数据-外部接口",
|
||||
type: "restapi",
|
||||
status: "stopped",
|
||||
syncMode: "incremental",
|
||||
frequency: "每天",
|
||||
lastSync: "2025-12-11 00:00:00",
|
||||
records: 12890456,
|
||||
errorCount: 0,
|
||||
config: { endpoint: "https://credit.external.com/api", method: "POST", auth: "API Key" },
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "交易流水-SFTP",
|
||||
type: "sftp",
|
||||
status: "running",
|
||||
syncMode: "incremental",
|
||||
frequency: "每30分钟",
|
||||
lastSync: "2025-12-12 14:30:00",
|
||||
records: 256789012,
|
||||
errorCount: 1,
|
||||
config: { host: "sftp.bank.com", path: "/data/transactions" },
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "聚宝盆-指标数据",
|
||||
type: "postgresql",
|
||||
status: "error",
|
||||
syncMode: "full",
|
||||
frequency: "每天",
|
||||
lastSync: "2025-12-10 00:00:00",
|
||||
records: 8901234,
|
||||
errorCount: 15,
|
||||
config: { host: "pg.jubao.com", port: "5432", database: "metrics" },
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
name: "微信消息-Webhook",
|
||||
type: "webhook",
|
||||
status: "running",
|
||||
syncMode: "realtime",
|
||||
frequency: "实时推送",
|
||||
lastSync: "2025-12-12 14:36:00",
|
||||
records: 34567890,
|
||||
errorCount: 0,
|
||||
config: { webhookUrl: "/api/webhook/wechat", secret: "***" },
|
||||
},
|
||||
]
|
||||
|
||||
const TYPE_ICONS = {
|
||||
mysql: Database,
|
||||
postgresql: Database,
|
||||
restapi: Globe,
|
||||
webhook: Link2,
|
||||
sftp: FileText,
|
||||
kafka: Server,
|
||||
}
|
||||
|
||||
const TYPE_COLORS = {
|
||||
mysql: "bg-blue-100 text-blue-700",
|
||||
postgresql: "bg-indigo-100 text-indigo-700",
|
||||
restapi: "bg-green-100 text-green-700",
|
||||
webhook: "bg-teal-100 text-teal-700",
|
||||
sftp: "bg-yellow-100 text-yellow-700",
|
||||
kafka: "bg-purple-100 text-purple-700",
|
||||
}
|
||||
|
||||
const TYPE_LABELS = {
|
||||
mysql: "MySQL",
|
||||
postgresql: "PostgreSQL",
|
||||
restapi: "REST API",
|
||||
webhook: "Webhook",
|
||||
sftp: "SFTP",
|
||||
kafka: "Kafka",
|
||||
}
|
||||
|
||||
const STATUS_CONFIG = {
|
||||
running: { color: "bg-green-100 text-green-700", icon: CheckCircle, label: "运行中" },
|
||||
stopped: { color: "bg-gray-100 text-gray-700", icon: Pause, label: "已停止" },
|
||||
error: { color: "bg-red-100 text-red-700", icon: XCircle, label: "错误" },
|
||||
syncing: { color: "bg-blue-100 text-blue-700", icon: RefreshCw, label: "同步中" },
|
||||
}
|
||||
|
||||
const syncLogs = [
|
||||
{ id: 1, time: "2025-12-12 14:30:00", level: "info", message: "开始增量同步...", records: null },
|
||||
{ id: 2, time: "2025-12-12 14:30:05", level: "info", message: "获取增量数据 2,350 条", records: 2350 },
|
||||
{ id: 3, time: "2025-12-12 14:30:12", level: "success", message: "同步完成,写入 2,350 条", records: 2350 },
|
||||
{ id: 4, time: "2025-12-12 14:25:00", level: "info", message: "开始增量同步...", records: null },
|
||||
{ id: 5, time: "2025-12-12 14:25:08", level: "success", message: "同步完成,写入 1,890 条", records: 1890 },
|
||||
]
|
||||
|
||||
export default function DataSourcesPage() {
|
||||
const [sources, setSources] = useState<DataSource[]>(MOCK_SOURCES)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [filterType, setFilterType] = useState<string>("all")
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||||
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false)
|
||||
const [logsDialogOpen, setLogsDialogOpen] = useState(false)
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||
const [selectedSource, setSelectedSource] = useState<DataSource | null>(null)
|
||||
const [newSource, setNewSource] = useState({
|
||||
name: "",
|
||||
type: "mysql",
|
||||
host: "",
|
||||
port: "",
|
||||
database: "",
|
||||
// API配置
|
||||
endpoint: "",
|
||||
method: "GET",
|
||||
authType: "none",
|
||||
authToken: "",
|
||||
headers: "",
|
||||
// Webhook配置
|
||||
webhookPath: "",
|
||||
webhookSecret: "",
|
||||
})
|
||||
const [addStep, setAddStep] = useState(1)
|
||||
|
||||
const filteredSources = sources.filter((source) => {
|
||||
const matchesSearch = source.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
const matchesType = filterType === "all" || source.type === filterType
|
||||
return matchesSearch && matchesType
|
||||
})
|
||||
|
||||
const stats = {
|
||||
total: sources.length,
|
||||
running: sources.filter((s) => s.status === "running").length,
|
||||
error: sources.filter((s) => s.status === "error").length,
|
||||
totalRecords: sources.reduce((sum, s) => sum + s.records, 0),
|
||||
apiCount: sources.filter((s) => s.type === "restapi" || s.type === "webhook").length,
|
||||
}
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
const handleToggleStatus = (id: string) => {
|
||||
setSources((prev) =>
|
||||
prev.map((s) => {
|
||||
if (s.id === id) {
|
||||
return { ...s, status: s.status === "running" ? "stopped" : "running" }
|
||||
}
|
||||
return s
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const handleAddSource = () => {
|
||||
const newId = (sources.length + 1).toString()
|
||||
const newSourceData: DataSource = {
|
||||
id: newId,
|
||||
name: newSource.name,
|
||||
type: newSource.type as DataSource["type"],
|
||||
status: "stopped",
|
||||
syncMode: newSource.type === "webhook" ? "realtime" : "incremental",
|
||||
frequency: newSource.type === "webhook" ? "实时推送" : "每小时",
|
||||
lastSync: "-",
|
||||
records: 0,
|
||||
errorCount: 0,
|
||||
config:
|
||||
newSource.type === "restapi"
|
||||
? { endpoint: newSource.endpoint, method: newSource.method, auth: newSource.authType }
|
||||
: newSource.type === "webhook"
|
||||
? { webhookUrl: newSource.webhookPath, secret: "***" }
|
||||
: { host: newSource.host, port: newSource.port, database: newSource.database },
|
||||
}
|
||||
setSources((prev) => [...prev, newSourceData])
|
||||
setIsAddDialogOpen(false)
|
||||
setAddStep(1)
|
||||
setNewSource({
|
||||
name: "",
|
||||
type: "mysql",
|
||||
host: "",
|
||||
port: "",
|
||||
database: "",
|
||||
endpoint: "",
|
||||
method: "GET",
|
||||
authType: "none",
|
||||
authToken: "",
|
||||
headers: "",
|
||||
webhookPath: "",
|
||||
webhookSecret: "",
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteSource = () => {
|
||||
if (selectedSource) {
|
||||
setSources((prev) => prev.filter((s) => s.id !== selectedSource.id))
|
||||
setDeleteDialogOpen(false)
|
||||
setSelectedSource(null)
|
||||
}
|
||||
}
|
||||
|
||||
const renderConfigForm = () => {
|
||||
switch (newSource.type) {
|
||||
case "restapi":
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>API端点URL</Label>
|
||||
<Input
|
||||
placeholder="https://api.example.com/v1/users"
|
||||
value={newSource.endpoint}
|
||||
onChange={(e) => setNewSource({ ...newSource, endpoint: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>请求方法</Label>
|
||||
<Select value={newSource.method} onValueChange={(v) => setNewSource({ ...newSource, method: v })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
<SelectItem value="PUT">PUT</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>认证方式</Label>
|
||||
<Select value={newSource.authType} onValueChange={(v) => setNewSource({ ...newSource, authType: v })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">无认证</SelectItem>
|
||||
<SelectItem value="bearer">Bearer Token</SelectItem>
|
||||
<SelectItem value="apikey">API Key</SelectItem>
|
||||
<SelectItem value="basic">Basic Auth</SelectItem>
|
||||
<SelectItem value="oauth2">OAuth 2.0</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{newSource.authType !== "none" && (
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
{newSource.authType === "bearer" ? "Token" : newSource.authType === "apikey" ? "API Key" : "凭证"}
|
||||
</Label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="输入认证凭证"
|
||||
value={newSource.authToken}
|
||||
onChange={(e) => setNewSource({ ...newSource, authToken: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<Label>自定义Headers (JSON格式,可选)</Label>
|
||||
<Textarea
|
||||
placeholder='{"Content-Type": "application/json"}'
|
||||
value={newSource.headers}
|
||||
onChange={(e) => setNewSource({ ...newSource, headers: e.target.value })}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case "webhook":
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-blue-50 rounded-lg">
|
||||
<p className="text-sm text-blue-700">
|
||||
Webhook模式下,外部系统将主动推送数据到您的接收端点。配置完成后,请将生成的Webhook URL提供给数据推送方。
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Webhook路径</Label>
|
||||
<div className="flex gap-2">
|
||||
<span className="flex items-center px-3 bg-gray-100 rounded-l-md text-sm text-gray-600">
|
||||
https://api.yourplatform.com
|
||||
</span>
|
||||
<Input
|
||||
placeholder="/webhook/your-source"
|
||||
value={newSource.webhookPath}
|
||||
onChange={(e) => setNewSource({ ...newSource, webhookPath: e.target.value })}
|
||||
className="rounded-l-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>签名密钥 (用于验证请求来源)</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="自动生成或手动输入"
|
||||
value={newSource.webhookSecret}
|
||||
onChange={(e) => setNewSource({ ...newSource, webhookSecret: e.target.value })}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setNewSource({ ...newSource, webhookSecret: Math.random().toString(36).slice(2) })}
|
||||
>
|
||||
生成
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>数据格式</Label>
|
||||
<Select defaultValue="json">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="json">JSON</SelectItem>
|
||||
<SelectItem value="xml">XML</SelectItem>
|
||||
<SelectItem value="form">Form Data</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>主机地址</Label>
|
||||
<Input
|
||||
placeholder="localhost"
|
||||
value={newSource.host}
|
||||
onChange={(e) => setNewSource({ ...newSource, host: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>端口</Label>
|
||||
<Input
|
||||
placeholder="3306"
|
||||
value={newSource.port}
|
||||
onChange={(e) => setNewSource({ ...newSource, port: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{newSource.type === "kafka" ? "Topic" : "数据库名称"}</Label>
|
||||
<Input
|
||||
placeholder={newSource.type === "kafka" ? "topic_name" : "database_name"}
|
||||
value={newSource.database}
|
||||
onChange={(e) => setNewSource({ ...newSource, database: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
{newSource.type !== "kafka" && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>用户名</Label>
|
||||
<Input placeholder="root" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>密码</Label>
|
||||
<Input type="password" placeholder="••••••••" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">数据源管理</h1>
|
||||
<p className="text-gray-500 mt-1">配置与管理多源数据接入,支持数据库、API、消息队列等</p>
|
||||
</div>
|
||||
<Dialog
|
||||
open={isAddDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setIsAddDialogOpen(open)
|
||||
if (!open) setAddStep(1)
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-gradient-to-r from-blue-500 to-purple-500 text-white">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
新增数据源
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>新增数据源</DialogTitle>
|
||||
<DialogDescription>{addStep === 1 ? "选择数据源类型并填写基本信息" : "配置连接参数"}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{addStep === 1 ? (
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>数据源名称</Label>
|
||||
<Input
|
||||
placeholder="例如:用户主库"
|
||||
value={newSource.name}
|
||||
onChange={(e) => setNewSource({ ...newSource, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>数据源类型</Label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[
|
||||
{ type: "mysql", label: "MySQL", icon: Database, desc: "关系型数据库" },
|
||||
{ type: "postgresql", label: "PostgreSQL", icon: Database, desc: "关系型数据库" },
|
||||
{ type: "restapi", label: "REST API", icon: Globe, desc: "HTTP接口拉取" },
|
||||
{ type: "webhook", label: "Webhook", icon: Link2, desc: "数据推送接收" },
|
||||
{ type: "kafka", label: "Kafka", icon: Server, desc: "消息队列" },
|
||||
{ type: "sftp", label: "SFTP", icon: FileText, desc: "文件传输" },
|
||||
].map((item) => (
|
||||
<button
|
||||
key={item.type}
|
||||
onClick={() => setNewSource({ ...newSource, type: item.type })}
|
||||
className={`p-3 rounded-lg border-2 text-left transition-all ${
|
||||
newSource.type === item.type
|
||||
? "border-blue-500 bg-blue-50"
|
||||
: "border-gray-200 hover:border-gray-300"
|
||||
}`}
|
||||
>
|
||||
<item.icon
|
||||
className={`w-5 h-5 mb-1 ${newSource.type === item.type ? "text-blue-500" : "text-gray-400"}`}
|
||||
/>
|
||||
<div className="font-medium text-sm">{item.label}</div>
|
||||
<div className="text-xs text-gray-500">{item.desc}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-4">{renderConfigForm()}</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
{addStep === 2 && (
|
||||
<Button variant="outline" onClick={() => setAddStep(1)}>
|
||||
上一步
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsAddDialogOpen(false)
|
||||
setAddStep(1)
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
{addStep === 1 ? (
|
||||
<Button onClick={() => setAddStep(2)} disabled={!newSource.name}>
|
||||
下一步
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleAddSource}>测试连接并添加</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Stats - 添加API数据源统计 */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4">
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<Database className="w-4 h-4" />
|
||||
总数据源
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.total}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<Globe className="w-4 h-4 text-green-500" />
|
||||
API接入
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-green-600">{stats.apiCount}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
运行中
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-green-600">{stats.running}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<AlertTriangle className="w-4 h-4 text-red-500" />
|
||||
异常
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-red-600">{stats.error}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<FileText className="w-4 h-4" />
|
||||
总记录数
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{formatNumber(stats.totalRecords)}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters - 更新筛选选项 */}
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
placeholder="搜索数据源..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 bg-white/60 backdrop-blur-sm"
|
||||
/>
|
||||
</div>
|
||||
<Select value={filterType} onValueChange={setFilterType}>
|
||||
<SelectTrigger className="w-full md:w-48 bg-white/60 backdrop-blur-sm">
|
||||
<SelectValue placeholder="筛选类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部类型</SelectItem>
|
||||
<SelectItem value="mysql">MySQL</SelectItem>
|
||||
<SelectItem value="postgresql">PostgreSQL</SelectItem>
|
||||
<SelectItem value="restapi">REST API</SelectItem>
|
||||
<SelectItem value="webhook">Webhook</SelectItem>
|
||||
<SelectItem value="sftp">SFTP文件</SelectItem>
|
||||
<SelectItem value="kafka">Kafka</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Data Source List */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{filteredSources.map((source) => {
|
||||
const TypeIcon = TYPE_ICONS[source.type]
|
||||
const StatusConfig = STATUS_CONFIG[source.status]
|
||||
const StatusIcon = StatusConfig.icon
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={source.id}
|
||||
className="border-none shadow-sm bg-white/60 backdrop-blur-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-xl ${TYPE_COLORS[source.type]} flex items-center justify-center`}
|
||||
>
|
||||
<TypeIcon className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900">{source.name}</h3>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{TYPE_LABELS[source.type]}
|
||||
</Badge>
|
||||
<Badge className={`text-xs ${StatusConfig.color}`}>
|
||||
<StatusIcon className={`w-3 h-3 mr-1 ${source.status === "syncing" ? "animate-spin" : ""}`} />
|
||||
{StatusConfig.label}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleToggleStatus(source.id)}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
{source.status === "running" ? (
|
||||
<Pause className="w-4 h-4 text-gray-500" />
|
||||
) : (
|
||||
<Play className="w-4 h-4 text-green-500" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => {
|
||||
setSelectedSource(source)
|
||||
setSettingsDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
<Settings className="w-4 h-4 text-gray-500" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => {
|
||||
setSelectedSource(source)
|
||||
setDeleteDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500">同步模式:</span>
|
||||
<span className="text-gray-900 ml-1">
|
||||
{source.syncMode === "full" ? "全量" : source.syncMode === "realtime" ? "实时" : "增量"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">频率:</span>
|
||||
<span className="text-gray-900 ml-1">{source.frequency}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">记录数:</span>
|
||||
<span className="text-gray-900 ml-1 font-medium">{formatNumber(source.records)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">错误:</span>
|
||||
<span className={`ml-1 font-medium ${source.errorCount > 0 ? "text-red-600" : "text-green-600"}`}>
|
||||
{source.errorCount}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(source.type === "restapi" || source.type === "webhook") && (
|
||||
<div className="mt-3 p-2 bg-gray-50 rounded-lg">
|
||||
<code className="text-xs text-gray-600">
|
||||
{source.type === "restapi"
|
||||
? `${source.config.method} ${source.config.endpoint}`
|
||||
: source.config.webhookUrl}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-gray-100">
|
||||
<div className="flex items-center justify-between text-xs text-gray-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
最后同步: {source.lastSync}
|
||||
</span>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="h-auto p-0 text-blue-500"
|
||||
onClick={() => {
|
||||
setSelectedSource(source)
|
||||
setLogsDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
查看日志
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Settings Dialog */}
|
||||
<Dialog open={settingsDialogOpen} onOpenChange={setSettingsDialogOpen}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>数据源设置 - {selectedSource?.name}</DialogTitle>
|
||||
<DialogDescription>配置数据源连接和同步参数</DialogDescription>
|
||||
</DialogHeader>
|
||||
{selectedSource && (
|
||||
<Tabs defaultValue="connection" className="py-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="connection">连接配置</TabsTrigger>
|
||||
<TabsTrigger value="sync">同步设置</TabsTrigger>
|
||||
<TabsTrigger value="mapping">字段映射</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="connection" className="space-y-4 mt-4">
|
||||
{selectedSource.type === "restapi" ? (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label>API端点</Label>
|
||||
<Input defaultValue={selectedSource.config.endpoint} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>请求方法</Label>
|
||||
<Select defaultValue={selectedSource.config.method}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>认证方式</Label>
|
||||
<Input defaultValue={selectedSource.config.auth} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : selectedSource.type === "webhook" ? (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label>Webhook URL</Label>
|
||||
<Input defaultValue={selectedSource.config.webhookUrl} readOnly />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>签名密钥</Label>
|
||||
<Input type="password" defaultValue={selectedSource.config.secret} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>主机地址</Label>
|
||||
<Input defaultValue={selectedSource.config.host} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>端口</Label>
|
||||
<Input defaultValue={selectedSource.config.port} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>数据库/Topic</Label>
|
||||
<Input defaultValue={selectedSource.config.database || selectedSource.config.topic} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="sync" className="space-y-4 mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label>同步模式</Label>
|
||||
<Select defaultValue={selectedSource.syncMode}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="full">全量同步</SelectItem>
|
||||
<SelectItem value="incremental">增量同步</SelectItem>
|
||||
<SelectItem value="realtime">实时同步</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>同步频率</Label>
|
||||
<Select defaultValue="5min">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="realtime">实时</SelectItem>
|
||||
<SelectItem value="1min">每分钟</SelectItem>
|
||||
<SelectItem value="5min">每5分钟</SelectItem>
|
||||
<SelectItem value="30min">每30分钟</SelectItem>
|
||||
<SelectItem value="1hour">每小时</SelectItem>
|
||||
<SelectItem value="1day">每天</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="mapping" className="mt-4">
|
||||
<div className="p-4 bg-gray-50 rounded-lg text-center text-gray-500">
|
||||
字段映射配置将在连接成功后自动加载
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setSettingsDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={() => setSettingsDialogOpen(false)}>保存</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Logs Dialog */}
|
||||
<Dialog open={logsDialogOpen} onOpenChange={setLogsDialogOpen}>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>同步日志 - {selectedSource?.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
<div className="space-y-2">
|
||||
{syncLogs.map((log) => (
|
||||
<div
|
||||
key={log.id}
|
||||
className={`flex items-start gap-3 p-3 rounded-lg text-sm ${
|
||||
log.level === "success" ? "bg-green-50" : log.level === "warning" ? "bg-yellow-50" : "bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<span className="text-gray-400 font-mono text-xs">{log.time}</span>
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded text-xs ${
|
||||
log.level === "success"
|
||||
? "bg-green-100 text-green-700"
|
||||
: log.level === "warning"
|
||||
? "bg-yellow-100 text-yellow-700"
|
||||
: "bg-gray-100 text-gray-700"
|
||||
}`}
|
||||
>
|
||||
{log.level.toUpperCase()}
|
||||
</span>
|
||||
<span className="flex-1">{log.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setLogsDialogOpen(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>确认删除</DialogTitle>
|
||||
<DialogDescription>确定要删除数据源 "{selectedSource?.name}" 吗?此操作不可恢复。</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleDeleteSource}>
|
||||
确认删除
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
314
app/data-governance/tasks/page.tsx
Normal file
314
app/data-governance/tasks/page.tsx
Normal file
@@ -0,0 +1,314 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Clock, Play, Pause, CheckCircle, XCircle, RefreshCw, Calendar, Settings, MoreHorizontal } from "lucide-react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { ScheduleCalendarDialog } from "@/components/dialogs/schedule-calendar-dialog"
|
||||
import { TaskDetailDialog } from "@/components/dialogs/task-detail-dialog"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
|
||||
interface ScheduledTask {
|
||||
id: string
|
||||
name: string
|
||||
type: "sync" | "cleaning" | "tagging" | "model"
|
||||
schedule: string
|
||||
status: "running" | "completed" | "failed" | "waiting" | "paused"
|
||||
progress: number
|
||||
nextRun: string
|
||||
lastRun: string
|
||||
duration: string
|
||||
logs: string[]
|
||||
}
|
||||
|
||||
const MOCK_TASKS: ScheduledTask[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "用户数据同步",
|
||||
type: "sync",
|
||||
schedule: "*/5 * * * *",
|
||||
status: "running",
|
||||
progress: 67,
|
||||
nextRun: "-",
|
||||
lastRun: "2025-12-12 14:30:00",
|
||||
duration: "2分30秒",
|
||||
logs: ["正在同步数据...", "已处理 67%"],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "交易数据清洗",
|
||||
type: "cleaning",
|
||||
schedule: "0 */1 * * *",
|
||||
status: "completed",
|
||||
progress: 100,
|
||||
nextRun: "2025-12-12 15:00:00",
|
||||
lastRun: "2025-12-12 14:00:00",
|
||||
duration: "5分12秒",
|
||||
logs: ["清洗完成,处理 89234567 条记录"],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "用户标签计算",
|
||||
type: "tagging",
|
||||
schedule: "0 2 * * *",
|
||||
status: "waiting",
|
||||
progress: 0,
|
||||
nextRun: "2025-12-13 02:00:00",
|
||||
lastRun: "2025-12-12 02:00:00",
|
||||
duration: "15分38秒",
|
||||
logs: ["等待执行"],
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "CLV模型训练",
|
||||
type: "model",
|
||||
schedule: "0 3 * * 0",
|
||||
status: "failed",
|
||||
progress: 45,
|
||||
nextRun: "2025-12-15 03:00:00",
|
||||
lastRun: "2025-12-08 03:00:00",
|
||||
duration: "-",
|
||||
logs: ["错误:内存不足", "任务中断于 45%"],
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "行为数据同步",
|
||||
type: "sync",
|
||||
schedule: "实时",
|
||||
status: "running",
|
||||
progress: 100,
|
||||
nextRun: "-",
|
||||
lastRun: "2025-12-12 14:35:12",
|
||||
duration: "持续运行",
|
||||
logs: ["Kafka消费正常", "当前lag: 125"],
|
||||
},
|
||||
]
|
||||
|
||||
const TYPE_CONFIG = {
|
||||
sync: { label: "数据同步", color: "bg-blue-100 text-blue-700" },
|
||||
cleaning: { label: "数据清洗", color: "bg-green-100 text-green-700" },
|
||||
tagging: { label: "标签计算", color: "bg-purple-100 text-purple-700" },
|
||||
model: { label: "模型训练", color: "bg-orange-100 text-orange-700" },
|
||||
}
|
||||
|
||||
const STATUS_CONFIG = {
|
||||
running: { label: "运行中", color: "bg-blue-100 text-blue-700", icon: RefreshCw },
|
||||
completed: { label: "已完成", color: "bg-green-100 text-green-700", icon: CheckCircle },
|
||||
failed: { label: "失败", color: "bg-red-100 text-red-700", icon: XCircle },
|
||||
waiting: { label: "等待中", color: "bg-gray-100 text-gray-700", icon: Clock },
|
||||
paused: { label: "已暂停", color: "bg-yellow-100 text-yellow-700", icon: Pause },
|
||||
}
|
||||
|
||||
export default function TaskSchedulePage() {
|
||||
const [tasks, setTasks] = useState<ScheduledTask[]>(MOCK_TASKS)
|
||||
const [currentTime, setCurrentTime] = useState(new Date())
|
||||
const [showCalendarDialog, setShowCalendarDialog] = useState(false)
|
||||
const [showDetailDialog, setShowDetailDialog] = useState(false)
|
||||
const [selectedTask, setSelectedTask] = useState<ScheduledTask | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentTime(new Date())
|
||||
// 模拟进度更新
|
||||
setTasks((prev) =>
|
||||
prev.map((task) => {
|
||||
if (task.status === "running" && task.progress < 100) {
|
||||
return { ...task, progress: Math.min(task.progress + Math.random() * 2, 100) }
|
||||
}
|
||||
return task
|
||||
}),
|
||||
)
|
||||
}, 2000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const stats = {
|
||||
total: tasks.length,
|
||||
running: tasks.filter((t) => t.status === "running").length,
|
||||
completed: tasks.filter((t) => t.status === "completed").length,
|
||||
failed: tasks.filter((t) => t.status === "failed").length,
|
||||
}
|
||||
|
||||
const handleViewDetail = (task: ScheduledTask) => {
|
||||
setSelectedTask(task)
|
||||
setShowDetailDialog(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">任务调度</h1>
|
||||
<p className="text-gray-500 mt-1">管理与监控数据处理任务</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="outline" className="px-3 py-1">
|
||||
<Clock className="w-3 h-3 mr-1" />
|
||||
{currentTime.toLocaleTimeString()}
|
||||
</Badge>
|
||||
<Button variant="outline" onClick={() => setShowCalendarDialog(true)}>
|
||||
<Calendar className="w-4 h-4 mr-2" />
|
||||
调度日历
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<Settings className="w-4 h-4" />
|
||||
总任务数
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.total}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<RefreshCw className="w-4 h-4 text-blue-500" />
|
||||
运行中
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-blue-600">{stats.running}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
已完成
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-green-600">{stats.completed}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<XCircle className="w-4 h-4 text-red-500" />
|
||||
失败
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-red-600">{stats.failed}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Task List */}
|
||||
<div className="space-y-4">
|
||||
{tasks.map((task) => {
|
||||
const StatusIcon = STATUS_CONFIG[task.status].icon
|
||||
|
||||
return (
|
||||
<Card key={task.id} className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="font-semibold text-gray-900">{task.name}</h3>
|
||||
<Badge className={TYPE_CONFIG[task.type].color}>{TYPE_CONFIG[task.type].label}</Badge>
|
||||
<Badge className={STATUS_CONFIG[task.status].color}>
|
||||
<StatusIcon className={`w-3 h-3 mr-1 ${task.status === "running" ? "animate-spin" : ""}`} />
|
||||
{STATUS_CONFIG[task.status].label}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{task.status === "running" && (
|
||||
<div className="mb-3">
|
||||
<div className="flex items-center justify-between text-sm text-gray-500 mb-1">
|
||||
<span>执行进度</span>
|
||||
<span>{Math.round(task.progress)}%</span>
|
||||
</div>
|
||||
<Progress value={task.progress} className="h-2" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500">调度规则:</span>
|
||||
<code className="ml-1 text-xs bg-gray-100 px-1 rounded">{task.schedule}</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">下次执行:</span>
|
||||
<span className="ml-1 text-gray-900">{task.nextRun}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">上次执行:</span>
|
||||
<span className="ml-1 text-gray-900">{task.lastRun}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">耗时:</span>
|
||||
<span className="ml-1 text-gray-900">{task.duration}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{task.logs.length > 0 && (
|
||||
<div className="mt-3 p-2 bg-gray-50 rounded-lg">
|
||||
<div className="text-xs text-gray-500 mb-1">最新日志:</div>
|
||||
{task.logs.slice(-2).map((log, i) => (
|
||||
<div key={i} className="text-xs text-gray-600">
|
||||
{log}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
{task.status === "running" ? (
|
||||
<Button variant="outline" size="sm">
|
||||
<Pause className="w-4 h-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="outline" size="sm">
|
||||
<Play className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleViewDetail(task)}>查看详情</DropdownMenuItem>
|
||||
<DropdownMenuItem>编辑任务</DropdownMenuItem>
|
||||
<DropdownMenuItem>查看日志</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-red-600">删除任务</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Schedule Calendar Dialog */}
|
||||
<ScheduleCalendarDialog open={showCalendarDialog} onOpenChange={setShowCalendarDialog} />
|
||||
|
||||
<TaskDetailDialog
|
||||
open={showDetailDialog}
|
||||
onOpenChange={setShowDetailDialog}
|
||||
task={
|
||||
selectedTask
|
||||
? {
|
||||
id: selectedTask.id,
|
||||
name: selectedTask.name,
|
||||
type: selectedTask.type,
|
||||
status: selectedTask.status,
|
||||
progress: selectedTask.progress,
|
||||
schedule: selectedTask.schedule,
|
||||
lastRun: selectedTask.lastRun,
|
||||
nextRun: selectedTask.nextRun,
|
||||
duration: selectedTask.duration,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
270
app/data-ingestion/lineage/page.tsx
Normal file
270
app/data-ingestion/lineage/page.tsx
Normal file
@@ -0,0 +1,270 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
GitBranch,
|
||||
Search,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
Maximize2,
|
||||
Database,
|
||||
Table,
|
||||
ArrowRight,
|
||||
Filter,
|
||||
Download,
|
||||
} from "lucide-react"
|
||||
|
||||
// 第二部分:数据接入 - 数据血缘
|
||||
export default function DataLineagePage() {
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [selectedNode, setSelectedNode] = useState<string | null>(null)
|
||||
|
||||
// 数据血缘节点
|
||||
const lineageNodes = [
|
||||
// 数据源层
|
||||
{ id: "source-1", type: "source", name: "存客宝-MySQL", level: 0, x: 50, y: 100 },
|
||||
{ id: "source-2", type: "source", name: "触客宝-MySQL", level: 0, x: 50, y: 200 },
|
||||
{ id: "source-3", type: "source", name: "数智员工-API", level: 0, x: 50, y: 300 },
|
||||
// 原始表层
|
||||
{ id: "raw-1", type: "table", name: "raw_users", level: 1, x: 250, y: 100 },
|
||||
{ id: "raw-2", type: "table", name: "raw_transactions", level: 1, x: 250, y: 200 },
|
||||
{ id: "raw-3", type: "table", name: "raw_behaviors", level: 1, x: 250, y: 300 },
|
||||
// 清洗层
|
||||
{ id: "clean-1", type: "table", name: "clean_users", level: 2, x: 450, y: 150 },
|
||||
{ id: "clean-2", type: "table", name: "clean_transactions", level: 2, x: 450, y: 250 },
|
||||
// 标签层
|
||||
{ id: "tag-1", type: "table", name: "user_tags", level: 3, x: 650, y: 150 },
|
||||
{ id: "tag-2", type: "table", name: "user_portraits", level: 3, x: 650, y: 250 },
|
||||
// 输出层
|
||||
{ id: "output-1", type: "output", name: "流量包-高价值用户", level: 4, x: 850, y: 150 },
|
||||
{ id: "output-2", type: "output", name: "API-用户画像", level: 4, x: 850, y: 250 },
|
||||
]
|
||||
|
||||
// 血缘关系
|
||||
const lineageEdges = [
|
||||
{ from: "source-1", to: "raw-1" },
|
||||
{ from: "source-1", to: "raw-2" },
|
||||
{ from: "source-2", to: "raw-3" },
|
||||
{ from: "source-3", to: "raw-1" },
|
||||
{ from: "raw-1", to: "clean-1" },
|
||||
{ from: "raw-2", to: "clean-2" },
|
||||
{ from: "raw-3", to: "clean-1" },
|
||||
{ from: "clean-1", to: "tag-1" },
|
||||
{ from: "clean-2", to: "tag-1" },
|
||||
{ from: "clean-1", to: "tag-2" },
|
||||
{ from: "tag-1", to: "output-1" },
|
||||
{ from: "tag-2", to: "output-2" },
|
||||
]
|
||||
|
||||
const getNodeColor = (type: string) => {
|
||||
switch (type) {
|
||||
case "source":
|
||||
return "bg-blue-100 border-blue-300 text-blue-700"
|
||||
case "table":
|
||||
return "bg-green-100 border-green-300 text-green-700"
|
||||
case "output":
|
||||
return "bg-purple-100 border-purple-300 text-purple-700"
|
||||
default:
|
||||
return "bg-gray-100 border-gray-300 text-gray-700"
|
||||
}
|
||||
}
|
||||
|
||||
const getNodeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "source":
|
||||
return <Database className="h-4 w-4" />
|
||||
case "table":
|
||||
return <Table className="h-4 w-4" />
|
||||
case "output":
|
||||
return <ArrowRight className="h-4 w-4" />
|
||||
default:
|
||||
return <GitBranch className="h-4 w-4" />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
|
||||
<div className="p-6 space-y-6">
|
||||
{/* 顶部标题 */}
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">数据血缘</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">可视化展示数据从源头到输出的完整流转路径</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索表名或数据源..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 w-64 bg-white"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline">
|
||||
<Filter className="h-4 w-4 mr-2" />
|
||||
筛选
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
导出
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 图例 */}
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded bg-blue-100 border border-blue-300" />
|
||||
<span className="text-sm text-gray-600">数据源</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded bg-green-100 border border-green-300" />
|
||||
<span className="text-sm text-gray-600">数据表</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded bg-purple-100 border border-purple-300" />
|
||||
<span className="text-sm text-gray-600">输出服务</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 血缘图 */}
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base font-semibold">血缘关系图</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="icon">
|
||||
<ZoomOut className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<ZoomIn className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="relative h-[500px] bg-gray-50 rounded-lg overflow-hidden">
|
||||
{/* SVG连线 */}
|
||||
<svg className="absolute inset-0 w-full h-full pointer-events-none">
|
||||
{lineageEdges.map((edge, index) => {
|
||||
const fromNode = lineageNodes.find((n) => n.id === edge.from)
|
||||
const toNode = lineageNodes.find((n) => n.id === edge.to)
|
||||
if (!fromNode || !toNode) return null
|
||||
return (
|
||||
<line
|
||||
key={index}
|
||||
x1={fromNode.x + 80}
|
||||
y1={fromNode.y + 20}
|
||||
x2={toNode.x}
|
||||
y2={toNode.y + 20}
|
||||
stroke="#94a3b8"
|
||||
strokeWidth="2"
|
||||
markerEnd="url(#arrowhead)"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<defs>
|
||||
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="#94a3b8" />
|
||||
</marker>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
{/* 节点 */}
|
||||
{lineageNodes.map((node) => (
|
||||
<div
|
||||
key={node.id}
|
||||
className={`absolute px-3 py-2 rounded-lg border-2 cursor-pointer transition-all hover:shadow-md ${getNodeColor(node.type)} ${
|
||||
selectedNode === node.id ? "ring-2 ring-blue-500 ring-offset-2" : ""
|
||||
}`}
|
||||
style={{ left: node.x, top: node.y }}
|
||||
onClick={() => setSelectedNode(node.id)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{getNodeIcon(node.type)}
|
||||
<span className="text-sm font-medium whitespace-nowrap">{node.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 层级标签 */}
|
||||
{[
|
||||
{ label: "数据源", x: 50 },
|
||||
{ label: "原始层", x: 250 },
|
||||
{ label: "清洗层", x: 450 },
|
||||
{ label: "标签层", x: 650 },
|
||||
{ label: "输出层", x: 850 },
|
||||
].map((level, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute top-2 text-xs text-gray-400 font-medium"
|
||||
style={{ left: level.x }}
|
||||
>
|
||||
{level.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 节点详情 */}
|
||||
{selectedNode && (
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base font-semibold">
|
||||
节点详情 - {lineageNodes.find((n) => n.id === selectedNode)?.name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-500 mb-2">上游依赖</h4>
|
||||
<div className="space-y-2">
|
||||
{lineageEdges
|
||||
.filter((e) => e.to === selectedNode)
|
||||
.map((edge, index) => (
|
||||
<Badge key={index} variant="secondary" className="mr-2">
|
||||
{lineageNodes.find((n) => n.id === edge.from)?.name}
|
||||
</Badge>
|
||||
))}
|
||||
{lineageEdges.filter((e) => e.to === selectedNode).length === 0 && (
|
||||
<span className="text-sm text-gray-400">无上游依赖</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-500 mb-2">下游影响</h4>
|
||||
<div className="space-y-2">
|
||||
{lineageEdges
|
||||
.filter((e) => e.from === selectedNode)
|
||||
.map((edge, index) => (
|
||||
<Badge key={index} variant="secondary" className="mr-2">
|
||||
{lineageNodes.find((n) => n.id === edge.to)?.name}
|
||||
</Badge>
|
||||
))}
|
||||
{lineageEdges.filter((e) => e.from === selectedNode).length === 0 && (
|
||||
<span className="text-sm text-gray-400">无下游影响</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-500 mb-2">更新信息</h4>
|
||||
<p className="text-sm text-gray-700">最后更新:2分钟前</p>
|
||||
<p className="text-sm text-gray-700">记录数:1,234,567</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
619
app/data-ingestion/sources/page.tsx
Normal file
619
app/data-ingestion/sources/page.tsx
Normal file
@@ -0,0 +1,619 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import {
|
||||
Database,
|
||||
Plus,
|
||||
Search,
|
||||
RefreshCw,
|
||||
Settings,
|
||||
Trash2,
|
||||
PlayCircle,
|
||||
PauseCircle,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Clock,
|
||||
FileText,
|
||||
Server,
|
||||
Globe,
|
||||
Webhook,
|
||||
MoreVertical,
|
||||
Eye,
|
||||
Edit,
|
||||
Activity,
|
||||
} from "lucide-react"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
|
||||
// 第二部分:数据接入 - 数据源管理
|
||||
export default function DataSourcesPage() {
|
||||
const [activeTab, setActiveTab] = useState("all")
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [showAddDialog, setShowAddDialog] = useState(false)
|
||||
const [showSettingsDialog, setShowSettingsDialog] = useState(false)
|
||||
const [showLogsDialog, setShowLogsDialog] = useState(false)
|
||||
const [selectedSource, setSelectedSource] = useState<any>(null)
|
||||
const [newSourceType, setNewSourceType] = useState("")
|
||||
|
||||
// 数据源列表
|
||||
const dataSources = [
|
||||
{
|
||||
id: "1",
|
||||
name: "存客宝-MySQL主库",
|
||||
type: "mysql",
|
||||
status: "connected",
|
||||
host: "10.88.182.62:3305",
|
||||
database: "cunke_prod",
|
||||
lastSync: "2分钟前",
|
||||
recordCount: 12584567,
|
||||
syncFrequency: "实时",
|
||||
tables: 45,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "触客宝-行为数据",
|
||||
type: "mysql",
|
||||
status: "connected",
|
||||
host: "10.88.182.63:3306",
|
||||
database: "chuke_behavior",
|
||||
lastSync: "5分钟前",
|
||||
recordCount: 89234156,
|
||||
syncFrequency: "5分钟",
|
||||
tables: 28,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "数智员工-API接口",
|
||||
type: "api",
|
||||
status: "connected",
|
||||
endpoint: "https://api.shuzhi.com/v1",
|
||||
lastSync: "1分钟前",
|
||||
recordCount: 4567890,
|
||||
syncFrequency: "实时",
|
||||
apiCalls: 125680,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "外部征信数据",
|
||||
type: "api",
|
||||
status: "warning",
|
||||
endpoint: "https://credit.external.com/api",
|
||||
lastSync: "30分钟前",
|
||||
recordCount: 234567,
|
||||
syncFrequency: "每小时",
|
||||
apiCalls: 8956,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "Webhook-实时事件",
|
||||
type: "webhook",
|
||||
status: "connected",
|
||||
webhookUrl: "/api/webhook/events",
|
||||
lastSync: "实时",
|
||||
recordCount: 567890,
|
||||
syncFrequency: "实时",
|
||||
events: 45678,
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "腾讯云MySQL",
|
||||
type: "mysql",
|
||||
status: "disconnected",
|
||||
host: "56b4c23f6853c.gz.cdb.myqcloud.com:14413",
|
||||
database: "analytics_db",
|
||||
lastSync: "2小时前",
|
||||
recordCount: 0,
|
||||
syncFrequency: "停止",
|
||||
tables: 0,
|
||||
},
|
||||
]
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case "connected":
|
||||
return <Badge className="bg-green-100 text-green-700">已连接</Badge>
|
||||
case "warning":
|
||||
return <Badge className="bg-yellow-100 text-yellow-700">警告</Badge>
|
||||
case "disconnected":
|
||||
return <Badge className="bg-red-100 text-red-700">已断开</Badge>
|
||||
default:
|
||||
return <Badge variant="secondary">未知</Badge>
|
||||
}
|
||||
}
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "mysql":
|
||||
return <Database className="h-5 w-5 text-blue-500" />
|
||||
case "api":
|
||||
return <Globe className="h-5 w-5 text-green-500" />
|
||||
case "webhook":
|
||||
return <Webhook className="h-5 w-5 text-purple-500" />
|
||||
default:
|
||||
return <Server className="h-5 w-5 text-gray-500" />
|
||||
}
|
||||
}
|
||||
|
||||
const filteredSources = dataSources.filter((source) => {
|
||||
if (activeTab !== "all" && source.type !== activeTab) return false
|
||||
if (searchQuery && !source.name.toLowerCase().includes(searchQuery.toLowerCase())) return false
|
||||
return true
|
||||
})
|
||||
|
||||
const handleOpenSettings = (source: any) => {
|
||||
setSelectedSource(source)
|
||||
setShowSettingsDialog(true)
|
||||
}
|
||||
|
||||
const handleOpenLogs = (source: any) => {
|
||||
setSelectedSource(source)
|
||||
setShowLogsDialog(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
|
||||
<div className="p-6 space-y-6">
|
||||
{/* 顶部标题 */}
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">数据源管理</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">统一管理所有数据接入源,支持MySQL、API、Webhook等多种接入方式</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button onClick={() => setShowAddDialog(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
添加数据源
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">数据源总数</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{dataSources.length}</p>
|
||||
</div>
|
||||
<Database className="h-8 w-8 text-blue-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">已连接</p>
|
||||
<p className="text-2xl font-bold text-green-600">
|
||||
{dataSources.filter((s) => s.status === "connected").length}
|
||||
</p>
|
||||
</div>
|
||||
<CheckCircle2 className="h-8 w-8 text-green-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">总记录数</p>
|
||||
<p className="text-2xl font-bold text-gray-900">107.2M</p>
|
||||
</div>
|
||||
<Activity className="h-8 w-8 text-purple-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">今日同步</p>
|
||||
<p className="text-2xl font-bold text-gray-900">2.3M</p>
|
||||
</div>
|
||||
<RefreshCw className="h-8 w-8 text-orange-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 搜索和筛选 */}
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索数据源名称..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 bg-white"
|
||||
/>
|
||||
</div>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="bg-white">
|
||||
<TabsTrigger value="all">全部</TabsTrigger>
|
||||
<TabsTrigger value="mysql">MySQL</TabsTrigger>
|
||||
<TabsTrigger value="api">API</TabsTrigger>
|
||||
<TabsTrigger value="webhook">Webhook</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* 数据源列表 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{filteredSources.map((source) => (
|
||||
<Card key={source.id} className="bg-white/80 backdrop-blur border-0 shadow-sm hover:shadow-md transition-shadow">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 rounded-xl bg-gray-50">
|
||||
{getTypeIcon(source.type)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-gray-900">{source.name}</h3>
|
||||
{getStatusBadge(source.status)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{source.type === "mysql" ? source.host : source.type === "api" ? source.endpoint : source.webhookUrl}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 mt-3 text-sm text-gray-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="h-4 w-4" />
|
||||
{source.lastSync}
|
||||
</span>
|
||||
<span>{source.recordCount.toLocaleString()} 条记录</span>
|
||||
<span>同步频率: {source.syncFrequency}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleOpenSettings(source)}>
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
设置
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleOpenLogs(source)}>
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
查看日志
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
立即同步
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
数据预览
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-red-600">
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 添加数据源弹窗 */}
|
||||
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>添加数据源</DialogTitle>
|
||||
<DialogDescription>选择数据源类型并配置连接参数</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-6 py-4">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{[
|
||||
{ type: "mysql", label: "MySQL数据库", icon: Database, desc: "连接MySQL/MariaDB数据库" },
|
||||
{ type: "api", label: "REST API", icon: Globe, desc: "通过API接口获取数据" },
|
||||
{ type: "webhook", label: "Webhook", icon: Webhook, desc: "接收实时推送数据" },
|
||||
].map((item) => (
|
||||
<button
|
||||
key={item.type}
|
||||
onClick={() => setNewSourceType(item.type)}
|
||||
className={`p-4 rounded-xl border-2 text-left transition-all ${
|
||||
newSourceType === item.type
|
||||
? "border-blue-500 bg-blue-50"
|
||||
: "border-gray-200 hover:border-gray-300"
|
||||
}`}
|
||||
>
|
||||
<item.icon className={`h-8 w-8 mb-2 ${newSourceType === item.type ? "text-blue-500" : "text-gray-400"}`} />
|
||||
<p className="font-medium text-gray-900">{item.label}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">{item.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{newSourceType === "mysql" && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>数据源名称</Label>
|
||||
<Input placeholder="例如:生产环境主库" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>主机地址</Label>
|
||||
<Input placeholder="例如:10.88.182.62:3306" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>数据库名</Label>
|
||||
<Input placeholder="数据库名称" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>用户名</Label>
|
||||
<Input placeholder="数据库用户名" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>密码</Label>
|
||||
<Input type="password" placeholder="数据库密码" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>同步频率</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择同步频率" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="realtime">实时</SelectItem>
|
||||
<SelectItem value="5min">每5分钟</SelectItem>
|
||||
<SelectItem value="15min">每15分钟</SelectItem>
|
||||
<SelectItem value="1hour">每小时</SelectItem>
|
||||
<SelectItem value="daily">每天</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{newSourceType === "api" && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>数据源名称</Label>
|
||||
<Input placeholder="例如:外部API接口" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>API端点</Label>
|
||||
<Input placeholder="https://api.example.com/v1" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>认证方式</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择认证方式" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="bearer">Bearer Token</SelectItem>
|
||||
<SelectItem value="apikey">API Key</SelectItem>
|
||||
<SelectItem value="basic">Basic Auth</SelectItem>
|
||||
<SelectItem value="oauth2">OAuth 2.0</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>认证凭证</Label>
|
||||
<Input type="password" placeholder="Token或密钥" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>请求方法</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择请求方法" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{newSourceType === "webhook" && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>数据源名称</Label>
|
||||
<Input placeholder="例如:实时事件推送" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Webhook路径</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-500">https://your-domain.com</span>
|
||||
<Input placeholder="/api/webhook/your-path" className="flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>签名密钥(可选)</Label>
|
||||
<Input placeholder="用于验证请求来源" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowAddDialog(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={() => setShowAddDialog(false)}>
|
||||
测试连接并保存
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 设置弹窗 */}
|
||||
<Dialog open={showSettingsDialog} onOpenChange={setShowSettingsDialog}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>数据源设置 - {selectedSource?.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Tabs defaultValue="connection" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="connection">连接配置</TabsTrigger>
|
||||
<TabsTrigger value="sync">同步设置</TabsTrigger>
|
||||
<TabsTrigger value="mapping">字段映射</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="connection" className="space-y-4 pt-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>主机地址</Label>
|
||||
<Input defaultValue={selectedSource?.host} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>数据库名</Label>
|
||||
<Input defaultValue={selectedSource?.database} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>用户名</Label>
|
||||
<Input defaultValue="root" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>密码</Label>
|
||||
<Input type="password" defaultValue="********" />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="sync" className="space-y-4 pt-4">
|
||||
<div className="space-y-2">
|
||||
<Label>同步频率</Label>
|
||||
<Select defaultValue="realtime">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="realtime">实时</SelectItem>
|
||||
<SelectItem value="5min">每5分钟</SelectItem>
|
||||
<SelectItem value="15min">每15分钟</SelectItem>
|
||||
<SelectItem value="1hour">每小时</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>增量字段</Label>
|
||||
<Input defaultValue="updated_at" placeholder="用于增量同步的时间字段" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="mapping" className="space-y-4 pt-4">
|
||||
<p className="text-sm text-gray-500">配置源表字段与目标字段的映射关系</p>
|
||||
<div className="border rounded-lg p-4 bg-gray-50">
|
||||
<p className="text-sm text-gray-600">字段映射配置将在下个版本支持</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowSettingsDialog(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={() => setShowSettingsDialog(false)}>
|
||||
保存设置
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 日志弹窗 */}
|
||||
<Dialog open={showLogsDialog} onOpenChange={setShowLogsDialog}>
|
||||
<DialogContent className="max-w-3xl max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>同步日志 - {selectedSource?.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{[
|
||||
{ time: "2026-01-31 14:32:45", level: "info", message: "同步任务开始执行" },
|
||||
{ time: "2026-01-31 14:32:46", level: "info", message: "连接数据库成功" },
|
||||
{ time: "2026-01-31 14:32:47", level: "info", message: "开始读取增量数据,起始时间: 2026-01-31 14:27:45" },
|
||||
{ time: "2026-01-31 14:32:50", level: "info", message: "读取到 1,256 条新记录" },
|
||||
{ time: "2026-01-31 14:32:52", level: "info", message: "数据写入目标表完成" },
|
||||
{ time: "2026-01-31 14:32:53", level: "success", message: "同步任务完成,耗时 8秒" },
|
||||
{ time: "2026-01-31 14:27:45", level: "info", message: "同步任务开始执行" },
|
||||
{ time: "2026-01-31 14:27:46", level: "info", message: "连接数据库成功" },
|
||||
{ time: "2026-01-31 14:27:48", level: "warning", message: "检测到 3 条数据格式异常,已跳过" },
|
||||
{ time: "2026-01-31 14:27:50", level: "info", message: "读取到 2,134 条新记录" },
|
||||
{ time: "2026-01-31 14:27:53", level: "success", message: "同步任务完成,耗时 8秒" },
|
||||
].map((log, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex items-start gap-3 p-3 rounded-lg text-sm ${
|
||||
log.level === "error"
|
||||
? "bg-red-50"
|
||||
: log.level === "warning"
|
||||
? "bg-yellow-50"
|
||||
: log.level === "success"
|
||||
? "bg-green-50"
|
||||
: "bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<span className="text-gray-400 font-mono text-xs whitespace-nowrap">{log.time}</span>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={`text-xs ${
|
||||
log.level === "error"
|
||||
? "bg-red-100 text-red-700"
|
||||
: log.level === "warning"
|
||||
? "bg-yellow-100 text-yellow-700"
|
||||
: log.level === "success"
|
||||
? "bg-green-100 text-green-700"
|
||||
: "bg-blue-100 text-blue-700"
|
||||
}`}
|
||||
>
|
||||
{log.level.toUpperCase()}
|
||||
</Badge>
|
||||
<span className="text-gray-700">{log.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowLogsDialog(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
导出日志
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
app/data-middle-platform/loading.tsx
Normal file
3
app/data-middle-platform/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
362
app/data-middle-platform/page.tsx
Normal file
362
app/data-middle-platform/page.tsx
Normal file
@@ -0,0 +1,362 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import {
|
||||
Database,
|
||||
RefreshCw,
|
||||
CheckCircle2,
|
||||
AlertTriangle,
|
||||
Search,
|
||||
FileText,
|
||||
Upload,
|
||||
Download,
|
||||
Filter,
|
||||
TrendingUp,
|
||||
Zap,
|
||||
Tag,
|
||||
} from "lucide-react"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
|
||||
export default function DataMiddlePlatform() {
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
|
||||
const dataSources = [
|
||||
{
|
||||
id: 1,
|
||||
name: "私域系统数据源",
|
||||
type: "微信生态",
|
||||
status: "active",
|
||||
lastSync: "2分钟前",
|
||||
users: 2850000000,
|
||||
syncRate: 98.5,
|
||||
dataSize: "850TB",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "电商平台数据",
|
||||
type: "淘宝/京东",
|
||||
status: "active",
|
||||
lastSync: "5分钟前",
|
||||
users: 680000000,
|
||||
syncRate: 95.2,
|
||||
dataSize: "210TB",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "表单收集数据",
|
||||
type: "第三方表单",
|
||||
status: "active",
|
||||
lastSync: "10分钟前",
|
||||
users: 425000000,
|
||||
syncRate: 92.8,
|
||||
dataSize: "125TB",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "碰撞补全数据",
|
||||
type: "数据碰撞",
|
||||
status: "syncing",
|
||||
lastSync: "正在同步",
|
||||
users: 280000000,
|
||||
syncRate: 76.3,
|
||||
dataSize: "95TB",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "项目API数据源",
|
||||
type: "外部API",
|
||||
status: "active",
|
||||
lastSync: "1分钟前",
|
||||
users: 50670000,
|
||||
syncRate: 99.1,
|
||||
dataSize: "18TB",
|
||||
},
|
||||
]
|
||||
|
||||
const cleaningTasks = [
|
||||
{ name: "手机号格式校验", total: 4285670000, processed: 4200000000, progress: 98.0, errors: 1250000 },
|
||||
{ name: "重复数据去除", total: 4285670000, processed: 3850000000, progress: 89.8, errors: 85000000 },
|
||||
{ name: "缺失字段补全", total: 4285670000, processed: 3250000000, progress: 75.8, errors: 125000000 },
|
||||
{ name: "异常值检测", total: 4285670000, processed: 4100000000, progress: 95.7, errors: 8500000 },
|
||||
]
|
||||
|
||||
const tagStats = { totalTags: 1258, systemTags: 485, customTags: 773, activeRules: 2850, todayTagged: 2845000 }
|
||||
|
||||
const qualityChecks = [
|
||||
{ type: "重复数据", count: 85420000, percentage: 2.0, severity: "warning" },
|
||||
{ type: "异常数据", count: 12850000, percentage: 0.3, severity: "error" },
|
||||
{ type: "缺失字段", count: 325680000, percentage: 7.6, severity: "warning" },
|
||||
{ type: "格式错误", count: 5280000, percentage: 0.12, severity: "error" },
|
||||
]
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(1)}B`
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">数据中台</h1>
|
||||
<p className="text-gray-500 mt-1">全平台数据源管理、清洗与质量监控</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline" className="bg-white/60 backdrop-blur-md">
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
上传数据
|
||||
</Button>
|
||||
<Button variant="outline" className="bg-white/60 backdrop-blur-md">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出报告
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="sources" className="space-y-6">
|
||||
<TabsList className="bg-white/60 backdrop-blur-md border border-gray-200 p-1 rounded-xl">
|
||||
<TabsTrigger
|
||||
value="sources"
|
||||
className="rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-sm"
|
||||
>
|
||||
数据源管理
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="cleaning"
|
||||
className="rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-sm"
|
||||
>
|
||||
数据清洗
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="tags" className="rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-sm">
|
||||
标签系统
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="quality"
|
||||
className="rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-sm"
|
||||
>
|
||||
质量检测
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="sources" className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索数据源..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 bg-white/60 backdrop-blur-md border-gray-200"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" className="bg-white/60 backdrop-blur-md">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
筛选
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{dataSources.map((source) => (
|
||||
<Card
|
||||
key={source.id}
|
||||
className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm hover:shadow-md transition-all"
|
||||
>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center">
|
||||
<Database className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900">{source.name}</h3>
|
||||
<p className="text-sm text-gray-500">{source.type}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
source.status === "active"
|
||||
? "bg-green-50 text-green-600 border-green-200"
|
||||
: "bg-yellow-50 text-yellow-600 border-yellow-200"
|
||||
}
|
||||
>
|
||||
{source.status === "active" ? (
|
||||
<>
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
运行中
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="w-3 h-3 mr-1 animate-spin" />
|
||||
同步中
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
<Button size="sm" variant="outline" className="bg-white">
|
||||
<RefreshCw className="w-3 h-3 mr-1" />
|
||||
同步
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-8 mb-4">
|
||||
<div>
|
||||
<div className="text-sm text-gray-500 mb-1">用户数量</div>
|
||||
<div className="text-xl font-bold text-gray-900">{formatNumber(source.users)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500 mb-1">同步率</div>
|
||||
<div className="text-xl font-bold text-green-600">{source.syncRate}%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500 mb-1">数据量</div>
|
||||
<div className="text-xl font-bold text-blue-600">{source.dataSize}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500 mb-1">最后同步</div>
|
||||
<div className="text-sm font-medium text-gray-900">{source.lastSync}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={source.syncRate} className="h-2 bg-gray-100" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="cleaning" className="space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{cleaningTasks.map((task, index) => (
|
||||
<Card key={index} className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Zap className="w-5 h-5 text-yellow-500" />
|
||||
{task.name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-500">进度</span>
|
||||
<span className="text-2xl font-bold text-green-600">{task.progress}%</span>
|
||||
</div>
|
||||
<Progress value={task.progress} className="h-2 bg-gray-100" />
|
||||
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-gray-100">
|
||||
<div>
|
||||
<div className="text-sm text-gray-500 mb-1">已处理</div>
|
||||
<div className="text-lg font-semibold text-gray-900">{formatNumber(task.processed)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500 mb-1">异常数据</div>
|
||||
<div className="text-lg font-semibold text-red-500">{formatNumber(task.errors)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="tags" className="space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
||||
{[
|
||||
{
|
||||
icon: Tag,
|
||||
label: "标签总数",
|
||||
value: tagStats.totalTags,
|
||||
color: "text-purple-600",
|
||||
bg: "bg-purple-100",
|
||||
},
|
||||
{
|
||||
icon: FileText,
|
||||
label: "系统标签",
|
||||
value: tagStats.systemTags,
|
||||
color: "text-blue-600",
|
||||
bg: "bg-blue-100",
|
||||
},
|
||||
{
|
||||
icon: Tag,
|
||||
label: "自定义标签",
|
||||
value: tagStats.customTags,
|
||||
color: "text-green-600",
|
||||
bg: "bg-green-100",
|
||||
},
|
||||
{
|
||||
icon: Zap,
|
||||
label: "活跃规则",
|
||||
value: tagStats.activeRules,
|
||||
color: "text-yellow-600",
|
||||
bg: "bg-yellow-100",
|
||||
},
|
||||
{
|
||||
icon: TrendingUp,
|
||||
label: "今日打标",
|
||||
value: formatNumber(tagStats.todayTagged),
|
||||
color: "text-orange-600",
|
||||
bg: "bg-orange-100",
|
||||
},
|
||||
].map((stat, index) => (
|
||||
<Card key={index} className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className={`w-10 h-10 rounded-full ${stat.bg} flex items-center justify-center mx-auto mb-3`}>
|
||||
<stat.icon className={`w-5 h-5 ${stat.color}`} />
|
||||
</div>
|
||||
<div className={`text-2xl font-bold ${stat.color} mb-1`}>{stat.value}</div>
|
||||
<div className="text-sm text-gray-500">{stat.label}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="quality" className="space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{qualityChecks.map((check, index) => (
|
||||
<Card key={index} className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<AlertTriangle
|
||||
className={`w-6 h-6 ${check.severity === "error" ? "text-red-500" : "text-yellow-500"}`}
|
||||
/>
|
||||
<h3 className="text-lg font-semibold text-gray-900">{check.type}</h3>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
check.severity === "error"
|
||||
? "bg-red-50 text-red-600 border-red-200"
|
||||
: "bg-yellow-50 text-yellow-600 border-yellow-200"
|
||||
}
|
||||
>
|
||||
{check.severity === "error" ? "严重" : "警告"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-500">问题数量</span>
|
||||
<span className="text-2xl font-bold text-gray-900">{formatNumber(check.count)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-500">占比</span>
|
||||
<span className="text-lg font-semibold text-gray-900">{check.percentage}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
159
app/globals.css
159
app/globals.css
@@ -4,14 +4,14 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--background: 240 10% 98%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card: 240 10% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover: 240 10% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 98%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
@@ -23,26 +23,26 @@
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 5.9% 10%;
|
||||
--radius: 0.5rem;
|
||||
--radius: 0.75rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--foreground: 240 5.9% 98%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--card-foreground: 240 5.9% 98%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--popover-foreground: 240 5.9% 98%;
|
||||
--primary: 240 5.9% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--secondary-foreground: 240 5.9% 98%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--accent-foreground: 240 5.9% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--destructive-foreground: 240 5.9% 98%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
@@ -53,7 +53,140 @@
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
html {
|
||||
@apply scroll-smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
@apply bg-gradient-to-br from-blue-50 via-white to-purple-50 text-foreground min-h-screen;
|
||||
background-attachment: fixed;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.dark body {
|
||||
@apply bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
background-attachment: scroll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.glass {
|
||||
@apply backdrop-blur-md border border-white/20;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
|
||||
}
|
||||
|
||||
.glass-light {
|
||||
@apply backdrop-blur-sm border border-white/30;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 4px 16px 0 rgba(31, 38, 135, 0.2);
|
||||
}
|
||||
|
||||
.glass-heavy {
|
||||
@apply backdrop-blur-xl border border-white/40;
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 16px 64px 0 rgba(31, 38, 135, 0.5);
|
||||
}
|
||||
|
||||
.glass-dark {
|
||||
@apply backdrop-blur-md border border-white/10;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.glass {
|
||||
@apply backdrop-blur-sm;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.glass-light {
|
||||
@apply backdrop-blur-sm;
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.glass-heavy {
|
||||
@apply backdrop-blur-md;
|
||||
background-color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
@apply glass rounded-2xl p-6 transition-all duration-300;
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.glass-card {
|
||||
@apply p-4 rounded-xl;
|
||||
}
|
||||
}
|
||||
|
||||
.glass-nav {
|
||||
@apply glass-light rounded-2xl transition-all duration-300;
|
||||
}
|
||||
|
||||
.glass-button {
|
||||
@apply glass-light rounded-xl px-4 py-2 transition-all duration-300;
|
||||
}
|
||||
|
||||
.glass-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.glass-button {
|
||||
@apply px-6 py-3 text-base;
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.glass-input {
|
||||
@apply glass-light rounded-xl px-4 py-2 transition-all duration-300;
|
||||
}
|
||||
|
||||
.glass-input:focus {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
@apply ring-2 ring-white/50;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.glass-input {
|
||||
@apply px-4 py-3 text-base;
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.safe-area-top {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
}
|
||||
|
||||
.safe-area-bottom {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.safe-area-left {
|
||||
padding-left: env(safe-area-inset-left);
|
||||
}
|
||||
|
||||
.safe-area-right {
|
||||
padding-right: env(safe-area-inset-right);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.overflow-scroll {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type React from "react"
|
||||
import type { Metadata } from "next"
|
||||
import { Inter } from "next/font/google"
|
||||
import ClientLayout from "./ClientLayout"
|
||||
import "./globals.css"
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] })
|
||||
|
||||
@@ -19,3 +18,6 @@ export default function RootLayout({
|
||||
}) {
|
||||
return <ClientLayout>{children}</ClientLayout>
|
||||
}
|
||||
|
||||
|
||||
import './globals.css'
|
||||
3
app/monitoring/alerts/loading.tsx
Normal file
3
app/monitoring/alerts/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
383
app/monitoring/alerts/page.tsx
Normal file
383
app/monitoring/alerts/page.tsx
Normal file
@@ -0,0 +1,383 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Bell,
|
||||
ArrowLeft,
|
||||
Plus,
|
||||
Search,
|
||||
Filter,
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Settings,
|
||||
Trash2,
|
||||
XCircle,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { CreateAlertRuleDialog } from "@/components/dialogs/create-alert-rule-dialog"
|
||||
import { AlertRuleSettingsDialog } from "@/components/dialogs/alert-rule-settings-dialog"
|
||||
|
||||
const alertRules = [
|
||||
{
|
||||
id: 1,
|
||||
name: "CPU使用率告警",
|
||||
condition: "CPU > 80%",
|
||||
duration: "5分钟",
|
||||
severity: "warning",
|
||||
status: "enabled",
|
||||
triggers: 3,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "内存使用率告警",
|
||||
condition: "Memory > 85%",
|
||||
duration: "5分钟",
|
||||
severity: "warning",
|
||||
status: "enabled",
|
||||
triggers: 1,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "API延迟告警",
|
||||
condition: "P99 > 200ms",
|
||||
duration: "3分钟",
|
||||
severity: "critical",
|
||||
status: "enabled",
|
||||
triggers: 5,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "磁盘空间告警",
|
||||
condition: "Disk > 90%",
|
||||
duration: "10分钟",
|
||||
severity: "critical",
|
||||
status: "enabled",
|
||||
triggers: 0,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "服务宕机告警",
|
||||
condition: "Service Down",
|
||||
duration: "1分钟",
|
||||
severity: "critical",
|
||||
status: "enabled",
|
||||
triggers: 0,
|
||||
},
|
||||
]
|
||||
|
||||
const alertHistory = [
|
||||
{
|
||||
id: 1,
|
||||
rule: "CPU使用率告警",
|
||||
message: "node-04 CPU使用率达到 85%",
|
||||
severity: "warning",
|
||||
status: "active",
|
||||
time: "10分钟前",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
rule: "磁盘空间告警",
|
||||
message: "磁盘使用率达到 80%,接近阈值",
|
||||
severity: "warning",
|
||||
status: "active",
|
||||
time: "2小时前",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
rule: "API延迟告警",
|
||||
message: "API网关P99延迟达到 250ms",
|
||||
severity: "critical",
|
||||
status: "resolved",
|
||||
time: "1小时前",
|
||||
resolvedAt: "45分钟前",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
rule: "内存使用率告警",
|
||||
message: "node-02 内存使用率达到 88%",
|
||||
severity: "warning",
|
||||
status: "resolved",
|
||||
time: "3小时前",
|
||||
resolvedAt: "2小时前",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
rule: "服务宕机告警",
|
||||
message: "Redis服务短暂不可用",
|
||||
severity: "critical",
|
||||
status: "resolved",
|
||||
time: "1天前",
|
||||
resolvedAt: "1天前",
|
||||
},
|
||||
]
|
||||
|
||||
export default function AlertsPage() {
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [activeTab, setActiveTab] = useState<"rules" | "history">("history")
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
||||
const [showSettingsDialog, setShowSettingsDialog] = useState(false)
|
||||
const [selectedRule, setSelectedRule] = useState<(typeof alertRules)[0] | null>(null)
|
||||
|
||||
const activeAlerts = alertHistory.filter((a) => a.status === "active")
|
||||
|
||||
const handleRuleSettings = (rule: (typeof alertRules)[0]) => {
|
||||
setSelectedRule(rule)
|
||||
setShowSettingsDialog(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-red-50/30 p-6">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/monitoring">
|
||||
<Button variant="ghost" size="icon" className="rounded-full">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">告警中心</h1>
|
||||
<p className="text-slate-500 mt-1">告警规则配置与历史记录</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="bg-gradient-to-r from-red-500 to-orange-600 hover:from-red-600 hover:to-orange-700"
|
||||
onClick={() => setShowCreateDialog(true)}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
新建告警规则
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 告警统计 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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">{activeAlerts.length}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-lg bg-red-100 flex items-center justify-center">
|
||||
<AlertTriangle className="w-5 h-5 text-red-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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">8</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-lg bg-emerald-100 flex items-center justify-center">
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-slate-800">{alertRules.length}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||
<Bell className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<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-slate-800">15分钟</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-lg bg-amber-100 flex items-center justify-center">
|
||||
<Clock className="w-5 h-5 text-amber-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 标签页切换 */}
|
||||
<div className="flex items-center gap-2 border-b border-slate-200">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={`rounded-none border-b-2 ${activeTab === "history" ? "border-slate-800 text-slate-800" : "border-transparent text-slate-500"}`}
|
||||
onClick={() => setActiveTab("history")}
|
||||
>
|
||||
告警历史
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={`rounded-none border-b-2 ${activeTab === "rules" ? "border-slate-800 text-slate-800" : "border-transparent text-slate-500"}`}
|
||||
onClick={() => setActiveTab("rules")}
|
||||
>
|
||||
告警规则
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{activeTab === "history" ? (
|
||||
<>
|
||||
{/* 筛选工具栏 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<Input
|
||||
placeholder="搜索告警..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-9 bg-white/70"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
筛选
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 告警历史列表 */}
|
||||
<div className="space-y-3">
|
||||
{alertHistory.map((alert) => (
|
||||
<Card
|
||||
key={alert.id}
|
||||
className={`bg-white/70 backdrop-blur border-slate-200/60 ${alert.status === "active" ? "border-l-4 border-l-red-500" : ""}`}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
{alert.status === "active" ? (
|
||||
<div className="w-10 h-10 rounded-lg bg-red-100 flex items-center justify-center">
|
||||
<AlertTriangle className="w-5 h-5 text-red-600" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded-lg bg-slate-100 flex items-center justify-center">
|
||||
<CheckCircle2 className="w-5 h-5 text-slate-400" />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-medium text-slate-800">{alert.rule}</h4>
|
||||
<Badge
|
||||
className={
|
||||
alert.severity === "critical"
|
||||
? "bg-red-100 text-red-700"
|
||||
: "bg-amber-100 text-amber-700"
|
||||
}
|
||||
>
|
||||
{alert.severity === "critical" ? "严重" : "警告"}
|
||||
</Badge>
|
||||
{alert.status === "active" ? (
|
||||
<Badge className="bg-red-100 text-red-700">活跃</Badge>
|
||||
) : (
|
||||
<Badge className="bg-emerald-100 text-emerald-700">已解决</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 mt-1">{alert.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-slate-600">{alert.time}</p>
|
||||
{alert.resolvedAt && <p className="text-xs text-slate-400">解决于 {alert.resolvedAt}</p>}
|
||||
</div>
|
||||
{alert.status === "active" && (
|
||||
<Button variant="outline" size="sm">
|
||||
<XCircle className="w-4 h-4 mr-1" />
|
||||
标记解决
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/* 告警规则列表 */
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardContent className="p-0">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200">
|
||||
<th className="text-left py-4 px-6 text-xs font-medium text-slate-500">规则名称</th>
|
||||
<th className="text-left py-4 px-6 text-xs font-medium text-slate-500">触发条件</th>
|
||||
<th className="text-left py-4 px-6 text-xs font-medium text-slate-500">持续时间</th>
|
||||
<th className="text-left py-4 px-6 text-xs font-medium text-slate-500">严重程度</th>
|
||||
<th className="text-left py-4 px-6 text-xs font-medium text-slate-500">触发次数</th>
|
||||
<th className="text-left py-4 px-6 text-xs font-medium text-slate-500">状态</th>
|
||||
<th className="text-left py-4 px-6 text-xs font-medium text-slate-500">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{alertRules.map((rule) => (
|
||||
<tr key={rule.id} className="border-b border-slate-100 hover:bg-slate-50/50">
|
||||
<td className="py-4 px-6 text-sm font-medium text-slate-800">{rule.name}</td>
|
||||
<td className="py-4 px-6">
|
||||
<code className="text-xs bg-slate-100 px-2 py-1 rounded">{rule.condition}</code>
|
||||
</td>
|
||||
<td className="py-4 px-6 text-sm text-slate-600">{rule.duration}</td>
|
||||
<td className="py-4 px-6">
|
||||
<Badge
|
||||
className={
|
||||
rule.severity === "critical" ? "bg-red-100 text-red-700" : "bg-amber-100 text-amber-700"
|
||||
}
|
||||
>
|
||||
{rule.severity === "critical" ? "严重" : "警告"}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="py-4 px-6 text-sm text-slate-600">{rule.triggers}</td>
|
||||
<td className="py-4 px-6">
|
||||
<Badge className="bg-emerald-100 text-emerald-700">启用</Badge>
|
||||
</td>
|
||||
<td className="py-4 px-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => handleRuleSettings(rule)}>
|
||||
<Settings className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Trash2 className="w-4 h-4 text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
<CreateAlertRuleDialog open={showCreateDialog} onOpenChange={setShowCreateDialog} />
|
||||
<AlertRuleSettingsDialog
|
||||
open={showSettingsDialog}
|
||||
onOpenChange={setShowSettingsDialog}
|
||||
rule={
|
||||
selectedRule
|
||||
? {
|
||||
name: selectedRule.name,
|
||||
condition: selectedRule.condition,
|
||||
severity: selectedRule.severity,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
app/monitoring/business/loading.tsx
Normal file
3
app/monitoring/business/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
299
app/monitoring/business/page.tsx
Normal file
299
app/monitoring/business/page.tsx
Normal file
@@ -0,0 +1,299 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import {
|
||||
TrendingUp,
|
||||
ArrowLeft,
|
||||
Users,
|
||||
Target,
|
||||
DollarSign,
|
||||
Activity,
|
||||
RefreshCw,
|
||||
ArrowUpRight,
|
||||
ArrowDownRight,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
} from "recharts"
|
||||
|
||||
const kpiData = [
|
||||
{ name: "数据接入成功率", value: 99.2, target: 99, unit: "%", trend: "up", change: 0.3 },
|
||||
{ name: "标签覆盖率", value: 87.5, target: 85, unit: "%", trend: "up", change: 2.1 },
|
||||
{ name: "价值预测准确率", value: 91.8, target: 90, unit: "%", trend: "up", change: 1.2 },
|
||||
{ name: "API调用增长", value: 15.2, target: 10, unit: "%", trend: "up", change: 3.5 },
|
||||
]
|
||||
|
||||
const userTrend = [
|
||||
{ date: "12-06", total: 4020000, active: 1250000, new: 12500 },
|
||||
{ date: "12-07", total: 4025000, active: 1280000, new: 13200 },
|
||||
{ date: "12-08", total: 4030000, active: 1260000, new: 11800 },
|
||||
{ date: "12-09", total: 4038000, active: 1320000, new: 14500 },
|
||||
{ date: "12-10", total: 4045000, active: 1350000, new: 15200 },
|
||||
{ date: "12-11", total: 4052000, active: 1380000, new: 16800 },
|
||||
{ date: "12-12", total: 4062000, active: 1420000, new: 18500 },
|
||||
]
|
||||
|
||||
const valueDistribution = [
|
||||
{ name: "S级", value: 328000, percent: 8.1, color: "#f59e0b" },
|
||||
{ name: "A级", value: 650000, percent: 16.0, color: "#3b82f6" },
|
||||
{ name: "B级", value: 1220000, percent: 30.0, color: "#10b981" },
|
||||
{ name: "C级", value: 1180000, percent: 29.1, color: "#8b5cf6" },
|
||||
{ name: "D级", value: 684000, percent: 16.8, color: "#6b7280" },
|
||||
]
|
||||
|
||||
const projectRanking = [
|
||||
{ rank: 1, name: "美妆护肤项目", users: 580000, value: 28.5, growth: 12.3 },
|
||||
{ rank: 2, name: "母婴亲子项目", users: 420000, value: 22.1, growth: 8.5 },
|
||||
{ rank: 3, name: "运动健康项目", users: 380000, value: 18.6, growth: 15.2 },
|
||||
{ rank: 4, name: "家居生活项目", users: 320000, value: 15.2, growth: 6.8 },
|
||||
{ rank: 5, name: "数码科技项目", users: 280000, value: 12.8, growth: 10.5 },
|
||||
]
|
||||
|
||||
export default function BusinessPage() {
|
||||
const [totalValue, setTotalValue] = useState(125.8)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setTotalValue((prev) => prev + Math.random() * 0.01)
|
||||
}, 5000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50/30 p-6">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/monitoring">
|
||||
<Button variant="ghost" size="icon" className="rounded-full">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">业务指标看板</h1>
|
||||
<p className="text-slate-500 mt-1">核心业务KPI监控与分析</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
刷新数据
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 核心KPI */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
{kpiData.map((kpi, i) => (
|
||||
<Card key={i} className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-sm text-slate-500">{kpi.name}</p>
|
||||
<Badge
|
||||
className={
|
||||
kpi.value >= kpi.target ? "bg-emerald-100 text-emerald-700" : "bg-amber-100 text-amber-700"
|
||||
}
|
||||
>
|
||||
目标: {kpi.target}
|
||||
{kpi.unit}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-end justify-between">
|
||||
<p className="text-3xl font-bold text-slate-800">
|
||||
{kpi.value}
|
||||
<span className="text-lg text-slate-500">{kpi.unit}</span>
|
||||
</p>
|
||||
<div
|
||||
className={`flex items-center gap-1 ${kpi.trend === "up" ? "text-emerald-600" : "text-red-600"}`}
|
||||
>
|
||||
{kpi.trend === "up" ? <ArrowUpRight className="w-4 h-4" /> : <ArrowDownRight className="w-4 h-4" />}
|
||||
<span className="text-sm font-medium">{kpi.change}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 资产总价值 */}
|
||||
<Card className="bg-gradient-to-r from-blue-500 to-indigo-600 border-0">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-white">
|
||||
<p className="text-sm opacity-80">数据资产总价值</p>
|
||||
<p className="text-5xl font-bold mt-2">{totalValue.toFixed(2)} 亿</p>
|
||||
<div className="flex items-center gap-4 mt-4">
|
||||
<div>
|
||||
<p className="text-xs opacity-60">总用户数</p>
|
||||
<p className="text-lg font-semibold">40.62 亿</p>
|
||||
</div>
|
||||
<div className="w-px h-10 bg-white/20" />
|
||||
<div>
|
||||
<p className="text-xs opacity-60">覆盖项目</p>
|
||||
<p className="text-lg font-semibold">156 个</p>
|
||||
</div>
|
||||
<div className="w-px h-10 bg-white/20" />
|
||||
<div>
|
||||
<p className="text-xs opacity-60">合作企业</p>
|
||||
<p className="text-lg font-semibold">89 家</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-32 h-32 rounded-full bg-white/20 flex items-center justify-center">
|
||||
<DollarSign className="w-16 h-16 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* 用户增长趋势 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Users className="w-4 h-4 text-slate-500" />
|
||||
用户增长趋势 (近7天)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={userTrend}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
|
||||
<XAxis dataKey="date" tick={{ fontSize: 12 }} stroke="#94a3b8" />
|
||||
<YAxis
|
||||
tick={{ fontSize: 12 }}
|
||||
stroke="#94a3b8"
|
||||
tickFormatter={(v) => `${(v / 10000).toFixed(0)}万`}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: "white",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
formatter={(value: number) => [`${(value / 10000).toFixed(1)}万`]}
|
||||
/>
|
||||
<Line type="monotone" dataKey="active" stroke="#3b82f6" strokeWidth={2} name="活跃用户" />
|
||||
<Line type="monotone" dataKey="new" stroke="#10b981" strokeWidth={2} name="新增用户" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 价值等级分布 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Target className="w-4 h-4 text-slate-500" />
|
||||
用户价值分布
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="h-64 flex-1">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={valueDistribution}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={90}
|
||||
dataKey="value"
|
||||
label={({ name, percent }) => `${name} ${percent}%`}
|
||||
>
|
||||
{valueDistribution.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip formatter={(value: number) => [`${(value / 10000).toFixed(1)}万`, "用户数"]} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{valueDistribution.map((item, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: item.color }} />
|
||||
<span className="text-sm text-slate-600">
|
||||
{item.name}: {(item.value / 10000).toFixed(1)}万
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 项目价值排名 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Activity className="w-4 h-4 text-slate-500" />
|
||||
项目价值排名 Top 5
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200">
|
||||
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500">排名</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500">项目名称</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500">用户数</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500">资产价值</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500">环比增长</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{projectRanking.map((project) => (
|
||||
<tr key={project.rank} className="border-b border-slate-100 hover:bg-slate-50/50">
|
||||
<td className="py-3 px-4">
|
||||
<div
|
||||
className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold ${
|
||||
project.rank === 1
|
||||
? "bg-amber-100 text-amber-700"
|
||||
: project.rank === 2
|
||||
? "bg-slate-200 text-slate-700"
|
||||
: project.rank === 3
|
||||
? "bg-orange-100 text-orange-700"
|
||||
: "bg-slate-100 text-slate-600"
|
||||
}`}
|
||||
>
|
||||
{project.rank}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm font-medium text-slate-800">{project.name}</td>
|
||||
<td className="py-3 px-4 text-sm text-slate-600">{(project.users / 10000).toFixed(1)}万</td>
|
||||
<td className="py-3 px-4 text-sm font-medium text-slate-800">{project.value}亿</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center gap-1 text-emerald-600">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">{project.growth}%</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
app/monitoring/health/loading.tsx
Normal file
3
app/monitoring/health/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
220
app/monitoring/health/page.tsx
Normal file
220
app/monitoring/health/page.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { ArrowLeft, Server, CheckCircle2, AlertTriangle, RefreshCw, Clock } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"
|
||||
|
||||
const performanceData = [
|
||||
{ time: "5m", p50: 32, p95: 85, p99: 120 },
|
||||
{ time: "10m", p50: 35, p95: 88, p99: 125 },
|
||||
{ time: "15m", p50: 30, p95: 82, p99: 115 },
|
||||
{ time: "20m", p50: 38, p95: 92, p99: 135 },
|
||||
{ time: "25m", p50: 33, p95: 86, p99: 122 },
|
||||
{ time: "30m", p50: 31, p95: 84, p99: 118 },
|
||||
]
|
||||
|
||||
const healthChecks = [
|
||||
{ name: "数据库连接池", status: "healthy", value: "48/50", description: "活跃连接数" },
|
||||
{ name: "Redis缓存", status: "healthy", value: "99.9%", description: "命中率" },
|
||||
{ name: "消息队列", status: "healthy", value: "0", description: "积压消息" },
|
||||
{ name: "定时任务", status: "healthy", value: "12/12", description: "运行中任务" },
|
||||
{ name: "外部API", status: "degraded", value: "95.2%", description: "成功率" },
|
||||
{ name: "文件存储", status: "healthy", value: "1.6TB", description: "已使用" },
|
||||
]
|
||||
|
||||
const nodeStatus = [
|
||||
{ id: 1, name: "node-01", role: "Master", cpu: 62, memory: 68, status: "healthy" },
|
||||
{ id: 2, name: "node-02", role: "Worker", cpu: 55, memory: 72, status: "healthy" },
|
||||
{ id: 3, name: "node-03", role: "Worker", cpu: 48, memory: 65, status: "healthy" },
|
||||
{ id: 4, name: "node-04", role: "Worker", cpu: 71, memory: 78, status: "degraded" },
|
||||
]
|
||||
|
||||
export default function HealthPage() {
|
||||
const [overallHealth, setOverallHealth] = useState(98.5)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setOverallHealth((prev) => Math.min(100, Math.max(95, prev + (Math.random() - 0.5) * 1)))
|
||||
}, 5000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-emerald-50/30 p-6">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/monitoring">
|
||||
<Button variant="ghost" size="icon" className="rounded-full">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">系统健康度</h1>
|
||||
<p className="text-slate-500 mt-1">服务状态、性能指标与集群监控</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 整体健康度 */}
|
||||
<Card className="bg-gradient-to-r from-emerald-500 to-teal-600 border-0">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-white">
|
||||
<p className="text-sm opacity-80">系统整体健康度</p>
|
||||
<p className="text-5xl font-bold mt-2">{overallHealth.toFixed(1)}%</p>
|
||||
<p className="text-sm opacity-80 mt-2">所有核心服务运行正常</p>
|
||||
</div>
|
||||
<div className="w-32 h-32 rounded-full bg-white/20 flex items-center justify-center">
|
||||
<CheckCircle2 className="w-16 h-16 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 健康检查项 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{healthChecks.map((check, i) => (
|
||||
<Card key={i} className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-700">{check.name}</p>
|
||||
<p className="text-2xl font-bold text-slate-800 mt-1">{check.value}</p>
|
||||
<p className="text-xs text-slate-500">{check.description}</p>
|
||||
</div>
|
||||
{check.status === "healthy" ? (
|
||||
<CheckCircle2 className="w-8 h-8 text-emerald-500" />
|
||||
) : (
|
||||
<AlertTriangle className="w-8 h-8 text-amber-500" />
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 响应时间分布 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Clock className="w-4 h-4 text-slate-500" />
|
||||
API响应时间分布 (最近30分钟)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={performanceData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
|
||||
<XAxis dataKey="time" tick={{ fontSize: 12 }} stroke="#94a3b8" />
|
||||
<YAxis tick={{ fontSize: 12 }} stroke="#94a3b8" tickFormatter={(v) => `${v}ms`} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: "white",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
formatter={(value: number) => [`${value}ms`]}
|
||||
/>
|
||||
<Line type="monotone" dataKey="p50" stroke="#10b981" 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 className="flex items-center justify-center gap-6 mt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-emerald-500" />
|
||||
<span className="text-xs text-slate-600">P50: 32ms</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-amber-500" />
|
||||
<span className="text-xs text-slate-600">P95: 85ms</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500" />
|
||||
<span className="text-xs text-slate-600">P99: 120ms</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 集群节点状态 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Server className="w-4 h-4 text-slate-500" />
|
||||
集群节点状态
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200">
|
||||
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500">节点</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500">角色</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500">CPU</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500">内存</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500">状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{nodeStatus.map((node) => (
|
||||
<tr key={node.id} className="border-b border-slate-100 hover:bg-slate-50/50">
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Server className="w-4 h-4 text-slate-400" />
|
||||
<span className="text-sm font-medium text-slate-800">{node.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<Badge variant="outline">{node.role}</Badge>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Progress value={node.cpu} className="w-20 h-2" />
|
||||
<span className="text-sm text-slate-600">{node.cpu}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Progress value={node.memory} className="w-20 h-2" />
|
||||
<span className="text-sm text-slate-600">{node.memory}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
{node.status === "healthy" ? (
|
||||
<Badge className="bg-emerald-100 text-emerald-700">
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
正常
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge className="bg-amber-100 text-amber-700">
|
||||
<AlertTriangle className="w-3 h-3 mr-1" />
|
||||
降级
|
||||
</Badge>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
17
app/monitoring/loading.tsx
Normal file
17
app/monitoring/loading.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-emerald-50/30 p-6">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
<Skeleton className="h-10 w-64" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<Skeleton key={i} className="h-28 rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
<Skeleton className="h-64 rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
381
app/monitoring/page.tsx
Normal file
381
app/monitoring/page.tsx
Normal file
@@ -0,0 +1,381 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import {
|
||||
Activity,
|
||||
Server,
|
||||
Cpu,
|
||||
HardDrive,
|
||||
Wifi,
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
Bell,
|
||||
RefreshCw,
|
||||
ArrowRight,
|
||||
TrendingUp,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from "recharts"
|
||||
|
||||
// 模拟系统资源数据
|
||||
const systemMetrics = [
|
||||
{ time: "00:00", cpu: 45, memory: 62, disk: 78, network: 120 },
|
||||
{ time: "04:00", cpu: 32, memory: 58, disk: 78, network: 80 },
|
||||
{ time: "08:00", cpu: 58, memory: 65, disk: 79, network: 180 },
|
||||
{ time: "12:00", cpu: 72, memory: 71, disk: 79, network: 320 },
|
||||
{ time: "16:00", cpu: 68, memory: 69, disk: 80, network: 280 },
|
||||
{ time: "20:00", cpu: 55, memory: 64, disk: 80, network: 200 },
|
||||
]
|
||||
|
||||
const recentAlerts = [
|
||||
{ id: 1, type: "warning", message: "数据同步任务延迟超过阈值", time: "10分钟前", status: "active" },
|
||||
{ id: 2, type: "info", message: "模型训练任务已完成", time: "30分钟前", status: "resolved" },
|
||||
{ id: 3, type: "error", message: "API网关响应超时", time: "1小时前", status: "resolved" },
|
||||
{ id: 4, type: "warning", message: "磁盘使用率超过80%", time: "2小时前", status: "active" },
|
||||
]
|
||||
|
||||
const serviceStatus = [
|
||||
{ name: "数据接入服务", status: "healthy", uptime: "99.99%", latency: "12ms" },
|
||||
{ name: "标签计算引擎", status: "healthy", uptime: "99.95%", latency: "45ms" },
|
||||
{ name: "AI推理服务", status: "healthy", uptime: "99.90%", latency: "120ms" },
|
||||
{ name: "API网关", status: "degraded", uptime: "99.85%", latency: "85ms" },
|
||||
{ name: "数据库集群", status: "healthy", uptime: "99.99%", latency: "8ms" },
|
||||
{ name: "缓存服务", status: "healthy", uptime: "99.99%", latency: "2ms" },
|
||||
]
|
||||
|
||||
export default function MonitoringPage() {
|
||||
const [cpuUsage, setCpuUsage] = useState(65)
|
||||
const [memoryUsage, setMemoryUsage] = useState(68)
|
||||
const [diskUsage, setDiskUsage] = useState(80)
|
||||
const [networkIO, setNetworkIO] = useState(256)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCpuUsage((prev) => Math.min(95, Math.max(30, prev + (Math.random() - 0.5) * 10)))
|
||||
setMemoryUsage((prev) => Math.min(90, Math.max(50, prev + (Math.random() - 0.5) * 5)))
|
||||
setNetworkIO((prev) => Math.min(500, Math.max(100, prev + (Math.random() - 0.5) * 50)))
|
||||
}, 3000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-emerald-50/30 p-6">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">系统监控与运营看板</h1>
|
||||
<p className="text-slate-500 mt-1">实时监控系统健康状态、服务运行和告警信息</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline">
|
||||
<Bell className="w-4 h-4 mr-2" />
|
||||
告警设置
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
<Link href="/monitoring/alerts">
|
||||
<Button className="bg-gradient-to-r from-emerald-500 to-teal-600 hover:from-emerald-600 hover:to-teal-700">
|
||||
<Activity className="w-4 h-4 mr-2" />
|
||||
告警中心
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 系统资源概览 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-cyan-600 flex items-center justify-center">
|
||||
<Cpu className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-700">CPU使用率</span>
|
||||
</div>
|
||||
<span
|
||||
className={`text-xl font-bold ${cpuUsage > 80 ? "text-red-600" : cpuUsage > 60 ? "text-amber-600" : "text-emerald-600"}`}
|
||||
>
|
||||
{cpuUsage.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={cpuUsage} className="h-2" />
|
||||
<p className="text-xs text-slate-500 mt-2">8核 / 16线程</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center">
|
||||
<Server className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-700">内存使用</span>
|
||||
</div>
|
||||
<span
|
||||
className={`text-xl font-bold ${memoryUsage > 85 ? "text-red-600" : memoryUsage > 70 ? "text-amber-600" : "text-emerald-600"}`}
|
||||
>
|
||||
{memoryUsage.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={memoryUsage} className="h-2" />
|
||||
<p className="text-xs text-slate-500 mt-2">54.4GB / 80GB</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-amber-500 to-orange-600 flex items-center justify-center">
|
||||
<HardDrive className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-700">磁盘使用</span>
|
||||
</div>
|
||||
<span className={`text-xl font-bold ${diskUsage > 85 ? "text-red-600" : "text-amber-600"}`}>
|
||||
{diskUsage}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={diskUsage} className="h-2" />
|
||||
<p className="text-xs text-slate-500 mt-2">1.6TB / 2TB</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-emerald-500 to-teal-600 flex items-center justify-center">
|
||||
<Wifi className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-700">网络IO</span>
|
||||
</div>
|
||||
<span className="text-xl font-bold text-emerald-600">{networkIO} MB/s</span>
|
||||
</div>
|
||||
<Progress value={(networkIO / 500) * 100} className="h-2" />
|
||||
<p className="text-xs text-slate-500 mt-2">入站: 180MB/s | 出站: 76MB/s</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 功能入口 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Link href="/monitoring/health">
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60 hover:shadow-lg hover:border-slate-300 transition-all cursor-pointer group h-full">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-emerald-500 to-teal-600 flex items-center justify-center">
|
||||
<Activity className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-slate-800">系统健康度</h3>
|
||||
<p className="text-sm text-slate-500">服务状态与性能指标</p>
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight className="w-5 h-5 text-slate-400 group-hover:text-slate-600 group-hover:translate-x-1 transition-all" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
<Link href="/monitoring/alerts">
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60 hover:shadow-lg hover:border-slate-300 transition-all cursor-pointer group h-full">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-red-500 to-orange-600 flex items-center justify-center">
|
||||
<Bell className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-slate-800">告警中心</h3>
|
||||
<p className="text-sm text-slate-500">告警规则与通知管理</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className="bg-red-100 text-red-700">2</Badge>
|
||||
<ArrowRight className="w-5 h-5 text-slate-400 group-hover:text-slate-600 group-hover:translate-x-1 transition-all" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
<Link href="/monitoring/business">
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60 hover:shadow-lg hover:border-slate-300 transition-all cursor-pointer group h-full">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center">
|
||||
<TrendingUp className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-slate-800">业务指标</h3>
|
||||
<p className="text-sm text-slate-500">核心业务KPI监控</p>
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight className="w-5 h-5 text-slate-400 group-hover:text-slate-600 group-hover:translate-x-1 transition-all" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* 系统资源趋势 */}
|
||||
<Card className="lg:col-span-2 bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">系统资源趋势 (24小时)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={systemMetrics}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
|
||||
<XAxis dataKey="time" tick={{ fontSize: 12 }} stroke="#94a3b8" />
|
||||
<YAxis tick={{ fontSize: 12 }} stroke="#94a3b8" />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: "white",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient id="colorCpu" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient id="colorMemory" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#8b5cf6" stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor="#8b5cf6" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area type="monotone" dataKey="cpu" stroke="#3b82f6" strokeWidth={2} fill="url(#colorCpu)" />
|
||||
<Area type="monotone" dataKey="memory" stroke="#8b5cf6" strokeWidth={2} fill="url(#colorMemory)" />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-6 mt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-blue-500" />
|
||||
<span className="text-xs text-slate-600">CPU</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-violet-500" />
|
||||
<span className="text-xs text-slate-600">内存</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 最近告警 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Bell className="w-4 h-4 text-red-500" />
|
||||
最近告警
|
||||
</CardTitle>
|
||||
<Link href="/monitoring/alerts">
|
||||
<Button variant="ghost" size="sm">
|
||||
查看全部
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{recentAlerts.map((alert) => (
|
||||
<div key={alert.id} className="flex items-start gap-3 p-3 bg-slate-50/80 rounded-lg">
|
||||
{alert.type === "error" ? (
|
||||
<AlertTriangle className="w-5 h-5 text-red-500 mt-0.5" />
|
||||
) : alert.type === "warning" ? (
|
||||
<AlertTriangle className="w-5 h-5 text-amber-500 mt-0.5" />
|
||||
) : (
|
||||
<CheckCircle2 className="w-5 h-5 text-blue-500 mt-0.5" />
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-slate-700">{alert.message}</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-xs text-slate-500">{alert.time}</span>
|
||||
{alert.status === "active" ? (
|
||||
<Badge className="bg-red-100 text-red-700 text-xs">待处理</Badge>
|
||||
) : (
|
||||
<Badge className="bg-emerald-100 text-emerald-700 text-xs">已解决</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 服务状态 */}
|
||||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Server className="w-4 h-4 text-slate-500" />
|
||||
服务状态
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{serviceStatus.map((service, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`flex items-center justify-between p-4 rounded-lg border ${
|
||||
service.status === "healthy"
|
||||
? "bg-emerald-50/50 border-emerald-200"
|
||||
: service.status === "degraded"
|
||||
? "bg-amber-50/50 border-amber-200"
|
||||
: "bg-red-50/50 border-red-200"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={`w-3 h-3 rounded-full ${
|
||||
service.status === "healthy"
|
||||
? "bg-emerald-500"
|
||||
: service.status === "degraded"
|
||||
? "bg-amber-500 animate-pulse"
|
||||
: "bg-red-500 animate-pulse"
|
||||
}`}
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-800">{service.name}</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
可用性: {service.uptime} | 延迟: {service.latency}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
className={
|
||||
service.status === "healthy"
|
||||
? "bg-emerald-100 text-emerald-700"
|
||||
: service.status === "degraded"
|
||||
? "bg-amber-100 text-amber-700"
|
||||
: "bg-red-100 text-red-700"
|
||||
}
|
||||
>
|
||||
{service.status === "healthy" ? "正常" : service.status === "degraded" ? "降级" : "故障"}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
580
app/page.tsx
580
app/page.tsx
@@ -1,280 +1,396 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Users, Database, Activity, DollarSign, TrendingUp, Search, ArrowUpRight, Zap } from "lucide-react"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import Link from "next/link"
|
||||
import {
|
||||
Users,
|
||||
Database,
|
||||
RefreshCw,
|
||||
Activity,
|
||||
Globe,
|
||||
Brain,
|
||||
Tags,
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
} from "recharts"
|
||||
Package,
|
||||
ArrowUpRight,
|
||||
Zap,
|
||||
Target,
|
||||
CheckCircle,
|
||||
} from "lucide-react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { useRouter } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
|
||||
// 模拟数据
|
||||
const growthData = [
|
||||
{ month: "1月", users: 3200, value: 420 },
|
||||
{ month: "2月", users: 3800, value: 480 },
|
||||
{ month: "3月", users: 4100, value: 520 },
|
||||
{ month: "4月", users: 4600, value: 580 },
|
||||
{ month: "5月", users: 5200, value: 650 },
|
||||
{ month: "6月", users: 5800, value: 720 },
|
||||
]
|
||||
interface SystemMetrics {
|
||||
dataTaskSuccessRate: number
|
||||
tagCoverageRate: number
|
||||
valuePredictionAccuracy: number
|
||||
apiDailyCallGrowth: number
|
||||
}
|
||||
|
||||
const valueDistribution = [
|
||||
{ name: "S级", value: 5, color: "#8B5CF6" },
|
||||
{ name: "A级", value: 15, color: "#3B82F6" },
|
||||
{ name: "B级", value: 35, color: "#10B981" },
|
||||
{ name: "C级", value: 30, color: "#F59E0B" },
|
||||
{ name: "D级", value: 15, color: "#EF4444" },
|
||||
]
|
||||
interface DataFlowStats {
|
||||
totalUsers: number
|
||||
totalProjects: number
|
||||
totalDataSources: number
|
||||
totalTags: number
|
||||
totalModels: number
|
||||
totalApiCalls: number
|
||||
}
|
||||
|
||||
const recentActivities = [
|
||||
{ id: 1, user: "张伟", action: "完成首次购买", time: "2分钟前", type: "purchase" },
|
||||
{ id: 2, user: "李娜", action: "升级为A级用户", time: "5分钟前", type: "upgrade" },
|
||||
{ id: 3, user: "王芳", action: "触发流失预警", time: "8分钟前", type: "warning" },
|
||||
{ id: 4, user: "刘洋", action: "完成问卷调查", time: "12分钟前", type: "survey" },
|
||||
{ id: 5, user: "陈明", action: "加入VIP计划", time: "15分钟前", type: "vip" },
|
||||
]
|
||||
|
||||
export default function HomePage() {
|
||||
export default function OverviewPage() {
|
||||
const router = useRouter()
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [currentTime, setCurrentTime] = useState(new Date())
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [lastUpdate, setLastUpdate] = useState(new Date())
|
||||
|
||||
// 核心OKR指标 (按PRD定义)
|
||||
const [metrics, setMetrics] = useState<SystemMetrics>({
|
||||
dataTaskSuccessRate: 99.7,
|
||||
tagCoverageRate: 82.3,
|
||||
valuePredictionAccuracy: 87.5,
|
||||
apiDailyCallGrowth: 32.8,
|
||||
})
|
||||
|
||||
// 数据流统计
|
||||
const [stats, setStats] = useState<DataFlowStats>({
|
||||
totalUsers: 4028567890,
|
||||
totalProjects: 1256,
|
||||
totalDataSources: 28,
|
||||
totalTags: 1892,
|
||||
totalModels: 15,
|
||||
totalApiCalls: 8956234,
|
||||
})
|
||||
|
||||
// 实时数据流
|
||||
const [dataFlows, setDataFlows] = useState([
|
||||
{ source: "存客宝", type: "用户行为", count: 125680, status: "running" },
|
||||
{ source: "触客宝", type: "互动数据", count: 89234, status: "running" },
|
||||
{ source: "数智员工", type: "账号数据", count: 45678, status: "running" },
|
||||
{ source: "外部API", type: "征信数据", count: 12890, status: "waiting" },
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setCurrentTime(new Date()), 1000)
|
||||
return () => clearInterval(timer)
|
||||
const interval = setInterval(() => {
|
||||
// 模拟实时数据更新
|
||||
setStats((prev) => ({
|
||||
...prev,
|
||||
totalUsers: prev.totalUsers + Math.floor(Math.random() * 1000),
|
||||
totalApiCalls: prev.totalApiCalls + Math.floor(Math.random() * 100),
|
||||
}))
|
||||
setDataFlows((prev) =>
|
||||
prev.map((flow) => ({
|
||||
...flow,
|
||||
count: flow.count + Math.floor(Math.random() * 100),
|
||||
})),
|
||||
)
|
||||
}, 3000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const refreshData = async () => {
|
||||
setIsRefreshing(true)
|
||||
await new Promise((r) => setTimeout(r, 1000))
|
||||
setLastUpdate(new Date())
|
||||
setIsRefreshing(false)
|
||||
}
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
// 5大功能模块快捷入口(按HTML文档重构)
|
||||
const modules = [
|
||||
{
|
||||
title: "数据接入",
|
||||
icon: Database,
|
||||
color: "text-blue-600",
|
||||
bg: "bg-blue-100",
|
||||
href: "/data-ingestion/sources",
|
||||
desc: "数据源管理、清洗规则、任务调度、数据血缘",
|
||||
stats: `${stats.totalDataSources}个数据源`,
|
||||
},
|
||||
{
|
||||
title: "标签画像",
|
||||
icon: Tags,
|
||||
color: "text-green-600",
|
||||
bg: "bg-green-100",
|
||||
href: "/tag-portrait/tags",
|
||||
desc: "标签体系、用户画像、人群圈选",
|
||||
stats: `${stats.totalTags}个标签`,
|
||||
},
|
||||
{
|
||||
title: "AI Agent",
|
||||
icon: Brain,
|
||||
color: "text-purple-600",
|
||||
bg: "bg-purple-100",
|
||||
href: "/ai-agent/chat",
|
||||
desc: "智能对话、AI打标、AI清洗、自然语言查询",
|
||||
stats: "5大AI能力",
|
||||
},
|
||||
{
|
||||
title: "数据输出",
|
||||
icon: Package,
|
||||
color: "text-orange-600",
|
||||
bg: "bg-orange-100",
|
||||
href: "/data-output/packages",
|
||||
desc: "流量包、API市场、数据订阅",
|
||||
stats: `${formatNumber(stats.totalApiCalls)}次调用`,
|
||||
},
|
||||
{
|
||||
title: "系统监控",
|
||||
icon: LineChart,
|
||||
color: "text-cyan-600",
|
||||
bg: "bg-cyan-100",
|
||||
href: "/system/health",
|
||||
desc: "系统健康、告警中心、操作日志",
|
||||
stats: "实时监控",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-6 p-6 md:p-8">
|
||||
{/* 顶部标题区 */}
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div className="space-y-6 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">数据资产中台</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{currentTime.toLocaleDateString("zh-CN", { weekday: "long", year: "numeric", month: "long", day: "numeric" })}
|
||||
{" · "}
|
||||
{currentTime.toLocaleTimeString("zh-CN")}
|
||||
</p>
|
||||
<h1 className="text-3xl font-bold text-gray-900">数据驾驶舱</h1>
|
||||
<p className="text-gray-500 mt-1">私域银行生态数据中枢实时监控</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative w-full md:w-80">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索用户、手机号、标签..."
|
||||
className="pl-9"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button>
|
||||
<Zap className="mr-2 h-4 w-4" />
|
||||
快速分析
|
||||
<Badge variant="outline" className="bg-green-50 text-green-600 border-green-200 px-3 py-1">
|
||||
<Activity className="w-3 h-3 mr-1" />
|
||||
系统正常
|
||||
</Badge>
|
||||
<span className="text-xs text-gray-400">更新于 {lastUpdate.toLocaleTimeString()}</span>
|
||||
<Button variant="outline" size="sm" onClick={refreshData} disabled={isRefreshing} className="bg-white/50">
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 核心指标卡片 */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card className="bg-gradient-to-br from-violet-500/10 to-purple-500/10 border-violet-200 dark:border-violet-800">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">用户资产总量</CardTitle>
|
||||
<Users className="h-4 w-4 text-violet-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">40.5亿</div>
|
||||
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
||||
<TrendingUp className="h-3 w-3 text-green-500 mr-1" />
|
||||
<span className="text-green-500">+20.1%</span>
|
||||
<span className="ml-1">较上月</span>
|
||||
{/* 核心OKR指标 */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-gray-500">数据接入成功率</span>
|
||||
<Badge className="bg-green-100 text-green-700 text-xs">目标≥99.5%</Badge>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-gray-900">{metrics.dataTaskSuccessRate}%</div>
|
||||
<Progress value={metrics.dataTaskSuccessRate} className="h-2 mt-2" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-blue-500/10 to-cyan-500/10 border-blue-200 dark:border-blue-800">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">活跃用户</CardTitle>
|
||||
<Activity className="h-4 w-4 text-blue-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">2,350万</div>
|
||||
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
||||
<TrendingUp className="h-3 w-3 text-green-500 mr-1" />
|
||||
<span className="text-green-500">+18.2%</span>
|
||||
<span className="ml-1">较上月</span>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-gray-500">标签覆盖率</span>
|
||||
<Badge className="bg-blue-100 text-blue-700 text-xs">目标≥80%</Badge>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-gray-900">{metrics.tagCoverageRate}%</div>
|
||||
<Progress value={metrics.tagCoverageRate} className="h-2 mt-2" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-emerald-500/10 to-green-500/10 border-emerald-200 dark:border-emerald-800">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">数据处理量</CardTitle>
|
||||
<Database className="h-4 w-4 text-emerald-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">125亿条</div>
|
||||
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
||||
<TrendingUp className="h-3 w-3 text-green-500 mr-1" />
|
||||
<span className="text-green-500">+32.5%</span>
|
||||
<span className="ml-1">较上月</span>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-gray-500">价值预测准确率</span>
|
||||
<Badge className="bg-purple-100 text-purple-700 text-xs">目标≥85%</Badge>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-gray-900">{metrics.valuePredictionAccuracy}%</div>
|
||||
<Progress value={metrics.valuePredictionAccuracy} className="h-2 mt-2" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-amber-500/10 to-orange-500/10 border-amber-200 dark:border-amber-800">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">资产总价值</CardTitle>
|
||||
<DollarSign className="h-4 w-4 text-amber-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">¥125.8亿</div>
|
||||
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
||||
<TrendingUp className="h-3 w-3 text-green-500 mr-1" />
|
||||
<span className="text-green-500">+45.2%</span>
|
||||
<span className="ml-1">较上月</span>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-gray-500">API调用增长</span>
|
||||
<Badge className="bg-orange-100 text-orange-700 text-xs">目标≥30%QoQ</Badge>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-gray-900">+{metrics.apiDailyCallGrowth}%</div>
|
||||
<Progress value={metrics.apiDailyCallGrowth} className="h-2 mt-2" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 图表区域 */}
|
||||
<div className="grid gap-4 md:grid-cols-7">
|
||||
<Card className="col-span-4">
|
||||
<CardHeader>
|
||||
<CardTitle>用户增长趋势</CardTitle>
|
||||
<CardDescription>近6个月用户与资产价值变化</CardDescription>
|
||||
{/* 用户资产总览 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<Card className="lg:col-span-2 border-none shadow-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white overflow-hidden relative">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-white/10 rounded-full -mr-16 -mt-16 blur-3xl" />
|
||||
<CardHeader className="pb-2 relative z-10">
|
||||
<CardTitle className="flex items-center gap-2 text-blue-100">
|
||||
<Users className="w-5 h-5" />
|
||||
全平台用户资产
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={growthData}>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||
<XAxis dataKey="month" className="text-xs" />
|
||||
<YAxis className="text-xs" />
|
||||
<Tooltip />
|
||||
<Line type="monotone" dataKey="users" stroke="#8B5CF6" strokeWidth={2} name="用户(万)" />
|
||||
<Line type="monotone" dataKey="value" stroke="#3B82F6" strokeWidth={2} name="价值(亿)" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="col-span-3">
|
||||
<CardHeader>
|
||||
<CardTitle>用户价值分布</CardTitle>
|
||||
<CardDescription>S/A/B/C/D级用户占比</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={valueDistribution}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={50}
|
||||
outerRadius={80}
|
||||
dataKey="value"
|
||||
label={({ name, value }) => `${name}: ${value}%`}
|
||||
>
|
||||
{valueDistribution.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="flex flex-wrap justify-center gap-2 mt-4">
|
||||
{valueDistribution.map((item) => (
|
||||
<Badge key={item.name} variant="outline" style={{ borderColor: item.color, color: item.color }}>
|
||||
{item.name}: {item.value}%
|
||||
</Badge>
|
||||
))}
|
||||
<CardContent className="relative z-10">
|
||||
<div className="flex items-baseline gap-2 mb-4">
|
||||
<span className="text-5xl font-bold">{formatNumber(stats.totalUsers)}</span>
|
||||
<span className="text-blue-100">总用户数</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4 pt-4 border-t border-white/20">
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-green-300">+2.3%</div>
|
||||
<div className="text-sm text-blue-100">日增长</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold">{stats.totalProjects.toLocaleString()}</div>
|
||||
<div className="text-sm text-blue-100">项目数</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold">125.6B</div>
|
||||
<div className="text-sm text-blue-100">估值(元)</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 底部区域 */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>实时动态</CardTitle>
|
||||
<CardDescription>用户行为实时监控</CardDescription>
|
||||
{/* 实时数据流 */}
|
||||
<Card className="border-none shadow-md bg-white/60 backdrop-blur-md">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-gray-700 text-sm font-medium flex items-center gap-2">
|
||||
<Zap className="w-4 h-4 text-yellow-500" />
|
||||
实时数据流入
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{recentActivities.map((activity) => (
|
||||
<div key={activity.id} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center text-sm font-medium">
|
||||
{activity.user.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{activity.user}</p>
|
||||
<p className="text-xs text-muted-foreground">{activity.action}</p>
|
||||
</div>
|
||||
<CardContent className="space-y-3">
|
||||
{dataFlows.map((flow, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-2 rounded-lg bg-gray-50">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${flow.status === "running" ? "bg-green-500 animate-pulse" : "bg-yellow-500"}`}
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{flow.source}</div>
|
||||
<div className="text-xs text-gray-500">{flow.type}</div>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{activity.time}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>快速入口</CardTitle>
|
||||
<CardDescription>常用功能快速访问</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Link href="/data-platform">
|
||||
<Button variant="outline" className="w-full justify-start h-auto py-4 bg-transparent">
|
||||
<Database className="mr-2 h-4 w-4" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">数据中台</div>
|
||||
<div className="text-xs text-muted-foreground">数据源管理</div>
|
||||
</div>
|
||||
<ArrowUpRight className="ml-auto h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/user-portrait">
|
||||
<Button variant="outline" className="w-full justify-start h-auto py-4 bg-transparent">
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">用户画像</div>
|
||||
<div className="text-xs text-muted-foreground">用户分析</div>
|
||||
</div>
|
||||
<ArrowUpRight className="ml-auto h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/value-assessment">
|
||||
<Button variant="outline" className="w-full justify-start h-auto py-4 bg-transparent">
|
||||
<DollarSign className="mr-2 h-4 w-4" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">价值评估</div>
|
||||
<div className="text-xs text-muted-foreground">RFM模型</div>
|
||||
</div>
|
||||
<ArrowUpRight className="ml-auto h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/ai-assistant">
|
||||
<Button variant="outline" className="w-full justify-start h-auto py-4 bg-transparent">
|
||||
<Zap className="mr-2 h-4 w-4" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">AI助手</div>
|
||||
<div className="text-xs text-muted-foreground">智能分析</div>
|
||||
</div>
|
||||
<ArrowUpRight className="ml-auto h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-semibold text-gray-900">{flow.count.toLocaleString()}</div>
|
||||
<div className="text-xs text-gray-500">条/分钟</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 5大功能模块入口 */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">功能模块</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||
{modules.map((module, index) => (
|
||||
<Link key={index} href={module.href}>
|
||||
<Card className="h-full cursor-pointer hover:shadow-lg transition-all duration-300 border-none shadow-sm bg-white/60 backdrop-blur-md group hover:-translate-y-1">
|
||||
<CardContent className="p-5">
|
||||
<div
|
||||
className={`w-12 h-12 rounded-2xl ${module.bg} flex items-center justify-center mb-3 group-hover:scale-110 transition-transform`}
|
||||
>
|
||||
<module.icon className={`w-6 h-6 ${module.color}`} />
|
||||
</div>
|
||||
<h3 className="font-bold text-gray-900 mb-1">{module.title}</h3>
|
||||
<p className="text-xs text-gray-500 mb-2 line-clamp-2">{module.desc}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{module.stats}
|
||||
</Badge>
|
||||
<ArrowUpRight className="w-4 h-4 text-gray-400 group-hover:text-blue-500 transition-colors" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 系统关联图说明 */}
|
||||
<Card className="border-none shadow-md bg-white/60 backdrop-blur-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-700 text-sm font-medium flex items-center gap-2">
|
||||
<Globe className="w-4 h-4" />
|
||||
系统关联与数据流向
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* 数据流入 */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-semibold text-gray-900 flex items-center gap-2">
|
||||
<ArrowUpRight className="w-4 h-4 text-blue-500" />
|
||||
数据流入
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ name: "存客宝", desc: "用户基础信息、交易流水" },
|
||||
{ name: "触客宝", desc: "互动行为、活动参与" },
|
||||
{ name: "数智员工", desc: "账号信息、粉丝画像" },
|
||||
{ name: "外部系统", desc: "征信数据、行业数据" },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex items-start gap-2 p-2 rounded-lg bg-blue-50">
|
||||
<CheckCircle className="w-4 h-4 text-blue-500 mt-0.5" />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{item.name}</div>
|
||||
<div className="text-xs text-gray-500">{item.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 神射手处理 */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-semibold text-gray-900 flex items-center gap-2">
|
||||
<Target className="w-4 h-4 text-purple-500" />
|
||||
神射手处理
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ name: "数据治理", desc: "清洗、标准化、质量监控" },
|
||||
{ name: "标签计算", desc: "规则/模型/AI标签生成" },
|
||||
{ name: "价值评估", desc: "CLV、RFM、流失预测" },
|
||||
{ name: "AI洞察", desc: "智能分析、报告生成" },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex items-start gap-2 p-2 rounded-lg bg-purple-50">
|
||||
<Zap className="w-4 h-4 text-purple-500 mt-0.5" />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{item.name}</div>
|
||||
<div className="text-xs text-gray-500">{item.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 服务输出 */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-semibold text-gray-900 flex items-center gap-2">
|
||||
<Package className="w-4 h-4 text-green-500" />
|
||||
服务输出
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ name: "存客宝/触客宝", desc: "画像API、人群包推送" },
|
||||
{ name: "数智员工", desc: "账号价值评估API" },
|
||||
{ name: "聚宝盆", desc: "核心指标聚合接口" },
|
||||
{ name: "外部客户", desc: "行业报告、趋势分析包" },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex items-start gap-2 p-2 rounded-lg bg-green-50">
|
||||
<ArrowUpRight className="w-4 h-4 text-green-500 mt-0.5" />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{item.name}</div>
|
||||
<div className="text-xs text-gray-500">{item.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
175
app/platform/ai-assistant/data-analysis/page.tsx
Normal file
175
app/platform/ai-assistant/data-analysis/page.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { BarChart3, TrendingUp, Users, Download, RefreshCw } from 'lucide-react'
|
||||
import { BarChart, Bar, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'
|
||||
|
||||
export default function DataAnalysis() {
|
||||
const userBehaviorData = [
|
||||
{ hour: '00:00', active: 1250, purchases: 85, interactions: 4200 },
|
||||
{ hour: '04:00', active: 850, purchases: 35, interactions: 1800 },
|
||||
{ hour: '08:00', active: 5600, purchases: 420, interactions: 18500 },
|
||||
{ hour: '12:00', active: 8900, purchases: 680, interactions: 28600 },
|
||||
{ hour: '16:00', active: 7200, purchases: 520, interactions: 22400 },
|
||||
{ hour: '20:00', active: 9500, purchases: 750, interactions: 32800 },
|
||||
]
|
||||
|
||||
const channelData = [
|
||||
{ name: '微信朋友圈', value: 35, color: '#8b5cf6' },
|
||||
{ name: '微信群', value: 28, color: '#3b82f6' },
|
||||
{ name: '一对一私聊', value: 22, color: '#10b981' },
|
||||
{ name: '小程序', value: 15, color: '#f59e0b' },
|
||||
]
|
||||
|
||||
const conversionData = [
|
||||
{ stage: '浏览', users: 100000, rate: 100 },
|
||||
{ stage: '兴趣', users: 45000, rate: 45 },
|
||||
{ stage: '咨询', users: 18000, rate: 18 },
|
||||
{ stage: '下单', users: 5400, rate: 5.4 },
|
||||
{ stage: '复购', users: 1620, rate: 1.6 },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">数据分析</h1>
|
||||
<p className="text-gray-600">用户行为分析、渠道分析与转化漏斗</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
刷新数据
|
||||
</Button>
|
||||
<Button>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出报告
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 关键指标 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 mb-6">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">今日活跃用户</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">285.6K</div>
|
||||
<Badge variant="outline" className="border-green-500 text-green-600 bg-green-50">+12.5%</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">转化率</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">5.4%</div>
|
||||
<Badge variant="outline" className="border-blue-500 text-blue-600 bg-blue-50">+0.8%</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">人均互动次数</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">8.6</div>
|
||||
<Badge variant="outline" className="border-purple-500 text-purple-600 bg-purple-50">+1.2</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">客单价</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">¥328</div>
|
||||
<Badge variant="outline" className="border-orange-500 text-orange-600 bg-orange-50">+15.2%</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 用户行为趋势 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Users className="w-5 h-5" />
|
||||
用户活跃时段分布
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={userBehaviorData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||
<XAxis dataKey="hour" stroke="#6b7280" />
|
||||
<YAxis stroke="#6b7280" />
|
||||
<Tooltip />
|
||||
<Bar dataKey="active" fill="#8b5cf6" name="活跃用户" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<BarChart3 className="w-5 h-5" />
|
||||
渠道分布
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={channelData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={({ name, value }) => `${name} ${value}%`}
|
||||
outerRadius={100}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
>
|
||||
{channelData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 转化漏斗 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
用户转化漏斗
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{conversionData.map((stage, index) => (
|
||||
<div key={index}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-gray-700 font-medium">{stage.stage}</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-gray-900 font-semibold">{stage.users.toLocaleString()}人</span>
|
||||
<span className="text-blue-600 font-semibold w-16 text-right">{stage.rate}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-12 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg flex items-center px-4 text-white font-semibold" style={{ width: `${stage.rate}%`, minWidth: '10%' }}>
|
||||
{stage.rate}%
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
236
app/platform/ai-assistant/page.tsx
Normal file
236
app/platform/ai-assistant/page.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Cpu, BarChart3, TrendingUp, Users, Target, Brain, ArrowRight, Send, Sparkles } from 'lucide-react'
|
||||
|
||||
export default function AIAssistant() {
|
||||
const [message, setMessage] = useState("")
|
||||
const [chatHistory, setChatHistory] = useState([
|
||||
{
|
||||
role: "assistant",
|
||||
content: "你好!我是神射手AI助手,可以帮助你分析用户数据、生成报告、预测趋势。有什么我可以帮助你的吗?",
|
||||
timestamp: "10:30",
|
||||
},
|
||||
])
|
||||
|
||||
// AI功能卡片
|
||||
const aiFeatures = [
|
||||
{
|
||||
icon: <BarChart3 className="w-8 h-8" />,
|
||||
title: "数据分析",
|
||||
description: "自动分析用户行为,生成洞察报告",
|
||||
color: "from-blue-500 to-cyan-500",
|
||||
href: "/platform/ai-assistant/data-analysis",
|
||||
stats: { reports: 1258, insights: 856 }
|
||||
},
|
||||
{
|
||||
icon: <TrendingUp className="w-8 h-8" />,
|
||||
title: "趋势预测",
|
||||
description: "预测用户行为和价值变化趋势",
|
||||
color: "from-purple-500 to-pink-500",
|
||||
href: "/platform/ai-assistant/trend-prediction",
|
||||
stats: { accuracy: 94.2, predictions: 2450 }
|
||||
},
|
||||
{
|
||||
icon: <Users className="w-8 h-8" />,
|
||||
title: "用户画像",
|
||||
description: "智能生成用户群体画像",
|
||||
color: "from-green-500 to-emerald-500",
|
||||
href: "/platform/ai-assistant/user-profiling",
|
||||
stats: { profiles: 4285, segments: 128 }
|
||||
},
|
||||
{
|
||||
icon: <Target className="w-8 h-8" />,
|
||||
title: "精准推荐",
|
||||
description: "推荐最佳营销策略和用户群",
|
||||
color: "from-orange-500 to-red-500",
|
||||
href: "/platform/ai-assistant/recommendation",
|
||||
stats: { campaigns: 586, conversion: 18.5 }
|
||||
},
|
||||
]
|
||||
|
||||
// 快捷问题
|
||||
const quickQuestions = [
|
||||
"分析今日新增用户特征",
|
||||
"预测本月用户增长趋势",
|
||||
"高价值用户流失预警",
|
||||
"生成本周运营报告",
|
||||
]
|
||||
|
||||
const handleSend = () => {
|
||||
if (!message.trim()) return
|
||||
|
||||
setChatHistory([
|
||||
...chatHistory,
|
||||
{
|
||||
role: "user",
|
||||
content: message,
|
||||
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: "正在分析您的问题,请稍候...",
|
||||
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
||||
},
|
||||
])
|
||||
setMessage("")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center">
|
||||
<Brain className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-gray-900">AI智能助手</h1>
|
||||
<p className="text-gray-600">数据分析、报告生成与智能预测</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* AI状态 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Cpu className="w-5 h-5 text-green-600" />
|
||||
AI运行状态
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">系统状态</span>
|
||||
<Badge variant="outline" className="border-green-500 text-green-600 bg-green-50">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse mr-2"></div>
|
||||
运行中
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">模型版本</span>
|
||||
<span className="text-gray-900 font-semibold">v3.5</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">响应速度</span>
|
||||
<span className="text-gray-900 font-semibold">0.8s</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">准确率</span>
|
||||
<span className="text-green-600 font-semibold">94.2%</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* AI功能卡片 */}
|
||||
<div className="lg:col-span-2 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{aiFeatures.map((feature, index) => (
|
||||
<Link key={index} href={feature.href}>
|
||||
<Card className="hover:shadow-lg transition-shadow cursor-pointer h-full">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
<div className={`w-16 h-16 rounded-xl bg-gradient-to-br ${feature.color} flex items-center justify-center text-white`}>
|
||||
{feature.icon}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-xl font-semibold text-gray-900">{feature.title}</h3>
|
||||
<ArrowRight className="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm">{feature.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-gray-200">
|
||||
{Object.entries(feature.stats).map(([key, value], i) => (
|
||||
<div key={i}>
|
||||
<div className="text-xs text-gray-600 mb-1">{key === 'reports' ? '报告数' : key === 'insights' ? '洞察数' : key === 'accuracy' ? '准确率' : key === 'predictions' ? '预测数' : key === 'profiles' ? '画像数' : key === 'segments' ? '分组数' : key === 'campaigns' ? '活动数' : '转化率'}</div>
|
||||
<div className="text-lg font-semibold text-gray-900">
|
||||
{typeof value === 'number' && value % 1 !== 0 ? `${value}%` : value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 对话区域 */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card className="bg-white/5 border-white/10 h-[calc(100vh-200px)]">
|
||||
<CardHeader className="border-b border-white/10">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<BarChart3 className="w-5 h-5" />
|
||||
AI对话
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6 flex flex-col h-[calc(100%-80px)]">
|
||||
{/* 聊天历史 */}
|
||||
<div className="flex-1 overflow-y-auto space-y-4 mb-6">
|
||||
{chatHistory.map((chat, index) => (
|
||||
<div key={index} className={`flex ${chat.role === "user" ? "justify-end" : "justify-start"}`}>
|
||||
<div
|
||||
className={`max-w-[80%] rounded-2xl p-4 ${
|
||||
chat.role === "user"
|
||||
? "bg-gradient-to-br from-blue-500 to-purple-500 text-white"
|
||||
: "bg-white/10 text-white"
|
||||
}`}
|
||||
>
|
||||
<div className="mb-1">{chat.content}</div>
|
||||
<div className="text-xs opacity-70">{chat.timestamp}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 快捷问题 */}
|
||||
<div className="mb-4 flex flex-wrap gap-2">
|
||||
{quickQuestions.map((question, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setMessage(question)}
|
||||
className="bg-white/5 border-white/10 hover:bg-white/10 text-xs"
|
||||
>
|
||||
<Sparkles className="w-3 h-3 mr-1" />
|
||||
{question}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 输入框 */}
|
||||
<div className="flex items-end gap-3">
|
||||
<Textarea
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
}
|
||||
}}
|
||||
placeholder="输入您的问题,按Enter发送..."
|
||||
className="bg-white/5 border-white/10 text-white resize-none"
|
||||
rows={3}
|
||||
/>
|
||||
<Button onClick={handleSend} className="bg-gradient-to-r from-blue-500 to-purple-500 h-[88px]">
|
||||
<Send className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
313
app/platform/ai-assistant/recommendation/page.tsx
Normal file
313
app/platform/ai-assistant/recommendation/page.tsx
Normal file
@@ -0,0 +1,313 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Target, Users, TrendingUp, Zap, Download, RefreshCw, ArrowRight } from 'lucide-react'
|
||||
|
||||
export default function Recommendation() {
|
||||
const recommendedCampaigns = [
|
||||
{
|
||||
id: 1,
|
||||
title: '美妆新品推广',
|
||||
targetGroup: '高净值女性用户',
|
||||
userCount: 856780,
|
||||
expectedRevenue: 12850000,
|
||||
probability: 82,
|
||||
roi: 3.5,
|
||||
timeframe: '未来7天',
|
||||
actions: ['朋友圈广告', '私聊推送', '限时优惠'],
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '会员升级计划',
|
||||
targetGroup: '活跃普通会员',
|
||||
userCount: 1285670,
|
||||
expectedRevenue: 8560000,
|
||||
probability: 75,
|
||||
roi: 4.2,
|
||||
timeframe: '未来15天',
|
||||
actions: ['会员权益推广', '专属优惠', 'VIP体验券'],
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '沉默用户唤醒',
|
||||
targetGroup: '30天未活跃用户',
|
||||
userCount: 2856780,
|
||||
expectedRevenue: 5680000,
|
||||
probability: 58,
|
||||
roi: 2.8,
|
||||
timeframe: '未来30天',
|
||||
actions: ['优惠券发放', '新品预告', '个性化推荐'],
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '交叉销售',
|
||||
targetGroup: '单品类购买用户',
|
||||
userCount: 1714283,
|
||||
expectedRevenue: 9280000,
|
||||
probability: 68,
|
||||
roi: 3.1,
|
||||
timeframe: '未来14天',
|
||||
actions: ['关联商品推荐', '搭配优惠', '场景营销'],
|
||||
priority: 'medium'
|
||||
},
|
||||
]
|
||||
|
||||
const recommendedSegments = [
|
||||
{
|
||||
name: '高转化潜力群体',
|
||||
count: 428567,
|
||||
conversion: 35.8,
|
||||
value: 580,
|
||||
characteristics: ['近期浏览频繁', '价格接受度高', '品牌认知强']
|
||||
},
|
||||
{
|
||||
name: '复购价值用户',
|
||||
count: 856780,
|
||||
conversion: 28.5,
|
||||
value: 420,
|
||||
characteristics: ['首购满意度高', '客单价适中', '活跃度稳定']
|
||||
},
|
||||
{
|
||||
name: '裂变传播节点',
|
||||
count: 285670,
|
||||
conversion: 22.3,
|
||||
value: 320,
|
||||
characteristics: ['社交影响力大', '分享意愿强', '粉丝基数高']
|
||||
},
|
||||
]
|
||||
|
||||
const contentRecommendations = [
|
||||
{
|
||||
type: '朋友圈内容',
|
||||
theme: '新品上市+限时优惠',
|
||||
targetUsers: 2856780,
|
||||
expectedEngagement: 18.5,
|
||||
bestTime: '20:00-22:00'
|
||||
},
|
||||
{
|
||||
type: '私聊话术',
|
||||
theme: '专属VIP权益介绍',
|
||||
targetUsers: 856780,
|
||||
expectedEngagement: 42.8,
|
||||
bestTime: '10:00-12:00'
|
||||
},
|
||||
{
|
||||
type: '社群活动',
|
||||
theme: '用户互动+抽奖',
|
||||
targetUsers: 1285670,
|
||||
expectedEngagement: 32.5,
|
||||
bestTime: '周末 15:00-17:00'
|
||||
},
|
||||
]
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
return priority === 'high' ? 'border-red-500 text-red-600 bg-red-50' : 'border-yellow-500 text-yellow-600 bg-yellow-50'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">精准推荐</h1>
|
||||
<p className="text-gray-600">AI智能推荐营销策略、目标用户群与内容方案</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
刷新推荐
|
||||
</Button>
|
||||
<Button>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出方案
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 核心指标 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 mb-6">
|
||||
<Card className="bg-gradient-to-br from-purple-50 to-purple-100 border-purple-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">推荐方案数</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-purple-600">586</div>
|
||||
<div className="text-sm text-gray-600">本月生成</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-blue-50 to-blue-100 border-blue-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">平均转化率</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-blue-600">18.5%</div>
|
||||
<Badge variant="outline" className="border-green-500 text-green-600 bg-green-50">+3.2%</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-green-50 to-green-100 border-green-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">预计收益</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-green-600">¥36.4M</div>
|
||||
<div className="text-sm text-gray-600">近期方案</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-orange-50 to-orange-100 border-orange-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">平均ROI</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-orange-600">3.4x</div>
|
||||
<div className="text-sm text-gray-600">投入产出比</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 推荐营销方案 */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Target className="w-5 h-5" />
|
||||
推荐营销方案
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{recommendedCampaigns.map((campaign) => (
|
||||
<div key={campaign.id} className="p-6 bg-white border border-gray-200 rounded-lg hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-xl font-bold text-gray-900">{campaign.title}</h3>
|
||||
<Badge variant="outline" className={getPriorityColor(campaign.priority)}>
|
||||
{campaign.priority === 'high' ? '高优先级' : '中优先级'}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-gray-300 text-gray-600">
|
||||
{campaign.timeframe}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-gray-600">目标群体: {campaign.targetGroup}</p>
|
||||
</div>
|
||||
<Button>
|
||||
执行方案
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-6 mb-4">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-1">目标用户数</div>
|
||||
<div className="text-lg font-semibold text-gray-900">{campaign.userCount.toLocaleString()}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-1">预期收益</div>
|
||||
<div className="text-lg font-semibold text-green-600">¥{(campaign.expectedRevenue / 10000).toFixed(1)}万</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-1">成功概率</div>
|
||||
<div className="text-lg font-semibold text-blue-600">{campaign.probability}%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-1">预期ROI</div>
|
||||
<div className="text-lg font-semibold text-purple-600">{campaign.roi}x</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-2">推荐动作</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{campaign.actions.map((action, index) => (
|
||||
<Badge key={index} variant="outline" className="border-blue-300 text-blue-600 bg-blue-50">
|
||||
<Zap className="w-3 h-3 mr-1" />
|
||||
{action}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 推荐目标群体 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
{recommendedSegments.map((segment, index) => (
|
||||
<Card key={index}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 text-lg">{segment.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 mb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">用户数</span>
|
||||
<span className="text-lg font-semibold text-gray-900">{segment.count.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">预期转化率</span>
|
||||
<span className="text-lg font-semibold text-green-600">{segment.conversion}%</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">人均价值</span>
|
||||
<span className="text-lg font-semibold text-blue-600">¥{segment.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-3 border-t border-gray-200">
|
||||
<div className="text-sm text-gray-600 mb-2">群体特征</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{segment.characteristics.map((char, i) => (
|
||||
<Badge key={i} variant="outline" className="border-gray-300 text-gray-700 text-xs">
|
||||
{char}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 内容推荐 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
内容推荐
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{contentRecommendations.map((content, index) => (
|
||||
<div key={index} className="p-4 bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg border border-blue-200">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold text-gray-900 mb-1">{content.type}</h4>
|
||||
<p className="text-gray-600">{content.theme}</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="border-purple-500 text-purple-600 bg-purple-50">
|
||||
最佳时间: {content.bestTime}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-600">目标触达: </span>
|
||||
<span className="font-semibold text-gray-900">{content.targetUsers.toLocaleString()} 人</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">预期互动率: </span>
|
||||
<span className="font-semibold text-green-600">{content.expectedEngagement}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
222
app/platform/ai-assistant/trend-prediction/page.tsx
Normal file
222
app/platform/ai-assistant/trend-prediction/page.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { TrendingUp, TrendingDown, AlertCircle, Download, RefreshCw } from 'lucide-react'
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts'
|
||||
|
||||
export default function TrendPrediction() {
|
||||
const userGrowthPrediction = [
|
||||
{ date: '2024-06', actual: 428, predicted: null },
|
||||
{ date: '2024-07', actual: null, predicted: 445 },
|
||||
{ date: '2024-08', actual: null, predicted: 468 },
|
||||
{ date: '2024-09', actual: null, predicted: 495 },
|
||||
{ date: '2024-10', actual: null, predicted: 528 },
|
||||
{ date: '2024-11', actual: null, predicted: 565 },
|
||||
{ date: '2024-12', actual: null, predicted: 608 },
|
||||
]
|
||||
|
||||
const valuePrediction = [
|
||||
{ date: '2024-06', actual: 125.8, predicted: null },
|
||||
{ date: '2024-07', actual: null, predicted: 132.5 },
|
||||
{ date: '2024-08', actual: null, predicted: 141.2 },
|
||||
{ date: '2024-09', actual: null, predicted: 151.8 },
|
||||
{ date: '2024-10', actual: null, predicted: 164.5 },
|
||||
{ date: '2024-11', actual: null, predicted: 179.2 },
|
||||
{ date: '2024-12', actual: null, predicted: 196.5 },
|
||||
]
|
||||
|
||||
const churnRiskData = [
|
||||
{ segment: '高价值用户', risk: 8.5, count: 1285, trend: 'down' },
|
||||
{ segment: 'VIP用户', risk: 12.3, count: 856, trend: 'stable' },
|
||||
{ segment: '活跃用户', risk: 18.6, count: 12856, trend: 'up' },
|
||||
{ segment: '沉默用户', risk: 45.8, count: 28560, trend: 'up' },
|
||||
]
|
||||
|
||||
const opportunityData = [
|
||||
{ name: '新品推广机会', potential: '¥85.6M', probability: 78, timeframe: '未来30天' },
|
||||
{ name: '复购唤醒', potential: '¥56.2M', probability: 65, timeframe: '未来15天' },
|
||||
{ name: '会员升级', potential: '¥38.5M', probability: 82, timeframe: '未来7天' },
|
||||
{ name: '交叉销售', potential: '¥92.8M', probability: 58, timeframe: '未来45天' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">趋势预测</h1>
|
||||
<p className="text-gray-600">用户增长预测、价值预测与流失预警</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
重新预测
|
||||
</Button>
|
||||
<Button>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出报告
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 预测指标 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 mb-6">
|
||||
<Card className="bg-gradient-to-br from-blue-50 to-blue-100 border-blue-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">下月预测用户数</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-blue-600 mb-1">445M</div>
|
||||
<div className="text-sm text-gray-600">+3.9% 增长</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-green-50 to-green-100 border-green-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">下月预测价值</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-green-600 mb-1">¥132.5亿</div>
|
||||
<div className="text-sm text-gray-600">+5.3% 增长</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-orange-50 to-orange-100 border-orange-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">流失风险用户</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-orange-600 mb-1">42.5K</div>
|
||||
<div className="text-sm text-gray-600">需重点关注</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-purple-50 to-purple-100 border-purple-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">预测准确率</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-purple-600 mb-1">94.2%</div>
|
||||
<div className="text-sm text-gray-600">基于历史数据</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 趋势图表 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-blue-600" />
|
||||
用户增长预测
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={userGrowthPrediction}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||
<XAxis dataKey="date" stroke="#6b7280" />
|
||||
<YAxis stroke="#6b7280" />
|
||||
<Tooltip />
|
||||
<Line type="monotone" dataKey="actual" stroke="#3b82f6" strokeWidth={2} name="实际值" />
|
||||
<Line type="monotone" dataKey="predicted" stroke="#8b5cf6" strokeWidth={2} strokeDasharray="5 5" name="预测值" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-green-600" />
|
||||
资产价值预测
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={valuePrediction}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||
<XAxis dataKey="date" stroke="#6b7280" />
|
||||
<YAxis stroke="#6b7280" />
|
||||
<Tooltip />
|
||||
<Area type="monotone" dataKey="actual" stroke="#10b981" fill="#10b981" fillOpacity={0.3} name="实际值" />
|
||||
<Area type="monotone" dataKey="predicted" stroke="#8b5cf6" fill="#8b5cf6" fillOpacity={0.2} strokeDasharray="5 5" name="预测值" />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 流失风险预警 */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<AlertCircle className="w-5 h-5 text-orange-600" />
|
||||
流失风险预警
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{churnRiskData.map((item, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center gap-4">
|
||||
<div>
|
||||
<div className="text-lg font-semibold text-gray-900">{item.segment}</div>
|
||||
<div className="text-sm text-gray-600">{item.count.toLocaleString()} 用户</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-orange-600">{item.risk}%</div>
|
||||
<div className="text-xs text-gray-600">流失风险</div>
|
||||
</div>
|
||||
{item.trend === 'down' ? (
|
||||
<TrendingDown className="w-6 h-6 text-green-600" />
|
||||
) : item.trend === 'up' ? (
|
||||
<TrendingUp className="w-6 h-6 text-red-600" />
|
||||
) : (
|
||||
<div className="w-6 h-6 flex items-center justify-center">
|
||||
<div className="w-4 h-0.5 bg-gray-400"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 商机预测 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-purple-600" />
|
||||
商机预测
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{opportunityData.map((item, index) => (
|
||||
<div key={index} className="p-4 bg-gradient-to-br from-purple-50 to-pink-50 rounded-lg border border-purple-200">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-lg font-semibold text-gray-900">{item.name}</h3>
|
||||
<Badge variant="outline" className="border-purple-500 text-purple-600 bg-purple-50">
|
||||
{item.timeframe}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-end justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-1">预测收益</div>
|
||||
<div className="text-2xl font-bold text-purple-600">{item.potential}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-gray-600 mb-1">成功概率</div>
|
||||
<div className="text-2xl font-bold text-green-600">{item.probability}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
239
app/platform/ai-assistant/user-profiling/page.tsx
Normal file
239
app/platform/ai-assistant/user-profiling/page.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Users, Tag, Download, RefreshCw, TrendingUp } from 'lucide-react'
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar } from 'recharts'
|
||||
|
||||
export default function UserProfiling() {
|
||||
const ageDistribution = [
|
||||
{ range: '18-25', count: 856780, percentage: 20 },
|
||||
{ range: '26-35', count: 1500000, percentage: 35 },
|
||||
{ range: '36-45', count: 1285670, percentage: 30 },
|
||||
{ range: '46-55', count: 428567, percentage: 10 },
|
||||
{ range: '55+', count: 214284, percentage: 5 },
|
||||
]
|
||||
|
||||
const behaviorProfile = [
|
||||
{ aspect: '消费能力', value: 85 },
|
||||
{ aspect: '活跃度', value: 72 },
|
||||
{ aspect: '互动频率', value: 68 },
|
||||
{ aspect: '忠诚度', value: 78 },
|
||||
{ aspect: '推荐意愿', value: 62 },
|
||||
{ aspect: '复购率', value: 75 },
|
||||
]
|
||||
|
||||
const segments = [
|
||||
{
|
||||
name: '高净值群体',
|
||||
count: 1284567,
|
||||
characteristics: ['消费能力强', '忠诚度高', '品牌认知强'],
|
||||
value: 580,
|
||||
color: 'purple'
|
||||
},
|
||||
{
|
||||
name: '年轻潮流族',
|
||||
count: 2856780,
|
||||
characteristics: ['追求时尚', '社交活跃', '冲动消费'],
|
||||
value: 280,
|
||||
color: 'blue'
|
||||
},
|
||||
{
|
||||
name: '理性消费者',
|
||||
count: 4285670,
|
||||
characteristics: ['价格敏感', '重视品质', '对比研究'],
|
||||
value: 180,
|
||||
color: 'green'
|
||||
},
|
||||
{
|
||||
name: '价格敏感型',
|
||||
count: 1714283,
|
||||
characteristics: ['促销驱动', '低频复购', '优惠敏感'],
|
||||
value: 85,
|
||||
color: 'yellow'
|
||||
},
|
||||
]
|
||||
|
||||
const interestTags = [
|
||||
{ tag: '美妆护肤', count: 2856780, heat: 95 },
|
||||
{ tag: '时尚穿搭', count: 2145680, heat: 88 },
|
||||
{ tag: '健康养生', count: 1856780, heat: 76 },
|
||||
{ tag: '数码科技', count: 1428567, heat: 72 },
|
||||
{ tag: '母婴育儿', count: 1285670, heat: 68 },
|
||||
{ tag: '美食烹饪', count: 985670, heat: 58 },
|
||||
{ tag: '运动健身', count: 856780, heat: 52 },
|
||||
{ tag: '旅游出行', count: 714283, heat: 45 },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">用户画像</h1>
|
||||
<p className="text-gray-600">智能分析用户群体特征与行为偏好</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
重新分析
|
||||
</Button>
|
||||
<Button>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出画像
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 核心指标 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 mb-6">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">用户群体数</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">128</div>
|
||||
<div className="text-sm text-gray-600">覆盖42.8亿用户</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">标签覆盖率</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">87.5%</div>
|
||||
<Badge variant="outline" className="border-green-500 text-green-600 bg-green-50">优秀</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">画像完整度</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">92.3%</div>
|
||||
<Badge variant="outline" className="border-blue-500 text-blue-600 bg-blue-50">优秀</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">更新频率</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">实时</div>
|
||||
<div className="text-sm text-gray-600">每小时更新</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 年龄分布与行为画像 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Users className="w-5 h-5" />
|
||||
年龄分布
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={ageDistribution}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||
<XAxis dataKey="range" stroke="#6b7280" />
|
||||
<YAxis stroke="#6b7280" />
|
||||
<Tooltip />
|
||||
<Bar dataKey="count" fill="#8b5cf6" name="用户数" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
行为画像
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<RadarChart data={behaviorProfile}>
|
||||
<PolarGrid stroke="#e5e7eb" />
|
||||
<PolarAngleAxis dataKey="aspect" stroke="#6b7280" />
|
||||
<PolarRadiusAxis stroke="#6b7280" />
|
||||
<Radar name="行为指数" dataKey="value" stroke="#8b5cf6" fill="#8b5cf6" fillOpacity={0.5} />
|
||||
<Tooltip />
|
||||
</RadarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 用户群体细分 */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Users className="w-5 h-5" />
|
||||
用户群体细分
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{segments.map((segment, index) => (
|
||||
<div key={index} className={`p-6 bg-gradient-to-br from-${segment.color}-50 to-${segment.color}-100 rounded-lg border border-${segment.color}-200`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-xl font-bold text-gray-900">{segment.name}</h3>
|
||||
<Badge variant="outline" className={`border-${segment.color}-500 text-${segment.color}-600 bg-${segment.color}-50`}>
|
||||
{segment.count.toLocaleString()} 人
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-2 mb-4">
|
||||
{segment.characteristics.map((char, i) => (
|
||||
<Badge key={i} variant="outline" className="mr-2 border-gray-300 text-gray-700">
|
||||
{char}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
人均价值: <span className="text-lg font-semibold text-gray-900">¥{segment.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 兴趣标签热度 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Tag className="w-5 h-5" />
|
||||
兴趣标签热度
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{interestTags.map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-gray-700 font-medium">{item.tag}</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-gray-600">{item.count.toLocaleString()} 人</span>
|
||||
<span className="text-sm font-semibold text-blue-600">热度 {item.heat}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"
|
||||
style={{ width: `${item.heat}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
399
app/platform/dashboard/page.tsx
Normal file
399
app/platform/dashboard/page.tsx
Normal file
@@ -0,0 +1,399 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Users, TrendingUp, Database, Activity, Zap, DollarSign, BarChart3, RefreshCw, AlertCircle, CheckCircle2, Clock, ArrowUpRight, ArrowDownRight, Cpu, HardDrive, Globe, Shield } from 'lucide-react'
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'
|
||||
|
||||
export default function PlatformDashboard() {
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [lastUpdate, setLastUpdate] = useState(new Date())
|
||||
|
||||
const monthlyData = [
|
||||
{ month: '1月', users: 3.85, value: 98.5 },
|
||||
{ month: '2月', users: 3.92, value: 102.5 },
|
||||
{ month: '3月', users: 4.05, value: 108.5 },
|
||||
{ month: '4月', users: 4.12, value: 114.2 },
|
||||
{ month: '5月', users: 4.21, value: 119.8 },
|
||||
{ month: '6月', users: 4.29, value: 125.8 },
|
||||
]
|
||||
|
||||
const levelData = [
|
||||
{ name: 'S级', value: 3.0, color: '#8b5cf6' },
|
||||
{ name: 'A级', value: 11.0, color: '#3b82f6' },
|
||||
{ name: 'B级', value: 25.0, color: '#10b981' },
|
||||
{ name: 'C级', value: 40.0, color: '#f59e0b' },
|
||||
{ name: 'D级', value: 21.0, color: '#9ca3af' },
|
||||
]
|
||||
|
||||
// 模拟真实平台数据
|
||||
const platformStats = {
|
||||
totalUsers: 4285670000, // 42.8亿用户
|
||||
totalProjects: 156, // 156个项目
|
||||
totalEnterprises: 48, // 48家企业
|
||||
totalWeChatAccounts: 892, // 892个微信号
|
||||
dataCoverage: 87.3, // 数据覆盖率
|
||||
assetValue: 12580000000, // 125.8亿资产价值
|
||||
todayGrowth: 2845000, // 今日新增用户
|
||||
activeRate: 68.5, // 活跃率
|
||||
aiModelStatus: "running", // AI运行状态
|
||||
dataSync: 94.2, // 数据同步进度
|
||||
}
|
||||
|
||||
// 项目分布数据
|
||||
const projectDistribution = [
|
||||
{ name: "美妆行业", projects: 42, users: 1285000000, value: 3850000000, growth: 12.5 },
|
||||
{ name: "教育培训", projects: 28, users: 856000000, value: 2280000000, growth: 8.3 },
|
||||
{ name: "电商零售", projects: 35, users: 1124000000, value: 3560000000, growth: 15.2 },
|
||||
{ name: "金融服务", projects: 18, users: 624000000, value: 1950000000, growth: 6.8 },
|
||||
{ name: "其他行业", projects: 33, users: 396670000, value: 940000000, growth: 4.2 },
|
||||
]
|
||||
|
||||
// 价值评分分布 (S/A/B/C/D级)
|
||||
const valueDistribution = [
|
||||
{ level: "S", count: 128456700, percentage: 3.0, avgValue: 580, color: "bg-purple-500" },
|
||||
{ level: "A", count: 471424550, percentage: 11.0, avgValue: 320, color: "bg-blue-500" },
|
||||
{ level: "B", count: 1071416750, percentage: 25.0, avgValue: 180, color: "bg-green-500" },
|
||||
{ level: "C", count: 1714283500, percentage: 40.0, avgValue: 85, color: "bg-yellow-500" },
|
||||
{ level: "D", count: 900088500, percentage: 21.0, avgValue: 25, color: "bg-gray-400" },
|
||||
]
|
||||
|
||||
// AI模型运行数据
|
||||
const aiModels = [
|
||||
{ name: "数据清洗模型", status: "running", accuracy: 96.8, processedToday: 2845000, avgTime: 1.2 },
|
||||
{ name: "用户标签模型", status: "running", accuracy: 94.5, processedToday: 2845000, avgTime: 0.8 },
|
||||
{ name: "价值预测模型", status: "running", accuracy: 91.2, processedToday: 1580000, avgTime: 2.5 },
|
||||
{ name: "流失预警模型", status: "training", accuracy: 88.9, processedToday: 0, avgTime: 0 },
|
||||
]
|
||||
|
||||
// 数据流转实时状态
|
||||
const dataFlow = {
|
||||
incoming: 4250, // 每秒接入数据
|
||||
processing: 3890, // 每秒处理数据
|
||||
outgoing: 3650, // 每秒输出数据
|
||||
queueSize: 12580, // 队列大小
|
||||
}
|
||||
|
||||
const refreshData = async () => {
|
||||
setRefreshing(true)
|
||||
// 模拟刷新延迟
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
setLastUpdate(new Date())
|
||||
setRefreshing(false)
|
||||
}
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(1)}B`
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
const formatCurrency = (num: number): string => {
|
||||
if (num >= 100000000) return `${(num / 100000000).toFixed(1)}亿`
|
||||
if (num >= 10000) return `${(num / 10000).toFixed(1)}万`
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold mb-2 text-gray-900">平台总览</h1>
|
||||
<p className="text-gray-600">神射手数据资产中台 - 实时监控</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Badge variant="outline" className="border-green-500 text-green-600 bg-green-50 px-4 py-2">
|
||||
<CheckCircle2 className="w-4 h-4 mr-2" />
|
||||
系统正常运行
|
||||
</Badge>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={refreshData}
|
||||
disabled={refreshing}
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${refreshing ? "animate-spin" : ""}`} />
|
||||
刷新
|
||||
</Button>
|
||||
<div className="text-sm text-gray-600">
|
||||
最后更新: {lastUpdate.toLocaleTimeString("zh-CN")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 核心指标 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 mb-6">
|
||||
<Card className="bg-gradient-to-br from-purple-50 to-purple-100 border-purple-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2 text-lg">
|
||||
<Users className="w-5 h-5 text-purple-600" />
|
||||
用户资产总量
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-4xl font-bold text-purple-600 mb-2">
|
||||
{formatNumber(platformStats.totalUsers)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<ArrowUpRight className="w-4 h-4 text-green-600" />
|
||||
<span className="text-green-600">+{formatNumber(platformStats.todayGrowth)}</span>
|
||||
<span className="text-gray-600">今日新增</span>
|
||||
</div>
|
||||
<div className="mt-3 text-xs text-gray-600">
|
||||
活跃率: {platformStats.activeRate}%
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-br from-blue-50 to-blue-100 border-blue-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2 text-lg">
|
||||
<DollarSign className="w-5 h-5 text-blue-600" />
|
||||
数据资产价值
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-4xl font-bold text-blue-600 mb-2">
|
||||
¥{formatCurrency(platformStats.assetValue)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<TrendingUp className="w-4 h-4 text-green-600" />
|
||||
<span className="text-green-600">+15.8%</span>
|
||||
<span className="text-gray-600">本月增长</span>
|
||||
</div>
|
||||
<div className="mt-3 text-xs text-gray-600">
|
||||
人均价值: ¥{(platformStats.assetValue / platformStats.totalUsers).toFixed(2)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-br from-green-50 to-green-100 border-green-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2 text-lg">
|
||||
<Globe className="w-5 h-5 text-green-600" />
|
||||
项目与企业
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600 text-sm">企业数</span>
|
||||
<span className="text-2xl font-bold text-green-600">{platformStats.totalEnterprises}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600 text-sm">项目数</span>
|
||||
<span className="text-2xl font-bold text-green-600">{platformStats.totalProjects}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600 text-sm">微信号</span>
|
||||
<span className="text-xl font-bold text-green-600">{platformStats.totalWeChatAccounts}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-br from-orange-50 to-orange-100 border-orange-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2 text-lg">
|
||||
<Cpu className="w-5 h-5 text-orange-600" />
|
||||
AI运行状态
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
|
||||
<span className="text-sm text-green-600">4个模型运行中</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600">平均准确率</span>
|
||||
<span className="text-orange-600 font-semibold">92.9%</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600">今日处理</span>
|
||||
<span className="text-orange-600 font-semibold">{formatNumber(7270000)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600">平均耗时</span>
|
||||
<span className="text-orange-600 font-semibold">1.2s</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
用户增长趋势
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={monthlyData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||
<XAxis dataKey="month" stroke="#6b7280" />
|
||||
<YAxis stroke="#6b7280" />
|
||||
<Tooltip />
|
||||
<Line type="monotone" dataKey="users" stroke="#8b5cf6" strokeWidth={2} name="用户数(亿)" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<DollarSign className="w-5 h-5" />
|
||||
资产价值趋势
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={monthlyData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||
<XAxis dataKey="month" stroke="#6b7280" />
|
||||
<YAxis stroke="#6b7280" />
|
||||
<Tooltip />
|
||||
<Line type="monotone" dataKey="value" stroke="#3b82f6" strokeWidth={2} name="价值(亿)" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<BarChart3 className="w-5 h-5" />
|
||||
价值评分分布
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={levelData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={({ name, value }) => `${name} ${value}%`}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
>
|
||||
{levelData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="grid grid-cols-5 gap-2 mt-4">
|
||||
{valueDistribution.map((item, index) => (
|
||||
<div key={index} className="text-center">
|
||||
<div className={`w-8 h-8 rounded ${item.color} flex items-center justify-center font-bold text-white mx-auto mb-1`}>
|
||||
{item.level}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600">{formatNumber(item.count)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Activity className="w-5 h-5" />
|
||||
数据流转实时状态
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowDownRight className="w-4 h-4 text-blue-600" />
|
||||
<span className="text-gray-700">数据接入</span>
|
||||
</div>
|
||||
<div className="text-xl font-semibold text-blue-600">{dataFlow.incoming}/s</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 bg-yellow-50 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="w-4 h-4 text-yellow-600" />
|
||||
<span className="text-gray-700">数据处理</span>
|
||||
</div>
|
||||
<div className="text-xl font-semibold text-yellow-600">{dataFlow.processing}/s</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowUpRight className="w-4 h-4 text-green-600" />
|
||||
<span className="text-gray-700">数据输出</span>
|
||||
</div>
|
||||
<div className="text-xl font-semibold text-green-600">{dataFlow.outgoing}/s</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 bg-purple-50 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<HardDrive className="w-4 h-4 text-purple-600" />
|
||||
<span className="text-gray-700">队列大小</span>
|
||||
</div>
|
||||
<div className="text-xl font-semibold text-purple-600">{dataFlow.queueSize}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 项目分布 */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<BarChart3 className="w-5 h-5" />
|
||||
项目运营与企业分布
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{projectDistribution.map((item, index) => (
|
||||
<div key={index} className="p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-lg font-semibold text-gray-900">{item.name}</div>
|
||||
<Badge variant="outline" className="border-blue-300 text-blue-600 bg-blue-50">
|
||||
{item.projects} 项目
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowUpRight className="w-4 h-4 text-green-600" />
|
||||
<span className="text-green-600 font-semibold">+{item.growth}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<div className="text-gray-600 mb-1">用户数</div>
|
||||
<div className="text-gray-900 font-semibold">{formatNumber(item.users)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-600 mb-1">资产价值</div>
|
||||
<div className="text-gray-900 font-semibold">¥{formatCurrency(item.value)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-600 mb-1">人均价值</div>
|
||||
<div className="text-gray-900 font-semibold">¥{(item.value / item.users).toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
348
app/platform/data-management/page.tsx
Normal file
348
app/platform/data-management/page.tsx
Normal file
@@ -0,0 +1,348 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Database, RefreshCw, CheckCircle2, AlertTriangle, Search, FileText, Upload, Download, Filter, TrendingUp, Zap, Tag } from 'lucide-react'
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
|
||||
export default function DataManagement() {
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
|
||||
// 数据源管理
|
||||
const dataSources = [
|
||||
{
|
||||
id: 1,
|
||||
name: "私域系统数据源",
|
||||
type: "微信生态",
|
||||
status: "active",
|
||||
lastSync: "2分钟前",
|
||||
users: 2850000000,
|
||||
syncRate: 98.5,
|
||||
dataSize: "850TB",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "电商平台数据",
|
||||
type: "淘宝/京东",
|
||||
status: "active",
|
||||
lastSync: "5分钟前",
|
||||
users: 680000000,
|
||||
syncRate: 95.2,
|
||||
dataSize: "210TB",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "表单收集数据",
|
||||
type: "第三方表单",
|
||||
status: "active",
|
||||
lastSync: "10分钟前",
|
||||
users: 425000000,
|
||||
syncRate: 92.8,
|
||||
dataSize: "125TB",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "碰撞补全数据",
|
||||
type: "数据碰撞",
|
||||
status: "syncing",
|
||||
lastSync: "正在同步",
|
||||
users: 280000000,
|
||||
syncRate: 76.3,
|
||||
dataSize: "95TB",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "项目API数据源",
|
||||
type: "外部API",
|
||||
status: "active",
|
||||
lastSync: "1分钟前",
|
||||
users: 50670000,
|
||||
syncRate: 99.1,
|
||||
dataSize: "18TB",
|
||||
},
|
||||
]
|
||||
|
||||
// 数据清洗进度
|
||||
const cleaningTasks = [
|
||||
{ name: "手机号格式校验", total: 4285670000, processed: 4200000000, progress: 98.0, errors: 1250000 },
|
||||
{ name: "重复数据去除", total: 4285670000, processed: 3850000000, progress: 89.8, errors: 85000000 },
|
||||
{ name: "缺失字段补全", total: 4285670000, processed: 3250000000, progress: 75.8, errors: 125000000 },
|
||||
{ name: "异常值检测", total: 4285670000, processed: 4100000000, progress: 95.7, errors: 8500000 },
|
||||
]
|
||||
|
||||
// 标签系统统计
|
||||
const tagStats = {
|
||||
totalTags: 1258,
|
||||
systemTags: 485,
|
||||
customTags: 773,
|
||||
activeRules: 2850,
|
||||
todayTagged: 2845000,
|
||||
}
|
||||
|
||||
// 数据质量检测
|
||||
const qualityChecks = [
|
||||
{ type: "重复数据", count: 85420000, percentage: 2.0, severity: "warning" },
|
||||
{ type: "异常数据", count: 12850000, percentage: 0.3, severity: "error" },
|
||||
{ type: "缺失字段", count: 325680000, percentage: 7.6, severity: "warning" },
|
||||
{ type: "格式错误", count: 5280000, percentage: 0.12, severity: "error" },
|
||||
]
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(1)}B`
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white p-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold mb-2">数据管理</h1>
|
||||
<p className="text-gray-400">全平台数据源管理、清洗与质量监控</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline" className="bg-white/5 border-white/10 hover:bg-white/10">
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
上传数据
|
||||
</Button>
|
||||
<Button variant="outline" className="bg-white/5 border-white/10 hover:bg-white/10">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出报告
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="sources" className="space-y-6">
|
||||
<TabsList className="bg-white/5 border border-white/10">
|
||||
<TabsTrigger value="sources">数据源管理</TabsTrigger>
|
||||
<TabsTrigger value="cleaning">数据清洗</TabsTrigger>
|
||||
<TabsTrigger value="tags">标签系统</TabsTrigger>
|
||||
<TabsTrigger value="quality">质量检测</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 数据源管理 */}
|
||||
<TabsContent value="sources" className="space-y-6">
|
||||
{/* 搜索栏 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索数据源..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 bg-white/5 border-white/10"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" className="bg-white/5 border-white/10 hover:bg-white/10">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
筛选
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 数据源列表 */}
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{dataSources.map((source) => (
|
||||
<Card key={source.id} className="bg-white/5 border-white/10">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center">
|
||||
<Database className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">{source.name}</h3>
|
||||
<p className="text-sm text-gray-400">{source.type}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
source.status === "active"
|
||||
? "border-green-500/50 text-green-400"
|
||||
: "border-yellow-500/50 text-yellow-400"
|
||||
}
|
||||
>
|
||||
{source.status === "active" ? (
|
||||
<>
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
运行中
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="w-3 h-3 mr-1 animate-spin" />
|
||||
同步中
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
<Button size="sm" variant="outline" className="bg-white/5 border-white/10 hover:bg-white/10">
|
||||
<RefreshCw className="w-3 h-3 mr-1" />
|
||||
同步
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4 mb-3">
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-1">用户数量</div>
|
||||
<div className="text-xl font-bold text-white">{formatNumber(source.users)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-1">同步率</div>
|
||||
<div className="text-xl font-bold text-green-400">{source.syncRate}%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-1">数据量</div>
|
||||
<div className="text-xl font-bold text-blue-400">{source.dataSize}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-1">最后同步</div>
|
||||
<div className="text-sm font-semibold text-white">{source.lastSync}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Progress value={source.syncRate} className="h-2" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 数据清洗 */}
|
||||
<TabsContent value="cleaning" className="space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{cleaningTasks.map((task, index) => (
|
||||
<Card key={index} className="bg-white/5 border-white/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Zap className="w-5 h-5 text-yellow-400" />
|
||||
{task.name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-400">进度</span>
|
||||
<span className="text-2xl font-bold text-green-400">{task.progress}%</span>
|
||||
</div>
|
||||
<Progress value={task.progress} className="h-2" />
|
||||
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-white/10">
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-1">已处理</div>
|
||||
<div className="text-lg font-semibold text-white">{formatNumber(task.processed)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-1">异常数据</div>
|
||||
<div className="text-lg font-semibold text-red-400">{formatNumber(task.errors)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 标签系统 */}
|
||||
<TabsContent value="tags" className="space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6 mb-6">
|
||||
<Card className="bg-gradient-to-br from-purple-500/20 to-purple-900/20 border-purple-500/30">
|
||||
<CardContent className="p-6 text-center">
|
||||
<Tag className="w-8 h-8 text-purple-400 mx-auto mb-2" />
|
||||
<div className="text-3xl font-bold text-purple-400 mb-1">{tagStats.totalTags}</div>
|
||||
<div className="text-sm text-gray-400">标签总数</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-white/5 border-white/10">
|
||||
<CardContent className="p-6 text-center">
|
||||
<FileText className="w-8 h-8 text-blue-400 mx-auto mb-2" />
|
||||
<div className="text-3xl font-bold text-blue-400 mb-1">{tagStats.systemTags}</div>
|
||||
<div className="text-sm text-gray-400">系统标签</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-white/5 border-white/10">
|
||||
<CardContent className="p-6 text-center">
|
||||
<Tag className="w-8 h-8 text-green-400 mx-auto mb-2" />
|
||||
<div className="text-3xl font-bold text-green-400 mb-1">{tagStats.customTags}</div>
|
||||
<div className="text-sm text-gray-400">自定义标签</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-white/5 border-white/10">
|
||||
<CardContent className="p-6 text-center">
|
||||
<Zap className="w-8 h-8 text-yellow-400 mx-auto mb-2" />
|
||||
<div className="text-3xl font-bold text-yellow-400 mb-1">{tagStats.activeRules}</div>
|
||||
<div className="text-sm text-gray-400">活跃规则</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-white/5 border-white/10">
|
||||
<CardContent className="p-6 text-center">
|
||||
<TrendingUp className="w-8 h-8 text-orange-400 mx-auto mb-2" />
|
||||
<div className="text-3xl font-bold text-orange-400 mb-1">{formatNumber(tagStats.todayTagged)}</div>
|
||||
<div className="text-sm text-gray-400">今日打标</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="bg-white/5 border-white/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">标签管理</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-gray-400 text-center py-8">
|
||||
标签详细管理界面开发中...
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 质量检测 */}
|
||||
<TabsContent value="quality" className="space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{qualityChecks.map((check, index) => (
|
||||
<Card key={index} className="bg-white/5 border-white/10">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{check.severity === "error" ? (
|
||||
<AlertTriangle className="w-6 h-6 text-red-400" />
|
||||
) : (
|
||||
<AlertTriangle className="w-6 h-6 text-yellow-400" />
|
||||
)}
|
||||
<h3 className="text-lg font-semibold text-white">{check.type}</h3>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
check.severity === "error"
|
||||
? "border-red-500/50 text-red-400"
|
||||
: "border-yellow-500/50 text-yellow-400"
|
||||
}
|
||||
>
|
||||
{check.severity === "error" ? "严重" : "警告"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-400">问题数量</span>
|
||||
<span className="text-2xl font-bold text-white">{formatNumber(check.count)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-400">占比</span>
|
||||
<span className="text-lg font-semibold text-white">{check.percentage}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
277
app/platform/user-portrait/page.tsx
Normal file
277
app/platform/user-portrait/page.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Users, Search, Tag, TrendingUp, Filter, UserCircle, Mail, Phone, MapPin, Calendar, Activity, Target } from 'lucide-react'
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
|
||||
export default function UserPortrait() {
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
|
||||
// 用户群体分布
|
||||
const userSegments = [
|
||||
{ name: "高价值用户", count: 128456700, percentage: 3.0, avgValue: 580, color: "purple" },
|
||||
{ name: "活跃用户", count: 1285670000, percentage: 30.0, avgValue: 280, color: "blue" },
|
||||
{ name: "沉默用户", count: 1714283500, percentage: 40.0, avgValue: 85, color: "yellow" },
|
||||
{ name: "流失预警", count: 857134000, percentage: 20.0, avgValue: 45, color: "orange" },
|
||||
{ name: "新增用户", count: 300125800, percentage: 7.0, avgValue: 120, color: "green" },
|
||||
]
|
||||
|
||||
// 标签分类统计
|
||||
const tagCategories = [
|
||||
{ name: "基础属性", count: 485, usage: 4285670000, coverage: 100 },
|
||||
{ name: "行为标签", count: 328, usage: 3256780000, coverage: 76 },
|
||||
{ name: "兴趣偏好", count: 256, usage: 2570000000, coverage: 60 },
|
||||
{ name: "消费能力", count: 89, usage: 1714283500, coverage: 40 },
|
||||
{ name: "自定义标签", count: 100, usage: 857134000, coverage: 20 },
|
||||
]
|
||||
|
||||
// 最近用户示例
|
||||
const recentUsers = [
|
||||
{
|
||||
id: "U001285",
|
||||
name: "张**",
|
||||
phone: "138****5678",
|
||||
level: "S",
|
||||
tags: ["高净值", "美妆爱好者", "VIP客户"],
|
||||
value: 5800,
|
||||
lastActive: "2小时前",
|
||||
},
|
||||
{
|
||||
id: "U002456",
|
||||
name: "李**",
|
||||
phone: "186****1234",
|
||||
level: "A",
|
||||
tags: ["活跃用户", "教育培训", "复购客户"],
|
||||
value: 3200,
|
||||
lastActive: "5小时前",
|
||||
},
|
||||
{
|
||||
id: "U003789",
|
||||
name: "王**",
|
||||
phone: "159****9876",
|
||||
level: "B",
|
||||
tags: ["潜力用户", "电商购物", "新客户"],
|
||||
value: 1800,
|
||||
lastActive: "1天前",
|
||||
},
|
||||
{
|
||||
id: "U004123",
|
||||
name: "赵**",
|
||||
phone: "177****5432",
|
||||
level: "C",
|
||||
tags: ["普通用户", "低频互动"],
|
||||
value: 850,
|
||||
lastActive: "3天前",
|
||||
},
|
||||
]
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(1)}B`
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
const getLevelColor = (level: string) => {
|
||||
const colors = {
|
||||
S: "border-purple-500/50 text-purple-400 bg-purple-500/10",
|
||||
A: "border-blue-500/50 text-blue-400 bg-blue-500/10",
|
||||
B: "border-green-500/50 text-green-400 bg-green-500/10",
|
||||
C: "border-yellow-500/50 text-yellow-400 bg-yellow-500/10",
|
||||
D: "border-gray-500/50 text-gray-400 bg-gray-500/10",
|
||||
}
|
||||
return colors[level as keyof typeof colors] || colors.D
|
||||
}
|
||||
|
||||
const getSegmentColor = (color: string) => {
|
||||
const colors = {
|
||||
purple: "from-purple-500/20 to-purple-900/20 border-purple-500/30",
|
||||
blue: "from-blue-500/20 to-blue-900/20 border-blue-500/30",
|
||||
green: "from-green-500/20 to-green-900/20 border-green-500/30",
|
||||
yellow: "from-yellow-500/20 to-yellow-900/20 border-yellow-500/30",
|
||||
orange: "from-orange-500/20 to-orange-900/20 border-orange-500/30",
|
||||
}
|
||||
return colors[color as keyof typeof colors]
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white p-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold mb-2">用户画像</h1>
|
||||
<p className="text-gray-400">用户管理、标签体系与群体分析</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline" className="bg-white/5 border-white/10 hover:bg-white/10">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
高级筛选
|
||||
</Button>
|
||||
<Button className="bg-gradient-to-r from-blue-500 to-purple-500">
|
||||
<Target className="w-4 h-4 mr-2" />
|
||||
创建用户群
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="segments" className="space-y-6">
|
||||
<TabsList className="bg-white/5 border border-white/10">
|
||||
<TabsTrigger value="segments">用户群体</TabsTrigger>
|
||||
<TabsTrigger value="tags">标签体系</TabsTrigger>
|
||||
<TabsTrigger value="users">用户列表</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 用户群体分布 */}
|
||||
<TabsContent value="segments" className="space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
||||
{userSegments.map((segment, index) => (
|
||||
<Card key={index} className={`bg-gradient-to-br ${getSegmentColor(segment.color)}`}>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white text-lg">{segment.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="text-3xl font-bold text-white">{formatNumber(segment.count)}</div>
|
||||
<div className="text-sm text-gray-300">占比 {segment.percentage}%</div>
|
||||
<div className="pt-2 border-t border-white/20">
|
||||
<div className="text-xs text-gray-300 mb-1">人均价值</div>
|
||||
<div className="text-lg font-semibold text-white">¥{segment.avgValue}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 用户增长趋势 */}
|
||||
<Card className="bg-white/5 border-white/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
用户增长趋势
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-64 flex items-center justify-center text-gray-400">
|
||||
用户增长趋势图表区域
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 标签体系 */}
|
||||
<TabsContent value="tags" className="space-y-6">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{tagCategories.map((category, index) => (
|
||||
<Card key={index} className="bg-white/5 border-white/10">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center">
|
||||
<Tag className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">{category.name}</h3>
|
||||
<p className="text-sm text-gray-400">{category.count} 个标签</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="border-blue-500/50 text-blue-400">
|
||||
覆盖率 {category.coverage}%
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">标签数量</div>
|
||||
<div className="text-xl font-bold text-white">{category.count}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">使用用户</div>
|
||||
<div className="text-xl font-bold text-white">{formatNumber(category.usage)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">覆盖率</div>
|
||||
<div className="text-xl font-bold text-green-400">{category.coverage}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 用户列表 */}
|
||||
<TabsContent value="users" className="space-y-6">
|
||||
{/* 搜索栏 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索用户手机号、姓名、标签..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 bg-white/5 border-white/10"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" className="bg-white/5 border-white/10 hover:bg-white/10">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
筛选
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 用户卡片列表 */}
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{recentUsers.map((user) => (
|
||||
<Card key={user.id} className="bg-white/5 border-white/10 hover:bg-white/10 transition-colors cursor-pointer">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="w-12 h-12">
|
||||
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-purple-500">
|
||||
{user.name[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<span className="text-lg font-semibold text-white">{user.name}</span>
|
||||
<Badge variant="outline" className={getLevelColor(user.level)}>
|
||||
{user.level}级
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-400">
|
||||
<div className="flex items-center gap-1">
|
||||
<Phone className="w-3 h-3" />
|
||||
{user.phone}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Activity className="w-3 h-3" />
|
||||
{user.lastActive}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-gray-400 mb-1">用户价值</div>
|
||||
<div className="text-2xl font-bold text-green-400">¥{user.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
{user.tags.map((tag, index) => (
|
||||
<Badge key={index} variant="outline" className="border-blue-500/30 text-blue-400">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
322
app/platform/value-assessment/page.tsx
Normal file
322
app/platform/value-assessment/page.tsx
Normal file
@@ -0,0 +1,322 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { DollarSign, TrendingUp, BarChart3, Calculator, Target, Users, ArrowUpRight, ArrowDownRight } from 'lucide-react'
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
|
||||
export default function ValueAssessment() {
|
||||
// 价值评估模型数据
|
||||
const valueModels = [
|
||||
{
|
||||
name: "RFM价值模型",
|
||||
description: "基于最近购买、购买频率、购买金额",
|
||||
totalValue: 8560000000,
|
||||
avgValue: 199.8,
|
||||
coverage: 92.5,
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
name: "用户生命周期价值",
|
||||
description: "预测用户终身价值(LTV)",
|
||||
totalValue: 12580000000,
|
||||
avgValue: 293.5,
|
||||
coverage: 78.3,
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
name: "行为价值模型",
|
||||
description: "基于用户行为数据评估",
|
||||
totalValue: 6780000000,
|
||||
avgValue: 158.2,
|
||||
coverage: 85.7,
|
||||
status: "training",
|
||||
},
|
||||
]
|
||||
|
||||
// 项目价值分布
|
||||
const projectValues = [
|
||||
{ name: "美妆行业项目A", users: 45600000, value: 2856000000, growth: 15.8, level: "A" },
|
||||
{ name: "教育培训项目B", users: 32800000, value: 1968000000, growth: 12.3, level: "A" },
|
||||
{ name: "电商零售项目C", users: 58900000, value: 3534000000, growth: 18.5, level: "S" },
|
||||
{ name: "金融服务项目D", users: 28500000, value: 1995000000, growth: 8.7, level: "B" },
|
||||
{ name: "本地生活项目E", users: 19200000, value: 1152000000, growth: 6.5, level: "B" },
|
||||
]
|
||||
|
||||
// 微信号价值排名
|
||||
const wechatRanking = [
|
||||
{ name: "微信号001", users: 2856000, value: 168560000, avgValue: 59.0, rank: 1 },
|
||||
{ name: "微信号002", users: 2345000, value: 145780000, avgValue: 62.2, rank: 2 },
|
||||
{ name: "微信号003", users: 1980000, value: 128640000, avgValue: 65.0, rank: 3 },
|
||||
{ name: "微信号004", users: 1756000, value: 112340000, avgValue: 64.0, rank: 4 },
|
||||
{ name: "微信号005", users: 1520000, value: 95600000, avgValue: 62.9, rank: 5 },
|
||||
]
|
||||
|
||||
// 价值增长趋势
|
||||
const growthTrend = [
|
||||
{ month: "1月", value: 9850000000, growth: 8.5 },
|
||||
{ month: "2月", value: 10250000000, growth: 4.1 },
|
||||
{ month: "3月", value: 10850000000, growth: 5.9 },
|
||||
{ month: "4月", value: 11420000000, growth: 5.3 },
|
||||
{ month: "5月", value: 11980000000, growth: 4.9 },
|
||||
{ month: "6月", value: 12580000000, growth: 5.0 },
|
||||
]
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(1)}B`
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
const formatCurrency = (num: number): string => {
|
||||
if (num >= 100000000) return `${(num / 100000000).toFixed(1)}亿`
|
||||
if (num >= 10000) return `${(num / 10000).toFixed(1)}万`
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
const getLevelColor = (level: string) => {
|
||||
const colors = {
|
||||
S: "border-purple-500/50 text-purple-400 bg-purple-500/10",
|
||||
A: "border-blue-500/50 text-blue-400 bg-blue-500/10",
|
||||
B: "border-green-500/50 text-green-400 bg-green-500/10",
|
||||
C: "border-yellow-500/50 text-yellow-400 bg-yellow-500/10",
|
||||
D: "border-gray-500/50 text-gray-400 bg-gray-500/10",
|
||||
}
|
||||
return colors[level as keyof typeof colors] || colors.D
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white p-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold mb-2">价值评估</h1>
|
||||
<p className="text-gray-400">用户价值分析、项目价值统计与预测</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline" className="bg-white/5 border-white/10 hover:bg-white/10">
|
||||
<Calculator className="w-4 h-4 mr-2" />
|
||||
价值计算器
|
||||
</Button>
|
||||
<Button className="bg-gradient-to-r from-blue-500 to-purple-500">
|
||||
<BarChart3 className="w-4 h-4 mr-2" />
|
||||
生成报告
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 核心价值指标 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
<Card className="bg-gradient-to-br from-purple-500/20 to-purple-900/20 border-purple-500/30">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<DollarSign className="w-5 h-5" />
|
||||
平台总价值
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-4xl font-bold text-purple-400 mb-2">¥{formatCurrency(12580000000)}</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<ArrowUpRight className="w-4 h-4 text-green-400" />
|
||||
<span className="text-green-400">+5.0%</span>
|
||||
<span className="text-gray-400">本月增长</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-br from-blue-500/20 to-blue-900/20 border-blue-500/30">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Users className="w-5 h-5" />
|
||||
人均价值
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-4xl font-bold text-blue-400 mb-2">¥293.5</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<ArrowUpRight className="w-4 h-4 text-green-400" />
|
||||
<span className="text-green-400">+2.8%</span>
|
||||
<span className="text-gray-400">环比提升</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-br from-green-500/20 to-green-900/20 border-green-500/30">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
价值覆盖率
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-4xl font-bold text-green-400 mb-2">85.5%</div>
|
||||
<div className="text-sm text-gray-400">已评估用户占比</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="models" className="space-y-6">
|
||||
<TabsList className="bg-white/5 border border-white/10">
|
||||
<TabsTrigger value="models">价值模型</TabsTrigger>
|
||||
<TabsTrigger value="projects">项目价值</TabsTrigger>
|
||||
<TabsTrigger value="wechat">微信号排名</TabsTrigger>
|
||||
<TabsTrigger value="trend">增长趋势</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 价值模型 */}
|
||||
<TabsContent value="models" className="space-y-6">
|
||||
{valueModels.map((model, index) => (
|
||||
<Card key={index} className="bg-white/5 border-white/10">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-white text-xl mb-1">{model.name}</CardTitle>
|
||||
<p className="text-sm text-gray-400">{model.description}</p>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
model.status === "active"
|
||||
? "border-green-500/50 text-green-400"
|
||||
: "border-yellow-500/50 text-yellow-400"
|
||||
}
|
||||
>
|
||||
{model.status === "active" ? "运行中" : "训练中"}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-3 gap-6 mb-4">
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-1">模型总价值</div>
|
||||
<div className="text-2xl font-bold text-white">¥{formatCurrency(model.totalValue)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-1">人均价值</div>
|
||||
<div className="text-2xl font-bold text-blue-400">¥{model.avgValue}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-1">覆盖率</div>
|
||||
<div className="text-2xl font-bold text-green-400">{model.coverage}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={model.coverage} className="h-2" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
{/* 项目价值 */}
|
||||
<TabsContent value="projects" className="space-y-6">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{projectValues.map((project, index) => (
|
||||
<Card key={index} className="bg-white/5 border-white/10">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-lg font-semibold text-white">{project.name}</div>
|
||||
<Badge variant="outline" className={getLevelColor(project.level)}>
|
||||
{project.level}级项目
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowUpRight className="w-4 h-4 text-green-400" />
|
||||
<span className="text-green-400 font-semibold">+{project.growth}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">用户数</div>
|
||||
<div className="text-lg font-semibold text-white">{formatNumber(project.users)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">项目价值</div>
|
||||
<div className="text-lg font-semibold text-green-400">¥{formatCurrency(project.value)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">人均价值</div>
|
||||
<div className="text-lg font-semibold text-blue-400">
|
||||
¥{(project.value / project.users).toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">增长率</div>
|
||||
<div className="text-lg font-semibold text-white">{project.growth}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 微信号排名 */}
|
||||
<TabsContent value="wechat" className="space-y-6">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{wechatRanking.map((wechat, index) => (
|
||||
<Card key={index} className="bg-white/5 border-white/10">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div
|
||||
className={`w-12 h-12 rounded-full flex items-center justify-center text-2xl font-bold ${
|
||||
wechat.rank === 1
|
||||
? "bg-gradient-to-br from-yellow-400 to-orange-500"
|
||||
: wechat.rank === 2
|
||||
? "bg-gradient-to-br from-gray-300 to-gray-500"
|
||||
: wechat.rank === 3
|
||||
? "bg-gradient-to-br from-orange-400 to-orange-600"
|
||||
: "bg-white/10"
|
||||
}`}
|
||||
>
|
||||
{wechat.rank}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-white mb-1">{wechat.name}</h3>
|
||||
<div className="text-sm text-gray-400">{formatNumber(wechat.users)} 用户</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-green-400">¥{formatCurrency(wechat.value)}</div>
|
||||
<div className="text-sm text-gray-400">人均 ¥{wechat.avgValue}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 增长趋势 */}
|
||||
<TabsContent value="trend" className="space-y-6">
|
||||
<Card className="bg-white/5 border-white/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
价值增长趋势
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{growthTrend.map((item, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-4 bg-white/5 rounded-lg">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-lg font-semibold text-white">{item.month}</div>
|
||||
<div className="text-2xl font-bold text-green-400">¥{formatCurrency(item.value)}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowUpRight className="w-4 h-4 text-green-400" />
|
||||
<span className="text-lg font-semibold text-green-400">+{item.growth}%</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
app/tag-portrait/crowd/loading.tsx
Normal file
3
app/tag-portrait/crowd/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
346
app/tag-portrait/crowd/page.tsx
Normal file
346
app/tag-portrait/crowd/page.tsx
Normal file
@@ -0,0 +1,346 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Target, Plus, Search, Users, Download, Share2, Trash2, Filter, BarChart3, PieChart } from "lucide-react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
|
||||
interface CrowdPack {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
userCount: number
|
||||
rules: string[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
status: "active" | "expired" | "computing"
|
||||
distribution: { label: string; value: number; color: string }[]
|
||||
}
|
||||
|
||||
const MOCK_CROWDS: CrowdPack[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "高价值活跃用户",
|
||||
description: "价值评分>80 AND 近7天活跃",
|
||||
userCount: 125678901,
|
||||
rules: ["价值评分 > 80", "最后登录 < 7天", "消费金额 > 1000"],
|
||||
createdAt: "2025-12-10",
|
||||
updatedAt: "2025-12-12",
|
||||
status: "active",
|
||||
distribution: [
|
||||
{ label: "25-35岁", value: 45, color: "bg-blue-500" },
|
||||
{ label: "35-45岁", value: 30, color: "bg-green-500" },
|
||||
{ label: "其他", value: 25, color: "bg-gray-300" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "流失预警用户",
|
||||
description: "流失风险>0.7 AND 近30天无消费",
|
||||
userCount: 34567890,
|
||||
rules: ["流失风险 > 0.7", "最后消费 > 30天", "活跃度等级 = 低"],
|
||||
createdAt: "2025-12-08",
|
||||
updatedAt: "2025-12-12",
|
||||
status: "active",
|
||||
distribution: [
|
||||
{ label: "一线城市", value: 35, color: "bg-purple-500" },
|
||||
{ label: "二线城市", value: 40, color: "bg-orange-500" },
|
||||
{ label: "其他", value: 25, color: "bg-gray-300" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "新注册潜力用户",
|
||||
description: "注册<30天 AND 有首购",
|
||||
userCount: 8901234,
|
||||
rules: ["注册时间 < 30天", "首购完成 = 是", "消费金额 > 100"],
|
||||
createdAt: "2025-12-05",
|
||||
updatedAt: "2025-12-11",
|
||||
status: "computing",
|
||||
distribution: [
|
||||
{ label: "女性", value: 55, color: "bg-pink-500" },
|
||||
{ label: "男性", value: 45, color: "bg-blue-500" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "品牌忠诚用户",
|
||||
description: "复购率>50% AND 品牌偏好明确",
|
||||
userCount: 56789012,
|
||||
rules: ["复购率 > 50%", "品牌偏好 != 空", "消费频次 > 3次/月"],
|
||||
createdAt: "2025-12-01",
|
||||
updatedAt: "2025-12-10",
|
||||
status: "active",
|
||||
distribution: [
|
||||
{ label: "高端消费", value: 60, color: "bg-yellow-500" },
|
||||
{ label: "中端消费", value: 30, color: "bg-green-500" },
|
||||
{ label: "其他", value: 10, color: "bg-gray-300" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const AVAILABLE_CONDITIONS = [
|
||||
{ category: "用户属性", options: ["性别", "年龄段", "城市等级", "注册时间"] },
|
||||
{ category: "行为标签", options: ["活跃度等级", "消费等级", "复购率", "最后登录", "最后消费"] },
|
||||
{ category: "价值标签", options: ["价值评分", "RFM评分", "CLV预测值"] },
|
||||
{ category: "风险标签", options: ["流失风险", "欺诈风险"] },
|
||||
{ category: "AI标签", options: ["品牌偏好", "价格敏感度", "内容偏好"] },
|
||||
]
|
||||
|
||||
export default function CrowdSelectionPage() {
|
||||
const [crowds, setCrowds] = useState<CrowdPack[]>(MOCK_CROWDS)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [selectedConditions, setSelectedConditions] = useState<string[]>([])
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
const totalUsers = crowds.reduce((sum, c) => sum + c.userCount, 0)
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">人群圈选</h1>
|
||||
<p className="text-gray-500 mt-1">组合条件筛选目标用户群体</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setIsCreating(!isCreating)}
|
||||
className="bg-gradient-to-r from-blue-500 to-purple-500 text-white"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
创建人群包
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Create Panel */}
|
||||
{isCreating && (
|
||||
<Card className="border-none shadow-md bg-white/80 backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">创建人群包</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">人群包名称</label>
|
||||
<Input placeholder="例如:高价值潜在流失用户" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">描述</label>
|
||||
<Input placeholder="简要描述人群特征" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="text-sm font-medium text-gray-700">筛选条件</label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{AVAILABLE_CONDITIONS.map((group) => (
|
||||
<div key={group.category} className="p-4 rounded-xl bg-gray-50">
|
||||
<h4 className="font-medium text-gray-900 mb-3">{group.category}</h4>
|
||||
<div className="space-y-2">
|
||||
{group.options.map((option) => (
|
||||
<div key={option} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={option}
|
||||
checked={selectedConditions.includes(option)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedConditions([...selectedConditions, option])
|
||||
} else {
|
||||
setSelectedConditions(selectedConditions.filter((c) => c !== option))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={option} className="text-sm text-gray-600">
|
||||
{option}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedConditions.length > 0 && (
|
||||
<div className="p-4 rounded-xl bg-blue-50">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-medium text-gray-900">已选条件</span>
|
||||
<Badge className="bg-blue-100 text-blue-700">{selectedConditions.length}个</Badge>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedConditions.map((cond) => (
|
||||
<Badge key={cond} variant="secondary" className="bg-white">
|
||||
{cond}
|
||||
<button
|
||||
onClick={() => setSelectedConditions(selectedConditions.filter((c) => c !== cond))}
|
||||
className="ml-1 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
x
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button variant="outline" onClick={() => setIsCreating(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button variant="outline">预估人数</Button>
|
||||
<Button>保存人群包</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<Target className="w-4 h-4" />
|
||||
人群包数
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{crowds.length}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<Users className="w-4 h-4" />
|
||||
覆盖用户
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{formatNumber(totalUsers)}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<Filter className="w-4 h-4" />
|
||||
可用条件
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{AVAILABLE_CONDITIONS.reduce((sum, g) => sum + g.options.length, 0)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<BarChart3 className="w-4 h-4" />
|
||||
运算中
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{crowds.filter((c) => c.status === "computing").length}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
placeholder="搜索人群包..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 bg-white/60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Crowd List */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{crowds
|
||||
.filter((c) => c.name.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
.map((crowd) => (
|
||||
<Card key={crowd.id} className="border-none shadow-sm bg-white/60 backdrop-blur-sm hover:shadow-md">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold text-gray-900">{crowd.name}</h3>
|
||||
<Badge
|
||||
className={
|
||||
crowd.status === "active"
|
||||
? "bg-green-100 text-green-700"
|
||||
: crowd.status === "computing"
|
||||
? "bg-blue-100 text-blue-700"
|
||||
: "bg-gray-100 text-gray-700"
|
||||
}
|
||||
>
|
||||
{crowd.status === "active" ? "生效中" : crowd.status === "computing" ? "计算中" : "已过期"}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">{crowd.description}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-gray-900">{formatNumber(crowd.userCount)}</div>
|
||||
<div className="text-xs text-gray-500">用户数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mb-4">
|
||||
<div className="text-sm text-gray-600">
|
||||
<span className="font-medium">筛选规则:</span>
|
||||
{crowd.rules.map((rule, i) => (
|
||||
<Badge key={i} variant="secondary" className="ml-1 text-xs">
|
||||
{rule}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Distribution */}
|
||||
<div className="mb-4">
|
||||
<div className="flex h-3 rounded-full overflow-hidden">
|
||||
{crowd.distribution.map((d, i) => (
|
||||
<div key={i} className={`${d.color}`} style={{ width: `${d.value}%` }} title={d.label} />
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between mt-2">
|
||||
{crowd.distribution.map((d, i) => (
|
||||
<div key={i} className="flex items-center gap-1 text-xs text-gray-500">
|
||||
<div className={`w-2 h-2 rounded-full ${d.color}`} />
|
||||
<span>
|
||||
{d.label} {d.value}%
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-4 border-t border-gray-100">
|
||||
<div className="text-xs text-gray-500">更新于 {crowd.updatedAt}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
<Download className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
<Share2 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
<PieChart className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0 text-red-500">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
12
app/tag-portrait/page.tsx
Normal file
12
app/tag-portrait/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
"use client"
|
||||
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect } from "react"
|
||||
|
||||
export default function TagPortraitPage() {
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
router.replace("/tag-portrait/tags")
|
||||
}, [router])
|
||||
return null
|
||||
}
|
||||
3
app/tag-portrait/portrait/loading.tsx
Normal file
3
app/tag-portrait/portrait/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
273
app/tag-portrait/portrait/page.tsx
Normal file
273
app/tag-portrait/portrait/page.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Search, Tags, Activity, TrendingUp, Calendar, MapPin, Phone, Mail, Star } from "lucide-react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
|
||||
interface UserProfile {
|
||||
id: string
|
||||
name: string
|
||||
avatar: string
|
||||
phone: string
|
||||
email: string
|
||||
city: string
|
||||
registerDate: string
|
||||
lastActive: string
|
||||
tags: { name: string; type: string }[]
|
||||
valueScore: number
|
||||
valueLevel: string
|
||||
rfmScores: { r: number; f: number; m: number }
|
||||
behaviors: { name: string; count: number; trend: string }[]
|
||||
timeline: { date: string; event: string; type: string }[]
|
||||
}
|
||||
|
||||
const MOCK_USER: UserProfile = {
|
||||
id: "U10086",
|
||||
name: "张明华",
|
||||
avatar: "/professional-chinese-man.jpg",
|
||||
phone: "138****5678",
|
||||
email: "zhang****@qq.com",
|
||||
city: "上海市",
|
||||
registerDate: "2023-05-15",
|
||||
lastActive: "2025-12-12 14:30:00",
|
||||
tags: [
|
||||
{ name: "高价值用户", type: "value" },
|
||||
{ name: "高复购高客单", type: "behavior" },
|
||||
{ name: "品牌忠诚", type: "ai" },
|
||||
{ name: "25-35岁", type: "basic" },
|
||||
{ name: "一线城市", type: "basic" },
|
||||
{ name: "活跃用户", type: "behavior" },
|
||||
{ name: "低流失风险", type: "model" },
|
||||
{ name: "科技爱好者", type: "ai" },
|
||||
],
|
||||
valueScore: 92,
|
||||
valueLevel: "S",
|
||||
rfmScores: { r: 95, f: 88, m: 90 },
|
||||
behaviors: [
|
||||
{ name: "登录次数", count: 156, trend: "+12%" },
|
||||
{ name: "浏览商品", count: 892, trend: "+8%" },
|
||||
{ name: "下单次数", count: 45, trend: "+15%" },
|
||||
{ name: "分享次数", count: 28, trend: "+5%" },
|
||||
],
|
||||
timeline: [
|
||||
{ date: "2025-12-12 14:30", event: "浏览商品: iPhone 16 Pro", type: "browse" },
|
||||
{ date: "2025-12-12 10:15", event: "完成订单: #ORD20251212001", type: "order" },
|
||||
{ date: "2025-12-11 18:20", event: "添加收藏: MacBook Pro", type: "favorite" },
|
||||
{ date: "2025-12-11 09:00", event: "登录App", type: "login" },
|
||||
{ date: "2025-12-10 20:30", event: "分享商品到微信", type: "share" },
|
||||
],
|
||||
}
|
||||
|
||||
const TAG_COLORS: Record<string, string> = {
|
||||
value: "bg-yellow-100 text-yellow-700",
|
||||
behavior: "bg-green-100 text-green-700",
|
||||
ai: "bg-purple-100 text-purple-700",
|
||||
basic: "bg-blue-100 text-blue-700",
|
||||
model: "bg-orange-100 text-orange-700",
|
||||
}
|
||||
|
||||
const EVENT_COLORS: Record<string, string> = {
|
||||
browse: "bg-blue-500",
|
||||
order: "bg-green-500",
|
||||
favorite: "bg-yellow-500",
|
||||
login: "bg-gray-500",
|
||||
share: "bg-purple-500",
|
||||
}
|
||||
|
||||
export default function UserPortraitPage() {
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [selectedUser, setSelectedUser] = useState<UserProfile>(MOCK_USER)
|
||||
|
||||
const handleSearch = () => {
|
||||
// 模拟搜索
|
||||
console.log("Searching:", searchQuery)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">用户画像</h1>
|
||||
<p className="text-gray-500 mt-1">查看个体用户全景画像</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="flex gap-4">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
placeholder="输入用户ID或手机号搜索..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyPress={(e) => e.key === "Enter" && handleSearch()}
|
||||
className="pl-10 bg-white/60"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleSearch}>搜索</Button>
|
||||
</div>
|
||||
|
||||
{/* User Profile Card */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Left: Basic Info */}
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center text-white text-2xl font-bold">
|
||||
{selectedUser.name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-900">{selectedUser.name}</h2>
|
||||
<p className="text-sm text-gray-500">ID: {selectedUser.id}</p>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Badge className="bg-yellow-100 text-yellow-700">
|
||||
<Star className="w-3 h-3 mr-1" />
|
||||
{selectedUser.valueLevel}级用户
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<Phone className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-gray-600">{selectedUser.phone}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<Mail className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-gray-600">{selectedUser.email}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<MapPin className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-gray-600">{selectedUser.city}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<Calendar className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-gray-600">注册于 {selectedUser.registerDate}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<Activity className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-gray-600">最后活跃 {selectedUser.lastActive}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Value Score */}
|
||||
<div className="mt-6 p-4 rounded-xl bg-gradient-to-br from-yellow-50 to-orange-50">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-gray-600">用户价值分</span>
|
||||
<span className="text-2xl font-bold text-orange-600">{selectedUser.valueScore}</span>
|
||||
</div>
|
||||
<Progress value={selectedUser.valueScore} className="h-2" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Middle: Tags & RFM */}
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Tags className="w-5 h-5" />
|
||||
用户标签
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{selectedUser.tags.map((tag, i) => (
|
||||
<Badge key={i} className={TAG_COLORS[tag.type]}>
|
||||
{tag.name}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-100 pt-4">
|
||||
<h4 className="font-medium text-gray-900 mb-4">RFM模型评分</h4>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-sm mb-1">
|
||||
<span className="text-gray-600">R - 最近消费</span>
|
||||
<span className="font-medium">{selectedUser.rfmScores.r}</span>
|
||||
</div>
|
||||
<Progress value={selectedUser.rfmScores.r} className="h-2" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-sm mb-1">
|
||||
<span className="text-gray-600">F - 消费频率</span>
|
||||
<span className="font-medium">{selectedUser.rfmScores.f}</span>
|
||||
</div>
|
||||
<Progress value={selectedUser.rfmScores.f} className="h-2" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-sm mb-1">
|
||||
<span className="text-gray-600">M - 消费金额</span>
|
||||
<span className="font-medium">{selectedUser.rfmScores.m}</span>
|
||||
</div>
|
||||
<Progress value={selectedUser.rfmScores.m} className="h-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Right: Behaviors & Timeline */}
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-0">
|
||||
<Tabs defaultValue="behaviors" className="w-full">
|
||||
<TabsList className="w-full rounded-none border-b bg-transparent p-0">
|
||||
<TabsTrigger
|
||||
value="behaviors"
|
||||
className="flex-1 rounded-none border-b-2 border-transparent data-[state=active]:border-blue-500"
|
||||
>
|
||||
行为指标
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="timeline"
|
||||
className="flex-1 rounded-none border-b-2 border-transparent data-[state=active]:border-blue-500"
|
||||
>
|
||||
行为轨迹
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="behaviors" className="p-4 space-y-4">
|
||||
{selectedUser.behaviors.map((behavior, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-3 rounded-xl bg-gray-50">
|
||||
<span className="text-gray-600">{behavior.name}</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-bold text-gray-900">{behavior.count}</span>
|
||||
<Badge className="bg-green-100 text-green-700 text-xs">
|
||||
<TrendingUp className="w-3 h-3 mr-1" />
|
||||
{behavior.trend}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="timeline" className="p-4">
|
||||
<div className="space-y-4">
|
||||
{selectedUser.timeline.map((event, i) => (
|
||||
<div key={i} className="flex gap-3">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`w-3 h-3 rounded-full ${EVENT_COLORS[event.type]}`} />
|
||||
{i < selectedUser.timeline.length - 1 && <div className="w-0.5 h-full bg-gray-200 mt-1" />}
|
||||
</div>
|
||||
<div className="flex-1 pb-4">
|
||||
<p className="text-sm text-gray-900">{event.event}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">{event.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
app/tag-portrait/tags/loading.tsx
Normal file
3
app/tag-portrait/tags/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
491
app/tag-portrait/tags/page.tsx
Normal file
491
app/tag-portrait/tags/page.tsx
Normal file
@@ -0,0 +1,491 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Tags,
|
||||
Plus,
|
||||
Search,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
Users,
|
||||
Cpu,
|
||||
Brain,
|
||||
FileText,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
Settings,
|
||||
} from "lucide-react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
|
||||
interface TagCategory {
|
||||
id: string
|
||||
name: string
|
||||
icon: any
|
||||
color: string
|
||||
tags: Tag[]
|
||||
expanded: boolean
|
||||
}
|
||||
|
||||
interface Tag {
|
||||
id: string
|
||||
name: string
|
||||
type: "basic" | "rule" | "model" | "ai"
|
||||
status: "online" | "testing" | "offline"
|
||||
coverage: number
|
||||
userCount: number
|
||||
rule?: string
|
||||
lastUpdate: string
|
||||
}
|
||||
|
||||
const TAG_TYPE_CONFIG = {
|
||||
basic: { label: "基础标签", color: "bg-blue-100 text-blue-700", icon: FileText },
|
||||
rule: { label: "规则标签", color: "bg-green-100 text-green-700", icon: Settings },
|
||||
model: { label: "模型标签", color: "bg-purple-100 text-purple-700", icon: Cpu },
|
||||
ai: { label: "AI标签", color: "bg-orange-100 text-orange-700", icon: Brain },
|
||||
}
|
||||
|
||||
const STATUS_CONFIG = {
|
||||
online: { label: "已上线", color: "bg-green-100 text-green-700" },
|
||||
testing: { label: "测试中", color: "bg-yellow-100 text-yellow-700" },
|
||||
offline: { label: "已下线", color: "bg-gray-100 text-gray-700" },
|
||||
}
|
||||
|
||||
const INITIAL_CATEGORIES: TagCategory[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "人口属性",
|
||||
icon: Users,
|
||||
color: "text-blue-600",
|
||||
expanded: true,
|
||||
tags: [
|
||||
{
|
||||
id: "t1",
|
||||
name: "性别",
|
||||
type: "basic",
|
||||
status: "online",
|
||||
coverage: 99.5,
|
||||
userCount: 4012345678,
|
||||
lastUpdate: "2025-12-12",
|
||||
},
|
||||
{
|
||||
id: "t2",
|
||||
name: "年龄段",
|
||||
type: "rule",
|
||||
status: "online",
|
||||
coverage: 95.2,
|
||||
userCount: 3825678901,
|
||||
rule: "根据身份证计算",
|
||||
lastUpdate: "2025-12-12",
|
||||
},
|
||||
{
|
||||
id: "t3",
|
||||
name: "城市等级",
|
||||
type: "rule",
|
||||
status: "online",
|
||||
coverage: 88.7,
|
||||
userCount: 3567890123,
|
||||
rule: "一线/新一线/二线/三线/其他",
|
||||
lastUpdate: "2025-12-11",
|
||||
},
|
||||
{
|
||||
id: "t4",
|
||||
name: "职业预测",
|
||||
type: "model",
|
||||
status: "testing",
|
||||
coverage: 72.3,
|
||||
userCount: 2908765432,
|
||||
lastUpdate: "2025-12-10",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "消费行为",
|
||||
icon: Tags,
|
||||
color: "text-green-600",
|
||||
expanded: true,
|
||||
tags: [
|
||||
{
|
||||
id: "t5",
|
||||
name: "消费等级",
|
||||
type: "rule",
|
||||
status: "online",
|
||||
coverage: 82.5,
|
||||
userCount: 3318765432,
|
||||
rule: "近90天消费金额分档",
|
||||
lastUpdate: "2025-12-12",
|
||||
},
|
||||
{
|
||||
id: "t6",
|
||||
name: "高复购高客单",
|
||||
type: "rule",
|
||||
status: "online",
|
||||
coverage: 15.2,
|
||||
userCount: 611543210,
|
||||
rule: "近30天消费次数>3 AND 客单价>500",
|
||||
lastUpdate: "2025-12-12",
|
||||
},
|
||||
{
|
||||
id: "t7",
|
||||
name: "价格敏感型",
|
||||
type: "ai",
|
||||
status: "online",
|
||||
coverage: 28.6,
|
||||
userCount: 1152172345,
|
||||
lastUpdate: "2025-12-11",
|
||||
},
|
||||
{
|
||||
id: "t8",
|
||||
name: "品牌偏好",
|
||||
type: "ai",
|
||||
status: "testing",
|
||||
coverage: 45.8,
|
||||
userCount: 1845098765,
|
||||
lastUpdate: "2025-12-10",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "社交互动",
|
||||
icon: Users,
|
||||
color: "text-purple-600",
|
||||
expanded: false,
|
||||
tags: [
|
||||
{
|
||||
id: "t9",
|
||||
name: "活跃度等级",
|
||||
type: "rule",
|
||||
status: "online",
|
||||
coverage: 78.9,
|
||||
userCount: 3176543210,
|
||||
rule: "近7天登录次数分档",
|
||||
lastUpdate: "2025-12-12",
|
||||
},
|
||||
{
|
||||
id: "t10",
|
||||
name: "内容偏好",
|
||||
type: "ai",
|
||||
status: "online",
|
||||
coverage: 65.4,
|
||||
userCount: 2632098765,
|
||||
lastUpdate: "2025-12-11",
|
||||
},
|
||||
{
|
||||
id: "t11",
|
||||
name: "社交影响力",
|
||||
type: "model",
|
||||
status: "online",
|
||||
coverage: 55.2,
|
||||
userCount: 2222345678,
|
||||
lastUpdate: "2025-12-10",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "风险等级",
|
||||
icon: AlertTriangle,
|
||||
color: "text-red-600",
|
||||
expanded: false,
|
||||
tags: [
|
||||
{
|
||||
id: "t12",
|
||||
name: "流失风险",
|
||||
type: "model",
|
||||
status: "online",
|
||||
coverage: 100,
|
||||
userCount: 4028567890,
|
||||
lastUpdate: "2025-12-12",
|
||||
},
|
||||
{
|
||||
id: "t13",
|
||||
name: "欺诈风险",
|
||||
type: "model",
|
||||
status: "online",
|
||||
coverage: 100,
|
||||
userCount: 4028567890,
|
||||
lastUpdate: "2025-12-12",
|
||||
},
|
||||
{
|
||||
id: "t14",
|
||||
name: "高流失风险用户",
|
||||
type: "rule",
|
||||
status: "online",
|
||||
coverage: 8.5,
|
||||
userCount: 342428270,
|
||||
rule: "流失风险概率>0.7",
|
||||
lastUpdate: "2025-12-12",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export default function TagManagementPage() {
|
||||
const [categories, setCategories] = useState<TagCategory[]>(INITIAL_CATEGORIES)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [filterType, setFilterType] = useState<string>("all")
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||||
|
||||
const toggleCategory = (id: string) => {
|
||||
setCategories((prev) => prev.map((cat) => (cat.id === id ? { ...cat, expanded: !cat.expanded } : cat)))
|
||||
}
|
||||
|
||||
const allTags = categories.flatMap((cat) => cat.tags)
|
||||
const stats = {
|
||||
totalTags: allTags.length,
|
||||
onlineTags: allTags.filter((t) => t.status === "online").length,
|
||||
avgCoverage: (allTags.reduce((sum, t) => sum + t.coverage, 0) / allTags.length).toFixed(1),
|
||||
totalCovered: allTags.reduce((sum, t) => sum + t.userCount, 0),
|
||||
}
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">标签体系</h1>
|
||||
<p className="text-gray-500 mt-1">管理用户标签分类与定义</p>
|
||||
</div>
|
||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-gradient-to-r from-blue-500 to-purple-500 text-white">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
创建标签
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>创建新标签</DialogTitle>
|
||||
<DialogDescription>定义标签属性和计算规则</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>标签名称</Label>
|
||||
<Input placeholder="例如:高价值用户" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>所属分类</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择分类" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">人口属性</SelectItem>
|
||||
<SelectItem value="2">消费行为</SelectItem>
|
||||
<SelectItem value="3">社交互动</SelectItem>
|
||||
<SelectItem value="4">风险等级</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>标签类型</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="basic">基础标签 - 直接映射</SelectItem>
|
||||
<SelectItem value="rule">规则标签 - 条件组合</SelectItem>
|
||||
<SelectItem value="model">模型标签 - 机器学习</SelectItem>
|
||||
<SelectItem value="ai">AI标签 - 大模型分析</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>计算规则/描述</Label>
|
||||
<Textarea placeholder="例如:近30天消费次数>3 AND 客单价>500" rows={3} />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsAddDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button>创建并测试</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<Tags className="w-4 h-4" />
|
||||
总标签数
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.totalTags}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
已上线
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-green-600">{stats.onlineTags}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<Users className="w-4 h-4" />
|
||||
平均覆盖率
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.avgCoverage}%</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<Brain className="w-4 h-4" />
|
||||
标签类型
|
||||
</div>
|
||||
<div className="flex gap-1 mt-1">
|
||||
<Badge className="bg-blue-100 text-blue-700 text-xs">
|
||||
基础 {allTags.filter((t) => t.type === "basic").length}
|
||||
</Badge>
|
||||
<Badge className="bg-green-100 text-green-700 text-xs">
|
||||
规则 {allTags.filter((t) => t.type === "rule").length}
|
||||
</Badge>
|
||||
<Badge className="bg-purple-100 text-purple-700 text-xs">
|
||||
模型 {allTags.filter((t) => t.type === "model").length}
|
||||
</Badge>
|
||||
<Badge className="bg-orange-100 text-orange-700 text-xs">
|
||||
AI {allTags.filter((t) => t.type === "ai").length}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Search & Filter */}
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
placeholder="搜索标签..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 bg-white/60"
|
||||
/>
|
||||
</div>
|
||||
<Select value={filterType} onValueChange={setFilterType}>
|
||||
<SelectTrigger className="w-full md:w-40 bg-white/60">
|
||||
<SelectValue placeholder="标签类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部类型</SelectItem>
|
||||
<SelectItem value="basic">基础标签</SelectItem>
|
||||
<SelectItem value="rule">规则标签</SelectItem>
|
||||
<SelectItem value="model">模型标签</SelectItem>
|
||||
<SelectItem value="ai">AI标签</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Tag Tree */}
|
||||
<div className="space-y-4">
|
||||
{categories.map((category) => (
|
||||
<Card key={category.id} className="border-none shadow-sm bg-white/60 backdrop-blur-sm overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleCategory(category.id)}
|
||||
className="w-full flex items-center justify-between p-4 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-xl bg-gray-100 flex items-center justify-center ${category.color}`}>
|
||||
<category.icon className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<h3 className="font-semibold text-gray-900">{category.name}</h3>
|
||||
<p className="text-sm text-gray-500">{category.tags.length} 个标签</p>
|
||||
</div>
|
||||
</div>
|
||||
{category.expanded ? (
|
||||
<ChevronDown className="w-5 h-5 text-gray-400" />
|
||||
) : (
|
||||
<ChevronRight className="w-5 h-5 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{category.expanded && (
|
||||
<div className="border-t border-gray-100">
|
||||
<div className="p-4 space-y-3">
|
||||
{category.tags
|
||||
.filter((tag) => {
|
||||
const matchesSearch = tag.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
const matchesType = filterType === "all" || tag.type === filterType
|
||||
return matchesSearch && matchesType
|
||||
})
|
||||
.map((tag) => {
|
||||
const TypeConfig = TAG_TYPE_CONFIG[tag.type]
|
||||
const TypeIcon = TypeConfig.icon
|
||||
|
||||
return (
|
||||
<div
|
||||
key={tag.id}
|
||||
className="flex items-center justify-between p-3 rounded-xl bg-gray-50 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<TypeIcon className="w-4 h-4 text-gray-400" />
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-900">{tag.name}</span>
|
||||
<Badge className={`text-xs ${TypeConfig.color}`}>{TypeConfig.label}</Badge>
|
||||
<Badge className={`text-xs ${STATUS_CONFIG[tag.status].color}`}>
|
||||
{STATUS_CONFIG[tag.status].label}
|
||||
</Badge>
|
||||
</div>
|
||||
{tag.rule && <p className="text-xs text-gray-500 mt-1">{tag.rule}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-6 text-sm">
|
||||
<div className="text-right">
|
||||
<div className="font-medium text-gray-900">{tag.coverage}%</div>
|
||||
<div className="text-xs text-gray-500">覆盖率</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-medium text-gray-900">{formatNumber(tag.userCount)}</div>
|
||||
<div className="text-xs text-gray-500">用户数</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Settings className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,13 +6,12 @@ import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Users, Search, Tag, TrendingUp, Filter, UserCircle, Mail, Phone, MapPin, Calendar, Activity, Target } from 'lucide-react'
|
||||
import { Search, Tag, TrendingUp, Filter, Phone, Activity, Target } from "lucide-react"
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
|
||||
export default function UserPortrait() {
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
|
||||
// 用户群体分布
|
||||
const userSegments = [
|
||||
{ name: "高价值用户", count: 128456700, percentage: 3.0, avgValue: 580, color: "purple" },
|
||||
{ name: "活跃用户", count: 1285670000, percentage: 30.0, avgValue: 280, color: "blue" },
|
||||
@@ -21,7 +20,6 @@ export default function UserPortrait() {
|
||||
{ name: "新增用户", count: 300125800, percentage: 7.0, avgValue: 120, color: "green" },
|
||||
]
|
||||
|
||||
// 标签分类统计
|
||||
const tagCategories = [
|
||||
{ name: "基础属性", count: 485, usage: 4285670000, coverage: 100 },
|
||||
{ name: "行为标签", count: 328, usage: 3256780000, coverage: 76 },
|
||||
@@ -30,7 +28,6 @@ export default function UserPortrait() {
|
||||
{ name: "自定义标签", count: 100, usage: 857134000, coverage: 20 },
|
||||
]
|
||||
|
||||
// 最近用户示例
|
||||
const recentUsers = [
|
||||
{
|
||||
id: "U001285",
|
||||
@@ -79,40 +76,39 @@ export default function UserPortrait() {
|
||||
|
||||
const getLevelColor = (level: string) => {
|
||||
const colors = {
|
||||
S: "border-purple-500/50 text-purple-400 bg-purple-500/10",
|
||||
A: "border-blue-500/50 text-blue-400 bg-blue-500/10",
|
||||
B: "border-green-500/50 text-green-400 bg-green-500/10",
|
||||
C: "border-yellow-500/50 text-yellow-400 bg-yellow-500/10",
|
||||
D: "border-gray-500/50 text-gray-400 bg-gray-500/10",
|
||||
S: "bg-purple-100 text-purple-700 border-purple-200",
|
||||
A: "bg-blue-100 text-blue-700 border-blue-200",
|
||||
B: "bg-green-100 text-green-700 border-green-200",
|
||||
C: "bg-yellow-100 text-yellow-700 border-yellow-200",
|
||||
D: "bg-gray-100 text-gray-700 border-gray-200",
|
||||
}
|
||||
return colors[level as keyof typeof colors] || colors.D
|
||||
}
|
||||
|
||||
const getSegmentColor = (color: string) => {
|
||||
const colors = {
|
||||
purple: "from-purple-500/20 to-purple-900/20 border-purple-500/30",
|
||||
blue: "from-blue-500/20 to-blue-900/20 border-blue-500/30",
|
||||
green: "from-green-500/20 to-green-900/20 border-green-500/30",
|
||||
yellow: "from-yellow-500/20 to-yellow-900/20 border-yellow-500/30",
|
||||
orange: "from-orange-500/20 to-orange-900/20 border-orange-500/30",
|
||||
purple: "from-purple-500 to-purple-600",
|
||||
blue: "from-blue-500 to-blue-600",
|
||||
green: "from-green-500 to-green-600",
|
||||
yellow: "from-yellow-400 to-orange-500",
|
||||
orange: "from-orange-500 to-red-500",
|
||||
}
|
||||
return colors[color as keyof typeof colors]
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white p-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold mb-2">用户画像</h1>
|
||||
<p className="text-gray-400">用户管理、标签体系与群体分析</p>
|
||||
<h1 className="text-3xl font-bold text-gray-900">用户画像</h1>
|
||||
<p className="text-gray-500 mt-1">用户管理、标签体系与群体分析</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline" className="bg-white/5 border-white/10 hover:bg-white/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline" className="bg-white/60 backdrop-blur-md">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
高级筛选
|
||||
</Button>
|
||||
<Button className="bg-gradient-to-r from-blue-500 to-purple-500">
|
||||
<Button className="bg-gradient-to-r from-blue-500 to-purple-500 text-white">
|
||||
<Target className="w-4 h-4 mr-2" />
|
||||
创建用户群
|
||||
</Button>
|
||||
@@ -120,27 +116,38 @@ export default function UserPortrait() {
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="segments" className="space-y-6">
|
||||
<TabsList className="bg-white/5 border border-white/10">
|
||||
<TabsTrigger value="segments">用户群体</TabsTrigger>
|
||||
<TabsTrigger value="tags">标签体系</TabsTrigger>
|
||||
<TabsTrigger value="users">用户列表</TabsTrigger>
|
||||
<TabsList className="bg-white/60 backdrop-blur-md border border-gray-200 p-1 rounded-xl">
|
||||
<TabsTrigger
|
||||
value="segments"
|
||||
className="rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-sm"
|
||||
>
|
||||
用户群体
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="tags" className="rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-sm">
|
||||
标签体系
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="users" className="rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-sm">
|
||||
用户列表
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 用户群体分布 */}
|
||||
<TabsContent value="segments" className="space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
||||
{userSegments.map((segment, index) => (
|
||||
<Card key={index} className={`bg-gradient-to-br ${getSegmentColor(segment.color)}`}>
|
||||
<Card
|
||||
key={index}
|
||||
className={`bg-gradient-to-br ${getSegmentColor(segment.color)} text-white border-none shadow-md`}
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white text-lg">{segment.name}</CardTitle>
|
||||
<CardTitle className="text-white/90 text-lg">{segment.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="text-3xl font-bold text-white">{formatNumber(segment.count)}</div>
|
||||
<div className="text-sm text-gray-300">占比 {segment.percentage}%</div>
|
||||
<div className="text-3xl font-bold">{formatNumber(segment.count)}</div>
|
||||
<div className="text-sm text-white/80">占比 {segment.percentage}%</div>
|
||||
<div className="pt-2 border-t border-white/20">
|
||||
<div className="text-xs text-gray-300 mb-1">人均价值</div>
|
||||
<div className="text-lg font-semibold text-white">¥{segment.avgValue}</div>
|
||||
<div className="text-xs text-white/70 mb-1">人均价值</div>
|
||||
<div className="text-lg font-semibold">¥{segment.avgValue}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -148,54 +155,52 @@ export default function UserPortrait() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 用户增长趋势 */}
|
||||
<Card className="bg-white/5 border-white/10">
|
||||
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
用户增长趋势
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-64 flex items-center justify-center text-gray-400">
|
||||
<div className="h-64 flex items-center justify-center text-gray-400 bg-gray-50 rounded-xl border border-dashed border-gray-200">
|
||||
用户增长趋势图表区域
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 标签体系 */}
|
||||
<TabsContent value="tags" className="space-y-6">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{tagCategories.map((category, index) => (
|
||||
<Card key={index} className="bg-white/5 border-white/10">
|
||||
<Card key={index} className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center">
|
||||
<Tag className="w-6 h-6" />
|
||||
<div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center">
|
||||
<Tag className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">{category.name}</h3>
|
||||
<p className="text-sm text-gray-400">{category.count} 个标签</p>
|
||||
<h3 className="text-lg font-bold text-gray-900">{category.name}</h3>
|
||||
<p className="text-sm text-gray-500">{category.count} 个标签</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="border-blue-500/50 text-blue-400">
|
||||
<Badge variant="outline" className="bg-blue-50 text-blue-600 border-blue-200">
|
||||
覆盖率 {category.coverage}%
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">标签数量</div>
|
||||
<div className="text-xl font-bold text-white">{category.count}</div>
|
||||
<div className="text-gray-500 mb-1">标签数量</div>
|
||||
<div className="text-xl font-bold text-gray-900">{category.count}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">使用用户</div>
|
||||
<div className="text-xl font-bold text-white">{formatNumber(category.usage)}</div>
|
||||
<div className="text-gray-500 mb-1">使用用户</div>
|
||||
<div className="text-xl font-bold text-gray-900">{formatNumber(category.usage)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">覆盖率</div>
|
||||
<div className="text-xl font-bold text-green-400">{category.coverage}%</div>
|
||||
<div className="text-gray-500 mb-1">覆盖率</div>
|
||||
<div className="text-xl font-bold text-green-600">{category.coverage}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -204,9 +209,7 @@ export default function UserPortrait() {
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 用户列表 */}
|
||||
<TabsContent value="users" className="space-y-6">
|
||||
{/* 搜索栏 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
@@ -214,35 +217,37 @@ export default function UserPortrait() {
|
||||
placeholder="搜索用户手机号、姓名、标签..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 bg-white/5 border-white/10"
|
||||
className="pl-10 bg-white/60 backdrop-blur-md border-gray-200"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" className="bg-white/5 border-white/10 hover:bg-white/10">
|
||||
<Button variant="outline" className="bg-white/60 backdrop-blur-md">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
筛选
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 用户卡片列表 */}
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{recentUsers.map((user) => (
|
||||
<Card key={user.id} className="bg-white/5 border-white/10 hover:bg-white/10 transition-colors cursor-pointer">
|
||||
<Card
|
||||
key={user.id}
|
||||
className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm hover:shadow-md transition-all cursor-pointer"
|
||||
>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="w-12 h-12">
|
||||
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-purple-500">
|
||||
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-purple-500 text-white">
|
||||
{user.name[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<span className="text-lg font-semibold text-white">{user.name}</span>
|
||||
<span className="text-lg font-bold text-gray-900">{user.name}</span>
|
||||
<Badge variant="outline" className={getLevelColor(user.level)}>
|
||||
{user.level}级
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-400">
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||
<div className="flex items-center gap-1">
|
||||
<Phone className="w-3 h-3" />
|
||||
{user.phone}
|
||||
@@ -255,13 +260,13 @@ export default function UserPortrait() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-gray-400 mb-1">用户价值</div>
|
||||
<div className="text-2xl font-bold text-green-400">¥{user.value}</div>
|
||||
<div className="text-sm text-gray-500 mb-1">用户价值</div>
|
||||
<div className="text-2xl font-bold text-green-600">¥{user.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
{user.tags.map((tag, index) => (
|
||||
<Badge key={index} variant="outline" className="border-blue-500/30 text-blue-400">
|
||||
<Badge key={index} variant="outline" className="bg-gray-50 text-gray-600 border-gray-200">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { DollarSign, TrendingUp, BarChart3, Calculator, Target, Users, ArrowUpRight, ArrowDownRight } from 'lucide-react'
|
||||
import { DollarSign, TrendingUp, BarChart3, Calculator, Users, ArrowUpRight } from "lucide-react"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
|
||||
export default function ValueAssessment() {
|
||||
// 价值评估模型数据
|
||||
const valueModels = [
|
||||
{
|
||||
name: "RFM价值模型",
|
||||
@@ -37,7 +35,6 @@ export default function ValueAssessment() {
|
||||
},
|
||||
]
|
||||
|
||||
// 项目价值分布
|
||||
const projectValues = [
|
||||
{ name: "美妆行业项目A", users: 45600000, value: 2856000000, growth: 15.8, level: "A" },
|
||||
{ name: "教育培训项目B", users: 32800000, value: 1968000000, growth: 12.3, level: "A" },
|
||||
@@ -46,7 +43,6 @@ export default function ValueAssessment() {
|
||||
{ name: "本地生活项目E", users: 19200000, value: 1152000000, growth: 6.5, level: "B" },
|
||||
]
|
||||
|
||||
// 微信号价值排名
|
||||
const wechatRanking = [
|
||||
{ name: "微信号001", users: 2856000, value: 168560000, avgValue: 59.0, rank: 1 },
|
||||
{ name: "微信号002", users: 2345000, value: 145780000, avgValue: 62.2, rank: 2 },
|
||||
@@ -55,7 +51,6 @@ export default function ValueAssessment() {
|
||||
{ name: "微信号005", users: 1520000, value: 95600000, avgValue: 62.9, rank: 5 },
|
||||
]
|
||||
|
||||
// 价值增长趋势
|
||||
const growthTrend = [
|
||||
{ month: "1月", value: 9850000000, growth: 8.5 },
|
||||
{ month: "2月", value: 10250000000, growth: 4.1 },
|
||||
@@ -80,109 +75,117 @@ export default function ValueAssessment() {
|
||||
|
||||
const getLevelColor = (level: string) => {
|
||||
const colors = {
|
||||
S: "border-purple-500/50 text-purple-400 bg-purple-500/10",
|
||||
A: "border-blue-500/50 text-blue-400 bg-blue-500/10",
|
||||
B: "border-green-500/50 text-green-400 bg-green-500/10",
|
||||
C: "border-yellow-500/50 text-yellow-400 bg-yellow-500/10",
|
||||
D: "border-gray-500/50 text-gray-400 bg-gray-500/10",
|
||||
S: "bg-purple-100 text-purple-700 border-purple-200",
|
||||
A: "bg-blue-100 text-blue-700 border-blue-200",
|
||||
B: "bg-green-100 text-green-700 border-green-200",
|
||||
C: "bg-yellow-100 text-yellow-700 border-yellow-200",
|
||||
D: "bg-gray-100 text-gray-700 border-gray-200",
|
||||
}
|
||||
return colors[level as keyof typeof colors] || colors.D
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white p-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold mb-2">价值评估</h1>
|
||||
<p className="text-gray-400">用户价值分析、项目价值统计与预测</p>
|
||||
<h1 className="text-3xl font-bold text-gray-900">价值评估</h1>
|
||||
<p className="text-gray-500 mt-1">用户价值分析、项目价值统计与预测</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline" className="bg-white/5 border-white/10 hover:bg-white/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline" className="bg-white/60 backdrop-blur-md">
|
||||
<Calculator className="w-4 h-4 mr-2" />
|
||||
价值计算器
|
||||
</Button>
|
||||
<Button className="bg-gradient-to-r from-blue-500 to-purple-500">
|
||||
<Button className="bg-gradient-to-r from-blue-500 to-purple-500 text-white">
|
||||
<BarChart3 className="w-4 h-4 mr-2" />
|
||||
生成报告
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 核心价值指标 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
<Card className="bg-gradient-to-br from-purple-500/20 to-purple-900/20 border-purple-500/30">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<Card className="bg-gradient-to-br from-purple-500 to-purple-600 text-white border-none shadow-md">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<CardTitle className="text-purple-100 flex items-center gap-2">
|
||||
<DollarSign className="w-5 h-5" />
|
||||
平台总价值
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-4xl font-bold text-purple-400 mb-2">¥{formatCurrency(12580000000)}</div>
|
||||
<div className="text-4xl font-bold mb-2">¥{formatCurrency(12580000000)}</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<ArrowUpRight className="w-4 h-4 text-green-400" />
|
||||
<span className="text-green-400">+5.0%</span>
|
||||
<span className="text-gray-400">本月增长</span>
|
||||
<ArrowUpRight className="w-4 h-4 text-green-300" />
|
||||
<span className="text-green-300">+5.0%</span>
|
||||
<span className="text-purple-200">本月增长</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-br from-blue-500/20 to-blue-900/20 border-blue-500/30">
|
||||
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Users className="w-5 h-5" />
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Users className="w-5 h-5 text-blue-600" />
|
||||
人均价值
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-4xl font-bold text-blue-400 mb-2">¥293.5</div>
|
||||
<div className="text-4xl font-bold text-blue-600 mb-2">¥293.5</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<ArrowUpRight className="w-4 h-4 text-green-400" />
|
||||
<span className="text-green-400">+2.8%</span>
|
||||
<span className="text-gray-400">环比提升</span>
|
||||
<ArrowUpRight className="w-4 h-4 text-green-600" />
|
||||
<span className="text-green-600">+2.8%</span>
|
||||
<span className="text-gray-500">环比提升</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-br from-green-500/20 to-green-900/20 border-green-500/30">
|
||||
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-green-600" />
|
||||
价值覆盖率
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-4xl font-bold text-green-400 mb-2">85.5%</div>
|
||||
<div className="text-sm text-gray-400">已评估用户占比</div>
|
||||
<div className="text-4xl font-bold text-green-600 mb-2">85.5%</div>
|
||||
<div className="text-sm text-gray-500">已评估用户占比</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="models" className="space-y-6">
|
||||
<TabsList className="bg-white/5 border border-white/10">
|
||||
<TabsTrigger value="models">价值模型</TabsTrigger>
|
||||
<TabsTrigger value="projects">项目价值</TabsTrigger>
|
||||
<TabsTrigger value="wechat">微信号排名</TabsTrigger>
|
||||
<TabsTrigger value="trend">增长趋势</TabsTrigger>
|
||||
<TabsList className="bg-white/60 backdrop-blur-md border border-gray-200 p-1 rounded-xl">
|
||||
<TabsTrigger value="models" className="rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-sm">
|
||||
价值模型
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="projects"
|
||||
className="rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-sm"
|
||||
>
|
||||
项目价值
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="wechat" className="rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-sm">
|
||||
微信号排名
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="trend" className="rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-sm">
|
||||
增长趋势
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 价值模型 */}
|
||||
<TabsContent value="models" className="space-y-6">
|
||||
{valueModels.map((model, index) => (
|
||||
<Card key={index} className="bg-white/5 border-white/10">
|
||||
<Card key={index} className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-white text-xl mb-1">{model.name}</CardTitle>
|
||||
<p className="text-sm text-gray-400">{model.description}</p>
|
||||
<CardTitle className="text-gray-900 text-xl mb-1">{model.name}</CardTitle>
|
||||
<p className="text-sm text-gray-500">{model.description}</p>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
model.status === "active"
|
||||
? "border-green-500/50 text-green-400"
|
||||
: "border-yellow-500/50 text-yellow-400"
|
||||
? "bg-green-50 text-green-600 border-green-200"
|
||||
: "bg-yellow-50 text-yellow-600 border-yellow-200"
|
||||
}
|
||||
>
|
||||
{model.status === "active" ? "运行中" : "训练中"}
|
||||
@@ -192,60 +195,59 @@ export default function ValueAssessment() {
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-3 gap-6 mb-4">
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-1">模型总价值</div>
|
||||
<div className="text-2xl font-bold text-white">¥{formatCurrency(model.totalValue)}</div>
|
||||
<div className="text-sm text-gray-500 mb-1">模型总价值</div>
|
||||
<div className="text-2xl font-bold text-gray-900">¥{formatCurrency(model.totalValue)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-1">人均价值</div>
|
||||
<div className="text-2xl font-bold text-blue-400">¥{model.avgValue}</div>
|
||||
<div className="text-sm text-gray-500 mb-1">人均价值</div>
|
||||
<div className="text-2xl font-bold text-blue-600">¥{model.avgValue}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-1">覆盖率</div>
|
||||
<div className="text-2xl font-bold text-green-400">{model.coverage}%</div>
|
||||
<div className="text-sm text-gray-500 mb-1">覆盖率</div>
|
||||
<div className="text-2xl font-bold text-green-600">{model.coverage}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={model.coverage} className="h-2" />
|
||||
<Progress value={model.coverage} className="h-2 bg-gray-100" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
{/* 项目价值 */}
|
||||
<TabsContent value="projects" className="space-y-6">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{projectValues.map((project, index) => (
|
||||
<Card key={index} className="bg-white/5 border-white/10">
|
||||
<Card key={index} className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-lg font-semibold text-white">{project.name}</div>
|
||||
<div className="text-lg font-bold text-gray-900">{project.name}</div>
|
||||
<Badge variant="outline" className={getLevelColor(project.level)}>
|
||||
{project.level}级项目
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowUpRight className="w-4 h-4 text-green-400" />
|
||||
<span className="text-green-400 font-semibold">+{project.growth}%</span>
|
||||
<ArrowUpRight className="w-4 h-4 text-green-600" />
|
||||
<span className="text-green-600 font-semibold">+{project.growth}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">用户数</div>
|
||||
<div className="text-lg font-semibold text-white">{formatNumber(project.users)}</div>
|
||||
<div className="text-gray-500 mb-1">用户数</div>
|
||||
<div className="text-lg font-semibold text-gray-900">{formatNumber(project.users)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">项目价值</div>
|
||||
<div className="text-lg font-semibold text-green-400">¥{formatCurrency(project.value)}</div>
|
||||
<div className="text-gray-500 mb-1">项目价值</div>
|
||||
<div className="text-lg font-semibold text-green-600">¥{formatCurrency(project.value)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">人均价值</div>
|
||||
<div className="text-lg font-semibold text-blue-400">
|
||||
<div className="text-gray-500 mb-1">人均价值</div>
|
||||
<div className="text-lg font-semibold text-blue-600">
|
||||
¥{(project.value / project.users).toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">增长率</div>
|
||||
<div className="text-lg font-semibold text-white">{project.growth}%</div>
|
||||
<div className="text-gray-500 mb-1">增长率</div>
|
||||
<div className="text-lg font-semibold text-gray-900">{project.growth}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -254,33 +256,24 @@ export default function ValueAssessment() {
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 微信号排名 */}
|
||||
<TabsContent value="wechat" className="space-y-6">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{wechatRanking.map((wechat, index) => (
|
||||
<Card key={index} className="bg-white/5 border-white/10">
|
||||
<Card key={index} className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div
|
||||
className={`w-12 h-12 rounded-full flex items-center justify-center text-2xl font-bold ${
|
||||
wechat.rank === 1
|
||||
? "bg-gradient-to-br from-yellow-400 to-orange-500"
|
||||
: wechat.rank === 2
|
||||
? "bg-gradient-to-br from-gray-300 to-gray-500"
|
||||
: wechat.rank === 3
|
||||
? "bg-gradient-to-br from-orange-400 to-orange-600"
|
||||
: "bg-white/10"
|
||||
}`}
|
||||
className={`w-12 h-12 rounded-full flex items-center justify-center text-2xl font-bold text-white ${wechat.rank === 1 ? "bg-gradient-to-br from-yellow-400 to-orange-500" : wechat.rank === 2 ? "bg-gradient-to-br from-gray-300 to-gray-500" : wechat.rank === 3 ? "bg-gradient-to-br from-orange-400 to-orange-600" : "bg-gray-200 text-gray-600"}`}
|
||||
>
|
||||
{wechat.rank}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-white mb-1">{wechat.name}</h3>
|
||||
<div className="text-sm text-gray-400">{formatNumber(wechat.users)} 用户</div>
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-1">{wechat.name}</h3>
|
||||
<div className="text-sm text-gray-500">{formatNumber(wechat.users)} 用户</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-green-400">¥{formatCurrency(wechat.value)}</div>
|
||||
<div className="text-sm text-gray-400">人均 ¥{wechat.avgValue}</div>
|
||||
<div className="text-2xl font-bold text-green-600">¥{formatCurrency(wechat.value)}</div>
|
||||
<div className="text-sm text-gray-500">人均 ¥{wechat.avgValue}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -289,11 +282,10 @@ export default function ValueAssessment() {
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 增长趋势 */}
|
||||
<TabsContent value="trend" className="space-y-6">
|
||||
<Card className="bg-white/5 border-white/10">
|
||||
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
价值增长趋势
|
||||
</CardTitle>
|
||||
@@ -301,14 +293,17 @@ export default function ValueAssessment() {
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{growthTrend.map((item, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-4 bg-white/5 rounded-lg">
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg border border-gray-100"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-lg font-semibold text-white">{item.month}</div>
|
||||
<div className="text-2xl font-bold text-green-400">¥{formatCurrency(item.value)}</div>
|
||||
<div className="text-lg font-semibold text-gray-900">{item.month}</div>
|
||||
<div className="text-2xl font-bold text-green-600">¥{formatCurrency(item.value)}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowUpRight className="w-4 h-4 text-green-400" />
|
||||
<span className="text-lg font-semibold text-green-400">+{item.growth}%</span>
|
||||
<ArrowUpRight className="w-4 h-4 text-green-600" />
|
||||
<span className="text-lg font-semibold text-green-600">+{item.growth}%</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
3
app/value-model/assessment/loading.tsx
Normal file
3
app/value-model/assessment/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
320
app/value-model/assessment/page.tsx
Normal file
320
app/value-model/assessment/page.tsx
Normal file
@@ -0,0 +1,320 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import {
|
||||
BarChart3,
|
||||
Plus,
|
||||
Play,
|
||||
Search,
|
||||
Users,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
RefreshCw,
|
||||
Download,
|
||||
Filter,
|
||||
} from "lucide-react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { CreateAssessmentTaskDialog } from "@/components/dialogs/create-assessment-task-dialog"
|
||||
|
||||
interface AssessmentTask {
|
||||
id: string
|
||||
name: string
|
||||
model: string
|
||||
targetCrowd: string
|
||||
userCount: number
|
||||
status: "running" | "completed" | "failed" | "scheduled"
|
||||
progress: number
|
||||
createdAt: string
|
||||
completedAt: string | null
|
||||
schedule: string
|
||||
resultSummary?: {
|
||||
avgScore: number
|
||||
highValue: number
|
||||
mediumValue: number
|
||||
lowValue: number
|
||||
}
|
||||
}
|
||||
|
||||
const MOCK_TASKS: AssessmentTask[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "全量用户CLV评估",
|
||||
model: "CLV预测模型 v2.3.1",
|
||||
targetCrowd: "全部用户",
|
||||
userCount: 4028567890,
|
||||
status: "completed",
|
||||
progress: 100,
|
||||
createdAt: "2025-12-12 00:00:00",
|
||||
completedAt: "2025-12-12 03:45:00",
|
||||
schedule: "每周一",
|
||||
resultSummary: {
|
||||
avgScore: 65.8,
|
||||
highValue: 15.2,
|
||||
mediumValue: 45.6,
|
||||
lowValue: 39.2,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "活跃用户RFM评分",
|
||||
model: "RFM评分模型 v1.5.0",
|
||||
targetCrowd: "近30天活跃用户",
|
||||
userCount: 1256789012,
|
||||
status: "running",
|
||||
progress: 67,
|
||||
createdAt: "2025-12-12 10:00:00",
|
||||
completedAt: null,
|
||||
schedule: "每天",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "流失风险预警",
|
||||
model: "流失预警模型 v3.1.2",
|
||||
targetCrowd: "沉默用户",
|
||||
userCount: 456789012,
|
||||
status: "completed",
|
||||
progress: 100,
|
||||
createdAt: "2025-12-11 22:00:00",
|
||||
completedAt: "2025-12-12 00:30:00",
|
||||
schedule: "每天",
|
||||
resultSummary: {
|
||||
avgScore: 0.45,
|
||||
highValue: 8.5,
|
||||
mediumValue: 25.3,
|
||||
lowValue: 66.2,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "高价值用户细分",
|
||||
model: "用户分群模型 v1.0.0",
|
||||
targetCrowd: "价值评分>80用户",
|
||||
userCount: 125678901,
|
||||
status: "scheduled",
|
||||
progress: 0,
|
||||
createdAt: "2025-12-12 14:00:00",
|
||||
completedAt: null,
|
||||
schedule: "每周",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "欺诈风险扫描",
|
||||
model: "欺诈检测模型 v2.0.0",
|
||||
targetCrowd: "近7天交易用户",
|
||||
userCount: 89012345,
|
||||
status: "failed",
|
||||
progress: 45,
|
||||
createdAt: "2025-12-12 08:00:00",
|
||||
completedAt: null,
|
||||
schedule: "实时",
|
||||
},
|
||||
]
|
||||
|
||||
const STATUS_CONFIG = {
|
||||
running: { label: "运行中", color: "bg-blue-100 text-blue-700", icon: RefreshCw },
|
||||
completed: { label: "已完成", color: "bg-green-100 text-green-700", icon: CheckCircle },
|
||||
failed: { label: "失败", color: "bg-red-100 text-red-700", icon: XCircle },
|
||||
scheduled: { label: "已排期", color: "bg-yellow-100 text-yellow-700", icon: Clock },
|
||||
}
|
||||
|
||||
export default function AssessmentServicePage() {
|
||||
const [tasks, setTasks] = useState<AssessmentTask[]>(MOCK_TASKS)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [statusFilter, setStatusFilter] = useState("all")
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
const filteredTasks = tasks.filter((task) => {
|
||||
const matchesSearch = task.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
const matchesStatus = statusFilter === "all" || task.status === statusFilter
|
||||
return matchesSearch && matchesStatus
|
||||
})
|
||||
|
||||
const stats = {
|
||||
total: tasks.length,
|
||||
running: tasks.filter((t) => t.status === "running").length,
|
||||
completed: tasks.filter((t) => t.status === "completed").length,
|
||||
totalEvaluated: tasks.filter((t) => t.status === "completed").reduce((sum, t) => sum + t.userCount, 0),
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">评估服务</h1>
|
||||
<p className="text-gray-500 mt-1">执行价值评估任务</p>
|
||||
</div>
|
||||
<Button
|
||||
className="bg-gradient-to-r from-blue-500 to-purple-500 text-white"
|
||||
onClick={() => setShowCreateDialog(true)}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
创建评估任务
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<BarChart3 className="w-4 h-4" />
|
||||
总任务数
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.total}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<RefreshCw className="w-4 h-4 text-blue-500" />
|
||||
运行中
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-blue-600">{stats.running}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
已完成
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-green-600">{stats.completed}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<Users className="w-4 h-4" />
|
||||
已评估用户
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{formatNumber(stats.totalEvaluated)}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
placeholder="搜索任务..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 bg-white/60"
|
||||
/>
|
||||
</div>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-full md:w-40 bg-white/60">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
<SelectValue placeholder="状态筛选" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部状态</SelectItem>
|
||||
<SelectItem value="running">运行中</SelectItem>
|
||||
<SelectItem value="completed">已完成</SelectItem>
|
||||
<SelectItem value="failed">失败</SelectItem>
|
||||
<SelectItem value="scheduled">已排期</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Task List */}
|
||||
<div className="space-y-4">
|
||||
{filteredTasks.map((task) => {
|
||||
const StatusIcon = STATUS_CONFIG[task.status].icon
|
||||
|
||||
return (
|
||||
<Card key={task.id} className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="font-semibold text-gray-900">{task.name}</h3>
|
||||
<Badge className={STATUS_CONFIG[task.status].color}>
|
||||
<StatusIcon className={`w-3 h-3 mr-1 ${task.status === "running" ? "animate-spin" : ""}`} />
|
||||
{STATUS_CONFIG[task.status].label}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-500">
|
||||
<span>模型: {task.model}</span>
|
||||
<span>目标: {task.targetCrowd}</span>
|
||||
<span>用户数: {formatNumber(task.userCount)}</span>
|
||||
<span>周期: {task.schedule}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{task.status === "completed" && (
|
||||
<Button variant="outline" size="sm">
|
||||
<Download className="w-4 h-4 mr-1" />
|
||||
导出
|
||||
</Button>
|
||||
)}
|
||||
{(task.status === "scheduled" || task.status === "failed") && (
|
||||
<Button variant="outline" size="sm">
|
||||
<Play className="w-4 h-4 mr-1" />
|
||||
执行
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{task.status === "running" && (
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between text-sm text-gray-500 mb-1">
|
||||
<span>执行进度</span>
|
||||
<span>{task.progress}%</span>
|
||||
</div>
|
||||
<Progress value={task.progress} className="h-2" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{task.resultSummary && (
|
||||
<div className="grid grid-cols-4 gap-4 p-4 rounded-xl bg-gray-50">
|
||||
<div className="text-center">
|
||||
<div className="text-xl font-bold text-gray-900">{task.resultSummary.avgScore}</div>
|
||||
<div className="text-xs text-gray-500">平均分</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-xl font-bold text-green-600">{task.resultSummary.highValue}%</div>
|
||||
<div className="text-xs text-gray-500">高价值</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-xl font-bold text-yellow-600">{task.resultSummary.mediumValue}%</div>
|
||||
<div className="text-xs text-gray-500">中价值</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-xl font-bold text-gray-600">{task.resultSummary.lowValue}%</div>
|
||||
<div className="text-xs text-gray-500">低价值</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-gray-500 mt-4 pt-4 border-t border-gray-100">
|
||||
<span>创建时间: {task.createdAt}</span>
|
||||
{task.completedAt && <span>完成时间: {task.completedAt}</span>}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Create Task Dialog */}
|
||||
<CreateAssessmentTaskDialog open={showCreateDialog} onOpenChange={setShowCreateDialog} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
app/value-model/models/loading.tsx
Normal file
3
app/value-model/models/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
358
app/value-model/models/page.tsx
Normal file
358
app/value-model/models/page.tsx
Normal file
@@ -0,0 +1,358 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Boxes,
|
||||
Plus,
|
||||
Search,
|
||||
Play,
|
||||
Settings,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
AlertTriangle,
|
||||
GitBranch,
|
||||
BarChart3,
|
||||
Cpu,
|
||||
TrendingUp,
|
||||
} from "lucide-react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { UploadModelDialog } from "@/components/dialogs/upload-model-dialog"
|
||||
|
||||
interface Model {
|
||||
id: string
|
||||
name: string
|
||||
type: "preset" | "custom"
|
||||
category: "clv" | "rfm" | "churn" | "fraud" | "segment"
|
||||
version: string
|
||||
status: "online" | "training" | "testing" | "offline"
|
||||
accuracy: number
|
||||
precision: number
|
||||
recall: number
|
||||
f1Score: number
|
||||
lastTrained: string
|
||||
inputFeatures: string[]
|
||||
outputFormat: string
|
||||
updateFrequency: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const MOCK_MODELS: Model[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "CLV预测模型",
|
||||
type: "preset",
|
||||
category: "clv",
|
||||
version: "v2.3.1",
|
||||
status: "online",
|
||||
accuracy: 87.5,
|
||||
precision: 85.2,
|
||||
recall: 89.1,
|
||||
f1Score: 87.1,
|
||||
lastTrained: "2025-12-10",
|
||||
inputFeatures: ["历史消费金额", "消费频次", "注册时长", "活跃度"],
|
||||
outputFormat: "CLV分数 (0-100)",
|
||||
updateFrequency: "每周",
|
||||
description: "基于用户历史行为预测客户终身价值",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "RFM评分模型",
|
||||
type: "preset",
|
||||
category: "rfm",
|
||||
version: "v1.5.0",
|
||||
status: "online",
|
||||
accuracy: 92.3,
|
||||
precision: 91.5,
|
||||
recall: 93.2,
|
||||
f1Score: 92.3,
|
||||
lastTrained: "2025-12-11",
|
||||
inputFeatures: ["最近消费时间", "消费频率", "消费金额"],
|
||||
outputFormat: "R/F/M各维度评分 (0-100)",
|
||||
updateFrequency: "每天",
|
||||
description: "基于RFM模型的用户价值分层",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "流失预警模型",
|
||||
type: "preset",
|
||||
category: "churn",
|
||||
version: "v3.1.2",
|
||||
status: "online",
|
||||
accuracy: 85.8,
|
||||
precision: 87.3,
|
||||
recall: 84.2,
|
||||
f1Score: 85.7,
|
||||
lastTrained: "2025-12-09",
|
||||
inputFeatures: ["活跃度变化", "消费趋势", "投诉记录", "登录频次"],
|
||||
outputFormat: "流失概率 (0-1)",
|
||||
updateFrequency: "每天",
|
||||
description: "预测用户未来30天流失概率",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "欺诈检测模型",
|
||||
type: "preset",
|
||||
category: "fraud",
|
||||
version: "v2.0.0",
|
||||
status: "online",
|
||||
accuracy: 96.2,
|
||||
precision: 94.8,
|
||||
recall: 97.5,
|
||||
f1Score: 96.1,
|
||||
lastTrained: "2025-12-08",
|
||||
inputFeatures: ["交易金额", "交易频率", "设备信息", "地理位置"],
|
||||
outputFormat: "欺诈风险等级 (低/中/高)",
|
||||
updateFrequency: "实时",
|
||||
description: "实时检测可疑交易行为",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "用户分群模型",
|
||||
type: "custom",
|
||||
category: "segment",
|
||||
version: "v1.0.0",
|
||||
status: "training",
|
||||
accuracy: 78.5,
|
||||
precision: 76.2,
|
||||
recall: 80.1,
|
||||
f1Score: 78.1,
|
||||
lastTrained: "2025-12-12",
|
||||
inputFeatures: ["消费行为", "浏览偏好", "互动记录", "人口属性"],
|
||||
outputFormat: "用户群体标签",
|
||||
updateFrequency: "每周",
|
||||
description: "基于多维特征的用户自动分群",
|
||||
},
|
||||
]
|
||||
|
||||
const CATEGORY_CONFIG = {
|
||||
clv: { label: "CLV模型", color: "bg-blue-100 text-blue-700" },
|
||||
rfm: { label: "RFM模型", color: "bg-green-100 text-green-700" },
|
||||
churn: { label: "流失预警", color: "bg-orange-100 text-orange-700" },
|
||||
fraud: { label: "欺诈检测", color: "bg-red-100 text-red-700" },
|
||||
segment: { label: "用户分群", color: "bg-purple-100 text-purple-700" },
|
||||
}
|
||||
|
||||
const STATUS_CONFIG = {
|
||||
online: { label: "已上线", color: "bg-green-100 text-green-700", icon: CheckCircle },
|
||||
training: { label: "训练中", color: "bg-blue-100 text-blue-700", icon: Cpu },
|
||||
testing: { label: "测试中", color: "bg-yellow-100 text-yellow-700", icon: Clock },
|
||||
offline: { label: "已下线", color: "bg-gray-100 text-gray-700", icon: AlertTriangle },
|
||||
}
|
||||
|
||||
export default function ModelManagementPage() {
|
||||
const [models, setModels] = useState<Model[]>(MOCK_MODELS)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [selectedModel, setSelectedModel] = useState<Model | null>(null)
|
||||
const [showUploadDialog, setShowUploadDialog] = useState(false)
|
||||
|
||||
const stats = {
|
||||
total: models.length,
|
||||
online: models.filter((m) => m.status === "online").length,
|
||||
avgAccuracy: (models.reduce((sum, m) => sum + m.accuracy, 0) / models.length).toFixed(1),
|
||||
presetModels: models.filter((m) => m.type === "preset").length,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">模型管理</h1>
|
||||
<p className="text-gray-500 mt-1">管理价值评估算法模型</p>
|
||||
</div>
|
||||
<Button
|
||||
className="bg-gradient-to-r from-blue-500 to-purple-500 text-white"
|
||||
onClick={() => setShowUploadDialog(true)}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
上传自定义模型
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<Boxes className="w-4 h-4" />
|
||||
总模型数
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.total}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
已上线
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-green-600">{stats.online}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
平均准确率
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.avgAccuracy}%</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<GitBranch className="w-4 h-4" />
|
||||
预置模型
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.presetModels}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
placeholder="搜索模型..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 bg-white/60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Model List */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{models
|
||||
.filter((m) => m.name.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
.map((model) => {
|
||||
const StatusIcon = STATUS_CONFIG[model.status].icon
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={model.id}
|
||||
className={`border-none shadow-sm bg-white/60 backdrop-blur-sm hover:shadow-md transition-shadow cursor-pointer ${selectedModel?.id === model.id ? "ring-2 ring-blue-500" : ""}`}
|
||||
onClick={() => setSelectedModel(model)}
|
||||
>
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold text-gray-900">{model.name}</h3>
|
||||
<Badge className={CATEGORY_CONFIG[model.category].color}>
|
||||
{CATEGORY_CONFIG[model.category].label}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">{model.description}</p>
|
||||
</div>
|
||||
<Badge className={STATUS_CONFIG[model.status].color}>
|
||||
<StatusIcon className={`w-3 h-3 mr-1 ${model.status === "training" ? "animate-spin" : ""}`} />
|
||||
{STATUS_CONFIG[model.status].label}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4 mb-4">
|
||||
<div className="text-center p-2 rounded-lg bg-gray-50">
|
||||
<div className="text-lg font-bold text-gray-900">{model.accuracy}%</div>
|
||||
<div className="text-xs text-gray-500">准确率</div>
|
||||
</div>
|
||||
<div className="text-center p-2 rounded-lg bg-gray-50">
|
||||
<div className="text-lg font-bold text-gray-900">{model.precision}%</div>
|
||||
<div className="text-xs text-gray-500">精确率</div>
|
||||
</div>
|
||||
<div className="text-center p-2 rounded-lg bg-gray-50">
|
||||
<div className="text-lg font-bold text-gray-900">{model.recall}%</div>
|
||||
<div className="text-xs text-gray-500">召回率</div>
|
||||
</div>
|
||||
<div className="text-center p-2 rounded-lg bg-gray-50">
|
||||
<div className="text-lg font-bold text-gray-900">{model.f1Score}%</div>
|
||||
<div className="text-xs text-gray-500">F1分数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-sm text-gray-500 pt-4 border-t border-gray-100">
|
||||
<div className="flex items-center gap-4">
|
||||
<span>版本: {model.version}</span>
|
||||
<span>更新: {model.updateFrequency}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Play className="w-4 h-4 mr-1" />
|
||||
测试
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Settings className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Model Detail Panel */}
|
||||
{selectedModel && (
|
||||
<Card className="border-none shadow-md bg-white/80 backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<BarChart3 className="w-5 h-5" />
|
||||
模型详情: {selectedModel.name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 mb-3">输入特征</h4>
|
||||
<div className="space-y-2">
|
||||
{selectedModel.inputFeatures.map((feature, i) => (
|
||||
<div key={i} className="flex items-center gap-2 p-2 rounded-lg bg-gray-50">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-500" />
|
||||
<span className="text-sm text-gray-700">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 mb-3">输出格式</h4>
|
||||
<div className="p-3 rounded-lg bg-gray-50">
|
||||
<code className="text-sm text-gray-700">{selectedModel.outputFormat}</code>
|
||||
</div>
|
||||
|
||||
<h4 className="font-medium text-gray-900 mb-3 mt-6">模型指标</h4>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>准确率</span>
|
||||
<span>{selectedModel.accuracy}%</span>
|
||||
</div>
|
||||
<Progress value={selectedModel.accuracy} className="h-2" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>精确率</span>
|
||||
<span>{selectedModel.precision}%</span>
|
||||
</div>
|
||||
<Progress value={selectedModel.precision} className="h-2" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>召回率</span>
|
||||
<span>{selectedModel.recall}%</span>
|
||||
</div>
|
||||
<Progress value={selectedModel.recall} className="h-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<UploadModelDialog open={showUploadDialog} onOpenChange={setShowUploadDialog} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
12
app/value-model/page.tsx
Normal file
12
app/value-model/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
"use client"
|
||||
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect } from "react"
|
||||
|
||||
export default function ValueModelPage() {
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
router.replace("/value-model/models")
|
||||
}, [router])
|
||||
return null
|
||||
}
|
||||
3
app/value-model/reports/loading.tsx
Normal file
3
app/value-model/reports/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
237
app/value-model/reports/page.tsx
Normal file
237
app/value-model/reports/page.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Download, Share2, Eye, Calendar, TrendingUp, TrendingDown, BarChart3, PieChart } from "lucide-react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
|
||||
interface Report {
|
||||
id: string
|
||||
name: string
|
||||
type: "distribution" | "trend" | "comparison"
|
||||
period: string
|
||||
generatedAt: string
|
||||
metrics: {
|
||||
label: string
|
||||
value: string
|
||||
change?: number
|
||||
}[]
|
||||
chartData?: any
|
||||
}
|
||||
|
||||
const MOCK_REPORTS: Report[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "用户价值分布报告",
|
||||
type: "distribution",
|
||||
period: "2025年12月",
|
||||
generatedAt: "2025-12-12 06:00:00",
|
||||
metrics: [
|
||||
{ label: "S级用户", value: "5.2%", change: 0.3 },
|
||||
{ label: "A级用户", value: "12.8%", change: 0.5 },
|
||||
{ label: "B级用户", value: "28.5%", change: -0.2 },
|
||||
{ label: "C级用户", value: "35.2%", change: -0.4 },
|
||||
{ label: "D级用户", value: "18.3%", change: -0.2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "价值趋势对比报告",
|
||||
type: "trend",
|
||||
period: "近6个月",
|
||||
generatedAt: "2025-12-12 06:00:00",
|
||||
metrics: [
|
||||
{ label: "平均CLV", value: "¥2,856", change: 12.5 },
|
||||
{ label: "高价值用户占比", value: "18.0%", change: 2.3 },
|
||||
{ label: "用户价值总量", value: "¥125.6B", change: 8.7 },
|
||||
{ label: "人均贡献", value: "¥31.2", change: 5.2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "项目价值排名报告",
|
||||
type: "comparison",
|
||||
period: "2025年12月",
|
||||
generatedAt: "2025-12-11 22:00:00",
|
||||
metrics: [
|
||||
{ label: "#1 华东区项目", value: "¥28.5B", change: 15.2 },
|
||||
{ label: "#2 华南区项目", value: "¥22.3B", change: 12.8 },
|
||||
{ label: "#3 华北区项目", value: "¥18.9B", change: 8.5 },
|
||||
{ label: "#4 西南区项目", value: "¥15.6B", change: 10.2 },
|
||||
{ label: "#5 华中区项目", value: "¥12.8B", change: 6.3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "流失风险分析报告",
|
||||
type: "distribution",
|
||||
period: "2025年12月",
|
||||
generatedAt: "2025-12-12 00:00:00",
|
||||
metrics: [
|
||||
{ label: "高风险用户", value: "8.5%", change: -1.2 },
|
||||
{ label: "中风险用户", value: "25.3%", change: 0.5 },
|
||||
{ label: "低风险用户", value: "66.2%", change: 0.7 },
|
||||
{ label: "预计流失数", value: "34.2M", change: -5.8 },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const TYPE_CONFIG = {
|
||||
distribution: { label: "分布报告", color: "bg-blue-100 text-blue-700", icon: PieChart },
|
||||
trend: { label: "趋势报告", color: "bg-green-100 text-green-700", icon: TrendingUp },
|
||||
comparison: { label: "对比报告", color: "bg-purple-100 text-purple-700", icon: BarChart3 },
|
||||
}
|
||||
|
||||
export default function AssessmentReportsPage() {
|
||||
const [reports, setReports] = useState<Report[]>(MOCK_REPORTS)
|
||||
const [selectedPeriod, setSelectedPeriod] = useState("month")
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">评估报告</h1>
|
||||
<p className="text-gray-500 mt-1">查看价值评估分析报告</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Select value={selectedPeriod} onValueChange={setSelectedPeriod}>
|
||||
<SelectTrigger className="w-32 bg-white/60">
|
||||
<Calendar className="w-4 h-4 mr-2" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="week">本周</SelectItem>
|
||||
<SelectItem value="month">本月</SelectItem>
|
||||
<SelectItem value="quarter">本季度</SelectItem>
|
||||
<SelectItem value="year">本年</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
批量导出
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Card className="border-none shadow-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-2 text-blue-100 mb-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
<span>用户价值总量</span>
|
||||
</div>
|
||||
<div className="text-4xl font-bold mb-2">¥125.6B</div>
|
||||
<div className="flex items-center gap-1 text-green-300 text-sm">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
<span>较上月增长 8.7%</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-none shadow-lg bg-gradient-to-br from-green-500 to-emerald-600 text-white">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-2 text-green-100 mb-2">
|
||||
<BarChart3 className="w-5 h-5" />
|
||||
<span>高价值用户占比</span>
|
||||
</div>
|
||||
<div className="text-4xl font-bold mb-2">18.0%</div>
|
||||
<div className="flex items-center gap-1 text-green-300 text-sm">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
<span>较上月提升 2.3%</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-none shadow-lg bg-gradient-to-br from-purple-500 to-violet-600 text-white">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-2 text-purple-100 mb-2">
|
||||
<PieChart className="w-5 h-5" />
|
||||
<span>平均CLV</span>
|
||||
</div>
|
||||
<div className="text-4xl font-bold mb-2">¥2,856</div>
|
||||
<div className="flex items-center gap-1 text-green-300 text-sm">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
<span>较上月增长 12.5%</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Report List */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{reports.map((report) => {
|
||||
const TypeIcon = TYPE_CONFIG[report.type].icon
|
||||
|
||||
return (
|
||||
<Card key={report.id} className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<CardTitle className="text-lg">{report.name}</CardTitle>
|
||||
<Badge className={TYPE_CONFIG[report.type].color}>
|
||||
<TypeIcon className="w-3 h-3 mr-1" />
|
||||
{TYPE_CONFIG[report.type].label}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">周期: {report.period}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
<Download className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
<Share2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{report.metrics.map((metric, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-3 rounded-xl bg-gray-50">
|
||||
<span className="text-gray-600">{metric.label}</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-bold text-gray-900">{metric.value}</span>
|
||||
{metric.change !== undefined && (
|
||||
<Badge
|
||||
className={
|
||||
metric.change > 0
|
||||
? "bg-green-100 text-green-700"
|
||||
: metric.change < 0
|
||||
? "bg-red-100 text-red-700"
|
||||
: "bg-gray-100 text-gray-700"
|
||||
}
|
||||
>
|
||||
{metric.change > 0 ? (
|
||||
<TrendingUp className="w-3 h-3 mr-1" />
|
||||
) : metric.change < 0 ? (
|
||||
<TrendingDown className="w-3 h-3 mr-1" />
|
||||
) : null}
|
||||
{metric.change > 0 ? "+" : ""}
|
||||
{metric.change}%
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-gray-100 text-xs text-gray-500">
|
||||
生成时间: {report.generatedAt}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
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 }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user