Files
users/components/data-analysis/user-selector.tsx

327 lines
12 KiB
TypeScript
Raw Normal View History

"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>
)
}