'use client' import { useState, useEffect, useCallback } from 'react' import { useParams, useRouter } from 'next/navigation' import { useToast } from '@/components/ui/Toast' import { Upload, Check, X, Folder, Bell, MessageCircle, XCircle, CheckCircle, Loader2, Scan, ArrowLeft, Bot, Users, Building2, Clock, FileText, Video, ChevronRight, AlertTriangle, Download, Eye, Target, Ban, ChevronDown, ChevronUp, File } from 'lucide-react' import { Modal } from '@/components/ui/Modal' import { Button } from '@/components/ui/Button' import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout' import { cn } from '@/lib/utils' import { api } from '@/lib/api' import { USE_MOCK } from '@/contexts/AuthContext' import { useSSE } from '@/contexts/SSEContext' import type { TaskResponse, AIReviewResult } from '@/types/task' import type { BriefResponse } from '@/types/brief' // 前端 UI 使用的任务阶段类型 type TaskPhase = 'script' | 'video' type TaskStage = | 'upload' | 'ai_reviewing' | 'ai_result' | 'agency_reviewing' | 'agency_rejected' | 'brand_reviewing' | 'brand_approved' | 'brand_rejected' type Issue = { title: string description: string timestamp?: string severity?: 'error' | 'warning' } type ReviewLog = { time: string message: string status: 'done' | 'loading' | 'pending' } type TaskData = { id: string title: string subtitle: string phase: TaskPhase stage: TaskStage progress?: number issues?: Issue[] reviewLogs?: ReviewLog[] rejectionReason?: string submittedAt?: string scriptContent?: string } type AgencyBriefFile = { id: string name: string size: string uploadedAt: string description?: string } // 将后端 TaskResponse 映射为前端 UI 的 TaskData function mapApiTaskToTaskData(task: TaskResponse): TaskData { const stage = task.stage let phase: TaskPhase = 'script' let uiStage: TaskStage = 'upload' let issues: Issue[] = [] let rejectionReason: string | undefined let submittedAt: string | undefined // 判断阶段 if (stage.startsWith('video_') || stage === 'completed') { phase = 'video' } // 映射阶段 switch (stage) { case 'script_upload': uiStage = 'upload'; break case 'script_ai_review': uiStage = 'ai_reviewing'; break case 'script_agency_review': uiStage = 'agency_reviewing'; submittedAt = task.script_uploaded_at || undefined; break case 'script_brand_review': uiStage = 'brand_reviewing'; submittedAt = task.script_uploaded_at || undefined; break case 'video_upload': uiStage = 'upload'; phase = 'video'; break case 'video_ai_review': uiStage = 'ai_reviewing'; phase = 'video'; break case 'video_agency_review': uiStage = 'agency_reviewing'; phase = 'video'; submittedAt = task.video_uploaded_at || undefined; break case 'video_brand_review': uiStage = 'brand_reviewing'; phase = 'video'; submittedAt = task.video_uploaded_at || undefined; break case 'completed': uiStage = 'brand_approved'; phase = 'video'; submittedAt = task.video_uploaded_at || undefined; break case 'rejected': { // 判断是哪个阶段被驳回 if (task.video_brand_status === 'rejected') { phase = 'video'; uiStage = 'brand_rejected' rejectionReason = task.video_brand_comment || undefined } else if (task.video_agency_status === 'rejected') { phase = 'video'; uiStage = 'agency_rejected' rejectionReason = task.video_agency_comment || undefined } else if (task.script_brand_status === 'rejected') { phase = 'script'; uiStage = 'brand_rejected' rejectionReason = task.script_brand_comment || undefined } else if (task.script_agency_status === 'rejected') { phase = 'script'; uiStage = 'agency_rejected' rejectionReason = task.script_agency_comment || undefined } else { uiStage = 'ai_result' } break } } // 处理驳回状态(非 rejected stage 但有驳回) if (task.script_agency_status === 'rejected' && stage !== 'rejected') { phase = 'script'; uiStage = 'agency_rejected' rejectionReason = task.script_agency_comment || undefined } if (task.script_brand_status === 'rejected' && stage !== 'rejected') { phase = 'script'; uiStage = 'brand_rejected' rejectionReason = task.script_brand_comment || undefined } if (task.video_agency_status === 'rejected' && stage !== 'rejected') { phase = 'video'; uiStage = 'agency_rejected' rejectionReason = task.video_agency_comment || undefined } if (task.video_brand_status === 'rejected' && stage !== 'rejected') { phase = 'video'; uiStage = 'brand_rejected' rejectionReason = task.video_brand_comment || undefined } // 提取 AI 审核结果中的 issues const aiResult = phase === 'script' ? task.script_ai_result : task.video_ai_result if (aiResult?.violations) { issues = aiResult.violations.map(v => ({ title: v.type, description: `${v.content}${v.suggestion ? ` — ${v.suggestion}` : ''}`, timestamp: v.timestamp ? `${v.timestamp}s` : undefined, severity: v.severity === 'warning' ? 'warning' as const : 'error' as const, })) } const subtitle = `${task.project.name} · ${task.project.brand_name || ''}` return { id: task.id, title: task.name, subtitle, phase, stage: uiStage, issues: issues.length > 0 ? issues : undefined, rejectionReason, submittedAt, } } // Mock Brief 数据 const mockBriefData = { files: [ { id: 'af1', name: '达人拍摄指南.pdf', size: '1.5MB', uploadedAt: '2026-02-02', description: '详细的拍摄流程和注意事项' }, { id: 'af2', name: '产品卖点话术.docx', size: '800KB', uploadedAt: '2026-02-02', description: '推荐使用的话术和表达方式' }, { id: 'af3', name: '品牌视觉参考.pdf', size: '3.2MB', uploadedAt: '2026-02-02', description: '视觉风格和拍摄参考示例' }, ] as AgencyBriefFile[], sellingPoints: [ { id: 'sp1', content: 'SPF50+ PA++++', required: true }, { id: 'sp2', content: '轻薄质地,不油腻', required: true }, { id: 'sp3', content: '延展性好,易推开', required: false }, { id: 'sp4', content: '适合敏感肌', required: false }, { id: 'sp5', content: '夏日必备防晒', required: true }, ], blacklistWords: [ { id: 'bw1', word: '最好', reason: '绝对化用语' }, { id: 'bw2', word: '第一', reason: '绝对化用语' }, { id: 'bw3', word: '神器', reason: '夸大宣传' }, { id: 'bw4', word: '完美', reason: '绝对化用语' }, ], } // Mock 任务数据 const mockTasksData: Record = { 'task-001': { id: 'task-001', title: 'XX品牌618推广', subtitle: '产品种草视频 · 当前步骤:上传脚本', phase: 'script', stage: 'upload' }, 'task-002': { id: 'task-002', title: 'YY美妆新品', subtitle: '口播测评 · 当前步骤:AI审核中', phase: 'script', stage: 'ai_reviewing', progress: 62, reviewLogs: [ { time: '14:32:01', message: '脚本上传完成', status: 'done' }, { time: '14:32:15', message: '任务规则已加载', status: 'done' }, { time: '14:33:45', message: '正在分析品牌调性匹配度...', status: 'loading' }, ]}, 'task-003': { id: 'task-003', title: 'ZZ饮品夏日', subtitle: '探店Vlog · 发现2处问题', phase: 'script', stage: 'ai_result', issues: [ { title: '检测到竞品提及', description: '脚本第3段提及了竞品「百事可乐」', severity: 'error' }, { title: '禁用词语出现', description: '脚本中出现「最好喝」等绝对化用语', severity: 'error' }, ]}, 'task-004': { id: 'task-004', title: 'AA数码新品发布', subtitle: '开箱测评 · 审核通过', phase: 'video', stage: 'brand_approved', submittedAt: '2026-02-01 10:30' }, 'task-008': { id: 'task-008', title: 'EE食品试吃', subtitle: '美食测评 · 脚本通过 · 待上传视频', phase: 'video', stage: 'upload' }, } // ========== UI 组件 ========== function StepIcon({ status, icon }: { status: 'done' | 'current' | 'error' | 'pending'; icon: 'upload' | 'bot' | 'users' | 'building' }) { const IconMap = { upload: Upload, bot: Bot, users: Users, building: Building2 } const Icon = IconMap[icon] const getStyle = () => { switch (status) { case 'done': return 'bg-accent-green' case 'current': return 'bg-accent-indigo' case 'error': return 'bg-accent-coral' default: return 'bg-bg-elevated border-[1.5px] border-border-subtle' } } const iconColor = status === 'pending' ? 'text-text-tertiary' : 'text-white' return (
{status === 'done' && } {status === 'current' && } {status === 'error' && } {status === 'pending' && }
) } function ReviewProgressBar({ task }: { task: TaskData }) { const { stage } = task const getStepStatus = (stepIndex: number): 'done' | 'current' | 'error' | 'pending' => { const stageMap: Record = { 'upload': 0, 'ai_reviewing': 1, 'ai_result': 1, 'agency_reviewing': 2, 'agency_rejected': 2, 'brand_reviewing': 3, 'brand_approved': 4, 'brand_rejected': 3, } const currentStepIndex = stageMap[stage] const isError = stage === 'ai_result' || stage === 'agency_rejected' || stage === 'brand_rejected' if (stepIndex < currentStepIndex) return 'done' if (stepIndex === currentStepIndex) { if (isError) return 'error' if (stage === 'brand_approved') return 'done' return 'current' } return 'pending' } const steps = [ { label: '已提交', icon: 'upload' as const }, { label: 'AI审核', icon: 'bot' as const }, { label: '代理商', icon: 'users' as const }, { label: '品牌方', icon: 'building' as const }, ] return (

{task.phase === 'script' ? '脚本审核流程' : '视频审核流程'}

{steps.map((step, index) => { const status = getStepStatus(index) return (
{step.label}
{index < steps.length - 1 && (
)}
) })}
) } // Brief 组件 function AgencyBriefSection({ toast, briefData }: { toast: ReturnType briefData: { files: AgencyBriefFile[]; sellingPoints: { id: string; content: string; required: boolean }[]; blacklistWords: { id: string; word: string; reason: string }[] } }) { const [isExpanded, setIsExpanded] = useState(true) const [previewFile, setPreviewFile] = useState(null) const handleDownload = (file: AgencyBriefFile) => { toast.info(`下载文件: ${file.name}`) } const requiredPoints = briefData.sellingPoints.filter(sp => sp.required) const optionalPoints = briefData.sellingPoints.filter(sp => !sp.required) return ( <>
Brief 文档与要求
{isExpanded && (

参考文档

{briefData.files.map((file) => (

{file.name}

{file.size}

))}

卖点要求

{requiredPoints.length > 0 && (

必选卖点(必须提及)

{requiredPoints.map((sp) => ( {sp.content} ))}
)} {optionalPoints.length > 0 && (

可选卖点

{optionalPoints.map((sp) => ( {sp.content} ))}
)}

违禁词(请勿使用)

{briefData.blacklistWords.map((bw) => ( 「{bw.word}」 ))}
)}
setPreviewFile(null)} title={previewFile?.name || '文件预览'} size="lg">

