feat: 前端剩余页面全面对接后端 API(Phase 2 完成)

为品牌方端(8页)、代理商端(10页)、达人端(6页)共24个页面添加真实API调用:
- 每页新增 USE_MOCK 条件分支,开发环境使用 mock 数据,生产环境调用真实 API
- 添加 loading 骨架屏、error toast 提示、submitting 状态管理
- 数据映射:TaskResponse → 页面视图模型,处理类型差异
- 审核操作(通过/驳回/强制通过)对接 api.reviewScript/reviewVideo
- Brief/规则/AI配置对接 api.getBrief/updateBrief/listForbiddenWords 等
- 申诉/历史/额度管理对接 api.listTasks + 状态过滤映射

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-02-09 16:29:43 +08:00
co-authored by Claude Opus 4.6
parent 54eaa54966
commit a8be7bbca9
24 changed files with 5244 additions and 1845 deletions
+192 -27
View File
@@ -1,6 +1,6 @@
'use client'
import { useState } from 'react'
import { useState, useEffect, useCallback } from 'react'
import { useRouter, useParams } from 'next/navigation'
import { useToast } from '@/components/ui/Toast'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
@@ -19,9 +19,13 @@ import {
Eye,
Shield,
Download,
MessageSquareWarning
MessageSquareWarning,
Loader2
} from 'lucide-react'
import { FilePreview, FileInfoCard, FilePreviewModal, type FileInfo } from '@/components/ui/FilePreview'
import { api } from '@/lib/api'
import { USE_MOCK } from '@/contexts/AuthContext'
import type { TaskResponse } from '@/types/task'
// 模拟脚本任务数据
const mockScriptTask = {
@@ -70,6 +74,53 @@ const mockScriptTask = {
},
}
// 从 TaskResponse 映射到页面视图模型
function mapTaskToViewModel(task: TaskResponse) {
return {
id: task.id,
title: task.name,
creatorName: task.creator?.name || '未知达人',
projectName: task.project?.name || '未知项目',
submittedAt: task.script_uploaded_at || task.created_at,
aiScore: task.script_ai_score ?? 0,
status: task.stage,
file: {
id: `file-${task.id}`,
fileName: task.script_file_name || '未知文件',
fileSize: '',
fileType: 'application/octet-stream',
fileUrl: task.script_file_url || '',
uploadedAt: task.script_uploaded_at || task.created_at,
} as FileInfo,
isAppeal: task.is_appeal,
appealReason: task.appeal_reason || '',
scriptContent: {
opening: '',
productIntro: '',
demo: '',
closing: '',
},
aiAnalysis: {
violations: (task.script_ai_result?.violations || []).map((v, idx) => ({
id: `v${idx + 1}`,
type: v.type,
content: v.content,
suggestion: v.suggestion,
severity: v.severity,
})),
complianceChecks: (task.script_ai_result?.soft_warnings || []).map((w) => ({
item: w.type,
passed: false,
note: w.content,
})),
sellingPoints: [] as Array<{ point: string; covered: boolean }>,
},
aiSummary: task.script_ai_result?.summary || '',
}
}
type ScriptTaskViewModel = ReturnType<typeof mapTaskToViewModel>
function ReviewProgressBar({ taskStatus }: { taskStatus: string }) {
const steps = getAgencyReviewSteps(taskStatus)
const currentStep = steps.find(s => s.status === 'current')
@@ -89,10 +140,40 @@ function ReviewProgressBar({ taskStatus }: { taskStatus: string }) {
)
}
function LoadingSkeleton() {
return (
<div className="space-y-4 animate-pulse">
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-bg-elevated rounded-full" />
<div className="flex-1 space-y-2">
<div className="h-6 bg-bg-elevated rounded w-1/3" />
<div className="h-4 bg-bg-elevated rounded w-1/4" />
</div>
</div>
<div className="h-16 bg-bg-elevated rounded-xl" />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-4">
<div className="h-32 bg-bg-elevated rounded-xl" />
<div className="h-64 bg-bg-elevated rounded-xl" />
</div>
<div className="space-y-4">
<div className="h-20 bg-bg-elevated rounded-xl" />
<div className="h-40 bg-bg-elevated rounded-xl" />
<div className="h-40 bg-bg-elevated rounded-xl" />
</div>
</div>
</div>
)
}
export default function AgencyScriptReviewPage() {
const router = useRouter()
const toast = useToast()
const params = useParams()
const taskId = params.id as string
const [loading, setLoading] = useState(!USE_MOCK)
const [submitting, setSubmitting] = useState(false)
const [showApproveModal, setShowApproveModal] = useState(false)
const [showRejectModal, setShowRejectModal] = useState(false)
const [showForcePassModal, setShowForcePassModal] = useState(false)
@@ -100,33 +181,99 @@ export default function AgencyScriptReviewPage() {
const [forcePassReason, setForcePassReason] = useState('')
const [viewMode, setViewMode] = useState<'file' | 'parsed'>('file') // 'file' 显示原文件, 'parsed' 显示解析内容
const [showFilePreview, setShowFilePreview] = useState(false)
const [task, setTask] = useState<ScriptTaskViewModel>(mockScriptTask as unknown as ScriptTaskViewModel)
const task = mockScriptTask
const loadTask = useCallback(async () => {
if (USE_MOCK) return
setLoading(true)
try {
const data = await api.getTask(taskId)
setTask(mapTaskToViewModel(data))
} catch (err: unknown) {
const message = err instanceof Error ? err.message : '加载任务详情失败'
toast.error(message)
} finally {
setLoading(false)
}
}, [taskId, toast])
const handleApprove = () => {
setShowApproveModal(false)
toast.success('已提交品牌方终审')
router.push('/agency/review')
useEffect(() => {
loadTask()
}, [loadTask])
const handleApprove = async () => {
if (USE_MOCK) {
setShowApproveModal(false)
toast.success('已提交品牌方终审')
router.push('/agency/review')
return
}
setSubmitting(true)
try {
await api.reviewScript(taskId, { action: 'pass' })
setShowApproveModal(false)
toast.success('已提交品牌方终审')
router.push('/agency/review')
} catch (err: unknown) {
const message = err instanceof Error ? err.message : '操作失败'
toast.error(message)
} finally {
setSubmitting(false)
}
}
const handleReject = () => {
const handleReject = async () => {
if (!rejectReason.trim()) {
toast.error('请填写驳回原因')
return
}
setShowRejectModal(false)
toast.success('已驳回')
router.push('/agency/review')
if (USE_MOCK) {
setShowRejectModal(false)
toast.success('已驳回')
router.push('/agency/review')
return
}
setSubmitting(true)
try {
await api.reviewScript(taskId, { action: 'reject', comment: rejectReason })
setShowRejectModal(false)
toast.success('已驳回')
router.push('/agency/review')
} catch (err: unknown) {
const message = err instanceof Error ? err.message : '操作失败'
toast.error(message)
} finally {
setSubmitting(false)
}
}
const handleForcePass = () => {
const handleForcePass = async () => {
if (!forcePassReason.trim()) {
toast.error('请填写强制通过原因')
return
}
setShowForcePassModal(false)
toast.success('已强制通过并提交品牌方终审')
router.push('/agency/review')
if (USE_MOCK) {
setShowForcePassModal(false)
toast.success('已强制通过并提交品牌方终审')
router.push('/agency/review')
return
}
setSubmitting(true)
try {
await api.reviewScript(taskId, { action: 'force_pass', comment: forcePassReason })
setShowForcePassModal(false)
toast.success('已强制通过并提交品牌方终审')
router.push('/agency/review')
} catch (err: unknown) {
const message = err instanceof Error ? err.message : '操作失败'
toast.error(message)
} finally {
setSubmitting(false)
}
}
if (loading) {
return <LoadingSkeleton />
}
return (
@@ -226,21 +373,27 @@ export default function AgencyScriptReviewPage() {
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{task.aiSummary ? (
<div className="p-4 bg-bg-elevated rounded-lg">
<div className="text-xs text-accent-indigo font-medium mb-2">AI </div>
<p className="text-text-primary">{task.aiSummary}</p>
</div>
) : null}
<div className="p-4 bg-bg-elevated rounded-lg">
<div className="text-xs text-accent-indigo font-medium mb-2"></div>
<p className="text-text-primary">{task.scriptContent.opening}</p>
<p className="text-text-primary">{task.scriptContent.opening || '(无内容)'}</p>
</div>
<div className="p-4 bg-bg-elevated rounded-lg">
<div className="text-xs text-purple-400 font-medium mb-2"></div>
<p className="text-text-primary">{task.scriptContent.productIntro}</p>
<p className="text-text-primary">{task.scriptContent.productIntro || '(无内容)'}</p>
</div>
<div className="p-4 bg-bg-elevated rounded-lg">
<div className="text-xs text-orange-400 font-medium mb-2">使</div>
<p className="text-text-primary">{task.scriptContent.demo}</p>
<p className="text-text-primary">{task.scriptContent.demo || '(无内容)'}</p>
</div>
<div className="p-4 bg-bg-elevated rounded-lg">
<div className="text-xs text-accent-green font-medium mb-2"></div>
<p className="text-text-primary">{task.scriptContent.closing}</p>
<p className="text-text-primary">{task.scriptContent.closing || '(无内容)'}</p>
</div>
</CardContent>
</Card>
@@ -275,7 +428,7 @@ export default function AgencyScriptReviewPage() {
<div className="flex items-center gap-2 mb-1">
<WarningTag>{v.type}</WarningTag>
</div>
<p className="text-sm text-text-primary">{v.content}</p>
<p className="text-sm text-text-primary">{v.content}</p>
<p className="text-xs text-accent-indigo mt-1">{v.suggestion}</p>
</div>
))}
@@ -331,6 +484,9 @@ export default function AgencyScriptReviewPage() {
<span className="text-sm text-text-primary">{sp.point}</span>
</div>
))}
{task.aiAnalysis.sellingPoints.length === 0 && (
<p className="text-sm text-text-tertiary text-center py-4"></p>
)}
</CardContent>
</Card>
</div>
@@ -344,13 +500,16 @@ export default function AgencyScriptReviewPage() {
{task.projectName}
</div>
<div className="flex gap-3">
<Button variant="danger" onClick={() => setShowRejectModal(true)}>
<Button variant="danger" onClick={() => setShowRejectModal(true)} disabled={submitting}>
{submitting ? <Loader2 size={16} className="animate-spin mr-1" /> : null}
</Button>
<Button variant="secondary" onClick={() => setShowForcePassModal(true)}>
<Button variant="secondary" onClick={() => setShowForcePassModal(true)} disabled={submitting}>
{submitting ? <Loader2 size={16} className="animate-spin mr-1" /> : null}
</Button>
<Button variant="success" onClick={() => setShowApproveModal(true)}>
<Button variant="success" onClick={() => setShowApproveModal(true)} disabled={submitting}>
{submitting ? <Loader2 size={16} className="animate-spin mr-1" /> : null}
</Button>
</div>
@@ -382,8 +541,11 @@ export default function AgencyScriptReviewPage() {
/>
</div>
<div className="flex gap-3 justify-end">
<Button variant="ghost" onClick={() => setShowRejectModal(false)}></Button>
<Button variant="danger" onClick={handleReject}></Button>
<Button variant="ghost" onClick={() => setShowRejectModal(false)} disabled={submitting}></Button>
<Button variant="danger" onClick={handleReject} disabled={submitting}>
{submitting ? <Loader2 size={16} className="animate-spin mr-1" /> : null}
</Button>
</div>
</div>
</Modal>
@@ -407,8 +569,11 @@ export default function AgencyScriptReviewPage() {
/>
</div>
<div className="flex gap-3 justify-end">
<Button variant="ghost" onClick={() => setShowForcePassModal(false)}></Button>
<Button onClick={handleForcePass}></Button>
<Button variant="ghost" onClick={() => setShowForcePassModal(false)} disabled={submitting}></Button>
<Button onClick={handleForcePass} disabled={submitting}>
{submitting ? <Loader2 size={16} className="animate-spin mr-1" /> : null}
</Button>
</div>
</div>
</Modal>