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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 19:27:31 +08:00

182 lines
7.6 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 { 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>
)
}