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:
co-authored by
Claude Opus 4.6
parent
54eaa54966
commit
a8be7bbca9
@@ -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 // 代理商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
|
||||
// ==================== 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>
|
||||
|
||||
Reference in New Issue
Block a user