'use client' import { useState, useEffect, useCallback } from 'react' import { useToast } from '@/components/ui/Toast' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card' import { Button } from '@/components/ui/Button' import { Modal } from '@/components/ui/Modal' import { SuccessTag, PendingTag, WarningTag } from '@/components/ui/Tag' import { Search, Plus, Users, TrendingUp, TrendingDown, Copy, CheckCircle, Clock, MoreVertical, FileText, Video, ChevronDown, ChevronRight, PlusCircle, UserPlus, AlertCircle, MessageSquareText, Trash2, FolderPlus, X, Loader2 } from 'lucide-react' import { getPlatformInfo } from '@/lib/platforms' import { api } from '@/lib/api' import { USE_MOCK } from '@/contexts/AuthContext' import type { CreatorDetail } from '@/types/organization' import type { TaskResponse } from '@/types/task' // 任务进度阶段 type TaskStage = 'script_pending' | 'script_ai_review' | 'script_agency_review' | 'script_brand_review' | 'video_pending' | 'video_ai_review' | 'video_agency_review' | 'video_brand_review' | 'completed' // 任务阶段配置 const stageConfig: Record = { script_pending: { label: '待提交脚本', color: 'text-text-tertiary', bgColor: 'bg-bg-elevated' }, script_ai_review: { label: '脚本AI审核中', color: 'text-accent-indigo', bgColor: 'bg-accent-indigo/15' }, script_agency_review: { label: '脚本代理商审核', color: 'text-purple-400', bgColor: 'bg-purple-500/15' }, script_brand_review: { label: '脚本品牌方终审', color: 'text-accent-blue', bgColor: 'bg-accent-blue/15' }, video_pending: { label: '待提交视频', color: 'text-accent-amber', bgColor: 'bg-accent-amber/15' }, video_ai_review: { label: '视频AI审核中', color: 'text-accent-indigo', bgColor: 'bg-accent-indigo/15' }, video_agency_review: { label: '视频代理商审核', color: 'text-purple-400', bgColor: 'bg-purple-500/15' }, video_brand_review: { label: '视频品牌方终审', color: 'text-accent-blue', bgColor: 'bg-accent-blue/15' }, completed: { label: '已完成', color: 'text-accent-green', bgColor: 'bg-accent-green/15' }, } // 后端 TaskStage 到本地 TaskStage 的映射 function mapBackendStage(backendStage: string): TaskStage { const mapping: Record = { 'script_upload': 'script_pending', 'script_ai_review': 'script_ai_review', 'script_agency_review': 'script_agency_review', 'script_brand_review': 'script_brand_review', 'video_upload': 'video_pending', 'video_ai_review': 'video_ai_review', 'video_agency_review': 'video_agency_review', 'video_brand_review': 'video_brand_review', 'completed': 'completed', 'rejected': 'completed', } return mapping[backendStage] || 'script_pending' } // 任务类型 interface CreatorTask { id: string name: string projectName: string platform: string stage: TaskStage appealRemaining: number appealUsed: number } // 达人类型 interface Creator { id: string creatorId: string // 达人ID(用于邀请和显示) name: string avatar: string status: 'active' | 'pending' | 'paused' projectCount: number scriptCount: { total: number; passed: number } videoCount: { total: number; passed: number } passRate: number trend: 'up' | 'down' | 'stable' joinedAt: string tasks: CreatorTask[] remark?: string // 备注 } // 模拟项目列表(用于分配达人) const mockProjects = [ { id: 'proj-001', name: 'XX品牌618推广' }, { id: 'proj-002', name: '口红系列推广' }, { id: 'proj-003', name: 'XX运动品牌' }, { id: 'proj-004', name: '护肤品秋季活动' }, ] // 模拟达人列表 const mockCreators: Creator[] = [ { id: 'c-001', creatorId: 'CR123456', name: '小美护肤', avatar: '小', status: 'active', projectCount: 5, scriptCount: { total: 12, passed: 10 }, videoCount: { total: 10, passed: 8 }, passRate: 85, trend: 'up', joinedAt: '2025-08-15', tasks: [ { id: 'task-001', name: '夏日护肤推广', projectName: 'XX品牌618', platform: 'douyin', stage: 'video_agency_review', appealRemaining: 1, appealUsed: 0 }, { id: 'task-002', name: '防晒霜测评', projectName: 'XX品牌618', platform: 'douyin', stage: 'script_brand_review', appealRemaining: 0, appealUsed: 1 }, ], }, { id: 'c-002', creatorId: 'CR789012', name: '美妆Lisa', avatar: 'L', status: 'active', projectCount: 3, scriptCount: { total: 8, passed: 7 }, videoCount: { total: 6, passed: 5 }, passRate: 80, trend: 'stable', joinedAt: '2025-10-20', tasks: [ { id: 'task-003', name: '新品口红试色', projectName: '口红系列推广', platform: 'xiaohongshu', stage: 'video_pending', appealRemaining: 2, appealUsed: 0 }, ], }, { id: 'c-003', creatorId: 'CR345678', name: '健身教练王', avatar: '王', status: 'active', projectCount: 2, scriptCount: { total: 5, passed: 5 }, videoCount: { total: 4, passed: 4 }, passRate: 100, trend: 'up', joinedAt: '2025-12-01', tasks: [ { id: 'task-004', name: '健身器材使用教程', projectName: 'XX运动品牌', platform: 'bilibili', stage: 'script_ai_review', appealRemaining: 1, appealUsed: 0 }, ], }, { id: 'c-004', creatorId: 'CR901234', name: '时尚达人', avatar: '时', status: 'pending', projectCount: 0, scriptCount: { total: 0, passed: 0 }, videoCount: { total: 0, passed: 0 }, passRate: 0, trend: 'stable', joinedAt: '2026-02-05', tasks: [], }, ] function StatusTag({ status }: { status: string }) { if (status === 'active') return 已激活 if (status === 'pending') return 待接受 return 已暂停 } function StageTag({ stage }: { stage: TaskStage }) { const config = stageConfig[stage] return ( {config.label} ) } export default function AgencyCreatorsPage() { const toast = useToast() const [searchQuery, setSearchQuery] = useState('') const [showInviteModal, setShowInviteModal] = useState(false) const [inviteCreatorId, setInviteCreatorId] = useState('') const [inviteResult, setInviteResult] = useState<{ success: boolean; message: string } | null>(null) const [expandedCreators, setExpandedCreators] = useState([]) const [creators, setCreators] = useState(USE_MOCK ? mockCreators : []) const [copiedId, setCopiedId] = useState(null) // 加载状态 const [loading, setLoading] = useState(!USE_MOCK) const [submitting, setSubmitting] = useState(false) // 项目列表(API 模式用于分配弹窗) const [projects, setProjects] = useState<{ id: string; name: string }[]>(USE_MOCK ? mockProjects : []) // 任务数据(API 模式按达人ID分组) const [creatorTasksMap, setCreatorTasksMap] = useState>({}) // 操作菜单状态 const [openMenuId, setOpenMenuId] = useState(null) // 备注弹窗状态 const [remarkModal, setRemarkModal] = useState<{ open: boolean; creator: Creator | null }>({ open: false, creator: null }) const [remarkText, setRemarkText] = useState('') // 删除确认弹窗状态 const [deleteModal, setDeleteModal] = useState<{ open: boolean; creator: Creator | null }>({ open: false, creator: null }) // 分配项目弹窗状态 const [assignModal, setAssignModal] = useState<{ open: boolean; creator: Creator | null }>({ open: false, creator: null }) const [selectedProject, setSelectedProject] = useState('') // API 模式下将 CreatorDetail 转换为 Creator 类型 const mapCreatorDetailToCreator = useCallback((detail: CreatorDetail, tasks: CreatorTask[]): Creator => { return { id: detail.id, creatorId: detail.id, name: detail.name, avatar: detail.avatar || detail.name.charAt(0), status: 'active', projectCount: 0, scriptCount: { total: 0, passed: 0 }, videoCount: { total: 0, passed: 0 }, passRate: 0, trend: 'stable', joinedAt: '-', tasks, } }, []) // 将后端 TaskResponse 转为本地 CreatorTask const mapTaskResponseToCreatorTask = useCallback((task: TaskResponse): CreatorTask => { return { id: task.id, name: task.name, projectName: task.project?.name || '-', platform: task.project?.platform || 'douyin', stage: mapBackendStage(task.stage), appealRemaining: task.appeal_count, appealUsed: task.is_appeal ? 1 : 0, } }, []) // 加载数据(API 模式) const fetchData = useCallback(async () => { if (USE_MOCK) return setLoading(true) try { // 并行加载达人列表、任务列表、项目列表 const [creatorsRes, tasksRes, projectsRes] = await Promise.all([ api.listAgencyCreators(), api.listTasks(1, 100), api.listProjects(1, 100), ]) // 构建项目列表 setProjects(projectsRes.items.map(p => ({ id: p.id, name: p.name }))) // 按达人ID分组任务 const tasksMap: Record = {} for (const task of tasksRes.items) { const cid = task.creator?.id if (cid) { if (!tasksMap[cid]) tasksMap[cid] = [] tasksMap[cid].push(mapTaskResponseToCreatorTask(task)) } } setCreatorTasksMap(tasksMap) // 构建达人列表 const mappedCreators = creatorsRes.items.map(detail => mapCreatorDetailToCreator(detail, tasksMap[detail.id] || []) ) setCreators(mappedCreators) } catch (err) { const message = err instanceof Error ? err.message : '加载达人数据失败' toast.error(message) } finally { setLoading(false) } }, [mapCreatorDetailToCreator, mapTaskResponseToCreatorTask, toast]) useEffect(() => { fetchData() }, [fetchData]) const filteredCreators = creators.filter(creator => creator.name.toLowerCase().includes(searchQuery.toLowerCase()) || creator.creatorId.toLowerCase().includes(searchQuery.toLowerCase()) ) // 统计数据 const totalCreators = creators.length const activeCreators = USE_MOCK ? creators.filter(c => c.status === 'active').length : creators.length // API 模式下返回的都是已关联达人 const totalScripts = USE_MOCK ? creators.reduce((sum, c) => sum + c.scriptCount.total, 0) : 0 const totalVideos = USE_MOCK ? creators.reduce((sum, c) => sum + c.videoCount.total, 0) : 0 // 切换展开状态 const toggleExpand = (creatorId: string) => { setExpandedCreators(prev => prev.includes(creatorId) ? prev.filter(id => id !== creatorId) : [...prev, creatorId] ) } // 复制达人ID const handleCopyCreatorId = async (creatorId: string) => { await navigator.clipboard.writeText(creatorId) setCopiedId(creatorId) setTimeout(() => setCopiedId(null), 2000) } // 增加申诉次数 const handleAddAppealQuota = async (creatorId: string, taskId: string) => { if (USE_MOCK) { setCreators(prev => prev.map(creator => { if (creator.id === creatorId) { return { ...creator, tasks: creator.tasks.map(task => { if (task.id === taskId) { return { ...task, appealRemaining: task.appealRemaining + 1 } } return task }), } } return creator })) return } setSubmitting(true) try { await api.increaseAppealCount(taskId) // 更新本地状态 setCreators(prev => prev.map(creator => { if (creator.id === creatorId) { return { ...creator, tasks: creator.tasks.map(task => { if (task.id === taskId) { return { ...task, appealRemaining: task.appealRemaining + 1 } } return task }), } } return creator })) toast.success('已增加 1 次申诉机会') } catch (err) { const message = err instanceof Error ? err.message : '增加申诉次数失败' toast.error(message) } finally { setSubmitting(false) } } // 邀请达人 const handleInvite = async () => { if (!inviteCreatorId.trim()) { setInviteResult({ success: false, message: '请输入达人ID' }) return } if (USE_MOCK) { // 模拟检查达人ID是否存在 const idPattern = /^CR\d{6}$/ if (!idPattern.test(inviteCreatorId.toUpperCase())) { setInviteResult({ success: false, message: '达人ID格式错误,应为CR+6位数字' }) return } // 检查是否已邀请 if (creators.some(c => c.creatorId === inviteCreatorId.toUpperCase())) { setInviteResult({ success: false, message: '该达人已在您的列表中' }) return } // 模拟发送邀请成功 setInviteResult({ success: true, message: `已向达人 ${inviteCreatorId.toUpperCase()} 发送邀请` }) return } // API 模式 setSubmitting(true) try { await api.inviteCreator(inviteCreatorId.trim()) setInviteResult({ success: true, message: `已向达人 ${inviteCreatorId.trim()} 发送邀请` }) toast.success('邀请已发送') } catch (err) { const message = err instanceof Error ? err.message : '邀请达人失败' setInviteResult({ success: false, message }) } finally { setSubmitting(false) } } const handleCloseInviteModal = () => { setShowInviteModal(false) setInviteCreatorId('') setInviteResult(null) } // 打开备注弹窗 const handleOpenRemark = (creator: Creator) => { setRemarkText(creator.remark || '') setRemarkModal({ open: true, creator }) setOpenMenuId(null) } // 保存备注 const handleSaveRemark = () => { if (remarkModal.creator) { setCreators(prev => prev.map(c => c.id === remarkModal.creator!.id ? { ...c, remark: remarkText } : c )) } setRemarkModal({ open: false, creator: null }) setRemarkText('') } // 打开删除确认 const handleOpenDelete = (creator: Creator) => { setDeleteModal({ open: true, creator }) setOpenMenuId(null) } // 确认删除 const handleConfirmDelete = async () => { if (!deleteModal.creator) return if (USE_MOCK) { setCreators(prev => prev.filter(c => c.id !== deleteModal.creator!.id)) setDeleteModal({ open: false, creator: null }) return } // API 模式 setSubmitting(true) try { await api.removeCreator(deleteModal.creator.id) setCreators(prev => prev.filter(c => c.id !== deleteModal.creator!.id)) toast.success(`已移除达人「${deleteModal.creator.name}」`) setDeleteModal({ open: false, creator: null }) } catch (err) { const message = err instanceof Error ? err.message : '移除达人失败' toast.error(message) } finally { setSubmitting(false) } } // 打开分配项目弹窗 const handleOpenAssign = (creator: Creator) => { setSelectedProject('') setAssignModal({ open: true, creator }) setOpenMenuId(null) } // 确认分配项目(创建任务) const handleConfirmAssign = async () => { const projectList = USE_MOCK ? mockProjects : projects if (!assignModal.creator || !selectedProject) return const project = projectList.find(p => p.id === selectedProject) if (USE_MOCK) { toast.success(`已将达人「${assignModal.creator.name}」分配到项目「${project?.name}」`) setAssignModal({ open: false, creator: null }) setSelectedProject('') return } setSubmitting(true) try { await api.createTask({ project_id: selectedProject, creator_id: assignModal.creator.creatorId, }) toast.success(`已将达人「${assignModal.creator.name}」分配到项目「${project?.name}」`) setAssignModal({ open: false, creator: null }) setSelectedProject('') await fetchData() // 刷新列表 } catch (err) { const message = err instanceof Error ? err.message : '分配失败' toast.error(message) } finally { setSubmitting(false) } } // 骨架屏 if (loading) { return (
{/* 页面标题 */}

