'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 (
)
}
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) => (
{i < SCRIPT_STEPS.length - 1 && (
)}
))}
{/* 分隔线 */}
{/* 视频阶段 */}
视频
{VIDEO_STEPS.map((step, i) => (
{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.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="修改截止日期">
)
}