feat: add detail component catalog renderer
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-21 23:51:40 +08:00
parent a1b6496976
commit 2f1eff956a
4 changed files with 222 additions and 22 deletions

View File

@ -43,7 +43,12 @@ function collectRenderableScreenComponents(surface: UiSurfaceSnapshot, parentId:
if (
component.type === "detail_text_block" ||
component.type === "detail_card" ||
component.type === "detail_badge"
component.type === "detail_badge" ||
component.type === "detail_feature_card" ||
component.type === "detail_spec_table" ||
component.type === "detail_comparison_strip" ||
component.type === "detail_proof_bar" ||
component.type === "detail_cta_banner"
) {
result.push(component);
continue;
@ -66,7 +71,12 @@ export function DetailSurfaceInspectorPanel(params: {
const totalRenderableBlocks =
countUiSurfaceComponents(surface, "detail_text_block") +
countUiSurfaceComponents(surface, "detail_card") +
countUiSurfaceComponents(surface, "detail_badge");
countUiSurfaceComponents(surface, "detail_badge") +
countUiSurfaceComponents(surface, "detail_feature_card") +
countUiSurfaceComponents(surface, "detail_spec_table") +
countUiSurfaceComponents(surface, "detail_comparison_strip") +
countUiSurfaceComponents(surface, "detail_proof_bar") +
countUiSurfaceComponents(surface, "detail_cta_banner");
const totalSections = countUiSurfaceComponents(surface, "detail_section");
const totalScreens = screenComponents.length;

View File

@ -159,7 +159,12 @@ function collectRenderableSurfaceComponents(params: {
if (
component.type === "detail_card" ||
component.type === "detail_badge" ||
component.type === "detail_text_block"
component.type === "detail_text_block" ||
component.type === "detail_feature_card" ||
component.type === "detail_spec_table" ||
component.type === "detail_comparison_strip" ||
component.type === "detail_proof_bar" ||
component.type === "detail_cta_banner"
) {
collected.push(component);
continue;

View File

@ -77,7 +77,15 @@ function buildScreenBadgeLabel(params: {
function createStyledSurfaceTextComponent(params: {
id: string;
type: "detail_card" | "detail_badge" | "detail_text_block";
type:
| "detail_card"
| "detail_badge"
| "detail_text_block"
| "detail_feature_card"
| "detail_spec_table"
| "detail_comparison_strip"
| "detail_proof_bar"
| "detail_cta_banner";
parentId: string;
frame: { x: number; y: number; width: number; height: number };
zIndex?: number;
@ -112,6 +120,55 @@ function createStyledSurfaceTextComponent(params: {
} satisfies UiSurfaceComponent;
}
function buildCatalogStrings(params: {
screenIndex: number;
totalScreens?: number;
screen: Pick<
DetailUiScreenInput,
"title" | "subtitle" | "copy" | "screenType" | "objective" | "layoutNote"
>;
}) {
const highlights = splitBodyCopyIntoHighlights(params.screen.copy).slice(0, 3);
const primaryTitle = String(params.screen.title || "").replace(/\s+/g, " ").trim();
const subtitle = String(params.screen.subtitle || "").replace(/\s+/g, " ").trim();
const objective = String(params.screen.objective || "").replace(/\s+/g, " ").trim();
const layoutNote = String(params.screen.layoutNote || "").replace(/\s+/g, " ").trim();
const catalogTitle = primaryTitle || "核心卖点";
const specRows = [
`核心卖点|${catalogTitle || "舒适支撑"}`,
objective ? `适用场景|${objective}` : "",
subtitle ? `结果表达|${subtitle}` : "",
layoutNote ? `设计要求|${layoutNote}` : "",
].filter(Boolean);
const comparisonText = [
"旧方式",
highlights[0] || "久坐容易疲劳、支撑不足",
"",
"现在",
highlights[1] || subtitle || "围绕护腰/舒适/效率完成升级",
]
.filter(Boolean)
.join("\n");
const proofText = layoutNote || objective || subtitle || catalogTitle;
const shouldShowCta =
params.screenIndex === (params.totalScreens || 0) ||
/下单|购买|收口|转化|cta|call to action/i.test(layoutNote);
const ctaText = shouldShowCta
? `立即聚焦:${catalogTitle}${subtitle ? `\n${subtitle}` : ""}`
: "";
return {
highlights,
specTableText: specRows.join("\n"),
comparisonText,
proofText,
ctaText,
};
}
function createSectionComponent(params: {
id: string;
parentId: string;
@ -139,6 +196,7 @@ function buildSectionedTextComponents(params: {
screenId: string;
textStackId: string;
screenIndex: number;
totalScreens?: number;
screenSize: { width: number; height: number };
screen: Pick<
DetailUiScreenInput,
@ -165,7 +223,11 @@ function buildSectionedTextComponents(params: {
screenType: params.screen.screenType,
objective: params.screen.objective,
});
const bodyHighlights = splitBodyCopyIntoHighlights(params.screen.copy);
const catalog = buildCatalogStrings({
screenIndex: params.screenIndex,
totalScreens: params.totalScreens,
screen: params.screen,
});
const titleBlock =
params.screenProtocol?.blocks.find((block) => block.type === "title") || null;
@ -342,8 +404,8 @@ function buildSectionedTextComponents(params: {
);
const supportingCards =
bodyHighlights.length > 0
? bodyHighlights.slice(0, 3)
catalog.highlights.length > 0
? catalog.highlights.slice(0, 3)
: [String(params.screen.copy || "").trim()].filter(Boolean);
const cardGap = 14;
const supportColumns = variant === "split_bottom" ? Math.min(3, supportingCards.length) : Math.min(2, Math.max(1, supportingCards.length));
@ -359,7 +421,7 @@ function buildSectionedTextComponents(params: {
components.push(
createStyledSurfaceTextComponent({
id: `${params.screenId}-support-card-${index + 1}`,
type: "detail_card",
type: "detail_feature_card",
parentId: `${params.screenId}-section-supporting`,
frame: {
x,
@ -396,15 +458,92 @@ function buildSectionedTextComponents(params: {
);
});
if (params.screen.layoutNote) {
if (catalog.specTableText && params.screen.screenType === "A") {
components.push(
createStyledSurfaceTextComponent({
id: `${params.screenId}-spec-table`,
type: "detail_spec_table",
parentId: `${params.screenId}-section-supporting`,
frame: {
x: safeArea.left,
y:
variant === "split_bottom"
? supportingSectionY - 122
: supportingSectionY + supportingSectionHeight - 204,
width: Math.min(contentWidth, 360),
height: 188,
},
zIndex: 2,
content: catalog.specTableText,
role: "spec_table",
style: {
fontSize: 16,
fontWeight: "600",
color: "#0f172a",
textAlign: "left",
lineHeight: 1.48,
backgroundColor: "rgba(255,255,255,0.94)",
borderColor: "rgba(59,130,246,0.18)",
borderWidth: 1,
borderRadius: 24,
padding: 20,
boxShadow: "0 18px 42px rgba(15,23,42,0.12)",
backdropBlur: 12,
},
metadata: {
screenIndex: params.screenIndex,
componentRole: "spec_table",
},
}),
);
}
if (catalog.comparisonText && params.screen.screenType === "C") {
components.push(
createStyledSurfaceTextComponent({
id: `${params.screenId}-comparison-strip`,
type: "detail_comparison_strip",
parentId: `${params.screenId}-section-supporting`,
frame: {
x: safeArea.left,
y: supportingSectionY - 112,
width: Math.min(contentWidth, 420),
height: 158,
},
zIndex: 2,
content: catalog.comparisonText,
role: "comparison_strip",
style: {
fontSize: 17,
fontWeight: "700",
color: "#f8fafc",
textAlign: "left",
lineHeight: 1.4,
backgroundColor: "rgba(15,23,42,0.88)",
borderColor: "rgba(255,255,255,0.18)",
borderWidth: 1,
borderRadius: 26,
padding: 20,
boxShadow: "0 20px 48px rgba(15,23,42,0.18)",
backdropBlur: 12,
},
metadata: {
screenIndex: params.screenIndex,
componentRole: "comparison_strip",
},
}),
);
}
if (catalog.proofText) {
const noteY = Math.min(
params.screenSize.height - 116,
supportingSectionY + supportingSectionHeight - 74,
);
components.push(
createStyledSurfaceTextComponent({
id: `${params.screenId}-proof-card`,
type: "detail_card",
id: `${params.screenId}-proof-bar`,
type: "detail_proof_bar",
parentId: `${params.screenId}-section-supporting`,
frame: {
x:
@ -416,8 +555,8 @@ function buildSectionedTextComponents(params: {
height: 62,
},
zIndex: 2,
content: String(params.screen.layoutNote || "").replace(/\s+/g, " ").trim(),
role: "proof_chip",
content: catalog.proofText,
role: "proof_bar",
style: {
fontSize: 16,
fontWeight: "600",
@ -434,7 +573,44 @@ function buildSectionedTextComponents(params: {
},
metadata: {
screenIndex: params.screenIndex,
componentRole: "proof_chip",
componentRole: "proof_bar",
},
}),
);
}
if (catalog.ctaText) {
components.push(
createStyledSurfaceTextComponent({
id: `${params.screenId}-cta-banner`,
type: "detail_cta_banner",
parentId: params.textStackId,
frame: {
x: safeArea.left,
y: Math.min(params.screenSize.height - 164, supportingSectionY + supportingSectionHeight + 8),
width: Math.min(contentWidth, 430),
height: 104,
},
zIndex: 3,
content: catalog.ctaText,
role: "cta_banner",
style: {
fontSize: 20,
fontWeight: "800",
color: "#f8fafc",
textAlign: "left",
lineHeight: 1.28,
backgroundColor: "rgba(37,99,235,0.88)",
borderColor: "rgba(255,255,255,0.24)",
borderWidth: 1,
borderRadius: 28,
padding: 22,
boxShadow: "0 22px 58px rgba(37,99,235,0.28)",
backdropBlur: 12,
},
metadata: {
screenIndex: params.screenIndex,
componentRole: "cta_banner",
},
}),
);
@ -445,6 +621,7 @@ function buildSectionedTextComponents(params: {
function createScreenSubtree(params: {
screenIndex: number;
totalScreens?: number;
screen: Pick<
DetailUiScreenInput,
"title" | "subtitle" | "copy" | "screenType" | "objective" | "layoutNote"
@ -500,13 +677,14 @@ function createScreenSubtree(params: {
];
components.push(
...buildSectionedTextComponents({
screenId,
textStackId,
screenIndex: params.screenIndex,
screenSize: params.screenSize,
screen: params.screen,
screenProtocol: params.screenProtocol,
...buildSectionedTextComponents({
screenId,
textStackId,
screenIndex: params.screenIndex,
totalScreens: params.totalScreens,
screenSize: params.screenSize,
screen: params.screen,
screenProtocol: params.screenProtocol,
}),
);
@ -561,6 +739,7 @@ export function buildDetailUiSurfaceSnapshot(params: {
components.push(
...createScreenSubtree({
screenIndex: screen.index,
totalScreens: params.screens.length,
screen,
screenSize: params.screenSize,
screenProtocol,
@ -613,6 +792,7 @@ export function buildDetailUiSurfacePatchFromReview(params: {
const textStackId = `${screenId}-text-stack`;
const replacementComponents = createScreenSubtree({
screenIndex: params.screenIndex,
totalScreens: params.screenIndex,
screen: params.screen,
screenSize: {
width: 790,

View File

@ -10,7 +10,12 @@ export type UiSurfaceComponentType =
| "detail_text_block"
| "detail_section"
| "detail_card"
| "detail_badge";
| "detail_badge"
| "detail_feature_card"
| "detail_spec_table"
| "detail_comparison_strip"
| "detail_proof_bar"
| "detail_cta_banner";
export type UiSurfaceTextBindingKey = "title" | "subtitle" | "copy";