Your Name a5a005db0c feat: 完善审核台文件预览与消息通知系统
主要更新:
- 新增 FilePreview 通用组件,支持视频/图片/PDF 内嵌预览
- 审核详情页添加文件信息卡片、预览/下载功能
- 审核列表和详情页添加申诉标识和申诉理由显示
- 完善三端消息通知系统(达人/代理商/品牌)
- 新增达人 Brief 查看页面
- 新增品牌方消息中心页面
- 创建后端开发备忘文档

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-09 12:20:47 +08:00

1068 lines
41 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
import {
Upload, Check, X, Folder, Bell, MessageCircle,
XCircle, CheckCircle, Loader2, Scan, ArrowLeft,
Bot, Users, Building2, Clock, FileText, Video,
ChevronRight, AlertTriangle, Download, Eye, Target, Ban,
ChevronDown, ChevronUp, File
} from 'lucide-react'
import { Modal } from '@/components/ui/Modal'
import { Button } from '@/components/ui/Button'
import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout'
import { cn } from '@/lib/utils'
// 详细的任务状态类型
type TaskPhase = 'script' | 'video'
type TaskStage =
| 'upload' // 待上传
| 'ai_reviewing' // AI审核中
| 'ai_result' // AI审核结果有问题需修改
| 'agency_reviewing' // 代理商审核中
| 'agency_rejected' // 代理商驳回
| 'brand_reviewing' // 品牌终审中
| 'brand_approved' // 品牌通过
| 'brand_rejected' // 品牌驳回
type Issue = {
title: string
description: string
timestamp?: string
severity?: 'error' | 'warning'
}
type ReviewLog = {
time: string
message: string
status: 'done' | 'loading' | 'pending'
}
type TaskData = {
id: string
title: string
subtitle: string
phase: TaskPhase
stage: TaskStage
progress?: number
issues?: Issue[]
reviewLogs?: ReviewLog[]
rejectionReason?: string
submittedAt?: string
scriptContent?: string
}
// 代理商Brief文档达人可查看
type AgencyBriefFile = {
id: string
name: string
size: string
uploadedAt: string
description?: string
}
const mockAgencyBrief = {
files: [
{ id: 'af1', name: '达人拍摄指南.pdf', size: '1.5MB', uploadedAt: '2026-02-02', description: '详细的拍摄流程和注意事项' },
{ id: 'af2', name: '产品卖点话术.docx', size: '800KB', uploadedAt: '2026-02-02', description: '推荐使用的话术和表达方式' },
{ id: 'af3', name: '品牌视觉参考.pdf', size: '3.2MB', uploadedAt: '2026-02-02', description: '视觉风格和拍摄参考示例' },
] as AgencyBriefFile[],
sellingPoints: [
{ id: 'sp1', content: 'SPF50+ PA++++', required: true },
{ id: 'sp2', content: '轻薄质地,不油腻', required: true },
{ id: 'sp3', content: '延展性好,易推开', required: false },
{ id: 'sp4', content: '适合敏感肌', required: false },
{ id: 'sp5', content: '夏日必备防晒', required: true },
],
blacklistWords: [
{ id: 'bw1', word: '最好', reason: '绝对化用语' },
{ id: 'bw2', word: '第一', reason: '绝对化用语' },
{ id: 'bw3', word: '神器', reason: '夸大宣传' },
{ id: 'bw4', word: '完美', reason: '绝对化用语' },
],
}
// 所有15个任务的详细数据
const allTasksData: Record<string, TaskData> = {
'task-001': {
id: 'task-001',
title: 'XX品牌618推广',
subtitle: '产品种草视频 · 时长要求 60-90秒 · 当前步骤:上传脚本',
phase: 'script',
stage: 'upload',
},
'task-002': {
id: 'task-002',
title: 'YY美妆新品',
subtitle: '口播测评 · 时长要求 60-90秒 · 当前步骤AI审核中',
phase: 'script',
stage: 'ai_reviewing',
progress: 62,
reviewLogs: [
{ time: '14:32:01', message: '脚本上传完成', status: 'done' },
{ time: '14:32:15', message: '任务规则已加载', status: 'done' },
{ time: '14:32:28', message: '开始内容合规性检测', status: 'done' },
{ time: '14:33:45', message: '正在分析品牌调性匹配度...', status: 'loading' },
],
},
'task-003': {
id: 'task-003',
title: 'ZZ饮品夏日',
subtitle: '探店Vlog · 发现2处问题 · 需修改后重新提交',
phase: 'script',
stage: 'ai_result',
issues: [
{
title: '检测到竞品提及',
description: '脚本第3段提及了竞品「百事可乐」可能造成品牌冲突风险。',
severity: 'error',
},
{
title: '禁用词语出现',
description: '脚本中出现「最好喝」「第一」等绝对化用语,可能违反广告法。',
severity: 'error',
},
],
},
'task-004': {
id: 'task-004',
title: 'AA数码新品发布',
subtitle: '开箱测评 · 时长要求 60-90秒 · 当前步骤:审核通过',
phase: 'video',
stage: 'brand_approved',
submittedAt: '2026-02-01 10:30',
},
'task-005': {
id: 'task-005',
title: 'BB运动饮料',
subtitle: '运动场景 · 时长要求 60-90秒 · 当前步骤AI审核中',
phase: 'script',
stage: 'ai_reviewing',
progress: 35,
reviewLogs: [
{ time: '15:10:22', message: '脚本上传完成', status: 'done' },
{ time: '15:10:35', message: '任务规则已加载', status: 'done' },
{ time: '15:10:48', message: '正在进行内容分析...', status: 'loading' },
],
},
'task-006': {
id: 'task-006',
title: 'CC服装春季款',
subtitle: '穿搭展示 · 时长要求 60-90秒 · 当前步骤:等待代理商审核',
phase: 'script',
stage: 'agency_reviewing',
submittedAt: '2026-02-03 09:15',
scriptContent: '春季新品穿搭指南,展示三套不同风格的搭配...',
},
'task-007': {
id: 'task-007',
title: 'DD家电测评',
subtitle: '开箱视频 · 时长要求 60-90秒 · 当前步骤:等待品牌方终审',
phase: 'script',
stage: 'brand_reviewing',
submittedAt: '2026-02-02 14:20',
scriptContent: '智能家电开箱测评,详细介绍产品功能特点...',
},
'task-008': {
id: 'task-008',
title: 'EE食品试吃',
subtitle: '美食测评 · 脚本已通过 · 当前步骤:下一步上传视频',
phase: 'video',
stage: 'upload',
},
'task-009': {
id: 'task-009',
title: 'FF护肤品',
subtitle: '使用教程 · 时长要求 60-90秒 · 当前步骤视频AI审核中',
phase: 'video',
stage: 'ai_reviewing',
progress: 78,
reviewLogs: [
{ time: '16:20:11', message: '视频上传完成', status: 'done' },
{ time: '16:20:25', message: '任务规则已加载', status: 'done' },
{ time: '16:20:38', message: '开始 ASR 语音识别', status: 'done' },
{ time: '16:21:52', message: '视觉合规性检测完成', status: 'done' },
{ time: '16:22:30', message: '正在生成审核报告...', status: 'loading' },
],
},
'task-010': {
id: 'task-010',
title: 'GG智能手表',
subtitle: '功能展示 · 时长要求 60-90秒 · 当前步骤:代理商驳回',
phase: 'script',
stage: 'agency_rejected',
rejectionReason: '脚本内容与品牌调性不符,产品卖点描述不够突出,建议重新调整创意方向。',
issues: [
{
title: '品牌调性不符',
description: '脚本整体风格偏向娱乐化,与品牌科技专业形象不匹配。',
severity: 'error',
},
{
title: '卖点不突出',
description: '产品核心功能(健康监测、长续航)未在脚本中重点体现。',
severity: 'warning',
},
],
},
'task-011': {
id: 'task-011',
title: 'HH美妆代言',
subtitle: '广告拍摄 · 时长要求 60-90秒 · 当前步骤:品牌驳回',
phase: 'script',
stage: 'brand_rejected',
rejectionReason: '品牌方认为脚本创意不够新颖,希望加入更多互动元素。',
issues: [
{
title: '创意不够新颖',
description: '脚本采用的是常见的口播形式,缺乏创新点和记忆点。',
severity: 'error',
},
{
title: '缺少互动元素',
description: '建议加入用户互动环节,如问答、挑战等,增加观众参与感。',
severity: 'warning',
},
],
},
'task-012': {
id: 'task-012',
title: 'II数码配件',
subtitle: '配件展示 · 时长要求 60-90秒 · 当前步骤:等待代理商审核',
phase: 'video',
stage: 'agency_reviewing',
submittedAt: '2026-02-04 11:30',
},
'task-013': {
id: 'task-013',
title: 'JJ旅行vlog',
subtitle: '旅行记录 · 时长要求 60-90秒 · 当前步骤:代理商驳回',
phase: 'video',
stage: 'agency_rejected',
rejectionReason: '视频背景音乐存在版权问题,需要更换为无版权音乐后重新提交。',
issues: [
{
title: '背景音乐版权问题',
description: '视频中使用的背景音乐「XXX」存在版权风险平台可能会限流或下架。',
severity: 'error',
},
],
},
'task-014': {
id: 'task-014',
title: 'KK宠物用品',
subtitle: '萌宠日常 · 时长要求 60-90秒 · 当前步骤:等待品牌方终审',
phase: 'video',
stage: 'brand_reviewing',
submittedAt: '2026-02-05 09:45',
},
'task-015': {
id: 'task-015',
title: 'LL厨房电器',
subtitle: '使用演示 · 时长要求 60-90秒 · 当前步骤:品牌驳回',
phase: 'video',
stage: 'brand_rejected',
rejectionReason: '产品使用场景不够真实,希望展示更多日常使用细节。',
issues: [
{
title: '使用场景不真实',
description: '视频中的厨房场景过于整洁,缺乏真实感,建议在真实家庭环境中拍摄。',
severity: 'error',
},
{
title: '产品功能展示不完整',
description: '仅展示了基础功能,建议补充展示智能预约、自清洁等特色功能。',
severity: 'warning',
},
],
},
}
// 进度条步骤图标组件
function StepIcon({
status,
icon
}: {
status: 'done' | 'current' | 'error' | 'pending'
icon: 'upload' | 'bot' | 'users' | 'building'
}) {
const IconMap = {
upload: Upload,
bot: Bot,
users: Users,
building: Building2,
}
const Icon = IconMap[icon]
const getStyle = () => {
switch (status) {
case 'done': return 'bg-accent-green'
case 'current': return 'bg-accent-indigo'
case 'error': return 'bg-accent-coral'
default: return 'bg-bg-elevated border-[1.5px] border-border-subtle'
}
}
const iconColor = status === 'pending' ? 'text-text-tertiary' : 'text-white'
return (
<div className={cn('w-8 h-8 rounded-full flex items-center justify-center', getStyle())}>
{status === 'done' && <Check size={16} className={iconColor} />}
{status === 'current' && <Loader2 size={16} className={cn(iconColor, 'animate-spin')} />}
{status === 'error' && <X size={16} className={iconColor} />}
{status === 'pending' && <Icon size={16} className={iconColor} />}
</div>
)
}
// 审核流程进度条
function ReviewProgressBar({ task }: { task: TaskData }) {
const { phase, stage } = task
const getStepStatus = (stepIndex: number): 'done' | 'current' | 'error' | 'pending' => {
// 步骤顺序: 0-提交, 1-AI审核, 2-代理商, 3-品牌
const stageMap: Record<TaskStage, number> = {
'upload': 0,
'ai_reviewing': 1,
'ai_result': 1,
'agency_reviewing': 2,
'agency_rejected': 2,
'brand_reviewing': 3,
'brand_approved': 4,
'brand_rejected': 3,
}
const currentStepIndex = stageMap[stage]
const isError = stage === 'ai_result' || stage === 'agency_rejected' || stage === 'brand_rejected'
if (stepIndex < currentStepIndex) return 'done'
if (stepIndex === currentStepIndex) {
if (isError) return 'error'
if (stage === 'brand_approved') return 'done'
return 'current'
}
return 'pending'
}
const steps = [
{ label: '已提交', icon: 'upload' as const },
{ label: 'AI审核', icon: 'bot' as const },
{ label: '代理商', icon: 'users' as const },
{ label: '品牌方', icon: 'building' as const },
]
return (
<div className="bg-bg-card rounded-2xl p-6 card-shadow">
<h3 className="text-lg font-semibold text-text-primary mb-5">
{phase === 'script' ? '脚本审核流程' : '视频审核流程'}
</h3>
<div className="flex items-center">
{steps.map((step, index) => {
const status = getStepStatus(index)
return (
<div key={step.label} className="flex items-center flex-1">
<div className="flex flex-col items-center gap-2 w-20">
<StepIcon status={status} icon={step.icon} />
<span className={cn(
'text-xs',
status === 'done' ? 'text-text-secondary' :
status === 'error' ? 'text-accent-coral font-semibold' :
status === 'current' ? 'text-accent-indigo font-semibold' :
'text-text-tertiary'
)}>
{step.label}
</span>
</div>
{index < steps.length - 1 && (
<div className={cn(
'h-0.5 flex-1',
getStepStatus(index) === 'done' ? 'bg-accent-green' : 'bg-border-subtle'
)} />
)}
</div>
)
})}
</div>
</div>
)
}
// Brief文档查看组件
function AgencyBriefSection() {
const [isExpanded, setIsExpanded] = useState(true)
const [previewFile, setPreviewFile] = useState<AgencyBriefFile | null>(null)
const handleDownload = (file: AgencyBriefFile) => {
alert(`下载文件: ${file.name}`)
}
const requiredPoints = mockAgencyBrief.sellingPoints.filter(sp => sp.required)
const optionalPoints = mockAgencyBrief.sellingPoints.filter(sp => !sp.required)
return (
<>
<div className="bg-bg-card rounded-2xl card-shadow border border-accent-indigo/30">
<div className="flex items-center justify-between p-4 border-b border-border-subtle">
<div className="flex items-center gap-2">
<File className="w-5 h-5 text-accent-indigo" />
<span className="text-base font-semibold text-text-primary">Brief </span>
</div>
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className="p-1.5 hover:bg-bg-elevated rounded-lg transition-colors"
>
{isExpanded ? (
<ChevronUp className="w-5 h-5 text-text-tertiary" />
) : (
<ChevronDown className="w-5 h-5 text-text-tertiary" />
)}
</button>
</div>
{isExpanded && (
<div className="p-4 space-y-4">
{/* Brief文档列表 */}
<div>
<h4 className="text-sm font-medium text-text-primary mb-2 flex items-center gap-2">
<FileText className="w-4 h-4 text-accent-indigo" />
</h4>
<div className="space-y-2">
{mockAgencyBrief.files.map((file) => (
<div key={file.id} className="flex items-center justify-between p-3 bg-bg-elevated rounded-xl">
<div className="flex items-center gap-3 min-w-0">
<div className="w-9 h-9 rounded-lg bg-accent-indigo/15 flex items-center justify-center flex-shrink-0">
<FileText className="w-4 h-4 text-accent-indigo" />
</div>
<div className="min-w-0">
<p className="text-sm font-medium text-text-primary truncate">{file.name}</p>
<p className="text-xs text-text-tertiary">{file.size}</p>
</div>
</div>
<div className="flex items-center gap-1 flex-shrink-0">
<button
type="button"
onClick={() => setPreviewFile(file)}
className="p-2 hover:bg-bg-page rounded-lg transition-colors"
>
<Eye className="w-4 h-4 text-text-secondary" />
</button>
<button
type="button"
onClick={() => handleDownload(file)}
className="p-2 hover:bg-bg-page rounded-lg transition-colors"
>
<Download className="w-4 h-4 text-text-secondary" />
</button>
</div>
</div>
))}
</div>
</div>
{/* 卖点要求 */}
<div>
<h4 className="text-sm font-medium text-text-primary mb-2 flex items-center gap-2">
<Target className="w-4 h-4 text-accent-green" />
</h4>
<div className="space-y-2">
{requiredPoints.length > 0 && (
<div className="p-3 bg-accent-coral/10 rounded-xl border border-accent-coral/30">
<p className="text-xs text-accent-coral font-medium mb-2"></p>
<div className="flex flex-wrap gap-2">
{requiredPoints.map((sp) => (
<span key={sp.id} className="px-2 py-1 text-xs bg-accent-coral/20 text-accent-coral rounded-lg">
{sp.content}
</span>
))}
</div>
</div>
)}
{optionalPoints.length > 0 && (
<div className="p-3 bg-bg-elevated rounded-xl">
<p className="text-xs text-text-tertiary font-medium mb-2"></p>
<div className="flex flex-wrap gap-2">
{optionalPoints.map((sp) => (
<span key={sp.id} className="px-2 py-1 text-xs bg-bg-page text-text-secondary rounded-lg">
{sp.content}
</span>
))}
</div>
</div>
)}
</div>
</div>
{/* 违禁词 */}
<div>
<h4 className="text-sm font-medium text-text-primary mb-2 flex items-center gap-2">
<Ban className="w-4 h-4 text-accent-coral" />
使
</h4>
<div className="flex flex-wrap gap-2">
{mockAgencyBrief.blacklistWords.map((bw) => (
<span key={bw.id} className="px-2 py-1 text-xs bg-accent-coral/15 text-accent-coral rounded-lg border border-accent-coral/30">
{bw.word}
</span>
))}
</div>
</div>
</div>
)}
</div>
{/* 文件预览弹窗 */}
<Modal
isOpen={!!previewFile}
onClose={() => setPreviewFile(null)}
title={previewFile?.name || '文件预览'}
size="lg"
>
<div className="space-y-4">
<div className="aspect-[4/3] bg-bg-elevated rounded-lg flex items-center justify-center">
<div className="text-center">
<FileText className="w-12 h-12 mx-auto text-accent-indigo mb-4" />
<p className="text-text-secondary"></p>
<p className="text-xs text-text-tertiary mt-1"></p>
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="secondary" onClick={() => setPreviewFile(null)}>
</Button>
{previewFile && (
<Button onClick={() => handleDownload(previewFile)}>
<Download className="w-4 h-4" />
</Button>
)}
</div>
</div>
</Modal>
</>
)
}
// 上传界面
function UploadView({ task }: { task: TaskData }) {
const [isDragging, setIsDragging] = useState(false)
const isScript = task.phase === 'script'
return (
<div className="flex flex-col gap-6 h-full">
{/* Brief文档区域 - 仅脚本阶段显示 */}
{isScript && <AgencyBriefSection />}
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-text-primary">
{isScript ? '上传脚本' : '上传视频'}
</h3>
<p className="text-sm text-text-tertiary">
{isScript ? '支持粘贴文本或上传文档' : '支持 MP4/MOV 格式,≤ 100MB'}
</p>
</div>
<span className="px-2.5 py-1 rounded-full text-xs font-semibold bg-accent-indigo/15 text-accent-indigo">
</span>
</div>
<div
className={cn(
'flex-1 flex flex-col items-center justify-center gap-5 rounded-2xl border-2 border-dashed transition-colors card-shadow bg-bg-card min-h-[400px]',
isDragging ? 'border-accent-indigo bg-accent-indigo/5' : 'border-border-subtle'
)}
onDragOver={(e) => { e.preventDefault(); setIsDragging(true) }}
onDragLeave={() => setIsDragging(false)}
onDrop={(e) => { e.preventDefault(); setIsDragging(false) }}
>
<div className="w-20 h-20 rounded-full bg-accent-indigo/15 flex items-center justify-center">
<Upload className="w-10 h-10 text-accent-indigo" />
</div>
<div className="flex flex-col items-center gap-2 text-center">
<p className="text-lg font-semibold text-text-primary"></p>
<p className="text-sm text-text-tertiary">
{isScript ? '支持 .doc、.docx、.txt 格式' : '支持 MP4/MOV 格式,≤ 100MB'}
</p>
</div>
<button
type="button"
className="flex items-center gap-2 px-8 py-3.5 rounded-xl bg-gradient-to-r from-accent-indigo to-[#4F46E5] text-white font-semibold"
>
<Upload className="w-5 h-5" />
{isScript ? '选择脚本文档' : '选择视频文件'}
</button>
<p className="text-xs text-text-tertiary">
{isScript ? '也可以直接粘贴脚本文本后提交' : '上传完成后将自动进入 AI 审核'}
</p>
</div>
</div>
)
}
// AI审核中界面
function AIReviewingView({ task }: { task: TaskData }) {
return (
<div className="flex-1 flex items-center justify-center">
<div className="bg-bg-card rounded-2xl p-10 card-shadow flex flex-col items-center gap-8 w-full max-w-md">
<div className="flex items-center gap-2 px-4 py-2 bg-bg-elevated rounded-lg">
<Folder className="w-3.5 h-3.5 text-text-tertiary" />
<span className="text-xs font-medium text-text-tertiary">
{task.phase === 'script' ? '脚本内容审核' : '视频内容审核'} ·
</span>
</div>
<div className="relative w-[180px] h-[180px] flex items-center justify-center">
<div className="absolute inset-0 rounded-full bg-gradient-radial from-accent-indigo/50 via-accent-indigo/20 to-transparent animate-pulse" />
<div className="w-[72px] h-[72px] rounded-full bg-gradient-to-br from-accent-indigo to-[#4F46E5] flex items-center justify-center shadow-[0_0_24px_rgba(99,102,241,0.5)]">
<Scan className="w-8 h-8 text-white animate-pulse" />
</div>
</div>
<div className="flex flex-col items-center gap-2 w-full">
<h2 className="text-[22px] font-semibold text-text-primary">
AI {task.phase === 'script' ? '脚本' : '视频'}
</h2>
<p className="text-sm text-text-secondary"> 2-3 </p>
<div className="flex items-center gap-3 w-full pt-3">
<div className="flex-1 h-2 bg-bg-elevated rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-accent-indigo to-[#4F46E5] rounded-full transition-all duration-300"
style={{ width: `${task.progress || 0}%` }}
/>
</div>
<span className="text-sm font-semibold text-accent-indigo">{task.progress || 0}%</span>
</div>
</div>
{task.reviewLogs && task.reviewLogs.length > 0 && (
<div className="w-full bg-bg-elevated rounded-xl p-5 flex flex-col gap-2.5">
<div className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-accent-green" />
<span className="text-xs font-medium text-text-secondary"></span>
</div>
<div className="flex flex-col gap-2">
{task.reviewLogs.map((log, index) => (
<div key={index} className="flex items-center gap-2 text-xs">
<span className="text-text-tertiary font-mono">{log.time}</span>
<span className={cn(
log.status === 'done' ? 'text-text-secondary' :
log.status === 'loading' ? 'text-accent-indigo' :
'text-text-tertiary'
)}>
{log.message}
</span>
{log.status === 'loading' && <Loader2 className="w-3 h-3 text-accent-indigo animate-spin" />}
</div>
))}
</div>
</div>
)}
<button
type="button"
className="flex items-center gap-2 px-6 py-3 rounded-xl bg-bg-page border border-border-subtle text-text-secondary text-[13px] font-medium"
>
<Bell className="w-4 h-4" />
</button>
</div>
</div>
)
}
// 审核结果/驳回界面AI结果、代理商驳回、品牌驳回
function RejectionView({ task, onAppeal }: { task: TaskData; onAppeal: () => void }) {
const getTitle = () => {
switch (task.stage) {
case 'ai_result': return 'AI 审核结果'
case 'agency_rejected': return '代理商审核结果'
case 'brand_rejected': return '品牌方审核结果'
default: return '审核结果'
}
}
const getStatusText = () => {
switch (task.stage) {
case 'ai_result': return 'AI 检测到问题'
case 'agency_rejected': return '代理商审核驳回'
case 'brand_rejected': return '品牌方审核驳回'
default: return '需要修改'
}
}
return (
<div className="flex flex-col gap-6 h-full">
{/* 进度条 */}
<ReviewProgressBar task={task} />
{/* 结果卡片 */}
<div className="bg-bg-card rounded-2xl p-6 card-shadow flex-1 flex flex-col">
{/* 状态标题 */}
<div className="flex items-center gap-3 pb-5 border-b border-border-subtle">
<div className="w-12 h-12 rounded-xl bg-accent-coral/15 flex items-center justify-center">
<XCircle className="w-6 h-6 text-accent-coral" />
</div>
<div className="flex flex-col gap-0.5">
<span className="text-lg font-semibold text-text-primary">{getTitle()}</span>
<span className="text-sm text-accent-coral font-medium">{getStatusText()}</span>
</div>
</div>
{/* 驳回原因 */}
{task.rejectionReason && (
<div className="py-4 border-b border-border-subtle">
<p className="text-sm text-text-secondary leading-relaxed">{task.rejectionReason}</p>
</div>
)}
{/* 问题列表 */}
{task.issues && task.issues.length > 0 && (
<div className="py-4 flex flex-col gap-4 flex-1">
<span className="text-sm font-semibold text-text-primary">
{task.issues.length}
</span>
<div className="flex flex-col gap-3">
{task.issues.map((issue, index) => (
<div key={index} className="bg-bg-elevated rounded-xl p-4 flex flex-col gap-2">
<div className="flex items-center gap-2">
<span className={cn(
'px-2 py-0.5 rounded text-xs font-semibold',
issue.severity === 'error'
? 'bg-accent-coral/15 text-accent-coral'
: 'bg-amber-500/15 text-amber-500'
)}>
{issue.severity === 'error' ? '违规' : '建议'}
</span>
<span className="text-sm font-semibold text-text-primary">{issue.title}</span>
</div>
<p className="text-[13px] text-text-secondary leading-relaxed">{issue.description}</p>
</div>
))}
</div>
</div>
)}
{/* 操作按钮 */}
<div className="flex items-center justify-between pt-4 border-t border-border-subtle">
<button
type="button"
onClick={onAppeal}
className="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-bg-elevated border border-border-subtle text-text-secondary text-sm font-medium hover:bg-bg-page transition-colors"
>
<MessageCircle className="w-[18px] h-[18px]" />
</button>
<button
type="button"
className="flex items-center gap-2 px-6 py-2.5 rounded-xl bg-accent-green text-white text-sm font-semibold hover:bg-accent-green/90 transition-colors"
>
<Upload className="w-[18px] h-[18px]" />
</button>
</div>
</div>
</div>
)
}
// 等待审核界面(代理商审核中、品牌终审中)
function WaitingReviewView({ task }: { task: TaskData }) {
const isAgency = task.stage === 'agency_reviewing'
const title = isAgency ? '等待代理商审核' : '等待品牌方终审'
const description = isAgency
? '您的内容已进入代理商审核环节,请耐心等待'
: '您的内容已进入品牌方终审环节,这是最后一步'
return (
<div className="flex flex-col gap-6 h-full">
{/* 进度条 */}
<ReviewProgressBar task={task} />
{/* 提交信息卡片 */}
<div className="bg-bg-card rounded-2xl p-6 card-shadow">
<div className="flex items-center gap-3 mb-4">
<FileText className="w-5 h-5 text-text-secondary" />
<span className="text-base font-semibold text-text-primary">
{task.phase === 'script' ? '脚本提交信息' : '视频提交信息'}
</span>
</div>
<div className="bg-bg-elevated rounded-xl p-4 flex flex-col gap-3">
<div className="flex items-center justify-between">
<span className="text-sm text-text-tertiary"></span>
<span className="text-sm text-text-primary">{task.submittedAt || '刚刚'}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-text-tertiary">AI审核</span>
<span className="text-sm text-accent-green font-medium"></span>
</div>
{isAgency && (
<div className="flex items-center justify-between">
<span className="text-sm text-text-tertiary"></span>
<span className="text-sm text-accent-indigo font-medium">...</span>
</div>
)}
{!isAgency && (
<>
<div className="flex items-center justify-between">
<span className="text-sm text-text-tertiary"></span>
<span className="text-sm text-accent-green font-medium"></span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-text-tertiary"></span>
<span className="text-sm text-accent-indigo font-medium">...</span>
</div>
</>
)}
</div>
</div>
{/* 等待提示 */}
<div className="bg-bg-card rounded-2xl p-6 card-shadow flex-1">
<div className="flex items-center gap-4 mb-4">
<div className="w-12 h-12 rounded-xl bg-accent-indigo/15 flex items-center justify-center">
<Clock className="w-6 h-6 text-accent-indigo" />
</div>
<div className="flex flex-col gap-0.5">
<span className="text-lg font-semibold text-text-primary">{title}</span>
<span className="text-sm text-text-secondary">{description}</span>
</div>
</div>
<div className="bg-accent-indigo/10 rounded-xl p-4">
<div className="flex items-start gap-3">
<AlertTriangle className="w-5 h-5 text-accent-indigo flex-shrink-0 mt-0.5" />
<div className="flex flex-col gap-1">
<span className="text-sm font-medium text-text-primary"></span>
<span className="text-[13px] text-text-secondary">
{isAgency
? '代理商通常会在 1-2 个工作日内完成审核,审核结果将通过消息中心通知您。'
: '品牌方终审通常需要 1-3 个工作日,通过后即可进入下一阶段。'}
</span>
</div>
</div>
</div>
</div>
</div>
)
}
// 审核通过界面
function ApprovedView({ task }: { task: TaskData }) {
const isVideoPhase = task.phase === 'video'
return (
<div className="flex flex-col gap-6 h-full">
{/* 进度条 */}
<ReviewProgressBar task={task} />
{/* 提交信息 */}
<div className="bg-bg-card rounded-2xl p-6 card-shadow">
<div className="flex items-center gap-3 mb-4">
<FileText className="w-5 h-5 text-text-secondary" />
<span className="text-base font-semibold text-text-primary">
{task.phase === 'script' ? '脚本提交信息' : '视频提交信息'}
</span>
</div>
<div className="bg-bg-elevated rounded-xl p-4 flex flex-col gap-3">
<div className="flex items-center justify-between">
<span className="text-sm text-text-tertiary"></span>
<span className="text-sm text-text-primary">{task.submittedAt || '2026-02-01 10:30'}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-text-tertiary">AI审核</span>
<span className="text-sm text-accent-green font-medium"></span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-text-tertiary"></span>
<span className="text-sm text-accent-green font-medium"></span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-text-tertiary"></span>
<span className="text-sm text-accent-green font-medium"></span>
</div>
</div>
</div>
{/* 审核结果 */}
<div className="bg-bg-card rounded-2xl p-6 card-shadow">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<CheckCircle className="w-6 h-6 text-accent-green" />
<span className="text-lg font-semibold text-text-primary"></span>
</div>
<span className="px-2.5 py-1 rounded-full text-xs font-semibold bg-accent-green/15 text-accent-green">
</span>
</div>
<p className="text-sm text-text-secondary">
{isVideoPhase
? '恭喜!视频已通过所有审核,可以发布了'
: '脚本已通过品牌方终审,请继续上传视频'}
</p>
</div>
{/* 通过提示 */}
<div className="bg-bg-card rounded-2xl p-6 card-shadow">
<div className="flex items-center gap-4 mb-4">
<div className="w-12 h-12 rounded-xl bg-accent-green/15 flex items-center justify-center">
<CheckCircle className="w-6 h-6 text-accent-green" />
</div>
<div className="flex flex-col gap-0.5">
<span className="text-lg font-semibold text-text-primary">
{isVideoPhase ? '全部审核通过' : '脚本审核通过'}
</span>
<span className="text-sm text-text-secondary">
{isVideoPhase ? '可以安排发布了' : '请在 7 天内上传视频'}
</span>
</div>
</div>
<div className="bg-accent-green/10 rounded-xl p-4">
<div className="flex items-start gap-3">
<CheckCircle className="w-5 h-5 text-accent-green flex-shrink-0 mt-0.5" />
<div className="flex flex-col gap-1">
<span className="text-sm font-medium text-text-primary">
{isVideoPhase ? '恭喜完成!' : '下一步'}
</span>
<span className="text-[13px] text-text-secondary">
{isVideoPhase
? '您的视频已通过全部审核流程,可以在平台发布了。发布后记得在消息中心查看数据反馈。'
: '脚本已通过审核,请在 7 天内上传对应视频。视频将再次经过 AI 审核 → 代理商审核 → 品牌方终审。'}
</span>
</div>
</div>
</div>
</div>
{/* 下一步按钮 */}
{!isVideoPhase && (
<div className="flex justify-center pt-4">
<button
type="button"
className="flex items-center gap-2 px-12 py-4 rounded-xl bg-accent-green text-white text-base font-semibold"
>
<Video className="w-5 h-5" />
<ChevronRight className="w-5 h-5" />
</button>
</div>
)}
</div>
)
}
export default function TaskDetailPage() {
const params = useParams()
const router = useRouter()
const taskId = params.id as string
const taskData = allTasksData[taskId]
if (!taskData) {
return (
<ResponsiveLayout role="creator">
<div className="flex items-center justify-center h-full">
<div className="flex flex-col items-center gap-4">
<XCircle className="w-16 h-16 text-text-tertiary" />
<p className="text-lg text-text-secondary"></p>
<button
type="button"
onClick={() => router.back()}
className="px-6 py-2.5 rounded-xl bg-accent-indigo text-white text-sm font-medium"
>
</button>
</div>
</div>
</ResponsiveLayout>
)
}
// 跳转到申诉页面
const handleAppeal = () => {
router.push(`/creator/appeals/new?taskId=${taskId}`)
}
// 根据状态渲染内容
const renderContent = () => {
switch (taskData.stage) {
case 'upload':
return <UploadView task={taskData} />
case 'ai_reviewing':
return <AIReviewingView task={taskData} />
case 'ai_result':
case 'agency_rejected':
case 'brand_rejected':
return <RejectionView task={taskData} onAppeal={handleAppeal} />
case 'agency_reviewing':
case 'brand_reviewing':
return <WaitingReviewView task={taskData} />
case 'brand_approved':
return <ApprovedView task={taskData} />
default:
return <div></div>
}
}
// 获取页面标题
const getPageTitle = () => {
switch (taskData.stage) {
case 'upload':
return taskData.phase === 'script' ? '上传脚本' : '上传视频'
case 'ai_reviewing':
return 'AI 智能审核'
case 'ai_result':
return 'AI 审核结果'
case 'agency_reviewing':
return '等待代理商审核'
case 'agency_rejected':
return '代理商审核驳回'
case 'brand_reviewing':
return '等待品牌方终审'
case 'brand_approved':
return '审核通过'
case 'brand_rejected':
return '品牌方审核驳回'
default:
return '任务详情'
}
}
return (
<ResponsiveLayout role="creator">
<div className="flex flex-col gap-6 h-full">
{/* 顶部栏 */}
<div className="flex items-center justify-between">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-3 mb-1">
<button
type="button"
onClick={() => router.back()}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bg-elevated text-text-secondary text-sm hover:bg-bg-card transition-colors"
>
<ArrowLeft className="w-4 h-4" />
</button>
</div>
<h1 className="text-xl lg:text-[28px] font-bold text-text-primary">{taskData.title}</h1>
<p className="text-sm lg:text-[15px] text-text-secondary">{taskData.subtitle}</p>
</div>
</div>
{/* 主内容 */}
<div className="flex-1 min-h-0 overflow-y-auto">
{renderContent()}
</div>
</div>
</ResponsiveLayout>
)
}