Files
users/components/data-analysis/user-selector.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

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