feat: sync Sidebar and BottomNav, standardize user profile API
Align Sidebar & BottomNav menus, remove "Search", add user profile mock data, implement /api/users, add FilterDrawer, complete Section, ProfileHeader, MetricsRFM components Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
@@ -1,516 +1,189 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { 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 { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import {
|
||||
BarChart3,
|
||||
Send,
|
||||
FileText,
|
||||
Mail,
|
||||
MessageSquare,
|
||||
Download,
|
||||
Database,
|
||||
Target,
|
||||
Zap,
|
||||
Eye,
|
||||
Share,
|
||||
} from "lucide-react"
|
||||
import { TooltipHelp, TooltipProvider } from "@/components/ui/tooltip-help"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog"
|
||||
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"
|
||||
|
||||
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 分层、价值区间分布与运营建议",
|
||||
},
|
||||
]
|
||||
|
||||
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" },
|
||||
]
|
||||
|
||||
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 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() },
|
||||
]
|
||||
|
||||
export default function AIAssistantPage() {
|
||||
const [selectedDatabase, setSelectedDatabase] = useState("all")
|
||||
const [reportTitle, setReportTitle] = useState("")
|
||||
const [isGeneratingReport, setIsGeneratingReport] = useState(false)
|
||||
const [isSendingReport, setIsSendingReport] = useState(false)
|
||||
const [selectedReport, setSelectedReport] = useState<string | null>(null)
|
||||
const [tasks, setTasks] = useState<AnalysisTask[]>(initialTasks)
|
||||
|
||||
// 数据库列表
|
||||
const databases = [
|
||||
{ id: "user_behavior", name: "用户行为数据库", records: "1.2M", description: "用户行为轨迹和交互数据" },
|
||||
{ id: "user_profile", name: "用户画像数据库", records: "856K", description: "用户基本信息和标签数据" },
|
||||
{ id: "transaction", name: "交易数据库", records: "324K", description: "用户交易和消费记录" },
|
||||
{ id: "content", name: "内容数据库", records: "567K", description: "内容互动和偏好数据" },
|
||||
{ id: "traffic", name: "流量数据库", records: "2.3M", description: "流量关键词和来源数据" },
|
||||
]
|
||||
// 进度推进(仅演示用)
|
||||
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 [reports, setReports] = useState([
|
||||
{
|
||||
id: "1",
|
||||
title: "用户行为分析报告",
|
||||
database: "用户行为数据库",
|
||||
generatedAt: "2024-01-15 14:30",
|
||||
status: "completed",
|
||||
insights: [
|
||||
"用户在晚间时段活跃度最高,占总活跃时间的35%",
|
||||
"移动端用户占比87%,其中iOS用户转化率更高",
|
||||
"用户平均会话时长为8.5分钟,高于行业平均水平",
|
||||
],
|
||||
recommendations: [
|
||||
"建议在晚间时段增加营销活动投入",
|
||||
"优化iOS端用户体验,提升转化率",
|
||||
"延长用户会话时长,增加内容推荐精准度",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "用户价值分析报告",
|
||||
database: "用户画像数据库",
|
||||
generatedAt: "2024-01-14 16:45",
|
||||
status: "completed",
|
||||
insights: [
|
||||
"高价值用户占比7.2%,贡献了45%的总收入",
|
||||
"用户生命周期价值平均为¥2,580",
|
||||
"新用户转化为高价值用户的概率为12.5%",
|
||||
],
|
||||
recommendations: [
|
||||
"针对高价值用户制定专属服务策略",
|
||||
"优化新用户引导流程,提升转化率",
|
||||
"建立用户价值预警机制,及时挽留流失用户",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "流量关键词分析报告",
|
||||
database: "流量数据库",
|
||||
generatedAt: "2024-01-13 10:20",
|
||||
status: "completed",
|
||||
insights: [
|
||||
"热门关键词TOP10贡献了60%的流量",
|
||||
"长尾关键词转化率普遍高于热门关键词",
|
||||
"品牌相关关键词的用户质量最高",
|
||||
],
|
||||
recommendations: ["加大长尾关键词的投入和优化", "提升品牌关键词的覆盖范围", "建立关键词效果监控体系"],
|
||||
},
|
||||
])
|
||||
|
||||
// 生成报告
|
||||
const handleGenerateReport = () => {
|
||||
if (!reportTitle.trim()) return
|
||||
|
||||
setIsGeneratingReport(true)
|
||||
|
||||
// 模拟报告生成过程
|
||||
const onCreate = (task: AnalysisTask) => {
|
||||
setTasks((prev) => [task, ...prev])
|
||||
// 模拟启动
|
||||
setTimeout(() => {
|
||||
const newReport = {
|
||||
id: Date.now().toString(),
|
||||
title: reportTitle,
|
||||
database: databases.find((db) => db.id === selectedDatabase)?.name || "全部数据库",
|
||||
generatedAt: new Date().toLocaleString(),
|
||||
status: "completed",
|
||||
insights: ["AI分析发现的关键洞察1", "AI分析发现的关键洞察2", "AI分析发现的关键洞察3"],
|
||||
recommendations: ["基于数据分析的建议1", "基于数据分析的建议2", "基于数据分析的建议3"],
|
||||
}
|
||||
|
||||
setReports((prev) => [newReport, ...prev])
|
||||
setReportTitle("")
|
||||
setIsGeneratingReport(false)
|
||||
}, 3000)
|
||||
setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, status: "running" } : t)))
|
||||
}, 800)
|
||||
}
|
||||
|
||||
// 发送报告
|
||||
const handleSendReport = (reportId: string, method: string, target: string) => {
|
||||
setIsSendingReport(true)
|
||||
|
||||
// 模拟发送过程
|
||||
setTimeout(() => {
|
||||
setIsSendingReport(false)
|
||||
setSelectedReport(null)
|
||||
// 这里可以添加成功提示
|
||||
}, 2000)
|
||||
}
|
||||
const sanitizedItems = useMemo(
|
||||
() => reportItems.map((i) => ({ ...i, description: sanitizeText(i.description) })),
|
||||
[],
|
||||
)
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">AI智能助手</h1>
|
||||
<p className="text-muted-foreground mt-2">专注数据库分析,生成智能数据报告</p>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
{/* 核心指标概览 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
数据库数量
|
||||
<TooltipHelp content="可分析的数据库总数,包含用户、交易、行为等各类数据" />
|
||||
</CardTitle>
|
||||
<Database className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{databases.length}</div>
|
||||
<p className="text-xs text-muted-foreground">覆盖全业务数据</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
生成报告
|
||||
<TooltipHelp content="AI自动生成的数据分析报告总数" />
|
||||
</CardTitle>
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{reports.length}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<span className="text-green-600">+2</span> 本周新增
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
分析准确率
|
||||
<TooltipHelp content="AI数据分析的准确率,基于历史验证结果计算" />
|
||||
</CardTitle>
|
||||
<Target className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">94.2%</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<span className="text-green-600">+1.8%</span> 较上月
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
处理速度
|
||||
<TooltipHelp content="AI分析处理的平均速度,包括数据读取和分析时间" />
|
||||
</CardTitle>
|
||||
<Zap className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">2.3s</div>
|
||||
<p className="text-xs text-muted-foreground">平均分析时间</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="generate" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="generate">生成报告</TabsTrigger>
|
||||
<TabsTrigger value="reports">报告管理</TabsTrigger>
|
||||
<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="generate" className="space-y-6">
|
||||
<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">
|
||||
<BarChart3 className="h-5 w-5" />
|
||||
数据库分析报告生成
|
||||
<TooltipHelp content="选择数据库并配置分析参数,AI将自动生成详细的数据分析报告" />
|
||||
<Database className="w-4 h-4" />
|
||||
数据库
|
||||
</CardTitle>
|
||||
<CardDescription>AI智能分析数据库,生成专业的数据洞察报告</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="report-title">报告标题</Label>
|
||||
<Input
|
||||
id="report-title"
|
||||
placeholder="请输入报告标题..."
|
||||
value={reportTitle}
|
||||
onChange={(e) => setReportTitle(e.target.value)}
|
||||
className="mt-1"
|
||||
/>
|
||||
<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>
|
||||
<Label htmlFor="database-select">选择数据库</Label>
|
||||
<Select value={selectedDatabase} onValueChange={setSelectedDatabase}>
|
||||
<SelectTrigger id="database-select" className="mt-1">
|
||||
<SelectValue placeholder="选择要分析的数据库" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部数据库</SelectItem>
|
||||
{databases.map((db) => (
|
||||
<SelectItem key={db.id} value={db.id}>
|
||||
{db.name} ({db.records})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="text-xs text-gray-500">
|
||||
最近更新 {new Date(db.lastUpdated).toLocaleString("zh-CN")}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="analysis-type">分析类型</Label>
|
||||
<Select defaultValue="comprehensive">
|
||||
<SelectTrigger id="analysis-type" className="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="comprehensive">综合分析</SelectItem>
|
||||
<SelectItem value="behavior">行为分析</SelectItem>
|
||||
<SelectItem value="value">价值分析</SelectItem>
|
||||
<SelectItem value="trend">趋势分析</SelectItem>
|
||||
<SelectItem value="conversion">转化分析</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="time-range">时间范围</Label>
|
||||
<Select defaultValue="30days">
|
||||
<SelectTrigger id="time-range" className="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="7days">近7天</SelectItem>
|
||||
<SelectItem value="30days">近30天</SelectItem>
|
||||
<SelectItem value="90days">近90天</SelectItem>
|
||||
<SelectItem value="1year">近1年</SelectItem>
|
||||
<SelectItem value="custom">自定义</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="analysis-depth">分析深度</Label>
|
||||
<Select defaultValue="standard">
|
||||
<SelectTrigger id="analysis-depth" className="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="basic">基础分析</SelectItem>
|
||||
<SelectItem value="standard">标准分析</SelectItem>
|
||||
<SelectItem value="deep">深度分析</SelectItem>
|
||||
<SelectItem value="expert">专家级分析</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="output-format">输出格式</Label>
|
||||
<Select defaultValue="detailed">
|
||||
<SelectTrigger id="output-format" className="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="summary">摘要报告</SelectItem>
|
||||
<SelectItem value="detailed">详细报告</SelectItem>
|
||||
<SelectItem value="executive">高管报告</SelectItem>
|
||||
<SelectItem value="technical">技术报告</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>包含内容</Label>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="include-insights" defaultChecked />
|
||||
<Label htmlFor="include-insights">数据洞察</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="include-recommendations" defaultChecked />
|
||||
<Label htmlFor="include-recommendations">优化建议</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="include-charts" defaultChecked />
|
||||
<Label htmlFor="include-charts">图表分析</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="include-predictions" />
|
||||
<Label htmlFor="include-predictions">趋势预测</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
onClick={handleGenerateReport}
|
||||
disabled={isGeneratingReport || !reportTitle.trim()}
|
||||
className="px-8"
|
||||
>
|
||||
{isGeneratingReport ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
生成中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<BarChart3 className="h-4 w-4 mr-2" />
|
||||
生成AI分析报告
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="reports" className="space-y-6">
|
||||
<TabsContent value="templates">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="h-5 w-5" />
|
||||
报告管理
|
||||
<TooltipHelp content="管理所有生成的AI分析报告,支持查看、下载和分享" />
|
||||
<FileText className="w-4 h-4" />
|
||||
报告模板
|
||||
</CardTitle>
|
||||
<CardDescription>查看和管理所有AI生成的数据分析报告</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>报告标题</TableHead>
|
||||
<TableHead>数据源</TableHead>
|
||||
<TableHead>生成时间</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{reports.map((report) => (
|
||||
<TableRow key={report.id}>
|
||||
<TableCell className="font-medium">{report.title}</TableCell>
|
||||
<TableCell>{report.database}</TableCell>
|
||||
<TableCell>{report.generatedAt}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className="bg-green-100 text-green-800">已完成</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="ghost" size="sm">
|
||||
<Eye className="h-4 w-4 mr-1" />
|
||||
查看
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Download className="h-4 w-4 mr-1" />
|
||||
下载
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setSelectedReport(report.id)}>
|
||||
<Share className="h-4 w-4 mr-1" />
|
||||
发送
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<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>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* 发送报告对话框 */}
|
||||
<Dialog open={!!selectedReport} onOpenChange={(open) => !open && setSelectedReport(null)}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>发送报告</DialogTitle>
|
||||
<DialogDescription>选择发送方式,将报告发送到指定邮箱或微信</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>发送方式</Label>
|
||||
<div className="grid grid-cols-2 gap-4 mt-2">
|
||||
<Card className="cursor-pointer hover:bg-gray-50 p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Mail className="h-6 w-6 text-blue-500" />
|
||||
<div>
|
||||
<div className="font-medium">邮箱发送</div>
|
||||
<div className="text-sm text-muted-foreground">发送到指定邮箱</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="cursor-pointer hover:bg-gray-50 p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<MessageSquare className="h-6 w-6 text-green-500" />
|
||||
<div>
|
||||
<div className="font-medium">微信发送</div>
|
||||
<div className="text-sm text-muted-foreground">发送到微信联系人</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="send-target">发送目标</Label>
|
||||
<Input id="send-target" placeholder="请输入邮箱地址或微信号" className="mt-1" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="send-message">附加消息</Label>
|
||||
<Textarea id="send-message" placeholder="可选:添加附加消息" className="mt-1" rows={3} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>发送选项</Label>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="include-pdf" defaultChecked />
|
||||
<Label htmlFor="include-pdf">包含PDF版本</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="include-data" />
|
||||
<Label htmlFor="include-data">包含原始数据</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="schedule-send" />
|
||||
<Label htmlFor="schedule-send">定时发送</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setSelectedReport(null)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleSendReport(selectedReport!, "email", "target@example.com")}
|
||||
disabled={isSendingReport}
|
||||
>
|
||||
{isSendingReport ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
发送中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
发送报告
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
56
app/api/rfm/analyze/route.ts
Normal file
56
app/api/rfm/analyze/route.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { analyzeUser, type AnalyzeInput } from "@/services/rfm-engine"
|
||||
import { generateText } from "ai"
|
||||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
/**
|
||||
* 可选 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("。")
|
||||
if (!text) return null
|
||||
if (!process.env.OPENAI_API_KEY) return null
|
||||
try {
|
||||
const { text: out } = await generateText({
|
||||
model: openai("gpt-4o"),
|
||||
system:
|
||||
"请根据用户聊天片段输出JSON,字段:emotion(积极|中性|消极), intent(弱意图|中等意图|强意图), behavior(数组,中文标签最多3个)。只输出JSON。",
|
||||
prompt: `聊天片段:${text}`,
|
||||
// @ts-expect-error ai sdk typings are flexible in Next.js
|
||||
maxTokens: 200,
|
||||
})
|
||||
const json = JSON.parse(out.trim())
|
||||
return json
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
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 results = []
|
||||
for (const input of inputs) {
|
||||
const base = analyzeUser(input)
|
||||
if (useAI) {
|
||||
const ai = await aiTagging(input.chat_logs)
|
||||
if (ai) {
|
||||
base.tags.emotion = ai.emotion ?? base.tags.emotion
|
||||
base.tags.intent = ai.intent ?? base.tags.intent
|
||||
if (Array.isArray(ai.behavior)) {
|
||||
base.tags.behavior = Array.from(new Set([...(base.tags.behavior ?? []), ...ai.behavior]))
|
||||
}
|
||||
}
|
||||
}
|
||||
results.push(base)
|
||||
}
|
||||
return NextResponse.json({ success: true, data: results })
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ success: false, error: e?.message || "Invalid input" }, { status: 400 })
|
||||
}
|
||||
}
|
||||
13
app/api/rfm/dump_csv/route.ts
Normal file
13
app/api/rfm/dump_csv/route.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { dumpCsv } from "@/services/rfm-engine"
|
||||
|
||||
export async function GET() {
|
||||
const csv = dumpCsv()
|
||||
return new NextResponse(csv, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "text/csv; charset=utf-8",
|
||||
"Content-Disposition": `attachment; filename="rfm_export_${Date.now()}.csv"`,
|
||||
},
|
||||
})
|
||||
}
|
||||
12
app/api/rfm/get_tags/route.ts
Normal file
12
app/api/rfm/get_tags/route.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getUserTags } from "@/services/rfm-engine"
|
||||
|
||||
export async function GET(req: Request) {
|
||||
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 })
|
||||
}
|
||||
7
app/api/rfm/group_summary/route.ts
Normal file
7
app/api/rfm/group_summary/route.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getGroupSummary } from "@/services/rfm-engine"
|
||||
|
||||
export async function GET() {
|
||||
const data = getGroupSummary()
|
||||
return NextResponse.json({ success: true, data })
|
||||
}
|
||||
12
app/api/rfm/score/route.ts
Normal file
12
app/api/rfm/score/route.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { computeRFM, type AnalyzeInput } from "@/services/rfm-engine"
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = (await req.json()) as AnalyzeInput
|
||||
const score = computeRFM(body)
|
||||
return NextResponse.json({ success: true, data: score })
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ success: false, error: e?.message || "Invalid input" }, { status: 400 })
|
||||
}
|
||||
}
|
||||
26
app/api/rfm/weights/route.ts
Normal file
26
app/api/rfm/weights/route.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getWeights, setWeights } from "@/services/rfm-engine"
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ success: true, data: getWeights() })
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
let R = Number(body.R ?? 0.5)
|
||||
let F = Number(body.F ?? 0.3)
|
||||
let M = Number(body.M ?? 0.2)
|
||||
|
||||
// 归一化
|
||||
const sum = Math.max(0.000001, R + F + M)
|
||||
R = R / sum
|
||||
F = F / sum
|
||||
M = M / sum
|
||||
|
||||
setWeights({ R, F, M })
|
||||
return NextResponse.json({ success: true, data: { R, F, M } })
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ success: false, error: e?.message || "Invalid input" }, { status: 400 })
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,5 @@
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import { getIntelligentSearchService } from "@/services/intelligent-search-service"
|
||||
import { getMindsDBConnector } from "@/lib/mindsdb-connector"
|
||||
|
||||
// 初始化MindsDB连接
|
||||
const mindsDBConfig = {
|
||||
host: process.env.MINDSDB_HOST || "localhost",
|
||||
port: Number.parseInt(process.env.MINDSDB_PORT || "47334"),
|
||||
username: process.env.MINDSDB_USERNAME || "mindsdb",
|
||||
password: process.env.MINDSDB_PASSWORD || "",
|
||||
database: process.env.MINDSDB_DATABASE || "mindsdb",
|
||||
}
|
||||
|
||||
// 初始化连接器
|
||||
getMindsDBConnector(mindsDBConfig)
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import { getMindsDBConnector } from "@/lib/mindsdb-connector"
|
||||
import { getIntelligentSearchService } from "@/services/intelligent-search-service"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const mindsDB = getMindsDBConnector()
|
||||
const searchService = getIntelligentSearchService()
|
||||
|
||||
// 获取系统状态
|
||||
const systemStatus = await mindsDB.getSystemStatus()
|
||||
const searchStats = searchService.getSearchStats()
|
||||
const status = await mindsDB.getSystemStatus()
|
||||
|
||||
return NextResponse.json({
|
||||
system: systemStatus,
|
||||
search: searchStats,
|
||||
success: true,
|
||||
status,
|
||||
timestamp: new Date().toISOString(),
|
||||
version: process.env.APP_VERSION || "1.0.0",
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("系统状态API错误:", error)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { NextResponse, NextRequest } from "next/server"
|
||||
import type { TrafficUser } from "@/types/traffic"
|
||||
import { addUser, filterUsers, getDistinctTags, getUserDetail } from "@/lib/mock-users"
|
||||
|
||||
// 中文名字生成器数据
|
||||
const familyNames = [
|
||||
@@ -191,82 +192,51 @@ const generateWechatFriends = (wechatId: string, count: number) => {
|
||||
// 微信好友数据缓存
|
||||
const wechatFriendsCache = new Map<string, TrafficUser[]>()
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const page = Number.parseInt(searchParams.get("page") || "1")
|
||||
const pageSize = Number.parseInt(searchParams.get("pageSize") || "10")
|
||||
const search = searchParams.get("search") || ""
|
||||
const category = searchParams.get("category") || "all"
|
||||
const source = searchParams.get("source") || "all"
|
||||
const status = searchParams.get("status") || "all"
|
||||
const startDate = searchParams.get("startDate")
|
||||
const endDate = searchParams.get("endDate")
|
||||
const wechatSource = searchParams.get("wechatSource") || ""
|
||||
|
||||
let filteredUsers = [...userPool]
|
||||
|
||||
// 如果有微信来源参数,生成或获取微信好友数据
|
||||
if (wechatSource) {
|
||||
if (!wechatFriendsCache.has(wechatSource)) {
|
||||
// 生成150-300个随机好友
|
||||
const friendCount = Math.floor(Math.random() * (300 - 150)) + 150
|
||||
wechatFriendsCache.set(wechatSource, generateWechatFriends(wechatSource, friendCount))
|
||||
}
|
||||
filteredUsers = wechatFriendsCache.get(wechatSource) || []
|
||||
}
|
||||
|
||||
// 应用过滤条件
|
||||
filteredUsers = filteredUsers.filter((user) => {
|
||||
const matchesSearch = search
|
||||
? user.nickname.toLowerCase().includes(search.toLowerCase()) ||
|
||||
user.wechatId.toLowerCase().includes(search.toLowerCase()) ||
|
||||
user.phone.includes(search)
|
||||
: true
|
||||
|
||||
const matchesCategory = category === "all" ? true : user.category === category
|
||||
const matchesSource = source === "all" ? true : user.source === source
|
||||
const matchesStatus = status === "all" ? true : user.status === status
|
||||
|
||||
const matchesDate =
|
||||
startDate && endDate
|
||||
? new Date(user.addTime) >= new Date(startDate) && new Date(user.addTime) <= new Date(endDate)
|
||||
: true
|
||||
|
||||
return matchesSearch && matchesCategory && matchesSource && matchesStatus && matchesDate
|
||||
})
|
||||
|
||||
// 按添加时间倒序排序
|
||||
filteredUsers.sort((a, b) => new Date(b.addTime).getTime() - new Date(a.addTime).getTime())
|
||||
|
||||
// 计算分页
|
||||
const total = filteredUsers.length
|
||||
const totalPages = Math.ceil(total / pageSize)
|
||||
const start = (page - 1) * pageSize
|
||||
const end = start + pageSize
|
||||
const users = filteredUsers.slice(start, end)
|
||||
|
||||
// 计算分类统计
|
||||
const categoryStats = {
|
||||
potential: userPool.filter((user) => user.category === "potential").length,
|
||||
customer: userPool.filter((user) => user.category === "customer").length,
|
||||
lost: userPool.filter((user) => user.category === "lost").length,
|
||||
}
|
||||
|
||||
// 模拟网络延迟
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
return NextResponse.json({
|
||||
users,
|
||||
pagination: {
|
||||
total,
|
||||
totalPages,
|
||||
currentPage: page,
|
||||
pageSize,
|
||||
},
|
||||
stats: {
|
||||
total: wechatSource ? filteredUsers.length : userPool.length,
|
||||
todayNew: wechatSource ? Math.floor(filteredUsers.length * 0.1) : todayUsers.length,
|
||||
categoryStats,
|
||||
},
|
||||
})
|
||||
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 meta = searchParams.get("meta")
|
||||
const id = searchParams.get("id")
|
||||
|
||||
if (meta === "tags") {
|
||||
const tags = getDistinctTags()
|
||||
return NextResponse.json({ success: true, data: { tags } })
|
||||
}
|
||||
|
||||
if (id) {
|
||||
const user = getUserDetail(id)
|
||||
if (!user) return NextResponse.json({ success: false, error: "NOT_FOUND" }, { status: 404 })
|
||||
return NextResponse.json({ success: true, data: user })
|
||||
}
|
||||
|
||||
const q = searchParams.get("q") || undefined
|
||||
const tagsParam = searchParams.get("tags") || ""
|
||||
const tags = tagsParam ? tagsParam.split(",").filter(Boolean) : []
|
||||
const statusParam = searchParams.get("status") || ""
|
||||
const status = statusParam ? (statusParam.split(",") as any) : []
|
||||
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 res = filterUsers({ q, tags, status, rfmMin, rfmMax, page, pageSize })
|
||||
return NextResponse.json({ success: true, data: res })
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.json().catch(() => null)
|
||||
if (!body || !body.name || !body.phone || !body.email) {
|
||||
return NextResponse.json({ success: false, error: "INVALID_PAYLOAD" }, { status: 400 })
|
||||
}
|
||||
const user = addUser({
|
||||
name: body.name,
|
||||
phone: body.phone,
|
||||
email: body.email,
|
||||
tags: Array.isArray(body.tags) ? body.tags.slice(0, 20) : [],
|
||||
})
|
||||
return NextResponse.json({ success: true, data: user })
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { Home, Database, Target, BrainCircuit } from "lucide-react" // 引入AI智能助手图标
|
||||
import { Home, Database, Target, BrainCircuit } from 'lucide-react' // 引入AI智能助手图标
|
||||
|
||||
const navItems = [
|
||||
{ href: "/", icon: Home, label: "概览" },
|
||||
{ href: "/", icon: Home, label: "首页" },
|
||||
{ href: "/data-platform", icon: Database, label: "数据中台" },
|
||||
{ href: "/user-portrait", icon: Target, label: "用户画像" }, // 整合用户池功能
|
||||
{ href: "/ai-assistant", icon: BrainCircuit, label: "AI助手" }, // 新增AI智能助手
|
||||
{ href: "/user-portrait", icon: Target, label: "画像" },
|
||||
{ href: "/ai-assistant", icon: BrainCircuit, label: "AI助手" },
|
||||
]
|
||||
|
||||
export default function BottomNav() {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { LayoutDashboard, Database, Users, BrainCircuit, Settings, ChevronLeft } from "lucide-react"
|
||||
import { LayoutDashboard, Database, Users, BrainCircuit, Settings, ChevronLeft } from 'lucide-react'
|
||||
|
||||
export default function Sidebar() {
|
||||
const pathname = usePathname()
|
||||
@@ -16,30 +16,10 @@ export default function Sidebar() {
|
||||
|
||||
// 简化的导航结构
|
||||
const navItems = [
|
||||
{
|
||||
title: "数据概览",
|
||||
href: "/",
|
||||
icon: <LayoutDashboard className="h-5 w-5" />,
|
||||
description: "平台整体数据分析与监控",
|
||||
},
|
||||
{
|
||||
title: "数据中台",
|
||||
href: "/data-platform",
|
||||
icon: <Database className="h-5 w-5" />,
|
||||
description: "多源数据整合与处理中心",
|
||||
},
|
||||
{
|
||||
title: "用户画像",
|
||||
href: "/user-portrait",
|
||||
icon: <Users className="h-5 w-5" />,
|
||||
description: "用户数据管理与画像分析",
|
||||
},
|
||||
{
|
||||
title: "AI智能助手",
|
||||
href: "/ai-assistant",
|
||||
icon: <BrainCircuit className="h-5 w-5" />,
|
||||
description: "AI数据分析与营销策略",
|
||||
},
|
||||
{ title: "首页", href: "/", icon: <LayoutDashboard className="h-5 w-5" />, description: "总览与搜索" },
|
||||
{ title: "数据中台", href: "/data-platform", icon: <Database className="h-5 w-5" />, description: "多源数据整合" },
|
||||
{ title: "画像", href: "/user-portrait", icon: <Users className="h-5 w-5" />, description: "用户管理与分群" },
|
||||
{ title: "AI助手", href: "/ai-assistant", icon: <BrainCircuit className="h-5 w-5" />, description: "AI分析与策略" },
|
||||
]
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -109,3 +109,11 @@ export {
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
||||
|
||||
export function Toaster() {
|
||||
return (
|
||||
<ToastProvider>
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,20 +12,7 @@ import { Avatar } from "@/components/ui/avatar"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import {
|
||||
Search,
|
||||
Sparkles,
|
||||
Clock,
|
||||
TrendingUp,
|
||||
Users,
|
||||
Brain,
|
||||
Zap,
|
||||
Download,
|
||||
RefreshCw,
|
||||
Eye,
|
||||
BarChart3,
|
||||
Lightbulb,
|
||||
} from "lucide-react"
|
||||
import { Search, Sparkles, Clock, TrendingUp, Users, Brain, Zap, Download, RefreshCw, Eye, BarChart3, Lightbulb } from 'lucide-react'
|
||||
import { TooltipHelp, TooltipProvider } from "@/components/ui/tooltip-help"
|
||||
|
||||
interface SearchResult {
|
||||
@@ -59,6 +46,20 @@ export default function IntelligentSearchPage() {
|
||||
const [searchResults, setSearchResults] = useState<SearchResponse | null>(null)
|
||||
const [searchHistory, setSearchHistory] = useState<string[]>([])
|
||||
const [activeTab, setActiveTab] = useState("search")
|
||||
const [rfmHighValueOnly, setRfmHighValueOnly] = useState(false);
|
||||
|
||||
const detectQueryType = (q: string) => {
|
||||
const s = q.trim()
|
||||
if (/^\d{5,11}$/.test(s)) {
|
||||
if (/^1[3-9]\d{9}$/.test(s) || /^\d{3}\*{4}\d{4}$/.test(s)) return "手机号"
|
||||
return "QQ号"
|
||||
}
|
||||
if (/^[A-F0-9]{14}$/.test(s)) return "MEID"
|
||||
if (/^0{6,}$/.test(s) || s.includes("000000")) return "流量词"
|
||||
return "通用"
|
||||
}
|
||||
|
||||
const queryType = detectQueryType(searchQuery)
|
||||
|
||||
// 执行搜索
|
||||
const performSearch = useCallback(
|
||||
@@ -78,6 +79,7 @@ export default function IntelligentSearchPage() {
|
||||
options: {
|
||||
useAI,
|
||||
includeInsights,
|
||||
rfmTag: includeInsights ? "高" : undefined,
|
||||
limit: 50,
|
||||
},
|
||||
}),
|
||||
@@ -193,6 +195,11 @@ export default function IntelligentSearchPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Badge variant="secondary" className="text-xs">识别: {queryType}</Badge>
|
||||
{queryType === "流量词" && <Badge className="text-xs">优先匹配流量池</Badge>}
|
||||
</div>
|
||||
|
||||
{/* 搜索选项 */}
|
||||
<div className="flex flex-wrap items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -226,6 +233,13 @@ export default function IntelligentSearchPage() {
|
||||
<TooltipHelp content="在搜索结果中包含AI生成的业务洞察和分析建议" />
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch id="rfm-high" checked={rfmHighValueOnly} onCheckedChange={setRfmHighValueOnly} />
|
||||
<Label htmlFor="rfm-high" className="flex items-center gap-1">
|
||||
仅看RFM高价值
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索历史 */}
|
||||
|
||||
548
app/page.tsx
548
app/page.tsx
@@ -1,335 +1,351 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Search, Users, TrendingUp, Database, RefreshCw, BarChart3, Activity, Globe } from 'lucide-react'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { TooltipHelp, TooltipProvider } from "@/components/ui/tooltip-help"
|
||||
import {
|
||||
Activity,
|
||||
TrendingUp,
|
||||
Database,
|
||||
Search,
|
||||
RefreshCw,
|
||||
Zap,
|
||||
BarChart3,
|
||||
Target,
|
||||
Globe,
|
||||
UserCheck,
|
||||
Clock,
|
||||
} from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Toaster } from "@/components/ui/toaster"
|
||||
|
||||
interface SystemStats {
|
||||
userCount: number
|
||||
keywordCount: number
|
||||
versionCount: number
|
||||
avgResponseTime: number
|
||||
cacheSize: number
|
||||
connected: boolean
|
||||
}
|
||||
|
||||
interface GrowthData {
|
||||
period: string
|
||||
userGrowth: number
|
||||
dataGrowth: number
|
||||
activeUsers: number
|
||||
}
|
||||
|
||||
export default function OverviewPage() {
|
||||
const router = useRouter()
|
||||
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 [coreData, setCoreData] = useState({
|
||||
totalUsers: 4000000000, // 40亿用户
|
||||
activeUsers: 2800000000, // 28亿活跃用户
|
||||
dataGrowthRate: 15.8, // 数据增长率
|
||||
assetGrowthRate: 23.5, // 资产增长率
|
||||
realTimeQueries: 156789, // 实时查询数
|
||||
dataVolume: "12.8TB", // 数据量
|
||||
systemHealth: 99.2, // 系统健康度
|
||||
aiAnalysis: 8456, // AI分析次数
|
||||
})
|
||||
|
||||
// 实时数据更新
|
||||
// 自动刷新数据
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCoreData((prev) => ({
|
||||
...prev,
|
||||
realTimeQueries: prev.realTimeQueries + Math.floor(Math.random() * 50) + 10,
|
||||
activeUsers: prev.activeUsers + Math.floor(Math.random() * 1000) + 100,
|
||||
}))
|
||||
setLastUpdate(new Date())
|
||||
}, 5000) // 每5秒更新一次
|
||||
refreshData()
|
||||
}, 30000) // 30秒刷新一次
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
// 手动刷新
|
||||
const handleRefresh = async () => {
|
||||
// 刷新数据
|
||||
const refreshData = async () => {
|
||||
setIsRefreshing(true)
|
||||
// 模拟数据刷新
|
||||
setTimeout(() => {
|
||||
setCoreData((prev) => ({
|
||||
try {
|
||||
// 模拟数据更新
|
||||
setSystemStats((prev) => ({
|
||||
...prev,
|
||||
totalUsers: prev.totalUsers + Math.floor(Math.random() * 10000) + 1000,
|
||||
dataGrowthRate: +(Math.random() * 5 + 12).toFixed(1),
|
||||
assetGrowthRate: +(Math.random() * 8 + 18).toFixed(1),
|
||||
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)
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
if (searchQuery.trim()) {
|
||||
// 跳转到智能搜索页面
|
||||
window.location.href = `/intelligent-search?q=${encodeURIComponent(searchQuery)}`
|
||||
router.push(`/intelligent-search?q=${encodeURIComponent(searchQuery)}`)
|
||||
}
|
||||
}
|
||||
|
||||
const formatNumber = (num: number) => {
|
||||
// 格式化数字显示
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000000) {
|
||||
return (num / 1000000000).toFixed(1) + "B"
|
||||
return `${(num / 1000000000).toFixed(1)}B`
|
||||
}
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + "M"
|
||||
return `${(num / 1000000).toFixed(1)}M`
|
||||
}
|
||||
if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + "K"
|
||||
return `${(num / 1000).toFixed(1)}K`
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="container mx-auto py-6 space-y-6">
|
||||
<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="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">概览</h1>
|
||||
<p className="text-muted-foreground mt-1 flex items-center gap-2">
|
||||
数据资产中台总览
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
{lastUpdate.toLocaleTimeString()}
|
||||
<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>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="搜索用户或流量关键词..."
|
||||
className="pl-10 pr-20"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyPress={(e) => e.key === "Enter" && handleSearch()}
|
||||
/>
|
||||
<Button size="sm" className="absolute right-1 top-1/2 transform -translate-y-1/2" onClick={handleSearch}>
|
||||
搜索
|
||||
<Button
|
||||
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>
|
||||
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={isRefreshing}>
|
||||
<RefreshCw className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||
{isRefreshing ? "刷新中" : "刷新"}
|
||||
</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" />
|
||||
<Input
|
||||
placeholder="搜索用户或流量关键词..."
|
||||
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>
|
||||
</div>
|
||||
|
||||
{/* 核心用户数据展示 - 40亿用户为中心 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* 中心用户总数卡片 */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card className="h-full border-2 border-blue-200 bg-gradient-to-br from-blue-50 to-indigo-50">
|
||||
<CardHeader className="text-center pb-2">
|
||||
<CardTitle className="text-lg flex items-center justify-center gap-2">
|
||||
<Globe className="h-6 w-6 text-blue-600" />
|
||||
用户总数
|
||||
<TooltipHelp content="平台累计用户总数,包括所有注册和识别的用户" />
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center">
|
||||
<div className="text-5xl font-bold text-blue-600 mb-2">{formatNumber(coreData.totalUsers)}</div>
|
||||
<div className="text-sm text-muted-foreground">全球用户覆盖</div>
|
||||
<div className="mt-4 grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="bg-white/50 rounded p-2">
|
||||
<div className="font-medium">活跃用户</div>
|
||||
<div className="text-green-600 font-bold">{formatNumber(coreData.activeUsers)}</div>
|
||||
</div>
|
||||
<div className="bg-white/50 rounded p-2">
|
||||
<div className="font-medium">活跃率</div>
|
||||
<div className="text-blue-600 font-bold">
|
||||
{((coreData.activeUsers / coreData.totalUsers) * 100).toFixed(1)}%
|
||||
{/* 核心数据展示 - 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>
|
||||
</div>
|
||||
|
||||
{/* 右侧数据指标 */}
|
||||
<div className="lg:col-span-2 grid grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-1">
|
||||
用户资产增长
|
||||
<TooltipHelp content="用户资产价值的增长率,反映用户价值提升情况" />
|
||||
</CardTitle>
|
||||
<TrendingUp className="h-4 w-4 text-green-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-600">+{coreData.assetGrowthRate}%</div>
|
||||
<p className="text-xs text-muted-foreground">月度增长率</p>
|
||||
<div className="mt-2 h-2 bg-gray-200 rounded-full">
|
||||
<div
|
||||
className="h-2 bg-green-500 rounded-full transition-all duration-500"
|
||||
style={{ width: `${Math.min(coreData.assetGrowthRate * 2, 100)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-1">
|
||||
数据增长
|
||||
<TooltipHelp content="数据量的增长速度,包括用户行为数据、交易数据等" />
|
||||
</CardTitle>
|
||||
<Database className="h-4 w-4 text-blue-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-blue-600">+{coreData.dataGrowthRate}%</div>
|
||||
<p className="text-xs text-muted-foreground">数据量: {coreData.dataVolume}</p>
|
||||
<div className="mt-2 h-2 bg-gray-200 rounded-full">
|
||||
<div
|
||||
className="h-2 bg-blue-500 rounded-full transition-all duration-500"
|
||||
style={{ width: `${Math.min(coreData.dataGrowthRate * 3, 100)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-1">
|
||||
实时查询
|
||||
<TooltipHelp content="当前实时查询次数,反映系统活跃度" />
|
||||
</CardTitle>
|
||||
<Zap className="h-4 w-4 text-yellow-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-yellow-600">{formatNumber(coreData.realTimeQueries)}</div>
|
||||
<p className="text-xs text-muted-foreground">实时查询次数</p>
|
||||
<div className="flex items-center mt-2">
|
||||
<div className="w-2 h-2 bg-yellow-500 rounded-full animate-pulse mr-2"></div>
|
||||
<span className="text-xs text-green-600">实时更新中</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-1">
|
||||
系统状态
|
||||
<TooltipHelp content="系统整体健康状态和AI分析能力" />
|
||||
</CardTitle>
|
||||
<Activity className="h-4 w-4 text-purple-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-purple-600">{coreData.systemHealth}%</div>
|
||||
<p className="text-xs text-muted-foreground">AI分析: {formatNumber(coreData.aiAnalysis)} 次</p>
|
||||
<div className="flex items-center mt-2">
|
||||
<Badge className="bg-green-100 text-green-800 text-xs">系统正常</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 数据分布概览 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">高价值用户</CardTitle>
|
||||
<Target className="h-4 w-4 text-red-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">2.8M</div>
|
||||
<p className="text-xs text-muted-foreground">占比 7.2%</p>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">活跃用户</CardTitle>
|
||||
<UserCheck className="h-4 w-4 text-green-500" />
|
||||
<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="text-2xl font-bold">28.5M</div>
|
||||
<p className="text-xs text-muted-foreground">日活跃用户</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<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>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">流量关键词</CardTitle>
|
||||
<BarChart3 className="h-4 w-4 text-blue-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">156K</div>
|
||||
<p className="text-xs text-muted-foreground">热门关键词数量</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<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>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">数据源</CardTitle>
|
||||
<Database className="h-4 w-4 text-purple-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">28</div>
|
||||
<p className="text-xs text-muted-foreground">已接入数据源</p>
|
||||
<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>
|
||||
|
||||
{/* 实时活动流 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Activity className="h-5 w-5" />
|
||||
实时数据流
|
||||
<TooltipHelp content="系统实时数据处理和分析活动" />
|
||||
</CardTitle>
|
||||
<CardDescription>实时监控数据处理和用户活动</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<div>
|
||||
<div className="font-medium">用户数据同步</div>
|
||||
<div className="text-sm text-muted-foreground">新增 1,247 个用户记录</div>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className="bg-green-100 text-green-800">实时</Badge>
|
||||
{/* 快速访问入口 */}
|
||||
<div 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>
|
||||
|
||||
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
|
||||
<div>
|
||||
<div className="font-medium">AI分析完成</div>
|
||||
<div className="text-sm text-muted-foreground">用户画像分析处理完成</div>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className="bg-blue-100 text-blue-800">2分钟前</Badge>
|
||||
<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>
|
||||
|
||||
<div className="flex items-center justify-between p-3 bg-purple-50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-2 h-2 bg-purple-500 rounded-full animate-pulse"></div>
|
||||
<div>
|
||||
<div className="font-medium">流量关键词更新</div>
|
||||
<div className="text-sm text-muted-foreground">发现 89 个新热门关键词</div>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className="bg-purple-100 text-purple-800">5分钟前</Badge>
|
||||
<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>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<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>
|
||||
</TooltipProvider>
|
||||
<Toaster />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
311
app/rfm/page.tsx
Normal file
311
app/rfm/page.tsx
Normal file
@@ -0,0 +1,311 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import RfmBadge from "@/components/rfm/rfm-badge"
|
||||
import { Download, BarChart3, Sparkles, Upload, SlidersHorizontal } from 'lucide-react'
|
||||
|
||||
type AnalyzeResult = {
|
||||
user_id: string
|
||||
rfm_score: { R: number; F: number; M: number; total: number; grade: "S" | "A" | "B" | "C" | "D" }
|
||||
tags: {
|
||||
emotion?: "积极" | "中性" | "消极"
|
||||
behavior?: string[]
|
||||
intent?: "弱意图" | "中等意图" | "强意图"
|
||||
lifecycle?: "新用户" | "活跃用户" | "沉睡用户" | "流失风险"
|
||||
value?: "高" | "中" | "低"
|
||||
}
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export default function RfmPage() {
|
||||
const [form, setForm] = useState({
|
||||
user_id: "wxid_demo_01",
|
||||
last_active: new Date().toISOString(),
|
||||
interactions: 12,
|
||||
amount: 880,
|
||||
chat_logs: "想了解一下价格;今天有活动吗",
|
||||
source: "wechat",
|
||||
useAI: false,
|
||||
})
|
||||
const [weights, setWeights] = useState<{ R: number; F: number; M: number }>({ R: 0.5, F: 0.3, M: 0.2 })
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [result, setResult] = useState<AnalyzeResult | null>(null)
|
||||
const [summary, setSummary] = useState<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
const res = await fetch("/api/rfm/weights")
|
||||
const data = await res.json()
|
||||
if (data.success) setWeights(data.data)
|
||||
})()
|
||||
}, [])
|
||||
|
||||
const analyze = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch("/api/rfm/analyze", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
...form,
|
||||
chat_logs: form.chat_logs.split(";").map((s) => s.trim()).filter(Boolean),
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
setResult(data.data[0])
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const loadSummary = async () => {
|
||||
const res = await fetch("/api/rfm/group_summary")
|
||||
const data = await res.json()
|
||||
if (data.success) setSummary(data.data)
|
||||
}
|
||||
|
||||
const exportCsv = () => {
|
||||
window.open("/api/rfm/dump_csv", "_blank")
|
||||
}
|
||||
|
||||
const onUploadCsv = async (file: File) => {
|
||||
const text = await file.text()
|
||||
const lines = text.split(/\r?\n/).filter(Boolean)
|
||||
if (lines.length <= 1) return
|
||||
const header = lines[0].split(",").map((s) => s.trim())
|
||||
const payload = lines.slice(1).map((line) => {
|
||||
const cols = line.split(",")
|
||||
const row: any = {}
|
||||
header.forEach((h, i) => (row[h] = cols[i]))
|
||||
return {
|
||||
user_id: row.user_id,
|
||||
last_active: row.last_active,
|
||||
interactions: Number(row.interactions || 0),
|
||||
amount: Number(row.amount || 0),
|
||||
chat_logs: (row.chat_logs || "").split(";").filter(Boolean),
|
||||
source: row.source || "import",
|
||||
}
|
||||
})
|
||||
await fetch("/api/rfm/analyze", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
await loadSummary()
|
||||
}
|
||||
|
||||
const saveWeights = async () => {
|
||||
// normalize via API
|
||||
const res = await fetch("/api/rfm/weights", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
R: Math.max(0.0001, weights.R),
|
||||
F: Math.max(0.0001, weights.F),
|
||||
M: Math.max(0.0001, weights.M),
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.success) setWeights(data.data)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">RFM 智能标签引擎</h1>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={loadSummary}>
|
||||
<BarChart3 className="w-4 h-4 mr-2" />
|
||||
分布汇总
|
||||
</Button>
|
||||
<Button variant="outline" onClick={exportCsv}>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出CSV
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 权重配置 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<SlidersHorizontal className="w-4 h-4" />
|
||||
权重配置(R/F/M)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-3 gap-3">
|
||||
{(["R", "F", "M"] as const).map((k) => (
|
||||
<div key={k} className="space-y-2">
|
||||
<Label htmlFor={`w-${k}`}>{k}</Label>
|
||||
<Input
|
||||
id={`w-${k}`}
|
||||
type="number"
|
||||
min={0}
|
||||
step="0.05"
|
||||
value={weights[k]}
|
||||
onChange={(e) => setWeights((p) => ({ ...p, [k]: Number(e.target.value) }))}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="col-span-3 flex justify-end">
|
||||
<Button onClick={saveWeights}>保存权重</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 在线分析 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
在线分析
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="user_id">用户ID</Label>
|
||||
<Input
|
||||
id="user_id"
|
||||
value={form.user_id}
|
||||
onChange={(e) => setForm((p) => ({ ...p, user_id: e.target.value }))}
|
||||
placeholder="wxid_xxx 或手机号散列等"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="last_active">最后活跃时间</Label>
|
||||
<Input
|
||||
id="last_active"
|
||||
type="datetime-local"
|
||||
value={new Date(form.last_active).toISOString().slice(0, 16)}
|
||||
onChange={(e) => setForm((p) => ({ ...p, last_active: new Date(e.target.value).toISOString() }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="interactions">行为频次</Label>
|
||||
<Input
|
||||
id="interactions"
|
||||
type="number"
|
||||
value={form.interactions}
|
||||
onChange={(e) => setForm((p) => ({ ...p, interactions: Number(e.target.value) }))}
|
||||
min={0}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="amount">累计金额</Label>
|
||||
<Input
|
||||
id="amount"
|
||||
type="number"
|
||||
value={form.amount}
|
||||
onChange={(e) => setForm((p) => ({ ...p, amount: Number(e.target.value) }))}
|
||||
min={0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="chat_logs">聊天片段(分号分隔)</Label>
|
||||
<Textarea
|
||||
id="chat_logs"
|
||||
rows={3}
|
||||
value={form.chat_logs}
|
||||
onChange={(e) => setForm((p) => ({ ...p, chat_logs: e.target.value }))}
|
||||
placeholder="想了解一下价格; 今天有活动吗"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="source">来源渠道</Label>
|
||||
<Input
|
||||
id="source"
|
||||
value={form.source}
|
||||
onChange={(e) => setForm((p) => ({ ...p, source: e.target.value }))}
|
||||
placeholder="wechat / douyin / xhs / form"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id="useAI"
|
||||
type="checkbox"
|
||||
checked={form.useAI}
|
||||
onChange={(e) => setForm((p) => ({ ...p, useAI: e.target.checked }))}
|
||||
/>
|
||||
<Label htmlFor="useAI">启用AI标签增强(自动识别情绪/意图/行为)</Label>
|
||||
</div>
|
||||
<Button onClick={analyze} disabled={loading}>
|
||||
{loading ? "分析中..." : "开始分析并入库"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{result && (
|
||||
<div className="mt-4 p-4 border rounded-lg space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<RfmBadge grade={result.rfm_score.grade} />
|
||||
<Badge variant="secondary">R: {result.rfm_score.R}</Badge>
|
||||
<Badge variant="secondary">F: {result.rfm_score.F}</Badge>
|
||||
<Badge variant="secondary">M: {result.rfm_score.M}</Badge>
|
||||
<Badge variant="outline">Total: {result.rfm_score.total}</Badge>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{result.tags.value && <Badge className="bg-indigo-100 text-indigo-800">价值:{result.tags.value}</Badge>}
|
||||
{result.tags.intent && <Badge className="bg-purple-100 text-purple-800">意图:{result.tags.intent}</Badge>}
|
||||
{result.tags.emotion && <Badge className="bg-pink-100 text-pink-800">情绪:{result.tags.emotion}</Badge>}
|
||||
{result.tags.lifecycle && (
|
||||
<Badge className="bg-amber-100 text-amber-800">生命周期:{result.tags.lifecycle}</Badge>
|
||||
)}
|
||||
{(result.tags.behavior ?? []).map((b, i) => (
|
||||
<Badge key={i} variant="secondary">
|
||||
{b}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{summary && (
|
||||
<div className="mt-4 p-4 border rounded-lg space-y-2">
|
||||
<div className="font-medium">分布汇总</div>
|
||||
<div className="text-sm">等级: {JSON.stringify(summary.gradeCount)}</div>
|
||||
<div className="text-sm">价值: {JSON.stringify(summary.valueCount)}</div>
|
||||
<div className="text-sm">生命周期: {JSON.stringify(summary.lifecycleCount)}</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 批量导入 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Upload className="w-4 h-4" />
|
||||
批量导入 CSV 并分析
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
CSV 列要求:user_id,last_active,interactions,amount,chat_logs,source(chat_logs 用分号分隔)
|
||||
</p>
|
||||
<Input
|
||||
type="file"
|
||||
accept=".csv,text/csv"
|
||||
onChange={(e) => {
|
||||
const f = e.target.files?.[0]
|
||||
if (f) onUploadCsv(f)
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,858 +1,96 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useParams } from "next/navigation"
|
||||
import MobileHeader from "@/app/components/MobileHeader"
|
||||
import BottomNav from "@/app/components/BottomNav"
|
||||
import Section from "@/components/user-portrait/mobile/section"
|
||||
import ProfileHeader from "@/components/user-portrait/mobile/profile-header"
|
||||
import MetricsRFM from "@/components/user-portrait/mobile/metrics-rfm"
|
||||
import InteractionsList from "@/components/user-portrait/mobile/interactions-list"
|
||||
import PurchaseHistory from "@/components/user-portrait/mobile/purchase-history"
|
||||
import WechatAccounts from "@/components/user-portrait/mobile/wechat-accounts"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Avatar } from "@/components/ui/avatar"
|
||||
import { ArrowLeft, Edit, User, Activity, DollarSign, Clock, Phone, MessageCircle } from "lucide-react"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
|
||||
// 模拟用户数据 - 详细版
|
||||
const getUserDetail = (id: string) => {
|
||||
const baseUsers = [
|
||||
{
|
||||
id: "1",
|
||||
name: "张三",
|
||||
avatar: "/placeholder.svg?height=80&width=80",
|
||||
company: "科技有限公司",
|
||||
position: "技术总监",
|
||||
email: "zhangsan@example.com",
|
||||
phone: "13800138001",
|
||||
tags: ["高价值", "活跃用户", "技术决策者", "iPhone用户", "iOS 16.5", "MacBook Pro用户"],
|
||||
recency: 1, // 天数
|
||||
frequency: 24, // 次数/月
|
||||
monetary: 12500, // 元
|
||||
rfmScore: 93, // 0-100
|
||||
lastActivity: "2023-03-22T08:30:00",
|
||||
interactions: [
|
||||
{ type: "咨询", count: 8, lastDate: "2023-03-22" },
|
||||
{ type: "产品演示", count: 3, lastDate: "2023-03-15" },
|
||||
{ type: "技术支持", count: 12, lastDate: "2023-03-20" },
|
||||
{ type: "培训", count: 1, lastDate: "2023-02-10" },
|
||||
],
|
||||
purchaseHistory: [
|
||||
{ product: "企业版SaaS", date: "2023-01-15", amount: 8500 },
|
||||
{ product: "技术支持服务", date: "2023-02-20", amount: 3000 },
|
||||
{ product: "定制开发", date: "2023-03-10", amount: 1000 },
|
||||
],
|
||||
wechatAccounts: [
|
||||
{ id: "wx1", name: "张三的微信", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号", "活跃"] },
|
||||
{ id: "wx2", name: "张三工作号", avatar: "/placeholder.svg?height=40&width=40", tags: ["工作账号"] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "李四",
|
||||
avatar: "/placeholder.svg?height=80&width=80",
|
||||
company: "服务科技公司",
|
||||
position: "市场总监",
|
||||
email: "lisi@example.com",
|
||||
phone: "13900139001",
|
||||
tags: ["中价值", "潜在客户", "市场负责人", "Android用户", "MIUI 14", "小米13用户"],
|
||||
recency: 5, // 天数
|
||||
frequency: 12, // 次数/月
|
||||
monetary: 6800, // 元
|
||||
rfmScore: 75, // 0-100
|
||||
lastActivity: "2023-03-18T10:15:00",
|
||||
interactions: [
|
||||
{ type: "咨询", count: 5, lastDate: "2023-03-18" },
|
||||
{ type: "产品演示", count: 2, lastDate: "2023-03-10" },
|
||||
{ type: "市场合作", count: 4, lastDate: "2023-03-05" },
|
||||
{ type: "方案讨论", count: 1, lastDate: "2023-02-25" },
|
||||
],
|
||||
purchaseHistory: [
|
||||
{ product: "标准版SaaS", date: "2023-02-10", amount: 4800 },
|
||||
{ product: "增值服务", date: "2023-03-15", amount: 2000 },
|
||||
],
|
||||
wechatAccounts: [
|
||||
{ id: "wx3", name: "李四的微信", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号"] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "王五",
|
||||
avatar: "/placeholder.svg?height=80&width=80",
|
||||
company: "小型创业公司",
|
||||
position: "技术主管",
|
||||
email: "wangwu@example.com",
|
||||
phone: "13700137001",
|
||||
tags: ["低价值", "非活跃", "技术团队", "iPhone用户", "iOS 15.6", "iPad用户"],
|
||||
recency: 15, // 天数
|
||||
frequency: 5, // 次数/月
|
||||
monetary: 2200, // 元
|
||||
rfmScore: 48, // 0-100
|
||||
lastActivity: "2023-03-08T14:20:00",
|
||||
interactions: [
|
||||
{ type: "咨询", count: 3, lastDate: "2023-03-08" },
|
||||
{ type: "技术支持", count: 2, lastDate: "2023-02-20" },
|
||||
],
|
||||
purchaseHistory: [{ product: "基础版SaaS", date: "2023-01-05", amount: 2200 }],
|
||||
wechatAccounts: [
|
||||
{ id: "wx4", name: "王五的微信", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号", "不活跃"] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "赵六",
|
||||
avatar: "/placeholder.svg?height=80&width=80",
|
||||
company: "金融科技公司",
|
||||
position: "财务总监",
|
||||
email: "zhaoliu@example.com",
|
||||
phone: "13600136001",
|
||||
tags: ["高价值", "活跃用户", "财务决策者", "Android用户", "EMUI 12", "华为P50用户"],
|
||||
recency: 2, // 天数
|
||||
frequency: 18, // 次数/月
|
||||
monetary: 9600, // 元
|
||||
rfmScore: 85, // 0-100
|
||||
lastActivity: "2023-03-21T09:45:00",
|
||||
interactions: [
|
||||
{ type: "咨询", count: 6, lastDate: "2023-03-21" },
|
||||
{ type: "产品演示", count: 2, lastDate: "2023-03-15" },
|
||||
{ type: "方案讨论", count: 8, lastDate: "2023-03-18" },
|
||||
{ type: "培训", count: 2, lastDate: "2023-02-28" },
|
||||
],
|
||||
purchaseHistory: [
|
||||
{ product: "企业版SaaS", date: "2023-01-20", amount: 7500 },
|
||||
{ product: "数据分析模块", date: "2023-02-15", amount: 2100 },
|
||||
],
|
||||
wechatAccounts: [
|
||||
{ id: "wx5", name: "赵六的微信", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号", "活跃"] },
|
||||
{ id: "wx6", name: "赵六小号", avatar: "/placeholder.svg?height=40&width=40", tags: ["辅助账号"] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "钱七",
|
||||
avatar: "/placeholder.svg?height=80&width=80",
|
||||
company: "软件服务公司",
|
||||
position: "产品经理",
|
||||
email: "qianqi@example.com",
|
||||
phone: "13500135001",
|
||||
tags: ["中价值", "一般活跃", "产品经理", "iPhone用户", "iOS 16.2", "MacBook Air用户"],
|
||||
recency: 7, // 天数
|
||||
frequency: 10, // 次数/月
|
||||
monetary: 5400, // 元
|
||||
rfmScore: 68, // 0-100
|
||||
lastActivity: "2023-03-16T11:30:00",
|
||||
interactions: [
|
||||
{ type: "咨询", count: 4, lastDate: "2023-03-16" },
|
||||
{ type: "产品演示", count: 1, lastDate: "2023-03-05" },
|
||||
{ type: "方案讨论", count: 3, lastDate: "2023-03-10" },
|
||||
{ type: "需求沟通", count: 2, lastDate: "2023-02-25" },
|
||||
],
|
||||
purchaseHistory: [
|
||||
{ product: "标准版SaaS", date: "2023-02-05", amount: 4800 },
|
||||
{ product: "增值服务", date: "2023-03-01", amount: 600 },
|
||||
],
|
||||
wechatAccounts: [
|
||||
{ id: "wx7", name: "钱七的微信", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号"] },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return baseUsers.find((user) => user.id === id) || baseUsers[0]
|
||||
type Detail = {
|
||||
id: string
|
||||
name: string
|
||||
avatar?: string
|
||||
email: string
|
||||
phone: string
|
||||
tags: string[]
|
||||
recency: number
|
||||
frequency: number
|
||||
monetary: number
|
||||
rfmScore: number
|
||||
lastActivity: string
|
||||
interactions: { id: string; type: string; time: string; note?: string }[]
|
||||
purchaseHistory: { id: string; amount: number; time: string; item: string }[]
|
||||
wechatAccounts: { id: string; nickname: string; avatar?: string }[]
|
||||
status: "活跃" | "沉睡" | "流失风险"
|
||||
}
|
||||
|
||||
// RFM评分卡组件
|
||||
function RFMScoreCard({
|
||||
title,
|
||||
value,
|
||||
maxValue,
|
||||
icon,
|
||||
description,
|
||||
color,
|
||||
}: {
|
||||
title: string
|
||||
value: number
|
||||
maxValue: number
|
||||
icon: React.ReactNode
|
||||
description: string
|
||||
color: string
|
||||
}) {
|
||||
const percentage = (value / maxValue) * 100
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||
{icon}
|
||||
</div>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{value}
|
||||
{maxValue === 1 ? "" : ""}
|
||||
</div>
|
||||
<Progress value={percentage} className={`h-2 mt-2 ${color}`} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default function UserPortraitDetail({ params }: { params: { id: string } }) {
|
||||
const router = useRouter()
|
||||
const [user, setUser] = useState<any>(null)
|
||||
const [activeTab, setActiveTab] = useState("profile")
|
||||
export default function UserDetailPage() {
|
||||
const params = useParams<{ id: string }>()
|
||||
const [data, setData] = useState<Detail | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// 获取用户详情
|
||||
const userDetail = getUserDetail(params.id)
|
||||
setUser(userDetail)
|
||||
}, [params.id])
|
||||
|
||||
if (!user) {
|
||||
return <div className="container mx-auto py-8">加载中...</div>
|
||||
}
|
||||
|
||||
// 获取RFM评分对应的背景颜色类
|
||||
const getRfmScoreBgClass = (score: number) => {
|
||||
if (score >= 81) return "bg-green-100"
|
||||
if (score >= 51) return "bg-yellow-100"
|
||||
return "bg-red-100"
|
||||
}
|
||||
|
||||
// 获取RFM评分对应的文本颜色类
|
||||
const getRfmScoreTextClass = (score: number) => {
|
||||
if (score >= 81) return "text-green-800"
|
||||
if (score >= 51) return "text-yellow-800"
|
||||
return "text-red-800"
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit" })
|
||||
}
|
||||
if (!params?.id) return
|
||||
setLoading(true)
|
||||
fetch(`/api/users?id=${params.id}`)
|
||||
.then((r) => r.json())
|
||||
.then((res) => setData(res?.data ?? null))
|
||||
.finally(() => setLoading(false))
|
||||
}, [params?.id])
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-6 space-y-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onClick={() => router.back()}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<h1 className="text-2xl font-bold tracking-tight">用户画像详情</h1>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="profile">基本信息</TabsTrigger>
|
||||
<TabsTrigger value="rfm">RFM估值分析</TabsTrigger>
|
||||
<TabsTrigger value="wechat">微信账号</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="profile" className="space-y-6 pt-4">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-1 space-y-6">
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<div className="flex justify-center mb-2">
|
||||
<Avatar className="h-24 w-24">
|
||||
<img src={user.avatar || "/placeholder.svg"} alt={user.name} />
|
||||
</Avatar>
|
||||
</div>
|
||||
<CardTitle>{user.name}</CardTitle>
|
||||
<CardDescription>
|
||||
{user.company} · {user.position}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm">{user.email}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Phone className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm">{user.phone}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm">最近活动:{formatDate(user.lastActivity)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageCircle className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm">关联微信:{user.wechatAccounts.length}个</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
编辑用户信息
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>用户标签</CardTitle>
|
||||
<CardDescription>用户的特征标签</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{user.tags.map((tag: string, index: number) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>互动记录</CardTitle>
|
||||
<CardDescription>用户最近的互动情况</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{user.interactions.map((interaction: any, index: number) => (
|
||||
<div key={index} className="flex justify-between items-center pb-2 border-b">
|
||||
<div>
|
||||
<p className="font-medium">{interaction.type}</p>
|
||||
<p className="text-xs text-muted-foreground">最近:{interaction.lastDate}</p>
|
||||
</div>
|
||||
<Badge variant="outline">{interaction.count}次</Badge>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>购买历史</CardTitle>
|
||||
<CardDescription>用户的产品购买记录</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-md border">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
产品
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
日期
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
金额
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{user.purchaseHistory.map((purchase: any, index: number) => (
|
||||
<tr key={index}>
|
||||
<td className="px-4 py-4 whitespace-nowrap">{purchase.product}</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">{purchase.date}</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-right">
|
||||
¥{purchase.amount.toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr className="bg-gray-50">
|
||||
<td className="px-4 py-4 whitespace-nowrap font-medium" colSpan={2}>
|
||||
总计
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-right font-bold">
|
||||
¥
|
||||
{user.purchaseHistory
|
||||
.reduce((total: number, purchase: any) => total + purchase.amount, 0)
|
||||
.toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>用户行为分析</CardTitle>
|
||||
<CardDescription>基于用户行为数据的分析</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium">行为特征</h3>
|
||||
<div className="p-3 rounded-md bg-blue-50">
|
||||
<p className="text-sm text-blue-800">
|
||||
{user.rfmScore >= 81
|
||||
? "该用户是高度活跃的价值客户,对产品功能有深入了解,经常与团队互动并提供反馈。"
|
||||
: user.rfmScore >= 51
|
||||
? "该用户展现出持续的参与意愿,使用核心功能,但尚未充分利用产品的全部价值。"
|
||||
: "该用户参与度较低,使用频率不高,需要主动引导以提升产品使用价值。"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium">建议行动</h3>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="border rounded-md p-3">
|
||||
<p className="text-sm font-medium">短期行动</p>
|
||||
<ul className="mt-2 space-y-1">
|
||||
{user.rfmScore >= 81 ? (
|
||||
<>
|
||||
<li className="text-xs">• 提供专属客户成功经理</li>
|
||||
<li className="text-xs">• 邀请参与产品路线图讨论</li>
|
||||
<li className="text-xs">• 考虑提供战略合作机会</li>
|
||||
</>
|
||||
) : user.rfmScore >= 51 ? (
|
||||
<>
|
||||
<li className="text-xs">• 安排深度产品培训</li>
|
||||
<li className="text-xs">• 定期分享使用最佳实践</li>
|
||||
<li className="text-xs">• 提供个性化的功能推荐</li>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<li className="text-xs">• 提供入门指南和基础培训</li>
|
||||
<li className="text-xs">• 发送使用提醒和简单教程</li>
|
||||
<li className="text-xs">• 收集反馈了解痛点</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="border rounded-md p-3">
|
||||
<p className="text-sm font-medium">长期策略</p>
|
||||
<ul className="mt-2 space-y-1">
|
||||
{user.rfmScore >= 81 ? (
|
||||
<>
|
||||
<li className="text-xs">• 建立战略合作伙伴关系</li>
|
||||
<li className="text-xs">• 开发定制化解决方案</li>
|
||||
<li className="text-xs">• 邀请参与早期测试计划</li>
|
||||
</>
|
||||
) : user.rfmScore >= 51 ? (
|
||||
<>
|
||||
<li className="text-xs">• 发展为高价值客户</li>
|
||||
<li className="text-xs">• 提供升级路径和方案</li>
|
||||
<li className="text-xs">• 增加产品使用深度</li>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<li className="text-xs">• 培养参与度提升频率</li>
|
||||
<li className="text-xs">• 提供简化版功能体验</li>
|
||||
<li className="text-xs">• 定期回访了解需求变化</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-purple-50">
|
||||
<MobileHeader onMenuToggle={() => {}} title="用户详情" />
|
||||
<main className="container mx-auto px-4 pb-24 space-y-4">
|
||||
{loading ? (
|
||||
<div className="rounded-xl bg-white/60 backdrop-blur p-6 text-sm text-muted-foreground">
|
||||
加载中…
|
||||
</div>
|
||||
</TabsContent>
|
||||
) : data ? (
|
||||
<div className="space-y-4">
|
||||
<section className="rounded-2xl bg-white/60 backdrop-blur-md p-4 shadow-sm border">
|
||||
<ProfileHeader
|
||||
name={data.name}
|
||||
avatar={data.avatar}
|
||||
email={data.email}
|
||||
phone={data.phone}
|
||||
tags={data.tags}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<TabsContent value="rfm" className="space-y-6 pt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<CardTitle>RFM估值模型分析</CardTitle>
|
||||
<CardDescription>综合用户价值评估</CardDescription>
|
||||
</div>
|
||||
<div className={`px-4 py-2 rounded-full ${getRfmScoreBgClass(user.rfmScore)}`}>
|
||||
<span className={`text-lg font-bold ${getRfmScoreTextClass(user.rfmScore)}`}>{user.rfmScore}</span>
|
||||
<span className={`text-sm ml-1 ${getRfmScoreTextClass(user.rfmScore)}`}>/ 100</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<RFMScoreCard
|
||||
title="R - 最近度"
|
||||
value={user.recency}
|
||||
maxValue={30}
|
||||
icon={<Clock className="h-4 w-4 text-blue-500" />}
|
||||
description="最近一次互动的天数"
|
||||
color="bg-blue-500"
|
||||
/>
|
||||
<RFMScoreCard
|
||||
title="F - 频率"
|
||||
value={user.frequency}
|
||||
maxValue={30}
|
||||
icon={<Activity className="h-4 w-4 text-purple-500" />}
|
||||
description="每月互动次数"
|
||||
color="bg-purple-500"
|
||||
/>
|
||||
<RFMScoreCard
|
||||
title="M - 金额"
|
||||
value={user.monetary}
|
||||
maxValue={15000}
|
||||
icon={<DollarSign className="h-4 w-4 text-green-500" />}
|
||||
description="消费金额(元)"
|
||||
color="bg-green-500"
|
||||
/>
|
||||
</div>
|
||||
<Section title="RFM 指标">
|
||||
<MetricsRFM
|
||||
recency={data.recency}
|
||||
frequency={data.frequency}
|
||||
monetary={data.monetary}
|
||||
rfmScore={data.rfmScore}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<div className="mt-6 space-y-4">
|
||||
<h3 className="text-lg font-medium">RFM分析解读</h3>
|
||||
<Section title="互动记录">
|
||||
<InteractionsList items={data.interactions} />
|
||||
</Section>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 border rounded-md">
|
||||
<h4 className="font-medium mb-2">R值分析 - 最近度</h4>
|
||||
<p className="text-sm text-gray-600">
|
||||
{user.recency <= 3
|
||||
? "用户近期有活动,保持高度活跃。"
|
||||
: user.recency <= 10
|
||||
? "用户最近活动频率适中,需要保持关注。"
|
||||
: "用户较长时间未活动,存在流失风险。"}
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
<div className="flex justify-between text-xs text-gray-500 mb-1">
|
||||
<span>高风险</span>
|
||||
<span>低风险</span>
|
||||
</div>
|
||||
<div className="h-2 w-full bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-blue-500 rounded-full"
|
||||
style={{ width: `${100 - (user.recency / 30) * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Section title="购买历史">
|
||||
<PurchaseHistory items={data.purchaseHistory} />
|
||||
</Section>
|
||||
|
||||
<div className="p-4 border rounded-md">
|
||||
<h4 className="font-medium mb-2">F值分析 - 频率</h4>
|
||||
<p className="text-sm text-gray-600">
|
||||
{user.frequency >= 20
|
||||
? "用户互动频率非常高,是核心用户。"
|
||||
: user.frequency >= 10
|
||||
? "用户互动频率良好,有较强的参与度。"
|
||||
: "用户互动频率较低,需要提升参与度。"}
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
<div className="flex justify-between text-xs text-gray-500 mb-1">
|
||||
<span>低频率</span>
|
||||
<span>高频率</span>
|
||||
</div>
|
||||
<div className="h-2 w-full bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-purple-500 rounded-full"
|
||||
style={{ width: `${(user.frequency / 30) * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-md">
|
||||
<h4 className="font-medium mb-2">M值分析 - 金额</h4>
|
||||
<p className="text-sm text-gray-600">
|
||||
{user.monetary >= 10000
|
||||
? "用户消费金额高,是高价值客户。"
|
||||
: user.monetary >= 5000
|
||||
? "用户消费金额中等,有提升空间。"
|
||||
: "用户消费金额较低,需要刺激消费。"}
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
<div className="flex justify-between text-xs text-gray-500 mb-1">
|
||||
<span>低消费</span>
|
||||
<span>高消费</span>
|
||||
</div>
|
||||
<div className="h-2 w-full bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-green-500 rounded-full"
|
||||
style={{ width: `${(user.monetary / 15000) * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-md bg-gray-50 mt-4">
|
||||
<h4 className="font-medium mb-2">用户分群结果</h4>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className={getRfmScoreBgClass(user.rfmScore)}>
|
||||
<span className={getRfmScoreTextClass(user.rfmScore)}>
|
||||
{user.rfmScore >= 81 ? "高价值用户" : user.rfmScore >= 51 ? "中价值用户" : "低价值用户"}
|
||||
</span>
|
||||
</Badge>
|
||||
<span className="text-sm text-gray-600">
|
||||
{user.rfmScore >= 81
|
||||
? "需要重点维护的核心客户"
|
||||
: user.rfmScore >= 51
|
||||
? "有发展潜力的成长客户"
|
||||
: "需要激活的沉睡客户"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>价值提升策略</CardTitle>
|
||||
<CardDescription>基于RFM模型的用户价值提升建议</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-4 border rounded-md">
|
||||
<h4 className="font-medium mb-2">营销策略建议</h4>
|
||||
<ul className="space-y-2">
|
||||
{user.rfmScore >= 81 ? (
|
||||
<>
|
||||
<li className="text-sm flex items-start gap-2">
|
||||
<span className="text-green-500 mt-0.5">•</span>
|
||||
<span>提供专属VIP服务和优先支持</span>
|
||||
</li>
|
||||
<li className="text-sm flex items-start gap-2">
|
||||
<span className="text-green-500 mt-0.5">•</span>
|
||||
<span>邀请参与新产品测试和反馈</span>
|
||||
</li>
|
||||
<li className="text-sm flex items-start gap-2">
|
||||
<span className="text-green-500 mt-0.5">•</span>
|
||||
<span>提供专属定制化解决方案</span>
|
||||
</li>
|
||||
</>
|
||||
) : user.rfmScore >= 51 ? (
|
||||
<>
|
||||
<li className="text-sm flex items-start gap-2">
|
||||
<span className="text-yellow-500 mt-0.5">•</span>
|
||||
<span>提供产品升级优惠和增值服务</span>
|
||||
</li>
|
||||
<li className="text-sm flex items-start gap-2">
|
||||
<span className="text-yellow-500 mt-0.5">•</span>
|
||||
<span>定期分享使用技巧和最佳实践</span>
|
||||
</li>
|
||||
<li className="text-sm flex items-start gap-2">
|
||||
<span className="text-yellow-500 mt-0.5">•</span>
|
||||
<span>提供个性化的功能推荐</span>
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<li className="text-sm flex items-start gap-2">
|
||||
<span className="text-red-500 mt-0.5">•</span>
|
||||
<span>提供限时优惠和特别促销</span>
|
||||
</li>
|
||||
<li className="text-sm flex items-start gap-2">
|
||||
<span className="text-red-500 mt-0.5">•</span>
|
||||
<span>发送使用提醒和简单教程</span>
|
||||
</li>
|
||||
<li className="text-sm flex items-start gap-2">
|
||||
<span className="text-red-500 mt-0.5">•</span>
|
||||
<span>提供基础培训和入门指导</span>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-md">
|
||||
<h4 className="font-medium mb-2">价值提升路径</h4>
|
||||
<ul className="space-y-2">
|
||||
{user.rfmScore >= 81 ? (
|
||||
<>
|
||||
<li className="text-sm flex items-start gap-2">
|
||||
<span className="text-green-500 mt-0.5">•</span>
|
||||
<span>发展战略合作伙伴关系</span>
|
||||
</li>
|
||||
<li className="text-sm flex items-start gap-2">
|
||||
<span className="text-green-500 mt-0.5">•</span>
|
||||
<span>探索更多业务合作机会</span>
|
||||
</li>
|
||||
<li className="text-sm flex items-start gap-2">
|
||||
<span className="text-green-500 mt-0.5">•</span>
|
||||
<span>建立长期稳定的合作模式</span>
|
||||
</li>
|
||||
</>
|
||||
) : user.rfmScore >= 51 ? (
|
||||
<>
|
||||
<li className="text-sm flex items-start gap-2">
|
||||
<span className="text-yellow-500 mt-0.5">•</span>
|
||||
<span>提高产品使用深度和广度</span>
|
||||
</li>
|
||||
<li className="text-sm flex items-start gap-2">
|
||||
<span className="text-yellow-500 mt-0.5">•</span>
|
||||
<span>增加用户互动频率和质量</span>
|
||||
</li>
|
||||
<li className="text-sm flex items-start gap-2">
|
||||
<span className="text-yellow-500 mt-0.5">•</span>
|
||||
<span>引导用户尝试高级功能</span>
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<li className="text-sm flex items-start gap-2">
|
||||
<span className="text-red-500 mt-0.5">•</span>
|
||||
<span>重新激活用户兴趣和参与度</span>
|
||||
</li>
|
||||
<li className="text-sm flex items-start gap-2">
|
||||
<span className="text-red-500 mt-0.5">•</span>
|
||||
<span>提供简化版功能体验</span>
|
||||
</li>
|
||||
<li className="text-sm flex items-start gap-2">
|
||||
<span className="text-red-500 mt-0.5">•</span>
|
||||
<span>了解用户需求变化和痛点</span>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-md mt-4">
|
||||
<h4 className="font-medium mb-2">预期效果</h4>
|
||||
<p className="text-sm text-gray-600">
|
||||
{user.rfmScore >= 81
|
||||
? "通过深化合作关系,预计可以提升客户忠诚度,增加高价值服务的采购,并发展为长期战略合作伙伴。"
|
||||
: user.rfmScore >= 51
|
||||
? "通过提升产品使用深度和增值服务,预计可以提高客户满意度,增加产品使用频率,逐步提升客户价值。"
|
||||
: "通过重新激活用户兴趣,预计可以提高用户活跃度,减少流失风险,逐步引导用户成为活跃客户。"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="wechat" className="space-y-6 pt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>关联微信账号</CardTitle>
|
||||
<CardDescription>用户关联的微信账号列表</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{user.wechatAccounts.map((account: any) => (
|
||||
<div key={account.id} className="flex items-center justify-between p-4 border rounded-md">
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="h-12 w-12">
|
||||
<img src={account.avatar || "/placeholder.svg"} alt={account.name} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="font-medium">{account.name}</p>
|
||||
<div className="flex gap-1 mt-1">
|
||||
{account.tags.map((tag: string, index: number) => (
|
||||
<Badge key={index} variant="secondary" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">
|
||||
查看详情
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="outline" className="w-full">
|
||||
<MessageCircle className="mr-2 h-4 w-4" />
|
||||
添加关联微信账号
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>微信互动历史</CardTitle>
|
||||
<CardDescription>用户通过微信的互动记录</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-md border">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
微信账号
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
互动类型
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
时间
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
内容
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{/* 模拟数据 */}
|
||||
<tr>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 h-8 w-8 mr-2">
|
||||
<img
|
||||
className="h-8 w-8 rounded-full"
|
||||
src={user.wechatAccounts[0]?.avatar || "/placeholder.svg"}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-900">{user.wechatAccounts[0]?.name}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
||||
消息
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-500">2023-07-18 14:30</td>
|
||||
<td className="px-4 py-4 text-sm text-gray-500">咨询产品功能和价格</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 h-8 w-8 mr-2">
|
||||
<img
|
||||
className="h-8 w-8 rounded-full"
|
||||
src={user.wechatAccounts[0]?.avatar || "/placeholder.svg"}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-900">{user.wechatAccounts[0]?.name}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">
|
||||
朋友圈
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-500">2023-07-15 09:45</td>
|
||||
<td className="px-4 py-4 text-sm text-gray-500">点赞公司发布的新产品介绍</td>
|
||||
</tr>
|
||||
{user.wechatAccounts.length > 1 && (
|
||||
<tr>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 h-8 w-8 mr-2">
|
||||
<img
|
||||
className="h-8 w-8 rounded-full"
|
||||
src={user.wechatAccounts[1]?.avatar || "/placeholder.svg"}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-900">{user.wechatAccounts[1]?.name}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-purple-100 text-purple-800">
|
||||
群聊
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-500">2023-07-10 16:20</td>
|
||||
<td className="px-4 py-4 text-sm text-gray-500">在技术交流群中分享使用经验</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<Section title="绑定微信账号">
|
||||
<WechatAccounts accounts={data.wechatAccounts.map((w) => ({ id: w.id, nickname: w.nickname, avatar: w.avatar }))} />
|
||||
</Section>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl bg-white/60 backdrop-blur p-6 text-sm text-red-600">
|
||||
未找到该用户
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
<BottomNav />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,567 +1,239 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
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 { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Users, UserPlus, Tags, Target, Activity, Plus, Edit, Search, Filter } from "lucide-react"
|
||||
import { TooltipHelp } from "@/components/ui/tooltip-help"
|
||||
import { TooltipProvider } from "@/components/ui/tooltip"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog"
|
||||
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 [selectedSegment, setSelectedSegment] = useState("all")
|
||||
const [isAddingUser, setIsAddingUser] = useState(false)
|
||||
const [isAddingTag, setIsAddingTag] = useState(false)
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [isAddingUser, setIsAddingUser] = useState(false)
|
||||
const [newUser, setNewUser] = useState({ name: "", phone: "", email: "", tags: [] as string[] })
|
||||
|
||||
// 用户数据
|
||||
const [users, setUsers] = useState([
|
||||
{
|
||||
id: "U001",
|
||||
name: "张三",
|
||||
phone: "13800138001",
|
||||
email: "zhangsan@example.com",
|
||||
tags: ["高价值", "活跃用户", "科技爱好者"],
|
||||
value: 12580,
|
||||
lastActive: "2小时前",
|
||||
source: "微信",
|
||||
registerDate: "2023-01-15",
|
||||
},
|
||||
{
|
||||
id: "U002",
|
||||
name: "李四",
|
||||
phone: "13800138002",
|
||||
email: "lisi@example.com",
|
||||
tags: ["潜在客户", "新用户", "价格敏感"],
|
||||
value: 3240,
|
||||
lastActive: "1天前",
|
||||
source: "抖音",
|
||||
registerDate: "2024-01-10",
|
||||
},
|
||||
{
|
||||
id: "U003",
|
||||
name: "王五",
|
||||
phone: "13800138003",
|
||||
email: "wangwu@example.com",
|
||||
tags: ["高价值", "忠实用户", "奢侈品偏好"],
|
||||
value: 25600,
|
||||
lastActive: "30分钟前",
|
||||
source: "小红书",
|
||||
registerDate: "2022-08-20",
|
||||
},
|
||||
])
|
||||
const [filterOpen, setFilterOpen] = useState(false)
|
||||
const [allTags, setAllTags] = useState<string[]>([])
|
||||
const [filters, setFilters] = useState<FilterValues>({ tags: [], status: [], rfm: [0, 100] })
|
||||
|
||||
// 标签分类数据
|
||||
const [tagCategories, setTagCategories] = useState([
|
||||
{
|
||||
id: "1",
|
||||
name: "用户价值",
|
||||
description: "基于用户消费能力和贡献度的分类",
|
||||
tags: [
|
||||
{ name: "高价值用户", count: 2847, color: "red" },
|
||||
{ name: "中价值用户", count: 12456, color: "blue" },
|
||||
{ name: "低价值用户", count: 8234, color: "gray" },
|
||||
{ name: "潜在价值用户", count: 5287, color: "yellow" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "活跃度",
|
||||
description: "基于用户活跃程度和互动频率的分类",
|
||||
tags: [
|
||||
{ name: "超级活跃", count: 1234, color: "green" },
|
||||
{ name: "活跃用户", count: 8429, color: "blue" },
|
||||
{ name: "一般活跃", count: 15678, color: "yellow" },
|
||||
{ name: "不活跃", count: 3456, color: "gray" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "消费行为",
|
||||
description: "基于用户购买习惯和消费偏好的分类",
|
||||
tags: [
|
||||
{ name: "冲动消费", count: 4567, color: "red" },
|
||||
{ name: "理性消费", count: 12345, color: "blue" },
|
||||
{ name: "价格敏感", count: 8901, color: "yellow" },
|
||||
{ name: "品质优先", count: 6789, color: "purple" },
|
||||
{ name: "奢侈品偏好", count: 2345, color: "pink" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "兴趣偏好",
|
||||
description: "基于用户兴趣爱好和内容偏好的分类",
|
||||
tags: [
|
||||
{ name: "科技爱好者", count: 5678, color: "blue" },
|
||||
{ name: "时尚达人", count: 7890, color: "pink" },
|
||||
{ name: "旅游爱好者", count: 4321, color: "green" },
|
||||
{ name: "美食家", count: 6543, color: "orange" },
|
||||
{ name: "运动健身", count: 3210, color: "red" },
|
||||
{ name: "文艺青年", count: 2109, color: "purple" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "生命周期",
|
||||
description: "基于用户在平台的生命周期阶段分类",
|
||||
tags: [
|
||||
{ name: "新用户", count: 8765, color: "green" },
|
||||
{ name: "成长期用户", count: 12345, color: "blue" },
|
||||
{ name: "成熟期用户", count: 15678, color: "purple" },
|
||||
{ name: "衰退期用户", count: 4321, color: "yellow" },
|
||||
{ name: "流失用户", count: 2109, color: "gray" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "地域分布",
|
||||
description: "基于用户地理位置的分类",
|
||||
tags: [
|
||||
{ name: "一线城市", count: 18765, color: "red" },
|
||||
{ name: "二线城市", count: 15432, color: "blue" },
|
||||
{ name: "三线城市", count: 9876, color: "green" },
|
||||
{ name: "四线及以下", count: 5432, color: "gray" },
|
||||
{ name: "海外用户", count: 1234, color: "purple" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
name: "设备偏好",
|
||||
description: "基于用户使用设备类型的分类",
|
||||
tags: [
|
||||
{ name: "iOS用户", count: 16789, color: "blue" },
|
||||
{ name: "Android用户", count: 23456, color: "green" },
|
||||
{ name: "PC用户", count: 8765, color: "gray" },
|
||||
{ name: "平板用户", count: 4321, color: "purple" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "8",
|
||||
name: "社交属性",
|
||||
description: "基于用户社交行为和影响力的分类",
|
||||
tags: [
|
||||
{ name: "意见领袖", count: 1234, color: "red" },
|
||||
{ name: "活跃分享者", count: 5678, color: "blue" },
|
||||
{ name: "默默关注者", count: 18765, color: "gray" },
|
||||
{ name: "社交新手", count: 8901, 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 getTagColor = (color: string) => {
|
||||
const colors = {
|
||||
red: "bg-red-100 text-red-800",
|
||||
blue: "bg-blue-100 text-blue-800",
|
||||
green: "bg-green-100 text-green-800",
|
||||
yellow: "bg-yellow-100 text-yellow-800",
|
||||
purple: "bg-purple-100 text-purple-800",
|
||||
pink: "bg-pink-100 text-pink-800",
|
||||
orange: "bg-orange-100 text-orange-800",
|
||||
gray: "bg-gray-100 text-gray-800",
|
||||
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])
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/users?meta=tags")
|
||||
.then((r) => r.json())
|
||||
.then((res: any) => setAllTags(res?.data?.tags ?? []))
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
return colors[color as keyof typeof colors] || colors.gray
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">用户画像</h1>
|
||||
<p className="text-muted-foreground mt-2">深度分析用户特征,精准定位目标群体</p>
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-purple-50">
|
||||
<MobileHeader onMenuToggle={() => {}} title="用户画像" />
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 核心指标概览 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
总用户数
|
||||
<TooltipHelp content="系统中所有已识别和分析的用户总数,包括活跃和非活跃用户" />
|
||||
</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">24,563</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<span className="text-green-600">+12.5%</span> 较上月
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<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>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
活跃用户
|
||||
<TooltipHelp content="近30天内有过互动行为的用户数量,包括点击、浏览、购买等行为" />
|
||||
</CardTitle>
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">18,429</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<span className="text-green-600">+8.2%</span> 较上月
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
用户分群
|
||||
<TooltipHelp content="基于用户行为和特征自动创建的用户群组数量,用于精准营销" />
|
||||
</CardTitle>
|
||||
<Target className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">156</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<span className="text-blue-600">+23</span> 新增分群
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
标签覆盖率
|
||||
<TooltipHelp content="已打标签用户占总用户的比例,反映用户画像的完整程度" />
|
||||
</CardTitle>
|
||||
<Tags className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">87.3%</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<span className="text-green-600">+2.1%</span> 较上月
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="users" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="users">用户管理</TabsTrigger>
|
||||
<TabsTrigger value="tags">标签管理</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="users" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
用户管理
|
||||
<TooltipHelp content="管理真实用户信息,支持添加、编辑和查看用户详细资料" />
|
||||
</CardTitle>
|
||||
<CardDescription>管理和查看所有真实用户的详细信息</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索用户..."
|
||||
className="pl-8 max-w-sm"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
<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)} />
|
||||
</div>
|
||||
<Select value={selectedSegment} onValueChange={setSelectedSegment}>
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue placeholder="选择用户分群" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部用户</SelectItem>
|
||||
<SelectItem value="high-value">高价值用户</SelectItem>
|
||||
<SelectItem value="active">活跃用户</SelectItem>
|
||||
<SelectItem value="potential">潜在用户</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="sm">
|
||||
<Filter className="h-4 w-4 mr-2" />
|
||||
<Button variant="outline" onClick={() => setFilterOpen(true)}>
|
||||
<Filter className="h-4 w-4 mr-1" />
|
||||
筛选
|
||||
</Button>
|
||||
<Button onClick={() => setIsAddingUser(true)}>
|
||||
<UserPlus className="h-4 w-4 mr-2" />
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加用户
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>用户ID</TableHead>
|
||||
<TableHead>姓名</TableHead>
|
||||
<TableHead>联系方式</TableHead>
|
||||
<TableHead>标签</TableHead>
|
||||
<TableHead>用户价值</TableHead>
|
||||
<TableHead>最后活跃</TableHead>
|
||||
<TableHead>来源</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell className="font-medium">{user.id}</TableCell>
|
||||
<TableCell>{user.name}</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm">
|
||||
<div>{user.phone}</div>
|
||||
<div className="text-muted-foreground">{user.email}</div>
|
||||
<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>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{user.tags.slice(0, 2).map((tag, index) => (
|
||||
<Badge key={index} variant="secondary" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
{user.tags.length > 2 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{user.tags.length - 2}
|
||||
</Badge>
|
||||
)}
|
||||
<div className="col-span-3">
|
||||
<div className="text-sm">{u.phone}</div>
|
||||
<div className="text-xs text-muted-foreground">{u.email}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>¥{user.value.toLocaleString()}</TableCell>
|
||||
<TableCell>{user.lastActive}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{user.source}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="ghost" size="sm">
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
查看详情
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="tags" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
标签管理
|
||||
<TooltipHelp content="管理用户标签体系,包括标签创建、分类、规则设置和自动化打标" />
|
||||
</CardTitle>
|
||||
<CardDescription>创建和管理用户标签,实现精准用户画像</CardDescription>
|
||||
</div>
|
||||
<Button onClick={() => setIsAddingTag(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建标签
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
{tagCategories.map((category) => (
|
||||
<Card key={category.id}>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<CardTitle className="text-base">{category.name}</CardTitle>
|
||||
<CardDescription>{category.description}</CardDescription>
|
||||
</div>
|
||||
<Badge variant="outline">{category.tags.length} 个标签</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
{category.tags.map((tag, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className={getTagColor(tag.color)}>{tag.name}</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{tag.count.toLocaleString()}</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>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</TabsContent>
|
||||
|
||||
{/* 添加用户对话框 */}
|
||||
<Dialog open={isAddingUser} onOpenChange={setIsAddingUser}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>添加真实用户</DialogTitle>
|
||||
<DialogDescription>录入真实用户信息,完善用户画像数据</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="user-name" className="text-right">
|
||||
姓名
|
||||
</Label>
|
||||
<Input id="user-name" className="col-span-3" placeholder="请输入真实姓名" />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="user-phone" className="text-right">
|
||||
手机号
|
||||
</Label>
|
||||
<Input id="user-phone" className="col-span-3" placeholder="请输入手机号码" />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="user-email" className="text-right">
|
||||
邮箱
|
||||
</Label>
|
||||
<Input id="user-email" className="col-span-3" placeholder="请输入邮箱地址" />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="user-source" className="text-right">
|
||||
来源渠道
|
||||
</Label>
|
||||
<Select>
|
||||
<SelectTrigger id="user-source" className="col-span-3">
|
||||
<SelectValue placeholder="选择用户来源" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="wechat">微信</SelectItem>
|
||||
<SelectItem value="douyin">抖音</SelectItem>
|
||||
<SelectItem value="xiaohongshu">小红书</SelectItem>
|
||||
<SelectItem value="website">官网</SelectItem>
|
||||
<SelectItem value="offline">线下</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-start gap-4">
|
||||
<Label htmlFor="user-tags" className="text-right pt-2">
|
||||
用户标签
|
||||
</Label>
|
||||
<Textarea id="user-tags" className="col-span-3" placeholder="请输入用户标签,用逗号分隔" rows={3} />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-start gap-4">
|
||||
<Label htmlFor="user-notes" className="text-right pt-2">
|
||||
备注信息
|
||||
</Label>
|
||||
<Textarea id="user-notes" className="col-span-3" placeholder="用户备注信息" rows={3} />
|
||||
</div>
|
||||
<TabsContent value="tags">
|
||||
<div className="text-sm text-muted-foreground py-6 text-center">标签管理将在接入数据字典后提供配置与统计</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</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>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsAddingUser(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={() => setIsAddingUser(false)}>
|
||||
<UserPlus className="mr-2 h-4 w-4" />
|
||||
添加用户
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 添加标签对话框 */}
|
||||
<Dialog open={isAddingTag} onOpenChange={setIsAddingTag}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>新建标签</DialogTitle>
|
||||
<DialogDescription>创建新的用户标签,完善标签分类体系</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="tag-category" className="text-right">
|
||||
标签分类
|
||||
</Label>
|
||||
<Select>
|
||||
<SelectTrigger id="tag-category" className="col-span-3">
|
||||
<SelectValue placeholder="选择标签分类" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{tagCategories.map((category) => (
|
||||
<SelectItem key={category.id} value={category.id}>
|
||||
{category.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="tag-name" className="text-right">
|
||||
标签名称
|
||||
</Label>
|
||||
<Input id="tag-name" className="col-span-3" placeholder="请输入标签名称" />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="tag-color" className="text-right">
|
||||
标签颜色
|
||||
</Label>
|
||||
<Select>
|
||||
<SelectTrigger id="tag-color" className="col-span-3">
|
||||
<SelectValue placeholder="选择标签颜色" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="red">红色</SelectItem>
|
||||
<SelectItem value="blue">蓝色</SelectItem>
|
||||
<SelectItem value="green">绿色</SelectItem>
|
||||
<SelectItem value="yellow">黄色</SelectItem>
|
||||
<SelectItem value="purple">紫色</SelectItem>
|
||||
<SelectItem value="pink">粉色</SelectItem>
|
||||
<SelectItem value="orange">橙色</SelectItem>
|
||||
<SelectItem value="gray">灰色</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-start gap-4">
|
||||
<Label htmlFor="tag-description" className="text-right pt-2">
|
||||
标签描述
|
||||
</Label>
|
||||
<Textarea id="tag-description" className="col-span-3" placeholder="标签描述信息" rows={3} />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<div className="text-right">
|
||||
<Label htmlFor="auto-tag">自动打标</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 col-span-3">
|
||||
<Switch id="auto-tag" />
|
||||
<Label htmlFor="auto-tag">启用自动打标规则</Label>
|
||||
<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>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsAddingTag(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={() => setIsAddingTag(false)}>
|
||||
<Tags className="mr-2 h-4 w-4" />
|
||||
创建标签
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setIsAddingUser(false)}>取消</Button>
|
||||
<Button onClick={handleAddUser}>添加用户</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user