From e7c9ee5159296a8724a699cc1513c7c1e6919e6d Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Sun, 29 Mar 2026 20:13:32 +0800 Subject: [PATCH] Add reports management to Sidebar and API index; introduce new route for reports with appropriate guard --- src/api/index.ts | 2 + src/api/reports.ts | 106 ++++++ src/components/Layout/Sidebar.tsx | 7 + src/pages/Reports.tsx | 568 ++++++++++++++++++++++++++++++ src/router/index.tsx | 11 + 5 files changed, 694 insertions(+) create mode 100644 src/api/reports.ts create mode 100644 src/pages/Reports.tsx diff --git a/src/api/index.ts b/src/api/index.ts index e0c9eea..7494f3a 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -16,3 +16,5 @@ export { dashboardApi } from './dashboard' export type { DashboardStatsData, ActivityTrendData, ContentStatsData, PendingContent } from './dashboard' export { subjectsApi, materialsApi } from './materials' export type { MaterialSubject, MaterialFile, MaterialFileType, SubjectUpsertRequest, MaterialCreateRequest, MaterialUpdateRequest, MaterialQueryParams } from './materials' +export { reportsApi } from './reports' +export type { ReportListItem, ReportDetail, ReportQueryParams, ReportStatus, ReportTargetType, HandleReportRequest, BatchHandleRequest, BatchHandleResponse } from './reports' diff --git a/src/api/reports.ts b/src/api/reports.ts new file mode 100644 index 0000000..1da00b7 --- /dev/null +++ b/src/api/reports.ts @@ -0,0 +1,106 @@ +/** + * 管理端举报 API + */ + +import apiClient from './client'; + +// 举报状态 +export type ReportStatus = 'pending' | 'processing' | 'resolved' | 'rejected'; + +// 举报目标类型 +export type ReportTargetType = 'post' | 'comment' | 'message'; + +// 举报列表项 +export interface ReportListItem { + id: string; + reporter_id: string; + reporter?: { + id: string; + nickname: string; + avatar: string; + }; + target_type: ReportTargetType; + target_id: string; + reason: string; + description?: string; + status: ReportStatus; + handled_by?: string; + handler?: { + id: string; + nickname: string; + avatar: string; + }; + handled_at?: string; + result?: string; + created_at: string; +} + +// 举报详情 +export interface ReportDetail extends ReportListItem { + target_content?: { + type: ReportTargetType; + id: string; + author?: { + id: string; + nickname: string; + avatar: string; + }; + content?: string; + title?: string; + images?: string[]; + status?: string; + created_at?: string; + }; +} + +// 查询参数 +export interface ReportQueryParams { + page?: number; + page_size?: number; + target_type?: ReportTargetType; + status?: ReportStatus; + start_date?: string; + end_date?: string; + keyword?: string; +} + +// 处理举报请求 +export interface HandleReportRequest { + action: 'approve' | 'reject'; + result?: string; +} + +// 批量处理请求 +export interface BatchHandleRequest { + ids: string[]; + action: 'approve' | 'reject'; + result?: string; +} + +// 批量处理响应 +export interface BatchHandleResponse { + success_count: number; + failed_count: number; + failed_ids?: string[]; +} + +// 举报API +export const reportsApi = { + // 获取举报列表 + getReports: (params: ReportQueryParams) => + apiClient.get('/admin/reports', { params }), + + // 获取举报详情 + getReportById: (id: string) => + apiClient.get(`/admin/reports/${id}`), + + // 处理举报 + handleReport: (id: string, data: HandleReportRequest) => + apiClient.put(`/admin/reports/${id}/handle`, data), + + // 批量处理 + batchHandle: (data: BatchHandleRequest) => + apiClient.post('/admin/reports/batch-handle', data), +}; + +export default reportsApi; \ No newline at end of file diff --git a/src/components/Layout/Sidebar.tsx b/src/components/Layout/Sidebar.tsx index 7675f31..84f8f74 100644 --- a/src/components/Layout/Sidebar.tsx +++ b/src/components/Layout/Sidebar.tsx @@ -14,6 +14,7 @@ import { BookOpen, ChevronDown, Layers, + Flag, } from 'lucide-react' import { Button } from '@/components/ui/button' import { useState } from 'react' @@ -68,6 +69,12 @@ const menuItems: MenuItem[] = [ icon: UsersIcon, roles: [ROLES.SUPER_ADMIN, ROLES.ADMIN, ROLES.MODERATOR], }, + { + title: '举报管理', + path: '/reports', + icon: Flag, + roles: [ROLES.SUPER_ADMIN, ROLES.ADMIN, ROLES.MODERATOR], + }, { title: '频道管理', path: '/channels', diff --git a/src/pages/Reports.tsx b/src/pages/Reports.tsx new file mode 100644 index 0000000..539f6e4 --- /dev/null +++ b/src/pages/Reports.tsx @@ -0,0 +1,568 @@ +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, +} from 'lucide-react' +import { format } from 'date-fns' +import type { PaginatedResponse } from '@/types' + +// 状态颜色映射 +const statusColors: Record = { + pending: 'bg-yellow-500', + processing: 'bg-blue-500', + resolved: 'bg-green-500', + rejected: 'bg-gray-500', +} + +// 状态文本映射 +const statusText: Record = { + pending: '待处理', + processing: '处理中', + resolved: '已确认', + rejected: '已驳回', +} + +// 目标类型图标 +const targetTypeIcons: Record = { + post: , + comment: , + message: , +} + +// 目标类型文本 +const targetTypeText: Record = { + post: '帖子', + comment: '评论', + message: '消息', +} + +// 举报原因文本 +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 [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) + } 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 ( +
+ {/* 标题 */} +
+
+

