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>
This commit is contained in:
62
components/data-analysis/rfm-analysis.tsx
Normal file
62
components/data-analysis/rfm-analysis.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
"use client"
|
||||
|
||||
import type { RFMAnalysisResult, UserRFMData } from "@/types/data-analysis"
|
||||
|
||||
interface RFMAnalysisProps {
|
||||
data: UserRFMData[]
|
||||
analysisResult: RFMAnalysisResult
|
||||
}
|
||||
|
||||
export function RFMAnalysis({ data, analysisResult }: RFMAnalysisProps) {
|
||||
// 获取用户分群的颜色
|
||||
const getSegmentColor = (segment: string) => {
|
||||
if (segment.includes("高价值")) return "rgb(16, 185, 129)"
|
||||
if (segment.includes("中价值")) return "rgb(59, 130, 246)"
|
||||
if (segment.includes("低价值")) return "rgb(156, 163, 175)"
|
||||
return "rgb(139, 92, 246)" // 新用户
|
||||
}
|
||||
|
||||
// 准备饼图数据
|
||||
const pieChartData = {
|
||||
labels: analysisResult.segmentDistribution.map(item => item.segment),
|
||||
datasets: [
|
||||
{
|
||||
data: analysisResult.segmentDistribution.map(item => item.count),
|
||||
backgroundColor: analysisResult.segmentDistribution.map(item => getSegmentColor(item.segment)),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// 准备RFM分布柱状图数据
|
||||
const rfmDistributionData = {
|
||||
labels: ["R1", "R2", "R3", "R4", "R5", "F1", "F2", "F3", "F4", "F5", "M1", "M2", "M3", "M4", "M5"],
|
||||
datasets: [
|
||||
{
|
||||
label: "用户数量",
|
||||
data: [
|
||||
// R分布
|
||||
data.filter(user => user.rfmScore.recency === 1).length,
|
||||
data.filter(user => user.rfmScore.recency === 2).length,
|
||||
data.filter(user => user.rfmScore.recency === 3).length,
|
||||
data.filter(user => user.rfmScore.recency === 4).length,
|
||||
data.filter(user => user.rfmScore.recency === 5).length,
|
||||
// F分布
|
||||
data.filter(user => user.rfmScore.frequency === 1).length,
|
||||
data.filter(user => user.rfmScore.frequency === 2).length,
|
||||
data.filter(user => user.rfmScore.frequency === 3).length,
|
||||
data.filter(user => user.rfmScore.frequency === 4).length,
|
||||
data.filter(user => user.rfmScore.frequency === 5).length,
|
||||
// M分布
|
||||
data.filter(user => user.rfmScore.monetary === 1).length,
|
||||
data.filter(user => user.rfmScore.monetary === 2).length,
|
||||
data.filter(user => user.rfmScore.monetary === 3).length,
|
||||
data.filter(user => user.rfmScore.monetary === 4).length,
|
||||
data.filter(user => user.rfmScore.monetary === 5).length,
|
||||
],
|
||||
backgroundColor: [
|
||||
// R颜色
|
||||
"rgba(239, 68, 68, 0.7)", "rgba(239, 68, 68, 0.7)", "rgba(239, 68, 68, 0.7)", "rgba(239, 68, 68, 0.7)", "rgba(239, 68, 68, 0.7)",
|
||||
// F颜色
|
||||
"rgba(16, 185, 129, 0.7)", "rgba(16, 185, 129, 0.7)", "rgba(16, 185, 129, 0.7)", "rgba(16, 185, 129, 0.7)", "rgba(16, 185, 129, 0.7)",
|
||||
// M颜色
|
||||
"rgba(59, 130, 246, 0.7)", "rgba(59, 130, 246, 0.7)", "rgba(59, 130\
|
||||
326
components/data-analysis/user-selector.tsx
Normal file
326
components/data-analysis/user-selector.tsx
Normal file
@@ -0,0 +1,326 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import type { UserRFMData, UserSegment, UserFilterOptions } from "@/types/data-analysis"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Search, Filter, RefreshCw, UserPlus, Download } from "lucide-react"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
|
||||
interface UserSelectorProps {
|
||||
users: UserRFMData[]
|
||||
selectedUsers: string[]
|
||||
onSelectUser: (userId: string) => void
|
||||
onSelectAll: (selected: boolean) => void
|
||||
onFilterChange: (options: UserFilterOptions) => void
|
||||
filterOptions: UserFilterOptions
|
||||
}
|
||||
|
||||
export function UserSelector({
|
||||
users,
|
||||
selectedUsers,
|
||||
onSelectUser,
|
||||
onSelectAll,
|
||||
onFilterChange,
|
||||
filterOptions,
|
||||
}: UserSelectorProps) {
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [showFilters, setShowFilters] = useState(false)
|
||||
|
||||
// 本地过滤用户(搜索功能)
|
||||
const filteredUsers = users.filter(
|
||||
(user) =>
|
||||
user.userName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
user.phone.includes(searchQuery) ||
|
||||
user.segment.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
)
|
||||
|
||||
// 处理分群选择
|
||||
const handleSegmentChange = (segment: UserSegment, checked: boolean) => {
|
||||
let newSegments = [...filterOptions.segments]
|
||||
if (checked) {
|
||||
newSegments.push(segment)
|
||||
} else {
|
||||
newSegments = newSegments.filter((s) => s !== segment)
|
||||
}
|
||||
onFilterChange({ ...filterOptions, segments: newSegments })
|
||||
}
|
||||
|
||||
// 处理日期范围变更
|
||||
const handleDateRangeChange = (field: "start" | "end", value: string) => {
|
||||
onFilterChange({
|
||||
...filterOptions,
|
||||
dateRange: {
|
||||
...filterOptions.dateRange,
|
||||
[field]: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 处理价值范围变更
|
||||
const handleValueRangeChange = (field: "min" | "max", value: string) => {
|
||||
onFilterChange({
|
||||
...filterOptions,
|
||||
valueRange: {
|
||||
...filterOptions.valueRange,
|
||||
[field]: Number.parseInt(value) || 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 获取用户分群的颜色
|
||||
const getSegmentColor = (segment: string) => {
|
||||
if (segment.includes("高价值")) return "bg-green-100 text-green-800"
|
||||
if (segment.includes("中价值")) return "bg-blue-100 text-blue-800"
|
||||
if (segment.includes("低价值")) return "bg-gray-100 text-gray-800"
|
||||
return "bg-purple-100 text-purple-800" // 新用户
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-center">
|
||||
<CardTitle>用户选择</CardTitle>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm" onClick={() => setShowFilters(!showFilters)}>
|
||||
<Filter className="h-4 w-4 mr-2" />
|
||||
筛选
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
导出
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* 搜索栏 */}
|
||||
<div className="mb-4 relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="搜索用户名、手机号、分群..."
|
||||
className="pl-8"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 筛选面板 */}
|
||||
{showFilters && (
|
||||
<div className="mb-6 p-4 border rounded-md bg-gray-50">
|
||||
<h3 className="text-sm font-medium mb-3">高级筛选</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{/* 分群筛选 */}
|
||||
<div>
|
||||
<h4 className="text-xs font-medium mb-2">用户分群</h4>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
"高价值活跃用户",
|
||||
"高价值流失风险用户",
|
||||
"高价值沉睡用户",
|
||||
"中价值活跃用户",
|
||||
"中价值流失风险用户",
|
||||
"中价值沉睡用户",
|
||||
"低价值活跃用户",
|
||||
"低价值流失风险用户",
|
||||
"低价值沉睡用户",
|
||||
"新用户",
|
||||
].map((segment) => (
|
||||
<div key={segment} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`segment-${segment}`}
|
||||
checked={filterOptions.segments.includes(segment as UserSegment)}
|
||||
onCheckedChange={(checked) => handleSegmentChange(segment as UserSegment, checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor={`segment-${segment}`} className="text-xs">
|
||||
{segment}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 日期范围筛选 */}
|
||||
<div>
|
||||
<h4 className="text-xs font-medium mb-2">最后购买日期</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label htmlFor="date-start" className="text-xs">
|
||||
开始日期
|
||||
</Label>
|
||||
<Input
|
||||
id="date-start"
|
||||
type="date"
|
||||
value={filterOptions.dateRange.start}
|
||||
onChange={(e) => handleDateRangeChange("start", e.target.value)}
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="date-end" className="text-xs">
|
||||
结束日期
|
||||
</Label>
|
||||
<Input
|
||||
id="date-end"
|
||||
type="date"
|
||||
value={filterOptions.dateRange.end}
|
||||
onChange={(e) => handleDateRangeChange("end", e.target.value)}
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 价值范围筛选 */}
|
||||
<div>
|
||||
<h4 className="text-xs font-medium mb-2">用户估值范围</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label htmlFor="value-min" className="text-xs">
|
||||
最小值
|
||||
</Label>
|
||||
<Input
|
||||
id="value-min"
|
||||
type="number"
|
||||
value={filterOptions.valueRange.min}
|
||||
onChange={(e) => handleValueRangeChange("min", e.target.value)}
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="value-max" className="text-xs">
|
||||
最大值
|
||||
</Label>
|
||||
<Input
|
||||
id="value-max"
|
||||
type="number"
|
||||
value={filterOptions.valueRange.max}
|
||||
onChange={(e) => handleValueRangeChange("max", e.target.value)}
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mr-2"
|
||||
onClick={() =>
|
||||
onFilterChange({
|
||||
segments: [],
|
||||
dateRange: { start: "", end: "" },
|
||||
valueRange: { min: 0, max: 0 },
|
||||
sources: [],
|
||||
})
|
||||
}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button size="sm">应用筛选</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 用户表格 */}
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-12">
|
||||
<Checkbox
|
||||
checked={selectedUsers.length === filteredUsers.length && filteredUsers.length > 0}
|
||||
onCheckedChange={(checked) => onSelectAll(!!checked)}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>用户名</TableHead>
|
||||
<TableHead>手机号</TableHead>
|
||||
<TableHead>最后购买</TableHead>
|
||||
<TableHead>购买频率</TableHead>
|
||||
<TableHead>消费总额</TableHead>
|
||||
<TableHead>RFM得分</TableHead>
|
||||
<TableHead>用户分群</TableHead>
|
||||
<TableHead>估值</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredUsers.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="text-center py-4">
|
||||
没有找到匹配的用户
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredUsers.map((user) => (
|
||||
<TableRow key={user.userId} className="cursor-pointer hover:bg-gray-50">
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedUsers.includes(user.userId)}
|
||||
onCheckedChange={() => onSelectUser(user.userId)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{user.userName}</TableCell>
|
||||
<TableCell>{user.phone}</TableCell>
|
||||
<TableCell>{user.lastPurchaseDate}</TableCell>
|
||||
<TableCell>{user.purchaseFrequency}次</TableCell>
|
||||
<TableCell>¥{user.totalSpent.toLocaleString()}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Badge variant="outline" className="bg-red-50 text-red-700">
|
||||
R{user.rfmScore.recency}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-green-50 text-green-700">
|
||||
F{user.rfmScore.frequency}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-blue-50 text-blue-700">
|
||||
M{user.rfmScore.monetary}
|
||||
</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={getSegmentColor(user.segment)}>{user.segment}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">¥{user.valueEstimation.toLocaleString()}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 选择统计 */}
|
||||
<div className="mt-4 flex justify-between items-center">
|
||||
<div className="text-sm text-gray-500">
|
||||
已选择 <span className="font-medium">{selectedUsers.length}</span> 个用户, 总估值{" "}
|
||||
<span className="font-medium">
|
||||
¥
|
||||
{users
|
||||
.filter((user) => selectedUsers.includes(user.userId))
|
||||
.reduce((sum, user) => sum + user.valueEstimation, 0)
|
||||
.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<Button size="sm">
|
||||
<UserPlus className="h-4 w-4 mr-2" />
|
||||
添加到分析
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user