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
+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 返回形状的窄化别名) */