Files
users/app/user-value/page.tsx
v0 2408d50cb0 refactor: overhaul UI for streamlined user experience
Redesign navigation, home overview, user portrait, and valuation pages
with improved functionality and responsive design.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
2025-07-18 13:47:12 +00:00

791 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState } from "react"
import { Card, CardContent, CardDescription, 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Input } from "@/components/ui/input"
import {
ArrowUpRight,
Download,
Filter,
RefreshCw,
Target,
TrendingUp,
Users,
Layers,
Tag,
BarChart3,
Search,
HelpCircle,
ChevronRight,
ChevronDown,
} from "lucide-react"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
// 真实用户数据
const mockUsers = [
{
id: "1",
name: "李明华",
company: "阿里巴巴集团",
recency: 1,
frequency: 28,
monetary: 45600,
rfmScore: 95,
level: "高价值",
growth: "+8.5%",
upgradeFromDays: 45,
nextUpgrade: "VIP客户",
upgradeProbability: 85,
},
{
id: "2",
name: "张雨婷",
company: "腾讯科技",
recency: 3,
frequency: 22,
monetary: 32800,
rfmScore: 88,
level: "高价值",
growth: "+6.2%",
upgradeFromDays: 28,
nextUpgrade: "白金会员",
upgradeProbability: 78,
},
{
id: "3",
name: "王建国",
company: "字节跳动",
recency: 2,
frequency: 25,
monetary: 38900,
rfmScore: 91,
level: "高价值",
growth: "+7.8%",
upgradeFromDays: 38,
nextUpgrade: "企业VIP",
upgradeProbability: 82,
},
{
id: "4",
name: "刘思琪",
company: "美团点评",
recency: 7,
frequency: 18,
monetary: 25400,
rfmScore: 76,
level: "中高价值",
growth: "+4.1%",
upgradeFromDays: 12,
nextUpgrade: "高价值用户",
upgradeProbability: 68,
},
{
id: "5",
name: "陈浩然",
company: "京东集团",
recency: 4,
frequency: 20,
monetary: 28700,
rfmScore: 82,
level: "中高价值",
growth: "+5.3%",
upgradeFromDays: 22,
nextUpgrade: "高价值用户",
upgradeProbability: 72,
},
{
id: "6",
name: "周欣妍",
company: "滴滴出行",
recency: 12,
frequency: 15,
monetary: 18600,
rfmScore: 65,
level: "中等价值",
growth: "+3.2%",
upgradeFromDays: 8,
nextUpgrade: "中高价值用户",
upgradeProbability: 55,
},
{
id: "7",
name: "马志强",
company: "华为技术",
recency: 6,
frequency: 16,
monetary: 22100,
rfmScore: 71,
level: "中等价值",
growth: "+4.8%",
upgradeFromDays: 15,
nextUpgrade: "中高价值用户",
upgradeProbability: 61,
},
{
id: "8",
name: "赵丽娟",
company: "小米科技",
recency: 15,
frequency: 12,
monetary: 15800,
rfmScore: 58,
level: "中等价值",
growth: "+2.1%",
upgradeFromDays: 5,
nextUpgrade: "中高价值用户",
upgradeProbability: 45,
},
]
export default function UserValuePage() {
const [timeRange, setTimeRange] = useState("30days")
const [searchQuery, setSearchQuery] = useState("")
const [showModelDialog, setShowModelDialog] = useState(false)
const [showUpgradeDialog, setShowUpgradeDialog] = useState(false)
const [expandedSections, setExpandedSections] = useState({
overview: true,
model: false,
analysis: true,
upgradePaths: false,
})
const filteredUsers = mockUsers.filter(
(user) =>
user.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
user.company.toLowerCase().includes(searchQuery.toLowerCase()),
)
const getRFMLevelColor = (level: string) => {
switch (level) {
case "高价值":
return "bg-green-100 text-green-800"
case "中高价值":
return "bg-blue-100 text-blue-800"
case "中等价值":
return "bg-yellow-100 text-yellow-800"
case "低价值":
return "bg-gray-100 text-gray-800"
case "流失风险":
return "bg-red-100 text-red-800"
default:
return "bg-gray-100 text-gray-800"
}
}
const toggleSection = (section: string) => {
setExpandedSections((prev) => ({
...prev,
[section]: !prev[section],
}))
}
return (
<div className="container mx-auto p-4 space-y-6">
{/* 页面标题和工具栏 */}
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<div>
<h1 className="text-3xl font-bold"></h1>
<p className="text-muted-foreground mt-1"></p>
</div>
<div className="flex flex-wrap gap-2">
<Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="选择时间范围" />
</SelectTrigger>
<SelectContent>
<SelectItem value="7days">7</SelectItem>
<SelectItem value="30days">30</SelectItem>
<SelectItem value="90days">90</SelectItem>
</SelectContent>
</Select>
<Button variant="outline" size="icon">
<Filter className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon">
<RefreshCw className="h-4 w-4" />
</Button>
<Button variant="outline">
<Download className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
{/* 用户估值概览 */}
<Collapsible
open={expandedSections.overview}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, overview: open }))}
>
<Card className="border-none shadow-md">
<CollapsibleTrigger className="w-full">
<CardHeader className="bg-gradient-to-r from-green-50 to-emerald-50 border-b hover:from-green-100 hover:to-emerald-100 transition-colors cursor-pointer">
<div className="flex justify-between items-center">
<div className="flex items-center gap-3">
<Target className="h-5 w-5 text-green-600" />
<div className="text-left">
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</div>
</div>
{expandedSections.overview ? <ChevronDown className="h-5 w-5" /> : <ChevronRight className="h-5 w-5" />}
</div>
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="p-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card className="bg-gradient-to-br from-green-50 to-emerald-50 border-none shadow-sm">
<CardContent className="p-4">
<div className="flex justify-between items-start">
<div>
<div className="text-sm font-medium text-green-600"></div>
<div className="text-2xl font-bold mt-2">¥3,248</div>
<div className="text-sm text-green-600 mt-1 flex items-center">
<ArrowUpRight className="h-4 w-4 mr-1" />
8.2%
</div>
</div>
<div className="bg-green-100 p-2 rounded-full">
<Target className="h-5 w-5 text-green-600" />
</div>
</div>
<div className="mt-3">
<div className="flex justify-between text-xs mb-1">
<span></span>
<span>94%</span>
</div>
<Progress value={94} className="h-1.5 bg-green-100" />
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-purple-50 to-pink-50 border-none shadow-sm">
<CardContent className="p-4">
<div className="flex justify-between items-start">
<div>
<div className="text-sm font-medium text-purple-600"></div>
<div className="text-2xl font-bold mt-2">45,678</div>
<div className="text-sm text-green-600 mt-1 flex items-center">
<ArrowUpRight className="h-4 w-4 mr-1" />
12.3%
</div>
</div>
<div className="bg-purple-100 p-2 rounded-full">
<TrendingUp className="h-5 w-5 text-purple-600" />
</div>
</div>
<div className="mt-3">
<div className="flex justify-between text-xs mb-1">
<span></span>
<span>28.6%</span>
</div>
<Progress value={28.6} className="h-1.5 bg-purple-100" />
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-blue-50 to-indigo-50 border-none shadow-sm">
<CardContent className="p-4">
<div className="flex justify-between items-start">
<div>
<div className="text-sm font-medium text-blue-600"></div>
<div className="text-2xl font-bold mt-2">15.8%</div>
<div className="text-sm text-green-600 mt-1 flex items-center">
<ArrowUpRight className="h-4 w-4 mr-1" />
2.1%
</div>
</div>
<div className="bg-blue-100 p-2 rounded-full">
<BarChart3 className="h-5 w-5 text-blue-600" />
</div>
</div>
<div className="mt-3">
<div className="flex justify-between text-xs mb-1">
<span></span>
<span>11.2%</span>
</div>
<Progress value={141} className="h-1.5 bg-blue-100" />
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-amber-50 to-yellow-50 border-none shadow-sm">
<CardContent className="p-4">
<div className="flex justify-between items-start">
<div>
<div className="text-sm font-medium text-amber-600"></div>
<div className="text-2xl font-bold mt-2">12.5%</div>
<div className="text-sm text-green-600 mt-1 flex items-center">
<ArrowUpRight className="h-4 w-4 mr-1" />
1.8%
</div>
</div>
<div className="bg-amber-100 p-2 rounded-full">
<Users className="h-5 w-5 text-amber-600" />
</div>
</div>
<div className="mt-3">
<div className="flex justify-between text-xs mb-1">
<span></span>
<span>15%</span>
</div>
<Progress value={83} className="h-1.5 bg-amber-100" />
</div>
</CardContent>
</Card>
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
{/* RFM估值模型 */}
<Collapsible
open={expandedSections.model}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, model: open }))}
>
<Card className="border-none shadow-md">
<CollapsibleTrigger className="w-full">
<CardHeader className="bg-gradient-to-r from-blue-50 to-indigo-50 border-b hover:from-blue-100 hover:to-indigo-100 transition-colors cursor-pointer">
<div className="flex justify-between items-center">
<div className="flex items-center gap-3">
<BarChart3 className="h-5 w-5 text-blue-600" />
<div className="text-left">
<CardTitle className="flex items-center gap-2">
RFM用户价值模型
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
setShowModelDialog(true)
}}
className="h-6 w-6 p-0"
>
<HelpCircle className="h-4 w-4" />
</Button>
</CardTitle>
<CardDescription></CardDescription>
</div>
</div>
{expandedSections.model ? <ChevronDown className="h-5 w-5" /> : <ChevronRight className="h-5 w-5" />}
</div>
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="p-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-2">
<h3 className="font-semibold flex items-center">
<span className="bg-red-100 text-red-800 w-6 h-6 rounded-full flex items-center justify-center mr-2">
R
</span>
Recency ()
</h3>
<p className="text-sm text-muted-foreground"></p>
<div className="bg-gray-50 p-3 rounded-md">
<div className="text-sm">
<div className="flex justify-between mb-1">
<span>1-3</span>
<span className="font-medium">5</span>
</div>
<div className="flex justify-between mb-1">
<span>4-7</span>
<span className="font-medium">4</span>
</div>
<div className="flex justify-between mb-1">
<span>8-14</span>
<span className="font-medium">3</span>
</div>
<div className="flex justify-between mb-1">
<span>15-30</span>
<span className="font-medium">2</span>
</div>
<div className="flex justify-between">
<span>30</span>
<span className="font-medium">1</span>
</div>
</div>
</div>
</div>
<div className="space-y-2">
<h3 className="font-semibold flex items-center">
<span className="bg-green-100 text-green-800 w-6 h-6 rounded-full flex items-center justify-center mr-2">
F
</span>
Frequency ()
</h3>
<p className="text-sm text-muted-foreground"></p>
<div className="bg-gray-50 p-3 rounded-md">
<div className="text-sm">
<div className="flex justify-between mb-1">
<span>25/</span>
<span className="font-medium">5</span>
</div>
<div className="flex justify-between mb-1">
<span>20-24/</span>
<span className="font-medium">4</span>
</div>
<div className="flex justify-between mb-1">
<span>15-19/</span>
<span className="font-medium">3</span>
</div>
<div className="flex justify-between mb-1">
<span>10-14/</span>
<span className="font-medium">2</span>
</div>
<div className="flex justify-between">
<span>1-9/</span>
<span className="font-medium">1</span>
</div>
</div>
</div>
</div>
<div className="space-y-2">
<h3 className="font-semibold flex items-center">
<span className="bg-blue-100 text-blue-800 w-6 h-6 rounded-full flex items-center justify-center mr-2">
M
</span>
Monetary ()
</h3>
<p className="text-sm text-muted-foreground"></p>
<div className="bg-gray-50 p-3 rounded-md">
<div className="text-sm">
<div className="flex justify-between mb-1">
<span>30000</span>
<span className="font-medium">5</span>
</div>
<div className="flex justify-between mb-1">
<span>20000-29999</span>
<span className="font-medium">4</span>
</div>
<div className="flex justify-between mb-1">
<span>10000-19999</span>
<span className="font-medium">3</span>
</div>
<div className="flex justify-between mb-1">
<span>5000-9999</span>
<span className="font-medium">2</span>
</div>
<div className="flex justify-between">
<span>5000</span>
<span className="font-medium">1</span>
</div>
</div>
</div>
</div>
</div>
<div className="mt-6">
<h3 className="font-semibold mb-3">RFM评分等级</h3>
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
<div className="bg-green-100 p-3 rounded-md text-center">
<p className="font-medium text-green-800"></p>
<p className="text-sm text-green-700">85-100</p>
</div>
<div className="bg-blue-100 p-3 rounded-md text-center">
<p className="font-medium text-blue-800"></p>
<p className="text-sm text-blue-700">70-84</p>
</div>
<div className="bg-yellow-100 p-3 rounded-md text-center">
<p className="font-medium text-yellow-800"></p>
<p className="text-sm text-yellow-700">55-69</p>
</div>
<div className="bg-gray-100 p-3 rounded-md text-center">
<p className="font-medium text-gray-800"></p>
<p className="text-sm text-gray-700">40-54</p>
</div>
<div className="bg-red-100 p-3 rounded-md text-center">
<p className="font-medium text-red-800"></p>
<p className="text-sm text-red-700">0-39</p>
</div>
</div>
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
{/* 用户价值分析 */}
<Collapsible
open={expandedSections.analysis}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, analysis: open }))}
>
<Card className="border-none shadow-md">
<CollapsibleTrigger className="w-full">
<CardHeader className="bg-gradient-to-r from-purple-50 to-pink-50 border-b hover:from-purple-100 hover:to-pink-100 transition-colors cursor-pointer">
<div className="flex justify-between items-center">
<div className="flex items-center gap-3">
<Users className="h-5 w-5 text-purple-600" />
<div className="text-left">
<CardTitle></CardTitle>
<CardDescription> ({filteredUsers.length})</CardDescription>
</div>
</div>
{expandedSections.analysis ? <ChevronDown className="h-5 w-5" /> : <ChevronRight className="h-5 w-5" />}
</div>
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="p-4">
<div className="mb-4">
<div className="relative w-64">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="搜索用户或公司..."
className="pl-8"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-center">RFM评分</TableHead>
<TableHead className="text-center">R值</TableHead>
<TableHead className="text-center">F值</TableHead>
<TableHead className="text-center">M值</TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredUsers.map((user) => (
<TableRow key={user.id} className="hover:bg-gray-50">
<TableCell className="font-medium">{user.name}</TableCell>
<TableCell>{user.company}</TableCell>
<TableCell className="text-center">
<Badge className={`${getRFMLevelColor(user.level)}`}>
{user.rfmScore} ({user.level})
</Badge>
</TableCell>
<TableCell className="text-center">{user.recency}</TableCell>
<TableCell className="text-center">{user.frequency}/</TableCell>
<TableCell className="text-center">¥{user.monetary.toLocaleString()}</TableCell>
<TableCell className="text-right text-green-600">{user.growth}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
{/* 用户升级路径分析 */}
<Collapsible
open={expandedSections.upgradePaths}
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, upgradePaths: open }))}
>
<Card className="border-none shadow-md">
<CollapsibleTrigger className="w-full">
<CardHeader className="bg-gradient-to-r from-orange-50 to-red-50 border-b hover:from-orange-100 hover:to-red-100 transition-colors cursor-pointer">
<div className="flex justify-between items-center">
<div className="flex items-center gap-3">
<TrendingUp className="h-5 w-5 text-orange-600" />
<div className="text-left">
<CardTitle className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
setShowUpgradeDialog(true)
}}
className="h-6 w-6 p-0"
>
<HelpCircle className="h-4 w-4" />
</Button>
</CardTitle>
<CardDescription></CardDescription>
</div>
</div>
{expandedSections.upgradePaths ? (
<ChevronDown className="h-5 w-5" />
) : (
<ChevronRight className="h-5 w-5" />
)}
</div>
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="p-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<Card className="bg-white shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-lg flex items-center">
<Target className="h-5 w-5 mr-2 text-green-600" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm font-medium">2,345</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm font-medium">5,678</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm font-medium">1,234</span>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-white shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-lg flex items-center">
<Layers className="h-5 w-5 mr-2 text-blue-600" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm font-medium">12.5%</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm font-medium">28</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm font-medium">1,856</span>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-white shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-lg flex items-center">
<Tag className="h-5 w-5 mr-2 text-purple-600" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm font-medium">¥2.3M</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm font-medium">¥1,245</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm">ROI提升</span>
<span className="text-sm font-medium">35.8%</span>
</div>
</div>
</CardContent>
</Card>
</div>
<div className="space-y-4">
<h3 className="text-lg font-medium"></h3>
<div className="space-y-3">
{filteredUsers.slice(0, 5).map((user) => (
<div
key={user.id}
className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50"
>
<div className="flex items-center gap-4">
<div>
<p className="font-medium">{user.name}</p>
<p className="text-sm text-gray-500">{user.company}</p>
</div>
<Badge className={getRFMLevelColor(user.level)}>{user.level}</Badge>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<p className="text-sm font-medium">: {user.nextUpgrade}</p>
<p className="text-sm text-gray-500">: {user.upgradeProbability}%</p>
</div>
<div className="w-24">
<Progress value={user.upgradeProbability} className="h-2" />
</div>
<Button variant="outline" size="sm">
</Button>
</div>
</div>
))}
</div>
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
{/* RFM模型说明对话框 */}
<Dialog open={showModelDialog} onOpenChange={setShowModelDialog}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>RFM模型说明</DialogTitle>
<DialogDescription>RFM模型是一种用于客户价值评估的经典方法</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="p-4 bg-blue-50 rounded-lg">
<h4 className="font-medium mb-2">RFM模型</h4>
<p className="text-sm text-gray-600">
RFM模型是一种用于分析客户价值和客户创利能力的重要工具RecencyFrequencyMonetary
</p>
</div>
<div className="p-4 bg-green-50 rounded-lg">
<h4 className="font-medium mb-2">使RFM评分</h4>
<p className="text-sm text-gray-600">
1-5150-100便
</p>
</div>
</div>
</DialogContent>
</Dialog>
{/* 升级路径说明对话框 */}
<Dialog open={showUpgradeDialog} onOpenChange={setShowUpgradeDialog}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="p-4 bg-purple-50 rounded-lg">
<h4 className="font-medium mb-2"></h4>
<p className="text-sm text-gray-600">
RFM评分
</p>
</div>
<div className="p-4 bg-orange-50 rounded-lg">
<h4 className="font-medium mb-2"></h4>
<p className="text-sm text-gray-600">
RFM中得分较低的
</p>
</div>
</div>
</DialogContent>
</Dialog>
</div>
)
}