Files
admin/src/pages/Reports.tsx

826 lines
34 KiB
TypeScript
Raw 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.
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<ReportStatus, string> = {
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<ReportStatus, string> = {
pending: '待处理',
processing: '处理中',
resolved: '已确认',
rejected: '已驳回',
}
// 状态图标映射
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" />,
}
// 举报原因图标和颜色
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: '消息' },
}
// 举报原因文本
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')
// 统计数据
const [stats, setStats] = useState({
pending: 0,
processing: 0,
resolved: 0,
rejected: 0,
})
// 批量选择
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)
// 更新统计数据(仅当无筛选时)
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 (
<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">
<div>
<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>
</div>
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
disabled={loading}
className="bg-white hover:bg-slate-50 border-slate-200"
>
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
</Button>
</div>
{/* 统计卡片 */}
<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>
{/* 筛选卡片 */}
<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>
</CardHeader>
<CardContent>
<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>
</div>
<Select value={typeFilter} onValueChange={setTypeFilter}>
<SelectTrigger className="w-[130px]">
<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}>
<SelectTrigger className="w-[130px]">
<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>
<Button onClick={handleSearch} className="bg-slate-900 hover:bg-slate-800">
<Search className="h-4 w-4 mr-2" />
</Button>
</div>
</CardContent>
</Card>
{/* 举报列表 */}
<Card className="border-slate-200/60 shadow-sm">
<CardHeader className="flex flex-row items-center justify-between pb-4">
<div>
<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>
</div>
{selectedIds.length > 0 && (
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleBatchAction('approve')}
disabled={batchLoading}
className="border-emerald-200 text-emerald-700 hover:bg-emerald-50"
>
{batchLoading ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Check className="h-4 w-4 mr-2" />
)}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleBatchAction('reject')}
disabled={batchLoading}
className="border-red-200 text-red-700 hover:bg-red-50"
>
<X className="h-4 w-4 mr-2" />
</Button>
</div>
)}
</CardHeader>
<CardContent>
<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>
</TableRow>
</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>
</div>
</TableCell>
</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>
</div>
</TableCell>
</TableRow>
) : (
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>
</CardContent>
</Card>
{/* 详情对话框 */}
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Flag className="h-5 w-5 text-red-500" />
</DialogTitle>
</DialogHeader>
{detailLoading ? (
<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>
</div>
) : selectedReport ? (
<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>
</div>
<div>
<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>
)
})()}
</div>
<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>
</div>
</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>
</div>
{selectedReport.description && (
<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>
</div>
)}
</div>
{/* 被举报内容 */}
{selectedReport.target_content && (
<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>
)}
{/* 处理信息 */}
{selectedReport.handler && (
<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">
<AvatarImage src={selectedReport.handler.avatar} />
<AvatarFallback className="text-xs">{selectedReport.handler.nickname?.[0]}</AvatarFallback>
</Avatar>
<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>
</div>
{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>
)}
</div>
)}
</div>
) : null}
</DialogContent>
</Dialog>
{/* 处理对话框 */}
<Dialog open={handleOpen} onOpenChange={setHandleOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Flag className="h-5 w-5 text-yellow-500" />
</DialogTitle>
<DialogDescription className="text-sm">
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<Button
variant={handleAction === 'approve' ? 'default' : 'outline'}
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'
}`}
onClick={() => setHandleAction('approve')}
>
<Check className="h-5 w-5" />
<span></span>
</Button>
<Button
variant={handleAction === 'reject' ? 'default' : 'outline'}
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'
}`}
onClick={() => setHandleAction('reject')}
>
<X className="h-5 w-5" />
<span></span>
</Button>
</div>
<div>
<Label className="text-sm font-medium"></Label>
<Textarea
value={handleResult}
onChange={(e) => setHandleResult(e.target.value)}
placeholder="请输入处理说明,将发送给举报人..."
rows={3}
className="mt-2 resize-none"
/>
</div>
</div>
<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'}
>
{handleLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}