'use client' import { useState, useEffect, useCallback } from 'react' import Link from 'next/link' 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 { useToast } from '@/components/ui/Toast' import { FileText, Video, Search, Filter, Clock, User, Building, ChevronRight, AlertTriangle, Download, Eye, File, 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 type { TaskResponse } from '@/types/task' // ==================== Mock 数据 ==================== const mockScriptTasks = [ { id: 'script-001', title: '夏日护肤推广脚本', fileName: '夏日护肤推广_脚本v2.docx', fileSize: '245 KB', creatorName: '小美护肤', agencyName: '星耀传媒', projectName: 'XX品牌618推广', platform: 'douyin', aiScore: 88, submittedAt: '2026-02-06 14:30', hasHighRisk: false, agencyApproved: true, isAppeal: false, }, { id: 'script-002', title: '新品口红试色脚本', fileName: '口红试色_脚本v1.docx', fileSize: '312 KB', creatorName: '美妆Lisa', agencyName: '创意无限', projectName: 'XX品牌618推广', platform: 'xiaohongshu', aiScore: 72, submittedAt: '2026-02-06 12:15', hasHighRisk: true, agencyApproved: true, isAppeal: true, appealReason: '已修改违规用词,请求品牌方重新审核', }, ] const mockVideoTasks = [ { id: 'video-001', title: '夏日护肤推广', fileName: '夏日护肤_成片v2.mp4', fileSize: '128 MB', creatorName: '小美护肤', agencyName: '星耀传媒', projectName: 'XX品牌618推广', platform: 'douyin', aiScore: 85, duration: '02:15', submittedAt: '2026-02-06 15:00', hasHighRisk: false, agencyApproved: true, isAppeal: false, }, { id: 'video-002', title: '新品口红试色', fileName: '口红试色_终版.mp4', fileSize: '256 MB', creatorName: '美妆Lisa', agencyName: '创意无限', projectName: 'XX品牌618推广', platform: 'xiaohongshu', aiScore: 68, duration: '03:42', submittedAt: '2026-02-06 13:45', hasHighRisk: true, agencyApproved: true, isAppeal: true, appealReason: '已按要求重新剪辑,删除了争议片段,请求终审', }, { id: 'video-003', title: '健身器材开箱', fileName: '健身器材_开箱v3.mp4', fileSize: '198 MB', creatorName: '健身教练王', agencyName: '美妆达人MCN', projectName: 'XX运动品牌', platform: 'bilibili', aiScore: 92, duration: '04:20', submittedAt: '2026-02-06 11:30', hasHighRisk: false, agencyApproved: true, isAppeal: false, }, ] // ==================== 类型定义 ==================== interface UITask { id: string title: string fileName: string fileSize: string creatorName: string agencyName: string projectName: string platform: string aiScore: number submittedAt: string hasHighRisk: boolean agencyApproved: boolean isAppeal: boolean appealReason?: string duration?: string } // ==================== 映射函数 ==================== /** * 将后端 TaskResponse 映射为 UI 任务格式 */ function mapTaskToUI(task: TaskResponse, type: 'script' | 'video'): UITask { const isScript = type === 'script' // AI 评分:脚本用 script_ai_score,视频用 video_ai_score const aiScore = isScript ? (task.script_ai_score ?? 0) : (task.video_ai_score ?? 0) // AI 审核结果中检测是否有高风险(severity === 'high') const aiResult = isScript ? task.script_ai_result : task.video_ai_result const hasHighRisk = aiResult?.violations?.some(v => v.severity === 'high') ?? false // 代理商审核状态 const agencyStatus = isScript ? task.script_agency_status : task.video_agency_status const agencyApproved = agencyStatus === 'passed' || agencyStatus === 'force_passed' // 文件名 const fileName = isScript ? (task.script_file_name ?? '未上传脚本') : (task.video_file_name ?? '未上传视频') // 视频时长:后端返回秒数,转为 mm:ss 格式 let duration: string | undefined if (!isScript && task.video_duration) { const minutes = Math.floor(task.video_duration / 60) const seconds = task.video_duration % 60 duration = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}` } // 格式化提交时间 const submittedAt = formatDateTime(task.updated_at) // 平台信息:从项目获取 const platform = task.project.platform || '' return { id: task.id, title: `${task.project.name} · ${task.name}`, fileName, fileSize: isScript ? '--' : '--', creatorName: task.creator.name, agencyName: task.agency.name, projectName: task.project.name, platform, aiScore, submittedAt, hasHighRisk, agencyApproved, isAppeal: task.is_appeal, appealReason: task.appeal_reason ?? undefined, duration, } } /** * 格式化日期时间为 YYYY-MM-DD HH:mm */ function formatDateTime(isoString: string): string { try { const d = new Date(isoString) const year = d.getFullYear() const month = String(d.getMonth() + 1).padStart(2, '0') const day = String(d.getDate()).padStart(2, '0') const hours = String(d.getHours()).padStart(2, '0') const minutes = String(d.getMinutes()).padStart(2, '0') return `${year}-${month}-${day} ${hours}:${minutes}` } catch { return isoString } } // ==================== 子组件 ==================== function ScoreTag({ score }: { score: number }) { if (score >= 85) return {score}分 if (score >= 70) return {score}分 return {score}分 } function TaskCard({ task, type, onPreview }: { task: UITask type: 'script' | 'video' onPreview: (task: UITask, type: 'script' | 'video') => void }) { const toast = useToast() const href = type === 'script' ? `/brand/review/script/${task.id}` : `/brand/review/video/${task.id}` const platform = getPlatformInfo(task.platform) const handlePreview = (e: React.MouseEvent) => { e.preventDefault() e.stopPropagation() onPreview(task, type) } const handleDownload = (e: React.MouseEvent) => { e.preventDefault() e.stopPropagation() toast.info(`下载文件: ${task.fileName}`) } return (
{/* 平台顶部条 */} {platform && (
{platform.icon} {platform.name} {/* 申诉标识 */} {task.isAppeal && ( 申诉 )}
)}

{task.title}

{task.hasHighRisk && ( 高风险 )}
{task.creatorName} {task.agencyName}
{/* 申诉理由 */} {task.isAppeal && task.appealReason && (

申诉理由

{task.appealReason}

)} {/* 文件信息 */}
{type === 'script' ? ( ) : (

{task.fileName}

{task.fileSize} {task.duration && ` · ${task.duration}`}

{task.projectName} {task.submittedAt}
) } function TaskListSkeleton({ count = 2 }: { count?: number }) { return ( <> {Array.from({ length: count }).map((_, i) => (
))} ) } // ==================== 主页面 ==================== export default function BrandReviewListPage() { const toast = useToast() const [searchQuery, setSearchQuery] = useState('') const [activeTab, setActiveTab] = useState<'all' | 'script' | 'video'>('all') const [previewTask, setPreviewTask] = useState<{ task: UITask; type: 'script' | 'video' } | null>(null) // API 数据状态 const [scriptTasks, setScriptTasks] = useState([]) const [videoTasks, setVideoTasks] = useState([]) const [loading, setLoading] = useState(!USE_MOCK) const [error, setError] = useState(null) // 从 API 加载数据 const fetchTasks = useCallback(async () => { if (USE_MOCK) return setLoading(true) setError(null) try { const [scriptRes, videoRes] = await Promise.all([ api.listTasks(1, 20, 'script_brand_review'), api.listTasks(1, 20, 'video_brand_review'), ]) setScriptTasks(scriptRes.items.map(t => mapTaskToUI(t, 'script'))) setVideoTasks(videoRes.items.map(t => mapTaskToUI(t, 'video'))) } catch (err) { const message = err instanceof Error ? err.message : '加载任务失败' setError(message) toast.error(message) } finally { setLoading(false) } }, [toast]) useEffect(() => { if (USE_MOCK) { setScriptTasks(mockScriptTasks) setVideoTasks(mockVideoTasks) } else { fetchTasks() } }, [fetchTasks]) // 搜索过滤 const filteredScripts = scriptTasks.filter(task => task.title.toLowerCase().includes(searchQuery.toLowerCase()) || task.creatorName.toLowerCase().includes(searchQuery.toLowerCase()) ) const filteredVideos = videoTasks.filter(task => task.title.toLowerCase().includes(searchQuery.toLowerCase()) || task.creatorName.toLowerCase().includes(searchQuery.toLowerCase()) ) // 计算申诉数量 const appealScriptCount = scriptTasks.filter(t => t.isAppeal).length const appealVideoCount = videoTasks.filter(t => t.isAppeal).length const handlePreview = (task: UITask, type: 'script' | 'video') => { setPreviewTask({ task, type }) } return (
{/* 页面标题 */}

终审台

审核代理商提交的脚本和视频

{loading ? ( 加载中... ) : ( <> 待审核: {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" />
{/* 加载错误提示 */} {error && (
加载失败: {error}
)} {/* 任务列表 */}
{/* 脚本待审列表 */} {(activeTab === 'all' || activeTab === 'script') && ( 脚本终审 {loading ? '...' : `${filteredScripts.length} 条待审`} {loading ? ( ) : filteredScripts.length > 0 ? ( filteredScripts.map((task) => ( )) ) : (

暂无待审脚本

)}
)} {/* 视频待审列表 */} {(activeTab === 'all' || activeTab === 'video') && ( {loading ? ( ) : filteredVideos.length > 0 ? ( filteredVideos.map((task) => ( )) ) : (
)}
)}
{/* 预览弹窗 */} setPreviewTask(null)} title={previewTask?.task.fileName || '文件预览'} size="lg" >
{/* 申诉理由 */} {previewTask?.task.isAppeal && previewTask?.task.appealReason && (

申诉理由

{previewTask.task.appealReason}

)} {/* 预览区域 */} {previewTask?.type === 'video' ? (
) : (

脚本预览区域

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

)} {/* 文件信息和操作 */}
{previewTask?.task.fileName} · {previewTask?.task.fileSize} {previewTask?.type === 'video' && previewTask?.task.duration && ( <> · {previewTask.task.duration} )}
) }