chore: 以本地为准,上传全部并替换 GitHub
This commit is contained in:
@@ -1,345 +1,634 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Target, Plus, Search, Users, Download, Share2, Trash2, Filter, BarChart3, PieChart } from "lucide-react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import {
|
||||
Target,
|
||||
Users,
|
||||
ChevronRight,
|
||||
ChevronLeft,
|
||||
Check,
|
||||
QrCode,
|
||||
Globe,
|
||||
MessageCircle,
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
Save,
|
||||
ArrowLeft,
|
||||
Eye,
|
||||
Package,
|
||||
Star,
|
||||
TrendingUp,
|
||||
MapPin,
|
||||
Activity,
|
||||
Zap,
|
||||
Shield,
|
||||
Crown,
|
||||
Sparkles,
|
||||
Filter,
|
||||
Download,
|
||||
Search,
|
||||
} from "lucide-react"
|
||||
|
||||
interface CrowdPack {
|
||||
// 流量池分类
|
||||
interface PoolCategory {
|
||||
id: string
|
||||
name: string
|
||||
icon: any
|
||||
color: string
|
||||
description: string
|
||||
userCount: number
|
||||
rules: string[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
status: "active" | "expired" | "computing"
|
||||
distribution: { label: string; value: number; color: string }[]
|
||||
pools: Pool[]
|
||||
}
|
||||
|
||||
const MOCK_CROWDS: CrowdPack[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "高价值活跃用户",
|
||||
description: "价值评分>80 AND 近7天活跃",
|
||||
userCount: 125678901,
|
||||
rules: ["价值评分 > 80", "最后登录 < 7天", "消费金额 > 1000"],
|
||||
createdAt: "2025-12-10",
|
||||
updatedAt: "2025-12-12",
|
||||
status: "active",
|
||||
distribution: [
|
||||
{ label: "25-35岁", value: 45, color: "bg-blue-500" },
|
||||
{ label: "35-45岁", value: 30, color: "bg-green-500" },
|
||||
{ label: "其他", value: 25, color: "bg-gray-300" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "流失预警用户",
|
||||
description: "流失风险>0.7 AND 近30天无消费",
|
||||
userCount: 34567890,
|
||||
rules: ["流失风险 > 0.7", "最后消费 > 30天", "活跃度等级 = 低"],
|
||||
createdAt: "2025-12-08",
|
||||
updatedAt: "2025-12-12",
|
||||
status: "active",
|
||||
distribution: [
|
||||
{ label: "一线城市", value: 35, color: "bg-purple-500" },
|
||||
{ label: "二线城市", value: 40, color: "bg-orange-500" },
|
||||
{ label: "其他", value: 25, color: "bg-gray-300" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "新注册潜力用户",
|
||||
description: "注册<30天 AND 有首购",
|
||||
userCount: 8901234,
|
||||
rules: ["注册时间 < 30天", "首购完成 = 是", "消费金额 > 100"],
|
||||
createdAt: "2025-12-05",
|
||||
updatedAt: "2025-12-11",
|
||||
status: "computing",
|
||||
distribution: [
|
||||
{ label: "女性", value: 55, color: "bg-pink-500" },
|
||||
{ label: "男性", value: 45, color: "bg-blue-500" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "品牌忠诚用户",
|
||||
description: "复购率>50% AND 品牌偏好明确",
|
||||
userCount: 56789012,
|
||||
rules: ["复购率 > 50%", "品牌偏好 != 空", "消费频次 > 3次/月"],
|
||||
createdAt: "2025-12-01",
|
||||
updatedAt: "2025-12-10",
|
||||
status: "active",
|
||||
distribution: [
|
||||
{ label: "高端消费", value: 60, color: "bg-yellow-500" },
|
||||
{ label: "中端消费", value: 30, color: "bg-green-500" },
|
||||
{ label: "其他", value: 10, color: "bg-gray-300" },
|
||||
],
|
||||
},
|
||||
]
|
||||
// 流量池/标签类型
|
||||
interface Pool {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
count: number
|
||||
avgScore?: number
|
||||
description?: string
|
||||
hotLevel?: number // 热度 1-5
|
||||
}
|
||||
|
||||
const AVAILABLE_CONDITIONS = [
|
||||
{ category: "用户属性", options: ["性别", "年龄段", "城市等级", "注册时间"] },
|
||||
{ category: "行为标签", options: ["活跃度等级", "消费等级", "复购率", "最后登录", "最后消费"] },
|
||||
{ category: "价值标签", options: ["价值评分", "RFM评分", "CLV预测值"] },
|
||||
{ category: "风险标签", options: ["流失风险", "欺诈风险"] },
|
||||
{ category: "AI标签", options: ["品牌偏好", "价格敏感度", "内容偏好"] },
|
||||
]
|
||||
// 用户类型
|
||||
interface PoolUser {
|
||||
id: string
|
||||
phone: string
|
||||
name: string
|
||||
score: number
|
||||
level: string
|
||||
tags: string[]
|
||||
province?: string
|
||||
city?: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export default function CrowdSelectionPage() {
|
||||
const [crowds, setCrowds] = useState<CrowdPack[]>(MOCK_CROWDS)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [selectedConditions, setSelectedConditions] = useState<string[]>([])
|
||||
// 流量池分类定义
|
||||
const POOL_CATEGORIES: { [key: string]: { name: string; icon: any; color: string; bgColor: string; description: string } } = {
|
||||
value: {
|
||||
name: '💎 价值分层',
|
||||
icon: Crown,
|
||||
color: 'text-purple-600',
|
||||
bgColor: 'bg-purple-50',
|
||||
description: '按用户估值分数划分的流量池'
|
||||
},
|
||||
behavior: {
|
||||
name: '⚡ 行为活跃',
|
||||
icon: Activity,
|
||||
color: 'text-blue-600',
|
||||
bgColor: 'bg-blue-50',
|
||||
description: '按用户活跃程度划分'
|
||||
},
|
||||
region: {
|
||||
name: '📍 地域分布',
|
||||
icon: MapPin,
|
||||
color: 'text-green-600',
|
||||
bgColor: 'bg-green-50',
|
||||
description: '按用户地理位置划分'
|
||||
},
|
||||
source: {
|
||||
name: '📥 数据来源',
|
||||
icon: QrCode,
|
||||
color: 'text-orange-600',
|
||||
bgColor: 'bg-orange-50',
|
||||
description: '按数据接入渠道划分'
|
||||
},
|
||||
lifecycle: {
|
||||
name: '🔄 生命周期',
|
||||
icon: TrendingUp,
|
||||
color: 'text-indigo-600',
|
||||
bgColor: 'bg-indigo-50',
|
||||
description: '按用户生命周期阶段划分'
|
||||
},
|
||||
}
|
||||
|
||||
export default function TrafficPoolPage() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [activeCategory, setActiveCategory] = useState('value')
|
||||
const [pools, setPools] = useState<Pool[]>([])
|
||||
const [categorizedPools, setCategorizedPools] = useState<{ [key: string]: Pool[] }>({})
|
||||
const [selectedPool, setSelectedPool] = useState<Pool | null>(null)
|
||||
const [users, setUsers] = useState<PoolUser[]>([])
|
||||
const [totalUsers, setTotalUsers] = useState(0)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [showUserDialog, setShowUserDialog] = useState(false)
|
||||
const [showSaveDialog, setShowSaveDialog] = useState(false)
|
||||
const [packageName, setPackageName] = useState('')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
// 加载流量池数据
|
||||
useEffect(() => {
|
||||
loadPools()
|
||||
}, [])
|
||||
|
||||
const loadPools = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
// 获取所有流量池数据
|
||||
const [poolsRes, tagsRes, packagesRes] = await Promise.all([
|
||||
fetch('/api/traffic-packages?action=pools'),
|
||||
fetch('/api/tags'),
|
||||
fetch('/api/traffic-packages')
|
||||
])
|
||||
|
||||
const poolsData = await poolsRes.json()
|
||||
const tagsData = await tagsRes.json()
|
||||
const packagesData = await packagesRes.json()
|
||||
|
||||
const allPools: Pool[] = []
|
||||
const categorized: { [key: string]: Pool[] } = {
|
||||
value: [],
|
||||
behavior: [],
|
||||
region: [],
|
||||
source: [],
|
||||
lifecycle: []
|
||||
}
|
||||
|
||||
// 处理流量池数据(价值分层)
|
||||
if (poolsData.success && poolsData.pools) {
|
||||
poolsData.pools.forEach((p: any) => {
|
||||
const pool: Pool = {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
type: 'value',
|
||||
count: p.count,
|
||||
avgScore: p.avgScore,
|
||||
description: `估值分 ${p.minScore || 0} - ${p.maxScore || '∞'}`,
|
||||
hotLevel: p.id === 'diamond' ? 5 : p.id === 'gold' ? 4 : p.id === 'silver' ? 3 : 2
|
||||
}
|
||||
allPools.push(pool)
|
||||
categorized.value.push(pool)
|
||||
})
|
||||
}
|
||||
|
||||
// 处理标签数据
|
||||
if (tagsData.success && tagsData.tags) {
|
||||
tagsData.tags.forEach((t: any) => {
|
||||
const pool: Pool = {
|
||||
id: `tag_${t.id}`,
|
||||
name: t.name,
|
||||
type: t.category,
|
||||
count: t.userCount,
|
||||
description: t.description,
|
||||
hotLevel: Math.min(5, Math.floor(t.userCount / 100000000) + 1)
|
||||
}
|
||||
allPools.push(pool)
|
||||
|
||||
// 按分类归类
|
||||
if (t.category === 'value') {
|
||||
categorized.value.push(pool)
|
||||
} else if (t.category === 'behavior') {
|
||||
categorized.behavior.push(pool)
|
||||
} else if (t.category === 'region') {
|
||||
categorized.region.push(pool)
|
||||
} else if (t.category === 'channel') {
|
||||
categorized.source.push(pool)
|
||||
} else if (t.category === 'lifecycle') {
|
||||
categorized.lifecycle.push(pool)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 添加数据来源流量池
|
||||
const sourceProjects = [
|
||||
{ name: '存客宝用户', count: 216000, type: 'source', description: 'CRM系统用户数据' },
|
||||
{ name: '点了码用户', count: 1000, type: 'source', description: '扫码营销用户' },
|
||||
{ name: '微博用户', count: 216000000, type: 'source', description: '微博平台用户' },
|
||||
{ name: 'QQ社交用户', count: 705000000, type: 'source', description: 'QQ平台用户' },
|
||||
{ name: '京东用户', count: 142000000, type: 'source', description: '京东电商用户' },
|
||||
{ name: '顺丰用户', count: 39200000, type: 'source', description: '快递地址数据' },
|
||||
]
|
||||
|
||||
sourceProjects.forEach((s, i) => {
|
||||
const pool: Pool = {
|
||||
id: `source_${i}`,
|
||||
name: s.name,
|
||||
type: 'source',
|
||||
count: s.count,
|
||||
description: s.description,
|
||||
hotLevel: Math.min(5, Math.floor(s.count / 100000000) + 1)
|
||||
}
|
||||
allPools.push(pool)
|
||||
categorized.source.push(pool)
|
||||
})
|
||||
|
||||
setPools(allPools)
|
||||
setCategorizedPools(categorized)
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载流量池失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载池内用户
|
||||
const loadPoolUsers = async (pool: Pool, page: number = 1) => {
|
||||
setLoading(true)
|
||||
setSelectedPool(pool)
|
||||
setCurrentPage(page)
|
||||
setShowUserDialog(true)
|
||||
|
||||
try {
|
||||
// 根据流量池类型构建查询
|
||||
let apiUrl = `/api/portrait?action=users&page=${page}&limit=20`
|
||||
|
||||
// 如果是价值分层流量池,添加分数筛选
|
||||
if (pool.type === 'value' && pool.id.startsWith('pool_')) {
|
||||
// 根据池名确定分数范围
|
||||
const scoreRanges: { [key: string]: { min?: number; max?: number } } = {
|
||||
'diamond': { min: 3000 },
|
||||
'gold': { min: 2000, max: 3000 },
|
||||
'silver': { min: 1000, max: 2000 },
|
||||
'bronze': { min: 500, max: 1000 },
|
||||
'potential': { max: 500 }
|
||||
}
|
||||
// 解析pool id
|
||||
}
|
||||
|
||||
const res = await fetch(apiUrl)
|
||||
const data = await res.json()
|
||||
|
||||
if (data.success) {
|
||||
setUsers(data.users || [])
|
||||
setTotalUsers(data.total || pool.count)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载用户失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
||||
return num.toString()
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`
|
||||
return num.toLocaleString()
|
||||
}
|
||||
|
||||
const totalUsers = crowds.reduce((sum, c) => sum + c.userCount, 0)
|
||||
// 保存为流量包
|
||||
const savePackage = async () => {
|
||||
if (!selectedPool) return
|
||||
setLoading(true)
|
||||
try {
|
||||
await fetch('/api/traffic-packages', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'create',
|
||||
name: packageName || selectedPool.name,
|
||||
poolId: selectedPool.id,
|
||||
userCount: totalUsers
|
||||
})
|
||||
})
|
||||
alert('流量包保存成功')
|
||||
setShowSaveDialog(false)
|
||||
setPackageName('')
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取热度星星
|
||||
const getHotStars = (level: number = 1) => {
|
||||
return Array(5).fill(0).map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`h-3 w-3 ${i < level ? 'text-yellow-400 fill-yellow-400' : 'text-gray-200'}`}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
// 筛选流量池
|
||||
const getFilteredPools = (categoryPools: Pool[]) => {
|
||||
if (!searchQuery) return categoryPools
|
||||
const query = searchQuery.toLowerCase()
|
||||
return categoryPools.filter(p =>
|
||||
p.name.toLowerCase().includes(query) ||
|
||||
p.description?.toLowerCase().includes(query)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">人群圈选</h1>
|
||||
<p className="text-gray-500 mt-1">组合条件筛选目标用户群体</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setIsCreating(!isCreating)}
|
||||
className="bg-gradient-to-r from-blue-500 to-purple-500 text-white"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
创建人群包
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Create Panel */}
|
||||
{isCreating && (
|
||||
<Card className="border-none shadow-md bg-white/80 backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">创建人群包</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">人群包名称</label>
|
||||
<Input placeholder="例如:高价值潜在流失用户" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">描述</label>
|
||||
<Input placeholder="简要描述人群特征" />
|
||||
</div>
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
|
||||
<div className="p-6 space-y-6">
|
||||
{/* 顶部标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">流量池</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">按分类管理用户流量池,查看真实用户数据</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索流量池..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 w-64 bg-white"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" onClick={loadPools}>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="text-sm font-medium text-gray-700">筛选条件</label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{AVAILABLE_CONDITIONS.map((group) => (
|
||||
<div key={group.category} className="p-4 rounded-xl bg-gray-50">
|
||||
<h4 className="font-medium text-gray-900 mb-3">{group.category}</h4>
|
||||
<div className="space-y-2">
|
||||
{group.options.map((option) => (
|
||||
<div key={option} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={option}
|
||||
checked={selectedConditions.includes(option)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedConditions([...selectedConditions, option])
|
||||
} else {
|
||||
setSelectedConditions(selectedConditions.filter((c) => c !== option))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={option} className="text-sm text-gray-600">
|
||||
{option}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-5 gap-4">
|
||||
{Object.entries(POOL_CATEGORIES).map(([key, cat]) => {
|
||||
const categoryPools = categorizedPools[key] || []
|
||||
const totalCount = categoryPools.reduce((sum, p) => sum + p.count, 0)
|
||||
const Icon = cat.icon
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={key}
|
||||
className={`border-0 shadow-sm cursor-pointer transition-all hover:shadow-md ${
|
||||
activeCategory === key ? 'ring-2 ring-purple-500' : ''
|
||||
} ${cat.bgColor}`}
|
||||
onClick={() => setActiveCategory(key)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg bg-white/80`}>
|
||||
<Icon className={`h-5 w-5 ${cat.color}`} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-gray-900">{cat.name.slice(2)}</div>
|
||||
<div className="text-xs text-gray-500">{categoryPools.length} 个池</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 pt-2 border-t border-white/50">
|
||||
<div className="text-xl font-bold text-gray-900">{formatNumber(totalCount)}</div>
|
||||
<div className="text-xs text-gray-500">总用户数</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{selectedConditions.length > 0 && (
|
||||
<div className="p-4 rounded-xl bg-blue-50">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-medium text-gray-900">已选条件</span>
|
||||
<Badge className="bg-blue-100 text-blue-700">{selectedConditions.length}个</Badge>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedConditions.map((cond) => (
|
||||
<Badge key={cond} variant="secondary" className="bg-white">
|
||||
{cond}
|
||||
<button
|
||||
onClick={() => setSelectedConditions(selectedConditions.filter((c) => c !== cond))}
|
||||
className="ml-1 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
x
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
{/* 流量池列表 */}
|
||||
<Card className="border-0 shadow-lg bg-white/90">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<span className="text-2xl">{POOL_CATEGORIES[activeCategory]?.name.slice(0, 2)}</span>
|
||||
{POOL_CATEGORIES[activeCategory]?.name.slice(2)}
|
||||
</CardTitle>
|
||||
<Badge variant="outline">{getFilteredPools(categorizedPools[activeCategory] || []).length} 个流量池</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">{POOL_CATEGORIES[activeCategory]?.description}</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading && !showUserDialog ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-purple-600" />
|
||||
<span className="ml-3 text-gray-500">加载中...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{getFilteredPools(categorizedPools[activeCategory] || []).map(pool => (
|
||||
<Card
|
||||
key={pool.id}
|
||||
className="border shadow-sm hover:shadow-lg transition-all cursor-pointer bg-white group"
|
||||
onClick={() => loadPoolUsers(pool)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{getHotStars(pool.hotLevel)}
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{pool.type === 'value' ? '价值' : pool.type === 'behavior' ? '行为' : pool.type === 'region' ? '地域' : pool.type === 'source' ? '来源' : '周期'}
|
||||
</Badge>
|
||||
</div>
|
||||
<h4 className="font-semibold text-gray-900 text-lg group-hover:text-purple-600 transition-colors">{pool.name}</h4>
|
||||
{pool.description && (
|
||||
<p className="text-xs text-gray-500 mt-1 line-clamp-1">{pool.description}</p>
|
||||
)}
|
||||
<div className="flex items-center justify-between mt-3 pt-2 border-t">
|
||||
<div>
|
||||
<div className="text-xl font-bold text-purple-600">{formatNumber(pool.count)}</div>
|
||||
<div className="text-xs text-gray-500">用户数</div>
|
||||
</div>
|
||||
{pool.avgScore && (
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-semibold text-orange-500">{Math.round(pool.avgScore)}</div>
|
||||
<div className="text-xs text-gray-500">均分</div>
|
||||
</div>
|
||||
)}
|
||||
<ChevronRight className="h-5 w-5 text-gray-300 group-hover:text-purple-500 transition-colors" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{getFilteredPools(categorizedPools[activeCategory] || []).length === 0 && (
|
||||
<div className="col-span-4 text-center py-12 text-gray-500">
|
||||
<Target className="h-12 w-12 mx-auto mb-3 opacity-30" />
|
||||
<p>该分类下暂无流量池</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button variant="outline" onClick={() => setIsCreating(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button variant="outline">预估人数</Button>
|
||||
<Button>保存人群包</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<Target className="w-4 h-4" />
|
||||
人群包数
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{crowds.length}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<Users className="w-4 h-4" />
|
||||
覆盖用户
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{formatNumber(totalUsers)}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<Filter className="w-4 h-4" />
|
||||
可用条件
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{AVAILABLE_CONDITIONS.reduce((sum, g) => sum + g.options.length, 0)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
|
||||
<BarChart3 className="w-4 h-4" />
|
||||
运算中
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{crowds.filter((c) => c.status === "computing").length}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
placeholder="搜索人群包..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 bg-white/60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Crowd List */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{crowds
|
||||
.filter((c) => c.name.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
.map((crowd) => (
|
||||
<Card key={crowd.id} className="border-none shadow-sm bg-white/60 backdrop-blur-sm hover:shadow-md">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold text-gray-900">{crowd.name}</h3>
|
||||
<Badge
|
||||
className={
|
||||
crowd.status === "active"
|
||||
? "bg-green-100 text-green-700"
|
||||
: crowd.status === "computing"
|
||||
? "bg-blue-100 text-blue-700"
|
||||
: "bg-gray-100 text-gray-700"
|
||||
}
|
||||
>
|
||||
{crowd.status === "active" ? "生效中" : crowd.status === "computing" ? "计算中" : "已过期"}
|
||||
</Badge>
|
||||
{/* 用户列表弹窗 */}
|
||||
<Dialog open={showUserDialog} onOpenChange={setShowUserDialog}>
|
||||
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Target className="h-5 w-5 text-purple-600" />
|
||||
{selectedPool?.name} - 用户列表
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{selectedPool?.description} · 共 {formatNumber(totalUsers)} 个用户
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Card className="border-0 shadow bg-gradient-to-r from-purple-50 to-blue-50">
|
||||
<CardContent className="p-4">
|
||||
<div className="text-sm text-gray-500">总用户数</div>
|
||||
<div className="text-3xl font-bold text-purple-600">{formatNumber(totalUsers)}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-0 shadow bg-white/80">
|
||||
<CardContent className="p-4">
|
||||
<div className="text-sm text-gray-500">热度等级</div>
|
||||
<div className="flex items-center gap-1 mt-1">
|
||||
{getHotStars(selectedPool?.hotLevel || 1)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">{crowd.description}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-gray-900">{formatNumber(crowd.userCount)}</div>
|
||||
<div className="text-xs text-gray-500">用户数</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-0 shadow bg-white/80">
|
||||
<CardContent className="p-4">
|
||||
<div className="text-sm text-gray-500">流量池类型</div>
|
||||
<Badge className="mt-1">{selectedPool?.type}</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mb-4">
|
||||
<div className="text-sm text-gray-600">
|
||||
<span className="font-medium">筛选规则:</span>
|
||||
{crowd.rules.map((rule, i) => (
|
||||
<Badge key={i} variant="secondary" className="ml-1 text-xs">
|
||||
{rule}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
{/* 用户表格 */}
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-purple-600" />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>手机号</TableHead>
|
||||
<TableHead>姓名</TableHead>
|
||||
<TableHead>估值分</TableHead>
|
||||
<TableHead>等级</TableHead>
|
||||
<TableHead>地区</TableHead>
|
||||
<TableHead>标签</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map(user => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell className="font-mono">{user.phone}</TableCell>
|
||||
<TableCell>{user.name}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={user.score > 2000 ? 'default' : 'secondary'}>
|
||||
{user.score || '-'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{user.level || '-'}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-500">
|
||||
{user.province} {user.city}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1 max-w-xs">
|
||||
{(user.tags || []).slice(0, 2).map((tag, i) => (
|
||||
<Badge key={i} variant="outline" className="text-xs">{tag}</Badge>
|
||||
))}
|
||||
{(user.tags || []).length > 2 && (
|
||||
<Badge variant="outline" className="text-xs">+{user.tags.length - 2}</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
|
||||
{/* 分页和操作 */}
|
||||
<div className="flex items-center justify-between pt-4 border-t">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={currentPage === 1 || loading}
|
||||
onClick={() => selectedPool && loadPoolUsers(selectedPool, currentPage - 1)}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
上一页
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={users.length < 20 || loading}
|
||||
onClick={() => selectedPool && loadPoolUsers(selectedPool, currentPage + 1)}
|
||||
>
|
||||
下一页
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => setShowUserDialog(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
导出数据
|
||||
</Button>
|
||||
<Button onClick={() => { setShowUserDialog(false); setShowSaveDialog(true); }}>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
保存为流量包
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Distribution */}
|
||||
<div className="mb-4">
|
||||
<div className="flex h-3 rounded-full overflow-hidden">
|
||||
{crowd.distribution.map((d, i) => (
|
||||
<div key={i} className={`${d.color}`} style={{ width: `${d.value}%` }} title={d.label} />
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between mt-2">
|
||||
{crowd.distribution.map((d, i) => (
|
||||
<div key={i} className="flex items-center gap-1 text-xs text-gray-500">
|
||||
<div className={`w-2 h-2 rounded-full ${d.color}`} />
|
||||
<span>
|
||||
{d.label} {d.value}%
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* 保存流量包弹窗 */}
|
||||
<Dialog open={showSaveDialog} onOpenChange={setShowSaveDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>保存为流量包</DialogTitle>
|
||||
<DialogDescription>将当前流量池保存为可复用的流量包</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>流量包名称</Label>
|
||||
<Input
|
||||
placeholder={selectedPool?.name}
|
||||
value={packageName}
|
||||
onChange={(e) => setPackageName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg bg-purple-50 space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">来源流量池</span>
|
||||
<span className="font-medium">{selectedPool?.name}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-4 border-t border-gray-100">
|
||||
<div className="text-xs text-gray-500">更新于 {crowd.updatedAt}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
<Download className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
<Share2 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
<PieChart className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0 text-red-500">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">流量池类型</span>
|
||||
<Badge>{selectedPool?.type}</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">用户数量</span>
|
||||
<span className="text-2xl font-bold text-purple-600">{formatNumber(totalUsers)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowSaveDialog(false)}>取消</Button>
|
||||
<Button onClick={savePackage} disabled={loading}>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : <Package className="h-4 w-4 mr-2" />}
|
||||
保存流量包
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,12 +1,272 @@
|
||||
"use client"
|
||||
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import Link from "next/link"
|
||||
import {
|
||||
Tags,
|
||||
Users,
|
||||
Target,
|
||||
TrendingUp,
|
||||
ArrowUpRight,
|
||||
Diamond,
|
||||
Award,
|
||||
Medal,
|
||||
Shield,
|
||||
Loader2,
|
||||
Star,
|
||||
Activity,
|
||||
MapPin,
|
||||
Clock,
|
||||
Heart,
|
||||
} from "lucide-react"
|
||||
|
||||
// 流量池配置
|
||||
const TRAFFIC_POOLS = [
|
||||
{ id: 'diamond', name: '钻石池', icon: Diamond, color: 'from-purple-500 to-pink-500', minScore: 80, desc: 'RFM≥80 顶级价值用户' },
|
||||
{ id: 'gold', name: '黄金池', icon: Award, color: 'from-yellow-500 to-orange-500', minScore: 60, maxScore: 80, desc: 'RFM 60-80 高价值用户' },
|
||||
{ id: 'silver', name: '白银池', icon: Medal, color: 'from-gray-400 to-gray-500', minScore: 40, maxScore: 60, desc: 'RFM 40-60 中等价值用户' },
|
||||
{ id: 'bronze', name: '青铜池', icon: Shield, color: 'from-orange-400 to-orange-600', minScore: 20, maxScore: 40, desc: 'RFM 20-40 待培育用户' },
|
||||
{ id: 'potential', name: '潜力池', icon: TrendingUp, color: 'from-blue-400 to-cyan-500', maxScore: 20, desc: 'RFM<20 潜在用户' },
|
||||
]
|
||||
|
||||
// 标签分类
|
||||
const TAG_CATEGORIES = [
|
||||
{ id: 'value', name: '价值标签', icon: Star, color: 'bg-yellow-100 text-yellow-700', desc: 'S/A/B/C/D等级', count: 0 },
|
||||
{ id: 'behavior', name: '行为标签', icon: Activity, color: 'bg-blue-100 text-blue-700', desc: '活跃度、互动频次', count: 0 },
|
||||
{ id: 'region', name: '地域标签', icon: MapPin, color: 'bg-green-100 text-green-700', desc: '省份、城市、区域', count: 0 },
|
||||
{ id: 'lifecycle', name: '生命周期', icon: Clock, color: 'bg-purple-100 text-purple-700', desc: '新用户、活跃、沉睡', count: 0 },
|
||||
{ id: 'preference', name: '偏好标签', icon: Heart, color: 'bg-red-100 text-red-700', desc: '兴趣、消费偏好', count: 0 },
|
||||
]
|
||||
|
||||
export default function TagPortraitPage() {
|
||||
const router = useRouter()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [pools, setPools] = useState<any[]>([])
|
||||
const [categories, setCategories] = useState(TAG_CATEGORIES)
|
||||
const [stats, setStats] = useState({
|
||||
totalTags: 0,
|
||||
totalUsers: 0,
|
||||
avgCoverage: 0
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
router.replace("/tag-portrait/tags")
|
||||
}, [router])
|
||||
return null
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
// 加载流量池数据
|
||||
const poolsRes = await fetch('/api/traffic-packages?action=pools')
|
||||
const poolsData = await poolsRes.json()
|
||||
if (poolsData.success) {
|
||||
setPools(poolsData.pools)
|
||||
}
|
||||
|
||||
// 加载标签分类数据
|
||||
const tagsRes = await fetch('/api/tags?action=categories')
|
||||
const tagsData = await tagsRes.json()
|
||||
if (tagsData.success) {
|
||||
setCategories(tagsData.categories.map((c: any) => ({
|
||||
...TAG_CATEGORIES.find(tc => tc.id === c.id) || {},
|
||||
...c
|
||||
})))
|
||||
setStats({
|
||||
totalTags: tagsData.categories.reduce((sum: number, c: any) => sum + (c.tagCount || 0), 0),
|
||||
totalUsers: tagsData.categories.reduce((sum: number, c: any) => sum + (c.userCount || 0), 0),
|
||||
avgCoverage: 82.3
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`
|
||||
return num.toLocaleString()
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20 flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
||||
<span className="ml-3 text-gray-500">加载标签画像...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
|
||||
<div className="p-6 space-y-6">
|
||||
{/* 顶部标题 */}
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">标签画像</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
用户资产数字化标签体系 · 流量池分层运营
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计概览 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">标签总数</p>
|
||||
<p className="text-3xl font-bold text-gray-900">{stats.totalTags}</p>
|
||||
</div>
|
||||
<div className="w-14 h-14 rounded-2xl bg-blue-100 flex items-center justify-center">
|
||||
<Tags className="h-7 w-7 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">覆盖用户</p>
|
||||
<p className="text-3xl font-bold text-gray-900">{formatNumber(stats.totalUsers)}</p>
|
||||
</div>
|
||||
<div className="w-14 h-14 rounded-2xl bg-green-100 flex items-center justify-center">
|
||||
<Users className="h-7 w-7 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">覆盖率</p>
|
||||
<p className="text-3xl font-bold text-gray-900">{stats.avgCoverage}%</p>
|
||||
</div>
|
||||
<div className="w-14 h-14 rounded-2xl bg-purple-100 flex items-center justify-center">
|
||||
<Target className="h-7 w-7 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={stats.avgCoverage} className="h-2 mt-3" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 流量池体系 */}
|
||||
<Card className="border-none shadow-lg bg-white/80 backdrop-blur">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Target className="h-5 w-5 text-purple-500" />
|
||||
流量池体系
|
||||
<Badge variant="outline" className="ml-2">RFM分层</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
{TRAFFIC_POOLS.map((pool) => {
|
||||
const poolData = pools.find(p => p.id === pool.id)
|
||||
const PoolIcon = pool.icon
|
||||
|
||||
return (
|
||||
<Link key={pool.id} href={`/tag-portrait/crowd?pool=${pool.id}`}>
|
||||
<Card className={`cursor-pointer hover:shadow-lg transition-all duration-300 border-0 overflow-hidden group`}>
|
||||
<div className={`h-2 bg-gradient-to-r ${pool.color}`} />
|
||||
<CardContent className="p-4">
|
||||
<div className={`w-12 h-12 rounded-xl bg-gradient-to-r ${pool.color} flex items-center justify-center mb-3 group-hover:scale-110 transition-transform`}>
|
||||
<PoolIcon className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<h3 className="font-bold text-gray-900 mb-1">{pool.name}</h3>
|
||||
<p className="text-xs text-gray-500 mb-3">{pool.desc}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xl font-bold text-gray-900">
|
||||
{formatNumber(poolData?.count || 0)}
|
||||
</span>
|
||||
<ArrowUpRight className="w-4 h-4 text-gray-400 group-hover:text-purple-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-4 text-center">
|
||||
基于 RFM(最近购买、购买频率、购买金额)评分模型自动分配流量池
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 标签分类 */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">标签分类</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||
{categories.map((category) => {
|
||||
const CategoryIcon = category.icon || Tags
|
||||
return (
|
||||
<Link key={category.id} href={`/tag-portrait/tags?category=${category.id}`}>
|
||||
<Card className="cursor-pointer hover:shadow-lg transition-all duration-300 border-0 shadow-sm bg-white/70 group hover:-translate-y-1">
|
||||
<CardContent className="p-5">
|
||||
<div className={`w-12 h-12 rounded-xl ${category.color.split(' ')[0]} flex items-center justify-center mb-3 group-hover:scale-110 transition-transform`}>
|
||||
<CategoryIcon className={`h-6 w-6 ${category.color.split(' ')[1]}`} />
|
||||
</div>
|
||||
<h3 className="font-bold text-gray-900 mb-1">{category.name}</h3>
|
||||
<p className="text-xs text-gray-500 mb-3">{category.desc}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge variant="secondary">{category.tagCount || category.count || 0} 个标签</Badge>
|
||||
<ArrowUpRight className="w-4 h-4 text-gray-400 group-hover:text-blue-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 快捷入口 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Link href="/tag-portrait/tags">
|
||||
<Card className="cursor-pointer hover:shadow-lg transition-all duration-300 border-0 shadow-sm bg-gradient-to-r from-blue-500 to-indigo-500 text-white group">
|
||||
<CardContent className="p-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold mb-1">标签管理</h3>
|
||||
<p className="text-blue-100 text-sm">查看和管理所有标签</p>
|
||||
</div>
|
||||
<Tags className="h-10 w-10 text-blue-200 group-hover:scale-110 transition-transform" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
<Link href="/tag-portrait/portrait">
|
||||
<Card className="cursor-pointer hover:shadow-lg transition-all duration-300 border-0 shadow-sm bg-gradient-to-r from-green-500 to-emerald-500 text-white group">
|
||||
<CardContent className="p-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold mb-1">用户画像</h3>
|
||||
<p className="text-green-100 text-sm">查询单个用户完整画像</p>
|
||||
</div>
|
||||
<Users className="h-10 w-10 text-green-200 group-hover:scale-110 transition-transform" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
<Link href="/tag-portrait/crowd">
|
||||
<Card className="cursor-pointer hover:shadow-lg transition-all duration-300 border-0 shadow-sm bg-gradient-to-r from-purple-500 to-pink-500 text-white group">
|
||||
<CardContent className="p-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold mb-1">人群圈选</h3>
|
||||
<p className="text-purple-100 text-sm">按条件筛选目标人群</p>
|
||||
</div>
|
||||
<Target className="h-10 w-10 text-purple-200 group-hover:scale-110 transition-transform" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user