'use client' import { useState, useEffect, useCallback } from 'react' import { useRouter, useParams } from 'next/navigation' import { ArrowLeft, Download, Play, Loader2 } from 'lucide-react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card' import { Button } from '@/components/ui/Button' import { SuccessTag, WarningTag, ErrorTag, PendingTag } from '@/components/ui/Tag' import { api } from '@/lib/api' import { USE_MOCK } from '@/contexts/AuthContext' import { getPlatformInfo } from '@/lib/platforms' import type { TaskResponse, TaskStage } from '@/types/task' function getPlatformLabel(platformId: string): string { return getPlatformInfo(platformId)?.name || platformId } // ==================== 本地视图模型 ==================== interface TaskViewModel { id: string videoTitle: string creatorName: string brandName: string platform: string status: string aiScore: number | null finalScore: number | null aiSummary: string submittedAt: string reviewedAt: string reviewerName: string reviewNotes: string videoUrl: string | null softWarnings: Array<{ id: string; content: string; suggestion: string }> timeline: Array<{ time: string; event: string; actor: string }> } // ==================== Mock 数据 ==================== const mockTaskDetail: TaskViewModel = { id: 'task-004', videoTitle: '美食探店vlog', creatorName: '吃货小胖', brandName: '某餐饮品牌', platform: '小红书', status: 'approved', aiScore: 95, finalScore: 95, aiSummary: '视频内容合规,无明显违规项', submittedAt: '2024-02-04 10:00', reviewedAt: '2024-02-04 12:00', reviewerName: '审核员A', reviewNotes: '内容积极正面,品牌露出合适,通过审核。', videoUrl: null, softWarnings: [ { id: 'w1', content: '品牌提及次数适中', suggestion: '可考虑适当增加品牌提及' }, ], timeline: [ { time: '2024-02-04 10:00', event: '达人提交视频', actor: '吃货小胖' }, { time: '2024-02-04 10:02', event: 'AI审核开始', actor: '系统' }, { time: '2024-02-04 10:05', event: 'AI审核完成,得分95分', actor: '系统' }, { time: '2024-02-04 12:00', event: '人工审核通过', actor: '审核员A' }, ], } // ==================== 辅助函数 ==================== function mapStageToStatus(stage: TaskStage, task: TaskResponse): string { if (stage === 'completed') return 'approved' if (stage === 'rejected') return 'rejected' // 检查视频审核状态 if (task.video_agency_status === 'passed' || task.video_brand_status === 'passed') return 'approved' if (task.video_agency_status === 'rejected' || task.video_brand_status === 'rejected') return 'rejected' // 检查脚本审核状态 if (task.script_agency_status === 'passed' || task.script_brand_status === 'passed') { // 脚本通过但视频还在流程中 if (stage.startsWith('video_')) return 'pending_review' return 'approved' } if (task.script_agency_status === 'rejected' || task.script_brand_status === 'rejected') return 'rejected' return 'pending_review' } function formatDateTime(isoStr: string | null | undefined): string { if (!isoStr) return '-' try { const d = new Date(isoStr) return d.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }) } catch { return isoStr } } function buildTimeline(task: TaskResponse): Array<{ time: string; event: string; actor: string }> { const timeline: Array<{ time: string; event: string; actor: string }> = [] // 任务创建 timeline.push({ time: formatDateTime(task.created_at), event: '任务创建', actor: '系统', }) // 脚本上传 if (task.script_uploaded_at) { timeline.push({ time: formatDateTime(task.script_uploaded_at), event: '达人提交脚本', actor: task.creator?.name || '达人', }) } // 脚本 AI 审核 if (task.script_ai_score != null) { timeline.push({ time: formatDateTime(task.script_uploaded_at), event: `AI 脚本审核完成,得分 ${task.script_ai_score} 分`, actor: '系统', }) } // 脚本代理商审核 if (task.script_agency_status && task.script_agency_status !== 'pending') { const statusText = task.script_agency_status === 'passed' ? '通过' : task.script_agency_status === 'rejected' ? '驳回' : '强制通过' timeline.push({ time: formatDateTime(task.updated_at), event: `代理商脚本审核${statusText}`, actor: task.agency?.name || '代理商', }) } // 脚本品牌方审核 if (task.script_brand_status && task.script_brand_status !== 'pending') { const statusText = task.script_brand_status === 'passed' ? '通过' : task.script_brand_status === 'rejected' ? '驳回' : '强制通过' timeline.push({ time: formatDateTime(task.updated_at), event: `品牌方脚本审核${statusText}`, actor: '品牌方', }) } // 视频上传 if (task.video_uploaded_at) { timeline.push({ time: formatDateTime(task.video_uploaded_at), event: '达人提交视频', actor: task.creator?.name || '达人', }) } // 视频 AI 审核 if (task.video_ai_score != null) { timeline.push({ time: formatDateTime(task.video_uploaded_at), event: `AI 视频审核完成,得分 ${task.video_ai_score} 分`, actor: '系统', }) } // 视频代理商审核 if (task.video_agency_status && task.video_agency_status !== 'pending') { const statusText = task.video_agency_status === 'passed' ? '通过' : task.video_agency_status === 'rejected' ? '驳回' : '强制通过' timeline.push({ time: formatDateTime(task.updated_at), event: `代理商视频审核${statusText}`, actor: task.agency?.name || '代理商', }) } // 视频品牌方审核 if (task.video_brand_status && task.video_brand_status !== 'pending') { const statusText = task.video_brand_status === 'passed' ? '通过' : task.video_brand_status === 'rejected' ? '驳回' : '强制通过' timeline.push({ time: formatDateTime(task.updated_at), event: `品牌方视频审核${statusText}`, actor: '品牌方', }) } // 申诉 if (task.is_appeal && task.appeal_reason) { timeline.push({ time: formatDateTime(task.updated_at), event: `达人发起申诉:${task.appeal_reason}`, actor: task.creator?.name || '达人', }) } return timeline } function mapTaskResponseToViewModel(task: TaskResponse): TaskViewModel { const status = mapStageToStatus(task.stage, task) // 选择最新的 AI 评分(优先视频,其次脚本) const aiScore = task.video_ai_score ?? task.script_ai_score ?? null const aiResult = task.video_ai_result ?? task.script_ai_result ?? null // 最终评分等于 AI 评分(人工审核不改分) const finalScore = aiScore // AI 摘要 const aiSummary = aiResult?.summary || '暂无 AI 分析摘要' // 审核备注(优先视频代理商审核意见) const reviewNotes = task.video_agency_comment || task.script_agency_comment || task.video_brand_comment || task.script_brand_comment || '' // 软警告 const softWarnings = (aiResult?.soft_warnings || []).map((w, i) => ({ id: `w-${i}`, content: w.content, suggestion: w.suggestion, })) // 时间线 const timeline = buildTimeline(task) return { id: task.id, videoTitle: task.name, creatorName: task.creator?.name || '未知达人', brandName: task.project?.brand_name || '未知品牌', platform: task.project?.platform ? getPlatformLabel(task.project.platform) : '未知平台', status, aiScore, finalScore, aiSummary, submittedAt: formatDateTime(task.video_uploaded_at || task.script_uploaded_at || task.created_at), reviewedAt: formatDateTime(task.updated_at), reviewerName: task.agency?.name || '-', reviewNotes, videoUrl: task.video_file_url || null, softWarnings, timeline, } } // ==================== 组件 ==================== function StatusBadge({ status }: { status: string }) { if (status === 'approved') return 已通过 if (status === 'rejected') return 已驳回 if (status === 'pending_review') return 待审核 return 处理中 } function TaskDetailSkeleton() { return (
) } export default function TaskDetailPage() { const router = useRouter() const params = useParams() const taskId = params.id as string const [task, setTask] = useState(mockTaskDetail) const [loading, setLoading] = useState(true) const loadData = useCallback(async () => { if (USE_MOCK) { setTask(mockTaskDetail) setLoading(false) return } try { const taskData = await api.getTask(taskId) setTask(mapTaskResponseToViewModel(taskData)) } catch (err) { console.error('加载任务详情失败:', err) // 加载失败时保持 mock 数据作为 fallback } finally { setLoading(false) } }, [taskId]) useEffect(() => { loadData() }, [loadData]) if (loading) { return } return (
{/* 顶部导航 */}

{task.videoTitle}

{task.creatorName} · {task.brandName} · {task.platform}

{/* 左侧:视频和基本信息 */}
{task.videoUrl ? (
审核结果
AI 评分
= 80 ? 'text-green-600' : 'text-yellow-600'}`}> {task.aiScore ?? '-'}
最终评分
= 80 ? 'text-green-600' : 'text-yellow-600'}`}> {task.finalScore ?? '-'}
AI 分析摘要

{task.aiSummary}

{task.reviewNotes && (
审核备注

{task.reviewNotes}

)}
{task.softWarnings.length > 0 && ( 优化建议 {task.softWarnings.map((w) => (

{w.content}

{w.suggestion}

))}
)}
{/* 右侧:详细信息和时间线 */}
任务信息
任务ID {task.id}
达人 {task.creatorName}
品牌 {task.brandName}
平台 {task.platform}
提交时间 {task.submittedAt}
审核时间 {task.reviewedAt}
审核员 {task.reviewerName}
处理时间线
{task.timeline.map((item, index) => (
{index < task.timeline.length - 1 &&
}

{item.event}

{item.time} · {item.actor}

))}
) }