import { Users, Search, Edit, Trash2, Eye, User, MessageSquare, Hash, Clock, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react' import { useState, useEffect, useMemo } from 'react' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { ScrollArea } from '@/components/ui/scroll-area' import { useToast } from '@/hooks/use-toast' import { Checkbox } from '@/components/ui/checkbox' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Switch } from '@/components/ui/switch' import { Textarea } from '@/components/ui/textarea' import type { PersonInfo, PersonUpdateRequest } from '@/types/person' import { getPersonList, getPersonDetail, updatePerson, deletePerson, getPersonStats, batchDeletePersons } from '@/lib/person-api' export function PersonManagementPage() { const [persons, setPersons] = useState([]) const [loading, setLoading] = useState(true) const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) const [search, setSearch] = useState('') const [filterKnown, setFilterKnown] = useState(undefined) const [filterPlatform, setFilterPlatform] = useState(undefined) const [selectedPerson, setSelectedPerson] = useState(null) const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false) const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) const [deleteConfirmPerson, setDeleteConfirmPerson] = useState(null) const [stats, setStats] = useState({ total: 0, known: 0, unknown: 0, platforms: {} as Record }) const [selectedPersons, setSelectedPersons] = useState>(new Set()) const [batchDeleteDialogOpen, setBatchDeleteDialogOpen] = useState(false) const [jumpToPage, setJumpToPage] = useState('') const { toast } = useToast() // 加载人物列表 const loadPersons = async () => { try { setLoading(true) const response = await getPersonList({ page, page_size: pageSize, search: search || undefined, is_known: filterKnown, platform: filterPlatform, }) setPersons(response.data) setTotal(response.total) } catch (error) { toast({ title: '加载失败', description: error instanceof Error ? error.message : '无法加载人物信息', variant: 'destructive', }) } finally { setLoading(false) } } // 加载统计数据 const loadStats = async () => { try { const response = await getPersonStats() if (response?.data) { setStats(response.data) } } catch (error) { console.error('加载统计数据失败:', error) } } // 初始加载 useEffect(() => { loadPersons() loadStats() // eslint-disable-next-line react-hooks/exhaustive-deps }, [page, pageSize, search, filterKnown, filterPlatform]) // 查看详情 const handleViewDetail = async (person: PersonInfo) => { try { const response = await getPersonDetail(person.person_id) setSelectedPerson(response.data) setIsDetailDialogOpen(true) } catch (error) { toast({ title: '加载详情失败', description: error instanceof Error ? error.message : '无法加载人物详情', variant: 'destructive', }) } } // 编辑人物 const handleEdit = (person: PersonInfo) => { setSelectedPerson(person) setIsEditDialogOpen(true) } // 删除人物 const handleDelete = async (person: PersonInfo) => { try { await deletePerson(person.person_id) toast({ title: '删除成功', description: `已删除人物信息: ${person.person_name || person.nickname || person.user_id}`, }) setDeleteConfirmPerson(null) loadPersons() loadStats() } catch (error) { toast({ title: '删除失败', description: error instanceof Error ? error.message : '无法删除人物信息', variant: 'destructive', }) } } // 获取平台列表 const platforms = useMemo(() => { return Object.keys(stats.platforms) }, [stats.platforms]) // 切换单个人物选择 const togglePersonSelection = (personId: string) => { const newSelected = new Set(selectedPersons) if (newSelected.has(personId)) { newSelected.delete(personId) } else { newSelected.add(personId) } setSelectedPersons(newSelected) } // 全选/取消全选 const toggleSelectAll = () => { if (selectedPersons.size === persons.length && persons.length > 0) { setSelectedPersons(new Set()) } else { setSelectedPersons(new Set(persons.map(p => p.person_id))) } } // 打开批量删除对话框 const openBatchDeleteDialog = () => { if (selectedPersons.size === 0) { toast({ title: '未选择任何人物', description: '请先选择要删除的人物', variant: 'destructive', }) return } setBatchDeleteDialogOpen(true) } // 批量删除确认 const handleBatchDelete = async () => { try { const result = await batchDeletePersons(Array.from(selectedPersons)) toast({ title: '批量删除完成', description: result.message, }) setSelectedPersons(new Set()) setBatchDeleteDialogOpen(false) loadPersons() loadStats() } catch (error) { toast({ title: '批量删除失败', description: error instanceof Error ? error.message : '批量删除失败', variant: 'destructive', }) } } // 页面跳转 const handleJumpToPage = () => { const targetPage = parseInt(jumpToPage) const totalPages = Math.ceil(total / pageSize) if (targetPage >= 1 && targetPage <= totalPages) { setPage(targetPage) setJumpToPage('') } else { toast({ title: '无效的页码', description: `请输入1-${totalPages}之间的页码`, variant: 'destructive', }) } } // 格式化时间 const formatTime = (timestamp: number | null) => { if (!timestamp) return '-' return new Date(timestamp * 1000).toLocaleString('zh-CN') } return (
{/* 页面标题 */}

人物信息管理

管理麦麦认识的所有人物信息

{/* 统计卡片 */}
总人数
{stats.total}
已认识
{stats.known}
未认识
{stats.unknown}
{/* 搜索和过滤 */}
setSearch(e.target.value)} className="pl-9" />
{/* 批量操作工具栏 */}
{selectedPersons.size > 0 && ( 已选择 {selectedPersons.size} 个人物 )}
{selectedPersons.size > 0 && ( <> )}
{/* 人物列表 */}
{/* 桌面端表格视图 */}
0 && selectedPersons.size === persons.length} onCheckedChange={toggleSelectAll} aria-label="全选" /> 状态 名称 昵称 平台 用户ID 最后更新 操作 {loading ? ( 加载中... ) : persons.length === 0 ? ( 暂无数据 ) : ( persons.map((person) => ( togglePersonSelection(person.person_id)} aria-label={`选择 ${person.person_name || person.nickname || person.user_id}`} />
{person.is_known ? '已认识' : '未认识'}
{person.person_name || -} {person.nickname || '-'} {person.platform} {person.user_id} {formatTime(person.last_know)}
)) )}
{/* 移动端卡片视图 */}
{loading ? (
加载中...
) : persons.length === 0 ? (
暂无数据
) : ( persons.map((person) => (
{/* 复选框和状态 */}
togglePersonSelection(person.person_id)} className="mt-1" />
{person.is_known ? '已认识' : '未认识'}

{person.person_name || 未命名}

{person.nickname && (

昵称: {person.nickname}

)}
{/* 平台和用户信息 */}
平台

{person.platform}

用户ID

{person.user_id}

最后更新

{formatTime(person.last_know)}

{/* 操作按钮 */}
)) )}
{/* 分页 - 增强版 */} {total > 0 && (
共 {total} 条记录,第 {page} / {Math.ceil(total / pageSize)} 页
{/* 首页 */} {/* 上一页 */} {/* 页码跳转 */}
setJumpToPage(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleJumpToPage()} placeholder={page.toString()} className="w-16 h-8 text-center" min={1} max={Math.ceil(total / pageSize)} />
{/* 下一页 */} {/* 末页 */}
)}
{/* 详情对话框 */} {/* 编辑对话框 */} { loadPersons() loadStats() setIsEditDialogOpen(false) }} /> {/* 删除确认对话框 */} setDeleteConfirmPerson(null)} > 确认删除 确定要删除人物信息 "{deleteConfirmPerson?.person_name || deleteConfirmPerson?.nickname || deleteConfirmPerson?.user_id}" 吗? 此操作不可撤销。 取消 deleteConfirmPerson && handleDelete(deleteConfirmPerson)} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > 删除 {/* 批量删除确认对话框 */} 确认批量删除 确定要删除选中的 {selectedPersons.size} 个人物信息吗? 此操作不可撤销。 取消 批量删除
) } // 人物详情对话框 function PersonDetailDialog({ person, open, onOpenChange, }: { person: PersonInfo | null open: boolean onOpenChange: (open: boolean) => void }) { if (!person) return null const formatTime = (timestamp: number | null) => { if (!timestamp) return '-' return new Date(timestamp * 1000).toLocaleString('zh-CN') } return ( 人物详情 查看 {person.person_name || person.nickname || person.user_id} 的完整信息
{/* 基本信息 */}
{/* 名称原因 */} {person.name_reason && (

{person.name_reason}

)} {/* 记忆点 */} {person.memory_points && (

{person.memory_points}

)} {/* 群昵称列表 */} {person.group_nick_name && person.group_nick_name.length > 0 && (
{person.group_nick_name.map((item, index) => (
{item.group_id} {item.group_nick_name}
))}
)} {/* 时间信息 */}
) } // 信息项组件 function InfoItem({ icon: Icon, label, value, mono = false, }: { icon?: typeof User label: string value: string | null | undefined mono?: boolean }) { return (
{value || '-'}
) } // 人物编辑对话框 function PersonEditDialog({ person, open, onOpenChange, onSuccess, }: { person: PersonInfo | null open: boolean onOpenChange: (open: boolean) => void onSuccess: () => void }) { const [formData, setFormData] = useState({}) const [saving, setSaving] = useState(false) const { toast } = useToast() useEffect(() => { if (person) { setFormData({ person_name: person.person_name || '', name_reason: person.name_reason || '', nickname: person.nickname || '', is_known: person.is_known, }) } }, [person]) const handleSave = async () => { if (!person) return try { setSaving(true) await updatePerson(person.person_id, formData) toast({ title: '保存成功', description: '人物信息已更新', }) onSuccess() } catch (error) { toast({ title: '保存失败', description: error instanceof Error ? error.message : '无法更新人物信息', variant: 'destructive', }) } finally { setSaving(false) } } if (!person) return null return ( 编辑人物信息 修改 {person.person_name || person.nickname || person.user_id} 的信息
setFormData({ ...formData, person_name: e.target.value })} placeholder="为这个人设置一个名称" />
setFormData({ ...formData, nickname: e.target.value })} placeholder="昵称" />