Your Name 0ef7650c09 feat: 审核体系全面改造 — 多维度评分 + 卖点优先级 + AI 语义匹配 + 品牌方 AI 状态通知
后端:
- 审核结果拆分为 4 个独立维度 (法规合规/平台规则/品牌安全/Brief匹配度)
- 卖点优先级从 required:bool 改为三级 (core/recommended/reference)
- AI 语义匹配卖点覆盖 + AI 整体 Brief 匹配度分析
- BriefMatchDetail 评分详情 (覆盖率+亮点+问题点)
- min_selling_points 代理商可配置最少卖点数 + Alembic 迁移
- AI 语境复核过滤误报
- Brief AI 解析 + 规则 AI 解析
- AI 未配置/异常时通知品牌方
- 种子数据更新 (新格式审核结果+brief_match_detail)

前端:
- 三端审核页面展示四维度评分卡片
- 卖点编辑改为三级优先级选择器
- BriefMatchDetail 展示 (覆盖率进度条+亮点+问题)
- min_selling_points 配置 UI
- AI 配置页未配置时静默处理
- 文件预览/下载/签名 URL 优化

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 19:11:54 +08:00

719 lines
29 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 { 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'
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,
CheckCircle,
XCircle,
AlertTriangle,
User,
Building,
Clock,
Eye,
Download,
Shield,
MessageSquare,
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: '夏日护肤推广脚本',
creatorName: '小美护肤',
agencyName: '星耀传媒',
projectName: 'XX品牌618推广',
submittedAt: '2026-02-06 14:30',
aiScore: 88,
status: 'brand_reviewing',
file: {
id: 'file-001',
fileName: '夏日护肤推广_脚本v2.docx',
fileSize: '245 KB',
fileType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
fileUrl: '/demo/scripts/script-001.docx',
uploadedAt: '2026-02-06 14:30',
} as FileInfo,
isAppeal: false,
appealReason: '',
scriptContent: {
opening: '大家好!今天给大家分享一款超级好用的夏日护肤神器~',
productIntro: '这款XX品牌的防晒霜SPF50+PA++++,真的是夏天出门必备!质地轻薄不油腻,涂上去清清爽爽的。',
demo: '我先在手背上试一下,大家看,延展性特别好,轻轻一抹就推开了,完全不会搓泥。',
closing: '夏天防晒真的很重要姐妹们赶紧冲链接在小黄车1号链接',
},
agencyReview: {
reviewer: '张经理',
result: 'approved',
comment: '脚本整体符合要求,卖点覆盖完整,建议通过。',
reviewedAt: '2026-02-06 15:00',
},
aiAnalysis: {
violations: [
{ id: 'v1', type: '违禁词', content: '神器', suggestion: '建议替换为"好物"或"必备品"', severity: 'medium', dimension: 'legal' },
],
complianceChecks: [
{ item: '品牌名称正确', passed: true },
{ item: 'SPF标注准确', passed: true },
{ item: '无绝对化用语', passed: false, note: '"超级好用"建议修改' },
{ item: '引导语规范', passed: true },
],
dimensions: {
legal: { score: 85, passed: true, issue_count: 1 },
platform: { score: 100, passed: true, issue_count: 0 },
brand_safety: { score: 100, passed: true, issue_count: 0 },
brief_match: { score: 100, passed: true, issue_count: 0 },
},
sellingPointMatches: [
{ content: 'SPF50+ PA++++', priority: 'core' as const, matched: true, evidence: '脚本提及 SPF50+PA++++' },
{ content: '轻薄质地', priority: 'core' as const, matched: true, evidence: '脚本描述质地轻薄不油腻' },
{ content: '延展性好', priority: 'recommended' as const, matched: true, evidence: '脚本演示延展性' },
],
sellingPoints: [
{ point: 'SPF50+ PA++++', covered: true },
{ point: '轻薄质地', covered: true },
{ point: '不油腻', covered: true },
{ point: '延展性好', covered: true },
],
},
}
// 从 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,
dimension: v.dimension,
}))
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,
dimensions: task.script_ai_result?.dimensions,
sellingPointMatches: task.script_ai_result?.selling_point_matches || [],
sellingPoints: [] as Array<{ point: string; covered: boolean }>,
},
}
}
function ReviewProgressBar({ taskStatus }: { taskStatus: string }) {
const steps = getBrandReviewSteps(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 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 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])
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 = async () => {
if (!rejectReason.trim()) {
toast.error('请填写驳回原因')
return
}
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 (
<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">
<div className="flex items-center gap-2">
<h1 className="text-xl font-bold text-text-primary">{task.title}</h1>
{task.isAppeal && (
<span className="flex items-center gap-1 px-2 py-1 text-xs bg-accent-amber/20 text-accent-amber rounded-full font-medium">
<MessageSquareWarning size={12} />
</span>
)}
</div>
<div className="flex items-center gap-4 mt-1 text-sm text-text-secondary">
<span className="flex items-center gap-1">
<User size={14} />
{task.creatorName}
</span>
<span className="flex items-center gap-1">
<Building size={14} />
{task.agencyName}
</span>
<span className="flex items-center gap-1">
<Clock size={14} />
{task.submittedAt}
</span>
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 p-1 bg-bg-elevated rounded-lg">
<button
type="button"
onClick={() => setViewMode('file')}
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
viewMode === 'file' ? 'bg-bg-card text-text-primary shadow-sm' : 'text-text-secondary hover:text-text-primary'
}`}
>
</button>
<button
type="button"
onClick={() => setViewMode('parsed')}
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
viewMode === 'parsed' ? 'bg-bg-card text-text-primary shadow-sm' : 'text-text-secondary hover:text-text-primary'
}`}
>
AI解析
</button>
</div>
</div>
</div>
{/* 申诉理由 */}
{task.isAppeal && task.appealReason && (
<div className="p-4 rounded-xl bg-accent-amber/10 border border-accent-amber/30">
<p className="text-sm text-accent-amber font-medium mb-1 flex items-center gap-1">
<MessageSquareWarning size={14} />
</p>
<p className="text-text-secondary">{task.appealReason}</p>
</div>
)}
{/* 审核流程进度条 */}
<ReviewProgressBar taskStatus={task.status} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 左侧:脚本内容 */}
<div className="lg:col-span-2 space-y-4">
{/* 文件信息卡片 */}
{task.file.fileUrl && (
<FileInfoCard
file={task.file}
onPreview={() => setShowFilePreview(true)}
/>
)}
{viewMode === 'file' ? (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText size={18} className="text-accent-indigo" />
</CardTitle>
</CardHeader>
<CardContent>
{task.file.fileUrl ? (
<FilePreview file={task.file} />
) : (
<p className="text-sm text-text-tertiary text-center py-8"></p>
)}
</CardContent>
</Card>
) : (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText size={18} className="text-accent-indigo" />
AI
<span className="text-xs font-normal text-text-tertiary ml-2">AI </span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{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>
)}
{/* 代理商初审意见 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MessageSquare size={18} className="text-blue-500" />
</CardTitle>
</CardHeader>
<CardContent>
{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>
</div>
) : (
<p className="text-sm text-text-tertiary text-center py-4"></p>
)}
</CardContent>
</Card>
</div>
{/* 右侧AI 分析面板 */}
<div className="space-y-4">
{/* AI 评分 */}
<Card>
<CardContent className="py-4">
<div className="flex items-center justify-between">
<span className="text-sm text-text-secondary">AI </span>
<span className={`text-3xl font-bold ${task.aiScore >= 85 ? 'text-accent-green' : task.aiScore >= 70 ? 'text-yellow-400' : 'text-accent-coral'}`}>
{task.aiScore}
</span>
</div>
</CardContent>
</Card>
{/* 维度评分 */}
{task.aiAnalysis.dimensions && (
<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">
{(['legal', 'platform', 'brand_safety', 'brief_match'] as const).map(key => {
const dim = (task.aiAnalysis.dimensions as unknown as Record<string, { score: number; passed: boolean; issue_count: number }>)?.[key]
if (!dim) return null
const label = { legal: '法规合规', platform: '平台规则', brand_safety: '品牌安全', brief_match: 'Brief 匹配' }[key]
return (
<div key={key} className={`flex items-center justify-between p-2 rounded-lg ${dim.passed ? 'bg-accent-green/5' : 'bg-accent-coral/5'}`}>
<span className="text-sm text-text-primary">{label}</span>
<div className="flex items-center gap-2">
<span className={`text-sm font-bold ${dim.passed ? 'text-accent-green' : 'text-accent-coral'}`}>{dim.score}</span>
{dim.passed ? <CheckCircle size={14} className="text-accent-green" /> : <XCircle size={14} className="text-accent-coral" />}
</div>
</div>
)
})}
</CardContent>
</Card>
)}
{/* 违规检测 */}
<Card>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base">
<AlertTriangle size={16} className="text-orange-500" />
({task.aiAnalysis.violations.length})
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{task.aiAnalysis.violations.map((v) => (
<div key={v.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>{v.type}</WarningTag>
{v.dimension && <span className="text-xs text-text-tertiary">{{ legal: '法规合规', platform: '平台规则', brand_safety: '品牌安全', brief_match: 'Brief 匹配' }[v.dimension as string]}</span>}
</div>
<p className="text-sm text-text-primary">{v.content}</p>
<p className="text-xs text-accent-indigo mt-1">{v.suggestion}</p>
</div>
))}
{task.aiAnalysis.violations.length === 0 && (
<p className="text-sm text-text-tertiary text-center py-4"></p>
)}
</CardContent>
</Card>
{/* 软性提醒 */}
{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>
))}
</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>
)}
{/* 卖点匹配 */}
{(task.aiAnalysis.sellingPointMatches?.length > 0 || 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.sellingPointMatches && task.aiAnalysis.sellingPointMatches.length > 0 ? (
task.aiAnalysis.sellingPointMatches.map((sp: { content: string; priority: string; matched: boolean; evidence?: string }, idx: number) => (
<div key={idx} className="flex items-start gap-2 p-2 rounded-lg bg-bg-elevated">
{sp.matched ? <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">
<div className="flex items-center gap-2">
<span className="text-sm text-text-primary">{sp.content}</span>
<span className={`px-1.5 py-0.5 text-xs rounded ${
sp.priority === 'core' ? 'bg-accent-coral/20 text-accent-coral' :
sp.priority === 'recommended' ? 'bg-accent-amber/20 text-accent-amber' :
'bg-bg-page text-text-tertiary'
}`}>{sp.priority === 'core' ? '核心' : sp.priority === 'recommended' ? '推荐' : '参考'}</span>
</div>
{sp.evidence && <p className="text-xs text-text-tertiary mt-0.5">{sp.evidence}</p>}
</div>
</div>
))
) : (
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>
{/* 底部决策栏 */}
<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">
{task.projectName}
</div>
<div className="flex gap-3">
<Button variant="danger" onClick={() => setShowRejectModal(true)} disabled={submitting}>
{submitting ? <Loader2 size={16} className="animate-spin" /> : null}
</Button>
<Button variant="success" onClick={() => setShowApproveModal(true)} disabled={submitting}>
{submitting ? <Loader2 size={16} className="animate-spin" /> : null}
</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>
<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)} disabled={submitting}></Button>
<Button variant="danger" onClick={handleReject} disabled={submitting}>
{submitting ? <Loader2 size={16} className="animate-spin mr-1" /> : null}
</Button>
</div>
</div>
</Modal>
{/* 文件预览弹窗 */}
<FilePreviewModal
file={task.file}
isOpen={showFilePreview}
onClose={() => setShowFilePreview(false)}
/>
</div>
)
}