'use client' import { useState, useEffect, useCallback } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import { ArrowLeft, Upload, X, FileText, Image, AlertTriangle, CheckCircle, 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' // 申诉原因选项 const appealReasons = [ { id: 'misjudge', label: '误判', description: 'AI或审核员误判了内容' }, { id: 'unclear', label: '标准不清晰', description: '审核标准不明确或有歧义' }, { id: 'evidence', label: '有证据支持', description: '有证据证明内容符合要求' }, { id: 'context', label: '上下文理解', description: '审核未考虑完整上下文' }, { id: 'other', label: '其他原因', description: '其他需要说明的情况' }, ] // Mock 任务信息类型 type TaskInfo = { title: string issue: string issueDesc: string type: string appealRemaining: number agencyName: string } // 任务信息(模拟从URL参数获取) const getTaskInfo = (taskId: string): TaskInfo => { const tasks: Record = { 'task-003': { title: 'ZZ饮品夏日', issue: '检测到竞品提及', issueDesc: '脚本第3段提及了竞品「百事可乐」,可能造成品牌冲突风险。', type: 'ai', appealRemaining: 1, agencyName: '星辰传媒', }, 'task-010': { title: 'GG智能手表', issue: '品牌调性不符', issueDesc: '脚本整体风格偏向娱乐化,与品牌科技专业形象不匹配。', type: 'agency', appealRemaining: 0, agencyName: '星辰传媒', }, 'task-011': { title: 'HH美妆代言', issue: '创意不够新颖', issueDesc: '脚本采用的是常见的口播形式,缺乏创新点和记忆点。', type: 'brand', appealRemaining: 1, agencyName: '晨曦文化', }, 'task-013': { title: 'JJ旅行vlog', issue: '背景音乐版权问题', issueDesc: '视频中使用的背景音乐存在版权风险。', type: 'agency', appealRemaining: 2, agencyName: '晨曦文化', }, 'task-015': { title: 'LL厨房电器', issue: '使用场景不真实', issueDesc: '视频中的厨房场景过于整洁,缺乏真实感。', type: 'brand', appealRemaining: 0, agencyName: '星辰传媒', }, } return tasks[taskId] || { title: '未知任务', issue: '未知问题', issueDesc: '', type: 'ai', appealRemaining: 0, agencyName: '未知代理商' } } // 将 TaskResponse 映射为 TaskInfo function mapTaskResponseToInfo(task: TaskResponse): TaskInfo { let type = 'ai' let issue = '审核驳回' let issueDesc = '' if (task.script_brand_status === 'rejected' || task.video_brand_status === 'rejected') { type = 'brand' issue = task.script_brand_comment || task.video_brand_comment || '品牌方审核驳回' issueDesc = task.script_brand_comment || task.video_brand_comment || '' } else if (task.script_agency_status === 'rejected' || task.video_agency_status === 'rejected') { type = 'agency' issue = task.script_agency_comment || task.video_agency_comment || '代理商审核驳回' issueDesc = task.script_agency_comment || task.video_agency_comment || '' } else { // AI rejection or default const aiResult = task.script_ai_result || task.video_ai_result if (aiResult && aiResult.violations.length > 0) { issue = aiResult.violations[0].content || 'AI审核不通过' issueDesc = aiResult.summary || aiResult.violations.map(v => v.content).join('; ') } } // Default appeal quota: 1 per task minus used appeals const defaultQuota = 1 const appealRemaining = Math.max(0, defaultQuota - task.appeal_count) return { title: task.name, issue, issueDesc, type, appealRemaining, agencyName: task.agency?.name || '未知代理商', } } // 表单骨架屏 function FormSkeleton() { return (
) } export default function NewAppealPage() { const router = useRouter() const searchParams = useSearchParams() const toast = useToast() const taskId = searchParams.get('taskId') || '' const [taskInfo, setTaskInfo] = useState(null) const [loading, setLoading] = useState(true) const [selectedReason, setSelectedReason] = useState('') const [content, setContent] = useState('') const [attachments, setAttachments] = useState<{ name: string; type: 'image' | 'document' }[]>([]) const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitted, setIsSubmitted] = useState(false) const [isRequestingQuota, setIsRequestingQuota] = useState(false) const [quotaRequested, setQuotaRequested] = useState(false) // Load task info const loadTaskInfo = useCallback(async () => { if (USE_MOCK) { setTaskInfo(getTaskInfo(taskId)) setLoading(false) return } if (!taskId) { toast.error('缺少任务ID参数') setLoading(false) return } try { setLoading(true) const task = await api.getTask(taskId) const info = mapTaskResponseToInfo(task) setTaskInfo(info) } catch (err) { console.error('加载任务信息失败:', err) toast.error('加载任务信息失败,请稍后重试') // Fallback to a default setTaskInfo({ title: '未知任务', issue: '未知问题', issueDesc: '', type: 'ai', appealRemaining: 0, agencyName: '未知代理商' }) } finally { setLoading(false) } }, [taskId, toast]) useEffect(() => { loadTaskInfo() }, [loadTaskInfo]) const hasAppealQuota = taskInfo ? taskInfo.appealRemaining > 0 : false const handleFileUpload = (e: React.ChangeEvent) => { const files = e.target.files if (files) { const newAttachments = Array.from(files).map(file => ({ name: file.name, type: file.type.startsWith('image/') ? 'image' as const : 'document' as const, })) setAttachments([...attachments, ...newAttachments]) } } const removeAttachment = (index: number) => { setAttachments(attachments.filter((_, i) => i !== index)) } const handleSubmit = async () => { if (!selectedReason || !content.trim()) return if (USE_MOCK) { setIsSubmitting(true) await new Promise(resolve => setTimeout(resolve, 1500)) setIsSubmitting(false) setIsSubmitted(true) setTimeout(() => { router.push('/creator/appeals') }, 2000) return } try { setIsSubmitting(true) const reasonLabel = appealReasons.find(r => r.id === selectedReason)?.label || selectedReason const appealReason = `[${reasonLabel}] ${content.trim()}` await api.submitAppeal(taskId, { reason: appealReason }) toast.success('申诉提交成功') setIsSubmitted(true) setTimeout(() => { router.push('/creator/appeals') }, 2000) } catch (err) { console.error('提交申诉失败:', err) toast.error('提交申诉失败,请稍后重试') } finally { setIsSubmitting(false) } } const canSubmit = selectedReason && content.trim().length >= 20 && hasAppealQuota // 申请增加申诉次数 const handleRequestQuota = async () => { if (USE_MOCK) { setIsRequestingQuota(true) await new Promise(resolve => setTimeout(resolve, 1000)) setIsRequestingQuota(false) setQuotaRequested(true) return } try { setIsRequestingQuota(true) await api.increaseAppealCount(taskId) toast.success('申请已发送,等待代理商处理') setQuotaRequested(true) // Reload task info to get updated appeal count loadTaskInfo() } catch (err) { console.error('申请增加申诉次数失败:', err) toast.error('申请失败,请稍后重试') } finally { setIsRequestingQuota(false) } } // 加载中骨架屏 if (loading) { return ( ) } // 提交成功界面 if (isSubmitted) { return (

