Reorganize navigation and module structure based on new requirements. Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
283 lines
12 KiB
TypeScript
283 lines
12 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
import { Search, Tag, TrendingUp, Filter, Phone, Activity, Target } from "lucide-react"
|
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
|
|
|
export default function UserPortrait() {
|
|
const [searchQuery, setSearchQuery] = useState("")
|
|
|
|
const userSegments = [
|
|
{ name: "高价值用户", count: 128456700, percentage: 3.0, avgValue: 580, color: "purple" },
|
|
{ name: "活跃用户", count: 1285670000, percentage: 30.0, avgValue: 280, color: "blue" },
|
|
{ name: "沉默用户", count: 1714283500, percentage: 40.0, avgValue: 85, color: "yellow" },
|
|
{ name: "流失预警", count: 857134000, percentage: 20.0, avgValue: 45, color: "orange" },
|
|
{ name: "新增用户", count: 300125800, percentage: 7.0, avgValue: 120, color: "green" },
|
|
]
|
|
|
|
const tagCategories = [
|
|
{ name: "基础属性", count: 485, usage: 4285670000, coverage: 100 },
|
|
{ name: "行为标签", count: 328, usage: 3256780000, coverage: 76 },
|
|
{ name: "兴趣偏好", count: 256, usage: 2570000000, coverage: 60 },
|
|
{ name: "消费能力", count: 89, usage: 1714283500, coverage: 40 },
|
|
{ name: "自定义标签", count: 100, usage: 857134000, coverage: 20 },
|
|
]
|
|
|
|
const recentUsers = [
|
|
{
|
|
id: "U001285",
|
|
name: "张**",
|
|
phone: "138****5678",
|
|
level: "S",
|
|
tags: ["高净值", "美妆爱好者", "VIP客户"],
|
|
value: 5800,
|
|
lastActive: "2小时前",
|
|
},
|
|
{
|
|
id: "U002456",
|
|
name: "李**",
|
|
phone: "186****1234",
|
|
level: "A",
|
|
tags: ["活跃用户", "教育培训", "复购客户"],
|
|
value: 3200,
|
|
lastActive: "5小时前",
|
|
},
|
|
{
|
|
id: "U003789",
|
|
name: "王**",
|
|
phone: "159****9876",
|
|
level: "B",
|
|
tags: ["潜力用户", "电商购物", "新客户"],
|
|
value: 1800,
|
|
lastActive: "1天前",
|
|
},
|
|
{
|
|
id: "U004123",
|
|
name: "赵**",
|
|
phone: "177****5432",
|
|
level: "C",
|
|
tags: ["普通用户", "低频互动"],
|
|
value: 850,
|
|
lastActive: "3天前",
|
|
},
|
|
]
|
|
|
|
const formatNumber = (num: number): string => {
|
|
if (num >= 1000000000) return `${(num / 1000000000).toFixed(1)}B`
|
|
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
|
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
|
return num.toString()
|
|
}
|
|
|
|
const getLevelColor = (level: string) => {
|
|
const colors = {
|
|
S: "bg-purple-100 text-purple-700 border-purple-200",
|
|
A: "bg-blue-100 text-blue-700 border-blue-200",
|
|
B: "bg-green-100 text-green-700 border-green-200",
|
|
C: "bg-yellow-100 text-yellow-700 border-yellow-200",
|
|
D: "bg-gray-100 text-gray-700 border-gray-200",
|
|
}
|
|
return colors[level as keyof typeof colors] || colors.D
|
|
}
|
|
|
|
const getSegmentColor = (color: string) => {
|
|
const colors = {
|
|
purple: "from-purple-500 to-purple-600",
|
|
blue: "from-blue-500 to-blue-600",
|
|
green: "from-green-500 to-green-600",
|
|
yellow: "from-yellow-400 to-orange-500",
|
|
orange: "from-orange-500 to-red-500",
|
|
}
|
|
return colors[color as keyof typeof colors]
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-gray-900">用户画像</h1>
|
|
<p className="text-gray-500 mt-1">用户管理、标签体系与群体分析</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Button variant="outline" className="bg-white/60 backdrop-blur-md">
|
|
<Filter className="w-4 h-4 mr-2" />
|
|
高级筛选
|
|
</Button>
|
|
<Button className="bg-gradient-to-r from-blue-500 to-purple-500 text-white">
|
|
<Target className="w-4 h-4 mr-2" />
|
|
创建用户群
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<Tabs defaultValue="segments" className="space-y-6">
|
|
<TabsList className="bg-white/60 backdrop-blur-md border border-gray-200 p-1 rounded-xl">
|
|
<TabsTrigger
|
|
value="segments"
|
|
className="rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-sm"
|
|
>
|
|
用户群体
|
|
</TabsTrigger>
|
|
<TabsTrigger value="tags" className="rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-sm">
|
|
标签体系
|
|
</TabsTrigger>
|
|
<TabsTrigger value="users" className="rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-sm">
|
|
用户列表
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="segments" className="space-y-6">
|
|
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
|
{userSegments.map((segment, index) => (
|
|
<Card
|
|
key={index}
|
|
className={`bg-gradient-to-br ${getSegmentColor(segment.color)} text-white border-none shadow-md`}
|
|
>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-white/90 text-lg">{segment.name}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-2">
|
|
<div className="text-3xl font-bold">{formatNumber(segment.count)}</div>
|
|
<div className="text-sm text-white/80">占比 {segment.percentage}%</div>
|
|
<div className="pt-2 border-t border-white/20">
|
|
<div className="text-xs text-white/70 mb-1">人均价值</div>
|
|
<div className="text-lg font-semibold">¥{segment.avgValue}</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
|
<CardHeader>
|
|
<CardTitle className="text-gray-900 flex items-center gap-2">
|
|
<TrendingUp className="w-5 h-5" />
|
|
用户增长趋势
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-64 flex items-center justify-center text-gray-400 bg-gray-50 rounded-xl border border-dashed border-gray-200">
|
|
用户增长趋势图表区域
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="tags" className="space-y-6">
|
|
<div className="grid grid-cols-1 gap-4">
|
|
{tagCategories.map((category, index) => (
|
|
<Card key={index} className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center">
|
|
<Tag className="w-6 h-6 text-blue-600" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-bold text-gray-900">{category.name}</h3>
|
|
<p className="text-sm text-gray-500">{category.count} 个标签</p>
|
|
</div>
|
|
</div>
|
|
<Badge variant="outline" className="bg-blue-50 text-blue-600 border-blue-200">
|
|
覆盖率 {category.coverage}%
|
|
</Badge>
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-4 text-sm">
|
|
<div>
|
|
<div className="text-gray-500 mb-1">标签数量</div>
|
|
<div className="text-xl font-bold text-gray-900">{category.count}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-gray-500 mb-1">使用用户</div>
|
|
<div className="text-xl font-bold text-gray-900">{formatNumber(category.usage)}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-gray-500 mb-1">覆盖率</div>
|
|
<div className="text-xl font-bold text-green-600">{category.coverage}%</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="users" className="space-y-6">
|
|
<div className="flex items-center gap-4">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
|
<Input
|
|
placeholder="搜索用户手机号、姓名、标签..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="pl-10 bg-white/60 backdrop-blur-md border-gray-200"
|
|
/>
|
|
</div>
|
|
<Button variant="outline" className="bg-white/60 backdrop-blur-md">
|
|
<Filter className="w-4 h-4 mr-2" />
|
|
筛选
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-4">
|
|
{recentUsers.map((user) => (
|
|
<Card
|
|
key={user.id}
|
|
className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm hover:shadow-md transition-all cursor-pointer"
|
|
>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<Avatar className="w-12 h-12">
|
|
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-purple-500 text-white">
|
|
{user.name[0]}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div>
|
|
<div className="flex items-center gap-3 mb-1">
|
|
<span className="text-lg font-bold text-gray-900">{user.name}</span>
|
|
<Badge variant="outline" className={getLevelColor(user.level)}>
|
|
{user.level}级
|
|
</Badge>
|
|
</div>
|
|
<div className="flex items-center gap-4 text-sm text-gray-500">
|
|
<div className="flex items-center gap-1">
|
|
<Phone className="w-3 h-3" />
|
|
{user.phone}
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Activity className="w-3 h-3" />
|
|
{user.lastActive}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="text-sm text-gray-500 mb-1">用户价值</div>
|
|
<div className="text-2xl font-bold text-green-600">¥{user.value}</div>
|
|
</div>
|
|
</div>
|
|
<div className="mt-4 flex items-center gap-2">
|
|
{user.tags.map((tag, index) => (
|
|
<Badge key={index} variant="outline" className="bg-gray-50 text-gray-600 border-gray-200">
|
|
{tag}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
)
|
|
}
|