refactor: restructure project into 5 core modules
Organize project by 5 core modules based on requirement docs. Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
BIN
app/.DS_Store
vendored
BIN
app/.DS_Store
vendored
Binary file not shown.
@@ -1,15 +1,14 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import "./globals.css"
|
||||
|
||||
import { MainNav } from "@/components/main-nav"
|
||||
import { UserNav } from "@/components/user-nav"
|
||||
import { ThemeToggle } from "@/components/theme-toggle"
|
||||
import { Search } from "lucide-react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Inter } from "next/font/google"
|
||||
import { useState, useEffect } from "react"
|
||||
import { usePathname } from "next/navigation"
|
||||
import Sidebar from "./components/Sidebar"
|
||||
import MobileHeader from "./components/MobileHeader"
|
||||
import MobileSidebar from "./components/MobileSidebar"
|
||||
import BottomNav from "./components/BottomNav"
|
||||
import { Toaster } from "@/components/ui/toaster"
|
||||
import "./globals.css"
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] })
|
||||
|
||||
@@ -18,74 +17,37 @@ export default function ClientLayout({
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const pathname = usePathname()
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
const isLoginPage = pathname === "/login"
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth < 768)
|
||||
}
|
||||
|
||||
checkMobile()
|
||||
window.addEventListener("resize", checkMobile)
|
||||
|
||||
return () => window.removeEventListener("resize", checkMobile)
|
||||
}, [])
|
||||
|
||||
if (isLoginPage) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<title>神射手 - 登录</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
</head>
|
||||
<body className={inter.className}>
|
||||
{children}
|
||||
<Toaster />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<title>用户数据资产中台</title>
|
||||
<meta name="description" content="基于苹果毛玻璃设计的用户数据资产中台" />
|
||||
<title>神射手数据资产中台</title>
|
||||
<meta name="description" content="基于IMEI、手机号的用户数据资产管理平台" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
</head>
|
||||
<body className={inter.className}>
|
||||
{/* 背景装饰 */}
|
||||
<div className="fixed inset-0 -z-10">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-50 via-white to-purple-50" />
|
||||
<div className="absolute top-0 left-0 w-72 h-72 md:w-96 md:h-96 bg-blue-200 rounded-full mix-blend-multiply filter blur-xl opacity-30 animate-pulse" />
|
||||
<div className="absolute top-0 right-0 w-72 h-72 md:w-96 md:h-96 bg-purple-200 rounded-full mix-blend-multiply filter blur-xl opacity-30 animate-pulse animation-delay-2000" />
|
||||
<div className="absolute bottom-0 left-1/2 w-72 h-72 md:w-96 md:h-96 bg-pink-200 rounded-full mix-blend-multiply filter blur-xl opacity-30 animate-pulse animation-delay-4000" />
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container flex h-16 items-center">
|
||||
<div className="mr-4 hidden md:flex">
|
||||
<a className="mr-6 flex items-center space-x-2" href="/">
|
||||
<span className="hidden font-bold sm:inline-block">神射手数据中台</span>
|
||||
</a>
|
||||
<MainNav />
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-between space-x-2 md:justify-end">
|
||||
<div className="w-full flex-1 md:w-auto md:flex-none">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input placeholder="搜索..." className="pl-8 h-9 md:w-[300px] lg:w-[300px]" />
|
||||
</div>
|
||||
</div>
|
||||
<ThemeToggle />
|
||||
<UserNav />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex-1 bg-background">{children}</main>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-screen">
|
||||
{/* 桌面端侧边栏 */}
|
||||
{!isMobile && <Sidebar />}
|
||||
|
||||
{/* 移动端侧边栏 */}
|
||||
{isMobile && <MobileSidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />}
|
||||
|
||||
{/* 主内容区域 */}
|
||||
<main className={`flex-1 ${isMobile ? "pb-20" : "p-6"}`}>
|
||||
{/* 移动端头部 */}
|
||||
{isMobile && <MobileHeader onMenuToggle={() => setSidebarOpen(true)} />}
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className={`glass-card min-h-full ${isMobile ? "mx-2 mb-4" : ""}`}>{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* 移动端底部导航 */}
|
||||
{isMobile && <BottomNav />}
|
||||
|
||||
<Toaster />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
175
app/ai-assistant/data-analysis/page.tsx
Normal file
175
app/ai-assistant/data-analysis/page.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { BarChart3, TrendingUp, Users, Download, RefreshCw } from 'lucide-react'
|
||||
import { BarChart, Bar, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'
|
||||
|
||||
export default function DataAnalysis() {
|
||||
const userBehaviorData = [
|
||||
{ hour: '00:00', active: 1250, purchases: 85, interactions: 4200 },
|
||||
{ hour: '04:00', active: 850, purchases: 35, interactions: 1800 },
|
||||
{ hour: '08:00', active: 5600, purchases: 420, interactions: 18500 },
|
||||
{ hour: '12:00', active: 8900, purchases: 680, interactions: 28600 },
|
||||
{ hour: '16:00', active: 7200, purchases: 520, interactions: 22400 },
|
||||
{ hour: '20:00', active: 9500, purchases: 750, interactions: 32800 },
|
||||
]
|
||||
|
||||
const channelData = [
|
||||
{ name: '微信朋友圈', value: 35, color: '#8b5cf6' },
|
||||
{ name: '微信群', value: 28, color: '#3b82f6' },
|
||||
{ name: '一对一私聊', value: 22, color: '#10b981' },
|
||||
{ name: '小程序', value: 15, color: '#f59e0b' },
|
||||
]
|
||||
|
||||
const conversionData = [
|
||||
{ stage: '浏览', users: 100000, rate: 100 },
|
||||
{ stage: '兴趣', users: 45000, rate: 45 },
|
||||
{ stage: '咨询', users: 18000, rate: 18 },
|
||||
{ stage: '下单', users: 5400, rate: 5.4 },
|
||||
{ stage: '复购', users: 1620, rate: 1.6 },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">数据分析</h1>
|
||||
<p className="text-gray-600">用户行为分析、渠道分析与转化漏斗</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
刷新数据
|
||||
</Button>
|
||||
<Button>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出报告
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 关键指标 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 mb-6">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">今日活跃用户</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">285.6K</div>
|
||||
<Badge variant="outline" className="border-green-500 text-green-600 bg-green-50">+12.5%</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">转化率</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">5.4%</div>
|
||||
<Badge variant="outline" className="border-blue-500 text-blue-600 bg-blue-50">+0.8%</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">人均互动次数</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">8.6</div>
|
||||
<Badge variant="outline" className="border-purple-500 text-purple-600 bg-purple-50">+1.2</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">客单价</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">¥328</div>
|
||||
<Badge variant="outline" className="border-orange-500 text-orange-600 bg-orange-50">+15.2%</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 用户行为趋势 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Users className="w-5 h-5" />
|
||||
用户活跃时段分布
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={userBehaviorData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||
<XAxis dataKey="hour" stroke="#6b7280" />
|
||||
<YAxis stroke="#6b7280" />
|
||||
<Tooltip />
|
||||
<Bar dataKey="active" fill="#8b5cf6" name="活跃用户" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<BarChart3 className="w-5 h-5" />
|
||||
渠道分布
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={channelData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={({ name, value }) => `${name} ${value}%`}
|
||||
outerRadius={100}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
>
|
||||
{channelData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 转化漏斗 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
用户转化漏斗
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{conversionData.map((stage, index) => (
|
||||
<div key={index}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-gray-700 font-medium">{stage.stage}</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-gray-900 font-semibold">{stage.users.toLocaleString()}人</span>
|
||||
<span className="text-blue-600 font-semibold w-16 text-right">{stage.rate}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-12 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg flex items-center px-4 text-white font-semibold" style={{ width: `${stage.rate}%`, minWidth: '10%' }}>
|
||||
{stage.rate}%
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,188 +1,235 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useMemo, useState } from "react"
|
||||
import { BarChart3, Database, FileText } from 'lucide-react'
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
|
||||
import Link from "next/link"
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import TaskList from "@/components/ai-assistant/task-list"
|
||||
import CreateTaskDialog from "@/components/ai-assistant/create-task-dialog"
|
||||
import ReportCards from "@/components/ai-assistant/report-cards"
|
||||
import { sanitizeText } from "@/lib/text-sanitize"
|
||||
import type { AnalysisTask, DatabaseInfo, ReportItem, ReportTemplate } from "@/types/ai-assistant"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Cpu, BarChart3, TrendingUp, Users, Target, Brain, ArrowRight, Send, Sparkles } from 'lucide-react'
|
||||
|
||||
const initialTasks: AnalysisTask[] = [
|
||||
{
|
||||
id: "task_001",
|
||||
name: "用户行为分析报告",
|
||||
database: "微信用户数据库",
|
||||
status: "completed",
|
||||
progress: 100,
|
||||
createdAt: "2025-01-15T09:00:00Z",
|
||||
completedAt: "2025-01-15T09:30:00Z",
|
||||
reportUrl: "/reports/user-behavior-analysis.pdf",
|
||||
description: "基于近30天行为的聚合与序列分析",
|
||||
},
|
||||
{
|
||||
id: "task_002",
|
||||
name: "流量关键词趋势分析",
|
||||
database: "流量关键词库",
|
||||
status: "running",
|
||||
progress: 65,
|
||||
createdAt: "2025-01-15T10:00:00Z",
|
||||
description: "关注核心流量词与曝光、点击、转化等指标",
|
||||
},
|
||||
{
|
||||
id: "task_003",
|
||||
name: "用户价值分层报告",
|
||||
database: "微信用户数据库",
|
||||
status: "pending",
|
||||
progress: 0,
|
||||
createdAt: "2025-01-15T10:30:00Z",
|
||||
description: "RFM 分层、价值区间分布与运营建议",
|
||||
},
|
||||
]
|
||||
export default function AIAssistant() {
|
||||
const [message, setMessage] = useState("")
|
||||
const [chatHistory, setChatHistory] = useState([
|
||||
{
|
||||
role: "assistant",
|
||||
content: "你好!我是神射手AI助手,可以帮助你分析用户数据、生成报告、预测趋势。有什么我可以帮助你的吗?",
|
||||
timestamp: "10:30",
|
||||
},
|
||||
])
|
||||
|
||||
const databases: DatabaseInfo[] = [
|
||||
{ id: "db_001", name: "微信用户数据库", type: "MySQL", tables: 25, records: 4000000000, lastUpdated: "2025-01-15T11:00:00Z" },
|
||||
{ id: "db_002", name: "流量关键词库", type: "PostgreSQL", tables: 8, records: 150000, lastUpdated: "2025-01-15T10:45:00Z" },
|
||||
{ id: "db_003", name: "用户行为日志", type: "MongoDB", tables: 12, records: 1500000000, lastUpdated: "2025-01-15T11:15:00Z" },
|
||||
]
|
||||
// AI功能卡片
|
||||
const aiFeatures = [
|
||||
{
|
||||
icon: <BarChart3 className="w-8 h-8" />,
|
||||
title: "数据分析",
|
||||
description: "自动分析用户行为,生成洞察报告",
|
||||
color: "from-blue-500 to-cyan-500",
|
||||
href: "/platform/ai-assistant/data-analysis",
|
||||
stats: { reports: 1258, insights: 856 }
|
||||
},
|
||||
{
|
||||
icon: <TrendingUp className="w-8 h-8" />,
|
||||
title: "趋势预测",
|
||||
description: "预测用户行为和价值变化趋势",
|
||||
color: "from-purple-500 to-pink-500",
|
||||
href: "/platform/ai-assistant/trend-prediction",
|
||||
stats: { accuracy: 94.2, predictions: 2450 }
|
||||
},
|
||||
{
|
||||
icon: <Users className="w-8 h-8" />,
|
||||
title: "用户画像",
|
||||
description: "智能生成用户群体画像",
|
||||
color: "from-green-500 to-emerald-500",
|
||||
href: "/platform/ai-assistant/user-profiling",
|
||||
stats: { profiles: 4285, segments: 128 }
|
||||
},
|
||||
{
|
||||
icon: <Target className="w-8 h-8" />,
|
||||
title: "精准推荐",
|
||||
description: "推荐最佳营销策略和用户群",
|
||||
color: "from-orange-500 to-red-500",
|
||||
href: "/platform/ai-assistant/recommendation",
|
||||
stats: { campaigns: 586, conversion: 18.5 }
|
||||
},
|
||||
]
|
||||
|
||||
const templates: ReportTemplate[] = [
|
||||
{ id: "template_001", name: "用户画像分析报告", description: "深度分析用户特征、行为模式和价值分层", category: "用户分析", fields: ["用户基本信息", "RFM分析", "行为轨迹", "价值评估", "推荐策略"] },
|
||||
{ id: "template_002", name: "流量趋势分析报告", description: "分析关键词搜索趋势和流量变化", category: "流量分析", fields: ["关键词热度", "搜索趋势", "竞争分析", "机会识别", "优化建议"] },
|
||||
{ id: "template_003", name: "业务运营报告", description: "综合业务数据分析和运营建议", category: "运营分析", fields: ["核心指标", "增长分析", "用户留存", "转化漏斗", "运营建议"] },
|
||||
{ id: "template_004", name: "数据质量报告", description: "评估数据完整性、准确性和一致性", category: "数据质量", fields: ["数据完整性", "准确性检查", "一致性验证", "异常检测", "改进建议"] },
|
||||
]
|
||||
// 快捷问题
|
||||
const quickQuestions = [
|
||||
"分析今日新增用户特征",
|
||||
"预测本月用户增长趋势",
|
||||
"高价值用户流失预警",
|
||||
"生成本周运营报告",
|
||||
]
|
||||
|
||||
const reportItems: ReportItem[] = [
|
||||
{ id: "rpt-1", title: "用户行为分析报告", source: "数据源:微信用户数据库", description: '本报告基于近30天互动行为,输出用户行为模式与高频路径。\\n"]]}', updatedAt: new Date().toISOString() },
|
||||
{ id: "rpt-2", title: "流量关键词趋势分析", source: "数据源:流量关键词库", description: '追踪 000000 类流量词及其曝光、点击、转化趋势,适配日/周/月视角。\\n" ]]}', updatedAt: new Date().toISOString() },
|
||||
{ id: "rpt-3", title: "用户价值分层报告", source: "数据源:微信用户数据库", description: '结合 RFM 得分与标签,给出 S/A/B/C/D 分层与经营建议。\\n" ]] }', updatedAt: new Date().toISOString() },
|
||||
]
|
||||
const handleSend = () => {
|
||||
if (!message.trim()) return
|
||||
|
||||
export default function AIAssistantPage() {
|
||||
const [tasks, setTasks] = useState<AnalysisTask[]>(initialTasks)
|
||||
|
||||
// 进度推进(仅演示用)
|
||||
const tick = useCallback(() => {
|
||||
setTasks((prev) =>
|
||||
prev.map((t) => {
|
||||
if (t.status === "running" && t.progress < 100) {
|
||||
const p = Math.min(100, t.progress + Math.random() * 12)
|
||||
if (p >= 100) {
|
||||
return {
|
||||
...t,
|
||||
progress: 100,
|
||||
status: "completed",
|
||||
completedAt: new Date().toISOString(),
|
||||
reportUrl: `/reports/${t.id}.pdf`,
|
||||
}
|
||||
}
|
||||
return { ...t, progress: p }
|
||||
}
|
||||
return t
|
||||
}),
|
||||
)
|
||||
}, [])
|
||||
|
||||
const onCreate = (task: AnalysisTask) => {
|
||||
setTasks((prev) => [task, ...prev])
|
||||
// 模拟启动
|
||||
setTimeout(() => {
|
||||
setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, status: "running" } : t)))
|
||||
}, 800)
|
||||
setChatHistory([
|
||||
...chatHistory,
|
||||
{
|
||||
role: "user",
|
||||
content: message,
|
||||
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: "正在分析您的问题,请稍候...",
|
||||
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
||||
},
|
||||
])
|
||||
setMessage("")
|
||||
}
|
||||
|
||||
const sanitizedItems = useMemo(
|
||||
() => reportItems.map((i) => ({ ...i, description: sanitizeText(i.description) })),
|
||||
[],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-indigo-50">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold">AI智能助手</h1>
|
||||
<p className="text-gray-600">数据库分析与智能报告生成</p>
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center">
|
||||
<Brain className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-gray-900">AI智能助手</h1>
|
||||
<p className="text-gray-600">数据分析、报告生成与智能预测</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* AI状态 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Cpu className="w-5 h-5 text-green-600" />
|
||||
AI运行状态
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">系统状态</span>
|
||||
<Badge variant="outline" className="border-green-500 text-green-600 bg-green-50">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse mr-2"></div>
|
||||
运行中
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">模型版本</span>
|
||||
<span className="text-gray-900 font-semibold">v3.5</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">响应速度</span>
|
||||
<span className="text-gray-900 font-semibold">0.8s</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">准确率</span>
|
||||
<span className="text-green-600 font-semibold">94.2%</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* AI功能卡片 */}
|
||||
<div className="lg:col-span-2 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{aiFeatures.map((feature, index) => (
|
||||
<Link key={index} href={feature.href}>
|
||||
<Card className="hover:shadow-lg transition-shadow cursor-pointer h-full">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
<div className={`w-16 h-16 rounded-xl bg-gradient-to-br ${feature.color} flex items-center justify-center text-white`}>
|
||||
{feature.icon}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-xl font-semibold text-gray-900">{feature.title}</h3>
|
||||
<ArrowRight className="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm">{feature.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-gray-200">
|
||||
{Object.entries(feature.stats).map(([key, value], i) => (
|
||||
<div key={i}>
|
||||
<div className="text-xs text-gray-600 mb-1">{key === 'reports' ? '报告数' : key === 'insights' ? '洞察数' : key === 'accuracy' ? '准确率' : key === 'predictions' ? '预测数' : key === 'profiles' ? '画像数' : key === 'segments' ? '分组数' : key === 'campaigns' ? '活动数' : '转化率'}</div>
|
||||
<div className="text-lg font-semibold text-gray-900">
|
||||
{typeof value === 'number' && value % 1 !== 0 ? `${value}%` : value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="analysis" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="analysis" className="flex items-center gap-2">
|
||||
<BarChart3 className="w-4 h-4" />
|
||||
数据分析
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="databases" className="flex items-center gap-2">
|
||||
<Database className="w-4 h-4" />
|
||||
数据库
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="templates" className="flex items-center gap-2">
|
||||
<FileText className="w-4 h-4" />
|
||||
报告模板
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="analysis" className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold">数据分析任务</h2>
|
||||
<CreateTaskDialog databases={databases} templates={templates} onCreate={onCreate} />
|
||||
</div>
|
||||
|
||||
<TaskList tasks={tasks} onTick={tick} />
|
||||
|
||||
<ReportCards items={sanitizedItems} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="databases">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database className="w-4 h-4" />
|
||||
数据库
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{databases.map((db) => (
|
||||
<Card key={db.id} className="border">
|
||||
<CardContent className="p-4 space-y-1">
|
||||
<div className="font-medium">{db.name}</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{db.type} · {db.tables} 表
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
最近更新 {new Date(db.lastUpdated).toLocaleString("zh-CN")}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* 对话区域 */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card className="bg-white/5 border-white/10 h-[calc(100vh-200px)]">
|
||||
<CardHeader className="border-b border-white/10">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<BarChart3 className="w-5 h-5" />
|
||||
AI对话
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6 flex flex-col h-[calc(100%-80px)]">
|
||||
{/* 聊天历史 */}
|
||||
<div className="flex-1 overflow-y-auto space-y-4 mb-6">
|
||||
{chatHistory.map((chat, index) => (
|
||||
<div key={index} className={`flex ${chat.role === "user" ? "justify-end" : "justify-start"}`}>
|
||||
<div
|
||||
className={`max-w-[80%] rounded-2xl p-4 ${
|
||||
chat.role === "user"
|
||||
? "bg-gradient-to-br from-blue-500 to-purple-500 text-white"
|
||||
: "bg-white/10 text-white"
|
||||
}`}
|
||||
>
|
||||
<div className="mb-1">{chat.content}</div>
|
||||
<div className="text-xs opacity-70">{chat.timestamp}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</div>
|
||||
|
||||
<TabsContent value="templates">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="w-4 h-4" />
|
||||
报告模板
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{templates.map((t) => (
|
||||
<Card key={t.id} className="border">
|
||||
<CardContent className="p-4 space-y-2">
|
||||
<div className="font-medium">{t.name}</div>
|
||||
<div className="text-sm text-gray-600">{t.category}</div>
|
||||
<div className="text-xs text-gray-500">字段: {t.fields.join(" / ")}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* 快捷问题 */}
|
||||
<div className="mb-4 flex flex-wrap gap-2">
|
||||
{quickQuestions.map((question, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setMessage(question)}
|
||||
className="bg-white/5 border-white/10 hover:bg-white/10 text-xs"
|
||||
>
|
||||
<Sparkles className="w-3 h-3 mr-1" />
|
||||
{question}
|
||||
</Button>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* 输入框 */}
|
||||
<div className="flex items-end gap-3">
|
||||
<Textarea
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
}
|
||||
}}
|
||||
placeholder="输入您的问题,按Enter发送..."
|
||||
className="bg-white/5 border-white/10 text-white resize-none"
|
||||
rows={3}
|
||||
/>
|
||||
<Button onClick={handleSend} className="bg-gradient-to-r from-blue-500 to-purple-500 h-[88px]">
|
||||
<Send className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
313
app/ai-assistant/recommendation/page.tsx
Normal file
313
app/ai-assistant/recommendation/page.tsx
Normal file
@@ -0,0 +1,313 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Target, Users, TrendingUp, Zap, Download, RefreshCw, ArrowRight } from 'lucide-react'
|
||||
|
||||
export default function Recommendation() {
|
||||
const recommendedCampaigns = [
|
||||
{
|
||||
id: 1,
|
||||
title: '美妆新品推广',
|
||||
targetGroup: '高净值女性用户',
|
||||
userCount: 856780,
|
||||
expectedRevenue: 12850000,
|
||||
probability: 82,
|
||||
roi: 3.5,
|
||||
timeframe: '未来7天',
|
||||
actions: ['朋友圈广告', '私聊推送', '限时优惠'],
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '会员升级计划',
|
||||
targetGroup: '活跃普通会员',
|
||||
userCount: 1285670,
|
||||
expectedRevenue: 8560000,
|
||||
probability: 75,
|
||||
roi: 4.2,
|
||||
timeframe: '未来15天',
|
||||
actions: ['会员权益推广', '专属优惠', 'VIP体验券'],
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '沉默用户唤醒',
|
||||
targetGroup: '30天未活跃用户',
|
||||
userCount: 2856780,
|
||||
expectedRevenue: 5680000,
|
||||
probability: 58,
|
||||
roi: 2.8,
|
||||
timeframe: '未来30天',
|
||||
actions: ['优惠券发放', '新品预告', '个性化推荐'],
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '交叉销售',
|
||||
targetGroup: '单品类购买用户',
|
||||
userCount: 1714283,
|
||||
expectedRevenue: 9280000,
|
||||
probability: 68,
|
||||
roi: 3.1,
|
||||
timeframe: '未来14天',
|
||||
actions: ['关联商品推荐', '搭配优惠', '场景营销'],
|
||||
priority: 'medium'
|
||||
},
|
||||
]
|
||||
|
||||
const recommendedSegments = [
|
||||
{
|
||||
name: '高转化潜力群体',
|
||||
count: 428567,
|
||||
conversion: 35.8,
|
||||
value: 580,
|
||||
characteristics: ['近期浏览频繁', '价格接受度高', '品牌认知强']
|
||||
},
|
||||
{
|
||||
name: '复购价值用户',
|
||||
count: 856780,
|
||||
conversion: 28.5,
|
||||
value: 420,
|
||||
characteristics: ['首购满意度高', '客单价适中', '活跃度稳定']
|
||||
},
|
||||
{
|
||||
name: '裂变传播节点',
|
||||
count: 285670,
|
||||
conversion: 22.3,
|
||||
value: 320,
|
||||
characteristics: ['社交影响力大', '分享意愿强', '粉丝基数高']
|
||||
},
|
||||
]
|
||||
|
||||
const contentRecommendations = [
|
||||
{
|
||||
type: '朋友圈内容',
|
||||
theme: '新品上市+限时优惠',
|
||||
targetUsers: 2856780,
|
||||
expectedEngagement: 18.5,
|
||||
bestTime: '20:00-22:00'
|
||||
},
|
||||
{
|
||||
type: '私聊话术',
|
||||
theme: '专属VIP权益介绍',
|
||||
targetUsers: 856780,
|
||||
expectedEngagement: 42.8,
|
||||
bestTime: '10:00-12:00'
|
||||
},
|
||||
{
|
||||
type: '社群活动',
|
||||
theme: '用户互动+抽奖',
|
||||
targetUsers: 1285670,
|
||||
expectedEngagement: 32.5,
|
||||
bestTime: '周末 15:00-17:00'
|
||||
},
|
||||
]
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
return priority === 'high' ? 'border-red-500 text-red-600 bg-red-50' : 'border-yellow-500 text-yellow-600 bg-yellow-50'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">精准推荐</h1>
|
||||
<p className="text-gray-600">AI智能推荐营销策略、目标用户群与内容方案</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
刷新推荐
|
||||
</Button>
|
||||
<Button>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出方案
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 核心指标 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 mb-6">
|
||||
<Card className="bg-gradient-to-br from-purple-50 to-purple-100 border-purple-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">推荐方案数</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-purple-600">586</div>
|
||||
<div className="text-sm text-gray-600">本月生成</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-blue-50 to-blue-100 border-blue-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">平均转化率</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-blue-600">18.5%</div>
|
||||
<Badge variant="outline" className="border-green-500 text-green-600 bg-green-50">+3.2%</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-green-50 to-green-100 border-green-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">预计收益</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-green-600">¥36.4M</div>
|
||||
<div className="text-sm text-gray-600">近期方案</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-orange-50 to-orange-100 border-orange-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">平均ROI</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-orange-600">3.4x</div>
|
||||
<div className="text-sm text-gray-600">投入产出比</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 推荐营销方案 */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Target className="w-5 h-5" />
|
||||
推荐营销方案
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{recommendedCampaigns.map((campaign) => (
|
||||
<div key={campaign.id} className="p-6 bg-white border border-gray-200 rounded-lg hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-xl font-bold text-gray-900">{campaign.title}</h3>
|
||||
<Badge variant="outline" className={getPriorityColor(campaign.priority)}>
|
||||
{campaign.priority === 'high' ? '高优先级' : '中优先级'}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-gray-300 text-gray-600">
|
||||
{campaign.timeframe}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-gray-600">目标群体: {campaign.targetGroup}</p>
|
||||
</div>
|
||||
<Button>
|
||||
执行方案
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-6 mb-4">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-1">目标用户数</div>
|
||||
<div className="text-lg font-semibold text-gray-900">{campaign.userCount.toLocaleString()}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-1">预期收益</div>
|
||||
<div className="text-lg font-semibold text-green-600">¥{(campaign.expectedRevenue / 10000).toFixed(1)}万</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-1">成功概率</div>
|
||||
<div className="text-lg font-semibold text-blue-600">{campaign.probability}%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-1">预期ROI</div>
|
||||
<div className="text-lg font-semibold text-purple-600">{campaign.roi}x</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-2">推荐动作</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{campaign.actions.map((action, index) => (
|
||||
<Badge key={index} variant="outline" className="border-blue-300 text-blue-600 bg-blue-50">
|
||||
<Zap className="w-3 h-3 mr-1" />
|
||||
{action}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 推荐目标群体 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
{recommendedSegments.map((segment, index) => (
|
||||
<Card key={index}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 text-lg">{segment.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 mb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">用户数</span>
|
||||
<span className="text-lg font-semibold text-gray-900">{segment.count.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">预期转化率</span>
|
||||
<span className="text-lg font-semibold text-green-600">{segment.conversion}%</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">人均价值</span>
|
||||
<span className="text-lg font-semibold text-blue-600">¥{segment.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-3 border-t border-gray-200">
|
||||
<div className="text-sm text-gray-600 mb-2">群体特征</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{segment.characteristics.map((char, i) => (
|
||||
<Badge key={i} variant="outline" className="border-gray-300 text-gray-700 text-xs">
|
||||
{char}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 内容推荐 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
内容推荐
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{contentRecommendations.map((content, index) => (
|
||||
<div key={index} className="p-4 bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg border border-blue-200">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold text-gray-900 mb-1">{content.type}</h4>
|
||||
<p className="text-gray-600">{content.theme}</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="border-purple-500 text-purple-600 bg-purple-50">
|
||||
最佳时间: {content.bestTime}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-600">目标触达: </span>
|
||||
<span className="font-semibold text-gray-900">{content.targetUsers.toLocaleString()} 人</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">预期互动率: </span>
|
||||
<span className="font-semibold text-green-600">{content.expectedEngagement}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
222
app/ai-assistant/trend-prediction/page.tsx
Normal file
222
app/ai-assistant/trend-prediction/page.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { TrendingUp, TrendingDown, AlertCircle, Download, RefreshCw } from 'lucide-react'
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts'
|
||||
|
||||
export default function TrendPrediction() {
|
||||
const userGrowthPrediction = [
|
||||
{ date: '2024-06', actual: 428, predicted: null },
|
||||
{ date: '2024-07', actual: null, predicted: 445 },
|
||||
{ date: '2024-08', actual: null, predicted: 468 },
|
||||
{ date: '2024-09', actual: null, predicted: 495 },
|
||||
{ date: '2024-10', actual: null, predicted: 528 },
|
||||
{ date: '2024-11', actual: null, predicted: 565 },
|
||||
{ date: '2024-12', actual: null, predicted: 608 },
|
||||
]
|
||||
|
||||
const valuePrediction = [
|
||||
{ date: '2024-06', actual: 125.8, predicted: null },
|
||||
{ date: '2024-07', actual: null, predicted: 132.5 },
|
||||
{ date: '2024-08', actual: null, predicted: 141.2 },
|
||||
{ date: '2024-09', actual: null, predicted: 151.8 },
|
||||
{ date: '2024-10', actual: null, predicted: 164.5 },
|
||||
{ date: '2024-11', actual: null, predicted: 179.2 },
|
||||
{ date: '2024-12', actual: null, predicted: 196.5 },
|
||||
]
|
||||
|
||||
const churnRiskData = [
|
||||
{ segment: '高价值用户', risk: 8.5, count: 1285, trend: 'down' },
|
||||
{ segment: 'VIP用户', risk: 12.3, count: 856, trend: 'stable' },
|
||||
{ segment: '活跃用户', risk: 18.6, count: 12856, trend: 'up' },
|
||||
{ segment: '沉默用户', risk: 45.8, count: 28560, trend: 'up' },
|
||||
]
|
||||
|
||||
const opportunityData = [
|
||||
{ name: '新品推广机会', potential: '¥85.6M', probability: 78, timeframe: '未来30天' },
|
||||
{ name: '复购唤醒', potential: '¥56.2M', probability: 65, timeframe: '未来15天' },
|
||||
{ name: '会员升级', potential: '¥38.5M', probability: 82, timeframe: '未来7天' },
|
||||
{ name: '交叉销售', potential: '¥92.8M', probability: 58, timeframe: '未来45天' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">趋势预测</h1>
|
||||
<p className="text-gray-600">用户增长预测、价值预测与流失预警</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
重新预测
|
||||
</Button>
|
||||
<Button>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出报告
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 预测指标 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 mb-6">
|
||||
<Card className="bg-gradient-to-br from-blue-50 to-blue-100 border-blue-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">下月预测用户数</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-blue-600 mb-1">445M</div>
|
||||
<div className="text-sm text-gray-600">+3.9% 增长</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-green-50 to-green-100 border-green-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">下月预测价值</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-green-600 mb-1">¥132.5亿</div>
|
||||
<div className="text-sm text-gray-600">+5.3% 增长</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-orange-50 to-orange-100 border-orange-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">流失风险用户</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-orange-600 mb-1">42.5K</div>
|
||||
<div className="text-sm text-gray-600">需重点关注</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-purple-50 to-purple-100 border-purple-200">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">预测准确率</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-purple-600 mb-1">94.2%</div>
|
||||
<div className="text-sm text-gray-600">基于历史数据</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 趋势图表 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-blue-600" />
|
||||
用户增长预测
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={userGrowthPrediction}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||
<XAxis dataKey="date" stroke="#6b7280" />
|
||||
<YAxis stroke="#6b7280" />
|
||||
<Tooltip />
|
||||
<Line type="monotone" dataKey="actual" stroke="#3b82f6" strokeWidth={2} name="实际值" />
|
||||
<Line type="monotone" dataKey="predicted" stroke="#8b5cf6" strokeWidth={2} strokeDasharray="5 5" name="预测值" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-green-600" />
|
||||
资产价值预测
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={valuePrediction}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||
<XAxis dataKey="date" stroke="#6b7280" />
|
||||
<YAxis stroke="#6b7280" />
|
||||
<Tooltip />
|
||||
<Area type="monotone" dataKey="actual" stroke="#10b981" fill="#10b981" fillOpacity={0.3} name="实际值" />
|
||||
<Area type="monotone" dataKey="predicted" stroke="#8b5cf6" fill="#8b5cf6" fillOpacity={0.2} strokeDasharray="5 5" name="预测值" />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 流失风险预警 */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<AlertCircle className="w-5 h-5 text-orange-600" />
|
||||
流失风险预警
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{churnRiskData.map((item, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center gap-4">
|
||||
<div>
|
||||
<div className="text-lg font-semibold text-gray-900">{item.segment}</div>
|
||||
<div className="text-sm text-gray-600">{item.count.toLocaleString()} 用户</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-orange-600">{item.risk}%</div>
|
||||
<div className="text-xs text-gray-600">流失风险</div>
|
||||
</div>
|
||||
{item.trend === 'down' ? (
|
||||
<TrendingDown className="w-6 h-6 text-green-600" />
|
||||
) : item.trend === 'up' ? (
|
||||
<TrendingUp className="w-6 h-6 text-red-600" />
|
||||
) : (
|
||||
<div className="w-6 h-6 flex items-center justify-center">
|
||||
<div className="w-4 h-0.5 bg-gray-400"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 商机预测 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-purple-600" />
|
||||
商机预测
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{opportunityData.map((item, index) => (
|
||||
<div key={index} className="p-4 bg-gradient-to-br from-purple-50 to-pink-50 rounded-lg border border-purple-200">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-lg font-semibold text-gray-900">{item.name}</h3>
|
||||
<Badge variant="outline" className="border-purple-500 text-purple-600 bg-purple-50">
|
||||
{item.timeframe}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-end justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-1">预测收益</div>
|
||||
<div className="text-2xl font-bold text-purple-600">{item.potential}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-gray-600 mb-1">成功概率</div>
|
||||
<div className="text-2xl font-bold text-green-600">{item.probability}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
239
app/ai-assistant/user-profiling/page.tsx
Normal file
239
app/ai-assistant/user-profiling/page.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Users, Tag, Download, RefreshCw, TrendingUp } from 'lucide-react'
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar } from 'recharts'
|
||||
|
||||
export default function UserProfiling() {
|
||||
const ageDistribution = [
|
||||
{ range: '18-25', count: 856780, percentage: 20 },
|
||||
{ range: '26-35', count: 1500000, percentage: 35 },
|
||||
{ range: '36-45', count: 1285670, percentage: 30 },
|
||||
{ range: '46-55', count: 428567, percentage: 10 },
|
||||
{ range: '55+', count: 214284, percentage: 5 },
|
||||
]
|
||||
|
||||
const behaviorProfile = [
|
||||
{ aspect: '消费能力', value: 85 },
|
||||
{ aspect: '活跃度', value: 72 },
|
||||
{ aspect: '互动频率', value: 68 },
|
||||
{ aspect: '忠诚度', value: 78 },
|
||||
{ aspect: '推荐意愿', value: 62 },
|
||||
{ aspect: '复购率', value: 75 },
|
||||
]
|
||||
|
||||
const segments = [
|
||||
{
|
||||
name: '高净值群体',
|
||||
count: 1284567,
|
||||
characteristics: ['消费能力强', '忠诚度高', '品牌认知强'],
|
||||
value: 580,
|
||||
color: 'purple'
|
||||
},
|
||||
{
|
||||
name: '年轻潮流族',
|
||||
count: 2856780,
|
||||
characteristics: ['追求时尚', '社交活跃', '冲动消费'],
|
||||
value: 280,
|
||||
color: 'blue'
|
||||
},
|
||||
{
|
||||
name: '理性消费者',
|
||||
count: 4285670,
|
||||
characteristics: ['价格敏感', '重视品质', '对比研究'],
|
||||
value: 180,
|
||||
color: 'green'
|
||||
},
|
||||
{
|
||||
name: '价格敏感型',
|
||||
count: 1714283,
|
||||
characteristics: ['促销驱动', '低频复购', '优惠敏感'],
|
||||
value: 85,
|
||||
color: 'yellow'
|
||||
},
|
||||
]
|
||||
|
||||
const interestTags = [
|
||||
{ tag: '美妆护肤', count: 2856780, heat: 95 },
|
||||
{ tag: '时尚穿搭', count: 2145680, heat: 88 },
|
||||
{ tag: '健康养生', count: 1856780, heat: 76 },
|
||||
{ tag: '数码科技', count: 1428567, heat: 72 },
|
||||
{ tag: '母婴育儿', count: 1285670, heat: 68 },
|
||||
{ tag: '美食烹饪', count: 985670, heat: 58 },
|
||||
{ tag: '运动健身', count: 856780, heat: 52 },
|
||||
{ tag: '旅游出行', count: 714283, heat: 45 },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">用户画像</h1>
|
||||
<p className="text-gray-600">智能分析用户群体特征与行为偏好</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
重新分析
|
||||
</Button>
|
||||
<Button>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出画像
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 核心指标 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 mb-6">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">用户群体数</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">128</div>
|
||||
<div className="text-sm text-gray-600">覆盖42.8亿用户</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">标签覆盖率</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">87.5%</div>
|
||||
<Badge variant="outline" className="border-green-500 text-green-600 bg-green-50">优秀</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">画像完整度</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">92.3%</div>
|
||||
<Badge variant="outline" className="border-blue-500 text-blue-600 bg-blue-50">优秀</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-600">更新频率</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-1">实时</div>
|
||||
<div className="text-sm text-gray-600">每小时更新</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 年龄分布与行为画像 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Users className="w-5 h-5" />
|
||||
年龄分布
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={ageDistribution}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||
<XAxis dataKey="range" stroke="#6b7280" />
|
||||
<YAxis stroke="#6b7280" />
|
||||
<Tooltip />
|
||||
<Bar dataKey="count" fill="#8b5cf6" name="用户数" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
行为画像
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<RadarChart data={behaviorProfile}>
|
||||
<PolarGrid stroke="#e5e7eb" />
|
||||
<PolarAngleAxis dataKey="aspect" stroke="#6b7280" />
|
||||
<PolarRadiusAxis stroke="#6b7280" />
|
||||
<Radar name="行为指数" dataKey="value" stroke="#8b5cf6" fill="#8b5cf6" fillOpacity={0.5} />
|
||||
<Tooltip />
|
||||
</RadarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 用户群体细分 */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Users className="w-5 h-5" />
|
||||
用户群体细分
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{segments.map((segment, index) => (
|
||||
<div key={index} className={`p-6 bg-gradient-to-br from-${segment.color}-50 to-${segment.color}-100 rounded-lg border border-${segment.color}-200`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-xl font-bold text-gray-900">{segment.name}</h3>
|
||||
<Badge variant="outline" className={`border-${segment.color}-500 text-${segment.color}-600 bg-${segment.color}-50`}>
|
||||
{segment.count.toLocaleString()} 人
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-2 mb-4">
|
||||
{segment.characteristics.map((char, i) => (
|
||||
<Badge key={i} variant="outline" className="mr-2 border-gray-300 text-gray-700">
|
||||
{char}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
人均价值: <span className="text-lg font-semibold text-gray-900">¥{segment.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 兴趣标签热度 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-gray-900 flex items-center gap-2">
|
||||
<Tag className="w-5 h-5" />
|
||||
兴趣标签热度
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{interestTags.map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-gray-700 font-medium">{item.tag}</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-gray-600">{item.count.toLocaleString()} 人</span>
|
||||
<span className="text-sm font-semibold text-blue-600">热度 {item.heat}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"
|
||||
style={{ width: `${item.heat}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
|
||||
/**
|
||||
* 本地登录 API - 支持邮箱/手机号 + 密码
|
||||
* 当未配置 NEXT_PUBLIC_API_BASE_URL 时使用
|
||||
* 开发账号: zhiqun@qq.com / Zhiqun1984
|
||||
*/
|
||||
const MOCK_USERS: Record<string, { password: string }> = {
|
||||
"zhiqun@qq.com": { password: "Zhiqun1984" },
|
||||
}
|
||||
|
||||
function isEmail(value: string): boolean {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData()
|
||||
const account = (formData.get("email") || formData.get("phone") || "").toString().trim()
|
||||
const password = (formData.get("password") || "").toString()
|
||||
const verificationCode = formData.get("verificationCode")?.toString()
|
||||
|
||||
if (!account) {
|
||||
return NextResponse.json(
|
||||
{ code: 40001, message: "请输入邮箱或手机号" },
|
||||
{ status: 200 }
|
||||
)
|
||||
}
|
||||
|
||||
// 验证码登录:开发环境下任意6位验证码通过
|
||||
if (verificationCode) {
|
||||
if (verificationCode.length >= 4) {
|
||||
const token = `mock_token_${Date.now()}_${account}`
|
||||
return NextResponse.json({
|
||||
code: 10000,
|
||||
message: "登录成功",
|
||||
data: { token },
|
||||
})
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ code: 40002, message: "验证码错误" },
|
||||
{ status: 200 }
|
||||
)
|
||||
}
|
||||
|
||||
// 密码登录
|
||||
if (!password) {
|
||||
return NextResponse.json(
|
||||
{ code: 40003, message: "请输入密码" },
|
||||
{ status: 200 }
|
||||
)
|
||||
}
|
||||
|
||||
const key = isEmail(account) ? account : account
|
||||
const user = MOCK_USERS[key]
|
||||
|
||||
if (user && user.password === password) {
|
||||
const token = `mock_token_${Date.now()}_${account}`
|
||||
return NextResponse.json({
|
||||
code: 10000,
|
||||
message: "登录成功",
|
||||
data: { token },
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ code: 40004, message: "邮箱/手机号或密码错误" },
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("[auth/login]", error)
|
||||
return NextResponse.json(
|
||||
{ code: 50000, message: "服务器错误" },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
|
||||
/**
|
||||
* 本地验证码发送 API (mock)
|
||||
* 开发环境下直接返回成功,验证码可为任意4位以上
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData()
|
||||
const phone = (formData.get("phone") || "").toString().trim()
|
||||
|
||||
if (!phone) {
|
||||
return NextResponse.json(
|
||||
{ code: 40001, message: "请输入手机号" },
|
||||
{ status: 200 }
|
||||
)
|
||||
}
|
||||
|
||||
// Mock: 模拟发送成功,开发时可用 123456 等作为验证码
|
||||
return NextResponse.json({
|
||||
code: 10000,
|
||||
message: "验证码已发送(开发模式:可使用任意4位以上数字)",
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[auth/send-code]", error)
|
||||
return NextResponse.json(
|
||||
{ code: 50000, message: "服务器错误" },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,12 @@
|
||||
/**
|
||||
* RFM 分析 API
|
||||
* 支持 MongoDB 真实数据 + 内存计算
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server"
|
||||
import { analyzeUser, type AnalyzeInput } from "@/services/rfm-engine"
|
||||
import { analyzeUserRFM } from "@/services/rfm-mongodb-service"
|
||||
import { generateText } from "ai"
|
||||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
/**
|
||||
* 可选 AI 标签增强
|
||||
* 可选 AI 标签增强:
|
||||
* - 使用 AI SDK (generateText + openai("gpt-4o")),符合统一标准 [^1]
|
||||
* - 无 OPENAI_API_KEY 时自动跳过,保持稳定
|
||||
*/
|
||||
async function aiTagging(chat_logs?: string[]) {
|
||||
const text = (chat_logs ?? []).slice(0, 8).join("。")
|
||||
@@ -37,38 +33,13 @@ export async function POST(req: Request) {
|
||||
const body = await req.json()
|
||||
const inputs: AnalyzeInput[] = Array.isArray(body) ? body : [body]
|
||||
const useAI = (Array.isArray(body) ? (body as any).useAI : (body as any)?.useAI) ?? false
|
||||
const useMongoData = (body as any)?.useMongoData ?? true
|
||||
|
||||
const results = []
|
||||
|
||||
for (const input of inputs) {
|
||||
let base: any
|
||||
let source = 'memory'
|
||||
|
||||
// 尝试从 MongoDB 获取真实数据
|
||||
if (useMongoData && input.user_id) {
|
||||
// 如果 user_id 是手机号格式,尝试从 MongoDB 查询
|
||||
const phone = input.user_id.replace(/\D/g, '')
|
||||
if (/^1[3-9]\d{9}$/.test(phone)) {
|
||||
const mongoResult = await analyzeUserRFM(phone)
|
||||
if (mongoResult.found) {
|
||||
base = mongoResult.data
|
||||
source = 'mongodb'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果 MongoDB 没有数据,使用内存计算
|
||||
if (!base) {
|
||||
base = analyzeUser(input)
|
||||
source = 'memory'
|
||||
}
|
||||
|
||||
// AI 标签增强
|
||||
const base = analyzeUser(input)
|
||||
if (useAI) {
|
||||
const ai = await aiTagging(input.chat_logs)
|
||||
if (ai) {
|
||||
base.tags = base.tags || {}
|
||||
base.tags.emotion = ai.emotion ?? base.tags.emotion
|
||||
base.tags.intent = ai.intent ?? base.tags.intent
|
||||
if (Array.isArray(ai.behavior)) {
|
||||
@@ -76,55 +47,10 @@ export async function POST(req: Request) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.push({ ...base, source })
|
||||
results.push(base)
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, data: results })
|
||||
|
||||
} catch (e: any) {
|
||||
console.error('RFM analyze error:', e)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: e?.message || "Invalid input"
|
||||
}, { status: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 方法:按手机号查询用户 RFM
|
||||
*/
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const phone = searchParams.get('phone')
|
||||
|
||||
if (!phone) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '请提供手机号参数'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const result = await analyzeUserRFM(phone)
|
||||
|
||||
if (!result.found) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '未找到该用户'
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
source: 'mongodb'
|
||||
})
|
||||
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: e?.message || "查询失败"
|
||||
}, { status: 500 })
|
||||
return NextResponse.json({ success: false, error: e?.message || "Invalid input" }, { status: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +1,12 @@
|
||||
/**
|
||||
* RFM 标签获取 API
|
||||
* 对接 MongoDB 真实数据
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server"
|
||||
import { getRFMTagsDistribution } from "@/services/rfm-mongodb-service"
|
||||
import { getUserTags } from "@/services/rfm-engine"
|
||||
|
||||
/**
|
||||
* GET /api/rfm/get_tags
|
||||
* 获取标签分布或指定用户的标签
|
||||
*/
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const userId = searchParams.get('user_id')
|
||||
|
||||
// 如果指定用户,返回用户标签
|
||||
if (userId) {
|
||||
const userTags = getUserTags(userId)
|
||||
if (userTags) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: userTags.tags,
|
||||
source: 'memory'
|
||||
})
|
||||
}
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '未找到该用户标签'
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
// 否则返回标签分布
|
||||
const distribution = await getRFMTagsDistribution()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: distribution,
|
||||
source: 'mongodb'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Get tags error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
}, { status: 500 })
|
||||
const url = new URL(req.url)
|
||||
const userId = url.searchParams.get("user_id")
|
||||
if (!userId) {
|
||||
return NextResponse.json({ success: false, error: "missing user_id" }, { status: 400 })
|
||||
}
|
||||
const data = getUserTags(userId)
|
||||
return NextResponse.json({ success: true, data })
|
||||
}
|
||||
|
||||
@@ -1,54 +1,7 @@
|
||||
/**
|
||||
* RFM 分组统计 API
|
||||
* 对接 MongoDB 真实数据
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server"
|
||||
import { getMongoRFMGroupSummary } from "@/services/rfm-mongodb-service"
|
||||
import { getGroupSummary } from "@/services/rfm-engine"
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// 尝试从 MongoDB 获取真实数据
|
||||
const mongoData = await getMongoRFMGroupSummary()
|
||||
|
||||
if (mongoData.totalUsers > 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
gradeCount: mongoData.gradeCount,
|
||||
valueCount: mongoData.valueCount,
|
||||
lifecycleCount: {}, // MongoDB 暂无此字段
|
||||
totalUsers: mongoData.totalUsers,
|
||||
avgScore: mongoData.avgScore
|
||||
},
|
||||
source: 'mongodb'
|
||||
})
|
||||
}
|
||||
|
||||
// 回退到内存数据
|
||||
const memData = getGroupSummary()
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: memData,
|
||||
source: 'memory'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('RFM group summary error:', error)
|
||||
|
||||
// 返回默认数据
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
gradeCount: { S: 0, A: 0, B: 0, C: 0, D: 0 },
|
||||
valueCount: { '高': 0, '中': 0, '低': 0 },
|
||||
lifecycleCount: {},
|
||||
totalUsers: 0,
|
||||
avgScore: 0
|
||||
},
|
||||
source: 'fallback',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
}
|
||||
const data = getGroupSummary()
|
||||
return NextResponse.json({ success: true, data })
|
||||
}
|
||||
|
||||
@@ -1,177 +1,52 @@
|
||||
/**
|
||||
* 智能搜索 API 路由
|
||||
* 对接神射手 MongoDB - 跨库查询
|
||||
*/
|
||||
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import {
|
||||
intelligentSearch,
|
||||
queryFullProfile,
|
||||
queryPhoneByQQ,
|
||||
UserValuationDoc
|
||||
} from "@/lib/mongodb"
|
||||
import { getIntelligentSearchService } from "@/services/intelligent-search-service"
|
||||
|
||||
/**
|
||||
* 脱敏手机号
|
||||
*/
|
||||
function maskPhone(phone: string | undefined): string {
|
||||
if (!phone) return ''
|
||||
if (phone.length !== 11) return phone
|
||||
return `${phone.slice(0, 3)}****${phone.slice(-4)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换搜索结果
|
||||
*/
|
||||
function transformSearchResult(doc: UserValuationDoc, queryType: string): any {
|
||||
const name = doc.name || '未知用户'
|
||||
return {
|
||||
id: doc._id?.toString(),
|
||||
type: 'user',
|
||||
title: name,
|
||||
subtitle: doc.phone_masked || maskPhone(doc.phone),
|
||||
description: `${doc.province || ''}${doc.city || ''} | ${doc.user_level || '未分级'} | RFM: ${doc.rfm_composite_score?.toFixed(2) || 'N/A'}`,
|
||||
data: {
|
||||
phone: doc.phone,
|
||||
phone_masked: doc.phone_masked || maskPhone(doc.phone),
|
||||
name: doc.name,
|
||||
province: doc.province,
|
||||
city: doc.city,
|
||||
userLevel: doc.user_level,
|
||||
rfmScore: doc.rfm_composite_score,
|
||||
tags: doc.tags || [],
|
||||
email: doc.email
|
||||
},
|
||||
matchedBy: queryType,
|
||||
relevanceScore: doc.rfm_composite_score || 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/search
|
||||
* 智能搜索
|
||||
*/
|
||||
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 })
|
||||
return NextResponse.json({ error: "搜索查询不能为空" }, { status: 400 })
|
||||
}
|
||||
|
||||
// 执行智能搜索
|
||||
const result = await intelligentSearch(query, { limit, offset })
|
||||
|
||||
// 转换结果
|
||||
const items = result.users.map(doc => transformSearchResult(doc, result.queryType))
|
||||
|
||||
// 如果是 QQ 查询,补充 QQ 信息
|
||||
if (result.queryType === 'qq' && result.users.length > 0) {
|
||||
const qqInfo = await queryPhoneByQQ(query.trim())
|
||||
if (qqInfo) {
|
||||
items[0].data.qq = qqInfo.qq
|
||||
items[0].data.qqScore = qqInfo.QQ号评分
|
||||
items[0].data.carrier = qqInfo.运营商
|
||||
}
|
||||
}
|
||||
const searchService = getIntelligentSearchService()
|
||||
|
||||
return NextResponse.json({
|
||||
query,
|
||||
queryType: result.queryType,
|
||||
total: result.total,
|
||||
items,
|
||||
pagination: {
|
||||
limit,
|
||||
offset,
|
||||
hasMore: offset + items.length < result.total
|
||||
}
|
||||
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: "搜索失败,请稍后重试",
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
}, { status: 500 })
|
||||
return NextResponse.json({ error: "搜索失败,请稍后重试" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/search
|
||||
* 高级搜索(支持更多参数)
|
||||
* 返回格式适配前端 SearchResponse 接口
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const startTime = Date.now()
|
||||
const body = await request.json()
|
||||
const { query, type = "all", options = {} } = body
|
||||
|
||||
if (!query || !query.trim()) {
|
||||
return NextResponse.json({
|
||||
error: "搜索查询不能为空"
|
||||
}, { status: 400 })
|
||||
return NextResponse.json({ error: "搜索查询不能为空" }, { status: 400 })
|
||||
}
|
||||
|
||||
const limit = options.limit || 50
|
||||
const offset = options.offset || 0
|
||||
|
||||
const result = await intelligentSearch(query, { limit, offset })
|
||||
|
||||
// 转换为前端期望的格式
|
||||
const results = result.users.map(doc => {
|
||||
const name = doc.name || '未知用户'
|
||||
return {
|
||||
id: doc._id?.toString() || '',
|
||||
type: 'user' as const,
|
||||
title: name,
|
||||
description: `${doc.province || ''}${doc.city || ''} | 估值: ${doc.user_evaluation_score || 'N/A'}`,
|
||||
tags: doc.tags || [],
|
||||
relevanceScore: doc.user_evaluation_score || 0,
|
||||
updatedAt: doc.computed_at?.toISOString() || new Date().toISOString(),
|
||||
metadata: {
|
||||
phone: doc.phone,
|
||||
phone_masked: doc.phone_masked || maskPhone(doc.phone),
|
||||
province: doc.province,
|
||||
city: doc.city,
|
||||
gender: doc.gender,
|
||||
age_range: doc.age_range,
|
||||
userLevel: doc.user_level,
|
||||
rfmScore: doc.rfm_composite_score,
|
||||
evaluationScore: doc.user_evaluation_score,
|
||||
dataQuality: doc.data_quality
|
||||
}
|
||||
}
|
||||
})
|
||||
const searchService = getIntelligentSearchService()
|
||||
const results = await searchService.search(query, type, options)
|
||||
|
||||
const queryTime = Date.now() - startTime
|
||||
|
||||
// 返回前端期望的 SearchResponse 格式
|
||||
return NextResponse.json({
|
||||
results,
|
||||
stats: {
|
||||
totalResults: result.total,
|
||||
queryTime,
|
||||
suggestions: [],
|
||||
filters: {
|
||||
queryType: result.queryType
|
||||
}
|
||||
},
|
||||
hasMore: offset + results.length < result.total
|
||||
})
|
||||
|
||||
return NextResponse.json(results)
|
||||
} catch (error) {
|
||||
console.error("搜索API错误:", error)
|
||||
return NextResponse.json({
|
||||
error: "搜索失败,请稍后重试",
|
||||
results: [],
|
||||
stats: { totalResults: 0, queryTime: 0, suggestions: [], filters: {} },
|
||||
hasMore: false
|
||||
}, { status: 500 })
|
||||
return NextResponse.json({ error: "搜索失败,请稍后重试" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,78 +1,18 @@
|
||||
/**
|
||||
* 系统状态 API 路由
|
||||
* 返回 MongoDB 数据库真实状态
|
||||
*/
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import { getMindsDBConnector } from "@/lib/mindsdb-connector"
|
||||
|
||||
import { NextResponse } from "next/server"
|
||||
import { getDatabaseStats, healthCheck } from "@/lib/mongodb"
|
||||
|
||||
/**
|
||||
* GET /api/system-status
|
||||
* 获取系统状态
|
||||
*/
|
||||
export async function GET() {
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// 健康检查
|
||||
const health = await healthCheck()
|
||||
|
||||
if (!health.mongodb) {
|
||||
return NextResponse.json({
|
||||
status: 'error',
|
||||
connected: false,
|
||||
latencyMs: health.latencyMs,
|
||||
error: health.error || 'MongoDB 连接失败',
|
||||
databases: [],
|
||||
totalDocuments: 0,
|
||||
totalSizeGB: 0,
|
||||
lastCheck: new Date().toISOString()
|
||||
}, { status: 503 })
|
||||
}
|
||||
|
||||
// 获取数据库统计
|
||||
const stats = await getDatabaseStats()
|
||||
|
||||
return NextResponse.json({
|
||||
status: 'healthy',
|
||||
connected: stats.connected,
|
||||
latencyMs: health.latencyMs,
|
||||
databases: stats.databases,
|
||||
totalDocuments: stats.totalDocuments,
|
||||
totalSizeGB: stats.totalSizeGB,
|
||||
lastCheck: new Date().toISOString(),
|
||||
// 格式化显示
|
||||
summary: {
|
||||
userCount: formatNumber(stats.totalDocuments),
|
||||
dataSize: `${stats.totalSizeGB} GB`,
|
||||
dbCount: stats.databases.length,
|
||||
responseTime: `${health.latencyMs}ms`
|
||||
}
|
||||
}, {
|
||||
headers: { 'Cache-Control': 'no-store, max-age=0' }
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('System status error:', error)
|
||||
return NextResponse.json({
|
||||
status: 'error',
|
||||
connected: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
lastCheck: new Date().toISOString()
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
const mindsDB = getMindsDBConnector()
|
||||
const status = await mindsDB.getSystemStatus()
|
||||
|
||||
/**
|
||||
* 格式化数字显示
|
||||
*/
|
||||
function formatNumber(num: number): string {
|
||||
if (num >= 1000000000) {
|
||||
return `${(num / 1000000000).toFixed(2)}B`
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
status,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("系统状态API错误:", error)
|
||||
return NextResponse.json({ error: "获取系统状态失败" }, { status: 500 })
|
||||
}
|
||||
if (num >= 1000000) {
|
||||
return `${(num / 1000000).toFixed(1)}M`
|
||||
}
|
||||
if (num >= 1000) {
|
||||
return `${(num / 1000).toFixed(1)}K`
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
@@ -1,186 +1,233 @@
|
||||
/**
|
||||
* 用户 API 路由
|
||||
* 对接神射手 MongoDB 数据库 - KR.用户估值
|
||||
*/
|
||||
import { NextResponse, type NextRequest } from "next/server"
|
||||
import type { TrafficUser } from "@/types/traffic"
|
||||
import { addUser, getUserById, queryUsers, type UserStatus } from "@/lib/mock-users"
|
||||
|
||||
import { NextResponse, NextRequest } from "next/server"
|
||||
import {
|
||||
queryUserList,
|
||||
queryUserByPhone,
|
||||
queryFullProfile,
|
||||
UserValuationDoc
|
||||
} from "@/lib/mongodb"
|
||||
// 中文名字生成器数据
|
||||
const familyNames = [
|
||||
"张",
|
||||
"王",
|
||||
"李",
|
||||
"赵",
|
||||
"陈",
|
||||
"刘",
|
||||
"杨",
|
||||
"黄",
|
||||
"周",
|
||||
"吴",
|
||||
"朱",
|
||||
"孙",
|
||||
"马",
|
||||
"胡",
|
||||
"郭",
|
||||
"林",
|
||||
"何",
|
||||
"高",
|
||||
"梁",
|
||||
"郑",
|
||||
"罗",
|
||||
"宋",
|
||||
"谢",
|
||||
"唐",
|
||||
"韩",
|
||||
"曹",
|
||||
"许",
|
||||
"邓",
|
||||
"萧",
|
||||
"冯",
|
||||
]
|
||||
const givenNames1 = [
|
||||
"志",
|
||||
"建",
|
||||
"文",
|
||||
"明",
|
||||
"永",
|
||||
"春",
|
||||
"秀",
|
||||
"金",
|
||||
"水",
|
||||
"玉",
|
||||
"国",
|
||||
"立",
|
||||
"德",
|
||||
"海",
|
||||
"和",
|
||||
"荣",
|
||||
"伟",
|
||||
"新",
|
||||
"英",
|
||||
"佳",
|
||||
]
|
||||
const givenNames2 = [
|
||||
"华",
|
||||
"平",
|
||||
"军",
|
||||
"强",
|
||||
"辉",
|
||||
"敏",
|
||||
"峰",
|
||||
"磊",
|
||||
"超",
|
||||
"艳",
|
||||
"娜",
|
||||
"霞",
|
||||
"燕",
|
||||
"娟",
|
||||
"静",
|
||||
"丽",
|
||||
"涛",
|
||||
"洋",
|
||||
"勇",
|
||||
"龙",
|
||||
]
|
||||
|
||||
/**
|
||||
* 脱敏手机号
|
||||
*/
|
||||
function maskPhone(phone: string | undefined): string {
|
||||
if (!phone) return ''
|
||||
if (phone.length !== 11) return phone
|
||||
return `${phone.slice(0, 3)}****${phone.slice(-4)}`
|
||||
}
|
||||
// 生成固定的用户数据池
|
||||
const userPool: TrafficUser[] = Array.from({ length: 1610 }, (_, i) => {
|
||||
const familyName = familyNames[Math.floor(Math.random() * familyNames.length)]
|
||||
const givenName1 = givenNames1[Math.floor(Math.random() * givenNames1.length)]
|
||||
const givenName2 = givenNames2[Math.floor(Math.random() * givenNames2.length)]
|
||||
const fullName = Math.random() > 0.5 ? familyName + givenName1 + givenName2 : familyName + givenName1
|
||||
|
||||
// 生成随机时间(在过去7天内)
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - Math.floor(Math.random() * 7))
|
||||
|
||||
/**
|
||||
* 转换用户数据格式(适配前端)
|
||||
*/
|
||||
function transformUser(doc: UserValuationDoc, index: number = 0): any {
|
||||
const name = doc.name || '未知用户'
|
||||
return {
|
||||
id: doc._id?.toString() || `user-${index}`,
|
||||
avatar: `/placeholder.svg?height=40&width=40&text=${name[0] || 'U'}`,
|
||||
nickname: name,
|
||||
wechatId: doc.phone ? `wxid_${doc.phone.slice(-8)}` : '',
|
||||
phone: doc.phone || '',
|
||||
phone_masked: doc.phone_masked || maskPhone(doc.phone),
|
||||
region: doc.province && doc.city ? `${doc.province}${doc.city}` : (doc.province || '未知'),
|
||||
note: '',
|
||||
status: 'added' as const,
|
||||
addTime: doc.created_at?.toISOString() || new Date().toISOString(),
|
||||
source: (doc.source_channels && doc.source_channels[0]) || '神射手',
|
||||
assignedTo: '',
|
||||
category: 'customer' as const,
|
||||
tags: doc.tags || [],
|
||||
// RFM 数据
|
||||
userLevel: doc.user_level || 'D',
|
||||
rfmScore: doc.rfm_composite_score || 0,
|
||||
rfmR: doc.rfm_r_score,
|
||||
rfmF: doc.rfm_f_score,
|
||||
rfmM: doc.rfm_m_score,
|
||||
totalAmount: doc.total_amount || 0,
|
||||
orderCount: doc.order_count || 0,
|
||||
// 额外信息
|
||||
email: doc.email,
|
||||
address: doc.address,
|
||||
province: doc.province,
|
||||
city: doc.city,
|
||||
id: `${Date.now()}-${i}`,
|
||||
avatar: `/placeholder.svg?height=40&width=40&text=${fullName[0]}`,
|
||||
nickname: fullName,
|
||||
wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`,
|
||||
phone: `1${["3", "5", "7", "8", "9"][Math.floor(Math.random() * 5)]}${Array.from({ length: 9 }, () => Math.floor(Math.random() * 10)).join("")}`,
|
||||
region: [
|
||||
"广东深圳",
|
||||
"浙江杭州",
|
||||
"江苏苏州",
|
||||
"北京",
|
||||
"上海",
|
||||
"四川成都",
|
||||
"湖北武汉",
|
||||
"福建厦门",
|
||||
"山东青岛",
|
||||
"河南郑州",
|
||||
][Math.floor(Math.random() * 10)],
|
||||
note: [
|
||||
"咨询产品价格",
|
||||
"对产品很感兴趣",
|
||||
"准备购买",
|
||||
"需要更多信息",
|
||||
"想了解优惠活动",
|
||||
"询问产品规格",
|
||||
"要求产品demo",
|
||||
"索要产品目录",
|
||||
"询问售后服务",
|
||||
"要求上门演示",
|
||||
][Math.floor(Math.random() * 10)],
|
||||
status: ["pending", "added", "failed"][Math.floor(Math.random() * 3)] as TrafficUser["status"],
|
||||
addTime: date.toISOString(),
|
||||
source: ["抖音直播", "小红书", "微信朋友圈", "视频号", "公众号", "个人主页"][Math.floor(Math.random() * 6)],
|
||||
assignedTo: "",
|
||||
category: ["potential", "customer", "lost"][Math.floor(Math.random() * 3)] as TrafficUser["category"],
|
||||
tags: [],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /api/users
|
||||
* 查询用户列表或单个用户详情
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
|
||||
// 单用户详情查询(按ID或手机号)
|
||||
const id = searchParams.get('id')
|
||||
const phone = searchParams.get('phone')
|
||||
|
||||
if (id || phone) {
|
||||
// 如果是手机号格式,按手机号查询
|
||||
const queryPhone = phone || (id && /^1[3-9]\d{9}$/.test(id) ? id : null)
|
||||
|
||||
if (queryPhone) {
|
||||
// 完整画像查询(跨库)
|
||||
const profile = await queryFullProfile(queryPhone)
|
||||
|
||||
if (profile.valuation) {
|
||||
const user = transformUser(profile.valuation)
|
||||
|
||||
// 补充 QQ 信息
|
||||
if (profile.qq) {
|
||||
user.qq = profile.qq.qq
|
||||
user.qqScore = profile.qq.QQ号评分
|
||||
user.phoneScore = profile.qq.手机号评分
|
||||
}
|
||||
|
||||
// 补充存客宝信息
|
||||
if (profile.ckb) {
|
||||
user.wechat = profile.ckb.social_accounts?.wechat
|
||||
user.trafficPool = profile.ckb.traffic_pool?.pool_name
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
data: user,
|
||||
sources: {
|
||||
valuation: !!profile.valuation,
|
||||
qq: !!profile.qq,
|
||||
ckb: !!profile.ckb
|
||||
}
|
||||
}, { headers: { 'Cache-Control': 'no-store' } })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
data: null,
|
||||
error: '未找到该用户'
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
data: null,
|
||||
error: '无效的查询参数'
|
||||
}, { status: 400 })
|
||||
// 计算今日新增数量
|
||||
const todayStart = new Date()
|
||||
todayStart.setHours(0, 0, 0, 0)
|
||||
const todayUsers = userPool.filter((user) => new Date(user.addTime) >= todayStart)
|
||||
|
||||
// 生成微信好友数据池
|
||||
const generateWechatFriends = (wechatId: string, count: number) => {
|
||||
return Array.from({ length: count }, (_, i) => {
|
||||
const familyName = familyNames[Math.floor(Math.random() * familyNames.length)]
|
||||
const givenName1 = givenNames1[Math.floor(Math.random() * givenNames1.length)]
|
||||
const givenName2 = givenNames2[Math.floor(Math.random() * givenNames2.length)]
|
||||
const fullName = Math.random() > 0.5 ? familyName + givenName1 + givenName2 : familyName + givenName1
|
||||
|
||||
// 生成随机时间(在过去30天内)
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - Math.floor(Math.random() * 30))
|
||||
|
||||
return {
|
||||
id: `wechat-${wechatId}-${i}`,
|
||||
avatar: `/placeholder.svg?height=40&width=40&text=${fullName[0]}`,
|
||||
nickname: fullName,
|
||||
wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`,
|
||||
phone: `1${["3", "5", "7", "8", "9"][Math.floor(Math.random() * 5)]}${Array.from({ length: 9 }, () => Math.floor(Math.random() * 10)).join("")}`,
|
||||
region: [
|
||||
"广东深圳",
|
||||
"浙江杭州",
|
||||
"江苏苏州",
|
||||
"北京",
|
||||
"上海",
|
||||
"四川成都",
|
||||
"湖北武汉",
|
||||
"福建厦门",
|
||||
"山东青岛",
|
||||
"河南郑州",
|
||||
][Math.floor(Math.random() * 10)],
|
||||
note: [
|
||||
"咨询产品价格",
|
||||
"对产品很感兴趣",
|
||||
"准备购买",
|
||||
"需要更多信息",
|
||||
"想了解优惠活动",
|
||||
"询问产品规格",
|
||||
"要求产品demo",
|
||||
"索要产品目录",
|
||||
"询问售后服务",
|
||||
"要求上门演示",
|
||||
][Math.floor(Math.random() * 10)],
|
||||
status: ["pending", "added", "failed"][Math.floor(Math.random() * 3)] as TrafficUser["status"],
|
||||
addTime: date.toISOString(),
|
||||
source: ["抖音直播", "小红书", "微信朋友圈", "视频号", "公众号", "个人主页", "微信好友"][
|
||||
Math.floor(Math.random() * 7)
|
||||
],
|
||||
assignedTo: "",
|
||||
category: ["potential", "customer", "lost"][Math.floor(Math.random() * 3)] as TrafficUser["category"],
|
||||
tags: [],
|
||||
}
|
||||
|
||||
// 列表查询
|
||||
const q = searchParams.get('q') || undefined
|
||||
const tagsStr = searchParams.get('tags') || ''
|
||||
const userLevel = searchParams.get('userLevel') || searchParams.get('status') || undefined
|
||||
const rfmMin = searchParams.get('rfmMin') ? Number(searchParams.get('rfmMin')) : undefined
|
||||
const rfmMax = searchParams.get('rfmMax') ? Number(searchParams.get('rfmMax')) : undefined
|
||||
const page = Number(searchParams.get('page') ?? 1)
|
||||
const pageSize = Number(searchParams.get('pageSize') ?? 20)
|
||||
|
||||
const tags = tagsStr ? tagsStr.split(',').filter(Boolean) : undefined
|
||||
|
||||
const result = await queryUserList({
|
||||
page,
|
||||
pageSize,
|
||||
userLevel,
|
||||
minRfm: rfmMin,
|
||||
maxRfm: rfmMax,
|
||||
search: q,
|
||||
tags
|
||||
})
|
||||
|
||||
const transformedData = result.data.map((doc, i) => transformUser(doc, i))
|
||||
|
||||
return NextResponse.json({
|
||||
data: transformedData,
|
||||
total: result.total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(result.total / pageSize)
|
||||
}, { headers: { 'Cache-Control': 'no-store' } })
|
||||
|
||||
} catch (error) {
|
||||
console.error('Users API error:', error)
|
||||
|
||||
// 数据库连接失败时返回模拟数据
|
||||
return NextResponse.json({
|
||||
data: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
totalPages: 0,
|
||||
error: error instanceof Error ? error.message : '查询失败',
|
||||
fallback: true
|
||||
}, {
|
||||
status: 500,
|
||||
headers: { 'Cache-Control': 'no-store' }
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/users
|
||||
* 创建用户(预留接口)
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json().catch(() => ({}))
|
||||
|
||||
// TODO: 实现用户创建逻辑
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '用户创建功能暂未开放'
|
||||
}, { status: 501 })
|
||||
|
||||
} catch (error) {
|
||||
return NextResponse.json({
|
||||
error: error instanceof Error ? error.message : '创建失败'
|
||||
}, { status: 500 })
|
||||
}
|
||||
// 微信好友数据缓存
|
||||
const wechatFriendsCache = new Map<string, TrafficUser[]>()
|
||||
|
||||
function parseArrayParam(v: string | null) {
|
||||
if (!v) return []
|
||||
return v
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const { searchParams } = new URL(req.url)
|
||||
|
||||
// 详情优先
|
||||
const id = searchParams.get("id")
|
||||
if (id) {
|
||||
const detail = getUserById(id)
|
||||
return NextResponse.json({ data: detail }, { headers: { "Cache-Control": "no-store" } })
|
||||
}
|
||||
|
||||
// 列表
|
||||
const q = searchParams.get("q") ?? undefined
|
||||
const tagsStr = searchParams.get("tags") ?? ""
|
||||
const statusStr = searchParams.get("status") ?? ""
|
||||
const rfmMin = Number(searchParams.get("rfmMin") ?? 0)
|
||||
const rfmMax = Number(searchParams.get("rfmMax") ?? 100)
|
||||
const page = Number(searchParams.get("page") ?? 1)
|
||||
const pageSize = Number(searchParams.get("pageSize") ?? 20)
|
||||
|
||||
const tags = tagsStr ? tagsStr.split(",").filter(Boolean) : undefined
|
||||
const status = statusStr ? (statusStr.split(",").filter(Boolean) as UserStatus[]) : undefined
|
||||
|
||||
const result = queryUsers({ q, tags, status, rfmMin, rfmMax, page, pageSize })
|
||||
return NextResponse.json(result, { headers: { "Cache-Control": "no-store" } })
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.json().catch(() => ({}))
|
||||
const created = addUser(body ?? {})
|
||||
return NextResponse.json({ data: created }, { status: 201 })
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
159
app/globals.css
159
app/globals.css
@@ -4,14 +4,14 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 240 10% 98%;
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--card: 240 10% 100%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--popover: 240 10% 100%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 240 5.9% 98%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
@@ -23,26 +23,26 @@
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 5.9% 10%;
|
||||
--radius: 0.75rem;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 240 5.9% 98%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 240 5.9% 98%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 240 5.9% 98%;
|
||||
--primary: 240 5.9% 98%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 240 5.9% 98%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 240 5.9% 98%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 240 5.9% 98%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
@@ -53,140 +53,7 @@
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
html {
|
||||
@apply scroll-smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-gradient-to-br from-blue-50 via-white to-purple-50 text-foreground min-h-screen;
|
||||
background-attachment: fixed;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.dark body {
|
||||
@apply bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
background-attachment: scroll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.glass {
|
||||
@apply backdrop-blur-md border border-white/20;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
|
||||
}
|
||||
|
||||
.glass-light {
|
||||
@apply backdrop-blur-sm border border-white/30;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 4px 16px 0 rgba(31, 38, 135, 0.2);
|
||||
}
|
||||
|
||||
.glass-heavy {
|
||||
@apply backdrop-blur-xl border border-white/40;
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 16px 64px 0 rgba(31, 38, 135, 0.5);
|
||||
}
|
||||
|
||||
.glass-dark {
|
||||
@apply backdrop-blur-md border border-white/10;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.glass {
|
||||
@apply backdrop-blur-sm;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.glass-light {
|
||||
@apply backdrop-blur-sm;
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.glass-heavy {
|
||||
@apply backdrop-blur-md;
|
||||
background-color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
@apply glass rounded-2xl p-6 transition-all duration-300;
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.glass-card {
|
||||
@apply p-4 rounded-xl;
|
||||
}
|
||||
}
|
||||
|
||||
.glass-nav {
|
||||
@apply glass-light rounded-2xl transition-all duration-300;
|
||||
}
|
||||
|
||||
.glass-button {
|
||||
@apply glass-light rounded-xl px-4 py-2 transition-all duration-300;
|
||||
}
|
||||
|
||||
.glass-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.glass-button {
|
||||
@apply px-6 py-3 text-base;
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.glass-input {
|
||||
@apply glass-light rounded-xl px-4 py-2 transition-all duration-300;
|
||||
}
|
||||
|
||||
.glass-input:focus {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
@apply ring-2 ring-white/50;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.glass-input {
|
||||
@apply px-4 py-3 text-base;
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.safe-area-top {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
}
|
||||
|
||||
.safe-area-bottom {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.safe-area-left {
|
||||
padding-left: env(safe-area-inset-left);
|
||||
}
|
||||
|
||||
.safe-area-right {
|
||||
padding-right: env(safe-area-inset-right);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.overflow-scroll {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type React from "react"
|
||||
import type { Metadata } from "next"
|
||||
import { Inter } from "next/font/google"
|
||||
import ClientLayout from "./ClientLayout"
|
||||
import "./globals.css"
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] })
|
||||
|
||||
@@ -18,6 +19,3 @@ export default function RootLayout({
|
||||
}) {
|
||||
return <ClientLayout>{children}</ClientLayout>
|
||||
}
|
||||
|
||||
|
||||
import './globals.css'
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type React from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { Eye, EyeOff, Mail, Phone } from "lucide-react"
|
||||
import { Eye, EyeOff, Phone } from "lucide-react"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
@@ -12,8 +12,8 @@ import { WeChatIcon } from "@/components/icons/wechat-icon"
|
||||
import { AppleIcon } from "@/components/icons/apple-icon"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
|
||||
// 使用环境变量:不配置则用本地 API(支持 zhiqun@qq.com / Zhiqun1984)
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? ""
|
||||
// 使用环境变量获取API域名
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.example.com"
|
||||
|
||||
// 定义登录响应类型
|
||||
interface LoginResponse {
|
||||
@@ -25,7 +25,7 @@ interface LoginResponse {
|
||||
}
|
||||
|
||||
interface LoginForm {
|
||||
account: string // 邮箱或手机号
|
||||
phone: string
|
||||
password: string
|
||||
verificationCode: string
|
||||
agreeToTerms: boolean
|
||||
@@ -36,7 +36,7 @@ export default function LoginPage() {
|
||||
const [activeTab, setActiveTab] = useState<"password" | "verification">("password")
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [form, setForm] = useState<LoginForm>({
|
||||
account: "",
|
||||
phone: "",
|
||||
password: "",
|
||||
verificationCode: "",
|
||||
agreeToTerms: false,
|
||||
@@ -55,11 +55,11 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
const validateForm = () => {
|
||||
if (!form.account.trim()) {
|
||||
if (!form.phone) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "请输入账号",
|
||||
description: "请输入邮箱或手机号",
|
||||
title: "请输入手机号",
|
||||
description: "手机号不能为空",
|
||||
})
|
||||
return false
|
||||
}
|
||||
@@ -101,9 +101,9 @@ export default function LoginPage() {
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
// 创建FormData对象
|
||||
const formData = new FormData()
|
||||
const isEmail = form.account.includes("@")
|
||||
formData.append(isEmail ? "email" : "phone", form.account)
|
||||
formData.append("phone", form.phone)
|
||||
|
||||
if (activeTab === "password") {
|
||||
formData.append("password", form.password)
|
||||
@@ -111,8 +111,8 @@ export default function LoginPage() {
|
||||
formData.append("verificationCode", form.verificationCode)
|
||||
}
|
||||
|
||||
const apiUrl = API_BASE_URL ? `${API_BASE_URL}/auth/login` : "/api/auth/login"
|
||||
const response = await fetch(apiUrl, {
|
||||
// 发送登录请求
|
||||
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
// 不需要设置Content-Type,浏览器会自动设置为multipart/form-data并添加boundary
|
||||
@@ -125,7 +125,7 @@ export default function LoginPage() {
|
||||
localStorage.setItem("token", result.data.token)
|
||||
|
||||
// 成功后跳转
|
||||
router.push("/")
|
||||
router.push("/profile")
|
||||
|
||||
toast({
|
||||
title: "登录成功",
|
||||
@@ -146,7 +146,7 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
const handleSendVerificationCode = async () => {
|
||||
if (!form.account.trim()) {
|
||||
if (!form.phone) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "请输入手机号",
|
||||
@@ -157,11 +157,12 @@ export default function LoginPage() {
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
// 创建FormData对象
|
||||
const formData = new FormData()
|
||||
formData.append("phone", form.account)
|
||||
formData.append("phone", form.phone)
|
||||
|
||||
const apiUrl = API_BASE_URL ? `${API_BASE_URL}/auth/send-code` : "/api/auth/send-code"
|
||||
const response = await fetch(apiUrl, {
|
||||
// 发送验证码请求
|
||||
const response = await fetch(`${API_BASE_URL}/auth/send-code`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
@@ -219,25 +220,22 @@ export default function LoginPage() {
|
||||
</TabsList>
|
||||
|
||||
<div className="mt-8">
|
||||
<p className="text-gray-600 mb-6">支持 邮箱 / 手机号 / 微信 / Apple 登录</p>
|
||||
<p className="text-gray-600 mb-6">你所在地区仅支持 手机号 / 微信 / Apple 登录</p>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="text"
|
||||
name="account"
|
||||
value={form.account}
|
||||
type="tel"
|
||||
name="phone"
|
||||
value={form.phone}
|
||||
onChange={handleInputChange}
|
||||
placeholder="邮箱或手机号"
|
||||
className="pl-12 border-gray-300 text-gray-900 h-12"
|
||||
placeholder="手机号"
|
||||
className="pl-16 border-gray-300 text-gray-900 h-12"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500">
|
||||
{form.account.includes("@") ? (
|
||||
<Mail className="h-4 w-4" />
|
||||
) : (
|
||||
<Phone className="h-4 w-4" />
|
||||
)}
|
||||
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500 flex items-center gap-1">
|
||||
<Phone className="h-4 w-4" />
|
||||
+86
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
603
app/page.tsx
603
app/page.tsx
@@ -1,373 +1,280 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Search, Users, TrendingUp, Database, RefreshCw, BarChart3, Activity, Globe } from 'lucide-react'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Users, Database, Activity, DollarSign, TrendingUp, Search, ArrowUpRight, Zap } from "lucide-react"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Toaster } from "@/components/ui/toaster"
|
||||
import UserSearch from '@/components/home/user-search'
|
||||
import UserList from '@/components/home/user-list'
|
||||
import Link from "next/link"
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
} from "recharts"
|
||||
|
||||
interface SystemStats {
|
||||
userCount: number
|
||||
keywordCount: number
|
||||
versionCount: number
|
||||
avgResponseTime: number
|
||||
cacheSize: number
|
||||
connected: boolean
|
||||
}
|
||||
// 模拟数据
|
||||
const growthData = [
|
||||
{ month: "1月", users: 3200, value: 420 },
|
||||
{ month: "2月", users: 3800, value: 480 },
|
||||
{ month: "3月", users: 4100, value: 520 },
|
||||
{ month: "4月", users: 4600, value: 580 },
|
||||
{ month: "5月", users: 5200, value: 650 },
|
||||
{ month: "6月", users: 5800, value: 720 },
|
||||
]
|
||||
|
||||
interface GrowthData {
|
||||
period: string
|
||||
userGrowth: number
|
||||
dataGrowth: number
|
||||
activeUsers: number
|
||||
}
|
||||
const valueDistribution = [
|
||||
{ name: "S级", value: 5, color: "#8B5CF6" },
|
||||
{ name: "A级", value: 15, color: "#3B82F6" },
|
||||
{ name: "B级", value: 35, color: "#10B981" },
|
||||
{ name: "C级", value: 30, color: "#F59E0B" },
|
||||
{ name: "D级", value: 15, color: "#EF4444" },
|
||||
]
|
||||
|
||||
function StatCard({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-white p-3">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-xs">{icon}<span>{label}</span></div>
|
||||
<div className="mt-1 text-lg font-semibold">{value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const recentActivities = [
|
||||
{ id: 1, user: "张伟", action: "完成首次购买", time: "2分钟前", type: "purchase" },
|
||||
{ id: 2, user: "李娜", action: "升级为A级用户", time: "5分钟前", type: "upgrade" },
|
||||
{ id: 3, user: "王芳", action: "触发流失预警", time: "8分钟前", type: "warning" },
|
||||
{ id: 4, user: "刘洋", action: "完成问卷调查", time: "12分钟前", type: "survey" },
|
||||
{ id: 5, user: "陈明", action: "加入VIP计划", time: "15分钟前", type: "vip" },
|
||||
]
|
||||
|
||||
export default function OverviewPage() {
|
||||
const router = useRouter()
|
||||
export default function HomePage() {
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [systemStats, setSystemStats] = useState<SystemStats>({
|
||||
userCount: 4000000000,
|
||||
keywordCount: 150000,
|
||||
versionCount: 25,
|
||||
avgResponseTime: 120,
|
||||
cacheSize: 0,
|
||||
connected: true,
|
||||
})
|
||||
const [growthData, setGrowthData] = useState<GrowthData[]>([
|
||||
{ period: "今日", userGrowth: 2.3, dataGrowth: 1.8, activeUsers: 85600000 },
|
||||
{ period: "本周", userGrowth: 12.5, dataGrowth: 8.9, activeUsers: 520000000 },
|
||||
{ period: "本月", userGrowth: 45.2, dataGrowth: 32.1, activeUsers: 1200000000 },
|
||||
])
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [lastUpdate, setLastUpdate] = useState(new Date())
|
||||
const [currentTime, setCurrentTime] = useState(new Date())
|
||||
|
||||
// 自动刷新数据
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
refreshData()
|
||||
}, 30000) // 30秒刷新一次
|
||||
|
||||
return () => clearInterval(interval)
|
||||
const timer = setInterval(() => setCurrentTime(new Date()), 1000)
|
||||
return () => clearInterval(timer)
|
||||
}, [])
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = async () => {
|
||||
setIsRefreshing(true)
|
||||
try {
|
||||
// 模拟数据更新
|
||||
setSystemStats((prev) => ({
|
||||
...prev,
|
||||
userCount: prev.userCount + Math.floor(Math.random() * 1000),
|
||||
avgResponseTime: Math.floor(Math.random() * 50) + 100,
|
||||
}))
|
||||
|
||||
setGrowthData((prev) =>
|
||||
prev.map((item) => ({
|
||||
...item,
|
||||
userGrowth: item.userGrowth + (Math.random() - 0.5) * 0.5,
|
||||
dataGrowth: item.dataGrowth + (Math.random() - 0.5) * 0.3,
|
||||
activeUsers: Math.floor(item.activeUsers * (1 + (Math.random() - 0.5) * 0.01)),
|
||||
})),
|
||||
)
|
||||
|
||||
setLastUpdate(new Date())
|
||||
} catch (error) {
|
||||
console.error("刷新数据失败:", error)
|
||||
} finally {
|
||||
setIsRefreshing(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
if (searchQuery.trim()) {
|
||||
router.push(`/intelligent-search?q=${encodeURIComponent(searchQuery)}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化数字显示
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000000) {
|
||||
return `${(num / 1000000000).toFixed(1)}B`
|
||||
}
|
||||
if (num >= 1000000) {
|
||||
return `${(num / 1000000).toFixed(1)}M`
|
||||
}
|
||||
if (num >= 1000) {
|
||||
return `${(num / 1000).toFixed(1)}K`
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
{/* 页面标题和搜索 */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">概览</h1>
|
||||
<p className="text-gray-600">数据资产中台</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Badge variant="outline" className="text-green-600 border-green-200">
|
||||
<Activity className="w-3 h-3 mr-1" />
|
||||
系统正常
|
||||
</Badge>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={refreshData}
|
||||
disabled={isRefreshing}
|
||||
className="flex items-center gap-2 bg-transparent"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<div className="relative max-w-2xl">
|
||||
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
||||
<div className="flex-1 space-y-6 p-6 md:p-8">
|
||||
{/* 顶部标题区 */}
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">数据资产中台</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{currentTime.toLocaleDateString("zh-CN", { weekday: "long", year: "numeric", month: "long", day: "numeric" })}
|
||||
{" · "}
|
||||
{currentTime.toLocaleTimeString("zh-CN")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative w-full md:w-80">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索用户或流量关键词..."
|
||||
placeholder="搜索用户、手机号、标签..."
|
||||
className="pl-9"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyPress={(e) => e.key === "Enter" && handleSearch()}
|
||||
className="pl-12 pr-24 h-14 text-lg border-2 border-gray-200 focus:border-blue-500 rounded-xl shadow-sm"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSearch}
|
||||
className="absolute right-2 top-1/2 transform -translate-y-1/2 px-6 rounded-lg"
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
</div>
|
||||
<Button>
|
||||
<Zap className="mr-2 h-4 w-4" />
|
||||
快速分析
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 核心数据展示 - 40亿用户为中心 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||
{/* 用户总数 - 主要指标 */}
|
||||
<Card className="lg:col-span-2 border-2 border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-3 text-2xl">
|
||||
<div className="p-3 bg-blue-500 rounded-xl">
|
||||
<Users className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
用户资产总量
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="text-center">
|
||||
<div className="text-6xl font-bold text-blue-600 mb-2">{formatNumber(systemStats.userCount)}</div>
|
||||
<div className="text-lg text-gray-600">总用户数</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4 pt-4 border-t">
|
||||
{growthData.map((data, index) => (
|
||||
<div key={index} className="text-center">
|
||||
<div className="text-2xl font-bold text-green-600">+{data.userGrowth.toFixed(1)}%</div>
|
||||
<div className="text-sm text-gray-500">{data.period}增长</div>
|
||||
<div className="text-xs text-gray-400 mt-1">活跃: {formatNumber(data.activeUsers)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 系统状态 */}
|
||||
<Card className="border-2 border-green-200 bg-gradient-to-r from-green-50 to-emerald-50">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-3">
|
||||
<div className="p-2 bg-green-500 rounded-lg">
|
||||
<Activity className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
实时状态
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600">响应时间</span>
|
||||
<Badge variant="secondary">{systemStats.avgResponseTime}ms</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600">数据源</span>
|
||||
<Badge variant="secondary">{systemStats.versionCount}个</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600">缓存大小</span>
|
||||
<Badge variant="secondary">{systemStats.cacheSize}MB</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 pt-2 border-t">最后更新: {lastUpdate.toLocaleTimeString()}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 数据增长趋势 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
<Card className="border-2 border-purple-200 bg-gradient-to-r from-purple-50 to-pink-50">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-3">
|
||||
<div className="p-2 bg-purple-500 rounded-lg">
|
||||
<TrendingUp className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
用户资产增长
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{growthData.map((data, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 bg-white rounded-lg shadow-sm">
|
||||
<div>
|
||||
<div className="font-semibold">{data.period}</div>
|
||||
<div className="text-sm text-gray-500">活跃用户: {formatNumber(data.activeUsers)}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-bold text-purple-600">+{data.userGrowth.toFixed(1)}%</div>
|
||||
<div className="text-xs text-gray-500">增长率</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-2 border-orange-200 bg-gradient-to-r from-orange-50 to-yellow-50">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-3">
|
||||
<div className="p-2 bg-orange-500 rounded-lg">
|
||||
<Database className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
数据增长情况
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-3 bg-white rounded-lg shadow-sm">
|
||||
<div>
|
||||
<div className="font-semibold">流量关键词</div>
|
||||
<div className="text-sm text-gray-500">总计 {formatNumber(systemStats.keywordCount)} 个</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-bold text-orange-600">+{growthData[0].dataGrowth.toFixed(1)}%</div>
|
||||
<div className="text-xs text-gray-500">今日增长</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 bg-white rounded-lg shadow-sm">
|
||||
<div>
|
||||
<div className="font-semibold">数据处理量</div>
|
||||
<div className="text-sm text-gray-500">实时处理中</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-bold text-orange-600">
|
||||
{formatNumber(Math.floor(systemStats.userCount * 0.001))}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">条/秒</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 bg-white rounded-lg shadow-sm">
|
||||
<div>
|
||||
<div className="font-semibold">存储容量</div>
|
||||
<div className="text-sm text-gray-500">云端存储</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-bold text-orange-600">
|
||||
{Math.floor(systemStats.userCount / 1000000)}TB
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">已使用</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 快速访问入口 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card
|
||||
className="cursor-pointer hover:shadow-lg transition-all duration-200 border-2 border-blue-100 hover:border-blue-300"
|
||||
onClick={() => router.push("/data-platform")}
|
||||
>
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="p-3 bg-blue-100 rounded-full w-fit mx-auto mb-3">
|
||||
<Database className="w-8 h-8 text-blue-600" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-lg mb-2">数据中台</h3>
|
||||
<p className="text-sm text-gray-600">数据源管理与AI模型</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
className="cursor-pointer hover:shadow-lg transition-all duration-200 border-2 border-green-100 hover:border-green-300"
|
||||
onClick={() => router.push("/user-portrait")}
|
||||
>
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="p-3 bg-green-100 rounded-full w-fit mx-auto mb-3">
|
||||
<Users className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-lg mb-2">用户画像</h3>
|
||||
<p className="text-sm text-gray-600">用户管理与标签体系</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
className="cursor-pointer hover:shadow-lg transition-all duration-200 border-2 border-purple-100 hover:border-purple-300"
|
||||
onClick={() => router.push("/ai-assistant")}
|
||||
>
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="p-3 bg-purple-100 rounded-full w-fit mx-auto mb-3">
|
||||
<BarChart3 className="w-8 h-8 text-purple-600" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-lg mb-2">AI智能助手</h3>
|
||||
<p className="text-sm text-gray-600">数据分析与报告生成</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
className="cursor-pointer hover:shadow-lg transition-all duration-200 border-2 border-orange-100 hover:border-orange-300"
|
||||
onClick={() => router.push("/intelligent-search")}
|
||||
>
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="p-3 bg-orange-100 rounded-full w-fit mx-auto mb-3">
|
||||
<Globe className="w-8 h-8 text-orange-600" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-lg mb-2">智能搜索</h3>
|
||||
<p className="text-sm text-gray-600">全局搜索与AI分析</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 快速指标示例(可后续接入真实数据) */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-8">
|
||||
<StatCard icon={<Users className="h-4 w-4" />} label="用户总量" value="~120+" />
|
||||
<StatCard icon={<Activity className="h-4 w-4" />} label="近7日活跃" value="动态计算" />
|
||||
<StatCard icon={<BarChart3 className="h-4 w-4" />} label="平均RFM" value="50-80" />
|
||||
<StatCard icon={<Users className="h-4 w-4" />} label="新客占比" value="≈25%" />
|
||||
</div>
|
||||
|
||||
{/* 搜索 + 条件过滤 + 列表 */}
|
||||
<UserSearch query={searchQuery} />
|
||||
</div>
|
||||
<Toaster />
|
||||
|
||||
{/* 核心指标卡片 */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card className="bg-gradient-to-br from-violet-500/10 to-purple-500/10 border-violet-200 dark:border-violet-800">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">用户资产总量</CardTitle>
|
||||
<Users className="h-4 w-4 text-violet-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">40.5亿</div>
|
||||
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
||||
<TrendingUp className="h-3 w-3 text-green-500 mr-1" />
|
||||
<span className="text-green-500">+20.1%</span>
|
||||
<span className="ml-1">较上月</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-blue-500/10 to-cyan-500/10 border-blue-200 dark:border-blue-800">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">活跃用户</CardTitle>
|
||||
<Activity className="h-4 w-4 text-blue-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">2,350万</div>
|
||||
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
||||
<TrendingUp className="h-3 w-3 text-green-500 mr-1" />
|
||||
<span className="text-green-500">+18.2%</span>
|
||||
<span className="ml-1">较上月</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-emerald-500/10 to-green-500/10 border-emerald-200 dark:border-emerald-800">
|
||||
<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-emerald-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">125亿条</div>
|
||||
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
||||
<TrendingUp className="h-3 w-3 text-green-500 mr-1" />
|
||||
<span className="text-green-500">+32.5%</span>
|
||||
<span className="ml-1">较上月</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-amber-500/10 to-orange-500/10 border-amber-200 dark:border-amber-800">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">资产总价值</CardTitle>
|
||||
<DollarSign className="h-4 w-4 text-amber-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">¥125.8亿</div>
|
||||
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
||||
<TrendingUp className="h-3 w-3 text-green-500 mr-1" />
|
||||
<span className="text-green-500">+45.2%</span>
|
||||
<span className="ml-1">较上月</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 图表区域 */}
|
||||
<div className="grid gap-4 md:grid-cols-7">
|
||||
<Card className="col-span-4">
|
||||
<CardHeader>
|
||||
<CardTitle>用户增长趋势</CardTitle>
|
||||
<CardDescription>近6个月用户与资产价值变化</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={growthData}>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||
<XAxis dataKey="month" className="text-xs" />
|
||||
<YAxis className="text-xs" />
|
||||
<Tooltip />
|
||||
<Line type="monotone" dataKey="users" stroke="#8B5CF6" strokeWidth={2} name="用户(万)" />
|
||||
<Line type="monotone" dataKey="value" stroke="#3B82F6" strokeWidth={2} name="价值(亿)" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="col-span-3">
|
||||
<CardHeader>
|
||||
<CardTitle>用户价值分布</CardTitle>
|
||||
<CardDescription>S/A/B/C/D级用户占比</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={valueDistribution}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={50}
|
||||
outerRadius={80}
|
||||
dataKey="value"
|
||||
label={({ name, value }) => `${name}: ${value}%`}
|
||||
>
|
||||
{valueDistribution.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="flex flex-wrap justify-center gap-2 mt-4">
|
||||
{valueDistribution.map((item) => (
|
||||
<Badge key={item.name} variant="outline" style={{ borderColor: item.color, color: item.color }}>
|
||||
{item.name}: {item.value}%
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 底部区域 */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>实时动态</CardTitle>
|
||||
<CardDescription>用户行为实时监控</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{recentActivities.map((activity) => (
|
||||
<div key={activity.id} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center text-sm font-medium">
|
||||
{activity.user.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{activity.user}</p>
|
||||
<p className="text-xs text-muted-foreground">{activity.action}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{activity.time}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>快速入口</CardTitle>
|
||||
<CardDescription>常用功能快速访问</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Link href="/data-platform">
|
||||
<Button variant="outline" className="w-full justify-start h-auto py-4 bg-transparent">
|
||||
<Database className="mr-2 h-4 w-4" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">数据中台</div>
|
||||
<div className="text-xs text-muted-foreground">数据源管理</div>
|
||||
</div>
|
||||
<ArrowUpRight className="ml-auto h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/user-portrait">
|
||||
<Button variant="outline" className="w-full justify-start h-auto py-4 bg-transparent">
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">用户画像</div>
|
||||
<div className="text-xs text-muted-foreground">用户分析</div>
|
||||
</div>
|
||||
<ArrowUpRight className="ml-auto h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/value-assessment">
|
||||
<Button variant="outline" className="w-full justify-start h-auto py-4 bg-transparent">
|
||||
<DollarSign className="mr-2 h-4 w-4" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">价值评估</div>
|
||||
<div className="text-xs text-muted-foreground">RFM模型</div>
|
||||
</div>
|
||||
<ArrowUpRight className="ml-auto h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/ai-assistant">
|
||||
<Button variant="outline" className="w-full justify-start h-auto py-4 bg-transparent">
|
||||
<Zap className="mr-2 h-4 w-4" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">AI助手</div>
|
||||
<div className="text-xs text-muted-foreground">智能分析</div>
|
||||
</div>
|
||||
<ArrowUpRight className="ml-auto h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,239 +1,277 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import MobileHeader from "@/app/components/MobileHeader"
|
||||
import BottomNav from "@/app/components/BottomNav"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Search, Filter, Plus } from 'lucide-react'
|
||||
import FilterDrawer, { type FilterValues } from "@/components/user-portrait/filter-drawer"
|
||||
|
||||
type User = {
|
||||
id: string
|
||||
name: string
|
||||
phone: string
|
||||
email: string
|
||||
tags: string[]
|
||||
rfmScore: number
|
||||
lastActivity: string
|
||||
status: "活跃" | "沉睡" | "已封禁"
|
||||
}
|
||||
|
||||
type UsersResponse = { success: true; data: { items: User[]; total: number; page: number; pageSize: number } }
|
||||
|
||||
export default function UserPortraitPage() {
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Users, Search, Tag, TrendingUp, Filter, UserCircle, Mail, Phone, MapPin, Calendar, Activity, Target } from 'lucide-react'
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
|
||||
export default function UserPortrait() {
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [isAddingUser, setIsAddingUser] = useState(false)
|
||||
const [newUser, setNewUser] = useState({ name: "", phone: "", email: "", tags: [] as string[] })
|
||||
|
||||
const [filterOpen, setFilterOpen] = useState(false)
|
||||
const [allTags, setAllTags] = useState<string[]>([])
|
||||
const [filters, setFilters] = useState<FilterValues>({ tags: [], status: [], rfm: [0, 100] })
|
||||
// 用户群体分布
|
||||
const userSegments = [
|
||||
{ name: "高价值用户", count: 128456700, percentage: 3.0, avgValue: 580, color: "purple" },
|
||||
{ name: "活跃用户", count: 1285670000, percentage: 30.0, avgValue: 280, color: "blue" },
|
||||
{ name: "沉默用户", count: 1714283500, percentage: 40.0, avgValue: 85, color: "yellow" },
|
||||
{ name: "流失预警", count: 857134000, percentage: 20.0, avgValue: 45, color: "orange" },
|
||||
{ name: "新增用户", count: 300125800, percentage: 7.0, avgValue: 120, color: "green" },
|
||||
]
|
||||
|
||||
const queryString = useMemo(() => {
|
||||
const p = new URLSearchParams()
|
||||
if (searchQuery) p.set("q", searchQuery)
|
||||
if (filters.tags.length) p.set("tags", filters.tags.join(","))
|
||||
if (filters.status.length) p.set("status", filters.status.join(","))
|
||||
p.set("rfmMin", String(filters.rfm[0]))
|
||||
p.set("rfmMax", String(filters.rfm[1]))
|
||||
p.set("page", "1")
|
||||
p.set("pageSize", "50")
|
||||
return p.toString()
|
||||
}, [searchQuery, filters])
|
||||
// 标签分类统计
|
||||
const tagCategories = [
|
||||
{ name: "基础属性", count: 485, usage: 4285670000, coverage: 100 },
|
||||
{ name: "行为标签", count: 328, usage: 3256780000, coverage: 76 },
|
||||
{ name: "兴趣偏好", count: 256, usage: 2570000000, coverage: 60 },
|
||||
{ name: "消费能力", count: 89, usage: 1714283500, coverage: 40 },
|
||||
{ name: "自定义标签", count: 100, usage: 857134000, coverage: 20 },
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/users?${queryString}`)
|
||||
.then((r) => r.json())
|
||||
.then((res: UsersResponse) => {
|
||||
if (res?.success) {
|
||||
setUsers(res.data.items)
|
||||
setTotal(res.data.total)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [queryString])
|
||||
// 最近用户示例
|
||||
const recentUsers = [
|
||||
{
|
||||
id: "U001285",
|
||||
name: "张**",
|
||||
phone: "138****5678",
|
||||
level: "S",
|
||||
tags: ["高净值", "美妆爱好者", "VIP客户"],
|
||||
value: 5800,
|
||||
lastActive: "2小时前",
|
||||
},
|
||||
{
|
||||
id: "U002456",
|
||||
name: "李**",
|
||||
phone: "186****1234",
|
||||
level: "A",
|
||||
tags: ["活跃用户", "教育培训", "复购客户"],
|
||||
value: 3200,
|
||||
lastActive: "5小时前",
|
||||
},
|
||||
{
|
||||
id: "U003789",
|
||||
name: "王**",
|
||||
phone: "159****9876",
|
||||
level: "B",
|
||||
tags: ["潜力用户", "电商购物", "新客户"],
|
||||
value: 1800,
|
||||
lastActive: "1天前",
|
||||
},
|
||||
{
|
||||
id: "U004123",
|
||||
name: "赵**",
|
||||
phone: "177****5432",
|
||||
level: "C",
|
||||
tags: ["普通用户", "低频互动"],
|
||||
value: 850,
|
||||
lastActive: "3天前",
|
||||
},
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/users?meta=tags")
|
||||
.then((r) => r.json())
|
||||
.then((res: any) => setAllTags(res?.data?.tags ?? []))
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(1)}B`
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
const handleAddUser = async () => {
|
||||
const resp = await fetch("/api/users", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(newUser),
|
||||
})
|
||||
const data = await resp.json()
|
||||
if (data?.success) {
|
||||
setIsAddingUser(false)
|
||||
setNewUser({ name: "", phone: "", email: "", tags: [] })
|
||||
// 触发刷新
|
||||
fetch(`/api/users?${queryString}`)
|
||||
.then((r) => r.json())
|
||||
.then((res: UsersResponse) => {
|
||||
if (res?.success) {
|
||||
setUsers(res.data.items)
|
||||
setTotal(res.data.total)
|
||||
}
|
||||
})
|
||||
const getLevelColor = (level: string) => {
|
||||
const colors = {
|
||||
S: "border-purple-500/50 text-purple-400 bg-purple-500/10",
|
||||
A: "border-blue-500/50 text-blue-400 bg-blue-500/10",
|
||||
B: "border-green-500/50 text-green-400 bg-green-500/10",
|
||||
C: "border-yellow-500/50 text-yellow-400 bg-yellow-500/10",
|
||||
D: "border-gray-500/50 text-gray-400 bg-gray-500/10",
|
||||
}
|
||||
return colors[level as keyof typeof colors] || colors.D
|
||||
}
|
||||
|
||||
const getSegmentColor = (color: string) => {
|
||||
const colors = {
|
||||
purple: "from-purple-500/20 to-purple-900/20 border-purple-500/30",
|
||||
blue: "from-blue-500/20 to-blue-900/20 border-blue-500/30",
|
||||
green: "from-green-500/20 to-green-900/20 border-green-500/30",
|
||||
yellow: "from-yellow-500/20 to-yellow-900/20 border-yellow-500/30",
|
||||
orange: "from-orange-500/20 to-orange-900/20 border-orange-500/30",
|
||||
}
|
||||
return colors[color as keyof typeof colors]
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-purple-50">
|
||||
<MobileHeader onMenuToggle={() => {}} title="用户画像" />
|
||||
<div className="min-h-screen bg-black text-white p-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold mb-2">用户画像</h1>
|
||||
<p className="text-gray-400">用户管理、标签体系与群体分析</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline" className="bg-white/5 border-white/10 hover:bg-white/10">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
高级筛选
|
||||
</Button>
|
||||
<Button className="bg-gradient-to-r from-blue-500 to-purple-500">
|
||||
<Target className="w-4 h-4 mr-2" />
|
||||
创建用户群
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="container mx-auto px-4 pb-24 space-y-4">
|
||||
<div className="rounded-2xl bg-white/60 backdrop-blur-md p-4 shadow-sm border">
|
||||
<div className="flex items-baseline justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">用户画像</h1>
|
||||
<p className="text-sm text-muted-foreground">管理与分群</p>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">共 {total} 人</div>
|
||||
<Tabs defaultValue="segments" className="space-y-6">
|
||||
<TabsList className="bg-white/5 border border-white/10">
|
||||
<TabsTrigger value="segments">用户群体</TabsTrigger>
|
||||
<TabsTrigger value="tags">标签体系</TabsTrigger>
|
||||
<TabsTrigger value="users">用户列表</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 用户群体分布 */}
|
||||
<TabsContent value="segments" className="space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
||||
{userSegments.map((segment, index) => (
|
||||
<Card key={index} className={`bg-gradient-to-br ${getSegmentColor(segment.color)}`}>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white text-lg">{segment.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="text-3xl font-bold text-white">{formatNumber(segment.count)}</div>
|
||||
<div className="text-sm text-gray-300">占比 {segment.percentage}%</div>
|
||||
<div className="pt-2 border-t border-white/20">
|
||||
<div className="text-xs text-gray-300 mb-1">人均价值</div>
|
||||
<div className="text-lg font-semibold text-white">¥{segment.avgValue}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<Tabs defaultValue="users" className="w-full">
|
||||
<TabsList className="grid grid-cols-2 w-full">
|
||||
<TabsTrigger value="users" className="data-[state=active]:bg-white">用户管理</TabsTrigger>
|
||||
<TabsTrigger value="tags" className="data-[state=active]:bg-white">标签管理</TabsTrigger>
|
||||
</TabsList>
|
||||
{/* 用户增长趋势 */}
|
||||
<Card className="bg-white/5 border-white/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
用户增长趋势
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-64 flex items-center justify-center text-gray-400">
|
||||
用户增长趋势图表区域
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="users" className="space-y-4">
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-500" />
|
||||
<Input className="pl-8" placeholder="搜索用户…" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} />
|
||||
{/* 标签体系 */}
|
||||
<TabsContent value="tags" className="space-y-6">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{tagCategories.map((category, index) => (
|
||||
<Card key={index} className="bg-white/5 border-white/10">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center">
|
||||
<Tag className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">{category.name}</h3>
|
||||
<p className="text-sm text-gray-400">{category.count} 个标签</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="border-blue-500/50 text-blue-400">
|
||||
覆盖率 {category.coverage}%
|
||||
</Badge>
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => setFilterOpen(true)}>
|
||||
<Filter className="h-4 w-4 mr-1" />
|
||||
筛选
|
||||
</Button>
|
||||
<Button onClick={() => setIsAddingUser(true)}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加用户
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">标签数量</div>
|
||||
<div className="text-xl font-bold text-white">{category.count}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">使用用户</div>
|
||||
<div className="text-xl font-bold text-white">{formatNumber(category.usage)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">覆盖率</div>
|
||||
<div className="text-xl font-bold text-green-400">{category.coverage}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<div className="grid gap-3">
|
||||
{users.map((u) => (
|
||||
<Card key={u.id} className="border bg-white/70 backdrop-blur-md shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="grid grid-cols-12 gap-3 items-center">
|
||||
<div className="col-span-5">
|
||||
<Link href={`/user-portrait/${u.id}`} className="font-medium hover:underline">
|
||||
{u.name}
|
||||
</Link>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
最后活跃 {new Date(u.lastActivity).toLocaleDateString("zh-CN")}
|
||||
</p>
|
||||
{/* 用户列表 */}
|
||||
<TabsContent value="users" className="space-y-6">
|
||||
{/* 搜索栏 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索用户手机号、姓名、标签..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 bg-white/5 border-white/10"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" className="bg-white/5 border-white/10 hover:bg-white/10">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
筛选
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 用户卡片列表 */}
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{recentUsers.map((user) => (
|
||||
<Card key={user.id} className="bg-white/5 border-white/10 hover:bg-white/10 transition-colors cursor-pointer">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="w-12 h-12">
|
||||
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-purple-500">
|
||||
{user.name[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<span className="text-lg font-semibold text-white">{user.name}</span>
|
||||
<Badge variant="outline" className={getLevelColor(user.level)}>
|
||||
{user.level}级
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-400">
|
||||
<div className="flex items-center gap-1">
|
||||
<Phone className="w-3 h-3" />
|
||||
{user.phone}
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<div className="text-sm">{u.phone}</div>
|
||||
<div className="text-xs text-muted-foreground">{u.email}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{u.tags.slice(0, 2).map((t) => (
|
||||
<Badge key={t} variant="secondary" className="text-xs">{t}</Badge>
|
||||
))}
|
||||
{u.tags.length > 2 && (
|
||||
<Badge variant="outline" className="text-xs">+{u.tags.length - 2}</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 text-sm font-semibold">{u.rfmScore}</div>
|
||||
<div className="col-span-1">
|
||||
<span className={`text-xs px-2 py-1 rounded-full ${
|
||||
u.status === "活跃" ? "bg-green-100 text-green-700" :
|
||||
u.status === "沉睡" ? "bg-yellow-100 text-yellow-800" : "bg-red-100 text-red-700"
|
||||
}`}>
|
||||
{u.status}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<Activity className="w-3 h-3" />
|
||||
{user.lastActive}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="tags">
|
||||
<div className="text-sm text-muted-foreground py-6 text-center">标签管理将在接入数据字典后提供配置与统计</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-gray-400 mb-1">用户价值</div>
|
||||
<div className="text-2xl font-bold text-green-400">¥{user.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
{user.tags.map((tag, index) => (
|
||||
<Badge key={index} variant="outline" className="border-blue-500/30 text-blue-400">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<BottomNav />
|
||||
|
||||
{/* 筛选抽屉 */}
|
||||
<FilterDrawer
|
||||
open={filterOpen}
|
||||
onOpenChange={setFilterOpen}
|
||||
allTags={allTags}
|
||||
value={filters}
|
||||
onApply={(v) => setFilters(v)}
|
||||
/>
|
||||
|
||||
{/* 添加用户 */}
|
||||
<Dialog open={isAddingUser} onOpenChange={setIsAddingUser}>
|
||||
<DialogContent>
|
||||
<DialogHeader><DialogTitle>添加新用户</DialogTitle></DialogHeader>
|
||||
<div className="space-y-3 py-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">姓名</Label>
|
||||
<Input id="name" value={newUser.name} onChange={(e) => setNewUser((p) => ({ ...p, name: e.target.value }))} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone">手机号</Label>
|
||||
<Input id="phone" value={newUser.phone} onChange={(e) => setNewUser((p) => ({ ...p, phone: e.target.value }))} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">邮箱</Label>
|
||||
<Input id="email" type="email" value={newUser.email} onChange={(e) => setNewUser((p) => ({ ...p, email: e.target.value }))} />
|
||||
</div>
|
||||
{!!allTags.length && (
|
||||
<div className="space-y-2">
|
||||
<Label>用户标签</Label>
|
||||
<div className="grid grid-cols-2 gap-2 max-h-40 overflow-auto">
|
||||
{allTags.map((t) => (
|
||||
<label key={t} className="flex items-center gap-2 text-sm">
|
||||
<Checkbox
|
||||
checked={newUser.tags.includes(t)}
|
||||
onCheckedChange={(ck) =>
|
||||
setNewUser((p) => ({ ...p, tags: ck ? [...p.tags, t] : p.tags.filter((x) => x !== t) }))
|
||||
}
|
||||
/>
|
||||
<span className="truncate">{t}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setIsAddingUser(false)}>取消</Button>
|
||||
<Button onClick={handleAddUser}>添加用户</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
322
app/value-assessment/page.tsx
Normal file
322
app/value-assessment/page.tsx
Normal file
@@ -0,0 +1,322 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { DollarSign, TrendingUp, BarChart3, Calculator, Target, Users, ArrowUpRight, ArrowDownRight } from 'lucide-react'
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
|
||||
export default function ValueAssessment() {
|
||||
// 价值评估模型数据
|
||||
const valueModels = [
|
||||
{
|
||||
name: "RFM价值模型",
|
||||
description: "基于最近购买、购买频率、购买金额",
|
||||
totalValue: 8560000000,
|
||||
avgValue: 199.8,
|
||||
coverage: 92.5,
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
name: "用户生命周期价值",
|
||||
description: "预测用户终身价值(LTV)",
|
||||
totalValue: 12580000000,
|
||||
avgValue: 293.5,
|
||||
coverage: 78.3,
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
name: "行为价值模型",
|
||||
description: "基于用户行为数据评估",
|
||||
totalValue: 6780000000,
|
||||
avgValue: 158.2,
|
||||
coverage: 85.7,
|
||||
status: "training",
|
||||
},
|
||||
]
|
||||
|
||||
// 项目价值分布
|
||||
const projectValues = [
|
||||
{ name: "美妆行业项目A", users: 45600000, value: 2856000000, growth: 15.8, level: "A" },
|
||||
{ name: "教育培训项目B", users: 32800000, value: 1968000000, growth: 12.3, level: "A" },
|
||||
{ name: "电商零售项目C", users: 58900000, value: 3534000000, growth: 18.5, level: "S" },
|
||||
{ name: "金融服务项目D", users: 28500000, value: 1995000000, growth: 8.7, level: "B" },
|
||||
{ name: "本地生活项目E", users: 19200000, value: 1152000000, growth: 6.5, level: "B" },
|
||||
]
|
||||
|
||||
// 微信号价值排名
|
||||
const wechatRanking = [
|
||||
{ name: "微信号001", users: 2856000, value: 168560000, avgValue: 59.0, rank: 1 },
|
||||
{ name: "微信号002", users: 2345000, value: 145780000, avgValue: 62.2, rank: 2 },
|
||||
{ name: "微信号003", users: 1980000, value: 128640000, avgValue: 65.0, rank: 3 },
|
||||
{ name: "微信号004", users: 1756000, value: 112340000, avgValue: 64.0, rank: 4 },
|
||||
{ name: "微信号005", users: 1520000, value: 95600000, avgValue: 62.9, rank: 5 },
|
||||
]
|
||||
|
||||
// 价值增长趋势
|
||||
const growthTrend = [
|
||||
{ month: "1月", value: 9850000000, growth: 8.5 },
|
||||
{ month: "2月", value: 10250000000, growth: 4.1 },
|
||||
{ month: "3月", value: 10850000000, growth: 5.9 },
|
||||
{ month: "4月", value: 11420000000, growth: 5.3 },
|
||||
{ month: "5月", value: 11980000000, growth: 4.9 },
|
||||
{ month: "6月", value: 12580000000, growth: 5.0 },
|
||||
]
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(1)}B`
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
const formatCurrency = (num: number): string => {
|
||||
if (num >= 100000000) return `${(num / 100000000).toFixed(1)}亿`
|
||||
if (num >= 10000) return `${(num / 10000).toFixed(1)}万`
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
const getLevelColor = (level: string) => {
|
||||
const colors = {
|
||||
S: "border-purple-500/50 text-purple-400 bg-purple-500/10",
|
||||
A: "border-blue-500/50 text-blue-400 bg-blue-500/10",
|
||||
B: "border-green-500/50 text-green-400 bg-green-500/10",
|
||||
C: "border-yellow-500/50 text-yellow-400 bg-yellow-500/10",
|
||||
D: "border-gray-500/50 text-gray-400 bg-gray-500/10",
|
||||
}
|
||||
return colors[level as keyof typeof colors] || colors.D
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white p-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold mb-2">价值评估</h1>
|
||||
<p className="text-gray-400">用户价值分析、项目价值统计与预测</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline" className="bg-white/5 border-white/10 hover:bg-white/10">
|
||||
<Calculator className="w-4 h-4 mr-2" />
|
||||
价值计算器
|
||||
</Button>
|
||||
<Button className="bg-gradient-to-r from-blue-500 to-purple-500">
|
||||
<BarChart3 className="w-4 h-4 mr-2" />
|
||||
生成报告
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 核心价值指标 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
<Card className="bg-gradient-to-br from-purple-500/20 to-purple-900/20 border-purple-500/30">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<DollarSign className="w-5 h-5" />
|
||||
平台总价值
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-4xl font-bold text-purple-400 mb-2">¥{formatCurrency(12580000000)}</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<ArrowUpRight className="w-4 h-4 text-green-400" />
|
||||
<span className="text-green-400">+5.0%</span>
|
||||
<span className="text-gray-400">本月增长</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-br from-blue-500/20 to-blue-900/20 border-blue-500/30">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Users className="w-5 h-5" />
|
||||
人均价值
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-4xl font-bold text-blue-400 mb-2">¥293.5</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<ArrowUpRight className="w-4 h-4 text-green-400" />
|
||||
<span className="text-green-400">+2.8%</span>
|
||||
<span className="text-gray-400">环比提升</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-br from-green-500/20 to-green-900/20 border-green-500/30">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
价值覆盖率
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-4xl font-bold text-green-400 mb-2">85.5%</div>
|
||||
<div className="text-sm text-gray-400">已评估用户占比</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="models" className="space-y-6">
|
||||
<TabsList className="bg-white/5 border border-white/10">
|
||||
<TabsTrigger value="models">价值模型</TabsTrigger>
|
||||
<TabsTrigger value="projects">项目价值</TabsTrigger>
|
||||
<TabsTrigger value="wechat">微信号排名</TabsTrigger>
|
||||
<TabsTrigger value="trend">增长趋势</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 价值模型 */}
|
||||
<TabsContent value="models" className="space-y-6">
|
||||
{valueModels.map((model, index) => (
|
||||
<Card key={index} className="bg-white/5 border-white/10">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-white text-xl mb-1">{model.name}</CardTitle>
|
||||
<p className="text-sm text-gray-400">{model.description}</p>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
model.status === "active"
|
||||
? "border-green-500/50 text-green-400"
|
||||
: "border-yellow-500/50 text-yellow-400"
|
||||
}
|
||||
>
|
||||
{model.status === "active" ? "运行中" : "训练中"}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-3 gap-6 mb-4">
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-1">模型总价值</div>
|
||||
<div className="text-2xl font-bold text-white">¥{formatCurrency(model.totalValue)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-1">人均价值</div>
|
||||
<div className="text-2xl font-bold text-blue-400">¥{model.avgValue}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-1">覆盖率</div>
|
||||
<div className="text-2xl font-bold text-green-400">{model.coverage}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={model.coverage} className="h-2" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
{/* 项目价值 */}
|
||||
<TabsContent value="projects" className="space-y-6">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{projectValues.map((project, index) => (
|
||||
<Card key={index} className="bg-white/5 border-white/10">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-lg font-semibold text-white">{project.name}</div>
|
||||
<Badge variant="outline" className={getLevelColor(project.level)}>
|
||||
{project.level}级项目
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowUpRight className="w-4 h-4 text-green-400" />
|
||||
<span className="text-green-400 font-semibold">+{project.growth}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">用户数</div>
|
||||
<div className="text-lg font-semibold text-white">{formatNumber(project.users)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">项目价值</div>
|
||||
<div className="text-lg font-semibold text-green-400">¥{formatCurrency(project.value)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">人均价值</div>
|
||||
<div className="text-lg font-semibold text-blue-400">
|
||||
¥{(project.value / project.users).toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">增长率</div>
|
||||
<div className="text-lg font-semibold text-white">{project.growth}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 微信号排名 */}
|
||||
<TabsContent value="wechat" className="space-y-6">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{wechatRanking.map((wechat, index) => (
|
||||
<Card key={index} className="bg-white/5 border-white/10">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div
|
||||
className={`w-12 h-12 rounded-full flex items-center justify-center text-2xl font-bold ${
|
||||
wechat.rank === 1
|
||||
? "bg-gradient-to-br from-yellow-400 to-orange-500"
|
||||
: wechat.rank === 2
|
||||
? "bg-gradient-to-br from-gray-300 to-gray-500"
|
||||
: wechat.rank === 3
|
||||
? "bg-gradient-to-br from-orange-400 to-orange-600"
|
||||
: "bg-white/10"
|
||||
}`}
|
||||
>
|
||||
{wechat.rank}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-white mb-1">{wechat.name}</h3>
|
||||
<div className="text-sm text-gray-400">{formatNumber(wechat.users)} 用户</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-green-400">¥{formatCurrency(wechat.value)}</div>
|
||||
<div className="text-sm text-gray-400">人均 ¥{wechat.avgValue}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 增长趋势 */}
|
||||
<TabsContent value="trend" className="space-y-6">
|
||||
<Card className="bg-white/5 border-white/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
价值增长趋势
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{growthTrend.map((item, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-4 bg-white/5 rounded-lg">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-lg font-semibold text-white">{item.month}</div>
|
||||
<div className="text-2xl font-bold text-green-400">¥{formatCurrency(item.value)}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowUpRight className="w-4 h-4 text-green-400" />
|
||||
<span className="text-lg font-semibold text-green-400">+{item.growth}%</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user