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:
co-authored by
Claude Opus 4.6
parent
54eaa54966
commit
a8be7bbca9
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user