v0.10.19: G2 页脚 Footer 一比一复刻(原站 elementor_library post 58)
- 导入脚本 scripts/import-footer-template.ts:从 legacy dump 提取 post 58 的 _elementor_data 写入 contents(type=template, slug=footer),并按线上 post-58.css/DOM 补齐快照后改动 (f95e22e Google Preferred Source、405d251 width:auto、2481635 桌面外距) - Layout/getLayoutData 下发 footerDoc,Footer 改用 ElementorRenderer 直出原模板, 短代码按 widget id 注入:menu-2 移动端折叠菜单、gtranslate 语言下拉(原生 select.gt_selector) - Renderer 增强:section html_tag/边框/content_position、column 响应式宽度与 populated padding/margin(tablet/mobile)、widget 响应式盒模型、icon widget(fixed 返回顶部)、 icon-list 图标/间距/文字排版与 CSS 变量悬停、image 宽度、divider Elementor DOM、 text-editor 对齐与 current-date-time 动态年份、shortcode 预渲染上下文 - slim 白名单补齐响应式/图标/动态标签字段;app.css 新增 FA Solid/Regular 字体、 页脚 scoped 规则、elementor-hidden-* 响应式可见性 - 实测 1436:页脚总高 590.4 与原站全等(列宽/内层列/logo/社交/下拉逐项一致); 390:56/15/347/337/97/46.4/61 逐项全等;语言切换写 ui_lang cookie;返回顶部回滚 0 - bun test 11 pass;版本 0.10.19
This commit is contained in:
@@ -26,7 +26,7 @@
|
|||||||
| # | 版块 | 状态 | 组件 |
|
| # | 版块 | 状态 | 组件 |
|
||||||
|---|------|------|------|
|
|---|------|------|------|
|
||||||
| G1 | 顶部导航 Header(含 mega menu、语言切换) | ✅ v0.10.3 | `Header.tsx` |
|
| G1 | 顶部导航 Header(含 mega menu、语言切换) | ✅ v0.10.3 | `Header.tsx` |
|
||||||
| G2 | 页脚 Footer | 🟡 | `Footer.tsx` |
|
| G2 | 页脚 Footer(原站 elementor_library post 58) | ✅ v0.10.19 | `Footer.tsx`(ElementorRenderer 直出 46d7afa/d834160/28c7f0b8/a428bb4 + menu-2 移动菜单 + gtranslate 下拉) |
|
||||||
| G3 | 询价弹窗/表单 | 🟡 | `InquiryForm.tsx` |
|
| G3 | 询价弹窗/表单 | 🟡 | `InquiryForm.tsx` |
|
||||||
|
|
||||||
## 三、首页版块清单(自上而下,20 版块)
|
## 三、首页版块清单(自上而下,20 版块)
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
| H16 | Developer Ecosystem | heading ×2 + 内嵌 section 两卡(heading+text+button/social-icons+image) | ✅ v0.10.17(审计 0 差异:节 ab35847 bg #F4F5F8 + heading 40/600/#000(_padding 10%)+副题 20px/_margin 顶 -6;两卡 680×528.4 白底圆角 6 边框 #E0E5F9、padding 48/48/16、margin 12(populated 扣减 scoped)、卡标题 24/700/#0D152E、文本 18px/_padding 3rem、Join Now 白底橙字 12px 56px、图 544×282 居中;随此版块:SocialIconsWidget 忠实重写(wrapper grid gap 10/a 44×44 官方底色 linkedin #0077B5/youtube #CD201F/facebook #3B5998/圆角 10%/白色字形)+FA Brands 字体子集(public/fonts/fa-brands-400.woff2 + 三码位 content 规则)+elementor-animation-shrink 通用类) |
|
| H16 | Developer Ecosystem | heading ×2 + 内嵌 section 两卡(heading+text+button/social-icons+image) | ✅ v0.10.17(审计 0 差异:节 ab35847 bg #F4F5F8 + heading 40/600/#000(_padding 10%)+副题 20px/_margin 顶 -6;两卡 680×528.4 白底圆角 6 边框 #E0E5F9、padding 48/48/16、margin 12(populated 扣减 scoped)、卡标题 24/700/#0D152E、文本 18px/_padding 3rem、Join Now 白底橙字 12px 56px、图 544×282 居中;随此版块:SocialIconsWidget 忠实重写(wrapper grid gap 10/a 44×44 官方底色 linkedin #0077B5/youtube #CD201F/facebook #3B5998/圆角 10%/白色字形)+FA Brands 字体子集(public/fonts/fa-brands-400.woff2 + 三码位 content 规则)+elementor-animation-shrink 通用类) |
|
||||||
| H17 | News & Updates 标题 | heading | ⬜ |
|
| H17 | News & Updates 标题 | heading | ⬜ |
|
||||||
| H18 | 最新博文 ×2 | 8bc6b68:左 posts 卡×2(3025217)+右 Latest blogs>>标题(1c96d68)+posts 列表×5(a22c185) | ✅ v0.10.18(双端节内几何/样式全等(sec 521):左 skin-cards grid 335×2/gap 16 10、卡 520 高(custom_css .ele-post-dif)、缩略图比率 0.66×内宽+img 铺满、NEWS 徽章(uppercase/白 70%/12px/圆角 4,绝对右上)、标题 18/400/#000+mb16、日期 F j,Y/#ADADAD/padding 10 12、摘要 18 词/#767676/底距 44(空摘要回退正文 18 词);右列卡 grid 630、卡 bg #F4F5F8/圆角 3、纯标题 44px 两行钳制+右侧箭头图标(custom_css);Latest blogs>> 标题 24px+#E0E5F9 下划线+橙线图背景;随此版块:PostsWidget 忠实重写(PostsDataContext 预取架构:loader 按 widget 收集 posts_include_term_ids→listPostsByTermLegacy)、posts cards 基础 CSS、slim 白名单 cards_* 全键、**数据修复** scripts/repair-content-terms.ts(从 legacy dump wp_term_relationships 补 1162 条分类关联,Blog 302/News 22——原导入仅 61/609 篇有关联)) |
|
| H18 | 最新博文 ×2 | 8bc6b68:左 posts 卡×2(3025217)+右 Latest blogs>>标题(1c96d68)+posts 列表×5(a22c185) | ✅ v0.10.18(双端节内几何/样式全等(sec 521):左 skin-cards grid 335×2/gap 16 10、卡 520 高(custom_css .ele-post-dif)、缩略图比率 0.66×内宽+img 铺满、NEWS 徽章(uppercase/白 70%/12px/圆角 4,绝对右上)、标题 18/400/#000+mb16、日期 F j,Y/#ADADAD/padding 10 12、摘要 18 词/#767676/底距 44(空摘要回退正文 18 词);右列卡 grid 630、卡 bg #F4F5F8/圆角 3、纯标题 44px 两行钳制+右侧箭头图标(custom_css);Latest blogs>> 标题 24px+#E0E5F9 下划线+橙线图背景;随此版块:PostsWidget 忠实重写(PostsDataContext 预取架构:loader 按 widget 收集 posts_include_term_ids→listPostsByTermLegacy)、posts cards 基础 CSS、slim 白名单 cards_* 全键、**数据修复** scripts/repair-content-terms.ts(从 legacy dump wp_term_relationships 补 1162 条分类关联,Blog 302/News 22——原导入仅 61/609 篇有关联)) |
|
||||||
| H19 | Explore more DusunIoT news > | 83ecbc5:button 居中 | ✅ v0.10.18(随 H18 交付自然对齐:节 h 123 与原站全等(dy 0.8 页级漂移),按钮设置驱动(白底橙字描边,ButtonWidget 通用实现)。**首页内容树 H0-H19 全部完成**;原站 home 页另有 4 节(46d7afa/d834160/28c7f0b8/a428bb4,合计 884.4px)属页脚 Elementor 模板,不在内容树,归 G2 Footer 待办) |
|
| H19 | Explore more DusunIoT news > | 83ecbc5:button 居中 | ✅ v0.10.18(随 H18 交付自然对齐:节 h 123 与原站全等(dy 0.8 页级漂移),按钮设置驱动(白底橙字描边,ButtonWidget 通用实现)。**首页内容树 H0-H19 全部完成**;原站 home 页另有 4 节(46d7afa/d834160/28c7f0b8/a428bb4)属页脚 Elementor 模板,已由 G2 v0.10.19 以原模板直出全覆盖) |
|
||||||
|
|
||||||
## 四、其他核心页型版块(自上而下,概览)
|
## 四、其他核心页型版块(自上而下,概览)
|
||||||
|
|
||||||
@@ -120,6 +120,16 @@ cookie-policy:19 段纯文本。
|
|||||||
> Elementor 通用布局三要素已落地 Renderer:section 根 padding、列 gap 默认 populated padding
|
> Elementor 通用布局三要素已落地 Renderer:section 根 padding、列 gap 默认 populated padding
|
||||||
> (no 0/narrow 5/default 10/extended 15/wide 20/wider 30)、widget-wrap flex wrap + 子元素 width:100%
|
> (no 0/narrow 5/default 10/extended 15/wide 20/wider 30)、widget-wrap flex wrap + 子元素 width:100%
|
||||||
> + `_element_width:auto` 例外。
|
> + `_element_width:auto` 例外。
|
||||||
|
>
|
||||||
|
> **G2 页脚取证(v0.10.19)**:原站页脚为 elementor_library post 58(两节 46d7afa/d834160 +
|
||||||
|
> 内嵌 28c7f0b8/a428bb4),导入脚本 `scripts/import-footer-template.ts` 从 legacy dump 取
|
||||||
|
> `_elementor_data` 写入 contents(type=template, slug=footer);快照之后的线上改动按线上
|
||||||
|
> post-58.css/DOM 补齐(f95e22e Google 徽章、405d251 width:auto、2481635 桌面外距)。
|
||||||
|
> 1436 实测(GTranslate 下拉已渲染、懒图已加载):页脚总高 590.4 两端一致,列宽
|
||||||
|
> 228/319.2/307.8/285、内层 615.6/171/193.8/159.6、logo 144.8×42.7、社交行 150×29、
|
||||||
|
> 下拉 202×39 全部全等;390 实测逐项全等(56/15/347/337/97/46.4/61)。
|
||||||
|
> 注意:原站 headless 下 WP Rocket 延迟加载 Elementor/jQuery,JS 组件(tabs/swiper)默认不初始化,
|
||||||
|
> 比对该类版块需先 `page.mouse.wheel` 或按线上真实状态换算,勿把"未初始化"当基线。
|
||||||
|
|
||||||
## 七、已知遗留(非当前版块范围,后续版块收口)
|
## 七、已知遗留(非当前版块范围,后续版块收口)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dusuniot-rebuild",
|
"name": "dusuniot-rebuild",
|
||||||
"version": "0.10.18",
|
"version": "0.10.19",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,124 @@
|
|||||||
|
/**
|
||||||
|
* # 脚本 导入页脚 Elementor 模板(legacy elementor_library post 58)
|
||||||
|
* 背景:WordPress 导入管线未迁移 elementor_library 类型(仅 page/post/landing),
|
||||||
|
* 页脚模板(post 58,含 46d7afa / d834160 两节及其内嵌 28c7f0b8 / a428bb4)因此缺失。
|
||||||
|
* 数据源:legacy/dusuniot-db-20261029.sql 中 `(meta_id,58,'_elementor_data','…')` 行。
|
||||||
|
* 补丁:线上页脚另有 f95e22e(Google Preferred Source 图,dump 快照之后新增),
|
||||||
|
* 依线上 DOM(/tmp/orig-footer.html 取证:class elementor-widget__width-inherit、
|
||||||
|
* img 528×64、链接 google.com/preferences/source?q=dusuniot.com)补入 2ccb47d 列末位。
|
||||||
|
* 结果写入 contents(type=template, slug=footer, meta.elementor),供 Layout 渲染。
|
||||||
|
* 运行:bun scripts/import-footer-template.ts
|
||||||
|
*/
|
||||||
|
import { SQL } from "bun";
|
||||||
|
|
||||||
|
const DUMP = "/home/bigcode/data/dev/sites/dusuniot/legacy/dusuniot-db-20261029.sql";
|
||||||
|
const db = new SQL("postgres://dusuniot:dusuniot@127.0.0.1:25432/dusuniot");
|
||||||
|
|
||||||
|
/** # 函数 从 dump 提取 post 58 的 _elementor_data 并反转义为 JSON
|
||||||
|
* 返回值 Elementor 元素数组
|
||||||
|
*/
|
||||||
|
async function extractTemplate(): Promise<Record<string, unknown>[]> {
|
||||||
|
const text = await Bun.file(DUMP).text();
|
||||||
|
const m = text.match(/\(\d+,58,'_elementor_data','((?:[^'\\]|\\.)*)'\)/);
|
||||||
|
if (!m) throw new Error("dump 中未找到 post 58 的 _elementor_data");
|
||||||
|
const raw = m[1]!;
|
||||||
|
// SQL dump 反斜杠转义:\" → "、\' → '、\\ → \
|
||||||
|
const json = raw.replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
||||||
|
const data = JSON.parse(json);
|
||||||
|
if (!Array.isArray(data)) throw new Error("_elementor_data 不是数组");
|
||||||
|
return data as Record<string, unknown>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** # 函数 深度查找指定 id 的元素节点 */
|
||||||
|
function findElement(els: Record<string, unknown>[], id: string): Record<string, unknown> | null {
|
||||||
|
for (const el of els) {
|
||||||
|
if (el.id === id) return el;
|
||||||
|
const children = el.elements as Record<string, unknown>[] | undefined;
|
||||||
|
if (children?.length) {
|
||||||
|
const hit = findElement(children, id);
|
||||||
|
if (hit) return hit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** # 函数 线上快照新增的 f95e22e(Google Preferred Source 图)
|
||||||
|
* 线上 post-58.css 取证:.elementor-element-f95e22e img{width:300px}、
|
||||||
|
* > .elementor-widget-container{margin:10px 0 0};dump 快照之后新增,故在此补全。
|
||||||
|
*/
|
||||||
|
function preferredSourceWidget(): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
id: "f95e22e",
|
||||||
|
elType: "widget",
|
||||||
|
settings: {
|
||||||
|
image: {
|
||||||
|
url: "http://www.dusuniot.com/wp-content/uploads/2022/06/google-preffered-source.png",
|
||||||
|
id: 161805,
|
||||||
|
alt: "google prefferd source",
|
||||||
|
source: "library",
|
||||||
|
size: "",
|
||||||
|
},
|
||||||
|
image_size: "full",
|
||||||
|
width: { unit: "px", size: 300, sizes: [] },
|
||||||
|
align: "left",
|
||||||
|
_element_width: "inherit",
|
||||||
|
_margin: { unit: "px", top: "10", right: "0", bottom: "0", left: "0", isLinked: false },
|
||||||
|
link: {
|
||||||
|
url: "https://www.google.com/preferences/source?q=dusuniot.com",
|
||||||
|
is_external: "on",
|
||||||
|
nofollow: "on",
|
||||||
|
custom_attributes: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
elements: [],
|
||||||
|
widgetType: "image",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const elements = await extractTemplate();
|
||||||
|
|
||||||
|
// 补丁:2ccb47d 列末尾追加 f95e22e(幂等:已存在则跳过)
|
||||||
|
const col = findElement(elements, "2ccb47d");
|
||||||
|
if (!col) throw new Error("模板缺少 2ccb47d 列");
|
||||||
|
const colChildren = (col.elements as Record<string, unknown>[]) || [];
|
||||||
|
const psIdx = colChildren.findIndex((el) => el.id === "f95e22e");
|
||||||
|
if (psIdx >= 0) colChildren[psIdx] = preferredSourceWidget();
|
||||||
|
else colChildren.push(preferredSourceWidget());
|
||||||
|
col.elements = colChildren;
|
||||||
|
|
||||||
|
// 补丁:社交图标行 405d251 宽度模式(线上 class=elementor-widget__width-auto、
|
||||||
|
// 生成 CSS width:auto;dump 快照为 inherit,实测线上为 auto → 与 gtranslate 同排)
|
||||||
|
const social = findElement(elements, "405d251");
|
||||||
|
if (social) {
|
||||||
|
const st = (social.settings as Record<string, unknown>) || {};
|
||||||
|
st._element_width = "auto";
|
||||||
|
social.settings = st;
|
||||||
|
}
|
||||||
|
// 补丁:gtranslate 短代码控件 2481635 桌面外距(线上 post-58.css:margin -10px 0 0 20px;
|
||||||
|
// dump 只有 _margin_mobile,桌面值来自快照之后的编辑)
|
||||||
|
const gt = findElement(elements, "2481635");
|
||||||
|
if (gt) {
|
||||||
|
const st = (gt.settings as Record<string, unknown>) || {};
|
||||||
|
st._margin = { unit: "px", top: "-10", right: "0", bottom: "0", left: "20", isLinked: false };
|
||||||
|
gt.settings = st;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = JSON.stringify({ elementor: elements });
|
||||||
|
|
||||||
|
// 写入 contents:type=template / slug=footer;已存在则更新
|
||||||
|
const existing = await db`SELECT id FROM contents WHERE slug='footer' AND type='template'`;
|
||||||
|
if (existing.length) {
|
||||||
|
await db`UPDATE contents SET meta=${meta}::jsonb, status='published', updated_at=now() WHERE id=${existing[0]!.id}`;
|
||||||
|
console.log("已更新 footer 模板,contents.id =", existing[0]!.id);
|
||||||
|
} else {
|
||||||
|
const inserted = await db`
|
||||||
|
INSERT INTO contents (type, slug, title, status, meta, legacy_id, published_at)
|
||||||
|
VALUES ('template', 'footer', 'Footer', 'published', ${meta}::jsonb, 58, now())
|
||||||
|
RETURNING id`;
|
||||||
|
console.log("已插入 footer 模板,contents.id =", inserted[0]!.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证:节数量与顶层 id
|
||||||
|
const top = elements.map((el) => el.id).join(", ");
|
||||||
|
console.log("顶层节:", top);
|
||||||
|
await db.end();
|
||||||
@@ -23,13 +23,13 @@ console.log("dump 关系对:", pairs.size);
|
|||||||
|
|
||||||
// 2) 本库映射表:legacyId → id
|
// 2) 本库映射表:legacyId → id
|
||||||
const contents = await db`SELECT id, legacy_id FROM contents WHERE legacy_id IS NOT NULL`;
|
const contents = await db`SELECT id, legacy_id FROM contents WHERE legacy_id IS NOT NULL`;
|
||||||
const contentMap = new Map<number, number>(contents.map((r) => [Number(r.legacy_id), Number(r.id)]));
|
const contentMap = new Map<number, number>(contents.map((r: { id: number; legacy_id: number }) => [Number(r.legacy_id), Number(r.id)]));
|
||||||
const cats = await db`SELECT id, legacy_id FROM categories WHERE legacy_id IS NOT NULL`;
|
const cats = await db`SELECT id, legacy_id FROM categories WHERE legacy_id IS NOT NULL`;
|
||||||
const catMap = new Map<number, number>(cats.map((r) => [Number(r.legacy_id), Number(r.id)]));
|
const catMap = new Map<number, number>(cats.map((r: { id: number; legacy_id: number }) => [Number(r.legacy_id), Number(r.id)]));
|
||||||
|
|
||||||
// 3) 已有关联集合(幂等)
|
// 3) 已有关联集合(幂等)
|
||||||
const existing = await db`SELECT content_id, category_id FROM content_terms`;
|
const existing = await db`SELECT content_id, category_id FROM content_terms`;
|
||||||
const have = new Set(existing.map((r) => `${r.content_id}:${r.category_id}`));
|
const have = new Set(existing.map((r: { content_id: number; category_id: number }) => `${r.content_id}:${r.category_id}`));
|
||||||
|
|
||||||
// 4) 补插
|
// 4) 补插
|
||||||
let inserted = 0;
|
let inserted = 0;
|
||||||
|
|||||||
@@ -59,6 +59,18 @@ function stripStructuralTags(html: string): string {
|
|||||||
return html.replace(/<\/?(?:main|body|html|head)[^>]*>/gi, "");
|
return html.replace(/<\/?(?:main|body|html|head)[^>]*>/gi, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** # 函数 CSS 长度折半(Elementor icon-list 间距:space/2 落在 padding/margin)
|
||||||
|
* 参数 v CSS 长度(如 "5px" / "1rem")
|
||||||
|
* 返回值 折半后的长度;无法解析返回 undefined
|
||||||
|
*/
|
||||||
|
function halfLength(v: string | undefined): string | undefined {
|
||||||
|
if (!v) return undefined;
|
||||||
|
const m = /^(-?[\d.]+)(px|rem|em|%)$/.exec(v.trim());
|
||||||
|
if (!m) return undefined;
|
||||||
|
const n = Number(m[1]) / 2;
|
||||||
|
return `${n}${m[2]}`;
|
||||||
|
}
|
||||||
|
|
||||||
/** # 函数 折叠 CSS 盒模型四值简写为最短等价形式(1/2/4 值)
|
/** # 函数 折叠 CSS 盒模型四值简写为最短等价形式(1/2/4 值)
|
||||||
* React 19 水合按 CSSOM 归一化后的字符串比对样式,"4rem 0rem 4rem 0rem"
|
* React 19 水合按 CSSOM 归一化后的字符串比对样式,"4rem 0rem 4rem 0rem"
|
||||||
* 会被浏览器归一为 "4rem 0rem",不折叠会导致水合不匹配。
|
* 会被浏览器归一为 "4rem 0rem",不折叠会导致水合不匹配。
|
||||||
@@ -190,6 +202,29 @@ function resolveGlobalsStr(v: string): string {
|
|||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** # 函数 响应式可见性类名(Elementor hide_* 设置 → elementor-hidden-*,配 app.css 媒体查询)
|
||||||
|
* 参数 settings 元素设置
|
||||||
|
* 返回值 空格分隔的类名(无隐藏设置则空串)
|
||||||
|
*/
|
||||||
|
function visibilityClass(settings: Record<string, unknown>): string {
|
||||||
|
const cls: string[] = [];
|
||||||
|
if (s(settings.hide_desktop) === "hidden-desktop") cls.push("elementor-hidden-desktop");
|
||||||
|
if (s(settings.hide_tablet) === "hidden-tablet") cls.push("elementor-hidden-tablet");
|
||||||
|
if (s(settings.hide_mobile) === "hidden-mobile") cls.push("elementor-hidden-mobile");
|
||||||
|
return cls.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** # 函数 section 的自定义 HTML 标签(Elementor html_tag 设置;默认 section)
|
||||||
|
* 参数 settings 元素设置
|
||||||
|
* 返回值 合法标签名
|
||||||
|
*/
|
||||||
|
function sectionTag(settings: Record<string, unknown>): "section" | "div" | "footer" | "header" | "aside" | "article" {
|
||||||
|
const tag = s(settings.html_tag);
|
||||||
|
return (["section", "div", "footer", "header", "aside", "article"] as const).includes(tag as never)
|
||||||
|
? (tag as "section" | "div" | "footer" | "header" | "aside" | "article")
|
||||||
|
: "section";
|
||||||
|
}
|
||||||
|
|
||||||
/** # 函数 section 的背景样式(纯色 / 图片) */
|
/** # 函数 section 的背景样式(纯色 / 图片) */
|
||||||
function sectionBgStyle(settings: Record<string, unknown>): CSSProperties {
|
function sectionBgStyle(settings: Record<string, unknown>): CSSProperties {
|
||||||
const style: CSSProperties = {};
|
const style: CSSProperties = {};
|
||||||
@@ -235,19 +270,26 @@ function normalizeDoc(doc: ElDocument): ElElement[] {
|
|||||||
/** # 成员变量 posts widget 文章数据上下文(loader 预取,经 widget id 分发) **/
|
/** # 成员变量 posts widget 文章数据上下文(loader 预取,经 widget id 分发) **/
|
||||||
const PostsDataContext = createContext<Record<string, PostCardItem[]>>({});
|
const PostsDataContext = createContext<Record<string, PostCardItem[]>>({});
|
||||||
|
|
||||||
/** # 组件 Elementor 渲染器入口 */
|
/** # 成员变量 WordPress 短代码渲染上下文(widget id → 预渲染节点;页脚 menu-2/gtranslate) **/
|
||||||
export function ElementorRenderer({ doc, postsData }: { doc: ElDocument; postsData?: Record<string, PostCardItem[]> }) {
|
const ShortcodesContext = createContext<Record<string, ReactNode>>({});
|
||||||
|
|
||||||
|
/** # 组件 Elementor 渲染器入口
|
||||||
|
* 参数 shortcodes 短代码 widget 的预渲染内容(键为 widget id;缺省渲染为空)
|
||||||
|
*/
|
||||||
|
export function ElementorRenderer({ doc, postsData, shortcodes }: { doc: ElDocument; postsData?: Record<string, PostCardItem[]>; shortcodes?: Record<string, ReactNode> }) {
|
||||||
const elements = normalizeDoc(doc);
|
const elements = normalizeDoc(doc);
|
||||||
if (!elements.length) return null;
|
if (!elements.length) return null;
|
||||||
const css = collectCustomCss(elements) + collectResponsiveCss(elements);
|
const css = collectCustomCss(elements) + collectResponsiveCss(elements);
|
||||||
return (
|
return (
|
||||||
<PostsDataContext.Provider value={postsData || {}}>
|
<PostsDataContext.Provider value={postsData || {}}>
|
||||||
|
<ShortcodesContext.Provider value={shortcodes || {}}>
|
||||||
<div className="content-html">
|
<div className="content-html">
|
||||||
{css ? <style dangerouslySetInnerHTML={{ __html: css }} /> : null}
|
{css ? <style dangerouslySetInnerHTML={{ __html: css }} /> : null}
|
||||||
{elements.map((el, i) => (
|
{elements.map((el, i) => (
|
||||||
<ElementNode key={el.id || i} el={el} />
|
<ElementNode key={el.id || i} el={el} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</ShortcodesContext.Provider>
|
||||||
</PostsDataContext.Provider>
|
</PostsDataContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -279,14 +321,64 @@ function collectResponsiveCss(elements: ElElement[]): string {
|
|||||||
let css = "";
|
let css = "";
|
||||||
function emit(el: ElElement) {
|
function emit(el: ElElement) {
|
||||||
const sel = `[data-el="${el.id}"]`;
|
const sel = `[data-el="${el.id}"]`;
|
||||||
const t = paddingCss((el.settings ?? {} as Record<string, unknown>).padding_tablet);
|
const st = (el.settings ?? {}) as Record<string, unknown>;
|
||||||
const m = paddingCss((el.settings ?? {} as Record<string, unknown>).padding_mobile);
|
// 列的内边距/外边距落在内层 populated(ColumnNode),section 落根元素
|
||||||
if (t) css += `@media (max-width:1024px){${sel}{padding:${t} !important;}}\n`;
|
const boxSel = s(el.elType) === "column" ? `${sel} > div` : sel;
|
||||||
if (m) css += `@media (max-width:767px){${sel}{padding:${m} !important;}}\n`;
|
const t = paddingCss(st.padding_tablet);
|
||||||
|
const m = paddingCss(st.padding_mobile);
|
||||||
|
if (t) css += `@media (max-width:1024px){${boxSel}{padding:${t} !important;}}\n`;
|
||||||
|
if (m) css += `@media (max-width:767px){${boxSel}{padding:${m} !important;}}\n`;
|
||||||
|
// 响应式外边距(页脚 a428bb4 tablet 70px / mobile 10px;Elementor 根元素 margin)
|
||||||
|
const mt = paddingCss(st.margin_tablet);
|
||||||
|
const mm = paddingCss(st.margin_mobile);
|
||||||
|
if (mt) css += `@media (max-width:1024px){${boxSel}{margin:${mt} !important;}}\n`;
|
||||||
|
if (mm) css += `@media (max-width:767px){${boxSel}{margin:${mm} !important;}}\n`;
|
||||||
|
// widget 高级盒模型响应式(_margin/_padding 的 tablet/mobile 变体挂内层容器)
|
||||||
|
if (s(el.elType) === "widget") {
|
||||||
|
const wsel = `${sel} > div`;
|
||||||
|
const wpt = paddingCss(st._padding_tablet);
|
||||||
|
const wpm = paddingCss(st._padding_mobile);
|
||||||
|
const wmt = paddingCss(st._margin_tablet);
|
||||||
|
const wmm = paddingCss(st._margin_mobile);
|
||||||
|
if (wpt) css += `@media (max-width:1024px){${wsel}{padding:${wpt} !important;}}\n`;
|
||||||
|
if (wpm) css += `@media (max-width:767px){${wsel}{padding:${wpm} !important;}}\n`;
|
||||||
|
if (wmt) css += `@media (max-width:1024px){${wsel}{margin:${wmt} !important;}}\n`;
|
||||||
|
if (wmm) css += `@media (max-width:767px){${wsel}{margin:${wmm} !important;}}\n`;
|
||||||
|
// widget 宽度模式响应式(页脚 gtranslate 移动端 width inherit)
|
||||||
|
const ewm = s(st._element_width_mobile);
|
||||||
|
if (ewm === "inherit") css += `@media (max-width:767px){${sel}{width:100% !important;max-width:100% !important;align-self:center;}}\n`;
|
||||||
|
else if (ewm === "auto") css += `@media (max-width:767px){${sel}{width:auto !important;max-width:auto !important;align-self:auto;}}\n`;
|
||||||
|
// image widget 图片移动端宽度(页脚 logo 47%)
|
||||||
|
if (s(el.widgetType) === "image") {
|
||||||
|
const iw = sizeCss(st.width_mobile);
|
||||||
|
if (iw) css += `@media (max-width:767px){${sel} img{width:${iw} !important;}}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 列响应式宽度(_inline_size_tablet / _inline_size_mobile;页脚内层四列 43%/100%)
|
||||||
|
const tw = st._inline_size_tablet;
|
||||||
|
const mw = st._inline_size_mobile;
|
||||||
|
if (typeof tw === "number" && tw > 0) {
|
||||||
|
css += `@media (max-width:1024px){${sel}{flex:0 0 ${tw}% !important;max-width:${tw}% !important;}}\n`;
|
||||||
|
}
|
||||||
|
if (typeof mw === "number" && mw > 0) {
|
||||||
|
css += `@media (max-width:767px){${sel}{flex:0 0 ${mw}% !important;max-width:${mw}% !important;}}\n`;
|
||||||
|
}
|
||||||
|
// icon-list 响应式项间距(space_between_tablet/mobile:页脚社交 10 → 7px)
|
||||||
|
if (s(el.widgetType) === "icon-list") {
|
||||||
|
const inlineItems = `${sel} .elementor-icon-list-items.elementor-inline-items`;
|
||||||
|
const stHalf = halfLength(sizeCss(st.space_between_tablet));
|
||||||
|
const smHalf = halfLength(sizeCss(st.space_between_mobile)) || stHalf;
|
||||||
|
if (stHalf) {
|
||||||
|
css += `@media (max-width:1024px){${inlineItems}{margin-inline:-${stHalf} !important;}${inlineItems} > .elementor-icon-list-item{margin-inline:${stHalf} !important;}}\n`;
|
||||||
|
}
|
||||||
|
if (smHalf) {
|
||||||
|
css += `@media (max-width:767px){${inlineItems}{margin-inline:-${smHalf} !important;}${inlineItems} > .elementor-icon-list-item{margin-inline:${smHalf} !important;}}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function walk(els: ElElement[]) {
|
function walk(els: ElElement[]) {
|
||||||
for (const el of els) {
|
for (const el of els) {
|
||||||
if (el.id && (s(el.elType) === "section" || s(el.elType) === "container" || s(el.elType) === "column")) emit(el);
|
if (el.id) emit(el);
|
||||||
if (el.elements?.length) walk(el.elements);
|
if (el.elements?.length) walk(el.elements);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -296,18 +388,19 @@ function collectResponsiveCss(elements: ElElement[]): string {
|
|||||||
|
|
||||||
/** # 组件 单个元素节点分发(section/column/container/widget)
|
/** # 组件 单个元素节点分发(section/column/container/widget)
|
||||||
* 参数 gapPad 所属 section 的默认列内边距(Elementor gap 档位,仅 column 消费)
|
* 参数 gapPad 所属 section 的默认列内边距(Elementor gap 档位,仅 column 消费)
|
||||||
|
* 参数 contentPos 所属 section 的 content_position(middle → 列的 widget-wrap 垂直居中)
|
||||||
*/
|
*/
|
||||||
function ElementNode({ el, gapPad }: { el: ElElement; gapPad?: string }) {
|
function ElementNode({ el, gapPad, contentPos }: { el: ElElement; gapPad?: string; contentPos?: string }) {
|
||||||
const type = s(el.elType);
|
const type = s(el.elType);
|
||||||
if (type === "widget") return <WidgetNode el={el} />;
|
if (type === "widget") return <WidgetNode el={el} />;
|
||||||
if (type === "section" || type === "container") return <SectionNode el={el} />;
|
if (type === "section" || type === "container") return <SectionNode el={el} />;
|
||||||
if (type === "column") return <ColumnNode el={el} gapPad={gapPad} />;
|
if (type === "column") return <ColumnNode el={el} gapPad={gapPad} contentPos={contentPos} />;
|
||||||
// 未知结构:有子元素则递归,否则忽略
|
// 未知结构:有子元素则递归,否则忽略
|
||||||
if (el.elements?.length) {
|
if (el.elements?.length) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{el.elements.map((c, i) => (
|
{el.elements.map((c, i) => (
|
||||||
<ElementNode key={c.id || i} el={c} gapPad={gapPad} />
|
<ElementNode key={c.id || i} el={c} gapPad={gapPad} contentPos={contentPos} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -369,17 +462,33 @@ function SectionNode({ el }: { el: ElElement }) {
|
|||||||
const ch = sizeCss(settings.custom_height);
|
const ch = sizeCss(settings.custom_height);
|
||||||
if (heightMode === "min-height" && ch) style.minHeight = ch;
|
if (heightMode === "min-height" && ch) style.minHeight = ch;
|
||||||
else if (heightMode === "full" && ch) style.height = ch;
|
else if (heightMode === "full" && ch) style.height = ch;
|
||||||
|
// section 边框(Elementor 仅在 border_border 有值时生成;页脚 a428bb4 顶部点线)
|
||||||
|
const secBorder = s(settings.border_border);
|
||||||
|
if (secBorder) {
|
||||||
|
style.borderStyle = secBorder as CSSProperties["borderStyle"];
|
||||||
|
const bw = paddingCss(settings.border_width);
|
||||||
|
if (bw) style.borderWidth = bw;
|
||||||
|
const bc = s(settings.border_color);
|
||||||
|
if (bc) style.borderColor = bc;
|
||||||
|
}
|
||||||
|
// 列内容垂直对齐(content_position=middle → align-items:center;页脚 d834160 实测)
|
||||||
|
const contentPos = s(settings.content_position);
|
||||||
|
if (contentPos === "middle") inner.alignItems = "center";
|
||||||
|
else if (contentPos === "top") inner.alignItems = "flex-start";
|
||||||
|
else if (contentPos === "bottom") inner.alignItems = "flex-end";
|
||||||
if (overlay) style.position = style.position || "relative";
|
if (overlay) style.position = style.position || "relative";
|
||||||
|
const Tag = sectionTag(settings);
|
||||||
|
const vis = visibilityClass(settings);
|
||||||
return (
|
return (
|
||||||
<section style={style} data-el={el.id}>
|
<Tag className={vis || undefined} style={style} data-el={el.id}>
|
||||||
{overlay}
|
{overlay}
|
||||||
{/* 行加 relative:与绝对定位遮罩同属定位层,DOM 靠后绘制在上,内容不被蒙灰 */}
|
{/* 行加 relative:与绝对定位遮罩同属定位层,DOM 靠后绘制在上,内容不被蒙灰 */}
|
||||||
<div className="mx-auto flex flex-wrap" style={{ ...inner, position: "relative" }}>
|
<div className="mx-auto flex flex-wrap" style={{ ...inner, position: "relative" }}>
|
||||||
{(el.elements || []).map((c, i) => (
|
{(el.elements || []).map((c, i) => (
|
||||||
<ElementNode key={c.id || i} el={c} gapPad={gapPad} />
|
<ElementNode key={c.id || i} el={c} gapPad={gapPad} contentPos={contentPos} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</Tag>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +514,7 @@ function boxNonZero(v: unknown): boolean {
|
|||||||
* (frontend.min.css .elementor-column-gap-default > … > .elementor-element-populated{padding:10px},
|
* (frontend.min.css .elementor-column-gap-default > … > .elementor-element-populated{padding:10px},
|
||||||
* 实测 H6 列 59bc2e1 四向 10px、H7 列 66de90a 设置覆盖为 8/16/16/0,两端一致)。
|
* 实测 H6 列 59bc2e1 四向 10px、H7 列 66de90a 设置覆盖为 8/16/16/0,两端一致)。
|
||||||
*/
|
*/
|
||||||
function ColumnNode({ el, gapPad }: { el: ElElement; gapPad?: string }) {
|
function ColumnNode({ el, gapPad, contentPos }: { el: ElElement; gapPad?: string; contentPos?: string }) {
|
||||||
const settings = resolveGlobals(el.settings || {});
|
const settings = resolveGlobals(el.settings || {});
|
||||||
const outer: CSSProperties = { minWidth: 0, flex: "1 1 0" };
|
const outer: CSSProperties = { minWidth: 0, flex: "1 1 0" };
|
||||||
const inline = settings._inline_size;
|
const inline = settings._inline_size;
|
||||||
@@ -413,7 +522,17 @@ function ColumnNode({ el, gapPad }: { el: ElElement; gapPad?: string }) {
|
|||||||
outer.flex = `0 0 ${inline}%`;
|
outer.flex = `0 0 ${inline}%`;
|
||||||
outer.maxWidth = `${inline}%`;
|
outer.maxWidth = `${inline}%`;
|
||||||
}
|
}
|
||||||
|
// widget-wrap 对齐:Elementor 默认 align-items:normal(stretch,与线上 computed 一致);
|
||||||
|
// section content_position=middle 时 align-content/align-items 均为 center
|
||||||
|
// (页脚 d834160 生成规则;社交行因此与 gtranslate 下拉同高 29px)
|
||||||
const inner: CSSProperties = {};
|
const inner: CSSProperties = {};
|
||||||
|
if (contentPos === "middle") {
|
||||||
|
inner.alignItems = "center";
|
||||||
|
inner.alignContent = "center";
|
||||||
|
} else if (contentPos === "bottom") {
|
||||||
|
inner.alignItems = "flex-end";
|
||||||
|
inner.alignContent = "flex-end";
|
||||||
|
}
|
||||||
const mg = paddingCss(settings.margin);
|
const mg = paddingCss(settings.margin);
|
||||||
if (mg) {
|
if (mg) {
|
||||||
// margin 写在 populated(与 Elementor 相同):宽保持 auto——块级填充会自动扣除自身外边距
|
// margin 写在 populated(与 Elementor 相同):宽保持 auto——块级填充会自动扣除自身外边距
|
||||||
@@ -440,7 +559,7 @@ function ColumnNode({ el, gapPad }: { el: ElElement; gapPad?: string }) {
|
|||||||
// margin 溢出);H11/H14/H15 等"扣除 margin"语义的列在 app.css 用 !important 覆写为 auto。
|
// margin 溢出);H11/H14/H15 等"扣除 margin"语义的列在 app.css 用 !important 覆写为 auto。
|
||||||
// 空列(无子元素)裸渲染:原站空列无 populated 盒(H15 占位格 margin/边框均为 0),
|
// 空列(无子元素)裸渲染:原站空列无 populated 盒(H15 占位格 margin/边框均为 0),
|
||||||
// 仅保留外层弹性宽度与拉伸高度
|
// 仅保留外层弹性宽度与拉伸高度
|
||||||
<div className="el-col min-w-0" style={outer} data-el={el.id}>
|
<div className={"el-col min-w-0 " + visibilityClass(settings)} style={outer} data-el={el.id}>
|
||||||
{(el.elements || []).length ? (
|
{(el.elements || []).length ? (
|
||||||
<div style={{ display: "flex", flexWrap: "wrap", alignContent: "flex-start", width: "100%", ...inner }}>
|
<div style={{ display: "flex", flexWrap: "wrap", alignContent: "flex-start", width: "100%", ...inner }}>
|
||||||
{(el.elements || []).map((c, i) => (
|
{(el.elements || []).map((c, i) => (
|
||||||
@@ -493,13 +612,19 @@ function WidgetNode({ el }: { el: ElElement }) {
|
|||||||
// widget 默认满宽(flex wrap 中独占一行),_element_width=auto 时按内容宽(横排)
|
// widget 默认满宽(flex wrap 中独占一行),_element_width=auto 时按内容宽(横排)
|
||||||
const widthAuto = s(settings._element_width) === "auto";
|
const widthAuto = s(settings._element_width) === "auto";
|
||||||
const cls = s(settings._css_classes) || undefined;
|
const cls = s(settings._css_classes) || undefined;
|
||||||
const baseCls = cls ? (widthAuto ? cls + " el-w-auto" : cls) : widthAuto ? "el-w-auto" : undefined;
|
const vis = visibilityClass(settings);
|
||||||
const hasContainerStyle = Object.keys(style).length > 0;
|
const baseCls = [cls, widthAuto ? "el-w-auto" : "", vis].filter(Boolean).join(" ") || undefined;
|
||||||
|
// 有桌面盒样式,或有响应式 _margin_*/_padding_*(移动端需挂在内层容器,页脚 c015d97
|
||||||
|
// -1rem 上外距靠内层负外距把整块高度压到 15px)→ 双层结构
|
||||||
|
const hasResponsiveBox =
|
||||||
|
boxNonZero(settings._margin_mobile) || boxNonZero(settings._padding_mobile) ||
|
||||||
|
boxNonZero(settings._margin_tablet) || boxNonZero(settings._padding_tablet);
|
||||||
|
const hasContainerStyle = Object.keys(style).length > 0 || hasResponsiveBox;
|
||||||
if (hasContainerStyle) {
|
if (hasContainerStyle) {
|
||||||
return (
|
return (
|
||||||
// 外层也要带 el-w-auto:列内基础规则按 :not(.el-w-auto) 放行 width:auto 的根,
|
// 外层也要带 el-w-auto:列内基础规则按 :not(.el-w-auto) 放行 width:auto 的根,
|
||||||
// 否则 width-auto widget(如图标)被 width:100% 强推成每行一个(H7 回归根因)
|
// 否则 width-auto widget(如图标)被 width:100% 强推成每行一个(H7 回归根因)
|
||||||
<div className={widthAuto ? "el-w-auto" : undefined} data-el={el.id}>
|
<div className={[widthAuto ? "el-w-auto" : "", vis].filter(Boolean).join(" ") || undefined} data-el={el.id}>
|
||||||
<div className={baseCls} style={{ width: widthAuto ? "auto" : "100%", ...style }}>{inner}</div>
|
<div className={baseCls} style={{ width: widthAuto ? "auto" : "100%", ...style }}>{inner}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -518,6 +643,7 @@ class WidgetGuard extends Component<{ widgetType: string; children: ReactNode },
|
|||||||
function WidgetNodeInner({ el }: { el: ElElement }) {
|
function WidgetNodeInner({ el }: { el: ElElement }) {
|
||||||
const w = s(el.widgetType);
|
const w = s(el.widgetType);
|
||||||
const settings = resolveGlobals(el.settings || {});
|
const settings = resolveGlobals(el.settings || {});
|
||||||
|
const shortcodes = useContext(ShortcodesContext);
|
||||||
const unknownFallback = () => {
|
const unknownFallback = () => {
|
||||||
const title = s(settings.heading_title) || s(settings.title);
|
const title = s(settings.heading_title) || s(settings.title);
|
||||||
const text = s(settings.editor) || s(settings.html) || s(settings.text);
|
const text = s(settings.editor) || s(settings.html) || s(settings.text);
|
||||||
@@ -562,10 +688,21 @@ function WidgetNodeInner({ el }: { el: ElElement }) {
|
|||||||
return <ReviewsWidget settings={settings} />;
|
return <ReviewsWidget settings={settings} />;
|
||||||
case "icon-list":
|
case "icon-list":
|
||||||
return <IconListWidget settings={settings} />;
|
return <IconListWidget settings={settings} />;
|
||||||
|
case "icon":
|
||||||
|
return <IconWidget settings={settings} />;
|
||||||
case "spacer":
|
case "spacer":
|
||||||
return <div style={{ height: sizeCss(settings.space, "px") || "20px" }} />;
|
return <div style={{ height: sizeCss(settings.space, "px") || "20px" }} />;
|
||||||
case "divider":
|
case "divider":
|
||||||
return <hr style={{ border: 0, borderTop: `1px solid ${s(settings.color) || "#e3e9f0"}`, margin: "18px 0" }} />;
|
// Elementor divider DOM(页脚 c015d97:padding-block 15px + 1px #4D4E53 分隔线,
|
||||||
|
// 高 31px,与 widget 的 20px 下外距合计 51px 间距,实测 2026-11-02)
|
||||||
|
return (
|
||||||
|
<div className="elementor-divider" style={{ paddingBlock: sizeCss(settings.gap) || "15px" }}>
|
||||||
|
<span
|
||||||
|
className="elementor-divider-separator"
|
||||||
|
style={{ display: "block", width: "100%", borderTop: `1px solid ${s(settings.color) || "#e3e9f0"}` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
case "toggle":
|
case "toggle":
|
||||||
case "accordion":
|
case "accordion":
|
||||||
return <AccordionWidget settings={settings} />;
|
return <AccordionWidget settings={settings} />;
|
||||||
@@ -595,7 +732,9 @@ function WidgetNodeInner({ el }: { el: ElElement }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case "shortcode":
|
case "shortcode":
|
||||||
return null; // WordPress shortcode 无法在 Node 端执行,忽略
|
// WordPress 短代码无法在 Node 端执行:由调用方按 widget id 传入预渲染节点
|
||||||
|
// (页脚 post 58:0683e73 → menu-2 移动端折叠菜单;2481635 → gtranslate 语言切换)
|
||||||
|
return shortcodes[s(el.id)] ?? null;
|
||||||
case "code-highlight":
|
case "code-highlight":
|
||||||
return s(settings.source_code) ? (
|
return s(settings.source_code) ? (
|
||||||
<pre className="mb-[18px]" style={{ background: "#0b1c2c", color: "#d7e5f5", padding: 16, borderRadius: 8, overflowX: "auto" }}>
|
<pre className="mb-[18px]" style={{ background: "#0b1c2c", color: "#d7e5f5", padding: 16, borderRadius: 8, overflowX: "auto" }}>
|
||||||
@@ -637,13 +776,32 @@ function HeadingWidget({ settings }: { settings: Record<string, unknown> }) {
|
|||||||
}
|
}
|
||||||
/** # 组件 text-editor widget:富文本 HTML */
|
/** # 组件 text-editor widget:富文本 HTML */
|
||||||
function TextEditorWidget({ settings }: { settings: Record<string, unknown> }) {
|
function TextEditorWidget({ settings }: { settings: Record<string, unknown> }) {
|
||||||
const html = s(settings.editor);
|
let html = s(settings.editor);
|
||||||
|
// 动态标签 current-date-time(页脚 1fe90e6 版权年份):editor 为空 + __dynamic__.editor
|
||||||
|
// 含 current-date-time → 按 before + Y 生成(原站实测 "Copyright ©2026")
|
||||||
|
const dynamic = s((settings.__dynamic__ as Record<string, unknown> | undefined)?.editor);
|
||||||
|
if (!html && dynamic.includes("current-date-time")) {
|
||||||
|
// settings 属性是 URL 编码的 JSON:settings="%7B%22date_format%22...%22before%22%3A%22Copyright%20%C2%A9%22"
|
||||||
|
let before = "Copyright ©";
|
||||||
|
const m = /settings="([^"]*)"/.exec(dynamic);
|
||||||
|
if (m) {
|
||||||
|
try {
|
||||||
|
const dj = JSON.parse(decodeURIComponent(m[1]!)) as { before?: string };
|
||||||
|
if (dj.before) before = dj.before;
|
||||||
|
} catch {
|
||||||
|
/* 解析失败用默认前缀 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html = `${before}${new Date().getFullYear()}`;
|
||||||
|
}
|
||||||
if (!html) return null;
|
if (!html) return null;
|
||||||
// 文本颜色可能来自全局色(如蓝底白字副标题),style 方式应用
|
// 文本颜色可能来自全局色(如蓝底白字副标题),style 方式应用
|
||||||
const color = s(settings.text_color);
|
const color = s(settings.text_color);
|
||||||
const style: CSSProperties = {};
|
const style: CSSProperties = {};
|
||||||
if (color) style.color = color;
|
if (color) style.color = color;
|
||||||
Object.assign(style, typographyCss(settings));
|
Object.assign(style, typographyCss(settings));
|
||||||
|
const align = s(settings.align);
|
||||||
|
if (align) style.textAlign = align as CSSProperties["textAlign"];
|
||||||
// _padding/_margin/_background_* 属 widget 包裹层(WidgetNode),不在此重复
|
// _padding/_margin/_background_* 属 widget 包裹层(WidgetNode),不在此重复
|
||||||
return <div className="el-texteditor" style={Object.keys(style).length ? style : undefined} dangerouslySetInnerHTML={{ __html: html }} />;
|
return <div className="el-texteditor" style={Object.keys(style).length ? style : undefined} dangerouslySetInnerHTML={{ __html: html }} />;
|
||||||
}
|
}
|
||||||
@@ -659,7 +817,10 @@ function ImageWidget({ settings }: { settings: Record<string, unknown> }) {
|
|||||||
const link = (settings.link as { url?: string } | undefined)?.url;
|
const link = (settings.link as { url?: string } | undefined)?.url;
|
||||||
const isExternal = (settings.link as { is_external?: string } | undefined)?.is_external === "on";
|
const isExternal = (settings.link as { is_external?: string } | undefined)?.is_external === "on";
|
||||||
const linkProps = link ? { href: link, ...(isExternal ? { target: "_blank" as const } : {}) } : undefined;
|
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" />;
|
// 图片宽度设置(Elementor 生成 `.elementor-element-X img{width:57%}`;页脚 logo 57%、
|
||||||
|
// Google 徽章 300px):作用于 img 本身,a 为 inline-block 时按固有宽度收缩(与原站一致)
|
||||||
|
const imgWidth = sizeCss(settings.width);
|
||||||
|
const img = <img className={animCls || undefined} src={url} alt={s(img0.alt)} loading="lazy" style={imgWidth ? { width: imgWidth } : undefined} />;
|
||||||
const cls = ("el-image " + (animCls || "")).trim();
|
const cls = ("el-image " + (animCls || "")).trim();
|
||||||
return (
|
return (
|
||||||
<div className={cls} style={{ textAlign: (align as CSSProperties["textAlign"]) || "center" }}>
|
<div className={cls} style={{ textAlign: (align as CSSProperties["textAlign"]) || "center" }}>
|
||||||
@@ -1291,30 +1452,60 @@ function IconListWidget({ settings }: { settings: Record<string, unknown> }) {
|
|||||||
const list = Array.isArray(settings.icon_list)
|
const list = Array.isArray(settings.icon_list)
|
||||||
? (settings.icon_list as { text?: string; _id?: string; link?: { url?: string; is_external?: string }; selected_icon?: { value?: string } }[])
|
? (settings.icon_list as { text?: string; _id?: string; link?: { url?: string; is_external?: string }; selected_icon?: { value?: string } }[])
|
||||||
: [];
|
: [];
|
||||||
const items = list.filter((it) => s(it.text));
|
// 允许只有图标没有文字的项(页脚社交图标行 text 为空、selected_icon=fab fa-*)
|
||||||
|
const items = list.filter((it) => s(it.text) || s(it.selected_icon?.value));
|
||||||
if (!items.length) return null;
|
if (!items.length) return null;
|
||||||
const inline = s(settings.view) === "inline";
|
const inline = s(settings.view) === "inline";
|
||||||
const color = s(settings.text_color) || undefined;
|
const color = s(settings.text_color) || undefined;
|
||||||
|
const iconColor = s(settings.icon_color) || undefined;
|
||||||
|
const iconSize = sizeCss(settings.icon_size);
|
||||||
|
const iconAlign = s(settings.icon_align);
|
||||||
|
// 项间距(space_between):Elementor 竖排 = padding-block-end(space/2) + margin-block-start(space/2),
|
||||||
|
// 横排 = li margin-inline(space/2) + ul 负 margin-inline(页脚 5px / 社交 10px,实测 li 高 23.5)
|
||||||
|
const half = halfLength(sizeCss(settings.space_between));
|
||||||
|
const typo = typographyCss(settings, "icon_typography");
|
||||||
|
// 颜色用 CSS 变量承载:文字/图标的内联色若写死十六进制,页面级 :hover 规则无法覆盖
|
||||||
|
// (内联样式优先级最高)。改为 span/i 引用 var(--el-il-text/icon),悬停规则改 li 上的变量值。
|
||||||
|
const vars: Record<string, string> = {};
|
||||||
|
if (color) vars["--el-il-text"] = color;
|
||||||
|
if (iconColor) vars["--el-il-icon"] = iconColor;
|
||||||
|
const ulStyle: CSSProperties = { ...typo };
|
||||||
|
Object.assign(ulStyle, vars);
|
||||||
|
if (iconAlign === "left") ulStyle.justifyContent = "flex-start";
|
||||||
|
else if (iconAlign === "center") ulStyle.justifyContent = "center";
|
||||||
|
else if (iconAlign === "right") ulStyle.justifyContent = "flex-end";
|
||||||
|
if (inline && half) ulStyle.marginInline = `-${half}`;
|
||||||
return (
|
return (
|
||||||
<ul className={"elementor-icon-list-items" + (inline ? " elementor-inline-items" : "")}>
|
<ul className={"elementor-icon-list-items" + (inline ? " elementor-inline-items" : "")} style={Object.keys(ulStyle).length ? ulStyle : undefined}>
|
||||||
{items.map((it, i) => {
|
{items.map((it, i) => {
|
||||||
const link = it.link?.url;
|
const link = it.link?.url;
|
||||||
const external = it.link?.is_external === "on";
|
const external = it.link?.is_external === "on";
|
||||||
|
const icon = s(it.selected_icon?.value);
|
||||||
|
const liStyle: CSSProperties | undefined = half
|
||||||
|
? inline
|
||||||
|
? { marginInline: half }
|
||||||
|
: { paddingBottom: i < items.length - 1 ? half : undefined, marginTop: i > 0 ? half : undefined }
|
||||||
|
: undefined;
|
||||||
|
const iconStyle: CSSProperties = { color: iconColor ? "var(--el-il-icon)" : undefined, fontSize: iconSize };
|
||||||
const text = (
|
const text = (
|
||||||
<span
|
<span
|
||||||
className="elementor-icon-list-text"
|
className="elementor-icon-list-text"
|
||||||
style={color ? { color } : undefined}
|
style={color ? { color: "var(--el-il-text)" } : undefined}
|
||||||
dangerouslySetInnerHTML={{ __html: s(it.text) }}
|
dangerouslySetInnerHTML={{ __html: s(it.text) }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<li key={it._id || i} className={"elementor-icon-list-item" + (inline ? " elementor-inline-item" : "")}>
|
<li key={it._id || i} className={"elementor-icon-list-item" + (inline ? " elementor-inline-item" : "")} style={liStyle}>
|
||||||
{link ? (
|
{link ? (
|
||||||
<a href={link} target={external ? "_blank" : undefined} rel={external ? "noopener" : undefined}>
|
<a href={link} target={external ? "_blank" : undefined} rel={external ? "noopener noreferrer" : undefined}>
|
||||||
|
{icon ? <span className="elementor-icon-list-icon"><i className={icon} aria-hidden="true" style={iconStyle} /></span> : null}
|
||||||
{text}
|
{text}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
text
|
<>
|
||||||
|
{icon ? <span className="elementor-icon-list-icon"><i className={icon} aria-hidden="true" style={iconStyle} /></span> : null}
|
||||||
|
{text}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -1323,6 +1514,70 @@ function IconListWidget({ settings }: { settings: Record<string, unknown> }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** # 组件 icon widget(Elementor .elementor-widget-icon)
|
||||||
|
* 页脚 post 58 的 5ffe573:view=stacked + _position=fixed(左下角返回顶部按钮),
|
||||||
|
* 48×48 圆(图标 24px + padding 0.5em)、底色 secondary #FFA11B、悬停主色 #ED6C00(post-58.css 取证);
|
||||||
|
* 088727e(询价弹窗入口)在桌面/平板/移动全部隐藏,由可见性类名隐藏。
|
||||||
|
*/
|
||||||
|
function IconWidget({ settings }: { settings: Record<string, unknown> }) {
|
||||||
|
const icon = s((settings.selected_icon as { value?: string } | undefined)?.value);
|
||||||
|
if (!icon) return null;
|
||||||
|
const view = s(settings.view);
|
||||||
|
const size = sizeCss(settings.size) || "24px";
|
||||||
|
const primary = s(settings.primary_color);
|
||||||
|
const secondary = s(settings.secondary_color);
|
||||||
|
const link = (settings.link as { url?: string } | undefined)?.url;
|
||||||
|
const wrapper: CSSProperties = {};
|
||||||
|
if (s(settings._position) === "fixed") {
|
||||||
|
wrapper.position = "fixed";
|
||||||
|
const x = sizeCss(settings._offset_x);
|
||||||
|
const xe = sizeCss(settings._offset_x_end);
|
||||||
|
const y = sizeCss(settings._offset_y);
|
||||||
|
const ye = sizeCss(settings._offset_y_end);
|
||||||
|
if (s(settings._offset_orientation_h) === "end") {
|
||||||
|
if (xe) wrapper.right = xe;
|
||||||
|
} else if (x) wrapper.left = x;
|
||||||
|
else if (xe) wrapper.right = xe;
|
||||||
|
if (s(settings._offset_orientation_v) === "end") {
|
||||||
|
if (ye) wrapper.bottom = ye;
|
||||||
|
} else if (y) wrapper.top = y;
|
||||||
|
else if (ye) wrapper.bottom = ye;
|
||||||
|
wrapper.zIndex = 99;
|
||||||
|
wrapper.width = "auto";
|
||||||
|
}
|
||||||
|
const iconStyle: CSSProperties = { fontSize: size, lineHeight: 0 };
|
||||||
|
if (view === "stacked") {
|
||||||
|
iconStyle.display = "inline-flex";
|
||||||
|
iconStyle.alignItems = "center";
|
||||||
|
iconStyle.justifyContent = "center";
|
||||||
|
iconStyle.padding = "0.5em";
|
||||||
|
iconStyle.borderRadius = "50%";
|
||||||
|
iconStyle.backgroundColor = secondary || primary || "#FFA11B";
|
||||||
|
iconStyle.color = "#ffffff";
|
||||||
|
} else {
|
||||||
|
if (primary) iconStyle.color = primary;
|
||||||
|
if (view === "framed") {
|
||||||
|
iconStyle.display = "inline-flex";
|
||||||
|
iconStyle.padding = "0.5em";
|
||||||
|
iconStyle.borderRadius = "50%";
|
||||||
|
iconStyle.border = `3px solid ${primary || "currentColor"}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="elementor-icon-wrapper" style={{ textAlign: "center", ...wrapper }}>
|
||||||
|
{link ? (
|
||||||
|
<a className="elementor-icon" href={link} style={iconStyle}>
|
||||||
|
<i className={icon} aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<div className="elementor-icon" style={iconStyle}>
|
||||||
|
<i className={icon} aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** # 组件 toggle / accordion widget:手风琴 */
|
/** # 组件 toggle / accordion widget:手风琴 */
|
||||||
function AccordionWidget({ settings }: { settings: Record<string, unknown> }) {
|
function AccordionWidget({ 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 { tab_title?: string; tab_content?: string }[]) : [];
|
||||||
|
|||||||
@@ -28,7 +28,17 @@ const SETTING_KEYS = new Set([
|
|||||||
"border_width",
|
"border_width",
|
||||||
"content_width",
|
"content_width",
|
||||||
"custom_height",
|
"custom_height",
|
||||||
|
// section 自定义标签(页脚 d834160 = footer)与列垂直对齐(middle)
|
||||||
|
"html_tag",
|
||||||
|
"content_position",
|
||||||
|
// 列响应式宽度(页脚内层四列 tablet 43% / mobile 100%)
|
||||||
|
"_inline_size_tablet",
|
||||||
|
"_inline_size_mobile",
|
||||||
"padding",
|
"padding",
|
||||||
|
"padding_tablet",
|
||||||
|
"padding_mobile",
|
||||||
|
"margin_tablet",
|
||||||
|
"margin_mobile",
|
||||||
"layout",
|
"layout",
|
||||||
"height",
|
"height",
|
||||||
"size",
|
"size",
|
||||||
@@ -59,6 +69,14 @@ const SETTING_KEYS = new Set([
|
|||||||
"description_typography_font_size",
|
"description_typography_font_size",
|
||||||
"description_typography_font_size_mobile",
|
"description_typography_font_size_mobile",
|
||||||
"description_typography_font_size_tablet",
|
"description_typography_font_size_tablet",
|
||||||
|
// icon-list 项间距与文字排版(页脚链接列 5px / 14px 300,实测 2026-11-02)
|
||||||
|
"space_between",
|
||||||
|
"space_between_tablet",
|
||||||
|
"space_between_mobile",
|
||||||
|
"icon_typography_typography",
|
||||||
|
"icon_typography_font_size",
|
||||||
|
"icon_typography_font_size_tablet",
|
||||||
|
"icon_typography_font_weight",
|
||||||
"gradient_angle",
|
"gradient_angle",
|
||||||
"color_a",
|
"color_a",
|
||||||
"color_b",
|
"color_b",
|
||||||
@@ -93,6 +111,7 @@ const SETTING_KEYS = new Set([
|
|||||||
"selected_icon",
|
"selected_icon",
|
||||||
"url",
|
"url",
|
||||||
"width",
|
"width",
|
||||||
|
"width_mobile",
|
||||||
"text",
|
"text",
|
||||||
"editor",
|
"editor",
|
||||||
"html",
|
"html",
|
||||||
@@ -162,8 +181,37 @@ const SETTING_KEYS = new Set([
|
|||||||
"hover_animation",
|
"hover_animation",
|
||||||
// widget 高级盒模型(如 eael-info-box 边框卡:_border_* + _padding/_margin)
|
// widget 高级盒模型(如 eael-info-box 边框卡:_border_* + _padding/_margin)
|
||||||
"_margin",
|
"_margin",
|
||||||
|
"_margin_tablet",
|
||||||
|
"_margin_mobile",
|
||||||
|
"_padding_tablet",
|
||||||
|
"_padding_mobile",
|
||||||
// widget 宽度模式(auto → 按内容宽横排,如 H7 图标行)
|
// widget 宽度模式(auto → 按内容宽横排,如 H7 图标行)
|
||||||
"_element_width",
|
"_element_width",
|
||||||
|
"_element_width_mobile",
|
||||||
|
"_element_vertical_align_mobile",
|
||||||
|
// icon widget:堆叠/描边视图、固定定位与偏移(页脚返回顶部按钮)
|
||||||
|
"primary_color",
|
||||||
|
"secondary_color",
|
||||||
|
"hover_primary_color",
|
||||||
|
"hover_secondary_color",
|
||||||
|
"_position",
|
||||||
|
"_offset_x",
|
||||||
|
"_offset_x_end",
|
||||||
|
"_offset_y",
|
||||||
|
"_offset_y_end",
|
||||||
|
"_offset_orientation_h",
|
||||||
|
"_offset_orientation_v",
|
||||||
|
// icon-list 图标样式(页脚社交行 icon_color/icon_size/icon_align)
|
||||||
|
"icon_color",
|
||||||
|
"icon_size",
|
||||||
|
"icon_align",
|
||||||
|
"icon_align_mobile",
|
||||||
|
// 响应式可见性(页脚 46d7afa 移动端隐藏、088727e 全断点隐藏)
|
||||||
|
"hide_desktop",
|
||||||
|
"hide_tablet",
|
||||||
|
"hide_mobile",
|
||||||
|
// 动态标签(页脚版权年份 current-date-time)
|
||||||
|
"__dynamic__",
|
||||||
// widget 容器背景(如 H4 标题下的橙线图)
|
// widget 容器背景(如 H4 标题下的橙线图)
|
||||||
"_background_image",
|
"_background_image",
|
||||||
"_background_position",
|
"_background_position",
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
/**
|
/**
|
||||||
* # 组件 网站页脚 Footer(对齐原站,Tailwind + shadcn/ui 实现)
|
* # 组件 网站页脚 Footer
|
||||||
* 深灰底两段:上段 4 个菜单组;下段左侧品牌区(logo + 简介 + 社交 + 语言下拉),
|
* 原站页脚为 Elementor 模板(elementor_library post 58),由 Layout 从 contents
|
||||||
* 右侧 3 个菜单组;底部版权条。
|
* (type=template, slug=footer)取出后交给 ElementorRenderer 一比一渲染:
|
||||||
* 数据来自导入的 footer 菜单(menu-2);为空时降级为内置链接。
|
* - 46d7afa:顶部四列链接(System on Modules / ODM-OEM / Multiprotocol IoT Gateway /
|
||||||
|
* One-Stop Hardware Solutions,桌面显示、移动隐藏);
|
||||||
|
* - d834160(html_tag=footer):返回顶部按钮(fixed 左下)、分隔线、内嵌 28c7f0b8
|
||||||
|
* (logo + 简介 + 社交图标 + Google 徽章 + About/Developer/Policy 三列)与
|
||||||
|
* a428bb4(版权条)。
|
||||||
|
* 短代码 widget 由本组件预渲染注入:menu-2 折叠菜单(仅移动端,桌面/平板隐藏)、
|
||||||
|
* gtranslate(原站该控件在桌面渲染为空,保持为空以对齐)。
|
||||||
|
* 模板缺失时降级为内置菜单页脚,保证站点可用。
|
||||||
*/
|
*/
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Headset } from "lucide-react";
|
import { Headset } from "lucide-react";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
|
||||||
|
import { ElementorRenderer, type ElDocument } from "~/components/elementor/Renderer";
|
||||||
import { useT } from "~/i18n";
|
import { useT } from "~/i18n";
|
||||||
import { LOCALES, getUiLocale, setUiLocale } from "~/i18n/shared";
|
import { LOCALES, getUiLocale, setUiLocale } from "~/i18n/shared";
|
||||||
import type { MenuNode } from "~/server/data";
|
import type { MenuNode } from "~/server/data";
|
||||||
@@ -26,10 +34,139 @@ const FALLBACK_GROUPS: MenuNode[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
/** # 组件 页脚 */
|
/** # 组件 页脚 */
|
||||||
export function Footer(props: { categories: FooterCategory[]; menu: MenuNode[] }) {
|
export function Footer(props: { categories: FooterCategory[]; menu: MenuNode[]; doc?: ElDocument | null }) {
|
||||||
|
// 优先渲染原站 Elementor 页脚模板(一比一);短代码按 widget id 注入
|
||||||
|
if (props.doc) {
|
||||||
|
const doc = props.doc;
|
||||||
|
const hasElements = Array.isArray(doc) ? doc.length > 0 : !!(doc as { elements?: unknown[] }).elements?.length;
|
||||||
|
if (hasElements) {
|
||||||
|
return (
|
||||||
|
<footer className="elementor elementor-58 elementor-location-footer" data-elementor-type="footer" data-elementor-id="58">
|
||||||
|
<ElementorRenderer
|
||||||
|
doc={doc}
|
||||||
|
shortcodes={{
|
||||||
|
"0683e73": <FooterMobileMenu menu={props.menu} />,
|
||||||
|
"2481635": <FooterLanguageSelect />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <FallbackFooter categories={props.categories} menu={props.menu} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** # 组件 移动端折叠菜单(原站 [maxmegamenu location=menu-2],桌面/平板由 Elementor 隐藏)
|
||||||
|
* 结构对齐 Max Mega Menu 输出(mega-menu-wrap / mega-menu-toggle / ul.mega-menu),
|
||||||
|
* 交互为点击展开(原站 data-effect-mobile=disabled,移动端即点击展开)。
|
||||||
|
*/
|
||||||
|
function FooterMobileMenu({ menu }: { menu: MenuNode[] }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const groups = menu.length > 0 ? menu : FALLBACK_GROUPS;
|
||||||
|
return (
|
||||||
|
<div id="mega-menu-wrap-menu-2" className="mega-menu-wrap">
|
||||||
|
<div className="mega-menu-toggle">
|
||||||
|
<div className="mega-toggle-blocks-left" />
|
||||||
|
<div className="mega-toggle-blocks-center">
|
||||||
|
<div className="mega-toggle-block mega-menu-toggle-animated-block mega-toggle-block-1" id="mega-toggle-block-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Toggle Menu"
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-controls="mega-menu-menu-2"
|
||||||
|
className="mega-toggle-animated mega-toggle-animated-slider"
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
>
|
||||||
|
<span className="mega-toggle-animated-box">
|
||||||
|
<span className="mega-toggle-animated-inner" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mega-toggle-blocks-right" />
|
||||||
|
</div>
|
||||||
|
<ul id="mega-menu-menu-2" className="mega-menu max-mega-menu" hidden={!open}>
|
||||||
|
{groups.map((g) => (
|
||||||
|
<li key={g.id} className={"mega-menu-item" + (g.children.length ? " mega-menu-item-has-children" : "")}>
|
||||||
|
<a className="mega-menu-link" href={g.url || "#"} target={g.target === "_blank" ? "_blank" : undefined}>
|
||||||
|
{g.title}
|
||||||
|
</a>
|
||||||
|
{g.children.length ? (
|
||||||
|
<ul className="mega-sub-menu">
|
||||||
|
{g.children.map((c) => (
|
||||||
|
<li key={c.id} className="mega-menu-item">
|
||||||
|
<a className="mega-menu-link" href={c.url || "#"} target={c.target === "_blank" ? "_blank" : undefined}>
|
||||||
|
{c.title}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : null}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** # 常量 页脚语言下拉选项(对齐原站 GTranslate 列表顺序与英文名;
|
||||||
|
* 切换写入 ui_lang cookie 后整页刷新,由自建 i18n 词典接管渲染) */
|
||||||
|
const FOOTER_LANGS: { code: string; label: string }[] = [
|
||||||
|
{ code: "en", label: "English" },
|
||||||
|
{ code: "de", label: "German" },
|
||||||
|
{ code: "fr", label: "French" },
|
||||||
|
{ code: "ja", label: "Japanese" },
|
||||||
|
{ code: "ko", label: "Korean" },
|
||||||
|
{ code: "ru", label: "Russian" },
|
||||||
|
{ code: "es", label: "Spanish" },
|
||||||
|
{ code: "th", label: "Thai" },
|
||||||
|
{ code: "ar", label: "Arabic" },
|
||||||
|
{ code: "zh-TW", label: "Chinese (Traditional)" },
|
||||||
|
{ code: "it", label: "Italian" },
|
||||||
|
{ code: "hi", label: "Hindi" },
|
||||||
|
{ code: "uk", label: "Ukrainian" },
|
||||||
|
{ code: "pt", label: "Portuguese" },
|
||||||
|
{ code: "tr", label: "Turkish" },
|
||||||
|
{ code: "nl", label: "Dutch" },
|
||||||
|
{ code: "tl", label: "Filipino" },
|
||||||
|
{ code: "id", label: "Indonesian" },
|
||||||
|
{ code: "no", label: "Norwegian" },
|
||||||
|
{ code: "zh-CN", label: "Chinese (Simplified)" },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** # 组件 页脚语言下拉(原站 [gtranslate] 短代码控件 2481635)
|
||||||
|
* 线上渲染为原生 select.gt_selector(202×39、白字、#3E3E3F 底、1px #666 边框,实测 2026-11-02);
|
||||||
|
* 这里用同结构原生 select 承载自建 i18n,选择后写 cookie 刷新。
|
||||||
|
*/
|
||||||
|
function FooterLanguageSelect() {
|
||||||
|
const [lang, setLang] = useState("en");
|
||||||
|
useEffect(() => setLang(getUiLocale()), []);
|
||||||
|
return (
|
||||||
|
<div className="gtranslate_wrapper gt_container">
|
||||||
|
<select
|
||||||
|
className="gt_selector notranslate"
|
||||||
|
aria-label="Select Language"
|
||||||
|
value={lang}
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = e.target.value;
|
||||||
|
if (v && v !== lang) setUiLocale(v);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">Select Language</option>
|
||||||
|
{FOOTER_LANGS.map((l) => (
|
||||||
|
<option key={l.code} value={l.code}>
|
||||||
|
{l.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** # 组件 降级页脚(Elementor 模板缺失时) */
|
||||||
|
function FallbackFooter(props: { categories: FooterCategory[]; menu: MenuNode[] }) {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const groups = props.menu.length > 0 ? props.menu : FALLBACK_GROUPS;
|
const groups = props.menu.length > 0 ? props.menu : FALLBACK_GROUPS;
|
||||||
// 前 4 组为上段列,后 3 组为下段右侧列(与原站 footer 菜单顺序一致)
|
|
||||||
const topGroups = groups.slice(0, 4);
|
const topGroups = groups.slice(0, 4);
|
||||||
const bottomGroups = groups.slice(4, 7);
|
const bottomGroups = groups.slice(4, 7);
|
||||||
return (
|
return (
|
||||||
@@ -43,22 +180,12 @@ export function Footer(props: { categories: FooterCategory[]; menu: MenuNode[] }
|
|||||||
<div className="border-t border-white/12" />
|
<div className="border-t border-white/12" />
|
||||||
<div className="grid grid-cols-1 gap-10 py-[30px] pb-9 min-[900px]:grid-cols-[1.3fr_2fr]">
|
<div className="grid grid-cols-1 gap-10 py-[30px] pb-9 min-[900px]:grid-cols-[1.3fr_2fr]">
|
||||||
<div>
|
<div>
|
||||||
{/* 直接复用原站页脚 logo(同图:wp-content/uploads/2023/01/dusun-iot-logo.png,实测渲染 144.77×42.73px) */}
|
|
||||||
<a href="/" className="inline-block">
|
<a href="/" className="inline-block">
|
||||||
<img src="/uploads/2023/01/dusun-iot-logo.png" width={254} height={75} alt="Zhejiang Dusun Electron Co., Ltd." className="h-auto w-[145px]" />
|
<img src="/uploads/2023/01/dusun-iot-logo.png" width={254} height={75} alt="Zhejiang Dusun Electron Co., Ltd." className="h-auto w-[145px]" />
|
||||||
</a>
|
</a>
|
||||||
<p className="mt-3.5 mb-4 leading-[1.7] text-[#b5b5b6]">{t("footer_intro")}</p>
|
<p className="mt-3.5 mb-4 leading-[1.7] text-[#b5b5b6]">{t("footer_intro")}</p>
|
||||||
<div className="mb-4 flex gap-2.5">
|
<div className="mb-4 flex gap-2.5">
|
||||||
<a href="https://www.linkedin.com/company/dusun-iot" aria-label="LinkedIn" target="_blank" rel="noreferrer" className="inline-flex size-7 items-center justify-center rounded bg-white/8 text-[#dcdcdc] hover:bg-brand hover:text-white">
|
<a href="https://www.linkedin.com/company/dusun-electron-ltd/" aria-label="LinkedIn" target="_blank" rel="noreferrer" className="inline-flex size-7 items-center justify-center rounded bg-white/8 text-[#dcdcdc] hover:bg-brand hover:text-white">
|
||||||
<LinkedinGlyph />
|
|
||||||
</a>
|
|
||||||
<a href="https://www.youtube.com/@dusuniot" aria-label="YouTube" target="_blank" rel="noreferrer" className="inline-flex size-7 items-center justify-center rounded bg-white/8 text-[#dcdcdc] hover:bg-brand hover:text-white">
|
|
||||||
<YoutubeGlyph />
|
|
||||||
</a>
|
|
||||||
<a href="https://www.facebook.com/dusuniot" aria-label="Facebook" target="_blank" rel="noreferrer" className="inline-flex size-7 items-center justify-center rounded bg-white/8 text-[#dcdcdc] hover:bg-brand hover:text-white">
|
|
||||||
<FacebookGlyph />
|
|
||||||
</a>
|
|
||||||
<a href="/contact-us/" aria-label="Contact" className="inline-flex size-7 items-center justify-center rounded bg-white/8 text-[#dcdcdc] hover:bg-brand hover:text-white">
|
|
||||||
<Headset size={15} aria-hidden />
|
<Headset size={15} aria-hidden />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,7 +201,7 @@ export function Footer(props: { categories: FooterCategory[]; menu: MenuNode[] }
|
|||||||
<div className="bg-footer-dark py-3.5 text-center text-xs text-[#a9a9aa]">
|
<div className="bg-footer-dark py-3.5 text-center text-xs text-[#a9a9aa]">
|
||||||
<div className="site-container flex justify-center">
|
<div className="site-container flex justify-center">
|
||||||
<span>
|
<span>
|
||||||
Copyright ©2026 <a href="/" className="text-brand hover:text-[#ff8a2a]">Dusuniot</a>. {t("footer_rights")} |{" "}
|
Copyright ©{new Date().getFullYear()} <a href="/" className="text-brand hover:text-[#ff8a2a]">Dusuniot</a>. {t("footer_rights")} |{" "}
|
||||||
<a href="/sitemap.xml" className="text-brand hover:text-[#ff8a2a]">{t("footer_sitemap")}</a>
|
<a href="/sitemap.xml" className="text-brand hover:text-[#ff8a2a]">{t("footer_sitemap")}</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,9 +210,8 @@ export function Footer(props: { categories: FooterCategory[]; menu: MenuNode[] }
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** # 组件 页脚语言下拉(选择后写 ui_lang cookie 并刷新,与 Header 切换共用 i18n) */
|
/** # 组件 页脚语言下拉(降级页脚用;选择后写 ui_lang cookie 并刷新) */
|
||||||
function FooterLangSelect() {
|
function FooterLangSelect() {
|
||||||
// 语言值客户端挂载后再读 cookie,避免 SSR 水合不一致
|
|
||||||
const [lang, setLang] = useState("en");
|
const [lang, setLang] = useState("en");
|
||||||
useEffect(() => setLang(getUiLocale()), []);
|
useEffect(() => setLang(getUiLocale()), []);
|
||||||
return (
|
return (
|
||||||
@@ -104,7 +230,7 @@ function FooterLangSelect() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** # 组件 页脚菜单组(橙色标题 + 子链接列表;无子项时仅渲染标题链接) */
|
/** # 组件 降级页脚菜单组 */
|
||||||
function FooterGroup({ group }: { group: MenuNode }) {
|
function FooterGroup({ group }: { group: MenuNode }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -131,26 +257,3 @@ function FooterGroup({ group }: { group: MenuNode }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** # 组件 页脚社交图标(lucide 新版无品牌图标,内联 SVG) */
|
|
||||||
function YoutubeGlyph() {
|
|
||||||
return (
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
||||||
<path d="M23 7.6a3 3 0 0 0-2.1-2.2C19 4.9 12 4.9 12 4.9s-7 0-8.9.5A3 3 0 0 0 1 7.6 32 32 0 0 0 .6 12 32 32 0 0 0 1 16.4a3 3 0 0 0 2.1 2.1c1.9.6 8.9.6 8.9.6s7 0 8.9-.6a3 3 0 0 0 2.1-2.1A32 32 0 0 0 23.4 12 32 32 0 0 0 23 7.6ZM9.8 15.1V8.9l5.8 3.1-5.8 3.1Z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
function FacebookGlyph() {
|
|
||||||
return (
|
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
||||||
<path d="M13.5 21v-8h2.7l.4-3.1h-3.1V7.9c0-.9.3-1.5 1.6-1.5h1.6V3.6c-.3 0-1.3-.1-2.4-.1-2.4 0-4 1.4-4 4.1v2.3H7.5V13h2.8v8h3.2Z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
function LinkedinGlyph() {
|
|
||||||
return (
|
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
||||||
<path d="M4.98 3.5A2.5 2.5 0 1 1 5 8.5a2.5 2.5 0 0 1-.02-5ZM3 9.5h4V21H3V9.5Zm6.5 0h3.8v1.6h.1c.5-1 1.8-2 3.7-2 4 0 4.7 2.6 4.7 6V21h-4v-5.2c0-1.2 0-2.9-1.8-2.9s-2.1 1.4-2.1 2.8V21h-4V9.5Z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* # 组件 网站公共布局 Layout
|
* # 组件 网站公共布局 Layout
|
||||||
* 组合 Header + 页面内容 + Footer;样式入口(Tailwind v4 + shadcn/ui)在此引入一次。
|
* 组合 Header + 页面内容 + Footer;样式入口(Tailwind v4 + shadcn/ui)在此引入一次。
|
||||||
* 布局数据(菜单/分类)由各路由 loader 通过 getLayoutData() 取得后传入。
|
* 布局数据(菜单/分类/页脚 Elementor 模板)由各路由 loader 通过 getLayoutData() 取得后传入。
|
||||||
*/
|
*/
|
||||||
import "../../styles/app.css";
|
import "../../styles/app.css";
|
||||||
import { Header } from "./Header";
|
import { Header } from "./Header";
|
||||||
import { Footer, type FooterCategory } from "./Footer";
|
import { Footer, type FooterCategory } from "./Footer";
|
||||||
import { createServerFn } from "@tanstack/react-start";
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
import { getMenuTree, getProductCategoryTree, type MenuNode } from "~/server/data";
|
import { getFooterTemplate, getMenuTree, getProductCategoryTree, type MenuNode } from "~/server/data";
|
||||||
|
import type { ElElement } from "~/components/elementor/Renderer";
|
||||||
|
|
||||||
/** # 类型 布局数据(Header/Footer 所需) */
|
/** # 类型 布局数据(Header/Footer 所需) */
|
||||||
export type LayoutData = {
|
export type LayoutData = {
|
||||||
@@ -17,12 +18,24 @@ export type LayoutData = {
|
|||||||
footerMenu: MenuNode[];
|
footerMenu: MenuNode[];
|
||||||
/** # 成员变量 产品分类树(含计数) **/
|
/** # 成员变量 产品分类树(含计数) **/
|
||||||
categories: FooterCategory[];
|
categories: FooterCategory[];
|
||||||
|
/** # 成员变量 页脚 Elementor 模板文档(原站 elementor_library post 58) **/
|
||||||
|
footerDoc: ElElement[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** # 函数 Server Function:获取布局数据(菜单 + 分类) */
|
/** # 函数 Server Function:获取布局数据(菜单 + 分类 + 页脚模板)
|
||||||
export const getLayoutData = createServerFn({ method: "GET" }).handler(async (): Promise<LayoutData> => {
|
* 返回类型标 any:Elementor 文档的 settings 是递归 unknown(JSON),TanStack 的
|
||||||
const [menu, footerMenu, cats] = await Promise.all([getMenuTree("max_mega_menu_1"), getMenuTree("menu-2"), getProductCategoryTree()]);
|
* 序列化静态校验无法展开递归 Record,会误报不可序列化;运行时就是纯 JSON。
|
||||||
return { menu, footerMenu, categories: cats };
|
*/
|
||||||
|
export const getLayoutData = createServerFn({ method: "GET" }).handler(async (): Promise<any> => {
|
||||||
|
const [menu, footerMenu, cats, footer] = await Promise.all([
|
||||||
|
getMenuTree("max_mega_menu_1"),
|
||||||
|
getMenuTree("menu-2"),
|
||||||
|
getProductCategoryTree(),
|
||||||
|
getFooterTemplate().catch(() => null),
|
||||||
|
]);
|
||||||
|
const el = (footer?.meta as { elementor?: unknown } | null)?.elementor;
|
||||||
|
const footerDoc = Array.isArray(el) ? (el as ElElement[]) : null;
|
||||||
|
return { menu, footerMenu, categories: cats, footerDoc } satisfies LayoutData;
|
||||||
});
|
});
|
||||||
|
|
||||||
/** # 类型 Layout 属性 */
|
/** # 类型 Layout 属性 */
|
||||||
@@ -31,7 +44,7 @@ export function Layout(props: { layout: LayoutData; children: React.ReactNode })
|
|||||||
<>
|
<>
|
||||||
<Header menu={props.layout.menu} />
|
<Header menu={props.layout.menu} />
|
||||||
<main>{props.children}</main>
|
<main>{props.children}</main>
|
||||||
<Footer categories={props.layout.categories} menu={props.layout.footerMenu} />
|
<Footer categories={props.layout.categories} menu={props.layout.footerMenu} doc={props.layout.footerDoc} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,11 @@ export async function getHomePage() {
|
|||||||
return getPublishedContent("home", "page");
|
return getPublishedContent("home", "page");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** # 函数 页脚 Elementor 模板(legacy elementor_library post 58,导入为 type=template) */
|
||||||
|
export async function getFooterTemplate() {
|
||||||
|
return getPublishedContent("footer", "template");
|
||||||
|
}
|
||||||
|
|
||||||
/** # 类型 产品筛选状态:每个筛选字段已选值列表 */
|
/** # 类型 产品筛选状态:每个筛选字段已选值列表 */
|
||||||
export type FilterState = Record<string, string[]>;
|
export type FilterState = Record<string, string[]>;
|
||||||
|
|
||||||
|
|||||||
+116
-2
@@ -39,8 +39,46 @@
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
.fa-linkedin::before { content: "\f08c"; }
|
.fa-linkedin::before { content: "\f08c"; }
|
||||||
|
.fa-linkedin-in::before { content: "\f0e1"; }
|
||||||
.fa-youtube::before { content: "\f167"; }
|
.fa-youtube::before { content: "\f167"; }
|
||||||
.fa-facebook::before { content: "\f09a"; }
|
.fa-facebook::before { content: "\f09a"; }
|
||||||
|
.fa-facebook-f::before { content: "\f39e"; }
|
||||||
|
.fa-discord::before { content: "\f392"; }
|
||||||
|
|
||||||
|
/* # 字体 Font Awesome Solid / Regular(页脚返回顶部图标 fas fa-chevron-up、
|
||||||
|
* 询价入口 far fa-comment-dots;文件取自 legacy elementor 插件库) */
|
||||||
|
@font-face {
|
||||||
|
font-family: "FA Solid";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 900;
|
||||||
|
font-display: block;
|
||||||
|
src: url("/fonts/fa-solid-900.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "FA Regular";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: block;
|
||||||
|
src: url("/fonts/fa-regular-400.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
.fas, .fa-solid {
|
||||||
|
font-family: "FA Solid";
|
||||||
|
font-weight: 900;
|
||||||
|
font-style: normal;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
.far, .fa-regular {
|
||||||
|
font-family: "FA Regular";
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
.fa-chevron-up::before { content: "\f077"; }
|
||||||
|
.fa-comment-dots::before { content: "\f4ad"; }
|
||||||
|
|
||||||
/* # 成员变量 Tailwind 主题令牌(与原 website.css 配色一一对应) **/
|
/* # 成员变量 Tailwind 主题令牌(与原 website.css 配色一一对应) **/
|
||||||
@theme {
|
@theme {
|
||||||
@@ -318,8 +356,14 @@
|
|||||||
* .ele-supplier-dif 注入;文字色 text_color 落 span、悬停主色走 per-widget 规则 */
|
* .ele-supplier-dif 注入;文字色 text_color 落 span、悬停主色走 per-widget 规则 */
|
||||||
.elementor-icon-list-items { list-style: none; margin: 0; padding: 0; }
|
.elementor-icon-list-items { list-style: none; margin: 0; padding: 0; }
|
||||||
.elementor-icon-list-items.elementor-inline-items { display: flex; flex-wrap: wrap; margin: 0 -8px; }
|
.elementor-icon-list-items.elementor-inline-items { display: flex; flex-wrap: wrap; margin: 0 -8px; }
|
||||||
.elementor-icon-list-item { display: flex; align-items: center; margin: 0 8px; }
|
.elementor-icon-list-item { display: flex; align-items: center; }
|
||||||
|
.elementor-icon-list-items.elementor-inline-items > .elementor-icon-list-item { margin: 0 8px; }
|
||||||
.elementor-icon-list-item a { display: flex; align-items: center; text-decoration: none; }
|
.elementor-icon-list-item a { display: flex; align-items: center; text-decoration: none; }
|
||||||
|
/* 图标盒:line-height 0 避免行盒把 li 撑高(页脚社交 20px 图标、图标与文字横排) */
|
||||||
|
.elementor-icon-list-icon { display: inline-flex; align-items: center; line-height: 0; }
|
||||||
|
.elementor-icon-list-icon i { font-style: normal; width: 1.25em; text-align: center; }
|
||||||
|
/* 图标与文字间距(widget-icon-list.min.css):文字左内边距 5px,图标行总宽 30px @20px */
|
||||||
|
.elementor-icon-list-icon + .elementor-icon-list-text { align-self: center; padding-inline-start: 5px; }
|
||||||
|
|
||||||
/* ===== 首页 H12 adv-tabs 生成规则(post-18.css 对应块;1436 实测 2026-11-01)
|
/* ===== 首页 H12 adv-tabs 生成规则(post-18.css 对应块;1436 实测 2026-11-01)
|
||||||
* li 背景 #F4F5F8 + 文字 #333 + 底边 3px(默认同背景色、active/hover 变 #ED6C00,故 li 高 = 24+16*2+3);
|
* li 背景 #F4F5F8 + 文字 #333 + 底边 3px(默认同背景色、active/hover 变 #ED6C00,故 li 高 = 24+16*2+3);
|
||||||
@@ -339,7 +383,7 @@
|
|||||||
* 楼体大图 = 列 bf3d0ba 背景(bottom left + 100% auto,组件内联输出,post-18 生成规则
|
* 楼体大图 = 列 bf3d0ba 背景(bottom left + 100% auto,组件内联输出,post-18 生成规则
|
||||||
* background_size initial → 100% auto);icon-list 悬停主色(globals text_color_hover);
|
* background_size initial → 100% auto);icon-list 悬停主色(globals text_color_hover);
|
||||||
* 描边盒/按钮/列 padding 均设置驱动或 custom_css 注入,无额外规则 */
|
* 描边盒/按钮/列 padding 均设置驱动或 custom_css 注入,无额外规则 */
|
||||||
[data-el="002494b"] .elementor-icon-list-item a:hover .elementor-icon-list-text { color: #ED6C00; }
|
[data-el="002494b"] .elementor-icon-list-item:hover { --el-il-text: #ED6C00; }
|
||||||
|
|
||||||
/* ===== 首页 H13.5 counter 统计条(post-18.css 对应块;1436 实测 2026-11-01)
|
/* ===== 首页 H13.5 counter 统计条(post-18.css 对应块;1436 实测 2026-11-01)
|
||||||
* 六列 16% 图标卡:列 padding 20px 16px 16px 36px + 边框 1px 0 1px 1px #E0E5F9
|
* 六列 16% 图标卡:列 padding 20px 16px 16px 36px + 边框 1px 0 1px 1px #E0E5F9
|
||||||
@@ -458,6 +502,9 @@
|
|||||||
/* ===== Elementor hover 动画(e-animation-shrink.min.css):hover 缩至 90%,0.3s 过渡 */
|
/* ===== Elementor hover 动画(e-animation-shrink.min.css):hover 缩至 90%,0.3s 过渡 */
|
||||||
/* heading 标题链接颜色继承(原站不受全局品牌橙影响,实测 #333/#000) */
|
/* heading 标题链接颜色继承(原站不受全局品牌橙影响,实测 #333/#000) */
|
||||||
.el-heading-title a { color: inherit; }
|
.el-heading-title a { color: inherit; }
|
||||||
|
/* Elementor 基础规则 .elementor-heading-title{margin:0}(页脚 Policy 为 <p>,
|
||||||
|
* 不加则 UA 段落下边距把列表推低 30px) */
|
||||||
|
.el-heading-title { margin: 0; }
|
||||||
/* kit 全局链接排版(post-15.css .elementor-kit-15 a):链接默认 Roboto 400。
|
/* kit 全局链接排版(post-15.css .elementor-kit-15 a):链接默认 Roboto 400。
|
||||||
* 取证:tab 卡片 .card 为 <a>,kit 字体使其内部 ul/li 用 Roboto 渲染(文字 273.6px),
|
* 取证:tab 卡片 .card 为 <a>,kit 字体使其内部 ul/li 用 Roboto 渲染(文字 273.6px),
|
||||||
* 否则被卡片内嵌样式的 Arial 截住(261.9px),第 4 条规格多折一行(H9 body 高 +21.6)。
|
* 否则被卡片内嵌样式的 Arial 截住(261.9px),第 4 条规格多折一行(H9 body 高 +21.6)。
|
||||||
@@ -666,8 +713,75 @@
|
|||||||
[data-el="fd97340"] .elementor-tab-title:after,
|
[data-el="fd97340"] .elementor-tab-title:after,
|
||||||
[data-el="fd97340"] .elementor-tab-content { border-width: 0; }
|
[data-el="fd97340"] .elementor-tab-content { border-width: 0; }
|
||||||
[data-el="fd97340"] .elementor-tab-title.elementor-active { color: #ED6C00; }
|
[data-el="fd97340"] .elementor-tab-title.elementor-active { color: #ED6C00; }
|
||||||
|
|
||||||
|
/* ===== 页脚 Elementor 模板 post 58(线上 1436 实测 + post-58.css,2026-11-02)
|
||||||
|
* 结构:46d7afa 四列链接(桌面) / d834160(html_tag=footer) 返回顶部 + 分隔线 +
|
||||||
|
* 28c7f0b8 品牌区三列 + a428bb4 版权条。以下为「仅存生成 CSS」与 DOM 语义补丁 */
|
||||||
|
.elementor-location-footer a { text-decoration: none; font-weight: inherit; }
|
||||||
|
/* logo:设置驱动 img width 57%(内联),a 收缩为内容宽 → 254×75 的 57% = 144.8×42.7 */
|
||||||
|
[data-el="211f98c"] > a { display: inline-block; }
|
||||||
|
[data-el="211f98c"] img { height: auto; }
|
||||||
|
/* 简介段落:原站 p 下外边距 0.9rem(实测 14.4px;三行文字高 77.4) */
|
||||||
|
[data-el="42365c0"] p { margin: 0 0 0.9rem; }
|
||||||
|
/* 链接列悬停文字变 secondary #FFA11B(post-58.css 生成规则;变量覆盖内联色) */
|
||||||
|
[data-el="d5fc3c1"] .elementor-icon-list-item:hover,
|
||||||
|
[data-el="cb239ea"] .elementor-icon-list-item:hover,
|
||||||
|
[data-el="115705e"] .elementor-icon-list-item:hover,
|
||||||
|
[data-el="f517f88"] .elementor-icon-list-item:hover,
|
||||||
|
[data-el="9cf608f"] .elementor-icon-list-item:hover,
|
||||||
|
[data-el="6ef07ee0"] .elementor-icon-list-item:hover,
|
||||||
|
[data-el="6c850ad1"] .elementor-icon-list-item:hover { --el-il-text: #FFA11B; }
|
||||||
|
/* 社交图标悬停变 secondary(白色 → #FFA11B) */
|
||||||
|
[data-el="405d251"] .elementor-icon-list-item:hover { --el-il-icon: #FFA11B; }
|
||||||
|
/* 返回顶部圆钮:悬停背景 primary #ED6C00(内联底色需 !important 覆盖) */
|
||||||
|
[data-el="5ffe573"] .elementor-icon:hover { background-color: #ED6C00 !important; color: #ffffff; }
|
||||||
|
/* gtranslate 语言下拉(线上 select.gt_selector 202×39、白字、#3E3E3F 底、1px #666,实测) */
|
||||||
|
[data-el="2481635"] select.gt_selector {
|
||||||
|
width: 202px; height: 39px; padding: 8px 16px; font-size: 16px; line-height: 1.2;
|
||||||
|
color: #ffffff; background-color: #3E3E3F; border: 1px solid #666666; border-radius: 0;
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
[data-el="2481635"] select.gt_selector { width: auto; max-width: 100%; }
|
||||||
|
}
|
||||||
|
/* 版权条两列:左列 populated 宽 auto(margin-right 2 扣除语义,实测 568 而非 570) */
|
||||||
|
[data-el="89e79b8"] > div { width: auto !important; }
|
||||||
|
/* icon widget 字形盒:Elementor .elementor-icon i 与 svg 同宽 1em(24px → 盒 48×48) */
|
||||||
|
.elementor-icon i { width: 1em; text-align: center; }
|
||||||
|
/* 版权条文字:白 12px 100(设置驱动),链接同为白色(无橙色) */
|
||||||
|
[data-el="a428bb4"] a { color: #ffffff; }
|
||||||
|
/* 移动端折叠菜单([maxmegamenu location=menu-2],桌面/平板由 elementor-hidden-* 隐藏) */
|
||||||
|
.mega-menu-wrap .mega-menu-toggle { display: flex; justify-content: center; align-items: center; min-height: 40px; }
|
||||||
|
.mega-menu-wrap .mega-toggle-animated-box {
|
||||||
|
position: relative; display: inline-block; width: 40px; height: 26px;
|
||||||
|
cursor: pointer; background: transparent; border: 0;
|
||||||
|
}
|
||||||
|
.mega-menu-wrap .mega-toggle-animated-inner,
|
||||||
|
.mega-menu-wrap .mega-toggle-animated-inner::before,
|
||||||
|
.mega-menu-wrap .mega-toggle-animated-inner::after {
|
||||||
|
position: absolute; left: 4px; right: 4px; height: 2px; background: #ffffff; content: "";
|
||||||
|
transition: transform .2s ease;
|
||||||
|
}
|
||||||
|
.mega-menu-wrap .mega-toggle-animated-inner { top: 12px; }
|
||||||
|
.mega-menu-wrap .mega-toggle-animated-inner::before { top: -8px; }
|
||||||
|
.mega-menu-wrap .mega-toggle-animated-inner::after { top: 8px; }
|
||||||
|
.mega-menu-wrap ul.mega-menu { list-style: none; margin: 0; padding: 0; }
|
||||||
|
.mega-menu-wrap ul.mega-menu li { border-bottom: 1px solid rgba(255, 255, 255, 0.12); }
|
||||||
|
.mega-menu-wrap .mega-menu-link { display: block; padding: 10px 0; color: #D9D9D9; font-size: 14px; }
|
||||||
|
.mega-menu-wrap .mega-sub-menu { padding-left: 16px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.ele-ban-title div { font-size: 30px; }
|
.ele-ban-title div { font-size: 30px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* # 成员变量 Elementor 响应式可见性(hide_desktop/tablet/mobile → 对应类)
|
||||||
|
* 断点同 Elementor:desktop >1024、tablet 768–1024、mobile ≤767(页脚移动菜单/隐藏节) **/
|
||||||
|
@media (min-width: 1025px) {
|
||||||
|
.elementor-hidden-desktop { display: none !important; }
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) and (max-width: 1024px) {
|
||||||
|
.elementor-hidden-tablet { display: none !important; }
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.elementor-hidden-mobile { display: none !important; }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user