'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 { Modal, ConfirmModal } from '@/components/ui/Modal' import { SuccessTag, WarningTag, ErrorTag } from '@/components/ui/Tag' import { ReviewSteps, getBrandReviewSteps } from '@/components/ui/ReviewSteps' import { ArrowLeft, Play, Pause, AlertTriangle, Shield, Radio, User, Building, Clock, CheckCircle, XCircle, MessageSquare, ExternalLink, MessageSquareWarning, Loader2, } from 'lucide-react' import { FileInfoCard, FilePreviewModal, type FileInfo } from '@/components/ui/FilePreview' import { api } from '@/lib/api' import { USE_MOCK } from '@/contexts/AuthContext' import type { TaskResponse } from '@/types/task' // ==================== AI 审核结果类型 ==================== interface AIReviewResult { score: number violations: Array<{ type: string content: string severity: string suggestion: string timestamp?: number source?: string }> soft_warnings: Array<{ type: string content: string suggestion: string }> summary?: string } // ==================== 本地视图数据类型 ==================== interface VideoTaskView { id: string title: string creatorName: string agencyName: string projectName: string submittedAt: string duration: number aiScore: number status: string file: FileInfo isAppeal: boolean appealReason: string agencyReview: { reviewer: string result: string comment: string reviewedAt?: string } hardViolations: Array<{ id: string type: string content: string timestamp: number source: string riskLevel: string aiConfidence: number suggestion: string }> sentimentWarnings: Array<{ id: string type: string timestamp: number content: string riskLevel: string }> sellingPointsCovered: Array<{ point: string covered: boolean timestamp: number }> aiSummary?: string } // ==================== Mock 数据 ==================== const mockVideoTask: VideoTaskView = { id: 'video-001', title: '夏日护肤推广', creatorName: '小美护肤', agencyName: '星耀传媒', projectName: 'XX品牌618推广', submittedAt: '2026-02-06 15:00', duration: 135, aiScore: 85, status: 'brand_reviewing', file: { id: 'file-video-001', fileName: '夏日护肤_成片v2.mp4', fileSize: '128 MB', fileType: 'video/mp4', fileUrl: '/demo/videos/video-001.mp4', uploadedAt: '2026-02-06 15:00', duration: '02:15', thumbnail: '/demo/videos/video-001-thumb.jpg', }, isAppeal: false, appealReason: '', agencyReview: { reviewer: '张经理', result: 'approved', comment: '视频质量良好,发现的问题已确认为误报,建议通过。', reviewedAt: '2026-02-06 16:00', }, hardViolations: [ { id: 'v1', type: '违禁词', content: '效果最好', timestamp: 15.5, source: 'speech', riskLevel: 'high', aiConfidence: 0.95, suggestion: '建议替换为"效果显著"', }, { id: 'v2', type: '竞品露出', content: '疑似竞品Logo', timestamp: 42.0, source: 'visual', riskLevel: 'medium', aiConfidence: 0.72, suggestion: '经代理商确认为背景杂物,非竞品', }, ], sentimentWarnings: [ { id: 's1', type: '表情预警', timestamp: 68.0, content: '表情过于夸张,可能引发不适', riskLevel: 'low' }, ], sellingPointsCovered: [ { point: 'SPF50+ PA++++', covered: true, timestamp: 25.0 }, { point: '轻薄质地', covered: true, timestamp: 38.0 }, { point: '不油腻', covered: true, timestamp: 52.0 }, { point: '延展性好', covered: true, timestamp: 45.0 }, ], } // ==================== 工具函数 ==================== function formatTimestamp(seconds: number): string { const mins = Math.floor(seconds / 60) const secs = Math.floor(seconds % 60) return `${mins}:${secs.toString().padStart(2, '0')}` } function formatDurationString(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 severityToRiskLevel(severity: string): string { if (severity === 'high' || severity === 'critical') return 'high' if (severity === 'medium') return 'medium' return 'low' } /** 将后端 TaskResponse 映射为本地视图数据 */ function mapTaskToView(task: TaskResponse): VideoTaskView { const aiResult = task.video_ai_result as AIReviewResult | null | undefined const hardViolations = (aiResult?.violations || []).map((v, idx) => ({ id: `v${idx}`, type: v.type, content: v.content, timestamp: v.timestamp ?? 0, source: v.source ?? 'unknown', riskLevel: severityToRiskLevel(v.severity), aiConfidence: 0.9, suggestion: v.suggestion, })) const sentimentWarnings = (aiResult?.soft_warnings || []).map((w, idx) => ({ id: `s${idx}`, type: w.type, timestamp: 0, content: w.content, riskLevel: 'low', })) const duration = task.video_duration || 0 return { id: task.id, title: task.name, creatorName: task.creator.name, agencyName: task.agency.name, projectName: task.project.name, submittedAt: task.video_uploaded_at || task.created_at, duration, aiScore: task.video_ai_score || 0, status: task.stage, file: { id: task.id, fileName: task.video_file_name || '视频文件', fileSize: '', fileType: 'video/mp4', fileUrl: task.video_file_url || '', uploadedAt: task.video_uploaded_at || task.created_at, duration: formatDurationString(duration), thumbnail: task.video_thumbnail_url || undefined, }, isAppeal: task.is_appeal, appealReason: task.appeal_reason || '', agencyReview: { reviewer: task.agency.name, result: task.video_agency_status === 'passed' || task.video_agency_status === 'force_passed' ? 'approved' : (task.video_agency_status || 'pending'), comment: task.video_agency_comment || '', }, hardViolations, sentimentWarnings, // 卖点覆盖目前后端暂无,保留空数组 sellingPointsCovered: [], aiSummary: aiResult?.summary, } } // ==================== 子组件 ==================== function ReviewProgressBar({ taskStatus }: { taskStatus: string }) { const steps = getBrandReviewSteps(taskStatus) const currentStep = steps.find(s => s.status === 'current') return (
审核流程 当前:{currentStep?.label || '品牌方终审'}
) } function RiskLevelTag({ level }: { level: string }) { if (level === 'high') return 高风险 if (level === 'medium') return 中风险 return 低风险 } function LoadingSkeleton() { return (
{/* 顶部导航骨架 */}
{/* 流程进度骨架 */}
{/* 主体骨架 */}
) } // ==================== 主页面 ==================== export default function BrandVideoReviewPage() { const router = useRouter() const params = useParams() const toast = useToast() const taskId = params.id as string const [task, setTask] = useState(null) const [loading, setLoading] = useState(true) const [submitting, setSubmitting] = useState(false) const [isPlaying, setIsPlaying] = useState(false) const [showApproveModal, setShowApproveModal] = useState(false) const [showRejectModal, setShowRejectModal] = useState(false) const [rejectReason, setRejectReason] = useState('') const [checkedViolations, setCheckedViolations] = useState>({}) const [showFilePreview, setShowFilePreview] = useState(false) const [videoError, setVideoError] = useState(false) // 加载任务数据 const loadTask = useCallback(async () => { if (!taskId) return if (USE_MOCK) { // Mock 模式下使用静态数据 await new Promise((resolve) => setTimeout(resolve, 300)) setTask(mockVideoTask) setLoading(false) return } try { setLoading(true) const response = await api.getTask(taskId) setTask(mapTaskToView(response)) } catch (err) { const message = err instanceof Error ? err.message : '加载任务失败' toast.error(message) } finally { setLoading(false) } }, [taskId, toast]) useEffect(() => { loadTask() }, [loadTask]) // 通过审核 const handleApprove = async () => { if (submitting) return setSubmitting(true) try { if (!USE_MOCK) { await api.reviewVideo(taskId, { action: 'pass', comment: '' }) } else { await new Promise((resolve) => setTimeout(resolve, 300)) } setShowApproveModal(false) toast.success('审核通过!') router.push('/brand/review') } catch (err) { const message = err instanceof Error ? err.message : '操作失败' toast.error(message) } finally { setSubmitting(false) } } // 驳回审核 const handleReject = async () => { if (!rejectReason.trim()) { toast.error('请填写驳回原因') return } if (submitting) return setSubmitting(true) try { if (!USE_MOCK) { await api.reviewVideo(taskId, { action: 'reject', comment: rejectReason }) } else { await new Promise((resolve) => setTimeout(resolve, 300)) } setShowRejectModal(false) toast.success('已驳回') router.push('/brand/review') } catch (err) { const message = err instanceof Error ? err.message : '操作失败' toast.error(message) } finally { setSubmitting(false) } } // 加载中状态 if (loading || !task) { return } // 计算问题时间点用于进度条展示 const timelineMarkers = [ ...task.hardViolations.map(v => ({ time: v.timestamp, type: 'hard' as const })), ...task.sentimentWarnings.filter(w => w.timestamp > 0).map(w => ({ time: w.timestamp, type: 'soft' as const })), ...task.sellingPointsCovered.filter(s => s.covered).map(s => ({ time: s.timestamp, type: 'selling' as const })), ].sort((a, b) => a.time - b.time) return (
{/* 顶部导航 */}

