v0.10.9 首页 H8 标题条 + H9 tabs 切换器 1:1:TabsWidget 忠实 DOM/React 切换;widget-tabs 基础块+生成规则+custom_css 三层;section margin/gap 渲染支持;全局 body 行高改无单位 1.5、kit 链接字体 Roboto;H1~H7 全量回归无差异

This commit is contained in:
bigcode
2026-11-01 10:34:54 +08:00
parent 2cbfc82a08
commit f66f4775d6
5 changed files with 148 additions and 19 deletions
+2 -2
View File
@@ -41,8 +41,8 @@
| H5 | 可定制/易生产 双主张条 | heading ×2(双栏大字) | ✅ v0.10.7(long-arrow 背景条、列 380、文字 20px/500/#fff 单行、arrow 194×39) |
| H6 | 从概念到量产 宣言条 | 全大写 heading(ACCELERATE YOUR IOT DEVICE LAUNCH…) | ✅ v0.10.8(Poppins 24px/600/行高 1 由 DB 内联、≤767 20px、列 59bc2e1 四向 10px、sec 根 padding 10px,五层盒模型 y 全对齐 0.8px 页级漂移内) |
| H7 | OS/芯片平台/语言/协议 四列 | heading ×4 图标列 | ✅ v0.10.8(外卡 66de90a 渐变+边框+圆角 8/16/16/0、四列 335.5 虚线分隔、_element_width:auto 图标横排 4+3 换行、el-w-auto 双层类修复 width:100% 强推回归) |
| H8 | 热门嵌入式硬件 标题 | heading | ⬜ |
| H9 | 热门硬件切换器(html) | html widget(原站 JS 切换逻辑,需看原站行为) | ⬜ |
| H8 | 热门嵌入式硬件 标题 | heading(Popular Embedded Hardware Products…) | ✅ v0.10.9(heading 40px/600/#000/行高 1 居中 x38/w1360;section 7df0244 gap=no→列 0px、margin-top 4%=57.4375px、bg #F4F5F8、padding 64px 0——section 级 margin/gap 渲染支持随此版块落地,slim 白名单补 gap) |
| H9 | 热门硬件切换器(tabs) | Elementor 原生 tabs widget ×6 标签(内容为自包含 HTML 卡片网格) | ✅ v0.10.9(TabsWidget 忠实 DOM+React 点击切换/aria;widget-tabs.min.css 基础块+fd97340 生成规则(border_width=0 含伪元素)+custom_css(.ele-soms-chip-dif 边框/内容覆盖)三层取证;全局修正 body 行高为无单位 1.5、补 kit 链接字体 Roboto(卡片 <a> 经 .elementor-kit-15 a 规则用 Roboto,否则 Arial 截断致规格多折一行);实测仅 0.8px 页级漂移) |
| H10 | 技术产品 Tab 行 ×3 | heading+image+image-box ×4+button(三段 Tab 面板) | ⬜ |
| H11 | Industries & Use Cases | heading + eael-adv-tabs + button(多标签竖排) | ⬜ |
| H12 | The Difference Maker 段一 | heading+text+icon-list+text+button(左侧列表优势) | ⬜ |
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "dusuniot-rebuild",
"version": "0.10.8",
"version": "0.10.9",
"private": true,
"type": "module",
"scripts": {
+65 -13
View File
@@ -5,7 +5,7 @@
* 样式以内联 style 复现视觉效果(背景、内边距、对齐、列宽百分比),
* 未识别的 widgetType 优雅降级:渲染 heading_title / editor 文本或忽略。
*/
import { Component, useEffect, useState, type CSSProperties, type ReactNode } from "react";
import { Component, Fragment, useEffect, useState, type CSSProperties, type ReactNode } from "react";
import { getLatestPosts, getLatestProducts, type LatestItem } from "~/server/data";
import TickerBar from "~/components/website/TickerBar";
import { InquiryForm } from "~/components/website/InquiryForm";
@@ -320,6 +320,9 @@ function SectionNode({ el }: { el: ElElement }) {
const width = sizeCss(settings.content_width) || sizeCss((settings as Record<string, unknown>).width) || "1140px";
const secPad = paddingCss(settings.padding);
if (secPad) style.padding = secPad;
// section 外边距(原站 computed 在根元素,如 H8 7df0244 margin-top 4% = 57.4px @1436)
const secMargin = paddingCss(settings.margin);
if (secMargin) style.margin = secMargin;
const inner: CSSProperties = {};
if (s(settings.layout) === "full_width") {
// 通栏 section:内容不设上限(如首页 hero 轮播)
@@ -1091,24 +1094,73 @@ function AccordionWidget({ settings }: { settings: Record<string, unknown> }) {
}
/** # 组件 tabs widget:标签页 */
/** # 组件 Elementor 原生 tabs widget(H9 热门嵌入式硬件切换器)
* DOM 对齐原站(1436 实测 2026-11-01):.elementor-tabs > .elementor-tabs-wrapper(desktop 标题行,
* role=tablist,flex 居中) + .elementor-tabs-content-wrapper(mobile 标题 ×N + 内容面板 ×N)。
* active 用 elementor-active 类;点击切换(React state,原站为 Elementor JS 行为);
* 内容 tab_content 为自包含 HTML(含 <style> 卡片网格样式),dangerouslySetInnerHTML 原样输出。
* 基础样式移植 widget-tabs.min.css(app.css「Elementor tabs」块),本节点 custom_css
* (.ele-soms-chip-dif 边框/内容覆盖)经 collectCustomCss 自动注入——因此类名必须用
* elementor 原名,不得改 el- 前缀。id 基数 2651:原站渲染计数 elementor-tab-title-2651…,
* 本地按序复刻(仅影响锚点,无视觉作用)。
*/
function TabsWidget({ settings }: { settings: Record<string, unknown> }) {
const tabs = Array.isArray(settings.tabs) ? (settings.tabs as { tab_title?: string; tab_content?: string }[]) : [];
const tabs = Array.isArray(settings.tabs)
? (settings.tabs as { _id?: string; tab_title?: string; tab_content?: string }[])
: [];
const [active, setActive] = useState(0);
if (!tabs.length) return null;
const align = s(settings.tabs_align_horizontal) || "center";
const alignCls =
align === "center" ? " elementor-tabs-alignment-center"
: align === "end" ? " elementor-tabs-alignment-end"
: align === "stretch" ? " elementor-tabs-alignment-stretch"
: "";
const base = 2651;
return (
<Tabs defaultValue="tab-0" className="mb-[18px]">
<TabsList>
<div className={`elementor-widget-tabs elementor-tabs elementor-tabs-view-horizontal${alignCls}`}>
<div className="elementor-tabs-wrapper" role="tablist">
{tabs.map((t, i) => (
<TabsTrigger key={i} value={`tab-${i}`}>
<div
key={t._id || i}
id={`elementor-tab-title-${base + i}`}
className={"elementor-tab-title elementor-tab-desktop-title" + (i === active ? " elementor-active" : "")}
role="tab"
data-tab={i + 1}
tabIndex={i === active ? 0 : -1}
aria-selected={i === active}
onClick={() => setActive(i)}
>
{s(t.tab_title)}
</TabsTrigger>
</div>
))}
</TabsList>
{tabs.map((t, i) => (
<TabsContent key={i} value={`tab-${i}`}>
<div className="content-html" dangerouslySetInnerHTML={{ __html: s(t.tab_content) }} />
</TabsContent>
))}
</Tabs>
</div>
<div className="elementor-tabs-content-wrapper" role="tablist">
{tabs.map((t, i) => (
<Fragment key={t._id || i}>
<div
className={"elementor-tab-title elementor-tab-mobile-title" + (i === active ? " elementor-active" : "")}
role="tab"
data-tab={i + 1}
tabIndex={i === active ? 0 : -1}
aria-selected={i === active}
onClick={() => setActive(i)}
>
{s(t.tab_title)}
</div>
<div
id={`elementor-tab-content-${base + i}`}
className="elementor-tab-content elementor-clearfix"
role="tabpanel"
data-tab={i + 1}
tabIndex={0}
style={{ display: i === active ? "block" : "none" }}
dangerouslySetInnerHTML={{ __html: s(t.tab_content) }}
/>
</Fragment>
))}
</div>
</div>
);
}
+4
View File
@@ -56,6 +56,8 @@ const SETTING_KEYS = new Set([
// 列结构
"_column_size",
"_inline_size",
// section 间隙档位(决定子列默认 populated padding:no 0 / default 10 / …)
"gap",
// 各 widget 数据
"heading_title",
"title",
@@ -79,6 +81,8 @@ const SETTING_KEYS = new Set([
"slides",
"slides_height",
"tabs",
"tabs_align_horizontal",
"tab_active_color",
"video_url",
"youtube_url",
"map",
+76 -3
View File
@@ -64,10 +64,11 @@
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
/* 基准取原站实测(2026-10-31 headless):16px / #333 / 行高 24px(hello-elementor 默认) */
/* 基准取原站实测(2026-10-31 headless):16px / #333;行高为无单位 1.5
* (原站 action 按钮 fs14 → 21px 随字号缩放,绝对 24px 会固定不变) */
font-family: var(--font-sans);
font-size: 16px;
line-height: 24px;
line-height: 1.5;
color: #333333;
background: #fff;
}
@@ -296,6 +297,12 @@
/* ===== Elementor hover 动画(e-animation-shrink.min.css):hover 缩至 90%,0.3s 过渡 */
/* heading 标题链接颜色继承(原站不受全局品牌橙影响,实测 #333/#000) */
.el-heading-title a { color: inherit; }
/* kit 全局链接排版(post-15.css .elementor-kit-15 a):链接默认 Roboto 400。
* 取证:tab 卡片 .card 为 <a>,kit 字体使其内部 ul/li 用 Roboto 渲染(文字 273.6px),
* 否则被卡片内嵌样式的 Arial 截住(261.9px),第 4 条规格多折一行(H9 body 高 +21.6)。
* 颜色不在此外加:各 widget 链接色已由自身规则(.el-heading-title a 等)与原站一致。
*/
.content-html a { font-family: "Roboto", sans-serif; font-weight: 400; }
.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); }
@@ -404,8 +411,74 @@
border-right: 0;
border-bottom: 1px dashed #E0E5F9;
}
[data-el="338fb86"] { padding-inline: 1rem; }
/* 338fb86 移动端 padding 由 padding_mobile(collectResponsiveCss)在根上生效 */
}
/* ===== Elementor 原生 tabs widget(widget-tabs.min.css 对应块;H9 热门硬件切换器)
* 原站为水平视图 + alignment-center;竖排/其余档位规则随源码保留(无实例不生效)。
* 源文件的 border:1px #d5d8dc 简写缺 style,按规范整条无效——与原站一致不产生边框,未移植。
* active 下边框 4px #ed6c00 与内容 wrapper 顶部 1px #C7CBDA 由节点 custom_css
* (.ele-soms-chip-dif …,collectCustomCss 注入)提供。
*/
.elementor-widget-tabs .elementor-tabs { text-align: start; }
/* 源文件的 border:1px #d5d8dc 简写(实测原站伪元素 color=#d5d8dc 生效)——按可解析移植,
* 仅取颜色;宽度随后被 fd97340 的生成规则(border_width=0, 含伪元素)归零 */
.elementor-widget-tabs .elementor-tab-content,
.elementor-widget-tabs .elementor-tab-title,
.elementor-widget-tabs .elementor-tab-title:after,
.elementor-widget-tabs .elementor-tab-title:before,
.elementor-widget-tabs .elementor-tabs-content-wrapper { border-color: #d5d8dc; }
.elementor-widget-tabs .elementor-tabs-wrapper { overflow: hidden; }
.elementor-widget-tabs .elementor-tab-title { cursor: pointer; outline: var(--focus-outline, none); }
.elementor-widget-tabs .elementor-tab-desktop-title {
border: solid transparent;
font-weight: 700;
line-height: 1;
padding: 20px 25px;
position: relative;
}
.elementor-widget-tabs .elementor-tab-desktop-title.elementor-active { border-color: #d5d8dc; }
.elementor-widget-tabs .elementor-tab-desktop-title.elementor-active:after,
.elementor-widget-tabs .elementor-tab-desktop-title.elementor-active:before {
content: ""; display: block; position: absolute;
}
.elementor-widget-tabs .elementor-tab-desktop-title:focus-visible { border: 1px solid #000; }
.elementor-widget-tabs .elementor-tab-mobile-title { cursor: pointer; padding: 10px; }
.elementor-widget-tabs .elementor-tab-content { display: none; padding: 20px; }
.elementor-widget-tabs.elementor-tabs-view-horizontal .elementor-tab-desktop-title { display: table-cell; }
.elementor-widget-tabs.elementor-tabs-view-horizontal .elementor-tab-desktop-title.elementor-active { border-block-end-style: none; }
.elementor-widget-tabs.elementor-tabs-view-horizontal .elementor-tab-desktop-title.elementor-active:after,
.elementor-widget-tabs.elementor-tabs-view-horizontal .elementor-tab-desktop-title.elementor-active:before {
border-block-end-style: solid; height: 0; inset-block-end: 0; width: 999em;
}
.elementor-widget-tabs.elementor-tabs-view-horizontal .elementor-tab-desktop-title.elementor-active:before { inset-inline-end: 100%; }
.elementor-widget-tabs.elementor-tabs-view-horizontal .elementor-tab-desktop-title.elementor-active:after { inset-inline-start: 100%; }
@media (min-width: 768px) {
.elementor-widget-tabs.elementor-tabs-alignment-center .elementor-tabs-wrapper,
.elementor-widget-tabs.elementor-tabs-alignment-end .elementor-tabs-wrapper,
.elementor-widget-tabs.elementor-tabs-alignment-stretch .elementor-tabs-wrapper { display: flex; }
.elementor-widget-tabs.elementor-tabs-alignment-center .elementor-tabs-wrapper { justify-content: center; }
.elementor-widget-tabs.elementor-tabs-alignment-end .elementor-tabs-wrapper { justify-content: flex-end; }
.elementor-widget-tabs.elementor-tabs-alignment-stretch.elementor-tabs-view-horizontal .elementor-tab-title { width: 100%; }
.elementor-tabs .elementor-tab-mobile-title { display: none; }
.elementor-widget-tabs.elementor-tabs-view-horizontal .elementor-tab-content { border-style: solid; border-block-start-style: none; }
}
@media (max-width: 767px) {
.elementor-tabs .elementor-tab-content, .elementor-tabs .elementor-tab-title { border-style: solid; border-block-end-style: none; }
.elementor-tabs .elementor-tabs-wrapper { display: none; }
.elementor-tabs .elementor-tabs-content-wrapper { border-block-end-style: solid; }
.elementor-tabs .elementor-tab-content { padding: 10px; }
}
.elementor-clearfix:after { clear: both; content: ""; display: block; height: 0; width: 0; }
/* fd97340 生成规则:border_width=0 → 标题/伪元素/内容面板 border-width 0(原站实测 content 无边框、
* active 伪元素 0px solid #d5d8dc,而移植的 view-horizontal 规则给了 border-style:solid,
* 宽度须归零,否则 medium 3px 挤窄内容 6px / 画出通栏橙线);
* active 下边框 4px 与颜色由 custom_css !important / 生成规则覆盖 */
[data-el="fd97340"] .elementor-tab-title,
[data-el="fd97340"] .elementor-tab-title:before,
[data-el="fd97340"] .elementor-tab-title:after,
[data-el="fd97340"] .elementor-tab-content { border-width: 0; }
[data-el="fd97340"] .elementor-tab-title.elementor-active { color: #ED6C00; }
}
@media (max-width: 900px) {