Your Name 964797d2e9 feat: 完善品牌方和代理商前端功能
品牌方功能:
- 项目看板: 添加截止日期编辑功能
- 项目详情: 添加代理商管理、截止日期编辑、最近任务显示代理商
- 项目创建: 代理商选择支持搜索(名称/ID/公司名)
- 代理商管理: 通过ID邀请、添加备注/分配项目/移除操作
- Brief配置: 新增项目级Brief和规则配置页面
- 系统设置: 完善账户安全(密码/2FA/邮箱/手机/设备管理)、数据导出、退出登录

代理商功能:
- 个人中心: 新增代理商ID展示、公司信息(企业验证)、个人信息编辑
- 账户设置: 密码修改、手机/邮箱绑定、两步验证
- 通知设置: 分类型和渠道的通知开关
- 审核历史: 搜索筛选和统计展示
- 帮助反馈: FAQ分类搜索和客服联系

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 17:40:11 +08:00

604 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 { useRouter, useParams } from 'next/navigation'
import Link from 'next/link'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Modal } from '@/components/ui/Modal'
import { SuccessTag, PendingTag, ErrorTag } from '@/components/ui/Tag'
import {
ArrowLeft,
Calendar,
Users,
FileText,
Video,
Clock,
CheckCircle,
XCircle,
ChevronRight,
Plus,
Settings,
Search,
Building2,
MoreHorizontal,
Trash2,
Check,
Pencil
} from 'lucide-react'
// 模拟项目详情数据
const mockProject = {
id: 'proj-001',
name: 'XX品牌618推广',
status: 'active',
deadline: '2026-06-18',
createdAt: '2026-02-01',
description: '618大促活动营销内容审核项目',
stats: {
scriptTotal: 20,
scriptPassed: 15,
scriptPending: 3,
scriptRejected: 2,
videoTotal: 20,
videoPassed: 12,
videoPending: 5,
videoRejected: 3,
},
agencies: [
{ id: 'AG789012', name: '星耀传媒', creatorCount: 8, passRate: 92 },
{ id: 'AG456789', name: '创意无限', creatorCount: 5, passRate: 88 },
],
recentTasks: [
{ id: 'task-001', type: 'video', creatorName: '小美护肤', agencyId: 'AG789012', agencyName: '星耀传媒', status: 'pending', submittedAt: '2026-02-06 14:30' },
{ id: 'task-002', type: 'script', creatorName: '美妆Lisa', agencyId: 'AG789012', agencyName: '星耀传媒', status: 'approved', submittedAt: '2026-02-06 12:15' },
{ id: 'task-003', type: 'video', creatorName: '健身王', agencyId: 'AG456789', agencyName: '创意无限', status: 'rejected', submittedAt: '2026-02-06 10:00' },
{ id: 'task-004', type: 'script', creatorName: '时尚达人', agencyId: 'AG456789', agencyName: '创意无限', status: 'pending', submittedAt: '2026-02-05 16:45' },
],
}
// 模拟品牌方已添加的代理商(来自代理商管理)
const mockManagedAgencies = [
{ id: 'AG789012', name: '星耀传媒', companyName: '上海星耀文化传媒有限公司' },
{ id: 'AG456789', name: '创意无限', companyName: '深圳创意无限广告有限公司' },
{ id: 'AG123456', name: '美妆达人MCN', companyName: '杭州美妆达人网络科技有限公司' },
{ id: 'AG111111', name: '蓝海科技', companyName: '北京蓝海数字科技有限公司' },
{ id: 'AG222222', name: '云创网络', companyName: '杭州云创网络技术有限公司' },
{ id: 'AG333333', name: '天府传媒', companyName: '成都天府传媒集团有限公司' },
]
function StatCard({ title, value, icon: Icon, color }: { title: string; value: number | string; icon: React.ElementType; color: string }) {
return (
<Card>
<CardContent className="py-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-text-secondary">{title}</p>
<p className={`text-2xl font-bold ${color}`}>{value}</p>
</div>
<div className={`w-10 h-10 rounded-lg ${color.replace('text-', 'bg-')}/20 flex items-center justify-center`}>
<Icon size={20} className={color} />
</div>
</div>
</CardContent>
</Card>
)
}
function TaskStatusTag({ status }: { status: string }) {
switch (status) {
case 'approved': return <SuccessTag></SuccessTag>
case 'pending': return <PendingTag></PendingTag>
case 'rejected': return <ErrorTag></ErrorTag>
default: return <PendingTag></PendingTag>
}
}
export default function ProjectDetailPage() {
const router = useRouter()
const params = useParams()
const projectId = params.id as string
const [project, setProject] = useState(mockProject)
// 添加代理商相关状态
const [showAddModal, setShowAddModal] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [selectedAgencies, setSelectedAgencies] = useState<string[]>([])
// 代理商操作菜单
const [activeAgencyMenu, setActiveAgencyMenu] = useState<string | null>(null)
// 删除确认
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [agencyToDelete, setAgencyToDelete] = useState<typeof project.agencies[0] | null>(null)
// 编辑截止日期
const [showDeadlineModal, setShowDeadlineModal] = useState(false)
const [newDeadline, setNewDeadline] = useState(project.deadline)
// 保存截止日期
const handleSaveDeadline = () => {
if (!newDeadline) return
setProject({ ...project, deadline: newDeadline })
setShowDeadlineModal(false)
}
const scriptPassRate = Math.round((project.stats.scriptPassed / project.stats.scriptTotal) * 100)
const videoPassRate = Math.round((project.stats.videoPassed / project.stats.videoTotal) * 100)
// 过滤可添加的代理商(排除已在项目中的)
const availableAgencies = mockManagedAgencies.filter(
agency => !project.agencies.some(a => a.id === agency.id)
)
// 搜索过滤
const filteredAgencies = availableAgencies.filter(agency =>
searchQuery === '' ||
agency.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
agency.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
agency.companyName.toLowerCase().includes(searchQuery.toLowerCase())
)
// 切换选择
const toggleSelectAgency = (agencyId: string) => {
setSelectedAgencies(prev =>
prev.includes(agencyId)
? prev.filter(id => id !== agencyId)
: [...prev, agencyId]
)
}
// 确认添加
const handleAddAgencies = () => {
const newAgencies = mockManagedAgencies
.filter(a => selectedAgencies.includes(a.id))
.map(a => ({ id: a.id, name: a.name, creatorCount: 0, passRate: 0 }))
setProject({
...project,
agencies: [...project.agencies, ...newAgencies]
})
setShowAddModal(false)
setSelectedAgencies([])
setSearchQuery('')
}
// 移除代理商
const handleRemoveAgency = async () => {
if (!agencyToDelete) return
setProject({
...project,
agencies: project.agencies.filter(a => a.id !== agencyToDelete.id)
})
setShowDeleteModal(false)
setAgencyToDelete(null)
}
return (
<div className="space-y-6">
{/* 顶部导航 */}
<div className="flex items-center gap-4">
<button type="button" onClick={() => router.back()} className="p-2 hover:bg-bg-elevated rounded-full">
<ArrowLeft size={20} className="text-text-primary" />
</button>
<div className="flex-1">
<h1 className="text-2xl font-bold text-text-primary">{project.name}</h1>
<p className="text-sm text-text-secondary">{project.description}</p>
</div>
<SuccessTag></SuccessTag>
</div>
{/* 项目信息 */}
<div className="flex items-center gap-6 text-sm text-text-secondary">
<span className="flex items-center gap-2">
<Calendar size={16} />
: {project.deadline}
<button
type="button"
onClick={() => {
setNewDeadline(project.deadline)
setShowDeadlineModal(true)
}}
className="p-1 rounded hover:bg-bg-elevated transition-colors"
title="修改截止日期"
>
<Pencil size={14} className="text-text-tertiary hover:text-accent-indigo" />
</button>
</span>
<span className="flex items-center gap-2">
<Clock size={16} />
: {project.createdAt}
</span>
</div>
{/* Brief和规则配置 - 大按钮 */}
<Link href={`/brand/projects/${projectId}/config`}>
<Card className="hover:border-accent-indigo transition-colors cursor-pointer">
<CardContent className="py-5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-accent-indigo/15 flex items-center justify-center">
<Settings size={24} className="text-accent-indigo" />
</div>
<div>
<p className="font-semibold text-text-primary">Brief和规则配置</p>
<p className="text-sm text-text-secondary">BriefAI检测项等</p>
</div>
</div>
<ChevronRight size={20} className="text-text-tertiary" />
</div>
</CardContent>
</Card>
</Link>
{/* 统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<StatCard title="脚本通过率" value={`${scriptPassRate}%`} icon={FileText} color="text-accent-green" />
<StatCard title="视频通过率" value={`${videoPassRate}%`} icon={Video} color="text-accent-indigo" />
<StatCard title="参与代理商" value={project.agencies.length} icon={Users} color="text-purple-400" />
<StatCard title="待审核任务" value={project.stats.scriptPending + project.stats.videoPending} icon={Clock} color="text-orange-400" />
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 审核进度 */}
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* 脚本审核 */}
<div>
<div className="flex items-center justify-between mb-3">
<span className="flex items-center gap-2 text-text-primary font-medium">
<FileText size={16} />
</span>
<span className="text-sm text-text-secondary">
{project.stats.scriptPassed}/{project.stats.scriptTotal}
</span>
</div>
<div className="flex h-4 rounded-full overflow-hidden bg-bg-elevated">
<div className="bg-accent-green" style={{ width: `${(project.stats.scriptPassed / project.stats.scriptTotal) * 100}%` }} />
<div className="bg-yellow-500" style={{ width: `${(project.stats.scriptPending / project.stats.scriptTotal) * 100}%` }} />
<div className="bg-accent-coral" style={{ width: `${(project.stats.scriptRejected / project.stats.scriptTotal) * 100}%` }} />
</div>
<div className="flex gap-6 mt-2 text-xs">
<span className="flex items-center gap-1 text-accent-green">
<CheckCircle size={12} /> {project.stats.scriptPassed}
</span>
<span className="flex items-center gap-1 text-yellow-500">
<Clock size={12} /> {project.stats.scriptPending}
</span>
<span className="flex items-center gap-1 text-accent-coral">
<XCircle size={12} /> {project.stats.scriptRejected}
</span>
</div>
</div>
{/* 视频审核 */}
<div>
<div className="flex items-center justify-between mb-3">
<span className="flex items-center gap-2 text-text-primary font-medium">
<Video size={16} />
</span>
<span className="text-sm text-text-secondary">
{project.stats.videoPassed}/{project.stats.videoTotal}
</span>
</div>
<div className="flex h-4 rounded-full overflow-hidden bg-bg-elevated">
<div className="bg-accent-green" style={{ width: `${(project.stats.videoPassed / project.stats.videoTotal) * 100}%` }} />
<div className="bg-yellow-500" style={{ width: `${(project.stats.videoPending / project.stats.videoTotal) * 100}%` }} />
<div className="bg-accent-coral" style={{ width: `${(project.stats.videoRejected / project.stats.videoTotal) * 100}%` }} />
</div>
<div className="flex gap-6 mt-2 text-xs">
<span className="flex items-center gap-1 text-accent-green">
<CheckCircle size={12} /> {project.stats.videoPassed}
</span>
<span className="flex items-center gap-1 text-yellow-500">
<Clock size={12} /> {project.stats.videoPending}
</span>
<span className="flex items-center gap-1 text-accent-coral">
<XCircle size={12} /> {project.stats.videoRejected}
</span>
</div>
</div>
</CardContent>
</Card>
{/* 代理商列表 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users size={16} />
<span className="text-sm font-normal text-text-tertiary">({project.agencies.length})</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{project.agencies.map((agency) => (
<div key={agency.id} className="flex items-center justify-between p-3 rounded-lg bg-bg-elevated">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-lg bg-accent-indigo/15 flex items-center justify-center">
<Building2 size={18} className="text-accent-indigo" />
</div>
<div>
<p className="font-medium text-text-primary text-sm">{agency.name}</p>
<p className="text-xs text-text-tertiary">{agency.creatorCount} · {agency.passRate}%</p>
</div>
</div>
<div className="relative">
<button
type="button"
onClick={() => setActiveAgencyMenu(activeAgencyMenu === agency.id ? null : agency.id)}
className="p-1.5 rounded hover:bg-bg-page transition-colors"
>
<MoreHorizontal size={16} className="text-text-tertiary" />
</button>
{activeAgencyMenu === agency.id && (
<div className="absolute right-0 top-8 z-10 w-32 py-1 bg-bg-card rounded-lg shadow-lg border border-border-subtle">
<button
type="button"
onClick={() => {
setAgencyToDelete(agency)
setShowDeleteModal(true)
setActiveAgencyMenu(null)
}}
className="w-full px-3 py-2 text-left text-sm text-accent-coral hover:bg-bg-elevated flex items-center gap-2"
>
<Trash2 size={14} />
</button>
</div>
)}
</div>
</div>
))}
{/* 添加代理商按钮 */}
<button
type="button"
onClick={() => setShowAddModal(true)}
className="w-full p-3 rounded-lg border-2 border-dashed border-border-subtle hover:border-accent-indigo hover:bg-accent-indigo/5 transition-all flex items-center justify-center gap-2 text-text-tertiary hover:text-accent-indigo"
>
<Plus size={18} />
<span className="text-sm font-medium"></span>
</button>
</CardContent>
</Card>
</div>
{/* 最近任务 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span></span>
<Link href="/brand/review">
<Button variant="ghost" size="sm">
<ChevronRight size={16} />
</Button>
</Link>
</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-border-subtle text-left text-sm text-text-secondary">
<th className="pb-3 font-medium"></th>
<th className="pb-3 font-medium"></th>
<th className="pb-3 font-medium"></th>
<th className="pb-3 font-medium"></th>
<th className="pb-3 font-medium"></th>
<th className="pb-3 font-medium"></th>
</tr>
</thead>
<tbody>
{project.recentTasks.map((task) => (
<tr key={task.id} className="border-b border-border-subtle last:border-0 hover:bg-bg-elevated">
<td className="py-4">
<span className="flex items-center gap-2">
{task.type === 'script' ? <FileText size={16} className="text-accent-indigo" /> : <Video size={16} className="text-purple-400" />}
{task.type === 'script' ? '脚本' : '视频'}
</span>
</td>
<td className="py-4 text-text-primary">{task.creatorName}</td>
<td className="py-4">
<span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-bg-elevated text-sm">
<Building2 size={14} className="text-accent-indigo" />
<span className="text-text-secondary">{task.agencyName}</span>
</span>
</td>
<td className="py-4"><TaskStatusTag status={task.status} /></td>
<td className="py-4 text-sm text-text-tertiary">{task.submittedAt}</td>
<td className="py-4">
<Link href={`/brand/review/${task.type}/${task.id}`}>
<Button size="sm" variant={task.status === 'pending' ? 'primary' : 'secondary'}>
{task.status === 'pending' ? '审核' : '查看'}
</Button>
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
{/* 添加代理商弹窗 */}
<Modal
isOpen={showAddModal}
onClose={() => {
setShowAddModal(false)
setSearchQuery('')
setSelectedAgencies([])
}}
title="添加代理商"
size="lg"
>
<div className="space-y-4">
{/* 搜索框 */}
<div className="relative">
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-tertiary" />
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="搜索代理商名称或ID..."
className="pl-10"
/>
</div>
{/* 代理商列表 */}
<div className="max-h-80 overflow-y-auto space-y-2">
{filteredAgencies.length > 0 ? (
filteredAgencies.map((agency) => {
const isSelected = selectedAgencies.includes(agency.id)
return (
<button
key={agency.id}
type="button"
onClick={() => toggleSelectAgency(agency.id)}
className={`w-full flex items-center gap-3 p-3 rounded-xl border-2 transition-all text-left ${
isSelected
? 'border-accent-indigo bg-accent-indigo/5'
: 'border-transparent bg-bg-elevated hover:bg-bg-page'
}`}
>
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
isSelected ? 'bg-accent-indigo' : 'bg-accent-indigo/15'
}`}>
{isSelected ? (
<Check size={20} className="text-white" />
) : (
<Building2 size={20} className="text-accent-indigo" />
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<p className="font-medium text-text-primary">{agency.name}</p>
<span className="text-xs text-text-tertiary font-mono">{agency.id}</span>
</div>
<p className="text-sm text-text-secondary truncate">{agency.companyName}</p>
</div>
</button>
)
})
) : (
<div className="text-center py-8 text-text-tertiary">
{availableAgencies.length === 0 ? (
<>
<Users size={32} className="mx-auto mb-2 opacity-50" />
<p></p>
</>
) : (
<>
<Search size={32} className="mx-auto mb-2 opacity-50" />
<p></p>
</>
)}
</div>
)}
</div>
{/* 已选择提示 */}
{selectedAgencies.length > 0 && (
<div className="flex items-center justify-between pt-3 border-t border-border-subtle">
<span className="text-sm text-text-secondary">
<span className="text-accent-indigo font-medium">{selectedAgencies.length}</span>
</span>
<Button variant="primary" onClick={handleAddAgencies}>
<Plus size={16} />
</Button>
</div>
)}
{/* 底部提示 */}
<p className="text-xs text-text-tertiary pt-2">
"代理商管理"
</p>
</div>
</Modal>
{/* 删除确认弹窗 */}
<Modal
isOpen={showDeleteModal}
onClose={() => { setShowDeleteModal(false); setAgencyToDelete(null) }}
title="移除代理商"
>
<div className="space-y-4">
<p className="text-text-secondary">
<span className="text-text-primary font-medium">{agencyToDelete?.name}</span>
</p>
<p className="text-sm text-accent-coral">
</p>
<div className="flex gap-3">
<Button variant="secondary" className="flex-1" onClick={() => { setShowDeleteModal(false); setAgencyToDelete(null) }}>
</Button>
<Button
variant="primary"
className="flex-1 bg-accent-coral hover:bg-accent-coral/80"
onClick={handleRemoveAgency}
>
</Button>
</div>
</div>
</Modal>
{/* 编辑截止日期弹窗 */}
<Modal
isOpen={showDeadlineModal}
onClose={() => setShowDeadlineModal(false)}
title="修改截止日期"
>
<div className="space-y-4">
<div className="p-3 rounded-lg bg-bg-elevated">
<p className="text-sm text-text-secondary"></p>
<p className="font-medium text-text-primary">{project.name}</p>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-2">
</label>
<div className="relative">
<Calendar size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-text-tertiary" />
<input
type="date"
value={newDeadline}
onChange={(e) => setNewDeadline(e.target.value)}
className="w-full pl-12 pr-4 py-3 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
/>
</div>
</div>
<div className="flex gap-3 pt-2">
<Button
variant="secondary"
className="flex-1"
onClick={() => setShowDeadlineModal(false)}
>
</Button>
<Button
variant="primary"
className="flex-1"
onClick={handleSaveDeadline}
disabled={!newDeadline}
>
</Button>
</div>
</div>
</Modal>
</div>
)
}