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}

View File

@@ -26,9 +26,19 @@ import {
Copy,
Check,
Zap,
Plus,
Download,
} 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 { Textarea } from "@/components/ui/textarea"
interface AIAnalysisTask {
id: string
@@ -297,8 +307,8 @@ export function AIAnalysisTools() {
description: "为特定用户分群生成营销策略",
type: "strategy-generation",
parameters: {
targetSegment: "所有用户",
campaignGoal: "提高转化率",
targetSegment: "流失风险用户",
campaignGoal: "提高留存率",
budgetConstraint: "中等",
channelPreference: ["短信", "应用内推送", "邮件"],
},
@@ -664,9 +674,7 @@ export function AIAnalysisTools() {
<Plus className="h-6 w-6 text-muted-foreground" />
</div>
<h3 className="text-lg font-medium mb-2"></h3>
<p className="text-sm text-muted-foreground text-center mb-4">
</p>
<p className="text-sm text-muted-foreground text-center mb-4"></p>
<Button variant="outline"></Button>
</CardContent>
</Card>
@@ -719,9 +727,7 @@ export function AIAnalysisTools() {
<SelectItem value="10">10</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
AI分析任务数量
</p>
<p className="text-sm text-muted-foreground">AI分析任务数量</p>
</div>
<Separator />
@@ -920,4 +926,231 @@ export function AIAnalysisTools() {
</TableHeader>
<TableBody>
{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"
import { useState, useEffect } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { useState } from "react"
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 { Plus, Trash2 } from "lucide-react"
import { Trash2, Plus, Save } from "lucide-react"
interface RuleCondition {
field: string
operator: string
value: string
logicalOperator?: "AND" | "OR"
}
interface TagRule {
@@ -24,151 +24,263 @@ interface TagRule {
addTags: string[]
removeTags: string[]
}
enabled: boolean
}
/**
* 简易标签规则编辑器
* 只实现基本字段编辑与本地状态保存,后端持久化可在后续迭代中接入
*/
export function RuleEditor({ ruleId }: { ruleId: string | null }) {
const [rule, setRule] = useState<TagRule | null>(null)
interface RuleEditorProps {
rule?: TagRule
onSave: (rule: TagRule) => void
onCancel: () => void
}
// ❗️此处仅示例:真实项目应替换为 API 请求
useEffect(() => {
if (!ruleId) {
setRule(null)
return
}
// 模拟异步加载
setTimeout(() => {
setRule({
id: ruleId,
name: "示例规则",
description: "这是一个示例规则,用于演示编辑器",
conditions: [
{ field: "orderAmount", operator: ">", value: "500", logicalOperator: "AND" },
{ field: "loginDays", operator: "<", value: "30" },
],
actions: { addTags: ["高价值用户"], removeTags: [] },
export function RuleEditor({ rule, onSave, onCancel }: RuleEditorProps) {
const [formData, setFormData] = useState<TagRule>(
rule || {
id: "",
name: "",
description: "",
conditions: [{ field: "", operator: "", value: "" }],
actions: {
addTags: [],
removeTags: [],
},
enabled: true,
},
)
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()],
},
})
}, 300)
}, [ruleId])
if (!ruleId) {
return (
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
</Card>
)
if (type === "addTags") {
setNewAddTag("")
} else {
setNewRemoveTag("")
}
}
}
if (!rule) {
return (
<Card>
<CardHeader>
<CardTitle>...</CardTitle>
</CardHeader>
</Card>
)
const removeTag = (type: "addTags" | "removeTags", index: number) => {
setFormData({
...formData,
actions: {
...formData.actions,
[type]: formData.actions[type].filter((_, i) => i !== index),
},
})
}
// === 内部更新函数 ===
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)
const handleSave = () => {
if (formData.name && formData.conditions.length > 0) {
onSave(formData)
}
}
return (
<Card>
<CardHeader>
<CardTitle>{rule.name}</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* 基本信息 */}
<div className="space-y-2">
<label className="block text-sm font-medium"></label>
<Input value={rule.name} onChange={(e) => updateRule({ name: e.target.value })} />
</div>
<div className="space-y-2">
<label className="block text-sm font-medium"></label>
<Textarea value={rule.description} onChange={(e) => updateRule({ description: e.target.value })} />
</div>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</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>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{formData.conditions.map((condition, index) => (
<div key={index} className="flex items-center space-x-2 p-3 border rounded-lg">
<Select value={condition.field} onValueChange={(value) => updateCondition(index, "field", value)}>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="选择字段" />
</SelectTrigger>
<SelectContent>
{fieldOptions.map((option) => (
<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
placeholder="字段"
value={c.field}
onChange={(e) => {
const next = [...rule.conditions]
next[idx].field = e.target.value
updateRule({ conditions: next })
}}
className="w-1/3"
value={condition.value}
onChange={(e) => updateCondition(index, "value", e.target.value)}
placeholder="输入值"
className="flex-1"
/>
<Input
placeholder="运算符"
value={c.operator}
onChange={(e) => {
const next = [...rule.conditions]
next[idx].operator = e.target.value
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="删除条件">
<Button
variant="outline"
size="icon"
onClick={() => removeCondition(index)}
disabled={formData.conditions.length === 1}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
<Button variant="outline" onClick={addCondition}>
<Plus className="mr-2 h-4 w-4" />
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</CardContent>
</Card>
{/* 动作(标签) */}
<div className="space-y-2">
<label className="block text-sm font-medium"></label>
<div className="flex flex-wrap gap-2">
{rule.actions.addTags.map((tag, i) => (
<Badge key={i}>{tag}</Badge>
))}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<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 className="text-right">
<Button onClick={saveRule}></Button>
</div>
</CardContent>
</Card>
<div>
<Label></Label>
<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>
</CardContent>
</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",
"private": true,
"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融合及界面优化
* 项目名称: "神射手" 用户资产数字中台
* 目标: 构建一个统一、灵活、高性能的用户数据中心,汇聚来自多个异构源系统的数据,形成可信的 360 度用户视图。
* 核心价值: 打破数据孤岛,沉淀统一、全面的用户资产,提升数据驱动能力。
### 1. 数据库连接方式调整 (模拟 MongoDB)
## 迭代记录
**背景:** 根据产品需求文档 (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)
* 数据查询 (Querying)
* 数据服务 (API Distribution)
* 身份识别与合并 (Identity Resolution)
* 分析支持
* **易用性改进**
* 将“设备管理”从二级菜单提升为一级菜单。
**架构影响:**
* **前端模拟:** 当前的数据库操作仍为前端模拟,不涉及真实的后端连接。
* **后端准备:** 此调整为未来后端使用 MongoDB 提供了清晰的数据模型和接口模拟参考。在实际后端开发中,需要使用 MongoDB 驱动程序(如 `mongodb``mongoose`)来实现真实的数据库操作。
## 开发架构
### 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
* QueryService
* IdentityService
### 3. 代码清理
### API
* **无用代码移除:** 审查并移除了在本次优化过程中发现的不再使用的变量、函数、组件引用或样式,以减少代码冗余,提高可读性和维护性。
* **链接和内容显示:** 确保所有显示的链接和内容都是当前功能所需且可用的,避免显示不相关或无效的信息。
* /api/users
* /api/users/{userId}
* /api/ingest
* /api/users/{userId}/tags
* /api/users/{userId}/activity
**本次开发完成百分比:** 界面优化与数据库连接调整 100%