'use client' import { useState, useEffect, useCallback } from 'react' import { useRouter, useParams } from 'next/navigation' import Link from 'next/link' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card' import { Button } from '@/components/ui/Button' import { Input } from '@/components/ui/Input' import { Modal } from '@/components/ui/Modal' import { SuccessTag, PendingTag, ErrorTag } from '@/components/ui/Tag' import { useToast } from '@/components/ui/Toast' import { ArrowLeft, Calendar, Users, FileText, Clock, CheckCircle, XCircle, ChevronRight, Plus, Settings, Search, Building2, MoreHorizontal, Trash2, Check, Pencil, Loader2 } from 'lucide-react' import { getPlatformInfo } from '@/lib/platforms' import { mapTaskToUI } from '@/lib/taskStageMapper' import { api } from '@/lib/api' import { USE_MOCK } from '@/contexts/AuthContext' import { useSSE } from '@/contexts/SSEContext' import type { ProjectResponse } from '@/types/project' import type { TaskResponse } from '@/types/task' import type { AgencyDetail } from '@/types/organization' // ==================== Mock 数据 ==================== const mockProject: ProjectResponse = { id: 'proj-001', name: 'XX品牌618推广', brand_id: 'br-001', brand_name: 'XX护肤品牌', description: '618大促活动营销内容审核项目', status: 'active', deadline: '2026-06-18', agencies: [ { id: 'AG789012', name: '星耀传媒' }, { id: 'AG456789', name: '创意无限' }, ], task_count: 20, created_at: '2026-02-01T00:00:00Z', updated_at: '2026-02-06T00:00:00Z', } const mockTasks: TaskResponse[] = [ { id: 'task-001', name: '夏日护肤推广', sequence: 1, stage: 'video_brand_review', project: { id: 'proj-001', name: 'XX品牌618推广' }, agency: { id: 'AG789012', name: '星耀传媒' }, creator: { id: 'cr-001', name: '小美护肤' }, appeal_count: 0, is_appeal: false, created_at: '2026-02-06T14:30:00Z', updated_at: '2026-02-06T14:30:00Z', }, { id: 'task-002', name: '新品口红试色', sequence: 2, stage: 'completed', project: { id: 'proj-001', name: 'XX品牌618推广' }, agency: { id: 'AG789012', name: '星耀传媒' }, creator: { id: 'cr-002', name: '美妆Lisa' }, appeal_count: 0, is_appeal: false, created_at: '2026-02-06T12:15:00Z', updated_at: '2026-02-06T12:15:00Z', }, { id: 'task-003', name: '健身器材推荐', sequence: 3, stage: 'rejected', project: { id: 'proj-001', name: 'XX品牌618推广' }, agency: { id: 'AG456789', name: '创意无限' }, creator: { id: 'cr-003', name: '健身王' }, appeal_count: 0, is_appeal: false, created_at: '2026-02-06T10:00:00Z', updated_at: '2026-02-06T10:00:00Z', }, { id: 'task-004', name: '美白精华测评', sequence: 4, stage: 'script_agency_review', project: { id: 'proj-001', name: 'XX品牌618推广' }, agency: { id: 'AG456789', name: '创意无限' }, creator: { id: 'cr-004', name: '时尚小王' }, appeal_count: 0, is_appeal: false, created_at: '2026-02-07T09:00:00Z', updated_at: '2026-02-07T09:00:00Z', }, { id: 'task-005', name: '防晒霜种草', sequence: 5, stage: 'script_upload', project: { id: 'proj-001', name: 'XX品牌618推广' }, agency: { id: 'AG789012', name: '星耀传媒' }, creator: { id: 'cr-001', name: '小美护肤' }, appeal_count: 0, is_appeal: false, created_at: '2026-02-07T11:00:00Z', updated_at: '2026-02-07T11:00:00Z', }, ] const mockManagedAgencies: AgencyDetail[] = [ { id: 'AG789012', name: '星耀传媒', force_pass_enabled: true, contact_name: '张经理' }, { id: 'AG456789', name: '创意无限', force_pass_enabled: false, contact_name: '李总' }, { id: 'AG123456', name: '美妆达人MCN', force_pass_enabled: false }, { id: 'AG111111', name: '蓝海科技', force_pass_enabled: true }, { id: 'AG222222', name: '云创网络', force_pass_enabled: false }, ] // ==================== 组件 ==================== function StatCard({ title, value, icon: Icon, color }: { title: string; value: number | string; icon: React.ElementType; color: string }) { return (

{title}

