'use client'
import { useState, useEffect, useCallback, useRef } from 'react'
import { Card, CardContent } from '@/components/ui/Card'
import { Button } from '@/components/ui/Button'
import { Modal } from '@/components/ui/Modal'
import { SuccessTag, PendingTag } from '@/components/ui/Tag'
import { useToast } from '@/components/ui/Toast'
import {
Search,
Plus,
Users,
Copy,
CheckCircle,
MoreVertical,
Building2,
AlertCircle,
UserPlus,
Trash2,
FolderPlus,
Loader2,
} from 'lucide-react'
import { api } from '@/lib/api'
import { USE_MOCK } from '@/contexts/AuthContext'
import type { AgencyDetail } from '@/types/organization'
import type { ProjectResponse } from '@/types/project'
// ==================== Mock 数据 ====================
const mockAgencies: AgencyDetail[] = [
{ id: 'AG789012', name: '星耀传媒', contact_name: '张经理', force_pass_enabled: true },
{ id: 'AG456789', name: '创意无限', contact_name: '李总', force_pass_enabled: false },
{ id: 'AG123456', name: '美妆达人MCN', contact_name: '王经理', force_pass_enabled: false },
{ id: 'AG111111', name: '蓝海科技', force_pass_enabled: true },
]
const mockProjects: ProjectResponse[] = [
{ id: 'PJ000001', name: 'XX品牌618推广', brand_id: 'BR000001', status: 'active', agencies: [], task_count: 5, created_at: '2025-06-01', updated_at: '2025-06-01' },
{ id: 'PJ000002', name: '口红系列推广', brand_id: 'BR000001', status: 'active', agencies: [], task_count: 3, created_at: '2025-07-01', updated_at: '2025-07-01' },
]
function StatusTag({ forcePass }: { forcePass: boolean }) {
if (forcePass) return 可强制通过
return 标准权限
}
function AgencySkeleton() {
return (
)
}
export default function AgenciesManagePage() {
const toast = useToast()
const [searchQuery, setSearchQuery] = useState('')
const [agencies, setAgencies] = useState([])
const [projects, setProjects] = useState([])
const [loading, setLoading] = useState(true)
const [copiedId, setCopiedId] = useState(null)
// 邀请代理商弹窗
const [showInviteModal, setShowInviteModal] = useState(false)
const [inviteAgencyId, setInviteAgencyId] = useState('')
const [inviting, setInviting] = useState(false)
const [inviteResult, setInviteResult] = useState<{ success: boolean; message: string } | null>(null)
// 操作菜单状态
const [openMenuId, setOpenMenuId] = useState(null)
const [menuPos, setMenuPos] = useState<{ top: number; left: number }>({ top: 0, left: 0 })
const menuRef = useRef(null)
const handleToggleMenu = (agencyId: string, e: React.MouseEvent) => {
if (openMenuId === agencyId) {
setOpenMenuId(null)
return
}
const rect = e.currentTarget.getBoundingClientRect()
setMenuPos({ top: rect.bottom + 4, left: rect.right - 160 }) // 160 = menu width
setOpenMenuId(agencyId)
}
// 点击外部关闭菜单
useEffect(() => {
if (!openMenuId) return
const handleClickOutside = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setOpenMenuId(null)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [openMenuId])
// 删除确认弹窗状态
const [deleteModal, setDeleteModal] = useState<{ open: boolean; agency: AgencyDetail | null }>({ open: false, agency: null })
const [deleting, setDeleting] = useState(false)
// 分配项目弹窗状态
const [assignModal, setAssignModal] = useState<{ open: boolean; agency: AgencyDetail | null }>({ open: false, agency: null })
const [selectedProjects, setSelectedProjects] = useState([])
const [assigning, setAssigning] = useState(false)
const loadData = useCallback(async () => {
if (USE_MOCK) {
setAgencies(mockAgencies)
setProjects(mockProjects)
setLoading(false)
return
}
try {
const [agencyRes, projectRes] = await Promise.all([
api.listBrandAgencies(),
api.listProjects(1, 100),
])
setAgencies(agencyRes.items)
setProjects(projectRes.items)
} catch (err) {
console.error('Failed to load data:', err)
toast.error('加载数据失败')
} finally {
setLoading(false)
}
}, [toast])
useEffect(() => { loadData() }, [loadData])
const filteredAgencies = agencies.filter(agency =>
agency.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
agency.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
(agency.contact_name || '').toLowerCase().includes(searchQuery.toLowerCase())
)
// 复制代理商ID
const handleCopyAgencyId = async (agencyId: string) => {
await navigator.clipboard.writeText(agencyId)
setCopiedId(agencyId)
setTimeout(() => setCopiedId(null), 2000)
}
// 邀请代理商
const handleInvite = async () => {
if (!inviteAgencyId.trim()) {
setInviteResult({ success: false, message: '请输入代理商ID' })
return
}
const idPattern = /^AG\d{6}$/
if (!idPattern.test(inviteAgencyId.toUpperCase())) {
setInviteResult({ success: false, message: '代理商ID格式错误,应为AG+6位数字' })
return
}
if (agencies.some(a => a.id === inviteAgencyId.toUpperCase())) {
setInviteResult({ success: false, message: '该代理商已在您的列表中' })
return
}
setInviting(true)
try {
if (USE_MOCK) {
await new Promise(resolve => setTimeout(resolve, 500))
} else {
await api.inviteAgency(inviteAgencyId.toUpperCase())
}
setInviteResult({ success: true, message: `已向代理商 ${inviteAgencyId.toUpperCase()} 发送邀请` })
} catch (err) {
setInviteResult({ success: false, message: err instanceof Error ? err.message : '邀请失败' })
} finally {
setInviting(false)
}
}
const handleCloseInviteModal = () => {
setShowInviteModal(false)
setInviteAgencyId('')
setInviteResult(null)
}
const handleConfirmInvite = async () => {
if (inviteResult?.success) {
handleCloseInviteModal()
await loadData()
}
}
// 打开删除确认
const handleOpenDelete = (agency: AgencyDetail) => {
setDeleteModal({ open: true, agency })
setOpenMenuId(null)
}
// 确认删除
const handleConfirmDelete = async () => {
if (!deleteModal.agency) return
setDeleting(true)
try {
if (USE_MOCK) {
await new Promise(resolve => setTimeout(resolve, 500))
setAgencies(prev => prev.filter(a => a.id !== deleteModal.agency!.id))
} else {
await api.removeAgency(deleteModal.agency.id)
await loadData()
}
toast.success('已移除代理商')
} catch (err) {
toast.error('移除失败')
} finally {
setDeleting(false)
setDeleteModal({ open: false, agency: null })
}
}
// 打开分配项目弹窗
const handleOpenAssign = (agency: AgencyDetail) => {
setSelectedProjects([])
setAssignModal({ open: true, agency })
setOpenMenuId(null)
}
// 切换项目选择
const toggleProjectSelection = (projectId: string) => {
setSelectedProjects(prev =>
prev.includes(projectId) ? prev.filter(id => id !== projectId) : [...prev, projectId]
)
}
// 确认分配项目
const handleConfirmAssign = async () => {
if (!assignModal.agency || selectedProjects.length === 0) return
setAssigning(true)
try {
if (USE_MOCK) {
await new Promise(resolve => setTimeout(resolve, 500))
} else {
for (const projectId of selectedProjects) {
await api.assignAgencies(projectId, [assignModal.agency.id])
}
}
const projectNames = projects
.filter(p => selectedProjects.includes(p.id))
.map(p => p.name)
.join('、')
toast.success(`已将代理商「${assignModal.agency.name}」分配到项目「${projectNames}」`)
} catch (err) {
toast.error('分配失败')
} finally {
setAssigning(false)
setAssignModal({ open: false, agency: null })
setSelectedProjects([])
}
}
return (
{/* 页面标题 */}
{/* 统计卡片 */}
可强制通过
{agencies.filter(a => a.force_pass_enabled).length}
{/* 搜索 */}
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"
/>
{/* 代理商列表 */}
{loading ? (
) : (
| 代理商 |
代理商ID |
联系人 |
权限 |
操作 |
{filteredAgencies.map((agency) => (
|
|
{agency.id}
|
{agency.contact_name || '-'}
|
|
|
))}
)}
{!loading && filteredAgencies.length === 0 && (
)}
{/* 操作菜单(fixed 定位,不受 overflow 裁剪) */}
{openMenuId && (
)}
{/* 邀请代理商弹窗 */}
输入代理商ID邀请合作。代理商ID可在代理商的个人中心查看。
{inviteResult && (
{inviteResult.success ? (
) : (
)}
{inviteResult.message}
)}
{/* 删除确认弹窗 */}
setDeleteModal({ open: false, agency: null })}
title="确认移除代理商"
>
确定要移除代理商「{deleteModal.agency?.name}」吗?
移除后该代理商将无法继续参与您的项目,该代理商下的达人也将受到影响。
{/* 分配项目弹窗 */}
{ setAssignModal({ open: false, agency: null }); setSelectedProjects([]); }}
title={`分配代理商到项目 - ${assignModal.agency?.name}`}
>
选择要将代理商分配到的项目,可多选。
{projects.map((project) => {
const isSelected = selectedProjects.includes(project.id)
return (
)
})}
{selectedProjects.length > 0 && (
已选择 {selectedProjects.length} 个项目
)}
)
}