refactor: restructure navigation and module layout

Reorganize navigation and module structure based on new requirements.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
v0
2026-01-31 04:32:36 +00:00
parent 22e725887a
commit b17b488f8e
105 changed files with 20530 additions and 3622 deletions

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View File

@@ -0,0 +1,346 @@
"use client"
import { useState } from "react"
import { Target, Plus, Search, Users, Download, Share2, Trash2, Filter, BarChart3, PieChart } from "lucide-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"
interface CrowdPack {
id: string
name: string
description: string
userCount: number
rules: string[]
createdAt: string
updatedAt: string
status: "active" | "expired" | "computing"
distribution: { label: string; value: number; color: string }[]
}
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" },
],
},
]
const AVAILABLE_CONDITIONS = [
{ category: "用户属性", options: ["性别", "年龄段", "城市等级", "注册时间"] },
{ category: "行为标签", options: ["活跃度等级", "消费等级", "复购率", "最后登录", "最后消费"] },
{ category: "价值标签", options: ["价值评分", "RFM评分", "CLV预测值"] },
{ category: "风险标签", options: ["流失风险", "欺诈风险"] },
{ category: "AI标签", options: ["品牌偏好", "价格敏感度", "内容偏好"] },
]
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 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()
}
const totalUsers = crowds.reduce((sum, c) => sum + c.userCount, 0)
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>
<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>
</div>
))}
</div>
</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>
</div>
)}
<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>
</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>
<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>
</div>
{/* 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>
</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>
</CardContent>
</Card>
))}
</div>
</div>
)
}

12
app/tag-portrait/page.tsx Normal file
View File

@@ -0,0 +1,12 @@
"use client"
import { useRouter } from "next/navigation"
import { useEffect } from "react"
export default function TagPortraitPage() {
const router = useRouter()
useEffect(() => {
router.replace("/tag-portrait/tags")
}, [router])
return null
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View File

@@ -0,0 +1,273 @@
"use client"
import { useState } from "react"
import { Search, Tags, Activity, TrendingUp, Calendar, MapPin, Phone, Mail, Star } from "lucide-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 { Progress } from "@/components/ui/progress"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
interface UserProfile {
id: string
name: string
avatar: string
phone: string
email: string
city: string
registerDate: string
lastActive: string
tags: { name: string; type: string }[]
valueScore: number
valueLevel: string
rfmScores: { r: number; f: number; m: number }
behaviors: { name: string; count: number; trend: string }[]
timeline: { date: string; event: string; type: string }[]
}
const MOCK_USER: UserProfile = {
id: "U10086",
name: "张明华",
avatar: "/professional-chinese-man.jpg",
phone: "138****5678",
email: "zhang****@qq.com",
city: "上海市",
registerDate: "2023-05-15",
lastActive: "2025-12-12 14:30:00",
tags: [
{ name: "高价值用户", type: "value" },
{ name: "高复购高客单", type: "behavior" },
{ name: "品牌忠诚", type: "ai" },
{ name: "25-35岁", type: "basic" },
{ name: "一线城市", type: "basic" },
{ name: "活跃用户", type: "behavior" },
{ name: "低流失风险", type: "model" },
{ name: "科技爱好者", type: "ai" },
],
valueScore: 92,
valueLevel: "S",
rfmScores: { r: 95, f: 88, m: 90 },
behaviors: [
{ name: "登录次数", count: 156, trend: "+12%" },
{ name: "浏览商品", count: 892, trend: "+8%" },
{ name: "下单次数", count: 45, trend: "+15%" },
{ name: "分享次数", count: 28, trend: "+5%" },
],
timeline: [
{ date: "2025-12-12 14:30", event: "浏览商品: iPhone 16 Pro", type: "browse" },
{ date: "2025-12-12 10:15", event: "完成订单: #ORD20251212001", type: "order" },
{ date: "2025-12-11 18:20", event: "添加收藏: MacBook Pro", type: "favorite" },
{ date: "2025-12-11 09:00", event: "登录App", type: "login" },
{ date: "2025-12-10 20:30", event: "分享商品到微信", type: "share" },
],
}
const TAG_COLORS: Record<string, string> = {
value: "bg-yellow-100 text-yellow-700",
behavior: "bg-green-100 text-green-700",
ai: "bg-purple-100 text-purple-700",
basic: "bg-blue-100 text-blue-700",
model: "bg-orange-100 text-orange-700",
}
const EVENT_COLORS: Record<string, string> = {
browse: "bg-blue-500",
order: "bg-green-500",
favorite: "bg-yellow-500",
login: "bg-gray-500",
share: "bg-purple-500",
}
export default function UserPortraitPage() {
const [searchQuery, setSearchQuery] = useState("")
const [selectedUser, setSelectedUser] = useState<UserProfile>(MOCK_USER)
const handleSearch = () => {
// 模拟搜索
console.log("Searching:", searchQuery)
}
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>
</div>
{/* Search */}
<div className="flex gap-4">
<div className="relative flex-1 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="输入用户ID或手机号搜索..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && handleSearch()}
className="pl-10 bg-white/60"
/>
</div>
<Button onClick={handleSearch}></Button>
</div>
{/* User Profile Card */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left: Basic Info */}
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
<CardContent className="p-6">
<div className="flex items-center gap-4 mb-6">
<div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center text-white text-2xl font-bold">
{selectedUser.name.charAt(0)}
</div>
<div>
<h2 className="text-xl font-bold text-gray-900">{selectedUser.name}</h2>
<p className="text-sm text-gray-500">ID: {selectedUser.id}</p>
<div className="flex items-center gap-2 mt-2">
<Badge className="bg-yellow-100 text-yellow-700">
<Star className="w-3 h-3 mr-1" />
{selectedUser.valueLevel}
</Badge>
</div>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center gap-3 text-sm">
<Phone className="w-4 h-4 text-gray-400" />
<span className="text-gray-600">{selectedUser.phone}</span>
</div>
<div className="flex items-center gap-3 text-sm">
<Mail className="w-4 h-4 text-gray-400" />
<span className="text-gray-600">{selectedUser.email}</span>
</div>
<div className="flex items-center gap-3 text-sm">
<MapPin className="w-4 h-4 text-gray-400" />
<span className="text-gray-600">{selectedUser.city}</span>
</div>
<div className="flex items-center gap-3 text-sm">
<Calendar className="w-4 h-4 text-gray-400" />
<span className="text-gray-600"> {selectedUser.registerDate}</span>
</div>
<div className="flex items-center gap-3 text-sm">
<Activity className="w-4 h-4 text-gray-400" />
<span className="text-gray-600"> {selectedUser.lastActive}</span>
</div>
</div>
{/* Value Score */}
<div className="mt-6 p-4 rounded-xl bg-gradient-to-br from-yellow-50 to-orange-50">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-600"></span>
<span className="text-2xl font-bold text-orange-600">{selectedUser.valueScore}</span>
</div>
<Progress value={selectedUser.valueScore} className="h-2" />
</div>
</CardContent>
</Card>
{/* Middle: Tags & RFM */}
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
<CardHeader className="pb-2">
<CardTitle className="text-lg flex items-center gap-2">
<Tags className="w-5 h-5" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2 mb-6">
{selectedUser.tags.map((tag, i) => (
<Badge key={i} className={TAG_COLORS[tag.type]}>
{tag.name}
</Badge>
))}
</div>
<div className="border-t border-gray-100 pt-4">
<h4 className="font-medium text-gray-900 mb-4">RFM模型评分</h4>
<div className="space-y-4">
<div>
<div className="flex items-center justify-between text-sm mb-1">
<span className="text-gray-600">R - </span>
<span className="font-medium">{selectedUser.rfmScores.r}</span>
</div>
<Progress value={selectedUser.rfmScores.r} className="h-2" />
</div>
<div>
<div className="flex items-center justify-between text-sm mb-1">
<span className="text-gray-600">F - </span>
<span className="font-medium">{selectedUser.rfmScores.f}</span>
</div>
<Progress value={selectedUser.rfmScores.f} className="h-2" />
</div>
<div>
<div className="flex items-center justify-between text-sm mb-1">
<span className="text-gray-600">M - </span>
<span className="font-medium">{selectedUser.rfmScores.m}</span>
</div>
<Progress value={selectedUser.rfmScores.m} className="h-2" />
</div>
</div>
</div>
</CardContent>
</Card>
{/* Right: Behaviors & Timeline */}
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
<CardContent className="p-0">
<Tabs defaultValue="behaviors" className="w-full">
<TabsList className="w-full rounded-none border-b bg-transparent p-0">
<TabsTrigger
value="behaviors"
className="flex-1 rounded-none border-b-2 border-transparent data-[state=active]:border-blue-500"
>
</TabsTrigger>
<TabsTrigger
value="timeline"
className="flex-1 rounded-none border-b-2 border-transparent data-[state=active]:border-blue-500"
>
</TabsTrigger>
</TabsList>
<TabsContent value="behaviors" className="p-4 space-y-4">
{selectedUser.behaviors.map((behavior, i) => (
<div key={i} className="flex items-center justify-between p-3 rounded-xl bg-gray-50">
<span className="text-gray-600">{behavior.name}</span>
<div className="flex items-center gap-3">
<span className="font-bold text-gray-900">{behavior.count}</span>
<Badge className="bg-green-100 text-green-700 text-xs">
<TrendingUp className="w-3 h-3 mr-1" />
{behavior.trend}
</Badge>
</div>
</div>
))}
</TabsContent>
<TabsContent value="timeline" className="p-4">
<div className="space-y-4">
{selectedUser.timeline.map((event, i) => (
<div key={i} className="flex gap-3">
<div className="flex flex-col items-center">
<div className={`w-3 h-3 rounded-full ${EVENT_COLORS[event.type]}`} />
{i < selectedUser.timeline.length - 1 && <div className="w-0.5 h-full bg-gray-200 mt-1" />}
</div>
<div className="flex-1 pb-4">
<p className="text-sm text-gray-900">{event.event}</p>
<p className="text-xs text-gray-500 mt-1">{event.date}</p>
</div>
</div>
))}
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View File

