Enhance Reports page with safe date formatting, improved status colors and icons, and add statistics display for report statuses.
This commit is contained in:
@@ -43,16 +43,30 @@ import {
|
|||||||
FileText,
|
FileText,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Mail,
|
Mail,
|
||||||
|
AlertTriangle,
|
||||||
|
Clock,
|
||||||
|
CheckCircle2,
|
||||||
|
XCircle,
|
||||||
|
Filter,
|
||||||
|
ChevronRight,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import type { PaginatedResponse } from '@/types'
|
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> = {
|
const statusColors: Record<ReportStatus, string> = {
|
||||||
pending: 'bg-yellow-500',
|
pending: 'bg-yellow-100 text-yellow-800 border-yellow-200',
|
||||||
processing: 'bg-blue-500',
|
processing: 'bg-blue-100 text-blue-800 border-blue-200',
|
||||||
resolved: 'bg-green-500',
|
resolved: 'bg-emerald-100 text-emerald-800 border-emerald-200',
|
||||||
rejected: 'bg-gray-500',
|
rejected: 'bg-gray-100 text-gray-600 border-gray-200',
|
||||||
}
|
}
|
||||||
|
|
||||||
// 状态文本映射
|
// 状态文本映射
|
||||||
@@ -63,18 +77,28 @@ const statusText: Record<ReportStatus, string> = {
|
|||||||
rejected: '已驳回',
|
rejected: '已驳回',
|
||||||
}
|
}
|
||||||
|
|
||||||
// 目标类型图标
|
// 状态图标映射
|
||||||
const targetTypeIcons: Record<ReportTargetType, React.ReactNode> = {
|
const statusIcons: Record<ReportStatus, React.ReactNode> = {
|
||||||
post: <FileText className="h-4 w-4" />,
|
pending: <Clock className="h-3 w-3" />,
|
||||||
comment: <MessageSquare className="h-4 w-4" />,
|
processing: <AlertTriangle className="h-3 w-3" />,
|
||||||
message: <Mail className="h-4 w-4" />,
|
resolved: <CheckCircle2 className="h-3 w-3" />,
|
||||||
|
rejected: <XCircle className="h-3 w-3" />,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 目标类型文本
|
// 举报原因图标和颜色
|
||||||
const targetTypeText: Record<ReportTargetType, string> = {
|
const reasonConfig: Record<string, { icon: React.ReactNode; color: string }> = {
|
||||||
post: '帖子',
|
spam: { icon: <Mail className="h-3 w-3" />, color: 'bg-purple-100 text-purple-700' },
|
||||||
comment: '评论',
|
inappropriate: { icon: <AlertTriangle className="h-3 w-3" />, color: 'bg-red-100 text-red-700' },
|
||||||
message: '消息',
|
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: '消息' },
|
||||||
}
|
}
|
||||||
|
|
||||||
// 举报原因文本
|
// 举报原因文本
|
||||||
@@ -97,6 +121,14 @@ export default function Reports() {
|
|||||||
const [statusFilter, setStatusFilter] = useState<string>('all')
|
const [statusFilter, setStatusFilter] = useState<string>('all')
|
||||||
const [typeFilter, setTypeFilter] = 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 [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||||
const [isAllSelected, setIsAllSelected] = useState(false)
|
const [isAllSelected, setIsAllSelected] = useState(false)
|
||||||
@@ -131,6 +163,15 @@ export default function Reports() {
|
|||||||
const data = response.data.data as PaginatedResponse<ReportListItem>
|
const data = response.data.data as PaginatedResponse<ReportListItem>
|
||||||
setReports(data.list || [])
|
setReports(data.list || [])
|
||||||
setTotal(data.total || 0)
|
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) {
|
} catch (error) {
|
||||||
console.error('Failed to load reports:', error)
|
console.error('Failed to load reports:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -235,36 +276,115 @@ export default function Reports() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6 pb-8">
|
||||||
{/* 标题 */}
|
{/* 页面标题 */}
|
||||||
<div className="flex items-center justify-between">
|
<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>
|
||||||
<h1 className="text-2xl font-bold">举报管理</h1>
|
<div className="flex items-center gap-3">
|
||||||
<p className="text-muted-foreground">处理用户举报的帖子、评论和消息</p>
|
<div className="p-2 bg-red-100 rounded-xl">
|
||||||
|
<Flag className="h-6 w-6 text-red-600" />
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={loading}>
|
<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' : ''}`} />
|
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
<Card className="border-slate-200/60 shadow-sm">
|
||||||
<CardHeader>
|
<CardHeader className="pb-3">
|
||||||
<CardTitle>筛选条件</CardTitle>
|
<CardTitle className="text-base font-semibold flex items-center gap-2">
|
||||||
|
<Filter className="h-4 w-4" />
|
||||||
|
筛选条件
|
||||||
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-3">
|
||||||
<div className="flex-1 min-w-[200px]">
|
<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
|
<Input
|
||||||
placeholder="搜索举报内容..."
|
placeholder="搜索举报内容、举报人..."
|
||||||
value={keyword}
|
value={keyword}
|
||||||
onChange={(e) => setKeyword(e.target.value)}
|
onChange={(e) => setKeyword(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||||
|
className="pl-9"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
||||||
<SelectTrigger className="w-[140px]">
|
<SelectTrigger className="w-[130px]">
|
||||||
<SelectValue placeholder="目标类型" />
|
<SelectValue placeholder="目标类型" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -275,7 +395,7 @@ export default function Reports() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||||
<SelectTrigger className="w-[140px]">
|
<SelectTrigger className="w-[130px]">
|
||||||
<SelectValue placeholder="处理状态" />
|
<SelectValue placeholder="处理状态" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -286,7 +406,7 @@ export default function Reports() {
|
|||||||
<SelectItem value="rejected">已驳回</SelectItem>
|
<SelectItem value="rejected">已驳回</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button onClick={handleSearch}>
|
<Button onClick={handleSearch} className="bg-slate-900 hover:bg-slate-800">
|
||||||
<Search className="h-4 w-4 mr-2" />
|
<Search className="h-4 w-4 mr-2" />
|
||||||
搜索
|
搜索
|
||||||
</Button>
|
</Button>
|
||||||
@@ -295,11 +415,18 @@ export default function Reports() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 举报列表 */}
|
{/* 举报列表 */}
|
||||||
<Card>
|
<Card className="border-slate-200/60 shadow-sm">
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between pb-4">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>举报列表</CardTitle>
|
<CardTitle className="text-base font-semibold">举报列表</CardTitle>
|
||||||
<CardDescription>共 {total} 条记录</CardDescription>
|
<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>
|
</div>
|
||||||
{selectedIds.length > 0 && (
|
{selectedIds.length > 0 && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -308,15 +435,21 @@ export default function Reports() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleBatchAction('approve')}
|
onClick={() => handleBatchAction('approve')}
|
||||||
disabled={batchLoading}
|
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" />}
|
{batchLoading ? (
|
||||||
批量确认 ({selectedIds.length})
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Check className="h-4 w-4 mr-2" />
|
||||||
|
)}
|
||||||
|
批量确认
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleBatchAction('reject')}
|
onClick={() => handleBatchAction('reject')}
|
||||||
disabled={batchLoading}
|
disabled={batchLoading}
|
||||||
|
className="border-red-200 text-red-700 hover:bg-red-50"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4 mr-2" />
|
<X className="h-4 w-4 mr-2" />
|
||||||
批量驳回
|
批量驳回
|
||||||
@@ -325,192 +458,297 @@ export default function Reports() {
|
|||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
<div className="rounded-lg border border-slate-200">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow className="bg-slate-50/50 hover:bg-slate-50">
|
||||||
<TableHead className="w-12">
|
<TableHead className="w-10">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isAllSelected}
|
checked={isAllSelected}
|
||||||
onCheckedChange={handleSelectAll}
|
onCheckedChange={handleSelectAll}
|
||||||
|
className="data-[state=checked]:bg-slate-900 data-[state=checked]:border-slate-900"
|
||||||
/>
|
/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>举报人</TableHead>
|
<TableHead className="font-semibold text-slate-700">举报人</TableHead>
|
||||||
<TableHead>目标类型</TableHead>
|
<TableHead className="font-semibold text-slate-700">目标类型</TableHead>
|
||||||
<TableHead>举报原因</TableHead>
|
<TableHead className="font-semibold text-slate-700">举报原因</TableHead>
|
||||||
<TableHead>状态</TableHead>
|
<TableHead className="font-semibold text-slate-700">状态</TableHead>
|
||||||
<TableHead>举报时间</TableHead>
|
<TableHead className="font-semibold text-slate-700">举报时间</TableHead>
|
||||||
<TableHead>操作</TableHead>
|
<TableHead className="font-semibold text-slate-700 text-right">操作</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={7} className="text-center py-8">
|
<TableCell colSpan={7} className="text-center py-12">
|
||||||
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
|
<div className="flex flex-col items-center gap-3">
|
||||||
<span className="text-muted-foreground mt-2 block">加载中...</span>
|
<Loader2 className="h-8 w-8 animate-spin text-slate-400" />
|
||||||
|
<span className="text-slate-500">加载中...</span>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : reports.length === 0 ? (
|
) : reports.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
|
<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>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
reports.map((report) => (
|
reports.map((report) => {
|
||||||
<TableRow key={report.id}>
|
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>
|
<TableCell>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedIds.includes(report.id)}
|
checked={selectedIds.includes(report.id)}
|
||||||
onCheckedChange={(checked) => handleSelect(report.id, !!checked)}
|
onCheckedChange={(checked) => handleSelect(report.id, !!checked)}
|
||||||
|
className="data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-600"
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2.5">
|
||||||
<Avatar className="h-8 w-8">
|
<Avatar className="h-9 w-9 ring-2 ring-white">
|
||||||
<AvatarImage src={report.reporter?.avatar} />
|
<AvatarImage src={report.reporter?.avatar} />
|
||||||
<AvatarFallback>{report.reporter?.nickname?.[0] || '?'}</AvatarFallback>
|
<AvatarFallback className="bg-slate-100 text-slate-600 text-sm font-medium">
|
||||||
|
{report.reporter?.nickname?.[0] || '?'}
|
||||||
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<span>{report.reporter?.nickname || '未知用户'}</span>
|
<div>
|
||||||
|
<span className="font-medium text-slate-900">
|
||||||
|
{report.reporter?.nickname || '未知用户'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-1">
|
<div className={`flex items-center gap-2 px-2.5 py-1 rounded-md w-fit ${typeCfg.color}`}>
|
||||||
{targetTypeIcons[report.target_type]}
|
{typeCfg.icon}
|
||||||
<span>{targetTypeText[report.target_type]}</span>
|
<span className="text-sm font-medium">{typeCfg.label}</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<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>
|
<span>{reasonText[report.reason] || report.reason}</span>
|
||||||
|
</div>
|
||||||
{report.description && (
|
{report.description && (
|
||||||
<p className="text-xs text-muted-foreground mt-1 line-clamp-1">{report.description}</p>
|
<p className="text-xs text-slate-500 line-clamp-1 max-w-[200px]">
|
||||||
|
{report.description}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge className={statusColors[report.status]}>
|
<Badge
|
||||||
{statusText[report.status]}
|
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>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{format(new Date(report.created_at), 'yyyy-MM-dd HH:mm')}
|
<span className="text-sm text-slate-600 font-mono">
|
||||||
|
{safeFormat(report.created_at, 'yyyy-MM-dd HH:mm')}
|
||||||
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex gap-1">
|
<div className="flex items-center justify-end gap-1">
|
||||||
<Button variant="ghost" size="sm" onClick={() => handleViewDetail(report)}>
|
<Button
|
||||||
<Eye className="h-4 w-4" />
|
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>
|
</Button>
|
||||||
{report.status === 'pending' && (
|
{report.status === 'pending' && (
|
||||||
<Button variant="ghost" size="sm" onClick={() => openHandleDialog(report)}>
|
<Button
|
||||||
<Flag className="h-4 w-4" />
|
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>
|
||||||
)}
|
)}
|
||||||
|
<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>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
)
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</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
|
<PaginationNavigator
|
||||||
currentPage={page}
|
currentPage={page}
|
||||||
totalPages={Math.ceil(total / pageSize)}
|
totalPages={Math.ceil(total / pageSize)}
|
||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 详情对话框 */}
|
{/* 详情对话框 */}
|
||||||
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>举报详情</DialogTitle>
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Flag className="h-5 w-5 text-red-500" />
|
||||||
|
举报详情
|
||||||
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{detailLoading ? (
|
{detailLoading ? (
|
||||||
<div className="flex justify-center py-8">
|
<div className="flex justify-center py-12">
|
||||||
<Loader2 className="h-6 w-6 animate-spin" />
|
<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>
|
</div>
|
||||||
) : selectedReport ? (
|
) : selectedReport ? (
|
||||||
<div className="space-y-4">
|
<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 className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-muted-foreground">举报人</Label>
|
<Label className="text-xs text-slate-500">举报人</Label>
|
||||||
<div className="flex items-center gap-2 mt-1">
|
<div className="flex items-center gap-2 mt-1.5">
|
||||||
<Avatar className="h-8 w-8">
|
<Avatar className="h-7 w-7">
|
||||||
<AvatarImage src={selectedReport.reporter?.avatar} />
|
<AvatarImage src={selectedReport.reporter?.avatar} />
|
||||||
<AvatarFallback>{selectedReport.reporter?.nickname?.[0]}</AvatarFallback>
|
<AvatarFallback className="text-xs">{selectedReport.reporter?.nickname?.[0]}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<span>{selectedReport.reporter?.nickname}</span>
|
<span className="text-sm font-medium text-slate-900">
|
||||||
|
{selectedReport.reporter?.nickname}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-muted-foreground">举报时间</Label>
|
<Label className="text-xs text-slate-500">举报时间</Label>
|
||||||
<p>{format(new Date(selectedReport.created_at), 'yyyy-MM-dd HH:mm:ss')}</p>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-muted-foreground">目标类型</Label>
|
<Label className="text-xs text-slate-500">目标类型</Label>
|
||||||
<p>{targetTypeText[selectedReport.target_type]}</p>
|
{(() => {
|
||||||
|
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>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-muted-foreground">处理状态</Label>
|
<Label className="text-xs text-slate-500">处理状态</Label>
|
||||||
<Badge className={statusColors[selectedReport.status]}>
|
<div className="mt-1.5">
|
||||||
{statusText[selectedReport.status]}
|
<Badge className={`flex items-center gap-1.5 ${statusColors[selectedReport.status]}`}>
|
||||||
|
{statusIcons[selectedReport.status]}
|
||||||
|
<span className="font-medium">{statusText[selectedReport.status]}</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
</div>
|
||||||
<Label className="text-muted-foreground">举报原因</Label>
|
</div>
|
||||||
<p>{reasonText[selectedReport.reason] || selectedReport.reason}</p>
|
</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>
|
</div>
|
||||||
{selectedReport.description && (
|
{selectedReport.description && (
|
||||||
<div className="col-span-2">
|
<div className="mt-3 p-3 bg-amber-50 border border-amber-200 rounded-lg">
|
||||||
<Label className="text-muted-foreground">详细描述</Label>
|
<Label className="text-xs text-amber-700">详细描述</Label>
|
||||||
<p>{selectedReport.description}</p>
|
<p className="text-sm text-amber-900 mt-1">{selectedReport.description}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 被举报内容 */}
|
||||||
{selectedReport.target_content && (
|
{selectedReport.target_content && (
|
||||||
<Card>
|
<div className="p-4 bg-slate-50 rounded-xl">
|
||||||
<CardHeader>
|
<h4 className="text-sm font-semibold text-slate-700 mb-3">被举报内容</h4>
|
||||||
<CardTitle className="text-base">被举报内容</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{selectedReport.target_content.title && (
|
{selectedReport.target_content.title && (
|
||||||
<p className="font-medium mb-2">{selectedReport.target_content.title}</p>
|
<p className="font-semibold text-slate-900 mb-2">{selectedReport.target_content.title}</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-sm text-muted-foreground">{selectedReport.target_content.content}</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 && (
|
{selectedReport.target_content.images && selectedReport.target_content.images.length > 0 && (
|
||||||
<div className="flex gap-2 mt-2">
|
<div className="flex flex-wrap gap-2 mt-3">
|
||||||
{selectedReport.target_content.images.map((img, i) => (
|
{selectedReport.target_content.images.map((img, i) => (
|
||||||
<img key={i} src={img} alt="" className="w-16 h-16 object-cover rounded" />
|
<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>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 处理信息 */}
|
||||||
{selectedReport.handler && (
|
{selectedReport.handler && (
|
||||||
<div>
|
<div className="p-4 bg-slate-50 rounded-xl">
|
||||||
<Label className="text-muted-foreground">处理人</Label>
|
<h4 className="text-sm font-semibold text-slate-700 mb-3">处理信息</h4>
|
||||||
<div className="flex items-center gap-2 mt-1">
|
<div className="flex items-center gap-3">
|
||||||
<Avatar className="h-6 w-6">
|
<Avatar className="h-8 w-8">
|
||||||
<AvatarImage src={selectedReport.handler.avatar} />
|
<AvatarImage src={selectedReport.handler.avatar} />
|
||||||
<AvatarFallback>{selectedReport.handler.nickname?.[0]}</AvatarFallback>
|
<AvatarFallback className="text-xs">{selectedReport.handler.nickname?.[0]}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<span>{selectedReport.handler.nickname}</span>
|
|
||||||
{selectedReport.handled_at && (
|
|
||||||
<span className="text-muted-foreground text-sm">
|
|
||||||
{format(new Date(selectedReport.handled_at), 'yyyy-MM-dd HH:mm')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedReport.result && (
|
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-muted-foreground">处理结果</Label>
|
<p className="text-sm font-medium text-slate-900">{selectedReport.handler.nickname}</p>
|
||||||
<p>{selectedReport.result}</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -520,43 +758,63 @@ export default function Reports() {
|
|||||||
|
|
||||||
{/* 处理对话框 */}
|
{/* 处理对话框 */}
|
||||||
<Dialog open={handleOpen} onOpenChange={setHandleOpen}>
|
<Dialog open={handleOpen} onOpenChange={setHandleOpen}>
|
||||||
<DialogContent>
|
<DialogContent className="max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>处理举报</DialogTitle>
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<DialogDescription>请选择处理方式</DialogDescription>
|
<Flag className="h-5 w-5 text-yellow-500" />
|
||||||
|
处理举报
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-sm">
|
||||||
|
请根据举报内容选择处理方式
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex gap-4">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<Button
|
<Button
|
||||||
variant={handleAction === 'approve' ? 'default' : 'outline'}
|
variant={handleAction === 'approve' ? 'default' : 'outline'}
|
||||||
className="flex-1"
|
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')}
|
onClick={() => setHandleAction('approve')}
|
||||||
>
|
>
|
||||||
<Check className="h-4 w-4 mr-2" />
|
<Check className="h-5 w-5" />
|
||||||
确认违规
|
<span>确认违规</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={handleAction === 'reject' ? 'default' : 'outline'}
|
variant={handleAction === 'reject' ? 'default' : 'outline'}
|
||||||
className="flex-1"
|
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')}
|
onClick={() => setHandleAction('reject')}
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4 mr-2" />
|
<X className="h-5 w-5" />
|
||||||
驳回举报
|
<span>驳回举报</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>处理说明(选填)</Label>
|
<Label className="text-sm font-medium">处理说明(选填)</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
value={handleResult}
|
value={handleResult}
|
||||||
onChange={(e) => setHandleResult(e.target.value)}
|
onChange={(e) => setHandleResult(e.target.value)}
|
||||||
placeholder="请输入处理说明..."
|
placeholder="请输入处理说明,将发送给举报人..."
|
||||||
rows={3}
|
rows={3}
|
||||||
|
className="mt-2 resize-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter className="gap-2 sm:gap-0">
|
||||||
<Button variant="outline" onClick={() => setHandleOpen(false)}>取消</Button>
|
<Button variant="outline" onClick={() => setHandleOpen(false)} className="border-slate-200">
|
||||||
<Button onClick={submitHandle} disabled={handleLoading}>
|
取消
|
||||||
|
</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" />}
|
{handleLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||||
确认提交
|
确认提交
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user