feat: refine detail catalog component layouts
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-22 00:28:58 +08:00
parent 2f1eff956a
commit 54f02be0f5
4 changed files with 387 additions and 96 deletions

View File

@ -45,10 +45,17 @@ function collectRenderableScreenComponents(surface: UiSurfaceSnapshot, parentId:
component.type === "detail_card" ||
component.type === "detail_badge" ||
component.type === "detail_feature_card" ||
component.type === "detail_feature_tag" ||
component.type === "detail_spec_table" ||
component.type === "detail_table_label" ||
component.type === "detail_table_value" ||
component.type === "detail_comparison_strip" ||
component.type === "detail_compare_before" ||
component.type === "detail_compare_after" ||
component.type === "detail_proof_bar" ||
component.type === "detail_cta_banner"
component.type === "detail_cta_banner" ||
component.type === "detail_cta_button" ||
component.type === "detail_cta_support"
) {
result.push(component);
continue;
@ -73,10 +80,17 @@ export function DetailSurfaceInspectorPanel(params: {
countUiSurfaceComponents(surface, "detail_card") +
countUiSurfaceComponents(surface, "detail_badge") +
countUiSurfaceComponents(surface, "detail_feature_card") +
countUiSurfaceComponents(surface, "detail_feature_tag") +
countUiSurfaceComponents(surface, "detail_spec_table") +
countUiSurfaceComponents(surface, "detail_table_label") +
countUiSurfaceComponents(surface, "detail_table_value") +
countUiSurfaceComponents(surface, "detail_comparison_strip") +
countUiSurfaceComponents(surface, "detail_compare_before") +
countUiSurfaceComponents(surface, "detail_compare_after") +
countUiSurfaceComponents(surface, "detail_proof_bar") +
countUiSurfaceComponents(surface, "detail_cta_banner");
countUiSurfaceComponents(surface, "detail_cta_banner") +
countUiSurfaceComponents(surface, "detail_cta_button") +
countUiSurfaceComponents(surface, "detail_cta_support");
const totalSections = countUiSurfaceComponents(surface, "detail_section");
const totalScreens = screenComponents.length;

View File

@ -161,10 +161,17 @@ function collectRenderableSurfaceComponents(params: {
component.type === "detail_badge" ||
component.type === "detail_text_block" ||
component.type === "detail_feature_card" ||
component.type === "detail_feature_tag" ||
component.type === "detail_spec_table" ||
component.type === "detail_table_label" ||
component.type === "detail_table_value" ||
component.type === "detail_comparison_strip" ||
component.type === "detail_compare_before" ||
component.type === "detail_compare_after" ||
component.type === "detail_proof_bar" ||
component.type === "detail_cta_banner"
component.type === "detail_cta_banner" ||
component.type === "detail_cta_button" ||
component.type === "detail_cta_support"
) {
collected.push(component);
continue;

View File

@ -82,10 +82,17 @@ function createStyledSurfaceTextComponent(params: {
| "detail_badge"
| "detail_text_block"
| "detail_feature_card"
| "detail_feature_tag"
| "detail_spec_table"
| "detail_table_label"
| "detail_table_value"
| "detail_comparison_strip"
| "detail_compare_before"
| "detail_compare_after"
| "detail_proof_bar"
| "detail_cta_banner";
| "detail_cta_banner"
| "detail_cta_button"
| "detail_cta_support";
parentId: string;
frame: { x: number; y: number; width: number; height: number };
zIndex?: number;
@ -135,37 +142,63 @@ function buildCatalogStrings(params: {
const layoutNote = String(params.screen.layoutNote || "").replace(/\s+/g, " ").trim();
const catalogTitle = primaryTitle || "核心卖点";
const featureCards = (highlights.length > 0
? highlights
: [subtitle || catalogTitle].filter(Boolean)
)
.slice(0, 3)
.map((item, index) => ({
tag:
index === 0
? "核心升级"
: index === 1
? "场景收益"
: "舒适证明",
title: item,
detail:
index === 0
? subtitle || objective || "围绕核心体验完成升级"
: index === 1
? objective || layoutNote || "匹配真实使用场景"
: layoutNote || subtitle || "用可感知结果证明价值",
accent: index === 0 ? "primary" : index === 1 ? "secondary" : "neutral",
}));
const specRows = [
`核心卖点|${catalogTitle || "舒适支撑"}`,
objective ? `适用场景|${objective}` : "",
subtitle ? `结果表达|${subtitle}` : "",
layoutNote ? `设计要求|${layoutNote}` : "",
].filter(Boolean);
{ label: "核心卖点", value: catalogTitle || "舒适支撑" },
...(objective ? [{ label: "适用场景", value: objective }] : []),
...(subtitle ? [{ label: "结果表达", value: subtitle }] : []),
...(layoutNote ? [{ label: "设计要求", value: layoutNote }] : []),
];
const comparisonText = [
"旧方式",
highlights[0] || "久坐容易疲劳、支撑不足",
"",
"现在",
highlights[1] || subtitle || "围绕护腰/舒适/效率完成升级",
]
.filter(Boolean)
.join("\n");
const comparison = {
beforeTitle: "旧方式",
beforeBody: highlights[0] || "久坐容易疲劳、支撑不足、使用场景不聚焦",
afterTitle: "现在",
afterBody: highlights[1] || subtitle || "围绕护腰/舒适/效率完成体验升级",
};
const proofText = layoutNote || objective || subtitle || catalogTitle;
const proof = {
label: params.screen.screenType === "A" ? "技术证明" : "结果证明",
value: 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}` : ""}`
: "";
const cta = shouldShowCta
? {
buttonLabel: params.screen.screenType === "C" ? "立即对比体验" : "立即了解详情",
supportText: subtitle || objective || "继续查看完整卖点与场景说明",
highlightText: catalogTitle,
}
: null;
return {
highlights,
specTableText: specRows.join("\n"),
comparisonText,
proofText,
ctaText,
featureCards,
specRows,
comparison,
proof,
cta,
};
}
@ -404,9 +437,16 @@ function buildSectionedTextComponents(params: {
);
const supportingCards =
catalog.highlights.length > 0
? catalog.highlights.slice(0, 3)
: [String(params.screen.copy || "").trim()].filter(Boolean);
catalog.featureCards.length > 0
? catalog.featureCards
: [
{
tag: "核心信息",
title: String(params.screen.copy || "").trim() || catalog.proof.value,
detail: catalog.proof.value,
accent: "primary",
},
];
const cardGap = 14;
const supportColumns = variant === "split_bottom" ? Math.min(3, supportingCards.length) : Math.min(2, Math.max(1, supportingCards.length));
const supportCardWidth = Math.floor(
@ -418,6 +458,55 @@ function buildSectionedTextComponents(params: {
const row = supportColumns === 0 ? 0 : Math.floor(index / supportColumns);
const x = safeArea.left + column * (supportCardWidth + cardGap);
const y = supportingSectionY + row * 116;
components.push(
createStyledSurfaceTextComponent({
id: `${params.screenId}-feature-tag-${index + 1}`,
type: "detail_feature_tag",
parentId: `${params.screenId}-section-supporting`,
frame: {
x: x + 16,
y: y + 8,
width: Math.min(112, Math.max(76, highlight.tag.length * 17)),
height: 26,
},
zIndex: 3,
content: `✦ ${highlight.tag}`,
role: "feature_tag",
style: {
fontSize: 13,
fontWeight: "700",
color:
highlight.accent === "primary"
? "#1d4ed8"
: highlight.accent === "secondary"
? "#7c3aed"
: "#334155",
textAlign: "center",
lineHeight: 1.1,
backgroundColor:
highlight.accent === "primary"
? "rgba(219,234,254,0.96)"
: highlight.accent === "secondary"
? "rgba(237,233,254,0.96)"
: "rgba(241,245,249,0.96)",
borderColor:
highlight.accent === "primary"
? "rgba(59,130,246,0.24)"
: highlight.accent === "secondary"
? "rgba(124,58,237,0.24)"
: "rgba(148,163,184,0.24)",
borderWidth: 1,
borderRadius: 999,
padding: 6,
},
metadata: {
screenIndex: params.screenIndex,
componentRole: "feature_tag",
},
}),
);
components.push(
createStyledSurfaceTextComponent({
id: `${params.screenId}-support-card-${index + 1}`,
@ -425,28 +514,32 @@ function buildSectionedTextComponents(params: {
parentId: `${params.screenId}-section-supporting`,
frame: {
x,
y,
y: y + 20,
width: supportCardWidth,
height: variant === "split_bottom" ? 102 : 96,
height: variant === "split_bottom" ? 116 : 110,
},
zIndex: 2,
content: highlight,
content: `${index + 1}. ${highlight.title}${highlight.detail ? `\n${highlight.detail}` : ""}`,
role: "support_card",
style: {
fontSize: bodyBlock ? Math.max(18, Number(bodyBlock.style.fontSize || 22)) : 20,
fontWeight: "600",
color: "#111827",
textAlign: "left",
lineHeight: 1.32,
lineHeight: 1.34,
backgroundColor:
variant === "split_bottom"
? "rgba(255,255,255,0.92)"
: "rgba(248,250,252,0.82)",
borderColor:
index === 0 ? "rgba(59,130,246,0.42)" : "rgba(148,163,184,0.28)",
highlight.accent === "primary"
? "rgba(59,130,246,0.42)"
: highlight.accent === "secondary"
? "rgba(124,58,237,0.34)"
: "rgba(148,163,184,0.28)",
borderWidth: 1,
borderRadius: 24,
padding: 18,
padding: 20,
boxShadow: "0 18px 42px rgba(15,23,42,0.12)",
backdropBlur: 12,
},
@ -458,84 +551,182 @@ function buildSectionedTextComponents(params: {
);
});
if (catalog.specTableText && params.screen.screenType === "A") {
if (catalog.specRows.length > 0 && params.screen.screenType === "A") {
const tableX = safeArea.left;
const tableY =
variant === "split_bottom"
? supportingSectionY - 140
: supportingSectionY + supportingSectionHeight - 220;
const tableWidth = Math.min(contentWidth, 396);
const labelWidth = 106;
const rowGap = 10;
components.push(
createStyledSurfaceTextComponent({
id: `${params.screenId}-spec-table`,
type: "detail_spec_table",
createSectionComponent({
id: `${params.screenId}-section-spec-table`,
parentId: `${params.screenId}-section-supporting`,
role: "spec_table_section",
frame: {
x: safeArea.left,
y:
variant === "split_bottom"
? supportingSectionY - 122
: supportingSectionY + supportingSectionHeight - 204,
width: Math.min(contentWidth, 360),
height: 188,
x: tableX,
y: tableY,
width: tableWidth,
height: 196,
},
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",
componentRole: "spec_table_section",
},
}),
);
catalog.specRows.slice(0, 4).forEach((row, index) => {
const rowY = tableY + index * (38 + rowGap);
components.push(
createStyledSurfaceTextComponent({
id: `${params.screenId}-table-label-${index + 1}`,
type: "detail_table_label",
parentId: `${params.screenId}-section-spec-table`,
frame: {
x: tableX,
y: rowY,
width: labelWidth,
height: 38,
},
zIndex: 3,
content: row.label,
role: "table_label",
style: {
fontSize: 14,
fontWeight: "700",
color: "#334155",
textAlign: "center",
lineHeight: 1.15,
backgroundColor: "rgba(241,245,249,0.96)",
borderColor: "rgba(148,163,184,0.22)",
borderWidth: 1,
borderRadius: 14,
padding: 10,
},
metadata: {
screenIndex: params.screenIndex,
componentRole: "table_label",
},
}),
);
components.push(
createStyledSurfaceTextComponent({
id: `${params.screenId}-table-value-${index + 1}`,
type: "detail_table_value",
parentId: `${params.screenId}-section-spec-table`,
frame: {
x: tableX + labelWidth + 10,
y: rowY,
width: tableWidth - labelWidth - 10,
height: 38,
},
zIndex: 3,
content: row.value,
role: "table_value",
style: {
fontSize: 15,
fontWeight: "600",
color: "#0f172a",
textAlign: "left",
lineHeight: 1.18,
backgroundColor: "rgba(255,255,255,0.94)",
borderColor: "rgba(59,130,246,0.16)",
borderWidth: 1,
borderRadius: 14,
padding: 12,
boxShadow: "0 8px 22px rgba(15,23,42,0.08)",
},
metadata: {
screenIndex: params.screenIndex,
componentRole: "table_value",
},
}),
);
});
}
if (catalog.comparisonText && params.screen.screenType === "C") {
if ((catalog.comparison.beforeBody || catalog.comparison.afterBody) && params.screen.screenType === "C") {
const compareY = supportingSectionY - 126;
const compareWidth = Math.min(contentWidth, 430);
const compareColumnWidth = Math.floor((compareWidth - 12) / 2);
components.push(
createStyledSurfaceTextComponent({
id: `${params.screenId}-comparison-strip`,
type: "detail_comparison_strip",
id: `${params.screenId}-compare-before`,
type: "detail_compare_before",
parentId: `${params.screenId}-section-supporting`,
frame: {
x: safeArea.left,
y: supportingSectionY - 112,
width: Math.min(contentWidth, 420),
height: 158,
y: compareY,
width: compareColumnWidth,
height: 170,
},
zIndex: 2,
content: catalog.comparisonText,
role: "comparison_strip",
content: `${catalog.comparison.beforeTitle}\n${catalog.comparison.beforeBody}`,
role: "comparison_before",
style: {
fontSize: 17,
fontWeight: "700",
color: "#f8fafc",
color: "#334155",
textAlign: "left",
lineHeight: 1.4,
backgroundColor: "rgba(15,23,42,0.88)",
backgroundColor: "rgba(255,255,255,0.92)",
borderColor: "rgba(148,163,184,0.20)",
borderWidth: 1,
borderRadius: 26,
padding: 20,
boxShadow: "0 18px 44px rgba(15,23,42,0.10)",
backdropBlur: 10,
},
metadata: {
screenIndex: params.screenIndex,
componentRole: "comparison_before",
},
}),
);
components.push(
createStyledSurfaceTextComponent({
id: `${params.screenId}-compare-after`,
type: "detail_compare_after",
parentId: `${params.screenId}-section-supporting`,
frame: {
x: safeArea.left + compareColumnWidth + 12,
y: compareY,
width: compareColumnWidth,
height: 170,
},
zIndex: 2,
content: `${catalog.comparison.afterTitle}\n${catalog.comparison.afterBody}`,
role: "comparison_after",
style: {
fontSize: 17,
fontWeight: "800",
color: "#eff6ff",
textAlign: "left",
lineHeight: 1.4,
backgroundColor: "rgba(37,99,235,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)",
boxShadow: "0 20px 48px rgba(37,99,235,0.22)",
backdropBlur: 12,
},
metadata: {
screenIndex: params.screenIndex,
componentRole: "comparison_strip",
componentRole: "comparison_after",
},
}),
);
}
if (catalog.proofText) {
if (catalog.proof.value) {
const noteY = Math.min(
params.screenSize.height - 116,
supportingSectionY + supportingSectionHeight - 74,
@ -555,7 +746,7 @@ function buildSectionedTextComponents(params: {
height: 62,
},
zIndex: 2,
content: catalog.proofText,
content: `${catalog.proof.label}|${catalog.proof.value}`,
role: "proof_bar",
style: {
fontSize: 16,
@ -579,34 +770,106 @@ function buildSectionedTextComponents(params: {
);
}
if (catalog.ctaText) {
if (catalog.cta) {
components.push(
createStyledSurfaceTextComponent({
id: `${params.screenId}-cta-support`,
type: "detail_cta_support",
parentId: params.textStackId,
frame: {
x: safeArea.left,
y: Math.min(params.screenSize.height - 198, supportingSectionY + supportingSectionHeight + 4),
width: Math.min(contentWidth, 430),
height: 70,
},
zIndex: 2,
content: catalog.cta.highlightText,
role: "cta_support",
style: {
fontSize: 18,
fontWeight: "800",
color: "#0f172a",
textAlign: "left",
lineHeight: 1.2,
backgroundColor: "rgba(255,255,255,0.88)",
borderColor: "rgba(37,99,235,0.14)",
borderWidth: 1,
borderRadius: 24,
padding: 18,
boxShadow: "0 14px 40px rgba(15,23,42,0.10)",
backdropBlur: 10,
},
metadata: {
screenIndex: params.screenIndex,
componentRole: "cta_support",
},
}),
);
components.push(
createStyledSurfaceTextComponent({
id: `${params.screenId}-cta-button`,
type: "detail_cta_button",
parentId: params.textStackId,
frame: {
x: safeArea.left,
y: Math.min(params.screenSize.height - 118, supportingSectionY + supportingSectionHeight + 76),
width: Math.min(268, contentWidth * 0.62),
height: 58,
},
zIndex: 3,
content: `${catalog.cta.buttonLabel} →`,
role: "cta_button",
style: {
fontSize: 19,
fontWeight: "800",
color: "#f8fafc",
textAlign: "center",
lineHeight: 1.05,
letterSpacing: 0.2,
backgroundColor: "rgba(37,99,235,0.94)",
borderColor: "rgba(255,255,255,0.24)",
borderWidth: 1,
borderRadius: 999,
padding: 18,
boxShadow: "0 22px 58px rgba(37,99,235,0.28)",
backdropBlur: 12,
textTransform: "uppercase",
},
metadata: {
screenIndex: params.screenIndex,
componentRole: "cta_button",
},
}),
);
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,
x: safeArea.left + Math.min(268, contentWidth * 0.62) + 12,
y: Math.min(params.screenSize.height - 118, supportingSectionY + supportingSectionHeight + 76),
width: Math.max(140, Math.min(430, contentWidth) - Math.min(268, contentWidth * 0.62) - 12),
height: 58,
},
zIndex: 3,
content: catalog.ctaText,
zIndex: 2,
content: catalog.cta.supportText,
role: "cta_banner",
style: {
fontSize: 20,
fontWeight: "800",
color: "#f8fafc",
fontSize: 15,
fontWeight: "600",
color: "#eff6ff",
textAlign: "left",
lineHeight: 1.28,
backgroundColor: "rgba(37,99,235,0.88)",
borderColor: "rgba(255,255,255,0.24)",
lineHeight: 1.22,
backgroundColor: "rgba(15,23,42,0.74)",
borderColor: "rgba(255,255,255,0.16)",
borderWidth: 1,
borderRadius: 28,
padding: 22,
boxShadow: "0 22px 58px rgba(37,99,235,0.28)",
backdropBlur: 12,
borderRadius: 22,
padding: 16,
boxShadow: "0 18px 42px rgba(15,23,42,0.18)",
backdropBlur: 10,
},
metadata: {
screenIndex: params.screenIndex,

View File

@ -12,10 +12,17 @@ export type UiSurfaceComponentType =
| "detail_card"
| "detail_badge"
| "detail_feature_card"
| "detail_feature_tag"
| "detail_spec_table"
| "detail_table_label"
| "detail_table_value"
| "detail_comparison_strip"
| "detail_compare_before"
| "detail_compare_after"
| "detail_proof_bar"
| "detail_cta_banner";
| "detail_cta_banner"
| "detail_cta_button"
| "detail_cta_support";
export type UiSurfaceTextBindingKey = "title" | "subtitle" | "copy";