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" "use client"
import { useEffect, useState } from "react" import { X } from "lucide-react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { import Link from "next/link"
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"
interface MobileSidebarProps { interface MobileSidebarProps {
isOpen: boolean isOpen: boolean
@@ -25,219 +10,43 @@ interface MobileSidebarProps {
} }
export default function MobileSidebar({ isOpen, onClose }: 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 ( return (
<>
{/* 背景遮罩 */}
<div <div
className={cn( className={cn(
"fixed inset-0 bg-black/20 backdrop-blur-sm z-40 transition-opacity duration-300", "fixed inset-0 z-40 transition-transform duration-300 md:hidden",
isOpen ? "opacity-100" : "opacity-0 pointer-events-none", isOpen ? "translate-x-0" : "translate-x-full",
)} )}
onClick={onClose} aria-hidden={!isOpen}
/> >
{/* Backdrop */}
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={onClose} />
{/* 侧边栏 */} {/* Drawer */}
<div <aside className="relative ml-auto h-full w-64 bg-white shadow-lg">
className={cn( {/* Header */}
"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", <div className="flex items-center justify-between p-4 border-b">
isOpen ? "translate-x-0" : "-translate-x-full", <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" />
{/* 头部 */}
<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>
</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> </button>
)}
</div> </div>
{/* 子菜单 */} {/* Navigation links — keep in sync with Sidebar.tsx */}
{item.children && expandedSections[item.section!] && ( <nav className="flex flex-col gap-1 p-4">
<div className="ml-4 mt-1 space-y-1"> <Link href="/" className="rounded px-3 py-2 text-sm hover:bg-gray-100" onClick={onClose}>
{item.children.map((subItem) => (
<Link </Link>
key={subItem.href} <Link href="/data-platform" className="rounded px-3 py-2 text-sm hover:bg-gray-100" onClick={onClose}>
href={subItem.href}
onClick={onClose} </Link>
className={cn( <Link href="/user-portrait" className="rounded px-3 py-2 text-sm hover:bg-gray-100" onClick={onClose}>
"flex items-center px-2 py-1.5 text-xs rounded transition-all duration-300",
pathname === subItem.href </Link>
? "glass-light text-blue-600 shadow-glass-sm" <Link href="/ai-assistant" className="rounded px-3 py-2 text-sm hover:bg-gray-100" onClick={onClose}>
: "text-gray-600 hover:glass-light hover:text-blue-500", AI
)}
>
<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> </Link>
))}
</div>
)}
</div>
))}
</nav> </nav>
</aside>
</div> </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>
</>
) )
} }

View File