申诉提交成功

您的申诉已提交,我们将在 1-3 个工作日内处理完成。处理结果将通过消息中心通知您。

正在跳转到申诉列表...

) } // Use fallback if taskInfo is somehow null after loading const info = taskInfo || { title: '未知任务', issue: '未知问题', issueDesc: '', type: 'ai', appealRemaining: 0, agencyName: '未知代理商' } return (
{/* 顶部栏 */}

发起申诉

对审核结果有异议?提交申诉让我们重新审核

本任务剩余 {info.appealRemaining} 次申诉机会
{/* 内容区 - 响应式布局 */}
{/* 左侧:申诉表单 */}
{/* 关联任务 */}

关联任务

{info.title} {info.type === 'ai' ? 'AI审核' : info.type === 'agency' ? '代理商审核' : '品牌方审核'}
{info.issue}

{info.issueDesc}

{/* 申诉次数不足提示 */} {!hasAppealQuota && (

申诉次数不足

本任务的申诉次数已用完,无法提交新的申诉。您可以向代理商「{info.agencyName}」申请增加申诉次数。

{quotaRequested ? (
申请已发送,等待代理商处理
) : ( )}
)} {/* 申诉原因 */}

申诉原因 *

{appealReasons.map((reason) => (
setSelectedReason(reason.id)} className={cn( 'p-4 rounded-xl border-2 cursor-pointer transition-all', selectedReason === reason.id ? 'border-accent-indigo bg-accent-indigo/5' : 'border-border-subtle hover:border-text-tertiary' )} > {reason.label}

{reason.description}

))}
{/* 申诉说明 */}

申诉说明 * 至少20字