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:
3
app/tag-portrait/crowd/loading.tsx
Normal file
3
app/tag-portrait/crowd/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
346
app/tag-portrait/crowd/page.tsx
Normal file
346
app/tag-portrait/crowd/page.tsx
Normal 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
12
app/tag-portrait/page.tsx
Normal 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
|
||||
}
|
||||
3
app/tag-portrait/portrait/loading.tsx
Normal file
3
app/tag-portrait/portrait/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
273
app/tag-portrait/portrait/page.tsx
Normal file
273
app/tag-portrait/portrait/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
3
app/tag-portrait/tags/loading.tsx
Normal file
3
app/tag-portrait/tags/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
491
app/tag-portrait/tags/page.tsx
Normal file
491
app/tag-portrait/tags/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user