{task.title}

{task.isAppeal && ( 申诉 )}
{task.creatorName} {task.agencyName} {task.submittedAt}
{/* 申诉理由 */} {task.isAppeal && task.appealReason && (

申诉理由

{task.appealReason}

)} {/* 审核流程进度条 */}
{/* 左侧:视频播放器 (3/5) */}
{/* 文件信息卡片 */} setShowFilePreview(true)} /> {/* 真实视频播放器 */}
{videoError ? (

视频加载失败

) : ( )}
{/* 智能进度条 */} {task.duration > 0 && (
智能进度条(点击跳转)
{/* 时间标记点 */} {timelineMarkers.map((marker, idx) => (
0:00 {formatTimestamp(task.duration)}
硬性问题 舆情提示 卖点覆盖
)}
{/* 代理商初审意见 */} 代理商初审意见
{task.agencyReview.result === 'approved' ? ( ) : ( )}
{task.agencyReview.reviewer} {task.agencyReview.result === 'approved' ? ( 建议通过 ) : ( 建议驳回 )}

{task.agencyReview.comment || '暂无评论'}

{task.agencyReview.reviewedAt && (

{task.agencyReview.reviewedAt}

)}
{/* AI 分析总结 */}
AI 分析总结 = 80 ? 'text-accent-green' : 'text-yellow-400'}`}> {task.aiScore}分

{task.aiSummary || `视频整体合规,发现${task.hardViolations.length}处硬性问题和${task.sentimentWarnings.length}处舆情提示,代理商已确认处理。`}

{/* 右侧:AI 检查单 (2/5) */}
{/* 硬性合规 */} 硬性合规 ({task.hardViolations.length}) {task.hardViolations.length === 0 && (

未发现硬性合规问题

)} {task.hardViolations.map((v) => (
setCheckedViolations((prev) => ({ ...prev, [v.id]: !prev[v.id] }))} className="mt-1 accent-accent-indigo" />
{v.type} {v.timestamp > 0 && ( {formatTimestamp(v.timestamp)} )}

{v.content}

{v.suggestion}

))}
{/* 舆情雷达 */} {task.sentimentWarnings.length > 0 && ( 舆情雷达(仅提示) {task.sentimentWarnings.map((w) => (
{w.type} {w.timestamp > 0 && ( {formatTimestamp(w.timestamp)} )}

{w.content}

软性风险仅作提示,不强制拦截

))}
)} {/* 卖点覆盖 */} {task.sellingPointsCovered.length > 0 && ( 卖点覆盖 {task.sellingPointsCovered.map((sp, idx) => (
{sp.covered ? ( ) : ( )} {sp.point}
{sp.covered && sp.timestamp > 0 && ( {formatTimestamp(sp.timestamp)} )}
))}
)}
{/* 底部决策栏 */}
已检查 {Object.values(checkedViolations).filter(Boolean).length}/{task.hardViolations.length} 个问题
{/* 通过确认弹窗 */} setShowApproveModal(false)} onConfirm={handleApprove} title="确认通过" message="确定要通过此视频的审核吗?通过后达人将收到通知。" confirmText={submitting ? '提交中...' : '确认通过'} /> {/* 驳回弹窗 */} setShowRejectModal(false)} title="驳回审核">

请填写驳回原因,已勾选的问题将自动打包发送给达人。

已选问题 ({Object.values(checkedViolations).filter(Boolean).length})

{task.hardViolations.filter(v => checkedViolations[v.id]).map(v => (
- {v.type}: {v.content}
))} {Object.values(checkedViolations).filter(Boolean).length === 0 && (
未选择任何问题
)}