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>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
0c59797d5b
commit
0ef7650c09
@@ -25,6 +25,7 @@ import {
|
||||
import { FilePreview, FileInfoCard, FilePreviewModal, type FileInfo } from '@/components/ui/FilePreview'
|
||||
import { api } from '@/lib/api'
|
||||
import { USE_MOCK } from '@/contexts/AuthContext'
|
||||
import { getPlatformInfo } from '@/lib/platforms'
|
||||
import type { TaskResponse } from '@/types/task'
|
||||
|
||||
// 模拟脚本任务数据
|
||||
@@ -81,6 +82,8 @@ function mapTaskToViewModel(task: TaskResponse) {
|
||||
title: task.name,
|
||||
creatorName: task.creator?.name || '未知达人',
|
||||
projectName: task.project?.name || '未知项目',
|
||||
brandName: task.project?.brand_name || '',
|
||||
platform: task.project?.platform || '',
|
||||
submittedAt: task.script_uploaded_at || task.created_at,
|
||||
aiScore: task.script_ai_score ?? 0,
|
||||
status: task.stage,
|
||||
@@ -107,12 +110,26 @@ function mapTaskToViewModel(task: TaskResponse) {
|
||||
content: v.content,
|
||||
suggestion: v.suggestion,
|
||||
severity: v.severity,
|
||||
dimension: v.dimension,
|
||||
})),
|
||||
complianceChecks: (task.script_ai_result?.soft_warnings || []).map((w) => ({
|
||||
item: w.type,
|
||||
passed: false,
|
||||
note: w.content,
|
||||
})),
|
||||
complianceChecks: (task.script_ai_result?.soft_warnings || []).map((w: any) => {
|
||||
const codeLabels: Record<string, string> = {
|
||||
missing_selling_points: '卖点缺失',
|
||||
tone_mismatch: '语气不符',
|
||||
length_warning: '时长提示',
|
||||
style_warning: '风格提示',
|
||||
sensitive_topic: '敏感话题',
|
||||
audience_mismatch: '受众偏差',
|
||||
}
|
||||
const rawLabel = w.type || w.code || '提示'
|
||||
return {
|
||||
item: codeLabels[rawLabel] || rawLabel,
|
||||
passed: false,
|
||||
note: w.content || w.message || '',
|
||||
}
|
||||
}),
|
||||
dimensions: task.script_ai_result?.dimensions,
|
||||
sellingPointMatches: task.script_ai_result?.selling_point_matches || [],
|
||||
sellingPoints: [] as Array<{ point: string; covered: boolean }>,
|
||||
},
|
||||
aiSummary: task.script_ai_result?.summary || '',
|
||||
@@ -183,6 +200,7 @@ export default function AgencyScriptReviewPage() {
|
||||
const [showFilePreview, setShowFilePreview] = useState(false)
|
||||
const [task, setTask] = useState<ScriptTaskViewModel>(mockScriptTask as unknown as ScriptTaskViewModel)
|
||||
|
||||
|
||||
const loadTask = useCallback(async () => {
|
||||
if (USE_MOCK) return
|
||||
setLoading(true)
|
||||
@@ -294,10 +312,9 @@ export default function AgencyScriptReviewPage() {
|
||||
)}
|
||||
</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>{task.creatorName}</span>
|
||||
{task.brandName && <span>{task.brandName}</span>}
|
||||
{task.platform && <span>{getPlatformInfo(task.platform)?.name || task.platform}</span>}
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock size={14} />
|
||||
{task.submittedAt}
|
||||
@@ -368,8 +385,7 @@ export default function AgencyScriptReviewPage() {
|
||||
<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>
|
||||
AI 审核分析
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -378,23 +394,41 @@ export default function AgencyScriptReviewPage() {
|
||||
<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>
|
||||
</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>
|
||||
) : (
|
||||
<p className="text-sm text-text-tertiary text-center py-4">暂无 AI 分析总结</p>
|
||||
)}
|
||||
{task.aiAnalysis.violations.length > 0 && (
|
||||
<div className="p-4 bg-bg-elevated rounded-lg">
|
||||
<div className="text-xs text-accent-coral font-medium mb-2">发现问题 ({task.aiAnalysis.violations.length})</div>
|
||||
<div className="space-y-2">
|
||||
{task.aiAnalysis.violations.map((v) => (
|
||||
<div key={v.id} className="text-sm">
|
||||
<span className="text-accent-coral font-medium">[{v.type}]</span>
|
||||
<span className="text-text-primary ml-1">{v.content}</span>
|
||||
<p className="text-xs text-accent-indigo mt-0.5">{v.suggestion}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{task.aiAnalysis.sellingPointMatches.length > 0 && (
|
||||
<div className="p-4 bg-bg-elevated rounded-lg">
|
||||
<div className="text-xs text-accent-green font-medium mb-2">卖点匹配概览</div>
|
||||
<div className="space-y-1">
|
||||
{task.aiAnalysis.sellingPointMatches.map((sp: { content: string; priority: string; matched: boolean; evidence?: string }, idx: number) => (
|
||||
<div key={idx} className="flex items-center gap-2 text-sm">
|
||||
{sp.matched ? <CheckCircle size={14} className="text-accent-green flex-shrink-0" /> : <XCircle size={14} className="text-accent-coral flex-shrink-0" />}
|
||||
<span className="text-text-primary">{sp.content}</span>
|
||||
<span className={`text-xs px-1.5 py-0.5 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>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
@@ -414,6 +448,34 @@ export default function AgencyScriptReviewPage() {
|
||||
</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">
|
||||
@@ -427,6 +489,7 @@ export default function AgencyScriptReviewPage() {
|
||||
<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>
|
||||
@@ -438,53 +501,58 @@ export default function AgencyScriptReviewPage() {
|
||||
</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>
|
||||
{/* 舆情提示 */}
|
||||
{task.aiAnalysis.complianceChecks.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<AlertTriangle size={16} className="text-orange-500" />
|
||||
舆情提示(仅参考)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{task.aiAnalysis.complianceChecks.map((check, idx) => (
|
||||
<div key={idx} className="p-3 bg-orange-500/10 rounded-lg border border-orange-500/30">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<WarningTag>{check.item}</WarningTag>
|
||||
</div>
|
||||
{check.note && (
|
||||
<p className="text-xs text-text-tertiary mt-0.5">{check.note}</p>
|
||||
<p className="text-sm text-text-secondary">{check.note}</p>
|
||||
)}
|
||||
<p className="text-xs text-text-tertiary mt-1">软性风险仅作提示,不影响审核结果</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</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>
|
||||
))}
|
||||
{task.aiAnalysis.sellingPoints.length === 0 && (
|
||||
{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>
|
||||
))
|
||||
) : (
|
||||
<p className="text-sm text-text-tertiary text-center py-4">暂无卖点数据</p>
|
||||
)}
|
||||
</CardContent>
|
||||
@@ -497,7 +565,9 @@ export default function AgencyScriptReviewPage() {
|
||||
<CardContent className="py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-text-secondary">
|
||||
项目:{task.projectName}
|
||||
{task.brandName && <span>{task.brandName} · </span>}
|
||||
{task.projectName}
|
||||
{task.platform && <span> · {getPlatformInfo(task.platform)?.name || task.platform}</span>}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="danger" onClick={() => setShowRejectModal(true)} disabled={submitting}>
|
||||
|
||||
Reference in New Issue
Block a user