v0.10.7 首页 H4 五卡组 1:1:eael-info-box 完整结构/WidgetNode 双层容器/列 margin 折叠/heading link/kit-spacing 20/image inline-block,sec.h 与原站一致(529.6)

This commit is contained in:
BigCode
2026-11-01 02:42:56 +08:00
parent 37b71ff230
commit 544cbdc5f6
5 changed files with 258 additions and 41 deletions
+1 -1
View File
@@ -37,7 +37,7 @@
| H1 | 公告滚动条 | eael-content-ticker | ✅ v0.10.4(badge 18px/600、内容 #222 hover #ED6C00、fade 200ms/5000ms、1300px 容器、bg #F4F5F8,数据取最新 3 产品) |
| H2 | 产品线入口 ×6 | image-box ×6(6 条产品线图标卡) | ✅ v0.10.5(图片上方图标+标题 16px/400/#333、figure 30% 宽、hover 亮度 90%、五列右侧 v-line 竖线、列 10px 内边距、≤767 50% 两列;ImageBoxWidget 重构为 Elementor 忠实 DOM 供 H13/about 复用) |
| H3 | 「Dusun Hardware & Service」标题条 | heading | ✅ v0.10.6(Poppins®600/40px/48 行高/#000 居中、widget 底 padding 24px、section 上 padding 64px;Poppins 与原站一致走系统 fallback 不载字体文件;typographyCss/_padding/resolveGlobals 修复基建) |
| H4 | 硬件与服务系列 ×5 组 | 每组 heading+text+image+eael-info-box ×4(横向四列) | ⬜ |
| H4 | 硬件与服务系列 ×5 组 | 每组 heading+text+image+eael-info-box ×2(五列卡组) | ✅ v0.10.7(heading 橙线下划线/20px/#333、text-editor BFC 62.4、image widget link/shrink 动画、eael-info-box icon-on-left 完整结构、列卡渐变+边框+margin 折叠(实测 sec 529.6/列 513.6/inf2 y1429.4 与原站一致);WidgetNode 双层容器(elementor-widget + widget-container)对齐原站 margin 折叠语义) |
| H5 | 可定制/易生产 双主张条 | heading ×2(双栏大字) | ⬜ |
| H6 | 从概念到量产 宣言条 | 全大写 heading(ACCELERATE YOUR IOT DEVICE LAUNCH…) | ⬜ |
| H7 | OS/芯片平台/语言/协议 四列 | heading ×4 图标列 | ⬜ |
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "dusuniot-rebuild",
"version": "0.10.6",
"version": "0.10.7",
"private": true,
"type": "module",
"scripts": {
+142 -34
View File
@@ -72,6 +72,26 @@ function collapseBoxValues(v: string): string {
return v;
}
/** # 函数 Elementor 背景位置词转 CSS 百分比(bottom center → 50% 100%) */
function mapBgPosition(v: string): string {
const parts = v.trim().split(/\s+/).filter(Boolean);
const to = (w: string): string | null => {
switch (w) {
case "top": return "0%";
case "center": return "50%";
case "bottom": return "100%";
case "left": return "0%";
case "right": return "100%";
default: return null;
}
};
// 顺序统一为 [x, y]
const [xs, ys] = parts.length === 2 && ["top", "bottom"].includes(parts[0]!) ? [parts[1]!, parts[0]!] : [parts[0]!, parts[1]!];
const x = to(xs) ?? xs;
const y = to(ys) ?? ys;
return `${x} ${y}`;
}
/** # 函数 解析 Typography 组合样式(typography_typography=custom 时的 typography_* 字段)
* 覆盖 font-family/size/weight/line-height/letter-spacing/word-spacing/text-transform/
* font-style/text-decoration(凡 settings 中有定义的值写入内联样式),
@@ -318,51 +338,98 @@ function SectionNode({ el }: { el: ElElement }) {
);
}
/** # 组件 column:按 _inline_size 百分比设置列宽 */
/** # 组件 column:双层对应原站 .elementor-column(外层)+ .elementor-element-populated(内层)
* 外层参与 flex 布局(按 _inline_size 百分比否则均分),不承载盒样式;
* 内层承载 populated 的盒样式:_margin(margin,移动卡片间距且不影响 flex 宽度,与原站一致)、
* padding、背景、边框、圆角。
*/
function ColumnNode({ el }: { el: ElElement }) {
const settings = resolveGlobals(el.settings || {});
// 未声明列宽时按 Elementor 语义均分(flex-basis 0),避免固定 240px 底宽把多列挤成换行
const style: CSSProperties = { flex: "1 1 0", minWidth: 0 };
const outer: CSSProperties = { minWidth: 0, flex: "1 1 0" };
const inline = settings._inline_size;
if (typeof inline === "number" && inline > 0) {
style.flex = `0 0 ${inline}%`;
style.maxWidth = `${inline}%`;
outer.flex = `0 0 ${inline}%`;
outer.maxWidth = `${inline}%`;
}
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;
}
const pad = paddingCss(settings.padding);
if (pad) style.padding = pad;
if (pad) inner.padding = pad;
const bg = sectionBgStyle(settings);
Object.assign(style, { backgroundColor: bg.backgroundColor, backgroundImage: bg.backgroundImage });
// 列圆角/边框(卡片样式)
Object.assign(inner, { backgroundColor: bg.backgroundColor, backgroundImage: bg.backgroundImage });
const radius = paddingCss(settings.border_radius);
if (radius) style.borderRadius = radius;
if (radius) inner.borderRadius = radius;
if (s(settings.border_border) === "solid") {
inner.border = `1px solid ${s(settings.border_color) || "#e3e9f0"}`;
const bw = paddingCss(settings.border_width);
style.border = `1px solid ${s(settings.border_color) || "#e3e9f0"}`;
if (bw && bw.split(" ").length === 4) {
const [t, r2, b, l] = bw.split(" ");
style.borderWidth = `${t} ${r2} ${b} ${l}`;
} else style.borderWidth = "1px";
inner.borderWidth = `${t} ${r2} ${b} ${l}`;
} else inner.borderWidth = "1px";
}
return (
<div className="min-w-0 px-2.5" style={style} data-el={el.id}>
<div className="el-col min-w-0" style={outer} data-el={el.id}>
<div className="px-2.5" style={inner}>
{(el.elements || []).map((c, i) => (
<ElementNode key={c.id || i} el={c} />
))}
</div>
</div>
);
}
/** # 组件 widget 分发:按 widgetType 渲染,未知类型优雅降级
* 外层 div 与原站 widget 包裹层一致(.elementor-widget …),挂 data-el=<id>,
* 页面级规则(post-18.css 对应块按 [data-el] 范围书写)与 _css_classes 因此都能命中。
* 双层对应原站结构:外层 div = .elementor-widget(挂 data-el=<id>,页面级规则与
* _css_classes 命中于此);内层 div = .elementor-widget-container(承载 _padding /
* _margin / _border 系列 / _border_radius / _background_* 等高级盒模型)。
* 双层的目的:外层仅带 kit-spacing 的 20px 下外距,内层负外距(如 H4 图片 -2.5rem 0 -1rem)
* 能与外层外距按标准块级折叠(max 规则),与原站实测间距(折叠后 20px)一致。
*/
function WidgetNode({ el }: { el: ElElement }) {
const settings = (el.settings || {}) as Record<string, unknown>;
const style: CSSProperties = {};
const pad = paddingCss(settings._padding);
if (pad) style.padding = pad;
const mrg = paddingCss(settings._margin);
if (mrg) 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);
if (bw && bw.split(" ").length === 4) {
const [t, r2, b, l] = bw.split(" ");
style.borderWidth = `${t} ${r2} ${b} ${l}`;
} else style.borderWidth = "1px";
}
const rad = paddingCss(settings._border_radius);
if (rad) style.borderRadius = rad;
// widget 容器背景(_background_*):仅图片(classic),位置/平铺逐字采用
const wbg = settings._background_image as { url?: string } | undefined;
if (wbg?.url) {
style.backgroundImage = `url("${wbg.url}")`;
// Elementor 位置词转 CSS:bottom center → 50% 100%(避免浏览器归一差异)
style.backgroundPosition = mapBgPosition(s(settings._background_position));
if (s(settings._background_repeat)) style.backgroundRepeat = s(settings._background_repeat);
}
const inner = (
<WidgetGuard widgetType={s(el.widgetType)}>
<WidgetNodeInner el={el} />
</WidgetGuard>
);
const cls = s(el.settings?._css_classes) || undefined;
const cls = s(settings._css_classes) || undefined;
const hasContainerStyle = Object.keys(style).length > 0;
if (hasContainerStyle) {
return (
<div data-el={el.id}>
<div className={cls} style={style}>{inner}</div>
</div>
);
}
return <div className={cls} data-el={el.id}>{inner}</div>;
}
@@ -464,7 +531,7 @@ function WidgetNodeInner({ el }: { el: ElElement }) {
}
}
/** # 组件 heading widget:标题级别/颜色/对齐 */
/** # 组件 heading widget:标题(可带 link,is_external → target=_blank;title 支持 HTML) */
function HeadingWidget({ settings }: { settings: Record<string, unknown> }) {
// 标签合法域同原站 Utils::validate_html_tag(h1..h6 / div / span / p);默认 h2
const size = s(settings.header_size) || "h2";
@@ -474,13 +541,24 @@ function HeadingWidget({ settings }: { settings: Record<string, unknown> }) {
const align = s(settings.align);
if (align) style.textAlign = align as CSSProperties["textAlign"];
Object.assign(style, typographyCss(settings));
// widget 自带内边距(Elementor 高级项 _padding)
const pad = paddingCss(settings._padding);
if (pad) style.padding = pad;
// 通用 margin 不设(实测原站 heading widget margin 0,间距全部来自原站 settings)
return <Tag style={style} dangerouslySetInnerHTML={{ __html: s(settings.title) }} />;
// 基础规则(frontend.min.css):.elementor-heading-title{line-height:1;margin:0;padding:0}
// settings 里未定义行高时用 1(H3 有显式 1.2 覆写,H4 无设置 → 实测原站 20px/20px)
if (!style.lineHeight) style.lineHeight = 1;
// _padding/_margin/_background_* 属 widget 包裹层(WidgetNode),不在标题标签内重复
const title = s(settings.title);
const link = (settings.link as { url?: string } | undefined)?.url;
const isExternal = (settings.link as { is_external?: string } | undefined)?.is_external === "on";
const linkProps = link ? { href: link, ...(isExternal ? { target: "_blank" as const } : {}) } : undefined;
return (
<Tag className="el-heading-title" style={style}>
{linkProps ? (
<a {...linkProps} dangerouslySetInnerHTML={{ __html: title }} />
) : (
<span dangerouslySetInnerHTML={{ __html: title }} />
)}
</Tag>
);
}
/** # 组件 text-editor widget:富文本 HTML */
function TextEditorWidget({ settings }: { settings: Record<string, unknown> }) {
const html = s(settings.editor);
@@ -490,19 +568,26 @@ function TextEditorWidget({ settings }: { settings: Record<string, unknown> }) {
const style: CSSProperties = {};
if (color) style.color = color;
Object.assign(style, typographyCss(settings));
const pad = paddingCss(settings._padding);
if (pad) style.padding = pad;
return <div className="mb-[18px]" style={Object.keys(style).length ? style : undefined} dangerouslySetInnerHTML={{ __html: html }} />;
// _padding/_margin/_background_* 属 widget 包裹层(WidgetNode),不在此重复
return <div className="el-texteditor" style={Object.keys(style).length ? style : undefined} dangerouslySetInnerHTML={{ __html: html }} />;
}
/** # 组件 image widget */
/** # 组件 image widget:图(可带 link;hover_animation=shrink 复刻原站缩小动画) */
function ImageWidget({ settings }: { settings: Record<string, unknown> }) {
const url = (settings.image as { url?: string } | undefined)?.url;
const img0 = settings.image as { url?: string; alt?: string } | undefined;
const url = img0?.url;
if (!url) return null;
const align = s(settings.align);
const anim = s(settings.hover_animation); // 原站映射 elementor-animation-* 类
const animCls = anim === "shrink" ? "el-anim-shrink" : "";
const link = (settings.link as { url?: string } | undefined)?.url;
const isExternal = (settings.link as { is_external?: string } | undefined)?.is_external === "on";
const linkProps = link ? { href: link, ...(isExternal ? { target: "_blank" as const } : {}) } : undefined;
const img = <img className={animCls || undefined} src={url} alt={s(img0.alt)} loading="lazy" />;
const cls = ("el-image " + (animCls || "")).trim();
return (
<div className="mb-[18px]" style={{ textAlign: (align as CSSProperties["textAlign"]) || "center" }}>
<img src={url} alt={s((settings.image as { alt?: string }).alt) || ""} loading="lazy" />
<div className={cls} style={{ textAlign: (align as CSSProperties["textAlign"]) || "center" }}>
{linkProps ? <a {...linkProps} tabIndex={-1}>{img}</a> : img}
</div>
);
}
@@ -765,16 +850,39 @@ function IconBoxWidget({ settings }: { settings: Record<string, unknown> }) {
/** # 组件 eael-info-box(EAEL Infobox 皮肤):标题(.title,custom_css 提供左侧图标背景) + 富文本描述(.infobox-content)
* 与原生 icon-box 的键不同:内容在 eael_infobox_title / eael_infobox_text。
*/
/** # 组件 eael-info-box(EAEL Infobox,忠实复刻 info-box.min.css + 原站渲染 DOM):
* eael-infobox(icon-on-left / 默认上下) > .infobox-icon(img,eael_infobox_img_margin)
* + .infobox-content > .infobox-title-section > .title(标题标签 eael_infobox_title_tag)
* [+ .sub_title][+ p 描述];图在左时为 flex 行(content padding-left 15px)。
* 标题/描述样式(颜色/字号/外距)由页面级规则与 eael_infobox_title_margin 提供。
*/
function EaelInfoBoxWidget({ settings }: { settings: Record<string, unknown> }) {
const title = s(settings.eael_infobox_title) || s(settings.title_text) || s(settings.title);
const desc = s(settings.eael_infobox_text) || s(settings.description_text) || s(settings.description);
if (!title && !desc) return null;
const img = settings.eael_infobox_image as { url?: string; alt?: string } | undefined;
if (!title && !desc && !img?.url) return null;
const tag = s(settings.eael_infobox_title_tag);
const Tag = (["h1", "h2", "h3", "h4", "h5", "h6", "div"].includes(tag) ? tag : "div") as "div";
const left = s(settings.eael_infobox_img_type) === "img-on-left" || s(settings.eael_infobox_img_type) === "icon-on-left";
const cls = "eael-infobox" + (left ? " icon-on-left" : "");
return (
<div className="eael-infobox mb-[18px]">
{title ? <Tag className="title" dangerouslySetInnerHTML={{ __html: title }} /> : null}
{desc ? <div className="infobox-content" dangerouslySetInnerHTML={{ __html: desc }} /> : null}
<div className={cls}>
{img?.url ? (
<div className="infobox-icon">
<img src={img.url} alt={s(img.alt)} loading="lazy" />
</div>
) : null}
<div className="infobox-content">
{title ? (
<div className="infobox-title-section">
<Tag className="title" dangerouslySetInnerHTML={{ __html: title }} />
{s(settings.eael_infobox_sub_title) && s(settings.eael_infobox_sub_title) !== "This is a sub title" ? (
<div className="sub_title" dangerouslySetInnerHTML={{ __html: s(settings.eael_infobox_sub_title) }} />
) : null}
</div>
) : null}
{desc ? <p dangerouslySetInnerHTML={{ __html: desc }} /> : null}
</div>
</div>
);
}
+19 -1
View File
@@ -92,6 +92,24 @@ const SETTING_KEYS = new Set([
"eael_infobox_title",
"eael_infobox_text",
"eael_infobox_title_tag",
"eael_infobox_image",
"eael_infobox_img_type",
"eael_infobox_img_margin",
"eael_infobox_title_margin",
"eael_infobox_title_margin_mobile",
"eael_infobox_title_typography_typography",
"eael_infobox_title_typography_font_size",
"hover_animation",
// widget 高级盒模型(如 eael-info-box 边框卡:_border_* + _padding/_margin)
"_margin",
// widget 容器背景(如 H4 标题下的橙线图)
"_background_image",
"_background_position",
"_background_repeat",
"_border_border",
"_border_color",
"_border_width",
"_border_radius",
"title_text_a",
"description_text_a",
"button_text",
@@ -111,7 +129,7 @@ const SETTING_KEYS = new Set([
function slimSettingValue(key: string, val: unknown): unknown {
if (!val || typeof val !== "object" || Array.isArray(val)) return val;
const v = val as Record<string, unknown>;
if (key === "image" || key === "background_image" || key === "background_a_image") {
if (key === "image" || key === "background_image" || key === "background_a_image" || key === "eael_infobox_image") {
// 渲染器只读 url / alt
return { url: v.url, alt: v.alt };
}
+92 -1
View File
@@ -261,7 +261,7 @@
background-repeat: no-repeat;
background-position: center right;
}
[data-el="e38a898"] > div > [data-el] { padding: 10px; }
[data-el="e38a898"] > div > [data-el] > div { padding: 10px; }
[data-el="e38a898"] .el-imagebox-img { width: 30%; }
[data-el="e38a898"] .el-imagebox:hover .el-imagebox-img img {
filter: brightness(90%) contrast(100%) saturate(100%) blur(0px) hue-rotate(0deg);
@@ -273,6 +273,97 @@
/* post-18.css 对六个 widget 各有 margin-bottom:0,特异性高于基础 CSS 的 15px(实测原站 0px) */
[data-el="e38a898"] .el-imagebox-img { margin-bottom: 0 !important; }
}
/* ===== Elementor 通用规则(frontend.min.css 对应块)
* kit-widget-spacing:非末位 widget 的下间距默认 20px(实测原站 H4 text-editor margin 0 0 20px)
* 挂在 .el-col 直属 widget(data-el 包裹层)上。
* text-editor:display flow-root 建立 BFC(子元素 p 的下外距计入容器高度,与原站
* 实测 62.4 = 2 行 48 + p margin 14.4 一致),p 下外距 0.9em(原站 hello 环境 14.4px)。
*/
.el-col > div > [data-el]:not(:last-child) { margin-bottom: 20px; }
.el-texteditor { display: flow-root; }
.el-texteditor p { margin: 0 0 0.9em; }
/* ===== Elementor widget-image 基础(widget-image.min.css 对应块)
* a 与 img 都是 inline-block 且 img vertical-align:middle,
* 消除行内盒基线空隙(否则容器多出 ~7px 行盒 descent,与原站 231.2≠238.2 对不上)。
*/
.el-image { text-align: center; }
.el-image a { display: inline-block; }
.el-image img { display: inline-block; vertical-align: middle; }
/* ===== Elementor hover 动画(e-animation-shrink.min.css):hover 缩至 90%,0.3s 过渡 */
/* heading 标题链接颜色继承(原站不受全局品牌橙影响,实测 #333/#000) */
.el-heading-title a { color: inherit; }
.el-anim-shrink { transition-duration: 0.3s; transition-property: transform; }
.el-anim-shrink:hover, .el-anim-shrink:focus, .el-anim-shrink:active { transform: scale(0.9); }
/* ===== EAEL infobox 基础(info-box.min.css 对应块):
* 图在左为 flex 行;内容列 padding-left 15px;标题行高 1(margin 由页面级覆盖);
* 图标列垂直居中。
*/
.eael-infobox.icon-on-left { display: flex; position: relative; }
.eael-infobox.icon-on-left .infobox-content { padding-left: 15px; flex: 1 1 auto; min-width: 0; }
.eael-infobox .infobox-icon {
display: flex; justify-content: flex-start; align-self: center;
/* flex 行内拉伸:H4 实测 icon 容器 39px(img 32px + 7px 排版余量),
* 与 .eael-infobox 盒 42px 对齐 */
}
.eael-infobox .infobox-icon img { transition: all 300ms ease-in-out 0s; }
.eael-infobox .infobox-content { display: flex; flex-direction: column; justify-content: center; }
.eael-infobox .infobox-content .infobox-title-section { display: flex; flex-direction: column; }
.eael-infobox .infobox-content .title { line-height: 1; }
.eael-infobox .infobox-content p { margin: 0 0 15px; }
/* ===== 首页 H4 硬件与服务系列 ×5(post-18.css 对应块;1436px 实测 2026-11-01)
* 卡列:渐变 #F4F5F9→透明 + 1px #E0E5F9 边框 + 4px 圆角 + 列间 margin(ColumnNode 由 settings 渲染);
* 标题 widget 容器:底部橙线图背景(bottom center no-repeat)+ 左右 3rem 内边距;
* 产品图 widget 容器:margin -2.5rem 0 -1rem 上提;
* info-box ×2:图标列 margin-top 3px、内容标题 14px/#4d4d4d/margin 7px 0 0 -10px;
* 第二个 info-box 容器上提 -10px。
*/
[data-el="314b8d5"] .infobox-icon, [data-el="8417167"] .infobox-icon,
[data-el="9e1009f"] .infobox-icon, [data-el="2032426"] .infobox-icon,
[data-el="7c9ceeb"] .infobox-icon, [data-el="9ff518a"] .infobox-icon,
[data-el="2c9eb1c"] .infobox-icon, [data-el="1aa62bd"] .infobox-icon,
[data-el="64432f0"] .infobox-icon, [data-el="2ba29de"] .infobox-icon {
margin: 3px 0 0 0;
height: 39px;
}
[data-el="314b8d5"] .infobox-content .title, [data-el="8417167"] .infobox-content .title,
[data-el="9e1009f"] .infobox-content .title, [data-el="2032426"] .infobox-content .title,
[data-el="7c9ceeb"] .infobox-content .title, [data-el="9ff518a"] .infobox-content .title,
[data-el="2c9eb1c"] .infobox-content .title, [data-el="1aa62bd"] .infobox-content .title,
[data-el="64432f0"] .infobox-content .title, [data-el="2ba29de"] .infobox-content .title {
color: #4d4d4d;
font-size: 14px;
margin: 7px 0 0 -10px;
}
/* inner 的 -10px 仅上移容器自身(原站实测 root 不位移),flow-root 阻止它与根折叠 */
[data-el="9ff518a"], [data-el="2c9eb1c"], [data-el="1aa62bd"],
[data-el="64432f0"], [data-el="2ba29de"] {
display: flow-root;
}
[data-el="9ff518a"] > div, [data-el="2c9eb1c"] > div,
[data-el="1aa62bd"] > div, [data-el="64432f0"] > div,
[data-el="2ba29de"] > div {
margin: -10px 0 0 0;
}
@media (max-width: 1024px) {
[data-el="68e06cb"], [data-el="c3c8429"], [data-el="8272123"],
[data-el="1d97802"], [data-el="4a8ad62"] {
margin: -2rem 0 -1rem;
}
}
@media (max-width: 767px) {
[data-el="9ff518a"] .infobox-content .title, [data-el="2c9eb1c"] .infobox-content .title,
[data-el="1aa62bd"] .infobox-content .title, [data-el="64432f0"] .infobox-content .title,
[data-el="2ba29de"] .infobox-content .title {
margin: 7px 0 0 -6px;
}
}
}
@media (max-width: 900px) {