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