'use client' import { useState, useEffect, useCallback } from 'react' import Link from 'next/link' import { useToast } from '@/components/ui/Toast' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card' import { Button } from '@/components/ui/Button' import { SuccessTag, PendingTag, WarningTag, ErrorTag } from '@/components/ui/Tag' import { FileText, Video, Search, Clock, Eye, File, Download, MessageSquareWarning, Loader2 } from 'lucide-react' import { Modal } from '@/components/ui/Modal' import { getPlatformInfo } from '@/lib/platforms' import { api } from '@/lib/api' import { USE_MOCK } from '@/contexts/AuthContext' import { useSSE } from '@/contexts/SSEContext' import type { TaskResponse } from '@/types/task' // ==================== Mock 数据 ==================== const mockScriptTasks: TaskResponse[] = [ { id: 'script-001', name: '夏日护肤推广脚本', sequence: 1, stage: 'script_agency_review', project: { id: 'proj-001', name: 'XX品牌618推广', brand_name: 'XX品牌' }, agency: { id: 'ag-001', name: '优创代理' }, creator: { id: 'cr-001', name: '小美护肤' }, script_file_name: '夏日护肤推广_脚本v2.docx', script_ai_score: 88, script_ai_result: { score: 88, violations: [], soft_warnings: [] }, appeal_count: 0, is_appeal: false, created_at: '2026-02-06T14:30:00Z', updated_at: '2026-02-06T14:30:00Z', }, { id: 'script-002', name: '新品口红试色脚本', sequence: 2, stage: 'script_agency_review', project: { id: 'proj-001', name: 'XX品牌618推广', brand_name: 'XX品牌' }, agency: { id: 'ag-001', name: '优创代理' }, creator: { id: 'cr-002', name: '美妆Lisa' }, script_file_name: '口红试色_脚本v1.docx', script_ai_score: 72, script_ai_result: { score: 72, violations: [{ type: '违禁词', content: '最好', severity: 'medium', suggestion: '替换' }], soft_warnings: [] }, appeal_count: 1, is_appeal: true, appeal_reason: '已修改违规用词,请求重新审核', created_at: '2026-02-06T12:15:00Z', updated_at: '2026-02-06T12:15:00Z', }, { id: 'script-003', name: '健身器材推荐脚本', sequence: 3, stage: 'script_agency_review', project: { id: 'proj-002', name: 'XX运动品牌', brand_name: 'XX运动' }, agency: { id: 'ag-001', name: '优创代理' }, creator: { id: 'cr-003', name: '健身教练王' }, script_file_name: '健身器材_推荐脚本.pdf', script_ai_score: 95, script_ai_result: { score: 95, violations: [], soft_warnings: [] }, appeal_count: 0, is_appeal: false, created_at: '2026-02-06T10:00:00Z', updated_at: '2026-02-06T10:00:00Z', }, ] const mockVideoTasks: TaskResponse[] = [ { id: 'video-001', name: '夏日护肤推广', sequence: 1, stage: 'video_agency_review', project: { id: 'proj-001', name: 'XX品牌618推广', brand_name: 'XX品牌' }, agency: { id: 'ag-001', name: '优创代理' }, creator: { id: 'cr-001', name: '小美护肤' }, video_file_name: '夏日护肤_成片v2.mp4', video_duration: 135, video_ai_score: 85, video_ai_result: { score: 85, violations: [], soft_warnings: [] }, appeal_count: 0, is_appeal: false, created_at: '2026-02-06T15:00:00Z', updated_at: '2026-02-06T15:00:00Z', }, { id: 'video-002', name: '新品口红试色', sequence: 2, stage: 'video_agency_review', project: { id: 'proj-001', name: 'XX品牌618推广', brand_name: 'XX品牌' }, agency: { id: 'ag-001', name: '优创代理' }, creator: { id: 'cr-002', name: '美妆Lisa' }, video_file_name: '口红试色_终版.mp4', video_duration: 222, video_ai_score: 68, video_ai_result: { score: 68, violations: [{ type: '竞品', content: '疑似竞品', severity: 'high', suggestion: '确认' }], soft_warnings: [] }, appeal_count: 1, is_appeal: true, appeal_reason: '已按要求重新剪辑,删除了争议片段', created_at: '2026-02-06T13:45:00Z', updated_at: '2026-02-06T13:45:00Z', }, { id: 'video-003', name: '美妆新品体验', sequence: 3, stage: 'video_agency_review', project: { id: 'proj-001', name: 'XX品牌618推广', brand_name: 'XX品牌' }, agency: { id: 'ag-001', name: '优创代理' }, creator: { id: 'cr-003', name: '达人C' }, video_file_name: '美妆体验_v3.mp4', video_duration: 260, video_ai_score: 58, video_ai_result: { score: 58, violations: [{ type: '违禁词', content: '最好', severity: 'high', suggestion: '替换' }], soft_warnings: [] }, appeal_count: 0, is_appeal: false, created_at: '2026-02-06T11:30:00Z', updated_at: '2026-02-06T11:30:00Z', }, ] // ==================== 工具函数 ==================== function getRiskLevel(task: TaskResponse, type: 'script' | 'video'): 'low' | 'medium' | 'high' { const score = type === 'script' ? task.script_ai_score : task.video_ai_score if (score == null) return 'low' if (score >= 85) return 'low' if (score >= 70) return 'medium' return 'high' } const riskLevelConfig = { low: { label: 'AI通过', color: 'bg-accent-green', textColor: 'text-accent-green' }, medium: { label: '风险:中', color: 'bg-accent-amber', textColor: 'text-accent-amber' }, high: { label: '风险:高', color: 'bg-accent-coral', textColor: 'text-accent-coral' }, } function formatDuration(seconds: number): string { const mins = Math.floor(seconds / 60) const secs = Math.floor(seconds % 60) return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` } function ScoreTag({ score }: { score: number }) { if (score >= 85) return {score}分 if (score >= 70) return {score}分 return {score}分 } // ==================== 卡片组件 ==================== function ScriptTaskCard({ task, onPreview, toast }: { task: TaskResponse; onPreview: (task: TaskResponse) => void; toast: ReturnType }) { const riskLevel = getRiskLevel(task, 'script') const riskConfig = riskLevelConfig[riskLevel] const handleDownload = (e: React.MouseEvent) => { e.stopPropagation() toast.info(`下载文件: ${task.script_file_name || '脚本文件'}`) } const handlePreview = (e: React.MouseEvent) => { e.stopPropagation() onPreview(task) } return (
{/* 顶部条 */}
{task.project.brand_name || task.project.name} {task.is_appeal && ( 申诉 )}
{task.creator.name} · {task.name}
{riskConfig.label}
{task.is_appeal && task.appeal_reason && (

申诉理由

{task.appeal_reason}

)}

