feat: upgrade detail section component renderer
This commit is contained in:
parent
d8aa1d1661
commit
a1b6496976
@ -32,15 +32,42 @@ function getActionClassName(action?: DetailSurfaceInspectorReviewState["action"]
|
|||||||
return "bg-emerald-50 text-emerald-700 ring-1 ring-inset ring-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300";
|
return "bg-emerald-50 text-emerald-700 ring-1 ring-inset ring-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectRenderableScreenComponents(surface: UiSurfaceSnapshot, parentId: string) {
|
||||||
|
const result = [] as typeof surface.components;
|
||||||
|
const queue = [...getUiSurfaceChildren(surface, parentId)];
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const component = queue.shift();
|
||||||
|
if (!component) continue;
|
||||||
|
|
||||||
|
if (
|
||||||
|
component.type === "detail_text_block" ||
|
||||||
|
component.type === "detail_card" ||
|
||||||
|
component.type === "detail_badge"
|
||||||
|
) {
|
||||||
|
result.push(component);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
queue.push(...getUiSurfaceChildren(surface, component.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export function DetailSurfaceInspectorPanel(params: {
|
export function DetailSurfaceInspectorPanel(params: {
|
||||||
surface: UiSurfaceSnapshot | null;
|
surface: UiSurfaceSnapshot | null;
|
||||||
latestScreenValidation?: DetailSurfaceInspectorReviewState | null;
|
latestScreenValidation?: DetailSurfaceInspectorReviewState | null;
|
||||||
}) {
|
}) {
|
||||||
if (!params.surface) return null;
|
if (!params.surface) return null;
|
||||||
|
const surface = params.surface;
|
||||||
|
|
||||||
const screenComponents = getUiSurfaceScreenComponents(params.surface);
|
const screenComponents = getUiSurfaceScreenComponents(surface);
|
||||||
const totalComponents = countUiSurfaceComponents(params.surface);
|
const totalRenderableBlocks =
|
||||||
const totalTextBlocks = countUiSurfaceComponents(params.surface, "detail_text_block");
|
countUiSurfaceComponents(surface, "detail_text_block") +
|
||||||
|
countUiSurfaceComponents(surface, "detail_card") +
|
||||||
|
countUiSurfaceComponents(surface, "detail_badge");
|
||||||
|
const totalSections = countUiSurfaceComponents(surface, "detail_section");
|
||||||
const totalScreens = screenComponents.length;
|
const totalScreens = screenComponents.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -51,12 +78,12 @@ export function DetailSurfaceInspectorPanel(params: {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-[11px] font-medium text-foreground">Detail Surface Inspector</p>
|
<p className="text-[11px] font-medium text-foreground">Detail Surface Inspector</p>
|
||||||
<p className="mt-0.5 text-[10px] text-muted-foreground">
|
<p className="mt-0.5 text-[10px] text-muted-foreground">
|
||||||
<span className="font-mono">{params.surface.surfaceId}</span>
|
<span className="font-mono">{surface.surfaceId}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="rounded-full bg-violet-50 px-2 py-0.5 text-[10px] font-medium text-violet-700 ring-1 ring-inset ring-violet-500/20 dark:bg-violet-500/10 dark:text-violet-300">
|
<span className="rounded-full bg-violet-50 px-2 py-0.5 text-[10px] font-medium text-violet-700 ring-1 ring-inset ring-violet-500/20 dark:bg-violet-500/10 dark:text-violet-300">
|
||||||
{params.surface.generationMode === "agent_layout" ? "AGENT Surface" : "Classic Surface"}
|
{surface.generationMode === "agent_layout" ? "AGENT Surface" : "Classic Surface"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -71,16 +98,16 @@ export function DetailSurfaceInspectorPanel(params: {
|
|||||||
<div className="rounded-lg border border-border/40 bg-background/80 px-3 py-2 dark:bg-zinc-950/40">
|
<div className="rounded-lg border border-border/40 bg-background/80 px-3 py-2 dark:bg-zinc-950/40">
|
||||||
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
|
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
|
||||||
<Blocks className="h-3.5 w-3.5" />
|
<Blocks className="h-3.5 w-3.5" />
|
||||||
Components
|
Sections
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-sm font-semibold text-foreground">{totalComponents}</div>
|
<div className="mt-1 text-sm font-semibold text-foreground">{totalSections}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border border-border/40 bg-background/80 px-3 py-2 dark:bg-zinc-950/40">
|
<div className="rounded-lg border border-border/40 bg-background/80 px-3 py-2 dark:bg-zinc-950/40">
|
||||||
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
|
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
|
||||||
<CheckCircle2 className="h-3.5 w-3.5" />
|
<CheckCircle2 className="h-3.5 w-3.5" />
|
||||||
Text Blocks
|
UI Blocks
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-sm font-semibold text-foreground">{totalTextBlocks}</div>
|
<div className="mt-1 text-sm font-semibold text-foreground">{totalRenderableBlocks}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -88,38 +115,44 @@ export function DetailSurfaceInspectorPanel(params: {
|
|||||||
<div>
|
<div>
|
||||||
<div className="text-[10px] text-muted-foreground">Surface Validation</div>
|
<div className="text-[10px] text-muted-foreground">Surface Validation</div>
|
||||||
<div className="mt-0.5 text-[11px] font-medium text-foreground">
|
<div className="mt-0.5 text-[11px] font-medium text-foreground">
|
||||||
{params.surface.validation?.passed === false
|
{surface.validation?.passed === false
|
||||||
? params.surface.validation.issues[0] || "存在待优化项"
|
? surface.validation.issues[0] || "存在待优化项"
|
||||||
: "当前 surface 已通过基础校验"}
|
: "当前 surface 已通过基础校验"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full px-2 py-0.5 text-[10px] font-medium",
|
"rounded-full px-2 py-0.5 text-[10px] font-medium",
|
||||||
params.surface.validation?.passed === false
|
surface.validation?.passed === false
|
||||||
? "bg-amber-50 text-amber-700 ring-1 ring-inset ring-amber-500/20 dark:bg-amber-500/10 dark:text-amber-300"
|
? "bg-amber-50 text-amber-700 ring-1 ring-inset ring-amber-500/20 dark:bg-amber-500/10 dark:text-amber-300"
|
||||||
: "bg-emerald-50 text-emerald-700 ring-1 ring-inset ring-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300",
|
: "bg-emerald-50 text-emerald-700 ring-1 ring-inset ring-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{params.surface.validation?.passed === false ? "待优化" : "通过"}
|
{surface.validation?.passed === false ? "待优化" : "通过"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 space-y-2">
|
||||||
{screenComponents.slice(0, 6).map((screenComponent) => {
|
{screenComponents.slice(0, 6).map((screenComponent) => {
|
||||||
const screenIndex = Number(screenComponent.props?.screenIndex || 0);
|
const screenIndex = Number(screenComponent.props?.screenIndex || 0);
|
||||||
const textStack = getUiSurfaceChildren(params.surface, screenComponent.id).find(
|
const textStack = getUiSurfaceChildren(surface, screenComponent.id).find(
|
||||||
(component) => component.type === "detail_text_stack",
|
(component) => component.type === "detail_text_stack",
|
||||||
);
|
);
|
||||||
const textBlocks = textStack
|
const textBlocks = textStack
|
||||||
? getUiSurfaceChildren(params.surface, textStack.id).filter(
|
? collectRenderableScreenComponents(surface, textStack.id)
|
||||||
(component) => component.type === "detail_text_block",
|
|
||||||
)
|
|
||||||
: [];
|
: [];
|
||||||
const titleBlock =
|
const titleBlock =
|
||||||
textBlocks.find((component) => component.props?.blockType === "title") || null;
|
textBlocks.find(
|
||||||
|
(component) =>
|
||||||
|
component.metadata?.componentRole === "title" ||
|
||||||
|
component.props?.blockType === "title",
|
||||||
|
) || null;
|
||||||
const subtitleBlock =
|
const subtitleBlock =
|
||||||
textBlocks.find((component) => component.props?.blockType === "subtitle") || null;
|
textBlocks.find(
|
||||||
|
(component) =>
|
||||||
|
component.metadata?.componentRole === "subtitle" ||
|
||||||
|
component.props?.blockType === "subtitle",
|
||||||
|
) || null;
|
||||||
const isLatestScreen = params.latestScreenValidation?.screenIndex === screenIndex;
|
const isLatestScreen = params.latestScreenValidation?.screenIndex === screenIndex;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -139,11 +172,11 @@ export function DetailSurfaceInspectorPanel(params: {
|
|||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-0.5 truncate text-[11px] text-foreground/85">
|
<div className="mt-0.5 truncate text-[11px] text-foreground/85">
|
||||||
{resolveUiSurfaceTextBinding(params.surface, titleBlock, "content") || "未定义标题"}
|
{resolveUiSurfaceTextBinding(surface, titleBlock, "content") || "未定义标题"}
|
||||||
</div>
|
</div>
|
||||||
{subtitleBlock ? (
|
{subtitleBlock ? (
|
||||||
<div className="mt-0.5 truncate text-[10px] text-muted-foreground">
|
<div className="mt-0.5 truncate text-[10px] text-muted-foreground">
|
||||||
{resolveUiSurfaceTextBinding(params.surface, subtitleBlock, "content")}
|
{resolveUiSurfaceTextBinding(surface, subtitleBlock, "content")}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -46,6 +46,41 @@ export function TextNode({ node, isSelected }: TextNodeProps) {
|
|||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const containerStyle = data.transparent
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
backgroundColor: data.backgroundColor || undefined,
|
||||||
|
borderColor: data.borderColor || undefined,
|
||||||
|
borderWidth:
|
||||||
|
typeof data.borderWidth === "number" ? `${data.borderWidth}px` : undefined,
|
||||||
|
borderStyle:
|
||||||
|
typeof data.borderWidth === "number" && data.borderWidth > 0 ? "solid" : undefined,
|
||||||
|
borderRadius:
|
||||||
|
typeof data.borderRadius === "number" ? `${data.borderRadius}px` : undefined,
|
||||||
|
padding: typeof data.padding === "number" ? `${data.padding}px` : undefined,
|
||||||
|
boxShadow: data.boxShadow || undefined,
|
||||||
|
backdropFilter:
|
||||||
|
typeof data.backdropBlur === "number" && data.backdropBlur > 0
|
||||||
|
? `blur(${data.backdropBlur}px)`
|
||||||
|
: undefined,
|
||||||
|
WebkitBackdropFilter:
|
||||||
|
typeof data.backdropBlur === "number" && data.backdropBlur > 0
|
||||||
|
? `blur(${data.backdropBlur}px)`
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const textStyle = {
|
||||||
|
fontSize: data.fontSize || 14,
|
||||||
|
fontFamily: data.fontFamily || undefined,
|
||||||
|
color: data.color || "inherit",
|
||||||
|
fontWeight: data.fontWeight || "normal",
|
||||||
|
lineHeight: data.lineHeight || 1.35,
|
||||||
|
letterSpacing:
|
||||||
|
typeof data.letterSpacing === "number" ? `${data.letterSpacing}px` : undefined,
|
||||||
|
textAlign: data.textAlign || "left",
|
||||||
|
textTransform: data.textTransform || "none",
|
||||||
|
} as const;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -56,6 +91,7 @@ export function TextNode({ node, isSelected }: TextNodeProps) {
|
|||||||
isSelected && (data.transparent ? "ring-1 ring-primary/50" : "border-primary")
|
isSelected && (data.transparent ? "ring-1 ring-primary/50" : "border-primary")
|
||||||
)}
|
)}
|
||||||
onDoubleClick={handleDoubleClick}
|
onDoubleClick={handleDoubleClick}
|
||||||
|
style={containerStyle}
|
||||||
>
|
>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<textarea
|
<textarea
|
||||||
@ -72,24 +108,12 @@ export function TextNode({ node, isSelected }: TextNodeProps) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-full h-full resize-none bg-transparent outline-none text-sm"
|
className="w-full h-full resize-none bg-transparent outline-none text-sm"
|
||||||
style={{
|
style={textStyle}
|
||||||
fontSize: data.fontSize || 14,
|
|
||||||
color: data.color || 'inherit',
|
|
||||||
fontWeight: data.fontWeight || 'normal',
|
|
||||||
lineHeight: data.lineHeight || 1.35,
|
|
||||||
textAlign: data.textAlign || "left",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="w-full h-full overflow-auto text-sm whitespace-pre-wrap"
|
className="w-full h-full overflow-auto text-sm whitespace-pre-wrap"
|
||||||
style={{
|
style={textStyle}
|
||||||
fontSize: data.fontSize || 14,
|
|
||||||
color: data.color || 'inherit',
|
|
||||||
fontWeight: data.fontWeight || 'normal',
|
|
||||||
lineHeight: data.lineHeight || 1.35,
|
|
||||||
textAlign: data.textAlign || "left",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{data.content || '双击编辑文本'}
|
{data.content || '双击编辑文本'}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -29,10 +29,21 @@ function createTextNodeDraft(params: {
|
|||||||
height: number;
|
height: number;
|
||||||
content: string;
|
content: string;
|
||||||
fontSize: number;
|
fontSize: number;
|
||||||
|
fontFamily?: string;
|
||||||
fontWeight: string;
|
fontWeight: string;
|
||||||
color: string;
|
color: string;
|
||||||
lineHeight: number;
|
lineHeight: number;
|
||||||
|
letterSpacing?: number;
|
||||||
textAlign: "left" | "center" | "right";
|
textAlign: "left" | "center" | "right";
|
||||||
|
transparent?: boolean;
|
||||||
|
backgroundColor?: string;
|
||||||
|
borderColor?: string;
|
||||||
|
borderWidth?: number;
|
||||||
|
borderRadius?: number;
|
||||||
|
padding?: number;
|
||||||
|
boxShadow?: string;
|
||||||
|
backdropBlur?: number;
|
||||||
|
textTransform?: "none" | "uppercase" | "lowercase" | "capitalize";
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
}): DetailTextLayerDraft {
|
}): DetailTextLayerDraft {
|
||||||
return {
|
return {
|
||||||
@ -42,11 +53,21 @@ function createTextNodeDraft(params: {
|
|||||||
data: {
|
data: {
|
||||||
content: params.content,
|
content: params.content,
|
||||||
fontSize: params.fontSize,
|
fontSize: params.fontSize,
|
||||||
|
fontFamily: params.fontFamily,
|
||||||
fontWeight: params.fontWeight,
|
fontWeight: params.fontWeight,
|
||||||
color: params.color,
|
color: params.color,
|
||||||
lineHeight: params.lineHeight,
|
lineHeight: params.lineHeight,
|
||||||
|
letterSpacing: params.letterSpacing,
|
||||||
textAlign: params.textAlign,
|
textAlign: params.textAlign,
|
||||||
transparent: true,
|
transparent: params.transparent ?? true,
|
||||||
|
backgroundColor: params.backgroundColor,
|
||||||
|
borderColor: params.borderColor,
|
||||||
|
borderWidth: params.borderWidth,
|
||||||
|
borderRadius: params.borderRadius,
|
||||||
|
padding: params.padding,
|
||||||
|
boxShadow: params.boxShadow,
|
||||||
|
backdropBlur: params.backdropBlur,
|
||||||
|
textTransform: params.textTransform,
|
||||||
metadata: params.metadata,
|
metadata: params.metadata,
|
||||||
} as TextNodeData & { metadata?: Record<string, unknown> },
|
} as TextNodeData & { metadata?: Record<string, unknown> },
|
||||||
connections: [],
|
connections: [],
|
||||||
@ -119,6 +140,43 @@ export function buildDetailTextLayerNodesFromReviewPatch(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectRenderableSurfaceComponents(params: {
|
||||||
|
surface: UiSurfaceSnapshot;
|
||||||
|
parentId: string;
|
||||||
|
}) {
|
||||||
|
const collected: UiSurfaceSnapshot["components"] = [];
|
||||||
|
const queue = getUiSurfaceChildren(params.surface, params.parentId)
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftZ = Number(left.layout?.zIndex || 0);
|
||||||
|
const rightZ = Number(right.layout?.zIndex || 0);
|
||||||
|
return leftZ - rightZ;
|
||||||
|
});
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const component = queue.shift();
|
||||||
|
if (!component) continue;
|
||||||
|
|
||||||
|
if (
|
||||||
|
component.type === "detail_card" ||
|
||||||
|
component.type === "detail_badge" ||
|
||||||
|
component.type === "detail_text_block"
|
||||||
|
) {
|
||||||
|
collected.push(component);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = getUiSurfaceChildren(params.surface, component.id)
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftZ = Number(left.layout?.zIndex || 0);
|
||||||
|
const rightZ = Number(right.layout?.zIndex || 0);
|
||||||
|
return leftZ - rightZ;
|
||||||
|
});
|
||||||
|
queue.unshift(...children);
|
||||||
|
}
|
||||||
|
|
||||||
|
return collected;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildDetailTextLayerNodesFromSurfaceScreen(params: {
|
export function buildDetailTextLayerNodesFromSurfaceScreen(params: {
|
||||||
parentNodeId: string;
|
parentNodeId: string;
|
||||||
parentPosition: { x: number; y: number };
|
parentPosition: { x: number; y: number };
|
||||||
@ -144,8 +202,10 @@ export function buildDetailTextLayerNodesFromSurfaceScreen(params: {
|
|||||||
const scaleX = params.parentSize.width / Math.max(1, params.surface.screenSize.width);
|
const scaleX = params.parentSize.width / Math.max(1, params.surface.screenSize.width);
|
||||||
const scaleY = params.parentSize.height / Math.max(1, params.surface.screenSize.height);
|
const scaleY = params.parentSize.height / Math.max(1, params.surface.screenSize.height);
|
||||||
|
|
||||||
return getUiSurfaceChildren(params.surface, textStack.id)
|
return collectRenderableSurfaceComponents({
|
||||||
.filter((component) => component.type === "detail_text_block")
|
surface: params.surface,
|
||||||
|
parentId: textStack.id,
|
||||||
|
})
|
||||||
.map((component) => {
|
.map((component) => {
|
||||||
const frame = (component.layout?.frame || {}) as {
|
const frame = (component.layout?.frame || {}) as {
|
||||||
x?: number;
|
x?: number;
|
||||||
@ -160,18 +220,42 @@ export function buildDetailTextLayerNodesFromSurfaceScreen(params: {
|
|||||||
y: params.parentPosition.y + Math.round(Number(frame.y || 0) * scaleY),
|
y: params.parentPosition.y + Math.round(Number(frame.y || 0) * scaleY),
|
||||||
width: Math.max(120, Math.round(Number(frame.width || 240) * scaleX)),
|
width: Math.max(120, Math.round(Number(frame.width || 240) * scaleX)),
|
||||||
height: Math.max(48, Math.round(Number(frame.height || 60) * scaleY)),
|
height: Math.max(48, Math.round(Number(frame.height || 60) * scaleY)),
|
||||||
content: resolveUiSurfaceTextBinding(params.surface, component, "content"),
|
content:
|
||||||
|
resolveUiSurfaceTextBinding(params.surface, component, "content") ||
|
||||||
|
String(component.props?.content || "").trim(),
|
||||||
fontSize: Math.max(
|
fontSize: Math.max(
|
||||||
12,
|
12,
|
||||||
Math.round(Number(style.fontSize || 24) * Math.min(scaleX, scaleY)),
|
Math.round(Number(style.fontSize || 24) * Math.min(scaleX, scaleY)),
|
||||||
),
|
),
|
||||||
|
fontFamily: typeof style.fontFamily === "string" ? style.fontFamily : undefined,
|
||||||
fontWeight: String(style.fontWeight || "500"),
|
fontWeight: String(style.fontWeight || "500"),
|
||||||
color: String(style.color || "#111827"),
|
color: String(style.color || "#111827"),
|
||||||
lineHeight: Number(style.lineHeight || 1.3),
|
lineHeight: Number(style.lineHeight || 1.3),
|
||||||
|
letterSpacing:
|
||||||
|
typeof style.letterSpacing === "number" ? style.letterSpacing : undefined,
|
||||||
textAlign:
|
textAlign:
|
||||||
style.textAlign === "center" || style.textAlign === "right"
|
style.textAlign === "center" || style.textAlign === "right"
|
||||||
? style.textAlign
|
? style.textAlign
|
||||||
: "left",
|
: "left",
|
||||||
|
transparent: component.type === "detail_text_block" ? true : false,
|
||||||
|
backgroundColor:
|
||||||
|
typeof style.backgroundColor === "string" ? style.backgroundColor : undefined,
|
||||||
|
borderColor:
|
||||||
|
typeof style.borderColor === "string" ? style.borderColor : undefined,
|
||||||
|
borderWidth:
|
||||||
|
typeof style.borderWidth === "number" ? style.borderWidth : undefined,
|
||||||
|
borderRadius:
|
||||||
|
typeof style.borderRadius === "number" ? style.borderRadius : undefined,
|
||||||
|
padding: typeof style.padding === "number" ? style.padding : undefined,
|
||||||
|
boxShadow: typeof style.boxShadow === "string" ? style.boxShadow : undefined,
|
||||||
|
backdropBlur:
|
||||||
|
typeof style.backdropBlur === "number" ? style.backdropBlur : undefined,
|
||||||
|
textTransform:
|
||||||
|
style.textTransform === "uppercase" ||
|
||||||
|
style.textTransform === "lowercase" ||
|
||||||
|
style.textTransform === "capitalize"
|
||||||
|
? style.textTransform
|
||||||
|
: "none",
|
||||||
metadata: {
|
metadata: {
|
||||||
detailUiManaged: true,
|
detailUiManaged: true,
|
||||||
detailUiBlockId: String(component.metadata?.blockId || component.id),
|
detailUiBlockId: String(component.metadata?.blockId || component.id),
|
||||||
|
|||||||
@ -12,12 +12,6 @@ import type {
|
|||||||
UiSurfaceSnapshot,
|
UiSurfaceSnapshot,
|
||||||
} from "@/lib/ui-surface-protocol";
|
} from "@/lib/ui-surface-protocol";
|
||||||
|
|
||||||
function mapBlockTypeToBindingKey(blockType: "title" | "subtitle" | "body") {
|
|
||||||
if (blockType === "title") return "title";
|
|
||||||
if (blockType === "subtitle") return "subtitle";
|
|
||||||
return "copy";
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDetailSurfaceId(params: {
|
function createDetailSurfaceId(params: {
|
||||||
generationMode: DetailGenerationMode;
|
generationMode: DetailGenerationMode;
|
||||||
screenCount: number;
|
screenCount: number;
|
||||||
@ -36,12 +30,426 @@ function createRootComponent(): UiSurfaceComponent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DetailSurfaceLayoutVariant = "hero_left" | "hero_right" | "split_bottom";
|
||||||
|
|
||||||
|
function splitBodyCopyIntoHighlights(copy?: string) {
|
||||||
|
const segments = String(copy || "")
|
||||||
|
.split(/[。\n!!??;;]+/g)
|
||||||
|
.map((segment) => segment.replace(/\s+/g, " ").trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (segments.length === 0) return [];
|
||||||
|
if (segments.length <= 3) return segments;
|
||||||
|
|
||||||
|
return [
|
||||||
|
segments.slice(0, 2).join(" · "),
|
||||||
|
segments.slice(2, 4).join(" · "),
|
||||||
|
segments.slice(4).join(" · "),
|
||||||
|
].filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickScreenLayoutVariant(params: {
|
||||||
|
screenIndex: number;
|
||||||
|
screenType?: string;
|
||||||
|
}): DetailSurfaceLayoutVariant {
|
||||||
|
if (params.screenIndex === 1 || params.screenType === "B") {
|
||||||
|
return "hero_left";
|
||||||
|
}
|
||||||
|
if (params.screenType === "A") {
|
||||||
|
return "hero_right";
|
||||||
|
}
|
||||||
|
return "split_bottom";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildScreenBadgeLabel(params: {
|
||||||
|
screenIndex: number;
|
||||||
|
screenType?: string;
|
||||||
|
objective?: string;
|
||||||
|
}) {
|
||||||
|
const normalizedObjective = String(params.objective || "").replace(/\s+/g, " ").trim();
|
||||||
|
if (normalizedObjective) return normalizedObjective.slice(0, 18);
|
||||||
|
|
||||||
|
if (params.screenType === "A") return "技术证明";
|
||||||
|
if (params.screenType === "B") return "场景共鸣";
|
||||||
|
if (params.screenType === "C") return "细节证明";
|
||||||
|
return `第 ${params.screenIndex} 屏`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStyledSurfaceTextComponent(params: {
|
||||||
|
id: string;
|
||||||
|
type: "detail_card" | "detail_badge" | "detail_text_block";
|
||||||
|
parentId: string;
|
||||||
|
frame: { x: number; y: number; width: number; height: number };
|
||||||
|
zIndex?: number;
|
||||||
|
content?: string;
|
||||||
|
bindingPath?: string;
|
||||||
|
role: string;
|
||||||
|
style: Record<string, unknown>;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
id: params.id,
|
||||||
|
type: params.type,
|
||||||
|
parentId: params.parentId,
|
||||||
|
props: {
|
||||||
|
role: params.role,
|
||||||
|
content: params.content || "",
|
||||||
|
},
|
||||||
|
bindings: params.bindingPath
|
||||||
|
? {
|
||||||
|
content: {
|
||||||
|
kind: "text" as const,
|
||||||
|
path: params.bindingPath,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
layout: {
|
||||||
|
frame: params.frame,
|
||||||
|
zIndex: params.zIndex || 1,
|
||||||
|
},
|
||||||
|
style: params.style,
|
||||||
|
metadata: params.metadata,
|
||||||
|
} satisfies UiSurfaceComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSectionComponent(params: {
|
||||||
|
id: string;
|
||||||
|
parentId: string;
|
||||||
|
role: string;
|
||||||
|
frame: { x: number; y: number; width: number; height: number };
|
||||||
|
zIndex?: number;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
id: params.id,
|
||||||
|
type: "detail_section" as const,
|
||||||
|
parentId: params.parentId,
|
||||||
|
props: {
|
||||||
|
role: params.role,
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
frame: params.frame,
|
||||||
|
zIndex: params.zIndex || 1,
|
||||||
|
},
|
||||||
|
metadata: params.metadata,
|
||||||
|
} satisfies UiSurfaceComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSectionedTextComponents(params: {
|
||||||
|
screenId: string;
|
||||||
|
textStackId: string;
|
||||||
|
screenIndex: number;
|
||||||
|
screenSize: { width: number; height: number };
|
||||||
|
screen: Pick<
|
||||||
|
DetailUiScreenInput,
|
||||||
|
"title" | "subtitle" | "copy" | "screenType" | "objective" | "layoutNote"
|
||||||
|
>;
|
||||||
|
screenProtocol?: DetailUiScreenProtocol;
|
||||||
|
}) {
|
||||||
|
const safeArea = params.screenProtocol?.safeArea || {
|
||||||
|
top: 64,
|
||||||
|
bottom: 360,
|
||||||
|
left: 56,
|
||||||
|
right: 56,
|
||||||
|
};
|
||||||
|
const contentWidth = Math.max(
|
||||||
|
260,
|
||||||
|
Math.round(params.screenSize.width - safeArea.left - safeArea.right),
|
||||||
|
);
|
||||||
|
const variant = pickScreenLayoutVariant({
|
||||||
|
screenIndex: params.screenIndex,
|
||||||
|
screenType: params.screen.screenType,
|
||||||
|
});
|
||||||
|
const badgeLabel = buildScreenBadgeLabel({
|
||||||
|
screenIndex: params.screenIndex,
|
||||||
|
screenType: params.screen.screenType,
|
||||||
|
objective: params.screen.objective,
|
||||||
|
});
|
||||||
|
const bodyHighlights = splitBodyCopyIntoHighlights(params.screen.copy);
|
||||||
|
|
||||||
|
const titleBlock =
|
||||||
|
params.screenProtocol?.blocks.find((block) => block.type === "title") || null;
|
||||||
|
const subtitleBlock =
|
||||||
|
params.screenProtocol?.blocks.find((block) => block.type === "subtitle") || null;
|
||||||
|
const bodyBlock =
|
||||||
|
params.screenProtocol?.blocks.find((block) => block.type === "body") || null;
|
||||||
|
|
||||||
|
const cardX =
|
||||||
|
variant === "hero_right"
|
||||||
|
? Math.round(params.screenSize.width - safeArea.right - Math.min(430, contentWidth))
|
||||||
|
: safeArea.left;
|
||||||
|
const cardWidth = Math.min(430, contentWidth);
|
||||||
|
const titleY = safeArea.top + 42;
|
||||||
|
const subtitleY = titleY + Math.max(124, Math.round(Number(titleBlock?.frame.height || 132))) + 14;
|
||||||
|
|
||||||
|
const components: UiSurfaceComponent[] = [];
|
||||||
|
|
||||||
|
components.push(
|
||||||
|
createSectionComponent({
|
||||||
|
id: `${params.screenId}-section-hero`,
|
||||||
|
parentId: params.textStackId,
|
||||||
|
role: "hero_section",
|
||||||
|
frame: {
|
||||||
|
x: cardX,
|
||||||
|
y: safeArea.top,
|
||||||
|
width: cardWidth,
|
||||||
|
height: Math.min(360, params.screenSize.height - safeArea.top - 120),
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
screenIndex: params.screenIndex,
|
||||||
|
variant,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
components.push(
|
||||||
|
createStyledSurfaceTextComponent({
|
||||||
|
id: `${params.screenId}-badge`,
|
||||||
|
type: "detail_badge",
|
||||||
|
parentId: `${params.screenId}-section-hero`,
|
||||||
|
frame: {
|
||||||
|
x: cardX,
|
||||||
|
y: safeArea.top,
|
||||||
|
width: Math.min(156, Math.max(104, badgeLabel.length * 18)),
|
||||||
|
height: 34,
|
||||||
|
},
|
||||||
|
zIndex: 3,
|
||||||
|
content: badgeLabel,
|
||||||
|
role: "screen_badge",
|
||||||
|
style: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "700",
|
||||||
|
color: "#f8fafc",
|
||||||
|
textAlign: "center",
|
||||||
|
lineHeight: 1.15,
|
||||||
|
letterSpacing: 0.4,
|
||||||
|
backgroundColor: "rgba(15,23,42,0.82)",
|
||||||
|
borderColor: "rgba(255,255,255,0.18)",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 999,
|
||||||
|
padding: 8,
|
||||||
|
boxShadow: "0 10px 28px rgba(15,23,42,0.20)",
|
||||||
|
backdropBlur: 8,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
screenIndex: params.screenIndex,
|
||||||
|
componentRole: "badge",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
components.push(
|
||||||
|
createStyledSurfaceTextComponent({
|
||||||
|
id: `${params.screenId}-title-card`,
|
||||||
|
type: "detail_card",
|
||||||
|
parentId: `${params.screenId}-section-hero`,
|
||||||
|
frame: {
|
||||||
|
x: cardX,
|
||||||
|
y: titleY,
|
||||||
|
width: cardWidth,
|
||||||
|
height: Math.max(132, Math.round(Number(titleBlock?.frame.height || 132) + 34)),
|
||||||
|
},
|
||||||
|
zIndex: 2,
|
||||||
|
bindingPath: `/screens/${params.screenIndex - 1}/title`,
|
||||||
|
role: "title_card",
|
||||||
|
style: {
|
||||||
|
fontFamily: titleBlock?.style.fontFamily || undefined,
|
||||||
|
fontSize: Math.max(34, Number(titleBlock?.style.fontSize || 52)),
|
||||||
|
fontWeight: titleBlock?.style.fontWeight || "800",
|
||||||
|
color: "#0f172a",
|
||||||
|
textAlign: "left",
|
||||||
|
lineHeight: 1.08,
|
||||||
|
letterSpacing: -0.8,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.86)",
|
||||||
|
borderColor: "rgba(255,255,255,0.38)",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 28,
|
||||||
|
padding: 24,
|
||||||
|
boxShadow: "0 22px 58px rgba(15,23,42,0.18)",
|
||||||
|
backdropBlur: 14,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
screenIndex: params.screenIndex,
|
||||||
|
componentRole: "title",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (params.screen.subtitle) {
|
||||||
|
components.push(
|
||||||
|
createStyledSurfaceTextComponent({
|
||||||
|
id: `${params.screenId}-subtitle-card`,
|
||||||
|
type: "detail_card",
|
||||||
|
parentId: `${params.screenId}-section-hero`,
|
||||||
|
frame: {
|
||||||
|
x: cardX,
|
||||||
|
y: subtitleY,
|
||||||
|
width: Math.min(cardWidth, 360),
|
||||||
|
height: Math.max(72, Math.round(Number(subtitleBlock?.frame.height || 64) + 26)),
|
||||||
|
},
|
||||||
|
zIndex: 2,
|
||||||
|
bindingPath: `/screens/${params.screenIndex - 1}/subtitle`,
|
||||||
|
role: "subtitle_card",
|
||||||
|
style: {
|
||||||
|
fontFamily: subtitleBlock?.style.fontFamily || undefined,
|
||||||
|
fontSize: Math.max(22, Number(subtitleBlock?.style.fontSize || 28)),
|
||||||
|
fontWeight: subtitleBlock?.style.fontWeight || "700",
|
||||||
|
color: "#f8fafc",
|
||||||
|
textAlign: "left",
|
||||||
|
lineHeight: 1.18,
|
||||||
|
backgroundColor:
|
||||||
|
variant === "hero_right" ? "rgba(37,99,235,0.82)" : "rgba(30,41,59,0.78)",
|
||||||
|
borderColor: "rgba(255,255,255,0.18)",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 22,
|
||||||
|
padding: 18,
|
||||||
|
boxShadow: "0 18px 42px rgba(15,23,42,0.18)",
|
||||||
|
backdropBlur: 12,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
screenIndex: params.screenIndex,
|
||||||
|
componentRole: "subtitle",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const supportingSectionY =
|
||||||
|
variant === "split_bottom" ? Math.round(params.screenSize.height * 0.72) : subtitleY + 96;
|
||||||
|
const supportingSectionHeight =
|
||||||
|
variant === "split_bottom"
|
||||||
|
? Math.min(320, params.screenSize.height - supportingSectionY - 80)
|
||||||
|
: 240;
|
||||||
|
|
||||||
|
components.push(
|
||||||
|
createSectionComponent({
|
||||||
|
id: `${params.screenId}-section-supporting`,
|
||||||
|
parentId: params.textStackId,
|
||||||
|
role: variant === "split_bottom" ? "supporting_bottom_section" : "supporting_grid",
|
||||||
|
frame: {
|
||||||
|
x: safeArea.left,
|
||||||
|
y: supportingSectionY,
|
||||||
|
width: contentWidth,
|
||||||
|
height: supportingSectionHeight,
|
||||||
|
},
|
||||||
|
zIndex: 1,
|
||||||
|
metadata: {
|
||||||
|
screenIndex: params.screenIndex,
|
||||||
|
variant,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const supportingCards =
|
||||||
|
bodyHighlights.length > 0
|
||||||
|
? bodyHighlights.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));
|
||||||
|
const supportCardWidth = Math.floor(
|
||||||
|
(contentWidth - cardGap * Math.max(0, supportColumns - 1)) / supportColumns,
|
||||||
|
);
|
||||||
|
|
||||||
|
supportingCards.forEach((highlight, index) => {
|
||||||
|
const column = supportColumns === 0 ? 0 : index % supportColumns;
|
||||||
|
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}-support-card-${index + 1}`,
|
||||||
|
type: "detail_card",
|
||||||
|
parentId: `${params.screenId}-section-supporting`,
|
||||||
|
frame: {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width: supportCardWidth,
|
||||||
|
height: variant === "split_bottom" ? 102 : 96,
|
||||||
|
},
|
||||||
|
zIndex: 2,
|
||||||
|
content: highlight,
|
||||||
|
role: "support_card",
|
||||||
|
style: {
|
||||||
|
fontSize: bodyBlock ? Math.max(18, Number(bodyBlock.style.fontSize || 22)) : 20,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#111827",
|
||||||
|
textAlign: "left",
|
||||||
|
lineHeight: 1.32,
|
||||||
|
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)",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 24,
|
||||||
|
padding: 18,
|
||||||
|
boxShadow: "0 18px 42px rgba(15,23,42,0.12)",
|
||||||
|
backdropBlur: 12,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
screenIndex: params.screenIndex,
|
||||||
|
componentRole: "support_card",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (params.screen.layoutNote) {
|
||||||
|
const noteY = Math.min(
|
||||||
|
params.screenSize.height - 116,
|
||||||
|
supportingSectionY + supportingSectionHeight - 74,
|
||||||
|
);
|
||||||
|
components.push(
|
||||||
|
createStyledSurfaceTextComponent({
|
||||||
|
id: `${params.screenId}-proof-card`,
|
||||||
|
type: "detail_card",
|
||||||
|
parentId: `${params.screenId}-section-supporting`,
|
||||||
|
frame: {
|
||||||
|
x:
|
||||||
|
variant === "hero_right"
|
||||||
|
? Math.round(params.screenSize.width - safeArea.right - Math.min(260, contentWidth * 0.46))
|
||||||
|
: safeArea.left,
|
||||||
|
y: noteY,
|
||||||
|
width: Math.min(260, contentWidth * 0.46),
|
||||||
|
height: 62,
|
||||||
|
},
|
||||||
|
zIndex: 2,
|
||||||
|
content: String(params.screen.layoutNote || "").replace(/\s+/g, " ").trim(),
|
||||||
|
role: "proof_chip",
|
||||||
|
style: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#1e293b",
|
||||||
|
textAlign: "left",
|
||||||
|
lineHeight: 1.22,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.88)",
|
||||||
|
borderColor: "rgba(15,23,42,0.10)",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 18,
|
||||||
|
padding: 14,
|
||||||
|
boxShadow: "0 14px 36px rgba(15,23,42,0.12)",
|
||||||
|
backdropBlur: 10,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
screenIndex: params.screenIndex,
|
||||||
|
componentRole: "proof_chip",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return components;
|
||||||
|
}
|
||||||
|
|
||||||
function createScreenSubtree(params: {
|
function createScreenSubtree(params: {
|
||||||
screenIndex: number;
|
screenIndex: number;
|
||||||
screen: Pick<
|
screen: Pick<
|
||||||
DetailUiScreenInput,
|
DetailUiScreenInput,
|
||||||
"title" | "subtitle" | "copy" | "screenType" | "objective" | "layoutNote"
|
"title" | "subtitle" | "copy" | "screenType" | "objective" | "layoutNote"
|
||||||
>;
|
>;
|
||||||
|
screenSize: { width: number; height: number };
|
||||||
screenProtocol?: DetailUiScreenProtocol;
|
screenProtocol?: DetailUiScreenProtocol;
|
||||||
rootId?: string;
|
rootId?: string;
|
||||||
}) {
|
}) {
|
||||||
@ -91,42 +499,16 @@ function createScreenSubtree(params: {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const blocks = params.screenProtocol?.blocks || [];
|
components.push(
|
||||||
blocks.forEach((block, blockIndex) => {
|
...buildSectionedTextComponents({
|
||||||
const bindingKey = mapBlockTypeToBindingKey(block.type);
|
screenId,
|
||||||
components.push({
|
textStackId,
|
||||||
id: `${screenId}-text-${blockIndex + 1}`,
|
screenIndex: params.screenIndex,
|
||||||
type: "detail_text_block",
|
screenSize: params.screenSize,
|
||||||
parentId: textStackId,
|
screen: params.screen,
|
||||||
props: {
|
screenProtocol: params.screenProtocol,
|
||||||
blockType: block.type,
|
}),
|
||||||
frame: block.frame,
|
);
|
||||||
textKey: bindingKey,
|
|
||||||
},
|
|
||||||
bindings: {
|
|
||||||
content: {
|
|
||||||
kind: "text",
|
|
||||||
path: `/screens/${params.screenIndex - 1}/${bindingKey}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
style: {
|
|
||||||
fontFamily: block.style.fontFamily || "",
|
|
||||||
fontSize: block.style.fontSize,
|
|
||||||
fontWeight: block.style.fontWeight,
|
|
||||||
lineHeight: block.style.lineHeight,
|
|
||||||
color: block.style.color,
|
|
||||||
textAlign: block.style.textAlign,
|
|
||||||
},
|
|
||||||
layout: {
|
|
||||||
frame: block.frame,
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
screenIndex: params.screenIndex,
|
|
||||||
blockType: block.type,
|
|
||||||
blockId: block.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return components;
|
return components;
|
||||||
}
|
}
|
||||||
@ -180,6 +562,7 @@ export function buildDetailUiSurfaceSnapshot(params: {
|
|||||||
...createScreenSubtree({
|
...createScreenSubtree({
|
||||||
screenIndex: screen.index,
|
screenIndex: screen.index,
|
||||||
screen,
|
screen,
|
||||||
|
screenSize: params.screenSize,
|
||||||
screenProtocol,
|
screenProtocol,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -231,9 +614,17 @@ export function buildDetailUiSurfacePatchFromReview(params: {
|
|||||||
const replacementComponents = createScreenSubtree({
|
const replacementComponents = createScreenSubtree({
|
||||||
screenIndex: params.screenIndex,
|
screenIndex: params.screenIndex,
|
||||||
screen: params.screen,
|
screen: params.screen,
|
||||||
|
screenSize: {
|
||||||
|
width: 790,
|
||||||
|
height: 1300,
|
||||||
|
},
|
||||||
screenProtocol: params.uiPatch.recommendedScreen,
|
screenProtocol: params.uiPatch.recommendedScreen,
|
||||||
rootId: screenId,
|
rootId: screenId,
|
||||||
}).filter((component) => component.id === textStackId || component.parentId === textStackId);
|
}).filter(
|
||||||
|
(component) =>
|
||||||
|
component.id !== screenId &&
|
||||||
|
component.id !== `${screenId}-background`,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: "ui-surface-patch/v1",
|
version: "ui-surface-patch/v1",
|
||||||
|
|||||||
@ -7,7 +7,10 @@ export type UiSurfaceComponentType =
|
|||||||
| "detail_screen"
|
| "detail_screen"
|
||||||
| "detail_background_image"
|
| "detail_background_image"
|
||||||
| "detail_text_stack"
|
| "detail_text_stack"
|
||||||
| "detail_text_block";
|
| "detail_text_block"
|
||||||
|
| "detail_section"
|
||||||
|
| "detail_card"
|
||||||
|
| "detail_badge";
|
||||||
|
|
||||||
export type UiSurfaceTextBindingKey = "title" | "subtitle" | "copy";
|
export type UiSurfaceTextBindingKey = "title" | "subtitle" | "copy";
|
||||||
|
|
||||||
|
|||||||
@ -77,11 +77,21 @@ export interface VideoNodeData {
|
|||||||
export interface TextNodeData {
|
export interface TextNodeData {
|
||||||
content: string;
|
content: string;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
|
fontFamily?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
fontWeight?: string;
|
fontWeight?: string;
|
||||||
lineHeight?: number;
|
lineHeight?: number;
|
||||||
|
letterSpacing?: number;
|
||||||
textAlign?: "left" | "center" | "right";
|
textAlign?: "left" | "center" | "right";
|
||||||
transparent?: boolean;
|
transparent?: boolean;
|
||||||
|
backgroundColor?: string;
|
||||||
|
borderColor?: string;
|
||||||
|
borderWidth?: number;
|
||||||
|
borderRadius?: number;
|
||||||
|
padding?: number;
|
||||||
|
boxShadow?: string;
|
||||||
|
backdropBlur?: number;
|
||||||
|
textTransform?: "none" | "uppercase" | "lowercase" | "capitalize";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 场景裂变节点数据
|
// 场景裂变节点数据
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user