2026-03-29 20:13:32 +08:00
|
|
|
|
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,
|
2026-03-30 18:01:38 +08:00
|
|
|
|
AlertTriangle,
|
|
|
|
|
|
Clock,
|
|
|
|
|
|
CheckCircle2,
|
|
|
|
|
|
XCircle,
|
|
|
|
|
|
Filter,
|
|
|
|
|
|
ChevronRight,
|
2026-03-29 20:13:32 +08:00
|
|
|
|
} from 'lucide-react'
|
|
|
|
|
|
import { format } from 'date-fns'
|
|
|
|
|
|
import type { PaginatedResponse } from '@/types'
|
|
|
|
|
|
|
2026-03-30 18:01:38 +08:00
|
|
|
|
// 安全的日期格式化函数,处理无效日期
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-29 20:13:32 +08:00
|
|
|
|
// 状态颜色映射
|
|
|
|
|
|
const statusColors: Record<ReportStatus, string> = {
|
2026-03-30 18:01:38 +08:00
|
|
|
|
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',
|
2026-03-29 20:13:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 状态文本映射
|
|
|
|
|
|
const statusText: Record<ReportStatus, string> = {
|
|
|
|
|
|
pending: '待处理',
|
|
|
|
|
|
processing: '处理中',
|
|
|
|
|
|
resolved: '已确认',
|
|
|
|
|
|
rejected: '已驳回',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-30 18:01:38 +08:00
|
|
|
|
// 状态图标映射
|
|
|
|
|
|
const statusIcons: Record<ReportStatus, React.ReactNode> = {
|
|
|
|
|
|
pending: <Clock className="h-3 w-3" />,
|
|
|
|
|
|
processing: <AlertTriangle className="h-3 w-3" />,
|
|
|
|
|
|
resolved: <CheckCircle2 className="h-3 w-3" />,
|
|
|
|
|
|
rejected: <XCircle className="h-3 w-3" />,
|
2026-03-29 20:13:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-30 18:01:38 +08:00
|
|
|
|
// 举报原因图标和颜色
|
|
|
|
|
|
const reasonConfig: Record<string, { icon: React.ReactNode; color: string }> = {
|
|
|
|
|
|
spam: { icon: <Mail className="h-3 w-3" />, color: 'bg-purple-100 text-purple-700' },
|
|
|
|
|
|
inappropriate: { icon: <AlertTriangle className="h-3 w-3" />, color: 'bg-red-100 text-red-700' },
|
|
|
|
|
|
harassment: { icon: <MessageSquare className="h-3 w-3" />, color: 'bg-orange-100 text-orange-700' },
|
|
|
|
|
|
misinformation: { icon: <FileText className="h-3 w-3" />, color: 'bg-amber-100 text-amber-700' },
|
|
|
|
|
|
other: { icon: <Flag className="h-3 w-3" />, color: 'bg-gray-100 text-gray-600' },
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 目标类型配置
|
|
|
|
|
|
const targetTypeConfig: Record<ReportTargetType, { icon: React.ReactNode; color: string; label: string }> = {
|
|
|
|
|
|
post: { icon: <FileText className="h-4 w-4" />, color: 'text-blue-600 bg-blue-50', label: '帖子' },
|
|
|
|
|
|
comment: { icon: <MessageSquare className="h-4 w-4" />, color: 'text-green-600 bg-green-50', label: '评论' },
|
|
|
|
|
|
message: { icon: <Mail className="h-4 w-4" />, color: 'text-purple-600 bg-purple-50', label: '消息' },
|
2026-03-29 20:13:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 举报原因文本
|
|
|
|
|
|
const reasonText: Record<string, string> = {
|
|
|
|
|
|
spam: '垃圾广告',
|
|
|
|
|
|
inappropriate: '违规内容',
|
|
|
|
|
|
harassment: '辱骂/攻击',
|
|
|
|
|
|
misinformation: '虚假信息',
|
|
|
|
|
|
other: '其他',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function Reports() {
|
|
|
|
|
|
// 状态
|
|
|
|
|
|
const [reports, setReports] = useState<ReportListItem[]>([])
|
|
|
|
|
|
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<string>('all')
|
|
|
|
|
|
const [typeFilter, setTypeFilter] = useState<string>('all')
|
|
|
|
|
|
|
2026-03-30 18:01:38 +08:00
|
|
|
|
// 统计数据
|
|
|
|
|
|
const [stats, setStats] = useState({
|
|
|
|
|
|
pending: 0,
|
|
|
|
|
|
processing: 0,
|
|
|
|
|
|
resolved: 0,
|
|
|
|
|
|
rejected: 0,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-29 20:13:32 +08:00
|
|
|
|
// 批量选择
|
|
|
|
|
|
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
|
|
|
|
|
const [isAllSelected, setIsAllSelected] = useState(false)
|
|
|
|
|
|
|
|
|
|
|
|
// 详情对话框
|
|
|
|
|
|
const [detailOpen, setDetailOpen] = useState(false)
|
|
|
|
|
|
const [selectedReport, setSelectedReport] = useState<ReportDetail | null>(null)
|
|
|
|
|
|
const [detailLoading, setDetailLoading] = useState(false)
|
|
|
|
|
|
|
|
|
|
|
|
// 处理对话框
|
|
|
|
|
|
const [handleOpen, setHandleOpen] = useState(false)
|
|
|
|
|
|
const [handleReport, setHandleReport] = useState<ReportListItem | null>(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<ReportListItem>
|
|
|
|
|
|
setReports(data.list || [])
|
|
|
|
|
|
setTotal(data.total || 0)
|
2026-03-30 18:01:38 +08:00
|
|
|
|
|
|
|
|
|
|
// 更新统计数据(仅当无筛选时)
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
2026-03-29 20:13:32 +08:00
|
|
|
|
} 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 (
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<div className="space-y-6 pb-8">
|
|
|
|
|
|
{/* 页面标题 */}
|
|
|
|
|
|
<div className="flex items-center justify-between bg-gradient-to-r from-slate-50 to-white p-6 rounded-2xl border border-slate-200/60 shadow-sm">
|
2026-03-29 20:13:32 +08:00
|
|
|
|
<div>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
<div className="p-2 bg-red-100 rounded-xl">
|
|
|
|
|
|
<Flag className="h-6 w-6 text-red-600" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h1 className="text-2xl font-bold text-slate-900">举报管理</h1>
|
|
|
|
|
|
<p className="text-sm text-slate-500 mt-0.5">处理用户举报的帖子、评论和消息,维护社区环境</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</div>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={handleRefresh}
|
|
|
|
|
|
disabled={loading}
|
|
|
|
|
|
className="bg-white hover:bg-slate-50 border-slate-200"
|
|
|
|
|
|
>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
|
|
|
|
|
刷新
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-30 18:01:38 +08:00
|
|
|
|
{/* 统计卡片 */}
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
|
|
|
|
<Card className="border-l-4 border-l-yellow-400 hover:shadow-md transition-shadow">
|
|
|
|
|
|
<CardContent className="p-4">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-sm font-medium text-slate-500">待处理</p>
|
|
|
|
|
|
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.pending}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="p-2 bg-yellow-100 rounded-lg">
|
|
|
|
|
|
<Clock className="h-5 w-5 text-yellow-600" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Card className="border-l-4 border-l-blue-400 hover:shadow-md transition-shadow">
|
|
|
|
|
|
<CardContent className="p-4">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-sm font-medium text-slate-500">处理中</p>
|
|
|
|
|
|
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.processing}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="p-2 bg-blue-100 rounded-lg">
|
|
|
|
|
|
<AlertTriangle className="h-5 w-5 text-blue-600" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Card className="border-l-4 border-l-emerald-400 hover:shadow-md transition-shadow">
|
|
|
|
|
|
<CardContent className="p-4">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-sm font-medium text-slate-500">已确认</p>
|
|
|
|
|
|
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.resolved}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="p-2 bg-emerald-100 rounded-lg">
|
|
|
|
|
|
<CheckCircle2 className="h-5 w-5 text-emerald-600" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Card className="border-l-4 border-l-gray-400 hover:shadow-md transition-shadow">
|
|
|
|
|
|
<CardContent className="p-4">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-sm font-medium text-slate-500">已驳回</p>
|
|
|
|
|
|
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.rejected}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="p-2 bg-gray-100 rounded-lg">
|
|
|
|
|
|
<XCircle className="h-5 w-5 text-gray-600" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-29 20:13:32 +08:00
|
|
|
|
{/* 筛选卡片 */}
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<Card className="border-slate-200/60 shadow-sm">
|
|
|
|
|
|
<CardHeader className="pb-3">
|
|
|
|
|
|
<CardTitle className="text-base font-semibold flex items-center gap-2">
|
|
|
|
|
|
<Filter className="h-4 w-4" />
|
|
|
|
|
|
筛选条件
|
|
|
|
|
|
</CardTitle>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<div className="flex flex-wrap gap-3">
|
|
|
|
|
|
<div className="flex-1 min-w-[240px]">
|
|
|
|
|
|
<div className="relative">
|
|
|
|
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="搜索举报内容、举报人..."
|
|
|
|
|
|
value={keyword}
|
|
|
|
|
|
onChange={(e) => setKeyword(e.target.value)}
|
|
|
|
|
|
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
|
|
|
|
|
className="pl-9"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<SelectTrigger className="w-[130px]">
|
2026-03-29 20:13:32 +08:00
|
|
|
|
<SelectValue placeholder="目标类型" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="all">全部类型</SelectItem>
|
|
|
|
|
|
<SelectItem value="post">帖子</SelectItem>
|
|
|
|
|
|
<SelectItem value="comment">评论</SelectItem>
|
|
|
|
|
|
<SelectItem value="message">消息</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<SelectTrigger className="w-[130px]">
|
2026-03-29 20:13:32 +08:00
|
|
|
|
<SelectValue placeholder="处理状态" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="all">全部状态</SelectItem>
|
|
|
|
|
|
<SelectItem value="pending">待处理</SelectItem>
|
|
|
|
|
|
<SelectItem value="processing">处理中</SelectItem>
|
|
|
|
|
|
<SelectItem value="resolved">已确认</SelectItem>
|
|
|
|
|
|
<SelectItem value="rejected">已驳回</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<Button onClick={handleSearch} className="bg-slate-900 hover:bg-slate-800">
|
2026-03-29 20:13:32 +08:00
|
|
|
|
<Search className="h-4 w-4 mr-2" />
|
|
|
|
|
|
搜索
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 举报列表 */}
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<Card className="border-slate-200/60 shadow-sm">
|
|
|
|
|
|
<CardHeader className="flex flex-row items-center justify-between pb-4">
|
2026-03-29 20:13:32 +08:00
|
|
|
|
<div>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<CardTitle className="text-base font-semibold">举报列表</CardTitle>
|
|
|
|
|
|
<CardDescription className="text-sm mt-1">
|
|
|
|
|
|
共 <span className="font-semibold text-slate-900">{total}</span> 条记录
|
|
|
|
|
|
{selectedIds.length > 0 && (
|
|
|
|
|
|
<span className="ml-2 text-blue-600 font-medium">
|
|
|
|
|
|
· 已选择 {selectedIds.length} 条
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</CardDescription>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
{selectedIds.length > 0 && (
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => handleBatchAction('approve')}
|
|
|
|
|
|
disabled={batchLoading}
|
2026-03-30 18:01:38 +08:00
|
|
|
|
className="border-emerald-200 text-emerald-700 hover:bg-emerald-50"
|
2026-03-29 20:13:32 +08:00
|
|
|
|
>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
{batchLoading ? (
|
|
|
|
|
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Check className="h-4 w-4 mr-2" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
批量确认
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => handleBatchAction('reject')}
|
|
|
|
|
|
disabled={batchLoading}
|
2026-03-30 18:01:38 +08:00
|
|
|
|
className="border-red-200 text-red-700 hover:bg-red-50"
|
2026-03-29 20:13:32 +08:00
|
|
|
|
>
|
|
|
|
|
|
<X className="h-4 w-4 mr-2" />
|
|
|
|
|
|
批量驳回
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<div className="rounded-lg border border-slate-200">
|
|
|
|
|
|
<Table>
|
|
|
|
|
|
<TableHeader>
|
|
|
|
|
|
<TableRow className="bg-slate-50/50 hover:bg-slate-50">
|
|
|
|
|
|
<TableHead className="w-10">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
checked={isAllSelected}
|
|
|
|
|
|
onCheckedChange={handleSelectAll}
|
|
|
|
|
|
className="data-[state=checked]:bg-slate-900 data-[state=checked]:border-slate-900"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TableHead>
|
|
|
|
|
|
<TableHead className="font-semibold text-slate-700">举报人</TableHead>
|
|
|
|
|
|
<TableHead className="font-semibold text-slate-700">目标类型</TableHead>
|
|
|
|
|
|
<TableHead className="font-semibold text-slate-700">举报原因</TableHead>
|
|
|
|
|
|
<TableHead className="font-semibold text-slate-700">状态</TableHead>
|
|
|
|
|
|
<TableHead className="font-semibold text-slate-700">举报时间</TableHead>
|
|
|
|
|
|
<TableHead className="font-semibold text-slate-700 text-right">操作</TableHead>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</TableRow>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
</TableHeader>
|
|
|
|
|
|
<TableBody>
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
|
<TableRow>
|
|
|
|
|
|
<TableCell colSpan={7} className="text-center py-12">
|
|
|
|
|
|
<div className="flex flex-col items-center gap-3">
|
|
|
|
|
|
<Loader2 className="h-8 w-8 animate-spin text-slate-400" />
|
|
|
|
|
|
<span className="text-slate-500">加载中...</span>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</TableCell>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
</TableRow>
|
|
|
|
|
|
) : reports.length === 0 ? (
|
|
|
|
|
|
<TableRow>
|
|
|
|
|
|
<TableCell colSpan={7} className="text-center py-12">
|
|
|
|
|
|
<div className="flex flex-col items-center gap-2">
|
|
|
|
|
|
<div className="p-3 bg-slate-100 rounded-full">
|
|
|
|
|
|
<Flag className="h-6 w-6 text-slate-400" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span className="text-slate-500 font-medium">暂无举报记录</span>
|
|
|
|
|
|
<span className="text-slate-400 text-sm">所有举报都已处理完毕</span>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
) : (
|
|
|
|
|
|
reports.map((report) => {
|
|
|
|
|
|
const reasonCfg = reasonConfig[report.reason] || reasonConfig.other
|
|
|
|
|
|
const typeCfg = targetTypeConfig[report.target_type] || {
|
|
|
|
|
|
icon: <FileText className="h-4 w-4" />,
|
|
|
|
|
|
color: 'text-slate-600 bg-slate-50',
|
|
|
|
|
|
label: report.target_type || '未知',
|
|
|
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
|
|
|
<TableRow
|
|
|
|
|
|
key={report.id}
|
|
|
|
|
|
className={`group transition-colors ${
|
|
|
|
|
|
selectedIds.includes(report.id) ? 'bg-blue-50/40' : 'hover:bg-slate-50/60'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
checked={selectedIds.includes(report.id)}
|
|
|
|
|
|
onCheckedChange={(checked) => handleSelect(report.id, !!checked)}
|
|
|
|
|
|
className="data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-600"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
<div className="flex items-center gap-2.5">
|
|
|
|
|
|
<Avatar className="h-9 w-9 ring-2 ring-white">
|
|
|
|
|
|
<AvatarImage src={report.reporter?.avatar} />
|
|
|
|
|
|
<AvatarFallback className="bg-slate-100 text-slate-600 text-sm font-medium">
|
|
|
|
|
|
{report.reporter?.nickname?.[0] || '?'}
|
|
|
|
|
|
</AvatarFallback>
|
|
|
|
|
|
</Avatar>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span className="font-medium text-slate-900">
|
|
|
|
|
|
{report.reporter?.nickname || '未知用户'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
<div className={`flex items-center gap-2 px-2.5 py-1 rounded-md w-fit ${typeCfg.color}`}>
|
|
|
|
|
|
{typeCfg.icon}
|
|
|
|
|
|
<span className="text-sm font-medium">{typeCfg.label}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
<div className="flex flex-col gap-1">
|
|
|
|
|
|
<div className={`flex items-center gap-2 w-fit px-2 py-0.5 rounded text-sm font-medium ${reasonCfg.color}`}>
|
|
|
|
|
|
{reasonCfg.icon}
|
|
|
|
|
|
<span>{reasonText[report.reason] || report.reason}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{report.description && (
|
|
|
|
|
|
<p className="text-xs text-slate-500 line-clamp-1 max-w-[200px]">
|
|
|
|
|
|
{report.description}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
<Badge
|
|
|
|
|
|
className={`flex items-center gap-1.5 px-2.5 py-1 border ${statusColors[report.status]}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{statusIcons[report.status]}
|
|
|
|
|
|
<span className="font-medium">{statusText[report.status]}</span>
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
<span className="text-sm text-slate-600 font-mono">
|
|
|
|
|
|
{safeFormat(report.created_at, 'yyyy-MM-dd HH:mm')}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
<div className="flex items-center justify-end gap-1">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => handleViewDetail(report)}
|
|
|
|
|
|
className="h-8 w-8 p-0 hover:bg-slate-100"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Eye className="h-4 w-4 text-slate-600" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
{report.status === 'pending' && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => openHandleDialog(report)}
|
|
|
|
|
|
className="h-8 w-8 p-0 hover:bg-yellow-50"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Flag className="h-4 w-4 text-yellow-600" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
className="h-8 w-8 p-0 opacity-0 group-hover:opacity-100 transition-opacity hover:bg-slate-100"
|
|
|
|
|
|
>
|
|
|
|
|
|
<ChevronRight className="h-4 w-4 text-slate-400" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
)}
|
|
|
|
|
|
</TableBody>
|
|
|
|
|
|
</Table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="mt-4 flex items-center justify-between">
|
|
|
|
|
|
<div className="text-sm text-slate-500">
|
|
|
|
|
|
显示第 {(page - 1) * pageSize + 1} - {Math.min(page * pageSize, total)} 条
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<PaginationNavigator
|
|
|
|
|
|
currentPage={page}
|
|
|
|
|
|
totalPages={Math.ceil(total / pageSize)}
|
|
|
|
|
|
onPageChange={setPage}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 详情对话框 */}
|
|
|
|
|
|
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
2026-03-29 20:13:32 +08:00
|
|
|
|
<DialogHeader>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<DialogTitle className="flex items-center gap-2">
|
|
|
|
|
|
<Flag className="h-5 w-5 text-red-500" />
|
|
|
|
|
|
举报详情
|
|
|
|
|
|
</DialogTitle>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
{detailLoading ? (
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<div className="flex justify-center py-12">
|
|
|
|
|
|
<div className="flex flex-col items-center gap-3">
|
|
|
|
|
|
<Loader2 className="h-8 w-8 animate-spin text-slate-400" />
|
|
|
|
|
|
<span className="text-slate-500">加载中...</span>
|
|
|
|
|
|
</div>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
) : selectedReport ? (
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<div className="space-y-5">
|
|
|
|
|
|
{/* 基本信息 */}
|
|
|
|
|
|
<div className="p-4 bg-slate-50 rounded-xl">
|
|
|
|
|
|
<h4 className="text-sm font-semibold text-slate-700 mb-3">基本信息</h4>
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-xs text-slate-500">举报人</Label>
|
|
|
|
|
|
<div className="flex items-center gap-2 mt-1.5">
|
|
|
|
|
|
<Avatar className="h-7 w-7">
|
|
|
|
|
|
<AvatarImage src={selectedReport.reporter?.avatar} />
|
|
|
|
|
|
<AvatarFallback className="text-xs">{selectedReport.reporter?.nickname?.[0]}</AvatarFallback>
|
|
|
|
|
|
</Avatar>
|
|
|
|
|
|
<span className="text-sm font-medium text-slate-900">
|
|
|
|
|
|
{selectedReport.reporter?.nickname}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-xs text-slate-500">举报时间</Label>
|
|
|
|
|
|
<p className="text-sm font-mono text-slate-700 mt-1.5">
|
|
|
|
|
|
{safeFormat(selectedReport.created_at, 'yyyy-MM-dd HH:mm:ss')}
|
|
|
|
|
|
</p>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<Label className="text-xs text-slate-500">目标类型</Label>
|
|
|
|
|
|
{(() => {
|
|
|
|
|
|
const detailTypeCfg = targetTypeConfig[selectedReport.target_type] || {
|
|
|
|
|
|
icon: <FileText className="h-4 w-4" />,
|
|
|
|
|
|
color: 'text-slate-600 bg-slate-50',
|
|
|
|
|
|
label: selectedReport.target_type || '未知',
|
|
|
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className={`flex items-center gap-2 mt-1.5 w-fit px-2 py-0.5 rounded ${detailTypeCfg.color}`}>
|
|
|
|
|
|
{detailTypeCfg.icon}
|
|
|
|
|
|
<span className="text-sm font-medium">{detailTypeCfg.label}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})()}
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</div>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-xs text-slate-500">处理状态</Label>
|
|
|
|
|
|
<div className="mt-1.5">
|
|
|
|
|
|
<Badge className={`flex items-center gap-1.5 ${statusColors[selectedReport.status]}`}>
|
|
|
|
|
|
{statusIcons[selectedReport.status]}
|
|
|
|
|
|
<span className="font-medium">{statusText[selectedReport.status]}</span>
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</div>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 举报原因 */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-xs text-slate-500">举报原因</Label>
|
|
|
|
|
|
<div className={`flex items-center gap-2 mt-2 w-fit px-3 py-1.5 rounded-lg ${reasonConfig[selectedReport.reason]?.color || reasonConfig.other.color}`}>
|
|
|
|
|
|
{reasonConfig[selectedReport.reason]?.icon || reasonConfig.other.icon}
|
|
|
|
|
|
<span className="text-sm font-medium">
|
|
|
|
|
|
{reasonText[selectedReport.reason] || selectedReport.reason}
|
|
|
|
|
|
</span>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
{selectedReport.description && (
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<div className="mt-3 p-3 bg-amber-50 border border-amber-200 rounded-lg">
|
|
|
|
|
|
<Label className="text-xs text-amber-700">详细描述</Label>
|
|
|
|
|
|
<p className="text-sm text-amber-900 mt-1">{selectedReport.description}</p>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-30 18:01:38 +08:00
|
|
|
|
{/* 被举报内容 */}
|
2026-03-29 20:13:32 +08:00
|
|
|
|
{selectedReport.target_content && (
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<div className="p-4 bg-slate-50 rounded-xl">
|
|
|
|
|
|
<h4 className="text-sm font-semibold text-slate-700 mb-3">被举报内容</h4>
|
|
|
|
|
|
{selectedReport.target_content.title && (
|
|
|
|
|
|
<p className="font-semibold text-slate-900 mb-2">{selectedReport.target_content.title}</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<p className="text-sm text-slate-600 leading-relaxed">{selectedReport.target_content.content}</p>
|
|
|
|
|
|
{selectedReport.target_content.images && selectedReport.target_content.images.length > 0 && (
|
|
|
|
|
|
<div className="flex flex-wrap gap-2 mt-3">
|
|
|
|
|
|
{selectedReport.target_content.images.map((img, i) => (
|
|
|
|
|
|
<img
|
|
|
|
|
|
key={i}
|
|
|
|
|
|
src={img}
|
|
|
|
|
|
alt=""
|
|
|
|
|
|
className="w-20 h-20 object-cover rounded-lg cursor-pointer hover:opacity-80 transition-opacity ring-1 ring-slate-200"
|
|
|
|
|
|
/>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
)}
|
|
|
|
|
|
|
2026-03-30 18:01:38 +08:00
|
|
|
|
{/* 处理信息 */}
|
2026-03-29 20:13:32 +08:00
|
|
|
|
{selectedReport.handler && (
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<div className="p-4 bg-slate-50 rounded-xl">
|
|
|
|
|
|
<h4 className="text-sm font-semibold text-slate-700 mb-3">处理信息</h4>
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
<Avatar className="h-8 w-8">
|
2026-03-29 20:13:32 +08:00
|
|
|
|
<AvatarImage src={selectedReport.handler.avatar} />
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<AvatarFallback className="text-xs">{selectedReport.handler.nickname?.[0]}</AvatarFallback>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</Avatar>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-sm font-medium text-slate-900">{selectedReport.handler.nickname}</p>
|
|
|
|
|
|
{selectedReport.handled_at && (
|
|
|
|
|
|
<p className="text-xs text-slate-500 font-mono mt-0.5">
|
|
|
|
|
|
{safeFormat(selectedReport.handled_at, 'yyyy-MM-dd HH:mm')}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</div>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
{selectedReport.result && (
|
|
|
|
|
|
<div className="mt-3 p-3 bg-white border border-slate-200 rounded-lg">
|
|
|
|
|
|
<Label className="text-xs text-slate-500">处理结果</Label>
|
|
|
|
|
|
<p className="text-sm text-slate-700 mt-1">{selectedReport.result}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</DialogContent>
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 处理对话框 */}
|
|
|
|
|
|
<Dialog open={handleOpen} onOpenChange={setHandleOpen}>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<DialogContent className="max-w-md">
|
2026-03-29 20:13:32 +08:00
|
|
|
|
<DialogHeader>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<DialogTitle className="flex items-center gap-2">
|
|
|
|
|
|
<Flag className="h-5 w-5 text-yellow-500" />
|
|
|
|
|
|
处理举报
|
|
|
|
|
|
</DialogTitle>
|
|
|
|
|
|
<DialogDescription className="text-sm">
|
|
|
|
|
|
请根据举报内容选择处理方式
|
|
|
|
|
|
</DialogDescription>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
<div className="space-y-4">
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
2026-03-29 20:13:32 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
variant={handleAction === 'approve' ? 'default' : 'outline'}
|
2026-03-30 18:01:38 +08:00
|
|
|
|
className={`h-20 flex-col gap-2 ${
|
|
|
|
|
|
handleAction === 'approve'
|
|
|
|
|
|
? 'bg-red-600 hover:bg-red-700'
|
|
|
|
|
|
: 'border-slate-200 hover:bg-red-50 hover:text-red-600 hover:border-red-200'
|
|
|
|
|
|
}`}
|
2026-03-29 20:13:32 +08:00
|
|
|
|
onClick={() => setHandleAction('approve')}
|
|
|
|
|
|
>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<Check className="h-5 w-5" />
|
|
|
|
|
|
<span>确认违规</span>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant={handleAction === 'reject' ? 'default' : 'outline'}
|
2026-03-30 18:01:38 +08:00
|
|
|
|
className={`h-20 flex-col gap-2 ${
|
|
|
|
|
|
handleAction === 'reject'
|
|
|
|
|
|
? 'bg-emerald-600 hover:bg-emerald-700'
|
|
|
|
|
|
: 'border-slate-200 hover:bg-emerald-50 hover:text-emerald-600 hover:border-emerald-200'
|
|
|
|
|
|
}`}
|
2026-03-29 20:13:32 +08:00
|
|
|
|
onClick={() => setHandleAction('reject')}
|
|
|
|
|
|
>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<X className="h-5 w-5" />
|
|
|
|
|
|
<span>驳回举报</span>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<Label className="text-sm font-medium">处理说明(选填)</Label>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
<Textarea
|
|
|
|
|
|
value={handleResult}
|
|
|
|
|
|
onChange={(e) => setHandleResult(e.target.value)}
|
2026-03-30 18:01:38 +08:00
|
|
|
|
placeholder="请输入处理说明,将发送给举报人..."
|
2026-03-29 20:13:32 +08:00
|
|
|
|
rows={3}
|
2026-03-30 18:01:38 +08:00
|
|
|
|
className="mt-2 resize-none"
|
2026-03-29 20:13:32 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-30 18:01:38 +08:00
|
|
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
|
|
|
|
<Button variant="outline" onClick={() => setHandleOpen(false)} className="border-slate-200">
|
|
|
|
|
|
取消
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
onClick={submitHandle}
|
|
|
|
|
|
disabled={handleLoading}
|
|
|
|
|
|
className={handleAction === 'approve' ? 'bg-red-600 hover:bg-red-700' : 'bg-emerald-600 hover:bg-emerald-700'}
|
|
|
|
|
|
>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
{handleLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
|
|
|
|
|
确认提交
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
|
</DialogContent>
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|