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>
809 lines
34 KiB
TypeScript
809 lines
34 KiB
TypeScript
"use client"
|
|
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Avatar } from "@/components/ui/avatar"
|
|
import { useState } from "react"
|
|
import { useRouter } from "next/navigation"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
import {
|
|
Search,
|
|
UserPlus,
|
|
Download,
|
|
MessageCircle,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
Tag,
|
|
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 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({
|
|
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: "企业服务",
|
|
pool: "私域池",
|
|
lastActive: "今天 14:30",
|
|
value: 28500,
|
|
rfmScore: 95,
|
|
wechatAccounts: [
|
|
{ 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: "内容营销",
|
|
pool: "公域池",
|
|
lastActive: "今天 13:45",
|
|
value: 15800,
|
|
rfmScore: 78,
|
|
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: "技术社区",
|
|
pool: "私域池",
|
|
lastActive: "今天 12:20",
|
|
value: 32400,
|
|
rfmScore: 89,
|
|
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: "公域池",
|
|
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: "技术架构",
|
|
pool: "私域池",
|
|
lastActive: "今天 10:30",
|
|
value: 45600,
|
|
rfmScore: 93,
|
|
wechatAccounts: [
|
|
{ id: "wx6", name: "陈浩然", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号", "技术分享"] },
|
|
{ id: "wx7", name: "陈浩然个人号", avatar: "/placeholder.svg?height=40&width=40", tags: ["个人账号"] },
|
|
],
|
|
deviceBrand: "vivo",
|
|
deviceModel: "X90 Pro+",
|
|
location: "杭州市",
|
|
platform: "微信",
|
|
status: "在线",
|
|
},
|
|
]
|
|
|
|
const [users, setUsers] = useState(mockUsers)
|
|
|
|
// 标签管理统计 (保持不变)
|
|
const tagStats = {
|
|
totalTags: 342,
|
|
activeTags: 287,
|
|
categories: 12,
|
|
coverage: 95.8,
|
|
topTags: [
|
|
{ tag: "高价值", count: 156, color: "bg-green-100 text-green-800" },
|
|
{ tag: "活跃用户", count: 134, color: "bg-blue-100 text-blue-800" },
|
|
{ tag: "技术专家", count: 98, color: "bg-purple-100 text-purple-800" },
|
|
{ tag: "决策者", count: 87, color: "bg-orange-100 text-orange-800" },
|
|
{ tag: "企业客户", count: 76, color: "bg-pink-100 text-pink-800" },
|
|
],
|
|
}
|
|
|
|
// 用户分析统计 (保持不变,但现在是用户画像的一部分)
|
|
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.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 && matchesDevice
|
|
})
|
|
|
|
// 处理用户点击
|
|
const handleUserClick = (userId: string) => {
|
|
router.push(`/user-portrait/${userId}`)
|
|
}
|
|
|
|
// 处理微信账号点击
|
|
const handleWechatClick = (userId: string) => {
|
|
const user = users.find((u) => u.id === userId)
|
|
if (user) {
|
|
setSelectedWechatDialog({
|
|
isOpen: true,
|
|
accounts: user.wechatAccounts,
|
|
})
|
|
}
|
|
}
|
|
|
|
const toggleSection = (section: string) => {
|
|
setExpandedSections((prev) => ({
|
|
...prev,
|
|
[section]: !prev[section],
|
|
}))
|
|
}
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case "在线":
|
|
return "bg-green-100 text-green-800"
|
|
case "活跃":
|
|
return "bg-blue-100 text-blue-800"
|
|
case "离线":
|
|
return "bg-gray-100 text-gray-800"
|
|
default:
|
|
return "bg-gray-100 text-gray-800"
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto py-4 md:py-6 space-y-4 md:space-y-6">
|
|
<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">基于IMEI、手机号的用户标签画像分析</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button size="sm" className="flex-1 md:flex-none">
|
|
<UserPlus className="mr-2 h-4 w-4" />
|
|
导入用户
|
|
</Button>
|
|
<Button variant="outline" size="sm" className="flex-1 md:flex-none bg-transparent">
|
|
<Download className="mr-2 h-4 w-4" />
|
|
导出
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 用户池概览 (从原 user-pool/page.tsx 整合) */}
|
|
<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">
|
|
<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>
|
|
</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-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-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-3 rounded-lg text-center">
|
|
<div className="text-lg md:text-2xl font-bold text-purple-600">
|
|
{poolData.overview.newUsers.toLocaleString()}
|
|
</div>
|
|
<div className="text-xs text-purple-700">新增用户</div>
|
|
</div>
|
|
<div className="bg-orange-50 p-3 rounded-lg text-center">
|
|
<div className="text-lg md:text-2xl font-bold text-orange-600">
|
|
{poolData.overview.privatePool.toLocaleString()}
|
|
</div>
|
|
<div className="text-xs text-orange-700">私域池</div>
|
|
</div>
|
|
<div className="bg-pink-50 p-3 rounded-lg text-center">
|
|
<div className="text-lg md:text-2xl font-bold text-pink-600">
|
|
{poolData.overview.publicPool.toLocaleString()}
|
|
</div>
|
|
<div className="text-xs text-pink-700">公域池</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="bg-gray-50 p-4 rounded-lg">
|
|
<h3 className="font-medium mb-3 flex items-center">
|
|
<Smartphone className="h-4 w-4 mr-2 text-gray-600" />
|
|
设备分布
|
|
</h3>
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between">
|
|
<span className="text-sm">iOS设备</span>
|
|
<span className="font-bold">{poolData.deviceStats.ios.toLocaleString()}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-sm">Android设备</span>
|
|
<span className="font-bold">{poolData.deviceStats.android.toLocaleString()}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-sm">其他设备</span>
|
|
<span className="font-bold">{poolData.deviceStats.other.toLocaleString()}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-gray-50 p-4 rounded-lg">
|
|
<h3 className="font-medium mb-3 flex items-center">
|
|
<MapPin className="h-4 w-4 mr-2 text-gray-600" />
|
|
地域分布
|
|
</h3>
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between">
|
|
<span className="text-sm">一线城市</span>
|
|
<span className="font-bold">{poolData.locationStats.tier1.toLocaleString()}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-sm">二线城市</span>
|
|
<span className="font-bold">{poolData.locationStats.tier2.toLocaleString()}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-sm">三线及以下</span>
|
|
<span className="font-bold">{poolData.locationStats.tier3.toLocaleString()}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-sm">海外用户</span>
|
|
<span className="font-bold">{poolData.locationStats.overseas.toLocaleString()}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</CollapsibleContent>
|
|
</Card>
|
|
</Collapsible>
|
|
|
|
{/* 用户管理 (原用户池的用户管理,现在是用户画像的一部分) */}
|
|
<Collapsible
|
|
open={expandedSections.userManagement}
|
|
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, userManagement: open }))}
|
|
>
|
|
<Card className="border-none shadow-md">
|
|
<CollapsibleTrigger className="w-full">
|
|
<CardHeader className="bg-gradient-to-r from-green-50 to-emerald-50 border-b hover:from-green-100 hover:to-emerald-100 transition-colors cursor-pointer py-3 md:py-4">
|
|
<div className="flex justify-between items-center">
|
|
<div className="flex items-center gap-3">
|
|
<UserCheck className="h-4 w-4 md:h-5 md:w-5 text-green-600" />
|
|
<div className="text-left">
|
|
<CardTitle className="text-base md:text-lg">用户管理</CardTitle>
|
|
<CardDescription className="text-xs md:text-sm">
|
|
IMEI、手机号管理 ({filteredUsers.length})
|
|
</CardDescription>
|
|
</div>
|
|
</div>
|
|
{expandedSections.userManagement ? (
|
|
<ChevronDown className="h-4 w-4 md:h-5 md:w-5" />
|
|
) : (
|
|
<ChevronRight className="h-4 w-4 md:h-5 md:w-5" />
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<CardContent className="p-4">
|
|
{/* 搜索和筛选 */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 md:gap-4 mb-4 md:mb-6">
|
|
<div className="relative">
|
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
type="search"
|
|
placeholder="搜索用户、IMEI、手机号、设备、位置..."
|
|
className="pl-8 text-sm"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<Select value={selectedPool} onValueChange={setSelectedPool}>
|
|
<SelectTrigger className="text-sm">
|
|
<SelectValue placeholder="选择流量池" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{pools.map((pool) => (
|
|
<SelectItem key={pool} value={pool}>
|
|
{pool === "all" ? "所有流量池" : pool}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Select value={selectedDevice} onValueChange={setSelectedDevice}>
|
|
<SelectTrigger className="text-sm">
|
|
<SelectValue placeholder="选择设备品牌" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{devices.map((device) => (
|
|
<SelectItem key={device} value={device}>
|
|
{device === "all" ? "所有设备" : device}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 移动端卡片布局 */}
|
|
<div className="space-y-3 md:hidden">
|
|
{filteredUsers.map((user) => (
|
|
<div key={user.id} className="bg-white border rounded-lg p-3 space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Avatar className="h-8 w-8">
|
|
<img src={user.avatar || "/placeholder.svg"} alt={user.name} />
|
|
</Avatar>
|
|
<div>
|
|
<p className="font-medium text-sm">{user.name}</p>
|
|
<p className="text-xs text-gray-500">{user.lastActive}</p>
|
|
</div>
|
|
</div>
|
|
<Badge className={getStatusColor(user.status)}>{user.status}</Badge>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
<div>
|
|
<p className="text-gray-500">IMEI</p>
|
|
<p className="font-mono">{user.imei.slice(-6)}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500">手机号</p>
|
|
<p className="font-medium">{user.phone}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500">设备</p>
|
|
<p className="font-medium">{user.deviceBrand}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500">位置</p>
|
|
<p className="font-medium">{user.location}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between text-xs">
|
|
<div>
|
|
<p className="text-gray-500">流量池</p>
|
|
<Badge variant="outline" className="text-xs">
|
|
{user.pool}
|
|
</Badge>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500">平台</p>
|
|
<Badge variant="outline" className="text-xs">
|
|
{user.platform}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-1">
|
|
{user.tags.map((tag, index) => (
|
|
<Badge key={index} variant="secondary" className="text-xs">
|
|
{tag}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Button variant="ghost" size="sm" className="text-xs">
|
|
<MessageCircle className="h-3 w-3 mr-1" />
|
|
联系
|
|
</Button>
|
|
<Button
|
|
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>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</CollapsibleContent>
|
|
</Card>
|
|
</Collapsible>
|
|
|
|
{/* 标签管理 (原用户画像的标签管理,现在是用户画像的一部分) */}
|
|
<Collapsible
|
|
open={expandedSections.tagManagement}
|
|
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, tagManagement: 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">
|
|
<Tag 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.tagManagement ? (
|
|
<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-2 md:grid-cols-4 gap-3 md:gap-4 mb-4">
|
|
<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">{tagStats.totalTags}</p>
|
|
</div>
|
|
<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">{tagStats.activeTags}</p>
|
|
</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">{tagStats.coverage}%</p>
|
|
</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/tags")}
|
|
>
|
|
<Settings className="mr-1 md:mr-2 h-3 w-3 md:h-4 md:w-4" />
|
|
管理
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-2">
|
|
{tagStats.topTags.map((item, index) => (
|
|
<div key={index} className="flex items-center justify-between p-2 bg-gray-50 rounded">
|
|
<Badge className={`${item.color} text-xs`}>{item.tag}</Badge>
|
|
<span className="text-xs font-bold ml-2">{item.count}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</CollapsibleContent>
|
|
</Card>
|
|
</Collapsible>
|
|
|
|
{/* 微信账号对话框 */}
|
|
<Dialog
|
|
open={selectedWechatDialog.isOpen}
|
|
onOpenChange={(open) => setSelectedWechatDialog({ ...selectedWechatDialog, isOpen: open })}
|
|
>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>关联微信账号</DialogTitle>
|
|
<DialogDescription>该用户关联的微信账号列表</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-4">
|
|
{selectedWechatDialog.accounts.map((account) => (
|
|
<div key={account.id} className="flex items-center justify-between p-2 border rounded-md">
|
|
<div className="flex items-center gap-3">
|
|
<Avatar className="h-10 w-10">
|
|
<img src={account.avatar || "/placeholder.svg"} alt={account.name} />
|
|
</Avatar>
|
|
<div>
|
|
<p className="font-medium">{account.name}</p>
|
|
<div className="flex gap-1 mt-1">
|
|
{account.tags.map((tag, index) => (
|
|
<Badge key={index} variant="secondary" className="text-xs">
|
|
{tag}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Button variant="outline" size="sm">
|
|
查看详情
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
)
|
|
}
|