diff --git a/docs/sections-inventory.md b/docs/sections-inventory.md index ef2de5f43..71919ee4a 100644 --- a/docs/sections-inventory.md +++ b/docs/sections-inventory.md @@ -67,16 +67,30 @@ ShopFilters/ShopProducts 注入。筛选 URL 用查询参数(原站 WPC 为 /shop/type-som/ 美化链接), 术语顺序/计数来自导入数据(term taxonomy 顺序 + product_attribute_map 计数)。 -### 1b. 落地页 /landing-pages/$slug(智能网关页已校对,v0.10.22 进行中) +### 1b. 落地页 /landing-pages/$slug(智能网关页已校对,v0.10.23 ✅ 桌面全对齐) - 原站落地页为 post(如 smart-gateways legacy 6509)Elementor 直出,经 catch-all 路由渲染。 -- smart-gateways 15 节中 10 节已与原站逐项对齐(541ea84/49dcb11/dcfbe08/de5742a/9b87079/ - 7c15af8/68babbb/028b4bc/5745b4f/2fee235,误差 ≤8px),期间修复:EAEL info-box - 默认标题外距/图片 block/标题 margin 设置、image `space` 宽度、列 populated auto 语义、 - 按钮图标(star/fire)、HTML 块 ul 默认外距。 -- 待办:8e90920 EAEL 高级标签页(原站 headless JS 未初始化,需按真实交互态校对)、 - 391a1ee toggle 手风琴(需换 Elementor toggle DOM + 默认全收起 + 28/58px 标题样式)、 - a36c301 `[elementor-template id="27143"]` 内嵌模板(含 posts.cards,需模板导入 + - 短代码渲染 + posts 预取)。 +- smart-gateways 15 节桌面端全部与原站对齐(v0.10.23 复测:541ea84/49dcb11/dcfbe08/f9e27ce/ + eb3462a/de5742a/9b87079/7c15af8/68babbb/028b4bc/5745b4f/2fee235/391a1ee/a36c301 逐节 + 高度 ≤1px;8e90920 按真实 JS 激活态 822.4 = 原站注入 active 后实测 822.4;页脚 590.5)。 +- v0.10.23 修复: + - 391a1ee toggle 换成 Elementor 原生 DOM(默认全收起、h3 28/1/700、标题 1.125rem、 + 图标 caret-down/up 左浮动 1em+12px、item 间距 4、内容 .875rem、FAQPage JSON-LD), + 桌面节高 496 / 移动 536 与原站全等;标题移动端响应式字号(1.5rem)补进 + collectResponsiveCss。 + - a36c301 内嵌模板:新增 scripts/import-embedded-templates.ts 导入 elementor_library + post 27143(slug=template-27143);catch-all 路由扫描 `[elementor-template id="N"]` + 短代码加载模板并注入渲染,同时预取 posts widget(多分类并集 53/54/703,发布时间倒序)。 + - posts widget 按 widget-posts.min.css cards 皮肤重写:meta 移到 text 之后(border-top + + margin-top:auto)、badge 仅在有缩略图时渲染且绝对右上、缩略图 img 静态宽度 calc(100%+1px)、 + 比率/内距/字号/边框全部按设置内联;桌面 H18 卡片图高 220.6/187.9(原站 220.4/188.1)。 + - 列 populated 宽度:新增 06632bb/3361c18/3317056/5938d26 的 auto 扣除规则(570 vs 580)。 + - info-box:p 外距/颜色(eael_infobox_content_margin/color)、图标型 info-box + (eael_infobox_icon_new/icon_size,fa-check-circle 码位)、标题行高; + - 按钮:图标型按钮 wrapper 2px 行盒(原站 inline-block 基线导致 28px 高)。 +- 已知数据差异:内嵌模板 posts 取本地快照(dump 2026-10-29),原站已新增 161855/161748 + 两篇(11-02/10-30),故卡片内容与原站当前列表不同;版式/卡片尺寸一致。 +- 移动端待细化:EAEL tabs 原站 headless 未初始化无法直接取基线;posts 图片经由 + srcset/medium 变体,个别卡片高度 ±5px。 ### 2. 方案页 - S1 `/solution/` 列表入口页 @@ -176,3 +190,6 @@ cookie-policy:19 段纯文本。 | 2 | 移动端 H1 ticker 高度差 ~14px(原站 117,本地 131.2) | H1 ticker 移动端 | 中 | | 3 | 全局 `a:hover` 为 --brand-dark(#c85600),kit 全局 secondary 是 #FFA11B | G1/全局样式 | 低 | | 4 | PG 连接曾被 stale vite dev 进程(3311 端口)泄漏占满(290 连接),已清理;重复发生时检查残留 dev 进程 | 环境 | 记录 | +| 5 | 内嵌模板文章列表取 dump 快照,原站新增文章(161855/161748)尚未导入,卡片内容不同步(版式一致) | 内容数据 | 中 | +| 6 | posts 卡片移动端个别卡高 ±5px(原站 srcset 选 768/medium 变体,本地用原图;宽高比略有差异) | posts widget | 低 | +| 7 | dev 脚本须用 `bun --bun vite dev`:纯 node 下 Vite module-runner 无法解析 Bun 内置模块;vite.config 已把 "bun"/node:* 标记为 ssr builtins | 环境 | 记录 | diff --git a/package.json b/package.json index 7860146cd..2dad7c4fb 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "dusuniot-rebuild", - "version": "0.10.22", + "version": "0.10.23", "private": true, "type": "module", "scripts": { - "dev": "vite dev --port 3311", + "dev": "bun --bun vite dev --port 3311", "build": "vite build", "start": "bun scripts/prod-server.ts", "db:generate": "drizzle-kit generate", diff --git a/scripts/import-embedded-templates.ts b/scripts/import-embedded-templates.ts new file mode 100644 index 000000000..314724231 --- /dev/null +++ b/scripts/import-embedded-templates.ts @@ -0,0 +1,56 @@ +/** + * # 脚本 导入内嵌 Elementor 模板([elementor-template id="N"] 短代码引用的 post) + * 背景:落地页/产品页里的 shortcode widget 会引用 elementor_library 模板 + * (如 smart-gateways 的 a36c301 → [elementor-template id="27143"],含 + * “Dusun Updates” 标题 + posts 卡片列表)。原导入管线未迁移 elementor_library, + * 这些模板在本地缺失,导致内嵌区块为空。 + * 数据源:legacy/dusuniot-db-20261029.sql 的 (meta_id, , '_elementor_data', …)。 + * 写入 contents:type=template,slug=`template-`,legacy_id=, + * 供路由按 legacy_id 查询后用 ElementorRenderer 递归渲染。 + * 运行:bun scripts/import-embedded-templates.ts [postId...] + */ +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"); + +/** # 成员变量 需要导入的模板 post id(默认:落地页 smart-gateways 内嵌的 27143) **/ +const ids = process.argv.slice(2).map(Number).filter((n) => Number.isFinite(n)); +const targets = ids.length ? ids : [27143]; + +const text = await Bun.file(DUMP).text(); + +/** # 函数 从 dump 提取指定 post 的 _elementor_data(同 import-shop-template 的解析方式) */ +function extract(postId: number): Record[] | null { + const re = new RegExp("\\(\\d+," + postId + ",'_elementor_data','((?:[^'\\\\]|\\\\.)*)'\\)"); + const m = text.match(re); + if (!m) return null; + const json = m[1]!.replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\\\/g, "\\"); + const data = JSON.parse(json); + return Array.isArray(data) ? (data as Record[]) : null; +} + +for (const id of targets) { + const elements = extract(id); + if (!elements) { + console.error(`post ${id} 未找到 _elementor_data,跳过`); + continue; + } + const slug = `template-${id}`; + const title = `Elementor Template ${id}`; + const meta = JSON.stringify({ elementor: elements }); + const existing = await db`SELECT id FROM contents WHERE legacy_id=${id} OR (slug=${slug} AND type='template')`; + if (existing.length) { + await db`UPDATE contents SET meta=${meta}::text::jsonb, status='published', updated_at=now() WHERE id=${existing[0]!.id}`; + console.log(`已更新 ${slug},contents.id = ${existing[0]!.id}`); + } else { + const inserted = await db` + INSERT INTO contents (type, slug, title, status, meta, legacy_id, published_at) + VALUES ('template', ${slug}, ${title}, 'published', ${meta}::text::jsonb, ${id}, now()) + RETURNING id`; + console.log(`已插入 ${slug},contents.id = ${inserted[0]!.id}`); + } + const top = elements.map((el) => String(el.id)).join(", "); + console.log(` post ${id} 顶层节: ${top}`); +} +await db.end(); diff --git a/scripts/import-footer-template.ts b/scripts/import-footer-template.ts index 28cb3cdac..9fbb12ff7 100644 --- a/scripts/import-footer-template.ts +++ b/scripts/import-footer-template.ts @@ -108,12 +108,12 @@ 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}`; + await db`UPDATE contents SET meta=${meta}::text::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()) + VALUES ('template', 'footer', 'Footer', 'published', ${meta}::text::jsonb, 58, now()) RETURNING id`; console.log("已插入 footer 模板,contents.id =", inserted[0]!.id); } diff --git a/scripts/import-shop-template.ts b/scripts/import-shop-template.ts index 9d0a6010e..292865114 100644 --- a/scripts/import-shop-template.ts +++ b/scripts/import-shop-template.ts @@ -29,12 +29,12 @@ const elements = await extractTemplate(1398); const meta = JSON.stringify({ elementor: elements }); const existing = await db`SELECT id FROM contents WHERE slug='shop-archive' AND type='template'`; if (existing.length) { - await db`UPDATE contents SET meta=${meta}::jsonb, status='published', updated_at=now() WHERE id=${existing[0]!.id}`; + await db`UPDATE contents SET meta=${meta}::text::jsonb, status='published', updated_at=now() WHERE id=${existing[0]!.id}`; console.log("已更新 shop-archive 模板,contents.id =", existing[0]!.id); } else { const inserted = await db` INSERT INTO contents (type, slug, title, status, meta, legacy_id, published_at) - VALUES ('template', 'shop-archive', 'Shop Archive', 'published', ${meta}::jsonb, 1398, now()) + VALUES ('template', 'shop-archive', 'Shop Archive', 'published', ${meta}::text::jsonb, 1398, now()) RETURNING id`; console.log("已插入 shop-archive 模板,contents.id =", inserted[0]!.id); } diff --git a/src/components/elementor/Renderer.tsx b/src/components/elementor/Renderer.tsx index 6bf6919d7..d483a7c78 100644 --- a/src/components/elementor/Renderer.tsx +++ b/src/components/elementor/Renderer.tsx @@ -375,6 +375,14 @@ function collectResponsiveCss(elements: ElElement[]): string { const iw = sizeCss(st.width_mobile); if (iw) css += `@media (max-width:767px){${sel} img{width:${iw} !important;}}\n`; } + // 标题响应式字号(typography_font_size_tablet/mobile;落地页 e8c891a + // 桌面 38px 来自主题,移动端设置 1.5rem=24px,原站实测 24px/24) + if (s(el.widgetType) === "heading") { + const fsT = sizeCss(st.typography_font_size_tablet); + const fsM = sizeCss(st.typography_font_size_mobile); + if (fsT) css += `@media (max-width:1024px){${sel} .el-heading-title{font-size:${fsT} !important;}}\n`; + if (fsM) css += `@media (max-width:767px){${sel} .el-heading-title{font-size:${fsM} !important;}}\n`; + } } // 列响应式宽度(_inline_size_tablet / _inline_size_mobile;页脚内层四列 43%/100%) const tw = st._inline_size_tablet; @@ -731,6 +739,7 @@ function WidgetNodeInner({ el }: { el: ElElement }) { ); case "toggle": + return ; case "accordion": return ; case "tabs": @@ -961,26 +970,45 @@ function TickerWidget({ settings }: { settings: Record }) { } /** # 组件 posts widget(Elementor Pro 卡片皮肤:文章/产品卡片网格) */ -/** # 组件 posts widget(Elementor skin cards 忠实 DOM,H18 实测 2026-11-01) +/** # 组件 posts widget(Elementor skin cards 忠实 DOM) + * DOM/样式对齐 widget-posts.min.css 的 cards 皮肤(原站 H18 与模板 27143 实测 2026-11-02): * .elementor-posts-container.elementor-posts.elementor-posts--skin-cards.elementor-grid - * (grid 列数 cards_columns、gap 16/10=row/column)> article.elementor-post > .elementor-post__card - * (flex 纵向、边框/底色/圆角 3 由设置)> [缩略图链接(比率 0.66 内宽、badge 绝对右上)] - * + .elementor-post__text(padding 12) > .elementor-post__title(div,fs18/400,#000) - * + .elementor-post__meta-data(日期 F j, Y,#ADADAD) + .elementor-post__excerpt(18 词,#767676,底距 44); - * 缩略图 thumbnail=none 时省略(右列卡 74 高纯文本);文章数据经 PostsDataContext 预取。 + * > article.elementor-post > .elementor-post__card(纵向 flex、白底、1px 边、圆角 3) + * > [a.__thumbnail__link > .__thumbnail(padding-bottom 比率) > img] + * + .__badge(仅缩略图存在时,绝对右上) + * + .__text(flex-grow)> .__title + .__excerpt + * + .__meta-data(text 之后,border-top + margin-top:auto,日期 F j, Y) + * 图片宽度 calc(100%+1px) 且为静态流(高度按原图比例,原站即如此); + * 缩略图比率来自 cards_item_ratio(默认 0.66)/ cards_item_ratio_mobile。 + * 文章数据经 PostsDataContext 预取(server 端 loadPostsWidgetsData)。 */ function PostsWidget({ el, settings }: { el: ElElement; settings: Record }) { const allPosts = useContext(PostsDataContext); const posts = (el.id && allPosts[el.id]) || []; if (!posts.length) return null; + const numVal = (v: unknown): number | undefined => { + const n = typeof v === "number" ? v : Number(s(v)); + return Number.isFinite(n) && n > 0 ? n : undefined; + }; const showThumb = s(settings.cards_thumbnail) !== "none"; - const cols = Number(s(settings.cards_columns)) || 1; + const cols = numVal(settings.cards_columns) ?? 1; + const colsT = numVal(settings.cards_columns_tablet) ?? Math.min(cols, 2); + const colsM = numVal(settings.cards_columns_mobile) ?? 1; const rowGap = sizeCss(settings.cards_row_gap) || "16px"; const colGap = sizeCss(settings.cards_column_gap) || "10px"; const textPad = sizeCss(settings.cards_card_padding) || "12px"; const titleSpacing = sizeCss(settings.cards_title_spacing) || "16px"; const excSpacing = sizeCss(settings.cards_excerpt_spacing) || "44px"; - const excerptColor = s(settings.cards_excerpt_color) || undefined; + const excerptColor = s(settings.cards_excerpt_color) || "#777"; + const excerptSize = sizeCss(settings.cards_excerpt_typography_font_size) || "14px"; + const titleSize = sizeCss(settings.cards_title_typography_font_size) || "18px"; + const metaSize = sizeCss(settings.cards_meta_typography_font_size) || "12px"; + const metaWeight = s(settings.cards_meta_typography_font_weight) || "400"; + const cardBorderColor = s(settings.cards_card_border_color) || "#69727d"; + const cardBorderWidth = sizeCss(settings.cards_card_border_width) || "1px"; + // 缩略图比率:desktop 默认 0.66,mobile 默认继承(27143/ H18 均为 0.6/0.67) + const ratio = numVal((settings.cards_item_ratio as { size?: unknown } | undefined)?.size) ?? 0.66; + const ratioM = numVal((settings.cards_item_ratio_mobile as { size?: unknown } | undefined)?.size) ?? ratio; const titleTag = (["h1", "h2", "h3", "h4", "h5", "h6", "div", "span", "p"].includes(s(settings.cards_title_tag)) ? s(settings.cards_title_tag) : "h3") as "h3"; const TitleTag = titleTag; // 徽章/日期/摘要开关:switcher 键缺省 = 开(左卡无键但有徽章/摘要的实测语义), @@ -995,44 +1023,57 @@ function PostsWidget({ el, settings }: { el: ElElement; settings: Record {posts.map((p) => { // 链接前缀按主分类:News → /news/,其余 → /blog/(原站 News 文章走 /news//) const href = (p.categoryName === "News" ? "/news/" : "/blog/") + p.slug + "/"; + const hasThumb = showThumb && !!p.imageUrl; return (
-
- {showThumb && p.imageUrl ? ( +
+ {hasThumb ? ( -
- {p.title} +
+ {p.title}
- {showBadge && p.categoryName ? ( - {p.categoryName} - ) : null}
) : null} -
- + {hasThumb && showBadge && p.categoryName ? ( +
{p.categoryName}
+ ) : null} +
+ {p.title} - {showMeta && p.publishedAt ? ( -
- {fmtDate(p.publishedAt)} + {showExcerpt ? ( +
+

) : null} - {showExcerpt ? ( -

- ) : null}

+ {showMeta && p.publishedAt ? ( +
+ {fmtDate(p.publishedAt)} +
+ ) : null}
); @@ -1147,13 +1188,16 @@ function ButtonWidget({ el, settings }: { el: ElElement; settings: Record +
{hoverBg || hoverColor ? ( ) : null} {content} + {/* 原站 wrapper 内含空白文本节点:inline-block 按钮所在行盒会叠加上正文 strut, + 父级高度 28(按钮 26 + 2px descender)。JSX 需显式补一个空格复现。 */} + {" "}
); } @@ -1287,6 +1331,8 @@ function EaelInfoBoxWidget({ settings }: { settings: Record }) const title = s(settings.eael_infobox_title) || s(settings.title_text) || s(settings.title); const desc = s(settings.eael_infobox_text) || s(settings.description_text) || s(settings.description); const img = settings.eael_infobox_image as { url?: string; alt?: string } | undefined; + const icon = settings.eael_infobox_icon_new as { value?: string; library?: string } | undefined; + const iconSize = sizeCss(settings.eael_infobox_icon_size) || "26px"; if (!title && !desc && !img?.url) return null; const tag = s(settings.eael_infobox_title_tag); const Tag = (["h1", "h2", "h3", "h4", "h5", "h6", "div"].includes(tag) ? tag : "div") as "div"; @@ -1305,12 +1351,38 @@ function EaelInfoBoxWidget({ settings }: { settings: Record }) const tm = paddingCss(tmRaw); if (tm) titleStyle.margin = tm; } + // 内容段落外距/颜色(EAEL 生成规则:eael_infobox_content_margin → p margin, + // eael_infobox_content_color → p color;570df98 实测 margin 10px 0 0、#969696) + const cmRaw = settings.eael_infobox_content_margin as Record | undefined; + const cmSet = !!cmRaw && ["top", "right", "bottom", "left"].some((k) => cmRaw[k] !== "" && cmRaw[k] != null); + const contentVars: CSSProperties = {}; + if (cmSet) { + const cm = paddingCss(cmRaw); + if (cm) { + const [t, r2, b, l] = cm.split(" "); + Object.assign(contentVars, { + "--ib-p-top": t ?? "0", + "--ib-p-right": r2 ?? "0", + "--ib-p-bottom": b ?? "0", + "--ib-p-left": l ?? "0", + }); + } + } + const contentColor = s(settings.eael_infobox_content_color); + if (contentColor) Object.assign(contentVars, { "--ib-p-color": contentColor }); return (
{img?.url ? (
{s(img.alt)}
+ ) : icon?.value ? ( + // 图标型 info-box(df5b95b:fas fa-check-circle 26px,左侧图标 + 标题) +
+
+