'use client' import { useState, useEffect, useCallback } 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, Loader2 } from 'lucide-react' import { getPlatformInfo } from '@/lib/platforms' import { api } from '@/lib/api' import { USE_MOCK } from '@/contexts/AuthContext' import type { TaskResponse } from '@/types/task' // 申诉状态类型 type AppealStatus = 'pending' | 'processing' | 'approved' | 'rejected' // 申诉类型 type AppealType = 'ai' | 'agency' // 申诉数据类型 interface Appeal { id: string taskId: string taskTitle: string creatorId: string creatorName: string platform: 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: '小美护肤', platform: 'douyin', 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', platform: 'xiaohongshu', 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: '健身教练王', platform: 'bilibili', 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: '达人小红', platform: 'xiaohongshu', type: 'agency', contentType: 'video', reason: '品牌调性理解差异', content: '代理商认为风格不符,但Brief中未明确禁止该风格,请提供更具体的标准。', status: 'rejected', createdAt: '2026-02-03 11:00', updatedAt: '2026-02-03 18:45', }, ] // 状态配置 const statusConfig: Record = { 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 = { ai: { label: 'AI审核申诉', color: 'text-accent-indigo' }, agency: { label: '代理商审核申诉', color: 'text-purple-400' }, } /** * Map a TaskResponse (with is_appeal === true) to the Appeal UI model. */ function mapTaskToAppeal(task: TaskResponse): Appeal { // Determine which stage the task was appealing from const isVideoStage = task.stage.startsWith('video') const contentType: 'script' | 'video' = isVideoStage ? 'video' : 'script' // Determine appeal type based on stage const type: AppealType = task.stage.includes('ai') ? 'ai' : 'agency' // Derive appeal status from the task stage let status: AppealStatus = 'pending' if (task.stage === 'completed') { status = 'approved' } else if (task.stage === 'rejected') { status = 'rejected' } else if (task.stage.includes('review')) { status = 'processing' } return { id: task.id, taskId: task.id, taskTitle: task.name, creatorId: task.creator.id, creatorName: task.creator.name, platform: task.project?.platform || 'douyin', type, contentType, reason: task.appeal_reason || '申诉', content: task.appeal_reason || '', status, createdAt: task.updated_at ? new Date(task.updated_at).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).replace(/\//g, '-') : '', updatedAt: task.stage === 'completed' || task.stage === 'rejected' ? new Date(task.updated_at).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).replace(/\//g, '-') : undefined, } } function AppealCard({ appeal }: { appeal: Appeal }) { const status = statusConfig[appeal.status] const type = typeConfig[appeal.type] const StatusIcon = status.icon const platform = getPlatformInfo(appeal.platform) return (
{/* 平台顶部条 */} {platform && (
{platform.icon} {platform.name}
)}
{/* 顶部:状态和类型 */}
{appeal.taskTitle}
{appeal.creatorName} · {appeal.contentType === 'script' ? :
{status.label}
{/* 申诉信息 */}
申诉类型: {type.label}
申诉原因: {appeal.reason}

{appeal.content}

{/* 底部时间 */}
提交时间: {appeal.createdAt} {appeal.updatedAt && 处理时间: {appeal.updatedAt}}
) } export default function AgencyAppealsPage() { const [filter, setFilter] = useState('all') const [searchQuery, setSearchQuery] = useState('') const [appeals, setAppeals] = useState([]) const [loading, setLoading] = useState(true) const fetchAppeals = useCallback(async () => { if (USE_MOCK) { setAppeals(mockAppeals) setLoading(false) return } try { setLoading(true) // Fetch tasks and filter for those with is_appeal === true const response = await api.listTasks(1, 50) const appealTasks = response.items.filter((t) => t.is_appeal === true) setAppeals(appealTasks.map(mapTaskToAppeal)) } catch (err) { console.error('Failed to fetch appeals:', err) setAppeals([]) } finally { setLoading(false) } }, []) useEffect(() => { fetchAppeals() }, [fetchAppeals]) // 统计 const pendingCount = appeals.filter(a => a.status === 'pending').length const processingCount = appeals.filter(a => a.status === 'processing').length // 筛选 const filteredAppeals = appeals.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 (
{/* 页面标题 */}

申诉处理

处理达人提交的申诉请求

{pendingCount} 待处理 {processingCount} 处理中
{/* 搜索和筛选 */}
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" />
{[ { value: 'all', label: '全部' }, { value: 'pending', label: '待处理' }, { value: 'processing', label: '处理中' }, { value: 'approved', label: '已通过' }, { value: 'rejected', label: '已驳回' }, ].map((tab) => ( ))}
{/* 申诉列表 */} 申诉列表 共 {filteredAppeals.length} 条 {loading ? (

加载中...

) : filteredAppeals.length > 0 ? ( filteredAppeals.map((appeal) => ( )) ) : (

{searchQuery || filter !== 'all' ? '没有找到匹配的申诉' : '暂无申诉记录'}

{(searchQuery || filter !== 'all') && ( )}
)}
) }