feat: 完善代理商端业务逻辑与前后端框架

主要更新:
- 更新代理商端文档,明确项目由品牌方分配流程
- 新增Brief配置详情页(已配置)设计稿
- 完善工作台紧急待办中品牌新任务功能
- 整理Pencil设计文件中代理商端页面顺序
- 新增后端FastAPI框架及核心API
- 新增前端Next.js页面和组件库
- 添加.gitignore排除构建和缓存文件

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-02-05 19:27:31 +08:00
co-authored by Claude Opus 4.5
parent d52509d630
commit e4959d584f
132 changed files with 58539 additions and 21353 deletions
+181
View File
@@ -0,0 +1,181 @@
'use client'
import { useRouter, useParams } from 'next/navigation'
import { ArrowLeft, Download, Play } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
import { Button } from '@/components/ui/Button'
import { SuccessTag, WarningTag, ErrorTag, PendingTag } from '@/components/ui/Tag'
// 模拟任务详情
const mockTaskDetail = {
id: 'task-004',
videoTitle: '美食探店vlog',
creatorName: '吃货小胖',
brandName: '某餐饮品牌',
platform: '小红书',
status: 'approved',
aiScore: 95,
finalScore: 95,
aiSummary: '视频内容合规,无明显违规项',
submittedAt: '2024-02-04 10:00',
reviewedAt: '2024-02-04 12:00',
reviewerName: '审核员A',
reviewNotes: '内容积极正面,品牌露出合适,通过审核。',
softWarnings: [
{ id: 'w1', content: '品牌提及次数适中', suggestion: '可考虑适当增加品牌提及' },
],
timeline: [
{ time: '2024-02-04 10:00', event: '达人提交视频', actor: '吃货小胖' },
{ time: '2024-02-04 10:02', event: 'AI审核开始', actor: '系统' },
{ time: '2024-02-04 10:05', event: 'AI审核完成,得分95分', actor: '系统' },
{ time: '2024-02-04 12:00', event: '人工审核通过', actor: '审核员A' },
],
}
function StatusBadge({ status }: { status: string }) {
if (status === 'approved') return <SuccessTag></SuccessTag>
if (status === 'rejected') return <ErrorTag></ErrorTag>
if (status === 'pending_review') return <WarningTag></WarningTag>
return <PendingTag></PendingTag>
}
export default function TaskDetailPage() {
const router = useRouter()
const params = useParams()
const task = mockTaskDetail
return (
<div className="space-y-6">
{/* 顶部导航 */}
<div className="flex items-center gap-4">
<button type="button" onClick={() => router.back()} className="p-2 hover:bg-gray-100 rounded-full">
<ArrowLeft size={20} />
</button>
<div className="flex-1">
<div className="flex items-center gap-3">
<h1 className="text-xl font-bold text-gray-900">{task.videoTitle}</h1>
<StatusBadge status={task.status} />
</div>
<p className="text-sm text-gray-500">{task.creatorName} · {task.brandName} · {task.platform}</p>
</div>
<Button variant="secondary" icon={Download}></Button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 左侧:视频和基本信息 */}
<div className="lg:col-span-2 space-y-4">
<Card>
<CardContent className="p-0">
<div className="aspect-video bg-gray-900 rounded-t-lg flex items-center justify-center">
<button type="button" className="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center hover:bg-white/30">
<Play size={32} className="text-white ml-1" />
</button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader><CardTitle></CardTitle></CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-6">
<div>
<div className="text-sm text-gray-500">AI </div>
<div className={`text-3xl font-bold ${task.aiScore >= 80 ? 'text-green-600' : 'text-yellow-600'}`}>
{task.aiScore}
</div>
</div>
<div>
<div className="text-sm text-gray-500"></div>
<div className={`text-3xl font-bold ${task.finalScore >= 80 ? 'text-green-600' : 'text-yellow-600'}`}>
{task.finalScore}
</div>
</div>
</div>
<div className="mt-4 pt-4 border-t">
<div className="text-sm text-gray-500 mb-1">AI </div>
<p className="text-gray-700">{task.aiSummary}</p>
</div>
{task.reviewNotes && (
<div className="mt-4 pt-4 border-t">
<div className="text-sm text-gray-500 mb-1"></div>
<p className="text-gray-700">{task.reviewNotes}</p>
</div>
)}
</CardContent>
</Card>
{task.softWarnings.length > 0 && (
<Card>
<CardHeader><CardTitle></CardTitle></CardHeader>
<CardContent className="space-y-3">
{task.softWarnings.map((w) => (
<div key={w.id} className="p-3 bg-yellow-50 rounded-lg">
<p className="font-medium text-yellow-800">{w.content}</p>
<p className="text-sm text-yellow-600 mt-1">{w.suggestion}</p>
</div>
))}
</CardContent>
</Card>
)}
</div>
{/* 右侧:详细信息和时间线 */}
<div className="space-y-4">
<Card>
<CardHeader><CardTitle></CardTitle></CardHeader>
<CardContent className="space-y-3">
<div className="flex justify-between">
<span className="text-gray-500">ID</span>
<span className="text-gray-900 font-mono text-sm">{task.id}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500"></span>
<span className="text-gray-900">{task.creatorName}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500"></span>
<span className="text-gray-900">{task.brandName}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500"></span>
<span className="text-gray-900">{task.platform}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500"></span>
<span className="text-gray-900 text-sm">{task.submittedAt}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500"></span>
<span className="text-gray-900 text-sm">{task.reviewedAt}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500"></span>
<span className="text-gray-900">{task.reviewerName}</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader><CardTitle>线</CardTitle></CardHeader>
<CardContent>
<div className="space-y-4">
{task.timeline.map((item, index) => (
<div key={index} className="flex gap-3">
<div className="flex flex-col items-center">
<div className="w-3 h-3 bg-blue-500 rounded-full" />
{index < task.timeline.length - 1 && <div className="w-0.5 h-full bg-gray-200 mt-1" />}
</div>
<div className="flex-1 pb-4">
<p className="text-sm font-medium text-gray-900">{item.event}</p>
<p className="text-xs text-gray-500 mt-1">{item.time} · {item.actor}</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</div>
</div>
)
}