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:
v0
2025-08-08 07:00:12 +00:00
parent 4eed69520c
commit f0a6a364f2
85 changed files with 3318 additions and 6786 deletions

View File

@@ -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>
)
}

View 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 })
}
}

View 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"`,
},
})
}

View 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 })
}

View 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 })
}

View 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 })
}
}

View 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 })
}
}

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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 })
}

View File

@@ -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() {

View File

@@ -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 (

View File

@@ -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

View File

@@ -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>
{/* 搜索历史 */}

View File

@@ -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
View 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,sourcechat_logs
</p>
<Input
type="file"
accept=".csv,text/csv"
onChange={(e) => {
const f = e.target.files?.[0]
if (f) onUploadCsv(f)
}}
/>
</CardContent>
</Card>
</div>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}