baseline: Clover 独立仓库首次基线提交

将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。
当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包),
M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yangqianqian
2026-06-16 11:30:22 +08:00
commit 6a2632da70
253 changed files with 27467 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
'use client';
// TextCandidateCard — 文案候选卡AI评委7维分 + 总评 + 违禁状态 + 导入角标
import { useState } from 'react';
import { TextCandidate } from '@/types/index';
import { ScoreDimBars } from './ScoreDimBars';
import { clsx } from 'clsx' ;
interface TextCandidateCardProps {
candidate: TextCandidate;
selected: boolean;
onToggle: (id: number) => void;
}
const BANNED_STATUS_STYLES = {
pass: null,
auto_fixed: 'bg-green-50 border-brand-green text-brand-green-dark text-xs px-2 py-0.5 rounded-full',
soft_warn: 'bg-yellow-50 border-yellow-400 text-yellow-700 text-xs px-2 py-0.5 rounded-full',
hard_block: 'bg-red-50 border-red-400 text-status-error text-xs px-2 py-0.5 rounded-full',
};
const VERDICT_STYLES: Record<string, string> = {
'优秀': 'text-status-success font-medium',
'合格': 'text-text-secondary',
'不合格':'text-status-error',
};
export function TextCandidateCard({ candidate, selected, onToggle }: TextCandidateCardProps) {
const [expanded, setExpanded] = useState(false);
return (
<article
onClick={() => onToggle(candidate.candidate_id)}
aria-selected={selected}
aria-label={`文案:${candidate.angle_label},总分${candidate.score.total}${selected ? '已选中' : '未选中'}`}
className={clsx(
'relative border rounded-xl p-4 cursor-pointer transition-all',
selected
? 'border-brand-orange bg-brand-cream shadow-sm'
: 'border-border-default bg-surface-primary hover:border-brand-orange/50'
)}
>
{/* 角标:导入来源 */}
{candidate.source === 'import' && (
<span className="absolute top-2 right-2 text-xs bg-surface-tertiary text-text-secondary px-2 py-0.5 rounded-full">
</span>
)}
{/* 选中勾 */}
{selected && (
<span className="absolute top-2 left-2 w-5 h-5 bg-brand-orange text-white rounded-full flex items-center justify-center text-xs"
aria-hidden="true"></span>
)}
<div className="mt-1">
<span className="text-xs font-medium text-brand-orange mb-1 block">{candidate.angle_label}</span>
<p className={clsx(
'text-sm text-text-primary whitespace-pre-wrap',
!expanded && 'line-clamp-4'
)}>{candidate.content}</p>
<button
type="button"
onClick={(e) => { e.stopPropagation(); setExpanded((v) => !v); }}
className="mt-1 text-xs text-brand-orange hover:underline"
>
{expanded ? '收起' : '展开全文'}
</button>
</div>
{/* 违禁词状态 */}
{candidate.banned_word_status !== 'pass' && (
<div className="mt-2">
<span className={BANNED_STATUS_STYLES[candidate.banned_word_status] ?? ''}>
{candidate.banned_word_status === 'auto_fixed' && '已自动改写'}
{candidate.banned_word_status === 'soft_warn' && '含敏感词,请检查'}
{candidate.banned_word_status === 'hard_block' && '含违禁词,已拦截'}
</span>
</div>
)}
{/* 七维分数条 */}
<ScoreDimBars score={candidate.score} passLine={85} />
{/* AI 评委总评verdict + summary旧数据空字符串时不渲染*/}
{candidate.summary && (
<p className="mt-2 text-xs text-text-secondary leading-relaxed">
{candidate.verdict && (
<span className={clsx('mr-1', VERDICT_STYLES[candidate.verdict] ?? 'text-text-secondary')}>
[{candidate.verdict}]
</span>
)}
{candidate.summary}
</p>
)}
</article>
);
}
/** 骨架屏占位 */
export function TextCandidateCardSkeleton() {
return (
<div className="border border-border-default rounded-xl p-4 animate-pulse space-y-2" aria-hidden="true">
<div className="h-3 bg-surface-tertiary rounded w-1/3" />
<div className="h-4 bg-surface-tertiary rounded w-full" />
<div className="h-4 bg-surface-tertiary rounded w-5/6" />
<div className="h-4 bg-surface-tertiary rounded w-4/6" />
<div className="mt-3 space-y-1">
{[1, 2, 3].map((i) => (
<div key={i} className="flex gap-2 items-center">
<div className="h-2 bg-surface-tertiary rounded w-12" />
<div className="h-1.5 bg-surface-tertiary rounded flex-1" />
</div>
))}
</div>
</div>
);
}