v0.10.11 首页 H11 Developer Center 三卡 1:1:节背景遮罩通用实现(电路板条图+渐变 overlay,行 relative);image-box pos-left 行布局;按钮设置驱动+悬停注入;列宽 margin 语义(全零 _margin 跳过/0.9rem/figure margin 0/kit 标题 Roboto);H4/H9/H10 全量回归零差异

This commit is contained in:
bigcode
2026-11-01 12:16:11 +08:00
parent b0b9c6d5a0
commit 46bd8d7655
5 changed files with 126 additions and 31 deletions
+92 -23
View File
@@ -200,9 +200,12 @@ function sectionBgStyle(settings: Record<string, unknown>): CSSProperties {
const img = (settings.background_image as { url?: string } | undefined)?.url;
if (img) {
style.backgroundImage = `url("${img}")`;
style.backgroundSize = "cover";
// "center" 会被浏览器归一为 "center center",直接写全值保证水合一致
style.backgroundPosition = "center center";
// 位置/重复取设置值(H11 底图 1920×240 条图:bottom center + no-repeat,
// 只在节底部铺一条,尺寸 auto 不缩放——写死 cover/center 会平铺满节,实测对不上)
const posRaw = s(settings.background_position);
style.backgroundPosition = posRaw ? mapBgPosition(posRaw) : "center center";
const rep = s(settings.background_repeat);
if (rep) style.backgroundRepeat = rep;
}
} else if (bgType === "gradient") {
const c1 = s(settings.background_color) || s(settings.color_a) || "#ffffff";
@@ -331,6 +334,25 @@ function SectionNode({ el }: { el: ElElement }) {
inner.maxWidth = width;
}
const gapPad = GAP_PAD[s(settings.gap) || "default"] || "10px";
// 背景遮罩(Elementor .elementor-background-overlay div):盖在背景图上、内容之下
// (H11 Developer Center:电路板底图 bottom center + F4F5F8→透明 180deg 渐变遮罩,
// 顶部近白、底部透出电路纹理,实测 2026-11-01)
const ovType = s((settings as Record<string, unknown>).background_overlay_background);
let overlay: ReactNode = null;
if (ovType === "gradient") {
const c1 = s((settings as Record<string, unknown>).background_overlay_color) || "transparent";
const c2 = s((settings as Record<string, unknown>).background_overlay_color_b) || "transparent";
const angle = sizeCss((settings as Record<string, unknown>).background_overlay_gradient_angle) || "180deg";
overlay = (
<div
aria-hidden
style={{ position: "absolute", inset: 0, background: `linear-gradient(${angle}, ${c1} 0%, ${c2} 100%)`, pointerEvents: "none" }}
/>
);
} else if (ovType === "classic") {
const c1 = s((settings as Record<string, unknown>).background_overlay_color);
if (c1) overlay = <div aria-hidden style={{ position: "absolute", inset: 0, background: c1, pointerEvents: "none" }} />;
}
// section 高度模式(Elementor: min-height / full),配合 custom_height;
// 取证(原站 09fe4b7 实测):height 未设置时 custom_height 不生效(min-height 0px),
// 故仅在显式 height 模式下应用
@@ -338,9 +360,12 @@ function SectionNode({ el }: { el: ElElement }) {
const ch = sizeCss(settings.custom_height);
if (heightMode === "min-height" && ch) style.minHeight = ch;
else if (heightMode === "full" && ch) style.height = ch;
if (overlay) style.position = style.position || "relative";
return (
<section style={style} data-el={el.id}>
<div className="mx-auto flex flex-wrap" style={inner}>
{overlay}
{/* 行加 relative:与绝对定位遮罩同属定位层,DOM 靠后绘制在上,内容不被蒙灰 */}
<div className="mx-auto flex flex-wrap" style={{ ...inner, position: "relative" }}>
{(el.elements || []).map((c, i) => (
<ElementNode key={c.id || i} el={c} gapPad={gapPad} />
))}
@@ -349,6 +374,20 @@ function SectionNode({ el }: { el: ElElement }) {
);
}
/** # 函数 盒模型值是否非全零(全零 _margin 视为未设置,kit-spacing 的 20px 才能生效;
* 原站 H11 text-editor _margin 全零但实测 margin-bottom 20px)
*/
function boxNonZero(v: unknown): boolean {
if (typeof v === "number") return v !== 0;
if (typeof v === "string") return /[^0\s.]/.test(v.replace(/(px|rem|em|%)/g, ""));
if (Array.isArray(v)) return v.some((x) => boxNonZero(x));
if (v && typeof v === "object") {
const o = v as Record<string, unknown>;
return (["top", "right", "bottom", "left"] as const).some((k) => boxNonZero(o[k]));
}
return false;
}
/** # 组件 column:双层对应原站 .elementor-column(外层)+ .elementor-element-populated(内层)
* 外层参与 flex 布局(按 _inline_size 百分比否则均分),不承载盒样式;
* 内层承载 populated 的盒样式:_margin(margin,移动卡片间距且不影响 flex 宽度,与原站一致)、
@@ -368,10 +407,10 @@ function ColumnNode({ el, gapPad }: { el: ElElement; gapPad?: string }) {
const inner: CSSProperties = {};
const mg = paddingCss(settings.margin);
if (mg) {
// margin 写在 populated(与 Elementor 相同):width 取 100% 外层宽而不随 margin 收缩
// (原站实测 populated rect 272px 与 1.6/6.4 margin 并存)
inner.width = "100%";
inner.margin = mg;
// margin 写在 populated(与 Elementor 相同):宽保持 auto——块级填充会自动扣除自身外边距
// (原站实测 H11 列 453.33 宽 + margin-right 10 → populated 443.33,与 auto 行为一致);
// 全零 margin 视为未设置(boxNonZero),避免覆盖 kit-spacing
if (boxNonZero(settings.margin)) inner.margin = mg;
}
inner.padding = paddingCss(settings.padding) || gapPad || "10px";
const bg = sectionBgStyle(settings);
@@ -387,6 +426,8 @@ function ColumnNode({ el, gapPad }: { el: ElElement; gapPad?: string }) {
} else inner.borderWidth = "1px";
}
return (
// 内层 width:100% 与列同宽(H4 卡 margin 内含、向右溢出,原站实测一致);
// H11 等少数列在 app.css 用 !important 覆写为 auto(原站 populated 扣除水平外边距 443.33)
<div className="el-col min-w-0" style={outer} data-el={el.id}>
<div style={{ display: "flex", flexWrap: "wrap", alignContent: "flex-start", width: "100%", ...inner }}>
{(el.elements || []).map((c, i) => (
@@ -410,7 +451,7 @@ function WidgetNode({ el }: { el: ElElement }) {
const pad = paddingCss(settings._padding);
if (pad) style.padding = pad;
const mrg = paddingCss(settings._margin);
if (mrg) style.margin = mrg;
if (mrg && boxNonZero(settings._margin)) style.margin = mrg;
if (s(settings._border_border) === "solid") {
style.border = `1px solid ${s(settings.border_color) || s(settings._border_color) || "#e3e9f0"}`;
const bw = paddingCss(settings._border_width);
@@ -482,7 +523,7 @@ function WidgetNodeInner({ el }: { el: ElElement }) {
case "image":
return <ImageWidget settings={settings} />;
case "button":
return <ButtonWidget settings={settings} />;
return <ButtonWidget el={el} settings={settings} />;
case "html":
// 间距交由 kit-spacing 列规则(.el-col > div > [data-el]),内层不再加 mb;
// 媒体地址经 localSiteUrl 改写为本地 /uploads(原站绝对地址 → 本地)
@@ -763,24 +804,43 @@ function ContactFormWidget({ settings }: { settings: Record<string, unknown> })
}
/** # 组件 button widget */
function ButtonWidget({ settings }: { settings: Record<string, unknown> }) {
/** # 组件 Elementor button widget:样式全部来自设置(背景/文字色/边框/圆角/内边距/排版,
* globals 已由 resolveGlobals 解析为实色;text_padding/typography 等实测 H11 按钮 12px 48px/18px)。
* 基础对齐 widget-button 默认:inline-block、line-height 1、圆角 3px;字重走 kit 链接 400
* (.content-html a),不在此硬编码。悬停态(button_background_hover_color/hover_color)
* 经节点级 <style> 注入(行内样式无法表达 :hover)。
*/
function ButtonWidget({ el, settings }: { el: ElElement; settings: Record<string, unknown> }) {
const text = s(settings.text);
const link = (settings.link as { url?: string; target?: string } | undefined)?.url;
if (!link && !text) return null;
const style: CSSProperties = {
display: "inline-block",
padding: "10px 24px",
borderRadius: 4,
background: s(settings.background_color) || "var(--brand)",
color: s(settings.button_text_color) || s(settings.text_color) || "#fff",
fontWeight: 600,
lineHeight: 1,
padding: paddingCss(settings.text_padding) || "12px 24px",
borderRadius: paddingCss(settings.border_radius) || "3px",
background: s(settings.background_color) || "#818a91",
color: s(settings.button_text_color) || "#fff",
textDecoration: "none",
};
if (s(settings.size) === "lg") style.padding = "14px 32px";
if (s(settings.size) === "sm") style.padding = "7px 16px";
if (!link && !text) return null;
// border_width 是标量/盒值,collapseBoxValues 折叠四值简写("1px 1px 1px 1px" → "1px"),
// 否则 `${bw} solid` 拼出非法 border 声明被浏览器丢弃
const bw = paddingCss(settings.border_width);
if (s(settings.border_border) === "solid") {
style.border = `${bw ? collapseBoxValues(bw) : "1px"} solid ${s(settings.border_color) || "transparent"}`;
} else if (bw) style.borderWidth = collapseBoxValues(bw);
const fs = sizeCss(settings.typography_font_size);
if (fs) style.fontSize = fs;
const fwt = settings.typography_font_weight;
if (fwt !== undefined && fwt !== null && fwt !== "") style.fontWeight = Number(fwt) || fwt as number;
const hoverBg = s(settings.button_background_hover_color);
const hoverColor = s(settings.hover_color);
return (
<div className="mb-[18px]" style={{ textAlign: (s(settings.align) as CSSProperties["textAlign"]) || "left" }}>
<a href={link || "#"} style={style} target={(settings.link as { target?: string } | undefined)?.target === "_blank" ? "_blank" : undefined} rel="noreferrer">
<div style={{ textAlign: (s(settings.align) as CSSProperties["textAlign"]) || "left" }}>
{hoverBg || hoverColor ? (
<style>{`[data-el="${el.id}"] .el-btn:hover{${hoverBg ? `background:${hoverBg};` : ""}${hoverColor ? `color:${hoverColor};` : ""}}`}</style>
) : null}
<a className="el-btn" href={link || "#"} style={style} target={(settings.link as { target?: string } | undefined)?.target === "_blank" ? "_blank" : undefined} rel="noreferrer">
{text}
</a>
</div>
@@ -819,11 +879,20 @@ function ImageBoxWidget({ settings }: { settings: Record<string, unknown> }) {
? tagOpt
: "h3") as "div";
const Tag = tag as "div";
// position left/right:wrapper 为 flex 行(widget-image-box.min.css .elementor-position-left
// .elementor-image-box-wrapper{display:flex;align-items:center},实测 H11 卡内图标行);
// 图宽 image_size%(post-18 由 image_size 控件生成 .elementor-image-box-img{width:18%})、
// 左侧布局 fig 右距 15px;标题底距 title_bottom_space(H11 实测 4px)
const isRow = pos === "left" || pos === "right";
const imgW = sizeCss(settings.image_size);
const titleSpace = sizeCss(settings.title_bottom_space);
// flex/边距在 app.css 的 .el-imagebox-pos-* 规则(带 768 断点),这里只带 per-widget 图宽
const figStyle: CSSProperties | undefined = isRow && imgW ? { width: imgW } : undefined;
return (
<div className={"el-imagebox" + posCls}>
<div className="el-imagebox-wrapper">
{url ? (
<figure className="el-imagebox-img">
<figure className="el-imagebox-img" style={figStyle}>
{link ? (
<a {...linkProps} tabIndex={-1}><img src={url} alt={alt} loading="lazy" /></a>
) : (
@@ -834,7 +903,7 @@ function ImageBoxWidget({ settings }: { settings: Record<string, unknown> }) {
{title || desc ? (
<div className="el-imagebox-content">
{title ? (
<Tag className="el-imagebox-title">
<Tag className="el-imagebox-title" style={titleSpace ? { marginBottom: titleSpace } : undefined}>
{link ? (
<a {...linkProps} dangerouslySetInnerHTML={{ __html: title }} />
) : (
+16
View File
@@ -15,6 +15,9 @@ const SETTING_KEYS = new Set([
"background_color_b",
"background_color_stop_b",
"background_image",
// section 级背景定位/平铺(H11 底图 bottom center + no-repeat)
"background_position",
"background_repeat",
"border_border",
"border_color",
"border_radius",
@@ -30,9 +33,16 @@ const SETTING_KEYS = new Set([
"text_color",
"title_color",
"button_text_color",
"button_background_hover_color",
"hover_color",
"text_padding",
// image-box 系列:图定位(position 默认 top)与标题标签(title_size 默认 h3)
"position",
"title_size",
// image-box 行布局(H11):图宽 image_size%、标题底距、内容垂直对齐
"image_size",
"title_bottom_space",
"content_vertical_alignment",
"gradient_angle",
"color_a",
"color_b",
@@ -112,6 +122,12 @@ const SETTING_KEYS = new Set([
"_background_image",
"_background_position",
"_background_repeat",
// section 背景遮罩(.elementor-background-overlay,H11 电路板图+渐变)
"background_overlay_background",
"background_overlay_color",
"background_overlay_color_b",
"background_overlay_opacity",
"background_overlay_gradient_angle",
"_border_border",
"_border_color",
"_border_width",