feat: 前端剩余页面全面对接后端 API(Phase 2 完成)

为品牌方端(8页)、代理商端(10页)、达人端(6页)共24个页面添加真实API调用:
- 每页新增 USE_MOCK 条件分支,开发环境使用 mock 数据,生产环境调用真实 API
- 添加 loading 骨架屏、error toast 提示、submitting 状态管理
- 数据映射:TaskResponse → 页面视图模型,处理类型差异
- 审核操作(通过/驳回/强制通过)对接 api.reviewScript/reviewVideo
- Brief/规则/AI配置对接 api.getBrief/updateBrief/listForbiddenWords 等
- 申诉/历史/额度管理对接 api.listTasks + 状态过滤映射

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-02-09 16:29:43 +08:00
co-authored by Claude Opus 4.6
parent 54eaa54966
commit a8be7bbca9
24 changed files with 5244 additions and 1845 deletions
+225 -320
View File
@@ -1,144 +1,112 @@
'use client'
import { useState } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
import { useState, useEffect, useCallback } from 'react'
import { Card, CardContent } 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 { SuccessTag, PendingTag } 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
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'
// 代理商类型
interface Agency {
id: string
agencyId: string // 代理商IDAG开头)
name: string
companyName: string
email: string
status: 'active' | 'pending' | 'paused'
creatorCount: number
projectCount: number
passRate: number
trend: 'up' | 'down' | 'stable'
joinedAt: string
remark?: string
// ==================== 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 <SuccessTag></SuccessTag>
return <PendingTag></PendingTag>
}
// 模拟项目列表(用于分配代理商)
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>
function AgencySkeleton() {
return (
<div className="animate-pulse">
<div className="h-20 bg-bg-elevated rounded-lg mb-2" />
<div className="h-20 bg-bg-elevated rounded-lg mb-2" />
<div className="h-20 bg-bg-elevated rounded-lg" />
</div>
)
}
export default function AgenciesManagePage() {
const toast = useToast()
const [searchQuery, setSearchQuery] = useState('')
const [agencies, setAgencies] = useState<Agency[]>(initialAgencies)
const [agencies, setAgencies] = useState<AgencyDetail[]>([])
const [projects, setProjects] = useState<ProjectResponse[]>([])
const [loading, setLoading] = useState(true)
const [copiedId, setCopiedId] = useState<string | null>(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<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 [deleteModal, setDeleteModal] = useState<{ open: boolean; agency: AgencyDetail | null }>({ open: false, agency: null })
const [deleting, setDeleting] = useState(false)
// 分配项目弹窗状态
const [assignModal, setAssignModal] = useState<{ open: boolean; agency: Agency | null }>({ open: false, agency: null })
const [assignModal, setAssignModal] = useState<{ open: boolean; agency: AgencyDetail | null }>({ open: false, agency: null })
const [selectedProjects, setSelectedProjects] = useState<string[]>([])
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.agencyId.toLowerCase().includes(searchQuery.toLowerCase()) ||
agency.companyName.toLowerCase().includes(searchQuery.toLowerCase())
agency.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
(agency.contact_name || '').toLowerCase().includes(searchQuery.toLowerCase())
)
// 复制代理商ID
@@ -149,27 +117,36 @@ export default function AgenciesManagePage() {
}
// 邀请代理商
const handleInvite = () => {
const handleInvite = async () => {
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())) {
if (agencies.some(a => a.id === inviteAgencyId.toUpperCase())) {
setInviteResult({ success: false, message: '该代理商已在您的列表中' })
return
}
// 模拟发送邀请成功
setInviteResult({ success: true, message: `已向代理商 ${inviteAgencyId.toUpperCase()} 发送邀请` })
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 = () => {
@@ -178,40 +155,42 @@ export default function AgenciesManagePage() {
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
))
const handleConfirmInvite = async () => {
if (inviteResult?.success) {
handleCloseInviteModal()
await loadData()
}
setRemarkModal({ open: false, agency: null })
setRemarkText('')
}
// 打开删除确认
const handleOpenDelete = (agency: Agency) => {
const handleOpenDelete = (agency: AgencyDetail) => {
setDeleteModal({ open: true, agency })
setOpenMenuId(null)
}
// 确认删除
const handleConfirmDelete = () => {
if (deleteModal.agency) {
setAgencies(prev => prev.filter(a => a.id !== deleteModal.agency!.id))
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 })
}
setDeleteModal({ open: false, agency: null })
}
// 打开分配项目弹窗
const handleOpenAssign = (agency: Agency) => {
const handleOpenAssign = (agency: AgencyDetail) => {
setSelectedProjects([])
setAssignModal({ open: true, agency })
setOpenMenuId(null)
@@ -220,23 +199,34 @@ export default function AgenciesManagePage() {
// 切换项目选择
const toggleProjectSelection = (projectId: string) => {
setSelectedProjects(prev =>
prev.includes(projectId)
? prev.filter(id => id !== projectId)
: [...prev, projectId]
prev.includes(projectId) ? prev.filter(id => id !== projectId) : [...prev, projectId]
)
}
// 确认分配项目
const handleConfirmAssign = () => {
if (assignModal.agency && selectedProjects.length > 0) {
const projectNames = mockProjects
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([])
}
setAssignModal({ open: false, agency: null })
setSelectedProjects([])
}
return (
@@ -254,7 +244,7 @@ export default function AgenciesManagePage() {
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardContent className="py-4">
<div className="flex items-center justify-between">
@@ -272,8 +262,8 @@ export default function AgenciesManagePage() {
<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>
<p className="text-sm text-text-secondary"></p>
<p className="text-2xl font-bold text-accent-green">{agencies.filter(a => a.force_pass_enabled).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" />
@@ -285,28 +275,11 @@ export default function AgenciesManagePage() {
<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>
<p className="text-sm text-text-secondary"></p>
<p className="text-2xl font-bold text-text-primary">{projects.length}</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" />
<Building2 size={20} className="text-purple-400" />
</div>
</div>
</CardContent>
@@ -318,7 +291,7 @@ export default function AgenciesManagePage() {
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-tertiary" />
<input
type="text"
placeholder="搜索代理商名称、ID或公司名..."
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"
@@ -328,127 +301,93 @@ export default function AgenciesManagePage() {
{/* 代理商列表 */}
<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>
{loading ? (
<div className="p-6"><AgencySkeleton /></div>
) : (
<table className="w-full min-w-[700px]">
<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>
</tr>
))}
</tbody>
</table>
</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>
<span className="font-medium text-text-primary">{agency.name}</span>
</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.id}
</code>
<button
type="button"
onClick={() => handleCopyAgencyId(agency.id)}
className="p-1 rounded hover:bg-bg-elevated transition-colors"
title="复制代理商ID"
>
{copiedId === agency.id ? (
<CheckCircle size={14} className="text-accent-green" />
) : (
<Copy size={14} className="text-text-tertiary" />
)}
</button>
</div>
</td>
<td className="px-6 py-4 text-text-secondary text-sm">
{agency.contact_name || '-'}
</td>
<td className="px-6 py-4">
<StatusTag forcePass={agency.force_pass_enabled} />
</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={() => 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 && (
{!loading && filteredAgencies.length === 0 && (
<div className="text-center py-12 text-text-tertiary">
<Building2 size={48} className="mx-auto mb-4 opacity-50" />
<p></p>
@@ -477,8 +416,8 @@ export default function AgenciesManagePage() {
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 variant="secondary" onClick={handleInvite} disabled={inviting}>
{inviting ? <Loader2 size={16} className="animate-spin" /> : '查找'}
</Button>
</div>
<p className="text-xs text-text-tertiary mt-2">ID格式AG + 6</p>
@@ -503,44 +442,9 @@ export default function AgenciesManagePage() {
<Button variant="ghost" onClick={handleCloseInviteModal}>
</Button>
<Button
onClick={() => {
if (inviteResult?.success) {
handleCloseInviteModal()
}
}}
disabled={!inviteResult?.success}
>
<Button onClick={handleConfirmInvite} 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>
@@ -572,8 +476,9 @@ export default function AgenciesManagePage() {
variant="secondary"
className="border-accent-coral text-accent-coral hover:bg-accent-coral/10"
onClick={handleConfirmDelete}
disabled={deleting}
>
<Trash2 size={16} />
{deleting ? <Loader2 size={16} className="animate-spin" /> : <Trash2 size={16} />}
</Button>
</div>
@@ -593,7 +498,7 @@ export default function AgenciesManagePage() {
<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) => {
{projects.map((project) => {
const isSelected = selectedProjects.includes(project.id)
return (
<button
@@ -626,8 +531,8 @@ export default function AgenciesManagePage() {
<Button variant="ghost" onClick={() => { setAssignModal({ open: false, agency: null }); setSelectedProjects([]); }}>
</Button>
<Button onClick={handleConfirmAssign} disabled={selectedProjects.length === 0}>
<FolderPlus size={16} />
<Button onClick={handleConfirmAssign} disabled={selectedProjects.length === 0 || assigning}>
{assigning ? <Loader2 size={16} className="animate-spin" /> : <FolderPlus size={16} />}
</Button>
</div>