diff --git a/components/canvas/DetailSurfaceInspectorPanel.tsx b/components/canvas/DetailSurfaceInspectorPanel.tsx index 07531a401..32fc71afd 100644 --- a/components/canvas/DetailSurfaceInspectorPanel.tsx +++ b/components/canvas/DetailSurfaceInspectorPanel.tsx @@ -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; diff --git a/lib/detail-ui-renderer.ts b/lib/detail-ui-renderer.ts index be08b051d..ce4427d47 100644 --- a/lib/detail-ui-renderer.ts +++ b/lib/detail-ui-renderer.ts @@ -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; diff --git a/lib/detail-ui-surface-adapter.ts b/lib/detail-ui-surface-adapter.ts index 2750fb6f4..5adac6355 100644 --- a/lib/detail-ui-surface-adapter.ts +++ b/lib/detail-ui-surface-adapter.ts @@ -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, diff --git a/lib/ui-surface-protocol.ts b/lib/ui-surface-protocol.ts index 218371384..5a72679f8 100644 --- a/lib/ui-surface-protocol.ts +++ b/lib/ui-surface-protocol.ts @@ -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";