v0.10.25: /solution/ 归档+落地页复刻(archive 组件/分页/摘录24词)、Header 全断点对齐、page_css 导入管线、reviews/populated 宽度/_css_classes/tabs HTML/图片变体修复
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user