feat(agency): 达人管理添加操作菜单功能

操作下拉菜单包含:
- 添加/编辑备注:可为达人添加备注信息,列表中显示备注标签和内容
- 分配到项目:选择项目将达人分配进去
- 移除达人:确认后移除该达人(保留历史数据)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-02-06 16:34:53 +08:00
parent 9a0e2cac03
commit 9f15709eed

View File

@ -21,7 +21,11 @@ import {
ChevronRight,
PlusCircle,
UserPlus,
AlertCircle
AlertCircle,
MessageSquareText,
Trash2,
FolderPlus,
X
} from 'lucide-react'
// 任务进度阶段
@ -65,8 +69,17 @@ interface Creator {
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[] = [
{
@ -158,6 +171,20 @@ export default function AgencyCreatorsPage() {
const [creators, setCreators] = useState(mockCreators)
const [copiedId, setCopiedId] = useState<string | null>(null)
// 操作菜单状态
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('')
const filteredCreators = creators.filter(creator =>
creator.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
creator.creatorId.toLowerCase().includes(searchQuery.toLowerCase())
@ -227,6 +254,55 @@ export default function AgencyCreatorsPage() {
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 = () => {
if (deleteModal.creator) {
setCreators(prev => prev.filter(c => c.id !== deleteModal.creator!.id))
}
setDeleteModal({ open: false, creator: null })
}
// 打开分配项目弹窗
const handleOpenAssign = (creator: Creator) => {
setSelectedProject('')
setAssignModal({ open: true, creator })
setOpenMenuId(null)
}
// 确认分配项目
const handleConfirmAssign = () => {
if (assignModal.creator && selectedProject) {
const project = mockProjects.find(p => p.id === selectedProject)
alert(`已将达人「${assignModal.creator.name}」分配到项目「${project?.name}`)
}
setAssignModal({ open: false, creator: null })
setSelectedProject('')
}
return (
<div className="space-y-6">
{/* 页面标题 */}
@ -355,7 +431,17 @@ export default function AgencyCreatorsPage() {
<span className="text-white font-medium">{creator.avatar}</span>
</div>
<div>
<div className="font-medium text-text-primary">{creator.name}</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"
@ -423,9 +509,44 @@ export default function AgencyCreatorsPage() {
</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>
<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>
{/* 展开的任务列表 */}
@ -542,6 +663,123 @@ export default function AgencyCreatorsPage() {
</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}
>
<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">
{mockProjects.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>
))}
</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>
)
}