import { useState, useEffect, useCallback } from 'react' import { reportsApi, type ReportListItem, type ReportDetail, type ReportQueryParams, type ReportStatus, type ReportTargetType } from '@/api' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Badge } from '@/components/ui/badge' import { Checkbox } from '@/components/ui/checkbox' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { PaginationNavigator } from '@/components/ui/pagination' import { Textarea } from '@/components/ui/textarea' import { Label } from '@/components/ui/label' import { Search, RefreshCw, Eye, Check, X, Loader2, Flag, FileText, MessageSquare, Mail, AlertTriangle, Clock, CheckCircle2, XCircle, Filter, ChevronRight, } from 'lucide-react' import { format } from 'date-fns' import type { PaginatedResponse } from '@/types' // 安全的日期格式化函数,处理无效日期 function safeFormat(dateValue: string | number | Date | null | undefined, formatStr: string): string { if (!dateValue) return '-' const date = new Date(dateValue) if (isNaN(date.getTime())) return '-' return format(date, formatStr) } // 状态颜色映射 const statusColors: Record = { pending: 'bg-yellow-100 text-yellow-800 border-yellow-200', processing: 'bg-blue-100 text-blue-800 border-blue-200', resolved: 'bg-emerald-100 text-emerald-800 border-emerald-200', rejected: 'bg-gray-100 text-gray-600 border-gray-200', } // 状态文本映射 const statusText: Record = { pending: '待处理', processing: '处理中', resolved: '已确认', rejected: '已驳回', } // 状态图标映射 const statusIcons: Record = { pending: , processing: , resolved: , rejected: , } // 举报原因图标和颜色 const reasonConfig: Record = { spam: { icon: , color: 'bg-purple-100 text-purple-700' }, inappropriate: { icon: , color: 'bg-red-100 text-red-700' }, harassment: { icon: , color: 'bg-orange-100 text-orange-700' }, misinformation: { icon: , color: 'bg-amber-100 text-amber-700' }, other: { icon: , color: 'bg-gray-100 text-gray-600' }, } // 目标类型配置 const targetTypeConfig: Record = { post: { icon: , color: 'text-blue-600 bg-blue-50', label: '帖子' }, comment: { icon: , color: 'text-green-600 bg-green-50', label: '评论' }, message: { icon: , color: 'text-purple-600 bg-purple-50', label: '消息' }, } // 举报原因文本 const reasonText: Record = { spam: '垃圾广告', inappropriate: '违规内容', harassment: '辱骂/攻击', misinformation: '虚假信息', other: '其他', } export default function Reports() { // 状态 const [reports, setReports] = useState([]) const [loading, setLoading] = useState(true) const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [pageSize] = useState(10) const [keyword, setKeyword] = useState('') const [statusFilter, setStatusFilter] = useState('all') const [typeFilter, setTypeFilter] = useState('all') // 统计数据 const [stats, setStats] = useState({ pending: 0, processing: 0, resolved: 0, rejected: 0, }) // 批量选择 const [selectedIds, setSelectedIds] = useState([]) const [isAllSelected, setIsAllSelected] = useState(false) // 详情对话框 const [detailOpen, setDetailOpen] = useState(false) const [selectedReport, setSelectedReport] = useState(null) const [detailLoading, setDetailLoading] = useState(false) // 处理对话框 const [handleOpen, setHandleOpen] = useState(false) const [handleReport, setHandleReport] = useState(null) const [handleAction, setHandleAction] = useState<'approve' | 'reject'>('approve') const [handleResult, setHandleResult] = useState('') const [handleLoading, setHandleLoading] = useState(false) // 批量操作加载状态 const [batchLoading, setBatchLoading] = useState(false) // 加载举报列表 const loadReports = useCallback(async () => { setLoading(true) try { const params: ReportQueryParams = { page, page_size: pageSize, keyword: keyword || undefined, status: statusFilter !== 'all' ? (statusFilter as ReportStatus) : undefined, target_type: typeFilter !== 'all' ? (typeFilter as ReportTargetType) : undefined, } const response = await reportsApi.getReports(params) const data = response.data.data as PaginatedResponse setReports(data.list || []) setTotal(data.total || 0) // 更新统计数据(仅当无筛选时) if (!keyword && statusFilter === 'all' && typeFilter === 'all') { const newStats = { pending: 0, processing: 0, resolved: 0, rejected: 0 } data.list?.forEach((report: ReportListItem) => { newStats[report.status] = (newStats[report.status] || 0) + 1 }) setStats(newStats) } } catch (error) { console.error('Failed to load reports:', error) } finally { setLoading(false) setSelectedIds([]) setIsAllSelected(false) } }, [page, pageSize, keyword, statusFilter, typeFilter]) useEffect(() => { loadReports() }, [loadReports]) // 处理搜索 const handleSearch = () => { setPage(1) loadReports() } // 处理刷新 const handleRefresh = () => { loadReports() } // 全选/取消全选 const handleSelectAll = (checked: boolean) => { if (checked) { setSelectedIds(reports.map(r => r.id)) setIsAllSelected(true) } else { setSelectedIds([]) setIsAllSelected(false) } } // 单选 const handleSelect = (id: string, checked: boolean) => { if (checked) { setSelectedIds([...selectedIds, id]) } else { setSelectedIds(selectedIds.filter(i => i !== id)) } setIsAllSelected(checked && selectedIds.length === reports.length - 1) } // 查看详情 const handleViewDetail = async (report: ReportListItem) => { setDetailLoading(true) setDetailOpen(true) try { const response = await reportsApi.getReportById(report.id) setSelectedReport(response.data) } catch (error) { console.error('Failed to load report detail:', error) setDetailOpen(false) } finally { setDetailLoading(false) } } // 打开处理对话框 const openHandleDialog = (report: ReportListItem) => { setHandleReport(report) setHandleAction('approve') setHandleResult('') setHandleOpen(true) } // 提交处理 const submitHandle = async () => { if (!handleReport) return setHandleLoading(true) try { await reportsApi.handleReport(handleReport.id, { action: handleAction, result: handleResult || undefined, }) setHandleOpen(false) loadReports() } catch (error) { console.error('Failed to handle report:', error) } finally { setHandleLoading(false) } } // 批量处理 const handleBatchAction = async (action: 'approve' | 'reject') => { if (selectedIds.length === 0) return setBatchLoading(true) try { await reportsApi.batchHandle({ ids: selectedIds, action, }) loadReports() } catch (error) { console.error('Failed to batch handle:', error) } finally { setBatchLoading(false) } } return (
{/* 页面标题 */}

