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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
@@ -8,6 +8,8 @@ import { Modal, ConfirmModal } from '@/components/ui/Modal'
|
||||
import { SuccessTag, WarningTag, ErrorTag, PendingTag } from '@/components/ui/Tag'
|
||||
import { ReviewSteps, getBrandReviewSteps } from '@/components/ui/ReviewSteps'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import { api } from '@/lib/api'
|
||||
import { USE_MOCK } from '@/contexts/AuthContext'
|
||||
import {
|
||||
ArrowLeft,
|
||||
FileText,
|
||||
@@ -21,11 +23,13 @@ import {
|
||||
Download,
|
||||
Shield,
|
||||
MessageSquare,
|
||||
MessageSquareWarning
|
||||
MessageSquareWarning,
|
||||
Loader2,
|
||||
} from 'lucide-react'
|
||||
import { FilePreview, FileInfoCard, FilePreviewModal, type FileInfo } from '@/components/ui/FilePreview'
|
||||
import type { TaskResponse } from '@/types/task'
|
||||
|
||||
// 模拟脚本任务数据
|
||||
// Mock 脚本任务数据(USE_MOCK 模式使用)
|
||||
const mockScriptTask = {
|
||||
id: 'script-001',
|
||||
title: '夏日护肤推广脚本',
|
||||
@@ -35,7 +39,6 @@ const mockScriptTask = {
|
||||
submittedAt: '2026-02-06 14:30',
|
||||
aiScore: 88,
|
||||
status: 'brand_reviewing',
|
||||
// 文件信息
|
||||
file: {
|
||||
id: 'file-001',
|
||||
fileName: '夏日护肤推广_脚本v2.docx',
|
||||
@@ -44,7 +47,6 @@ const mockScriptTask = {
|
||||
fileUrl: '/demo/scripts/script-001.docx',
|
||||
uploadedAt: '2026-02-06 14:30',
|
||||
} as FileInfo,
|
||||
// 申诉信息
|
||||
isAppeal: false,
|
||||
appealReason: '',
|
||||
scriptContent: {
|
||||
@@ -78,6 +80,69 @@ const mockScriptTask = {
|
||||
},
|
||||
}
|
||||
|
||||
// 从 TaskResponse 映射出页面所需的数据结构
|
||||
function mapTaskToView(task: TaskResponse) {
|
||||
const violations = (task.script_ai_result?.violations || []).map((v, idx) => ({
|
||||
id: `v-${idx}`,
|
||||
type: v.type,
|
||||
content: v.content,
|
||||
suggestion: v.suggestion,
|
||||
severity: v.severity,
|
||||
}))
|
||||
|
||||
const softWarnings = (task.script_ai_result?.soft_warnings || []).map((w, idx) => ({
|
||||
id: `w-${idx}`,
|
||||
type: w.type,
|
||||
content: w.content,
|
||||
suggestion: w.suggestion,
|
||||
}))
|
||||
|
||||
const fileExtension = task.script_file_name?.split('.').pop()?.toLowerCase() || ''
|
||||
const mimeTypeMap: Record<string, string> = {
|
||||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
doc: 'application/msword',
|
||||
pdf: 'application/pdf',
|
||||
txt: 'text/plain',
|
||||
rtf: 'application/rtf',
|
||||
}
|
||||
|
||||
const agencyResult = task.script_agency_status || 'pending'
|
||||
const agencyResultLabel = agencyResult === 'passed' ? '建议通过' : agencyResult === 'rejected' ? '建议驳回' : '待审核'
|
||||
|
||||
return {
|
||||
id: task.id,
|
||||
title: task.name,
|
||||
creatorName: task.creator.name,
|
||||
agencyName: task.agency.name,
|
||||
projectName: task.project.name,
|
||||
submittedAt: task.script_uploaded_at || task.created_at,
|
||||
aiScore: task.script_ai_score || 0,
|
||||
status: task.stage,
|
||||
file: {
|
||||
id: task.id,
|
||||
fileName: task.script_file_name || '未上传文件',
|
||||
fileSize: '',
|
||||
fileType: mimeTypeMap[fileExtension] || 'application/octet-stream',
|
||||
fileUrl: task.script_file_url || '',
|
||||
uploadedAt: task.script_uploaded_at || undefined,
|
||||
} as FileInfo,
|
||||
isAppeal: task.is_appeal,
|
||||
appealReason: task.appeal_reason || '',
|
||||
agencyReview: {
|
||||
reviewer: task.agency.name,
|
||||
result: agencyResult,
|
||||
resultLabel: agencyResultLabel,
|
||||
comment: task.script_agency_comment || '',
|
||||
reviewedAt: '',
|
||||
},
|
||||
aiAnalysis: {
|
||||
violations,
|
||||
softWarnings,
|
||||
sellingPoints: [] as Array<{ point: string; covered: boolean }>,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function ReviewProgressBar({ taskStatus }: { taskStatus: string }) {
|
||||
const steps = getBrandReviewSteps(taskStatus)
|
||||
const currentStep = steps.find(s => s.status === 'current')
|
||||
@@ -97,32 +162,142 @@ 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/2" />
|
||||
</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-20 bg-bg-elevated rounded-xl" />
|
||||
<div className="h-64 bg-bg-elevated rounded-xl" />
|
||||
<div className="h-32 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 BrandScriptReviewPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const toast = useToast()
|
||||
const taskId = params.id as string
|
||||
|
||||
const [showApproveModal, setShowApproveModal] = useState(false)
|
||||
const [showRejectModal, setShowRejectModal] = useState(false)
|
||||
const [rejectReason, setRejectReason] = useState('')
|
||||
const [viewMode, setViewMode] = useState<'file' | 'parsed'>('file')
|
||||
const [showFilePreview, setShowFilePreview] = useState(false)
|
||||
const [loading, setLoading] = useState(!USE_MOCK)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [taskData, setTaskData] = useState<ReturnType<typeof mapTaskToView> | null>(null)
|
||||
|
||||
const task = mockScriptTask
|
||||
// 加载任务数据
|
||||
const loadTask = useCallback(async () => {
|
||||
if (USE_MOCK) return
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await api.getTask(taskId)
|
||||
setTaskData(mapTaskToView(response))
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : '加载任务失败'
|
||||
toast.error(message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [taskId, toast])
|
||||
|
||||
const handleApprove = () => {
|
||||
setShowApproveModal(false)
|
||||
toast.success('审核通过')
|
||||
router.push('/brand/review')
|
||||
useEffect(() => {
|
||||
loadTask()
|
||||
}, [loadTask])
|
||||
|
||||
// USE_MOCK 模式下使用 mock 数据
|
||||
const task = USE_MOCK ? {
|
||||
...mockScriptTask,
|
||||
agencyReview: {
|
||||
...mockScriptTask.agencyReview,
|
||||
resultLabel: '建议通过',
|
||||
},
|
||||
aiAnalysis: {
|
||||
...mockScriptTask.aiAnalysis,
|
||||
softWarnings: [] as Array<{ id: string; type: string; content: string; suggestion: string }>,
|
||||
},
|
||||
} : taskData
|
||||
|
||||
const handleApprove = async () => {
|
||||
if (USE_MOCK) {
|
||||
setShowApproveModal(false)
|
||||
toast.success('审核通过')
|
||||
router.push('/brand/review')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(true)
|
||||
await api.reviewScript(taskId, { action: 'pass', comment: '' })
|
||||
setShowApproveModal(false)
|
||||
toast.success('审核通过')
|
||||
router.push('/brand/review')
|
||||
} catch (err) {
|
||||
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('/brand/review')
|
||||
|
||||
if (USE_MOCK) {
|
||||
setShowRejectModal(false)
|
||||
toast.success('已驳回')
|
||||
router.push('/brand/review')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(true)
|
||||
await api.reviewScript(taskId, { action: 'reject', comment: rejectReason })
|
||||
setShowRejectModal(false)
|
||||
toast.success('已驳回')
|
||||
router.push('/brand/review')
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : '操作失败'
|
||||
toast.error(message)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载中
|
||||
if (loading) {
|
||||
return <LoadingSkeleton />
|
||||
}
|
||||
|
||||
// 数据未加载到
|
||||
if (!task) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-20">
|
||||
<p className="text-text-secondary mb-4">任务数据加载失败</p>
|
||||
<Button variant="secondary" onClick={() => router.back()}>返回</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -199,10 +374,12 @@ export default function BrandScriptReviewPage() {
|
||||
{/* 左侧:脚本内容 */}
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
{/* 文件信息卡片 */}
|
||||
<FileInfoCard
|
||||
file={task.file}
|
||||
onPreview={() => setShowFilePreview(true)}
|
||||
/>
|
||||
{task.file.fileUrl && (
|
||||
<FileInfoCard
|
||||
file={task.file}
|
||||
onPreview={() => setShowFilePreview(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{viewMode === 'file' ? (
|
||||
<Card>
|
||||
@@ -213,7 +390,11 @@ export default function BrandScriptReviewPage() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FilePreview file={task.file} />
|
||||
{task.file.fileUrl ? (
|
||||
<FilePreview file={task.file} />
|
||||
) : (
|
||||
<p className="text-sm text-text-tertiary text-center py-8">暂无文件</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
@@ -226,22 +407,31 @@ export default function BrandScriptReviewPage() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
{USE_MOCK && 'scriptContent' in task ? (
|
||||
<>
|
||||
<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 as typeof mockScriptTask).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 as typeof mockScriptTask).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 as typeof mockScriptTask).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 as typeof mockScriptTask).scriptContent.closing}</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<FileText size={32} className="mx-auto text-text-tertiary mb-3" />
|
||||
<p className="text-sm text-text-tertiary">API 暂不支持解析内容预览,请切换到「原文件」查看</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
@@ -255,23 +445,35 @@ export default function BrandScriptReviewPage() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`p-2 rounded-full ${task.agencyReview.result === 'approved' ? 'bg-accent-green/20' : 'bg-accent-coral/20'}`}>
|
||||
{task.agencyReview.result === 'approved' ? (
|
||||
<CheckCircle size={20} className="text-accent-green" />
|
||||
) : (
|
||||
<XCircle size={20} className="text-accent-coral" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-medium text-text-primary">{task.agencyReview.reviewer}</span>
|
||||
<SuccessTag>建议通过</SuccessTag>
|
||||
{task.agencyReview.comment ? (
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`p-2 rounded-full ${task.agencyReview.result === 'passed' || task.agencyReview.result === 'approved' ? 'bg-accent-green/20' : 'bg-accent-coral/20'}`}>
|
||||
{task.agencyReview.result === 'passed' || task.agencyReview.result === 'approved' ? (
|
||||
<CheckCircle size={20} className="text-accent-green" />
|
||||
) : (
|
||||
<XCircle size={20} className="text-accent-coral" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-medium text-text-primary">{task.agencyReview.reviewer}</span>
|
||||
{(task.agencyReview.result === 'passed' || task.agencyReview.result === 'approved') ? (
|
||||
<SuccessTag>{task.agencyReview.resultLabel}</SuccessTag>
|
||||
) : task.agencyReview.result === 'rejected' ? (
|
||||
<ErrorTag>{task.agencyReview.resultLabel}</ErrorTag>
|
||||
) : (
|
||||
<PendingTag>{task.agencyReview.resultLabel}</PendingTag>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-text-secondary text-sm">{task.agencyReview.comment}</p>
|
||||
{task.agencyReview.reviewedAt && (
|
||||
<p className="text-xs text-text-tertiary mt-2">{task.agencyReview.reviewedAt}</p>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-text-secondary text-sm">{task.agencyReview.comment}</p>
|
||||
<p className="text-xs text-text-tertiary mt-2">{task.agencyReview.reviewedAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-text-tertiary text-center py-4">暂无代理商审核意见</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -304,7 +506,7 @@ export default function BrandScriptReviewPage() {
|
||||
<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>
|
||||
))}
|
||||
@@ -314,54 +516,81 @@ export default function BrandScriptReviewPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 合规检查 */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Shield size={16} className="text-accent-indigo" />
|
||||
合规检查
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{task.aiAnalysis.complianceChecks.map((check, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2 p-2 rounded-lg bg-bg-elevated">
|
||||
{check.passed ? (
|
||||
<CheckCircle size={16} className="text-accent-green flex-shrink-0 mt-0.5" />
|
||||
) : (
|
||||
<XCircle size={16} className="text-accent-coral flex-shrink-0 mt-0.5" />
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<span className="text-sm text-text-primary">{check.item}</span>
|
||||
{check.note && (
|
||||
<p className="text-xs text-text-tertiary mt-0.5">{check.note}</p>
|
||||
)}
|
||||
{/* 软性提醒 */}
|
||||
{task.aiAnalysis.softWarnings && task.aiAnalysis.softWarnings.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Shield size={16} className="text-accent-indigo" />
|
||||
软性提醒 ({task.aiAnalysis.softWarnings.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{task.aiAnalysis.softWarnings.map((w) => (
|
||||
<div key={w.id} className="p-3 bg-accent-indigo/10 rounded-lg border border-accent-indigo/30">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<PendingTag>{w.type}</PendingTag>
|
||||
</div>
|
||||
<p className="text-sm text-text-primary">{w.content}</p>
|
||||
<p className="text-xs text-accent-indigo mt-1">{w.suggestion}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 合规检查 - 仅 mock 模式显示 */}
|
||||
{USE_MOCK && 'complianceChecks' in task.aiAnalysis && (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Shield size={16} className="text-accent-indigo" />
|
||||
合规检查
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{(task.aiAnalysis as typeof mockScriptTask.aiAnalysis).complianceChecks.map((check, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2 p-2 rounded-lg bg-bg-elevated">
|
||||
{check.passed ? (
|
||||
<CheckCircle size={16} className="text-accent-green flex-shrink-0 mt-0.5" />
|
||||
) : (
|
||||
<XCircle size={16} className="text-accent-coral flex-shrink-0 mt-0.5" />
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<span className="text-sm text-text-primary">{check.item}</span>
|
||||
{check.note && (
|
||||
<p className="text-xs text-text-tertiary mt-0.5">{check.note}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 卖点覆盖 */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<CheckCircle size={16} className="text-accent-green" />
|
||||
卖点覆盖
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{task.aiAnalysis.sellingPoints.map((sp, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2 p-2 rounded-lg bg-bg-elevated">
|
||||
{sp.covered ? (
|
||||
<CheckCircle size={16} className="text-accent-green" />
|
||||
) : (
|
||||
<XCircle size={16} className="text-accent-coral" />
|
||||
)}
|
||||
<span className="text-sm text-text-primary">{sp.point}</span>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{task.aiAnalysis.sellingPoints.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<CheckCircle size={16} className="text-accent-green" />
|
||||
卖点覆盖
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{task.aiAnalysis.sellingPoints.map((sp, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2 p-2 rounded-lg bg-bg-elevated">
|
||||
{sp.covered ? (
|
||||
<CheckCircle size={16} className="text-accent-green" />
|
||||
) : (
|
||||
<XCircle size={16} className="text-accent-coral" />
|
||||
)}
|
||||
<span className="text-sm text-text-primary">{sp.point}</span>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -373,10 +602,12 @@ export default function BrandScriptReviewPage() {
|
||||
项目:{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" /> : null}
|
||||
驳回
|
||||
</Button>
|
||||
<Button variant="success" onClick={() => setShowApproveModal(true)}>
|
||||
<Button variant="success" onClick={() => setShowApproveModal(true)} disabled={submitting}>
|
||||
{submitting ? <Loader2 size={16} className="animate-spin" /> : null}
|
||||
通过
|
||||
</Button>
|
||||
</div>
|
||||
@@ -408,8 +639,11 @@ export default function BrandScriptReviewPage() {
|
||||
/>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user