举报管理

+

处理用户举报的帖子、评论和消息

+
+ +
+ + {/* 筛选卡片 */} + + + 筛选条件 + + +
+
+ setKeyword(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + /> +
+ + + +
+
+
+ + {/* 举报列表 */} + + +
+ 举报列表 + 共 {total} 条记录 +
+ {selectedIds.length > 0 && ( +
+ + +
+ )} +
+ + + + + + + + 举报人 + 目标类型 + 举报原因 + 状态 + 举报时间 + 操作 + + + + {loading ? ( + + + + 加载中... + + + ) : reports.length === 0 ? ( + + + 暂无举报记录 + + + ) : ( + reports.map((report) => ( + + + handleSelect(report.id, !!checked)} + /> + + +
+ + + {report.reporter?.nickname?.[0] || '?'} + + {report.reporter?.nickname || '未知用户'} +
+
+ +
+ {targetTypeIcons[report.target_type]} + {targetTypeText[report.target_type]} +
+
+ + {reasonText[report.reason] || report.reason} + {report.description && ( +

{report.description}

+ )} +
+ + + {statusText[report.status]} + + + + {format(new Date(report.created_at), 'yyyy-MM-dd HH:mm')} + + +
+ + {report.status === 'pending' && ( + + )} +
+
+
+ )) + )} +
+
+ +
+
+ + {/* 详情对话框 */} + + + + 举报详情 + + {detailLoading ? ( +
+ +
+ ) : selectedReport ? ( +
+
+
+ +
+ + + {selectedReport.reporter?.nickname?.[0]} + + {selectedReport.reporter?.nickname} +
+
+
+ +

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

+
+
+ +

{targetTypeText[selectedReport.target_type]}

+
+
+ + + {statusText[selectedReport.status]} + +
+
+ +

{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 && ( + + {format(new Date(selectedReport.handled_at), 'yyyy-MM-dd HH:mm')} + + )} +
+
+ )} + + {selectedReport.result && ( +
+ +

{selectedReport.result}

+
+ )} +
+ ) : null} +
+
+ + {/* 处理对话框 */} + + + + 处理举报 + 请选择处理方式 + +
+
+ + +
+
+ +