'use client' import { useState, useEffect, useCallback } from 'react' import { useRouter, useParams } from 'next/navigation' import { useToast } from '@/components/ui/Toast' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card' import { Button } from '@/components/ui/Button' import { SuccessTag, WarningTag, ErrorTag } from '@/components/ui/Tag' import { ReviewSteps, getReviewSteps } from '@/components/ui/ReviewSteps' import { ArrowLeft, Upload, FileText, CheckCircle, XCircle, AlertTriangle, Clock, Loader2, RefreshCw, Eye, Download, File, Target, Ban, ChevronDown, ChevronUp } from 'lucide-react' import { Modal } from '@/components/ui/Modal' import { api } from '@/lib/api' import { USE_MOCK } from '@/contexts/AuthContext' import { useSSE } from '@/contexts/SSEContext' import { useOSSUpload } from '@/hooks/useOSSUpload' import type { TaskResponse, AIReviewResult } from '@/types/task' import type { BriefResponse } from '@/types/brief' // ========== 类型 ========== type AgencyBriefFile = { id: string; name: string; size: string; uploadedAt: string; description?: string } type ScriptTaskUI = { projectName: string brandName: string scriptStatus: string scriptFile: string | null aiResult: null | { score: number violations: Array<{ type: string; content: string; suggestion: string }> complianceChecks: Array<{ item: string; passed: boolean; note?: string }> } agencyReview: null | { result: 'approved' | 'rejected'; comment: string; reviewer: string; time: string } brandReview: null | { result: 'approved' | 'rejected'; comment: string; reviewer: string; time: string } } type BriefUI = { files: AgencyBriefFile[] sellingPoints: { id: string; content: string; required: boolean }[] blacklistWords: { id: string; word: string; reason: string }[] } // ========== 映射 ========== function mapApiToScriptUI(task: TaskResponse): ScriptTaskUI { const stage = task.stage let status = 'pending_upload' switch (stage) { case 'script_upload': status = 'pending_upload'; break case 'script_ai_review': status = 'ai_reviewing'; break case 'script_agency_review': status = 'agent_reviewing'; break case 'script_brand_review': status = 'brand_reviewing'; break default: if (stage.startsWith('video_') || stage === 'completed') status = 'brand_passed' if (stage === 'rejected') { if (task.script_brand_status === 'rejected') status = 'brand_rejected' else if (task.script_agency_status === 'rejected') status = 'agent_rejected' else status = 'ai_result' } } // 有 AI 结果且还在脚本审核阶段 → ai_result if (task.script_ai_result && stage === 'script_agency_review') status = 'agent_reviewing' const aiResult = task.script_ai_result ? { score: task.script_ai_result.score, violations: task.script_ai_result.violations.map(v => ({ type: v.type, content: v.content, suggestion: v.suggestion })), complianceChecks: task.script_ai_result.violations.map(v => ({ item: v.type, passed: v.severity !== 'error' && v.severity !== 'warning', note: v.suggestion, })), } : null const agencyReview = task.script_agency_status && task.script_agency_status !== 'pending' ? { result: (task.script_agency_status === 'passed' || task.script_agency_status === 'force_passed' ? 'approved' : 'rejected') as 'approved' | 'rejected', comment: task.script_agency_comment || '', reviewer: task.agency?.name || '代理商', time: task.updated_at, } : null const brandReview = task.script_brand_status && task.script_brand_status !== 'pending' ? { result: (task.script_brand_status === 'passed' || task.script_brand_status === 'force_passed' ? 'approved' : 'rejected') as 'approved' | 'rejected', comment: task.script_brand_comment || '', reviewer: '品牌方审核员', time: task.updated_at, } : null return { projectName: task.project?.name || task.name, brandName: task.project?.brand_name || '', scriptStatus: status, scriptFile: task.script_file_name || null, aiResult, agencyReview, brandReview, } } function mapBriefToUI(brief: BriefResponse): BriefUI { return { 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 })), } } // Mock 数据 const mockBrief: BriefUI = { files: [ { id: 'af1', name: '达人拍摄指南.pdf', size: '1.5MB', uploadedAt: '2026-02-02' }, { id: 'af2', name: '产品卖点话术.docx', size: '800KB', uploadedAt: '2026-02-02' }, ], sellingPoints: [ { id: 'sp1', content: 'SPF50+ PA++++', required: true }, { id: 'sp2', content: '轻薄质地,不油腻', required: true }, { id: 'sp3', content: '延展性好,易推开', required: false }, ], blacklistWords: [ { id: 'bw1', word: '最好', reason: '绝对化用语' }, { id: 'bw2', word: '第一', reason: '绝对化用语' }, ], } const mockDefaultTask: ScriptTaskUI = { projectName: 'XX品牌618推广', brandName: 'XX护肤品牌', scriptStatus: 'pending_upload', scriptFile: null, aiResult: null, agencyReview: null, brandReview: null, } // ========== UI 组件 ========== function AgencyBriefSection({ toast, briefData }: { toast: ReturnType; briefData: BriefUI }) { 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 UploadSection({ taskId, onUploaded }: { taskId: string; onUploaded: () => void }) { const [file, setFile] = useState(null) const { upload, isUploading, progress } = useOSSUpload('script') const toast = useToast() const handleFileChange = (e: React.ChangeEvent) => { const selectedFile = e.target.files?.[0] if (selectedFile) setFile(selectedFile) } const handleSubmit = async () => { if (!file) return try { const result = await upload(file) if (!USE_MOCK) { await api.uploadTaskScript(taskId, { file_url: result.url, file_name: result.file_name }) } toast.success('脚本已提交,等待 AI 审核') onUploaded() } catch (err) { toast.error(err instanceof Error ? err.message : '上传失败') } } return ( 上传脚本
{file ? (
{file.name} {!isUploading && ( )}
{isUploading && (

上传中 {progress}%

)}
) : ( )}
) } function AIReviewingSection() { const [progress, setProgress] = useState(0) const [logs, setLogs] = useState(['开始解析脚本文件...']) useEffect(() => { const timer = setInterval(() => { setProgress(prev => prev >= 100 ? (clearInterval(timer), 100) : prev + 10) }, 500) const t1 = setTimeout(() => setLogs(prev => [...prev, '正在提取文本内容...']), 1000) const t2 = setTimeout(() => setLogs(prev => [...prev, '正在进行违禁词检测...']), 2000) const t3 = setTimeout(() => setLogs(prev => [...prev, '正在分析卖点覆盖...']), 3000) return () => { clearInterval(timer); clearTimeout(t1); clearTimeout(t2); clearTimeout(t3) } }, []) return (

AI 正在审核您的脚本

请稍候,预计需要 1-2 分钟

{progress}%

处理日志

{logs.map((log, idx) =>

{log}

)}
) } function AIResultSection({ task }: { task: ScriptTaskUI }) { if (!task.aiResult) return null return ( AI 审核结果 = 85 ? 'text-accent-green' : task.aiResult.score >= 70 ? 'text-yellow-400' : 'text-accent-coral'}`}>{task.aiResult.score}分 {task.aiResult.violations.length > 0 && (

违规检测 ({task.aiResult.violations.length})

{task.aiResult.violations.map((v, idx) => (
{v.type}

「{v.content}」

{v.suggestion}

))}
)}

合规检查

{task.aiResult.complianceChecks.map((check, idx) => (
{check.passed ? : }
{check.item} {check.note &&

{check.note}

}
))}
) } function ReviewFeedbackSection({ review, type }: { review: { result: string; comment: string; reviewer: string; time: string }; type: 'agency' | 'brand' }) { const isApproved = review.result === 'approved' const title = type === 'agency' ? '代理商审核意见' : '品牌方终审意见' return ( {isApproved ? : }{title}
{review.reviewer} {isApproved ? 通过 : 驳回}

{review.comment}

{review.time}

) } function WaitingSection({ message }: { message: string }) { return (

{message}

请耐心等待,审核结果将通过消息通知您

) } function SuccessSection({ onContinue }: { onContinue: () => void }) { return (

脚本审核通过!

您可以开始拍摄视频了

) } // ========== 主页面 ========== export default function CreatorScriptPage() { const router = useRouter() const params = useParams() const toast = useToast() const { subscribe } = useSSE() const taskId = params.id as string const [task, setTask] = useState(mockDefaultTask) const [briefData, setBriefData] = useState(mockBrief) const [isLoading, setIsLoading] = useState(true) const loadTask = useCallback(async () => { if (USE_MOCK) { setIsLoading(false) return } try { const apiTask = await api.getTask(taskId) setTask(mapApiToScriptUI(apiTask)) if (apiTask.project?.id) { try { const brief = await api.getBrief(apiTask.project.id) setBriefData(mapBriefToUI(brief)) } catch { /* Brief may not exist */ } } } catch (err) { toast.error('加载任务失败') } finally { setIsLoading(false) } }, [taskId, toast]) useEffect(() => { loadTask() }, [loadTask]) 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]) const handleContinueToVideo = () => { router.push(`/creator/task/${params.id}/video`) } const getStatusDisplay = () => { const map: Record = { pending_upload: '待上传脚本', ai_reviewing: 'AI 审核中', ai_result: 'AI 审核完成', agent_reviewing: '代理商审核中', agent_rejected: '代理商驳回', brand_reviewing: '品牌方终审中', brand_passed: '审核通过', brand_rejected: '品牌方驳回', } return map[task.scriptStatus] || '未知状态' } if (isLoading) { return
} return (

{task.projectName}

脚本阶段 · {getStatusDisplay()}

{task.scriptStatus === 'pending_upload' && } {task.scriptStatus === 'ai_reviewing' && } {task.scriptStatus === 'ai_result' && <>} {task.scriptStatus === 'agent_reviewing' && <>} {task.scriptStatus === 'agent_rejected' && task.agencyReview && ( <>
)} {task.scriptStatus === 'brand_reviewing' && task.agencyReview && ( <> )} {task.scriptStatus === 'brand_passed' && task.agencyReview && task.brandReview && ( <> )} {task.scriptStatus === 'brand_rejected' && task.agencyReview && task.brandReview && ( <>
)}
) }