达人管理

管理合作达人,查看任务进度和申诉次数

{/* 统计卡片骨架 */}
{[1, 2, 3, 4].map(i => (
))}
{/* 搜索骨架 */}
{/* 表格骨架 */}
加载达人数据...
) } const projectList = USE_MOCK ? mockProjects : projects return (
{/* 页面标题 */}

达人管理

管理合作达人,查看任务进度和申诉次数

{/* 统计卡片 */}

总达人数

{totalCreators}

已激活

{activeCreators}

总脚本数

{USE_MOCK ? totalScripts : '-'}

总视频数

{USE_MOCK ? totalVideos : '-'}

{/* 搜索 */}
setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2.5 border border-border-subtle rounded-xl bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo" />
{/* 达人列表 */} {filteredCreators.map((creator) => { const isExpanded = expandedCreators.includes(creator.id) const hasActiveTasks = creator.tasks.length > 0 return ( <> {/* 展开的任务列表 */} {isExpanded && hasActiveTasks && ( )} ) })}
达人 达人ID 状态 脚本 视频 通过率 加入时间 操作
{/* 展开按钮 */} {hasActiveTasks ? ( ) : (
)}
{creator.avatar}
{creator.name} {creator.remark && ( 有备注 )}
{creator.remark && (

{creator.remark}

)} {hasActiveTasks && ( )}
{creator.creatorId}
{USE_MOCK ? ( ) : ( 已关联 )} {USE_MOCK ? ( <> {creator.scriptCount.passed} /{creator.scriptCount.total} ) : ( - )} {USE_MOCK ? ( <> {creator.videoCount.passed} /{creator.videoCount.total} ) : ( - )} {USE_MOCK && creator.status === 'active' && creator.passRate > 0 ? (
= 90 ? 'text-accent-green' : creator.passRate >= 80 ? 'text-accent-indigo' : 'text-orange-400'}`}> {creator.passRate}% {creator.trend === 'up' && } {creator.trend === 'down' && }
) : ( - )}
{creator.joinedAt}
{openMenuId === creator.id && (
)}
进行中的任务
{creator.tasks.map(task => { const taskPlatform = getPlatformInfo(task.platform) return (
{/* 平台顶部条 */} {taskPlatform && (
{taskPlatform.icon} {taskPlatform.name}
)}
{task.name}
项目: {task.projectName}
申诉次数: {task.appealRemaining} / 已用 {task.appealUsed}
) })}
{filteredCreators.length === 0 && (

没有找到匹配的达人

)}
{/* 邀请达人弹窗 */}

输入达人ID邀请合作。达人ID可在达人的个人中心查看。

{ setInviteCreatorId(e.target.value.toUpperCase()) setInviteResult(null) }} placeholder="例如: CR123456" className="flex-1 px-4 py-2.5 border border-border-subtle rounded-xl bg-bg-elevated text-text-primary font-mono focus:outline-none focus:ring-2 focus:ring-accent-indigo" />

达人ID格式:CR + 6位数字

{inviteResult && (
{inviteResult.success ? ( ) : ( )} {inviteResult.message}
)}
{/* 备注弹窗 */} { setRemarkModal({ open: false, creator: null }); setRemarkText(''); }} title={`${remarkModal.creator?.remark ? '编辑' : '添加'}备注 - ${remarkModal.creator?.name}`} >