Your Name 9a0e2cac03 feat(agency): 达人管理添加任务进度展开提示
- 达人名称下方添加可点击的提示文字
- 未展开时显示"查看 N 个任务进度"
- 展开后显示"收起任务进度"
- 提示用户可以点击查看任务详情

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 16:31:11 +08:00

548 lines
22 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 } from 'react'
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
} from 'lucide-react'
// 任务进度阶段
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' },
}
// 任务类型
interface CreatorTask {
id: string
name: string
projectName: 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[]
}
// 模拟达人列表
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', stage: 'video_agency_review', appealRemaining: 1, appealUsed: 0 },
{ id: 'task-002', name: '防晒霜测评', projectName: 'XX品牌618', 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: '口红系列推广', 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运动品牌', 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 [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(mockCreators)
const [copiedId, setCopiedId] = useState<string | null>(null)
const filteredCreators = creators.filter(creator =>
creator.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
creator.creatorId.toLowerCase().includes(searchQuery.toLowerCase())
)
// 切换展开状态
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 = (creatorId: string, taskId: string) => {
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
}))
}
// 邀请达人
const handleInvite = () => {
if (!inviteCreatorId.trim()) {
setInviteResult({ success: false, message: '请输入达人ID' })
return
}
// 模拟检查达人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()} 发送邀请` })
}
const handleCloseInviteModal = () => {
setShowInviteModal(false)
setInviteCreatorId('')
setInviteResult(null)
}
return (
<div className="space-y-6">
{/* 页面标题 */}
<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">{mockCreators.length}</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">{mockCreators.filter(c => c.status === 'active').length}</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">{mockCreators.reduce((sum, c) => sum + c.scriptCount.total, 0)}</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">{mockCreators.reduce((sum, c) => sum + c.videoCount.total, 0)}</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">
<table className="w-full">
<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="font-medium text-text-primary">{creator.name}</div>
{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">
<StatusTag status={creator.status} />
</td>
<td className="px-6 py-4">
<span className="text-text-primary">{creator.scriptCount.passed}</span>
<span className="text-text-tertiary">/{creator.scriptCount.total}</span>
</td>
<td className="px-6 py-4">
<span className="text-text-primary">{creator.videoCount.passed}</span>
<span className="text-text-tertiary">/{creator.videoCount.total}</span>
</td>
<td className="px-6 py-4">
{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">
<Button variant="ghost" size="sm">
<MoreVertical size={16} />
</Button>
</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 => (
<div key={task.id} className="flex items-center justify-between p-4 bg-bg-card rounded-xl">
<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"
onClick={() => handleAddAppealQuota(creator.id, task.id)}
>
<PlusCircle size={14} />
+1
</Button>
</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}>
</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()
}
}}
disabled={!inviteResult?.success}
>
<UserPlus size={16} />
</Button>
</div>
</div>
</Modal>
</div>
)
}