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>
327 lines
12 KiB
TypeScript
327 lines
12 KiB
TypeScript
"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>
|
||
)
|
||
}
|