'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 (
{task.appealReason}
视频加载失败
{task.agencyReview.comment || '暂无评论'}
{task.agencyReview.reviewedAt && ({task.agencyReview.reviewedAt}
)}{task.aiSummary || `视频整体合规,发现${task.hardViolations.length}处硬性问题和${task.sentimentWarnings.length}处舆情提示,代理商已确认处理。`}
未发现硬性合规问题
)} {task.hardViolations.map((v) => ({v.content}
{v.suggestion}
{w.content}
软性风险仅作提示,不强制拦截
请填写驳回原因,已勾选的问题将自动打包发送给达人。
已选问题 ({Object.values(checkedViolations).filter(Boolean).length})
{task.hardViolations.filter(v => checkedViolations[v.id]).map(v => (