feat(agency): 优化达人管理页面
- 邀请达人改为使用达人ID(CR+6位数字格式) - 达人列表新增达人ID列,支持复制 - 每个达人的任务显示当前进度阶段: - 脚本阶段:待提交/AI审核/代理商审核/品牌方终审 - 视频阶段:待提交/AI审核/代理商审核/品牌方终审/已完成 - 展开任务可查看申诉次数并增加配额 - 搜索支持按达人名称或达人ID搜索 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
66748d4f19
commit
ad45da5286
@ -11,7 +11,6 @@ import {
|
||||
Users,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Mail,
|
||||
Copy,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
@ -20,30 +19,50 @@ import {
|
||||
Video,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
PlusCircle
|
||||
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
|
||||
status: 'in_progress' | 'completed'
|
||||
}
|
||||
|
||||
// 达人类型
|
||||
interface Creator {
|
||||
id: string
|
||||
creatorId: string // 达人ID(用于邀请和显示)
|
||||
name: string
|
||||
email: string
|
||||
avatar: null
|
||||
status: string
|
||||
avatar: string
|
||||
status: 'active' | 'pending' | 'paused'
|
||||
projectCount: number
|
||||
scriptCount: { total: number; passed: number }
|
||||
videoCount: { total: number; passed: number }
|
||||
passRate: number
|
||||
trend: string
|
||||
trend: 'up' | 'down' | 'stable'
|
||||
joinedAt: string
|
||||
tasks: CreatorTask[]
|
||||
}
|
||||
@ -51,10 +70,10 @@ interface Creator {
|
||||
// 模拟达人列表
|
||||
const mockCreators: Creator[] = [
|
||||
{
|
||||
id: 'creator-001',
|
||||
id: 'c-001',
|
||||
creatorId: 'CR123456',
|
||||
name: '小美护肤',
|
||||
email: 'xiaomei@example.com',
|
||||
avatar: null,
|
||||
avatar: '小',
|
||||
status: 'active',
|
||||
projectCount: 5,
|
||||
scriptCount: { total: 12, passed: 10 },
|
||||
@ -63,15 +82,15 @@ const mockCreators: Creator[] = [
|
||||
trend: 'up',
|
||||
joinedAt: '2025-08-15',
|
||||
tasks: [
|
||||
{ id: 'task-001', name: '夏日护肤推广', appealRemaining: 1, appealUsed: 0, status: 'in_progress' },
|
||||
{ id: 'task-002', name: '防晒霜测评', appealRemaining: 0, appealUsed: 1, status: 'in_progress' },
|
||||
{ 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: 'creator-002',
|
||||
id: 'c-002',
|
||||
creatorId: 'CR789012',
|
||||
name: '美妆Lisa',
|
||||
email: 'lisa@example.com',
|
||||
avatar: null,
|
||||
avatar: 'L',
|
||||
status: 'active',
|
||||
projectCount: 3,
|
||||
scriptCount: { total: 8, passed: 7 },
|
||||
@ -80,14 +99,14 @@ const mockCreators: Creator[] = [
|
||||
trend: 'stable',
|
||||
joinedAt: '2025-10-20',
|
||||
tasks: [
|
||||
{ id: 'task-003', name: '新品口红试色', appealRemaining: 2, appealUsed: 0, status: 'in_progress' },
|
||||
{ id: 'task-003', name: '新品口红试色', projectName: '口红系列推广', stage: 'video_pending', appealRemaining: 2, appealUsed: 0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'creator-003',
|
||||
id: 'c-003',
|
||||
creatorId: 'CR345678',
|
||||
name: '健身教练王',
|
||||
email: 'wang@example.com',
|
||||
avatar: null,
|
||||
avatar: '王',
|
||||
status: 'active',
|
||||
projectCount: 2,
|
||||
scriptCount: { total: 5, passed: 5 },
|
||||
@ -96,14 +115,14 @@ const mockCreators: Creator[] = [
|
||||
trend: 'up',
|
||||
joinedAt: '2025-12-01',
|
||||
tasks: [
|
||||
{ id: 'task-004', name: '健身器材使用教程', appealRemaining: 1, appealUsed: 0, status: 'in_progress' },
|
||||
{ id: 'task-004', name: '健身器材使用教程', projectName: 'XX运动品牌', stage: 'script_ai_review', appealRemaining: 1, appealUsed: 0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'creator-004',
|
||||
id: 'c-004',
|
||||
creatorId: 'CR901234',
|
||||
name: '时尚达人',
|
||||
email: 'fashion@example.com',
|
||||
avatar: null,
|
||||
avatar: '时',
|
||||
status: 'pending',
|
||||
projectCount: 0,
|
||||
scriptCount: { total: 0, passed: 0 },
|
||||
@ -121,17 +140,27 @@ function StatusTag({ status }: { status: string }) {
|
||||
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 [inviteEmail, setInviteEmail] = useState('')
|
||||
const [inviteLink, setInviteLink] = useState('')
|
||||
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.email.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
creator.creatorId.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
|
||||
// 切换展开状态
|
||||
@ -143,6 +172,13 @@ export default function AgencyCreatorsPage() {
|
||||
)
|
||||
}
|
||||
|
||||
// 复制达人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 => {
|
||||
@ -161,25 +197,34 @@ export default function AgencyCreatorsPage() {
|
||||
}))
|
||||
}
|
||||
|
||||
// 邀请达人
|
||||
const handleInvite = () => {
|
||||
if (!inviteEmail.trim()) {
|
||||
alert('请输入达人邮箱')
|
||||
if (!inviteCreatorId.trim()) {
|
||||
setInviteResult({ success: false, message: '请输入达人ID' })
|
||||
return
|
||||
}
|
||||
const link = `https://miaosi.app/invite/creator/${Date.now()}`
|
||||
setInviteLink(link)
|
||||
|
||||
// 模拟检查达人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 handleCopyLink = () => {
|
||||
navigator.clipboard.writeText(inviteLink)
|
||||
alert('链接已复制')
|
||||
}
|
||||
|
||||
const handleSendInvite = () => {
|
||||
alert(`邀请已发送至 ${inviteEmail}`)
|
||||
const handleCloseInviteModal = () => {
|
||||
setShowInviteModal(false)
|
||||
setInviteEmail('')
|
||||
setInviteLink('')
|
||||
setInviteCreatorId('')
|
||||
setInviteResult(null)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -188,7 +233,7 @@ export default function AgencyCreatorsPage() {
|
||||
<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>
|
||||
<p className="text-sm text-text-secondary mt-1">管理合作达人,查看任务进度和申诉次数</p>
|
||||
</div>
|
||||
<Button onClick={() => setShowInviteModal(true)}>
|
||||
<Plus size={16} />
|
||||
@ -257,10 +302,10 @@ export default function AgencyCreatorsPage() {
|
||||
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-tertiary" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索达人名称或邮箱..."
|
||||
placeholder="搜索达人名称或达人ID..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||||
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>
|
||||
|
||||
@ -271,8 +316,8 @@ export default function AgencyCreatorsPage() {
|
||||
<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>
|
||||
@ -283,7 +328,7 @@ export default function AgencyCreatorsPage() {
|
||||
<tbody>
|
||||
{filteredCreators.map((creator) => {
|
||||
const isExpanded = expandedCreators.includes(creator.id)
|
||||
const hasActiveTasks = creator.tasks.filter(t => t.status === 'in_progress').length > 0
|
||||
const hasActiveTasks = creator.tasks.length > 0
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -306,19 +351,34 @@ export default function AgencyCreatorsPage() {
|
||||
) : (
|
||||
<div className="w-6" />
|
||||
)}
|
||||
<div className="w-10 h-10 rounded-full bg-accent-indigo/20 flex items-center justify-center">
|
||||
<span className="text-accent-indigo font-medium">{creator.name[0]}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-text-primary">{creator.name}</div>
|
||||
<div className="text-sm text-text-tertiary">{creator.email}</div>
|
||||
<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 className="font-medium text-text-primary">{creator.name}</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 text-text-primary">{creator.projectCount}</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>
|
||||
@ -347,31 +407,38 @@ export default function AgencyCreatorsPage() {
|
||||
</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="text-sm font-medium text-text-secondary mb-3">进行中的任务</div>
|
||||
<div className="space-y-2">
|
||||
{creator.tasks.filter(t => t.status === 'in_progress').map(task => (
|
||||
<div key={task.id} className="flex items-center justify-between p-3 bg-bg-card rounded-lg">
|
||||
{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">
|
||||
<span className="text-sm font-medium text-text-primary">{task.name}</span>
|
||||
<div className="flex items-center gap-2 text-xs text-text-tertiary">
|
||||
<span>剩余: <span className="text-accent-indigo font-medium">{task.appealRemaining}</span></span>
|
||||
<span>|</span>
|
||||
<span>已用: {task.appealUsed}</span>
|
||||
<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>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => handleAddAppealQuota(creator.id, task.id)}
|
||||
>
|
||||
<PlusCircle size={14} />
|
||||
+1 次
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -384,53 +451,71 @@ export default function AgencyCreatorsPage() {
|
||||
})}
|
||||
</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={() => { setShowInviteModal(false); setInviteEmail(''); setInviteLink(''); }} title="邀请达人">
|
||||
<Modal isOpen={showInviteModal} onClose={handleCloseInviteModal} title="邀请达人">
|
||||
<div className="space-y-4">
|
||||
<p className="text-text-secondary text-sm">输入达人邮箱,系统将发送邀请链接。</p>
|
||||
<p className="text-text-secondary text-sm">
|
||||
输入达人ID邀请合作。达人ID可在达人的个人中心查看。
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">达人邮箱</label>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">达人ID</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="email"
|
||||
value={inviteEmail}
|
||||
onChange={(e) => setInviteEmail(e.target.value)}
|
||||
placeholder="creator@example.com"
|
||||
className="flex-1 px-4 py-2 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||||
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>
|
||||
|
||||
{inviteLink && (
|
||||
<div className="p-4 bg-bg-elevated rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-text-primary">邀请链接</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopyLink}
|
||||
className="flex items-center gap-1 text-sm text-accent-indigo hover:underline"
|
||||
>
|
||||
<Copy size={14} />
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary break-all">{inviteLink}</p>
|
||||
{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={() => { setShowInviteModal(false); setInviteEmail(''); setInviteLink(''); }}>
|
||||
<Button variant="ghost" onClick={handleCloseInviteModal}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSendInvite} disabled={!inviteEmail.trim()}>
|
||||
<Mail size={16} />
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (inviteResult?.success) {
|
||||
handleCloseInviteModal()
|
||||
}
|
||||
}}
|
||||
disabled={!inviteResult?.success}
|
||||
>
|
||||
<UserPlus size={16} />
|
||||
发送邀请
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user