Your Name a8be7bbca9 feat: 前端剩余页面全面对接后端 API(Phase 2 完成)
为品牌方端(8页)、代理商端(10页)、达人端(6页)共24个页面添加真实API调用:
- 每页新增 USE_MOCK 条件分支,开发环境使用 mock 数据,生产环境调用真实 API
- 添加 loading 骨架屏、error toast 提示、submitting 状态管理
- 数据映射:TaskResponse → 页面视图模型,处理类型差异
- 审核操作(通过/驳回/强制通过)对接 api.reviewScript/reviewVideo
- Brief/规则/AI配置对接 api.getBrief/updateBrief/listForbiddenWords 等
- 申诉/历史/额度管理对接 api.listTasks + 状态过滤映射

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 16:29:43 +08:00

1063 lines
42 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, 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<TaskStage, { label: string; color: string; bgColor: string }> = {
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<string, TaskStage> = {
'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 <SuccessTag></SuccessTag>
if (status === 'pending') return <PendingTag></PendingTag>
return <WarningTag></WarningTag>
}
function StageTag({ stage }: { stage: TaskStage }) {
const config = stageConfig[stage]
return (
<span className={`px-2 py-1 rounded-md text-xs font-medium ${config.bgColor} ${config.color}`}>
{config.label}
</span>
)
}
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<string[]>([])
const [creators, setCreators] = useState<Creator[]>(USE_MOCK ? mockCreators : [])
const [copiedId, setCopiedId] = useState<string | null>(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<Record<string, CreatorTask[]>>({})
// 操作菜单状态
const [openMenuId, setOpenMenuId] = useState<string | null>(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: '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<string, CreatorTask[]> = {}
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 = () => {
const projectList = USE_MOCK ? mockProjects : projects
if (assignModal.creator && selectedProject) {
const project = projectList.find(p => p.id === selectedProject)
toast.success(`已将达人「${assignModal.creator.name}」分配到项目「${project?.name}`)
}
setAssignModal({ open: false, creator: null })
setSelectedProject('')
}
// 骨架屏
if (loading) {
return (
<div className="space-y-6 min-h-0">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-text-primary"></h1>
<p className="text-sm text-text-secondary mt-1"></p>
</div>
<Button disabled>
<Plus size={16} />
</Button>
</div>
{/* 统计卡片骨架 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{[1, 2, 3, 4].map(i => (
<Card key={i}>
<CardContent className="py-4">
<div className="flex items-center justify-between">
<div className="space-y-2">
<div className="h-4 w-16 bg-bg-elevated rounded animate-pulse" />
<div className="h-8 w-10 bg-bg-elevated rounded animate-pulse" />
</div>
<div className="w-10 h-10 rounded-lg bg-bg-elevated animate-pulse" />
</div>
</CardContent>
</Card>
))}
</div>
{/* 搜索骨架 */}
<div className="h-11 w-full max-w-md bg-bg-elevated rounded-xl animate-pulse" />
{/* 表格骨架 */}
<Card>
<CardContent className="p-0">
<div className="flex items-center justify-center py-20">
<Loader2 size={32} className="animate-spin text-accent-indigo" />
<span className="ml-3 text-text-secondary">...</span>
</div>
</CardContent>
</Card>
</div>
)
}
const projectList = USE_MOCK ? mockProjects : projects
return (
<div className="space-y-6 min-h-0">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-text-primary"></h1>
<p className="text-sm text-text-secondary mt-1"></p>
</div>
<Button onClick={() => setShowInviteModal(true)}>
<Plus size={16} />
</Button>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="py-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-text-secondary"></p>
<p className="text-2xl font-bold text-text-primary">{totalCreators}</p>
</div>
<div className="w-10 h-10 rounded-lg bg-accent-indigo/20 flex items-center justify-center">
<Users size={20} className="text-accent-indigo" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="py-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-text-secondary"></p>
<p className="text-2xl font-bold text-accent-green">{activeCreators}</p>
</div>
<div className="w-10 h-10 rounded-lg bg-accent-green/20 flex items-center justify-center">
<CheckCircle size={20} className="text-accent-green" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="py-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-text-secondary"></p>
<p className="text-2xl font-bold text-text-primary">{USE_MOCK ? totalScripts : '-'}</p>
</div>
<div className="w-10 h-10 rounded-lg bg-purple-500/20 flex items-center justify-center">
<FileText size={20} className="text-purple-400" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="py-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-text-secondary"></p>
<p className="text-2xl font-bold text-text-primary">{USE_MOCK ? totalVideos : '-'}</p>
</div>
<div className="w-10 h-10 rounded-lg bg-orange-500/20 flex items-center justify-center">
<Video size={20} className="text-orange-400" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* 搜索 */}
<div className="relative max-w-md">
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-tertiary" />
<input
type="text"
placeholder="搜索达人名称或达人ID..."
value={searchQuery}
onChange={(e) => 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"
/>
</div>
{/* 达人列表 */}
<Card>
<CardContent className="p-0 overflow-x-auto">
<table className="w-full min-w-[900px]">
<thead>
<tr className="border-b border-border-subtle text-left text-sm text-text-secondary bg-bg-elevated">
<th className="px-6 py-4 font-medium"></th>
<th className="px-6 py-4 font-medium">ID</th>
<th className="px-6 py-4 font-medium"></th>
<th className="px-6 py-4 font-medium"></th>
<th className="px-6 py-4 font-medium"></th>
<th className="px-6 py-4 font-medium"></th>
<th className="px-6 py-4 font-medium"></th>
<th className="px-6 py-4 font-medium"></th>
</tr>
</thead>
<tbody>
{filteredCreators.map((creator) => {
const isExpanded = expandedCreators.includes(creator.id)
const hasActiveTasks = creator.tasks.length > 0
return (
<>
<tr key={creator.id} className="border-b border-border-subtle hover:bg-bg-elevated/50">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
{/* 展开按钮 */}
{hasActiveTasks ? (
<button
type="button"
onClick={() => toggleExpand(creator.id)}
className="w-6 h-6 rounded flex items-center justify-center hover:bg-bg-elevated"
>
{isExpanded ? (
<ChevronDown size={16} className="text-text-secondary" />
) : (
<ChevronRight size={16} className="text-text-secondary" />
)}
</button>
) : (
<div className="w-6" />
)}
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-accent-indigo to-purple-500 flex items-center justify-center">
<span className="text-white font-medium">{creator.avatar}</span>
</div>
<div>
<div className="flex items-center gap-2">
<span className="font-medium text-text-primary">{creator.name}</span>
{creator.remark && (
<span className="px-2 py-0.5 text-xs rounded bg-accent-amber/15 text-accent-amber" title={creator.remark}>
</span>
)}
</div>
{creator.remark && (
<p className="text-xs text-text-tertiary mt-0.5 line-clamp-1">{creator.remark}</p>
)}
{hasActiveTasks && (
<button
type="button"
onClick={() => toggleExpand(creator.id)}
className="text-xs text-accent-indigo hover:underline flex items-center gap-1 mt-0.5"
>
{isExpanded ? (
<>
<ChevronDown size={12} />
</>
) : (
<>
<ChevronRight size={12} />
{creator.tasks.length}
</>
)}
</button>
)}
</div>
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<code className="px-2 py-1 rounded bg-bg-elevated text-sm font-mono text-accent-indigo">
{creator.creatorId}
</code>
<button
type="button"
onClick={() => handleCopyCreatorId(creator.creatorId)}
className="p-1 rounded hover:bg-bg-elevated transition-colors"
title="复制达人ID"
>
{copiedId === creator.creatorId ? (
<CheckCircle size={14} className="text-accent-green" />
) : (
<Copy size={14} className="text-text-tertiary" />
)}
</button>
</div>
</td>
<td className="px-6 py-4">
{USE_MOCK ? (
<StatusTag status={creator.status} />
) : (
<SuccessTag></SuccessTag>
)}
</td>
<td className="px-6 py-4">
{USE_MOCK ? (
<>
<span className="text-text-primary">{creator.scriptCount.passed}</span>
<span className="text-text-tertiary">/{creator.scriptCount.total}</span>
</>
) : (
<span className="text-text-tertiary">-</span>
)}
</td>
<td className="px-6 py-4">
{USE_MOCK ? (
<>
<span className="text-text-primary">{creator.videoCount.passed}</span>
<span className="text-text-tertiary">/{creator.videoCount.total}</span>
</>
) : (
<span className="text-text-tertiary">-</span>
)}
</td>
<td className="px-6 py-4">
{USE_MOCK && creator.status === 'active' && creator.passRate > 0 ? (
<div className="flex items-center gap-2">
<span className={`font-medium ${creator.passRate >= 90 ? 'text-accent-green' : creator.passRate >= 80 ? 'text-accent-indigo' : 'text-orange-400'}`}>
{creator.passRate}%
</span>
{creator.trend === 'up' && <TrendingUp size={14} className="text-accent-green" />}
{creator.trend === 'down' && <TrendingDown size={14} className="text-accent-coral" />}
</div>
) : (
<span className="text-text-tertiary">-</span>
)}
</td>
<td className="px-6 py-4 text-sm text-text-tertiary">{creator.joinedAt}</td>
<td className="px-6 py-4">
<div className="relative">
<Button
variant="ghost"
size="sm"
onClick={() => setOpenMenuId(openMenuId === creator.id ? null : creator.id)}
>
<MoreVertical size={16} />
</Button>
{/* 下拉菜单 */}
{openMenuId === creator.id && (
<div className="absolute right-0 top-full mt-1 w-40 bg-bg-card rounded-xl shadow-lg border border-border-subtle z-10 overflow-hidden">
<button
type="button"
onClick={() => handleOpenRemark(creator)}
className="w-full px-4 py-2.5 text-left text-sm text-text-primary hover:bg-bg-elevated flex items-center gap-2"
>
<MessageSquareText size={14} className="text-text-secondary" />
{creator.remark ? '编辑备注' : '添加备注'}
</button>
<button
type="button"
onClick={() => handleOpenAssign(creator)}
className="w-full px-4 py-2.5 text-left text-sm text-text-primary hover:bg-bg-elevated flex items-center gap-2"
>
<FolderPlus size={14} className="text-text-secondary" />
</button>
<button
type="button"
onClick={() => handleOpenDelete(creator)}
className="w-full px-4 py-2.5 text-left text-sm text-accent-coral hover:bg-accent-coral/10 flex items-center gap-2"
>
<Trash2 size={14} />
</button>
</div>
)}
</div>
</td>
</tr>
{/* 展开的任务列表 */}
{isExpanded && hasActiveTasks && (
<tr key={`${creator.id}-tasks`} className="bg-bg-elevated/30">
<td colSpan={8} className="px-6 py-4">
<div className="ml-9 pl-6 border-l-2 border-accent-indigo/30">
<div className="text-sm font-medium text-text-secondary mb-3"></div>
<div className="space-y-2">
{creator.tasks.map(task => {
const taskPlatform = getPlatformInfo(task.platform)
return (
<div key={task.id} className="bg-bg-card rounded-xl overflow-hidden">
{/* 平台顶部条 */}
{taskPlatform && (
<div className={`px-4 py-1.5 ${taskPlatform.bgColor} border-b ${taskPlatform.borderColor} flex items-center gap-1.5`}>
<span className="text-sm">{taskPlatform.icon}</span>
<span className={`text-xs font-medium ${taskPlatform.textColor}`}>{taskPlatform.name}</span>
</div>
)}
<div className="flex items-center justify-between p-4">
<div className="flex items-center gap-4">
<div>
<div className="font-medium text-text-primary">{task.name}</div>
<div className="text-xs text-text-tertiary mt-0.5">: {task.projectName}</div>
</div>
<StageTag stage={task.stage} />
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-3 text-sm">
<span className="text-text-tertiary">:</span>
<span className="text-accent-indigo font-medium">{task.appealRemaining}</span>
<span className="text-text-tertiary">/</span>
<span className="text-text-tertiary"> {task.appealUsed}</span>
</div>
<Button
variant="secondary"
size="sm"
disabled={submitting}
onClick={() => handleAddAppealQuota(creator.id, task.id)}
>
{submitting ? (
<Loader2 size={14} className="animate-spin" />
) : (
<PlusCircle size={14} />
)}
+1
</Button>
</div>
</div>
</div>
)
})}
</div>
</div>
</td>
</tr>
)}
</>
)
})}
</tbody>
</table>
{filteredCreators.length === 0 && (
<div className="text-center py-12 text-text-tertiary">
<Users size={48} className="mx-auto mb-4 opacity-50" />
<p></p>
</div>
)}
</CardContent>
</Card>
{/* 邀请达人弹窗 */}
<Modal isOpen={showInviteModal} onClose={handleCloseInviteModal} title="邀请达人">
<div className="space-y-4">
<p className="text-text-secondary text-sm">
ID邀请合作ID可在达人的个人中心查看
</p>
<div>
<label className="block text-sm font-medium text-text-primary mb-2">ID</label>
<div className="flex gap-2">
<input
type="text"
value={inviteCreatorId}
onChange={(e) => {
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"
/>
<Button variant="secondary" onClick={handleInvite} disabled={submitting}>
{submitting ? <Loader2 size={16} className="animate-spin" /> : null}
</Button>
</div>
<p className="text-xs text-text-tertiary mt-2">ID格式CR + 6</p>
</div>
{inviteResult && (
<div className={`p-4 rounded-xl flex items-start gap-3 ${
inviteResult.success ? 'bg-accent-green/10 border border-accent-green/20' : 'bg-accent-coral/10 border border-accent-coral/20'
}`}>
{inviteResult.success ? (
<CheckCircle size={18} className="text-accent-green flex-shrink-0 mt-0.5" />
) : (
<AlertCircle size={18} className="text-accent-coral flex-shrink-0 mt-0.5" />
)}
<span className={`text-sm ${inviteResult.success ? 'text-accent-green' : 'text-accent-coral'}`}>
{inviteResult.message}
</span>
</div>
)}
<div className="flex gap-3 justify-end pt-4">
<Button variant="ghost" onClick={handleCloseInviteModal}>
</Button>
<Button
onClick={() => {
if (inviteResult?.success) {
handleCloseInviteModal()
if (!USE_MOCK) {
fetchData() // 刷新达人列表
}
}
}}
disabled={!inviteResult?.success}
>
<UserPlus size={16} />
</Button>
</div>
</div>
</Modal>
{/* 备注弹窗 */}
<Modal
isOpen={remarkModal.open}
onClose={() => { setRemarkModal({ open: false, creator: null }); setRemarkText(''); }}
title={`${remarkModal.creator?.remark ? '编辑' : '添加'}备注 - ${remarkModal.creator?.name}`}
>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-2"></label>
<textarea
value={remarkText}
onChange={(e) => setRemarkText(e.target.value)}
placeholder="输入备注信息,如达人特点、合作注意事项等..."
className="w-full h-32 px-4 py-3 border border-border-subtle rounded-xl bg-bg-elevated text-text-primary placeholder-text-tertiary resize-none focus:outline-none focus:ring-2 focus:ring-accent-indigo"
/>
</div>
<div className="flex gap-3 justify-end">
<Button variant="ghost" onClick={() => { setRemarkModal({ open: false, creator: null }); setRemarkText(''); }}>
</Button>
<Button onClick={handleSaveRemark}>
<CheckCircle size={16} />
</Button>
</div>
</div>
</Modal>
{/* 删除确认弹窗 */}
<Modal
isOpen={deleteModal.open}
onClose={() => setDeleteModal({ open: false, creator: null })}
title="确认移除达人"
>
<div className="space-y-4">
<div className="p-4 rounded-xl bg-accent-coral/10 border border-accent-coral/20">
<div className="flex items-start gap-3">
<AlertCircle size={20} className="text-accent-coral flex-shrink-0 mt-0.5" />
<div>
<p className="text-text-primary font-medium">{deleteModal.creator?.name}</p>
<p className="text-sm text-text-secondary mt-1">
</p>
</div>
</div>
</div>
<div className="flex gap-3 justify-end">
<Button variant="ghost" onClick={() => setDeleteModal({ open: false, creator: null })}>
</Button>
<Button
variant="secondary"
className="border-accent-coral text-accent-coral hover:bg-accent-coral/10"
onClick={handleConfirmDelete}
disabled={submitting}
>
{submitting ? <Loader2 size={16} className="animate-spin" /> : <Trash2 size={16} />}
</Button>
</div>
</div>
</Modal>
{/* 分配项目弹窗 */}
<Modal
isOpen={assignModal.open}
onClose={() => { setAssignModal({ open: false, creator: null }); setSelectedProject(''); }}
title={`分配达人到项目 - ${assignModal.creator?.name}`}
>
<div className="space-y-4">
<p className="text-text-secondary text-sm">
</p>
<div>
<label className="block text-sm font-medium text-text-primary mb-2"></label>
<div className="space-y-2">
{projectList.map((project) => (
<label
key={project.id}
className={`flex items-center gap-3 p-4 rounded-xl border cursor-pointer transition-colors ${
selectedProject === project.id
? 'border-accent-indigo bg-accent-indigo/10'
: 'border-border-subtle hover:border-accent-indigo/50'
}`}
>
<input
type="radio"
name="project"
value={project.id}
checked={selectedProject === project.id}
onChange={(e) => setSelectedProject(e.target.value)}
className="w-4 h-4 text-accent-indigo"
/>
<span className="text-text-primary">{project.name}</span>
</label>
))}
{projectList.length === 0 && (
<p className="text-text-tertiary text-sm text-center py-4"></p>
)}
</div>
</div>
<div className="flex gap-3 justify-end pt-2">
<Button variant="ghost" onClick={() => { setAssignModal({ open: false, creator: null }); setSelectedProject(''); }}>
</Button>
<Button onClick={handleConfirmAssign} disabled={!selectedProject}>
<FolderPlus size={16} />
</Button>
</div>
</div>
</Modal>
{/* 点击其他地方关闭菜单 */}
{openMenuId && (
<div
className="fixed inset-0 z-0"
onClick={() => setOpenMenuId(null)}
/>
)}
</div>
)
}