feat: add detail component catalog renderer
This commit is contained in:
parent
a1b6496976
commit
2f1eff956a
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user