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">
<Users className="h-16 w-16 mx-auto mb-4" />
<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" />
</Button>
@@ -612,7 +616,11 @@ export default function AIAssistantPage() {
<div className="text-center text-muted-foreground">
<Users className="h-16 w-16 mx-auto mb-4" />
<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" />
</Button>

View File

@@ -1,5 +1,5 @@
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) {
try {

View File

@@ -2,14 +2,13 @@
import Link from "next/link"
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 = [
{ href: "/", icon: Home, label: "概览" },
{ href: "/data-platform", icon: Database, label: "数据中台" },
{ href: "/user-discovery", icon: Search, label: "用户发现" },
{ href: "/user-valuation", icon: Target, label: "用户估值" },
{ href: "/ai-analysis", icon: Brain, label: "AI分析" },
{ href: "/user-portrait", icon: Target, label: "用户画像" }, // 整合用户池功能
{ href: "/ai-assistant", icon: BrainCircuit, label: "AI助手" }, // 新增AI智能助手
]
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" />
<Input
type="text"
placeholder="搜索用户、标签、活动..."
placeholder="搜索用户、IMEI、手机号、标签..."
className="border-none focus-visible:ring-0 focus-visible:ring-offset-0"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}

View File

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

View File

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

View File

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

View File

@@ -18,10 +18,15 @@ import {
Settings,
ChevronRight,
ChevronDown,
Brain,
} from "lucide-react"
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() {
const { toast } = useToast() // 初始化 toast
const [timeRange, setTimeRange] = useState("today")
const [expandedSections, setExpandedSections] = useState({
overview: true,
@@ -29,6 +34,7 @@ export default function DataPlatformPage() {
integration: false,
quality: false,
})
const [activeTab, setActiveTab] = useState("overview")
// 数据中台数据
const platformData = {
@@ -50,6 +56,7 @@ export default function DataPlatformPage() {
lastSync: "1分钟前",
quality: 96.8,
description: "用户行为轨迹数据",
aiReady: true, // 新增 AI 就绪状态
},
{
name: "CRM系统",
@@ -59,6 +66,7 @@ export default function DataPlatformPage() {
lastSync: "3分钟前",
quality: 94.2,
description: "客户关系管理数据",
aiReady: true,
},
{
name: "内容平台数据",
@@ -68,6 +76,7 @@ export default function DataPlatformPage() {
lastSync: "正在同步",
quality: 92.5,
description: "抖音、小红书等平台数据",
aiReady: false,
},
{
name: "触客宝后台",
@@ -77,6 +86,7 @@ export default function DataPlatformPage() {
lastSync: "5分钟前",
quality: 95.1,
description: "呼入电话信息数据",
aiReady: true,
},
{
name: "飞书妙记",
@@ -86,6 +96,7 @@ export default function DataPlatformPage() {
lastSync: "10分钟前",
quality: 91.3,
description: "会议记录和内容摘要",
aiReady: true,
},
{
name: "表单提交数据",
@@ -95,6 +106,7 @@ export default function DataPlatformPage() {
lastSync: "2分钟前",
quality: 97.2,
description: "在线表单提交数据",
aiReady: true,
},
],
integrationStats: {
@@ -150,6 +162,14 @@ export default function DataPlatformPage() {
}
}
const handleConnectToAI = (sourceName: string) => {
toast({
title: "连接到AI",
description: `数据源 "${sourceName}" 已准备好连接到AI模型。`,
})
// 实际应用中这里会触发一个后端API调用或跳转到AI配置页面
}
return (
<div className="container mx-auto py-4 md:py-6 space-y-4 md:space-y-6">
{/* 页面标题和工具栏 */}
@@ -182,309 +202,346 @@ export default function DataPlatformPage() {
</div>
</div>
{/* 数据中台概览 */}
<Collapsible
open={expandedSections.overview}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, overview: open }))}
>
<Card className="border-none shadow-md">
<CollapsibleTrigger className="w-full">
<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 justify-between items-center">
<div className="flex items-center gap-3">
<Database className="h-4 w-4 md:h-5 md:w-5 text-blue-600" />
<div className="text-left">
<CardTitle className="text-base md:text-lg"></CardTitle>
<CardDescription className="text-xs md:text-sm"></CardDescription>
</div>
</div>
{expandedSections.overview ? (
<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-2 md:grid-cols-4 gap-3 md:gap-4">
<div className="bg-blue-50 p-3 rounded-lg text-center">
<div className="text-lg md:text-2xl font-bold text-blue-600">
{platformData.overview.totalRecords.toLocaleString()}
</div>
<div className="text-xs text-blue-700"></div>
</div>
<div className="bg-green-50 p-3 rounded-lg text-center">
<div className="text-lg md:text-2xl font-bold text-green-600">
{platformData.overview.dataSources}
</div>
<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">
{platformData.overview.dataQuality}%
</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">
{platformData.overview.storageUsed}GB
</div>
<div className="text-xs text-orange-700">使</div>
</div>
</div>
<div className="mt-4 space-y-3">
<div>
<div className="flex justify-between text-sm mb-1">
<span>使</span>
<span>
{platformData.overview.storageUsed}GB / {platformData.overview.storageTotal}GB
</span>
</div>
<Progress value={(platformData.overview.storageUsed / platformData.overview.storageTotal) * 100} />
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span></span>
<span>{platformData.overview.dataQuality}%</span>
</div>
<Progress value={platformData.overview.dataQuality} />
</div>
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
<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>
{/* 数据源管理 */}
<Collapsible
open={expandedSections.sources}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, sources: 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">
<FileText 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">
({platformData.dataSources.length})
</CardDescription>
</div>
</div>
{expandedSections.sources ? (
<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="space-y-3">
{platformData.dataSources.map((source, index) => (
<div key={index} className="bg-white border rounded-lg p-3 md:p-4">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
{getStatusIcon(source.status)}
<h3 className="font-medium text-sm md:text-base">{source.name}</h3>
<Badge className={getStatusColor(source.status)}>{source.status}</Badge>
<Badge variant="outline" className="text-xs">
{source.type}
</Badge>
</div>
<p className="text-xs md:text-sm text-gray-600 mb-2">{source.description}</p>
<div className="flex flex-wrap gap-4 text-xs">
<span>: {source.records.toLocaleString()}</span>
<span>: {source.lastSync}</span>
<span>: {source.quality}%</span>
</div>
</div>
<div className="flex flex-col md:items-end gap-2">
<div className="text-right">
<div className="text-sm font-bold text-green-600">{source.quality}%</div>
<div className="text-xs text-gray-500"></div>
</div>
<Button variant="outline" size="sm" className="bg-transparent">
<Settings className="h-3 w-3 mr-1" />
</Button>
<TabsContent value="overview" className="space-y-6">
{/* 数据中台概览 */}
<Collapsible
open={expandedSections.overview}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, overview: open }))}
>
<Card className="border-none shadow-md">
<CollapsibleTrigger className="w-full">
<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 justify-between items-center">
<div className="flex items-center gap-3">
<Database className="h-4 w-4 md:h-5 md:w-5 text-blue-600" />
<div className="text-left">
<CardTitle className="text-base md:text-lg"></CardTitle>
<CardDescription className="text-xs md:text-sm"></CardDescription>
</div>
</div>
<div className="mt-3">
<Progress value={source.quality} className="h-1.5" />
{expandedSections.overview ? (
<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-2 md:grid-cols-4 gap-3 md:gap-4">
<div className="bg-blue-50 p-3 rounded-lg text-center">
<div className="text-lg md:text-2xl font-bold text-blue-600">
{platformData.overview.totalRecords.toLocaleString()}
</div>
<div className="text-xs text-blue-700"></div>
</div>
<div className="bg-green-50 p-3 rounded-lg text-center">
<div className="text-lg md:text-2xl font-bold text-green-600">
{platformData.overview.dataSources}
</div>
<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">
{platformData.overview.dataQuality}%
</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">
{platformData.overview.storageUsed}GB
</div>
<div className="text-xs text-orange-700">使</div>
</div>
</div>
))}
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
<div className="mt-4 space-y-3">
<div>
<div className="flex justify-between text-sm mb-1">
<span>使</span>
<span>
{platformData.overview.storageUsed}GB / {platformData.overview.storageTotal}GB
</span>
</div>
<Progress
value={(platformData.overview.storageUsed / platformData.overview.storageTotal) * 100}
/>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span></span>
<span>{platformData.overview.dataQuality}%</span>
</div>
<Progress value={platformData.overview.dataQuality} />
</div>
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
</TabsContent>
{/* 数据集成统计 */}
<Collapsible
open={expandedSections.integration}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, integration: 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">
<RefreshCw 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.integration ? (
<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">
<div className="bg-blue-50 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Database className="h-4 w-4 text-blue-600" />
<span className="text-sm font-medium"></span>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-xs text-gray-600"></span>
<span className="text-sm font-bold">{platformData.integrationStats.totalIntegrations}</span>
</div>
<div className="flex justify-between">
<span className="text-xs text-gray-600"></span>
<span className="text-sm font-bold">{platformData.integrationStats.activeIntegrations}</span>
</div>
<div className="flex justify-between">
<span className="text-xs text-gray-600"></span>
<span className="text-sm font-bold">{platformData.integrationStats.pendingIntegrations}</span>
<TabsContent value="sources" className="space-y-6">
{/* 数据源管理 */}
<Collapsible
open={expandedSections.sources}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, sources: 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">
<FileText 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">
({platformData.dataSources.length})
</CardDescription>
</div>
</div>
{expandedSections.sources ? (
<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>
</div>
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="p-4">
<div className="space-y-3">
{platformData.dataSources.map((source, index) => (
<div key={index} className="bg-white border rounded-lg p-3 md:p-4">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
{getStatusIcon(source.status)}
<h3 className="font-medium text-sm md:text-base">{source.name}</h3>
<Badge className={getStatusColor(source.status)}>{source.status}</Badge>
<Badge variant="outline" className="text-xs">
{source.type}
</Badge>
</div>
<p className="text-xs md:text-sm text-gray-600 mb-2">{source.description}</p>
<div className="flex flex-wrap gap-4 text-xs">
<span>: {source.records.toLocaleString()}</span>
<span>: {source.lastSync}</span>
<span>: {source.quality}%</span>
</div>
</div>
<div className="flex flex-col md:items-end gap-2">
<div className="text-right">
<div className="text-sm font-bold text-green-600">{source.quality}%</div>
<div className="text-xs text-gray-500"></div>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" className="bg-transparent">
<Settings className="h-3 w-3 mr-1" />
</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 className="mt-3">
<Progress value={source.quality} className="h-1.5" />
</div>
</div>
))}
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
</TabsContent>
<div className="bg-green-50 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<CheckCircle className="h-4 w-4 text-green-600" />
<span className="text-sm font-medium"></span>
<TabsContent value="integration" className="space-y-6">
{/* 数据集成统计 */}
<Collapsible
open={expandedSections.integration}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, integration: 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">
<RefreshCw 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.integration ? (
<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>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-xs text-gray-600"></span>
<span className="text-sm font-bold">{platformData.integrationStats.successRate}%</span>
</div>
<div className="flex justify-between">
<span className="text-xs text-gray-600"></span>
<span className="text-sm font-bold">{platformData.integrationStats.avgSyncTime}</span>
</div>
<div className="flex justify-between">
<span className="text-xs text-gray-600"></span>
<span className="text-sm font-bold">{platformData.integrationStats.failedIntegrations}</span>
</div>
</div>
</div>
<div className="bg-purple-50 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Clock className="h-4 w-4 text-purple-600" />
<span className="text-sm font-medium"></span>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-xs text-gray-600"></span>
<span className="text-sm font-bold">{platformData.overview.lastUpdate}</span>
</div>
<div className="flex justify-between">
<span className="text-xs text-gray-600"></span>
<span className="text-sm font-bold text-green-600">{platformData.overview.syncStatus}</span>
</div>
<div className="flex justify-between">
<span className="text-xs text-gray-600"></span>
<span className="text-sm font-bold"></span>
</div>
</div>
</div>
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
{/* 数据质量监控 */}
<Collapsible
open={expandedSections.quality}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, quality: open }))}
>
<Card className="border-none shadow-md">
<CollapsibleTrigger className="w-full">
<CardHeader className="bg-gradient-to-r from-orange-50 to-red-50 border-b hover:from-orange-100 hover:to-red-100 transition-colors cursor-pointer py-3 md:py-4">
<div className="flex justify-between items-center">
<div className="flex items-center gap-3">
<CheckCircle className="h-4 w-4 md:h-5 md:w-5 text-orange-600" />
<div className="text-left">
<CardTitle className="text-base md:text-lg"></CardTitle>
<CardDescription className="text-xs md:text-sm"></CardDescription>
</div>
</div>
{expandedSections.quality ? (
<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">
{Object.entries(platformData.dataQuality).map(([key, value], index) => {
const labels = {
completeness: "完整性",
accuracy: "准确性",
consistency: "一致性",
timeliness: "及时性",
validity: "有效性",
uniqueness: "唯一性",
}
const colors = [
"bg-blue-50 text-blue-600",
"bg-green-50 text-green-600",
"bg-purple-50 text-purple-600",
"bg-orange-50 text-orange-600",
"bg-pink-50 text-pink-600",
"bg-indigo-50 text-indigo-600",
]
return (
<div key={key} className={`p-4 rounded-lg ${colors[index]}`}>
<div className="text-center">
<div className="text-2xl font-bold mb-1">{value}%</div>
<div className="text-sm">{labels[key as keyof typeof labels]}</div>
<div className="mt-2">
<Progress value={value} className="h-1.5" />
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="p-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="bg-blue-50 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Database className="h-4 w-4 text-blue-600" />
<span className="text-sm font-medium"></span>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-xs text-gray-600"></span>
<span className="text-sm font-bold">{platformData.integrationStats.totalIntegrations}</span>
</div>
<div className="flex justify-between">
<span className="text-xs text-gray-600"></span>
<span className="text-sm font-bold">{platformData.integrationStats.activeIntegrations}</span>
</div>
<div className="flex justify-between">
<span className="text-xs text-gray-600"></span>
<span className="text-sm font-bold">{platformData.integrationStats.pendingIntegrations}</span>
</div>
</div>
</div>
)
})}
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
<div className="bg-green-50 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<CheckCircle className="h-4 w-4 text-green-600" />
<span className="text-sm font-medium"></span>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-xs text-gray-600"></span>
<span className="text-sm font-bold">{platformData.integrationStats.successRate}%</span>
</div>
<div className="flex justify-between">
<span className="text-xs text-gray-600"></span>
<span className="text-sm font-bold">{platformData.integrationStats.avgSyncTime}</span>
</div>
<div className="flex justify-between">
<span className="text-xs text-gray-600"></span>
<span className="text-sm font-bold">{platformData.integrationStats.failedIntegrations}</span>
</div>
</div>
</div>
<div className="bg-purple-50 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Clock className="h-4 w-4 text-purple-600" />
<span className="text-sm font-medium"></span>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-xs text-gray-600"></span>
<span className="text-sm font-bold">{platformData.overview.lastUpdate}</span>
</div>
<div className="flex justify-between">
<span className="text-xs text-gray-600"></span>
<span className="text-sm font-bold text-green-600">{platformData.overview.syncStatus}</span>
</div>
<div className="flex justify-between">
<span className="text-xs text-gray-600"></span>
<span className="text-sm font-bold"></span>
</div>
</div>
</div>
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
</TabsContent>
<TabsContent value="quality" className="space-y-6">
{/* 数据质量监控 */}
<Collapsible
open={expandedSections.quality}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, quality: open }))}
>
<Card className="border-none shadow-md">
<CollapsibleTrigger className="w-full">
<CardHeader className="bg-gradient-to-r from-orange-50 to-red-50 border-b hover:from-orange-100 hover:to-red-100 transition-colors cursor-pointer py-3 md:py-4">
<div className="flex justify-between items-center">
<div className="flex items-center gap-3">
<CheckCircle className="h-4 w-4 md:h-5 md:w-5 text-orange-600" />
<div className="text-left">
<CardTitle className="text-base md:text-lg"></CardTitle>
<CardDescription className="text-xs md:text-sm"></CardDescription>
</div>
</div>
{expandedSections.quality ? (
<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">
{Object.entries(platformData.dataQuality).map(([key, value], index) => {
const labels = {
completeness: "完整性",
accuracy: "准确性",
consistency: "一致性",
timeliness: "及时性",
validity: "有效性",
uniqueness: "唯一性",
}
const colors = [
"bg-blue-50 text-blue-600",
"bg-green-50 text-green-600",
"bg-purple-50 text-purple-600",
"bg-orange-50 text-orange-600",
"bg-pink-50 text-pink-600",
"bg-indigo-50 text-indigo-600",
]
return (
<div key={key} className={`p-4 rounded-lg ${colors[index]}`}>
<div className="text-center">
<div className="text-2xl font-bold mb-1">{value}%</div>
<div className="text-sm">{labels[key as keyof typeof labels]}</div>
<div className="mt-2">
<Progress value={value} className="h-1.5" />
</div>
</div>
</div>
)
})}
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
</TabsContent>
<TabsContent value="ai-models" className="space-y-6">
{/* AI 模型管理 */}
<AIAnalysisTools />
</TabsContent>
</Tabs>
</div>
)
}

View File

@@ -6,8 +6,8 @@ import ClientLayout from "./ClientLayout"
const inter = Inter({ subsets: ["latin"] })
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "卡若数据资产中台",
description: "基于IMEI、手机号的用户数据资产管理平台",
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 {
Database,
Users,
Target,
Brain,
Search,
BarChart3,
TrendingUp,
RefreshCw,
Download,
ChevronRight,
Activity,
Zap,
UserCheck,
Layers,
Tag,
} from "lucide-react"
import Link from "next/link"
@@ -31,24 +33,24 @@ export default function HomePage() {
sources: 8,
lastUpdate: "2分钟前",
},
userDiscovery: {
userPool: {
totalUsers: 125678,
activeUsers: 45678,
newUsers: 1234,
engagement: "87.5%",
growth: "+8.2%",
},
userPortrait: {
totalTags: 342,
activeTags: 287,
coverage: "95.8%",
growth: "+5.3%",
},
userValuation: {
avgValue: 3248,
highValue: 12456,
growth: "+15.8%",
upgradeRate: "12.5%",
},
aiAnalysis: {
insights: 156,
anomalies: 3,
predictions: 89,
accuracy: "94.2%",
},
}
// 实时活动数据
@@ -56,21 +58,21 @@ export default function HomePage() {
{
id: 1,
type: "data_sync",
message: "电商平台数据同步完成",
message: "IMEI数据同步完成",
time: "刚刚",
status: "success",
},
{
id: 2,
type: "user_discovery",
message: "发现新用户行为模式",
type: "user_pool",
message: "新增用户池分群",
time: "2分钟前",
status: "info",
},
{
id: 3,
type: "ai_analysis",
message: "AI检测到异常流量",
type: "user_portrait",
message: "标签规则自动执行",
time: "5分钟前",
status: "warning",
},
@@ -87,12 +89,12 @@ export default function HomePage() {
switch (type) {
case "data_sync":
return <Database className="h-4 w-4" />
case "user_discovery":
return <Search className="h-4 w-4" />
case "user_valuation":
case "user_pool":
return <Users className="h-4 w-4" />
case "user_portrait":
return <Target className="h-4 w-4" />
case "ai_analysis":
return <Brain className="h-4 w-4" />
case "user_valuation":
return <TrendingUp className="h-4 w-4" />
default:
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>
<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 className="flex gap-2">
<Select value={timeRange} onValueChange={setTimeRange}>
@@ -178,34 +180,34 @@ export default function HomePage() {
</Card>
</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">
<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 gap-2">
<Search className="h-5 w-5 text-green-600" />
<CardTitle className="text-base"></CardTitle>
<Users className="h-5 w-5 text-green-600" />
<CardTitle className="text-base"></CardTitle>
</div>
</div>
<CardDescription className="text-xs"></CardDescription>
<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.userPool.totalUsers.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<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 className="flex justify-between items-center">
<span className="text-sm text-gray-600"></span>
<span className="font-bold">{todayStats.userDiscovery.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>
<span className="font-bold">{todayStats.userPool.newUsers.toLocaleString()}</span>
</div>
<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" />
</div>
</div>
@@ -213,13 +215,48 @@ export default function HomePage() {
</Card>
</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">
<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 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>
</div>
</div>
@@ -237,46 +274,10 @@ export default function HomePage() {
</div>
<div className="flex justify-between items-center">
<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 className="flex items-center justify-between mt-3">
<span className="text-xs text-purple-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>
<span className="text-xs text-orange-600"> {todayStats.userValuation.growth}</span>
<ChevronRight className="h-4 w-4 text-gray-400 group-hover:text-orange-600 transition-colors" />
</div>
</div>
@@ -291,7 +292,7 @@ export default function HomePage() {
<Card className="lg:col-span-2 border-none shadow-md">
<CardHeader>
<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>
<CardDescription></CardDescription>
@@ -307,12 +308,12 @@ export default function HomePage() {
<div className="text-xs text-gray-600"></div>
</div>
<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-xs text-gray-600">AI洞察</div>
<div className="text-2xl font-bold text-purple-600">342</div>
<div className="text-xs text-gray-600"></div>
</div>
<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-xs text-gray-600"></div>
<div className="text-2xl font-bold text-orange-600">95.8%</div>
<div className="text-xs text-gray-600"></div>
</div>
</div>
<div className="mt-4 space-y-3">
@@ -325,7 +326,7 @@ export default function HomePage() {
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span>AI模型训练</span>
<span></span>
<span>92%</span>
</div>
<Progress value={92} className="h-2" />
@@ -381,22 +382,22 @@ export default function HomePage() {
<span className="text-xs"></span>
</Button>
</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">
<Search className="h-5 w-5" />
<span className="text-xs"></span>
<UserCheck className="h-5 w-5" />
<span className="text-xs"></span>
</Button>
</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">
<Target className="h-5 w-5" />
<span className="text-xs"></span>
<Layers className="h-5 w-5" />
<span className="text-xs"></span>
</Button>
</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">
<Brain className="h-5 w-5" />
<span className="text-xs">AI预测</span>
<Tag className="h-5 w-5" />
<span className="text-xs"></span>
</Button>
</Link>
</div>

View File

@@ -257,7 +257,7 @@ export default function ApiPage() {
</div>
<div className="flex items-center space-x-2 text-gray-500">
<Clock className="w-4 h-4" />
<span>: {task.nextExecutionTime}</span>
<span><EFBFBD><EFBFBD>: {task.nextExecutionTime}</span>
</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,
Eye,
Settings,
Smartphone,
MapPin,
Layers,
UserCheck,
} from "lucide-react"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
export default function UserPortrait() {
export default function UserPortraitPage() {
const router = useRouter()
const [searchQuery, setSearchQuery] = useState("")
const [selectedCategory, setSelectedCategory] = useState("all")
const [selectedPool, setSelectedPool] = useState("all")
const [selectedDevice, setSelectedDevice] = useState("all") // 新增设备筛选
const [selectedWechatDialog, setSelectedWechatDialog] = useState<{ isOpen: boolean; accounts: any[] }>({
isOpen: false,
accounts: [],
})
const [expandedSections, setExpandedSections] = useState({
userKeywords: true,
tagManagement: false,
userList: true,
overview: true, // 用户池概览
userManagement: 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 = [
{
id: "1",
name: "张明华",
company: "科技创新有限公司",
imei: "860123456789012",
phone: "13800138001",
avatar: "/placeholder.svg?height=40&width=40",
tags: ["高价值", "技术决策者", "活跃用户", "iOS用户", "企业级客户"],
category: "企业服务",
@@ -55,11 +85,18 @@ export default function UserPortrait() {
{ id: "wx1", 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",
name: "李雨婷",
company: "数字营销公司",
imei: "860987654321098",
phone: "13900139001",
avatar: "/placeholder.svg?height=40&width=40",
tags: ["中高价值", "产品经理", "内容创作者", "Android用户"],
category: "内容营销",
@@ -70,11 +107,18 @@ export default function UserPortrait() {
wechatAccounts: [
{ id: "wx3", name: "李雨婷", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号"] },
],
deviceBrand: "华为",
deviceModel: "Mate 50 Pro",
location: "上海市",
platform: "抖音",
status: "活跃",
},
{
id: "3",
name: "王建国",
company: "软件开发工作室",
imei: "860456789123456",
phone: "13700137001",
avatar: "/placeholder.svg?height=40&width=40",
tags: ["高价值", "技术专家", "开源贡献者", "MacOS用户"],
category: "技术社区",
@@ -85,26 +129,40 @@ export default function UserPortrait() {
wechatAccounts: [
{ id: "wx4", name: "王建国", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号", "技术交流"] },
],
deviceBrand: "小米",
deviceModel: "小米13 Ultra",
location: "深圳市",
platform: "微信",
status: "在线",
},
{
id: "4",
name: "刘思琪",
company: "数据分析咨询",
imei: "860789123456789",
phone: "13600136001",
avatar: "/placeholder.svg?height=40&width=40",
tags: ["中价值", "运营专家", "数据分析师", "Windows用户"],
category: "数据分析",
pool: "域池",
pool: "域池",
lastActive: "今天 11:15",
value: 18900,
rfmScore: 72,
wechatAccounts: [
{ id: "wx5", name: "刘思琪", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号"] },
],
deviceBrand: "OPPO",
deviceModel: "Find X6 Pro",
location: "广州市",
platform: "小红书",
status: "活跃",
},
{
id: "5",
name: "陈浩然",
company: "云计算服务商",
imei: "860321654987321",
phone: "13500135001",
avatar: "/placeholder.svg?height=40&width=40",
tags: ["高价值", "架构师", "技术领导", "Linux用户"],
category: "技术架构",
@@ -116,72 +174,17 @@ export default function UserPortrait() {
{ id: "wx6", name: "陈浩然", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号", "技术分享"] },
{ id: "wx7", name: "陈浩然个人号", avatar: "/placeholder.svg?height=40&width=40", tags: ["个人账号"] },
],
},
{
id: "6",
name: "周欣妍",
company: "用户体验设计",
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: ["工作账号"] },
],
deviceBrand: "vivo",
deviceModel: "X90 Pro+",
location: "杭州市",
platform: "微信",
status: "在线",
},
]
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 = {
totalTags: 342,
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 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 matchesSearch =
user.name.includes(searchQuery) ||
user.company.includes(searchQuery) ||
user.imei.includes(searchQuery) ||
user.phone.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 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) => {
switch (value) {
case "":
const getStatusColor = (status: string) => {
switch (status) {
case "在线":
return "bg-green-100 text-green-800"
case "中高":
case "活跃":
return "bg-blue-100 text-blue-800"
case "":
return "bg-yellow-100 text-yellow-800"
case "离线":
return "bg-gray-100 text-gray-800"
default:
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>
<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 className="flex gap-2">
<Button size="sm" className="flex-1 md:flex-none">
@@ -269,23 +314,23 @@ export default function UserPortrait() {
</div>
</div>
{/* 流量关键词管理 */}
{/* 用户池概览 (从原 user-pool/page.tsx 整合) */}
<Collapsible
open={expandedSections.userKeywords}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, userKeywords: open }))}
open={expandedSections.overview}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, overview: open }))}
>
<Card className="border shadow-sm">
<Card className="border-none shadow-md">
<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">
<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 justify-between items-center">
<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">
<CardTitle className="text-base md:text-lg"></CardTitle>
<CardDescription className="text-xs md:text-sm"></CardDescription>
<CardTitle className="text-base md:text-lg"></CardTitle>
<CardDescription className="text-xs md:text-sm"></CardDescription>
</div>
</div>
{expandedSections.userKeywords ? (
{expandedSections.overview ? (
<ChevronDown 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>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="pt-0 px-4 md:px-6">
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-4 mb-4">
<div className="bg-blue-50 p-2 md:p-3 rounded-lg">
<p className="text-xs md:text-sm font-medium text-blue-600"></p>
<p className="text-lg md:text-2xl font-bold text-blue-900">{keywordStats.totalKeywords}</p>
<CardContent className="p-4">
<div className="grid grid-cols-2 md:grid-cols-5 gap-3 md:gap-4 mb-6">
<div className="bg-blue-50 p-3 rounded-lg text-center">
<div className="text-lg md:text-2xl font-bold text-blue-600">
{poolData.overview.totalUsers.toLocaleString()}
</div>
<div className="text-xs text-blue-700"></div>
</div>
<div className="bg-green-50 p-2 md:p-3 rounded-lg">
<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 className="bg-green-50 p-3 rounded-lg text-center">
<div className="text-lg md:text-2xl font-bold text-green-600">
{poolData.overview.activeUsers.toLocaleString()}
</div>
<div className="text-xs text-green-700"></div>
</div>
<div className="bg-purple-50 p-2 md:p-3 rounded-lg">
<p className="text-xs md:text-sm font-medium text-purple-600"></p>
<p className="text-lg md:text-2xl font-bold text-purple-900">{keywordStats.coverage}%</p>
<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-2 md:p-3 rounded-lg">
<Button
variant="outline"
size="sm"
className="w-full h-full bg-transparent text-xs md:text-sm"
onClick={() => router.push("/user-portrait/keywords")}
>
<Settings className="mr-1 md:mr-2 h-3 w-3 md:h-4 md:w-4" />
</Button>
<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="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 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="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="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
variant="outline"
size="sm"
className="text-xs bg-transparent"
onClick={() => handleUserClick(user.id)}
>
<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">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>
))}
@@ -339,7 +703,7 @@ export default function UserPortrait() {
</Card>
</Collapsible>
{/* 标签管理 */}
{/* 标签管理 (原用户画像的标签管理,现在是用户画像的一部分) */}
<Collapsible
open={expandedSections.tagManagement}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, tagManagement: open }))}
@@ -403,223 +767,6 @@ export default function UserPortrait() {
</Card>
</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
open={selectedWechatDialog.isOpen}