文件预览区域

{previewFile && }
) } function UploadView({ task, toast, briefData }: { task: TaskData; toast: ReturnType; briefData: typeof mockBriefData }) { const router = useRouter() const { id } = useParams() const isScript = task.phase === 'script' const uploadPath = isScript ? `/creator/task/${id}/script` : `/creator/task/${id}/video` const handleUploadClick = () => { router.push(uploadPath) } return (
{isScript && }

{isScript ? '上传脚本' : '上传视频'}

{isScript ? '支持粘贴文本或上传文档' : '支持 MP4/MOV 格式,≤ 100MB'}

待提交

点击进入上传页面

{isScript ? '支持 .doc、.docx、.txt 格式' : '支持 MP4/MOV 格式,≤ 100MB'}

) } function AIReviewingView({ task }: { task: TaskData }) { return (
{task.phase === 'script' ? '脚本内容审核' : '视频内容审核'} · 智能分析中

AI 正在审核您的{task.phase === 'script' ? '脚本' : '视频'}

预计还需 2-3 分钟,可先离开页面

{task.progress || 0}%
{task.reviewLogs && task.reviewLogs.length > 0 && (
处理日志
{task.reviewLogs.map((log, index) => (
{log.time} {log.message} {log.status === 'loading' && }
))}
)}
) } function RejectionView({ task, onAppeal }: { task: TaskData; onAppeal: () => void }) { const getTitle = () => { switch (task.stage) { case 'ai_result': return 'AI 审核结果' case 'agency_rejected': return '代理商审核结果' case 'brand_rejected': return '品牌方审核结果' default: return '审核结果' } } const getStatusText = () => { switch (task.stage) { case 'ai_result': return 'AI 检测到问题' case 'agency_rejected': return '代理商审核驳回' case 'brand_rejected': return '品牌方审核驳回' default: return '需要修改' } } return (
{getTitle()} {getStatusText()}
{task.rejectionReason && (

{task.rejectionReason}

)} {task.issues && task.issues.length > 0 && (
发现 {task.issues.length} 处问题
{task.issues.map((issue, index) => (
{issue.severity === 'error' ? '违规' : '建议'} {issue.title}

{issue.description}

))}
)}
) } function WaitingReviewView({ task }: { task: TaskData }) { const isAgency = task.stage === 'agency_reviewing' const title = isAgency ? '等待代理商审核' : '等待品牌方终审' const description = isAgency ? '您的内容已进入代理商审核环节,请耐心等待' : '您的内容已进入品牌方终审环节,这是最后一步' return (
{task.phase === 'script' ? '脚本提交信息' : '视频提交信息'}
提交时间 {task.submittedAt || '刚刚'}
AI审核 已通过
{isAgency && (
代理商审核 审核中...
)} {!isAgency && ( <>
代理商审核 已通过
品牌方终审 审核中...
)}
{title} {description}
温馨提示 {isAgency ? '代理商通常会在 1-2 个工作日内完成审核。' : '品牌方终审通常需要 1-3 个工作日。'}
) } function ApprovedView({ task }: { task: TaskData }) { const isVideoPhase = task.phase === 'video' return (
{task.phase === 'script' ? '脚本提交信息' : '视频提交信息'}
提交时间{task.submittedAt || '2026-02-01 10:30'}
AI审核已通过
代理商审核已通过
品牌方终审已通过
品牌方审核通过
已通过

{isVideoPhase ? '恭喜!视频已通过所有审核,可以发布了' : '脚本已通过品牌方终审,请继续上传视频'}

{isVideoPhase ? '全部审核通过' : '脚本审核通过'} {isVideoPhase ? '可以安排发布了' : '请在 7 天内上传视频'}
{isVideoPhase ? '恭喜完成!' : '下一步'} {isVideoPhase ? '您的视频已通过全部审核流程,可以在平台发布了。' : '脚本已通过审核,请在 7 天内上传对应视频。'}
{!isVideoPhase && (
)}
) } // ========== 主页面 ========== export default function TaskDetailPage() { const params = useParams() const router = useRouter() const toast = useToast() const { subscribe } = useSSE() const taskId = params.id as string const [taskData, setTaskData] = useState(null) const [briefData, setBriefData] = useState(mockBriefData) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const loadTask = useCallback(async () => { if (USE_MOCK) { const mock = mockTasksData[taskId] setTaskData(mock || null) setIsLoading(false) return } try { const task = await api.getTask(taskId) setTaskData(mapApiTaskToTaskData(task)) // 加载 Brief if (task.project?.id) { try { const brief = await api.getBrief(task.project.id) setBriefData({ files: (brief.attachments || []).map((a, i) => ({ id: a.id || `att-${i}`, name: a.name, size: a.size || '', uploadedAt: brief.updated_at || '', })), sellingPoints: (brief.selling_points || []).map((sp, i) => ({ id: `sp-${i}`, content: sp.content, required: sp.required, })), blacklistWords: (brief.blacklist_words || []).map((bw, i) => ({ id: `bw-${i}`, word: bw.word, reason: bw.reason, })), }) } catch { // Brief 可能不存在,不影响任务展示 } } } catch (err) { setError(err instanceof Error ? err.message : '加载失败') } finally { setIsLoading(false) } }, [taskId]) useEffect(() => { loadTask() }, [loadTask]) // SSE 实时更新 useEffect(() => { const unsub1 = subscribe('task_updated', (data) => { if ((data as { task_id?: string }).task_id === taskId) loadTask() }) const unsub2 = subscribe('review_completed', (data) => { if ((data as { task_id?: string }).task_id === taskId) loadTask() }) return () => { unsub1(); unsub2() } }, [subscribe, taskId, loadTask]) if (isLoading) { return (
) } if (error || !taskData) { return (

{error || '任务不存在'}

) } const handleAppeal = () => { router.push(`/creator/appeals/new?taskId=${taskId}`) } const renderContent = () => { switch (taskData.stage) { case 'upload': return case 'ai_reviewing': return case 'ai_result': case 'agency_rejected': case 'brand_rejected': return case 'agency_reviewing': case 'brand_reviewing': return case 'brand_approved': return default: return
未知状态
} } const getPageTitle = () => { switch (taskData.stage) { case 'upload': return taskData.phase === 'script' ? '上传脚本' : '上传视频' case 'ai_reviewing': return 'AI 智能审核' case 'ai_result': return 'AI 审核结果' case 'agency_reviewing': return '等待代理商审核' case 'agency_rejected': return '代理商审核驳回' case 'brand_reviewing': return '等待品牌方终审' case 'brand_approved': return '审核通过' case 'brand_rejected': return '品牌方审核驳回' default: return '任务详情' } } return (

{taskData.title}

{taskData.subtitle}

{renderContent()}
) }