feat: refactor data asset center for enhanced search and analytics

Refactor homepage for focused search and data display; streamline data platform; enhance user and tag management; focus AI assistant on data analysis and report generation.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
v0
2025-07-25 06:42:34 +00:00
parent ecd8a48863
commit 4eed69520c
40 changed files with 6853 additions and 6111 deletions

File diff suppressed because it is too large Load Diff

31
app/api/ai-query/route.ts Normal file
View File

@@ -0,0 +1,31 @@
import { type NextRequest, NextResponse } from "next/server"
import { getMindsDBConnector } from "@/lib/mindsdb-connector"
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { query, model = "gpt4", parameters = {}, useCache = true } = body
if (!query || !query.trim()) {
return NextResponse.json({ error: "AI查询不能为空" }, { status: 400 })
}
const mindsDB = getMindsDBConnector()
const result = await mindsDB.aiQuery({
query,
model,
parameters,
useCache,
})
return NextResponse.json({
success: true,
result,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error("AI查询API错误:", error)
return NextResponse.json({ error: "AI查询失败请稍后重试" }, { status: 500 })
}
}

105
app/api/ingest/route.ts Normal file
View File

@@ -0,0 +1,105 @@
import { type NextRequest, NextResponse } from "next/server"
import { IngestionService, type IngestionRequest } from "@/services/IngestionService"
// POST /api/ingest - 数据接入端点
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// 验证请求数据
if (!body.source || !body.originalData) {
return NextResponse.json({ error: "缺少必需字段: source 和 originalData" }, { status: 400 })
}
const ingestionRequest: IngestionRequest = {
source: body.source,
sourceUserId: body.sourceUserId,
sourceRecordId: body.sourceRecordId,
originalData: body.originalData,
timestamp: body.timestamp || new Date().toISOString(),
}
const ingestionService = IngestionService.getInstance()
const result = await ingestionService.processIngestionRequest(ingestionRequest)
return NextResponse.json({
success: true,
data: {
userId: result.userId,
coreProfileFields: Object.keys(result.coreProfile).length,
tagsCount: result.unifiedTags.length,
sourceProfilesCount: result.sourceProfiles.length,
},
message: "数据接入成功",
})
} catch (error) {
console.error("数据接入API错误:", error)
return NextResponse.json(
{
error: "数据接入失败",
details: (error as Error).message,
},
{ status: 500 },
)
}
}
// POST /api/ingest/batch - 批量数据接入端点
export async function PUT(request: NextRequest) {
try {
const body = await request.json()
if (!Array.isArray(body.requests)) {
return NextResponse.json({ error: "requests 必须是数组" }, { status: 400 })
}
const ingestionService = IngestionService.getInstance()
const results = await ingestionService.processBatchIngestion(body.requests)
return NextResponse.json({
success: true,
data: {
processedCount: results.length,
totalRequests: body.requests.length,
results: results.map((result) => ({
userId: result.userId,
coreProfileFields: Object.keys(result.coreProfile).length,
tagsCount: result.unifiedTags.length,
})),
},
message: `批量处理完成,成功处理 ${results.length}/${body.requests.length} 条记录`,
})
} catch (error) {
console.error("批量数据接入API错误:", error)
return NextResponse.json(
{
error: "批量数据接入失败",
details: (error as Error).message,
},
{ status: 500 },
)
}
}
// GET /api/ingest/status - 获取数据接入状态
export async function GET(request: NextRequest) {
try {
// 模拟获取接入状态数据
const status = {
totalIngested: 125678,
todayIngested: 1234,
activeSources: 8,
lastIngestionTime: new Date().toISOString(),
dataQuality: 94.6,
processingQueue: 23,
}
return NextResponse.json({
success: true,
data: status,
})
} catch (error) {
console.error("获取接入状态API错误:", error)
return NextResponse.json({ error: "获取状态失败" }, { status: 500 })
}
}

65
app/api/search/route.ts Normal file
View File

@@ -0,0 +1,65 @@
import { type NextRequest, NextResponse } from "next/server"
import { getIntelligentSearchService } from "@/services/intelligent-search-service"
import { getMindsDBConnector } from "@/lib/mindsdb-connector"
// 初始化MindsDB连接
const mindsDBConfig = {
host: process.env.MINDSDB_HOST || "localhost",
port: Number.parseInt(process.env.MINDSDB_PORT || "47334"),
username: process.env.MINDSDB_USERNAME || "mindsdb",
password: process.env.MINDSDB_PASSWORD || "",
database: process.env.MINDSDB_DATABASE || "mindsdb",
}
// 初始化连接器
getMindsDBConnector(mindsDBConfig)
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const query = searchParams.get("q") || ""
const type = (searchParams.get("type") as "user" | "traffic" | "all") || "all"
const limit = Number.parseInt(searchParams.get("limit") || "50")
const offset = Number.parseInt(searchParams.get("offset") || "0")
const useAI = searchParams.get("ai") === "true"
const includeInsights = searchParams.get("insights") === "true"
if (!query.trim()) {
return NextResponse.json({ error: "搜索查询不能为空" }, { status: 400 })
}
const searchService = getIntelligentSearchService()
const results = await searchService.search(query, type, {
limit,
offset,
useAI,
includeInsights,
filters: {},
})
return NextResponse.json(results)
} catch (error) {
console.error("搜索API错误:", error)
return NextResponse.json({ error: "搜索失败,请稍后重试" }, { status: 500 })
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { query, type = "all", options = {} } = body
if (!query || !query.trim()) {
return NextResponse.json({ error: "搜索查询不能为空" }, { status: 400 })
}
const searchService = getIntelligentSearchService()
const results = await searchService.search(query, type, options)
return NextResponse.json(results)
} catch (error) {
console.error("搜索API错误:", error)
return NextResponse.json({ error: "搜索失败,请稍后重试" }, { status: 500 })
}
}

View File

@@ -0,0 +1,24 @@
import { type NextRequest, NextResponse } from "next/server"
import { getMindsDBConnector } from "@/lib/mindsdb-connector"
import { getIntelligentSearchService } from "@/services/intelligent-search-service"
export async function GET(request: NextRequest) {
try {
const mindsDB = getMindsDBConnector()
const searchService = getIntelligentSearchService()
// 获取系统状态
const systemStatus = await mindsDB.getSystemStatus()
const searchStats = searchService.getSearchStats()
return NextResponse.json({
system: systemStatus,
search: searchStats,
timestamp: new Date().toISOString(),
version: process.env.APP_VERSION || "1.0.0",
})
} catch (error) {
console.error("系统状态API错误:", error)
return NextResponse.json({ error: "获取系统状态失败" }, { status: 500 })
}
}

View File

@@ -1,23 +1,8 @@
"use client"
import { useEffect, useState } from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
import {
Database,
LayoutDashboard,
Target,
X,
ChevronDown,
ChevronRight,
UserCheck,
Layers,
Tag,
BrainCircuit,
} from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import Link from "next/link"
interface MobileSidebarProps {
isOpen: boolean
@@ -25,219 +10,43 @@ interface MobileSidebarProps {
}
export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
const pathname = usePathname()
const [expandedSections, setExpandedSections] = useState({
"user-portrait": false, // 用户画像默认不展开
})
const toggleSection = (section: string) => {
setExpandedSections((prev) => ({
...prev,
[section]: !prev[section],
}))
}
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden"
} else {
document.body.style.overflow = "unset"
}
return () => {
document.body.style.overflow = "unset"
}
}, [isOpen])
const navItems = [
{
title: "数据概览",
href: "/",
icon: <LayoutDashboard className="h-4 w-4" />,
primary: true,
description: "平台整体数据分析",
},
{
title: "数据中台",
href: "/data-platform",
icon: <Database className="h-4 w-4" />,
primary: true,
tag: "核心",
description: "多源数据整合中心",
},
{
title: "用户画像", // 整合用户池功能
href: "/user-portrait",
icon: <Target className="h-4 w-4" />,
primary: true,
expandable: true,
section: "user-portrait",
description: "用户数据管理与画像分析",
children: [
{
title: "用户管理", // 原用户池的用户管理
href: "/user-portrait/management",
icon: <UserCheck className="h-3 w-3" />,
description: "IMEI、手机号管理",
},
{
title: "用户分群", // 原用户池的用户分群
href: "/user-portrait/segmentation",
icon: <Layers className="h-3 w-3" />,
description: "智能用户分群",
},
{
title: "标签管理", // 原用户画像的标签管理
href: "/user-portrait/tags",
icon: <Tag className="h-3 w-3" />,
description: "用户标签体系",
},
],
},
{
title: "AI智能助手", // 新增AI智能助手
href: "/ai-assistant",
icon: <BrainCircuit className="h-4 w-4" />,
primary: true,
description: "AI数据分析与营销策略",
},
]
const getTagColor = (tag: string) => {
switch (tag) {
case "核心":
return "bg-blue-100 text-blue-700 border-blue-200"
default:
return "bg-gray-100 text-gray-700 border-gray-200"
}
}
return (
<>
{/* 背景遮罩 */}
<div
className={cn(
"fixed inset-0 bg-black/20 backdrop-blur-sm z-40 transition-opacity duration-300",
isOpen ? "opacity-100" : "opacity-0 pointer-events-none",
)}
onClick={onClose}
/>
<div
className={cn(
"fixed inset-0 z-40 transition-transform duration-300 md:hidden",
isOpen ? "translate-x-0" : "translate-x-full",
)}
aria-hidden={!isOpen}
>
{/* Backdrop */}
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={onClose} />
{/* 侧边栏 */}
<div
className={cn(
"fixed left-0 top-0 h-full w-80 glass-nav safe-area-top safe-area-left z-50 transition-transform duration-300 ease-out",
isOpen ? "translate-x-0" : "-translate-x-full",
)}
>
{/* 头部 */}
<div className="flex items-center justify-between px-4 py-3 border-b border-white/20">
<div className="flex items-center space-x-2">
<div className="w-6 h-6 rounded-lg glass-light flex items-center justify-center">
<Database className="h-4 w-4 text-blue-600" />
</div>
<h1 className="text-sm font-semibold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
</h1>
</div>
<Button variant="ghost" size="icon" onClick={onClose} className="glass-light rounded-lg h-8 w-8">
<X className="h-4 w-4" />
</Button>
{/* Drawer */}
<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>
<button onClick={onClose} aria-label="Close menu" className="rounded p-1 hover:bg-gray-100">
<X className="h-5 w-5" />
</button>
</div>
{/* 导航菜单 */}
<div className="flex-1 overflow-y-auto py-2">
<nav className="space-y-1 px-3">
{navItems.map((item) => (
<div key={item.href}>
<div
className={cn(
"flex items-center px-3 py-2 text-xs font-medium rounded-lg transition-all duration-300 group",
pathname === item.href || (item.children && pathname.startsWith(item.href))
? "glass-heavy text-blue-700 shadow-glass"
: "glass-light text-gray-700 hover:glass-heavy hover:text-blue-600",
)}
>
<Link href={item.href} onClick={onClose} className="flex items-center flex-1 min-w-0">
<div className="transition-colors duration-300 flex-shrink-0">{item.icon}</div>
<div className="flex-1 ml-2 min-w-0">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1">
<span className="font-medium truncate">{item.title}</span>
</div>
{item.tag && (
<Badge
className={cn("text-xs px-1.5 py-0.5 rounded ml-1 flex-shrink-0", getTagColor(item.tag))}
>
{item.tag}
</Badge>
)}
</div>
<p className="text-xs text-gray-500 truncate mt-0.5">{item.description}</p>
</div>
</Link>
{item.expandable && (
<button
onClick={() => toggleSection(item.section!)}
className="ml-1 p-1 hover:bg-white/20 rounded transition-colors duration-200 flex-shrink-0"
>
{expandedSections[item.section!] ? (
<ChevronDown className="h-3 w-3" />
) : (
<ChevronRight className="h-3 w-3" />
)}
</button>
)}
</div>
{/* 子菜单 */}
{item.children && expandedSections[item.section!] && (
<div className="ml-4 mt-1 space-y-1">
{item.children.map((subItem) => (
<Link
key={subItem.href}
href={subItem.href}
onClick={onClose}
className={cn(
"flex items-center px-2 py-1.5 text-xs rounded transition-all duration-300",
pathname === subItem.href
? "glass-light text-blue-600 shadow-glass-sm"
: "text-gray-600 hover:glass-light hover:text-blue-500",
)}
>
<div className="mr-2 text-gray-400 transition-colors duration-300 flex-shrink-0">
{subItem.icon}
</div>
<div className="min-w-0 flex-1">
<p className="font-medium truncate">{subItem.title}</p>
<p className="text-xs text-gray-500 truncate">{subItem.description}</p>
</div>
</Link>
))}
</div>
)}
</div>
))}
</nav>
</div>
{/* 底部信息 */}
<div className="border-t border-white/20 p-3">
<div className="glass-light rounded-lg p-2">
<div className="flex items-center justify-between text-xs">
<span className="text-gray-600"></span>
<div className="flex items-center">
<div className="w-2 h-2 bg-green-500 rounded-full mr-1"></div>
<span className="text-green-600"></span>
</div>
</div>
<div className="flex items-center justify-between text-xs mt-1">
<span className="text-gray-600"></span>
<span className="text-gray-500">125,678</span>
</div>
</div>
</div>
</div>
</>
{/* Navigation links — keep in sync with Sidebar.tsx */}
<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>
</aside>
</div>
)
}

View File