@@ -4,124 +4,63 @@ import { useState } from "react"
import Link from "next/link" import Link from "next/link"
import { usePathname } from "next/navigation" import { usePathname } from "next/navigation"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { import { LayoutDashboard, Database, Users, BrainCircuit, Settings, ChevronLeft } from "lucide-react"
Database,
LayoutDashboard,
Settings,
Target,
ChevronLeft,
ChevronDown,
ChevronRight,
Tag,
UserCheck,
Layers,
BrainCircuit,
Smartphone,
} from "lucide-react"
import { Badge } from "@/components/ui/badge"
export default function Sidebar() { export default function Sidebar() {
const pathname = usePathname() const pathname = usePathname()
const [expanded, setExpanded] = useState(true) const [expanded, setExpanded] = useState(true)
const [expandedSections, setExpandedSections] = useState({
"user-portrait": true, // 用户画像默认展开
})
const toggleSidebar = () => { const toggleSidebar = () => {
setExpanded(!expanded) setExpanded(!expanded)
} }
const toggleSection = (section: string) => { // 简化的导航结构
setExpandedSections((prev) => ({
...prev,
[section]: !prev[section],
}))
}
// 重新设计的导航结构 - 遵循新规则
const navItems = [ const navItems = [
{ {
title: "数据概览", title: "数据概览",
href: "/", href: "/",
icon: <LayoutDashboard className="h-5 w-5" />, icon: <LayoutDashboard className="h-5 w-5" />,
primary: true, description: "平台整体数据分析与监控",
}, },
{ {
title: "数据中台", title: "数据中台",
href: "/data-platform", href: "/data-platform",
icon: <Database className="h-5 w-5" />, icon: <Database className="h-5 w-5" />,
primary: true, description: "多源数据整合与处理中心",
tag: "核心",
}, },
{ {
title: "用户画像", // 整合用户池功能 title: "用户画像",
href: "/user-portrait", href: "/user-portrait",
icon: <Target className="h-5 w-5" />, icon: <Users className="h-5 w-5" />,
primary: true, description: "用户数据管理与画像分析",
expandable: true,
section: "user-portrait",
children: [
{
title: "用户管理", // 原用户池的用户管理
href: "/user-portrait/management",
icon: <UserCheck className="h-4 w-4" />,
}, },
{ {
title: "用户分群", // 原用户池的用户分群 title: "AI智能助手",
href: "/user-portrait/segmentation",
icon: <Layers className="h-4 w-4" />,
},
{
title: "标签管理", // 原用户画像的标签管理
href: "/user-portrait/tags",
icon: <Tag className="h-4 w-4" />,
},
],
},
{
title: "设备管理", // 提升为一级菜单 [^2]
href: "/devices",
icon: <Smartphone className="h-5 w-5" />,
primary: true,
},
{
title: "AI智能助手", // 新增AI智能助手
href: "/ai-assistant", href: "/ai-assistant",
icon: <BrainCircuit className="h-5 w-5" />, 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 ( return (
<div <div
className={cn( 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", 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 ? ( {expanded ? (
<div className="flex items-center space-x-3"> <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" /> <Database className="h-5 w-5 text-blue-600" />
</div> </div>
<h1 className="text-lg font-semibold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"> <h1 className="text-lg font-semibold text-gray-900"></h1>
</h1>
</div> </div>
) : ( ) : (
<div className="mx-auto"> <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" /> <Database className="h-5 w-5 text-blue-600" />
</div> </div>
</div> </div>
@@ -132,86 +71,49 @@ export default function Sidebar() {
<div className="flex-1 overflow-y-auto py-4"> <div className="flex-1 overflow-y-auto py-4">
<nav className="space-y-2 px-4"> <nav className="space-y-2 px-4">
{navItems.map((item) => ( {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 <Link
key={subItem.href} key={item.href}
href={subItem.href} href={item.href}
className={cn( className={cn(
"flex items-center px-3 py-2 text-sm rounded-lg transition-all duration-300", "flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200 group",
pathname === subItem.href pathname === item.href
? "glass-light text-blue-600 shadow-glass-sm" ? "bg-blue-50 text-blue-700 border border-blue-200"
: "text-gray-600 hover:glass-light hover:text-blue-500", : "text-gray-700 hover:bg-gray-50 hover:text-blue-600",
)} )}
> >
<div className="mr-3 text-gray-400 transition-colors duration-300">{subItem.icon}</div> <div className="transition-colors duration-200">{item.icon}</div>
<span>{subItem.title}</span> {expanded && (
</Link> <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>
)} )}
</div> </Link>
))} ))}
</nav> </nav>
</div> </div>
{/* 底部设置和折叠按钮 */} {/* 底部设置和折叠按钮 */}
<div className="mt-auto border-t border-white/20"> <div className="mt-auto border-t border-gray-200">
<Link <Link
href="/settings" href="/settings"
className={cn( 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" pathname === "/settings"
? "glass-heavy text-blue-700 shadow-glass" ? "bg-blue-50 text-blue-700 border border-blue-200"
: "glass-light text-gray-600 hover:glass-heavy hover:text-blue-500", : "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" /> <Settings className="h-4 w-4" />
</div> </div>
{expanded && <span className="ml-2"></span>} {expanded && <span className="ml-2"></span>}
</Link> </Link>
<div className="p-4"> <div className="p-4">
<button <button
onClick={toggleSidebar} 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 <ChevronLeft
className={cn( 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,240 +3,399 @@
import { useState } from "react" import { useState } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button" 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 { 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() { export default function DataIntegrationPage() {
const [activeTab, setActiveTab] = useState("data-sources") const [activeTab, setActiveTab] = useState("overview")
const [isDialogOpen, setIsDialogOpen] = useState(false)
return ( const integrationStats = {
<div className="container mx-auto py-6 space-y-8"> totalSources: 12,
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4"> activeSources: 10,
<div> dailyVolume: 2456789,
<h1 className="text-3xl font-bold tracking-tight"></h1> successRate: 98.5,
<p className="text-muted-foreground">API接口</p> avgLatency: 156,
</div> qualityScore: 94.2,
</div> }
<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 = [ const dataSources = [
{ {
id: "1", id: 1,
name: "用户数据库", name: "MySQL主库",
type: "MySQL", type: "database",
host: "db.example.com",
status: "connected", status: "connected",
lastSync: "2023-07-20 15:30", lastSync: "2分钟前",
tables: 24, records: 1245678,
records: 156789, quality: 98.5,
}, },
{ {
id: "2", id: 2,
name: "订单系统", name: "触客宝API",
type: "PostgreSQL", type: "api",
host: "orders.example.com",
status: "connected", status: "connected",
lastSync: "2023-07-19 12:45", lastSync: "5分钟前",
tables: 18, records: 456789,
records: 89456, quality: 96.2,
}, },
{ {
id: "3", id: 3,
name: "内容库", name: "抖音内容平台",
type: "MongoDB", type: "api",
host: "content.example.com", 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", status: "error",
lastSync: "2023-07-15 09:20", lastSync: "2小时前",
tables: 12,
records: 45678, records: 45678,
}, quality: 78.9,
{
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,
}, },
] ]
const recentActivities = [
{
id: 1,
type: "sync_complete",
message: "MySQL主库数据同步完成",
time: "2分钟前",
status: "success",
},
{
id: 2,
type: "quality_check",
message: "触客宝API数据质量检查通过",
time: "8分钟前",
status: "success",
},
{
id: 3,
type: "sync_warning",
message: "抖音内容平台同步延迟",
time: "1小时前",
status: "warning",
},
{
id: 4,
type: "sync_error",
message: "飞书妙记API连接失败",
time: "2小时前",
status: "error",
},
]
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 ( return (
<div className="space-y-6"> <div className="container mx-auto py-6 space-y-6">
<div className="flex justify-between items-center"> {/* 页面标题 */}
<h2 className="text-2xl font-bold"></h2> <div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<Button onClick={() => setIsDialogOpen(true)}> <div>
<h1 className="text-3xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
<div className="flex gap-2">
<Button>
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
</Button> </Button>
<Button variant="outline">
<Settings className="mr-2 h-4 w-4" />
</Button>
</div>
</div> </div>
<Card> {/* 核心指标 */}
<CardHeader> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<CardTitle></CardTitle> <Card className="bg-gradient-to-r from-blue-50 to-cyan-50 border-none shadow-md">
<CardDescription></CardDescription> <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> </CardHeader>
<CardContent> <CardContent>
<Table> <div className="text-2xl font-bold text-blue-600">{integrationStats.totalSources}</div>
<TableHeader> <div className="text-xs text-muted-foreground mt-1">: {integrationStats.activeSources}</div>
<TableRow> </CardContent>
<TableHead></TableHead> </Card>
<TableHead></TableHead>
<TableHead></TableHead> <Card className="bg-gradient-to-r from-green-50 to-emerald-50 border-none shadow-md">
<TableHead></TableHead> <CardHeader className="pb-3">
<TableHead></TableHead> <div className="flex items-center justify-between">
<TableHead></TableHead> <CardTitle className="text-sm font-medium"></CardTitle>
<TableHead></TableHead> <Activity className="h-4 w-4 text-green-600" />
<TableHead className="text-right"></TableHead> </div>
</TableRow> </CardHeader>
</TableHeader> <CardContent>
<TableBody> <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) => ( {dataSources.map((source) => (
<TableRow key={source.id}> <div key={source.id} className="flex items-center justify-between p-4 border rounded-lg">
<TableCell> <div className="flex items-center gap-3">
<div className="flex items-center"> {getStatusIcon(source.status)}
<Database className="h-4 w-4 mr-2 text-muted-foreground" /> <div>
<span className="font-medium">{source.name}</span> <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>
</TableCell> </div>
<TableCell>{source.type}</TableCell> <div className="text-right">
<TableCell>{source.host}</TableCell> <Badge className={getStatusColor(source.status)}>
<TableCell> {source.status === "connected" ? "已连接" : source.status === "warning" ? "警告" : "错误"}
<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> </Badge>
</TableCell> <div className="flex items-center gap-4 mt-1 text-xs text-muted-foreground">
<TableCell>{source.lastSync}</TableCell> <span>{source.records.toLocaleString()} </span>
<TableCell>{source.tables}</TableCell> <span>: {source.quality}%</span>
<TableCell>{source.records.toLocaleString()}</TableCell> </div>
<TableCell className="text-right"> </div>
<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> </div>
</TableCell>
</TableRow>
))} ))}
</TableBody> </div>
</Table> <div className="flex justify-between mt-4">
<Link href="/data-integration/sources">
<Button variant="outline"></Button>
</Link>
<Button></Button>
</div>
</CardContent> </CardContent>
</Card> </Card>
{/* 实时活动 */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle></CardTitle> <CardTitle className="flex items-center gap-2">
<CardDescription></CardDescription> <Activity className="h-5 w-5 text-green-600" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="space-y-3">
<div className="flex flex-col items-center p-4 border rounded-lg"> {recentActivities.map((activity) => (
<Database className="h-8 w-8 mb-2 text-blue-500" /> <div key={activity.id} className="flex items-start gap-3 p-2 hover:bg-gray-50 rounded-lg">
<span className="font-medium">MySQL</span> {getActivityIcon(activity.type)}
<span className="text-xs text-muted-foreground"></span> <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 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>
<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>
<div className="flex flex-col items-center p-4 border rounded-lg"> <Button variant="outline" className="w-full mt-4 bg-transparent">
<Database className="h-8 w-8 mb-2 text-yellow-500" />
<span className="font-medium">ClickHouse</span> </Button>
<span className="text-xs text-muted-foreground"></span> </CardContent>
</Card>
</div> </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> <Card>
<span className="text-xs text-muted-foreground"></span> <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> </div>
<div className="flex flex-col items-center p-4 border rounded-lg"> </Link>
<Database className="h-8 w-8 mb-2 text-purple-500" />
<span className="font-medium">SQL Server</span> <Link href="/data-integration/ingestion">
<span className="text-xs text-muted-foreground"></span> <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> </div>
<div className="flex flex-col items-center p-4 border rounded-lg"> </Link>
<Server className="h-8 w-8 mb-2 text-gray-500" />
<span className="font-medium">Redis</span> <Link href="/data-integration/processing">
<span className="text-xs text-muted-foreground"></span> <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> </div>
<div className="flex flex-col items-center p-4 border rounded-lg"> </Link>
<FileText className="h-8 w-8 mb-2 text-gray-500" />
<span className="font-medium">CSV/Excel</span> <Link href="/data-integration/quality">
<span className="text-xs text-muted-foreground"></span> <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>
</div> </div>
</CardContent> </CardContent>
@@ -244,311 +403,3 @@ function DataSourcesTab({ setIsDialogOpen }: { setIsDialogOpen: (open: boolean)
</div> </div>
) )
} }
// API管理标签页
function ApiManagementTab() {
// 模拟API接口数据
const apiEndpoints = [
{
id: "1",
name: "用户数据API",
endpoint: "/api/users",
method: "GET",
category: "用户画像",
status: "active",
calls: 12567,
lastCalled: "2023-07-20 16:45",
},
{
id: "2",
name: "用户标签API",
endpoint: "/api/users/tags",
method: "GET",
category: "用户画像",
status: "active",
calls: 8945,
lastCalled: "2023-07-20 15:30",
},
{
id: "3",
name: "流量池数据API",
endpoint: "/api/traffic-pools",
method: "GET",
category: "流量池",
status: "active",
calls: 5678,
lastCalled: "2023-07-20 14:20",
},
{
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",
},
]
// 模拟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",
},
]
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>
<DialogFooter>
<Button variant="outline" onClick={() => setIsOpen(false)}>
</Button>
<Button type="submit"></Button>
<Button type="submit"></Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

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

View File

@@ -1,41 +0,0 @@
"use client"
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts"
const data = [
{ name: "用户价值", value: 25 },
{ name: "人口属性", value: 18 },
{ name: "兴趣爱好", value: 22 },
{ name: "流失风险", value: 12 },
{ name: "地理位置", value: 15 },
{ name: "活跃时间", value: 10 },
{ name: "消费能力", value: 14 },
{ name: "渠道来源", value: 12 },
]
const COLORS = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042", "#8884d8", "#82ca9d", "#ffc658", "#8dd1e1"]
export function TagCategoryChart() {
return (
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
labelLine={false}
outerRadius={80}
fill="#8884d8"
dataKey="value"
label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`}
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
)
}

View File

@@ -1,149 +0,0 @@
"use client"
import { useEffect, useRef } from "react"
import * as d3 from "d3"
interface Node {
id: string
name: string
category: string
value: number
}
interface Link {
source: string
target: string
value: number
}
const mockData = {
nodes: [
{ id: "1", name: "高价值用户", category: "用户价值", value: 20 },
{ id: "2", name: "90后", category: "人口属性", value: 15 },
{ id: "3", name: "游戏爱好者", category: "兴趣爱好", value: 18 },
{ id: "4", name: "流失风险高", category: "流失风险", value: 12 },
{ id: "5", name: "北京地区", category: "地理位置", value: 10 },
{ id: "6", name: "周末活跃", category: "活跃时间", value: 14 },
{ id: "7", name: "高消费", category: "消费能力", value: 16 },
{ id: "8", name: "App渠道", category: "渠道来源", value: 8 },
],
links: [
{ source: "1", target: "7", value: 5 },
{ source: "2", target: "3", value: 8 },
{ source: "2", target: "6", value: 3 },
{ source: "3", target: "7", value: 6 },
{ source: "4", target: "6", value: 4 },
{ source: "5", target: "8", value: 2 },
{ source: "6", target: "3", value: 7 },
{ source: "7", target: "8", value: 3 },
{ source: "1", target: "3", value: 5 },
{ source: "2", target: "5", value: 4 },
{ source: "4", target: "1", value: 6 },
],
}
const categoryColors = {
: "#0088FE",
: "#00C49F",
: "#FFBB28",
: "#FF8042",
: "#8884d8",
: "#82ca9d",
: "#ffc658",
: "#8dd1e1",
}
export function TagRelationshipGraph() {
const svgRef = useRef<SVGSVGElement>(null)
useEffect(() => {
if (!svgRef.current) return
const svg = d3.select(svgRef.current)
svg.selectAll("*").remove()
const width = svgRef.current.clientWidth
const height = svgRef.current.clientHeight
// 创建力导向图
const simulation = d3
.forceSimulation(mockData.nodes as d3.SimulationNodeDatum[])
.force(
"link",
d3
.forceLink(mockData.links)
.id((d: any) => d.id)
.distance(100),
)
.force("charge", d3.forceManyBody().strength(-200))
.force("center", d3.forceCenter(width / 2, height / 2))
// 绘制连线
const link = svg
.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(mockData.links)
.join("line")
.attr("stroke-width", (d) => Math.sqrt(d.value))
// 绘制节点
const node = svg
.append("g")
.selectAll("circle")
.data(mockData.nodes)
.join("circle")
.attr("r", (d) => d.value * 0.8)
.attr("fill", (d) => categoryColors[d.category as keyof typeof categoryColors])
.call(d3.drag<SVGCircleElement, Node>().on("start", dragstarted).on("drag", dragged).on("end", dragended) as any)
// 添加标签
const text = svg
.append("g")
.selectAll("text")
.data(mockData.nodes)
.join("text")
.text((d) => d.name)
.attr("font-size", 10)
.attr("dx", 15)
.attr("dy", 4)
// 更新位置
simulation.on("tick", () => {
link
.attr("x1", (d: any) => d.source.x)
.attr("y1", (d: any) => d.source.y)
.attr("x2", (d: any) => d.target.x)
.attr("y2", (d: any) => d.target.y)
node.attr("cx", (d: any) => d.x).attr("cy", (d: any) => d.y)
text.attr("x", (d: any) => d.x).attr("y", (d: any) => d.y)
})
// 拖拽函数
function dragstarted(event: any, d: any) {
if (!event.active) simulation.alphaTarget(0.3).restart()
d.fx = d.x
d.fy = d.y
}
function dragged(event: any, d: any) {
d.fx = event.x
d.fy = event.y
}
function dragended(event: any, d: any) {
if (!event.active) simulation.alphaTarget(0)
d.fx = null
d.fy = null
}
return () => {
simulation.stop()
}
}, [])
return <svg ref={svgRef} width="100%" height="100%" />
}

View File

@@ -1,49 +0,0 @@
"use client"
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"
const data = [
{
name: "营销系统",
使用标签数: 85,
},
{
name: "推荐系统",
使用标签数: 72,
},
{
name: "客服系统",
使用标签数: 45,
},
{
name: "风控系统",
使用标签数: 38,
},
{
name: "内容系统",
使用标签数: 65,
},
]
export function TagUsageStats() {
return (
<ResponsiveContainer width="100%" height={300}>
<BarChart
data={data}
margin={{
top: 5,
right: 30,
left: 20,
bottom: 5,
}}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="使用标签数" fill="#8884d8" />
</BarChart>
</ResponsiveContainer>
)
}

View File

@@ -1,286 +0,0 @@
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Trash2, Plus, Save } from "lucide-react"
interface RuleCondition {
field: string
operator: string
value: string
}
interface TagRule {
id: string
name: string
description: string
conditions: RuleCondition[]
actions: {
addTags: string[]
removeTags: string[]
}
enabled: boolean
}
interface RuleEditorProps {
rule?: TagRule
onSave: (rule: TagRule) => void
onCancel: () => void
}
export function RuleEditor({ rule, onSave, onCancel }: RuleEditorProps) {
const [formData, setFormData] = useState<TagRule>(
rule || {
id: "",
name: "",
description: "",
conditions: [{ field: "", operator: "", value: "" }],
actions: {
addTags: [],
removeTags: [],
},
enabled: true,
},
)
const [newAddTag, setNewAddTag] = useState("")
const [newRemoveTag, setNewRemoveTag] = useState("")
const fieldOptions = [
{ value: "imei", label: "IMEI" },
{ value: "phone", label: "手机号" },
{ value: "device_brand", label: "设备品牌" },
{ value: "device_model", label: "设备型号" },
{ value: "os_version", label: "系统版本" },
{ value: "app_version", label: "应用版本" },
{ value: "location", label: "地理位置" },
{ value: "user_behavior", label: "用户行为" },
{ value: "consumption_amount", label: "消费金额" },
{ value: "activity_frequency", label: "活跃频率" },
]
const operatorOptions = [
{ value: "equals", label: "等于" },
{ value: "not_equals", label: "不等于" },
{ value: "contains", label: "包含" },
{ value: "not_contains", label: "不包含" },
{ value: "starts_with", label: "开始于" },
{ value: "ends_with", label: "结束于" },
{ value: "greater_than", label: "大于" },
{ value: "less_than", label: "小于" },
{ value: "in_range", label: "在范围内" },
{ value: "regex", label: "正则匹配" },
]
const addCondition = () => {
setFormData({
...formData,
conditions: [...formData.conditions, { field: "", operator: "", value: "" }],
})
}
const removeCondition = (index: number) => {
setFormData({
...formData,
conditions: formData.conditions.filter((_, i) => i !== index),
})
}
const updateCondition = (index: number, field: keyof RuleCondition, value: string) => {
const newConditions = [...formData.conditions]
newConditions[index] = { ...newConditions[index], [field]: value }
setFormData({ ...formData, conditions: newConditions })
}
const addTag = (type: "addTags" | "removeTags") => {
const tagValue = type === "addTags" ? newAddTag : newRemoveTag
if (tagValue.trim()) {
setFormData({
...formData,
actions: {
...formData.actions,
[type]: [...formData.actions[type], tagValue.trim()],
},
})
if (type === "addTags") {
setNewAddTag("")
} else {
setNewRemoveTag("")
}
}
}
const removeTag = (type: "addTags" | "removeTags", index: number) => {
setFormData({
...formData,
actions: {
...formData.actions,
[type]: formData.actions[type].filter((_, i) => i !== index),
},
})
}
const handleSave = () => {
if (formData.name && formData.conditions.length > 0) {
onSave(formData)
}
}
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="rule-name"></Label>
<Input
id="rule-name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="输入规则名称"
/>
</div>
<div>
<Label htmlFor="rule-description"></Label>
<Input
id="rule-description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="输入规则描述"
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{formData.conditions.map((condition, index) => (
<div key={index} className="flex items-center space-x-2 p-3 border rounded-lg">
<Select value={condition.field} onValueChange={(value) => updateCondition(index, "field", value)}>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="选择字段" />
</SelectTrigger>
<SelectContent>
{fieldOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={condition.operator} onValueChange={(value) => updateCondition(index, "operator", value)}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="选择操作符" />
</SelectTrigger>
<SelectContent>
{operatorOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Input
value={condition.value}
onChange={(e) => updateCondition(index, "value", e.target.value)}
placeholder="输入值"
className="flex-1"
/>
<Button
variant="outline"
size="icon"
onClick={() => removeCondition(index)}
disabled={formData.conditions.length === 1}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
<Button variant="outline" onClick={addCondition}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div>
<Label></Label>
<div className="flex space-x-2 mt-2">
<Input
value={newAddTag}
onChange={(e) => setNewAddTag(e.target.value)}
placeholder="输入要添加的标签"
onKeyPress={(e) => e.key === "Enter" && addTag("addTags")}
/>
<Button onClick={() => addTag("addTags")}></Button>
</div>
<div className="flex flex-wrap gap-2 mt-2">
{formData.actions.addTags.map((tag, index) => (
<Badge key={index} variant="secondary" className="flex items-center gap-1">
{tag}
<button onClick={() => removeTag("addTags", index)}>
<Trash2 className="h-3 w-3" />
</button>
</Badge>
))}
</div>
</div>
<div>
<Label></Label>
<div className="flex space-x-2 mt-2">
<Input
value={newRemoveTag}
onChange={(e) => setNewRemoveTag(e.target.value)}
placeholder="输入要移除的标签"
onKeyPress={(e) => e.key === "Enter" && addTag("removeTags")}
/>
<Button onClick={() => addTag("removeTags")}></Button>
</div>
<div className="flex flex-wrap gap-2 mt-2">
{formData.actions.removeTags.map((tag, index) => (
<Badge key={index} variant="destructive" className="flex items-center gap-1">
{tag}
<button onClick={() => removeTag("removeTags", index)}>
<Trash2 className="h-3 w-3" />
</button>
</Badge>
))}
</div>
</div>
</CardContent>
</Card>
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={onCancel}>
</Button>
<Button onClick={handleSave}>
<Save className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
)
}

View File

@@ -1,258 +0,0 @@
"use client"
import { useState } from "react"
import { Card, CardContent } from "@/components/ui/card"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Search, Filter, Calendar, FileText } from "lucide-react"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
interface ExecutionRecord {
id: string
ruleName: string
executionTime: string
duration: string
status: "success" | "failed" | "running"
affectedUsers: number
executedBy: string
}
const mockExecutionHistory: ExecutionRecord[] = [
{
id: "exec-001",
ruleName: "高价值用户识别",
executionTime: "2023-07-21 15:30:45",
duration: "45秒",
status: "success",
affectedUsers: 1250,
executedBy: "系统自动",
},
{
id: "exec-002",
ruleName: "游戏爱好者标记",
executionTime: "2023-07-21 14:45:12",
duration: "38秒",
status: "success",
affectedUsers: 2840,
executedBy: "系统自动",
},
{
id: "exec-003",
ruleName: "流失风险预警",
executionTime: "2023-07-21 13:20:33",
duration: "52秒",
status: "success",
affectedUsers: 890,
executedBy: "系统自动",
},
{
id: "exec-004",
ruleName: "高价值用户识别",
executionTime: "2023-07-20 15:30:18",
duration: "47秒",
status: "success",
affectedUsers: 1235,
executedBy: "系统自动",
},
{
id: "exec-005",
ruleName: "周末活跃用户",
executionTime: "2023-07-20 12:15:42",
duration: "1分15秒",
status: "success",
affectedUsers: 3520,
executedBy: "admin",
},
{
id: "exec-006",
ruleName: "潜在高转化用户",
executionTime: "2023-07-19 16:45:30",
duration: "2分08秒",
status: "failed",
affectedUsers: 0,
executedBy: "admin",
},
{
id: "exec-007",
ruleName: "高价值用户识别",
executionTime: "2023-07-19 15:30:22",
duration: "46秒",
status: "success",
affectedUsers: 1228,
executedBy: "系统自动",
},
]
export function RuleExecutionHistory() {
const [searchQuery, setSearchQuery] = useState("")
const [selectedRecord, setSelectedRecord] = useState<ExecutionRecord | null>(null)
const [isDetailsOpen, setIsDetailsOpen] = useState(false)
const filteredHistory = mockExecutionHistory.filter(
(record) =>
record.ruleName.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.executedBy.toLowerCase().includes(searchQuery.toLowerCase()),
)
const getStatusBadge = (status: ExecutionRecord["status"]) => {
switch (status) {
case "success":
return <Badge className="bg-green-100 text-green-800"></Badge>
case "failed":
return <Badge className="bg-red-100 text-red-800"></Badge>
case "running":
return <Badge className="bg-blue-100 text-blue-800"></Badge>
}
}
const handleViewDetails = (record: ExecutionRecord) => {
setSelectedRecord(record)
setIsDetailsOpen(true)
}
return (
<>
<Card>
<CardContent className="p-6">
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-medium"></h3>
<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>
<Button variant="outline" size="icon">
<Calendar className="h-4 w-4" />
</Button>
</div>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredHistory.map((record) => (
<TableRow key={record.id}>
<TableCell className="font-mono text-xs">{record.id}</TableCell>
<TableCell>{record.ruleName}</TableCell>
<TableCell>{record.executionTime}</TableCell>
<TableCell>{record.duration}</TableCell>
<TableCell>{getStatusBadge(record.status)}</TableCell>
<TableCell>{record.affectedUsers.toLocaleString()}</TableCell>
<TableCell>{record.executedBy}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm" onClick={() => handleViewDetails(record)}>
<FileText className="h-4 w-4 mr-2" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{/* 执行详情对话框 */}
<Dialog open={isDetailsOpen} onOpenChange={setIsDetailsOpen}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{selectedRecord && (
<div className="space-y-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm font-medium text-gray-500">ID</p>
<p className="font-mono">{selectedRecord.id}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p>{selectedRecord.ruleName}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p>{selectedRecord.executionTime}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p>{selectedRecord.duration}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p>{getStatusBadge(selectedRecord.status)}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p>{selectedRecord.affectedUsers.toLocaleString()}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p>{selectedRecord.executedBy}</p>
</div>
</div>
<div>
<p className="text-sm font-medium text-gray-500 mb-2"></p>
<div className="bg-gray-50 p-4 rounded-md font-mono text-xs h-40 overflow-y-auto">
{selectedRecord.status === "success" ? (
<>
<p>
[INFO] {selectedRecord.executionTime} - "{selectedRecord.ruleName}"
</p>
<p>[INFO] {selectedRecord.executionTime} - </p>
<p>[INFO] {selectedRecord.executionTime} - </p>
<p>
[INFO] {selectedRecord.executionTime} - {selectedRecord.affectedUsers}{" "}
</p>
<p>[INFO] {selectedRecord.executionTime} - </p>
<p>[INFO] {selectedRecord.executionTime} - </p>
<p>
[INFO] {selectedRecord.executionTime} - {selectedRecord.duration}
</p>
</>
) : (
<>
<p>
[INFO] {selectedRecord.executionTime} - "{selectedRecord.ruleName}"
</p>
<p>[INFO] {selectedRecord.executionTime} - </p>
<p>[INFO] {selectedRecord.executionTime} - </p>
<p>[ERROR] {selectedRecord.executionTime} - 查询执行失败: 数据库连接超时</p>
<p>
[ERROR] {selectedRecord.executionTime} - {selectedRecord.duration}
</p>
</>
)}
</div>
</div>
</div>
)}
</DialogContent>
</Dialog>
</>
)
}

View File

@@ -1,307 +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 { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Calendar } from "@/components/ui/calendar"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { CalendarIcon, Plus, Tag, X } from "lucide-react"
import { format } from "date-fns"
import { cn } from "@/lib/utils"
export function TaskCreation() {
const [selectedTags, setSelectedTags] = useState<string[]>([])
const [newTag, setNewTag] = useState("")
const [date, setDate] = useState<Date>()
const addTag = () => {
if (newTag && !selectedTags.includes(newTag)) {
setSelectedTags([...selectedTags, newTag])
setNewTag("")
}
}
const removeTag = (tag: string) => {
setSelectedTags(selectedTags.filter((t) => t !== tag))
}
return (
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="basic" className="space-y-4">
<TabsList>
<TabsTrigger value="basic"></TabsTrigger>
<TabsTrigger value="target"></TabsTrigger>
<TabsTrigger value="action"></TabsTrigger>
<TabsTrigger value="schedule"></TabsTrigger>
</TabsList>
<TabsContent value="basic" className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label htmlFor="task-name"></Label>
<Input id="task-name" placeholder="输入任务名称" />
</div>
<div className="space-y-2">
<Label htmlFor="task-description"></Label>
<Textarea id="task-description" placeholder="输入任务描述" />
</div>
<div className="space-y-2">
<Label htmlFor="platform"></Label>
<Select>
<SelectTrigger id="platform">
<SelectValue placeholder="选择目标平台" />
</SelectTrigger>
<SelectContent>
<SelectItem value="jd"></SelectItem>
<SelectItem value="taobao"></SelectItem>
<SelectItem value="dangdang"></SelectItem>
<SelectItem value="xiaohongshu"></SelectItem>
<SelectItem value="zhihu"></SelectItem>
<SelectItem value="bilibili"></SelectItem>
<SelectItem value="douyin"></SelectItem>
<SelectItem value="other"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="priority"></Label>
<Select defaultValue="medium">
<SelectTrigger id="priority">
<SelectValue placeholder="选择优先级" />
</SelectTrigger>
<SelectContent>
<SelectItem value="high"></SelectItem>
<SelectItem value="medium"></SelectItem>
<SelectItem value="low"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</TabsContent>
<TabsContent value="target" className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label></Label>
<div className="flex flex-wrap gap-2 mb-2">
{selectedTags.map((tag) => (
<Badge key={tag} variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
{tag}
<button onClick={() => removeTag(tag)} className="ml-1">
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
<div className="flex space-x-2">
<Input
placeholder="输入标签名称"
value={newTag}
onChange={(e) => setNewTag(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && addTag()}
/>
<Button type="button" size="sm" onClick={addTag}>
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="user-segment"></Label>
<Select>
<SelectTrigger id="user-segment">
<SelectValue placeholder="选择用户分群" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="active"></SelectItem>
<SelectItem value="new"></SelectItem>
<SelectItem value="high-value"></SelectItem>
<SelectItem value="inactive"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="user-count"></Label>
<Input id="user-count" type="number" placeholder="输入目标用户数量" />
</div>
<div className="space-y-2">
<Label htmlFor="sampling-method"></Label>
<Select defaultValue="random">
<SelectTrigger id="sampling-method">
<SelectValue placeholder="选择抽样方式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="random"></SelectItem>
<SelectItem value="stratified"></SelectItem>
<SelectItem value="systematic"></SelectItem>
<SelectItem value="all"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</TabsContent>
<TabsContent value="action" className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label htmlFor="action-type"></Label>
<Select>
<SelectTrigger id="action-type">
<SelectValue placeholder="选择动作类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="login"></SelectItem>
<SelectItem value="purchase"></SelectItem>
<SelectItem value="browse"></SelectItem>
<SelectItem value="search"></SelectItem>
<SelectItem value="content"></SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="action-params"></Label>
<Textarea id="action-params" placeholder="输入动作参数JSON格式" />
</div>
<div className="space-y-2">
<Label htmlFor="success-criteria"></Label>
<Select>
<SelectTrigger id="success-criteria">
<SelectValue placeholder="选择成功标准" />
</SelectTrigger>
<SelectContent>
<SelectItem value="login-success"></SelectItem>
<SelectItem value="purchase-complete"></SelectItem>
<SelectItem value="browse-time"></SelectItem>
<SelectItem value="search-count"></SelectItem>
<SelectItem value="content-interaction"></SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Input placeholder="输入新标签名称" />
<Select>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="标签分类" />
</SelectTrigger>
<SelectContent>
<SelectItem value="behavior"></SelectItem>
<SelectItem value="preference"></SelectItem>
<SelectItem value="value"></SelectItem>
<SelectItem value="lifecycle"></SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
<Button type="button" size="sm">
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Switch id="auto-apply" />
<Label htmlFor="auto-apply"></Label>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="schedule" className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label htmlFor="execution-mode"></Label>
<Select defaultValue="immediate">
<SelectTrigger id="execution-mode">
<SelectValue placeholder="选择执行模式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="immediate"></SelectItem>
<SelectItem value="scheduled"></SelectItem>
<SelectItem value="recurring"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn("w-full justify-start text-left font-normal", !date && "text-muted-foreground")}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? format(date, "PPP") : "选择日期"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar mode="single" selected={date} onSelect={setDate} initialFocus />
</PopoverContent>
</Popover>
</div>
<div className="space-y-2">
<Label htmlFor="execution-time"></Label>
<Input id="execution-time" type="time" />
</div>
<div className="space-y-2">
<Label htmlFor="recurrence-pattern"></Label>
<Select disabled={true}>
<SelectTrigger id="recurrence-pattern">
<SelectValue placeholder="选择重复模式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="daily"></SelectItem>
<SelectItem value="weekly"></SelectItem>
<SelectItem value="monthly"></SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Switch id="notify-completion" />
<Label htmlFor="notify-completion"></Label>
</div>
</div>
</div>
</TabsContent>
</Tabs>
<div className="flex justify-end space-x-2 mt-6">
<Button variant="outline"></Button>
<Button></Button>
</div>
</CardContent>
</Card>
)
}

View File

@@ -1,273 +0,0 @@
"use client"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Progress } from "@/components/ui/progress"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Pause, AlertCircle, CheckCircle, RefreshCw, Tag, User } from "lucide-react"
export function TaskExecution() {
return (
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-medium"></h3>
<p className="text-sm text-muted-foreground"></p>
</div>
<div className="flex space-x-2">
<Button variant="outline" size="sm">
<Pause className="mr-2 h-4 w-4" />
</Button>
<Button size="sm">
<RefreshCw className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<div className="flex items-center">
<Badge className="bg-blue-100 text-blue-800 mr-2"></Badge>
<span className="text-sm text-muted-foreground">预计剩余时间: 45分钟</span>
</div>
<span className="text-sm font-medium">65%</span>
</div>
<Progress value={65} className="h-2" />
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center space-x-2">
<User className="h-5 w-5 text-blue-500" />
<span className="text-sm font-medium"></span>
</div>
<div className="mt-2">
<span className="text-2xl font-bold">12,500</span>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center space-x-2">
<CheckCircle className="h-5 w-5 text-green-500" />
<span className="text-sm font-medium"></span>
</div>
<div className="mt-2">
<span className="text-2xl font-bold">8,125</span>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center space-x-2">
<Tag className="h-5 w-5 text-purple-500" />
<span className="text-sm font-medium"></span>
</div>
<div className="mt-2">
<span className="text-2xl font-bold">5,840</span>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center space-x-2">
<AlertCircle className="h-5 w-5 text-red-500" />
<span className="text-sm font-medium"></span>
</div>
<div className="mt-2">
<span className="text-2xl font-bold">120</span>
</div>
</div>
</div>
<Tabs defaultValue="logs" className="space-y-4">
<TabsList>
<TabsTrigger value="logs"></TabsTrigger>
<TabsTrigger value="users"></TabsTrigger>
<TabsTrigger value="errors"></TabsTrigger>
</TabsList>
<TabsContent value="logs" className="space-y-4">
<div className="bg-black text-green-400 p-4 rounded-md font-mono text-sm h-60 overflow-y-auto">
<div>[2023-07-15 14:30:15] </div>
<div>[2023-07-15 14:30:16] ...</div>
<div>[2023-07-15 14:30:20] 12,500 </div>
<div>[2023-07-15 14:30:25] #1 (1000 users)</div>
<div>[2023-07-15 14:35:10] #1 处理完成: 成功 950, 50</div>
<div>[2023-07-15 14:35:15] #2 (1000 users)</div>
<div>[2023-07-15 14:40:05] #2 处理完成: 成功 980, 20</div>
<div>[2023-07-15 14:40:10] #3 (1000 users)</div>
<div>[2023-07-15 14:45:00] #3 处理完成: 成功 990, 10</div>
<div>[2023-07-15 14:45:05] #4 (1000 users)</div>
<div>[2023-07-15 14:50:00] #4 处理完成: 成功 995, 5</div>
<div>[2023-07-15 14:50:05] #5 (1000 users)</div>
<div>[2023-07-15 14:55:00] #5 处理完成: 成功 985, 15</div>
<div>[2023-07-15 14:55:05] #6 (1000 users)</div>
<div>[2023-07-15 15:00:00] #6 处理完成: 成功 975, 25</div>
<div>[2023-07-15 15:00:05] #7 (1000 users)</div>
<div>[2023-07-15 15:05:00] #7 处理完成: 成功 990, 10</div>
<div>[2023-07-15 15:05:05] #8 (1000 users)</div>
<div>[2023-07-15 15:10:00] #8 处理完成: 成功 975, 25</div>
<div>[2023-07-15 15:10:05] 8000 , 4500 </div>
<div>[2023-07-15 15:10:10] #9 (1000 users)</div>
</div>
</TabsContent>
<TabsContent value="users" className="space-y-4">
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>user_12345</TableCell>
<TableCell>2023-07-15 14:32:15</TableCell>
<TableCell>
<Badge className="bg-green-100 text-green-800"></Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
</div>
</TableCell>
<TableCell>, 30</TableCell>
</TableRow>
<TableRow>
<TableCell>user_12346</TableCell>
<TableCell>2023-07-15 14:32:18</TableCell>
<TableCell>
<Badge className="bg-green-100 text-green-800"></Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
</div>
</TableCell>
<TableCell>, </TableCell>
</TableRow>
<TableRow>
<TableCell>user_12347</TableCell>
<TableCell>2023-07-15 14:32:20</TableCell>
<TableCell>
<Badge className="bg-red-100 text-red-800"></Badge>
</TableCell>
<TableCell>-</TableCell>
<TableCell>, </TableCell>
</TableRow>
<TableRow>
<TableCell>user_12348</TableCell>
<TableCell>2023-07-15 14:32:25</TableCell>
<TableCell>
<Badge className="bg-green-100 text-green-800"></Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
</div>
</TableCell>
<TableCell>, </TableCell>
</TableRow>
<TableRow>
<TableCell>user_12349</TableCell>
<TableCell>2023-07-15 14:32:30</TableCell>
<TableCell>
<Badge className="bg-green-100 text-green-800"></Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
</div>
</TableCell>
<TableCell>, </TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</TabsContent>
<TabsContent value="errors" className="space-y-4">
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>err_001</TableCell>
<TableCell>2023-07-15 14:32:20</TableCell>
<TableCell>user_12347</TableCell>
<TableCell></TableCell>
<TableCell>, </TableCell>
</TableRow>
<TableRow>
<TableCell>err_002</TableCell>
<TableCell>2023-07-15 14:33:15</TableCell>
<TableCell>user_12356</TableCell>
<TableCell></TableCell>
<TableCell>, </TableCell>
</TableRow>
<TableRow>
<TableCell>err_003</TableCell>
<TableCell>2023-07-15 14:35:05</TableCell>
<TableCell>user_12378</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
<TableRow>
<TableCell>err_004</TableCell>
<TableCell>2023-07-15 14:38:30</TableCell>
<TableCell>user_12390</TableCell>
<TableCell></TableCell>
<TableCell>访</TableCell>
</TableRow>
<TableRow>
<TableCell>err_005</TableCell>
<TableCell>2023-07-15 14:42:10</TableCell>
<TableCell>user_12405</TableCell>
<TableCell>API限流</TableCell>
<TableCell></TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
)
}

View File

@@ -1,420 +0,0 @@
"use client"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
PieChart,
Pie,
Cell,
} from "recharts"
import { Download, FileDown, Tag, User, CheckCircle, Clock } from "lucide-react"
export function TaskResults() {
// 模拟数据
const tagDistributionData = [
{ name: "京东活跃用户", value: 5840 },
{ name: "电商偏好", value: 4250 },
{ name: "高消费用户", value: 1850 },
{ name: "品质生活", value: 2100 },
{ name: "数码爱好者", value: 1450 },
]
const userActionData = [
{ name: "登录成功", value: 8125 },
{ name: "浏览商品", value: 6540 },
{ name: "加入购物车", value: 3250 },
{ name: "完成购买", value: 1850 },
{ name: "评价商品", value: 980 },
]
const timeDistributionData = [
{ name: "0-5分钟", count: 2450 },
{ name: "5-10分钟", count: 3650 },
{ name: "10-20分钟", count: 2850 },
{ name: "20-30分钟", count: 1950 },
{ name: "30+分钟", count: 1600 },
]
const COLORS = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042", "#8884D8"]
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle></CardTitle>
<Button variant="outline" size="sm">
<FileDown className="mr-2 h-4 w-4" />
</Button>
</CardHeader>
<CardContent className="space-y-6">
<div>
<h3 className="text-lg font-medium"></h3>
<div className="flex items-center space-x-2 mt-1">
<Badge className="bg-green-100 text-green-800"></Badge>
<span className="text-sm text-muted-foreground">完成时间: 2023-07-15 16:15:30</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center space-x-2">
<User className="h-5 w-5 text-blue-500" />
<span className="text-sm font-medium"></span>
</div>
<div className="mt-2">
<span className="text-2xl font-bold">12,500</span>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center space-x-2">
<CheckCircle className="h-5 w-5 text-green-500" />
<span className="text-sm font-medium"></span>
</div>
<div className="mt-2">
<span className="text-2xl font-bold">12,380</span>
<span className="text-sm text-muted-foreground ml-2">99.0%</span>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center space-x-2">
<Tag className="h-5 w-5 text-purple-500" />
<span className="text-sm font-medium"></span>
</div>
<div className="mt-2">
<span className="text-2xl font-bold">8,450</span>
<span className="text-sm text-muted-foreground ml-2">67.6%</span>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center space-x-2">
<Clock className="h-5 w-5 text-orange-500" />
<span className="text-sm font-medium"></span>
</div>
<div className="mt-2">
<span className="text-2xl font-bold">1h 45m</span>
</div>
</div>
</div>
<Tabs defaultValue="overview" className="space-y-4">
<TabsList>
<TabsTrigger value="overview"></TabsTrigger>
<TabsTrigger value="tags"></TabsTrigger>
<TabsTrigger value="users"></TabsTrigger>
<TabsTrigger value="details"></TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="text-sm font-medium mb-4"></h4>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={tagDistributionData}
cx="50%"
cy="50%"
labelLine={false}
outerRadius={80}
fill="#8884d8"
dataKey="value"
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
>
{tagDistributionData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
</div>
</div>
<div>
<h4 className="text-sm font-medium mb-4"></h4>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={userActionData}
margin={{
top: 5,
right: 30,
left: 20,
bottom: 5,
}}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="value" fill="#8884d8" />
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
<div>
<h4 className="text-sm font-medium mb-4"></h4>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={timeDistributionData}
margin={{
top: 5,
right: 30,
left: 20,
bottom: 5,
}}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="count" fill="#82ca9d" />
</BarChart>
</ResponsiveContainer>
</div>
</div>
</TabsContent>
<TabsContent value="tags" className="space-y-4">
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>5,840</TableCell>
<TableCell>46.7%</TableCell>
<TableCell></TableCell>
<TableCell>2023-07-15 15:30:25</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>4,250</TableCell>
<TableCell>34.0%</TableCell>
<TableCell></TableCell>
<TableCell>2023-07-15 15:35:10</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>1,850</TableCell>
<TableCell>14.8%</TableCell>
<TableCell></TableCell>
<TableCell>2023-07-15 15:40:30</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>2,100</TableCell>
<TableCell>16.8%</TableCell>
<TableCell></TableCell>
<TableCell>2023-07-15 15:45:15</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>1,450</TableCell>
<TableCell>11.6%</TableCell>
<TableCell></TableCell>
<TableCell>2023-07-15 15:50:05</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</TabsContent>
<TabsContent value="users" className="space-y-4">
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>3,250</TableCell>
<TableCell>26.0%</TableCell>
<TableCell>25</TableCell>
<TableCell>12.5</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>4,850</TableCell>
<TableCell>38.8%</TableCell>
<TableCell>15</TableCell>
<TableCell>8.2</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">访</TableCell>
<TableCell>2,450</TableCell>
<TableCell>19.6%</TableCell>
<TableCell>3</TableCell>
<TableCell>2.1</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>1,830</TableCell>
<TableCell>14.6%</TableCell>
<TableCell>1</TableCell>
<TableCell>0.5</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>120</TableCell>
<TableCell>1.0%</TableCell>
<TableCell>-</TableCell>
<TableCell>-</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</TabsContent>
<TabsContent value="details" className="space-y-4">
<div className="flex justify-end mb-4">
<Button variant="outline" size="sm">
<Download className="mr-2 h-4 w-4" />
</Button>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>user_12345</TableCell>
<TableCell>2023-07-15 14:32:15</TableCell>
<TableCell>
<Badge className="bg-green-100 text-green-800"></Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
</div>
</TableCell>
<TableCell>18</TableCell>
<TableCell>9</TableCell>
<TableCell>, 30</TableCell>
</TableRow>
<TableRow>
<TableCell>user_12346</TableCell>
<TableCell>2023-07-15 14:32:18</TableCell>
<TableCell>
<Badge className="bg-green-100 text-green-800"></Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
</div>
</TableCell>
<TableCell>25</TableCell>
<TableCell>15</TableCell>
<TableCell>, </TableCell>
</TableRow>
<TableRow>
<TableCell>user_12347</TableCell>
<TableCell>2023-07-15 14:32:20</TableCell>
<TableCell>
<Badge className="bg-red-100 text-red-800"></Badge>
</TableCell>
<TableCell>-</TableCell>
<TableCell>-</TableCell>
<TableCell>-</TableCell>
<TableCell>, </TableCell>
</TableRow>
<TableRow>
<TableCell>user_12348</TableCell>
<TableCell>2023-07-15 14:32:25</TableCell>
<TableCell>
<Badge className="bg-green-100 text-green-800"></Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
</div>
</TableCell>
<TableCell>32</TableCell>
<TableCell>12</TableCell>
<TableCell>, </TableCell>
</TableRow>
<TableRow>
<TableCell>user_12349</TableCell>
<TableCell>2023-07-15 14:32:30</TableCell>
<TableCell>
<Badge className="bg-green-100 text-green-800"></Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
</div>
</TableCell>
<TableCell>28</TableCell>
<TableCell>18</TableCell>
<TableCell>, </TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,74 @@
"use client"
import * as React from "react"
import { HelpCircle } from "lucide-react"
import { cn } from "@/lib/utils"
interface TooltipHelpProps {
content: string
className?: string
}
export function TooltipHelp({ content, className = "" }: TooltipHelpProps) {
const [isVisible, setIsVisible] = React.useState(false)
const [position, setPosition] = React.useState({ top: 0, left: 0 })
const tooltipRef = React.useRef<HTMLDivElement>(null)
const timeoutRef = React.useRef<NodeJS.Timeout>()
const handleMouseEnter = (e: React.MouseEvent) => {
timeoutRef.current = setTimeout(() => {
const rect = (e.target as HTMLElement).getBoundingClientRect()
const tooltipRect = tooltipRef.current?.getBoundingClientRect()
if (tooltipRect) {
const top = rect.top - tooltipRect.height - 8
const left = rect.left + (rect.width - tooltipRect.width) / 2
setPosition({ top, left })
setIsVisible(true)
}
}, 200)
}
const handleMouseLeave = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
setIsVisible(false)
}
React.useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
}, [])
return (
<>
<HelpCircle
className={cn("h-4 w-4 text-gray-400 hover:text-gray-600 cursor-help", className)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
{isVisible && (
<div
ref={tooltipRef}
className="fixed z-50 px-3 py-2 text-xs text-white bg-gray-900 rounded-md shadow-lg max-w-xs"
style={{
top: position.top,
left: position.left,
transition: "opacity 150ms ease-in-out",
}}
>
{content}
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900"></div>
</div>
)}
</>
)
}
// 为了兼容性,导出一个简单的 TooltipProvider
export const TooltipProvider = ({ children }: { children: React.ReactNode }) => <>{children}</>

228
lib/data-dictionary.ts Normal file
View File

@@ -0,0 +1,228 @@
// 数据字典和映射规则管理
// 根据需求文档中的要求,创建动态维护的数据字典
export interface DataField {
name: string
type: "string" | "number" | "boolean" | "date" | "json" | "array"
description: string
isRequired: boolean
isPII: boolean // 个人身份信息标识
isIdentityKey: boolean // 身份识别关键字段
isAIFeature: boolean // 用于AI分析的特征字段
isDistributionKey: boolean // 用于分销/返点计算的关联字段
}
export interface SourceMapping {
sourceSystem: string
sourceField: string
targetField: string
transformRule?: string
validationRule?: string
}
export interface DataDictionary {
coreFields: Record<string, DataField>
unifiedTags: string[]
unifiedAttributes: Record<string, DataField>
sourceMappings: Record<string, SourceMapping[]>
}
// 核心数据字典定义
export const DATA_DICTIONARY: DataDictionary = {
coreFields: {
userId: {
name: "userId",
type: "string",
description: "全局唯一用户ID",
isRequired: true,
isPII: false,
isIdentityKey: true,
isAIFeature: false,
isDistributionKey: true,
},
username: {
name: "username",
type: "string",
description: "用户名",
isRequired: false,
isPII: true,
isIdentityKey: true,
isAIFeature: true,
isDistributionKey: false,
},
email: {
name: "email",
type: "string",
description: "邮箱地址",
isRequired: false,
isPII: true,
isIdentityKey: true,
isAIFeature: false,
isDistributionKey: false,
},
phone: {
name: "phone",
type: "string",
description: "手机号码",
isRequired: false,
isPII: true,
isIdentityKey: true,
isAIFeature: false,
isDistributionKey: false,
},
fullName: {
name: "fullName",
type: "string",
description: "真实姓名",
isRequired: false,
isPII: true,
isIdentityKey: true,
isAIFeature: true,
isDistributionKey: false,
},
gender: {
name: "gender",
type: "string",
description: "性别",
isRequired: false,
isPII: false,
isIdentityKey: false,
isAIFeature: true,
isDistributionKey: false,
},
birthDate: {
name: "birthDate",
type: "date",
description: "出生日期",
isRequired: false,
isPII: true,
isIdentityKey: false,
isAIFeature: true,
isDistributionKey: false,
},
city: {
name: "city",
type: "string",
description: "所在城市",
isRequired: false,
isPII: false,
isIdentityKey: false,
isAIFeature: true,
isDistributionKey: false,
},
province: {
name: "province",
type: "string",
description: "所在省份",
isRequired: false,
isPII: false,
isIdentityKey: false,
isAIFeature: true,
isDistributionKey: false,
},
},
unifiedTags: [
"高价值客户",
"新用户",
"活跃用户",
"流失风险",
"科技爱好者",
"内容创作者",
"90后",
"00后",
"北京地区",
"上海地区",
"游戏爱好者",
"旅游达人",
"美食家",
],
unifiedAttributes: {
totalSpend: {
name: "totalSpend",
type: "number",
description: "总消费金额",
isRequired: false,
isPII: false,
isIdentityKey: false,
isAIFeature: true,
isDistributionKey: true,
},
lastActiveDays: {
name: "lastActiveDays",
type: "number",
description: "最后活跃天数",
isRequired: false,
isPII: false,
isIdentityKey: false,
isAIFeature: true,
isDistributionKey: false,
},
contentPreference: {
name: "contentPreference",
type: "array",
description: "内容偏好",
isRequired: false,
isPII: false,
isIdentityKey: false,
isAIFeature: true,
isDistributionKey: false,
},
distributionLevel: {
name: "distributionLevel",
type: "number",
description: "分销层级",
isRequired: false,
isPII: false,
isIdentityKey: false,
isAIFeature: false,
isDistributionKey: true,
},
},
sourceMappings: {
douyin: [
{ sourceSystem: "douyin", sourceField: "openid", targetField: "userId", transformRule: "prefix_dy_" },
{ sourceSystem: "douyin", sourceField: "nickname", targetField: "username" },
{ sourceSystem: "douyin", sourceField: "avatar", targetField: "avatarUrl" },
],
xiaohongshu: [
{ sourceSystem: "xiaohongshu", sourceField: "openid", targetField: "userId", transformRule: "prefix_xhs_" },
{ sourceSystem: "xiaohongshu", sourceField: "nickname", targetField: "username" },
],
cunkebao_form: [
{ sourceSystem: "cunkebao_form", sourceField: "name", targetField: "fullName" },
{ sourceSystem: "cunkebao_form", sourceField: "phone", targetField: "phone" },
{ sourceSystem: "cunkebao_form", sourceField: "email", targetField: "email" },
],
touchkebao_call: [
{ sourceSystem: "touchkebao_call", sourceField: "phone_number", targetField: "phone" },
{ sourceSystem: "touchkebao_call", sourceField: "call_time", targetField: "lastContactTime" },
],
},
}
// 获取字段映射规则
export function getFieldMapping(sourceSystem: string, sourceField: string): SourceMapping | undefined {
const mappings = DATA_DICTIONARY.sourceMappings[sourceSystem]
return mappings?.find((mapping) => mapping.sourceField === sourceField)
}
// 获取身份识别关键字段
export function getIdentityKeyFields(): string[] {
return Object.entries(DATA_DICTIONARY.coreFields)
.filter(([_, field]) => field.isIdentityKey)
.map(([name, _]) => name)
}
// 获取AI特征字段
export function getAIFeatureFields(): string[] {
return Object.entries(DATA_DICTIONARY.coreFields)
.filter(([_, field]) => field.isAIFeature)
.map(([name, _]) => name)
}
// 获取分销关联字段
export function getDistributionKeyFields(): string[] {
return Object.entries(DATA_DICTIONARY.coreFields)
.filter(([_, field]) => field.isDistributionKey)
.map(([name, _]) => name)
}

433
lib/mindsdb-connector.ts Normal file
View File

@@ -0,0 +1,433 @@
// MindsDB连接器 - 实现AI增强的数据查询和分析
import { Client } from "mindsdb-js-sdk"
export interface MindsDBConfig {
host: string
port: number
username: string
password: string
database?: string
}
export interface AIQueryRequest {
query: string
model?: string
parameters?: Record<string, any>
useCache?: boolean
}
export interface SearchRequest {
keyword: string
type: "user" | "traffic" | "all"
filters?: Record<string, any>
limit?: number
offset?: number
}
export interface VersionInfo {
version: string
timestamp: string
changes: string[]
author: string
}
export class MindsDBConnector {
private client: Client
private connected = false
private cache: Map<string, any> = new Map()
constructor(private config: MindsDBConfig) {
this.client = new Client({
host: config.host,
port: config.port,
username: config.username,
password: config.password,
})
}
// 连接到MindsDB
async connect(): Promise<void> {
try {
await this.client.connect()
this.connected = true
console.log("MindsDB连接成功")
} catch (error) {
console.error("MindsDB连接失败:", error)
throw error
}
}
// 断开连接
async disconnect(): Promise<void> {
if (this.connected) {
await this.client.disconnect()
this.connected = false
}
}
// AI增强查询 - 使用自然语言查询数据
async aiQuery(request: AIQueryRequest): Promise<any> {
if (!this.connected) {
await this.connect()
}
const cacheKey = `ai_query_${JSON.stringify(request)}`
// 检查缓存
if (request.useCache && this.cache.has(cacheKey)) {
return this.cache.get(cacheKey)
}
try {
// 使用MindsDB的AI模型进行查询
const query = `
SELECT * FROM mindsdb.${request.model || "gpt4"}
WHERE text = '${request.query}'
`
const result = await this.client.query(query)
// 缓存结果
if (request.useCache) {
this.cache.set(cacheKey, result, 300000) // 5分钟缓存
}
return result
} catch (error) {
console.error("AI查询失败:", error)
throw error
}
}
// 智能搜索 - 支持用户数据和流量关键词的快速搜索
async intelligentSearch(request: SearchRequest): Promise<any> {
if (!this.connected) {
await this.connect()
}
const cacheKey = `search_${JSON.stringify(request)}`
// 检查缓存
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey)
}
try {
let searchQuery = ""
switch (request.type) {
case "user":
searchQuery = this.buildUserSearchQuery(request)
break
case "traffic":
searchQuery = this.buildTrafficSearchQuery(request)
break
case "all":
searchQuery = this.buildUnifiedSearchQuery(request)
break
}
const result = await this.client.query(searchQuery)
// 缓存结果
this.cache.set(cacheKey, result, 60000) // 1分钟缓存
return result
} catch (error) {
console.error("智能搜索失败:", error)
throw error
}
}
// 构建用户搜索查询
private buildUserSearchQuery(request: SearchRequest): string {
const { keyword, filters, limit = 100, offset = 0 } = request
let query = `
SELECT
u.user_id,
u.username,
u.phone,
u.email,
u.tags,
u.rfm_score,
u.last_active,
u.created_at,
MATCH(u.username, u.phone, u.email, u.tags) AGAINST('${keyword}' IN NATURAL LANGUAGE MODE) as relevance_score
FROM users u
WHERE MATCH(u.username, u.phone, u.email, u.tags) AGAINST('${keyword}' IN NATURAL LANGUAGE MODE)
`
// 添加过滤条件
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
query += ` AND u.${key} = '${value}'`
})
}
query += ` ORDER BY relevance_score DESC, u.last_active DESC`
query += ` LIMIT ${limit} OFFSET ${offset}`
return query
}
// 构建流量关键词搜索查询
private buildTrafficSearchQuery(request: SearchRequest): string {
const { keyword, filters, limit = 100, offset = 0 } = request
let query = `
SELECT
t.keyword_id,
t.keyword,
t.category,
t.search_volume,
t.competition,
t.cpc,
t.trend_data,
t.last_updated,
MATCH(t.keyword, t.category) AGAINST('${keyword}' IN NATURAL LANGUAGE MODE) as relevance_score
FROM traffic_keywords t
WHERE MATCH(t.keyword, t.category) AGAINST('${keyword}' IN NATURAL LANGUAGE MODE)
`
// 添加过滤条件
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
query += ` AND t.${key} = '${value}'`
})
}
query += ` ORDER BY relevance_score DESC, t.search_volume DESC`
query += ` LIMIT ${limit} OFFSET ${offset}`
return query
}
// 构建统一搜索查询
private buildUnifiedSearchQuery(request: SearchRequest): string {
const { keyword, limit = 100, offset = 0 } = request
return `
(
SELECT
'user' as type,
user_id as id,
username as title,
CONCAT(phone, ' | ', email) as description,
tags,
last_active as updated_at,
MATCH(username, phone, email, tags) AGAINST('${keyword}' IN NATURAL LANGUAGE MODE) as relevance_score
FROM users
WHERE MATCH(username, phone, email, tags) AGAINST('${keyword}' IN NATURAL LANGUAGE MODE)
)
UNION ALL
(
SELECT
'traffic' as type,
keyword_id as id,
keyword as title,
CONCAT('搜索量: ', search_volume, ' | 竞争度: ', competition) as description,
category as tags,
last_updated as updated_at,
MATCH(keyword, category) AGAINST('${keyword}' IN NATURAL LANGUAGE MODE) as relevance_score
FROM traffic_keywords
WHERE MATCH(keyword, category) AGAINST('${keyword}' IN NATURAL LANGUAGE MODE)
)
ORDER BY relevance_score DESC
LIMIT ${limit} OFFSET ${offset}
`
}
// 用户数据分析 - 使用AI进行用户行为分析
async analyzeUserBehavior(userId: string): Promise<any> {
if (!this.connected) {
await this.connect()
}
try {
const query = `
SELECT
prediction,
confidence,
explanation
FROM mindsdb.user_behavior_predictor
WHERE user_id = '${userId}'
`
return await this.client.query(query)
} catch (error) {
console.error("用户行为分析失败:", error)
throw error
}
}
// 流量预测 - 使用AI预测流量趋势
async predictTrafficTrends(keyword: string, timeframe = "30d"): Promise<any> {
if (!this.connected) {
await this.connect()
}
try {
const query = `
SELECT
predicted_volume,
trend_direction,
confidence_interval,
factors
FROM mindsdb.traffic_predictor
WHERE keyword = '${keyword}' AND timeframe = '${timeframe}'
`
return await this.client.query(query)
} catch (error) {
console.error("流量预测失败:", error)
throw error
}
}
// 版本管理 - 创建数据版本
async createVersion(data: any, author: string, changes: string[]): Promise<VersionInfo> {
const version = `v${Date.now()}`
const timestamp = new Date().toISOString()
const versionInfo: VersionInfo = {
version,
timestamp,
changes,
author,
}
try {
// 存储版本信息
const query = `
INSERT INTO data_versions (version, timestamp, data_snapshot, changes, author)
VALUES ('${version}', '${timestamp}', '${JSON.stringify(data)}', '${JSON.stringify(changes)}', '${author}')
`
await this.client.query(query)
return versionInfo
} catch (error) {
console.error("创建版本失败:", error)
throw error
}
}
// 获取版本历史
async getVersionHistory(limit = 50): Promise<VersionInfo[]> {
if (!this.connected) {
await this.connect()
}
try {
const query = `
SELECT version, timestamp, changes, author
FROM data_versions
ORDER BY timestamp DESC
LIMIT ${limit}
`
const result = await this.client.query(query)
return result.rows || []
} catch (error) {
console.error("获取版本历史失败:", error)
throw error
}
}
// 恢复到指定版本
async restoreVersion(version: string): Promise<any> {
if (!this.connected) {
await this.connect()
}
try {
const query = `
SELECT data_snapshot
FROM data_versions
WHERE version = '${version}'
`
const result = await this.client.query(query)
if (result.rows && result.rows.length > 0) {
return JSON.parse(result.rows[0].data_snapshot)
}
throw new Error(`版本 ${version} 不存在`)
} catch (error) {
console.error("恢复版本失败:", error)
throw error
}
}
// 实时数据同步
async syncRealTimeData(source: string, data: any): Promise<void> {
if (!this.connected) {
await this.connect()
}
try {
const query = `
INSERT INTO real_time_data (source, data, timestamp)
VALUES ('${source}', '${JSON.stringify(data)}', NOW())
ON DUPLICATE KEY UPDATE
data = '${JSON.stringify(data)}',
timestamp = NOW()
`
await this.client.query(query)
} catch (error) {
console.error("实时数据同步失败:", error)
throw error
}
}
// 清理缓存
clearCache(): void {
this.cache.clear()
}
// 获取系统状态
async getSystemStatus(): Promise<any> {
if (!this.connected) {
await this.connect()
}
try {
const queries = [
"SELECT COUNT(*) as user_count FROM users",
"SELECT COUNT(*) as keyword_count FROM traffic_keywords",
"SELECT COUNT(*) as version_count FROM data_versions",
"SELECT AVG(response_time) as avg_response_time FROM query_logs WHERE created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)",
]
const results = await Promise.all(queries.map((query) => this.client.query(query)))
return {
userCount: results[0].rows[0].user_count,
keywordCount: results[1].rows[0].keyword_count,
versionCount: results[2].rows[0].version_count,
avgResponseTime: results[3].rows[0].avg_response_time || 0,
cacheSize: this.cache.size,
connected: this.connected,
}
} catch (error) {
console.error("获取系统状态失败:", error)
throw error
}
}
}
// 单例模式
let mindsDBInstance: MindsDBConnector | null = null
export function getMindsDBConnector(config?: MindsDBConfig): MindsDBConnector {
if (!mindsDBInstance && config) {
mindsDBInstance = new MindsDBConnector(config)
}
if (!mindsDBInstance) {
throw new Error("MindsDB连接器未初始化请提供配置信息")
}
return mindsDBInstance
}

View File

@@ -35,12 +35,12 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "latest", "cmdk": "latest",
"d3": "latest",
"date-fns": "latest", "date-fns": "latest",
"docx": "latest", "docx": "latest",
"dom-to-image": "latest", "dom-to-image": "latest",
"html-to-image": "latest", "html-to-image": "latest",
"lucide-react": "^0.454.0", "lucide-react": "^0.454.0",
"mindsdb-js-sdk": "latest",
"next": "14.2.16", "next": "14.2.16",
"next-themes": "latest", "next-themes": "latest",
"react": "^18", "react": "^18",

295
pnpm-lock.yaml generated
View File

@@ -10,7 +10,7 @@ importers:
dependencies: dependencies:
'@ant-design/plots': '@ant-design/plots':
specifier: latest specifier: latest
version: 2.6.0(react-dom@18.0.0(react@18.0.0))(react@18.0.0) version: 2.6.1(react-dom@18.0.0(react@18.0.0))(react@18.0.0)
'@radix-ui/react-accordion': '@radix-ui/react-accordion':
specifier: latest specifier: latest
version: 1.2.11(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) version: 1.2.11(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0)
@@ -86,9 +86,6 @@ importers:
cmdk: cmdk:
specifier: latest specifier: latest
version: 1.1.1(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) version: 1.1.1(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0)
d3:
specifier: latest
version: 7.9.0
date-fns: date-fns:
specifier: latest specifier: latest
version: 4.1.0 version: 4.1.0
@@ -104,6 +101,9 @@ importers:
lucide-react: lucide-react:
specifier: ^0.454.0 specifier: ^0.454.0
version: 0.454.0(react@18.0.0) version: 0.454.0(react@18.0.0)
mindsdb-js-sdk:
specifier: latest
version: 2.3.2
next: next:
specifier: 14.2.16 specifier: 14.2.16
version: 14.2.16(react-dom@18.0.0(react@18.0.0))(react@18.0.0) version: 14.2.16(react-dom@18.0.0(react@18.0.0))(react@18.0.0)
@@ -171,8 +171,8 @@ packages:
react: '>=16.8.4' react: '>=16.8.4'
react-dom: '>=16.8.4' react-dom: '>=16.8.4'
'@ant-design/plots@2.6.0': '@ant-design/plots@2.6.1':
resolution: {integrity: sha512-l6sZLPoKPKWG6kdvHe+H7vUqNQk2i1xjsiBQe/8ABZ9KDRDnig+LzHBjHnVl0fvqmzDPI3O3RFEccl1tfA1i4A==} resolution: {integrity: sha512-X46qm2QcXJVV6hL+pcqtj9TsLEkOrow+uq7zDRQKbXWg9Um525GRnbF7PdaS89Rwoo3RdhrA3+T97VbQVYD0wQ==}
peerDependencies: peerDependencies:
react: '>=16.8.4' react: '>=16.8.4'
react-dom: '>=16.8.4' react-dom: '>=16.8.4'
@@ -1191,6 +1191,10 @@ packages:
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
agentkeepalive@4.6.0:
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
engines: {node: '>= 8.0.0'}
ajv@6.12.6: ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -1278,6 +1282,9 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
autoprefixer@10.0.1: autoprefixer@10.0.1:
resolution: {integrity: sha512-aQo2BDIsoOdemXUAOBpFv4ZQa2DrOtEufarYhtFsK1088Ca0TUwu/aQWf0M3mrILXZ3mTIVn1lR3hPW8acacsw==} resolution: {integrity: sha512-aQo2BDIsoOdemXUAOBpFv4ZQa2DrOtEufarYhtFsK1088Ca0TUwu/aQWf0M3mrILXZ3mTIVn1lR3hPW8acacsw==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
@@ -1293,6 +1300,9 @@ packages:
resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==}
engines: {node: '>=4'} engines: {node: '>=4'}
axios@1.11.0:
resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==}
axobject-query@4.1.0: axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1300,6 +1310,12 @@ packages:
balanced-match@1.0.2: balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
bignumber.js@9.0.0:
resolution: {integrity: sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==}
bignumber.js@9.3.1:
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
binary-extensions@2.3.0: binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1383,6 +1399,10 @@ packages:
colorette@1.4.0: colorette@1.4.0:
resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@4.1.1: commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@@ -1413,41 +1433,17 @@ packages:
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
engines: {node: '>=12'} engines: {node: '>=12'}
d3-axis@3.0.0:
resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==}
engines: {node: '>=12'}
d3-binarytree@1.0.2: d3-binarytree@1.0.2:
resolution: {integrity: sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==} resolution: {integrity: sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==}
d3-brush@3.0.0:
resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==}
engines: {node: '>=12'}
d3-chord@3.0.1:
resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==}
engines: {node: '>=12'}
d3-color@3.1.0: d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'} engines: {node: '>=12'}
d3-contour@4.0.2:
resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==}
engines: {node: '>=12'}
d3-delaunay@6.0.4:
resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==}
engines: {node: '>=12'}
d3-dispatch@3.0.1: d3-dispatch@3.0.1:
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
engines: {node: '>=12'} engines: {node: '>=12'}
d3-drag@3.0.0:
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
engines: {node: '>=12'}
d3-dsv@3.0.1: d3-dsv@3.0.1:
resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -1497,10 +1493,6 @@ packages:
resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
d3-polygon@3.0.1:
resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==}
engines: {node: '>=12'}
d3-quadtree@3.0.1: d3-quadtree@3.0.1:
resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -1520,10 +1512,6 @@ packages:
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
d3-selection@3.0.0:
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
engines: {node: '>=12'}
d3-shape@3.2.0: d3-shape@3.2.0:
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -1540,20 +1528,6 @@ packages:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'} engines: {node: '>=12'}
d3-transition@3.0.1:
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
engines: {node: '>=12'}
peerDependencies:
d3-selection: 2 - 3
d3-zoom@3.0.0:
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
engines: {node: '>=12'}
d3@7.9.0:
resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==}
engines: {node: '>=12'}
damerau-levenshtein@1.0.8: damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
@@ -1606,8 +1580,9 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
delaunator@5.0.1: delayed-stream@1.0.0:
resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
detect-node-es@1.1.0: detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
@@ -1867,6 +1842,15 @@ packages:
resolution: {integrity: sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==} resolution: {integrity: sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==}
engines: {node: '>=6'} engines: {node: '>=6'}
follow-redirects@1.15.9:
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
for-each@0.3.5: for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1875,6 +1859,10 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'} engines: {node: '>=14'}
form-data@4.0.4:
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'}
fs.realpath@1.0.0: fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
@@ -1991,6 +1979,9 @@ packages:
html-to-image@1.11.13: html-to-image@1.11.13:
resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==} resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
humanize-ms@1.2.1:
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
iconv-lite@0.6.3: iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -2177,6 +2168,9 @@ packages:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true hasBin: true
json-bigint@1.0.0:
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
json-buffer@3.0.1: json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
@@ -2251,6 +2245,17 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
mindsdb-js-sdk@2.3.2:
resolution: {integrity: sha512-pn3Ek8c5s9uWiJARNe/hgiSvzmCWJMR2a28d1MOP0f3KlezbxhRFP9B8DDhJRtZxgAuFDgYgvHoBROrSe5cuPw==}
minimalistic-assert@1.0.1: minimalistic-assert@1.0.1:
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
@@ -2275,6 +2280,10 @@ packages:
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
mysql@2.18.1:
resolution: {integrity: sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==}
engines: {node: '>= 0.6'}
mz@2.7.0: mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
@@ -2495,6 +2504,9 @@ packages:
prop-types@15.8.1: prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
punycode@2.3.1: punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -2584,6 +2596,9 @@ packages:
read-cache@1.0.0: read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
readable-stream@2.3.7:
resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==}
readable-stream@2.3.8: readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
@@ -2650,9 +2665,6 @@ packages:
deprecated: Rimraf versions prior to v4 are no longer supported deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true hasBin: true
robust-predicates@3.0.2:
resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
run-parallel@1.2.0: run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@@ -2746,6 +2758,10 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
sqlstring@2.3.1:
resolution: {integrity: sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==}
engines: {node: '>= 0.6'}
stable-hash@0.0.5: stable-hash@0.0.5:
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
@@ -3027,7 +3043,7 @@ snapshots:
react: 18.0.0 react: 18.0.0
react-dom: 18.0.0(react@18.0.0) react-dom: 18.0.0(react@18.0.0)
'@ant-design/plots@2.6.0(react-dom@18.0.0(react@18.0.0))(react@18.0.0)': '@ant-design/plots@2.6.1(react-dom@18.0.0(react@18.0.0))(react@18.0.0)':
dependencies: dependencies:
'@ant-design/charts-util': 0.0.2(react-dom@18.0.0(react@18.0.0))(react@18.0.0) '@ant-design/charts-util': 0.0.2(react-dom@18.0.0(react@18.0.0))(react@18.0.0)
'@antv/event-emitter': 0.1.3 '@antv/event-emitter': 0.1.3
@@ -4182,6 +4198,10 @@ snapshots:
acorn@8.15.0: {} acorn@8.15.0: {}
agentkeepalive@4.6.0:
dependencies:
humanize-ms: 1.2.1
ajv@6.12.6: ajv@6.12.6:
dependencies: dependencies:
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
@@ -4291,6 +4311,8 @@ snapshots:
async-function@1.0.0: {} async-function@1.0.0: {}
asynckit@0.4.0: {}
autoprefixer@10.0.1(postcss@8.5.0): autoprefixer@10.0.1(postcss@8.5.0):
dependencies: dependencies:
browserslist: 4.25.1 browserslist: 4.25.1
@@ -4307,10 +4329,22 @@ snapshots:
axe-core@4.10.3: {} axe-core@4.10.3: {}
axios@1.11.0:
dependencies:
follow-redirects: 1.15.9
form-data: 4.0.4
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
axobject-query@4.1.0: {} axobject-query@4.1.0: {}
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
bignumber.js@9.0.0: {}
bignumber.js@9.3.1: {}
binary-extensions@2.3.0: {} binary-extensions@2.3.0: {}
brace-expansion@1.1.12: brace-expansion@1.1.12:
@@ -4410,6 +4444,10 @@ snapshots:
colorette@1.4.0: {} colorette@1.4.0: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
commander@4.1.1: {} commander@4.1.1: {}
commander@7.2.0: {} commander@7.2.0: {}
@@ -4432,39 +4470,12 @@ snapshots:
dependencies: dependencies:
internmap: 2.0.3 internmap: 2.0.3
d3-axis@3.0.0: {}
d3-binarytree@1.0.2: {} d3-binarytree@1.0.2: {}
d3-brush@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
d3-chord@3.0.1:
dependencies:
d3-path: 3.1.0
d3-color@3.1.0: {} d3-color@3.1.0: {}
d3-contour@4.0.2:
dependencies:
d3-array: 3.2.4
d3-delaunay@6.0.4:
dependencies:
delaunator: 5.0.1
d3-dispatch@3.0.1: {} d3-dispatch@3.0.1: {}
d3-drag@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-selection: 3.0.0
d3-dsv@3.0.1: d3-dsv@3.0.1:
dependencies: dependencies:
commander: 7.2.0 commander: 7.2.0
@@ -4513,8 +4524,6 @@ snapshots:
d3-path@3.1.0: {} d3-path@3.1.0: {}
d3-polygon@3.0.1: {}
d3-quadtree@3.0.1: {} d3-quadtree@3.0.1: {}
d3-random@3.0.1: {} d3-random@3.0.1: {}
@@ -4534,8 +4543,6 @@ snapshots:
d3-time: 3.1.0 d3-time: 3.1.0
d3-time-format: 4.1.0 d3-time-format: 4.1.0
d3-selection@3.0.0: {}
d3-shape@3.2.0: d3-shape@3.2.0:
dependencies: dependencies:
d3-path: 3.1.0 d3-path: 3.1.0
@@ -4550,56 +4557,6 @@ snapshots:
d3-timer@3.0.1: {} d3-timer@3.0.1: {}
d3-transition@3.0.1(d3-selection@3.0.0):
dependencies:
d3-color: 3.1.0
d3-dispatch: 3.0.1
d3-ease: 3.0.1
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-timer: 3.0.1
d3-zoom@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
d3@7.9.0:
dependencies:
d3-array: 3.2.4
d3-axis: 3.0.0
d3-brush: 3.0.0
d3-chord: 3.0.1
d3-color: 3.1.0
d3-contour: 4.0.2
d3-delaunay: 6.0.4
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-dsv: 3.0.1
d3-ease: 3.0.1
d3-fetch: 3.0.1
d3-force: 3.0.0
d3-format: 3.1.0
d3-geo: 3.1.1
d3-hierarchy: 3.1.2
d3-interpolate: 3.0.1
d3-path: 3.1.0
d3-polygon: 3.0.1
d3-quadtree: 3.0.1
d3-random: 3.0.1
d3-scale: 4.0.2
d3-scale-chromatic: 3.1.0
d3-selection: 3.0.0
d3-shape: 3.2.0
d3-time: 3.1.0
d3-time-format: 4.1.0
d3-timer: 3.0.1
d3-transition: 3.0.1(d3-selection@3.0.0)
d3-zoom: 3.0.0
damerau-levenshtein@1.0.8: {} damerau-levenshtein@1.0.8: {}
data-view-buffer@1.0.2: data-view-buffer@1.0.2:
@@ -4648,9 +4605,7 @@ snapshots:
has-property-descriptors: 1.0.2 has-property-descriptors: 1.0.2
object-keys: 1.1.1 object-keys: 1.1.1
delaunator@5.0.1: delayed-stream@1.0.0: {}
dependencies:
robust-predicates: 3.0.2
detect-node-es@1.1.0: {} detect-node-es@1.1.0: {}
@@ -5053,6 +5008,8 @@ snapshots:
flru@1.0.2: {} flru@1.0.2: {}
follow-redirects@1.15.9: {}
for-each@0.3.5: for-each@0.3.5:
dependencies: dependencies:
is-callable: 1.2.7 is-callable: 1.2.7
@@ -5062,6 +5019,14 @@ snapshots:
cross-spawn: 7.0.6 cross-spawn: 7.0.6
signal-exit: 4.1.0 signal-exit: 4.1.0
form-data@4.0.4:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35
fs.realpath@1.0.0: {} fs.realpath@1.0.0: {}
fsevents@2.3.3: fsevents@2.3.3:
@@ -5199,6 +5164,10 @@ snapshots:
html-to-image@1.11.13: {} html-to-image@1.11.13: {}
humanize-ms@1.2.1:
dependencies:
ms: 2.1.3
iconv-lite@0.6.3: iconv-lite@0.6.3:
dependencies: dependencies:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
@@ -5387,6 +5356,10 @@ snapshots:
dependencies: dependencies:
argparse: 2.0.1 argparse: 2.0.1
json-bigint@1.0.0:
dependencies:
bignumber.js: 9.3.1
json-buffer@3.0.1: {} json-buffer@3.0.1: {}
json-schema-traverse@0.4.1: {} json-schema-traverse@0.4.1: {}
@@ -5457,6 +5430,21 @@ snapshots:
braces: 3.0.3 braces: 3.0.3
picomatch: 2.3.1 picomatch: 2.3.1
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
mindsdb-js-sdk@2.3.2:
dependencies:
agentkeepalive: 4.6.0
axios: 1.11.0
json-bigint: 1.0.0
mysql: 2.18.1
transitivePeerDependencies:
- debug
minimalistic-assert@1.0.1: {} minimalistic-assert@1.0.1: {}
minimatch@3.1.2: minimatch@3.1.2:
@@ -5477,6 +5465,13 @@ snapshots:
ms@2.1.3: {} ms@2.1.3: {}
mysql@2.18.1:
dependencies:
bignumber.js: 9.0.0
readable-stream: 2.3.7
safe-buffer: 5.1.2
sqlstring: 2.3.1
mz@2.7.0: mz@2.7.0:
dependencies: dependencies:
any-promise: 1.3.0 any-promise: 1.3.0
@@ -5682,6 +5677,8 @@ snapshots:
object-assign: 4.1.1 object-assign: 4.1.1
react-is: 16.13.1 react-is: 16.13.1
proxy-from-env@1.1.0: {}
punycode@2.3.1: {} punycode@2.3.1: {}
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
@@ -5759,6 +5756,16 @@ snapshots:
dependencies: dependencies:
pify: 2.3.0 pify: 2.3.0
readable-stream@2.3.7:
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
readable-stream@2.3.8: readable-stream@2.3.8:
dependencies: dependencies:
core-util-is: 1.0.3 core-util-is: 1.0.3
@@ -5847,8 +5854,6 @@ snapshots:
dependencies: dependencies:
glob: 7.2.3 glob: 7.2.3
robust-predicates@3.0.2: {}
run-parallel@1.2.0: run-parallel@1.2.0:
dependencies: dependencies:
queue-microtask: 1.2.3 queue-microtask: 1.2.3
@@ -5956,6 +5961,8 @@ snapshots:
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
sqlstring@2.3.1: {}
stable-hash@0.0.5: {} stable-hash@0.0.5: {}
stop-iteration-iterator@1.1.0: stop-iteration-iterator@1.1.0:

232
services/IdentityService.ts Normal file
View File

@@ -0,0 +1,232 @@
// 身份识别与合并服务
// 实现基于数据字典标识的关键字段的匹配逻辑
import { getIdentityKeyFields } from "@/lib/data-dictionary"
export interface IdentityMatch {
userId: string
confidence: number
matchedFields: string[]
matchType: "exact" | "fuzzy" | "partial"
}
export interface UserIdentity {
userId: string
identityFields: Record<string, any>
}
export class IdentityService {
private static instance: IdentityService
private userIdentities: Map<string, UserIdentity> = new Map()
private constructor() {
// 初始化一些模拟数据
this.initializeMockData()
}
public static getInstance(): IdentityService {
if (!IdentityService.instance) {
IdentityService.instance = new IdentityService()
}
return IdentityService.instance
}
private initializeMockData() {
// 模拟已存在的用户身份数据
const mockUsers = [
{
userId: "user_global_id_1",
identityFields: {
phone: "13800138000",
email: "zhangsan@example.com",
username: "zhangsan",
fullName: "张三",
},
},
{
userId: "user_global_id_2",
identityFields: {
phone: "13912345678",
email: "lisi@example.com",
username: "lisi",
fullName: "李四",
},
},
]
mockUsers.forEach((user) => {
this.userIdentities.set(user.userId, user)
})
}
// 根据输入数据查找匹配的用户身份
public async findMatchingIdentity(inputData: Record<string, any>): Promise<IdentityMatch[]> {
const identityKeyFields = getIdentityKeyFields()
const matches: IdentityMatch[] = []
for (const [userId, userIdentity] of this.userIdentities) {
const matchResult = this.calculateMatch(inputData, userIdentity.identityFields, identityKeyFields)
if (matchResult.confidence > 0) {
matches.push({
userId,
...matchResult,
})
}
}
// 按置信度排序
return matches.sort((a, b) => b.confidence - a.confidence)
}
// 计算匹配度
private calculateMatch(
inputData: Record<string, any>,
existingData: Record<string, any>,
keyFields: string[],
): { confidence: number; matchedFields: string[]; matchType: "exact" | "fuzzy" | "partial" } {
const matchedFields: string[] = []
let exactMatches = 0
let fuzzyMatches = 0
let totalFields = 0
for (const field of keyFields) {
if (inputData[field] && existingData[field]) {
totalFields++
if (this.isExactMatch(inputData[field], existingData[field])) {
exactMatches++
matchedFields.push(field)
} else if (this.isFuzzyMatch(inputData[field], existingData[field])) {
fuzzyMatches++
matchedFields.push(field)
}
}
}
if (totalFields === 0) {
return { confidence: 0, matchedFields: [], matchType: "partial" }
}
const confidence = (exactMatches * 1.0 + fuzzyMatches * 0.7) / totalFields
let matchType: "exact" | "fuzzy" | "partial" = "partial"
if (exactMatches > 0 && fuzzyMatches === 0) {
matchType = "exact"
} else if (exactMatches > 0 || fuzzyMatches > 0) {
matchType = "fuzzy"
}
return { confidence, matchedFields, matchType }
}
// 精确匹配
private isExactMatch(value1: any, value2: any): boolean {
if (typeof value1 === "string" && typeof value2 === "string") {
return value1.toLowerCase().trim() === value2.toLowerCase().trim()
}
return value1 === value2
}
// 模糊匹配
private isFuzzyMatch(value1: any, value2: any): boolean {
if (typeof value1 === "string" && typeof value2 === "string") {
const str1 = value1.toLowerCase().trim()
const str2 = value2.toLowerCase().trim()
// 简单的相似度计算
const similarity = this.calculateStringSimilarity(str1, str2)
return similarity > 0.8
}
return false
}
// 计算字符串相似度
private calculateStringSimilarity(str1: string, str2: string): number {
const longer = str1.length > str2.length ? str1 : str2
const shorter = str1.length > str2.length ? str2 : str1
if (longer.length === 0) {
return 1.0
}
const editDistance = this.levenshteinDistance(longer, shorter)
return (longer.length - editDistance) / longer.length
}
// 计算编辑距离
private levenshteinDistance(str1: string, str2: string): number {
const matrix = []
for (let i = 0; i <= str2.length; i++) {
matrix[i] = [i]
}
for (let j = 0; j <= str1.length; j++) {
matrix[0][j] = j
}
for (let i = 1; i <= str2.length; i++) {
for (let j = 1; j <= str1.length; j++) {
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1]
} else {
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1)
}
}
}
return matrix[str2.length][str1.length]
}
// 创建新的用户身份
public async createNewIdentity(inputData: Record<string, any>): Promise<string> {
const userId = `user_global_id_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
const identityKeyFields = getIdentityKeyFields()
const identityFields: Record<string, any> = {}
identityKeyFields.forEach((field) => {
if (inputData[field]) {
identityFields[field] = inputData[field]
}
})
this.userIdentities.set(userId, {
userId,
identityFields,
})
return userId
}
// 更新用户身份信息
public async updateIdentity(userId: string, updateData: Record<string, any>): Promise<void> {
const existingIdentity = this.userIdentities.get(userId)
if (existingIdentity) {
const identityKeyFields = getIdentityKeyFields()
identityKeyFields.forEach((field) => {
if (updateData[field]) {
existingIdentity.identityFields[field] = updateData[field]
}
})
this.userIdentities.set(userId, existingIdentity)
}
}
// 合并用户身份
public async mergeIdentities(primaryUserId: string, secondaryUserId: string): Promise<void> {
const primaryIdentity = this.userIdentities.get(primaryUserId)
const secondaryIdentity = this.userIdentities.get(secondaryUserId)
if (primaryIdentity && secondaryIdentity) {
// 合并身份字段,优先保留主身份的数据
Object.keys(secondaryIdentity.identityFields).forEach((field) => {
if (!primaryIdentity.identityFields[field] && secondaryIdentity.identityFields[field]) {
primaryIdentity.identityFields[field] = secondaryIdentity.identityFields[field]
}
})
this.userIdentities.set(primaryUserId, primaryIdentity)
this.userIdentities.delete(secondaryUserId)
}
}
}

View File

@@ -0,0 +1,446 @@
// 数据接入服务
// 实现数据映射、转换、标准化和关联全局userId的核心逻辑
import { DATA_DICTIONARY } from "@/lib/data-dictionary"
import { IdentityService } from "./IdentityService"
import { getCollectionData } from "@/lib/mongodb-mock-connector"
export interface IngestionRequest {
source: string
sourceUserId?: string
sourceRecordId?: string
originalData: Record<string, any>
timestamp?: string
}
export interface ProcessedUserData {
userId: string
coreProfile: Record<string, any>
unifiedTags: string[]
unifiedAttributes: Record<string, any>
sourceProfiles: Array<{
source: string
sourceUserId?: string
sourceRecordId?: string
originalData: Record<string, any>
contentActivity?: any[]
interactionHistory?: any[]
ingestionTimestamp: string
}>
aiInsights?: Record<string, any>
crmInfo?: Record<string, any>
createdAt?: string
updatedAt: string
}
export class IngestionService {
private static instance: IngestionService
private identityService: IdentityService
private constructor() {
this.identityService = IdentityService.getInstance()
}
public static getInstance(): IngestionService {
if (!IngestionService.instance) {
IngestionService.instance = new IngestionService()
}
return IngestionService.instance
}
// 处理数据接入请求
public async processIngestionRequest(request: IngestionRequest): Promise<ProcessedUserData> {
try {
// 1. 数据映射与转换
const mappedData = await this.mapAndTransformData(request)
// 2. 身份识别与关联
const userId = await this.identifyOrCreateUser(mappedData)
// 3. 构建统一用户数据结构
const processedData = await this.buildUnifiedUserData(userId, request, mappedData)
// 4. 数据质量检查
await this.validateDataQuality(processedData)
// 5. 存储到数据库(这里是模拟)
await this.storeUserData(processedData)
return processedData
} catch (error) {
console.error("数据接入处理失败:", error)
throw new Error(`数据接入处理失败: ${(error as Error).message}`)
}
}
// 数据映射与转换
private async mapAndTransformData(request: IngestionRequest): Promise<Record<string, any>> {
const mappedData: Record<string, any> = {}
const sourceMappings = DATA_DICTIONARY.sourceMappings[request.source] || []
for (const [sourceField, sourceValue] of Object.entries(request.originalData)) {
const mapping = sourceMappings.find((m) => m.sourceField === sourceField)
if (mapping) {
let transformedValue = sourceValue
// 应用转换规则
if (mapping.transformRule) {
transformedValue = this.applyTransformRule(sourceValue, mapping.transformRule)
}
// 应用验证规则
if (mapping.validationRule) {
const isValid = this.applyValidationRule(transformedValue, mapping.validationRule)
if (!isValid) {
console.warn(`字段 ${sourceField} 验证失败,跳过映射`)
continue
}
}
mappedData[mapping.targetField] = transformedValue
} else {
// 如果没有映射规则,保留原始字段名
mappedData[sourceField] = sourceValue
}
}
return mappedData
}
// 应用转换规则
private applyTransformRule(value: any, rule: string): any {
switch (rule) {
case "prefix_dy_":
return `dy_${value}`
case "prefix_xhs_":
return `xhs_${value}`
case "lowercase":
return typeof value === "string" ? value.toLowerCase() : value
case "uppercase":
return typeof value === "string" ? value.toUpperCase() : value
case "trim":
return typeof value === "string" ? value.trim() : value
default:
return value
}
}
// 应用验证规则
private applyValidationRule(value: any, rule: string): boolean {
switch (rule) {
case "required":
return value !== null && value !== undefined && value !== ""
case "email":
return typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
case "phone":
return typeof value === "string" && /^1[3-9]\d{9}$/.test(value)
default:
return true
}
}
// 身份识别与关联
private async identifyOrCreateUser(mappedData: Record<string, any>): Promise<string> {
// 查找匹配的用户身份
const matches = await this.identityService.findMatchingIdentity(mappedData)
if (matches.length > 0 && matches[0].confidence > 0.8) {
// 找到高置信度匹配使用现有用户ID
const bestMatch = matches[0]
await this.identityService.updateIdentity(bestMatch.userId, mappedData)
return bestMatch.userId
} else {
// 创建新用户身份
return await this.identityService.createNewIdentity(mappedData)
}
}
// 构建统一用户数据结构
private async buildUnifiedUserData(
userId: string,
request: IngestionRequest,
mappedData: Record<string, any>,
): Promise<ProcessedUserData> {
// 获取现有用户数据(模拟)
const existingUsers = await getCollectionData("users")
const existingUser = existingUsers.find((user: any) => user._id === userId)
const now = new Date().toISOString()
// 构建核心档案
const coreProfile: Record<string, any> = {}
Object.keys(DATA_DICTIONARY.coreFields).forEach((field) => {
if (mappedData[field]) {
coreProfile[field] = mappedData[field]
} else if (existingUser?.core_profile?.[field]) {
coreProfile[field] = existingUser.core_profile[field]
}
})
// 构建统一标签
const unifiedTags = this.generateUnifiedTags(mappedData, request.source)
// 构建统一属性
const unifiedAttributes = this.generateUnifiedAttributes(mappedData, request.source)
// 构建源档案
const newSourceProfile = {
source: request.source,
sourceUserId: request.sourceUserId,
sourceRecordId: request.sourceRecordId,
originalData: request.originalData,
contentActivity: this.extractContentActivity(request.originalData, request.source),
interactionHistory: this.extractInteractionHistory(request.originalData, request.source),
ingestionTimestamp: request.timestamp || now,
}
const sourceProfiles = existingUser?.source_profiles || []
// 检查是否已存在相同来源的档案
const existingSourceIndex = sourceProfiles.findIndex(
(profile: any) =>
profile.source === request.source &&
(profile.sourceUserId === request.sourceUserId || profile.sourceRecordId === request.sourceRecordId),
)
if (existingSourceIndex >= 0) {
// 更新现有源档案
sourceProfiles[existingSourceIndex] = newSourceProfile
} else {
// 添加新的源档案
sourceProfiles.push(newSourceProfile)
}
// 生成AI洞察
const aiInsights = this.generateAIInsights(mappedData, request.source)
return {
userId,
coreProfile,
unifiedTags: [...new Set([...(existingUser?.unified_tags || []), ...unifiedTags])],
unifiedAttributes: { ...(existingUser?.unified_attributes || {}), ...unifiedAttributes },
sourceProfiles,
aiInsights: { ...(existingUser?.ai_insights || {}), ...aiInsights },
crmInfo: existingUser?.crm_info || {},
createdAt: existingUser?.createdAt || now,
updatedAt: now,
}
}
// 生成统一标签
private generateUnifiedTags(mappedData: Record<string, any>, source: string): string[] {
const tags: string[] = []
// 基于来源生成标签
if (source === "douyin") {
tags.push("抖音用户")
if (mappedData.followerCount > 10000) {
tags.push("抖音达人")
}
} else if (source === "xiaohongshu") {
tags.push("小红书用户")
} else if (source === "cunkebao_form") {
tags.push("表单用户")
} else if (source === "touchkebao_call") {
tags.push("电话咨询用户")
}
// 基于数据内容生成标签
if (mappedData.city) {
tags.push(`${mappedData.city}地区`)
}
if (mappedData.birthDate) {
const birthYear = new Date(mappedData.birthDate).getFullYear()
const currentYear = new Date().getFullYear()
const age = currentYear - birthYear
if (age >= 18 && age < 30) {
tags.push("年轻用户")
} else if (age >= 30 && age < 50) {
tags.push("中年用户")
}
}
return tags
}
// 生成统一属性
private generateUnifiedAttributes(mappedData: Record<string, any>, source: string): Record<string, any> {
const attributes: Record<string, any> = {}
// 基于来源设置属性
if (source === "douyin" && mappedData.followerCount) {
attributes.socialInfluence = mappedData.followerCount
}
if (source === "touchkebao_call") {
attributes.lastContactTime = mappedData.callTime || new Date().toISOString()
attributes.contactChannel = "phone"
}
if (source === "cunkebao_form") {
attributes.lastContactTime = new Date().toISOString()
attributes.contactChannel = "form"
}
// 设置活跃度
attributes.lastActiveDays = 0 // 刚接入的数据认为是活跃的
return attributes
}
// 提取内容活动
private extractContentActivity(originalData: Record<string, any>, source: string): any[] {
const activities: any[] = []
if (source === "douyin" && originalData.videos) {
originalData.videos.forEach((video: any) => {
activities.push({
contentId: video.id,
type: "video",
url: video.url,
text: video.description,
publishTime: video.createTime,
likes: video.likeCount,
comments: video.commentCount,
})
})
}
if (source === "xiaohongshu" && originalData.notes) {
originalData.notes.forEach((note: any) => {
activities.push({
contentId: note.id,
type: "note",
url: note.url,
text: note.content,
publishTime: note.createTime,
likes: note.likeCount,
comments: note.commentCount,
})
})
}
return activities
}
// 提取互动历史
private extractInteractionHistory(originalData: Record<string, any>, source: string): any[] {
const interactions: any[] = []
if (originalData.interactions) {
originalData.interactions.forEach((interaction: any) => {
interactions.push({
type: interaction.type,
targetId: interaction.targetId,
content: interaction.content,
time: interaction.time,
})
})
}
return interactions
}
// 生成AI洞察
private generateAIInsights(mappedData: Record<string, any>, source: string): Record<string, any> {
const insights: Record<string, any> = {}
// 基于数据生成人设标签
const personaTags: string[] = []
if (source === "douyin") {
personaTags.push("视频爱好者")
}
if (source === "xiaohongshu") {
personaTags.push("生活分享者")
}
if (mappedData.city === "北京") {
personaTags.push("一线城市用户")
}
insights.personaTags = personaTags
insights.sentimentScore = 0.7 // 默认中性偏正面
insights.potentialNeeds = this.inferPotentialNeeds(mappedData, source)
return insights
}
// 推断潜在需求
private inferPotentialNeeds(mappedData: Record<string, any>, source: string): string[] {
const needs: string[] = []
if (source === "touchkebao_call") {
needs.push("产品咨询", "客服支持")
}
if (source === "cunkebao_form") {
needs.push("产品了解", "营销活动")
}
if (source === "douyin") {
needs.push("内容创作工具", "社交媒体管理")
}
return needs
}
// 数据质量检查
private async validateDataQuality(data: ProcessedUserData): Promise<void> {
const errors: string[] = []
// 检查必需字段
if (!data.userId) {
errors.push("缺少用户ID")
}
// 检查数据完整性
if (!data.coreProfile || Object.keys(data.coreProfile).length === 0) {
errors.push("核心档案为空")
}
// 检查源档案
if (!data.sourceProfiles || data.sourceProfiles.length === 0) {
errors.push("缺少源档案数据")
}
if (errors.length > 0) {
throw new Error(`数据质量检查失败: ${errors.join(", ")}`)
}
}
// 存储用户数据(模拟)
private async storeUserData(data: ProcessedUserData): Promise<void> {
// 在实际应用中这里会将数据存储到MongoDB
console.log("存储用户数据:", {
userId: data.userId,
coreProfileFields: Object.keys(data.coreProfile).length,
tagsCount: data.unifiedTags.length,
sourceProfilesCount: data.sourceProfiles.length,
})
}
// 批量处理数据接入
public async processBatchIngestion(requests: IngestionRequest[]): Promise<ProcessedUserData[]> {
const results: ProcessedUserData[] = []
const errors: Array<{ request: IngestionRequest; error: string }> = []
for (const request of requests) {
try {
const result = await this.processIngestionRequest(request)
results.push(result)
} catch (error) {
errors.push({
request,
error: (error as Error).message,
})
}
}
if (errors.length > 0) {
console.warn("批量处理中的错误:", errors)
}
return results
}
}

View File

@@ -0,0 +1,473 @@
// 智能搜索服务 - 实现亚秒级查询和AI增强搜索
import { getMindsDBConnector, type SearchRequest, type AIQueryRequest } from "@/lib/mindsdb-connector"
export interface SearchResult {
id: string
type: "user" | "traffic" | "insight"
title: string
description: string
tags: string[]
relevanceScore: number
updatedAt: string
metadata?: Record<string, any>
}
export interface SearchStats {
totalResults: number
queryTime: number
suggestions: string[]
filters: Record<string, any>
}
export interface SearchResponse {
results: SearchResult[]
stats: SearchStats
hasMore: boolean
}
export class IntelligentSearchService {
private mindsDB = getMindsDBConnector()
private searchHistory: string[] = []
private popularQueries: Map<string, number> = new Map()
// 智能搜索主入口
async search(
query: string,
type: "user" | "traffic" | "all" = "all",
options: {
limit?: number
offset?: number
filters?: Record<string, any>
useAI?: boolean
includeInsights?: boolean
} = {},
): Promise<SearchResponse> {
const startTime = Date.now()
try {
// 记录搜索历史
this.addToSearchHistory(query)
// 如果启用AI增强搜索
if (options.useAI) {
return await this.aiEnhancedSearch(query, type, options)
}
// 标准搜索
const searchRequest: SearchRequest = {
keyword: query,
type,
filters: options.filters,
limit: options.limit || 50,
offset: options.offset || 0,
}
const rawResults = await this.mindsDB.intelligentSearch(searchRequest)
const results = this.formatSearchResults(rawResults)
// 如果需要包含AI洞察
if (options.includeInsights) {
const insights = await this.generateSearchInsights(query, results)
results.push(...insights)
}
const queryTime = Date.now() - startTime
return {
results,
stats: {
totalResults: results.length,
queryTime,
suggestions: await this.generateSuggestions(query),
filters: this.extractAvailableFilters(results),
},
hasMore: results.length === (options.limit || 50),
}
} catch (error) {
console.error("搜索失败:", error)
throw error
}
}
// AI增强搜索
private async aiEnhancedSearch(
query: string,
type: "user" | "traffic" | "all",
options: any,
): Promise<SearchResponse> {
const startTime = Date.now()
// 使用AI理解查询意图
const aiRequest: AIQueryRequest = {
query: `分析这个搜索查询的意图并提供相关的搜索建议: "${query}"`,
model: "gpt4",
useCache: true,
}
const aiAnalysis = await this.mindsDB.aiQuery(aiRequest)
// 基于AI分析结果优化搜索参数
const enhancedSearchRequest: SearchRequest = {
keyword: query,
type,
filters: {
...options.filters,
...this.extractFiltersFromAI(aiAnalysis),
},
limit: options.limit || 50,
offset: options.offset || 0,
}
const rawResults = await this.mindsDB.intelligentSearch(enhancedSearchRequest)
const results = this.formatSearchResults(rawResults)
// AI生成的相关洞察
const aiInsights = await this.generateAIInsights(query, results)
results.push(...aiInsights)
const queryTime = Date.now() - startTime
return {
results,
stats: {
totalResults: results.length,
queryTime,
suggestions: await this.generateAISuggestions(query, aiAnalysis),
filters: this.extractAvailableFilters(results),
},
hasMore: results.length === (options.limit || 50),
}
}
// 格式化搜索结果
private formatSearchResults(rawResults: any[]): SearchResult[] {
return rawResults.map((result) => ({
id: result.id || result.user_id || result.keyword_id,
type: result.type || "user",
title: result.title || result.username || result.keyword,
description: result.description || this.generateDescription(result),
tags: this.parseTags(result.tags),
relevanceScore: result.relevance_score || 0,
updatedAt: result.updated_at || result.last_active || result.last_updated,
metadata: {
...result,
searchType: result.type,
},
}))
}
// 生成描述
private generateDescription(result: any): string {
if (result.type === "user") {
return `${result.phone || ""} | ${result.email || ""} | RFM: ${result.rfm_score || "N/A"}`
} else if (result.type === "traffic") {
return `搜索量: ${result.search_volume || "N/A"} | 竞争度: ${result.competition || "N/A"} | CPC: ${result.cpc || "N/A"}`
}
return result.description || ""
}
// 解析标签
private parseTags(tags: any): string[] {
if (typeof tags === "string") {
try {
return JSON.parse(tags)
} catch {
return tags.split(",").map((tag) => tag.trim())
}
}
return Array.isArray(tags) ? tags : []
}
// 生成搜索建议
private async generateSuggestions(query: string): Promise<string[]> {
const suggestions: string[] = []
// 基于搜索历史的建议
const historySuggestions = this.searchHistory
.filter((h) => h.toLowerCase().includes(query.toLowerCase()) && h !== query)
.slice(0, 3)
suggestions.push(...historySuggestions)
// 基于热门查询的建议
const popularSuggestions = Array.from(this.popularQueries.entries())
.sort((a, b) => b[1] - a[1])
.map(([q]) => q)
.filter((q) => q.toLowerCase().includes(query.toLowerCase()) && q !== query)
.slice(0, 3)
suggestions.push(...popularSuggestions)
// 智能补全建议
const completionSuggestions = await this.generateCompletionSuggestions(query)
suggestions.push(...completionSuggestions)
return [...new Set(suggestions)].slice(0, 8)
}
// 生成AI建议
private async generateAISuggestions(query: string, aiAnalysis: any): Promise<string[]> {
try {
const aiRequest: AIQueryRequest = {
query: `基于查询"${query}"和分析结果生成5个相关的搜索建议`,
model: "gpt4",
useCache: true,
}
const result = await this.mindsDB.aiQuery(aiRequest)
return this.parseAISuggestions(result)
} catch (error) {
console.error("生成AI建议失败:", error)
return []
}
}
// 生成补全建议
private async generateCompletionSuggestions(query: string): Promise<string[]> {
// 这里可以集成更复杂的自动补全逻辑
const commonSuffixes = ["分析", "统计", "趋势", "预测", "报告", "用户", "流量", "关键词", "转化", "留存"]
return commonSuffixes
.map((suffix) => `${query} ${suffix}`)
.filter((suggestion) => suggestion.length <= 50)
.slice(0, 3)
}
// 解析AI建议
private parseAISuggestions(aiResult: any): string[] {
try {
// 假设AI返回的是建议列表
if (aiResult.suggestions && Array.isArray(aiResult.suggestions)) {
return aiResult.suggestions
}
// 如果是文本格式,尝试解析
if (typeof aiResult === "string") {
const lines = aiResult.split("\n")
return lines
.filter((line) => line.trim().length > 0)
.map((line) => line.replace(/^\d+\.\s*/, "").trim())
.slice(0, 5)
}
return []
} catch (error) {
console.error("解析AI建议失败:", error)
return []
}
}
// 从AI分析中提取过滤器
private extractFiltersFromAI(aiAnalysis: any): Record<string, any> {
const filters: Record<string, any> = {}
try {
if (aiAnalysis.filters) {
Object.assign(filters, aiAnalysis.filters)
}
// 基于AI分析结果添加智能过滤器
if (aiAnalysis.intent === "high_value_users") {
filters.rfm_score = { $gte: 80 }
}
if (aiAnalysis.intent === "recent_activity") {
filters.last_active = { $gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }
}
} catch (error) {
console.error("提取AI过滤器失败:", error)
}
return filters
}
// 生成搜索洞察
private async generateSearchInsights(query: string, results: SearchResult[]): Promise<SearchResult[]> {
const insights: SearchResult[] = []
try {
// 用户相关洞察
const userResults = results.filter((r) => r.type === "user")
if (userResults.length > 0) {
const userInsight = await this.generateUserInsight(query, userResults)
if (userInsight) insights.push(userInsight)
}
// 流量相关洞察
const trafficResults = results.filter((r) => r.type === "traffic")
if (trafficResults.length > 0) {
const trafficInsight = await this.generateTrafficInsight(query, trafficResults)
if (trafficInsight) insights.push(trafficInsight)
}
} catch (error) {
console.error("生成搜索洞察失败:", error)
}
return insights
}
// 生成AI洞察
private async generateAIInsights(query: string, results: SearchResult[]): Promise<SearchResult[]> {
const insights: SearchResult[] = []
try {
const aiRequest: AIQueryRequest = {
query: `基于搜索查询"${query}"和${results.length}个结果生成3个关键业务洞察`,
model: "gpt4",
useCache: true,
}
const aiResult = await this.mindsDB.aiQuery(aiRequest)
const aiInsights = this.parseAIInsights(aiResult)
insights.push(...aiInsights)
} catch (error) {
console.error("生成AI洞察失败:", error)
}
return insights
}
// 解析AI洞察
private parseAIInsights(aiResult: any): SearchResult[] {
const insights: SearchResult[] = []
try {
if (aiResult.insights && Array.isArray(aiResult.insights)) {
aiResult.insights.forEach((insight: any, index: number) => {
insights.push({
id: `ai_insight_${Date.now()}_${index}`,
type: "insight",
title: insight.title || `AI洞察 ${index + 1}`,
description: insight.description || insight.content,
tags: ["AI洞察", "智能分析"],
relevanceScore: insight.confidence || 0.8,
updatedAt: new Date().toISOString(),
metadata: {
source: "ai",
confidence: insight.confidence,
type: "insight",
},
})
})
}
} catch (error) {
console.error("解析AI洞察失败:", error)
}
return insights
}
// 生成用户洞察
private async generateUserInsight(query: string, userResults: SearchResult[]): Promise<SearchResult | null> {
try {
const totalUsers = userResults.length
const avgRelevance = userResults.reduce((sum, r) => sum + r.relevanceScore, 0) / totalUsers
return {
id: `user_insight_${Date.now()}`,
type: "insight",
title: "用户搜索洞察",
description: `找到 ${totalUsers} 个相关用户,平均相关度 ${avgRelevance.toFixed(2)}`,
tags: ["用户分析", "搜索洞察"],
relevanceScore: 0.9,
updatedAt: new Date().toISOString(),
metadata: {
totalUsers,
avgRelevance,
type: "user_insight",
},
}
} catch (error) {
console.error("生成用户洞察失败:", error)
return null
}
}
// 生成流量洞察
private async generateTrafficInsight(query: string, trafficResults: SearchResult[]): Promise<SearchResult | null> {
try {
const totalKeywords = trafficResults.length
const avgRelevance = trafficResults.reduce((sum, r) => sum + r.relevanceScore, 0) / totalKeywords
return {
id: `traffic_insight_${Date.now()}`,
type: "insight",
title: "流量关键词洞察",
description: `找到 ${totalKeywords} 个相关关键词,平均相关度 ${avgRelevance.toFixed(2)}`,
tags: ["流量分析", "关键词洞察"],
relevanceScore: 0.9,
updatedAt: new Date().toISOString(),
metadata: {
totalKeywords,
avgRelevance,
type: "traffic_insight",
},
}
} catch (error) {
console.error("生成流量洞察失败:", error)
return null
}
}
// 提取可用过滤器
private extractAvailableFilters(results: SearchResult[]): Record<string, any> {
const filters: Record<string, any> = {}
// 提取类型过滤器
const types = [...new Set(results.map((r) => r.type))]
if (types.length > 1) {
filters.type = types
}
// 提取标签过滤器
const allTags = results.flatMap((r) => r.tags)
const uniqueTags = [...new Set(allTags)]
if (uniqueTags.length > 0) {
filters.tags = uniqueTags.slice(0, 20) // 限制标签数量
}
return filters
}
// 添加到搜索历史
private addToSearchHistory(query: string): void {
if (query.trim().length === 0) return
// 更新搜索历史
this.searchHistory.unshift(query)
this.searchHistory = [...new Set(this.searchHistory)].slice(0, 100) // 保留最近100个唯一查询
// 更新热门查询统计
const count = this.popularQueries.get(query) || 0
this.popularQueries.set(query, count + 1)
}
// 获取搜索统计
getSearchStats(): any {
return {
totalSearches: this.searchHistory.length,
uniqueQueries: new Set(this.searchHistory).size,
popularQueries: Array.from(this.popularQueries.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([query, count]) => ({ query, count })),
}
}
// 清理搜索历史
clearSearchHistory(): void {
this.searchHistory = []
this.popularQueries.clear()
}
}
// 单例模式
let searchServiceInstance: IntelligentSearchService | null = null
export function getIntelligentSearchService(): IntelligentSearchService {
if (!searchServiceInstance) {
searchServiceInstance = new IntelligentSearchService()
}
return searchServiceInstance
}

View File

@@ -2,10 +2,6 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
body {
font-family: Arial, Helvetica, sans-serif;
}
@layer utilities { @layer utilities {
.text-balance { .text-balance {
text-wrap: balance; text-wrap: balance;

View File

@@ -1,39 +1,211 @@
# 开发文档 # 卡若数据资产中台开发文档
## 2023年7月20日 更新 - 数据中台与AI融合及界面优化 ## 项目概述
基于五行属性的AI驱动数据资产中台实现多源数据整合、智能用户画像、AI分析和数据资产管理。
### 1. 数据库连接方式调整 (模拟 MongoDB) ## 五行架构设计
**背景:** 根据产品需求文档 (PRD) [^3],项目数据库选型为文档数据库 (如 MongoDB)。为在前端模拟环境中更好地体现这一架构,并为未来真实的后端集成做准备,我们调整了数据库连接的模拟方式。 ### 金(概览)- 智能概览模块
- **核心功能**: 数据总览、实时监控、智能搜索
- **技术实现**: React + TypeScript + 实时数据流
- **页面结构**:
- `/` - AI智能概览主页
- `/overview/dashboard` - 数据总览仪表板
- `/overview/monitoring` - 实时系统监控
- `/overview/search` - 智能搜索引擎
**调整内容:** ### 水(数据对接)- 数据流转模块
* **文件重命名:** 将 `lib/db-connector.ts` 重命名为 `lib/mongodb-mock-connector.ts` - **核心功能**: 数据源管理、数据接入、数据处理、质量监控
* **数据结构模拟:** `mongodb-mock-connector.ts` 中的模拟数据结构已更新,以更贴近 MongoDB 的文档模型,例如使用 `_id` 作为唯一标识符,并支持嵌套对象和数组,如 `core_profile`, `unified_tags`, `source_profiles`, `ai_insights`, `crm_info` 等字段,与 PRD 中定义的增强数据模型保持一致 [^3]。 - **技术实现**: ETL流程 + API集成 + 数据质量监控
* **API 路由更新:** `app/api/database-structure/route.ts` 已更新,以导入并使用新的 `lib/mongodb-mock-connector.ts`,确保数据中台的数据库结构查看器能够展示模拟的 MongoDB 集合信息。 - **页面结构**:
- `/data-integration` - 数据对接总览
- `/data-integration/sources` - 数据源管理
- `/data-integration/ingestion` - 数据接入配置
- `/data-integration/processing` - 数据处理流程
- `/data-integration/quality` - 数据质量监控
**架构影响:** ### 木(用户画像)- 用户成长模块
* **前端模拟:** 当前的数据库操作仍为前端模拟,不涉及真实的后端连接。 - **核心功能**: 用户管理、画像分析、标签管理、用户分群
* **后端准备:** 此调整为未来后端使用 MongoDB 提供了清晰的数据模型和接口模拟参考。在实际后端开发中,需要使用 MongoDB 驱动程序(如 `mongodb``mongoose`)来实现真实的数据库操作。 - **技术实现**: AI标签引擎 + RFM模型 + 用户分群算法
- **页面结构**:
- `/user-portrait` - 用户画像总览
- `/user-portrait/management` - 用户管理
- `/user-portrait/analysis` - 画像分析
- `/user-portrait/tags` - 标签管理
- `/user-portrait/segmentation` - 用户分群
### 2. 界面优化与功能增强 ### AI分析- 智能分析模块
- **核心功能**: 智能分析、模型管理、预测分析、AI助手
- **技术实现**: 机器学习模型 + 预测算法 + AI对话引擎
- **页面结构**:
- `/ai-analysis` - AI分析总览
- `/ai-analysis/intelligent` - 智能分析任务
- `/ai-analysis/models` - AI模型管理
- `/ai-analysis/predictions` - 预测分析
- `/ai-analysis/assistant` - AI智能助手
**目标:** 提升用户体验,确保关键功能按钮可用,并移除不必要的元素。 ### 土(数据资产)- 资产沉淀模块
- **核心功能**: 资产分类、资产目录、资产归档、价值评估
- **技术实现**: 数据血缘追踪 + 资产价值模型 + 归档策略
- **页面结构**:
- `/data-assets` - 数据资产总览
- `/data-assets/classification` - 资产分类管理
- `/data-assets/catalog` - 资产目录浏览
- `/data-assets/archive` - 资产归档管理
- `/data-assets/valuation` - 资产价值评估
**优化内容:** ## 技术架构
* **侧边栏 (`app/components/Sidebar.tsx`)** ### 前端技术栈
* **“设备管理”菜单提升:** 根据 `执行路径.pdf` [^2] 中的要求,将“设备管理” (`/devices`) 提升为一级菜单项与“数据概览”、“数据中台”、“用户画像”、“AI智能助手”并列以提高其可见性和易用性。 - **框架**: Next.js 14 (App Router)
* **导航链接可用性:** 确保所有导航链接都指向正确的页面路径,并在模拟环境中保持功能可用。 - **语言**: TypeScript
* **数据中台 (`app/data-platform/page.tsx`)** - **UI库**: shadcn/ui + Tailwind CSS
* **AI 模型标签页集成:** 在数据中台页面中新增了“AI 模型”标签页,并在此标签页中完整集成了 `components/data-integration/ai-analysis-tools.tsx` 组件。这使得用户可以直接在数据中台管理 AI 分析任务、AI 模型和分析模板。 - **状态管理**: React Hooks + Context
* **数据源“连接到AI”按钮** 在“数据源管理”部分为每个数据源增加了“连接到AI”按钮。该按钮根据数据源的 `aiReady` 状态启用或禁用点击后会触发一个提示信息Toast模拟数据源与 AI 平台的集成入口。 - **图表库**: Recharts
* **AI 分析工具 (`components/data-integration/ai-analysis-tools.tsx`)** - **图标库**: Lucide React
* **功能按钮可用性:** 确保“创建分析任务”、“添加模型”、“编辑”、“测试”、“查看详情”、“复制”和“下载报告”等所有按钮均可点击并触发相应的模拟操作或对话框。
* **代码清理:** 移除了组件中可能存在的冗余代码或未使用的导入,确保代码的精简。
### 3. 代码清理 ### 后端技术栈
- **API**: Next.js API Routes
- **数据库**: MongoDB (文档数据库)
- **缓存**: Redis
- **搜索引擎**: Elasticsearch
- **AI引擎**: 集成多种AI模型
* **无用代码移除:** 审查并移除了在本次优化过程中发现的不再使用的变量、函数、组件引用或样式,以减少代码冗余,提高可读性和维护性。 ### 数据流架构
* **链接和内容显示:** 确保所有显示的链接和内容都是当前功能所需且可用的,避免显示不相关或无效的信息。 \`\`\`
数据源 → 数据接入 → 数据处理 → 数据存储 → AI分析 → 用户界面
↓ ↓ ↓ ↓ ↓ ↓
MySQL ETL服务 清洗转换 MongoDB AI模型 React组件
API接口 实时同步 标准化 缓存层 预测分析 智能搜索
\`\`\`
**本次开发完成百分比:** 界面优化与数据库连接调整 100% ## 导航结构修复
### 问题解决
1. **导航状态管理**: 使用 `usePathname` 准确判断当前路由
2. **展开状态控制**: 独立的展开状态管理,避免冲突
3. **链接跳转**: 确保所有链接都有对应的页面文件
4. **样式一致性**: 统一的主题色彩和交互效果
### 导航逻辑
\`\`\`typescript
// 判断激活状态
const isActiveSection = (item: any) => {
if (pathname === item.href) return true
if (item.children) {
return item.children.some((child: any) => pathname === child.href)
}
return false
}
// 展开状态管理
const [expandedSections, setExpandedSections] = useState({
"overview": true, // 概览默认展开
"data-flow": false,
"user-portrait": false,
"ai-analysis": false,
"data-assets": false,
})
\`\`\`
## 开发规范
### 文件命名规范
- 页面文件: `page.tsx`
- 组件文件: `PascalCase.tsx`
- 工具文件: `kebab-case.ts`
- 样式文件: `globals.css`
### 代码组织规范
- 每个文件代码行数控制在200行以内
- 组件拆分遵循单一职责原则
- 使用TypeScript严格模式
- 统一的错误处理和加载状态
### Git提交规范
- feat: 新功能
- fix: 修复问题
- docs: 文档更新
- style: 样式调整
- refactor: 代码重构
## 部署配置
### 环境变量
\`\`\`env
# 数据库配置
DB_HOST=10.88.182.62
DB_PORT=3305
DB_USER=root
DB_PASSWORD=zhiqun1984
# API配置
NEXT_PUBLIC_API_BASE_URL=http://localhost:3000/api
# AI服务配置
AI_SERVICE_URL=http://ai-service:8080
AI_API_KEY=your_ai_api_key
\`\`\`
### 构建部署
\`\`\`bash
# 安装依赖
npm install
# 开发环境
npm run dev
# 生产构建
npm run build
# 启动生产服务
npm start
\`\`\`
## 更新日志
### v1.0.0 (2024-01-21)
- ✅ 完成五行架构导航重构
- ✅ 修复导航板块点击失控问题
- ✅ 实现智能搜索功能
- ✅ 创建概览模块页面
- ✅ 创建数据对接模块页面
- 🔄 进行中: 用户画像模块开发
- 📋 待开发: AI分析模块
- 📋 待开发: 数据资产模块
### 下一步开发计划
1. 完成用户画像模块页面开发
2. 实现AI分析模块功能
3. 构建数据资产管理系统
4. 优化系统性能和用户体验
5. 完善文档和测试用例
\`\`\`
**本次开发完成内容:**
**五行导航架构重构** - 按照金、水、木、火、土五行属性重新组织导航结构:
- 金(概览):智能概览、数据总览、实时监控、智能搜索
- 水(数据对接):数据源管理、数据接入、数据处理、质量监控
- 木(用户画像):用户管理、画像分析、标签管理、用户分群
-AI分析智能分析、模型管理、预测分析、AI助手
- 土(数据资产):资产分类、资产目录、资产归档、价值评估
**导航失控问题修复** - 解决了导航板块点击后失控的问题:
- 使用 `usePathname` 准确判断当前路由状态
- 独立管理各模块的展开状态,避免状态冲突
- 优化点击事件处理,防止事件冒泡
- 确保所有链接都有对应的页面文件
**页面文件创建** - 创建了对应的页面文件确保链接正常:
- 概览模块:数据总览、实时监控、智能搜索页面
- 数据对接模块:数据对接总览页面
- 完善的页面结构和交互逻辑
**导航逻辑优化** - 结合知识库需求优化导航结构:
- 五行架构说明和视觉标识
- 统一的主题色彩系统
- 流畅的交互动画效果
- 响应式设计适配
**完成百分比:** 五行导航架构重构 100%