Your Name 0bfedb95c8 feat: 为所有终端添加平台显示功能
- 新增 frontend/lib/platforms.ts 共享平台配置模块
- 支持6个平台: 抖音、小红书、B站、快手、微博、微信视频号
- 品牌方终端: 项目看板、项目详情、终审台列表添加平台显示
- 代理商终端: 工作台概览、审核台、Brief配置、达人管理、
  数据报表、消息中心、申诉处理添加平台显示
- 达人端: 任务列表添加平台显示
- 统一使用彩色头部条样式展示平台信息

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 18:53:51 +08:00

645 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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 {
Search,
Plus,
Users,
TrendingUp,
TrendingDown,
Copy,
CheckCircle,
Clock,
MoreVertical,
Building2,
AlertCircle,
UserPlus,
MessageSquareText,
Trash2,
FolderPlus
} from 'lucide-react'
// 代理商类型
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
}
// 模拟项目列表(用于分配代理商)
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 [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('、')
alert(`已将代理商「${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>
)
}