v0.10.25: /solution/ 归档+落地页复刻(archive 组件/分页/摘录24词)、Header 全断点对齐、page_css 导入管线、reviews/populated 宽度/_css_classes/tabs HTML/图片变体修复

This commit is contained in:
bigcode
2026-11-03 03:59:04 +08:00
parent 7559977ae1
commit 9a51994860
17 changed files with 681 additions and 146 deletions
+66 -29
View File
@@ -291,15 +291,18 @@ export function ElementorRenderer({
postsData,
shortcodes,
vars,
pageCss,
}: {
doc: ElDocument;
postsData?: Record<string, PostCardItem[]>;
shortcodes?: Record<string, ReactNode>;
vars?: RendererVars;
/** # 成员变量 页面级生成 CSS(原站 post-<id>.css 翻译成 data-el 选择器,scripts/import-page-css.ts 导入) **/
pageCss?: string;
}) {
const elements = normalizeDoc(doc);
if (!elements.length) return null;
const css = collectCustomCss(elements) + collectResponsiveCss(elements);
const css = (pageCss || "") + collectCustomCss(elements) + collectResponsiveCss(elements);
return (
<VarsContext.Provider value={vars || {}}>
<PostsDataContext.Provider value={postsData || {}}>
@@ -527,8 +530,10 @@ function SectionNode({ el }: { el: ElElement }) {
if (overlay) style.position = style.position || "relative";
const Tag = sectionTag(settings);
const vis = visibilityClass(settings);
// _css_classes(页面级 custom_css 的选择器挂在节点的 class 上,如 ele-secondDep-dif)
const secCls = [vis, s(settings._css_classes)].filter(Boolean).join(" ") || undefined;
return (
<Tag className={vis || undefined} style={style} data-el={el.id}>
<Tag className={secCls} style={style} data-el={el.id}>
{overlay}
{/* 行加 relative:与绝对定位遮罩同属定位层,DOM 靠后绘制在上,内容不被蒙灰 */}
<div className="mx-auto flex flex-wrap" style={{ ...inner, position: "relative" }}>
@@ -605,13 +610,13 @@ function ColumnNode({ el, gapPad, contentPos }: { el: ElElement; gapPad?: string
} else inner.borderWidth = "1px";
}
return (
// 内层(populated)宽 100%:与列同宽、水平 margin 向右溢出(H4 卡实测 populated=272 含
// margin 溢出);H11/H14/H15 等"扣除 margin"语义的列在 app.css 用 !important 覆写为 auto。
// 空列(无子元素)裸渲染:原站空列无 populated 盒(H15 占位格 margin/边框均为 0),
// 仅保留外层弹性宽度与拉伸高度
<div className={"el-col min-w-0 " + visibilityClass(settings)} style={outer} data-el={el.id}>
// 内层(populated)块级宽 auto:与原站 .elementor-element-populated 一致——水平 margin
// 存在时块盒收缩进列宽(6c765c4 卡列 margin 8/8 实测 populated=294=310-16、图片 272);
// margin 为 0 时与 100% 等价;负 margin 自然外扩(H4 图)。空列(无子元素)裸渲染:
// 原站空列无 populated 盒(H15 占位格 margin/边框均为 0),仅保留外层弹性宽度与拉伸高度
<div className={("el-col min-w-0 " + [visibilityClass(settings), s(settings._css_classes)].filter(Boolean).join(" ")).trimEnd()} style={outer} data-el={el.id}>
{(el.elements || []).length ? (
<div style={{ display: "flex", flexWrap: "wrap", alignContent: "flex-start", width: "100%", ...inner }}>
<div style={{ display: "flex", flexWrap: "wrap", alignContent: "flex-start", ...inner }}>
{(el.elements || []).map((c, i) => (
<ElementNode key={c.id || i} el={c} gapPad={gapPad} />
))}
@@ -1610,28 +1615,62 @@ function TestimonialWidget({ settings }: { settings: Record<string, unknown> })
);
}
/** # 组件 reviews(评价轮播):静态卡片网格(slides 复用:name/title/rating/content/image) */
/** # 组件 reviews(Elementor 原生 reviews widget,swiper 轮播静态化)
* 取证(smart-building 07ecb19,1440 实测 2026-11-03):
* 外层左右 28px 留白(箭头槽);slide 宽 (100%-10)/2(slides_per_view=2、gap 10),
* 白底 radius 5;header flex pad 20 20 18(图 79×79 圆形 + cite 左 20:姓名 24px/700 #1C2022、
* 星 14px #CCD6DF 右距 5、职位 16px #697882);content pad 18 20 20,内容 HTML
* (.ele-sol-review-box 等)由页面级 CSS(pageCss / custom_css)提供。
*/
function ReviewsWidget({ settings }: { settings: Record<string, unknown> }) {
const slides = Array.isArray(settings.slides)
? (settings.slides as { name?: string; title?: string; rating?: string; content?: string; image?: { url?: string; alt?: string } }[])
? (settings.slides as { _id?: string; name?: string; title?: string; rating?: string; content?: string; image?: { url?: string; alt?: string } }[])
: [];
if (!slides.length) return null;
const stars = (n: string) => "★★★★★".slice(0, Math.max(1, Math.min(5, Math.round(Number(n) || 5))));
const perView = Math.max(1, Math.round(Number(s(settings.slides_per_view)) || 2));
return (
<div className="mb-[18px] grid gap-4 min-[760px]:grid-cols-2">
{slides.map((it, i) => (
<div key={i} className="rounded-md border p-4">
<div className="text-[var(--brand)]">{stars(s(it.rating))}</div>
{s(it.content) ? <div className="content-html mt-2" dangerouslySetInnerHTML={{ __html: s(it.content) }} /> : null}
<div className="mt-3 flex items-center gap-3">
{it.image?.url ? <img src={it.image.url} alt={it.image.alt || it.name || ""} className="h-10 w-10 rounded-full object-cover" /> : null}
<div>
<div className="font-medium">{s(it.name)}</div>
<div className="text-sm text-muted-foreground">{s(it.title)}</div>
<div className="el-reviews">
<style>{`
.el-reviews .el-swiper { padding: 0 28px; }
.el-reviews .el-swiper-wrapper { display: grid; grid-template-columns: repeat(${perView}, 1fr); gap: 10px; }
.el-reviews .swiper-slide { background: ${s(settings.slide_background_color) || "#fff"}; border-radius: 5px; }
.el-reviews .elementor-testimonial__header { display: flex; align-items: center; padding: 20px 20px 18px; }
.el-reviews .elementor-testimonial__image { flex: 0 0 auto; display: flex; }
.el-reviews .elementor-testimonial__image img { display: block; width: 79px; height: 79px; border-radius: 999px; object-fit: cover; }
.el-reviews .elementor-testimonial__cite { display: flex; flex-direction: column; margin: 0 0 0 20px; font-style: normal; }
.el-reviews .elementor-testimonial__name { font-size: ${sizeCss(settings.name_typography_font_size) || "24px"}; line-height: 1.5; font-weight: 700; color: #1C2022; }
.el-reviews .elementor-star-rating { font-size: 14px; line-height: 21px; color: #CCD6DF; white-space: nowrap; }
.el-reviews .elementor-star-rating i { display: inline-block; font-style: normal; margin-right: 5px; }
.el-reviews .elementor-testimonial__title { font-size: 16px; color: #697882; }
.el-reviews .elementor-testimonial__content { padding: 18px 20px 20px; font-size: ${sizeCss(settings.content_typography_font_size) || "16px"}; line-height: ${sizeCss(settings.content_typography_line_height) || "24px"}; }
`}</style>
<div className="el-swiper">
<div className="el-swiper-wrapper">
{slides.map((it, i) => (
<div key={it._id || i} className="swiper-slide">
<div className="elementor-testimonial">
<div className="elementor-testimonial__header">
<div className="elementor-testimonial__image">
{it.image?.url ? <img src={it.image.url} alt={it.image.alt || it.name || ""} loading="lazy" /> : null}
</div>
<cite className="elementor-testimonial__cite">
<span className="elementor-testimonial__name">{s(it.name)}</span>
<div className="elementor-star-rating" aria-label={it.rating}>
{[1, 2, 3, 4, 5].map((n) => (
<i key={n} className={"elementor-star-" + (n <= Math.round(Number(it.rating) || 5) ? "full" : "empty")}>★</i>
))}
</div>
<span className="elementor-testimonial__title">{s(it.title)}</span>
</cite>
</div>
<div className="elementor-testimonial__content">
<div className="elementor-testimonial__text" dangerouslySetInnerHTML={{ __html: localSiteUrl(s(it.content)) }} />
</div>
</div>
</div>
</div>
))}
</div>
))}
</div>
</div>
);
}
@@ -1944,9 +1983,8 @@ function TabsWidget({ settings }: { settings: Record<string, unknown> }) {
tabIndex={i === active ? 0 : -1}
aria-selected={i === active}
onClick={() => setActive(i)}
>
{s(t.tab_title)}
</div>
dangerouslySetInnerHTML={{ __html: s(t.tab_title) }}
/>
))}
</div>
<div className="elementor-tabs-content-wrapper" role="tablist">
@@ -1959,9 +1997,8 @@ function TabsWidget({ settings }: { settings: Record<string, unknown> }) {
tabIndex={i === active ? 0 : -1}
aria-selected={i === active}
onClick={() => setActive(i)}
>
{s(t.tab_title)}
</div>
dangerouslySetInnerHTML={{ __html: s(t.tab_title) }}
/>
<div
id={`elementor-tab-content-${base + i}`}
className="elementor-tab-content elementor-clearfix"