chore: 以本地为准,上传全部并替换 GitHub
This commit is contained in:
714
app/page.tsx
714
app/page.tsx
@@ -1,396 +1,478 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useState, useEffect, useRef } from "react"
|
||||
import {
|
||||
Users,
|
||||
Database,
|
||||
RefreshCw,
|
||||
Activity,
|
||||
Globe,
|
||||
Brain,
|
||||
Tags,
|
||||
LineChart,
|
||||
Package,
|
||||
ArrowUpRight,
|
||||
Zap,
|
||||
Send,
|
||||
Loader2,
|
||||
Search,
|
||||
Phone,
|
||||
User,
|
||||
TrendingUp,
|
||||
Settings,
|
||||
ChevronRight,
|
||||
Sparkles,
|
||||
Target,
|
||||
CheckCircle,
|
||||
} from "lucide-react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import Link from "next/link"
|
||||
|
||||
interface SystemMetrics {
|
||||
dataTaskSuccessRate: number
|
||||
tagCoverageRate: number
|
||||
valuePredictionAccuracy: number
|
||||
apiDailyCallGrowth: number
|
||||
interface ChatMessage {
|
||||
role: "user" | "assistant" | "thinking"
|
||||
content: string
|
||||
timestamp: string
|
||||
data?: any
|
||||
thinking?: string // AI思考过程
|
||||
portrait?: UserPortrait // 用户画像
|
||||
}
|
||||
|
||||
interface DataFlowStats {
|
||||
totalUsers: number
|
||||
totalProjects: number
|
||||
totalDataSources: number
|
||||
totalTags: number
|
||||
totalModels: number
|
||||
totalApiCalls: number
|
||||
// 标准用户画像模板
|
||||
interface UserPortrait {
|
||||
phone?: string
|
||||
qq?: string
|
||||
name?: string
|
||||
gender?: string
|
||||
province?: string
|
||||
city?: string
|
||||
level?: string
|
||||
rfmScore?: number
|
||||
tags?: string[]
|
||||
behavior?: {
|
||||
lastActive?: string
|
||||
frequency?: string
|
||||
}
|
||||
dataCompleteness?: number
|
||||
}
|
||||
|
||||
export default function OverviewPage() {
|
||||
const router = useRouter()
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [lastUpdate, setLastUpdate] = useState(new Date())
|
||||
interface AIStatus {
|
||||
status: string
|
||||
database?: {
|
||||
connected: boolean
|
||||
totalUsers: number
|
||||
latency: number
|
||||
}
|
||||
}
|
||||
|
||||
// 核心OKR指标 (按PRD定义)
|
||||
const [metrics, setMetrics] = useState<SystemMetrics>({
|
||||
dataTaskSuccessRate: 99.7,
|
||||
tagCoverageRate: 82.3,
|
||||
valuePredictionAccuracy: 87.5,
|
||||
apiDailyCallGrowth: 32.8,
|
||||
})
|
||||
|
||||
// 数据流统计
|
||||
const [stats, setStats] = useState<DataFlowStats>({
|
||||
totalUsers: 4028567890,
|
||||
totalProjects: 1256,
|
||||
totalDataSources: 28,
|
||||
totalTags: 1892,
|
||||
totalModels: 15,
|
||||
totalApiCalls: 8956234,
|
||||
})
|
||||
|
||||
// 实时数据流
|
||||
const [dataFlows, setDataFlows] = useState([
|
||||
{ source: "存客宝", type: "用户行为", count: 125680, status: "running" },
|
||||
{ source: "触客宝", type: "互动数据", count: 89234, status: "running" },
|
||||
{ source: "数智员工", type: "账号数据", count: 45678, status: "running" },
|
||||
{ source: "外部API", type: "征信数据", count: 12890, status: "waiting" },
|
||||
export default function HomePage() {
|
||||
// AI对话状态
|
||||
const [query, setQuery] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [aiStatus, setAiStatus] = useState<AIStatus | null>(null)
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||
{
|
||||
role: "assistant",
|
||||
content: "你好!我是神射手AI助手。\n\n你可以:\n• 输入手机号查询用户画像\n• 输入QQ号查询关联信息\n• 问我任何关于用户数据的问题",
|
||||
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
||||
}
|
||||
])
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 统计数据
|
||||
const [stats, setStats] = useState({
|
||||
totalUsers: 0,
|
||||
totalDataSources: 0,
|
||||
latency: 0,
|
||||
})
|
||||
|
||||
// 获取AI状态
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
// 模拟实时数据更新
|
||||
setStats((prev) => ({
|
||||
...prev,
|
||||
totalUsers: prev.totalUsers + Math.floor(Math.random() * 1000),
|
||||
totalApiCalls: prev.totalApiCalls + Math.floor(Math.random() * 100),
|
||||
}))
|
||||
setDataFlows((prev) =>
|
||||
prev.map((flow) => ({
|
||||
...flow,
|
||||
count: flow.count + Math.floor(Math.random() * 100),
|
||||
})),
|
||||
)
|
||||
}, 3000)
|
||||
return () => clearInterval(interval)
|
||||
fetch("/api/ai-chat")
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setAiStatus(data)
|
||||
if (data.database) {
|
||||
setStats({
|
||||
totalUsers: data.database.totalUsers || 0,
|
||||
totalDataSources: 26,
|
||||
latency: data.database.latency || 0,
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
}, [])
|
||||
|
||||
const refreshData = async () => {
|
||||
setIsRefreshing(true)
|
||||
await new Promise((r) => setTimeout(r, 1000))
|
||||
setLastUpdate(new Date())
|
||||
setIsRefreshing(false)
|
||||
// 自动滚动到底部
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
||||
}, [messages])
|
||||
|
||||
// 解析用户画像数据
|
||||
const parsePortrait = (data: any): UserPortrait | undefined => {
|
||||
if (!data) return undefined
|
||||
return {
|
||||
phone: data.phone || data.手机号,
|
||||
qq: data.qq || data.QQ,
|
||||
name: data.name || data.姓名,
|
||||
gender: data.gender || data.性别,
|
||||
province: data.province || data.省份,
|
||||
city: data.city || data.城市,
|
||||
level: data.level || data.user_level || data.等级,
|
||||
rfmScore: data.rfm_score || data.rfmScore || data.RFM评分,
|
||||
tags: data.tags || [],
|
||||
behavior: {
|
||||
lastActive: data.last_active || data.最近活跃,
|
||||
frequency: data.frequency || data.活跃频率,
|
||||
},
|
||||
dataCompleteness: data.data_completeness || data.数据完整度,
|
||||
}
|
||||
}
|
||||
|
||||
// 生成思考过程
|
||||
const generateThinking = (query: string): string => {
|
||||
const isPhone = /^1[3-9]\d{9}$/.test(query.replace(/\s/g, ''))
|
||||
const isQQ = /^\d{5,11}$/.test(query.replace(/\s|qq/gi, ''))
|
||||
|
||||
if (isPhone) {
|
||||
return `🔍 识别到手机号查询\n→ 拆解意图: 查询用户画像\n→ 数据源: KR.用户估值, KR_腾讯\n→ 执行: 手机号精确匹配\n→ 输出: 标准用户画像模板`
|
||||
} else if (isQQ) {
|
||||
return `🔍 识别到QQ号查询\n→ 拆解意图: QQ关联查询\n→ 数据源: KR_腾讯.qq_phone\n→ 执行: QQ号精确匹配\n→ 输出: 关联手机号 + 用户画像`
|
||||
} else {
|
||||
return `🔍 自然语言理解中...\n→ 拆解意图: ${query.slice(0, 20)}...\n→ 思考策略: 语义分析\n→ 执行: Skill查询`
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
const handleSend = async () => {
|
||||
if (!query.trim() || loading) return
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: query,
|
||||
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
||||
}
|
||||
|
||||
// 添加思考过程消息
|
||||
const thinkingMessage: ChatMessage = {
|
||||
role: "thinking",
|
||||
content: generateThinking(query),
|
||||
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
||||
}
|
||||
|
||||
setMessages(prev => [...prev, userMessage, thinkingMessage])
|
||||
setQuery("")
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/ai-chat", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: query })
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// 解析用户画像
|
||||
const portrait = parsePortrait(data.response?.data)
|
||||
|
||||
setMessages(prev => {
|
||||
// 移除思考消息,添加结果
|
||||
const filtered = prev.filter(m => m.role !== "thinking")
|
||||
return [...filtered, {
|
||||
role: "assistant",
|
||||
content: data.success ? data.response.content : `查询失败: ${data.error || "未知错误"}`,
|
||||
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
||||
data: data.response?.data,
|
||||
portrait,
|
||||
thinking: generateThinking(query),
|
||||
}]
|
||||
})
|
||||
} catch (error: any) {
|
||||
setMessages(prev => {
|
||||
const filtered = prev.filter(m => m.role !== "thinking")
|
||||
return [...filtered, {
|
||||
role: "assistant",
|
||||
content: `网络错误: ${error.message}`,
|
||||
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })
|
||||
}]
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 快捷查询
|
||||
const quickQuery = (q: string) => {
|
||||
setQuery(q)
|
||||
setTimeout(() => handleSend(), 100)
|
||||
}
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
||||
return num.toString()
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`
|
||||
return num.toLocaleString()
|
||||
}
|
||||
|
||||
// 5大功能模块快捷入口(按HTML文档重构)
|
||||
// 核心功能模块
|
||||
const modules = [
|
||||
{
|
||||
title: "数据接入",
|
||||
icon: Database,
|
||||
color: "text-blue-600",
|
||||
bg: "bg-blue-100",
|
||||
color: "from-blue-500 to-cyan-500",
|
||||
href: "/data-ingestion/sources",
|
||||
desc: "数据源管理、清洗规则、任务调度、数据血缘",
|
||||
stats: `${stats.totalDataSources}个数据源`,
|
||||
desc: "数据源 · 清洗 · 调度",
|
||||
},
|
||||
{
|
||||
title: "标签画像",
|
||||
icon: Tags,
|
||||
color: "text-green-600",
|
||||
bg: "bg-green-100",
|
||||
href: "/tag-portrait/tags",
|
||||
desc: "标签体系、用户画像、人群圈选",
|
||||
stats: `${stats.totalTags}个标签`,
|
||||
color: "from-green-500 to-emerald-500",
|
||||
href: "/tag-portrait",
|
||||
desc: "标签 · 画像 · 流量池",
|
||||
},
|
||||
{
|
||||
title: "AI Agent",
|
||||
icon: Brain,
|
||||
color: "text-purple-600",
|
||||
bg: "bg-purple-100",
|
||||
href: "/ai-agent/chat",
|
||||
desc: "智能对话、AI打标、AI清洗、自然语言查询",
|
||||
stats: "5大AI能力",
|
||||
},
|
||||
{
|
||||
title: "数据输出",
|
||||
title: "数据市场",
|
||||
icon: Package,
|
||||
color: "text-orange-600",
|
||||
bg: "bg-orange-100",
|
||||
href: "/data-output/packages",
|
||||
desc: "流量包、API市场、数据订阅",
|
||||
stats: `${formatNumber(stats.totalApiCalls)}次调用`,
|
||||
},
|
||||
{
|
||||
title: "系统监控",
|
||||
icon: LineChart,
|
||||
color: "text-cyan-600",
|
||||
bg: "bg-cyan-100",
|
||||
href: "/system/health",
|
||||
desc: "系统健康、告警中心、操作日志",
|
||||
stats: "实时监控",
|
||||
color: "from-orange-500 to-red-500",
|
||||
href: "/data-market",
|
||||
desc: "API · 订阅 · 变现",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20 p-4 md:p-6">
|
||||
{/* 顶部状态栏 */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">数据驾驶舱</h1>
|
||||
<p className="text-gray-500 mt-1">私域银行生态数据中枢实时监控</p>
|
||||
<h1 className="text-xl font-bold text-gray-900">神射手</h1>
|
||||
<p className="text-xs text-gray-500">用户资产数字化中台</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="outline" className="bg-green-50 text-green-600 border-green-200 px-3 py-1">
|
||||
<Activity className="w-3 h-3 mr-1" />
|
||||
系统正常
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className={`${aiStatus?.status === 'online' ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'} text-xs`}>
|
||||
<div className={`w-1.5 h-1.5 rounded-full ${aiStatus?.status === 'online' ? 'bg-green-500' : 'bg-yellow-500'} mr-1`}></div>
|
||||
{stats.latency}ms
|
||||
</Badge>
|
||||
<span className="text-xs text-gray-400">更新于 {lastUpdate.toLocaleTimeString()}</span>
|
||||
<Button variant="outline" size="sm" onClick={refreshData} disabled={isRefreshing} className="bg-white/50">
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||
刷新
|
||||
</Button>
|
||||
<Link href="/monitoring/health">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<Activity className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/settings">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 核心OKR指标 */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-gray-500">数据接入成功率</span>
|
||||
<Badge className="bg-green-100 text-green-700 text-xs">目标≥99.5%</Badge>
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-3 gap-3 mb-4">
|
||||
<Card className="border-0 shadow-sm bg-white/70">
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="h-4 w-4 text-blue-500" />
|
||||
<div>
|
||||
<div className="text-lg font-bold">{formatNumber(stats.totalUsers)}</div>
|
||||
<div className="text-xs text-gray-500">用户</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-gray-900">{metrics.dataTaskSuccessRate}%</div>
|
||||
<Progress value={metrics.dataTaskSuccessRate} className="h-2 mt-2" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-gray-500">标签覆盖率</span>
|
||||
<Badge className="bg-blue-100 text-blue-700 text-xs">目标≥80%</Badge>
|
||||
<Card className="border-0 shadow-sm bg-white/70">
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-4 w-4 text-green-500" />
|
||||
<div>
|
||||
<div className="text-lg font-bold">{stats.totalDataSources}</div>
|
||||
<div className="text-xs text-gray-500">数据源</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-gray-900">{metrics.tagCoverageRate}%</div>
|
||||
<Progress value={metrics.tagCoverageRate} className="h-2 mt-2" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-gray-500">价值预测准确率</span>
|
||||
<Badge className="bg-purple-100 text-purple-700 text-xs">目标≥85%</Badge>
|
||||
<Card className="border-0 shadow-sm bg-white/70">
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="h-4 w-4 text-orange-500" />
|
||||
<div>
|
||||
<div className="text-lg font-bold">{stats.latency}ms</div>
|
||||
<div className="text-xs text-gray-500">延迟</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-gray-900">{metrics.valuePredictionAccuracy}%</div>
|
||||
<Progress value={metrics.valuePredictionAccuracy} className="h-2 mt-2" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-gray-500">API调用增长</span>
|
||||
<Badge className="bg-orange-100 text-orange-700 text-xs">目标≥30%QoQ</Badge>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-gray-900">+{metrics.apiDailyCallGrowth}%</div>
|
||||
<Progress value={metrics.apiDailyCallGrowth} className="h-2 mt-2" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 用户资产总览 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<Card className="lg:col-span-2 border-none shadow-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white overflow-hidden relative">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-white/10 rounded-full -mr-16 -mt-16 blur-3xl" />
|
||||
<CardHeader className="pb-2 relative z-10">
|
||||
<CardTitle className="flex items-center gap-2 text-blue-100">
|
||||
<Users className="w-5 h-5" />
|
||||
全平台用户资产
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="relative z-10">
|
||||
<div className="flex items-baseline gap-2 mb-4">
|
||||
<span className="text-5xl font-bold">{formatNumber(stats.totalUsers)}</span>
|
||||
<span className="text-blue-100">总用户数</span>
|
||||
{/* AI对话区域 - 核心功能 */}
|
||||
<Card className="border-0 shadow-lg bg-white/90 backdrop-blur mb-4 flex flex-col" style={{ height: 'calc(100vh - 340px)', minHeight: '300px' }}>
|
||||
<CardHeader className="pb-2 border-b flex-shrink-0">
|
||||
<CardTitle className="text-sm font-medium flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Brain className="h-4 w-4 text-purple-500" />
|
||||
AI 智能对话
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4 pt-4 border-t border-white/20">
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-green-300">+2.3%</div>
|
||||
<div className="text-sm text-blue-100">日增长</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold">{stats.totalProjects.toLocaleString()}</div>
|
||||
<div className="text-sm text-blue-100">项目数</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold">125.6B</div>
|
||||
<div className="text-sm text-blue-100">估值(元)</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 实时数据流 */}
|
||||
<Card className="border-none shadow-md bg-white/60 backdrop-blur-md">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-gray-700 text-sm font-medium flex items-center gap-2">
|
||||
<Zap className="w-4 h-4 text-yellow-500" />
|
||||
实时数据流入
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{dataFlows.map((flow, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-2 rounded-lg bg-gray-50">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${flow.status === "running" ? "bg-green-500 animate-pulse" : "bg-yellow-500"}`}
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{flow.source}</div>
|
||||
<div className="text-xs text-gray-500">{flow.type}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-semibold text-gray-900">{flow.count.toLocaleString()}</div>
|
||||
<div className="text-xs text-gray-500">条/分钟</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 5大功能模块入口 */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">功能模块</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||
{modules.map((module, index) => (
|
||||
<Link key={index} href={module.href}>
|
||||
<Card className="h-full cursor-pointer hover:shadow-lg transition-all duration-300 border-none shadow-sm bg-white/60 backdrop-blur-md group hover:-translate-y-1">
|
||||
<CardContent className="p-5">
|
||||
<div
|
||||
className={`w-12 h-12 rounded-2xl ${module.bg} flex items-center justify-center mb-3 group-hover:scale-110 transition-transform`}
|
||||
>
|
||||
<module.icon className={`w-6 h-6 ${module.color}`} />
|
||||
</div>
|
||||
<h3 className="font-bold text-gray-900 mb-1">{module.title}</h3>
|
||||
<p className="text-xs text-gray-500 mb-2 line-clamp-2">{module.desc}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{module.stats}
|
||||
</Badge>
|
||||
<ArrowUpRight className="w-4 h-4 text-gray-400 group-hover:text-blue-500 transition-colors" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 系统关联图说明 */}
|
||||
<Card className="border-none shadow-md bg-white/60 backdrop-blur-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-700 text-sm font-medium flex items-center gap-2">
|
||||
<Globe className="w-4 h-4" />
|
||||
系统关联与数据流向
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{formatNumber(stats.totalUsers)} 可查询
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* 数据流入 */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-semibold text-gray-900 flex items-center gap-2">
|
||||
<ArrowUpRight className="w-4 h-4 text-blue-500" />
|
||||
数据流入
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ name: "存客宝", desc: "用户基础信息、交易流水" },
|
||||
{ name: "触客宝", desc: "互动行为、活动参与" },
|
||||
{ name: "数智员工", desc: "账号信息、粉丝画像" },
|
||||
{ name: "外部系统", desc: "征信数据、行业数据" },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex items-start gap-2 p-2 rounded-lg bg-blue-50">
|
||||
<CheckCircle className="w-4 h-4 text-blue-500 mt-0.5" />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{item.name}</div>
|
||||
<div className="text-xs text-gray-500">{item.desc}</div>
|
||||
</div>
|
||||
|
||||
{/* 消息列表 */}
|
||||
<CardContent className="flex-1 overflow-y-auto p-4 space-y-3">
|
||||
{messages.map((msg, i) => (
|
||||
<div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||
{/* 思考过程 */}
|
||||
{msg.role === 'thinking' && (
|
||||
<div className="max-w-[85%] rounded-2xl px-4 py-2.5 bg-yellow-50 border border-yellow-200">
|
||||
<div className="flex items-center gap-2 text-yellow-700 text-xs mb-1">
|
||||
<Sparkles className="h-3 w-3 animate-pulse" />
|
||||
AI思考中...
|
||||
</div>
|
||||
))}
|
||||
<div className="text-xs text-yellow-800 whitespace-pre-wrap font-mono">{msg.content}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 用户消息 */}
|
||||
{msg.role === 'user' && (
|
||||
<div className="max-w-[85%] rounded-2xl px-4 py-2.5 bg-gradient-to-r from-blue-500 to-purple-500 text-white">
|
||||
<div className="text-sm whitespace-pre-wrap">{msg.content}</div>
|
||||
<div className="text-xs mt-1 text-blue-100">{msg.timestamp}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI回复 */}
|
||||
{msg.role === 'assistant' && (
|
||||
<div className="max-w-[85%] space-y-2">
|
||||
{/* 思考过程折叠显示 */}
|
||||
{msg.thinking && (
|
||||
<div className="rounded-xl px-3 py-2 bg-yellow-50 border border-yellow-100 text-xs">
|
||||
<div className="text-yellow-600 font-medium mb-1">💭 思考过程</div>
|
||||
<div className="text-yellow-800 whitespace-pre-wrap font-mono">{msg.thinking}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 用户画像卡片 */}
|
||||
{msg.portrait && (msg.portrait.phone || msg.portrait.qq) && (
|
||||
<div className="rounded-xl p-3 bg-gradient-to-r from-purple-50 to-blue-50 border border-purple-100">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-purple-500 to-blue-500 flex items-center justify-center text-white font-bold">
|
||||
{(msg.portrait.name || '?')[0]}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-gray-900">{msg.portrait.name || '未知用户'}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{msg.portrait.phone && <span className="mr-2">📱 {msg.portrait.phone}</span>}
|
||||
{msg.portrait.qq && <span>QQ: {msg.portrait.qq}</span>}
|
||||
</div>
|
||||
</div>
|
||||
{msg.portrait.level && (
|
||||
<Badge className="ml-auto bg-purple-100 text-purple-700">{msg.portrait.level}级</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
{msg.portrait.rfmScore !== undefined && (
|
||||
<div className="p-2 rounded bg-white/80">
|
||||
<div className="text-gray-500">RFM</div>
|
||||
<div className="font-bold text-purple-600">{msg.portrait.rfmScore}</div>
|
||||
</div>
|
||||
)}
|
||||
{msg.portrait.province && (
|
||||
<div className="p-2 rounded bg-white/80">
|
||||
<div className="text-gray-500">地区</div>
|
||||
<div className="font-medium">{msg.portrait.province} {msg.portrait.city}</div>
|
||||
</div>
|
||||
)}
|
||||
{msg.portrait.dataCompleteness !== undefined && (
|
||||
<div className="p-2 rounded bg-white/80">
|
||||
<div className="text-gray-500">完整度</div>
|
||||
<div className="font-medium text-green-600">{msg.portrait.dataCompleteness}%</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{msg.portrait.tags && msg.portrait.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{msg.portrait.tags.slice(0, 5).map((tag, ti) => (
|
||||
<Badge key={ti} variant="secondary" className="text-xs">{tag}</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 文本回复 */}
|
||||
<div className="rounded-2xl px-4 py-2.5 bg-gray-100 text-gray-800">
|
||||
<div className="text-sm whitespace-pre-wrap">{msg.content}</div>
|
||||
<div className="text-xs mt-1 text-gray-400">{msg.timestamp}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{loading && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-gray-100 rounded-2xl px-4 py-2.5 flex items-center gap-2 text-gray-500">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span className="text-sm">查询中...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 神射手处理 */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-semibold text-gray-900 flex items-center gap-2">
|
||||
<Target className="w-4 h-4 text-purple-500" />
|
||||
神射手处理
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ name: "数据治理", desc: "清洗、标准化、质量监控" },
|
||||
{ name: "标签计算", desc: "规则/模型/AI标签生成" },
|
||||
{ name: "价值评估", desc: "CLV、RFM、流失预测" },
|
||||
{ name: "AI洞察", desc: "智能分析、报告生成" },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex items-start gap-2 p-2 rounded-lg bg-purple-50">
|
||||
<Zap className="w-4 h-4 text-purple-500 mt-0.5" />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{item.name}</div>
|
||||
<div className="text-xs text-gray-500">{item.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 服务输出 */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-semibold text-gray-900 flex items-center gap-2">
|
||||
<Package className="w-4 h-4 text-green-500" />
|
||||
服务输出
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ name: "存客宝/触客宝", desc: "画像API、人群包推送" },
|
||||
{ name: "数智员工", desc: "账号价值评估API" },
|
||||
{ name: "聚宝盆", desc: "核心指标聚合接口" },
|
||||
{ name: "外部客户", desc: "行业报告、趋势分析包" },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex items-start gap-2 p-2 rounded-lg bg-green-50">
|
||||
<ArrowUpRight className="w-4 h-4 text-green-500 mt-0.5" />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{item.name}</div>
|
||||
<div className="text-xs text-gray-500">{item.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</CardContent>
|
||||
|
||||
{/* 快捷查询 */}
|
||||
<div className="px-4 py-2 border-t flex-shrink-0">
|
||||
<div className="flex gap-2 overflow-x-auto pb-1">
|
||||
<Button variant="outline" size="sm" className="text-xs shrink-0 h-7" onClick={() => quickQuery("13407000001")}>
|
||||
<Phone className="w-3 h-3 mr-1" /> 查手机
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="text-xs shrink-0 h-7" onClick={() => quickQuery("28533368 qq")}>
|
||||
<User className="w-3 h-3 mr-1" /> 查QQ
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="text-xs shrink-0 h-7" onClick={() => quickQuery("系统状态")}>
|
||||
<Activity className="w-3 h-3 mr-1" /> 系统状态
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="text-xs shrink-0 h-7" onClick={() => quickQuery("高价值用户")}>
|
||||
<TrendingUp className="w-3 h-3 mr-1" /> 高价值
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 输入框 */}
|
||||
<div className="p-3 border-t flex-shrink-0">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyPress={(e) => e.key === "Enter" && handleSend()}
|
||||
placeholder="输入手机号、QQ号或问题..."
|
||||
className="flex-1 h-10 bg-gray-50 border-0"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
disabled={loading || !query.trim()}
|
||||
className="h-10 px-4 bg-gradient-to-r from-blue-500 to-purple-500"
|
||||
>
|
||||
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 功能模块入口 */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{modules.map((module, i) => (
|
||||
<Link key={i} href={module.href}>
|
||||
<Card className="border-0 shadow-sm bg-white/70 cursor-pointer hover:shadow-md transition-all group">
|
||||
<CardContent className="p-3">
|
||||
<div className={`w-10 h-10 rounded-xl bg-gradient-to-r ${module.color} flex items-center justify-center mb-2 group-hover:scale-110 transition-transform`}>
|
||||
<module.icon className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<h3 className="font-medium text-sm text-gray-900">{module.title}</h3>
|
||||
<p className="text-xs text-gray-500 mt-0.5">{module.desc}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user