refactor: restructure navigation and module layout

Reorganize navigation and module structure based on new requirements.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
v0
2026-01-31 04:32:36 +00:00
parent 22e725887a
commit b17b488f8e
105 changed files with 20530 additions and 3622 deletions

View File

@@ -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
View 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
View 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>
)
}

View 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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View 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>
)
}

View 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
View 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
View 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>
)
}

View 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>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View 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,200DAU/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>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View 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>
)
}

View 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>
)
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View 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>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View 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
View 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>
)
}

View 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>
)
}

View 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
}

View 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>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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">MySQLAPIWebhook等多种接入方式</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>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View 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

View File

@@ -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;
}
}

View File

@@ -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'

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View 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>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View 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>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View 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>
)
}

View 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
View 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>
)
}

View File

@@ -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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View 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
View 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
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View 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>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View 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>
)
}

View File

@@ -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>
))}

View File

@@ -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>
))}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View 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>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View 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
View 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
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View 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>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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
View 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