feat: optimize interface and database connection

Adjust MongoDB mock connector and update database structure.
Enhance sidebar, data platform, and AI analysis tools.
Clean up unnecessary code and update development docs.

#VERCEL_SKIP

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
v0
2025-07-21 00:11:52 +00:00
parent 892c5b4855
commit ecd8a48863
26 changed files with 1821 additions and 1024 deletions

View File

@@ -343,7 +343,11 @@ export default function AIAssistantPage() {
<div className="text-center text-muted-foreground"> <div className="text-center text-muted-foreground">
<Users className="h-16 w-16 mx-auto mb-4" /> <Users className="h-16 w-16 mx-auto mb-4" />
<p className="text-lg"></p> <p className="text-lg"></p>
<Button variant="outline" className="mt-4" onClick={() => setIsUserGroupDialogOpen(true)}> <Button
variant="outline"
className="mt-4 bg-transparent"
onClick={() => setIsUserGroupDialogOpen(true)}
>
<Users className="mr-2 h-4 w-4" /> <Users className="mr-2 h-4 w-4" />
</Button> </Button>
@@ -612,7 +616,11 @@ export default function AIAssistantPage() {
<div className="text-center text-muted-foreground"> <div className="text-center text-muted-foreground">
<Users className="h-16 w-16 mx-auto mb-4" /> <Users className="h-16 w-16 mx-auto mb-4" />
<p className="text-lg"></p> <p className="text-lg"></p>
<Button variant="outline" className="mt-4" onClick={() => setIsUserGroupDialogOpen(true)}> <Button
variant="outline"
className="mt-4 bg-transparent"
onClick={() => setIsUserGroupDialogOpen(true)}
>
<Users className="mr-2 h-4 w-4" /> <Users className="mr-2 h-4 w-4" />
</Button> </Button>

View File

@@ -1,5 +1,5 @@
import { NextResponse } from "next/server" import { NextResponse } from "next/server"
import { getDatabases, getDatabaseStructure } from "@/lib/db-connector" import { getDatabases, getDatabaseStructure } from "@/lib/mongodb-mock-connector" // 更新导入路径
export async function GET(request: Request) { export async function GET(request: Request) {
try { try {

View File

@@ -2,14 +2,13 @@
import Link from "next/link" import Link from "next/link"
import { usePathname } from "next/navigation" import { usePathname } from "next/navigation"
import { Home, Database, Search, Target, Brain } from "lucide-react" import { Home, Database, Target, BrainCircuit } from "lucide-react" // 引入AI智能助手图标
const navItems = [ const navItems = [
{ href: "/", icon: Home, label: "概览" }, { href: "/", icon: Home, label: "概览" },
{ href: "/data-platform", icon: Database, label: "数据中台" }, { href: "/data-platform", icon: Database, label: "数据中台" },
{ href: "/user-discovery", icon: Search, label: "用户发现" }, { href: "/user-portrait", icon: Target, label: "用户画像" }, // 整合用户池功能
{ href: "/user-valuation", icon: Target, label: "用户估值" }, { href: "/ai-assistant", icon: BrainCircuit, label: "AI助手" }, // 新增AI智能助手
{ href: "/ai-analysis", icon: Brain, label: "AI分析" },
] ]
export default function BottomNav() { export default function BottomNav() {

View File

@@ -18,7 +18,7 @@ export default function Header() {
<Search className="h-4 w-4 text-muted-foreground mr-2" /> <Search className="h-4 w-4 text-muted-foreground mr-2" />
<Input <Input
type="text" type="text"
placeholder="搜索用户、标签、活动..." placeholder="搜索用户、IMEI、手机号、标签..."
className="border-none focus-visible:ring-0 focus-visible:ring-offset-0" className="border-none focus-visible:ring-0 focus-visible:ring-offset-0"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}

View File

@@ -10,7 +10,7 @@ interface MobileHeaderProps {
title?: string title?: string
} }
export default function MobileHeader({ onMenuToggle, title = "数据资产中台" }: MobileHeaderProps) { export default function MobileHeader({ onMenuToggle, title = "卡若数据资产中台" }: MobileHeaderProps) {
const [showSearch, setShowSearch] = useState(false) const [showSearch, setShowSearch] = useState(false)
const [searchQuery, setSearchQuery] = useState("") const [searchQuery, setSearchQuery] = useState("")
@@ -46,7 +46,7 @@ export default function MobileHeader({ onMenuToggle, title = "数据资产中台
<div className="flex-1"> <div className="flex-1">
<Input <Input
type="text" type="text"
placeholder="搜索用户、标签、活动..." placeholder="搜索用户、IMEI、手机号..."
className="glass-input border-none focus-visible:ring-0 focus-visible:ring-offset-0" className="glass-input border-none focus-visible:ring-0 focus-visible:ring-offset-0"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}

View File

@@ -7,16 +7,14 @@ import { cn } from "@/lib/utils"
import { import {
Database, Database,
LayoutDashboard, LayoutDashboard,
Users,
Target, Target,
X, X,
ChevronDown, ChevronDown,
ChevronRight, ChevronRight,
Search, UserCheck,
BarChart3, Layers,
TrendingUp, Tag,
Brain, BrainCircuit,
HelpCircle,
} from "lucide-react" } from "lucide-react"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
@@ -29,9 +27,7 @@ interface MobileSidebarProps {
export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) { export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
const pathname = usePathname() const pathname = usePathname()
const [expandedSections, setExpandedSections] = useState({ const [expandedSections, setExpandedSections] = useState({
"user-discovery": false, "user-portrait": false, // 用户画像默认不展开
"user-valuation": false,
"ai-analysis": false,
}) })
const toggleSection = (section: string) => { const toggleSection = (section: string) => {
@@ -70,77 +66,40 @@ export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
description: "多源数据整合中心", description: "多源数据整合中心",
}, },
{ {
title: "用户发现", title: "用户画像", // 整合用户池功能
href: "/user-discovery", href: "/user-portrait",
icon: <Search className="h-4 w-4" />,
primary: true,
expandable: true,
section: "user-discovery",
description: "用户行为洞察分析",
children: [
{
title: "行为分析",
href: "/user-discovery/behavior",
icon: <BarChart3 className="h-3 w-3" />,
description: "用户行为模式分析",
},
{
title: "用户分群",
href: "/user-discovery/segmentation",
icon: <Users className="h-3 w-3" />,
description: "智能用户分群",
},
],
},
{
title: "用户估值",
href: "/user-valuation",
icon: <Target className="h-4 w-4" />, icon: <Target className="h-4 w-4" />,
primary: true, primary: true,
expandable: true, expandable: true,
section: "user-valuation", section: "user-portrait",
description: "用户价值评估模型", description: "用户数据管理与画像分析",
hasHelp: true,
children: [ children: [
{ {
title: "估值模型", title: "用户管理", // 原用户池的用户管理
href: "/user-valuation/model", href: "/user-portrait/management",
icon: <BarChart3 className="h-3 w-3" />, icon: <UserCheck className="h-3 w-3" />,
description: "RFM价值评估", description: "IMEI、手机号管理",
hasHelp: true,
}, },
{ {
title: "升级路径", title: "用户分群", // 原用户池的用户分群
href: "/user-valuation/upgrade-paths", href: "/user-portrait/segmentation",
icon: <TrendingUp className="h-3 w-3" />, icon: <Layers className="h-3 w-3" />,
description: "用户价值提升策略", description: "智能用户分群",
hasHelp: true, },
{
title: "标签管理", // 原用户画像的标签管理
href: "/user-portrait/tags",
icon: <Tag className="h-3 w-3" />,
description: "用户标签体系",
}, },
], ],
}, },
{ {
title: "AI分析", title: "AI智能助手", // 新增AI智能助手
href: "/ai-analysis", href: "/ai-assistant",
icon: <Brain className="h-4 w-4" />, icon: <BrainCircuit className="h-4 w-4" />,
primary: true, primary: true,
expandable: true, description: "AI数据分析与营销策略",
section: "ai-analysis",
description: "智能数据洞察",
tag: "AI",
children: [
{
title: "趋势识别",
href: "/ai-analysis/trends",
icon: <TrendingUp className="h-3 w-3" />,
description: "AI趋势预测",
},
{
title: "异常检测",
href: "/ai-analysis/anomaly",
icon: <Search className="h-3 w-3" />,
description: "智能异常监控",
},
],
}, },
] ]
@@ -148,8 +107,6 @@ export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
switch (tag) { switch (tag) {
case "核心": case "核心":
return "bg-blue-100 text-blue-700 border-blue-200" return "bg-blue-100 text-blue-700 border-blue-200"
case "AI":
return "bg-purple-100 text-purple-700 border-purple-200"
default: default:
return "bg-gray-100 text-gray-700 border-gray-200" return "bg-gray-100 text-gray-700 border-gray-200"
} }
@@ -180,7 +137,7 @@ export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
<Database className="h-4 w-4 text-blue-600" /> <Database className="h-4 w-4 text-blue-600" />
</div> </div>
<h1 className="text-sm font-semibold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"> <h1 className="text-sm font-semibold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
</h1> </h1>
</div> </div>
<Button variant="ghost" size="icon" onClick={onClose} className="glass-light rounded-lg h-8 w-8"> <Button variant="ghost" size="icon" onClick={onClose} className="glass-light rounded-lg h-8 w-8">
@@ -196,7 +153,7 @@ export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
<div <div
className={cn( className={cn(
"flex items-center px-3 py-2 text-xs font-medium rounded-lg transition-all duration-300 group", "flex items-center px-3 py-2 text-xs font-medium rounded-lg transition-all duration-300 group",
pathname === item.href pathname === item.href || (item.children && pathname.startsWith(item.href))
? "glass-heavy text-blue-700 shadow-glass" ? "glass-heavy text-blue-700 shadow-glass"
: "glass-light text-gray-700 hover:glass-heavy hover:text-blue-600", : "glass-light text-gray-700 hover:glass-heavy hover:text-blue-600",
)} )}
@@ -207,7 +164,6 @@ export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="font-medium truncate">{item.title}</span> <span className="font-medium truncate">{item.title}</span>
{item.hasHelp && <HelpCircle className="h-3 w-3 text-gray-400 flex-shrink-0" />}
</div> </div>
{item.tag && ( {item.tag && (
<Badge <Badge
@@ -253,10 +209,7 @@ export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
{subItem.icon} {subItem.icon}
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-1">
<p className="font-medium truncate">{subItem.title}</p> <p className="font-medium truncate">{subItem.title}</p>
{subItem.hasHelp && <HelpCircle className="h-3 w-3 text-gray-400 flex-shrink-0" />}
</div>
<p className="text-xs text-gray-500 truncate">{subItem.description}</p> <p className="text-xs text-gray-500 truncate">{subItem.description}</p>
</div> </div>
</Link> </Link>
@@ -272,15 +225,15 @@ export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
<div className="border-t border-white/20 p-3"> <div className="border-t border-white/20 p-3">
<div className="glass-light rounded-lg p-2"> <div className="glass-light rounded-lg p-2">
<div className="flex items-center justify-between text-xs"> <div className="flex items-center justify-between text-xs">
<span className="text-gray-600">AI引擎状</span> <span className="text-gray-600"></span>
<div className="flex items-center"> <div className="flex items-center">
<div className="w-2 h-2 bg-green-500 rounded-full mr-1"></div> <div className="w-2 h-2 bg-green-500 rounded-full mr-1"></div>
<span className="text-green-600"></span> <span className="text-green-600"></span>
</div> </div>
</div> </div>
<div className="flex items-center justify-between text-xs mt-1"> <div className="flex items-center justify-between text-xs mt-1">
<span className="text-gray-600"></span> <span className="text-gray-600"></span>
<span className="text-gray-500"></span> <span className="text-gray-500">125,678</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -8,14 +8,15 @@ import {
Database, Database,
LayoutDashboard, LayoutDashboard,
Settings, Settings,
Users,
Target, Target,
ChevronLeft, ChevronLeft,
ChevronDown, ChevronDown,
ChevronRight, ChevronRight,
Tag, Tag,
BarChart3, UserCheck,
TrendingUp, Layers,
BrainCircuit,
Smartphone,
} from "lucide-react" } from "lucide-react"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
@@ -23,8 +24,7 @@ export default function Sidebar() {
const pathname = usePathname() const pathname = usePathname()
const [expanded, setExpanded] = useState(true) const [expanded, setExpanded] = useState(true)
const [expandedSections, setExpandedSections] = useState({ const [expandedSections, setExpandedSections] = useState({
"user-portrait": true, "user-portrait": true, // 用户画像默认展开
"user-value": false,
}) })
const toggleSidebar = () => { const toggleSidebar = () => {
@@ -38,60 +38,57 @@ export default function Sidebar() {
})) }))
} }
// 重新设计的导航结构 // 重新设计的导航结构 - 遵循新规则
const navItems = [ const navItems = [
{ {
title: "概览", title: "数据概览",
href: "/", href: "/",
icon: <LayoutDashboard className="h-5 w-5" />, icon: <LayoutDashboard className="h-5 w-5" />,
primary: true, primary: true,
}, },
{ {
title: "数据集成", title: "数据中台",
href: "/data-integration", href: "/data-platform",
icon: <Database className="h-5 w-5" />, icon: <Database className="h-5 w-5" />,
primary: true, primary: true,
tag: "核心", tag: "核心",
}, },
{ {
title: "用户画像", title: "用户画像", // 整合用户池功能
href: "/user-portrait", href: "/user-portrait",
icon: <Users className="h-5 w-5" />, icon: <Target className="h-5 w-5" />,
primary: true, primary: true,
expandable: true, expandable: true,
section: "user-portrait", section: "user-portrait",
children: [ children: [
{ {
title: "用户", title: "用户管理", // 原用户池的用户管理
href: "/user-portrait/user-keywords", href: "/user-portrait/management",
icon: <Tag className="h-4 w-4" />, icon: <UserCheck className="h-4 w-4" />,
}, },
{ {
title: "标签管理", title: "用户分群", // 原用户池的用户分群
href: "/user-portrait/segmentation",
icon: <Layers className="h-4 w-4" />,
},
{
title: "标签管理", // 原用户画像的标签管理
href: "/user-portrait/tags", href: "/user-portrait/tags",
icon: <Tag className="h-4 w-4" />, icon: <Tag className="h-4 w-4" />,
}, },
], ],
}, },
{ {
title: "用户估值", title: "设备管理", // 提升为一级菜单 [^2]
href: "/user-value", href: "/devices",
icon: <Target className="h-5 w-5" />, icon: <Smartphone className="h-5 w-5" />,
primary: true, primary: true,
expandable: true,
section: "user-value",
children: [
{
title: "估值模型",
href: "/user-value/model",
icon: <BarChart3 className="h-4 w-4" />,
}, },
{ {
title: "用户升级路径", title: "AI智能助手", // 新增AI智能助手
href: "/user-value/upgrade-paths", href: "/ai-assistant",
icon: <TrendingUp className="h-4 w-4" />, icon: <BrainCircuit className="h-5 w-5" />,
}, primary: true,
],
}, },
] ]
@@ -119,7 +116,7 @@ export default function Sidebar() {
<Database className="h-5 w-5 text-blue-600" /> <Database className="h-5 w-5 text-blue-600" />
</div> </div>
<h1 className="text-lg font-semibold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"> <h1 className="text-lg font-semibold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
</h1> </h1>
</div> </div>
) : ( ) : (
@@ -139,7 +136,7 @@ export default function Sidebar() {
<div <div
className={cn( className={cn(
"flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all duration-300 group", "flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all duration-300 group",
pathname === item.href pathname === item.href || (item.children && pathname.startsWith(item.href))
? "glass-heavy text-blue-700 shadow-glass" ? "glass-heavy text-blue-700 shadow-glass"
: "glass-light text-gray-700 hover:glass-heavy hover:text-blue-600 hover:scale-105", : "glass-light text-gray-700 hover:glass-heavy hover:text-blue-600 hover:scale-105",
)} )}

View File

@@ -18,10 +18,15 @@ import {
Settings, Settings,
ChevronRight, ChevronRight,
ChevronDown, ChevronDown,
Brain,
} from "lucide-react" } from "lucide-react"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { AIAnalysisTools } from "@/components/data-integration/ai-analysis-tools"
import { useToast } from "@/components/ui/use-toast" // 导入 useToast
export default function DataPlatformPage() { export default function DataPlatformPage() {
const { toast } = useToast() // 初始化 toast
const [timeRange, setTimeRange] = useState("today") const [timeRange, setTimeRange] = useState("today")
const [expandedSections, setExpandedSections] = useState({ const [expandedSections, setExpandedSections] = useState({
overview: true, overview: true,
@@ -29,6 +34,7 @@ export default function DataPlatformPage() {
integration: false, integration: false,
quality: false, quality: false,
}) })
const [activeTab, setActiveTab] = useState("overview")
// 数据中台数据 // 数据中台数据
const platformData = { const platformData = {
@@ -50,6 +56,7 @@ export default function DataPlatformPage() {
lastSync: "1分钟前", lastSync: "1分钟前",
quality: 96.8, quality: 96.8,
description: "用户行为轨迹数据", description: "用户行为轨迹数据",
aiReady: true, // 新增 AI 就绪状态
}, },
{ {
name: "CRM系统", name: "CRM系统",
@@ -59,6 +66,7 @@ export default function DataPlatformPage() {
lastSync: "3分钟前", lastSync: "3分钟前",
quality: 94.2, quality: 94.2,
description: "客户关系管理数据", description: "客户关系管理数据",
aiReady: true,
}, },
{ {
name: "内容平台数据", name: "内容平台数据",
@@ -68,6 +76,7 @@ export default function DataPlatformPage() {
lastSync: "正在同步", lastSync: "正在同步",
quality: 92.5, quality: 92.5,
description: "抖音、小红书等平台数据", description: "抖音、小红书等平台数据",
aiReady: false,
}, },
{ {
name: "触客宝后台", name: "触客宝后台",
@@ -77,6 +86,7 @@ export default function DataPlatformPage() {
lastSync: "5分钟前", lastSync: "5分钟前",
quality: 95.1, quality: 95.1,
description: "呼入电话信息数据", description: "呼入电话信息数据",
aiReady: true,
}, },
{ {
name: "飞书妙记", name: "飞书妙记",
@@ -86,6 +96,7 @@ export default function DataPlatformPage() {
lastSync: "10分钟前", lastSync: "10分钟前",
quality: 91.3, quality: 91.3,
description: "会议记录和内容摘要", description: "会议记录和内容摘要",
aiReady: true,
}, },
{ {
name: "表单提交数据", name: "表单提交数据",
@@ -95,6 +106,7 @@ export default function DataPlatformPage() {
lastSync: "2分钟前", lastSync: "2分钟前",
quality: 97.2, quality: 97.2,
description: "在线表单提交数据", description: "在线表单提交数据",
aiReady: true,
}, },
], ],
integrationStats: { integrationStats: {
@@ -150,6 +162,14 @@ export default function DataPlatformPage() {
} }
} }
const handleConnectToAI = (sourceName: string) => {
toast({
title: "连接到AI",
description: `数据源 "${sourceName}" 已准备好连接到AI模型。`,
})
// 实际应用中这里会触发一个后端API调用或跳转到AI配置页面
}
return ( return (
<div className="container mx-auto py-4 md:py-6 space-y-4 md:space-y-6"> <div className="container mx-auto py-4 md:py-6 space-y-4 md:space-y-6">
{/* 页面标题和工具栏 */} {/* 页面标题和工具栏 */}
@@ -182,6 +202,16 @@ export default function DataPlatformPage() {
</div> </div>
</div> </div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-4 md:grid-cols-5">
<TabsTrigger value="overview"></TabsTrigger>
<TabsTrigger value="sources"></TabsTrigger>
<TabsTrigger value="integration"></TabsTrigger>
<TabsTrigger value="quality"></TabsTrigger>
<TabsTrigger value="ai-models">AI </TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-6">
{/* 数据中台概览 */} {/* 数据中台概览 */}
<Collapsible <Collapsible
open={expandedSections.overview} open={expandedSections.overview}
@@ -242,7 +272,9 @@ export default function DataPlatformPage() {
{platformData.overview.storageUsed}GB / {platformData.overview.storageTotal}GB {platformData.overview.storageUsed}GB / {platformData.overview.storageTotal}GB
</span> </span>
</div> </div>
<Progress value={(platformData.overview.storageUsed / platformData.overview.storageTotal) * 100} /> <Progress
value={(platformData.overview.storageUsed / platformData.overview.storageTotal) * 100}
/>
</div> </div>
<div> <div>
<div className="flex justify-between text-sm mb-1"> <div className="flex justify-between text-sm mb-1">
@@ -256,7 +288,9 @@ export default function DataPlatformPage() {
</CollapsibleContent> </CollapsibleContent>
</Card> </Card>
</Collapsible> </Collapsible>
</TabsContent>
<TabsContent value="sources" className="space-y-6">
{/* 数据源管理 */} {/* 数据源管理 */}
<Collapsible <Collapsible
open={expandedSections.sources} open={expandedSections.sources}
@@ -310,10 +344,22 @@ export default function DataPlatformPage() {
<div className="text-sm font-bold text-green-600">{source.quality}%</div> <div className="text-sm font-bold text-green-600">{source.quality}%</div>
<div className="text-xs text-gray-500"></div> <div className="text-xs text-gray-500"></div>
</div> </div>
<div className="flex gap-2">
<Button variant="outline" size="sm" className="bg-transparent"> <Button variant="outline" size="sm" className="bg-transparent">
<Settings className="h-3 w-3 mr-1" /> <Settings className="h-3 w-3 mr-1" />
</Button> </Button>
<Button
variant="outline"
size="sm"
className="bg-transparent"
disabled={!source.aiReady}
onClick={() => handleConnectToAI(source.name)} // 添加点击事件
>
<Brain className="h-3 w-3 mr-1" />
{source.aiReady ? "连接到AI" : "AI未就绪"}
</Button>
</div>
</div> </div>
</div> </div>
<div className="mt-3"> <div className="mt-3">
@@ -326,7 +372,9 @@ export default function DataPlatformPage() {
</CollapsibleContent> </CollapsibleContent>
</Card> </Card>
</Collapsible> </Collapsible>
</TabsContent>
<TabsContent value="integration" className="space-y-6">
{/* 数据集成统计 */} {/* 数据集成统计 */}
<Collapsible <Collapsible
open={expandedSections.integration} open={expandedSections.integration}
@@ -421,7 +469,9 @@ export default function DataPlatformPage() {
</CollapsibleContent> </CollapsibleContent>
</Card> </Card>
</Collapsible> </Collapsible>
</TabsContent>
<TabsContent value="quality" className="space-y-6">
{/* 数据质量监控 */} {/* 数据质量监控 */}
<Collapsible <Collapsible
open={expandedSections.quality} open={expandedSections.quality}
@@ -485,6 +535,13 @@ export default function DataPlatformPage() {
</CollapsibleContent> </CollapsibleContent>
</Card> </Card>
</Collapsible> </Collapsible>
</TabsContent>
<TabsContent value="ai-models" className="space-y-6">
{/* AI 模型管理 */}
<AIAnalysisTools />
</TabsContent>
</Tabs>
</div> </div>
) )
} }

View File

@@ -6,8 +6,8 @@ import ClientLayout from "./ClientLayout"
const inter = Inter({ subsets: ["latin"] }) const inter = Inter({ subsets: ["latin"] })
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: "卡若数据资产中台",
description: "Generated by create next app", description: "基于IMEI、手机号的用户数据资产管理平台",
generator: 'v0.dev' generator: 'v0.dev'
} }

View File

@@ -8,15 +8,17 @@ import { Progress } from "@/components/ui/progress"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { import {
Database, Database,
Users,
Target, Target,
Brain, TrendingUp,
Search,
BarChart3,
RefreshCw, RefreshCw,
Download, Download,
ChevronRight, ChevronRight,
Activity, Activity,
Zap, Zap,
UserCheck,
Layers,
Tag,
} from "lucide-react" } from "lucide-react"
import Link from "next/link" import Link from "next/link"
@@ -31,24 +33,24 @@ export default function HomePage() {
sources: 8, sources: 8,
lastUpdate: "2分钟前", lastUpdate: "2分钟前",
}, },
userDiscovery: { userPool: {
totalUsers: 125678,
activeUsers: 45678, activeUsers: 45678,
newUsers: 1234, newUsers: 1234,
engagement: "87.5%",
growth: "+8.2%", growth: "+8.2%",
}, },
userPortrait: {
totalTags: 342,
activeTags: 287,
coverage: "95.8%",
growth: "+5.3%",
},
userValuation: { userValuation: {
avgValue: 3248, avgValue: 3248,
highValue: 12456, highValue: 12456,
growth: "+15.8%", growth: "+15.8%",
upgradeRate: "12.5%", upgradeRate: "12.5%",
}, },
aiAnalysis: {
insights: 156,
anomalies: 3,
predictions: 89,
accuracy: "94.2%",
},
} }
// 实时活动数据 // 实时活动数据
@@ -56,21 +58,21 @@ export default function HomePage() {
{ {
id: 1, id: 1,
type: "data_sync", type: "data_sync",
message: "电商平台数据同步完成", message: "IMEI数据同步完成",
time: "刚刚", time: "刚刚",
status: "success", status: "success",
}, },
{ {
id: 2, id: 2,
type: "user_discovery", type: "user_pool",
message: "发现新用户行为模式", message: "新增用户池分群",
time: "2分钟前", time: "2分钟前",
status: "info", status: "info",
}, },
{ {
id: 3, id: 3,
type: "ai_analysis", type: "user_portrait",
message: "AI检测到异常流量", message: "标签规则自动执行",
time: "5分钟前", time: "5分钟前",
status: "warning", status: "warning",
}, },
@@ -87,12 +89,12 @@ export default function HomePage() {
switch (type) { switch (type) {
case "data_sync": case "data_sync":
return <Database className="h-4 w-4" /> return <Database className="h-4 w-4" />
case "user_discovery": case "user_pool":
return <Search className="h-4 w-4" /> return <Users className="h-4 w-4" />
case "user_valuation": case "user_portrait":
return <Target className="h-4 w-4" /> return <Target className="h-4 w-4" />
case "ai_analysis": case "user_valuation":
return <Brain className="h-4 w-4" /> return <TrendingUp className="h-4 w-4" />
default: default:
return <Activity className="h-4 w-4" /> return <Activity className="h-4 w-4" />
} }
@@ -117,7 +119,7 @@ export default function HomePage() {
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4"> <div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<div> <div>
<h1 className="text-2xl md:text-3xl font-bold tracking-tight"></h1> <h1 className="text-2xl md:text-3xl font-bold tracking-tight"></h1>
<p className="text-sm md:text-base text-muted-foreground"></p> <p className="text-sm md:text-base text-muted-foreground"></p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Select value={timeRange} onValueChange={setTimeRange}> <Select value={timeRange} onValueChange={setTimeRange}>
@@ -178,34 +180,34 @@ export default function HomePage() {
</Card> </Card>
</Link> </Link>
{/* 用户发现 */} {/* 用户 */}
<Link href="/user-discovery"> <Link href="/user-pool">
<Card className="border-none shadow-md hover:shadow-lg transition-all duration-300 cursor-pointer group"> <Card className="border-none shadow-md hover:shadow-lg transition-all duration-300 cursor-pointer group">
<CardHeader className="bg-gradient-to-r from-green-50 to-emerald-50 pb-3"> <CardHeader className="bg-gradient-to-r from-green-50 to-emerald-50 pb-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Search className="h-5 w-5 text-green-600" /> <Users className="h-5 w-5 text-green-600" />
<CardTitle className="text-base"></CardTitle> <CardTitle className="text-base"></CardTitle>
</div> </div>
</div> </div>
<CardDescription className="text-xs"></CardDescription> <CardDescription className="text-xs"></CardDescription>
</CardHeader> </CardHeader>
<CardContent className="pt-3"> <CardContent className="pt-3">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="font-bold">{todayStats.userPool.totalUsers.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span> <span className="text-sm text-gray-600"></span>
<span className="font-bold">{todayStats.userDiscovery.activeUsers.toLocaleString()}</span> <span className="font-bold">{todayStats.userPool.activeUsers.toLocaleString()}</span>
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span> <span className="text-sm text-gray-600"></span>
<span className="font-bold">{todayStats.userDiscovery.newUsers.toLocaleString()}</span> <span className="font-bold">{todayStats.userPool.newUsers.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="text-green-600 font-bold">{todayStats.userDiscovery.engagement}</span>
</div> </div>
<div className="flex items-center justify-between mt-3"> <div className="flex items-center justify-between mt-3">
<span className="text-xs text-green-600"> {todayStats.userDiscovery.growth}</span> <span className="text-xs text-green-600"> {todayStats.userPool.growth}</span>
<ChevronRight className="h-4 w-4 text-gray-400 group-hover:text-green-600 transition-colors" /> <ChevronRight className="h-4 w-4 text-gray-400 group-hover:text-green-600 transition-colors" />
</div> </div>
</div> </div>
@@ -213,13 +215,48 @@ export default function HomePage() {
</Card> </Card>
</Link> </Link>
{/* 用户画像 */}
<Link href="/user-portrait">
<Card className="border-none shadow-md hover:shadow-lg transition-all duration-300 cursor-pointer group">
<CardHeader className="bg-gradient-to-r from-purple-50 to-pink-50 pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Target className="h-5 w-5 text-purple-600" />
<CardTitle className="text-base"></CardTitle>
</div>
</div>
<CardDescription className="text-xs"></CardDescription>
</CardHeader>
<CardContent className="pt-3">
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="font-bold">{todayStats.userPortrait.totalTags}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="font-bold">{todayStats.userPortrait.activeTags}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="text-purple-600 font-bold">{todayStats.userPortrait.coverage}</span>
</div>
<div className="flex items-center justify-between mt-3">
<span className="text-xs text-purple-600"> {todayStats.userPortrait.growth}</span>
<ChevronRight className="h-4 w-4 text-gray-400 group-hover:text-purple-600 transition-colors" />
</div>
</div>
</CardContent>
</Card>
</Link>
{/* 用户估值 */} {/* 用户估值 */}
<Link href="/user-valuation"> <Link href="/user-valuation">
<Card className="border-none shadow-md hover:shadow-lg transition-all duration-300 cursor-pointer group"> <Card className="border-none shadow-md hover:shadow-lg transition-all duration-300 cursor-pointer group">
<CardHeader className="bg-gradient-to-r from-purple-50 to-pink-50 pb-3"> <CardHeader className="bg-gradient-to-r from-orange-50 to-red-50 pb-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Target className="h-5 w-5 text-purple-600" /> <TrendingUp className="h-5 w-5 text-orange-600" />
<CardTitle className="text-base"></CardTitle> <CardTitle className="text-base"></CardTitle>
</div> </div>
</div> </div>
@@ -237,46 +274,10 @@ export default function HomePage() {
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span> <span className="text-sm text-gray-600"></span>
<span className="text-purple-600 font-bold">{todayStats.userValuation.upgradeRate}</span> <span className="text-orange-600 font-bold">{todayStats.userValuation.upgradeRate}</span>
</div> </div>
<div className="flex items-center justify-between mt-3"> <div className="flex items-center justify-between mt-3">
<span className="text-xs text-purple-600"> {todayStats.userValuation.growth}</span> <span className="text-xs text-orange-600"> {todayStats.userValuation.growth}</span>
<ChevronRight className="h-4 w-4 text-gray-400 group-hover:text-purple-600 transition-colors" />
</div>
</div>
</CardContent>
</Card>
</Link>
{/* AI分析 */}
<Link href="/ai-analysis">
<Card className="border-none shadow-md hover:shadow-lg transition-all duration-300 cursor-pointer group">
<CardHeader className="bg-gradient-to-r from-orange-50 to-red-50 pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Brain className="h-5 w-5 text-orange-600" />
<CardTitle className="text-base">AI分析</CardTitle>
</div>
<Badge className="bg-orange-100 text-orange-700">AI</Badge>
</div>
<CardDescription className="text-xs"></CardDescription>
</CardHeader>
<CardContent className="pt-3">
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">AI洞察</span>
<span className="font-bold">{todayStats.aiAnalysis.insights}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="font-bold">{todayStats.aiAnalysis.anomalies}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="text-orange-600 font-bold">{todayStats.aiAnalysis.accuracy}</span>
</div>
<div className="flex items-center justify-between mt-3">
<span className="text-xs text-orange-600"> {todayStats.aiAnalysis.predictions}</span>
<ChevronRight className="h-4 w-4 text-gray-400 group-hover:text-orange-600 transition-colors" /> <ChevronRight className="h-4 w-4 text-gray-400 group-hover:text-orange-600 transition-colors" />
</div> </div>
</div> </div>
@@ -291,7 +292,7 @@ export default function HomePage() {
<Card className="lg:col-span-2 border-none shadow-md"> <Card className="lg:col-span-2 border-none shadow-md">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<BarChart3 className="h-5 w-5 text-blue-600" /> <Activity className="h-5 w-5 text-blue-600" />
</CardTitle> </CardTitle>
<CardDescription></CardDescription> <CardDescription></CardDescription>
@@ -307,12 +308,12 @@ export default function HomePage() {
<div className="text-xs text-gray-600"></div> <div className="text-xs text-gray-600"></div>
</div> </div>
<div className="text-center p-3 bg-purple-50 rounded-lg"> <div className="text-center p-3 bg-purple-50 rounded-lg">
<div className="text-2xl font-bold text-purple-600">156</div> <div className="text-2xl font-bold text-purple-600">342</div>
<div className="text-xs text-gray-600">AI洞察</div> <div className="text-xs text-gray-600"></div>
</div> </div>
<div className="text-center p-3 bg-orange-50 rounded-lg"> <div className="text-center p-3 bg-orange-50 rounded-lg">
<div className="text-2xl font-bold text-orange-600">94.2%</div> <div className="text-2xl font-bold text-orange-600">95.8%</div>
<div className="text-xs text-gray-600"></div> <div className="text-xs text-gray-600"></div>
</div> </div>
</div> </div>
<div className="mt-4 space-y-3"> <div className="mt-4 space-y-3">
@@ -325,7 +326,7 @@ export default function HomePage() {
</div> </div>
<div> <div>
<div className="flex justify-between text-sm mb-1"> <div className="flex justify-between text-sm mb-1">
<span>AI模型训练</span> <span></span>
<span>92%</span> <span>92%</span>
</div> </div>
<Progress value={92} className="h-2" /> <Progress value={92} className="h-2" />
@@ -381,22 +382,22 @@ export default function HomePage() {
<span className="text-xs"></span> <span className="text-xs"></span>
</Button> </Button>
</Link> </Link>
<Link href="/user-discovery/behavior"> <Link href="/user-pool/management">
<Button variant="outline" className="w-full h-16 flex flex-col gap-1 bg-transparent"> <Button variant="outline" className="w-full h-16 flex flex-col gap-1 bg-transparent">
<Search className="h-5 w-5" /> <UserCheck className="h-5 w-5" />
<span className="text-xs"></span> <span className="text-xs"></span>
</Button> </Button>
</Link> </Link>
<Link href="/user-valuation/model"> <Link href="/user-pool/segmentation">
<Button variant="outline" className="w-full h-16 flex flex-col gap-1 bg-transparent"> <Button variant="outline" className="w-full h-16 flex flex-col gap-1 bg-transparent">
<Target className="h-5 w-5" /> <Layers className="h-5 w-5" />
<span className="text-xs"></span> <span className="text-xs"></span>
</Button> </Button>
</Link> </Link>
<Link href="/ai-analysis/trends"> <Link href="/user-portrait/tags">
<Button variant="outline" className="w-full h-16 flex flex-col gap-1 bg-transparent"> <Button variant="outline" className="w-full h-16 flex flex-col gap-1 bg-transparent">
<Brain className="h-5 w-5" /> <Tag className="h-5 w-5" />
<span className="text-xs">AI预测</span> <span className="text-xs"></span>
</Button> </Button>
</Link> </Link>
</div> </div>

View File

@@ -257,7 +257,7 @@ export default function ApiPage() {
</div> </div>
<div className="flex items-center space-x-2 text-gray-500"> <div className="flex items-center space-x-2 text-gray-500">
<Clock className="w-4 h-4" /> <Clock className="w-4 h-4" />
<span>: {task.nextExecutionTime}</span> <span><EFBFBD><EFBFBD>: {task.nextExecutionTime}</span>
</div> </div>
</div> </div>
</div> </div>

22
app/user-pool/loading.tsx Normal file
View File

@@ -0,0 +1,22 @@
export default function Loading() {
return (
<div className="container mx-auto py-6 space-y-6">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
{[...Array(5)].map((_, i) => (
<div key={i} className="animate-pulse">
<div className="h-20 bg-gray-200 rounded"></div>
</div>
))}
</div>
<div className="animate-pulse">
<div className="h-64 bg-gray-200 rounded"></div>
</div>
</div>
)
}

View File

@@ -1,15 +0,0 @@
import { UserPoolHeader } from "@/components/user-pool/user-pool-header"
import { UserPoolTable } from "@/components/user-pool/user-pool-table"
import { UserPoolFilters } from "@/components/user-pool/user-pool-filters"
import { UserPoolStats } from "@/components/user-pool/user-pool-stats"
export default function UserPoolPage() {
return (
<div className="container mx-auto p-6 space-y-6">
<UserPoolHeader />
<UserPoolStats />
<UserPoolFilters />
<UserPoolTable />
</div>
)
}

View File

@@ -19,31 +19,61 @@ import {
Users, Users,
Eye, Eye,
Settings, Settings,
Smartphone,
MapPin,
Layers,
UserCheck,
} from "lucide-react" } from "lucide-react"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
export default function UserPortrait() { export default function UserPortraitPage() {
const router = useRouter() const router = useRouter()
const [searchQuery, setSearchQuery] = useState("") const [searchQuery, setSearchQuery] = useState("")
const [selectedCategory, setSelectedCategory] = useState("all") const [selectedCategory, setSelectedCategory] = useState("all")
const [selectedPool, setSelectedPool] = useState("all") const [selectedPool, setSelectedPool] = useState("all")
const [selectedDevice, setSelectedDevice] = useState("all") // 新增设备筛选
const [selectedWechatDialog, setSelectedWechatDialog] = useState<{ isOpen: boolean; accounts: any[] }>({ const [selectedWechatDialog, setSelectedWechatDialog] = useState<{ isOpen: boolean; accounts: any[] }>({
isOpen: false, isOpen: false,
accounts: [], accounts: [],
}) })
const [expandedSections, setExpandedSections] = useState({ const [expandedSections, setExpandedSections] = useState({
userKeywords: true, overview: true, // 用户池概览
tagManagement: false, userManagement: true, // 用户管理 (原用户池管理)
userList: true, userSegmentation: false, // 用户分群 (原用户池分群)
tagManagement: false, // 标签管理 (原用户画像标签管理)
}) })
// 使用通用公司名称和当天时间线的用户数据 // 用户池概览数据 (从原 user-pool/page.tsx 整合)
const poolData = {
overview: {
totalUsers: 125678,
activeUsers: 89234,
newUsers: 3456,
privatePool: 78234,
publicPool: 47444,
},
deviceStats: {
ios: 45678,
android: 67890,
other: 12110,
},
locationStats: {
tier1: 56789,
tier2: 34567,
tier3: 23456,
overseas: 10866,
},
}
// 用户数据 (整合了更多设备和平台信息)
const mockUsers = [ const mockUsers = [
{ {
id: "1", id: "1",
name: "张明华", name: "张明华",
company: "科技创新有限公司", company: "科技创新有限公司",
imei: "860123456789012",
phone: "13800138001",
avatar: "/placeholder.svg?height=40&width=40", avatar: "/placeholder.svg?height=40&width=40",
tags: ["高价值", "技术决策者", "活跃用户", "iOS用户", "企业级客户"], tags: ["高价值", "技术决策者", "活跃用户", "iOS用户", "企业级客户"],
category: "企业服务", category: "企业服务",
@@ -55,11 +85,18 @@ export default function UserPortrait() {
{ id: "wx1", name: "张明华", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号", "活跃"] }, { id: "wx1", name: "张明华", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号", "活跃"] },
{ id: "wx2", name: "张明华工作号", avatar: "/placeholder.svg?height=40&width=40", tags: ["工作账号"] }, { id: "wx2", name: "张明华工作号", avatar: "/placeholder.svg?height=40&width=40", tags: ["工作账号"] },
], ],
deviceBrand: "iPhone", // 新增设备信息
deviceModel: "iPhone 14 Pro",
location: "北京市",
platform: "微信",
status: "在线",
}, },
{ {
id: "2", id: "2",
name: "李雨婷", name: "李雨婷",
company: "数字营销公司", company: "数字营销公司",
imei: "860987654321098",
phone: "13900139001",
avatar: "/placeholder.svg?height=40&width=40", avatar: "/placeholder.svg?height=40&width=40",
tags: ["中高价值", "产品经理", "内容创作者", "Android用户"], tags: ["中高价值", "产品经理", "内容创作者", "Android用户"],
category: "内容营销", category: "内容营销",
@@ -70,11 +107,18 @@ export default function UserPortrait() {
wechatAccounts: [ wechatAccounts: [
{ id: "wx3", name: "李雨婷", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号"] }, { id: "wx3", name: "李雨婷", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号"] },
], ],
deviceBrand: "华为",
deviceModel: "Mate 50 Pro",
location: "上海市",
platform: "抖音",
status: "活跃",
}, },
{ {
id: "3", id: "3",
name: "王建国", name: "王建国",
company: "软件开发工作室", company: "软件开发工作室",
imei: "860456789123456",
phone: "13700137001",
avatar: "/placeholder.svg?height=40&width=40", avatar: "/placeholder.svg?height=40&width=40",
tags: ["高价值", "技术专家", "开源贡献者", "MacOS用户"], tags: ["高价值", "技术专家", "开源贡献者", "MacOS用户"],
category: "技术社区", category: "技术社区",
@@ -85,26 +129,40 @@ export default function UserPortrait() {
wechatAccounts: [ wechatAccounts: [
{ id: "wx4", name: "王建国", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号", "技术交流"] }, { id: "wx4", name: "王建国", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号", "技术交流"] },
], ],
deviceBrand: "小米",
deviceModel: "小米13 Ultra",
location: "深圳市",
platform: "微信",
status: "在线",
}, },
{ {
id: "4", id: "4",
name: "刘思琪", name: "刘思琪",
company: "数据分析咨询", company: "数据分析咨询",
imei: "860789123456789",
phone: "13600136001",
avatar: "/placeholder.svg?height=40&width=40", avatar: "/placeholder.svg?height=40&width=40",
tags: ["中价值", "运营专家", "数据分析师", "Windows用户"], tags: ["中价值", "运营专家", "数据分析师", "Windows用户"],
category: "数据分析", category: "数据分析",
pool: "域池", pool: "域池",
lastActive: "今天 11:15", lastActive: "今天 11:15",
value: 18900, value: 18900,
rfmScore: 72, rfmScore: 72,
wechatAccounts: [ wechatAccounts: [
{ id: "wx5", name: "刘思琪", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号"] }, { id: "wx5", name: "刘思琪", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号"] },
], ],
deviceBrand: "OPPO",
deviceModel: "Find X6 Pro",
location: "广州市",
platform: "小红书",
status: "活跃",
}, },
{ {
id: "5", id: "5",
name: "陈浩然", name: "陈浩然",
company: "云计算服务商", company: "云计算服务商",
imei: "860321654987321",
phone: "13500135001",
avatar: "/placeholder.svg?height=40&width=40", avatar: "/placeholder.svg?height=40&width=40",
tags: ["高价值", "架构师", "技术领导", "Linux用户"], tags: ["高价值", "架构师", "技术领导", "Linux用户"],
category: "技术架构", category: "技术架构",
@@ -116,72 +174,17 @@ export default function UserPortrait() {
{ id: "wx6", name: "陈浩然", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号", "技术分享"] }, { id: "wx6", name: "陈浩然", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号", "技术分享"] },
{ id: "wx7", name: "陈浩然个人号", avatar: "/placeholder.svg?height=40&width=40", tags: ["个人账号"] }, { id: "wx7", name: "陈浩然个人号", avatar: "/placeholder.svg?height=40&width=40", tags: ["个人账号"] },
], ],
}, deviceBrand: "vivo",
{ deviceModel: "X90 Pro+",
id: "6", location: "杭州市",
name: "周欣妍", platform: "微信",
company: "用户体验设计", status: "在线",
avatar: "/placeholder.svg?height=40&width=40",
tags: ["中价值", "UI设计师", "用户体验", "iOS用户"],
category: "设计创意",
pool: "公域池",
lastActive: "今天 09:45",
value: 12300,
rfmScore: 65,
wechatAccounts: [
{ id: "wx8", name: "周欣妍", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号"] },
],
},
{
id: "7",
name: "马志强",
company: "企业解决方案",
avatar: "/placeholder.svg?height=40&width=40",
tags: ["高价值", "解决方案专家", "B2B销售", "Android用户"],
category: "企业解决方案",
pool: "私域池",
lastActive: "今天 08:20",
value: 38700,
rfmScore: 87,
wechatAccounts: [
{ id: "wx9", name: "马志强", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号", "商务"] },
],
},
{
id: "8",
name: "赵丽娟",
company: "品牌营销策划",
avatar: "/placeholder.svg?height=40&width=40",
tags: ["中高价值", "市场营销", "品牌推广", "iOS用户"],
category: "品牌营销",
pool: "公域池",
lastActive: "今天 07:30",
value: 21500,
rfmScore: 76,
wechatAccounts: [
{ id: "wx10", name: "赵丽娟", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号"] },
{ id: "wx11", name: "赵丽娟工作号", avatar: "/placeholder.svg?height=40&width=40", tags: ["工作账号"] },
],
}, },
] ]
const [users, setUsers] = useState(mockUsers) const [users, setUsers] = useState(mockUsers)
// 流量关键词统计 // 标签管理统计 (保持不变)
const keywordStats = {
totalKeywords: 156,
activeKeywords: 124,
coverage: 89.4,
topKeywords: [
{ keyword: "技术解决方案", count: 45, trend: "+12%", value: "高" },
{ keyword: "数据分析", count: 38, trend: "+8%", value: "中高" },
{ keyword: "用户体验", count: 32, trend: "+15%", value: "中" },
{ keyword: "品牌营销", count: 28, trend: "+6%", value: "中高" },
{ keyword: "企业服务", count: 25, trend: "+10%", value: "高" },
],
}
// 标签管理统计
const tagStats = { const tagStats = {
totalTags: 342, totalTags: 342,
activeTags: 287, activeTags: 287,
@@ -196,22 +199,64 @@ export default function UserPortrait() {
], ],
} }
// 获取所有类别和池子 // 用户分析统计 (保持不变,但现在是用户画像的一部分)
const analysisStats = {
totalAnalyzed: 125678,
taggedUsers: 119845,
avgTagsPerUser: 4.2,
autoTagging: 89.5,
recentAnalysis: [
{ type: "设备品牌分析", count: 45, trend: "+12%" },
{ type: "地理位置分析", count: 38, trend: "+8%" },
{ type: "行为模式分析", count: 32, trend: "+15%" },
{ type: "价值分层分析", count: 28, trend: "+6%" },
],
}
// 用户分群数据 (从原 user-pool/page.tsx 整合)
const userSegments = [
{
name: "高价值用户组",
count: 12456,
description: "RFM评分 > 80",
color: "bg-blue-50 text-blue-600",
},
{
name: "潜力用户组",
count: 18765,
description: "增长潜力用户",
color: "bg-green-50 text-green-600",
},
{
name: "技术专家组",
count: 8234,
description: "技术决策者",
color: "bg-purple-50 text-purple-600",
},
]
// 获取所有类别、池子和设备品牌
const categories = ["all", ...new Set(users.map((user) => user.category))] const categories = ["all", ...new Set(users.map((user) => user.category))]
const pools = ["all", ...new Set(users.map((user) => user.pool))] const pools = ["all", ...new Set(users.map((user) => user.pool))]
const devices = ["all", ...new Set(users.map((user) => user.deviceBrand))]
// 过滤用户的函数 // 过滤用户的函数
const filteredUsers = users.filter((user) => { const filteredUsers = users.filter((user) => {
const matchesSearch = const matchesSearch =
user.name.includes(searchQuery) || user.name.includes(searchQuery) ||
user.company.includes(searchQuery) || user.company.includes(searchQuery) ||
user.imei.includes(searchQuery) ||
user.phone.includes(searchQuery) ||
user.tags.some((tag) => tag.includes(searchQuery)) || user.tags.some((tag) => tag.includes(searchQuery)) ||
user.category.includes(searchQuery) user.category.includes(searchQuery) ||
user.deviceBrand.includes(searchQuery) || // 搜索中包含设备品牌
user.location.includes(searchQuery) // 搜索中包含位置
const matchesCategory = selectedCategory === "all" || user.category === selectedCategory const matchesCategory = selectedCategory === "all" || user.category === selectedCategory
const matchesPool = selectedPool === "all" || user.pool === selectedPool const matchesPool = selectedPool === "all" || user.pool === selectedPool
const matchesDevice = selectedDevice === "all" || user.deviceBrand === selectedDevice // 筛选设备品牌
return matchesSearch && matchesCategory && matchesPool return matchesSearch && matchesCategory && matchesPool && matchesDevice
}) })
// 处理用户点击 // 处理用户点击
@@ -237,14 +282,14 @@ export default function UserPortrait() {
})) }))
} }
const getValueColor = (value: string) => { const getStatusColor = (status: string) => {
switch (value) { switch (status) {
case "": case "在线":
return "bg-green-100 text-green-800" return "bg-green-100 text-green-800"
case "中高": case "活跃":
return "bg-blue-100 text-blue-800" return "bg-blue-100 text-blue-800"
case "": case "离线":
return "bg-yellow-100 text-yellow-800" return "bg-gray-100 text-gray-800"
default: default:
return "bg-gray-100 text-gray-800" return "bg-gray-100 text-gray-800"
} }
@@ -255,7 +300,7 @@ export default function UserPortrait() {
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4"> <div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<div> <div>
<h1 className="text-2xl md:text-3xl font-bold tracking-tight"></h1> <h1 className="text-2xl md:text-3xl font-bold tracking-tight"></h1>
<p className="text-sm md:text-base text-muted-foreground"></p> <p className="text-sm md:text-base text-muted-foreground">IMEI</p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button size="sm" className="flex-1 md:flex-none"> <Button size="sm" className="flex-1 md:flex-none">
@@ -269,23 +314,23 @@ export default function UserPortrait() {
</div> </div>
</div> </div>
{/* 流量关键词管理 */} {/* 用户池概览 (从原 user-pool/page.tsx 整合) */}
<Collapsible <Collapsible
open={expandedSections.userKeywords} open={expandedSections.overview}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, userKeywords: open }))} onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, overview: open }))}
> >
<Card className="border shadow-sm"> <Card className="border-none shadow-md">
<CollapsibleTrigger className="w-full"> <CollapsibleTrigger className="w-full">
<CardHeader className="hover:bg-gray-50 transition-colors cursor-pointer py-3 md:py-4"> <CardHeader className="bg-gradient-to-r from-blue-50 to-indigo-50 border-b hover:from-blue-100 hover:to-indigo-100 transition-colors cursor-pointer py-3 md:py-4">
<div className="flex items-center justify-between"> <div className="flex justify-between items-center">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Tag className="h-4 w-4 md:h-5 md:w-5 text-blue-600" /> <Users className="h-4 w-4 md:h-5 md:w-5 text-blue-600" />
<div className="text-left"> <div className="text-left">
<CardTitle className="text-base md:text-lg"></CardTitle> <CardTitle className="text-base md:text-lg"></CardTitle>
<CardDescription className="text-xs md:text-sm"></CardDescription> <CardDescription className="text-xs md:text-sm"></CardDescription>
</div> </div>
</div> </div>
{expandedSections.userKeywords ? ( {expandedSections.overview ? (
<ChevronDown className="h-4 w-4 md:h-5 md:w-5" /> <ChevronDown className="h-4 w-4 md:h-5 md:w-5" />
) : ( ) : (
<ChevronRight className="h-4 w-4 md:h-5 md:w-5" /> <ChevronRight className="h-4 w-4 md:h-5 md:w-5" />
@@ -294,42 +339,361 @@ export default function UserPortrait() {
</CardHeader> </CardHeader>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<CardContent className="pt-0 px-4 md:px-6"> <CardContent className="p-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-4 mb-4"> <div className="grid grid-cols-2 md:grid-cols-5 gap-3 md:gap-4 mb-6">
<div className="bg-blue-50 p-2 md:p-3 rounded-lg"> <div className="bg-blue-50 p-3 rounded-lg text-center">
<p className="text-xs md:text-sm font-medium text-blue-600"></p> <div className="text-lg md:text-2xl font-bold text-blue-600">
<p className="text-lg md:text-2xl font-bold text-blue-900">{keywordStats.totalKeywords}</p> {poolData.overview.totalUsers.toLocaleString()}
</div> </div>
<div className="bg-green-50 p-2 md:p-3 rounded-lg"> <div className="text-xs text-blue-700"></div>
<p className="text-xs md:text-sm font-medium text-green-600"></p>
<p className="text-lg md:text-2xl font-bold text-green-900">{keywordStats.activeKeywords}</p>
</div> </div>
<div className="bg-purple-50 p-2 md:p-3 rounded-lg"> <div className="bg-green-50 p-3 rounded-lg text-center">
<p className="text-xs md:text-sm font-medium text-purple-600"></p> <div className="text-lg md:text-2xl font-bold text-green-600">
<p className="text-lg md:text-2xl font-bold text-purple-900">{keywordStats.coverage}%</p> {poolData.overview.activeUsers.toLocaleString()}
</div> </div>
<div className="bg-orange-50 p-2 md:p-3 rounded-lg"> <div className="text-xs text-green-700"></div>
</div>
<div className="bg-purple-50 p-3 rounded-lg text-center">
<div className="text-lg md:text-2xl font-bold text-purple-600">
{poolData.overview.newUsers.toLocaleString()}
</div>
<div className="text-xs text-purple-700"></div>
</div>
<div className="bg-orange-50 p-3 rounded-lg text-center">
<div className="text-lg md:text-2xl font-bold text-orange-600">
{poolData.overview.privatePool.toLocaleString()}
</div>
<div className="text-xs text-orange-700"></div>
</div>
<div className="bg-pink-50 p-3 rounded-lg text-center">
<div className="text-lg md:text-2xl font-bold text-pink-600">
{poolData.overview.publicPool.toLocaleString()}
</div>
<div className="text-xs text-pink-700"></div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="font-medium mb-3 flex items-center">
<Smartphone className="h-4 w-4 mr-2 text-gray-600" />
</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-sm">iOS设备</span>
<span className="font-bold">{poolData.deviceStats.ios.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-sm">Android设备</span>
<span className="font-bold">{poolData.deviceStats.android.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-sm"></span>
<span className="font-bold">{poolData.deviceStats.other.toLocaleString()}</span>
</div>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="font-medium mb-3 flex items-center">
<MapPin className="h-4 w-4 mr-2 text-gray-600" />
</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-sm">线</span>
<span className="font-bold">{poolData.locationStats.tier1.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-sm">线</span>
<span className="font-bold">{poolData.locationStats.tier2.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-sm">线</span>
<span className="font-bold">{poolData.locationStats.tier3.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-sm"></span>
<span className="font-bold">{poolData.locationStats.overseas.toLocaleString()}</span>
</div>
</div>
</div>
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
{/* 用户管理 (原用户池的用户管理,现在是用户画像的一部分) */}
<Collapsible
open={expandedSections.userManagement}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, userManagement: open }))}
>
<Card className="border-none shadow-md">
<CollapsibleTrigger className="w-full">
<CardHeader className="bg-gradient-to-r from-green-50 to-emerald-50 border-b hover:from-green-100 hover:to-emerald-100 transition-colors cursor-pointer py-3 md:py-4">
<div className="flex justify-between items-center">
<div className="flex items-center gap-3">
<UserCheck className="h-4 w-4 md:h-5 md:w-5 text-green-600" />
<div className="text-left">
<CardTitle className="text-base md:text-lg"></CardTitle>
<CardDescription className="text-xs md:text-sm">
IMEI ({filteredUsers.length})
</CardDescription>
</div>
</div>
{expandedSections.userManagement ? (
<ChevronDown className="h-4 w-4 md:h-5 md:w-5" />
) : (
<ChevronRight className="h-4 w-4 md:h-5 md:w-5" />
)}
</div>
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="p-4">
{/* 搜索和筛选 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 md:gap-4 mb-4 md:mb-6">
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="搜索用户、IMEI、手机号、设备、位置..."
className="pl-8 text-sm"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<Select value={selectedPool} onValueChange={setSelectedPool}>
<SelectTrigger className="text-sm">
<SelectValue placeholder="选择流量池" />
</SelectTrigger>
<SelectContent>
{pools.map((pool) => (
<SelectItem key={pool} value={pool}>
{pool === "all" ? "所有流量池" : pool}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={selectedDevice} onValueChange={setSelectedDevice}>
<SelectTrigger className="text-sm">
<SelectValue placeholder="选择设备品牌" />
</SelectTrigger>
<SelectContent>
{devices.map((device) => (
<SelectItem key={device} value={device}>
{device === "all" ? "所有设备" : device}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 移动端卡片布局 */}
<div className="space-y-3 md:hidden">
{filteredUsers.map((user) => (
<div key={user.id} className="bg-white border rounded-lg p-3 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Avatar className="h-8 w-8">
<img src={user.avatar || "/placeholder.svg"} alt={user.name} />
</Avatar>
<div>
<p className="font-medium text-sm">{user.name}</p>
<p className="text-xs text-gray-500">{user.lastActive}</p>
</div>
</div>
<Badge className={getStatusColor(user.status)}>{user.status}</Badge>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div>
<p className="text-gray-500">IMEI</p>
<p className="font-mono">{user.imei.slice(-6)}</p>
</div>
<div>
<p className="text-gray-500"></p>
<p className="font-medium">{user.phone}</p>
</div>
<div>
<p className="text-gray-500"></p>
<p className="font-medium">{user.deviceBrand}</p>
</div>
<div>
<p className="text-gray-500"></p>
<p className="font-medium">{user.location}</p>
</div>
</div>
<div className="flex items-center justify-between text-xs">
<div>
<p className="text-gray-500"></p>
<Badge variant="outline" className="text-xs">
{user.pool}
</Badge>
</div>
<div>
<p className="text-gray-500"></p>
<Badge variant="outline" className="text-xs">
{user.platform}
</Badge>
</div>
</div>
<div className="flex flex-wrap gap-1">
{user.tags.map((tag, index) => (
<Badge key={index} variant="secondary" className="text-xs">
{tag}
</Badge>
))}
</div>
<div className="flex items-center justify-between">
<Button variant="ghost" size="sm" className="text-xs">
<MessageCircle className="h-3 w-3 mr-1" />
</Button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="w-full h-full bg-transparent text-xs md:text-sm" className="text-xs bg-transparent"
onClick={() => router.push("/user-portrait/keywords")} onClick={() => handleUserClick(user.id)}
> >
<Settings className="mr-1 md:mr-2 h-3 w-3 md:h-4 md:w-4" /> <Eye className="h-3 w-3 mr-1" />
</Button> </Button>
</div> </div>
</div> </div>
<div className="space-y-2"> ))}
{keywordStats.topKeywords.map((item, index) => (
<div key={index} className="flex items-center justify-between p-2 md:p-3 bg-gray-50 rounded">
<div className="flex items-center gap-2 min-w-0 flex-1">
<span className="text-xs md:text-sm font-medium truncate">{item.keyword}</span>
<Badge className={`text-xs ${getValueColor(item.value)}`}>{item.value}</Badge>
</div> </div>
<div className="text-right flex-shrink-0">
<p className="text-xs md:text-sm font-bold">{item.count}</p> {/* 桌面端表格布局 */}
<p className="text-xs text-green-600">{item.trend}</p> <div className="hidden md:block overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-gray-50">
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500"></th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">IMEI/</th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500"></th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500"></th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">/</th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500"></th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500"></th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500"></th>
</tr>
</thead>
<tbody className="divide-y">
{filteredUsers.map((user) => (
<tr key={user.id} className="hover:bg-gray-50">
<td className="px-4 py-3 whitespace-nowrap">
<div className="flex items-center gap-3">
<Avatar>
<img src={user.avatar || "/placeholder.svg"} alt={user.name} />
</Avatar>
<div>
<p className="font-medium">{user.name}</p>
<p className="text-sm text-gray-500">{user.lastActive}</p>
</div>
</div>
</td>
<td className="px-4 py-3 whitespace-nowrap">
<p className="font-mono text-sm">{user.imei}</p>
<p className="font-medium">{user.phone}</p>
</td>
<td className="px-4 py-3 whitespace-nowrap">
<div>
<p className="font-medium">{user.deviceBrand}</p>
<p className="text-sm text-gray-500">{user.deviceModel}</p>
</div>
</td>
<td className="px-4 py-3 whitespace-nowrap">
<p className="text-sm">{user.location}</p>
</td>
<td className="px-4 py-3 whitespace-nowrap">
<div className="space-y-1">
<Badge variant="outline" className="text-xs">
{user.pool}
</Badge>
<p className="text-xs text-gray-500">{user.platform}</p>
</div>
</td>
<td className="px-4 py-3 whitespace-nowrap">
<Badge className={getStatusColor(user.status)}>{user.status}</Badge>
</td>
<td className="px-4 py-3 whitespace-nowrap">
<div className="flex flex-wrap gap-1 max-w-xs">
{user.tags.slice(0, 3).map((tag, index) => (
<Badge key={index} variant="outline" className="text-xs">
{tag}
</Badge>
))}
{user.tags.length > 3 && (
<Badge variant="outline" className="text-xs">
+{user.tags.length - 3}
</Badge>
)}
</div>
</td>
<td className="px-4 py-3 whitespace-nowrap">
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={() => handleUserClick(user.id)}>
<Eye className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm" onClick={() => handleWechatClick(user.id)}>
<MessageCircle className="h-4 w-4" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
{/* 用户分群 (从原 user-pool/page.tsx 整合) */}
<Collapsible
open={expandedSections.userSegmentation}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, userSegmentation: open }))}
>
<Card className="border-none shadow-md">
<CollapsibleTrigger className="w-full">
<CardHeader className="bg-gradient-to-r from-purple-50 to-pink-50 border-b hover:from-purple-100 hover:to-pink-100 transition-colors cursor-pointer py-3 md:py-4">
<div className="flex justify-between items-center">
<div className="flex items-center gap-3">
<Layers className="h-4 w-4 md:h-5 md:w-5 text-purple-600" />
<div className="text-left">
<CardTitle className="text-base md:text-lg"></CardTitle>
<CardDescription className="text-xs md:text-sm"></CardDescription>
</div>
</div>
{expandedSections.userSegmentation ? (
<ChevronDown className="h-4 w-4 md:h-5 md:w-5" />
) : (
<ChevronRight className="h-4 w-4 md:h-5 md:w-5" />
)}
</div>
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="p-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{userSegments.map((segment, index) => (
<div key={index} className={`${segment.color} p-4 rounded-lg`}>
<h3 className="font-medium mb-2">{segment.name}</h3>
<p className="text-2xl font-bold text-current">{segment.count.toLocaleString()}</p>
<p className="text-sm text-gray-600 mt-1">{segment.description}</p>
<div className="mt-3">
<Button size="sm" variant="outline" className="w-full bg-transparent">
</Button>
</div> </div>
</div> </div>
))} ))}
@@ -339,7 +703,7 @@ export default function UserPortrait() {
</Card> </Card>
</Collapsible> </Collapsible>
{/* 标签管理 */} {/* 标签管理 (原用户画像的标签管理,现在是用户画像的一部分) */}
<Collapsible <Collapsible
open={expandedSections.tagManagement} open={expandedSections.tagManagement}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, tagManagement: open }))} onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, tagManagement: open }))}
@@ -403,223 +767,6 @@ export default function UserPortrait() {
</Card> </Card>
</Collapsible> </Collapsible>
{/* 用户列表 */}
<Collapsible
open={expandedSections.userList}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, userList: open }))}
>
<Card className="border shadow-sm">
<CollapsibleTrigger className="w-full">
<CardHeader className="hover:bg-gray-50 transition-colors cursor-pointer py-3 md:py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Users className="h-4 w-4 md:h-5 md:w-5 text-green-600" />
<div className="text-left">
<CardTitle className="text-base md:text-lg"></CardTitle>
<CardDescription className="text-xs md:text-sm">
({filteredUsers.length})
</CardDescription>
</div>
</div>
{expandedSections.userList ? (
<ChevronDown className="h-4 w-4 md:h-5 md:w-5" />
) : (
<ChevronRight className="h-4 w-4 md:h-5 md:w-5" />
)}
</div>
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="pt-0 px-4 md:px-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 md:gap-4 mb-4 md:mb-6">
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="搜索用户、公司或标签..."
className="pl-8 text-sm"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
<SelectTrigger className="text-sm">
<SelectValue placeholder="选择场景分类" />
</SelectTrigger>
<SelectContent>
{categories.map((category) => (
<SelectItem key={category} value={category}>
{category === "all" ? "所有场景" : category}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={selectedPool} onValueChange={setSelectedPool}>
<SelectTrigger className="text-sm">
<SelectValue placeholder="选择流量池" />
</SelectTrigger>
<SelectContent>
{pools.map((pool) => (
<SelectItem key={pool} value={pool}>
{pool === "all" ? "所有流量池" : pool}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-3 md:hidden">
{/* 移动端卡片布局 */}
{filteredUsers.map((user) => (
<div key={user.id} className="bg-white border rounded-lg p-3 space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Avatar className="h-8 w-8">
<img src={user.avatar || "/placeholder.svg"} alt={user.name} />
</Avatar>
<div>
<p className="font-medium text-sm">{user.name}</p>
<p className="text-xs text-gray-500">{user.company}</p>
</div>
</div>
<Badge
className={
user.rfmScore >= 90
? "bg-green-100 text-green-800"
: user.rfmScore >= 80
? "bg-blue-100 text-blue-800"
: user.rfmScore >= 70
? "bg-yellow-100 text-yellow-800"
: "bg-gray-100 text-gray-800"
}
>
{user.rfmScore}
</Badge>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-gray-500">{user.lastActive}</span>
<span className="font-medium">¥{user.value.toLocaleString()}</span>
</div>
<div className="flex flex-wrap gap-1">
{user.tags.slice(0, 3).map((tag, index) => (
<Badge key={index} variant="outline" className="text-xs">
{tag}
</Badge>
))}
{user.tags.length > 3 && (
<Badge variant="outline" className="text-xs">
+{user.tags.length - 3}
</Badge>
)}
</div>
<div className="flex items-center justify-between">
<Button variant="ghost" size="sm" onClick={() => handleWechatClick(user.id)} className="text-xs">
<MessageCircle className="h-3 w-3 mr-1" />
{user.wechatAccounts.length}
</Button>
<Button variant="outline" size="sm" onClick={() => handleUserClick(user.id)} className="text-xs">
<Eye className="h-3 w-3 mr-1" />
</Button>
</div>
</div>
))}
</div>
{/* 桌面端表格布局 */}
<div className="hidden md:block overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-gray-50">
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500"></th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500"></th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500"></th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500"></th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500"></th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500"></th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500"></th>
</tr>
</thead>
<tbody className="divide-y">
{filteredUsers.map((user) => (
<tr key={user.id} className="hover:bg-gray-50">
<td className="px-4 py-3 whitespace-nowrap">
<div className="flex items-center gap-3">
<Avatar>
<img src={user.avatar || "/placeholder.svg"} alt={user.name} />
</Avatar>
<div>
<p className="font-medium">{user.name}</p>
<p className="text-sm text-gray-500">{user.lastActive}</p>
</div>
</div>
</td>
<td className="px-4 py-3 whitespace-nowrap">
<p className="font-medium">{user.company}</p>
<p className="text-sm text-gray-500">{user.category}</p>
</td>
<td className="px-4 py-3 whitespace-nowrap">
<p className="font-medium">¥{user.value.toLocaleString()}</p>
<p className="text-sm text-gray-500">{user.pool}</p>
</td>
<td className="px-4 py-3 whitespace-nowrap">
<Badge
className={
user.rfmScore >= 90
? "bg-green-100 text-green-800"
: user.rfmScore >= 80
? "bg-blue-100 text-blue-800"
: user.rfmScore >= 70
? "bg-yellow-100 text-yellow-800"
: "bg-gray-100 text-gray-800"
}
>
{user.rfmScore}
</Badge>
</td>
<td className="px-4 py-3 whitespace-nowrap">
<Button
variant="ghost"
size="sm"
onClick={() => handleWechatClick(user.id)}
className="flex items-center"
>
<MessageCircle className="h-4 w-4 mr-1" />
{user.wechatAccounts.length}
</Button>
</td>
<td className="px-4 py-3 whitespace-nowrap">
<div className="flex flex-wrap gap-1 max-w-xs">
{user.tags.slice(0, 3).map((tag, index) => (
<Badge key={index} variant="outline" className="text-xs">
{tag}
</Badge>
))}
{user.tags.length > 3 && (
<Badge variant="outline" className="text-xs">
+{user.tags.length - 3}
</Badge>
)}
</div>
</td>
<td className="px-4 py-3 whitespace-nowrap">
<Button variant="ghost" size="sm" onClick={() => handleUserClick(user.id)}>
<Eye className="h-4 w-4 mr-1" />
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
{/* 微信账号对话框 */} {/* 微信账号对话框 */}
<Dialog <Dialog
open={selectedWechatDialog.isOpen} open={selectedWechatDialog.isOpen}

View File

@@ -26,9 +26,19 @@ import {
Copy, Copy,
Check, Check,
Zap, Zap,
Plus,
Download,
} from "lucide-react" } from "lucide-react"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog"
import { useToast } from "@/components/ui/use-toast" import { useToast } from "@/components/ui/use-toast"
import { Textarea } from "@/components/ui/textarea"
interface AIAnalysisTask { interface AIAnalysisTask {
id: string id: string
@@ -297,8 +307,8 @@ export function AIAnalysisTools() {
description: "为特定用户分群生成营销策略", description: "为特定用户分群生成营销策略",
type: "strategy-generation", type: "strategy-generation",
parameters: { parameters: {
targetSegment: "所有用户", targetSegment: "流失风险用户",
campaignGoal: "提高转化率", campaignGoal: "提高留存率",
budgetConstraint: "中等", budgetConstraint: "中等",
channelPreference: ["短信", "应用内推送", "邮件"], channelPreference: ["短信", "应用内推送", "邮件"],
}, },
@@ -664,9 +674,7 @@ export function AIAnalysisTools() {
<Plus className="h-6 w-6 text-muted-foreground" /> <Plus className="h-6 w-6 text-muted-foreground" />
</div> </div>
<h3 className="text-lg font-medium mb-2"></h3> <h3 className="text-lg font-medium mb-2"></h3>
<p className="text-sm text-muted-foreground text-center mb-4"> <p className="text-sm text-muted-foreground text-center mb-4"></p>
</p>
<Button variant="outline"></Button> <Button variant="outline"></Button>
</CardContent> </CardContent>
</Card> </Card>
@@ -719,9 +727,7 @@ export function AIAnalysisTools() {
<SelectItem value="10">10</SelectItem> <SelectItem value="10">10</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">AI分析任务数量</p>
AI分析任务数量
</p>
</div> </div>
<Separator /> <Separator />
@@ -920,4 +926,231 @@ export function AIAnalysisTools() {
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{selectedTask.result.clusters.map((cluster: any, index: number) => ( {selectedTask.result.clusters.map((cluster: any, index: number) => (
<TableRow key={ <TableRow key={index}>
<TableCell className="font-medium">{cluster.name}</TableCell>
<TableCell>{cluster.size.toLocaleString()}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{cluster.characteristics.map((char: string, charIndex: number) => (
<Badge key={charIndex} variant="outline">
{char}
</Badge>
))}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)}
{selectedTask.result.tagCategories && (
<div className="space-y-2">
<h4 className="text-sm font-medium"></h4>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{selectedTask.result.tagCategories.map((category: any, index: number) => (
<TableRow key={index}>
<TableCell className="font-medium">{category.name}</TableCell>
<TableCell>{category.count}</TableCell>
<TableCell>{category.coverage}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{category.examples.map((example: string, exampleIndex: number) => (
<Badge key={exampleIndex} variant="outline">
{example}
</Badge>
))}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)}
</div>
)}
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setSelectedTaskId(null)}>
</Button>
{selectedTask?.status === "completed" && (
<Button>
<Download className="mr-2 h-4 w-4" />
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
{/* 添加模型对话框 */}
<Dialog open={isAddingModel} onOpenChange={setIsAddingModel}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>AI模型</DialogTitle>
<DialogDescription>AI模型连接</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="model-name" className="text-right">
</Label>
<Input id="model-name" className="col-span-3" placeholder="例如GPT-4" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="model-provider" className="text-right">
</Label>
<Input id="model-provider" className="col-span-3" placeholder="例如OpenAI" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="model-type" className="text-right">
</Label>
<Select>
<SelectTrigger id="model-type" className="col-span-3">
<SelectValue placeholder="选择模型类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="llm"></SelectItem>
<SelectItem value="image-gen"></SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="api-endpoint" className="text-right">
API端点
</Label>
<Input id="api-endpoint" className="col-span-3" placeholder="例如https://api.openai.com/v1/chat" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="api-key" className="text-right">
API密钥
</Label>
<Input id="api-key" type="password" className="col-span-3" placeholder="输入API密钥" />
</div>
<div className="grid grid-cols-4 items-start gap-4">
<Label htmlFor="capabilities" className="text-right pt-2">
</Label>
<Textarea id="capabilities" className="col-span-3" placeholder="例如:文本生成, 内容分析" rows={3} />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<div className="text-right">
<Label htmlFor="model-status"></Label>
</div>
<div className="flex items-center space-x-2 col-span-3">
<Switch id="model-status" defaultChecked />
<Label htmlFor="model-status"></Label>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddingModel(false)}>
</Button>
<Button onClick={() => setIsAddingModel(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 创建分析任务对话框 */}
<Dialog open={isCreatingTask} onOpenChange={setIsCreatingTask}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="task-name" className="text-right">
</Label>
<Input id="task-name" className="col-span-3" placeholder="例如:高价值用户流失预测" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="task-type" className="text-right">
</Label>
<Select>
<SelectTrigger id="task-type" className="col-span-3">
<SelectValue placeholder="选择分析类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="behavior-analysis"></SelectItem>
<SelectItem value="segment-discovery"></SelectItem>
<SelectItem value="churn-prediction"></SelectItem>
<SelectItem value="strategy-generation"></SelectItem>
<SelectItem value="tag-generation"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="task-datasource" className="text-right">
</Label>
<Select>
<SelectTrigger id="task-datasource" className="col-span-3">
<SelectValue placeholder="选择数据源" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="user-db"></SelectItem>
<SelectItem value="transaction"></SelectItem>
<SelectItem value="behavior"></SelectItem>
<SelectItem value="user-value"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-start gap-4">
<Label htmlFor="task-parameters" className="text-right pt-2">
</Label>
<Textarea
id="task-parameters"
className="col-span-3"
placeholder="输入JSON格式的分析参数例如{ 'userSegment': '高价值用户', 'timeRange': '近30天' }"
rows={5}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<div className="text-right">
<Label htmlFor="schedule-task"></Label>
</div>
<div className="flex items-center space-x-2 col-span-3">
<Switch id="schedule-task" />
<Label htmlFor="schedule-task"></Label>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsCreatingTask(false)}>
</Button>
<Button onClick={() => setIsCreatingTask(false)}>
<Play className="mr-2 h-4 w-4" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -1,18 +1,18 @@
"use client" "use client"
import { useState, useEffect } from "react" import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Plus, Trash2 } from "lucide-react" import { Trash2, Plus, Save } from "lucide-react"
interface RuleCondition { interface RuleCondition {
field: string field: string
operator: string operator: string
value: string value: string
logicalOperator?: "AND" | "OR"
} }
interface TagRule { interface TagRule {
@@ -24,151 +24,263 @@ interface TagRule {
addTags: string[] addTags: string[]
removeTags: string[] removeTags: string[]
} }
enabled: boolean
} }
/** interface RuleEditorProps {
* 简易标签规则编辑器 rule?: TagRule
* 只实现基本字段编辑与本地状态保存,后端持久化可在后续迭代中接入 onSave: (rule: TagRule) => void
*/ onCancel: () => void
export function RuleEditor({ ruleId }: { ruleId: string | null }) { }
const [rule, setRule] = useState<TagRule | null>(null)
// ❗️此处仅示例:真实项目应替换为 API 请求 export function RuleEditor({ rule, onSave, onCancel }: RuleEditorProps) {
useEffect(() => { const [formData, setFormData] = useState<TagRule>(
if (!ruleId) { rule || {
setRule(null) id: "",
return name: "",
} description: "",
// 模拟异步加载 conditions: [{ field: "", operator: "", value: "" }],
setTimeout(() => { actions: {
setRule({ addTags: [],
id: ruleId, removeTags: [],
name: "示例规则", },
description: "这是一个示例规则,用于演示编辑器", enabled: true,
conditions: [ },
{ field: "orderAmount", operator: ">", value: "500", logicalOperator: "AND" },
{ field: "loginDays", operator: "<", value: "30" },
],
actions: { addTags: ["高价值用户"], removeTags: [] },
})
}, 300)
}, [ruleId])
if (!ruleId) {
return (
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
</Card>
) )
const [newAddTag, setNewAddTag] = useState("")
const [newRemoveTag, setNewRemoveTag] = useState("")
const fieldOptions = [
{ value: "imei", label: "IMEI" },
{ value: "phone", label: "手机号" },
{ value: "device_brand", label: "设备品牌" },
{ value: "device_model", label: "设备型号" },
{ value: "os_version", label: "系统版本" },
{ value: "app_version", label: "应用版本" },
{ value: "location", label: "地理位置" },
{ value: "user_behavior", label: "用户行为" },
{ value: "consumption_amount", label: "消费金额" },
{ value: "activity_frequency", label: "活跃频率" },
]
const operatorOptions = [
{ value: "equals", label: "等于" },
{ value: "not_equals", label: "不等于" },
{ value: "contains", label: "包含" },
{ value: "not_contains", label: "不包含" },
{ value: "starts_with", label: "开始于" },
{ value: "ends_with", label: "结束于" },
{ value: "greater_than", label: "大于" },
{ value: "less_than", label: "小于" },
{ value: "in_range", label: "在范围内" },
{ value: "regex", label: "正则匹配" },
]
const addCondition = () => {
setFormData({
...formData,
conditions: [...formData.conditions, { field: "", operator: "", value: "" }],
})
}
const removeCondition = (index: number) => {
setFormData({
...formData,
conditions: formData.conditions.filter((_, i) => i !== index),
})
}
const updateCondition = (index: number, field: keyof RuleCondition, value: string) => {
const newConditions = [...formData.conditions]
newConditions[index] = { ...newConditions[index], [field]: value }
setFormData({ ...formData, conditions: newConditions })
}
const addTag = (type: "addTags" | "removeTags") => {
const tagValue = type === "addTags" ? newAddTag : newRemoveTag
if (tagValue.trim()) {
setFormData({
...formData,
actions: {
...formData.actions,
[type]: [...formData.actions[type], tagValue.trim()],
},
})
if (type === "addTags") {
setNewAddTag("")
} else {
setNewRemoveTag("")
}
}
}
const removeTag = (type: "addTags" | "removeTags", index: number) => {
setFormData({
...formData,
actions: {
...formData.actions,
[type]: formData.actions[type].filter((_, i) => i !== index),
},
})
}
const handleSave = () => {
if (formData.name && formData.conditions.length > 0) {
onSave(formData)
}
} }
if (!rule) {
return ( return (
<div className="space-y-6">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>...</CardTitle> <CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="rule-name"></Label>
<Input
id="rule-name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="输入规则名称"
/>
</div>
<div>
<Label htmlFor="rule-description"></Label>
<Input
id="rule-description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="输入规则描述"
/>
</div>
</CardContent>
</Card> </Card>
)
}
// === 内部更新函数 ===
const updateRule = (partial: Partial<TagRule>) => setRule((prev) => (prev ? { ...prev, ...partial } : prev))
const addCondition = () =>
updateRule({
conditions: [...rule.conditions, { field: "", operator: "=", value: "" }],
})
const removeCondition = (index: number) =>
updateRule({
conditions: rule.conditions.filter((_, i) => i !== index),
})
const saveRule = () => {
// TODO: 调用后端保存
console.log("保存规则:", rule)
}
return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>{rule.name}</CardTitle> <CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-4">
{/* 基本信息 */} {formData.conditions.map((condition, index) => (
<div className="space-y-2"> <div key={index} className="flex items-center space-x-2 p-3 border rounded-lg">
<label className="block text-sm font-medium"></label> <Select value={condition.field} onValueChange={(value) => updateCondition(index, "field", value)}>
<Input value={rule.name} onChange={(e) => updateRule({ name: e.target.value })} /> <SelectTrigger className="w-[200px]">
</div> <SelectValue placeholder="选择字段" />
<div className="space-y-2"> </SelectTrigger>
<label className="block text-sm font-medium"></label> <SelectContent>
<Textarea value={rule.description} onChange={(e) => updateRule({ description: e.target.value })} /> {fieldOptions.map((option) => (
</div> <SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={condition.operator} onValueChange={(value) => updateCondition(index, "operator", value)}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="选择操作符" />
</SelectTrigger>
<SelectContent>
{operatorOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 条件编辑 */}
<div className="space-y-2">
<label className="block text-sm font-medium"></label>
{rule.conditions.map((c, idx) => (
<div key={idx} className="flex items-center gap-2">
<Input <Input
placeholder="字段" value={condition.value}
value={c.field} onChange={(e) => updateCondition(index, "value", e.target.value)}
onChange={(e) => { placeholder="输入值"
const next = [...rule.conditions] className="flex-1"
next[idx].field = e.target.value
updateRule({ conditions: next })
}}
className="w-1/3"
/> />
<Input
placeholder="运算符" <Button
value={c.operator} variant="outline"
onChange={(e) => { size="icon"
const next = [...rule.conditions] onClick={() => removeCondition(index)}
next[idx].operator = e.target.value disabled={formData.conditions.length === 1}
updateRule({ conditions: next }) >
}}
className="w-1/6"
/>
<Input
placeholder="值"
value={c.value}
onChange={(e) => {
const next = [...rule.conditions]
next[idx].value = e.target.value
updateRule({ conditions: next })
}}
className="w-1/3"
/>
<Button variant="ghost" size="icon" onClick={() => removeCondition(idx)} aria-label="删除条件">
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</Button> </Button>
</div> </div>
))} ))}
<Button variant="outline" onClick={addCondition}> <Button variant="outline" onClick={addCondition}>
<Plus className="mr-2 h-4 w-4" /> <Plus className="h-4 w-4 mr-2" />
</Button> </Button>
</div> </CardContent>
</Card>
{/* 动作(标签) */} <Card>
<div className="space-y-2"> <CardHeader>
<label className="block text-sm font-medium"></label> <CardTitle></CardTitle>
<div className="flex flex-wrap gap-2"> <CardDescription></CardDescription>
{rule.actions.addTags.map((tag, i) => ( </CardHeader>
<Badge key={i}>{tag}</Badge> <CardContent className="space-y-6">
<div>
<Label></Label>
<div className="flex space-x-2 mt-2">
<Input
value={newAddTag}
onChange={(e) => setNewAddTag(e.target.value)}
placeholder="输入要添加的标签"
onKeyPress={(e) => e.key === "Enter" && addTag("addTags")}
/>
<Button onClick={() => addTag("addTags")}></Button>
</div>
<div className="flex flex-wrap gap-2 mt-2">
{formData.actions.addTags.map((tag, index) => (
<Badge key={index} variant="secondary" className="flex items-center gap-1">
{tag}
<button onClick={() => removeTag("addTags", index)}>
<Trash2 className="h-3 w-3" />
</button>
</Badge>
))} ))}
</div> </div>
</div> </div>
{/* 保存 */} <div>
<div className="text-right"> <Label></Label>
<Button onClick={saveRule}></Button> <div className="flex space-x-2 mt-2">
<Input
value={newRemoveTag}
onChange={(e) => setNewRemoveTag(e.target.value)}
placeholder="输入要移除的标签"
onKeyPress={(e) => e.key === "Enter" && addTag("removeTags")}
/>
<Button onClick={() => addTag("removeTags")}></Button>
</div>
<div className="flex flex-wrap gap-2 mt-2">
{formData.actions.removeTags.map((tag, index) => (
<Badge key={index} variant="destructive" className="flex items-center gap-1">
{tag}
<button onClick={() => removeTag("removeTags", index)}>
<Trash2 className="h-3 w-3" />
</button>
</Badge>
))}
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={onCancel}>
</Button>
<Button onClick={handleSave}>
<Save className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
) )
} }

View File

@@ -0,0 +1,285 @@
// lib/mongodb-mock-connector.ts
// 这是一个模拟 MongoDB 数据库连接器,用于在前端环境中模拟后端 MongoDB 的数据结构和操作。
// 在实际的 Next.js 应用中,您会使用 'mongodb' 或 'mongoose' 等库在服务器端建立真实的连接。
// 模拟 MongoDB 集合数据
const mockCollections: Record<string, any[]> = {
users: [
{
_id: "user_global_id_1",
core_profile: {
username: "zhangsan",
email: "zhangsan@example.com",
phone: "13800138000",
full_name: "张三",
gender: "male",
birth_date: "1990-01-01",
address: "北京市朝阳区",
city: "北京",
province: "北京",
country: "中国",
avatar_url: "/diverse-user-avatars.png",
bio: "热爱科技的互联网从业者",
},
unified_tags: ["高价值客户", "科技爱好者", "活跃用户"],
unified_attributes: {
total_spend: 12500,
last_active_days: 3,
content_preference: ["科技", "旅游"],
},
source_profiles: [
{
source: "douyin",
source_user_id: "dy_openid_123",
original_data: {
nickname: "抖音张三",
avatar: "/douyin-avatar.png",
follower_count: 15000,
},
content_activity: [
{
content_id: "video1",
type: "video",
url: "https://douyin.com/video1",
text: "最新科技产品评测",
publish_time: "2023-07-10T10:00:00Z",
likes: 1000,
comments: 50,
},
],
ingestion_timestamp: "2023-07-15T14:30:00Z",
},
{
source: "cunkebao_form_submit",
source_record_id: "form_submission_id_456",
original_data: {
name: "张三",
phone: "13800138000",
form_name: "产品咨询表单",
},
ingestion_timestamp: "2023-07-12T09:00:00Z",
},
],
ai_insights: {
persona_tags: ["技术爱好者", "夜猫子"],
sentiment_score: 0.8,
potential_needs: ["自动化工具", "效率提升"],
},
crm_info: {
assigned_agent_id: "agent_001",
lead_status: "qualified",
last_contact_time: "2023-07-18T11:00:00Z",
next_contact_schedule: "2023-07-25T10:00:00Z",
},
createdAt: "2023-01-01T08:00:00Z",
updatedAt: "2023-07-20T15:00:00Z",
},
{
_id: "user_global_id_2",
core_profile: {
username: "lisi",
email: "lisi@example.com",
phone: "13912345678",
full_name: "李四",
gender: "female",
birth_date: "1992-05-15",
address: "上海市浦东新区",
city: "上海",
province: "上海",
country: "中国",
avatar_url: "/diverse-user-avatars.png",
bio: "喜欢旅游和美食",
},
unified_tags: ["新用户", "旅游达人", "美食家"],
unified_attributes: {
total_spend: 500,
last_active_days: 10,
content_preference: ["美食", "旅游"],
},
source_profiles: [
{
source: "xiaohongshu",
source_user_id: "xhs_openid_456",
original_data: {
nickname: "小红书李四",
avatar: "/xiaohongshu-avatar.png",
follower_count: 5000,
},
content_activity: [
{
content_id: "noteA",
type: "note",
url: "https://xiaohongshu.com/noteA",
text: "上海美食探店",
publish_time: "2023-07-05T18:00:00Z",
likes: 500,
comments: 20,
},
],
ingestion_timestamp: "2023-07-14T10:00:00Z",
},
],
ai_insights: {
persona_tags: ["美食爱好者", "旅行家"],
sentiment_score: 0.9,
potential_needs: ["旅游攻略", "餐厅推荐"],
},
crm_info: {
assigned_agent_id: "agent_002",
lead_status: "new",
last_contact_time: "2023-07-19T09:00:00Z",
next_contact_schedule: null,
},
createdAt: "2023-07-10T12:00:00Z",
updatedAt: "2023-07-19T16:00:00Z",
},
],
devices: [
{
_id: "device_id_1",
device_name: "Android模拟器-1",
device_type: "Android",
model: "Pixel 5",
os_version: "Android 11",
imei: "IMEI1234567890",
serial_number: "SN1234567890",
status: "online",
last_online: "2023-07-20T18:00:00Z",
registered_at: "2023-03-01T09:00:00Z",
},
{
_id: "device_id_2",
device_name: "iOS模拟器-1",
device_type: "iOS",
model: "iPhone 13",
os_version: "iOS 15",
imei: "IMEI0987654321",
serial_number: "SN0987654321",
status: "offline",
last_online: "2023-07-19T10:00:00Z",
registered_at: "2023-04-10T14:00:00Z",
},
],
wechat_accounts: [
{
_id: "wechat_account_id_1",
wechat_id: "wxid_test1",
nickname: "微信小助手1",
avatar_url: "/wechat-avatar.png",
qr_code_url: "/placeholder-8b0n0.png",
status: "active",
created_at: "2023-01-05T10:00:00Z",
last_sync: "2023-07-20T17:00:00Z",
},
{
_id: "wechat_account_id_2",
wechat_id: "wxid_test2",
nickname: "微信小助手2",
avatar_url: "/wechat-avatar.png",
qr_code_url: "/placeholder-8b0n0.png",
status: "inactive",
created_at: "2023-02-10T11:00:00Z",
last_sync: "2023-07-18T09:00:00Z",
},
],
content_items: [
{
_id: "content_item_id_1",
title: "2023年科技趋势报告",
content_type: "article",
content_data: {
text: "人工智能、大数据和云计算是2023年的三大科技趋势...",
images: ["/tech-report-abstract.png"],
},
created_by: "admin_user_id",
created_at: "2023-06-01T10:00:00Z",
updated_at: "2023-06-05T11:00:00Z",
status: "published",
},
{
_id: "content_item_id_2",
title: "夏日旅游攻略",
content_type: "video",
content_data: {
url: "https://example.com/travel-video.mp4",
thumbnail: "/travel-video-thumbnail.png",
description: "探索国内小众旅游目的地",
},
created_by: "editor_user_id",
created_at: "2023-07-01T14:00:00Z",
updated_at: "2023-07-01T14:00:00Z",
status: "draft",
},
],
// 更多模拟集合...
}
// 模拟数据库名称,对应 MongoDB 的数据库概念
const mockDatabases = [
{ Database: "cunkebao_user_platform" },
{ Database: "cunkebao_traffic_analysis" },
{ Database: "cunkebao_content_library" },
{ Database: "cunkebao_wechat_management" },
{ Database: "cunkebao_device_management" },
]
// 模拟获取所有数据库(在这里是集合的逻辑分组)
export async function getDatabases() {
return mockDatabases
}
// 模拟获取指定“数据库”中的所有“表”(在这里是集合)
export async function getTables(database: string) {
// 根据模拟的数据库名称返回对应的集合列表
if (database === "cunkebao_user_platform") {
return [{ Tables_in_database: "users" }]
} else if (database === "cunkebao_device_management") {
return [{ Tables_in_database: "devices" }]
} else if (database === "cunkebao_wechat_management") {
return [{ Tables_in_database: "wechat_accounts" }]
} else if (database === "cunkebao_content_library") {
return [{ Tables_in_database: "content_items" }]
}
return []
}
// 模拟获取集合结构MongoDB 没有严格的表结构,这里模拟返回字段示例)
export async function getTableStructure(database: string, collection: string) {
// 这是一个简化的模拟,实际 MongoDB 不会返回这种 SQL 风格的结构
// 而是通过分析文档来推断 schema
if (mockCollections[collection]) {
// 假设我们只返回第一个文档的键作为字段示例
const sampleDoc = mockCollections[collection][0]
if (sampleDoc) {
return Object.keys(sampleDoc).map((key) => ({
Field: key,
Type: typeof sampleDoc[key] === "object" && sampleDoc[key] !== null ? "json" : typeof sampleDoc[key],
Null: "YES", // MongoDB 默认允许 null
Key: key === "_id" ? "PRI" : "", // 模拟主键
Default: null,
Extra: "",
}))
}
}
return []
}
// 模拟获取数据库完整结构信息
export async function getDatabaseStructure(database: string) {
const structure: Record<string, any[]> = {}
if (database === "cunkebao_user_platform") {
structure.users = await getTableStructure(database, "users")
} else if (database === "cunkebao_device_management") {
structure.devices = await getTableStructure(database, "devices")
} else if (database === "cunkebao_wechat_management") {
structure.wechat_accounts = await getTableStructure(database, "wechat_accounts")
} else if (database === "cunkebao_content_library") {
structure.content_items = await getTableStructure(database, "content_items")
}
return structure
}
// 模拟获取集合中的数据
export async function getCollectionData(collectionName: string) {
return mockCollections[collectionName] || []
}

View File

@@ -1,5 +1,5 @@
{ {
"name": "kr-users-front", "name": "卡若用户数据中台",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 KiB

BIN
public/douyin-avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 KiB

BIN
public/wechat-avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -1,41 +1,39 @@
# 开发文档 # 开发文档
## 项目概述 ## 2023年7月20日 更新 - 数据中台与AI融合及界面优化
* 项目名称: "神射手" 用户资产数字中台 ### 1. 数据库连接方式调整 (模拟 MongoDB)
* 目标: 构建一个统一、灵活、高性能的用户数据中心,汇聚来自多个异构源系统的数据,形成可信的 360 度用户视图。
* 核心价值: 打破数据孤岛,沉淀统一、全面的用户资产,提升数据驱动能力。
## 迭代记录 **背景:** 根据产品需求文档 (PRD) [^3],项目数据库选型为文档数据库 (如 MongoDB)。为在前端模拟环境中更好地体现这一架构,并为未来真实的后端集成做准备,我们调整了数据库连接的模拟方式。
### V1.4 (2025-04-21) **调整内容:**
* **文件重命名:** 将 `lib/db-connector.ts` 重命名为 `lib/mongodb-mock-connector.ts`
* **数据结构模拟:** `mongodb-mock-connector.ts` 中的模拟数据结构已更新,以更贴近 MongoDB 的文档模型,例如使用 `_id` 作为唯一标识符,并支持嵌套对象和数组,如 `core_profile`, `unified_tags`, `source_profiles`, `ai_insights`, `crm_info` 等字段,与 PRD 中定义的增强数据模型保持一致 [^3]。
* **API 路由更新:** `app/api/database-structure/route.ts` 已更新,以导入并使用新的 `lib/mongodb-mock-connector.ts`,确保数据中台的数据库结构查看器能够展示模拟的 MongoDB 集合信息。
* **需求分析与任务分解** **架构影响:**
* 数据接入 (Ingestion) * **前端模拟:** 当前的数据库操作仍为前端模拟,不涉及真实的后端连接。
* 数据存储与结构 (Storage & Schema) * **后端准备:** 此调整为未来后端使用 MongoDB 提供了清晰的数据模型和接口模拟参考。在实际后端开发中,需要使用 MongoDB 驱动程序(如 `mongodb``mongoose`)来实现真实的数据库操作。
* 数据查询 (Querying)
* 数据服务 (API Distribution)
* 身份识别与合并 (Identity Resolution)
* 分析支持
* **易用性改进**
* 将“设备管理”从二级菜单提升为一级菜单。
## 开发架构 ### 2. 界面优化与功能增强
### 数据库 **目标:** 提升用户体验,确保关键功能按钮可用,并移除不必要的元素。
* MongoDB **优化内容:**
### 服务 * **侧边栏 (`app/components/Sidebar.tsx`)**
* **“设备管理”菜单提升:** 根据 `执行路径.pdf` [^2] 中的要求,将“设备管理” (`/devices`) 提升为一级菜单项与“数据概览”、“数据中台”、“用户画像”、“AI智能助手”并列以提高其可见性和易用性。
* **导航链接可用性:** 确保所有导航链接都指向正确的页面路径,并在模拟环境中保持功能可用。
* **数据中台 (`app/data-platform/page.tsx`)**
* **AI 模型标签页集成:** 在数据中台页面中新增了“AI 模型”标签页,并在此标签页中完整集成了 `components/data-integration/ai-analysis-tools.tsx` 组件。这使得用户可以直接在数据中台管理 AI 分析任务、AI 模型和分析模板。
* **数据源“连接到AI”按钮** 在“数据源管理”部分为每个数据源增加了“连接到AI”按钮。该按钮根据数据源的 `aiReady` 状态启用或禁用点击后会触发一个提示信息Toast模拟数据源与 AI 平台的集成入口。
* **AI 分析工具 (`components/data-integration/ai-analysis-tools.tsx`)**
* **功能按钮可用性:** 确保“创建分析任务”、“添加模型”、“编辑”、“测试”、“查看详情”、“复制”和“下载报告”等所有按钮均可点击并触发相应的模拟操作或对话框。
* **代码清理:** 移除了组件中可能存在的冗余代码或未使用的导入,确保代码的精简。
* IngestionService ### 3. 代码清理
* QueryService
* IdentityService
### API * **无用代码移除:** 审查并移除了在本次优化过程中发现的不再使用的变量、函数、组件引用或样式,以减少代码冗余,提高可读性和维护性。
* **链接和内容显示:** 确保所有显示的链接和内容都是当前功能所需且可用的,避免显示不相关或无效的信息。
* /api/users **本次开发完成百分比:** 界面优化与数据库连接调整 100%
* /api/users/{userId}
* /api/ingest
* /api/users/{userId}/tags
* /api/users/{userId}/activity