@@ -0,0 +1,491 @@
"use client"
import { useState } from "react"
import {
Tags,
Plus,
Search,
ChevronRight,
ChevronDown,
Users,
Cpu,
Brain,
FileText,
CheckCircle,
AlertTriangle,
Settings,
} from "lucide-react"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
interface TagCategory {
id: string
name: string
icon: any
color: string
tags: Tag[]
expanded: boolean
}
interface Tag {
id: string
name: string
type: "basic" | "rule" | "model" | "ai"
status: "online" | "testing" | "offline"
coverage: number
userCount: number
rule?: string
lastUpdate: string
}
const TAG_TYPE_CONFIG = {
basic: { label: "基础标签", color: "bg-blue-100 text-blue-700", icon: FileText },
rule: { label: "规则标签", color: "bg-green-100 text-green-700", icon: Settings },
model: { label: "模型标签", color: "bg-purple-100 text-purple-700", icon: Cpu },
ai: { label: "AI标签", color: "bg-orange-100 text-orange-700", icon: Brain },
}
const STATUS_CONFIG = {
online: { label: "已上线", color: "bg-green-100 text-green-700" },
testing: { label: "测试中", color: "bg-yellow-100 text-yellow-700" },
offline: { label: "已下线", color: "bg-gray-100 text-gray-700" },
}
const INITIAL_CATEGORIES: TagCategory[] = [
{
id: "1",
name: "人口属性",
icon: Users,
color: "text-blue-600",
expanded: true,
tags: [
{
id: "t1",
name: "性别",
type: "basic",
status: "online",
coverage: 99.5,
userCount: 4012345678,
lastUpdate: "2025-12-12",
},
{
id: "t2",
name: "年龄段",
type: "rule",
status: "online",
coverage: 95.2,
userCount: 3825678901,
rule: "根据身份证计算",
lastUpdate: "2025-12-12",
},
{
id: "t3",
name: "城市等级",
type: "rule",
status: "online",
coverage: 88.7,
userCount: 3567890123,
rule: "一线/新一线/二线/三线/其他",
lastUpdate: "2025-12-11",
},
{
id: "t4",
name: "职业预测",
type: "model",
status: "testing",
coverage: 72.3,
userCount: 2908765432,
lastUpdate: "2025-12-10",
},
],
},
{
id: "2",
name: "消费行为",
icon: Tags,
color: "text-green-600",
expanded: true,
tags: [
{
id: "t5",
name: "消费等级",
type: "rule",
status: "online",
coverage: 82.5,
userCount: 3318765432,
rule: "近90天消费金额分档",
lastUpdate: "2025-12-12",
},
{
id: "t6",
name: "高复购高客单",
type: "rule",
status: "online",
coverage: 15.2,
userCount: 611543210,
rule: "近30天消费次数>3 AND 客单价>500",
lastUpdate: "2025-12-12",
},
{
id: "t7",
name: "价格敏感型",
type: "ai",
status: "online",
coverage: 28.6,
userCount: 1152172345,
lastUpdate: "2025-12-11",
},
{
id: "t8",
name: "品牌偏好",
type: "ai",
status: "testing",
coverage: 45.8,
userCount: 1845098765,
lastUpdate: "2025-12-10",
},
],
},
{
id: "3",
name: "社交互动",
icon: Users,
color: "text-purple-600",
expanded: false,
tags: [
{
id: "t9",
name: "活跃度等级",
type: "rule",
status: "online",
coverage: 78.9,
userCount: 3176543210,
rule: "近7天登录次数分档",
lastUpdate: "2025-12-12",
},
{
id: "t10",
name: "内容偏好",
type: "ai",
status: "online",
coverage: 65.4,
userCount: 2632098765,
lastUpdate: "2025-12-11",
},
{
id: "t11",
name: "社交影响力",
type: "model",
status: "online",
coverage: 55.2,
userCount: 2222345678,
lastUpdate: "2025-12-10",
},
],
},
{
id: "4",
name: "风险等级",
icon: AlertTriangle,
color: "text-red-600",
expanded: false,
tags: [
{
id: "t12",
name: "流失风险",
type: "model",
status: "online",
coverage: 100,
userCount: 4028567890,
lastUpdate: "2025-12-12",
},
{
id: "t13",
name: "欺诈风险",
type: "model",
status: "online",
coverage: 100,
userCount: 4028567890,
lastUpdate: "2025-12-12",
},
{
id: "t14",
name: "高流失风险用户",
type: "rule",
status: "online",
coverage: 8.5,
userCount: 342428270,
rule: "流失风险概率>0.7",
lastUpdate: "2025-12-12",
},
],
},
]
export default function TagManagementPage() {
const [categories, setCategories] = useState<TagCategory[]>(INITIAL_CATEGORIES)
const [searchQuery, setSearchQuery] = useState("")
const [filterType, setFilterType] = useState<string>("all")
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
const toggleCategory = (id: string) => {
setCategories((prev) => prev.map((cat) => (cat.id === id ? { ...cat, expanded: !cat.expanded } : cat)))
}
const allTags = categories.flatMap((cat) => cat.tags)
const stats = {
totalTags: allTags.length,
onlineTags: allTags.filter((t) => t.status === "online").length,
avgCoverage: (allTags.reduce((sum, t) => sum + t.coverage, 0) / allTags.length).toFixed(1),
totalCovered: allTags.reduce((sum, t) => sum + t.userCount, 0),
}
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()
}
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>
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogTrigger asChild>
<Button className="bg-gradient-to-r from-blue-500 to-purple-500 text-white">
<Plus className="w-4 h-4 mr-2" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Input placeholder="例如:高价值用户" />
</div>
<div className="space-y-2">
<Label></Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="选择分类" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1"></SelectItem>
<SelectItem value="2"></SelectItem>
<SelectItem value="3"></SelectItem>
<SelectItem value="4"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label></Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="选择类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="basic"> - </SelectItem>
<SelectItem value="rule"> - </SelectItem>
<SelectItem value="model"> - </SelectItem>
<SelectItem value="ai">AI标签 - </SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>/</Label>
<Textarea placeholder="例如近30天消费次数>3 AND 客单价>500" rows={3} />
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddDialogOpen(false)}>
</Button>
<Button></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
{/* 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">
<Tags className="w-4 h-4" />
</div>
<div className="text-2xl font-bold text-gray-900">{stats.totalTags}</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">
<CheckCircle className="w-4 h-4 text-green-500" />
线
</div>
<div className="text-2xl font-bold text-green-600">{stats.onlineTags}</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">{stats.avgCoverage}%</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">
<Brain className="w-4 h-4" />
</div>
<div className="flex gap-1 mt-1">
<Badge className="bg-blue-100 text-blue-700 text-xs">
{allTags.filter((t) => t.type === "basic").length}
</Badge>
<Badge className="bg-green-100 text-green-700 text-xs">
{allTags.filter((t) => t.type === "rule").length}
</Badge>
<Badge className="bg-purple-100 text-purple-700 text-xs">
{allTags.filter((t) => t.type === "model").length}
</Badge>
<Badge className="bg-orange-100 text-orange-700 text-xs">
AI {allTags.filter((t) => t.type === "ai").length}
</Badge>
</div>
</CardContent>
</Card>
</div>
{/* Search & Filter */}
<div className="flex flex-col md:flex-row gap-4">
<div className="relative flex-1">
<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>
<Select value={filterType} onValueChange={setFilterType}>
<SelectTrigger className="w-full md:w-40 bg-white/60">
<SelectValue placeholder="标签类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="basic"></SelectItem>
<SelectItem value="rule"></SelectItem>
<SelectItem value="model"></SelectItem>
<SelectItem value="ai">AI标签</SelectItem>
</SelectContent>
</Select>
</div>
{/* Tag Tree */}
<div className="space-y-4">
{categories.map((category) => (
<Card key={category.id} className="border-none shadow-sm bg-white/60 backdrop-blur-sm overflow-hidden">
<button
onClick={() => toggleCategory(category.id)}
className="w-full flex items-center justify-between p-4 hover:bg-gray-50 transition-colors"
>
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-xl bg-gray-100 flex items-center justify-center ${category.color}`}>
<category.icon className="w-5 h-5" />
</div>
<div className="text-left">
<h3 className="font-semibold text-gray-900">{category.name}</h3>
<p className="text-sm text-gray-500">{category.tags.length} </p>
</div>
</div>
{category.expanded ? (
<ChevronDown className="w-5 h-5 text-gray-400" />
) : (
<ChevronRight className="w-5 h-5 text-gray-400" />
)}
</button>
{category.expanded && (
<div className="border-t border-gray-100">
<div className="p-4 space-y-3">
{category.tags
.filter((tag) => {
const matchesSearch = tag.name.toLowerCase().includes(searchQuery.toLowerCase())
const matchesType = filterType === "all" || tag.type === filterType
return matchesSearch && matchesType
})
.map((tag) => {
const TypeConfig = TAG_TYPE_CONFIG[tag.type]
const TypeIcon = TypeConfig.icon
return (
<div
key={tag.id}
className="flex items-center justify-between p-3 rounded-xl bg-gray-50 hover:bg-gray-100 transition-colors"
>
<div className="flex items-center gap-3">
<TypeIcon className="w-4 h-4 text-gray-400" />
<div>
<div className="flex items-center gap-2">
<span className="font-medium text-gray-900">{tag.name}</span>
<Badge className={`text-xs ${TypeConfig.color}`}>{TypeConfig.label}</Badge>
<Badge className={`text-xs ${STATUS_CONFIG[tag.status].color}`}>
{STATUS_CONFIG[tag.status].label}
</Badge>
</div>
{tag.rule && <p className="text-xs text-gray-500 mt-1">{tag.rule}</p>}
</div>
</div>
<div className="flex items-center gap-6 text-sm">
<div className="text-right">
<div className="font-medium text-gray-900">{tag.coverage}%</div>
<div className="text-xs text-gray-500"></div>
</div>
<div className="text-right">
<div className="font-medium text-gray-900">{formatNumber(tag.userCount)}</div>
<div className="text-xs text-gray-500"></div>
</div>
<Button variant="ghost" size="sm">
<Settings className="w-4 h-4" />
</Button>
</div>
</div>
)
})}
</div>
</div>
)}
</Card>
))}
</div>
</div>
)
}