diff --git a/docs/sections-inventory.md b/docs/sections-inventory.md index 626e769a3..48985f746 100644 --- a/docs/sections-inventory.md +++ b/docs/sections-inventory.md @@ -26,7 +26,7 @@ | # | 版块 | 状态 | 组件 | |---|------|------|------| | 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` | ## 三、首页版块清单(自上而下,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 通用类) | | 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 篇有关联)) | -| 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 > (no 0/narrow 5/default 10/extended 15/wide 20/wider 30)、widget-wrap flex wrap + 子元素 width:100% > + `_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` 或按线上真实状态换算,勿把"未初始化"当基线。 ## 七、已知遗留(非当前版块范围,后续版块收口) diff --git a/package.json b/package.json index f42b44624..3f2ac666c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dusuniot-rebuild", - "version": "0.10.18", + "version": "0.10.19", "private": true, "type": "module", "scripts": { diff --git a/public/fonts/fa-regular-400.woff2 b/public/fonts/fa-regular-400.woff2 new file mode 100644 index 000000000..9df490e8c Binary files /dev/null and b/public/fonts/fa-regular-400.woff2 differ diff --git a/public/fonts/fa-solid-900.woff2 b/public/fonts/fa-solid-900.woff2 new file mode 100644 index 000000000..dc52d954d Binary files /dev/null and b/public/fonts/fa-solid-900.woff2 differ diff --git a/scripts/import-footer-template.ts b/scripts/import-footer-template.ts new file mode 100644 index 000000000..28cb3cdac --- /dev/null +++ b/scripts/import-footer-template.ts @@ -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[]> { + 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[]; +} + +/** # 函数 深度查找指定 id 的元素节点 */ +function findElement(els: Record[], id: string): Record | null { + for (const el of els) { + if (el.id === id) return el; + const children = el.elements as Record[] | 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 { + 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[]) || []; +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) || {}; + 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) || {}; + 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(); diff --git a/scripts/repair-content-terms.ts b/scripts/repair-content-terms.ts index a0c7ac9f4..6ceca15d2 100644 --- a/scripts/repair-content-terms.ts +++ b/scripts/repair-content-terms.ts @@ -23,13 +23,13 @@ console.log("dump 关系对:", pairs.size); // 2) 本库映射表:legacyId → id const contents = await db`SELECT id, legacy_id FROM contents WHERE legacy_id IS NOT NULL`; -const contentMap = new Map(contents.map((r) => [Number(r.legacy_id), Number(r.id)])); +const contentMap = new Map(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 catMap = new Map(cats.map((r) => [Number(r.legacy_id), Number(r.id)])); +const catMap = new Map(cats.map((r: { id: number; legacy_id: number }) => [Number(r.legacy_id), Number(r.id)])); // 3) 已有关联集合(幂等) 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) 补插 let inserted = 0; diff --git a/src/components/elementor/Renderer.tsx b/src/components/elementor/Renderer.tsx index e7cea80d1..160c26e27 100644 --- a/src/components/elementor/Renderer.tsx +++ b/src/components/elementor/Renderer.tsx @@ -59,6 +59,18 @@ function stripStructuralTags(html: string): string { 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 值) * React 19 水合按 CSSOM 归一化后的字符串比对样式,"4rem 0rem 4rem 0rem" * 会被浏览器归一为 "4rem 0rem",不折叠会导致水合不匹配。 @@ -190,6 +202,29 @@ function resolveGlobalsStr(v: string): string { return v; } +/** # 函数 响应式可见性类名(Elementor hide_* 设置 → elementor-hidden-*,配 app.css 媒体查询) + * 参数 settings 元素设置 + * 返回值 空格分隔的类名(无隐藏设置则空串) + */ +function visibilityClass(settings: Record): 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): "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 的背景样式(纯色 / 图片) */ function sectionBgStyle(settings: Record): CSSProperties { const style: CSSProperties = {}; @@ -235,19 +270,26 @@ function normalizeDoc(doc: ElDocument): ElElement[] { /** # 成员变量 posts widget 文章数据上下文(loader 预取,经 widget id 分发) **/ const PostsDataContext = createContext>({}); -/** # 组件 Elementor 渲染器入口 */ -export function ElementorRenderer({ doc, postsData }: { doc: ElDocument; postsData?: Record }) { +/** # 成员变量 WordPress 短代码渲染上下文(widget id → 预渲染节点;页脚 menu-2/gtranslate) **/ +const ShortcodesContext = createContext>({}); + +/** # 组件 Elementor 渲染器入口 + * 参数 shortcodes 短代码 widget 的预渲染内容(键为 widget id;缺省渲染为空) + */ +export function ElementorRenderer({ doc, postsData, shortcodes }: { doc: ElDocument; postsData?: Record; shortcodes?: Record }) { const elements = normalizeDoc(doc); if (!elements.length) return null; const css = collectCustomCss(elements) + collectResponsiveCss(elements); return ( -
- {css ?