v0.10.23: 落地页收口(toggle/内嵌模板 27143/posts cards/info-box)+ dev 3311 修复

This commit is contained in:
bigcode
2026-11-02 16:15:14 +08:00
parent 07774c715f
commit 864a737c58
13 changed files with 605 additions and 83 deletions
+26 -9
View File
@@ -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 | 环境 | 记录 |
+2 -2
View File
@@ -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",
+56
View File
@@ -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, <postId>, '_elementor_data', …)。
* 写入 contents:type=template,slug=`template-<postId>`,legacy_id=<postId>,
* 供路由按 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<string, unknown>[] | 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<string, unknown>[]) : 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();
+2 -2
View File
@@ -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);
}
+2 -2
View File
@@ -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);
}
+218 -33
View File
@@ -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 }) {
</div>
);
case "toggle":
return <ToggleWidget el={el} settings={settings} />;
case "accordion":
return <AccordionWidget settings={settings} />;
case "tabs":
@@ -961,26 +970,45 @@ function TickerWidget({ settings }: { settings: Record<string, unknown> }) {
}
/** # 组件 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<string, unknown> }) {
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<string,
if (isNaN(dt.getTime())) return null;
return dt.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
};
const thumbVars = { "--ep-ratio": String(ratio), "--ep-ratio-m": String(ratioM) } as CSSProperties;
return (
<div
className="elementor-posts-container elementor-posts elementor-posts--skin-cards elementor-grid"
style={{ display: "grid", gridTemplateColumns: `repeat(${cols}, 1fr)`, gap: `${rowGap} ${colGap}` }}
className="elementor-posts-container elementor-posts elementor-posts--skin-cards elementor-posts--thumbnail-top elementor-posts__hover-gradient elementor-grid"
style={{
display: "grid",
gap: `${rowGap} ${colGap}`,
// 列数随断点(Elementor grid-4 / tablet-2 / mobile-1;值由 app.css 媒体查询读取)
["--ep-cols" as string]: String(cols),
["--ep-cols-t" as string]: String(colsT),
["--ep-cols-m" as string]: String(colsM),
}}
>
{posts.map((p) => {
// 链接前缀按主分类:News → /news/,其余 → /blog/(原站 News 文章走 /news/<slug>/)
const href = (p.categoryName === "News" ? "/news/" : "/blog/") + p.slug + "/";
const hasThumb = showThumb && !!p.imageUrl;
return (
<article key={p.slug} className="elementor-post elementor-grid-item post type-post status-publish format-standard has-post-thumbnail">
<div className="elementor-post__card">
{showThumb && p.imageUrl ? (
<div className="elementor-post__card" style={{ border: `${cardBorderWidth} solid ${cardBorderColor}` }}>
{hasThumb ? (
<a className="elementor-post__thumbnail__link" href={href} tabIndex={-1}>
<div className="elementor-post__thumbnail">
<img src={localSiteUrl(p.imageUrl)} alt={p.title} loading="lazy" />
<div className="elementor-post__thumbnail" style={thumbVars}>
<img src={localSiteUrl(p.imageUrl!)} alt={p.title} loading="lazy" />
</div>
{showBadge && p.categoryName ? (
<span className="elementor-post__badge">{p.categoryName}</span>
) : null}
</a>
) : null}
<div className="elementor-post__text">
<TitleTag className="elementor-post__title" style={{ marginBottom: titleSpacing }}>
{hasThumb && showBadge && p.categoryName ? (
<div className="elementor-post__badge">{p.categoryName}</div>
) : null}
<div className="elementor-post__text" style={{ padding: `0 ${textPad}` }}>
<TitleTag className="elementor-post__title" style={{ fontSize: titleSize, marginBottom: titleSpacing }}>
<a href={href}>{p.title}</a>
</TitleTag>
{showMeta && p.publishedAt ? (
<div className="elementor-post__meta-data">
<span className="elementor-post-date">{fmtDate(p.publishedAt)}</span>
{showExcerpt ? (
<div className="elementor-post__excerpt" style={{ marginBottom: excSpacing }}>
<p
style={{ color: excerptColor, fontSize: excerptSize }}
dangerouslySetInnerHTML={{ __html: postExcerpt(p) }}
/>
</div>
) : null}
{showExcerpt ? (
<p
className="elementor-post__excerpt"
style={{ color: excerptColor, marginBottom: excSpacing }}
dangerouslySetInnerHTML={{ __html: postExcerpt(p) }}
/>
) : null}
</div>
{showMeta && p.publishedAt ? (
<div
className="elementor-post__meta-data"
style={{ padding: `10px ${textPad}`, fontSize: metaSize, fontWeight: metaWeight }}
>
<span className="elementor-post-date">{fmtDate(p.publishedAt)}</span>
</div>
) : null}
</div>
</article>
);
@@ -1147,13 +1188,16 @@ function ButtonWidget({ el, settings }: { el: ElElement; settings: Record<string
text
);
return (
<div style={{ textAlign: (s(settings.align) as CSSProperties["textAlign"]) || "left" }}>
<div style={{ textAlign: (s(settings.align) as CSSProperties["textAlign"]) || "left", ...(icon ? { paddingBottom: 2 } : {}) }}>
{hoverBg || hoverColor ? (
<style>{`[data-el="${el.id}"] .el-btn:hover{${hoverBg ? `background:${hoverBg};` : ""}${hoverColor ? `color:${hoverColor};` : ""}}`}</style>
) : null}
<a className="el-btn" href={link || "#"} style={style} target={(settings.link as { target?: string } | undefined)?.target === "_blank" ? "_blank" : undefined} rel="noreferrer">
{content}
</a>
{/* 原站 wrapper 内含空白文本节点:inline-block 按钮所在行盒会叠加上正文 strut,
父级高度 28(按钮 26 + 2px descender)。JSX 需显式补一个空格复现。 */}
{" "}
</div>
);
}
@@ -1287,6 +1331,8 @@ function EaelInfoBoxWidget({ settings }: { settings: Record<string, unknown> })
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<string, unknown> })
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<string, unknown> | 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 (
<div className={cls}>
{img?.url ? (
<div className="infobox-icon">
<img src={img.url} alt={s(img.alt)} loading="lazy" />
</div>
) : icon?.value ? (
// 图标型 info-box(df5b95b:fas fa-check-circle 26px,左侧图标 + 标题)
<div className="infobox-icon">
<div className="infobox-icon-wrap">
<i className={icon.value} style={{ fontSize: iconSize, lineHeight: 1 }} aria-hidden="true" />
</div>
</div>
) : null}
<div className="infobox-content">
{title ? (
@@ -1321,7 +1393,7 @@ function EaelInfoBoxWidget({ settings }: { settings: Record<string, unknown> })
) : null}
</div>
) : null}
{desc ? <div dangerouslySetInnerHTML={{ __html: desc }} /> : null}
{desc ? <div className="infobox-text" style={Object.keys(contentVars).length ? contentVars : undefined} dangerouslySetInnerHTML={{ __html: desc }} /> : null}
</div>
</div>
);
@@ -1390,7 +1462,7 @@ function AdvTabsWidget({ el, settings }: { el: ElElement; settings: Record<strin
// 内容面板 id:标题 slug 化(原站 building-automation / building-automation-tab 等)
const slug = (t: string) => t.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-");
return (
<div id={`eael-advance-tabs-${el.id}`} className="eael-advance-tabs eael-tabs-horizontal">
<div id={`eael-advance-tabs-${el.id}`} className="eael-advance-tabs eael-tabs-horizontal eael-tab-auto-active active-caret-on">
<div className="eael-tabs-nav">
<ul role="tablist">
{items.map((t, i) => (
@@ -1644,7 +1716,120 @@ function IconWidget({ settings }: { settings: Record<string, unknown> }) {
);
}
/** # 组件 toggle / accordion widget:手风琴 */
/** # 组件 Elementor 原生 toggle widget(FAQ 折叠问答)
* DOM 严格对齐原站(smart-gateways 846aeb5 实测 2026-11-02):
* .elementor-toggle > .elementor-toggle-item > h3.elementor-tab-title
* (> .elementor-toggle-icon > closed/opened 两个 span SVG + a.elementor-toggle-title)
* + div.elementor-tab-content。
* 交互:Elementor 默认全部收起、各条目独立开合;active 用 elementor-active 类,
* 收起/展开的图标与内容显隐由 widget-toggle.min.css 对应规则完成(app.css「Elementor toggle」)。
* 开启 faq_schema 时在原位置输出 FAQPage JSON-LD(与原站一致)。
*/
function ToggleWidget({ el, settings }: { el: ElElement; settings: Record<string, unknown> }) {
const tabs = Array.isArray(settings.tabs) ? (settings.tabs as { tab_title?: string; tab_content?: string }[]) : [];
/** # 成员变量 已展开条目的索引集合(默认空:全部收起) **/
const [openSet, setOpenSet] = useState<Set<number>>(() => new Set());
if (!tabs.length) return null;
// 锚点 id 基数:原站为 Elementor 渲染计数器(1381…);本地用 widget id 的数值化
// 保证 SSR/水合稳定且同页多个 toggle 不冲突,仅影响锚点,无视觉作用
const base = parseInt(el.id ?? "0", 16) || 0;
const titleTag = s(settings.title_html_tag) || "h3";
const TitleTag = titleTag as "h3";
const iconValue = s((settings.selected_icon as { value?: string } | undefined)?.value) || "fas fa-caret-down";
const isCaret = iconValue.includes("caret");
/** # 函数 切换单个条目(Elementor toggle 允许同时展开多项) */
const toggle = (i: number) => {
setOpenSet((prev) => {
const next = new Set(prev);
if (next.has(i)) next.delete(i);
else next.add(i);
return next;
});
};
const faq = s(settings.faq_schema) === "yes";
return (
<>
<div className="elementor-toggle" style={{ paddingBottom: 4 }}>
{/* 原站 widget 元素比其容器高 4px(容器 310 + 4),桌面 314/移动 424;
该 4px 为 Elementor 布局尾巴,补在 toggle 底部以保持节高与后续元素位置一致 */}
{tabs.map((t, i) => {
const active = openSet.has(i);
const titleId = `elementor-tab-title-${base}${i + 1}`;
const contentId = `elementor-tab-content-${base}${i + 1}`;
return (
<div className="elementor-toggle-item" key={i}>
<TitleTag
id={titleId}
className={`elementor-tab-title${active ? " elementor-active" : ""}`}
data-tab={i + 1}
role="button"
aria-controls={contentId}
aria-expanded={active ? "true" : "false"}
onClick={() => toggle(i)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
toggle(i);
}
}}
>
<span className="elementor-toggle-icon elementor-toggle-icon-left" aria-hidden="true">
<span className="elementor-toggle-icon-closed">
{isCaret ? (
<svg className="e-font-icon-svg e-fas-caret-down" viewBox="0 0 320 512" xmlns="http://www.w3.org/2000/svg">
<path d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z" />
</svg>
) : (
<i className={iconValue} />
)}
</span>
<span className="elementor-toggle-icon-opened">
{isCaret ? (
<svg className="elementor-toggle-icon-opened e-font-icon-svg e-fas-caret-up" viewBox="0 0 320 512" xmlns="http://www.w3.org/2000/svg">
<path d="M288.662 352H31.338c-17.818 0-26.741-21.543-14.142-34.142l128.662-128.662c7.81-7.81 20.474-7.81 28.284 0l128.662 128.662c12.6 12.599 3.676 34.142-14.142 34.142z" />
</svg>
) : (
<i className={iconValue} />
)}
</span>
</span>
<a className="elementor-toggle-title" tabIndex={0}>
{s(t.tab_title)}
</a>
</TitleTag>
<div
id={contentId}
className={`elementor-tab-content elementor-clearfix${active ? " elementor-active" : ""}`}
data-tab={i + 1}
role="region"
aria-labelledby={titleId}
dangerouslySetInnerHTML={{ __html: s(t.tab_content) }}
/>
</div>
);
})}
</div>
{faq ? (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: tabs.map((t) => ({
"@type": "Question",
name: s(t.tab_title),
acceptedAnswer: { "@type": "Answer", text: s(t.tab_content) },
})),
}),
}}
/>
) : null}
</>
);
}
/** # 组件 toggle(Pro accordion)旧实现:Radix 手风琴(待独立页面审计后替换) */
function AccordionWidget({ settings }: { settings: Record<string, unknown> }) {
const tabs = Array.isArray(settings.tabs) ? (settings.tabs as { tab_title?: string; tab_content?: string }[]) : [];
if (!tabs.length) return null;
+19
View File
@@ -132,12 +132,15 @@ const SETTING_KEYS = new Set([
"youtube_url",
"map",
"posts_per_page",
// shortcode widget 源码([elementor-template id="N"] 内嵌模板、页脚菜单/gtranslate)
"shortcode",
"_skin",
"posts_include",
"posts_include_term_ids",
"cards_posts_per_page",
"cards_columns",
"cards_columns_tablet",
"cards_columns_mobile",
"cards_row_gap",
"cards_column_gap",
"cards_meta_data",
@@ -158,6 +161,16 @@ const SETTING_KEYS = new Set([
"cards_read_more_alignment",
"cards_title_color",
"cards_card_bg_color",
"cards_card_border_color",
"cards_meta_separator",
// cards 皮肤排版与缩略图比率(H18 meta 1rem/300、标题 1.125rem、移动比率 0.67;
// 模板 27143 默认 meta 12px、比率 0.66/0.6)
"cards_meta_typography_font_size",
"cards_meta_typography_font_weight",
"cards_title_typography_font_size",
"cards_title_typography_font_weight",
"cards_excerpt_typography_font_size",
"cards_item_ratio",
"cards_item_ratio_mobile",
"post_type",
"eael_ticker_tag_text",
@@ -168,16 +181,22 @@ const SETTING_KEYS = new Set([
"eael_infobox_text",
"eael_infobox_title_tag",
"eael_infobox_image",
"eael_infobox_img_or_icon",
"eael_infobox_icon_new",
"eael_infobox_icon_size",
"eael_infobox_img_type",
"eael_infobox_img_margin",
"eael_infobox_title_margin",
"eael_infobox_title_margin_mobile",
"eael_infobox_title_typography_typography",
"eael_infobox_title_typography_font_size",
"eael_infobox_title_typography_line_height",
"eael_infobox_title_typography_font_size_mobile",
"eael_infobox_title_typography_font_size_tablet",
"eael_infobox_title_typography_font_weight",
"eael_infobox_title_color",
"eael_infobox_content_margin",
"eael_infobox_content_color",
"hover_animation",
// widget 高级盒模型(如 eael-info-box 边框卡:_border_* + _padding/_margin)
"_margin",
+6 -1
View File
@@ -1,8 +1,9 @@
/**
* # 模块 catch-all 页面共享逻辑($.tsx 只导出 Route)
*/
import { getPublishedContent, listPosts, type PostListItem, type ProductListResult } from "~/server/data";
import { getPublishedContent, listPosts, type PostCardItem, type PostListItem, type ProductListResult } from "~/server/data";
import type { LayoutData } from "~/components/website/Layout";
import type { ElElement } from "~/components/elementor/Renderer";
/** # 类型 兜底 loader 返回结构 */
export type CatchAllData = {
@@ -28,6 +29,10 @@ export type CatchAllData = {
hotProducts: ProductListResult;
/** # 成员变量 WP /%category%/ 归档命中:分类 + 分页文章(content 为 null 时可能命中) **/
archive: null | { name: string; slug: string; posts: { items: PostListItem[]; total: number; page: number; pageSize: number } };
/** # 成员变量 内嵌 Elementor 模板:shortcode widget id → 模板元素树 **/
templates: Record<string, ElElement[]>;
/** # 成员变量 posts widget 预取文章:widget id → 文章卡 **/
postsData: Record<string, PostCardItem[]>;
};
/** # 类型 归档分页结果(listPosts 返回形状的窄化别名) */
+13 -4
View File
@@ -19,10 +19,19 @@ let _initing: Promise<BunSQLDatabase<typeof schema>> | null = null;
* 返回值 BunSQLDatabase 实例
*/
async function build(): Promise<BunSQLDatabase<typeof schema>> {
// 变量指示符 + @vite-ignore:Vite/Rolldown 不做静态解析(客户端图不含
// "bun"),服务端运行时由 Bun 原生解析内置模块
const bunModule = "bun";
const { SQL } = await import(/* @vite-ignore */ bunModule);
// 优先取全局 Bun 内置对象:dev(Vite module-runner)与生产(bun 启动
// dist/server/server.js)进程都由 Bun 运行,globalThis.Bun 一定存在,
// 且不经过打包器/模块解析器;缺失时才退回动态 import("bun")。
const g = globalThis as { Bun?: { SQL?: typeof import("bun").SQL } };
let SQL: typeof import("bun").SQL | undefined = g.Bun?.SQL;
if (!SQL) {
// 变量指示符 + @vite-ignore:Vite/Rolldown 不做静态解析(客户端图不含
// "bun"),服务端运行时由 Bun 原生解析内置模块
const bunModule = "bun";
const mod = (await import(/* @vite-ignore */ bunModule)) as typeof import("bun");
SQL = mod.SQL;
}
if (!SQL) throw new Error("Bun SQL 不可用:请使用 bun 运行时启动服务");
const { drizzle } = await import("drizzle-orm/bun-sql");
const sql = new SQL(process.env.DATABASE_URL || "postgres://dusuniot:dusuniot@127.0.0.1:25432/dusuniot");
return drizzle(sql, { schema }) as BunSQLDatabase<typeof schema>;
+76 -4
View File
@@ -10,24 +10,78 @@ import { useEffect, useState } from "react";
import { createFileRoute, Link, notFound, redirect } from "@tanstack/react-router";
import { getLayoutData, Layout, type LayoutData } from "~/components/website/Layout";
import { Pagination } from "~/components/website/Pagination";
import { ElementorRenderer, extractElementor } from "~/components/elementor/Renderer";
import { ElementorRenderer, extractElementor, type ElElement } from "~/components/elementor/Renderer";
import { InquiryForm } from "~/components/website/InquiryForm";
import { Button } from "~/components/ui/button";
import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator } from "~/components/ui/breadcrumb";
import { ProductCard } from "~/components/website/ProductCard";
import { isLangCode, LANG_NAMES, pickContent, type CatchAllData } from "~/components/website/catchall-shared";
import {
getEmbeddedTemplate,
getPublishedContent,
getPostCategoryBySlug,
getProductChildCategory,
hasProductSlug,
listPosts,
listProducts,
loadPostsWidgetsData,
resolveRedirect,
type PostCardItem,
type ProductListResult,
} from "~/server/data";
import { useT } from "~/i18n";
/** # 函数 把 Elementor 文档归一化为元素数组(裸数组 / {elements} 两种形态) */
function asElements(doc: unknown): ElElement[] {
if (Array.isArray(doc)) return doc as ElElement[];
const els = (doc as { elements?: unknown } | null)?.elements;
return Array.isArray(els) ? (els as ElElement[]) : [];
}
/** # 函数 扫描文档里的 [elementor-template id="N"] 短代码并加载模板
* 参数 elements 主文档元素树
* 返回值 templates: shortcode widget id → 模板元素树;elements: 模板元素树拼接
* (供 posts widget 预取)
*/
async function loadEmbeddedTemplates(elements: ElElement[]): Promise<{ templates: Record<string, ElElement[]>; elements: ElElement[] }> {
const templates: Record<string, ElElement[]> = {};
const collected: ElElement[] = [];
const ids = new Map<number, ElElement[]>();
const walk = (els: ElElement[] | undefined) => {
for (const el of els || []) {
const st = (el.settings || {}) as Record<string, unknown>;
const shortcode = typeof st.shortcode === "string" ? st.shortcode : "";
const m = /\[elementor-template\s+id=["']?(\d+)/.exec(shortcode);
if (m && el.id) ids.set(Number(m[1]), []);
if (el.elements?.length) walk(el.elements);
}
};
walk(elements);
for (const id of ids.keys()) {
const row = await getEmbeddedTemplate(id);
const doc = asElements(row?.meta ? (row.meta as Record<string, unknown>).elementor : null);
if (doc.length) {
ids.set(id, doc);
collected.push(...doc);
}
}
// 按 widget id 回填模板(同一文档仅取第一个匹配的 widget)
const walk2 = (els: ElElement[] | undefined) => {
for (const el of els || []) {
const st = (el.settings || {}) as Record<string, unknown>;
const shortcode = typeof st.shortcode === "string" ? st.shortcode : "";
const m = /\[elementor-template\s+id=["']?(\d+)/.exec(shortcode);
if (m && el.id) {
const doc = ids.get(Number(m[1]));
if (doc?.length) templates[el.id] = doc;
}
if (el.elements?.length) walk2(el.elements);
}
};
walk2(elements);
return { templates, elements: collected };
}
export const Route = createFileRoute("/$")({
// 归档分页用 ?page=N
validateSearch: (search: Record<string, unknown>): { page?: number } => {
@@ -111,7 +165,7 @@ export const Route = createFileRoute("/$")({
const cat = await getPostCategoryBySlug(last).catch(() => null);
if (cat) {
const posts = await listPosts(deps.page || 1, 12, cat.slug).catch(() => ({ items: [], total: 0, page: deps.page || 1, pageSize: 12 }));
return { layout, content: null, lang, hotProducts, archive: { name: cat.name, slug: cat.slug, posts } } satisfies CatchAllData;
return { layout, content: null, lang, hotProducts, archive: { name: cat.name, slug: cat.slug, posts }, templates: {}, postsData: {} } satisfies CatchAllData;
}
}
// 未命中任何内容 → 404(SSR 返回 404 状态码,由 notFoundComponent 渲染)
@@ -120,7 +174,12 @@ export const Route = createFileRoute("/$")({
// 语言前缀首页等内容为空时的兜底
if (!content) throw notFound();
return { layout, content, lang, hotProducts, archive: null } satisfies CatchAllData;
// 内嵌 Elementor 模板([elementor-template id="N"])+ posts widget 数据预取
const mainEls = asElements(extractElementor(content.meta));
const embedded = await loadEmbeddedTemplates(mainEls);
const postsData = await loadPostsWidgetsData([...mainEls, ...embedded.elements]);
return { layout, content, lang, hotProducts, archive: null, templates: embedded.templates, postsData } satisfies CatchAllData;
},
head: ({ loaderData }) => {
const langLabel = loaderData?.lang ? ` (${LANG_NAMES[loaderData.lang] || loaderData.lang})` : "";
@@ -249,7 +308,20 @@ function CatchAllPage() {
</div>
</div>
) : null}
<ElementorRenderer doc={elementor} />
<ElementorRenderer
doc={elementor}
postsData={data.postsData}
shortcodes={
Object.keys(data.templates || {}).length
? Object.fromEntries(
Object.entries(data.templates).map(([id, doc]) => [
id,
<ElementorRenderer key={id} doc={doc} postsData={data.postsData} />,
]),
)
: undefined
}
/>
<section className="mx-auto mt-10 mb-14 max-w-[860px] px-4">
<InquiryForm formKey="contact-us-form" productInterest={c.type === "post" ? "" : c.title} title="Get in Touch" />
</section>
+50 -5
View File
@@ -4,6 +4,8 @@
* 全部走索引 + 稳定分页,供 Server Function / SSR loader 使用。
*/
import { db } from "~/db";
// 仅类型引用(编译期擦除):避免与渲染器产生运行期循环依赖
import type { ElElement } from "~/components/elementor/Renderer";
import {
contents,
categories,
@@ -119,6 +121,39 @@ export async function getShopTemplate() {
return getPublishedContent("shop-archive", "template");
}
/** # 函数 内嵌 Elementor 模板([elementor-template id="N"] 短代码引用的 post)
* 由 scripts/import-embedded-templates.ts 以 slug=`template-<legacyId>` 落库。
* 参数 legacyId WP 模板 post id(如 27143)
* 返回值 内容行(meta.elementor 为模板元素树);不存在返回 null
*/
export async function getEmbeddedTemplate(legacyId: number) {
return getPublishedContent(`template-${legacyId}`, "template").catch(() => null);
}
/** # 函数 收集 Elementor 文档中 posts widget 的查询并预取文章
* posts widget(skin cards)按 settings.posts_include_term_ids(WP 术语 legacy_id)+
* cards_posts_per_page 查询最新文章;结果以 widget id 为键返回给 ElementorRenderer。
* 参数 elements Elementor 文档(裸元素数组,可含多篇文档拼接后的元素)
* 返回值 widgetId → 文章卡数组
*/
export async function loadPostsWidgetsData(elements: ElElement[] | undefined): Promise<Record<string, PostCardItem[]>> {
const out: Record<string, PostCardItem[]> = {};
const walk = async (els: ElElement[] | undefined) => {
for (const el of els || []) {
if (el.widgetType === "posts" && el.id) {
const st = (el.settings || {}) as Record<string, unknown>;
const termIds = Array.isArray(st.posts_include_term_ids) ? (st.posts_include_term_ids as number[]) : [];
const perPage = Number(st.cards_posts_per_page) || 5;
// 多术语取并集按发布时间倒序(27143:53/54/703)
if (termIds.length) out[el.id] = await listPostsByTermsLegacy(termIds.map(Number), perPage).catch(() => []);
}
if (el.elements?.length) await walk(el.elements);
}
};
await walk(elements);
return out;
}
/** # 类型 产品筛选状态:每个筛选字段已选值列表 */
export type FilterState = Record<string, string[]>;
@@ -598,12 +633,22 @@ export type PostCardItem = {
* 返回值 文章卡数组(标题/摘要/发布时间/特色图/首个分类名)
*/
export async function listPostsByTermLegacy(legacyTermId: number, limit: number) {
const [cat] = await db
return listPostsByTermsLegacy([legacyTermId], limit);
}
/** # 函数 按多个 WP 术语 legacy_id 取最新文章(posts widget 多分类并集)
* Elementor posts widget 的 posts_include_term_ids 可含多个分类,按发布时间倒序
* 取并集(原站落地页 27143:terms 53/54/703 → Blog/Case Study/News,最新 4 篇)。
* 参数 legacyTermIds WP 术语 id 列表;limit 取条数
* 返回值 文章卡数组(去重后按发布时间倒序)
*/
export async function listPostsByTermsLegacy(legacyTermIds: number[], limit: number) {
const catRows = await db
.select({ id: categories.id })
.from(categories)
.where(eq(categories.legacyId, legacyTermId))
.limit(1);
if (!cat) return [] as PostCardItem[];
.where(inArray(categories.legacyId, legacyTermIds));
if (!catRows.length) return [] as PostCardItem[];
const catIds = catRows.map((c) => c.id);
const rows = await db
.select({
slug: contents.slug,
@@ -620,7 +665,7 @@ export async function listPostsByTermLegacy(legacyTermId: number, limit: number)
and(
eq(contents.type, "post"),
eq(contents.status, "published"),
sql`EXISTS (SELECT 1 FROM content_terms ct WHERE ct.content_id=${contents.id} AND ct.category_id=${cat.id})`,
sql`EXISTS (SELECT 1 FROM content_terms ct WHERE ct.content_id=${contents.id} AND ct.category_id IN (${sql.join(catIds.map((id) => sql`${id}`), sql`, `)}))`,
),
)
.orderBy(desc(contents.publishedAt))
+127 -20
View File
@@ -83,6 +83,7 @@
}
.fa-chevron-up::before { content: "\f077"; }
.fa-comment-dots::before { content: "\f4ad"; }
.fa-check-circle::before { content: "\f058"; }
/* # 成员变量 Tailwind 主题令牌(与原 website.css 配色一一对应) **/
@theme {
@@ -351,8 +352,9 @@
.eael-advance-tabs .eael-tabs-nav > ul li .eael-tab-title {
margin: unset; font-size: unset; line-height: unset; font-family: unset; font-weight: unset; color: unset;
}
.eael-tabs-content { display: flex; flex: 1 1 auto; overflow: hidden; }
.eael-advance-tabs .eael-tabs-content > div { display: none; opacity: 0; }
.eael-advance-tabs .eael-tabs-content > div.active { display: block; width: 100%; opacity: 1; }
.eael-advance-tabs .eael-tabs-content > div.active { display: block; width: 100%; padding: 1em; opacity: 1; }
/* ===== Elementor icon-list(widget-icon-list.min.css 语义;H13 链接条实测 2026-11-01)
* inline 视图:ul flex wrap + 负边距 -8px 与 li 8px 侧边距对冲(实测 ul margin 0 -8px);
@@ -379,10 +381,26 @@
[data-el="ef0fcd9"] .eael-tabs-nav > ul li.active,
[data-el="ef0fcd9"] .eael-tabs-nav > ul li:hover { color: #ED6C00; border-bottom-color: #ED6C00; }
[data-el="ef0fcd9"] .eael-tabs-nav > ul li .eael-tab-title { font-family: "Roboto", sans-serif; }
[data-el="ef0fcd9"] .eael-tab-content-item {
[data-el="ef0fcd9"] .eael-advance-tabs .eael-tabs-content > .eael-tab-content-item.active {
color: #333333; padding: 16px 0 0; border-top: 1px solid #C7CBDA;
}
/* ===== 落地页 8e90920 adv-tabs 生成规则(post-6509.css ca60c10;1440 实测 2026-11-02)
* li 背景 #F4F5F8(globals 26331af)+ 文字 #333 + 底边 3px;hover/active 文字与底边
* 变主色 #ED6C00;内容面板 color #333 + padding 16px 0 0 + 顶边 1px #C7CBDA。
* 容器 active-caret-on:激活项不显示小三角。 */
[data-el="ca60c10"] .eael-tabs-nav > ul li {
background-color: #F4F5F8; color: #333333;
border-style: solid; border-width: 0 0 3px; border-color: #F4F5F8;
}
[data-el="ca60c10"] .eael-tabs-nav > ul li.active,
[data-el="ca60c10"] .eael-tabs-nav > ul li:hover { color: #ED6C00; border-bottom-color: #ED6C00; }
[data-el="ca60c10"] .eael-advance-tabs .eael-tabs-content > .eael-tab-content-item.active {
color: #333333; padding: 16px 0 0; border-top: 1px solid #C7CBDA;
}
.eael-advance-tabs.active-caret-on .eael-tabs-nav > ul li.active:after { display: none; }
.eael-advance-tabs .eael-tabs-nav > ul li.active:after { display: block; }
/* ===== 首页 H13 Difference Maker(post-18.css 对应块;1436 实测 2026-11-01)
* 楼体大图 = 列 bf3d0ba 背景(bottom left + 100% auto,组件内联输出,post-18 生成规则
* background_size initial → 100% auto);icon-list 悬停主色(globals text_color_hover);
@@ -420,31 +438,63 @@
* padding 48/48/16 + 白底 + 圆角 6 + 边框 #E0E5F9 均设置驱动 */
[data-el="65282de"] > div, [data-el="cc3e8ca"] > div { width: auto !important; }
/* ===== Elementor posts widget(skin cards 基础;H18 实测 2026-11-01)
* 卡 = flex 纵向(h 520 由节点 custom_css .ele-post-dif .elementor-post 注入);
* 缩略图比率 0.66×内宽(img 绝对定位铺满 object-fit fill)、badge 绝对右上
* (bg rgba(255,255,255,.7)/12px/圆角 4,自定义样式经 custom_css 注入);
* 标题 18/400/#000、meta 16/#ADADAD/padding 10 12、摘要在 H18 组件内联 */
.elementor-post__card {
display: flex; flex-direction: column; overflow: hidden;
border-radius: 3px;
/* ===== Elementor posts widget(skin cards 基础;H18 + 模板 27143 实测 2026-11-02)
* 移植 widget-posts.min.css cards 皮肤:article 纵向 flex;__card 白底/圆角 3/
* overflow hidden/纵向 flex;缩略图容器用 padding-bottom 比率撑高、img 静态
* 宽度 calc(100%+1px)(高度按原图比例);hover-gradient 覆盖渐变;badge 绝对
* 右上(默认 #69727d 胶囊,H18 由 custom_css 覆盖为白底圆角 4);
* text flex-grow;meta 在 text 之后(border-top + margin-top:auto)。
* 各尺寸/颜色由 PostsWidget 按设置内联(padding/字号/边框),此处只留结构。 */
.elementor-posts-container:not(.elementor-posts-masonry) { align-items: stretch; }
/* 列数:Elementor grid-4 / tablet-2 / mobile-1(变量由 PostsWidget 内联) */
.elementor-posts-container.elementor-posts--skin-cards {
grid-template-columns: repeat(var(--ep-cols, 4), 1fr);
}
.elementor-post__thumbnail__link { display: block; position: relative; margin: 0 0 20px; }
.elementor-post__thumbnail { position: relative; overflow: hidden; padding-bottom: 66%; }
.elementor-post__thumbnail img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: fill; }
@media (max-width: 1024px) {
.elementor-posts-container.elementor-posts--skin-cards {
grid-template-columns: repeat(var(--ep-cols-t, 2), 1fr);
}
}
@media (max-width: 767px) {
.elementor-posts-container.elementor-posts--skin-cards {
grid-template-columns: repeat(var(--ep-cols-m, 1), 1fr);
}
}
.elementor-posts .elementor-post { display: flex; flex-direction: column; }
.elementor-post__card {
display: flex; flex-direction: column; position: relative; overflow: hidden;
width: 100%; min-height: 100%; border-radius: 3px; background-color: #fff;
}
.elementor-post__thumbnail__link { display: block; position: relative; width: 100%; margin: 0 0 20px; }
.elementor-post__thumbnail {
position: relative; overflow: hidden;
padding-bottom: calc(var(--ep-ratio, 0.66) * 100%);
}
.elementor-post__thumbnail img { display: block; width: calc(100% + 1px); max-width: none; height: auto; }
.elementor-post__badge {
position: absolute; top: 0; right: 0; z-index: 1;
font-size: 12px; line-height: 1; padding: 7.2px 14.4px;
position: absolute; top: 0; right: 0; z-index: 1; margin: 20px;
background: #69727d; color: #fff; border-radius: 999px;
font-size: 12px; font-weight: 400; line-height: 1; padding: 0.6em 1.2em;
text-transform: uppercase;
}
.elementor-post__text { padding: 0 12px; flex-grow: 1; }
.elementor-post__title { font-size: 18px; line-height: 27px; font-weight: 400; color: #000; }
.elementor-post__text { flex-grow: 1; }
.elementor-post__title { font-size: 18px; line-height: 27px; font-weight: 400; color: #000; margin: 0; }
.elementor-post__title a { color: inherit; text-decoration: none; }
.elementor-post__meta-data {
display: block; margin: 0; padding: 10px 12px;
font-size: 16px; color: #ADADAD;
display: block; margin: 0; border-top: 1px solid #eaeaea;
color: #adadad; font-size: 12px; line-height: 1.3;
}
.elementor-post__excerpt p { color: #777; font-size: 14px; line-height: 1.5em; margin: 0; }
/* 缩略图 hover 渐变(elementor-posts__hover-gradient):盖住图片与比率留白区 */
.elementor-posts__hover-gradient .elementor-post__card .elementor-post__thumbnail__link:after {
content: ""; position: absolute; inset: 0; width: 100%; height: 100%;
background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.35), transparent 75%);
background-repeat: no-repeat; opacity: 1; transition: all 0.3s ease-out;
}
.elementor-posts__hover-gradient .elementor-post__card:hover .elementor-post__thumbnail__link:after { opacity: 0.5; }
@media (max-width: 767px) {
.elementor-post__thumbnail { padding-bottom: calc(var(--ep-ratio-m, var(--ep-ratio, 0.66)) * 100%); }
}
.elementor-post__excerpt { font-size: 16px; line-height: 1.7; color: #333; }
/* 右列卡(a22c185 .ele-post-dif-2):纯文本卡 bg #F4F5F8、标题链接右侧箭头图标
* 与 44px 两行钳制均由 custom_css 注入;此处理品牌底色/箭头 hover 位移 */
[data-el="a22c185"] .elementor-post__card { background-color: #F4F5F8; }
@@ -530,6 +580,8 @@
.eael-infobox.icon-on-left { display: flex; position: relative; }
.eael-infobox.icon-on-left .infobox-content { padding-left: 15px; flex: 1 1 auto; min-width: 0; }
.eael-infobox .infobox-icon img { transition: all 300ms ease-in-out 0s; }
/* 图标型 info-box:图标包容器行高归零,行盒不再附加字体 descender(df5b95b 实测 26px) */
.eael-infobox .infobox-icon .infobox-icon-wrap { display: flex; align-items: center; line-height: 0; }
.eael-infobox .infobox-content .infobox-title-section { display: flex; flex-direction: column; }
.eael-infobox .infobox-content .infobox-title-section .title {
width: max-content; max-width: 100%; overflow-wrap: break-word; word-break: break-word;
@@ -538,6 +590,12 @@
.eael-infobox.icon-on-top .infobox-icon img { display: block; width: 100%; height: auto; }
.eael-infobox.icon-on-left .infobox-content .title { line-height: 1; margin: 0 0 10px 0; }
.eael-infobox .infobox-content p { margin: 0 0 15px; }
/* 内容段落生成规则(EAEL:eael_infobox_content_margin/color 落到 p;570df98
* 实测 margin 10px 0 0、color #969696;未设置时保持 EAEL 默认 0 0 15px) */
.eael-infobox .infobox-content .infobox-text p {
margin: var(--ib-p-top, 0) var(--ib-p-right, 0) var(--ib-p-bottom, 15px) var(--ib-p-left, 0);
color: var(--ib-p-color, inherit);
}
/* 内容里的 ul/ol:hello-elementor 主题 reset(H14 药丸列表实测 margin 0,
* UA 默认 1em 上下外距会撑高内容 16px);列表样式与内边距由节点 custom_css 定义 */
.eael-infobox .infobox-content ul,
@@ -720,6 +778,48 @@
[data-el="fd97340"] .elementor-tab-content { border-width: 0; }
[data-el="fd97340"] .elementor-tab-title.elementor-active { color: #ED6C00; }
/* ===== Elementor 原生 toggle widget(widget-toggle.min.css 移植 + 846aeb5 生成规则)
* 原站实测(1440):条目高 58(标题 padding 15 + 行高 28)、间距 4;标题 h3 28px/1/700
* #333、背景 #F0F1F5;标题链接 1.125rem/1 #000;图标左浮动 1em、右距 12;内容
* padding 15 / .875rem,默认 display:none,展开加 elementor-active。
* 主题 h3 字号由 hello-elementor 提供(1.75rem=28px、700),此处显式写出以脱离宿主 reset。 */
.elementor-tab-title a { color: inherit; }
.elementor-toggle { text-align: start; }
.elementor-toggle .elementor-tab-title {
border-block-end: 1px solid #d5d8dc;
cursor: pointer;
font-size: 1.75rem;
font-weight: 700;
line-height: 1;
margin: 0;
outline: none;
padding: 15px;
color: #000;
}
.elementor-toggle .elementor-tab-title .elementor-toggle-icon { display: inline-block; width: 1em; }
.elementor-toggle .elementor-tab-title .elementor-toggle-icon svg { height: 1em; margin-inline-start: -5px; width: 1em; fill: currentColor; }
.elementor-toggle .elementor-tab-title .elementor-toggle-icon.elementor-toggle-icon-left { float: left; text-align: left; }
.elementor-toggle .elementor-tab-title .elementor-toggle-icon.elementor-toggle-icon-right { float: right; text-align: right; }
.elementor-toggle .elementor-tab-title .elementor-toggle-icon-closed { display: block; }
.elementor-toggle .elementor-tab-title .elementor-toggle-icon-opened { display: none; }
.elementor-toggle .elementor-tab-title.elementor-active { border-block-end: none; }
.elementor-toggle .elementor-tab-title.elementor-active .elementor-toggle-icon-closed { display: none; }
.elementor-toggle .elementor-tab-title.elementor-active .elementor-toggle-icon-opened { display: block; }
.elementor-toggle .elementor-tab-content { border-block-end: 1px solid #d5d8dc; display: none; padding: 15px; }
.elementor-toggle .elementor-tab-content.elementor-active { display: block; }
/* 标题链接:生成规则 font-size 1.125rem(移动端 1rem),行高继承标题 1 */
.elementor-toggle .elementor-toggle-title { font-size: 1.125rem; color: #000; }
.elementor-toggle .elementor-toggle-icon { margin-inline-end: 12px; }
.elementor-toggle .elementor-toggle-item:not(:last-child) { margin-block-end: 4px; }
/* 生成规则:title/content border-width 0、标题背景 #F0F1F5、内容背景 #FFFFFF */
.elementor-toggle .elementor-tab-title { border-width: 0; border-color: #FFFFFF; background-color: #F0F1F5; }
.elementor-toggle .elementor-tab-content { border-width: 0; border-block-end-color: #FFFFFF; background-color: #FFFFFF; }
@media (max-width: 767px) {
.elementor-toggle .elementor-tab-title { padding: 12px; }
.elementor-toggle .elementor-tab-content { padding: 12px 10px; font-size: 0.875rem; }
.elementor-toggle .elementor-toggle-title { font-size: 1rem; }
}
/* ===== 页脚 Elementor 模板 post 58(线上 1436 实测 + post-58.css,2026-11-02)
* 结构:46d7afa 四列链接(桌面) / d834160(html_tag=footer) 返回顶部 + 分隔线 +
* 28c7f0b8 品牌区三列 + a428bb4 版权条。以下为「仅存生成 CSS」与 DOM 语义补丁 */
@@ -1012,3 +1112,10 @@ body.dialog-prevent-scroll { overflow: hidden; }
[data-el="71f7329"] > div,
[data-el="2ec9a62"] > div,
[data-el="7bdf4c0"] > div { width: auto !important; }
/* Landing 智能网关 028b4bc/5745b4f:列 06632bb/3317056 padding 20 + margin-right 10,
* populated 按 auto 扣除 margin(原站内层节与信息框宽 570;100% 时为 580) */
[data-el="06632bb"] > div,
[data-el="3361c18"] > div,
[data-el="3317056"] > div,
[data-el="5938d26"] > div { width: auto !important; }
+8 -1
View File
@@ -16,13 +16,20 @@ export default defineConfig({
rolldownOptions: { external: ["bun"] },
},
// 客户端环境用 stub 解析 "bun"(db 模块经 server fn 依赖链进入客户端图,
// 动态 import("bun") 只在服务端执行)
// 动态 import("bun") 只在服务端执行);SSR 环境把 "bun" 标记为外部模块,
// 由 Vite dev 进程(bun)原生解析 —— 否则 dev 下 module-runner 报 ERR_MODULE_NOT_FOUND
environments: {
client: {
resolve: {
alias: [{ find: /^bun$/, replacement: path.resolve(here, "src/stubs/bun-client.ts") }],
},
},
ssr: {
// builtins:把 "bun" 声明为该运行时环境的内置模块,dev 下由 bun 原生
// 解析(不进入 module-runner 的依赖图,否则报 ERR_MODULE_NOT_FOUND)。
// 注意:builtins 是覆写语义,必须带上 node:*,否则 node 内置模块失效。
resolve: { external: ["bun"], builtins: ["bun", /^node:/] },
},
},
plugins: [
tsConfigPaths({ projects: ["./tsconfig.json"] }),