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
+18
View File
@@ -0,0 +1,18 @@
'use client'
import { DesktopLayout } from '@/components/layout/DesktopLayout'
import { AuthGuard } from '@/components/auth/AuthGuard'
export default function AgencyLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<AuthGuard allowedRoles={['agency']}>
<DesktopLayout role="agency">
{children}
</DesktopLayout>
</AuthGuard>
)
}
+337
View File
@@ -0,0 +1,337 @@
'use client'
import { useState } from 'react'
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 {
AlertTriangle,
Clock,
CheckCircle,
XCircle,
ChevronRight,
FileVideo,
MessageSquare,
TrendingUp
} from 'lucide-react'
// 模拟统计数据
const stats = {
pendingReview: 12,
pendingAppeal: 3,
todayPassed: 28,
inProgress: 45,
}
// 模拟紧急待办
const urgentTodos = [
{
id: 'urgent-001',
type: 'violation',
title: '达人A视频 - 竞品露出',
description: 'XX品牌618推广',
time: '2小时前',
level: 'high',
},
{
id: 'urgent-002',
type: 'appeal',
title: '达人B申诉 - 待仲裁',
description: '对违禁词检测结果有异议',
time: '30分钟前',
level: 'medium',
},
{
id: 'urgent-003',
type: 'ai_done',
title: '达人C视频 - AI审核完成',
description: '新品口红试色',
time: '10分钟前',
level: 'low',
},
]
// 模拟项目概览
const projectOverview = [
{
id: 'proj-001',
name: 'XX品牌618推广',
total: 20,
submitted: 15,
passed: 10,
reviewing: 3,
needRevision: 2,
},
{
id: 'proj-002',
name: '新品口红系列',
total: 12,
submitted: 8,
passed: 6,
reviewing: 1,
needRevision: 1,
},
{
id: 'proj-003',
name: '护肤品秋季活动',
total: 15,
submitted: 12,
passed: 9,
reviewing: 2,
needRevision: 1,
},
]
// 模拟待审核任务列表
const pendingTasks = [
{
id: 'task-001',
videoTitle: '夏日护肤推广',
creatorName: '小美护肤',
brandName: 'XX品牌',
aiScore: 85,
submittedAt: '2026-02-04 14:30',
hasHighRisk: false,
},
{
id: 'task-002',
videoTitle: '新品口红试色',
creatorName: '美妆达人Lisa',
brandName: 'XX品牌',
aiScore: 72,
submittedAt: '2026-02-04 13:45',
hasHighRisk: true,
},
{
id: 'task-003',
videoTitle: '健身器材开箱',
creatorName: '健身教练王',
brandName: 'XX运动',
aiScore: 68,
submittedAt: '2026-02-04 14:50',
hasHighRisk: true,
},
]
function UrgentLevelIcon({ level }: { level: string }) {
if (level === 'high') return <AlertTriangle size={16} className="text-red-500" />
if (level === 'medium') return <MessageSquare size={16} className="text-orange-500" />
return <CheckCircle size={16} className="text-yellow-500" />
}
export default function AgencyDashboard() {
return (
<div className="space-y-6">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-text-primary"></h1>
<div className="text-sm text-text-secondary">{new Date().toLocaleString('zh-CN')}</div>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="bg-gradient-to-br from-accent-coral/20 to-bg-card border-accent-coral/30">
<CardContent className="py-4">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-text-secondary"></div>
<div className="text-3xl font-bold text-accent-coral">{stats.pendingReview}</div>
</div>
<div className="w-12 h-12 rounded-full bg-accent-coral/20 flex items-center justify-center">
<Clock size={24} className="text-accent-coral" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-orange-500/20 to-bg-card border-orange-500/30">
<CardContent className="py-4">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-text-secondary"></div>
<div className="text-3xl font-bold text-orange-400">{stats.pendingAppeal}</div>
</div>
<div className="w-12 h-12 rounded-full bg-orange-500/20 flex items-center justify-center">
<MessageSquare size={24} className="text-orange-400" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-accent-green/20 to-bg-card border-accent-green/30">
<CardContent className="py-4">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-text-secondary"></div>
<div className="text-3xl font-bold text-accent-green">{stats.todayPassed}</div>
</div>
<div className="w-12 h-12 rounded-full bg-accent-green/20 flex items-center justify-center">
<CheckCircle size={24} className="text-accent-green" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-accent-indigo/20 to-bg-card border-accent-indigo/30">
<CardContent className="py-4">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-text-secondary"></div>
<div className="text-3xl font-bold text-accent-indigo">{stats.inProgress}</div>
</div>
<div className="w-12 h-12 rounded-full bg-accent-indigo/20 flex items-center justify-center">
<FileVideo size={24} className="text-accent-indigo" />
</div>
</div>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 紧急待办 */}
<Card className="lg:col-span-1">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<AlertTriangle size={18} className="text-red-500" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{urgentTodos.map((todo) => (
<Link
key={todo.id}
href={todo.type === 'violation' || todo.type === 'ai_done' ? `/agency/review/${todo.id}` : `/agency/appeals/${todo.id}`}
className="block p-3 rounded-lg border border-border-subtle hover:border-accent-indigo hover:bg-accent-indigo/5 transition-colors"
>
<div className="flex items-start gap-3">
<UrgentLevelIcon level={todo.level} />
<div className="flex-1 min-w-0">
<div className="font-medium text-text-primary truncate">{todo.title}</div>
<div className="text-sm text-text-secondary">{todo.description}</div>
<div className="text-xs text-text-tertiary mt-1">{todo.time}</div>
</div>
<ChevronRight size={16} className="text-text-tertiary flex-shrink-0" />
</div>
</Link>
))}
</CardContent>
</Card>
{/* 项目概览 */}
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp size={18} className="text-blue-500" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{projectOverview.map((project) => (
<div key={project.id} className="p-4 rounded-lg bg-bg-elevated">
<div className="flex items-center justify-between mb-3">
<span className="font-medium text-text-primary">{project.name}</span>
<span className="text-sm text-text-secondary">
{project.submitted}/{project.total}
</span>
</div>
<div className="flex h-3 rounded-full overflow-hidden bg-bg-page">
<div
className="bg-accent-green transition-all"
style={{ width: `${(project.passed / project.total) * 100}%` }}
title={`已通过: ${project.passed}`}
/>
<div
className="bg-accent-indigo transition-all"
style={{ width: `${(project.reviewing / project.total) * 100}%` }}
title={`审核中: ${project.reviewing}`}
/>
<div
className="bg-orange-500 transition-all"
style={{ width: `${(project.needRevision / project.total) * 100}%` }}
title={`需修改: ${project.needRevision}`}
/>
</div>
<div className="flex gap-4 mt-2 text-xs text-text-secondary">
<span className="flex items-center gap-1">
<span className="w-2 h-2 bg-accent-green rounded-full" />
{project.passed}
</span>
<span className="flex items-center gap-1">
<span className="w-2 h-2 bg-accent-indigo rounded-full" />
{project.reviewing}
</span>
<span className="flex items-center gap-1">
<span className="w-2 h-2 bg-orange-500 rounded-full" />
{project.needRevision}
</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* 待审核列表 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span></span>
<Link href="/agency/tasks">
<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">AI评分</th>
<th className="pb-3 font-medium"></th>
<th className="pb-3 font-medium"></th>
</tr>
</thead>
<tbody>
{pendingTasks.map((task) => (
<tr key={task.id} className="border-b border-border-subtle last:border-0 hover:bg-bg-elevated">
<td className="py-4">
<div className="flex items-center gap-2">
<div className="font-medium text-text-primary">{task.videoTitle}</div>
{task.hasHighRisk && (
<span className="px-1.5 py-0.5 text-xs bg-accent-coral/20 text-accent-coral rounded">
</span>
)}
</div>
</td>
<td className="py-4 text-text-secondary">{task.creatorName}</td>
<td className="py-4 text-text-secondary">{task.brandName}</td>
<td className="py-4">
<span className={`font-medium ${
task.aiScore >= 80 ? 'text-accent-green' : task.aiScore >= 60 ? 'text-yellow-400' : 'text-accent-coral'
}`}>
{task.aiScore}
</span>
</td>
<td className="py-4 text-sm text-text-tertiary">{task.submittedAt}</td>
<td className="py-4">
<Link href={`/agency/review/${task.id}`}>
<Button size="sm"></Button>
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
)
}
+367
View File
@@ -0,0 +1,367 @@
'use client'
import { useState } from 'react'
import { useRouter, useParams } from 'next/navigation'
import { ArrowLeft, Play, Pause, AlertTriangle, Shield, Radio } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
import { Button } from '@/components/ui/Button'
import { SuccessTag, WarningTag, ErrorTag } from '@/components/ui/Tag'
import { Modal, ConfirmModal } from '@/components/ui/Modal'
import { ReviewSteps, getAgencyReviewSteps } from '@/components/ui/ReviewSteps'
// 模拟审核任务数据
const mockTask = {
id: 'task-001',
videoTitle: '夏日护肤推广',
creatorName: '小美护肤',
brandName: 'XX护肤品牌',
platform: '抖音',
aiScore: 85,
aiSummary: '视频整体合规,发现2处硬性问题和1处舆情提示需人工确认',
reviewSteps: [
{ key: 'submitted', label: '已提交', status: 'done' as const, time: '2/3 10:30' },
{ key: 'ai_review', label: 'AI审核', status: 'done' as const, time: '2/3 10:35' },
{ key: 'agent_review', label: '代理商审核', status: 'current' as const },
{ key: 'final', label: '最终结果', status: 'pending' as const },
],
hardViolations: [
{
id: 'v1',
type: '违禁词',
content: '效果最好',
timestamp: 15.5,
source: 'speech',
riskLevel: 'high',
aiConfidence: 0.95,
suggestion: '建议替换为"效果显著"',
},
{
id: 'v2',
type: '竞品露出',
content: '疑似竞品Logo',
timestamp: 42.0,
source: 'visual',
riskLevel: 'high',
aiConfidence: 0.72,
suggestion: '需人工确认是否为竞品露出',
},
],
sentimentWarnings: [
{ id: 's1', type: '油腻预警', timestamp: 42.0, content: '达人表情过于夸张,建议检查', riskLevel: 'medium' },
],
}
function ReviewProgressBar({ taskStatus }: { taskStatus: string }) {
const steps = getAgencyReviewSteps(taskStatus)
const currentStep = steps.find(s => s.status === 'current')
return (
<Card className="mb-6">
<CardContent className="py-4">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-medium text-text-primary"></span>
<span className="text-sm text-accent-indigo font-medium">
{currentStep?.label || '代理商审核'}
</span>
</div>
<ReviewSteps steps={steps} />
</CardContent>
</Card>
)
}
function RiskLevelTag({ level }: { level: string }) {
if (level === 'high') return <ErrorTag></ErrorTag>
if (level === 'medium') return <WarningTag></WarningTag>
return <SuccessTag></SuccessTag>
}
function formatTimestamp(seconds: number): string {
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs.toString().padStart(2, '0')}`
}
export default function ReviewPage() {
const router = useRouter()
const params = useParams()
const [isPlaying, setIsPlaying] = useState(false)
const [showApproveModal, setShowApproveModal] = useState(false)
const [showRejectModal, setShowRejectModal] = useState(false)
const [showForcePassModal, setShowForcePassModal] = useState(false)
const [rejectReason, setRejectReason] = useState('')
const [forcePassReason, setForcePassReason] = useState('')
const [saveAsException, setSaveAsException] = useState(false)
const [checkedViolations, setCheckedViolations] = useState<Record<string, boolean>>({})
const task = mockTask
const handleApprove = () => {
setShowApproveModal(false)
router.push('/agency')
}
const handleReject = () => {
if (!rejectReason.trim()) {
alert('请填写驳回原因')
return
}
setShowRejectModal(false)
router.push('/agency')
}
const handleForcePass = () => {
if (!forcePassReason.trim()) {
alert('请填写强制通过原因')
return
}
setShowForcePassModal(false)
router.push('/agency')
}
// 计算问题时间点用于进度条展示
const timelineMarkers = [
...task.hardViolations.map(v => ({ time: v.timestamp, type: 'hard' as const })),
...task.sentimentWarnings.map(w => ({ time: w.timestamp, type: 'soft' as const })),
].sort((a, b) => a.time - b.time)
return (
<div className="space-y-4">
{/* 顶部导航 */}
<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-xl font-bold text-text-primary">{task.videoTitle}</h1>
<p className="text-sm text-text-secondary">{task.creatorName} · {task.brandName} · {task.platform}</p>
</div>
</div>
{/* 审核流程进度条 */}
<ReviewProgressBar taskStatus="agent_reviewing" />
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
{/* 左侧:视频播放器 (3/5) */}
<div className="lg:col-span-3 space-y-4">
<Card>
<CardContent className="p-0">
<div className="aspect-video bg-gray-900 rounded-t-lg flex items-center justify-center relative">
<button
type="button"
className="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center hover:bg-white/30 transition-colors"
onClick={() => setIsPlaying(!isPlaying)}
>
{isPlaying ? <Pause size={32} className="text-white" /> : <Play size={32} className="text-white ml-1" />}
</button>
</div>
{/* 智能进度条 */}
<div className="p-4 border-t border-border-subtle">
<div className="text-sm font-medium text-text-primary mb-3"></div>
<div className="relative h-3 bg-bg-elevated rounded-full">
{/* 时间标记点 */}
{timelineMarkers.map((marker, idx) => (
<button
key={idx}
type="button"
className={`absolute top-1/2 -translate-y-1/2 w-4 h-4 rounded-full border-2 border-bg-card shadow-md cursor-pointer transition-transform hover:scale-125 ${
marker.type === 'hard' ? 'bg-accent-coral' : 'bg-orange-500'
}`}
style={{ left: `${(marker.time / 120) * 100}%` }}
title={`${formatTimestamp(marker.time)} - ${marker.type === 'hard' ? '硬性问题' : '舆情提示'}`}
/>
))}
</div>
<div className="flex justify-between text-xs text-text-tertiary mt-1">
<span>0:00</span>
<span>2:00</span>
</div>
<div className="flex gap-4 mt-3 text-xs text-text-secondary">
<span className="flex items-center gap-1">
<span className="w-3 h-3 bg-accent-coral rounded-full" />
</span>
<span className="flex items-center gap-1">
<span className="w-3 h-3 bg-orange-500 rounded-full" />
</span>
<span className="flex items-center gap-1">
<span className="w-3 h-3 bg-accent-green rounded-full" />
</span>
</div>
</div>
</CardContent>
</Card>
{/* AI 分析总结 */}
<Card>
<CardContent className="py-4">
<div className="flex items-center justify-between mb-2">
<span className="font-medium text-text-primary">AI </span>
<span className={`text-xl font-bold ${task.aiScore >= 80 ? 'text-accent-green' : 'text-yellow-400'}`}>
{task.aiScore}
</span>
</div>
<p className="text-text-secondary text-sm">{task.aiSummary}</p>
</CardContent>
</Card>
</div>
{/* 右侧:AI 检查单 (2/5) */}
<div className="lg:col-span-2 space-y-4">
{/* 硬性合规 */}
<Card>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base">
<Shield size={16} className="text-red-500" />
({task.hardViolations.length})
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{task.hardViolations.map((v) => (
<div key={v.id} className={`p-3 rounded-lg border ${checkedViolations[v.id] ? 'bg-bg-elevated border-border-subtle' : 'bg-accent-coral/10 border-accent-coral/30'}`}>
<div className="flex items-start gap-2">
<input
type="checkbox"
checked={checkedViolations[v.id] || false}
onChange={() => setCheckedViolations((prev) => ({ ...prev, [v.id]: !prev[v.id] }))}
className="mt-1 accent-accent-indigo"
/>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<ErrorTag>{v.type}</ErrorTag>
<span className="text-xs text-text-tertiary">{formatTimestamp(v.timestamp)}</span>
</div>
<p className="text-sm font-medium text-text-primary">{v.content}</p>
<p className="text-xs text-accent-indigo mt-1">{v.suggestion}</p>
</div>
</div>
</div>
))}
</CardContent>
</Card>
{/* 舆情雷达 */}
{task.sentimentWarnings.length > 0 && (
<Card>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base">
<Radio size={16} className="text-orange-500" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{task.sentimentWarnings.map((w) => (
<div key={w.id} className="p-3 bg-orange-500/10 rounded-lg border border-orange-500/30">
<div className="flex items-center gap-2 mb-1">
<WarningTag>{w.type}</WarningTag>
<span className="text-xs text-text-tertiary">{formatTimestamp(w.timestamp)}</span>
</div>
<p className="text-sm text-orange-400">{w.content}</p>
<p className="text-xs text-text-tertiary mt-1"> </p>
</div>
))}
</CardContent>
</Card>
)}
</div>
</div>
{/* 底部决策栏 */}
<Card className="sticky bottom-4 shadow-lg">
<CardContent className="py-4">
<div className="flex items-center justify-between">
<div className="text-sm text-text-secondary">
{Object.values(checkedViolations).filter(Boolean).length}/{task.hardViolations.length}
</div>
<div className="flex gap-3">
<Button variant="danger" onClick={() => setShowRejectModal(true)}>
</Button>
<Button variant="secondary" onClick={() => setShowForcePassModal(true)}>
</Button>
<Button variant="success" onClick={() => setShowApproveModal(true)}>
</Button>
</div>
</div>
</CardContent>
</Card>
{/* 通过确认弹窗 */}
<ConfirmModal
isOpen={showApproveModal}
onClose={() => setShowApproveModal(false)}
onConfirm={handleApprove}
title="确认通过"
message="确定要通过此视频的审核吗?通过后达人将收到通知。"
confirmText="确认通过"
/>
{/* 驳回弹窗 */}
<Modal isOpen={showRejectModal} onClose={() => setShowRejectModal(false)} title="驳回审核">
<div className="space-y-4">
<p className="text-text-secondary text-sm"></p>
<div className="p-3 bg-bg-elevated rounded-lg">
<p className="text-sm font-medium text-text-primary mb-2"> ({Object.values(checkedViolations).filter(Boolean).length})</p>
{task.hardViolations.filter(v => checkedViolations[v.id]).map(v => (
<div key={v.id} className="text-sm text-text-secondary"> {v.type}: {v.content}</div>
))}
{Object.values(checkedViolations).filter(Boolean).length === 0 && (
<div className="text-sm text-text-tertiary"></div>
)}
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1"></label>
<textarea
className="w-full h-24 p-3 border border-border-subtle rounded-lg resize-none bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
placeholder="请详细说明驳回原因..."
value={rejectReason}
onChange={(e) => setRejectReason(e.target.value)}
/>
</div>
<div className="flex gap-3 justify-end">
<Button variant="ghost" onClick={() => setShowRejectModal(false)}></Button>
<Button variant="danger" onClick={handleReject}></Button>
</div>
</div>
</Modal>
{/* 强制通过弹窗 */}
<Modal isOpen={showForcePassModal} onClose={() => setShowForcePassModal(false)} title="强制通过">
<div className="space-y-4">
<div className="p-3 bg-yellow-500/10 rounded-lg border border-yellow-500/30">
<p className="text-sm text-yellow-400">
<AlertTriangle size={14} className="inline mr-1" />
</p>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1"></label>
<textarea
className="w-full h-24 p-3 border border-border-subtle rounded-lg resize-none bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
placeholder="例如:达人玩的新梗,品牌方认可"
value={forcePassReason}
onChange={(e) => setForcePassReason(e.target.value)}
/>
</div>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={saveAsException}
onChange={(e) => setSaveAsException(e.target.checked)}
className="rounded accent-accent-indigo"
/>
<span className="text-sm text-text-secondary"></span>
</label>
<div className="flex gap-3 justify-end">
<Button variant="ghost" onClick={() => setShowForcePassModal(false)}></Button>
<Button onClick={handleForcePass}></Button>
</div>
</div>
</Modal>
</div>
)
}
+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>
)
}