审核台列表页优化: - 每个任务项显示文件名、文件大小 - 添加下载按钮支持文件下载 - 点击"审核"按钮才跳转到详情页 - 按风险等级显示不同颜色的状态标签和按钮 新增申诉处理功能: - 申诉列表页:展示所有达人申诉,支持搜索和状态筛选 - 申诉详情页:查看申诉内容、附件、原审核问题 - 处理决策面板:通过/驳回申诉并填写处理意见 - 侧边栏添加"申诉处理"菜单入口 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
281 lines
10 KiB
TypeScript
281 lines
10 KiB
TypeScript
'use client'
|
||
|
||
import { useState } from 'react'
|
||
import Link from 'next/link'
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||
import { Button } from '@/components/ui/Button'
|
||
import {
|
||
MessageSquare,
|
||
Clock,
|
||
CheckCircle,
|
||
XCircle,
|
||
AlertTriangle,
|
||
Search,
|
||
Filter,
|
||
ChevronRight,
|
||
User,
|
||
FileText,
|
||
Video
|
||
} from 'lucide-react'
|
||
|
||
// 申诉状态类型
|
||
type AppealStatus = 'pending' | 'processing' | 'approved' | 'rejected'
|
||
|
||
// 申诉类型
|
||
type AppealType = 'ai' | 'agency'
|
||
|
||
// 申诉数据类型
|
||
interface Appeal {
|
||
id: string
|
||
taskId: string
|
||
taskTitle: string
|
||
creatorId: string
|
||
creatorName: string
|
||
type: AppealType
|
||
contentType: 'script' | 'video'
|
||
reason: string
|
||
content: string
|
||
status: AppealStatus
|
||
createdAt: string
|
||
updatedAt?: string
|
||
}
|
||
|
||
// 模拟申诉数据
|
||
const mockAppeals: Appeal[] = [
|
||
{
|
||
id: 'appeal-001',
|
||
taskId: 'task-001',
|
||
taskTitle: '夏日护肤推广脚本',
|
||
creatorId: 'creator-001',
|
||
creatorName: '小美护肤',
|
||
type: 'ai',
|
||
contentType: 'script',
|
||
reason: 'AI误判',
|
||
content: '脚本中提到的"某品牌"是泛指,并非特指竞品,请重新审核。',
|
||
status: 'pending',
|
||
createdAt: '2026-02-06 10:30',
|
||
},
|
||
{
|
||
id: 'appeal-002',
|
||
taskId: 'task-002',
|
||
taskTitle: '新品口红试色',
|
||
creatorId: 'creator-002',
|
||
creatorName: '美妆Lisa',
|
||
type: 'agency',
|
||
contentType: 'video',
|
||
reason: '审核标准不清晰',
|
||
content: '视频中的背景音乐已获得授权,附上授权证明。代理商反馈的"版权问题"不成立。',
|
||
status: 'processing',
|
||
createdAt: '2026-02-05 14:20',
|
||
},
|
||
{
|
||
id: 'appeal-003',
|
||
taskId: 'task-003',
|
||
taskTitle: '健身器材推荐',
|
||
creatorId: 'creator-003',
|
||
creatorName: '健身教练王',
|
||
type: 'ai',
|
||
contentType: 'script',
|
||
reason: '违禁词误判',
|
||
content: 'AI标记的"最好"是在描述个人体验,并非绝对化用语,符合广告法要求。',
|
||
status: 'approved',
|
||
createdAt: '2026-02-04 09:15',
|
||
updatedAt: '2026-02-04 16:30',
|
||
},
|
||
{
|
||
id: 'appeal-004',
|
||
taskId: 'task-004',
|
||
taskTitle: '美妆新品测评',
|
||
creatorId: 'creator-004',
|
||
creatorName: '达人小红',
|
||
type: 'agency',
|
||
contentType: 'video',
|
||
reason: '品牌调性理解差异',
|
||
content: '代理商认为风格不符,但Brief中未明确禁止该风格,请提供更具体的标准。',
|
||
status: 'rejected',
|
||
createdAt: '2026-02-03 11:00',
|
||
updatedAt: '2026-02-03 18:45',
|
||
},
|
||
]
|
||
|
||
// 状态配置
|
||
const statusConfig: Record<AppealStatus, { label: string; color: string; bgColor: string; icon: React.ElementType }> = {
|
||
pending: { label: '待处理', color: 'text-accent-amber', bgColor: 'bg-accent-amber/15', icon: Clock },
|
||
processing: { label: '处理中', color: 'text-accent-indigo', bgColor: 'bg-accent-indigo/15', icon: MessageSquare },
|
||
approved: { label: '已通过', color: 'text-accent-green', bgColor: 'bg-accent-green/15', icon: CheckCircle },
|
||
rejected: { label: '已驳回', color: 'text-accent-coral', bgColor: 'bg-accent-coral/15', icon: XCircle },
|
||
}
|
||
|
||
// 类型配置
|
||
const typeConfig: Record<AppealType, { label: string; color: string }> = {
|
||
ai: { label: 'AI审核申诉', color: 'text-accent-indigo' },
|
||
agency: { label: '代理商审核申诉', color: 'text-purple-400' },
|
||
}
|
||
|
||
function AppealCard({ appeal }: { appeal: Appeal }) {
|
||
const status = statusConfig[appeal.status]
|
||
const type = typeConfig[appeal.type]
|
||
const StatusIcon = status.icon
|
||
|
||
return (
|
||
<Link href={`/agency/appeals/${appeal.id}`}>
|
||
<div className="p-4 rounded-xl bg-bg-elevated hover:bg-bg-elevated/80 transition-colors cursor-pointer">
|
||
{/* 顶部:状态和类型 */}
|
||
<div className="flex items-center justify-between mb-3">
|
||
<div className="flex items-center gap-2">
|
||
<div className={`w-8 h-8 rounded-lg ${status.bgColor} flex items-center justify-center`}>
|
||
<StatusIcon size={16} className={status.color} />
|
||
</div>
|
||
<div>
|
||
<span className="font-medium text-text-primary">{appeal.taskTitle}</span>
|
||
<div className="flex items-center gap-2 text-xs text-text-tertiary">
|
||
<span className="flex items-center gap-1">
|
||
<User size={10} />
|
||
{appeal.creatorName}
|
||
</span>
|
||
<span>·</span>
|
||
<span className="flex items-center gap-1">
|
||
{appeal.contentType === 'script' ? <FileText size={10} /> : <Video size={10} />}
|
||
{appeal.contentType === 'script' ? '脚本' : '视频'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${status.bgColor} ${status.color}`}>
|
||
{status.label}
|
||
</span>
|
||
<ChevronRight size={16} className="text-text-tertiary" />
|
||
</div>
|
||
</div>
|
||
|
||
{/* 申诉信息 */}
|
||
<div className="space-y-2 mb-3">
|
||
<div className="flex items-center gap-2 text-sm">
|
||
<span className="text-text-tertiary">申诉类型:</span>
|
||
<span className={type.color}>{type.label}</span>
|
||
</div>
|
||
<div className="flex items-center gap-2 text-sm">
|
||
<span className="text-text-tertiary">申诉原因:</span>
|
||
<span className="text-text-primary">{appeal.reason}</span>
|
||
</div>
|
||
<p className="text-sm text-text-secondary line-clamp-2">{appeal.content}</p>
|
||
</div>
|
||
|
||
{/* 底部时间 */}
|
||
<div className="flex items-center justify-between text-xs text-text-tertiary pt-3 border-t border-border-subtle">
|
||
<span>提交时间: {appeal.createdAt}</span>
|
||
{appeal.updatedAt && <span>处理时间: {appeal.updatedAt}</span>}
|
||
</div>
|
||
</div>
|
||
</Link>
|
||
)
|
||
}
|
||
|
||
export default function AgencyAppealsPage() {
|
||
const [filter, setFilter] = useState<AppealStatus | 'all'>('all')
|
||
const [searchQuery, setSearchQuery] = useState('')
|
||
|
||
// 统计
|
||
const pendingCount = mockAppeals.filter(a => a.status === 'pending').length
|
||
const processingCount = mockAppeals.filter(a => a.status === 'processing').length
|
||
|
||
// 筛选
|
||
const filteredAppeals = mockAppeals.filter(appeal => {
|
||
const matchesSearch = searchQuery === '' ||
|
||
appeal.taskTitle.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
appeal.creatorName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
appeal.reason.toLowerCase().includes(searchQuery.toLowerCase())
|
||
const matchesFilter = filter === 'all' || appeal.status === filter
|
||
return matchesSearch && matchesFilter
|
||
})
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* 页面标题 */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-text-primary">申诉处理</h1>
|
||
<p className="text-sm text-text-secondary mt-1">处理达人提交的申诉请求</p>
|
||
</div>
|
||
<div className="flex items-center gap-2 text-sm">
|
||
<span className="px-3 py-1.5 bg-accent-amber/20 text-accent-amber rounded-lg font-medium">
|
||
{pendingCount} 待处理
|
||
</span>
|
||
<span className="px-3 py-1.5 bg-accent-indigo/20 text-accent-indigo rounded-lg font-medium">
|
||
{processingCount} 处理中
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 搜索和筛选 */}
|
||
<div className="flex items-center gap-4">
|
||
<div className="relative flex-1 max-w-md">
|
||
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-tertiary" />
|
||
<input
|
||
type="text"
|
||
placeholder="搜索任务名称或达人..."
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
className="w-full pl-10 pr-4 py-2.5 border border-border-subtle rounded-xl bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||
/>
|
||
</div>
|
||
<div className="flex items-center gap-1 p-1 bg-bg-elevated rounded-lg">
|
||
{[
|
||
{ value: 'all', label: '全部' },
|
||
{ value: 'pending', label: '待处理' },
|
||
{ value: 'processing', label: '处理中' },
|
||
{ value: 'approved', label: '已通过' },
|
||
{ value: 'rejected', label: '已驳回' },
|
||
].map((tab) => (
|
||
<button
|
||
key={tab.value}
|
||
type="button"
|
||
onClick={() => setFilter(tab.value as AppealStatus | 'all')}
|
||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||
filter === tab.value ? 'bg-bg-card text-text-primary shadow-sm' : 'text-text-secondary hover:text-text-primary'
|
||
}`}
|
||
>
|
||
{tab.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 申诉列表 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<MessageSquare size={18} className="text-accent-indigo" />
|
||
申诉列表
|
||
<span className="ml-auto text-sm font-normal text-text-secondary">
|
||
共 {filteredAppeals.length} 条
|
||
</span>
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3">
|
||
{filteredAppeals.length > 0 ? (
|
||
filteredAppeals.map((appeal) => (
|
||
<AppealCard key={appeal.id} appeal={appeal} />
|
||
))
|
||
) : (
|
||
<div className="text-center py-12 text-text-tertiary">
|
||
<MessageSquare size={48} className="mx-auto mb-4 opacity-50" />
|
||
<p>{searchQuery || filter !== 'all' ? '没有找到匹配的申诉' : '暂无申诉记录'}</p>
|
||
{(searchQuery || filter !== 'all') && (
|
||
<button
|
||
type="button"
|
||
onClick={() => { setSearchQuery(''); setFilter('all'); }}
|
||
className="mt-3 text-sm text-accent-indigo hover:underline"
|
||
>
|
||
清除筛选条件
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|