'use client' import { useState, useEffect, useCallback } from 'react' import { useRouter, useParams } from 'next/navigation' import { useToast } from '@/components/ui/Toast' import { ArrowLeft, FileText, Download, Eye, Target, Ban, File, Building2, Calendar, Clock, ChevronRight, Loader2 } from 'lucide-react' import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout' import { Modal } from '@/components/ui/Modal' import { Button } from '@/components/ui/Button' import { api } from '@/lib/api' import { USE_MOCK } from '@/contexts/AuthContext' import type { BriefResponse } from '@/types/brief' import type { TaskResponse } from '@/types/task' // 代理商Brief文档类型 type AgencyBriefFile = { id: string name: string size: string uploadedAt: string description?: string url?: string } // 页面视图模型 type BriefViewModel = { taskName: string agencyName: string brandName: string deadline: string createdAt: string files: AgencyBriefFile[] sellingPoints: { id: string; content: string; required: boolean }[] blacklistWords: { id: string; word: string; reason: string }[] contentRequirements: string[] } // 模拟任务数据 const mockTaskInfo = { id: 'task-001', taskName: 'XX品牌618推广', agencyName: '星辰传媒', brandName: 'XX护肤品牌', deadline: '2026-06-18', createdAt: '2026-02-08', } // 模拟代理商Brief数据 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: '视觉风格和拍摄参考示例' }, { id: 'af4', name: '产品素材包.zip', size: '15.6MB', 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: '绝对化用语' }, { id: 'bw5', word: '100%', reason: '虚假宣传' }, ], contentRequirements: [ '视频时长:60-90秒', '需展示产品质地和使用效果', '需在户外或阳光下拍摄', '需提及产品核心卖点', ], } function buildMockViewModel(): BriefViewModel { return { taskName: mockTaskInfo.taskName, agencyName: mockTaskInfo.agencyName, brandName: mockTaskInfo.brandName, deadline: mockTaskInfo.deadline, createdAt: mockTaskInfo.createdAt, files: mockAgencyBrief.files, sellingPoints: mockAgencyBrief.sellingPoints, blacklistWords: mockAgencyBrief.blacklistWords, contentRequirements: mockAgencyBrief.contentRequirements, } } function buildViewModelFromAPI(task: TaskResponse, brief: BriefResponse): BriefViewModel { // 优先显示代理商上传的文档,没有则降级到品牌方附件 const agencyAtts = brief.agency_attachments ?? [] const brandAtts = brief.attachments ?? [] const sourceAtts = agencyAtts.length > 0 ? agencyAtts : brandAtts const files: AgencyBriefFile[] = sourceAtts.map((att, idx) => ({ id: att.id || `att-${idx}`, name: att.name, size: att.size || '', uploadedAt: brief.updated_at?.split('T')[0] || '', description: undefined, url: att.url, })) // Map selling points const sellingPoints = (brief.selling_points ?? []).map((sp, idx) => ({ id: `sp-${idx}`, content: sp.content, required: sp.required, })) // Map blacklist words const blacklistWords = (brief.blacklist_words ?? []).map((bw, idx) => ({ id: `bw-${idx}`, word: bw.word, reason: bw.reason, })) // Build content requirements const contentRequirements: string[] = [] if (brief.min_duration != null || brief.max_duration != null) { const minStr = brief.min_duration != null ? `${brief.min_duration}` : '?' const maxStr = brief.max_duration != null ? `${brief.max_duration}` : '?' contentRequirements.push(`视频时长:${minStr}-${maxStr}秒`) } if (brief.other_requirements) { contentRequirements.push(brief.other_requirements) } return { taskName: task.name, agencyName: task.agency.name, brandName: task.project.brand_name || task.project.name, deadline: '', // backend task has no deadline field yet createdAt: task.created_at.split('T')[0], files, sellingPoints, blacklistWords, contentRequirements, } } // 骨架屏 function BriefSkeleton() { return (
{/* 顶部导航骨架 */}
{/* 任务信息骨架 */}
{[...Array(4)].map((_, i) => (
))}
{/* 内容区域骨架 */}
{[...Array(3)].map((_, i) => (
))}
) } export default function TaskBriefPage() { const router = useRouter() const params = useParams() const toast = useToast() const taskId = params.id as string const [previewFile, setPreviewFile] = useState(null) const [loading, setLoading] = useState(true) const [viewModel, setViewModel] = useState(null) const loadBriefData = useCallback(async () => { if (USE_MOCK) { setViewModel(buildMockViewModel()) setLoading(false) return } try { setLoading(true) // First get the task to find its project ID const task = await api.getTask(taskId) // Then get the brief for that project const brief = await api.getBrief(task.project.id) setViewModel(buildViewModelFromAPI(task, brief)) } catch (err) { const message = err instanceof Error ? err.message : '加载Brief失败' toast.error(message) console.error('加载Brief失败:', err) // Fallback: still show task info if brief load fails } finally { setLoading(false) } }, [taskId, toast]) useEffect(() => { loadBriefData() }, [loadBriefData]) const handleDownload = async (file: AgencyBriefFile) => { if (USE_MOCK || !file.url) { toast.info(`下载文件: ${file.name}`) return } try { const signedUrl = await api.getSignedUrl(file.url) window.open(signedUrl, '_blank') } catch { toast.error('获取下载链接失败') } } const handleDownloadAll = () => { if (!viewModel) return viewModel.files.forEach(f => handleDownload(f)) } if (loading || !viewModel) { return ( ) } const requiredPoints = viewModel.sellingPoints.filter(sp => sp.required) const optionalPoints = viewModel.sellingPoints.filter(sp => !sp.required) return (
{/* 顶部导航 */}

{viewModel.taskName}

查看任务要求和Brief文档

{/* 任务基本信息 */}

任务信息

代理商

{viewModel.agencyName}

品牌方

{viewModel.brandName}

分配时间

{viewModel.createdAt}

{viewModel.deadline && (

截止日期

{viewModel.deadline}

)}
{/* 主要内容区域 - 可滚动 */}
{/* Brief文档列表 */} {viewModel.files.length > 0 && (

Brief 文档

({viewModel.files.length}个文件)
{viewModel.files.map((file) => (

{file.name}

{file.size}

{file.description && (

{file.description}

)}
))}
)} {/* 内容要求 */} {viewModel.contentRequirements.length > 0 && (

内容要求

    {viewModel.contentRequirements.map((req, index) => (
  • {req}
  • ))}
)} {/* 卖点要求 */} {viewModel.sellingPoints.length > 0 && (

卖点要求

{requiredPoints.length > 0 && (

必选卖点(必须在内容中提及)

{requiredPoints.map((sp) => ( {sp.content} ))}
)} {optionalPoints.length > 0 && (

可选卖点(建议提及)

{optionalPoints.map((sp) => ( {sp.content} ))}
)}
)} {/* 违禁词 */} {viewModel.blacklistWords.length > 0 && (

违禁词(请勿在内容中使用)

{viewModel.blacklistWords.map((bw) => ( 「{bw.word}」{bw.reason} ))}
)} {/* 底部操作按钮 */}
{/* 文件预览弹窗 */} setPreviewFile(null)} title={previewFile?.name || '文件预览'} size="lg" >

文件预览区域

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

{previewFile && ( )}
) }