'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, Video, CheckCircle, XCircle, AlertTriangle, Clock, Loader2, RefreshCw, Play, Radio, Shield } from 'lucide-react' import { api } from '@/lib/api' import { USE_MOCK } from '@/contexts/AuthContext' import { useSSE } from '@/contexts/SSEContext' import type { TaskResponse } from '@/types/task' // ========== 类型 ========== type VideoTaskUI = { projectName: string brandName: string videoStatus: string videoFile: string | null aiResult: null | { score: number hardViolations: Array<{ type: string; content: string; timestamp: number; suggestion: string }> sentimentWarnings: Array<{ type: string; content: string; timestamp: number }> sellingPointsCovered: Array<{ point: string; covered: boolean; timestamp?: number }> } agencyReview: null | { result: 'approved' | 'rejected'; comment: string; reviewer: string; time: string } brandReview: null | { result: 'approved' | 'rejected'; comment: string; reviewer: string; time: string } } // ========== 映射 ========== function mapApiToVideoUI(task: TaskResponse): VideoTaskUI { const stage = task.stage let status = 'pending_upload' switch (stage) { case 'video_upload': status = 'pending_upload'; break case 'video_ai_review': status = 'ai_reviewing'; break case 'video_agency_review': status = 'agent_reviewing'; break case 'video_brand_review': status = 'brand_reviewing'; break case 'completed': status = 'brand_passed'; break default: if (stage.startsWith('script_')) status = 'pending_upload' // 还没到视频阶段 if (stage === 'rejected') { if (task.video_brand_status === 'rejected') status = 'brand_rejected' else if (task.video_agency_status === 'rejected') status = 'agent_rejected' else status = 'ai_result' } } const aiResult = task.video_ai_result ? { score: task.video_ai_result.score, hardViolations: task.video_ai_result.violations .filter(v => v.severity === 'error' || v.severity === 'high') .map(v => ({ type: v.type, content: v.content, timestamp: v.timestamp || 0, suggestion: v.suggestion })), sentimentWarnings: (task.video_ai_result.soft_warnings || []) .map(w => ({ type: w.type, content: w.content, timestamp: 0 })), sellingPointsCovered: [], // 后端暂无此字段 } : null const agencyReview = task.video_agency_status && task.video_agency_status !== 'pending' ? { result: (task.video_agency_status === 'passed' || task.video_agency_status === 'force_passed' ? 'approved' : 'rejected') as 'approved' | 'rejected', comment: task.video_agency_comment || '', reviewer: task.agency?.name || '代理商', time: task.updated_at, } : null const brandReview = task.video_brand_status && task.video_brand_status !== 'pending' ? { result: (task.video_brand_status === 'passed' || task.video_brand_status === 'force_passed' ? 'approved' : 'rejected') as 'approved' | 'rejected', comment: task.video_brand_comment || '', reviewer: '品牌方审核员', time: task.updated_at, } : null return { projectName: task.project?.name || task.name, brandName: task.project?.brand_name || '', videoStatus: status, videoFile: task.video_file_name || null, aiResult, agencyReview, brandReview, } } const mockDefaultTask: VideoTaskUI = { projectName: 'XX品牌618推广', brandName: 'XX护肤品牌', videoStatus: 'pending_upload', videoFile: null, aiResult: null, agencyReview: null, brandReview: null, } function formatTimestamp(seconds: number): string { const mins = Math.floor(seconds / 60) const secs = Math.floor(seconds % 60) return `${mins}:${secs.toString().padStart(2, '0')}` } // ========== UI 组件 ========== function UploadSection({ taskId, onUploaded }: { taskId: string; onUploaded: () => void }) { const [file, setFile] = useState(null) const [isUploading, setIsUploading] = useState(false) const [progress, setProgress] = useState(0) const [uploadError, setUploadError] = useState(null) const toast = useToast() const handleFileChange = (e: React.ChangeEvent) => { const selectedFile = e.target.files?.[0] if (selectedFile) { setFile(selectedFile) setUploadError(null) } } const handleUpload = async () => { if (!file) return setIsUploading(true) setProgress(0) setUploadError(null) try { if (USE_MOCK) { for (let i = 0; i <= 100; i += 10) { await new Promise(r => setTimeout(r, 300)) setProgress(i) } toast.success('视频已提交,等待 AI 审核') onUploaded() } else { const result = await api.proxyUpload(file, 'video', (pct) => { setProgress(Math.min(90, Math.round(pct * 0.9))) }) setProgress(95) await api.uploadTaskVideo(taskId, { file_url: result.url, file_name: result.file_name }) setProgress(100) toast.success('视频已提交,等待 AI 审核') onUploaded() } } catch (err) { const msg = err instanceof Error ? err.message : '上传失败' setUploadError(msg) toast.error(msg) } finally { setIsUploading(false) } } const formatSize = (bytes: number) => { if (bytes < 1024) return bytes + 'B' if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB' return (bytes / (1024 * 1024)).toFixed(1) + 'MB' } return ( 上传视频 {!file ? ( ) : (
已选文件
{isUploading ? ( ) : uploadError ? ( ) : ( )}
{isUploading && (
)} {isUploading && (

上传中 {progress}%

)} {uploadError && (

{uploadError}

)}
)} ) } function AIReviewingSection() { const [progress, setProgress] = useState(0) const [currentStep, setCurrentStep] = useState('正在解析视频...') useEffect(() => { const steps = ['正在解析视频...', '正在提取音频转文字...', '正在分析画面内容...', '正在检测违禁内容...', '正在分析卖点覆盖...', '正在生成审核报告...'] let stepIndex = 0 const timer = setInterval(() => { setProgress(prev => prev >= 100 ? (clearInterval(timer), 100) : prev + 5) }, 300) const stepTimer = setInterval(() => { stepIndex = (stepIndex + 1) % steps.length; setCurrentStep(steps[stepIndex]) }, 1500) return () => { clearInterval(timer); clearInterval(stepTimer) } }, []) return (

AI 正在审核您的视频

请稍候,视频审核可能需要 3-5 分钟

{progress}%

{currentStep}

) } function AIResultSection({ task }: { task: VideoTaskUI }) { 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.hardViolations.length > 0 && ( 硬性合规 ({task.aiResult.hardViolations.length}) {task.aiResult.hardViolations.map((v, idx) => (
{v.type}{formatTimestamp(v.timestamp)}

「{v.content}」

{v.suggestion}

))}
)} {task.aiResult.sentimentWarnings.length > 0 && ( 舆情雷达 {task.aiResult.sentimentWarnings.map((w, idx) => (
{w.type}{formatTimestamp(w.timestamp)}

{w.content}

))}
)} {task.aiResult.sellingPointsCovered.length > 0 && ( 卖点覆盖 {task.aiResult.sellingPointsCovered.map((sp, idx) => (
{sp.covered ? : } {sp.point}
{sp.covered && sp.timestamp && {formatTimestamp(sp.timestamp)}}
))}
)}
) } 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() { return (

视频审核通过!

恭喜您,视频已通过所有审核,可以发布了

) } // ========== 主页面 ========== export default function CreatorVideoPage() { const router = useRouter() const params = useParams() const toast = useToast() const { subscribe } = useSSE() const taskId = params.id as string const [task, setTask] = useState(mockDefaultTask) const [isLoading, setIsLoading] = useState(true) const loadTask = useCallback(async () => { if (USE_MOCK) { setIsLoading(false); return } try { const apiTask = await api.getTask(taskId) setTask(mapApiToVideoUI(apiTask)) } catch { 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 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.videoStatus] || '未知状态' } if (isLoading) { return
} return (

{task.projectName}

视频阶段 · {getStatusDisplay()}

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