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:
File diff suppressed because it is too large
Load Diff
31
app/api/ai-query/route.ts
Normal file
31
app/api/ai-query/route.ts
Normal 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
105
app/api/ingest/route.ts
Normal 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
65
app/api/search/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
24
app/api/system-status/route.ts
Normal file
24
app/api/system-status/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { X } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Database,
|
||||
LayoutDashboard,
|
||||
Target,
|
||||
X,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
UserCheck,
|
||||
Layers,
|
||||
Tag,
|
||||
BrainCircuit,
|
||||
} from "lucide-react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import Link from "next/link"
|
||||
|
||||
interface MobileSidebarProps {
|
||||
isOpen: boolean
|
||||
@@ -25,219 +10,43 @@ interface MobileSidebarProps {
|
||||
}
|
||||
|
||||
export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
|
||||
const pathname = usePathname()
|
||||
const [expandedSections, setExpandedSections] = useState({
|
||||
"user-portrait": false, // 用户画像默认不展开
|
||||
})
|
||||
|
||||
const toggleSection = (section: string) => {
|
||||
setExpandedSections((prev) => ({
|
||||
...prev,
|
||||
[section]: !prev[section],
|
||||
}))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = "hidden"
|
||||
} else {
|
||||
document.body.style.overflow = "unset"
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = "unset"
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: "数据概览",
|
||||
href: "/",
|
||||
icon: <LayoutDashboard className="h-4 w-4" />,
|
||||
primary: true,
|
||||
description: "平台整体数据分析",
|
||||
},
|
||||
{
|
||||
title: "数据中台",
|
||||
href: "/data-platform",
|
||||
icon: <Database className="h-4 w-4" />,
|
||||
primary: true,
|
||||
tag: "核心",
|
||||
description: "多源数据整合中心",
|
||||
},
|
||||
{
|
||||
title: "用户画像", // 整合用户池功能
|
||||
href: "/user-portrait",
|
||||
icon: <Target className="h-4 w-4" />,
|
||||
primary: true,
|
||||
expandable: true,
|
||||
section: "user-portrait",
|
||||
description: "用户数据管理与画像分析",
|
||||
children: [
|
||||
{
|
||||
title: "用户管理", // 原用户池的用户管理
|
||||
href: "/user-portrait/management",
|
||||
icon: <UserCheck className="h-3 w-3" />,
|
||||
description: "IMEI、手机号管理",
|
||||
},
|
||||
{
|
||||
title: "用户分群", // 原用户池的用户分群
|
||||
href: "/user-portrait/segmentation",
|
||||
icon: <Layers className="h-3 w-3" />,
|
||||
description: "智能用户分群",
|
||||
},
|
||||
{
|
||||
title: "标签管理", // 原用户画像的标签管理
|
||||
href: "/user-portrait/tags",
|
||||
icon: <Tag className="h-3 w-3" />,
|
||||
description: "用户标签体系",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "AI智能助手", // 新增AI智能助手
|
||||
href: "/ai-assistant",
|
||||
icon: <BrainCircuit className="h-4 w-4" />,
|
||||
primary: true,
|
||||
description: "AI数据分析与营销策略",
|
||||
},
|
||||
]
|
||||
|
||||
const getTagColor = (tag: string) => {
|
||||
switch (tag) {
|
||||
case "核心":
|
||||
return "bg-blue-100 text-blue-700 border-blue-200"
|
||||
default:
|
||||
return "bg-gray-100 text-gray-700 border-gray-200"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 背景遮罩 */}
|
||||
<div
|
||||
className={cn(
|
||||
"fixed inset-0 bg-black/20 backdrop-blur-sm z-40 transition-opacity duration-300",
|
||||
isOpen ? "opacity-100" : "opacity-0 pointer-events-none",
|
||||
)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"fixed inset-0 z-40 transition-transform duration-300 md:hidden",
|
||||
isOpen ? "translate-x-0" : "translate-x-full",
|
||||
)}
|
||||
aria-hidden={!isOpen}
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={onClose} />
|
||||
|
||||
{/* 侧边栏 */}
|
||||
<div
|
||||
className={cn(
|
||||
"fixed left-0 top-0 h-full w-80 glass-nav safe-area-top safe-area-left z-50 transition-transform duration-300 ease-out",
|
||||
isOpen ? "translate-x-0" : "-translate-x-full",
|
||||
)}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-white/20">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-6 h-6 rounded-lg glass-light flex items-center justify-center">
|
||||
<Database className="h-4 w-4 text-blue-600" />
|
||||
</div>
|
||||
<h1 className="text-sm font-semibold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||
卡若数据资产中台
|
||||
</h1>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" onClick={onClose} className="glass-light rounded-lg h-8 w-8">
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
{/* Drawer */}
|
||||
<aside className="relative ml-auto h-full w-64 bg-white shadow-lg">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b">
|
||||
<h2 className="text-lg font-semibold">导航</h2>
|
||||
<button onClick={onClose} aria-label="Close menu" className="rounded p-1 hover:bg-gray-100">
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 导航菜单 */}
|
||||
<div className="flex-1 overflow-y-auto py-2">
|
||||
<nav className="space-y-1 px-3">
|
||||
{navItems.map((item) => (
|
||||
<div key={item.href}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center px-3 py-2 text-xs font-medium rounded-lg transition-all duration-300 group",
|
||||
pathname === item.href || (item.children && pathname.startsWith(item.href))
|
||||
? "glass-heavy text-blue-700 shadow-glass"
|
||||
: "glass-light text-gray-700 hover:glass-heavy hover:text-blue-600",
|
||||
)}
|
||||
>
|
||||
<Link href={item.href} onClick={onClose} className="flex items-center flex-1 min-w-0">
|
||||
<div className="transition-colors duration-300 flex-shrink-0">{item.icon}</div>
|
||||
<div className="flex-1 ml-2 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-medium truncate">{item.title}</span>
|
||||
</div>
|
||||
{item.tag && (
|
||||
<Badge
|
||||
className={cn("text-xs px-1.5 py-0.5 rounded ml-1 flex-shrink-0", getTagColor(item.tag))}
|
||||
>
|
||||
{item.tag}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 truncate mt-0.5">{item.description}</p>
|
||||
</div>
|
||||
</Link>
|
||||
{item.expandable && (
|
||||
<button
|
||||
onClick={() => toggleSection(item.section!)}
|
||||
className="ml-1 p-1 hover:bg-white/20 rounded transition-colors duration-200 flex-shrink-0"
|
||||
>
|
||||
{expandedSections[item.section!] ? (
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 子菜单 */}
|
||||
{item.children && expandedSections[item.section!] && (
|
||||
<div className="ml-4 mt-1 space-y-1">
|
||||
{item.children.map((subItem) => (
|
||||
<Link
|
||||
key={subItem.href}
|
||||
href={subItem.href}
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
"flex items-center px-2 py-1.5 text-xs rounded transition-all duration-300",
|
||||
pathname === subItem.href
|
||||
? "glass-light text-blue-600 shadow-glass-sm"
|
||||
: "text-gray-600 hover:glass-light hover:text-blue-500",
|
||||
)}
|
||||
>
|
||||
<div className="mr-2 text-gray-400 transition-colors duration-300 flex-shrink-0">
|
||||
{subItem.icon}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium truncate">{subItem.title}</p>
|
||||
<p className="text-xs text-gray-500 truncate">{subItem.description}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* 底部信息 */}
|
||||
<div className="border-t border-white/20 p-3">
|
||||
<div className="glass-light rounded-lg p-2">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-600">数据同步状态</span>
|
||||
<div className="flex items-center">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full mr-1"></div>
|
||||
<span className="text-green-600">实时</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs mt-1">
|
||||
<span className="text-gray-600">用户池总数</span>
|
||||
<span className="text-gray-500">125,678</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
{/* Navigation links — keep in sync with Sidebar.tsx */}
|
||||
<nav className="flex flex-col gap-1 p-4">
|
||||
<Link href="/" className="rounded px-3 py-2 text-sm hover:bg-gray-100" onClick={onClose}>
|
||||
数据概览
|
||||
</Link>
|
||||
<Link href="/data-platform" className="rounded px-3 py-2 text-sm hover:bg-gray-100" onClick={onClose}>
|
||||
数据中台
|
||||
</Link>
|
||||
<Link href="/user-portrait" className="rounded px-3 py-2 text-sm hover:bg-gray-100" onClick={onClose}>
|
||||
用户画像
|
||||
</Link>
|
||||
<Link href="/ai-assistant" className="rounded px-3 py-2 text-sm hover:bg-gray-100" onClick={onClose}>
|
||||
AI 智能助手
|
||||
</Link>
|
||||
</nav>
|
||||
</aside>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,124 +4,63 @@ import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Database,
|
||||
LayoutDashboard,
|
||||
Settings,
|
||||
Target,
|
||||
ChevronLeft,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Tag,
|
||||
UserCheck,
|
||||
Layers,
|
||||
BrainCircuit,
|
||||
Smartphone,
|
||||
} from "lucide-react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { LayoutDashboard, Database, Users, BrainCircuit, Settings, ChevronLeft } from "lucide-react"
|
||||
|
||||
export default function Sidebar() {
|
||||
const pathname = usePathname()
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
const [expandedSections, setExpandedSections] = useState({
|
||||
"user-portrait": true, // 用户画像默认展开
|
||||
})
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setExpanded(!expanded)
|
||||
}
|
||||
|
||||
const toggleSection = (section: string) => {
|
||||
setExpandedSections((prev) => ({
|
||||
...prev,
|
||||
[section]: !prev[section],
|
||||
}))
|
||||
}
|
||||
|
||||
// 重新设计的导航结构 - 遵循新规则
|
||||
// 简化的导航结构
|
||||
const navItems = [
|
||||
{
|
||||
title: "数据概览",
|
||||
href: "/",
|
||||
icon: <LayoutDashboard className="h-5 w-5" />,
|
||||
primary: true,
|
||||
description: "平台整体数据分析与监控",
|
||||
},
|
||||
{
|
||||
title: "数据中台",
|
||||
href: "/data-platform",
|
||||
icon: <Database className="h-5 w-5" />,
|
||||
primary: true,
|
||||
tag: "核心",
|
||||
description: "多源数据整合与处理中心",
|
||||
},
|
||||
{
|
||||
title: "用户画像", // 整合用户池功能
|
||||
title: "用户画像",
|
||||
href: "/user-portrait",
|
||||
icon: <Target className="h-5 w-5" />,
|
||||
primary: true,
|
||||
expandable: true,
|
||||
section: "user-portrait",
|
||||
children: [
|
||||
{
|
||||
title: "用户管理", // 原用户池的用户管理
|
||||
href: "/user-portrait/management",
|
||||
icon: <UserCheck className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "用户分群", // 原用户池的用户分群
|
||||
href: "/user-portrait/segmentation",
|
||||
icon: <Layers className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "标签管理", // 原用户画像的标签管理
|
||||
href: "/user-portrait/tags",
|
||||
icon: <Tag className="h-4 w-4" />,
|
||||
},
|
||||
],
|
||||
icon: <Users className="h-5 w-5" />,
|
||||
description: "用户数据管理与画像分析",
|
||||
},
|
||||
{
|
||||
title: "设备管理", // 提升为一级菜单 [^2]
|
||||
href: "/devices",
|
||||
icon: <Smartphone className="h-5 w-5" />,
|
||||
primary: true,
|
||||
},
|
||||
{
|
||||
title: "AI智能助手", // 新增AI智能助手
|
||||
title: "AI智能助手",
|
||||
href: "/ai-assistant",
|
||||
icon: <BrainCircuit className="h-5 w-5" />,
|
||||
primary: true,
|
||||
description: "AI数据分析与营销策略",
|
||||
},
|
||||
]
|
||||
|
||||
const getTagColor = (tag: string) => {
|
||||
switch (tag) {
|
||||
case "核心":
|
||||
return "glass-light text-orange-700 border-orange-200"
|
||||
default:
|
||||
return "glass-light text-gray-700 border-gray-200"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col h-screen glass-nav m-4 transition-all duration-300 ease-in-out",
|
||||
"flex flex-col h-screen bg-white border-r border-gray-200 transition-all duration-300 ease-in-out",
|
||||
expanded ? "w-72" : "w-20",
|
||||
)}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center h-16 px-6 border-b border-white/20">
|
||||
<div className="flex items-center h-16 px-6 border-b border-gray-200">
|
||||
{expanded ? (
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 rounded-lg glass-light flex items-center justify-center">
|
||||
<div className="w-8 h-8 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||
<Database className="h-5 w-5 text-blue-600" />
|
||||
</div>
|
||||
<h1 className="text-lg font-semibold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||
卡若数据资产中台
|
||||
</h1>
|
||||
<h1 className="text-lg font-semibold text-gray-900">数据资产中台</h1>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mx-auto">
|
||||
<div className="w-8 h-8 rounded-lg glass-light flex items-center justify-center">
|
||||
<div className="w-8 h-8 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||
<Database className="h-5 w-5 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,86 +71,49 @@ export default function Sidebar() {
|
||||
<div className="flex-1 overflow-y-auto py-4">
|
||||
<nav className="space-y-2 px-4">
|
||||
{navItems.map((item) => (
|
||||
<div key={item.href}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all duration-300 group",
|
||||
pathname === item.href || (item.children && pathname.startsWith(item.href))
|
||||
? "glass-heavy text-blue-700 shadow-glass"
|
||||
: "glass-light text-gray-700 hover:glass-heavy hover:text-blue-600 hover:scale-105",
|
||||
)}
|
||||
>
|
||||
<Link href={item.href} className={cn("flex items-center flex-1", !expanded && "justify-center")}>
|
||||
<div className="transition-colors duration-300">{item.icon}</div>
|
||||
{expanded && (
|
||||
<div className="flex-1 flex items-center justify-between ml-3">
|
||||
<span className="font-medium">{item.title}</span>
|
||||
{item.tag && (
|
||||
<Badge className={cn("text-xs px-2 py-1 rounded-lg", getTagColor(item.tag))}>{item.tag}</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
{expanded && item.expandable && (
|
||||
<button
|
||||
onClick={() => toggleSection(item.section!)}
|
||||
className="ml-2 p-1 hover:bg-white/20 rounded-lg transition-colors duration-200"
|
||||
>
|
||||
{expandedSections[item.section!] ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 子菜单 */}
|
||||
{expanded && item.children && expandedSections[item.section!] && (
|
||||
<div className="ml-6 mt-2 space-y-1">
|
||||
{item.children.map((subItem) => (
|
||||
<Link
|
||||
key={subItem.href}
|
||||
href={subItem.href}
|
||||
className={cn(
|
||||
"flex items-center px-3 py-2 text-sm rounded-lg transition-all duration-300",
|
||||
pathname === subItem.href
|
||||
? "glass-light text-blue-600 shadow-glass-sm"
|
||||
: "text-gray-600 hover:glass-light hover:text-blue-500",
|
||||
)}
|
||||
>
|
||||
<div className="mr-3 text-gray-400 transition-colors duration-300">{subItem.icon}</div>
|
||||
<span>{subItem.title}</span>
|
||||
</Link>
|
||||
))}
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200 group",
|
||||
pathname === item.href
|
||||
? "bg-blue-50 text-blue-700 border border-blue-200"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-blue-600",
|
||||
)}
|
||||
>
|
||||
<div className="transition-colors duration-200">{item.icon}</div>
|
||||
{expanded && (
|
||||
<div className="flex-1 ml-3">
|
||||
<div className="font-medium">{item.title}</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">{item.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* 底部设置和折叠按钮 */}
|
||||
<div className="mt-auto border-t border-white/20">
|
||||
<div className="mt-auto border-t border-gray-200">
|
||||
<Link
|
||||
href="/settings"
|
||||
className={cn(
|
||||
"flex items-center px-4 py-2 mx-4 my-2 text-xs font-medium rounded-lg transition-all duration-300",
|
||||
"flex items-center px-4 py-3 mx-4 my-2 text-sm font-medium rounded-lg transition-all duration-200",
|
||||
pathname === "/settings"
|
||||
? "glass-heavy text-blue-700 shadow-glass"
|
||||
: "glass-light text-gray-600 hover:glass-heavy hover:text-blue-500",
|
||||
? "bg-blue-50 text-blue-700 border border-blue-200"
|
||||
: "text-gray-600 hover:bg-gray-50 hover:text-blue-600",
|
||||
)}
|
||||
>
|
||||
<div className={cn("transition-colors duration-300", !expanded && "mx-auto")}>
|
||||
<div className={cn("transition-colors duration-200", !expanded && "mx-auto")}>
|
||||
<Settings className="h-4 w-4" />
|
||||
</div>
|
||||
{expanded && <span className="ml-2">设置</span>}
|
||||
{expanded && <span className="ml-2">系统设置</span>}
|
||||
</Link>
|
||||
|
||||
<div className="p-4">
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="w-full flex items-center justify-center p-3 rounded-xl glass-button hover:scale-105 transition-all duration-300"
|
||||
className="w-full flex items-center justify-center p-3 rounded-lg bg-gray-50 hover:bg-gray-100 transition-all duration-200"
|
||||
>
|
||||
<ChevronLeft
|
||||
className={cn(
|
||||
|
||||
514
app/data-ingestion/page.tsx
Normal file
514
app/data-ingestion/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -3,552 +3,403 @@
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Database, Server, Link, Plus, FileText, RefreshCw } from "lucide-react"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import {
|
||||
Database,
|
||||
GitBranch,
|
||||
Zap,
|
||||
Shield,
|
||||
Activity,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
Clock,
|
||||
TrendingUp,
|
||||
Settings,
|
||||
Plus,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
export default function DataIntegrationPage() {
|
||||
const [activeTab, setActiveTab] = useState("data-sources")
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState("overview")
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-6 space-y-8">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">数据中台</h1>
|
||||
<p className="text-muted-foreground">管理数据源和API接口</p>
|
||||
</div>
|
||||
</div>
|
||||
const integrationStats = {
|
||||
totalSources: 12,
|
||||
activeSources: 10,
|
||||
dailyVolume: 2456789,
|
||||
successRate: 98.5,
|
||||
avgLatency: 156,
|
||||
qualityScore: 94.2,
|
||||
}
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full max-w-md grid-cols-2">
|
||||
<TabsTrigger value="data-sources">数据集成</TabsTrigger>
|
||||
<TabsTrigger value="api-management">API管理</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="data-sources" className="space-y-6">
|
||||
<DataSourcesTab setIsDialogOpen={setIsDialogOpen} />
|
||||
<div className="mt-4">
|
||||
<Button variant="outline" asChild>
|
||||
<a href="/database-structure">
|
||||
<Database className="mr-2 h-4 w-4" />
|
||||
查看数据库结构
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="api-management" className="space-y-6">
|
||||
<ApiManagementTab />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<AddDataSourceDialog isOpen={isDialogOpen} setIsOpen={setIsDialogOpen} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 数据源管理标签页
|
||||
function DataSourcesTab({ setIsDialogOpen }: { setIsDialogOpen: (open: boolean) => void }) {
|
||||
// 模拟数据源列表
|
||||
const dataSources = [
|
||||
{
|
||||
id: "1",
|
||||
name: "用户数据库",
|
||||
type: "MySQL",
|
||||
host: "db.example.com",
|
||||
id: 1,
|
||||
name: "MySQL主库",
|
||||
type: "database",
|
||||
status: "connected",
|
||||
lastSync: "2023-07-20 15:30",
|
||||
tables: 24,
|
||||
records: 156789,
|
||||
lastSync: "2分钟前",
|
||||
records: 1245678,
|
||||
quality: 98.5,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "订单系统",
|
||||
type: "PostgreSQL",
|
||||
host: "orders.example.com",
|
||||
id: 2,
|
||||
name: "触客宝API",
|
||||
type: "api",
|
||||
status: "connected",
|
||||
lastSync: "2023-07-19 12:45",
|
||||
tables: 18,
|
||||
records: 89456,
|
||||
lastSync: "5分钟前",
|
||||
records: 456789,
|
||||
quality: 96.2,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "内容库",
|
||||
type: "MongoDB",
|
||||
host: "content.example.com",
|
||||
id: 3,
|
||||
name: "抖音内容平台",
|
||||
type: "api",
|
||||
status: "warning",
|
||||
lastSync: "1小时前",
|
||||
records: 234567,
|
||||
quality: 89.3,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "小红书API",
|
||||
type: "api",
|
||||
status: "connected",
|
||||
lastSync: "10分钟前",
|
||||
records: 123456,
|
||||
quality: 95.8,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "微信视频号",
|
||||
type: "api",
|
||||
status: "connected",
|
||||
lastSync: "15分钟前",
|
||||
records: 98765,
|
||||
quality: 92.4,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "飞书妙记API",
|
||||
type: "api",
|
||||
status: "error",
|
||||
lastSync: "2023-07-15 09:20",
|
||||
tables: 12,
|
||||
lastSync: "2小时前",
|
||||
records: 45678,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "用户行为分析",
|
||||
type: "ClickHouse",
|
||||
host: "analytics.example.com",
|
||||
status: "connected",
|
||||
lastSync: "2023-07-20 10:15",
|
||||
tables: 8,
|
||||
records: 2345678,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "CRM系统",
|
||||
type: "Oracle",
|
||||
host: "crm.example.com",
|
||||
status: "pending",
|
||||
lastSync: "等待连接",
|
||||
tables: 0,
|
||||
records: 0,
|
||||
quality: 78.9,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-2xl font-bold">数据源管理</h2>
|
||||
<Button onClick={() => setIsDialogOpen(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
添加数据源
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>已连接的数据源</CardTitle>
|
||||
<CardDescription>管理和监控所有数据源连接</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>数据源名称</TableHead>
|
||||
<TableHead>类型</TableHead>
|
||||
<TableHead>主机地址</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>最后同步</TableHead>
|
||||
<TableHead>表数量</TableHead>
|
||||
<TableHead>记录数</TableHead>
|
||||
<TableHead className="text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{dataSources.map((source) => (
|
||||
<TableRow key={source.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center">
|
||||
<Database className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||
<span className="font-medium">{source.name}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{source.type}</TableCell>
|
||||
<TableCell>{source.host}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
className={
|
||||
source.status === "connected"
|
||||
? "bg-green-100 text-green-800"
|
||||
: source.status === "error"
|
||||
? "bg-red-100 text-red-800"
|
||||
: "bg-yellow-100 text-yellow-800"
|
||||
}
|
||||
>
|
||||
{source.status === "connected" ? "已连接" : source.status === "error" ? "错误" : "等待中"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{source.lastSync}</TableCell>
|
||||
<TableCell>{source.tables}</TableCell>
|
||||
<TableCell>{source.records.toLocaleString()}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Link className="h-4 w-4 mr-1" />
|
||||
查看
|
||||
</Button>
|
||||
<Button size="sm">
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
同步
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>数据源类型</CardTitle>
|
||||
<CardDescription>支持的数据库和数据源类型</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="flex flex-col items-center p-4 border rounded-lg">
|
||||
<Database className="h-8 w-8 mb-2 text-blue-500" />
|
||||
<span className="font-medium">MySQL</span>
|
||||
<span className="text-xs text-muted-foreground">关系型数据库</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-4 border rounded-lg">
|
||||
<Database className="h-8 w-8 mb-2 text-blue-500" />
|
||||
<span className="font-medium">PostgreSQL</span>
|
||||
<span className="text-xs text-muted-foreground">关系型数据库</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-4 border rounded-lg">
|
||||
<Database className="h-8 w-8 mb-2 text-green-500" />
|
||||
<span className="font-medium">MongoDB</span>
|
||||
<span className="text-xs text-muted-foreground">文档型数据库</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-4 border rounded-lg">
|
||||
<Database className="h-8 w-8 mb-2 text-yellow-500" />
|
||||
<span className="font-medium">ClickHouse</span>
|
||||
<span className="text-xs text-muted-foreground">列式数据库</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-4 border rounded-lg">
|
||||
<Database className="h-8 w-8 mb-2 text-red-500" />
|
||||
<span className="font-medium">Oracle</span>
|
||||
<span className="text-xs text-muted-foreground">关系型数据库</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-4 border rounded-lg">
|
||||
<Database className="h-8 w-8 mb-2 text-purple-500" />
|
||||
<span className="font-medium">SQL Server</span>
|
||||
<span className="text-xs text-muted-foreground">关系型数据库</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-4 border rounded-lg">
|
||||
<Server className="h-8 w-8 mb-2 text-gray-500" />
|
||||
<span className="font-medium">Redis</span>
|
||||
<span className="text-xs text-muted-foreground">键值存储</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-4 border rounded-lg">
|
||||
<FileText className="h-8 w-8 mb-2 text-gray-500" />
|
||||
<span className="font-medium">CSV/Excel</span>
|
||||
<span className="text-xs text-muted-foreground">文件导入</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// API管理标签页
|
||||
function ApiManagementTab() {
|
||||
// 模拟API接口数据
|
||||
const apiEndpoints = [
|
||||
const recentActivities = [
|
||||
{
|
||||
id: "1",
|
||||
name: "用户数据API",
|
||||
endpoint: "/api/users",
|
||||
method: "GET",
|
||||
category: "用户画像",
|
||||
status: "active",
|
||||
calls: 12567,
|
||||
lastCalled: "2023-07-20 16:45",
|
||||
id: 1,
|
||||
type: "sync_complete",
|
||||
message: "MySQL主库数据同步完成",
|
||||
time: "2分钟前",
|
||||
status: "success",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "用户标签API",
|
||||
endpoint: "/api/users/tags",
|
||||
method: "GET",
|
||||
category: "用户画像",
|
||||
status: "active",
|
||||
calls: 8945,
|
||||
lastCalled: "2023-07-20 15:30",
|
||||
id: 2,
|
||||
type: "quality_check",
|
||||
message: "触客宝API数据质量检查通过",
|
||||
time: "8分钟前",
|
||||
status: "success",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "流量池数据API",
|
||||
endpoint: "/api/traffic-pools",
|
||||
method: "GET",
|
||||
category: "流量池",
|
||||
status: "active",
|
||||
calls: 5678,
|
||||
lastCalled: "2023-07-20 14:20",
|
||||
id: 3,
|
||||
type: "sync_warning",
|
||||
message: "抖音内容平台同步延迟",
|
||||
time: "1小时前",
|
||||
status: "warning",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "AI分析API",
|
||||
endpoint: "/api/ai/analyze",
|
||||
method: "POST",
|
||||
category: "AI分析",
|
||||
status: "active",
|
||||
calls: 3456,
|
||||
lastCalled: "2023-07-20 13:15",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "数据同步API",
|
||||
endpoint: "/api/sync",
|
||||
method: "POST",
|
||||
category: "数据集成",
|
||||
status: "maintenance",
|
||||
calls: 2345,
|
||||
lastCalled: "2023-07-19 10:30",
|
||||
id: 4,
|
||||
type: "sync_error",
|
||||
message: "飞书妙记API连接失败",
|
||||
time: "2小时前",
|
||||
status: "error",
|
||||
},
|
||||
]
|
||||
|
||||
// 模拟API密钥数据
|
||||
const apiKeys = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Web应用",
|
||||
key: "sk_web_*************",
|
||||
created: "2023-05-15",
|
||||
lastUsed: "2023-07-20",
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "移动应用",
|
||||
key: "sk_mobile_*************",
|
||||
created: "2023-06-10",
|
||||
lastUsed: "2023-07-19",
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "第三方集成",
|
||||
key: "sk_partner_*************",
|
||||
created: "2023-04-20",
|
||||
lastUsed: "2023-07-18",
|
||||
status: "active",
|
||||
},
|
||||
]
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case "connected":
|
||||
return <CheckCircle className="h-4 w-4 text-green-500" />
|
||||
case "warning":
|
||||
return <AlertTriangle className="h-4 w-4 text-yellow-500" />
|
||||
case "error":
|
||||
return <AlertTriangle className="h-4 w-4 text-red-500" />
|
||||
default:
|
||||
return <Clock className="h-4 w-4 text-gray-500" />
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "connected":
|
||||
return "bg-green-100 text-green-800"
|
||||
case "warning":
|
||||
return "bg-yellow-100 text-yellow-800"
|
||||
case "error":
|
||||
return "bg-red-100 text-red-800"
|
||||
default:
|
||||
return "bg-gray-100 text-gray-800"
|
||||
}
|
||||
}
|
||||
|
||||
const getActivityIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "sync_complete":
|
||||
return <CheckCircle className="h-4 w-4 text-green-500" />
|
||||
case "quality_check":
|
||||
return <Shield className="h-4 w-4 text-blue-500" />
|
||||
case "sync_warning":
|
||||
return <AlertTriangle className="h-4 w-4 text-yellow-500" />
|
||||
case "sync_error":
|
||||
return <AlertTriangle className="h-4 w-4 text-red-500" />
|
||||
default:
|
||||
return <Activity className="h-4 w-4 text-gray-500" />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-2xl font-bold">API接口管理</h2>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
创建新API
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>API接口列表</CardTitle>
|
||||
<CardDescription>所有可用的API接口</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>API名称</TableHead>
|
||||
<TableHead>接口地址</TableHead>
|
||||
<TableHead>方法</TableHead>
|
||||
<TableHead>分类</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>调用次数</TableHead>
|
||||
<TableHead>最后调用</TableHead>
|
||||
<TableHead className="text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{apiEndpoints.map((api) => (
|
||||
<TableRow key={api.id}>
|
||||
<TableCell className="font-medium">{api.name}</TableCell>
|
||||
<TableCell>
|
||||
<code className="bg-muted px-1 py-0.5 rounded text-sm">{api.endpoint}</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
className={
|
||||
api.method === "GET"
|
||||
? "bg-blue-100 text-blue-800"
|
||||
: api.method === "POST"
|
||||
? "bg-green-100 text-green-800"
|
||||
: api.method === "PUT"
|
||||
? "bg-yellow-100 text-yellow-800"
|
||||
: "bg-red-100 text-red-800"
|
||||
}
|
||||
>
|
||||
{api.method}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{api.category}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
className={
|
||||
api.status === "active" ? "bg-green-100 text-green-800" : "bg-yellow-100 text-yellow-800"
|
||||
}
|
||||
>
|
||||
{api.status === "active" ? "正常" : "维护中"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{api.calls.toLocaleString()}</TableCell>
|
||||
<TableCell>{api.lastCalled}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
文档
|
||||
</Button>
|
||||
<Button size="sm">测试</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>API密钥管理</CardTitle>
|
||||
<CardDescription>管理API访问密钥</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>应用名称</TableHead>
|
||||
<TableHead>密钥</TableHead>
|
||||
<TableHead>创建时间</TableHead>
|
||||
<TableHead>最后使用</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead className="text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{apiKeys.map((key) => (
|
||||
<TableRow key={key.id}>
|
||||
<TableCell className="font-medium">{key.name}</TableCell>
|
||||
<TableCell>
|
||||
<code className="bg-muted px-1 py-0.5 rounded text-sm">{key.key}</code>
|
||||
</TableCell>
|
||||
<TableCell>{key.created}</TableCell>
|
||||
<TableCell>{key.lastUsed}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className="bg-green-100 text-green-800">{key.status === "active" ? "有效" : "已禁用"}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
重置
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="text-red-500">
|
||||
撤销
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>API使用统计</CardTitle>
|
||||
<CardDescription>API调用量和性能统计</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="h-80 flex items-center justify-center">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p>API调用统计图表</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 添加数据源对话框
|
||||
function AddDataSourceDialog({ isOpen, setIsOpen }: { isOpen: boolean; setIsOpen: (open: boolean) => void }) {
|
||||
const [dbType, setDbType] = useState("mysql")
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>添加新数据源</DialogTitle>
|
||||
<DialogDescription>连接到新的数据库或数据源</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="db-type" className="text-right">
|
||||
数据库类型
|
||||
</Label>
|
||||
<Select value={dbType} onValueChange={setDbType} className="col-span-3">
|
||||
<SelectTrigger id="db-type">
|
||||
<SelectValue placeholder="选择数据库类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="mysql">MySQL</SelectItem>
|
||||
<SelectItem value="postgresql">PostgreSQL</SelectItem>
|
||||
<SelectItem value="mongodb">MongoDB</SelectItem>
|
||||
<SelectItem value="clickhouse">ClickHouse</SelectItem>
|
||||
<SelectItem value="oracle">Oracle</SelectItem>
|
||||
<SelectItem value="sqlserver">SQL Server</SelectItem>
|
||||
<SelectItem value="redis">Redis</SelectItem>
|
||||
<SelectItem value="file">CSV/Excel文件</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right">
|
||||
数据源名称
|
||||
</Label>
|
||||
<Input id="name" placeholder="给数据源起个名字" className="col-span-3" />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="host" className="text-right">
|
||||
主机地址
|
||||
</Label>
|
||||
<Input id="host" placeholder="例如: db.example.com" className="col-span-3" />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="port" className="text-right">
|
||||
端口
|
||||
</Label>
|
||||
<Input
|
||||
id="port"
|
||||
placeholder={dbType === "mysql" ? "3306" : dbType === "postgresql" ? "5432" : "27017"}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="database" className="text-right">
|
||||
数据库名
|
||||
</Label>
|
||||
<Input id="database" placeholder="数据库名称" className="col-span-3" />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="username" className="text-right">
|
||||
用户名
|
||||
</Label>
|
||||
<Input id="username" placeholder="数据库用户名" className="col-span-3" />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="password" className="text-right">
|
||||
密码
|
||||
</Label>
|
||||
<Input id="password" type="password" placeholder="数据库密码" className="col-span-3" />
|
||||
</div>
|
||||
<div className="container mx-auto py-6 space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">数据对接</h1>
|
||||
<p className="text-muted-foreground">数据源管理与全流程监控</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsOpen(false)}>
|
||||
取消
|
||||
<div className="flex gap-2">
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
添加数据源
|
||||
</Button>
|
||||
<Button type="submit">测试连接</Button>
|
||||
<Button type="submit">保存</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button variant="outline">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
配置管理
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 核心指标 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card className="bg-gradient-to-r from-blue-50 to-cyan-50 border-none shadow-md">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm font-medium">数据源总数</CardTitle>
|
||||
<Database className="h-4 w-4 text-blue-600" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-blue-600">{integrationStats.totalSources}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">活跃: {integrationStats.activeSources}个</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-r from-green-50 to-emerald-50 border-none shadow-md">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm font-medium">日处理量</CardTitle>
|
||||
<Activity className="h-4 w-4 text-green-600" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{(integrationStats.dailyVolume / 1000000).toFixed(1)}M
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">条记录</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-r from-purple-50 to-pink-50 border-none shadow-md">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm font-medium">成功率</CardTitle>
|
||||
<TrendingUp className="h-4 w-4 text-purple-600" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-purple-600">{integrationStats.successRate}%</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">同步成功率</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-r from-orange-50 to-red-50 border-none shadow-md">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm font-medium">平均延迟</CardTitle>
|
||||
<Zap className="h-4 w-4 text-orange-600" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-orange-600">{integrationStats.avgLatency}ms</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">响应时间</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 数据源状态 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<GitBranch className="h-5 w-5 text-blue-600" />
|
||||
数据源状态
|
||||
</CardTitle>
|
||||
<CardDescription>各数据源连接状态与同步情况</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{dataSources.map((source) => (
|
||||
<div key={source.id} className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
{getStatusIcon(source.status)}
|
||||
<div>
|
||||
<p className="font-medium text-sm">{source.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{source.type === "database" ? "数据库" : "API接口"} • 最后同步: {source.lastSync}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<Badge className={getStatusColor(source.status)}>
|
||||
{source.status === "connected" ? "已连接" : source.status === "warning" ? "警告" : "错误"}
|
||||
</Badge>
|
||||
<div className="flex items-center gap-4 mt-1 text-xs text-muted-foreground">
|
||||
<span>{source.records.toLocaleString()} 条</span>
|
||||
<span>质量: {source.quality}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between mt-4">
|
||||
<Link href="/data-integration/sources">
|
||||
<Button variant="outline">管理数据源</Button>
|
||||
</Link>
|
||||
<Button>添加新数据源</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 实时活动 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Activity className="h-5 w-5 text-green-600" />
|
||||
实时活动
|
||||
</CardTitle>
|
||||
<CardDescription>数据同步活动日志</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{recentActivities.map((activity) => (
|
||||
<div key={activity.id} className="flex items-start gap-3 p-2 hover:bg-gray-50 rounded-lg">
|
||||
{getActivityIcon(activity.type)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">{activity.message}</p>
|
||||
<p className="text-xs text-gray-500">{activity.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button variant="outline" className="w-full mt-4 bg-transparent">
|
||||
查看全部日志
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 数据流程概览 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Zap className="h-5 w-5 text-purple-600" />
|
||||
数据流程概览
|
||||
</CardTitle>
|
||||
<CardDescription>数据从接入到处理的完整流程</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<Link href="/data-integration/sources">
|
||||
<div className="text-center p-4 border rounded-lg hover:bg-gray-50 transition-colors cursor-pointer">
|
||||
<GitBranch className="h-8 w-8 mx-auto mb-2 text-blue-600" />
|
||||
<h3 className="font-medium mb-1">数据源管理</h3>
|
||||
<p className="text-xs text-muted-foreground">配置和管理各种数据源</p>
|
||||
<Badge className="mt-2 bg-blue-100 text-blue-700">{integrationStats.totalSources}个源</Badge>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/data-integration/ingestion">
|
||||
<div className="text-center p-4 border rounded-lg hover:bg-gray-50 transition-colors cursor-pointer">
|
||||
<Database className="h-8 w-8 mx-auto mb-2 text-green-600" />
|
||||
<h3 className="font-medium mb-1">数据接入</h3>
|
||||
<p className="text-xs text-muted-foreground">实时数据采集和同步</p>
|
||||
<Badge className="mt-2 bg-green-100 text-green-700">
|
||||
{(integrationStats.dailyVolume / 1000000).toFixed(1)}M/日
|
||||
</Badge>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/data-integration/processing">
|
||||
<div className="text-center p-4 border rounded-lg hover:bg-gray-50 transition-colors cursor-pointer">
|
||||
<Zap className="h-8 w-8 mx-auto mb-2 text-purple-600" />
|
||||
<h3 className="font-medium mb-1">数据处理</h3>
|
||||
<p className="text-xs text-muted-foreground">清洗、转换和标准化</p>
|
||||
<Badge className="mt-2 bg-purple-100 text-purple-700">{integrationStats.successRate}%成功</Badge>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/data-integration/quality">
|
||||
<div className="text-center p-4 border rounded-lg hover:bg-gray-50 transition-colors cursor-pointer">
|
||||
<Shield className="h-8 w-8 mx-auto mb-2 text-orange-600" />
|
||||
<h3 className="font-medium mb-1">质量监控</h3>
|
||||
<p className="text-xs text-muted-foreground">数据质量评估和监控</p>
|
||||
<Badge className="mt-2 bg-orange-100 text-orange-700">{integrationStats.qualityScore}%质量</Badge>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 性能监控 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5 text-green-600" />
|
||||
性能监控
|
||||
</CardTitle>
|
||||
<CardDescription>数据处理性能指标</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-2">
|
||||
<span>数据处理速度</span>
|
||||
<span className="font-medium">85%</span>
|
||||
</div>
|
||||
<Progress value={85} className="h-2 mb-1" />
|
||||
<p className="text-xs text-muted-foreground">1.2M 记录/小时</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-2">
|
||||
<span>系统资源使用</span>
|
||||
<span className="font-medium">67%</span>
|
||||
</div>
|
||||
<Progress value={67} className="h-2 mb-1" />
|
||||
<p className="text-xs text-muted-foreground">CPU + 内存</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-2">
|
||||
<span>存储使用率</span>
|
||||
<span className="font-medium">42%</span>
|
||||
</div>
|
||||
<Progress value={42} className="h-2 mb-1" />
|
||||
<p className="text-xs text-muted-foreground">2.1TB / 5TB</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
567
app/intelligent-search/page.tsx
Normal file
567
app/intelligent-search/page.tsx
Normal 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">目标: <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>
|
||||
)
|
||||
}
|
||||
268
app/overview/dashboard/page.tsx
Normal file
268
app/overview/dashboard/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
283
app/overview/monitoring/page.tsx
Normal file
283
app/overview/monitoring/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
439
app/overview/search/page.tsx
Normal file
439
app/overview/search/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
635
app/page.tsx
635
app/page.tsx
@@ -1,408 +1,335 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { TooltipHelp, TooltipProvider } from "@/components/ui/tooltip-help"
|
||||
import {
|
||||
Database,
|
||||
Users,
|
||||
Target,
|
||||
TrendingUp,
|
||||
RefreshCw,
|
||||
Download,
|
||||
ChevronRight,
|
||||
Activity,
|
||||
TrendingUp,
|
||||
Database,
|
||||
Search,
|
||||
RefreshCw,
|
||||
Zap,
|
||||
BarChart3,
|
||||
Target,
|
||||
Globe,
|
||||
UserCheck,
|
||||
Layers,
|
||||
Tag,
|
||||
Clock,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
export default function HomePage() {
|
||||
const [timeRange, setTimeRange] = useState("today")
|
||||
export default function OverviewPage() {
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [lastUpdate, setLastUpdate] = useState(new Date())
|
||||
|
||||
// 今日数据概览
|
||||
const todayStats = {
|
||||
dataSync: {
|
||||
total: 1245678,
|
||||
growth: "+12.5%",
|
||||
sources: 8,
|
||||
lastUpdate: "2分钟前",
|
||||
},
|
||||
userPool: {
|
||||
totalUsers: 125678,
|
||||
activeUsers: 45678,
|
||||
newUsers: 1234,
|
||||
growth: "+8.2%",
|
||||
},
|
||||
userPortrait: {
|
||||
totalTags: 342,
|
||||
activeTags: 287,
|
||||
coverage: "95.8%",
|
||||
growth: "+5.3%",
|
||||
},
|
||||
userValuation: {
|
||||
avgValue: 3248,
|
||||
highValue: 12456,
|
||||
growth: "+15.8%",
|
||||
upgradeRate: "12.5%",
|
||||
},
|
||||
// 核心数据状态
|
||||
const [coreData, setCoreData] = useState({
|
||||
totalUsers: 4000000000, // 40亿用户
|
||||
activeUsers: 2800000000, // 28亿活跃用户
|
||||
dataGrowthRate: 15.8, // 数据增长率
|
||||
assetGrowthRate: 23.5, // 资产增长率
|
||||
realTimeQueries: 156789, // 实时查询数
|
||||
dataVolume: "12.8TB", // 数据量
|
||||
systemHealth: 99.2, // 系统健康度
|
||||
aiAnalysis: 8456, // AI分析次数
|
||||
})
|
||||
|
||||
// 实时数据更新
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCoreData((prev) => ({
|
||||
...prev,
|
||||
realTimeQueries: prev.realTimeQueries + Math.floor(Math.random() * 50) + 10,
|
||||
activeUsers: prev.activeUsers + Math.floor(Math.random() * 1000) + 100,
|
||||
}))
|
||||
setLastUpdate(new Date())
|
||||
}, 5000) // 每5秒更新一次
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
// 手动刷新
|
||||
const handleRefresh = async () => {
|
||||
setIsRefreshing(true)
|
||||
// 模拟数据刷新
|
||||
setTimeout(() => {
|
||||
setCoreData((prev) => ({
|
||||
...prev,
|
||||
totalUsers: prev.totalUsers + Math.floor(Math.random() * 10000) + 1000,
|
||||
dataGrowthRate: +(Math.random() * 5 + 12).toFixed(1),
|
||||
assetGrowthRate: +(Math.random() * 8 + 18).toFixed(1),
|
||||
}))
|
||||
setLastUpdate(new Date())
|
||||
setIsRefreshing(false)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// 实时活动数据
|
||||
const realtimeActivities = [
|
||||
{
|
||||
id: 1,
|
||||
type: "data_sync",
|
||||
message: "IMEI数据同步完成",
|
||||
time: "刚刚",
|
||||
status: "success",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: "user_pool",
|
||||
message: "新增用户池分群",
|
||||
time: "2分钟前",
|
||||
status: "info",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: "user_portrait",
|
||||
message: "标签规则自动执行",
|
||||
time: "5分钟前",
|
||||
status: "warning",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: "user_valuation",
|
||||
message: "用户价值模型更新完成",
|
||||
time: "8分钟前",
|
||||
status: "success",
|
||||
},
|
||||
]
|
||||
|
||||
const getActivityIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "data_sync":
|
||||
return <Database className="h-4 w-4" />
|
||||
case "user_pool":
|
||||
return <Users className="h-4 w-4" />
|
||||
case "user_portrait":
|
||||
return <Target className="h-4 w-4" />
|
||||
case "user_valuation":
|
||||
return <TrendingUp className="h-4 w-4" />
|
||||
default:
|
||||
return <Activity className="h-4 w-4" />
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
if (searchQuery.trim()) {
|
||||
// 跳转到智能搜索页面
|
||||
window.location.href = `/intelligent-search?q=${encodeURIComponent(searchQuery)}`
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "success":
|
||||
return "text-green-600"
|
||||
case "warning":
|
||||
return "text-yellow-600"
|
||||
case "info":
|
||||
return "text-blue-600"
|
||||
default:
|
||||
return "text-gray-600"
|
||||
const formatNumber = (num: number) => {
|
||||
if (num >= 1000000000) {
|
||||
return (num / 1000000000).toFixed(1) + "B"
|
||||
}
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + "M"
|
||||
}
|
||||
if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + "K"
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-4 md:py-6 space-y-4 md:space-y-6">
|
||||
{/* 页面标题和工具栏 */}
|
||||
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">数据概览</h1>
|
||||
<p className="text-sm md:text-base text-muted-foreground">卡若数据资产中台总览</p>
|
||||
<TooltipProvider>
|
||||
<div className="container mx-auto py-6 space-y-6">
|
||||
{/* 页面标题和搜索 */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">概览</h1>
|
||||
<p className="text-muted-foreground mt-1 flex items-center gap-2">
|
||||
数据资产中台总览
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
{lastUpdate.toLocaleTimeString()}
|
||||
</Badge>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="搜索用户或流量关键词..."
|
||||
className="pl-10 pr-20"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyPress={(e) => e.key === "Enter" && handleSearch()}
|
||||
/>
|
||||
<Button size="sm" className="absolute right-1 top-1/2 transform -translate-y-1/2" onClick={handleSearch}>
|
||||
搜索
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={isRefreshing}>
|
||||
<RefreshCw className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||
{isRefreshing ? "刷新中" : "刷新"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||
<SelectTrigger className="w-[120px] text-sm">
|
||||
<SelectValue placeholder="时间范围" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="today">今日</SelectItem>
|
||||
<SelectItem value="week">本周</SelectItem>
|
||||
<SelectItem value="month">本月</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9 bg-transparent">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="hidden md:flex bg-transparent">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
导出
|
||||
</Button>
|
||||
|
||||
{/* 核心用户数据展示 - 40亿用户为中心 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* 中心用户总数卡片 */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card className="h-full border-2 border-blue-200 bg-gradient-to-br from-blue-50 to-indigo-50">
|
||||
<CardHeader className="text-center pb-2">
|
||||
<CardTitle className="text-lg flex items-center justify-center gap-2">
|
||||
<Globe className="h-6 w-6 text-blue-600" />
|
||||
用户总数
|
||||
<TooltipHelp content="平台累计用户总数,包括所有注册和识别的用户" />
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center">
|
||||
<div className="text-5xl font-bold text-blue-600 mb-2">{formatNumber(coreData.totalUsers)}</div>
|
||||
<div className="text-sm text-muted-foreground">全球用户覆盖</div>
|
||||
<div className="mt-4 grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="bg-white/50 rounded p-2">
|
||||
<div className="font-medium">活跃用户</div>
|
||||
<div className="text-green-600 font-bold">{formatNumber(coreData.activeUsers)}</div>
|
||||
</div>
|
||||
<div className="bg-white/50 rounded p-2">
|
||||
<div className="font-medium">活跃率</div>
|
||||
<div className="text-blue-600 font-bold">
|
||||
{((coreData.activeUsers / coreData.totalUsers) * 100).toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 右侧数据指标 */}
|
||||
<div className="lg:col-span-2 grid grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-1">
|
||||
用户资产增长
|
||||
<TooltipHelp content="用户资产价值的增长率,反映用户价值提升情况" />
|
||||
</CardTitle>
|
||||
<TrendingUp className="h-4 w-4 text-green-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-600">+{coreData.assetGrowthRate}%</div>
|
||||
<p className="text-xs text-muted-foreground">月度增长率</p>
|
||||
<div className="mt-2 h-2 bg-gray-200 rounded-full">
|
||||
<div
|
||||
className="h-2 bg-green-500 rounded-full transition-all duration-500"
|
||||
style={{ width: `${Math.min(coreData.assetGrowthRate * 2, 100)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-1">
|
||||
数据增长
|
||||
<TooltipHelp content="数据量的增长速度,包括用户行为数据、交易数据等" />
|
||||
</CardTitle>
|
||||
<Database className="h-4 w-4 text-blue-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-blue-600">+{coreData.dataGrowthRate}%</div>
|
||||
<p className="text-xs text-muted-foreground">数据量: {coreData.dataVolume}</p>
|
||||
<div className="mt-2 h-2 bg-gray-200 rounded-full">
|
||||
<div
|
||||
className="h-2 bg-blue-500 rounded-full transition-all duration-500"
|
||||
style={{ width: `${Math.min(coreData.dataGrowthRate * 3, 100)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-1">
|
||||
实时查询
|
||||
<TooltipHelp content="当前实时查询次数,反映系统活跃度" />
|
||||
</CardTitle>
|
||||
<Zap className="h-4 w-4 text-yellow-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-yellow-600">{formatNumber(coreData.realTimeQueries)}</div>
|
||||
<p className="text-xs text-muted-foreground">实时查询次数</p>
|
||||
<div className="flex items-center mt-2">
|
||||
<div className="w-2 h-2 bg-yellow-500 rounded-full animate-pulse mr-2"></div>
|
||||
<span className="text-xs text-green-600">实时更新中</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-1">
|
||||
系统状态
|
||||
<TooltipHelp content="系统整体健康状态和AI分析能力" />
|
||||
</CardTitle>
|
||||
<Activity className="h-4 w-4 text-purple-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-purple-600">{coreData.systemHealth}%</div>
|
||||
<p className="text-xs text-muted-foreground">AI分析: {formatNumber(coreData.aiAnalysis)} 次</p>
|
||||
<div className="flex items-center mt-2">
|
||||
<Badge className="bg-green-100 text-green-800 text-xs">系统正常</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 核心功能模块 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* 数据中台 */}
|
||||
<Link href="/data-platform">
|
||||
<Card className="border-none shadow-md hover:shadow-lg transition-all duration-300 cursor-pointer group">
|
||||
<CardHeader className="bg-gradient-to-r from-blue-50 to-indigo-50 pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5 text-blue-600" />
|
||||
<CardTitle className="text-base">数据中台</CardTitle>
|
||||
</div>
|
||||
<Badge className="bg-blue-100 text-blue-700">核心</Badge>
|
||||
</div>
|
||||
<CardDescription className="text-xs">多源数据整合中心</CardDescription>
|
||||
{/* 数据分布概览 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">高价值用户</CardTitle>
|
||||
<Target className="h-4 w-4 text-red-500" />
|
||||
</CardHeader>
|
||||
<CardContent className="pt-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">数据总量</span>
|
||||
<span className="font-bold">{todayStats.dataSync.total.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">数据源</span>
|
||||
<span className="font-bold">{todayStats.dataSync.sources}个</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">增长率</span>
|
||||
<span className="text-green-600 font-bold">{todayStats.dataSync.growth}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<span className="text-xs text-gray-500">最后更新: {todayStats.dataSync.lastUpdate}</span>
|
||||
<ChevronRight className="h-4 w-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">2.8M</div>
|
||||
<p className="text-xs text-muted-foreground">占比 7.2%</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
{/* 用户池 */}
|
||||
<Link href="/user-pool">
|
||||
<Card className="border-none shadow-md hover:shadow-lg transition-all duration-300 cursor-pointer group">
|
||||
<CardHeader className="bg-gradient-to-r from-green-50 to-emerald-50 pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="h-5 w-5 text-green-600" />
|
||||
<CardTitle className="text-base">用户池</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
<CardDescription className="text-xs">用户数据管理中心</CardDescription>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">活跃用户</CardTitle>
|
||||
<UserCheck className="h-4 w-4 text-green-500" />
|
||||
</CardHeader>
|
||||
<CardContent className="pt-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">总用户数</span>
|
||||
<span className="font-bold">{todayStats.userPool.totalUsers.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">活跃用户</span>
|
||||
<span className="font-bold">{todayStats.userPool.activeUsers.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">新增用户</span>
|
||||
<span className="font-bold">{todayStats.userPool.newUsers.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<span className="text-xs text-green-600">增长 {todayStats.userPool.growth}</span>
|
||||
<ChevronRight className="h-4 w-4 text-gray-400 group-hover:text-green-600 transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">28.5M</div>
|
||||
<p className="text-xs text-muted-foreground">日活跃用户</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
{/* 用户画像 */}
|
||||
<Link href="/user-portrait">
|
||||
<Card className="border-none shadow-md hover:shadow-lg transition-all duration-300 cursor-pointer group">
|
||||
<CardHeader className="bg-gradient-to-r from-purple-50 to-pink-50 pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Target className="h-5 w-5 text-purple-600" />
|
||||
<CardTitle className="text-base">用户画像</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
<CardDescription className="text-xs">用户标签画像分析</CardDescription>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">流量关键词</CardTitle>
|
||||
<BarChart3 className="h-4 w-4 text-blue-500" />
|
||||
</CardHeader>
|
||||
<CardContent className="pt-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">标签总数</span>
|
||||
<span className="font-bold">{todayStats.userPortrait.totalTags}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">活跃标签</span>
|
||||
<span className="font-bold">{todayStats.userPortrait.activeTags}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">覆盖率</span>
|
||||
<span className="text-purple-600 font-bold">{todayStats.userPortrait.coverage}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<span className="text-xs text-purple-600">增长 {todayStats.userPortrait.growth}</span>
|
||||
<ChevronRight className="h-4 w-4 text-gray-400 group-hover:text-purple-600 transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">156K</div>
|
||||
<p className="text-xs text-muted-foreground">热门关键词数量</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
{/* 用户估值 */}
|
||||
<Link href="/user-valuation">
|
||||
<Card className="border-none shadow-md hover:shadow-lg transition-all duration-300 cursor-pointer group">
|
||||
<CardHeader className="bg-gradient-to-r from-orange-50 to-red-50 pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5 text-orange-600" />
|
||||
<CardTitle className="text-base">用户估值</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
<CardDescription className="text-xs">用户价值评估模型</CardDescription>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">数据源</CardTitle>
|
||||
<Database className="h-4 w-4 text-purple-500" />
|
||||
</CardHeader>
|
||||
<CardContent className="pt-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">平均估值</span>
|
||||
<span className="font-bold">¥{todayStats.userValuation.avgValue.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">高价值用户</span>
|
||||
<span className="font-bold">{todayStats.userValuation.highValue.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">升级率</span>
|
||||
<span className="text-orange-600 font-bold">{todayStats.userValuation.upgradeRate}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<span className="text-xs text-orange-600">增长 {todayStats.userValuation.growth}</span>
|
||||
<ChevronRight className="h-4 w-4 text-gray-400 group-hover:text-orange-600 transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">28</div>
|
||||
<p className="text-xs text-muted-foreground">已接入数据源</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 实时数据流 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 md:gap-6">
|
||||
{/* 今日关键指标 */}
|
||||
<Card className="lg:col-span-2 border-none shadow-md">
|
||||
{/* 实时活动流 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Activity className="h-5 w-5 text-blue-600" />
|
||||
今日关键指标
|
||||
<Activity className="h-5 w-5" />
|
||||
实时数据流
|
||||
<TooltipHelp content="系统实时数据处理和分析活动" />
|
||||
</CardTitle>
|
||||
<CardDescription>实时数据监控</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="text-center p-3 bg-blue-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-blue-600">98.5%</div>
|
||||
<div className="text-xs text-gray-600">系统可用性</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-green-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-green-600">2.3s</div>
|
||||
<div className="text-xs text-gray-600">平均响应时间</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-purple-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-purple-600">342</div>
|
||||
<div className="text-xs text-gray-600">活跃标签数</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-orange-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-orange-600">95.8%</div>
|
||||
<div className="text-xs text-gray-600">标签覆盖率</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 space-y-3">
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>数据处理进度</span>
|
||||
<span>87%</span>
|
||||
</div>
|
||||
<Progress value={87} className="h-2" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>标签规则执行</span>
|
||||
<span>92%</span>
|
||||
</div>
|
||||
<Progress value={92} className="h-2" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 实时活动 */}
|
||||
<Card className="border-none shadow-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Activity className="h-5 w-5 text-green-600" />
|
||||
实时活动
|
||||
</CardTitle>
|
||||
<CardDescription>系统活动监控</CardDescription>
|
||||
<CardDescription>实时监控数据处理和用户活动</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{realtimeActivities.map((activity) => (
|
||||
<div key={activity.id} className="flex items-start gap-3 p-2 hover:bg-gray-50 rounded-lg">
|
||||
<div className={`p-1 rounded-full ${getStatusColor(activity.status)}`}>
|
||||
{getActivityIcon(activity.type)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">{activity.message}</p>
|
||||
<p className="text-xs text-gray-500">{activity.time}</p>
|
||||
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<div>
|
||||
<div className="font-medium">用户数据同步</div>
|
||||
<div className="text-sm text-muted-foreground">新增 1,247 个用户记录</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Badge className="bg-green-100 text-green-800">实时</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
|
||||
<div>
|
||||
<div className="font-medium">AI分析完成</div>
|
||||
<div className="text-sm text-muted-foreground">用户画像分析处理完成</div>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className="bg-blue-100 text-blue-800">2分钟前</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 bg-purple-50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-2 h-2 bg-purple-500 rounded-full animate-pulse"></div>
|
||||
<div>
|
||||
<div className="font-medium">流量关键词更新</div>
|
||||
<div className="text-sm text-muted-foreground">发现 89 个新热门关键词</div>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className="bg-purple-100 text-purple-800">5分钟前</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" className="w-full mt-4 text-sm bg-transparent">
|
||||
查看全部活动
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 快速操作 */}
|
||||
<Card className="border-none shadow-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Zap className="h-5 w-5 text-yellow-600" />
|
||||
快速操作
|
||||
</CardTitle>
|
||||
<CardDescription>常用功能快捷入口</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<Link href="/data-platform">
|
||||
<Button variant="outline" className="w-full h-16 flex flex-col gap-1 bg-transparent">
|
||||
<Database className="h-5 w-5" />
|
||||
<span className="text-xs">数据导入</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/user-pool/management">
|
||||
<Button variant="outline" className="w-full h-16 flex flex-col gap-1 bg-transparent">
|
||||
<UserCheck className="h-5 w-5" />
|
||||
<span className="text-xs">用户管理</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/user-pool/segmentation">
|
||||
<Button variant="outline" className="w-full h-16 flex flex-col gap-1 bg-transparent">
|
||||
<Layers className="h-5 w-5" />
|
||||
<span className="text-xs">用户分群</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/user-portrait/tags">
|
||||
<Button variant="outline" className="w-full h-16 flex flex-col gap-1 bg-transparent">
|
||||
<Tag className="h-5 w-5" />
|
||||
<span className="text-xs">标签管理</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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%" />
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
74
components/ui/tooltip-help.tsx
Normal file
74
components/ui/tooltip-help.tsx
Normal 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
228
lib/data-dictionary.ts
Normal 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
433
lib/mindsdb-connector.ts
Normal 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
|
||||
}
|
||||
@@ -35,12 +35,12 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "latest",
|
||||
"d3": "latest",
|
||||
"date-fns": "latest",
|
||||
"docx": "latest",
|
||||
"dom-to-image": "latest",
|
||||
"html-to-image": "latest",
|
||||
"lucide-react": "^0.454.0",
|
||||
"mindsdb-js-sdk": "latest",
|
||||
"next": "14.2.16",
|
||||
"next-themes": "latest",
|
||||
"react": "^18",
|
||||
|
||||
295
pnpm-lock.yaml
generated
295
pnpm-lock.yaml
generated
@@ -10,7 +10,7 @@ importers:
|
||||
dependencies:
|
||||
'@ant-design/plots':
|
||||
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':
|
||||
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)
|
||||
@@ -86,9 +86,6 @@ importers:
|
||||
cmdk:
|
||||
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)
|
||||
d3:
|
||||
specifier: latest
|
||||
version: 7.9.0
|
||||
date-fns:
|
||||
specifier: latest
|
||||
version: 4.1.0
|
||||
@@ -104,6 +101,9 @@ importers:
|
||||
lucide-react:
|
||||
specifier: ^0.454.0
|
||||
version: 0.454.0(react@18.0.0)
|
||||
mindsdb-js-sdk:
|
||||
specifier: latest
|
||||
version: 2.3.2
|
||||
next:
|
||||
specifier: 14.2.16
|
||||
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-dom: '>=16.8.4'
|
||||
|
||||
'@ant-design/plots@2.6.0':
|
||||
resolution: {integrity: sha512-l6sZLPoKPKWG6kdvHe+H7vUqNQk2i1xjsiBQe/8ABZ9KDRDnig+LzHBjHnVl0fvqmzDPI3O3RFEccl1tfA1i4A==}
|
||||
'@ant-design/plots@2.6.1':
|
||||
resolution: {integrity: sha512-X46qm2QcXJVV6hL+pcqtj9TsLEkOrow+uq7zDRQKbXWg9Um525GRnbF7PdaS89Rwoo3RdhrA3+T97VbQVYD0wQ==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.4'
|
||||
react-dom: '>=16.8.4'
|
||||
@@ -1191,6 +1191,10 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
agentkeepalive@4.6.0:
|
||||
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
|
||||
ajv@6.12.6:
|
||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||
|
||||
@@ -1278,6 +1282,9 @@ packages:
|
||||
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
autoprefixer@10.0.1:
|
||||
resolution: {integrity: sha512-aQo2BDIsoOdemXUAOBpFv4ZQa2DrOtEufarYhtFsK1088Ca0TUwu/aQWf0M3mrILXZ3mTIVn1lR3hPW8acacsw==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@@ -1293,6 +1300,9 @@ packages:
|
||||
resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
axios@1.11.0:
|
||||
resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==}
|
||||
|
||||
axobject-query@4.1.0:
|
||||
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1300,6 +1310,12 @@ packages:
|
||||
balanced-match@1.0.2:
|
||||
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:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1383,6 +1399,10 @@ packages:
|
||||
colorette@1.4.0:
|
||||
resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
commander@4.1.1:
|
||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -1413,41 +1433,17 @@ packages:
|
||||
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-axis@3.0.0:
|
||||
resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-binarytree@1.0.2:
|
||||
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:
|
||||
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-drag@3.0.0:
|
||||
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-dsv@3.0.1:
|
||||
resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1497,10 +1493,6 @@ packages:
|
||||
resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-polygon@3.0.1:
|
||||
resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-quadtree@3.0.1:
|
||||
resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1520,10 +1512,6 @@ packages:
|
||||
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-selection@3.0.0:
|
||||
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-shape@3.2.0:
|
||||
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1540,20 +1528,6 @@ packages:
|
||||
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||
|
||||
@@ -1606,8 +1580,9 @@ packages:
|
||||
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
delaunator@5.0.1:
|
||||
resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==}
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
detect-node-es@1.1.0:
|
||||
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
||||
@@ -1867,6 +1842,15 @@ packages:
|
||||
resolution: {integrity: sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==}
|
||||
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:
|
||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1875,6 +1859,10 @@ packages:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
form-data@4.0.4:
|
||||
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
fs.realpath@1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
|
||||
@@ -1991,6 +1979,9 @@ packages:
|
||||
html-to-image@1.11.13:
|
||||
resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
|
||||
|
||||
humanize-ms@1.2.1:
|
||||
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2177,6 +2168,9 @@ packages:
|
||||
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
||||
hasBin: true
|
||||
|
||||
json-bigint@1.0.0:
|
||||
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
|
||||
|
||||
json-buffer@3.0.1:
|
||||
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
||||
|
||||
@@ -2251,6 +2245,17 @@ packages:
|
||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
||||
|
||||
@@ -2275,6 +2280,10 @@ packages:
|
||||
ms@2.1.3:
|
||||
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:
|
||||
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
|
||||
|
||||
@@ -2495,6 +2504,9 @@ packages:
|
||||
prop-types@15.8.1:
|
||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||
|
||||
proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
|
||||
punycode@2.3.1:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2584,6 +2596,9 @@ packages:
|
||||
read-cache@1.0.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||
|
||||
@@ -2650,9 +2665,6 @@ packages:
|
||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||
hasBin: true
|
||||
|
||||
robust-predicates@3.0.2:
|
||||
resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
|
||||
|
||||
run-parallel@1.2.0:
|
||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
|
||||
@@ -2746,6 +2758,10 @@ packages:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
|
||||
|
||||
@@ -3027,7 +3043,7 @@ snapshots:
|
||||
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:
|
||||
'@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
|
||||
@@ -4182,6 +4198,10 @@ snapshots:
|
||||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
agentkeepalive@4.6.0:
|
||||
dependencies:
|
||||
humanize-ms: 1.2.1
|
||||
|
||||
ajv@6.12.6:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
@@ -4291,6 +4311,8 @@ snapshots:
|
||||
|
||||
async-function@1.0.0: {}
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
autoprefixer@10.0.1(postcss@8.5.0):
|
||||
dependencies:
|
||||
browserslist: 4.25.1
|
||||
@@ -4307,10 +4329,22 @@ snapshots:
|
||||
|
||||
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: {}
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
bignumber.js@9.0.0: {}
|
||||
|
||||
bignumber.js@9.3.1: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
@@ -4410,6 +4444,10 @@ snapshots:
|
||||
|
||||
colorette@1.4.0: {}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
commander@4.1.1: {}
|
||||
|
||||
commander@7.2.0: {}
|
||||
@@ -4432,39 +4470,12 @@ snapshots:
|
||||
dependencies:
|
||||
internmap: 2.0.3
|
||||
|
||||
d3-axis@3.0.0: {}
|
||||
|
||||
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-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-drag@3.0.0:
|
||||
dependencies:
|
||||
d3-dispatch: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
|
||||
d3-dsv@3.0.1:
|
||||
dependencies:
|
||||
commander: 7.2.0
|
||||
@@ -4513,8 +4524,6 @@ snapshots:
|
||||
|
||||
d3-path@3.1.0: {}
|
||||
|
||||
d3-polygon@3.0.1: {}
|
||||
|
||||
d3-quadtree@3.0.1: {}
|
||||
|
||||
d3-random@3.0.1: {}
|
||||
@@ -4534,8 +4543,6 @@ snapshots:
|
||||
d3-time: 3.1.0
|
||||
d3-time-format: 4.1.0
|
||||
|
||||
d3-selection@3.0.0: {}
|
||||
|
||||
d3-shape@3.2.0:
|
||||
dependencies:
|
||||
d3-path: 3.1.0
|
||||
@@ -4550,56 +4557,6 @@ snapshots:
|
||||
|
||||
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: {}
|
||||
|
||||
data-view-buffer@1.0.2:
|
||||
@@ -4648,9 +4605,7 @@ snapshots:
|
||||
has-property-descriptors: 1.0.2
|
||||
object-keys: 1.1.1
|
||||
|
||||
delaunator@5.0.1:
|
||||
dependencies:
|
||||
robust-predicates: 3.0.2
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
detect-node-es@1.1.0: {}
|
||||
|
||||
@@ -5053,6 +5008,8 @@ snapshots:
|
||||
|
||||
flru@1.0.2: {}
|
||||
|
||||
follow-redirects@1.15.9: {}
|
||||
|
||||
for-each@0.3.5:
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
@@ -5062,6 +5019,14 @@ snapshots:
|
||||
cross-spawn: 7.0.6
|
||||
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: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
@@ -5199,6 +5164,10 @@ snapshots:
|
||||
|
||||
html-to-image@1.11.13: {}
|
||||
|
||||
humanize-ms@1.2.1:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
@@ -5387,6 +5356,10 @@ snapshots:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
json-bigint@1.0.0:
|
||||
dependencies:
|
||||
bignumber.js: 9.3.1
|
||||
|
||||
json-buffer@3.0.1: {}
|
||||
|
||||
json-schema-traverse@0.4.1: {}
|
||||
@@ -5457,6 +5430,21 @@ snapshots:
|
||||
braces: 3.0.3
|
||||
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: {}
|
||||
|
||||
minimatch@3.1.2:
|
||||
@@ -5477,6 +5465,13 @@ snapshots:
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
@@ -5682,6 +5677,8 @@ snapshots:
|
||||
object-assign: 4.1.1
|
||||
react-is: 16.13.1
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
@@ -5759,6 +5756,16 @@ snapshots:
|
||||
dependencies:
|
||||
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:
|
||||
dependencies:
|
||||
core-util-is: 1.0.3
|
||||
@@ -5847,8 +5854,6 @@ snapshots:
|
||||
dependencies:
|
||||
glob: 7.2.3
|
||||
|
||||
robust-predicates@3.0.2: {}
|
||||
|
||||
run-parallel@1.2.0:
|
||||
dependencies:
|
||||
queue-microtask: 1.2.3
|
||||
@@ -5956,6 +5961,8 @@ snapshots:
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
sqlstring@2.3.1: {}
|
||||
|
||||
stable-hash@0.0.5: {}
|
||||
|
||||
stop-iteration-iterator@1.1.0:
|
||||
|
||||
232
services/IdentityService.ts
Normal file
232
services/IdentityService.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
446
services/IngestionService.ts
Normal file
446
services/IngestionService.ts
Normal 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
|
||||
}
|
||||
}
|
||||
473
services/intelligent-search-service.ts
Normal file
473
services/intelligent-search-service.ts
Normal 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
|
||||
}
|
||||
@@ -2,10 +2,6 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
|
||||
226
开发文档/开发文档.md
226
开发文档/开发文档.md
@@ -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]。
|
||||
* **API 路由更新:** `app/api/database-structure/route.ts` 已更新,以导入并使用新的 `lib/mongodb-mock-connector.ts`,确保数据中台的数据库结构查看器能够展示模拟的 MongoDB 集合信息。
|
||||
### 水(数据对接)- 数据流转模块
|
||||
- **核心功能**: 数据源管理、数据接入、数据处理、质量监控
|
||||
- **技术实现**: ETL流程 + API集成 + 数据质量监控
|
||||
- **页面结构**:
|
||||
- `/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智能助手”并列,以提高其可见性和易用性。
|
||||
* **导航链接可用性:** 确保所有导航链接都指向正确的页面路径,并在模拟环境中保持功能可用。
|
||||
* **数据中台 (`app/data-platform/page.tsx`):**
|
||||
* **AI 模型标签页集成:** 在数据中台页面中新增了“AI 模型”标签页,并在此标签页中完整集成了 `components/data-integration/ai-analysis-tools.tsx` 组件。这使得用户可以直接在数据中台管理 AI 分析任务、AI 模型和分析模板。
|
||||
* **数据源“连接到AI”按钮:** 在“数据源管理”部分,为每个数据源增加了“连接到AI”按钮。该按钮根据数据源的 `aiReady` 状态启用或禁用,点击后会触发一个提示信息(Toast),模拟数据源与 AI 平台的集成入口。
|
||||
* **AI 分析工具 (`components/data-integration/ai-analysis-tools.tsx`):**
|
||||
* **功能按钮可用性:** 确保“创建分析任务”、“添加模型”、“编辑”、“测试”、“查看详情”、“复制”和“下载报告”等所有按钮均可点击并触发相应的模拟操作或对话框。
|
||||
* **代码清理:** 移除了组件中可能存在的冗余代码或未使用的导入,确保代码的精简。
|
||||
### 前端技术栈
|
||||
- **框架**: Next.js 14 (App Router)
|
||||
- **语言**: TypeScript
|
||||
- **UI库**: shadcn/ui + Tailwind CSS
|
||||
- **状态管理**: React Hooks + Context
|
||||
- **图表库**: Recharts
|
||||
- **图标库**: 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%
|
||||
|
||||
Reference in New Issue
Block a user