{value}

) } function TaskStatusTag({ stage }: { stage: string }) { if (stage === 'completed') return 已通过 if (stage === 'rejected') return 已驳回 if (stage.includes('review')) return 审核中 return 进行中 } function DetailSkeleton() { return (
{[1, 2, 3, 4].map(i =>
)}
) } // ==================== 任务进度条 ==================== const SCRIPT_STEPS = [ { key: 'script_upload', label: '上传' }, { key: 'script_ai_review', label: 'AI' }, { key: 'script_agency_review', label: '代理商' }, { key: 'script_brand_review', label: '品牌' }, ] const VIDEO_STEPS = [ { key: 'video_upload', label: '上传' }, { key: 'video_ai_review', label: 'AI' }, { key: 'video_agency_review', label: '代理商' }, { key: 'video_brand_review', label: '品牌' }, ] function StepDot({ status }: { status: 'done' | 'current' | 'error' | 'pending' }) { const base = 'w-3 h-3 rounded-full border-2 flex-shrink-0' if (status === 'done') return
if (status === 'current') return
if (status === 'error') return
return
} function StepLine({ status }: { status: 'done' | 'pending' | 'error' }) { if (status === 'done') return
if (status === 'error') return
return
} function TaskProgressBar({ task }: { task: TaskResponse }) { const ui = mapTaskToUI(task) const scriptStatuses: Array<'done' | 'current' | 'error' | 'pending'> = [ ui.scriptStage.submit, ui.scriptStage.ai, ui.scriptStage.agency, ui.scriptStage.brand, ] const videoStatuses: Array<'done' | 'current' | 'error' | 'pending'> = [ ui.videoStage.submit, ui.videoStage.ai, ui.videoStage.agency, ui.videoStage.brand, ] const isCompleted = task.stage === 'completed' const isRejected = task.stage === 'rejected' return (
{/* 脚本阶段 */}
脚本 {SCRIPT_STEPS.map((step, i) => (
{step.label}
{i < SCRIPT_STEPS.length - 1 && ( )}
))}
{/* 分隔线 */}
{/* 视频阶段 */}
视频 {VIDEO_STEPS.map((step, i) => (
{step.label}
{i < VIDEO_STEPS.length - 1 && ( )}
))}
{/* 完成标记 */}
{isCompleted ? ( ) : isRejected ? ( ) : (
)}
) } interface TaskGroup { agencyId: string agencyName: string creators: { creatorId: string creatorName: string tasks: TaskResponse[] }[] } function groupTasksByAgencyCreator(tasks: TaskResponse[]): TaskGroup[] { const agencyMap = new Map() for (const task of tasks) { if (!agencyMap.has(task.agency.id)) { agencyMap.set(task.agency.id, { agencyId: task.agency.id, agencyName: task.agency.name, creators: [], }) } const group = agencyMap.get(task.agency.id)! let creator = group.creators.find(c => c.creatorId === task.creator.id) if (!creator) { creator = { creatorId: task.creator.id, creatorName: task.creator.name, tasks: [] } group.creators.push(creator) } creator.tasks.push(task) } return Array.from(agencyMap.values()) } export default function ProjectDetailPage() { const router = useRouter() const params = useParams() const toast = useToast() const projectId = params.id as string const { subscribe } = useSSE() const [project, setProject] = useState(null) const [allTasks, setAllTasks] = useState([]) const [managedAgencies, setManagedAgencies] = useState([]) const [loading, setLoading] = useState(true) // UI states const [showAddModal, setShowAddModal] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [selectedAgencies, setSelectedAgencies] = useState([]) const [activeAgencyMenu, setActiveAgencyMenu] = useState(null) const [showDeleteModal, setShowDeleteModal] = useState(false) const [agencyToDelete, setAgencyToDelete] = useState<{ id: string; name: string } | null>(null) const [showDeadlineModal, setShowDeadlineModal] = useState(false) const [newDeadline, setNewDeadline] = useState('') const [submitting, setSubmitting] = useState(false) const loadData = useCallback(async () => { if (USE_MOCK) { setProject(mockProject) setAllTasks(mockTasks) setManagedAgencies(mockManagedAgencies) setLoading(false) return } try { const [projectData, tasksData, agenciesData] = await Promise.all([ api.getProject(projectId), api.listTasks(1, 100, undefined, projectId), api.listBrandAgencies(), ]) setProject(projectData) setAllTasks(tasksData.items) setManagedAgencies(agenciesData.items) } catch (err) { console.error('Failed to load project:', err) toast.error('加载项目详情失败') } finally { setLoading(false) } }, [projectId, toast]) useEffect(() => { loadData() }, [loadData]) useEffect(() => { const unsub = subscribe('task_updated', () => loadData()) return unsub }, [subscribe, loadData]) if (loading || !project) return const availableAgencies = managedAgencies.filter( agency => !project.agencies.some(a => a.id === agency.id) ) const filteredAgencies = availableAgencies.filter(agency => searchQuery === '' || agency.name.toLowerCase().includes(searchQuery.toLowerCase()) || agency.id.toLowerCase().includes(searchQuery.toLowerCase()) ) const toggleSelectAgency = (agencyId: string) => { setSelectedAgencies(prev => prev.includes(agencyId) ? prev.filter(id => id !== agencyId) : [...prev, agencyId] ) } const handleAddAgencies = async () => { setSubmitting(true) try { if (!USE_MOCK) { await api.assignAgencies(projectId, selectedAgencies) } const newAgencies = managedAgencies .filter(a => selectedAgencies.includes(a.id)) .map(a => ({ id: a.id, name: a.name })) setProject({ ...project, agencies: [...project.agencies, ...newAgencies] }) toast.success('代理商已添加') } catch (err) { console.error('Failed to add agencies:', err) toast.error('添加失败') } finally { setSubmitting(false) setShowAddModal(false) setSelectedAgencies([]) setSearchQuery('') } } const handleRemoveAgency = async () => { if (!agencyToDelete) return setSubmitting(true) try { if (!USE_MOCK) { await api.removeAgencyFromProject(projectId, agencyToDelete.id) } setProject({ ...project, agencies: project.agencies.filter(a => a.id !== agencyToDelete.id) }) toast.success('代理商已移除') } catch (err) { console.error('Failed to remove agency:', err) toast.error('移除失败') } finally { setSubmitting(false) setShowDeleteModal(false) setAgencyToDelete(null) } } const handleSaveDeadline = async () => { if (!newDeadline) return setSubmitting(true) try { if (!USE_MOCK) { await api.updateProject(projectId, { deadline: newDeadline }) } setProject({ ...project, deadline: newDeadline }) toast.success('截止日期已更新') } catch (err) { console.error('Failed to update deadline:', err) toast.error('更新失败') } finally { setSubmitting(false) setShowDeadlineModal(false) } } return (
{/* 顶部导航 */}

{project.name}

{project.description && (

{project.description}

)}
{project.status === 'active' ? '进行中' : project.status === 'completed' ? '已完成' : '已归档'}
{/* 项目信息 */}
截止日期: {project.deadline ? new Date(project.deadline).toLocaleDateString('zh-CN') : '未设置'} 创建时间: {new Date(project.created_at).toLocaleDateString('zh-CN')}
{/* Brief和规则配置 */}

Brief和规则配置

配置项目Brief、审核规则、AI检测项等

{/* 统计卡片 */}
{/* 任务进度 */} 任务进度 {allTasks.length > 0 ? (
{/* 图例 */}
已完成 进行中 待处理 已驳回
{groupTasksByAgencyCreator(allTasks).map((group) => (
{/* 代理商标题 */}
{group.agencyName} ({group.creators.reduce((sum, c) => sum + c.tasks.length, 0)} 个任务)
{/* 达人列表 */}
{group.creators.map((creator) => (
{/* 达人名称 */}
{creator.creatorName}
{/* 任务进度条 */}
{creator.tasks.map((task) => (
{task.name}
))}
))}
))}
) : (
暂无任务
)}
{/* 代理商列表 */} 参与代理商 ({project.agencies.length}) {project.agencies.map((agency) => (

{agency.name}

{agency.id}

{activeAgencyMenu === agency.id && (
)}
))}
{/* 添加代理商弹窗 */} { setShowAddModal(false); setSearchQuery(''); setSelectedAgencies([]) }} title="添加代理商" size="lg" >
setSearchQuery(e.target.value)} placeholder="搜索代理商名称或ID..." className="pl-10" />
{filteredAgencies.length > 0 ? ( filteredAgencies.map((agency) => { const isSelected = selectedAgencies.includes(agency.id) return ( ) }) ) : (
{availableAgencies.length === 0 ? ( <>

所有代理商都已添加到此项目

) : ( <>

未找到匹配的代理商

)}
)}
{selectedAgencies.length > 0 && (
已选择 {selectedAgencies.length} 个代理商
)}
{/* 删除确认弹窗 */} { setShowDeleteModal(false); setAgencyToDelete(null) }} title="移除代理商">

确定要将 {agencyToDelete?.name} 从此项目中移除吗?

移除后,该代理商下的达人将无法继续参与此项目的任务。

{/* 编辑截止日期弹窗 */} setShowDeadlineModal(false)} title="修改截止日期">
setNewDeadline(e.target.value)} className="w-full pl-12 pr-4 py-3 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo" />
) }