举报管理

处理用户举报的帖子、评论和消息,维护社区环境

{/* 统计卡片 */}

待处理

{stats.pending}

处理中

{stats.processing}

已确认

{stats.resolved}

已驳回

{stats.rejected}

{/* 筛选卡片 */} 筛选条件
setKeyword(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} className="pl-9" />
{/* 举报列表 */}
举报列表 {total} 条记录 {selectedIds.length > 0 && ( · 已选择 {selectedIds.length} 条 )}
{selectedIds.length > 0 && (
)}
举报人 目标类型 举报原因 状态 举报时间 操作 {loading ? (
加载中...
) : reports.length === 0 ? (
暂无举报记录 所有举报都已处理完毕
) : ( reports.map((report) => { const reasonCfg = reasonConfig[report.reason] || reasonConfig.other const typeCfg = targetTypeConfig[report.target_type] || { icon: , color: 'text-slate-600 bg-slate-50', label: report.target_type || '未知', } return ( handleSelect(report.id, !!checked)} className="data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-600" />
{report.reporter?.nickname?.[0] || '?'}
{report.reporter?.nickname || '未知用户'}
{typeCfg.icon} {typeCfg.label}
{reasonCfg.icon} {reasonText[report.reason] || report.reason}
{report.description && (

{report.description}

)}
{statusIcons[report.status]} {statusText[report.status]} {safeFormat(report.created_at, 'yyyy-MM-dd HH:mm')}
{report.status === 'pending' && ( )}
) }) )}
显示第 {(page - 1) * pageSize + 1} - {Math.min(page * pageSize, total)} 条
{/* 详情对话框 */} 举报详情 {detailLoading ? (
加载中...
) : selectedReport ? (
{/* 基本信息 */}

基本信息

{selectedReport.reporter?.nickname?.[0]} {selectedReport.reporter?.nickname}

{safeFormat(selectedReport.created_at, 'yyyy-MM-dd HH:mm:ss')}

{(() => { const detailTypeCfg = targetTypeConfig[selectedReport.target_type] || { icon: , color: 'text-slate-600 bg-slate-50', label: selectedReport.target_type || '未知', } return (
{detailTypeCfg.icon} {detailTypeCfg.label}
) })()}
{statusIcons[selectedReport.status]} {statusText[selectedReport.status]}
{/* 举报原因 */}
{reasonConfig[selectedReport.reason]?.icon || reasonConfig.other.icon} {reasonText[selectedReport.reason] || selectedReport.reason}
{selectedReport.description && (

{selectedReport.description}

)}
{/* 被举报内容 */} {selectedReport.target_content && (

被举报内容

{selectedReport.target_content.title && (

{selectedReport.target_content.title}

)}

{selectedReport.target_content.content}

{selectedReport.target_content.images && selectedReport.target_content.images.length > 0 && (
{selectedReport.target_content.images.map((img, i) => ( ))}
)}
)} {/* 处理信息 */} {selectedReport.handler && (

处理信息

{selectedReport.handler.nickname?.[0]}

{selectedReport.handler.nickname}

{selectedReport.handled_at && (

{safeFormat(selectedReport.handled_at, 'yyyy-MM-dd HH:mm')}

)}
{selectedReport.result && (

{selectedReport.result}

)}
)}
) : null}
{/* 处理对话框 */} 处理举报 请根据举报内容选择处理方式