'use client' import { useState, useEffect, useCallback } from 'react' import { useParams, useRouter } from 'next/navigation' import { ArrowLeft, MessageCircle, Clock, CheckCircle, XCircle, FileText, Image as ImageIcon, Send, AlertTriangle, Loader2 } from 'lucide-react' import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout' import { cn } from '@/lib/utils' import { api } from '@/lib/api' import { USE_MOCK } from '@/contexts/AuthContext' import { useToast } from '@/components/ui/Toast' import type { TaskResponse } from '@/types/task' // 申诉状态类型 type AppealStatus = 'pending' | 'processing' | 'approved' | 'rejected' // 申诉详情数据类型 type AppealDetail = { id: string taskId: string taskTitle: string type: 'ai' | 'agency' | 'brand' reason: string content: string status: AppealStatus createdAt: string updatedAt?: string result?: string attachments?: { name: string; type: 'image' | 'document'; url: string }[] timeline?: { time: string; action: string; operator?: string }[] originalIssue?: { title: string; description: string } } // 模拟申诉详情数据 const mockAppealDetails: Record = { 'appeal-001': { id: 'appeal-001', taskId: 'task-003', taskTitle: 'ZZ饮品夏日', type: 'ai', reason: '误判', content: '视频中出现的是我们自家品牌的历史产品,并非竞品。已附上品牌授权证明。', status: 'approved', createdAt: '2026-02-01 10:30', updatedAt: '2026-02-02 15:20', result: '经核实,该产品确为品牌方授权产品,申诉通过。AI已学习此案例,后续将避免类似误判。', attachments: [ { name: '品牌授权书.pdf', type: 'document', url: '#' }, { name: '产品对比图.jpg', type: 'image', url: '#' }, ], timeline: [ { time: '2026-02-01 10:30', action: '提交申诉' }, { time: '2026-02-01 14:15', action: '进入处理队列' }, { time: '2026-02-02 09:00', action: '开始审核', operator: '审核员 A' }, { time: '2026-02-02 15:20', action: '申诉通过', operator: '审核员 A' }, ], originalIssue: { title: '检测到竞品 Logo', description: '画面中 0:15-0:18 出现竞品「百事可乐」的 Logo,可能造成合规风险。', }, }, 'appeal-002': { id: 'appeal-002', taskId: 'task-010', taskTitle: 'GG智能手表', type: 'agency', reason: '审核标准不清晰', content: '代理商反馈品牌调性不符,但Brief中并未明确说明科技专业形象的具体要求。请明确审核标准。', status: 'processing', createdAt: '2026-02-04 09:15', timeline: [ { time: '2026-02-04 09:15', action: '提交申诉' }, { time: '2026-02-04 11:30', action: '进入处理队列' }, { time: '2026-02-05 10:00', action: '开始审核', operator: '审核员 B' }, ], originalIssue: { title: '品牌调性不符', description: '脚本整体风格偏向娱乐化,与品牌科技专业形象不匹配。', }, }, 'appeal-003': { id: 'appeal-003', taskId: 'task-011', taskTitle: 'HH美妆代言', type: 'brand', reason: '创意理解差异', content: '品牌方认为创意不够新颖,但该创意形式在同类型产品推广中效果显著,已附上数据支持。', status: 'pending', createdAt: '2026-02-05 14:00', attachments: [ { name: '同类案例数据.xlsx', type: 'document', url: '#' }, ], timeline: [ { time: '2026-02-05 14:00', action: '提交申诉' }, ], originalIssue: { title: '创意不够新颖', description: '脚本采用的是常见的口播形式,缺乏创新点和记忆点。', }, }, 'appeal-004': { id: 'appeal-004', taskId: 'task-013', taskTitle: 'JJ旅行vlog', type: 'agency', reason: '版权问题异议', content: '使用的背景音乐来自无版权音乐库 Epidemic Sound,已购买商用授权。附上授权证明截图。', status: 'rejected', createdAt: '2026-01-28 11:30', updatedAt: '2026-01-30 16:45', result: '经核实,该音乐虽有授权,但授权范围不包含商业广告用途。建议更换音乐后重新提交。', attachments: [ { name: '授权截图.png', type: 'image', url: '#' }, ], timeline: [ { time: '2026-01-28 11:30', action: '提交申诉' }, { time: '2026-01-28 15:00', action: '进入处理队列' }, { time: '2026-01-29 09:30', action: '开始审核', operator: '审核员 C' }, { time: '2026-01-30 16:45', action: '申诉驳回', operator: '审核员 C' }, ], originalIssue: { title: '背景音乐版权问题', description: '视频中使用的背景音乐「XXX」存在版权风险,平台可能会限流或下架。', }, }, } // 将 TaskResponse 映射为 AppealDetail UI 类型 function mapTaskToAppealDetail(task: TaskResponse): AppealDetail { let type: 'ai' | 'agency' | 'brand' = 'ai' if (task.script_brand_status === 'rejected' || task.video_brand_status === 'rejected') { type = 'brand' } else if (task.script_agency_status === 'rejected' || task.video_agency_status === 'rejected') { type = 'agency' } let status: AppealStatus = 'pending' if (task.stage === 'completed') { status = 'approved' } else if (task.stage === 'rejected') { status = 'rejected' } else if (task.is_appeal) { status = 'processing' } const formatDate = (dateStr: string) => { return new Date(dateStr).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }) } // Build original issue from review comments let originalIssue: { title: string; description: string } | undefined const rejectionComment = task.script_brand_comment || task.script_agency_comment || task.video_brand_comment || task.video_agency_comment if (rejectionComment) { originalIssue = { title: '审核驳回', description: rejectionComment, } } // Build timeline from task dates const timeline: { time: string; action: string; operator?: string }[] = [] if (task.created_at) { timeline.push({ time: formatDate(task.created_at), action: '任务创建' }) } if (task.updated_at) { timeline.push({ time: formatDate(task.updated_at), action: '提交申诉' }) } return { id: task.id, taskId: task.id, taskTitle: task.name, type, reason: task.appeal_reason || '申诉', content: task.appeal_reason || '', status, createdAt: task.created_at ? formatDate(task.created_at) : '', updatedAt: task.updated_at ? formatDate(task.updated_at) : undefined, originalIssue, timeline: timeline.length > 0 ? timeline : undefined, } } // 状态配置 const statusConfig: Record = { pending: { label: '待处理', color: 'text-amber-500', bgColor: 'bg-amber-500/15', icon: Clock }, processing: { label: '处理中', color: 'text-accent-indigo', bgColor: 'bg-accent-indigo/15', icon: MessageCircle }, 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' }, brand: { label: '品牌方审核', color: 'text-accent-blue' }, } // 骨架屏组件 function DetailSkeleton() { return (
) } export default function AppealDetailPage() { const params = useParams() const router = useRouter() const toast = useToast() const appealId = params.id as string const [newComment, setNewComment] = useState('') const [appeal, setAppeal] = useState(null) const [loading, setLoading] = useState(true) const [submitting, setSubmitting] = useState(false) const loadAppealDetail = useCallback(async () => { if (USE_MOCK) { const mockAppeal = mockAppealDetails[appealId] setAppeal(mockAppeal || null) setLoading(false) return } try { setLoading(true) const task = await api.getTask(appealId) const mapped = mapTaskToAppealDetail(task) setAppeal(mapped) } catch (err) { console.error('加载申诉详情失败:', err) toast.error('加载申诉详情失败,请稍后重试') setAppeal(null) } finally { setLoading(false) } }, [appealId, toast]) useEffect(() => { loadAppealDetail() }, [loadAppealDetail]) const handleSendComment = async () => { if (!newComment.trim()) return if (USE_MOCK) { toast.success('补充说明已发送') setNewComment('') return } try { setSubmitting(true) // Use submitAppeal to add supplementary info (re-appeal with updated reason) await api.submitAppeal(appealId, { reason: newComment.trim() }) toast.success('补充说明已发送') setNewComment('') // Reload to reflect any changes loadAppealDetail() } catch (err) { console.error('发送补充说明失败:', err) toast.error('发送失败,请稍后重试') } finally { setSubmitting(false) } } if (loading) { return ( ) } if (!appeal) { return (

申诉记录不存在

) } const status = statusConfig[appeal.status] const type = typeConfig[appeal.type] const StatusIcon = status.icon return (
{/* 顶部栏 */}

申诉详情

申诉编号: {appeal.id}

{status.label}
{/* 内容区 - 响应式布局 */}
{/* 左侧:申诉信息 */}
{/* 原始问题 */} {appeal.originalIssue && (

原始审核问题

{appeal.originalIssue.title}

{appeal.originalIssue.description}

)} {/* 申诉内容 */}

申诉内容

关联任务: {appeal.taskTitle}
申诉对象: {type.label}
申诉原因: {appeal.reason}

{appeal.content}

{/* 附件 */} {appeal.attachments && appeal.attachments.length > 0 && (

证明材料

{appeal.attachments.map((attachment, index) => (
{attachment.type === 'image' ? ( ) : ( )} {attachment.name}
))}
)} {/* 处理结果 */} {appeal.result && (

处理结果

{appeal.result}

)} {/* 补充说明(处理中状态可用) */} {(appeal.status === 'pending' || appeal.status === 'processing') && (

补充说明

setNewComment(e.target.value)} className="flex-1 px-4 py-3 bg-bg-elevated rounded-xl text-sm text-text-primary placeholder-text-tertiary focus:outline-none focus:ring-2 focus:ring-accent-indigo" disabled={submitting} />
)}
{/* 右侧:时间线 */}

处理进度

{appeal.timeline?.map((item, index) => (
{index < (appeal.timeline?.length || 0) - 1 && (
)}
{item.time} {item.action} {item.operator && ( {item.operator} )}
))}
) }