feat: 完成代理商/品牌方前端及文档更新
代理商端前端: - 新增达人管理页面(含任务申诉次数管理) - 新增消息中心(含申诉次数申请审批) - 新增 Brief 管理(列表、详情) - 新增审核中心(脚本审核、视频审核) - 新增数据报表页面 品牌方端前端: - 优化首页仪表盘布局 - 新增项目管理(列表、详情、创建) - 新增代理商管理页面 - 新增审核中心(脚本终审、视频终审) - 新增系统设置页面 文档更新: - 申诉次数改为按任务分配(每任务初始1次) - 更新 PRD、FeatureSummary、User_Role_Interfaces 等文档 - 更新 UI 设计规范和开发计划 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
2f9b7f05fd
commit
4753626e5a
@@ -0,0 +1,270 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Upload,
|
||||
Calendar,
|
||||
FileText,
|
||||
CheckCircle,
|
||||
X,
|
||||
Users
|
||||
} from 'lucide-react'
|
||||
|
||||
// 模拟代理商列表
|
||||
const mockAgencies = [
|
||||
{ id: 'agency-001', name: '星耀传媒', creatorCount: 50, passRate: 92 },
|
||||
{ id: 'agency-002', name: '创意无限', creatorCount: 35, passRate: 88 },
|
||||
{ id: 'agency-003', name: '美妆达人MCN', creatorCount: 28, passRate: 82 },
|
||||
{ id: 'agency-004', name: '时尚风向标', creatorCount: 42, passRate: 85 },
|
||||
]
|
||||
|
||||
export default function CreateProjectPage() {
|
||||
const router = useRouter()
|
||||
const [projectName, setProjectName] = useState('')
|
||||
const [deadline, setDeadline] = useState('')
|
||||
const [briefFile, setBriefFile] = useState<File | null>(null)
|
||||
const [selectedAgencies, setSelectedAgencies] = useState<string[]>([])
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (file) {
|
||||
setBriefFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleAgency = (agencyId: string) => {
|
||||
setSelectedAgencies(prev =>
|
||||
prev.includes(agencyId)
|
||||
? prev.filter(id => id !== agencyId)
|
||||
: [...prev, agencyId]
|
||||
)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!projectName.trim() || !deadline || !briefFile || selectedAgencies.length === 0) {
|
||||
alert('请填写完整信息')
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
// 模拟提交
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
alert('项目创建成功!')
|
||||
router.push('/brand')
|
||||
}
|
||||
|
||||
const isValid = projectName.trim() && deadline && briefFile && selectedAgencies.length > 0
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-4xl">
|
||||
{/* 顶部导航 */}
|
||||
<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>
|
||||
<h1 className="text-2xl font-bold text-text-primary">创建项目</h1>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6 space-y-6">
|
||||
{/* 项目名称 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">
|
||||
项目名称 <span className="text-accent-coral">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={projectName}
|
||||
onChange={(e) => setProjectName(e.target.value)}
|
||||
placeholder="例如:XX品牌618推广"
|
||||
className="w-full px-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>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">
|
||||
截止日期 <span className="text-accent-coral">*</span>
|
||||
</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={deadline}
|
||||
onChange={(e) => setDeadline(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>
|
||||
|
||||
{/* Brief 上传 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">
|
||||
上传 Brief <span className="text-accent-coral">*</span>
|
||||
</label>
|
||||
<div className="border-2 border-dashed border-border-subtle rounded-lg p-8 text-center hover:border-accent-indigo/50 transition-colors">
|
||||
{briefFile ? (
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<FileText size={24} className="text-accent-indigo" />
|
||||
<span className="text-text-primary">{briefFile.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBriefFile(null)}
|
||||
className="p-1 hover:bg-bg-elevated rounded-full"
|
||||
>
|
||||
<X size={16} className="text-text-tertiary" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<label className="cursor-pointer">
|
||||
<Upload size={32} className="mx-auto text-text-tertiary mb-3" />
|
||||
<p className="text-text-secondary mb-1">点击或拖拽上传 Brief 文件</p>
|
||||
<p className="text-xs text-text-tertiary">支持 PDF、Word、Excel 格式</p>
|
||||
<input
|
||||
type="file"
|
||||
accept=".pdf,.doc,.docx,.xls,.xlsx"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 选择代理商 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">
|
||||
选择代理商 <span className="text-accent-coral">*</span>
|
||||
<span className="text-text-tertiary font-normal ml-2">
|
||||
已选择 {selectedAgencies.length} 个
|
||||
</span>
|
||||
</label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{mockAgencies.map((agency) => {
|
||||
const isSelected = selectedAgencies.includes(agency.id)
|
||||
return (
|
||||
<button
|
||||
key={agency.id}
|
||||
type="button"
|
||||
onClick={() => toggleAgency(agency.id)}
|
||||
className={`p-4 rounded-lg border-2 text-left transition-all ${
|
||||
isSelected
|
||||
? 'border-accent-indigo bg-accent-indigo/10'
|
||||
: 'border-border-subtle hover:border-accent-indigo/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-text-primary">{agency.name}</div>
|
||||
<div className="flex items-center gap-4 mt-1 text-sm text-text-secondary">
|
||||
<span className="flex items-center gap-1">
|
||||
<Users size={14} />
|
||||
{agency.creatorCount} 达人
|
||||
</span>
|
||||
<span className={agency.passRate >= 90 ? 'text-accent-green' : agency.passRate >= 80 ? 'text-accent-indigo' : 'text-orange-400'}>
|
||||
通过率 {agency.passRate}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<CheckCircle size={20} className="text-accent-indigo" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex items-center justify-end gap-4 pt-4 border-t border-border-subtle">
|
||||
<Button variant="secondary" onClick={() => router.back()}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!isValid || isSubmitting}
|
||||
>
|
||||
{isSubmitting ? '创建中...' : '创建项目'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user