From 544cbdc5f62f5d5fff75469d5f33a8c23cd7f93a Mon Sep 17 00:00:00 2001 From: BigCode Date: Sun, 1 Nov 2026 02:42:56 +0800 Subject: [PATCH] =?UTF-8?q?v0.10.7=20=E9=A6=96=E9=A1=B5=20H4=20=E4=BA=94?= =?UTF-8?q?=E5=8D=A1=E7=BB=84=201:1:eael-info-box=20=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E7=BB=93=E6=9E=84/WidgetNode=20=E5=8F=8C=E5=B1=82=E5=AE=B9?= =?UTF-8?q?=E5=99=A8/=E5=88=97=20margin=20=E6=8A=98=E5=8F=A0/heading=20lin?= =?UTF-8?q?k/kit-spacing=2020/image=20inline-block,sec.h=20=E4=B8=8E?= =?UTF-8?q?=E5=8E=9F=E7=AB=99=E4=B8=80=E8=87=B4(529.6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/sections-inventory.md | 2 +- package.json | 2 +- src/components/elementor/Renderer.tsx | 182 ++++++++++++++++++++------ src/components/elementor/slim.ts | 20 ++- src/styles/app.css | 93 ++++++++++++- 5 files changed, 258 insertions(+), 41 deletions(-) diff --git a/docs/sections-inventory.md b/docs/sections-inventory.md index 516eb3545..040986310 100644 --- a/docs/sections-inventory.md +++ b/docs/sections-inventory.md @@ -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 图标列 | ⬜ | diff --git a/package.json b/package.json index e7fdb0436..340942923 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dusuniot-rebuild", - "version": "0.10.6", + "version": "0.10.7", "private": true, "type": "module", "scripts": { diff --git a/src/components/elementor/Renderer.tsx b/src/components/elementor/Renderer.tsx index 96ad98ff1..ac487e93d 100644 --- a/src/components/elementor/Renderer.tsx +++ b/src/components/elementor/Renderer.tsx @@ -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 ( -
- {(el.elements || []).map((c, i) => ( - - ))} +
+
+ {(el.elements || []).map((c, i) => ( + + ))} +
); } /** # 组件 widget 分发:按 widgetType 渲染,未知类型优雅降级 - * 外层 div 与原站 widget 包裹层一致(.elementor-widget …),挂 data-el=, - * 页面级规则(post-18.css 对应块按 [data-el] 范围书写)与 _css_classes 因此都能命中。 + * 双层对应原站结构:外层 div = .elementor-widget(挂 data-el=,页面级规则与 + * _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; + 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 = ( ); - 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 ( +
+
{inner}
+
+ ); + } return
{inner}
; } @@ -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 }) { // 标签合法域同原站 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 }) { 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 ; + // 基础规则(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 ( + + {linkProps ? ( + + ) : ( + + )} + + ); } - /** # 组件 text-editor widget:富文本 HTML */ function TextEditorWidget({ settings }: { settings: Record }) { const html = s(settings.editor); @@ -490,19 +568,26 @@ function TextEditorWidget({ settings }: { settings: Record }) { const style: CSSProperties = {}; if (color) style.color = color; Object.assign(style, typographyCss(settings)); - const pad = paddingCss(settings._padding); - if (pad) style.padding = pad; - return
; + // _padding/_margin/_background_* 属 widget 包裹层(WidgetNode),不在此重复 + return
; } -/** # 组件 image widget */ +/** # 组件 image widget:图(可带 link;hover_animation=shrink 复刻原站缩小动画) */ function ImageWidget({ settings }: { settings: Record }) { - 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 = {s(img0.alt)}; + const cls = ("el-image " + (animCls || "")).trim(); return ( -
- {s((settings.image + ); } @@ -765,16 +850,39 @@ function IconBoxWidget({ settings }: { settings: Record }) { /** # 组件 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 }) { 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 ( -
- {title ? : null} - {desc ?
: null} +
+ {img?.url ? ( +
+ {s(img.alt)} +
+ ) : null} +
+ {title ? ( +
+ + {s(settings.eael_infobox_sub_title) && s(settings.eael_infobox_sub_title) !== "This is a sub title" ? ( +
+ ) : null} +
+ ) : null} + {desc ?

: null} +

); } diff --git a/src/components/elementor/slim.ts b/src/components/elementor/slim.ts index 6748b1422..96da4c5e4 100644 --- a/src/components/elementor/slim.ts +++ b/src/components/elementor/slim.ts @@ -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; - 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 }; } diff --git a/src/styles/app.css b/src/styles/app.css index 772159b25..2e424e2a3 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -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) {