{task.script_file_name || '脚本文件'}

{new Date(task.created_at).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}
) } function VideoTaskCard({ task, onPreview, toast }: { task: TaskResponse; onPreview: (task: TaskResponse) => void; toast: ReturnType }) { const riskLevel = getRiskLevel(task, 'video') const riskConfig = riskLevelConfig[riskLevel] const handleDownload = (e: React.MouseEvent) => { e.stopPropagation() toast.info(`下载文件: ${task.video_file_name || '视频文件'}`) } const handlePreview = (e: React.MouseEvent) => { e.stopPropagation() onPreview(task) } return (
{task.project.brand_name || task.project.name} {task.is_appeal && ( 申诉 )}
{task.creator.name} · {task.name}
{riskConfig.label}
{task.is_appeal && task.appeal_reason && (

申诉理由

{task.appeal_reason}

)}

{task.video_file_name || '视频文件'}

{task.video_duration && (

{formatDuration(task.video_duration)}

)}
{new Date(task.created_at).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}
) } // ==================== 骨架屏 ==================== function ReviewListSkeleton() { return (
{[1, 2].map(i => (
{[1, 2, 3].map(j => (
))}
))}
) } // ==================== 主页面 ==================== export default function AgencyReviewListPage() { const [searchQuery, setSearchQuery] = useState('') const [activeTab, setActiveTab] = useState<'all' | 'script' | 'video'>('all') const [previewTask, setPreviewTask] = useState(null) const [previewType, setPreviewType] = useState<'script' | 'video'>('script') const [scriptTasks, setScriptTasks] = useState([]) const [videoTasks, setVideoTasks] = useState([]) const [loading, setLoading] = useState(true) const toast = useToast() const { subscribe } = useSSE() const loadData = useCallback(async () => { if (USE_MOCK) { setScriptTasks(mockScriptTasks) setVideoTasks(mockVideoTasks) setLoading(false) return } try { const [scriptData, videoData] = await Promise.all([ api.listTasks(1, 50, 'script_agency_review'), api.listTasks(1, 50, 'video_agency_review'), ]) setScriptTasks(scriptData.items) setVideoTasks(videoData.items) } catch (err) { console.error('Failed to load review tasks:', err) toast.error('加载审核任务失败') } finally { setLoading(false) } }, [toast]) useEffect(() => { loadData() }, [loadData]) useEffect(() => { const unsub1 = subscribe('task_updated', () => loadData()) const unsub2 = subscribe('new_task', () => loadData()) return () => { unsub1(); unsub2() } }, [subscribe, loadData]) if (loading) return const filteredScripts = scriptTasks.filter(task => task.name.toLowerCase().includes(searchQuery.toLowerCase()) || task.creator.name.toLowerCase().includes(searchQuery.toLowerCase()) ) const filteredVideos = videoTasks.filter(task => task.name.toLowerCase().includes(searchQuery.toLowerCase()) || task.creator.name.toLowerCase().includes(searchQuery.toLowerCase()) ) const appealScriptCount = scriptTasks.filter(t => t.is_appeal).length const appealVideoCount = videoTasks.filter(t => t.is_appeal).length const handleScriptPreview = (task: TaskResponse) => { setPreviewTask(task) setPreviewType('script') } const handleVideoPreview = (task: TaskResponse) => { setPreviewTask(task) setPreviewType('video') } return (
{/* 页面标题 */}