@@ -4,124 +4,63 @@ import { useState } from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { cn } from "@/lib/utils"
import {
Database,
LayoutDashboard,
Settings,
Target,
ChevronLeft,
ChevronDown,
ChevronRight,
Tag,
UserCheck,
Layers,
BrainCircuit,
Smartphone,
} from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { LayoutDashboard, Database, Users, BrainCircuit, Settings, ChevronLeft } from "lucide-react"
export default function Sidebar() {
const pathname = usePathname()
const [expanded, setExpanded] = useState(true)
const [expandedSections, setExpandedSections] = useState({
"user-portrait": true, // 用户画像默认展开
})
const toggleSidebar = () => {
setExpanded(!expanded)
}
const toggleSection = (section: string) => {
setExpandedSections((prev) => ({
...prev,
[section]: !prev[section],
}))
}
// 重新设计的导航结构 - 遵循新规则
// 简化的导航结构
const navItems = [
{
title: "数据概览",
href: "/",
icon: <LayoutDashboard className="h-5 w-5" />,
primary: true,
description: "平台整体数据分析与监控",
},
{
title: "数据中台",
href: "/data-platform",
icon: <Database className="h-5 w-5" />,
primary: true,
tag: "核心",
description: "多源数据整合与处理中心",
},
{
title: "用户画像", // 整合用户池功能
title: "用户画像",
href: "/user-portrait",
icon: <Target className="h-5 w-5" />,
primary: true,
expandable: true,
section: "user-portrait",
children: [
{
title: "用户管理", // 原用户池的用户管理
href: "/user-portrait/management",
icon: <UserCheck className="h-4 w-4" />,
},
{
title: "用户分群", // 原用户池的用户分群
href: "/user-portrait/segmentation",
icon: <Layers className="h-4 w-4" />,
},
{
title: "标签管理", // 原用户画像的标签管理
href: "/user-portrait/tags",
icon: <Tag className="h-4 w-4" />,
},
],
icon: <Users className="h-5 w-5" />,
description: "用户数据管理与画像分析",
},
{
title: "设备管理", // 提升为一级菜单 [^2]
href: "/devices",
icon: <Smartphone className="h-5 w-5" />,
primary: true,
},
{
title: "AI智能助手", // 新增AI智能助手
title: "AI智能助手",
href: "/ai-assistant",
icon: <BrainCircuit className="h-5 w-5" />,
primary: true,
description: "AI数据分析与营销策略",
},
]
const getTagColor = (tag: string) => {
switch (tag) {
case "核心":
return "glass-light text-orange-700 border-orange-200"
default:
return "glass-light text-gray-700 border-gray-200"
}
}
return (
<div
className={cn(
"flex flex-col h-screen glass-nav m-4 transition-all duration-300 ease-in-out",
"flex flex-col h-screen bg-white border-r border-gray-200 transition-all duration-300 ease-in-out",
expanded ? "w-72" : "w-20",
)}
>
{/* 头部 */}
<div className="flex items-center h-16 px-6 border-b border-white/20">
<div className="flex items-center h-16 px-6 border-b border-gray-200">
{expanded ? (
<div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-lg glass-light flex items-center justify-center">
<div className="w-8 h-8 rounded-lg bg-blue-100 flex items-center justify-center">
<Database className="h-5 w-5 text-blue-600" />
</div>
<h1 className="text-lg font-semibold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
</h1>
<h1 className="text-lg font-semibold text-gray-900"></h1>
</div>
) : (
<div className="mx-auto">
<div className="w-8 h-8 rounded-lg glass-light flex items-center justify-center">
<div className="w-8 h-8 rounded-lg bg-blue-100 flex items-center justify-center">
<Database className="h-5 w-5 text-blue-600" />
</div>
</div>
@@ -132,86 +71,49 @@ export default function Sidebar() {
<div className="flex-1 overflow-y-auto py-4">
<nav className="space-y-2 px-4">
{navItems.map((item) => (
<div key={item.href}>
<div
className={cn(
"flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all duration-300 group",
pathname === item.href || (item.children && pathname.startsWith(item.href))
? "glass-heavy text-blue-700 shadow-glass"
: "glass-light text-gray-700 hover:glass-heavy hover:text-blue-600 hover:scale-105",
)}
>
<Link href={item.href} className={cn("flex items-center flex-1", !expanded && "justify-center")}>
<div className="transition-colors duration-300">{item.icon}</div>
{expanded && (
<div className="flex-1 flex items-center justify-between ml-3">
<span className="font-medium">{item.title}</span>
{item.tag && (
<Badge className={cn("text-xs px-2 py-1 rounded-lg", getTagColor(item.tag))}>{item.tag}</Badge>
)}
</div>
)}
</Link>
{expanded && item.expandable && (
<button
onClick={() => toggleSection(item.section!)}
className="ml-2 p-1 hover:bg-white/20 rounded-lg transition-colors duration-200"
>
{expandedSections[item.section!] ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</button>
)}
</div>
{/* 子菜单 */}
{expanded && item.children && expandedSections[item.section!] && (
<div className="ml-6 mt-2 space-y-1">
{item.children.map((subItem) => (
<Link
key={subItem.href}
href={subItem.href}
className={cn(
"flex items-center px-3 py-2 text-sm rounded-lg transition-all duration-300",
pathname === subItem.href
? "glass-light text-blue-600 shadow-glass-sm"
: "text-gray-600 hover:glass-light hover:text-blue-500",
)}
>
<div className="mr-3 text-gray-400 transition-colors duration-300">{subItem.icon}</div>
<span>{subItem.title}</span>
</Link>
))}
<Link
key={item.href}
href={item.href}
className={cn(
"flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200 group",
pathname === item.href
? "bg-blue-50 text-blue-700 border border-blue-200"
: "text-gray-700 hover:bg-gray-50 hover:text-blue-600",
)}
>
<div className="transition-colors duration-200">{item.icon}</div>
{expanded && (
<div className="flex-1 ml-3">
<div className="font-medium">{item.title}</div>
<div className="text-xs text-gray-500 mt-0.5">{item.description}</div>
</div>
)}
</div>
</Link>
))}
</nav>
</div>
{/* 底部设置和折叠按钮 */}
<div className="mt-auto border-t border-white/20">
<div className="mt-auto border-t border-gray-200">
<Link
href="/settings"
className={cn(
"flex items-center px-4 py-2 mx-4 my-2 text-xs font-medium rounded-lg transition-all duration-300",
"flex items-center px-4 py-3 mx-4 my-2 text-sm font-medium rounded-lg transition-all duration-200",
pathname === "/settings"
? "glass-heavy text-blue-700 shadow-glass"
: "glass-light text-gray-600 hover:glass-heavy hover:text-blue-500",
? "bg-blue-50 text-blue-700 border border-blue-200"
: "text-gray-600 hover:bg-gray-50 hover:text-blue-600",
)}
>
<div className={cn("transition-colors duration-300", !expanded && "mx-auto")}>
<div className={cn("transition-colors duration-200", !expanded && "mx-auto")}>
<Settings className="h-4 w-4" />
</div>
{expanded && <span className="ml-2"></span>}
{expanded && <span className="ml-2"></span>}
</Link>
<div className="p-4">
<button
onClick={toggleSidebar}
className="w-full flex items-center justify-center p-3 rounded-xl glass-button hover:scale-105 transition-all duration-300"
className="w-full flex items-center justify-center p-3 rounded-lg bg-gray-50 hover:bg-gray-100 transition-all duration-200"
>
<ChevronLeft
className={cn(

514
app/data-ingestion/page.tsx Normal file
View File

@@ -0,0 +1,514 @@
"use client"
import type React from "react"
import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, 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 { 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
Database,
Upload,
Download,
RefreshCw,
CheckCircle,
AlertCircle,
Clock,
Settings,
Pause,
BarChart3,
TrendingUp,
Zap,
} from "lucide-react"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { useToast } from "@/components/ui/use-toast"
export default function DataIngestionPage() {
const { toast } = useToast()
const [activeTab, setActiveTab] = useState("overview")
const [ingestionStatus, setIngestionStatus] = useState<any>(null)
const [isManualIngesting, setIsManualIngesting] = useState(false)
// 模拟数据接入状态
const [mockStatus] = useState({
totalIngested: 125678,
todayIngested: 1234,
activeSources: 8,
lastIngestionTime: "2分钟前",
dataQuality: 94.6,
processingQueue: 23,
recentIngestions: [
{
id: "ing_001",
source: "抖音API",
recordsCount: 156,
status: "completed",
startTime: "14:30",
duration: "2分钟",
quality: 96.8,
},
{
id: "ing_002",
source: "触客宝后台",
recordsCount: 89,
status: "completed",
startTime: "14:25",
duration: "1分钟",
quality: 95.2,
},
{
id: "ing_003",
source: "表单提交",
recordsCount: 234,
status: "processing",
startTime: "14:32",
duration: "进行中",
quality: 0,
},
{
id: "ing_004",
source: "小红书API",
recordsCount: 67,
status: "failed",
startTime: "14:20",
duration: "失败",
quality: 0,
},
],
sourceStats: [
{ name: "抖音API", records: 45678, quality: 96.8, status: "active" },
{ name: "小红书API", records: 23456, quality: 92.5, status: "active" },
{ name: "触客宝后台", records: 12345, quality: 95.1, status: "active" },
{ name: "表单提交", records: 34567, quality: 97.2, status: "active" },
{ name: "飞书妙记", records: 5678, quality: 91.3, status: "active" },
{ name: "微信公众号", records: 8901, quality: 93.7, status: "inactive" },
],
})
useEffect(() => {
// 模拟获取接入状态
setIngestionStatus(mockStatus)
}, [])
const handleManualIngest = async (formData: any) => {
setIsManualIngesting(true)
try {
const response = await fetch("/api/ingest", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
})
const result = await response.json()
if (result.success) {
toast({
title: "数据接入成功",
description: `成功处理用户数据用户ID: ${result.data.userId}`,
})
} else {
throw new Error(result.error)
}
} catch (error) {
toast({
title: "数据接入失败",
description: (error as Error).message,
variant: "destructive",
})
} finally {
setIsManualIngesting(false)
}
}
const getStatusColor = (status: string) => {
switch (status) {
case "completed":
return "bg-green-100 text-green-800"
case "processing":
return "bg-yellow-100 text-yellow-800"
case "failed":
return "bg-red-100 text-red-800"
case "active":
return "bg-green-100 text-green-800"
case "inactive":
return "bg-gray-100 text-gray-800"
default:
return "bg-gray-100 text-gray-800"
}
}
const getStatusIcon = (status: string) => {
switch (status) {
case "completed":
return <CheckCircle className="h-4 w-4 text-green-600" />
case "processing":
return <Clock className="h-4 w-4 text-yellow-600 animate-spin" />
case "failed":
return <AlertCircle className="h-4 w-4 text-red-600" />
case "active":
return <CheckCircle className="h-4 w-4 text-green-600" />
case "inactive":
return <Pause className="h-4 w-4 text-gray-600" />
default:
return <AlertCircle className="h-4 w-4 text-gray-600" />
}
}
if (!ingestionStatus) {
return <div className="container mx-auto py-8">...</div>
}
return (
<div className="container mx-auto py-4 md:py-6 space-y-4 md:space-y-6">
{/* 页面标题和工具栏 */}
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<div>
<h1 className="text-2xl md:text-3xl font-bold"></h1>
<p className="text-sm md:text-base text-muted-foreground mt-1"></p>
</div>
<div className="flex flex-wrap gap-2">
<Dialog>
<DialogTrigger asChild>
<Button>
<Upload className="mr-2 h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<ManualIngestionForm onSubmit={handleManualIngest} isLoading={isManualIngesting} />
</DialogContent>
</Dialog>
<Button variant="outline" size="icon" className="h-9 w-9 bg-transparent">
<RefreshCw className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm" className="hidden md:flex bg-transparent">
<Download className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="overview"></TabsTrigger>
<TabsTrigger value="sources"></TabsTrigger>
<TabsTrigger value="history"></TabsTrigger>
<TabsTrigger value="monitoring"></TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-6">
{/* 接入概览 */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-2xl font-bold">{ingestionStatus.totalIngested.toLocaleString()}</p>
</div>
<Database className="h-8 w-8 text-blue-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-2xl font-bold">{ingestionStatus.todayIngested.toLocaleString()}</p>
</div>
<TrendingUp className="h-8 w-8 text-green-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-2xl font-bold">{ingestionStatus.activeSources}</p>
</div>
<Zap className="h-8 w-8 text-purple-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-2xl font-bold">{ingestionStatus.dataQuality}%</p>
</div>
<BarChart3 className="h-8 w-8 text-orange-600" />
</div>
</CardContent>
</Card>
</div>
{/* 实时状态 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<Badge variant="outline">{ingestionStatus.processingQueue} </Badge>
</div>
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm font-medium">{ingestionStatus.lastIngestionTime}</span>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span></span>
<span>{ingestionStatus.dataQuality}%</span>
</div>
<Progress value={ingestionStatus.dataQuality} className="h-2" />
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="sources" className="space-y-6">
{/* 数据源状态 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{ingestionStatus.sourceStats.map((source: any, index: number) => (
<div key={index} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-4">
{getStatusIcon(source.status)}
<div>
<h3 className="font-medium">{source.name}</h3>
<p className="text-sm text-muted-foreground">{source.records.toLocaleString()} </p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<p className="text-sm font-medium">{source.quality}%</p>
<p className="text-xs text-muted-foreground"></p>
</div>
<Badge className={getStatusColor(source.status)}>
{source.status === "active" ? "活跃" : "非活跃"}
</Badge>
<Button variant="outline" size="sm">
<Settings className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="history" className="space-y-6">
{/* 接入历史 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{ingestionStatus.recentIngestions.map((ingestion: any) => (
<div key={ingestion.id} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-4">
{getStatusIcon(ingestion.status)}
<div>
<h3 className="font-medium">{ingestion.source}</h3>
<p className="text-sm text-muted-foreground">
{ingestion.recordsCount} {ingestion.startTime} {ingestion.duration}
</p>
</div>
</div>
<div className="flex items-center gap-4">
{ingestion.quality > 0 && (
<div className="text-right">
<p className="text-sm font-medium">{ingestion.quality}%</p>
<p className="text-xs text-muted-foreground"></p>
</div>
)}
<Badge className={getStatusColor(ingestion.status)}>
{ingestion.status === "completed"
? "完成"
: ingestion.status === "processing"
? "处理中"
: "失败"}
</Badge>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="monitoring" className="space-y-6">
{/* 监控统计 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>7</CardDescription>
</CardHeader>
<CardContent>
<div className="h-[200px] flex items-center justify-center text-muted-foreground">
<BarChart3 className="h-12 w-12 mb-2" />
<p></p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{ingestionStatus.sourceStats.slice(0, 5).map((source: any, index: number) => (
<div key={index} className="space-y-1">
<div className="flex justify-between text-sm">
<span>{source.name}</span>
<span>{source.quality}%</span>
</div>
<Progress value={source.quality} className="h-2" />
</div>
))}
</div>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</div>
)
}
// 手动接入表单组件
function ManualIngestionForm({ onSubmit, isLoading }: { onSubmit: (data: any) => void; isLoading: boolean }) {
const [formData, setFormData] = useState({
source: "",
sourceUserId: "",
sourceRecordId: "",
originalData: "",
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
try {
const parsedData = JSON.parse(formData.originalData)
onSubmit({
...formData,
originalData: parsedData,
})
} catch (error) {
alert("原始数据必须是有效的JSON格式")
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="source"></Label>
<Select value={formData.source} onValueChange={(value) => setFormData({ ...formData, source: value })}>
<SelectTrigger>
<SelectValue placeholder="选择数据源" />
</SelectTrigger>
<SelectContent>
<SelectItem value="douyin"></SelectItem>
<SelectItem value="xiaohongshu"></SelectItem>
<SelectItem value="cunkebao_form"></SelectItem>
<SelectItem value="touchkebao_call"></SelectItem>
<SelectItem value="feishu_notes"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="sourceUserId">ID</Label>
<Input
id="sourceUserId"
value={formData.sourceUserId}
onChange={(e) => setFormData({ ...formData, sourceUserId: e.target.value })}
placeholder="可选"
/>
</div>
<div className="space-y-2">
<Label htmlFor="sourceRecordId">ID</Label>
<Input
id="sourceRecordId"
value={formData.sourceRecordId}
onChange={(e) => setFormData({ ...formData, sourceRecordId: e.target.value })}
placeholder="可选"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="originalData"> (JSON格式)</Label>
<Textarea
id="originalData"
value={formData.originalData}
onChange={(e) => setFormData({ ...formData, originalData: e.target.value })}
placeholder='{"name": "张三", "phone": "13800138000", "email": "zhangsan@example.com"}'
rows={8}
required
/>
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" disabled={isLoading}>
</Button>
<Button type="submit" disabled={isLoading || !formData.source || !formData.originalData}>
{isLoading ? (
<>
<Clock className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
</>
)}
</Button>
</div>
</form>
)
}

View File

@@ -3,552 +3,403 @@
import { useState } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { Database, Server, Link, Plus, FileText, RefreshCw } from "lucide-react"
import { Progress } from "@/components/ui/progress"
import {
Database,
GitBranch,
Zap,
Shield,
Activity,
CheckCircle,
AlertTriangle,
Clock,
TrendingUp,
Settings,
Plus,
} from "lucide-react"
import Link from "next/link"
export default function DataIntegrationPage() {
const [activeTab, setActiveTab] = useState("data-sources")
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [activeTab, setActiveTab] = useState("overview")
return (
<div className="container mx-auto py-6 space-y-8">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div>
<h1 className="text-3xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground">API接口</p>
</div>
</div>
const integrationStats = {
totalSources: 12,
activeSources: 10,
dailyVolume: 2456789,
successRate: 98.5,
avgLatency: 156,
qualityScore: 94.2,
}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full max-w-md grid-cols-2">
<TabsTrigger value="data-sources"></TabsTrigger>
<TabsTrigger value="api-management">API管理</TabsTrigger>
</TabsList>
<TabsContent value="data-sources" className="space-y-6">
<DataSourcesTab setIsDialogOpen={setIsDialogOpen} />
<div className="mt-4">
<Button variant="outline" asChild>
<a href="/database-structure">
<Database className="mr-2 h-4 w-4" />
</a>
</Button>
</div>
</TabsContent>
<TabsContent value="api-management" className="space-y-6">
<ApiManagementTab />
</TabsContent>
</Tabs>
<AddDataSourceDialog isOpen={isDialogOpen} setIsOpen={setIsDialogOpen} />
</div>
)
}
// 数据源管理标签页
function DataSourcesTab({ setIsDialogOpen }: { setIsDialogOpen: (open: boolean) => void }) {
// 模拟数据源列表
const dataSources = [
{
id: "1",
name: "用户数据库",
type: "MySQL",
host: "db.example.com",
id: 1,
name: "MySQL主库",
type: "database",
status: "connected",
lastSync: "2023-07-20 15:30",
tables: 24,
records: 156789,
lastSync: "2分钟前",
records: 1245678,
quality: 98.5,
},
{
id: "2",
name: "订单系统",
type: "PostgreSQL",
host: "orders.example.com",
id: 2,
name: "触客宝API",
type: "api",
status: "connected",
lastSync: "2023-07-19 12:45",
tables: 18,
records: 89456,
lastSync: "5分钟前",
records: 456789,
quality: 96.2,
},
{
id: "3",
name: "内容库",
type: "MongoDB",
host: "content.example.com",
id: 3,
name: "抖音内容平台",
type: "api",
status: "warning",
lastSync: "1小时前",
records: 234567,
quality: 89.3,
},
{
id: 4,
name: "小红书API",
type: "api",
status: "connected",
lastSync: "10分钟前",
records: 123456,
quality: 95.8,
},
{
id: 5,
name: "微信视频号",
type: "api",
status: "connected",
lastSync: "15分钟前",
records: 98765,
quality: 92.4,
},
{
id: 6,
name: "飞书妙记API",
type: "api",
status: "error",
lastSync: "2023-07-15 09:20",
tables: 12,
lastSync: "2小时前",
records: 45678,
},
{
id: "4",
name: "用户行为分析",
type: "ClickHouse",
host: "analytics.example.com",
status: "connected",
lastSync: "2023-07-20 10:15",
tables: 8,
records: 2345678,
},
{
id: "5",
name: "CRM系统",
type: "Oracle",
host: "crm.example.com",
status: "pending",
lastSync: "等待连接",
tables: 0,
records: 0,
quality: 78.9,
},
]
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold"></h2>
<Button onClick={() => setIsDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{dataSources.map((source) => (
<TableRow key={source.id}>
<TableCell>
<div className="flex items-center">
<Database className="h-4 w-4 mr-2 text-muted-foreground" />
<span className="font-medium">{source.name}</span>
</div>
</TableCell>
<TableCell>{source.type}</TableCell>
<TableCell>{source.host}</TableCell>
<TableCell>
<Badge
className={
source.status === "connected"
? "bg-green-100 text-green-800"
: source.status === "error"
? "bg-red-100 text-red-800"
: "bg-yellow-100 text-yellow-800"
}
>
{source.status === "connected" ? "已连接" : source.status === "error" ? "错误" : "等待中"}
</Badge>
</TableCell>
<TableCell>{source.lastSync}</TableCell>
<TableCell>{source.tables}</TableCell>
<TableCell>{source.records.toLocaleString()}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button variant="outline" size="sm">
<Link className="h-4 w-4 mr-1" />
</Button>
<Button size="sm">
<RefreshCw className="h-4 w-4 mr-1" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="flex flex-col items-center p-4 border rounded-lg">
<Database className="h-8 w-8 mb-2 text-blue-500" />
<span className="font-medium">MySQL</span>
<span className="text-xs text-muted-foreground"></span>
</div>
<div className="flex flex-col items-center p-4 border rounded-lg">
<Database className="h-8 w-8 mb-2 text-blue-500" />
<span className="font-medium">PostgreSQL</span>
<span className="text-xs text-muted-foreground"></span>
</div>
<div className="flex flex-col items-center p-4 border rounded-lg">
<Database className="h-8 w-8 mb-2 text-green-500" />
<span className="font-medium">MongoDB</span>
<span className="text-xs text-muted-foreground"></span>
</div>
<div className="flex flex-col items-center p-4 border rounded-lg">
<Database className="h-8 w-8 mb-2 text-yellow-500" />
<span className="font-medium">ClickHouse</span>
<span className="text-xs text-muted-foreground"></span>
</div>
<div className="flex flex-col items-center p-4 border rounded-lg">
<Database className="h-8 w-8 mb-2 text-red-500" />
<span className="font-medium">Oracle</span>
<span className="text-xs text-muted-foreground"></span>
</div>
<div className="flex flex-col items-center p-4 border rounded-lg">
<Database className="h-8 w-8 mb-2 text-purple-500" />
<span className="font-medium">SQL Server</span>
<span className="text-xs text-muted-foreground"></span>
</div>
<div className="flex flex-col items-center p-4 border rounded-lg">
<Server className="h-8 w-8 mb-2 text-gray-500" />
<span className="font-medium">Redis</span>
<span className="text-xs text-muted-foreground"></span>
</div>
<div className="flex flex-col items-center p-4 border rounded-lg">
<FileText className="h-8 w-8 mb-2 text-gray-500" />
<span className="font-medium">CSV/Excel</span>
<span className="text-xs text-muted-foreground"></span>
</div>
</div>
</CardContent>
</Card>
</div>
)
}
// API管理标签页
function ApiManagementTab() {
// 模拟API接口数据
const apiEndpoints = [
const recentActivities = [
{
id: "1",
name: "用户数据API",
endpoint: "/api/users",
method: "GET",
category: "用户画像",
status: "active",
calls: 12567,
lastCalled: "2023-07-20 16:45",
id: 1,
type: "sync_complete",
message: "MySQL主库数据同步完成",
time: "2分钟前",
status: "success",
},
{
id: "2",
name: "用户标签API",
endpoint: "/api/users/tags",
method: "GET",
category: "用户画像",
status: "active",
calls: 8945,
lastCalled: "2023-07-20 15:30",
id: 2,
type: "quality_check",
message: "触客宝API数据质量检查通过",
time: "8分钟前",
status: "success",
},
{
id: "3",
name: "流量池数据API",
endpoint: "/api/traffic-pools",
method: "GET",
category: "流量池",
status: "active",
calls: 5678,
lastCalled: "2023-07-20 14:20",
id: 3,
type: "sync_warning",
message: "抖音内容平台同步延迟",
time: "1小时前",
status: "warning",
},
{
id: "4",
name: "AI分析API",
endpoint: "/api/ai/analyze",
method: "POST",
category: "AI分析",
status: "active",
calls: 3456,
lastCalled: "2023-07-20 13:15",
},
{
id: "5",
name: "数据同步API",
endpoint: "/api/sync",
method: "POST",
category: "数据集成",
status: "maintenance",
calls: 2345,
lastCalled: "2023-07-19 10:30",
id: 4,
type: "sync_error",
message: "飞书妙记API连接失败",
time: "2小时前",
status: "error",
},
]
// 模拟API密钥数据
const apiKeys = [
{
id: "1",
name: "Web应用",
key: "sk_web_*************",
created: "2023-05-15",
lastUsed: "2023-07-20",
status: "active",
},
{
id: "2",
name: "移动应用",
key: "sk_mobile_*************",
created: "2023-06-10",
lastUsed: "2023-07-19",
status: "active",
},
{
id: "3",
name: "第三方集成",
key: "sk_partner_*************",
created: "2023-04-20",
lastUsed: "2023-07-18",
status: "active",
},
]
const getStatusIcon = (status: string) => {
switch (status) {
case "connected":
return <CheckCircle className="h-4 w-4 text-green-500" />
case "warning":
return <AlertTriangle className="h-4 w-4 text-yellow-500" />
case "error":
return <AlertTriangle className="h-4 w-4 text-red-500" />
default:
return <Clock className="h-4 w-4 text-gray-500" />
}
}
const getStatusColor = (status: string) => {
switch (status) {
case "connected":
return "bg-green-100 text-green-800"
case "warning":
return "bg-yellow-100 text-yellow-800"
case "error":
return "bg-red-100 text-red-800"
default:
return "bg-gray-100 text-gray-800"
}
}
const getActivityIcon = (type: string) => {
switch (type) {
case "sync_complete":
return <CheckCircle className="h-4 w-4 text-green-500" />
case "quality_check":
return <Shield className="h-4 w-4 text-blue-500" />
case "sync_warning":
return <AlertTriangle className="h-4 w-4 text-yellow-500" />
case "sync_error":
return <AlertTriangle className="h-4 w-4 text-red-500" />
default:
return <Activity className="h-4 w-4 text-gray-500" />
}
}
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold">API接口管理</h2>
<Button>
<Plus className="mr-2 h-4 w-4" />
API
</Button>
</div>
<Card>
<CardHeader>
<CardTitle>API接口列表</CardTitle>
<CardDescription>API接口</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>API名称</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{apiEndpoints.map((api) => (
<TableRow key={api.id}>
<TableCell className="font-medium">{api.name}</TableCell>
<TableCell>
<code className="bg-muted px-1 py-0.5 rounded text-sm">{api.endpoint}</code>
</TableCell>
<TableCell>
<Badge
className={
api.method === "GET"
? "bg-blue-100 text-blue-800"
: api.method === "POST"
? "bg-green-100 text-green-800"
: api.method === "PUT"
? "bg-yellow-100 text-yellow-800"
: "bg-red-100 text-red-800"
}
>
{api.method}
</Badge>
</TableCell>
<TableCell>{api.category}</TableCell>
<TableCell>
<Badge
className={
api.status === "active" ? "bg-green-100 text-green-800" : "bg-yellow-100 text-yellow-800"
}
>
{api.status === "active" ? "正常" : "维护中"}
</Badge>
</TableCell>
<TableCell>{api.calls.toLocaleString()}</TableCell>
<TableCell>{api.lastCalled}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button variant="outline" size="sm">
</Button>
<Button size="sm"></Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>API密钥管理</CardTitle>
<CardDescription>API访问密钥</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>使</TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{apiKeys.map((key) => (
<TableRow key={key.id}>
<TableCell className="font-medium">{key.name}</TableCell>
<TableCell>
<code className="bg-muted px-1 py-0.5 rounded text-sm">{key.key}</code>
</TableCell>
<TableCell>{key.created}</TableCell>
<TableCell>{key.lastUsed}</TableCell>
<TableCell>
<Badge className="bg-green-100 text-green-800">{key.status === "active" ? "有效" : "已禁用"}</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button variant="outline" size="sm">
</Button>
<Button variant="outline" size="sm" className="text-red-500">
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>API使用统计</CardTitle>
<CardDescription>API调用量和性能统计</CardDescription>
</CardHeader>
<CardContent className="h-80 flex items-center justify-center">
<div className="text-center text-muted-foreground">
<p>API调用统计图表</p>
</div>
</CardContent>
</Card>
</div>
)
}
// 添加数据源对话框
function AddDataSourceDialog({ isOpen, setIsOpen }: { isOpen: boolean; setIsOpen: (open: boolean) => void }) {
const [dbType, setDbType] = useState("mysql")
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="db-type" className="text-right">
</Label>
<Select value={dbType} onValueChange={setDbType} className="col-span-3">
<SelectTrigger id="db-type">
<SelectValue placeholder="选择数据库类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="mysql">MySQL</SelectItem>
<SelectItem value="postgresql">PostgreSQL</SelectItem>
<SelectItem value="mongodb">MongoDB</SelectItem>
<SelectItem value="clickhouse">ClickHouse</SelectItem>
<SelectItem value="oracle">Oracle</SelectItem>
<SelectItem value="sqlserver">SQL Server</SelectItem>
<SelectItem value="redis">Redis</SelectItem>
<SelectItem value="file">CSV/Excel文件</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
</Label>
<Input id="name" placeholder="给数据源起个名字" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="host" className="text-right">
</Label>
<Input id="host" placeholder="例如: db.example.com" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="port" className="text-right">
</Label>
<Input
id="port"
placeholder={dbType === "mysql" ? "3306" : dbType === "postgresql" ? "5432" : "27017"}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="database" className="text-right">
</Label>
<Input id="database" placeholder="数据库名称" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="username" className="text-right">
</Label>
<Input id="username" placeholder="数据库用户名" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="password" className="text-right">
</Label>
<Input id="password" type="password" placeholder="数据库密码" className="col-span-3" />
</div>
<div className="container mx-auto py-6 space-y-6">
{/* 页面标题 */}
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<div>
<h1 className="text-3xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsOpen(false)}>
<div className="flex gap-2">
<Button>
<Plus className="mr-2 h-4 w-4" />
</Button>
<Button type="submit"></Button>
<Button type="submit"></Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Button variant="outline">
<Settings className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
{/* 核心指标 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="bg-gradient-to-r from-blue-50 to-cyan-50 border-none shadow-md">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium"></CardTitle>
<Database className="h-4 w-4 text-blue-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-600">{integrationStats.totalSources}</div>
<div className="text-xs text-muted-foreground mt-1">: {integrationStats.activeSources}</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-green-50 to-emerald-50 border-none shadow-md">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium"></CardTitle>
<Activity className="h-4 w-4 text-green-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">
{(integrationStats.dailyVolume / 1000000).toFixed(1)}M
</div>
<div className="text-xs text-muted-foreground mt-1"></div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-purple-50 to-pink-50 border-none shadow-md">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium"></CardTitle>
<TrendingUp className="h-4 w-4 text-purple-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-purple-600">{integrationStats.successRate}%</div>
<div className="text-xs text-muted-foreground mt-1"></div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-orange-50 to-red-50 border-none shadow-md">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium"></CardTitle>
<Zap className="h-4 w-4 text-orange-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-orange-600">{integrationStats.avgLatency}ms</div>
<div className="text-xs text-muted-foreground mt-1"></div>
</CardContent>
</Card>
</div>
{/* 数据源状态 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<GitBranch className="h-5 w-5 text-blue-600" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{dataSources.map((source) => (
<div key={source.id} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-3">
{getStatusIcon(source.status)}
<div>
<p className="font-medium text-sm">{source.name}</p>
<p className="text-xs text-muted-foreground">
{source.type === "database" ? "数据库" : "API接口"} : {source.lastSync}
</p>
</div>
</div>
<div className="text-right">
<Badge className={getStatusColor(source.status)}>
{source.status === "connected" ? "已连接" : source.status === "warning" ? "警告" : "错误"}
</Badge>
<div className="flex items-center gap-4 mt-1 text-xs text-muted-foreground">
<span>{source.records.toLocaleString()} </span>
<span>: {source.quality}%</span>
</div>
</div>
</div>
))}
</div>
<div className="flex justify-between mt-4">
<Link href="/data-integration/sources">
<Button variant="outline"></Button>
</Link>
<Button></Button>
</div>
</CardContent>
</Card>
{/* 实时活动 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5 text-green-600" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{recentActivities.map((activity) => (
<div key={activity.id} className="flex items-start gap-3 p-2 hover:bg-gray-50 rounded-lg">
{getActivityIcon(activity.type)}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{activity.message}</p>
<p className="text-xs text-gray-500">{activity.time}</p>
</div>
</div>
))}
</div>
<Button variant="outline" className="w-full mt-4 bg-transparent">
</Button>
</CardContent>
</Card>
</div>
{/* 数据流程概览 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Zap className="h-5 w-5 text-purple-600" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Link href="/data-integration/sources">
<div className="text-center p-4 border rounded-lg hover:bg-gray-50 transition-colors cursor-pointer">
<GitBranch className="h-8 w-8 mx-auto mb-2 text-blue-600" />
<h3 className="font-medium mb-1"></h3>
<p className="text-xs text-muted-foreground"></p>
<Badge className="mt-2 bg-blue-100 text-blue-700">{integrationStats.totalSources}</Badge>
</div>
</Link>
<Link href="/data-integration/ingestion">
<div className="text-center p-4 border rounded-lg hover:bg-gray-50 transition-colors cursor-pointer">
<Database className="h-8 w-8 mx-auto mb-2 text-green-600" />
<h3 className="font-medium mb-1"></h3>
<p className="text-xs text-muted-foreground"></p>
<Badge className="mt-2 bg-green-100 text-green-700">
{(integrationStats.dailyVolume / 1000000).toFixed(1)}M/
</Badge>
</div>
</Link>
<Link href="/data-integration/processing">
<div className="text-center p-4 border rounded-lg hover:bg-gray-50 transition-colors cursor-pointer">
<Zap className="h-8 w-8 mx-auto mb-2 text-purple-600" />
<h3 className="font-medium mb-1"></h3>
<p className="text-xs text-muted-foreground"></p>
<Badge className="mt-2 bg-purple-100 text-purple-700">{integrationStats.successRate}%</Badge>
</div>
</Link>
<Link href="/data-integration/quality">
<div className="text-center p-4 border rounded-lg hover:bg-gray-50 transition-colors cursor-pointer">
<Shield className="h-8 w-8 mx-auto mb-2 text-orange-600" />
<h3 className="font-medium mb-1"></h3>
<p className="text-xs text-muted-foreground"></p>
<Badge className="mt-2 bg-orange-100 text-orange-700">{integrationStats.qualityScore}%</Badge>
</div>
</Link>
</div>
</CardContent>
</Card>
{/* 性能监控 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5 text-green-600" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<div className="flex justify-between text-sm mb-2">
<span></span>
<span className="font-medium">85%</span>
</div>
<Progress value={85} className="h-2 mb-1" />
<p className="text-xs text-muted-foreground">1.2M /</p>
</div>
<div>
<div className="flex justify-between text-sm mb-2">
<span>使</span>
<span className="font-medium">67%</span>
</div>
<Progress value={67} className="h-2 mb-1" />
<p className="text-xs text-muted-foreground">CPU + </p>
</div>
<div>
<div className="flex justify-between text-sm mb-2">
<span>使</span>
<span className="font-medium">42%</span>
</div>
<Progress value={42} className="h-2 mb-1" />
<p className="text-xs text-muted-foreground">2.1TB / 5TB</p>
</div>
</div>
</CardContent>
</Card>
</div>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,567 @@
"use client"
import type React from "react"
import { useState, useCallback } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Avatar } from "@/components/ui/avatar"
import { Progress } from "@/components/ui/progress"
import { Switch } from "@/components/ui/switch"
import { Label } from "@/components/ui/label"
import {
Search,
Sparkles,
Clock,
TrendingUp,
Users,
Brain,
Zap,
Download,
RefreshCw,
Eye,
BarChart3,
Lightbulb,
} from "lucide-react"
import { TooltipHelp, TooltipProvider } from "@/components/ui/tooltip-help"
interface SearchResult {
id: string
type: "user" | "traffic" | "insight"
title: string
description: string
tags: string[]
relevanceScore: number
updatedAt: string
metadata?: Record<string, any>
}
interface SearchResponse {
results: SearchResult[]
stats: {
totalResults: number
queryTime: number
suggestions: string[]
filters: Record<string, any>
}
hasMore: boolean
}
export default function IntelligentSearchPage() {
const [searchQuery, setSearchQuery] = useState("")
const [searchType, setSearchType] = useState<"all" | "user" | "traffic">("all")
const [useAI, setUseAI] = useState(true)
const [includeInsights, setIncludeInsights] = useState(true)
const [isSearching, setIsSearching] = useState(false)
const [searchResults, setSearchResults] = useState<SearchResponse | null>(null)
const [searchHistory, setSearchHistory] = useState<string[]>([])
const [activeTab, setActiveTab] = useState("search")
// 执行搜索
const performSearch = useCallback(
async (query: string) => {
if (!query.trim()) return
setIsSearching(true)
try {
const response = await fetch("/api/search", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query,
type: searchType,
options: {
useAI,
includeInsights,
limit: 50,
},
}),
})
if (!response.ok) {
throw new Error("搜索失败")
}
const data: SearchResponse = await response.json()
setSearchResults(data)
// 添加到搜索历史
setSearchHistory((prev) => {
const newHistory = [query, ...prev.filter((h) => h !== query)].slice(0, 10)
return newHistory
})
} catch (error) {
console.error("搜索错误:", error)
} finally {
setIsSearching(false)
}
},
[searchType, useAI, includeInsights],
)
// 处理搜索输入
const handleSearch = () => {
performSearch(searchQuery)
}
// 处理回车键搜索
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
handleSearch()
}
}
// 获取类型图标
const getTypeIcon = (type: string) => {
switch (type) {
case "user":
return <Users className="h-4 w-4 text-blue-500" />
case "traffic":
return <TrendingUp className="h-4 w-4 text-green-500" />
case "insight":
return <Lightbulb className="h-4 w-4 text-purple-500" />
default:
return <Search className="h-4 w-4 text-gray-500" />
}
}
// 获取类型标签颜色
const getTypeBadgeColor = (type: string) => {
switch (type) {
case "user":
return "bg-blue-100 text-blue-800"
case "traffic":
return "bg-green-100 text-green-800"
case "insight":
return "bg-purple-100 text-purple-800"
default:
return "bg-gray-100 text-gray-800"
}
}
return (
<TooltipProvider>
<div className="container mx-auto p-6 space-y-6">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold flex items-center gap-2">
<Brain className="h-8 w-8 text-purple-600" />
</h1>
<p className="text-muted-foreground mt-2">AI驱动的亚秒级数据搜索与洞察分析</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">
<Download className="h-4 w-4 mr-2" />
</Button>
<Button variant="outline" size="sm">
<BarChart3 className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
{/* 搜索配置卡片 */}
<Card className="bg-gradient-to-r from-purple-50 to-blue-50 border-none shadow-lg">
<CardContent className="p-6">
<div className="space-y-4">
{/* 主搜索框 */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground" />
<Input
type="text"
placeholder="输入搜索关键词,支持自然语言查询..."
className="pl-10 h-12 text-base border-2 border-purple-200 focus:border-purple-400"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyPress={handleKeyPress}
/>
<Button
onClick={handleSearch}
disabled={isSearching || !searchQuery.trim()}
className="absolute right-2 top-1/2 transform -translate-y-1/2"
size="sm"
>
{isSearching ? <RefreshCw className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />}
</Button>
</div>
{/* 搜索选项 */}
<div className="flex flex-wrap items-center gap-6">
<div className="flex items-center gap-2">
<Label htmlFor="search-type">:</Label>
<select
id="search-type"
value={searchType}
onChange={(e) => setSearchType(e.target.value as any)}
className="px-3 py-1 border rounded-md text-sm"
>
<option value="all"></option>
<option value="user"></option>
<option value="traffic"></option>
</select>
</div>
<div className="flex items-center gap-2">
<Switch id="use-ai" checked={useAI} onCheckedChange={setUseAI} />
<Label htmlFor="use-ai" className="flex items-center gap-1">
<Sparkles className="h-4 w-4 text-purple-500" />
AI增强搜索
<TooltipHelp content="启用AI增强搜索提供更智能的查询理解和结果优化" />
</Label>
</div>
<div className="flex items-center gap-2">
<Switch id="include-insights" checked={includeInsights} onCheckedChange={setIncludeInsights} />
<Label htmlFor="include-insights" className="flex items-center gap-1">
<Brain className="h-4 w-4 text-blue-500" />
AI洞察
<TooltipHelp content="在搜索结果中包含AI生成的业务洞察和分析建议" />
</Label>
</div>
</div>
{/* 搜索历史 */}
{searchHistory.length > 0 && (
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">:</span>
<div className="flex flex-wrap gap-1">
{searchHistory.slice(0, 5).map((query, index) => (
<Button
key={index}
variant="outline"
size="sm"
className="h-6 px-2 text-xs bg-transparent"
onClick={() => {
setSearchQuery(query)
performSearch(query)
}}
>
{query}
</Button>
))}
</div>
</div>
)}
</div>
</CardContent>
</Card>
{/* 搜索结果 */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="search"></TabsTrigger>
<TabsTrigger value="insights">AI洞察</TabsTrigger>
<TabsTrigger value="analytics"></TabsTrigger>
<TabsTrigger value="history"></TabsTrigger>
</TabsList>
{/* 搜索结果标签页 */}
<TabsContent value="search" className="space-y-4">
{searchResults && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2">
<Search className="h-5 w-5" />
<Badge variant="outline">{searchResults.stats.totalResults} </Badge>
</CardTitle>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Zap className="h-4 w-4" />
: {searchResults.stats.queryTime}ms
</div>
</div>
{searchResults.stats.suggestions.length > 0 && (
<CardDescription>
<div className="flex items-center gap-2 mt-2">
<span>:</span>
<div className="flex flex-wrap gap-1">
{searchResults.stats.suggestions.map((suggestion, index) => (
<Button
key={index}
variant="ghost"
size="sm"
className="h-6 px-2 text-xs text-blue-600 hover:text-blue-800"
onClick={() => {
setSearchQuery(suggestion)
performSearch(suggestion)
}}
>
{suggestion}
</Button>
))}
</div>
</div>
</CardDescription>
)}
</CardHeader>
<CardContent>
{searchResults.results.length > 0 ? (
<div className="space-y-4">
{searchResults.results.map((result) => (
<div
key={result.id}
className="flex items-start gap-4 p-4 border rounded-lg hover:bg-gray-50 transition-colors cursor-pointer"
>
{/* 类型图标 */}
<div className="flex-shrink-0 mt-1">
{result.type === "user" ? (
<Avatar className="h-10 w-10">
<div className="w-full h-full bg-blue-100 flex items-center justify-center">
<Users className="h-5 w-5 text-blue-600" />
</div>
</Avatar>
) : (
<div className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center">
{getTypeIcon(result.type)}
</div>
)}
</div>
{/* 内容 */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<h3 className="font-semibold text-lg truncate">{result.title}</h3>
<Badge className={getTypeBadgeColor(result.type)}>
{result.type === "user" ? "用户" : result.type === "traffic" ? "流量" : "洞察"}
</Badge>
<Badge variant="outline" className="text-xs">
: {(result.relevanceScore * 100).toFixed(1)}%
</Badge>
</div>
<p className="text-sm text-muted-foreground mb-2 line-clamp-2">{result.description}</p>
{/* 标签 */}
<div className="flex flex-wrap gap-1 mb-2">
{result.tags.slice(0, 5).map((tag, index) => (
<Badge key={index} variant="secondary" className="text-xs">
{tag}
</Badge>
))}
</div>
{/* 元数据 */}
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{new Date(result.updatedAt).toLocaleString("zh-CN")}
</span>
{result.metadata?.searchType && <span>: {result.metadata.searchType}</span>}
</div>
</div>
{/* 操作按钮 */}
<div className="flex-shrink-0">
<Button variant="outline" size="sm">
<Eye className="h-4 w-4 mr-1" />
</Button>
</div>
</div>
))}
{/* 加载更多 */}
{searchResults.hasMore && (
<div className="text-center pt-4">
<Button variant="outline"></Button>
</div>
)}
</div>
) : (
<div className="text-center py-12">
<Search className="h-12 w-12 mx-auto mb-4 text-muted-foreground opacity-50" />
<h3 className="text-lg font-medium mb-2"></h3>
<p className="text-muted-foreground">使</p>
</div>
)}
</CardContent>
</Card>
)}
{/* 空状态 */}
{!searchResults && (
<Card>
<CardContent className="text-center py-12">
<Brain className="h-16 w-16 mx-auto mb-4 text-purple-400" />
<h3 className="text-xl font-medium mb-2"></h3>
<p className="text-muted-foreground mb-4">AI增强分析</p>
<div className="flex flex-wrap justify-center gap-2">
{["高价值用户", "流量趋势", "用户行为分析", "RFM分群"].map((example) => (
<Button
key={example}
variant="outline"
size="sm"
onClick={() => {
setSearchQuery(example)
performSearch(example)
}}
>
{example}
</Button>
))}
</div>
</CardContent>
</Card>
)}
</TabsContent>
{/* AI洞察标签页 */}
<TabsContent value="insights" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-purple-600" />
AI智能洞察
<TooltipHelp content="基于搜索结果生成的AI洞察和业务建议" />
</CardTitle>
<CardDescription>AI分析搜索数据</CardDescription>
</CardHeader>
<CardContent>
{searchResults?.results.filter((r) => r.type === "insight").length > 0 ? (
<div className="space-y-4">
{searchResults.results
.filter((r) => r.type === "insight")
.map((insight) => (
<div key={insight.id} className="p-4 border rounded-lg bg-purple-50">
<div className="flex items-start gap-3">
<Lightbulb className="h-5 w-5 text-purple-600 mt-1" />
<div className="flex-1">
<h4 className="font-medium mb-2">{insight.title}</h4>
<p className="text-sm text-muted-foreground mb-2">{insight.description}</p>
<div className="flex items-center gap-2">
<Badge variant="secondary" className="text-xs">
: {(insight.relevanceScore * 100).toFixed(1)}%
</Badge>
<span className="text-xs text-muted-foreground">
{new Date(insight.updatedAt).toLocaleString("zh-CN")}
</span>
</div>
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<Sparkles className="h-12 w-12 mx-auto mb-4 text-purple-400 opacity-50" />
<p className="text-muted-foreground">AI将为您生成智能洞察</p>
</div>
)}
</CardContent>
</Card>
</TabsContent>
{/* 搜索分析标签页 */}
<TabsContent value="analytics" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold mb-2">{searchResults?.stats.queryTime || 0}ms</div>
<Progress value={Math.min((searchResults?.stats.queryTime || 0) / 10, 100)} className="h-2" />
<p className="text-xs text-muted-foreground mt-2">: &lt;1000ms</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold mb-2">
{searchResults
? Math.round(
(searchResults.results.reduce((sum, r) => sum + r.relevanceScore, 0) /
searchResults.results.length) *
100,
)
: 0}
%
</div>
<Progress
value={
searchResults
? Math.round(
(searchResults.results.reduce((sum, r) => sum + r.relevanceScore, 0) /
searchResults.results.length) *
100,
)
: 0
}
className="h-2"
/>
<p className="text-xs text-muted-foreground mt-2"></p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">AI增强率</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold mb-2">{useAI ? "100" : "0"}%</div>
<Progress value={useAI ? 100 : 0} className="h-2" />
<p className="text-xs text-muted-foreground mt-2">AI功能启用状态</p>
</CardContent>
</Card>
</div>
</TabsContent>
{/* 搜索历史标签页 */}
<TabsContent value="history" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Clock className="h-5 w-5" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
{searchHistory.length > 0 ? (
<div className="space-y-2">
{searchHistory.map((query, index) => (
<div
key={index}
className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50 cursor-pointer"
onClick={() => {
setSearchQuery(query)
performSearch(query)
}}
>
<div className="flex items-center gap-3">
<Search className="h-4 w-4 text-muted-foreground" />
<span>{query}</span>
</div>
<Button variant="ghost" size="sm">
</Button>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<Clock className="h-12 w-12 mx-auto mb-4 text-muted-foreground opacity-50" />
<p className="text-muted-foreground"></p>
</div>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
</TooltipProvider>
)
}

View File

@@ -0,0 +1,268 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Progress } from "@/components/ui/progress"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import {
BarChart3,
TrendingUp,
Users,
Database,
Activity,
RefreshCw,
Download,
Eye,
Zap,
Target,
AlertCircle,
} from "lucide-react"
export default function DashboardPage() {
const [timeRange, setTimeRange] = useState("today")
const dashboardStats = {
totalUsers: 125678,
activeUsers: 45678,
dataVolume: 1245678,
aiAccuracy: 96.8,
growthRate: 12.5,
qualityScore: 94.2,
}
const recentActivities = [
{
id: 1,
type: "data_sync",
message: "用户数据同步完成",
time: "2分钟前",
status: "success",
},
{
id: 2,
type: "ai_analysis",
message: "AI模型训练完成",
time: "5分钟前",
status: "success",
},
{
id: 3,
type: "quality_check",
message: "数据质量检查通过",
time: "10分钟前",
status: "warning",
},
]
return (
<div className="container mx-auto py-6 space-y-6">
{/* 页面标题 */}
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<div>
<h1 className="text-3xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
<div className="flex gap-2">
<Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="时间范围" />
</SelectTrigger>
<SelectContent>
<SelectItem value="today"></SelectItem>
<SelectItem value="week"></SelectItem>
<SelectItem value="month"></SelectItem>
</SelectContent>
</Select>
<Button variant="outline" size="icon">
<RefreshCw className="h-4 w-4" />
</Button>
<Button variant="outline">
<Download className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
{/* 核心指标卡片 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="bg-gradient-to-r from-blue-50 to-cyan-50 border-none shadow-md">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium"></CardTitle>
<Users className="h-4 w-4 text-blue-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-600">{dashboardStats.totalUsers.toLocaleString()}</div>
<div className="flex items-center mt-2">
<TrendingUp className="h-3 w-3 text-green-500 mr-1" />
<span className="text-xs text-green-600">+{dashboardStats.growthRate}%</span>
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-green-50 to-emerald-50 border-none shadow-md">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium"></CardTitle>
<Activity className="h-4 w-4 text-green-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{dashboardStats.activeUsers.toLocaleString()}</div>
<div className="text-xs text-muted-foreground mt-2">
{((dashboardStats.activeUsers / dashboardStats.totalUsers) * 100).toFixed(1)}%
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-purple-50 to-pink-50 border-none shadow-md">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium"></CardTitle>
<Database className="h-4 w-4 text-purple-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-purple-600">{dashboardStats.dataVolume.toLocaleString()}</div>
<div className="text-xs text-muted-foreground mt-2"></div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-orange-50 to-red-50 border-none shadow-md">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium">AI准确率</CardTitle>
<Target className="h-4 w-4 text-orange-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-orange-600">{dashboardStats.aiAccuracy}%</div>
<div className="text-xs text-muted-foreground mt-2"></div>
</CardContent>
</Card>
</div>
{/* 详细分析 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 数据质量监控 */}
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BarChart3 className="h-5 w-5 text-blue-600" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<div className="flex justify-between text-sm mb-2">
<span></span>
<span className="font-medium">{dashboardStats.qualityScore}%</span>
</div>
<Progress value={dashboardStats.qualityScore} className="h-2" />
</div>
<div>
<div className="flex justify-between text-sm mb-2">
<span></span>
<span className="font-medium">98.5%</span>
</div>
<Progress value={98.5} className="h-2" />
</div>
<div>
<div className="flex justify-between text-sm mb-2">
<span></span>
<span className="font-medium">96.2%</span>
</div>
<Progress value={96.2} className="h-2" />
</div>
<div>
<div className="flex justify-between text-sm mb-2">
<span></span>
<span className="font-medium">94.8%</span>
</div>
<Progress value={94.8} className="h-2" />
</div>
</div>
</CardContent>
</Card>
{/* 实时活动 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5 text-green-600" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{recentActivities.map((activity) => (
<div key={activity.id} className="flex items-start gap-3 p-2 hover:bg-gray-50 rounded-lg">
<div
className={`p-1 rounded-full ${
activity.status === "success"
? "text-green-600"
: activity.status === "warning"
? "text-yellow-600"
: "text-red-600"
}`}
>
{activity.status === "success" ? (
<Zap className="h-4 w-4" />
) : activity.status === "warning" ? (
<AlertCircle className="h-4 w-4" />
) : (
<AlertCircle className="h-4 w-4" />
)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{activity.message}</p>
<p className="text-xs text-gray-500">{activity.time}</p>
</div>
</div>
))}
</div>
<Button variant="outline" className="w-full mt-4 bg-transparent">
</Button>
</CardContent>
</Card>
</div>
{/* 快速操作 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Zap className="h-5 w-5 text-yellow-600" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Button variant="outline" className="h-16 flex flex-col gap-1 bg-transparent">
<Eye className="h-5 w-5" />
<span className="text-xs"></span>
</Button>
<Button variant="outline" className="h-16 flex flex-col gap-1 bg-transparent">
<Database className="h-5 w-5" />
<span className="text-xs"></span>
</Button>
<Button variant="outline" className="h-16 flex flex-col gap-1 bg-transparent">
<Users className="h-5 w-5" />
<span className="text-xs"></span>
</Button>
<Button variant="outline" className="h-16 flex flex-col gap-1 bg-transparent">
<Target className="h-5 w-5" />
<span className="text-xs">AI分析</span>
</Button>
</div>
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,283 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardDescription, 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,
Database,
Cpu,
HardDrive,
Network,
AlertTriangle,
CheckCircle,
XCircle,
RefreshCw,
Bell,
} from "lucide-react"
export default function MonitoringPage() {
const [isRefreshing, setIsRefreshing] = useState(false)
const [lastUpdate, setLastUpdate] = useState(new Date())
const systemMetrics = {
cpu: 45.2,
memory: 67.8,
disk: 34.5,
network: 23.1,
}
const serviceStatus = [
{ name: "数据接入服务", status: "running", uptime: "99.9%", responseTime: "120ms" },
{ name: "AI分析引擎", status: "running", uptime: "99.7%", responseTime: "340ms" },
{ name: "用户画像服务", status: "warning", uptime: "98.5%", responseTime: "580ms" },
{ name: "数据存储服务", status: "running", uptime: "99.8%", responseTime: "45ms" },
{ name: "搜索引擎", status: "running", uptime: "99.6%", responseTime: "89ms" },
]
const alerts = [
{
id: 1,
level: "warning",
message: "用户画像服务响应时间超过阈值",
time: "5分钟前",
service: "用户画像服务",
},
{
id: 2,
level: "info",
message: "数据同步任务完成",
time: "10分钟前",
service: "数据接入服务",
},
{
id: 3,
level: "success",
message: "AI模型训练完成",
time: "15分钟前",
service: "AI分析引擎",
},
]
const handleRefresh = () => {
setIsRefreshing(true)
setTimeout(() => {
setIsRefreshing(false)
setLastUpdate(new Date())
}, 2000)
}
const getStatusIcon = (status: string) => {
switch (status) {
case "running":
return <CheckCircle className="h-4 w-4 text-green-500" />
case "warning":
return <AlertTriangle className="h-4 w-4 text-yellow-500" />
case "error":
return <XCircle className="h-4 w-4 text-red-500" />
default:
return <CheckCircle className="h-4 w-4 text-gray-500" />
}
}
const getStatusColor = (status: string) => {
switch (status) {
case "running":
return "bg-green-100 text-green-800"
case "warning":
return "bg-yellow-100 text-yellow-800"
case "error":
return "bg-red-100 text-red-800"
default:
return "bg-gray-100 text-gray-800"
}
}
const getAlertIcon = (level: string) => {
switch (level) {
case "warning":
return <AlertTriangle className="h-4 w-4 text-yellow-500" />
case "error":
return <XCircle className="h-4 w-4 text-red-500" />
case "success":
return <CheckCircle className="h-4 w-4 text-green-500" />
default:
return <Bell className="h-4 w-4 text-blue-500" />
}
}
return (
<div className="container mx-auto py-6 space-y-6">
{/* 页面标题 */}
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<div>
<h1 className="text-3xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">: {lastUpdate.toLocaleTimeString()}</span>
<Button variant="outline" size="icon" onClick={handleRefresh} disabled={isRefreshing}>
<RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
</Button>
</div>
</div>
{/* 系统资源监控 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="bg-gradient-to-r from-blue-50 to-cyan-50 border-none shadow-md">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium">CPU使用率</CardTitle>
<Cpu className="h-4 w-4 text-blue-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-600">{systemMetrics.cpu}%</div>
<Progress value={systemMetrics.cpu} className="mt-2 h-2" />
<div className="text-xs text-muted-foreground mt-1"></div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-green-50 to-emerald-50 border-none shadow-md">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium">使</CardTitle>
<HardDrive className="h-4 w-4 text-green-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{systemMetrics.memory}%</div>
<Progress value={systemMetrics.memory} className="mt-2 h-2" />
<div className="text-xs text-muted-foreground mt-1"></div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-purple-50 to-pink-50 border-none shadow-md">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium">使</CardTitle>
<Database className="h-4 w-4 text-purple-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-purple-600">{systemMetrics.disk}%</div>
<Progress value={systemMetrics.disk} className="mt-2 h-2" />
<div className="text-xs text-muted-foreground mt-1"></div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-orange-50 to-red-50 border-none shadow-md">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium"></CardTitle>
<Network className="h-4 w-4 text-orange-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-orange-600">{systemMetrics.network}%</div>
<Progress value={systemMetrics.network} className="mt-2 h-2" />
<div className="text-xs text-muted-foreground mt-1"></div>
</CardContent>
</Card>
</div>
{/* 服务状态监控 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Server className="h-5 w-5 text-blue-600" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{serviceStatus.map((service, index) => (
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex items-center gap-3">
{getStatusIcon(service.status)}
<div>
<p className="font-medium text-sm">{service.name}</p>
<p className="text-xs text-muted-foreground">: {service.responseTime}</p>
</div>
</div>
<div className="text-right">
<Badge className={getStatusColor(service.status)}>
{service.status === "running" ? "运行中" : service.status === "warning" ? "警告" : "错误"}
</Badge>
<p className="text-xs text-muted-foreground mt-1">: {service.uptime}</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* 告警信息 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Bell className="h-5 w-5 text-yellow-600" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{alerts.map((alert) => (
<div key={alert.id} className="flex items-start gap-3 p-3 border rounded-lg">
{getAlertIcon(alert.level)}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium">{alert.message}</p>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs text-muted-foreground">{alert.service}</span>
<span className="text-xs text-muted-foreground"></span>
<span className="text-xs text-muted-foreground">{alert.time}</span>
</div>
</div>
</div>
))}
</div>
<Button variant="outline" className="w-full mt-4 bg-transparent">
</Button>
</CardContent>
</Card>
</div>
{/* 性能趋势 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5 text-green-600" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="text-center p-4 bg-blue-50 rounded-lg">
<div className="text-2xl font-bold text-blue-600">99.8%</div>
<div className="text-sm text-muted-foreground"></div>
<div className="text-xs text-green-600 mt-1"> 0.2%</div>
</div>
<div className="text-center p-4 bg-green-50 rounded-lg">
<div className="text-2xl font-bold text-green-600">156ms</div>
<div className="text-sm text-muted-foreground"></div>
<div className="text-xs text-green-600 mt-1"> 12ms</div>
</div>
<div className="text-center p-4 bg-purple-50 rounded-lg">
<div className="text-2xl font-bold text-purple-600">1.2M</div>
<div className="text-sm text-muted-foreground"></div>
<div className="text-xs text-green-600 mt-1"> 15%</div>
</div>
</div>
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,439 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Avatar } from "@/components/ui/avatar"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import {
Search,
Filter,
SortAsc,
User,
Tag,
Database,
BrainCircuit,
Phone,
Mail,
MapPin,
Calendar,
TrendingUp,
Eye,
RefreshCw,
} from "lucide-react"
export default function SearchPage() {
const [searchQuery, setSearchQuery] = useState("")
const [searchType, setSearchType] = useState("all")
const [sortBy, setSortBy] = useState("relevance")
const [isSearching, setIsSearching] = useState(false)
const [searchResults, setSearchResults] = useState<any[]>([])
// 模拟搜索数据
const mockSearchData = [
{
id: "user_001",
type: "user",
name: "张明华",
phone: "13800138000",
email: "zhangsan@example.com",
company: "科技创新有限公司",
tags: ["高价值", "技术决策者", "活跃用户", "iOS用户"],
aiInsights: ["科技爱好者", "决策影响者", "高消费潜力"],
lastActive: "2小时前",
location: "北京市",
avatar: "/placeholder.svg?height=40&width=40",
rfmScore: 95,
sources: ["抖音", "微信", "表单提交"],
joinDate: "2023-06-15",
relevanceScore: 98,
},
{
id: "user_002",
type: "user",
name: "李雨婷",
phone: "13900139001",
email: "lisi@example.com",
company: "数字营销公司",
tags: ["中高价值", "产品经理", "内容创作者"],
aiInsights: ["创意思维", "社交活跃", "品牌敏感"],
lastActive: "1天前",
location: "上海市",
avatar: "/placeholder.svg?height=40&width=40",
rfmScore: 78,
sources: ["小红书", "微信", "客服咨询"],
joinDate: "2023-08-22",
relevanceScore: 85,
},
{
id: "tag_001",
type: "tag",
name: "高价值用户",
description: "RFM评分超过80分的用户",
userCount: 12456,
category: "价值分类",
createdDate: "2023-05-10",
relevanceScore: 92,
},
{
id: "insight_001",
type: "insight",
title: "用户行为模式分析",
description: "基于AI分析的用户行为偏好洞察",
confidence: 94.5,
affectedUsers: 8765,
createdDate: "2023-12-01",
relevanceScore: 88,
},
]
// 搜索处理
const handleSearch = async (query: string) => {
if (!query.trim()) {
setSearchResults([])
return
}
setIsSearching(true)
// 模拟搜索延迟
setTimeout(() => {
const filtered = mockSearchData.filter((item) => {
const searchText = query.toLowerCase()
if (searchType !== "all" && item.type !== searchType) {
return false
}
// 根据不同类型进行搜索
switch (item.type) {
case "user":
return (
item.name.toLowerCase().includes(searchText) ||
item.phone.includes(searchText) ||
item.email.toLowerCase().includes(searchText) ||
item.company.toLowerCase().includes(searchText) ||
item.tags.some((tag: string) => tag.toLowerCase().includes(searchText)) ||
item.aiInsights.some((insight: string) => insight.toLowerCase().includes(searchText)) ||
item.location.toLowerCase().includes(searchText)
)
case "tag":
return (
item.name.toLowerCase().includes(searchText) ||
item.description.toLowerCase().includes(searchText) ||
item.category.toLowerCase().includes(searchText)
)
case "insight":
return item.title.toLowerCase().includes(searchText) || item.description.toLowerCase().includes(searchText)
default:
return false
}
})
// 排序
if (sortBy === "relevance") {
filtered.sort((a, b) => b.relevanceScore - a.relevanceScore)
} else if (sortBy === "date") {
filtered.sort(
(a, b) => new Date(b.createdDate || b.joinDate).getTime() - new Date(a.createdDate || a.joinDate).getTime(),
)
} else if (sortBy === "name") {
filtered.sort((a, b) => (a.name || a.title).localeCompare(b.name || b.title))
}
setSearchResults(filtered)
setIsSearching(false)
}, 800)
}
// 搜索输入处理
useEffect(() => {
const timeoutId = setTimeout(() => {
if (searchQuery) {
handleSearch(searchQuery)
}
}, 500)
return () => clearTimeout(timeoutId)
}, [searchQuery, searchType, sortBy])
const getTypeIcon = (type: string) => {
switch (type) {
case "user":
return <User className="h-4 w-4 text-blue-500" />
case "tag":
return <Tag className="h-4 w-4 text-green-500" />
case "insight":
return <BrainCircuit className="h-4 w-4 text-purple-500" />
default:
return <Database className="h-4 w-4 text-gray-500" />
}
}
const getTypeBadgeColor = (type: string) => {
switch (type) {
case "user":
return "bg-blue-100 text-blue-700"
case "tag":
return "bg-green-100 text-green-700"
case "insight":
return "bg-purple-100 text-purple-700"
default:
return "bg-gray-100 text-gray-700"
}
}
return (
<div className="container mx-auto py-6 space-y-6">
{/* 页面标题 */}
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<div>
<h1 className="text-3xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground">AI驱动的全局智能搜索引擎</p>
</div>
</div>
{/* 搜索栏 */}
<Card className="bg-gradient-to-r from-purple-50 to-blue-50 border-none shadow-lg">
<CardContent className="p-6">
<div className="space-y-4">
{/* 主搜索框 */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground" />
<Input
type="text"
placeholder="搜索用户、标签、洞察或任何相关信息..."
className="pl-10 h-12 text-base border-2 border-purple-200 focus:border-purple-400"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
{isSearching && (
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
<RefreshCw className="h-5 w-5 animate-spin text-purple-500" />
</div>
)}
</div>
{/* 搜索选项 */}
<div className="flex flex-wrap gap-4">
<div className="flex items-center gap-2">
<Filter className="h-4 w-4 text-muted-foreground" />
<Select value={searchType} onValueChange={setSearchType}>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="user"></SelectItem>
<SelectItem value="tag"></SelectItem>
<SelectItem value="insight"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<SortAsc className="h-4 w-4 text-muted-foreground" />
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="排序" />
</SelectTrigger>
<SelectContent>
<SelectItem value="relevance"></SelectItem>
<SelectItem value="date"></SelectItem>
<SelectItem value="name"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</CardContent>
</Card>
{/* 搜索结果 */}
{searchQuery && (
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span></span>
<Badge variant="outline">{searchResults.length} </Badge>
</CardTitle>
<CardDescription>
: "{searchQuery}"{searchType !== "all" && ` · 类型: ${searchType}`}
</CardDescription>
</CardHeader>
<CardContent>
{searchResults.length > 0 ? (
<div className="space-y-4">
{searchResults.map((result) => (
<div
key={result.id}
className="flex items-start gap-4 p-4 border rounded-lg hover:bg-gray-50 transition-colors cursor-pointer"
>
{/* 类型图标 */}
<div className="flex-shrink-0 mt-1">
{result.type === "user" ? (
<Avatar className="h-10 w-10">
<img src={result.avatar || "/placeholder.svg"} alt={result.name} />
</Avatar>
) : (
<div className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center">
{getTypeIcon(result.type)}
</div>
)}
</div>
{/* 内容 */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<h3 className="font-semibold text-lg truncate">{result.name || result.title}</h3>
<Badge className={getTypeBadgeColor(result.type)}>
{result.type === "user" ? "用户" : result.type === "tag" ? "标签" : "洞察"}
</Badge>
{result.relevanceScore && (
<Badge variant="outline" className="text-xs">
: {result.relevanceScore}%
</Badge>
)}
</div>
{/* 用户信息 */}
{result.type === "user" && (
<div className="space-y-2">
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<Phone className="h-3 w-3" />
{result.phone}
</span>
<span className="flex items-center gap-1">
<Mail className="h-3 w-3" />
{result.email}
</span>
<span className="flex items-center gap-1">
<MapPin className="h-3 w-3" />
{result.location}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">:</span>
<span className="text-sm">{result.company}</span>
<Badge className="bg-orange-100 text-orange-700 text-xs">RFM: {result.rfmScore}</Badge>
</div>
<div className="flex flex-wrap gap-1">
{result.aiInsights.slice(0, 3).map((insight: string, index: number) => (
<Badge key={index} className="text-xs bg-purple-100 text-purple-700">
{insight}
</Badge>
))}
</div>
</div>
)}
{/* 标签信息 */}
{result.type === "tag" && (
<div className="space-y-2">
<p className="text-sm text-muted-foreground">{result.description}</p>
<div className="flex items-center gap-4 text-sm">
<span className="flex items-center gap-1">
<User className="h-3 w-3" />
{result.userCount.toLocaleString()}
</span>
<span className="flex items-center gap-1">
<Tag className="h-3 w-3" />
{result.category}
</span>
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
{result.createdDate}
</span>
</div>
</div>
)}
{/* 洞察信息 */}
{result.type === "insight" && (
<div className="space-y-2">
<p className="text-sm text-muted-foreground">{result.description}</p>
<div className="flex items-center gap-4 text-sm">
<span className="flex items-center gap-1">
<TrendingUp className="h-3 w-3" />
: {result.confidence}%
</span>
<span className="flex items-center gap-1">
<User className="h-3 w-3" />
: {result.affectedUsers.toLocaleString()}
</span>
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
{result.createdDate}
</span>
</div>
</div>
)}
</div>
{/* 操作按钮 */}
<div className="flex-shrink-0">
<Button variant="outline" size="sm">
<Eye className="h-4 w-4 mr-1" />
</Button>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-12">
<Search className="h-12 w-12 mx-auto mb-4 text-muted-foreground opacity-50" />
<h3 className="text-lg font-medium mb-2"></h3>
<p className="text-muted-foreground">使</p>
</div>
)}
</CardContent>
</Card>
)}
{/* 搜索建议 */}
{!searchQuery && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BrainCircuit className="h-5 w-5 text-purple-600" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="font-medium mb-3"></h4>
<div className="flex flex-wrap gap-2">
{["高价值用户", "技术决策者", "活跃用户", "流失风险", "新用户"].map((term) => (
<Button
key={term}
variant="outline"
size="sm"
onClick={() => setSearchQuery(term)}
className="text-xs"
>
{term}
</Button>
))}
</div>
</div>
<div>
<h4 className="font-medium mb-3"></h4>
<div className="space-y-2">
<div className="text-sm text-muted-foreground"> 搜索特定用户: 输入姓名</div>
<div className="text-sm text-muted-foreground"> 查找标签: 输入标签名称或描述</div>
<div className="text-sm text-muted-foreground"> 发现洞察: 输入行为模式或分析结果</div>
</div>
</div>
</div>
</CardContent>
</Card>
)}
</div>
)
}

View File

@@ -1,408 +1,335 @@
"use client"
import { useState } from "react"
import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { TooltipHelp, TooltipProvider } from "@/components/ui/tooltip-help"
import {
Database,
Users,
Target,
TrendingUp,
RefreshCw,
Download,
ChevronRight,
Activity,
TrendingUp,
Database,
Search,
RefreshCw,
Zap,
BarChart3,
Target,
Globe,
UserCheck,
Layers,
Tag,
Clock,
} from "lucide-react"
import Link from "next/link"
export default function HomePage() {
const [timeRange, setTimeRange] = useState("today")
export default function OverviewPage() {
const [searchQuery, setSearchQuery] = useState("")
const [isRefreshing, setIsRefreshing] = useState(false)
const [lastUpdate, setLastUpdate] = useState(new Date())
// 今日数据概览
const todayStats = {
dataSync: {
total: 1245678,
growth: "+12.5%",
sources: 8,
lastUpdate: "2分钟前",
},
userPool: {
totalUsers: 125678,
activeUsers: 45678,
newUsers: 1234,
growth: "+8.2%",
},
userPortrait: {
totalTags: 342,
activeTags: 287,
coverage: "95.8%",
growth: "+5.3%",
},
userValuation: {
avgValue: 3248,
highValue: 12456,
growth: "+15.8%",
upgradeRate: "12.5%",
},
// 核心数据状态
const [coreData, setCoreData] = useState({
totalUsers: 4000000000, // 40亿用户
activeUsers: 2800000000, // 28亿活跃用户
dataGrowthRate: 15.8, // 数据增长率
assetGrowthRate: 23.5, // 资产增长率
realTimeQueries: 156789, // 实时查询数
dataVolume: "12.8TB", // 数据量
systemHealth: 99.2, // 系统健康度
aiAnalysis: 8456, // AI分析次数
})
// 实时数据更新
useEffect(() => {
const interval = setInterval(() => {
setCoreData((prev) => ({
...prev,
realTimeQueries: prev.realTimeQueries + Math.floor(Math.random() * 50) + 10,
activeUsers: prev.activeUsers + Math.floor(Math.random() * 1000) + 100,
}))
setLastUpdate(new Date())
}, 5000) // 每5秒更新一次
return () => clearInterval(interval)
}, [])
// 手动刷新
const handleRefresh = async () => {
setIsRefreshing(true)
// 模拟数据刷新
setTimeout(() => {
setCoreData((prev) => ({
...prev,
totalUsers: prev.totalUsers + Math.floor(Math.random() * 10000) + 1000,
dataGrowthRate: +(Math.random() * 5 + 12).toFixed(1),
assetGrowthRate: +(Math.random() * 8 + 18).toFixed(1),
}))
setLastUpdate(new Date())
setIsRefreshing(false)
}, 2000)
}
// 实时活动数据
const realtimeActivities = [
{
id: 1,
type: "data_sync",
message: "IMEI数据同步完成",
time: "刚刚",
status: "success",
},
{
id: 2,
type: "user_pool",
message: "新增用户池分群",
time: "2分钟前",
status: "info",
},
{
id: 3,
type: "user_portrait",
message: "标签规则自动执行",
time: "5分钟前",
status: "warning",
},
{
id: 4,
type: "user_valuation",
message: "用户价值模型更新完成",
time: "8分钟前",
status: "success",
},
]
const getActivityIcon = (type: string) => {
switch (type) {
case "data_sync":
return <Database className="h-4 w-4" />
case "user_pool":
return <Users className="h-4 w-4" />
case "user_portrait":
return <Target className="h-4 w-4" />
case "user_valuation":
return <TrendingUp className="h-4 w-4" />
default:
return <Activity className="h-4 w-4" />
// 搜索处理
const handleSearch = () => {
if (searchQuery.trim()) {
// 跳转到智能搜索页面
window.location.href = `/intelligent-search?q=${encodeURIComponent(searchQuery)}`
}
}
const getStatusColor = (status: string) => {
switch (status) {
case "success":
return "text-green-600"
case "warning":
return "text-yellow-600"
case "info":
return "text-blue-600"
default:
return "text-gray-600"
const formatNumber = (num: number) => {
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="container mx-auto py-4 md:py-6 space-y-4 md:space-y-6">
{/* 页面标题和工具栏 */}
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<div>
<h1 className="text-2xl md:text-3xl font-bold tracking-tight"></h1>
<p className="text-sm md:text-base text-muted-foreground"></p>
<TooltipProvider>
<div className="container mx-auto py-6 space-y-6">
{/* 页面标题和搜索 */}
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground mt-1 flex items-center gap-2">
<Badge variant="outline" className="text-xs">
<Clock className="h-3 w-3 mr-1" />
{lastUpdate.toLocaleTimeString()}
</Badge>
</p>
</div>
<div className="flex items-center space-x-3">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder="搜索用户或流量关键词..."
className="pl-10 pr-20"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && handleSearch()}
/>
<Button size="sm" className="absolute right-1 top-1/2 transform -translate-y-1/2" onClick={handleSearch}>
</Button>
</div>
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={isRefreshing}>
<RefreshCw className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
{isRefreshing ? "刷新中" : "刷新"}
</Button>
</div>
</div>
<div className="flex gap-2">
<Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger className="w-[120px] text-sm">
<SelectValue placeholder="时间范围" />
</SelectTrigger>
<SelectContent>
<SelectItem value="today"></SelectItem>
<SelectItem value="week"></SelectItem>
<SelectItem value="month"></SelectItem>
</SelectContent>
</Select>
<Button variant="outline" size="icon" className="h-9 w-9 bg-transparent">
<RefreshCw className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm" className="hidden md:flex bg-transparent">
<Download className="mr-2 h-4 w-4" />
</Button>
{/* 核心用户数据展示 - 40亿用户为中心 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 中心用户总数卡片 */}
<div className="lg:col-span-1">
<Card className="h-full border-2 border-blue-200 bg-gradient-to-br from-blue-50 to-indigo-50">
<CardHeader className="text-center pb-2">
<CardTitle className="text-lg flex items-center justify-center gap-2">
<Globe className="h-6 w-6 text-blue-600" />
<TooltipHelp content="平台累计用户总数,包括所有注册和识别的用户" />
</CardTitle>
</CardHeader>
<CardContent className="text-center">
<div className="text-5xl font-bold text-blue-600 mb-2">{formatNumber(coreData.totalUsers)}</div>
<div className="text-sm text-muted-foreground"></div>
<div className="mt-4 grid grid-cols-2 gap-2 text-xs">
<div className="bg-white/50 rounded p-2">
<div className="font-medium"></div>
<div className="text-green-600 font-bold">{formatNumber(coreData.activeUsers)}</div>
</div>
<div className="bg-white/50 rounded p-2">
<div className="font-medium"></div>
<div className="text-blue-600 font-bold">
{((coreData.activeUsers / coreData.totalUsers) * 100).toFixed(1)}%
</div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* 右侧数据指标 */}
<div className="lg:col-span-2 grid grid-cols-2 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-1">
<TooltipHelp content="用户资产价值的增长率,反映用户价值提升情况" />
</CardTitle>
<TrendingUp className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">+{coreData.assetGrowthRate}%</div>
<p className="text-xs text-muted-foreground"></p>
<div className="mt-2 h-2 bg-gray-200 rounded-full">
<div
className="h-2 bg-green-500 rounded-full transition-all duration-500"
style={{ width: `${Math.min(coreData.assetGrowthRate * 2, 100)}%` }}
></div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-1">
<TooltipHelp content="数据量的增长速度,包括用户行为数据、交易数据等" />
</CardTitle>
<Database className="h-4 w-4 text-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-600">+{coreData.dataGrowthRate}%</div>
<p className="text-xs text-muted-foreground">: {coreData.dataVolume}</p>
<div className="mt-2 h-2 bg-gray-200 rounded-full">
<div
className="h-2 bg-blue-500 rounded-full transition-all duration-500"
style={{ width: `${Math.min(coreData.dataGrowthRate * 3, 100)}%` }}
></div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-1">
<TooltipHelp content="当前实时查询次数,反映系统活跃度" />
</CardTitle>
<Zap className="h-4 w-4 text-yellow-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-yellow-600">{formatNumber(coreData.realTimeQueries)}</div>
<p className="text-xs text-muted-foreground"></p>
<div className="flex items-center mt-2">
<div className="w-2 h-2 bg-yellow-500 rounded-full animate-pulse mr-2"></div>
<span className="text-xs text-green-600"></span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-1">
<TooltipHelp content="系统整体健康状态和AI分析能力" />
</CardTitle>
<Activity className="h-4 w-4 text-purple-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-purple-600">{coreData.systemHealth}%</div>
<p className="text-xs text-muted-foreground">AI分析: {formatNumber(coreData.aiAnalysis)} </p>
<div className="flex items-center mt-2">
<Badge className="bg-green-100 text-green-800 text-xs"></Badge>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
{/* 核心功能模块 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* 数据中台 */}
<Link href="/data-platform">
<Card className="border-none shadow-md hover:shadow-lg transition-all duration-300 cursor-pointer group">
<CardHeader className="bg-gradient-to-r from-blue-50 to-indigo-50 pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Database className="h-5 w-5 text-blue-600" />
<CardTitle className="text-base"></CardTitle>
</div>
<Badge className="bg-blue-100 text-blue-700"></Badge>
</div>
<CardDescription className="text-xs"></CardDescription>
{/* 数据分布概览 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<Target className="h-4 w-4 text-red-500" />
</CardHeader>
<CardContent className="pt-3">
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="font-bold">{todayStats.dataSync.total.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="font-bold">{todayStats.dataSync.sources}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="text-green-600 font-bold">{todayStats.dataSync.growth}</span>
</div>
<div className="flex items-center justify-between mt-3">
<span className="text-xs text-gray-500">: {todayStats.dataSync.lastUpdate}</span>
<ChevronRight className="h-4 w-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
</div>
</div>
<CardContent>
<div className="text-2xl font-bold">2.8M</div>
<p className="text-xs text-muted-foreground"> 7.2%</p>
</CardContent>
</Card>
</Link>
{/* 用户池 */}
<Link href="/user-pool">
<Card className="border-none shadow-md hover:shadow-lg transition-all duration-300 cursor-pointer group">
<CardHeader className="bg-gradient-to-r from-green-50 to-emerald-50 pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Users className="h-5 w-5 text-green-600" />
<CardTitle className="text-base"></CardTitle>
</div>
</div>
<CardDescription className="text-xs"></CardDescription>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<UserCheck className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent className="pt-3">
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="font-bold">{todayStats.userPool.totalUsers.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="font-bold">{todayStats.userPool.activeUsers.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="font-bold">{todayStats.userPool.newUsers.toLocaleString()}</span>
</div>
<div className="flex items-center justify-between mt-3">
<span className="text-xs text-green-600"> {todayStats.userPool.growth}</span>
<ChevronRight className="h-4 w-4 text-gray-400 group-hover:text-green-600 transition-colors" />
</div>
</div>
<CardContent>
<div className="text-2xl font-bold">28.5M</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
</Link>
{/* 用户画像 */}
<Link href="/user-portrait">
<Card className="border-none shadow-md hover:shadow-lg transition-all duration-300 cursor-pointer group">
<CardHeader className="bg-gradient-to-r from-purple-50 to-pink-50 pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Target className="h-5 w-5 text-purple-600" />
<CardTitle className="text-base"></CardTitle>
</div>
</div>
<CardDescription className="text-xs"></CardDescription>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<BarChart3 className="h-4 w-4 text-blue-500" />
</CardHeader>
<CardContent className="pt-3">
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="font-bold">{todayStats.userPortrait.totalTags}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="font-bold">{todayStats.userPortrait.activeTags}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="text-purple-600 font-bold">{todayStats.userPortrait.coverage}</span>
</div>
<div className="flex items-center justify-between mt-3">
<span className="text-xs text-purple-600"> {todayStats.userPortrait.growth}</span>
<ChevronRight className="h-4 w-4 text-gray-400 group-hover:text-purple-600 transition-colors" />
</div>
</div>
<CardContent>
<div className="text-2xl font-bold">156K</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
</Link>
{/* 用户估值 */}
<Link href="/user-valuation">
<Card className="border-none shadow-md hover:shadow-lg transition-all duration-300 cursor-pointer group">
<CardHeader className="bg-gradient-to-r from-orange-50 to-red-50 pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<TrendingUp className="h-5 w-5 text-orange-600" />
<CardTitle className="text-base"></CardTitle>
</div>
</div>
<CardDescription className="text-xs"></CardDescription>
<Card>
<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-purple-500" />
</CardHeader>
<CardContent className="pt-3">
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="font-bold">¥{todayStats.userValuation.avgValue.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="font-bold">{todayStats.userValuation.highValue.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="text-orange-600 font-bold">{todayStats.userValuation.upgradeRate}</span>
</div>
<div className="flex items-center justify-between mt-3">
<span className="text-xs text-orange-600"> {todayStats.userValuation.growth}</span>
<ChevronRight className="h-4 w-4 text-gray-400 group-hover:text-orange-600 transition-colors" />
</div>
</div>
<CardContent>
<div className="text-2xl font-bold">28</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
</Link>
</div>
</div>
{/* 实时数据流 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 md:gap-6">
{/* 今日关键指标 */}
<Card className="lg:col-span-2 border-none shadow-md">
{/* 实时活动流 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5 text-blue-600" />
<Activity className="h-5 w-5" />
<TooltipHelp content="系统实时数据处理和分析活动" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center p-3 bg-blue-50 rounded-lg">
<div className="text-2xl font-bold text-blue-600">98.5%</div>
<div className="text-xs text-gray-600"></div>
</div>
<div className="text-center p-3 bg-green-50 rounded-lg">
<div className="text-2xl font-bold text-green-600">2.3s</div>
<div className="text-xs text-gray-600"></div>
</div>
<div className="text-center p-3 bg-purple-50 rounded-lg">
<div className="text-2xl font-bold text-purple-600">342</div>
<div className="text-xs text-gray-600"></div>
</div>
<div className="text-center p-3 bg-orange-50 rounded-lg">
<div className="text-2xl font-bold text-orange-600">95.8%</div>
<div className="text-xs text-gray-600"></div>
</div>
</div>
<div className="mt-4 space-y-3">
<div>
<div className="flex justify-between text-sm mb-1">
<span></span>
<span>87%</span>
</div>
<Progress value={87} className="h-2" />
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span></span>
<span>92%</span>
</div>
<Progress value={92} className="h-2" />
</div>
</div>
</CardContent>
</Card>
{/* 实时活动 */}
<Card className="border-none shadow-md">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5 text-green-600" />
</CardTitle>
<CardDescription></CardDescription>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{realtimeActivities.map((activity) => (
<div key={activity.id} className="flex items-start gap-3 p-2 hover:bg-gray-50 rounded-lg">
<div className={`p-1 rounded-full ${getStatusColor(activity.status)}`}>
{getActivityIcon(activity.type)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{activity.message}</p>
<p className="text-xs text-gray-500">{activity.time}</p>
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
<div className="flex items-center gap-3">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<div>
<div className="font-medium"></div>
<div className="text-sm text-muted-foreground"> 1,247 </div>
</div>
</div>
))}
<Badge className="bg-green-100 text-green-800"></Badge>
</div>
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<div className="flex items-center gap-3">
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
<div>
<div className="font-medium">AI分析完成</div>
<div className="text-sm text-muted-foreground"></div>
</div>
</div>
<Badge className="bg-blue-100 text-blue-800">2</Badge>
</div>
<div className="flex items-center justify-between p-3 bg-purple-50 rounded-lg">
<div className="flex items-center gap-3">
<div className="w-2 h-2 bg-purple-500 rounded-full animate-pulse"></div>
<div>
<div className="font-medium"></div>
<div className="text-sm text-muted-foreground"> 89 </div>
</div>
</div>
<Badge className="bg-purple-100 text-purple-800">5</Badge>
</div>
</div>
<Button variant="outline" className="w-full mt-4 text-sm bg-transparent">
</Button>
</CardContent>
</Card>
</div>
{/* 快速操作 */}
<Card className="border-none shadow-md">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Zap className="h-5 w-5 text-yellow-600" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Link href="/data-platform">
<Button variant="outline" className="w-full h-16 flex flex-col gap-1 bg-transparent">
<Database className="h-5 w-5" />
<span className="text-xs"></span>
</Button>
</Link>
<Link href="/user-pool/management">
<Button variant="outline" className="w-full h-16 flex flex-col gap-1 bg-transparent">
<UserCheck className="h-5 w-5" />
<span className="text-xs"></span>
</Button>
</Link>
<Link href="/user-pool/segmentation">
<Button variant="outline" className="w-full h-16 flex flex-col gap-1 bg-transparent">
<Layers className="h-5 w-5" />
<span className="text-xs"></span>
</Button>
</Link>
<Link href="/user-portrait/tags">
<Button variant="outline" className="w-full h-16 flex flex-col gap-1 bg-transparent">
<Tag className="h-5 w-5" />
<span className="text-xs"></span>
</Button>
</Link>
</div>
</CardContent>
</Card>
</div>
</TooltipProvider>
)
}

View File

@@ -1,414 +0,0 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Search, Plus, Filter, Download, Tag, Edit, Trash2, MoreHorizontal } from "lucide-react"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
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 { Checkbox } from "@/components/ui/checkbox"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { TagCategoryChart } from "@/components/tag-management/tag-category-chart"
import { TagUsageStats } from "@/components/tag-management/tag-usage-stats"
import { TagRelationshipGraph } from "@/components/tag-management/tag-relationship-graph"
interface TagItem {
id: string
name: string
category: string
type: "system" | "custom" | "derived"
source: string
coverage: number
createdAt: string
updatedAt: string
}
const mockTags: TagItem[] = [
{
id: "1",
name: "高价值用户",
category: "用户价值",
type: "derived",
source: "RFM模型",
coverage: 15.3,
createdAt: "2023-05-12",
updatedAt: "2023-07-15",
},
{
id: "2",
name: "90后",
category: "人口属性",
type: "system",
source: "注册信息",
coverage: 42.7,
createdAt: "2023-01-05",
updatedAt: "2023-01-05",
},
{
id: "3",
name: "游戏爱好者",
category: "兴趣爱好",
type: "custom",
source: "行为分析",
coverage: 28.4,
createdAt: "2023-04-18",
updatedAt: "2023-06-22",
},
{
id: "4",
name: "流失风险高",
category: "流失风险",
type: "derived",
source: "预测模型",
coverage: 8.9,
createdAt: "2023-06-30",
updatedAt: "2023-07-20",
},
{
id: "5",
name: "北京地区",
category: "地理位置",
type: "system",
source: "IP分析",
coverage: 12.5,
createdAt: "2023-02-15",
updatedAt: "2023-02-15",
},
{
id: "6",
name: "周末活跃",
category: "活跃时间",
type: "custom",
source: "行为分析",
coverage: 35.2,
createdAt: "2023-05-28",
updatedAt: "2023-07-10",
},
]
const tagCategories = ["用户价值", "人口属性", "兴趣爱好", "流失风险", "地理位置", "活跃时间", "消费能力", "渠道来源"]
export default function TagManagementPage() {
const [searchQuery, setSearchQuery] = useState("")
const [selectedTags, setSelectedTags] = useState<string[]>([])
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [newTag, setNewTag] = useState({
name: "",
category: "",
type: "custom",
source: "",
})
const handleSelectAllTags = (checked: boolean) => {
if (checked) {
setSelectedTags(mockTags.map((tag) => tag.id))
} else {
setSelectedTags([])
}
}
const handleSelectTag = (tagId: string, checked: boolean) => {
if (checked) {
setSelectedTags([...selectedTags, tagId])
} else {
setSelectedTags(selectedTags.filter((id) => id !== tagId))
}
}
const handleCreateTag = () => {
// 这里应该是创建标签的逻辑
console.log("创建标签:", newTag)
setIsCreateDialogOpen(false)
setNewTag({
name: "",
category: "",
type: "custom",
source: "",
})
}
const filteredTags = mockTags.filter(
(tag) =>
tag.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
tag.category.toLowerCase().includes(searchQuery.toLowerCase()),
)
return (
<div className="container mx-auto p-6 space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold"></h1>
<p className="text-muted-foreground mt-1"></p>
</div>
<div className="flex space-x-2">
<Button variant="outline">
<Download className="mr-2 h-4 w-4" />
</Button>
<Button onClick={() => setIsCreateDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center">
<Tag className="h-5 w-5 text-blue-500 mr-2" />
<div className="text-2xl font-bold">128</div>
</div>
<p className="text-xs text-muted-foreground mt-1"> 12 </p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">87.5%</div>
<p className="text-xs text-muted-foreground mt-1"> 2.3%</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">使</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">76.2%</div>
<p className="text-xs text-muted-foreground mt-1"> 5.1%</p>
</CardContent>
</Card>
</div>
<Tabs defaultValue="list" className="space-y-4">
<div className="flex justify-between items-center">
<TabsList>
<TabsTrigger value="list"></TabsTrigger>
<TabsTrigger value="analytics"></TabsTrigger>
<TabsTrigger value="relationships"></TabsTrigger>
</TabsList>
<div className="flex space-x-2">
<div className="relative w-64">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder="搜索标签..."
className="pl-8"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<Button variant="outline" size="icon">
<Filter className="h-4 w-4" />
</Button>
</div>
</div>
<TabsContent value="list" className="space-y-4">
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">
<Checkbox
checked={selectedTags.length === mockTags.length}
onCheckedChange={handleSelectAllTags}
/>
</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredTags.map((tag) => (
<TableRow key={tag.id}>
<TableCell>
<Checkbox
checked={selectedTags.includes(tag.id)}
onCheckedChange={(checked) => handleSelectTag(tag.id, !!checked)}
/>
</TableCell>
<TableCell className="font-medium">{tag.name}</TableCell>
<TableCell>{tag.category}</TableCell>
<TableCell>
<Badge
variant="outline"
className={
tag.type === "system"
? "bg-blue-50 text-blue-700 border-blue-200"
: tag.type === "derived"
? "bg-purple-50 text-purple-700 border-purple-200"
: "bg-green-50 text-green-700 border-green-200"
}
>
{tag.type === "system" ? "系统" : tag.type === "derived" ? "衍生" : "自定义"}
</Badge>
</TableCell>
<TableCell>{tag.source}</TableCell>
<TableCell>{tag.coverage}%</TableCell>
<TableCell>{tag.createdAt}</TableCell>
<TableCell>{tag.updatedAt}</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>
<Edit className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem>
<Trash2 className="mr-2 h-4 w-4" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="analytics" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<TagCategoryChart />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>使</CardTitle>
<CardDescription>使</CardDescription>
</CardHeader>
<CardContent>
<TagUsageStats />
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="relationships" className="space-y-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="h-[500px]">
<TagRelationshipGraph />
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* 创建标签对话框 */}
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="tag-name" className="text-right">
</Label>
<Input
id="tag-name"
value={newTag.name}
onChange={(e) => setNewTag({ ...newTag, name: e.target.value })}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="tag-category" className="text-right">
</Label>
<Select value={newTag.category} onValueChange={(value) => setNewTag({ ...newTag, category: value })}>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="选择标签分类" />
</SelectTrigger>
<SelectContent>
{tagCategories.map((category) => (
<SelectItem key={category} value={category}>
{category}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="tag-type" className="text-right">
</Label>
<Select
value={newTag.type}
onValueChange={(value: "system" | "custom" | "derived") => setNewTag({ ...newTag, type: value })}
>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="选择标签类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="system"></SelectItem>
<SelectItem value="custom"></SelectItem>
<SelectItem value="derived"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="tag-source" className="text-right">
</Label>
<Input
id="tag-source"
value={newTag.source}
onChange={(e) => setNewTag({ ...newTag, source: e.target.value })}
className="col-span-3"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsCreateDialogOpen(false)}>
</Button>
<Button onClick={handleCreateTag}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -1,398 +0,0 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Search, Plus, Filter, Play, Pause, Edit, Trash2, MoreHorizontal, FileText, Clock } from "lucide-react"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
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 { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Switch } from "@/components/ui/switch"
import { RuleEditor } from "@/components/tag-rules/rule-editor"
import { RuleExecutionHistory } from "@/components/tag-rules/rule-execution-history"
interface TagRule {
id: string
name: string
description: string
targetTag: string
condition: string
status: "active" | "inactive" | "draft"
priority: number
createdAt: string
lastRun: string
affectedUsers: number
}
const mockRules: TagRule[] = [
{
id: "1",
name: "高价值用户识别",
description: "根据用户消费金额和频次识别高价值用户",
targetTag: "高价值用户",
condition: "消费金额 > 5000 AND 消费频次 > 10",
status: "active",
priority: 1,
createdAt: "2023-05-12",
lastRun: "2023-07-21 15:30",
affectedUsers: 1250,
},
{
id: "2",
name: "游戏爱好者标记",
description: "根据用户浏览和购买行为识别游戏爱好者",
targetTag: "游戏爱好者",
condition: "游戏类目浏览时长 > 30min OR 游戏类目购买次数 > 2",
status: "active",
priority: 2,
createdAt: "2023-04-18",
lastRun: "2023-07-21 14:45",
affectedUsers: 2840,
},
{
id: "3",
name: "流失风险预警",
description: "识别30天内可能流失的用户",
targetTag: "流失风险高",
condition: "最近登录时间 > 15天 AND 最近30天活跃度下降率 > 50%",
status: "active",
priority: 1,
createdAt: "2023-06-30",
lastRun: "2023-07-21 13:20",
affectedUsers: 890,
},
{
id: "4",
name: "周末活跃用户",
description: "识别周末活跃度高的用户",
targetTag: "周末活跃",
condition: "周末活跃时长 > 工作日活跃时长 * 1.5",
status: "inactive",
priority: 3,
createdAt: "2023-05-28",
lastRun: "2023-07-10 09:15",
affectedUsers: 3520,
},
{
id: "5",
name: "潜在高转化用户",
description: "识别浏览量高但未转化的潜在用户",
targetTag: "潜在高转化",
condition: "浏览商品数 > 20 AND 加购次数 > 5 AND 购买次数 = 0",
status: "draft",
priority: 2,
createdAt: "2023-07-15",
lastRun: "-",
affectedUsers: 0,
},
]
export default function TagRulesPage() {
const [searchQuery, setSearchQuery] = useState("")
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [newRule, setNewRule] = useState({
name: "",
description: "",
targetTag: "",
condition: "",
priority: 2,
status: "draft",
})
const [selectedRuleId, setSelectedRuleId] = useState<string | null>(null)
const handleCreateRule = () => {
// 这里应该是创建规则的逻辑
console.log("创建规则:", newRule)
setIsCreateDialogOpen(false)
setNewRule({
name: "",
description: "",
targetTag: "",
condition: "",
priority: 2,
status: "draft",
})
}
const filteredRules = mockRules.filter(
(rule) =>
rule.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
rule.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
rule.targetTag.toLowerCase().includes(searchQuery.toLowerCase()),
)
const getStatusBadge = (status: TagRule["status"]) => {
switch (status) {
case "active":
return <Badge className="bg-green-100 text-green-800"></Badge>
case "inactive":
return <Badge className="bg-yellow-100 text-yellow-800"></Badge>
case "draft":
return <Badge className="bg-gray-100 text-gray-800">稿</Badge>
}
}
return (
<div className="container mx-auto p-6 space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold"></h1>
<p className="text-muted-foreground mt-1"></p>
</div>
<div className="flex space-x-2">
<Button variant="outline">
<Clock className="mr-2 h-4 w-4" />
</Button>
<Button onClick={() => setIsCreateDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">24</div>
<p className="text-xs text-muted-foreground mt-1"> 3 </p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">18</div>
<p className="text-xs text-muted-foreground mt-1"> 75%</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">36</div>
<p className="text-xs text-muted-foreground mt-1"> 12,580 </p>
</CardContent>
</Card>
</div>
<Tabs defaultValue="rules" className="space-y-4">
<div className="flex justify-between items-center">
<TabsList>
<TabsTrigger value="rules"></TabsTrigger>
<TabsTrigger value="editor"></TabsTrigger>
<TabsTrigger value="history"></TabsTrigger>
</TabsList>
<div className="flex space-x-2">
<div className="relative w-64">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder="搜索规则..."
className="pl-8"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<Button variant="outline" size="icon">
<Filter className="h-4 w-4" />
</Button>
</div>
</div>
<TabsContent value="rules" className="space-y-4">
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredRules.map((rule) => (
<TableRow key={rule.id}>
<TableCell className="font-medium">
<div>{rule.name}</div>
<div className="text-xs text-muted-foreground">{rule.description}</div>
</TableCell>
<TableCell>{rule.targetTag}</TableCell>
<TableCell>{getStatusBadge(rule.status)}</TableCell>
<TableCell>{rule.priority}</TableCell>
<TableCell>{rule.createdAt}</TableCell>
<TableCell>{rule.lastRun}</TableCell>
<TableCell>{rule.affectedUsers.toLocaleString()}</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setSelectedRuleId(rule.id)}>
<Edit className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem>
{rule.status === "active" ? (
<>
<Pause className="mr-2 h-4 w-4" />
</>
) : (
<>
<Play className="mr-2 h-4 w-4" />
</>
)}
</DropdownMenuItem>
<DropdownMenuItem>
<FileText className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem>
<Trash2 className="mr-2 h-4 w-4" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="editor" className="space-y-4">
<RuleEditor ruleId={selectedRuleId} />
</TabsContent>
<TabsContent value="history" className="space-y-4">
<RuleExecutionHistory />
</TabsContent>
</Tabs>
{/* 创建规则对话框 */}
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="rule-name" className="text-right">
</Label>
<Input
id="rule-name"
value={newRule.name}
onChange={(e) => setNewRule({ ...newRule, name: e.target.value })}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="rule-description" className="text-right">
</Label>
<Textarea
id="rule-description"
value={newRule.description}
onChange={(e) => setNewRule({ ...newRule, description: e.target.value })}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="target-tag" className="text-right">
</Label>
<Input
id="target-tag"
value={newRule.targetTag}
onChange={(e) => setNewRule({ ...newRule, targetTag: e.target.value })}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="rule-condition" className="text-right">
</Label>
<Textarea
id="rule-condition"
value={newRule.condition}
onChange={(e) => setNewRule({ ...newRule, condition: e.target.value })}
className="col-span-3"
placeholder="例如: 消费金额 > 5000 AND 消费频次 > 10"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="rule-priority" className="text-right">
</Label>
<Select
value={newRule.priority.toString()}
onValueChange={(value) => setNewRule({ ...newRule, priority: Number.parseInt(value) })}
>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="选择优先级" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1 - </SelectItem>
<SelectItem value="2">2 - </SelectItem>
<SelectItem value="3">3 - </SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="rule-status" className="text-right">
</Label>
<div className="flex items-center space-x-2 col-span-3">
<Switch
id="rule-status"
checked={newRule.status === "active"}
onCheckedChange={(checked) => setNewRule({ ...newRule, status: checked ? "active" : "draft" })}
/>
<Label htmlFor="rule-status">{newRule.status === "active" ? "立即启用" : "保存为草稿"}</Label>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsCreateDialogOpen(false)}>
</Button>
<Button onClick={handleCreateRule}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

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

View File

@@ -1,134 +0,0 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Search, Plus, Filter, Calendar, CheckCircle2, XCircle, PlayCircle } from "lucide-react"
import { TaskList } from "@/components/tag-tasks/task-list"
export default function TagTasksPage() {
const [activeTab, setActiveTab] = useState("all")
const [searchQuery, setSearchQuery] = useState("")
return (
<div className="container mx-auto py-6 space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold"></h1>
<div className="flex space-x-2">
<Button variant="outline">
<Calendar className="mr-2 h-4 w-4" />
</Button>
<Button>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">128</div>
<p className="text-xs text-muted-foreground mt-1"> 24 </p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center">
<PlayCircle className="h-5 w-5 text-blue-500 mr-2" />
<div className="text-2xl font-bold">12</div>
</div>
<p className="text-xs text-muted-foreground mt-1"> 2</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center">
<CheckCircle2 className="h-5 w-5 text-green-500 mr-2" />
<div className="text-2xl font-bold">98</div>
</div>
<p className="text-xs text-muted-foreground mt-1"> 96.2%</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center">
<XCircle className="h-5 w-5 text-red-500 mr-2" />
<div className="text-2xl font-bold">18</div>
</div>
<p className="text-xs text-muted-foreground mt-1"> 3.8%</p>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
<div className="flex justify-between items-center">
<TabsList>
<TabsTrigger value="all"></TabsTrigger>
<TabsTrigger value="running"></TabsTrigger>
<TabsTrigger value="completed"></TabsTrigger>
<TabsTrigger value="failed"></TabsTrigger>
<TabsTrigger value="scheduled"></TabsTrigger>
</TabsList>
<div className="flex space-x-2">
<div className="relative w-64">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="搜索任务..."
className="pl-8"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<Button variant="outline" size="icon">
<Filter className="h-4 w-4" />
</Button>
</div>
</div>
<TabsContent value="all" className="space-y-4">
<TaskList status="all" searchQuery={searchQuery} />
</TabsContent>
<TabsContent value="running" className="space-y-4">
<TaskList status="running" searchQuery={searchQuery} />
</TabsContent>
<TabsContent value="completed" className="space-y-4">
<TaskList status="completed" searchQuery={searchQuery} />
</TabsContent>
<TabsContent value="failed" className="space-y-4">
<TaskList status="failed" searchQuery={searchQuery} />
</TabsContent>
<TabsContent value="scheduled" className="space-y-4">
<TaskList status="scheduled" searchQuery={searchQuery} />
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
)
}

File diff suppressed because it is too large Load Diff