Enhance Reports page with safe date formatting, improved status colors and icons, and add statistics display for report statuses.

This commit is contained in:
lafay
2026-03-30 18:01:38 +08:00
parent e7c9ee5159
commit 3e06cfde76

View File

@@ -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>