diff --git a/components/canvas/DetailSurfaceInspectorPanel.tsx b/components/canvas/DetailSurfaceInspectorPanel.tsx index 563139514..07531a401 100644 --- a/components/canvas/DetailSurfaceInspectorPanel.tsx +++ b/components/canvas/DetailSurfaceInspectorPanel.tsx @@ -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; diff --git a/lib/detail-ui-renderer.ts b/lib/detail-ui-renderer.ts index ab9b6d465..be08b051d 100644 --- a/lib/detail-ui-renderer.ts +++ b/lib/detail-ui-renderer.ts @@ -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; diff --git a/lib/detail-ui-surface-adapter.ts b/lib/detail-ui-surface-adapter.ts index ee8c0c1b0..2750fb6f4 100644 --- a/lib/detail-ui-surface-adapter.ts +++ b/lib/detail-ui-surface-adapter.ts @@ -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, diff --git a/lib/ui-surface-protocol.ts b/lib/ui-surface-protocol.ts index 38445b9ff..218371384 100644 --- a/lib/ui-surface-protocol.ts +++ b/lib/ui-surface-protocol.ts @@ -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";