- 创建 Toast 通知组件,替换所有 alert() 调用 - 修复 useReview hook 内存泄漏(setInterval 清理) - 移除所有 console.error 和 console.log 语句 - 为复制操作失败添加用户友好的 toast 提示 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
647 lines
25 KiB
TypeScript
647 lines
25 KiB
TypeScript
'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 { useToast } from '@/components/ui/Toast'
|
||
import {
|
||
Search,
|
||
Plus,
|
||
Users,
|
||
TrendingUp,
|
||
TrendingDown,
|
||
Copy,
|
||
CheckCircle,
|
||
Clock,
|
||
MoreVertical,
|
||
Building2,
|
||
AlertCircle,
|
||
UserPlus,
|
||
MessageSquareText,
|
||
Trash2,
|
||
FolderPlus
|
||
} from 'lucide-react'
|
||
|
||
// 代理商类型
|
||
interface Agency {
|
||
id: string
|
||
agencyId: string // 代理商ID(AG开头)
|
||
name: string
|
||
companyName: string
|
||
email: string
|
||
status: 'active' | 'pending' | 'paused'
|
||
creatorCount: number
|
||
projectCount: number
|
||
passRate: number
|
||
trend: 'up' | 'down' | 'stable'
|
||
joinedAt: string
|
||
remark?: string
|
||
}
|
||
|
||
// 模拟项目列表(用于分配代理商)
|
||
const mockProjects = [
|
||
{ id: 'proj-001', name: 'XX品牌618推广' },
|
||
{ id: 'proj-002', name: '口红系列推广' },
|
||
{ id: 'proj-003', name: 'XX运动品牌' },
|
||
{ id: 'proj-004', name: '护肤品秋季活动' },
|
||
]
|
||
|
||
// 模拟代理商列表
|
||
const initialAgencies: Agency[] = [
|
||
{
|
||
id: 'a-001',
|
||
agencyId: 'AG789012',
|
||
name: '星耀传媒',
|
||
companyName: '上海星耀文化传媒有限公司',
|
||
email: 'contact@xingyao.com',
|
||
status: 'active',
|
||
creatorCount: 50,
|
||
projectCount: 8,
|
||
passRate: 92,
|
||
trend: 'up',
|
||
joinedAt: '2025-06-15',
|
||
},
|
||
{
|
||
id: 'a-002',
|
||
agencyId: 'AG456789',
|
||
name: '创意无限',
|
||
companyName: '深圳创意无限广告有限公司',
|
||
email: 'hello@chuangyi.com',
|
||
status: 'active',
|
||
creatorCount: 35,
|
||
projectCount: 5,
|
||
passRate: 88,
|
||
trend: 'up',
|
||
joinedAt: '2025-08-20',
|
||
},
|
||
{
|
||
id: 'a-003',
|
||
agencyId: 'AG123456',
|
||
name: '美妆达人MCN',
|
||
companyName: '杭州美妆达人网络科技有限公司',
|
||
email: 'biz@meizhuang.com',
|
||
status: 'active',
|
||
creatorCount: 28,
|
||
projectCount: 4,
|
||
passRate: 75,
|
||
trend: 'down',
|
||
joinedAt: '2025-10-10',
|
||
},
|
||
{
|
||
id: 'a-004',
|
||
agencyId: 'AG111111',
|
||
name: '蓝海科技',
|
||
companyName: '北京蓝海数字科技有限公司',
|
||
email: 'info@lanhai.com',
|
||
status: 'pending',
|
||
creatorCount: 0,
|
||
projectCount: 0,
|
||
passRate: 0,
|
||
trend: 'stable',
|
||
joinedAt: '2026-02-01',
|
||
},
|
||
]
|
||
|
||
function StatusTag({ status }: { status: string }) {
|
||
if (status === 'active') return <SuccessTag>已激活</SuccessTag>
|
||
if (status === 'pending') return <PendingTag>待接受</PendingTag>
|
||
return <WarningTag>已暂停</WarningTag>
|
||
}
|
||
|
||
export default function AgenciesManagePage() {
|
||
const toast = useToast()
|
||
const [searchQuery, setSearchQuery] = useState('')
|
||
const [agencies, setAgencies] = useState<Agency[]>(initialAgencies)
|
||
const [copiedId, setCopiedId] = useState<string | null>(null)
|
||
|
||
// 邀请代理商弹窗
|
||
const [showInviteModal, setShowInviteModal] = useState(false)
|
||
const [inviteAgencyId, setInviteAgencyId] = useState('')
|
||
const [inviteResult, setInviteResult] = useState<{ success: boolean; message: string } | null>(null)
|
||
|
||
// 操作菜单状态
|
||
const [openMenuId, setOpenMenuId] = useState<string | null>(null)
|
||
|
||
// 备注弹窗状态
|
||
const [remarkModal, setRemarkModal] = useState<{ open: boolean; agency: Agency | null }>({ open: false, agency: null })
|
||
const [remarkText, setRemarkText] = useState('')
|
||
|
||
// 删除确认弹窗状态
|
||
const [deleteModal, setDeleteModal] = useState<{ open: boolean; agency: Agency | null }>({ open: false, agency: null })
|
||
|
||
// 分配项目弹窗状态
|
||
const [assignModal, setAssignModal] = useState<{ open: boolean; agency: Agency | null }>({ open: false, agency: null })
|
||
const [selectedProjects, setSelectedProjects] = useState<string[]>([])
|
||
|
||
const filteredAgencies = agencies.filter(agency =>
|
||
agency.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
agency.agencyId.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
agency.companyName.toLowerCase().includes(searchQuery.toLowerCase())
|
||
)
|
||
|
||
// 复制代理商ID
|
||
const handleCopyAgencyId = async (agencyId: string) => {
|
||
await navigator.clipboard.writeText(agencyId)
|
||
setCopiedId(agencyId)
|
||
setTimeout(() => setCopiedId(null), 2000)
|
||
}
|
||
|
||
// 邀请代理商
|
||
const handleInvite = () => {
|
||
if (!inviteAgencyId.trim()) {
|
||
setInviteResult({ success: false, message: '请输入代理商ID' })
|
||
return
|
||
}
|
||
|
||
// 检查代理商ID格式
|
||
const idPattern = /^AG\d{6}$/
|
||
if (!idPattern.test(inviteAgencyId.toUpperCase())) {
|
||
setInviteResult({ success: false, message: '代理商ID格式错误,应为AG+6位数字' })
|
||
return
|
||
}
|
||
|
||
// 检查是否已邀请
|
||
if (agencies.some(a => a.agencyId === inviteAgencyId.toUpperCase())) {
|
||
setInviteResult({ success: false, message: '该代理商已在您的列表中' })
|
||
return
|
||
}
|
||
|
||
// 模拟发送邀请成功
|
||
setInviteResult({ success: true, message: `已向代理商 ${inviteAgencyId.toUpperCase()} 发送邀请` })
|
||
}
|
||
|
||
const handleCloseInviteModal = () => {
|
||
setShowInviteModal(false)
|
||
setInviteAgencyId('')
|
||
setInviteResult(null)
|
||
}
|
||
|
||
// 打开备注弹窗
|
||
const handleOpenRemark = (agency: Agency) => {
|
||
setRemarkText(agency.remark || '')
|
||
setRemarkModal({ open: true, agency })
|
||
setOpenMenuId(null)
|
||
}
|
||
|
||
// 保存备注
|
||
const handleSaveRemark = () => {
|
||
if (remarkModal.agency) {
|
||
setAgencies(prev => prev.map(a =>
|
||
a.id === remarkModal.agency!.id ? { ...a, remark: remarkText } : a
|
||
))
|
||
}
|
||
setRemarkModal({ open: false, agency: null })
|
||
setRemarkText('')
|
||
}
|
||
|
||
// 打开删除确认
|
||
const handleOpenDelete = (agency: Agency) => {
|
||
setDeleteModal({ open: true, agency })
|
||
setOpenMenuId(null)
|
||
}
|
||
|
||
// 确认删除
|
||
const handleConfirmDelete = () => {
|
||
if (deleteModal.agency) {
|
||
setAgencies(prev => prev.filter(a => a.id !== deleteModal.agency!.id))
|
||
}
|
||
setDeleteModal({ open: false, agency: null })
|
||
}
|
||
|
||
// 打开分配项目弹窗
|
||
const handleOpenAssign = (agency: Agency) => {
|
||
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 = () => {
|
||
if (assignModal.agency && selectedProjects.length > 0) {
|
||
const projectNames = mockProjects
|
||
.filter(p => selectedProjects.includes(p.id))
|
||
.map(p => p.name)
|
||
.join('、')
|
||
toast.success(`已将代理商「${assignModal.agency.name}」分配到项目「${projectNames}」`)
|
||
}
|
||
setAssignModal({ open: false, agency: null })
|
||
setSelectedProjects([])
|
||
}
|
||
|
||
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">{agencies.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">{agencies.filter(a => a.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-yellow-400">{agencies.filter(a => a.status === 'pending').length}</p>
|
||
</div>
|
||
<div className="w-10 h-10 rounded-lg bg-yellow-500/20 flex items-center justify-center">
|
||
<Clock size={20} className="text-yellow-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">
|
||
{agencies.filter(a => a.status === 'active').length > 0
|
||
? Math.round(agencies.filter(a => a.status === 'active').reduce((sum, a) => sum + a.passRate, 0) / agencies.filter(a => a.status === 'active').length)
|
||
: 0}%
|
||
</p>
|
||
</div>
|
||
<div className="w-10 h-10 rounded-lg bg-purple-500/20 flex items-center justify-center">
|
||
<TrendingUp size={20} className="text-purple-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>
|
||
{filteredAgencies.map((agency) => (
|
||
<tr key={agency.id} className="border-b border-border-subtle last:border-0 hover:bg-bg-elevated/50">
|
||
<td className="px-6 py-4">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 rounded-lg bg-accent-indigo/15 flex items-center justify-center">
|
||
<Building2 size={20} className="text-accent-indigo" />
|
||
</div>
|
||
<div>
|
||
<div className="flex items-center gap-2">
|
||
<span className="font-medium text-text-primary">{agency.name}</span>
|
||
{agency.remark && (
|
||
<span className="px-2 py-0.5 text-xs rounded bg-accent-amber/15 text-accent-amber" title={agency.remark}>
|
||
有备注
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className="text-sm text-text-tertiary">{agency.companyName}</div>
|
||
{agency.remark && (
|
||
<p className="text-xs text-text-tertiary mt-0.5 line-clamp-1">{agency.remark}</p>
|
||
)}
|
||
</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">
|
||
{agency.agencyId}
|
||
</code>
|
||
<button
|
||
type="button"
|
||
onClick={() => handleCopyAgencyId(agency.agencyId)}
|
||
className="p-1 rounded hover:bg-bg-elevated transition-colors"
|
||
title="复制代理商ID"
|
||
>
|
||
{copiedId === agency.agencyId ? (
|
||
<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={agency.status} />
|
||
</td>
|
||
<td className="px-6 py-4 text-text-primary">{agency.creatorCount}</td>
|
||
<td className="px-6 py-4 text-text-primary">{agency.projectCount}</td>
|
||
<td className="px-6 py-4">
|
||
{agency.status === 'active' ? (
|
||
<div className="flex items-center gap-2">
|
||
<span className={`font-medium ${agency.passRate >= 90 ? 'text-accent-green' : agency.passRate >= 80 ? 'text-accent-indigo' : 'text-orange-400'}`}>
|
||
{agency.passRate}%
|
||
</span>
|
||
{agency.trend === 'up' && <TrendingUp size={14} className="text-accent-green" />}
|
||
{agency.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">{agency.joinedAt}</td>
|
||
<td className="px-6 py-4">
|
||
<div className="relative">
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => setOpenMenuId(openMenuId === agency.id ? null : agency.id)}
|
||
>
|
||
<MoreVertical size={16} />
|
||
</Button>
|
||
{/* 下拉菜单 */}
|
||
{openMenuId === agency.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(agency)}
|
||
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" />
|
||
{agency.remark ? '编辑备注' : '添加备注'}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => handleOpenAssign(agency)}
|
||
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(agency)}
|
||
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>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
|
||
{filteredAgencies.length === 0 && (
|
||
<div className="text-center py-12 text-text-tertiary">
|
||
<Building2 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={inviteAgencyId}
|
||
onChange={(e) => {
|
||
setInviteAgencyId(e.target.value.toUpperCase())
|
||
setInviteResult(null)
|
||
}}
|
||
placeholder="例如: AG789012"
|
||
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格式:AG + 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>
|
||
|
||
{/* 备注弹窗 */}
|
||
<Modal
|
||
isOpen={remarkModal.open}
|
||
onClose={() => { setRemarkModal({ open: false, agency: null }); setRemarkText(''); }}
|
||
title={`${remarkModal.agency?.remark ? '编辑' : '添加'}备注 - ${remarkModal.agency?.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, agency: null }); setRemarkText(''); }}>
|
||
取消
|
||
</Button>
|
||
<Button onClick={handleSaveRemark}>
|
||
<CheckCircle size={16} />
|
||
保存
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
|
||
{/* 删除确认弹窗 */}
|
||
<Modal
|
||
isOpen={deleteModal.open}
|
||
onClose={() => setDeleteModal({ open: false, agency: 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.agency?.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, agency: 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, agency: null }); setSelectedProjects([]); }}
|
||
title={`分配代理商到项目 - ${assignModal.agency?.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 max-h-60 overflow-y-auto">
|
||
{mockProjects.map((project) => {
|
||
const isSelected = selectedProjects.includes(project.id)
|
||
return (
|
||
<button
|
||
key={project.id}
|
||
type="button"
|
||
onClick={() => toggleProjectSelection(project.id)}
|
||
className={`w-full flex items-center gap-3 p-4 rounded-xl border-2 text-left transition-colors ${
|
||
isSelected
|
||
? 'border-accent-indigo bg-accent-indigo/10'
|
||
: 'border-border-subtle hover:border-accent-indigo/50'
|
||
}`}
|
||
>
|
||
<div className={`w-5 h-5 rounded border-2 flex items-center justify-center ${
|
||
isSelected ? 'border-accent-indigo bg-accent-indigo' : 'border-border-subtle'
|
||
}`}>
|
||
{isSelected && <CheckCircle size={12} className="text-white" />}
|
||
</div>
|
||
<span className="text-text-primary">{project.name}</span>
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
{selectedProjects.length > 0 && (
|
||
<p className="text-sm text-text-secondary">
|
||
已选择 <span className="text-accent-indigo font-medium">{selectedProjects.length}</span> 个项目
|
||
</p>
|
||
)}
|
||
<div className="flex gap-3 justify-end pt-2">
|
||
<Button variant="ghost" onClick={() => { setAssignModal({ open: false, agency: null }); setSelectedProjects([]); }}>
|
||
取消
|
||
</Button>
|
||
<Button onClick={handleConfirmAssign} disabled={selectedProjects.length === 0}>
|
||
<FolderPlus size={16} />
|
||
确认分配
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
|
||
{/* 点击其他地方关闭菜单 */}
|
||
{openMenuId && (
|
||
<div
|
||
className="fixed inset-0 z-0"
|
||
onClick={() => setOpenMenuId(null)}
|
||
/>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|