Your Name 4753626e5a feat: 完成代理商/品牌方前端及文档更新
代理商端前端:
- 新增达人管理页面(含任务申诉次数管理)
- 新增消息中心(含申诉次数申请审批)
- 新增 Brief 管理(列表、详情)
- 新增审核中心(脚本审核、视频审核)
- 新增数据报表页面

品牌方端前端:
- 优化首页仪表盘布局
- 新增项目管理(列表、详情、创建)
- 新增代理商管理页面
- 新增审核中心(脚本终审、视频终审)
- 新增系统设置页面

文档更新:
- 申诉次数改为按任务分配(每任务初始1次)
- 更新 PRD、FeatureSummary、User_Role_Interfaces 等文档
- 更新 UI 设计规范和开发计划

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 15:39:23 +08:00

271 lines
12 KiB
TypeScript

'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 { SuccessTag, PendingTag, WarningTag, ErrorTag } from '@/components/ui/Tag'
import {
ArrowLeft,
Calendar,
Users,
FileText,
Video,
TrendingUp,
Clock,
CheckCircle,
XCircle,
ChevronRight
} from 'lucide-react'
// 模拟项目详情数据
const mockProject = {
id: 'proj-001',
name: 'XX品牌618推广',
status: 'active',
deadline: '2026-06-18',
createdAt: '2026-02-01',
description: '618大促活动营销内容审核项目',
briefUrl: '/briefs/xx-brand-618.pdf',
stats: {
scriptTotal: 20,
scriptPassed: 15,
scriptPending: 3,
scriptRejected: 2,
videoTotal: 20,
videoPassed: 12,
videoPending: 5,
videoRejected: 3,
},
agencies: [
{ id: 'agency-001', name: '星耀传媒', creatorCount: 8, passRate: 92 },
{ id: 'agency-002', name: '创意无限', creatorCount: 5, passRate: 88 },
{ id: 'agency-003', name: '美妆达人MCN', creatorCount: 2, passRate: 75 },
],
recentTasks: [
{ id: 'task-001', type: 'video', creatorName: '小美护肤', status: 'pending', submittedAt: '2026-02-06 14:30' },
{ id: 'task-002', type: 'script', creatorName: '美妆Lisa', status: 'approved', submittedAt: '2026-02-06 12:15' },
{ id: 'task-003', type: 'video', creatorName: '健身王', status: 'rejected', submittedAt: '2026-02-06 10:00' },
{ id: 'task-004', type: 'script', creatorName: '时尚达人', status: 'pending', submittedAt: '2026-02-05 16:45' },
],
}
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 project = mockProject
const scriptPassRate = Math.round((project.stats.scriptPassed / project.stats.scriptTotal) * 100)
const videoPassRate = Math.round((project.stats.videoPassed / project.stats.videoTotal) * 100)
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}
</span>
<span className="flex items-center gap-2">
<Clock size={16} />
: {project.createdAt}
</span>
<Link href={project.briefUrl} className="flex items-center gap-2 text-accent-indigo hover:underline">
<FileText size={16} />
Brief
</Link>
</div>
{/* 统计卡片 */}
<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} />
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{project.agencies.map((agency) => (
<div key={agency.id} className="p-3 rounded-lg bg-bg-elevated">
<div className="flex items-center justify-between mb-2">
<span className="font-medium text-text-primary">{agency.name}</span>
<span className={`text-sm font-medium ${agency.passRate >= 90 ? 'text-accent-green' : agency.passRate >= 80 ? 'text-accent-indigo' : 'text-orange-400'}`}>
{agency.passRate}%
</span>
</div>
<div className="text-xs text-text-secondary">{agency.creatorCount} </div>
</div>
))}
</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>
</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"><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>
</div>
)
}