前端文案挑选页按 A/B/C 三套分组展示

- 后端 _fmt_text 返回 strategy 字段,SSE 与 hydrate 两条路径贯通
- 新建 TextStrategyGroup 组件 + groupByStrategy(A/B/C/未分套排序,旧 NULL 数据归未分套)
- text 页平铺改分组渲染,选中态/改稿回调透传不变
- 复用 ImageStrategyGroup 的 STRATEGY_LABEL,单向依赖无环

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yangqianqian
2026-06-22 10:29:35 +08:00
parent e095b1162e
commit cb041e8d3a
6 changed files with 85 additions and 15 deletions

View File

@@ -0,0 +1,62 @@
'use client';
/**
* TextStrategyGroup — 文案按"套"(A/B/C)分组展示
* A8倩倩姐2026-06-18拍板"按三套分组展示"):让运营一眼看出三套不同角度,
* 与图片侧 ImageStrategyGroup 同轴(套1痛点/套2场景/套3成分背书),复用 STRATEGY_LABEL。
* 无 strategy 的旧数据归入"未分套"组,向后兼容不丢卡。
*/
import { TextCandidate } from '@/types/index';
import { TextCandidateCard } from './TextCandidateCard';
import { STRATEGY_LABEL } from './ImageStrategyGroup';
interface TextStrategyGroupProps {
strategy: string; // 'A'|'B'|'C'|'_'(未分套)
candidates: TextCandidate[];
selectedTextIds: number[];
onToggle: (id: number) => void;
onSaveEdit?: (id: number, content: string) => Promise<void>;
}
export function TextStrategyGroup({
strategy, candidates, selectedTextIds, onToggle, onSaveEdit,
}: TextStrategyGroupProps) {
const label = strategy === '_' ? '未分套' : (STRATEGY_LABEL[strategy] || `${strategy}`);
return (
<section className="mb-6">
<h3 className="mb-3 flex items-center text-sm font-semibold text-text-primary">
{label}
<span className="ml-2 text-xs font-normal text-text-tertiary">
{candidates.length}
</span>
</h3>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-4">
{candidates.map((c) => (
<TextCandidateCard
key={c.candidate_id}
candidate={c}
selected={selectedTextIds.includes(c.candidate_id)}
onToggle={onToggle}
onSaveEdit={onSaveEdit}
/>
))}
</div>
</section>
);
}
// 把平铺候选按 strategy 分组,返回有序 [strategy, candidates][]A/B/C 固定序,未分套垫底)。
// 仅含真实存在的套(不硬塞空套),避免某套还没生成出来时显示空壳。
export function groupByStrategy(
candidates: TextCandidate[],
): Array<[string, TextCandidate[]]> {
const order = ['A', 'B', 'C', '_'];
const buckets = new Map<string, TextCandidate[]>();
for (const c of candidates) {
const key = c.strategy ?? '_';
if (!buckets.has(key)) buckets.set(key, []);
buckets.get(key)!.push(c);
}
return order
.filter((k) => buckets.has(k))
.map((k) => [k, buckets.get(k) ?? []] as [string, TextCandidate[]]);
}