审核台

审核达人提交的脚本和视频

待审核: {scriptTasks.length} 脚本 {videoTasks.length} 视频 {(appealScriptCount + appealVideoCount) > 0 && ( {appealScriptCount + appealVideoCount} 申诉 )}
{/* 搜索和筛选 */}
setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo" />
{/* 任务列表 */}
{(activeTab === 'all' || activeTab === 'script') && ( 脚本审核 {filteredScripts.length} 条待审核 {filteredScripts.length > 0 ? ( filteredScripts.map((task) => ( )) ) : (

暂无待审脚本

)}
)} {(activeTab === 'all' || activeTab === 'video') && ( {filteredVideos.length > 0 ? ( filteredVideos.map((task) => ( )) ) : (
)}
)}
{/* 预览弹窗 */} setPreviewTask(null)} title={previewType === 'script' ? (previewTask?.script_file_name || '脚本预览') : (previewTask?.video_file_name || '视频预览')} size="lg" >
{previewTask?.is_appeal && previewTask?.appeal_reason && (

申诉理由

{previewTask.appeal_reason}

)} {previewType === 'script' ? (

脚本预览区域

实际开发中将嵌入文档预览组件

) : (
)}
{previewType === 'script' ? previewTask?.script_file_name : previewTask?.video_file_name} {previewType === 'video' && previewTask?.video_duration && ( <> · {formatDuration(previewTask.video_duration)} )}
) }