v0.10.18: H18 最新博文(8bc6b68)完成 + 文章分类关系数据修复
- PostsWidget 忠实重写(skin cards):预取架构 loader 按 widget 收集术语→查文章→ PostsDataContext 分发;卡 DOM/缩略图比率 0.66/NEWS 徽章(uppercase)/日期 F j,Y/ 摘要 18 词(空回退正文);右列纯文本卡(44px 两行钳制+箭头图标,custom_css 注入) - posts cards 基础 CSS + slim 白名单 cards_*/_skin/posts_include* 全键 - scripts/repair-content-terms.ts:从 legacy dump wp_term_relationships 提取 67709 对, 经 legacy_id 映射补插 1162 条 content_terms(Blog 302/News 22 篇,原仅 61 篇有关联) - 补 icon-arrow-light-orange.png;双端节内几何/样式全等(sec 521),截图目视一致
This commit is contained in:
@@ -5,8 +5,8 @@
|
||||
* 样式以内联 style 复现视觉效果(背景、内边距、对齐、列宽百分比),
|
||||
* 未识别的 widgetType 优雅降级:渲染 heading_title / editor 文本或忽略。
|
||||
*/
|
||||
import { Component, Fragment, useEffect, useState, type CSSProperties, type ReactNode } from "react";
|
||||
import { getLatestPosts, getLatestProducts, type LatestItem } from "~/server/data";
|
||||
import { Component, createContext, useContext, Fragment, useEffect, useState, type CSSProperties, type ReactNode } from "react";
|
||||
import { getLatestPosts, getLatestProducts, type LatestItem, type PostCardItem } from "~/server/data";
|
||||
import TickerBar from "~/components/website/TickerBar";
|
||||
import { InquiryForm } from "~/components/website/InquiryForm";
|
||||
import { localSiteUrl } from "~/utils/media";
|
||||
@@ -232,18 +232,23 @@ function normalizeDoc(doc: ElDocument): ElElement[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** # 成员变量 posts widget 文章数据上下文(loader 预取,经 widget id 分发) **/
|
||||
const PostsDataContext = createContext<Record<string, PostCardItem[]>>({});
|
||||
|
||||
/** # 组件 Elementor 渲染器入口 */
|
||||
export function ElementorRenderer({ doc }: { doc: ElDocument }) {
|
||||
export function ElementorRenderer({ doc, postsData }: { doc: ElDocument; postsData?: Record<string, PostCardItem[]> }) {
|
||||
const elements = normalizeDoc(doc);
|
||||
if (!elements.length) return null;
|
||||
const css = collectCustomCss(elements) + collectResponsiveCss(elements);
|
||||
return (
|
||||
<div className="content-html">
|
||||
{css ? <style dangerouslySetInnerHTML={{ __html: css }} /> : null}
|
||||
{elements.map((el, i) => (
|
||||
<ElementNode key={el.id || i} el={el} />
|
||||
))}
|
||||
</div>
|
||||
<PostsDataContext.Provider value={postsData || {}}>
|
||||
<div className="content-html">
|
||||
{css ? <style dangerouslySetInnerHTML={{ __html: css }} /> : null}
|
||||
{elements.map((el, i) => (
|
||||
<ElementNode key={el.id || i} el={el} />
|
||||
))}
|
||||
</div>
|
||||
</PostsDataContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -571,7 +576,7 @@ function WidgetNodeInner({ el }: { el: ElElement }) {
|
||||
case "eael-content-ticker":
|
||||
return <TickerWidget settings={settings} />;
|
||||
case "posts":
|
||||
return <PostsWidget settings={settings} />;
|
||||
return <PostsWidget el={el} settings={settings} />;
|
||||
case "social-icons":
|
||||
return <SocialIconsWidget settings={settings} />;
|
||||
case "eael-contact-form-7":
|
||||
@@ -750,42 +755,99 @@ function TickerWidget({ settings }: { settings: Record<string, unknown> }) {
|
||||
}
|
||||
|
||||
/** # 组件 posts widget(Elementor Pro 卡片皮肤:文章/产品卡片网格) */
|
||||
function PostsWidget({ settings }: { settings: Record<string, unknown> }) {
|
||||
const perPage = Number(s(settings.cards_posts_per_page)) || 3;
|
||||
const [posts, setPosts] = useState<Array<{ title: string; slug: string; excerpt: string | null; publishedAt: Date | string | null; image: string | null }>>([]);
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
getLatestPosts()
|
||||
.then((rows) => { if (alive) setPosts(rows.slice(0, perPage).map((r) => ({ title: r.title, slug: r.url, excerpt: null as string | null, publishedAt: null, image: null }))); })
|
||||
.catch(() => {});
|
||||
return () => { alive = false; };
|
||||
}, [perPage]);
|
||||
/** # 组件 posts widget(Elementor skin cards 忠实 DOM,H18 实测 2026-11-01)
|
||||
* .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 预取。
|
||||
*/
|
||||
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 showThumb = s(settings.cards_thumbnail) !== "none";
|
||||
const cols = Number(s(settings.cards_columns)) || 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 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 键缺省 = 开(左卡无键但有徽章/摘要的实测语义),
|
||||
// 空串 = 显式关(右卡 cards_show_badge/cards_show_excerpt 均为空串)
|
||||
const showBadge = settings.cards_show_badge === undefined ? true : s(settings.cards_show_badge) !== "";
|
||||
const showMeta = Array.isArray(settings.cards_meta_data) && (settings.cards_meta_data as string[]).includes("date");
|
||||
const showExcerpt = settings.cards_show_excerpt === undefined ? true : s(settings.cards_show_excerpt) !== "";
|
||||
// 日期格式 F j, Y(October 21, 2026)
|
||||
const fmtDate = (d: Date | string | null) => {
|
||||
if (!d) return null;
|
||||
const dt = typeof d === "string" ? new Date(d) : d;
|
||||
if (isNaN(dt.getTime())) return null;
|
||||
return dt.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
|
||||
};
|
||||
return (
|
||||
<div className="mx-auto grid max-w-[1140px] grid-cols-1 gap-[22px] px-6 py-2.5 min-[900px]:grid-cols-3">
|
||||
{posts.map((p) => (
|
||||
<a
|
||||
className="block overflow-hidden rounded-lg border border-line bg-white text-inherit transition-shadow hover:shadow-[0_10px_26px_rgba(16,42,76,0.12)]"
|
||||
key={p.slug}
|
||||
href={"/blog/" + p.slug + "/"}
|
||||
>
|
||||
{p.image ? (
|
||||
<div className="aspect-video overflow-hidden bg-[#f2f5f9]">
|
||||
<img src={p.image} alt="" loading="lazy" className="h-full w-full object-cover" />
|
||||
<div
|
||||
className="elementor-posts-container elementor-posts elementor-posts--skin-cards elementor-grid"
|
||||
style={{ display: "grid", gridTemplateColumns: `repeat(${cols}, 1fr)`, gap: `${rowGap} ${colGap}` }}
|
||||
>
|
||||
{posts.map((p) => {
|
||||
// 链接前缀按主分类:News → /news/,其余 → /blog/(原站 News 文章走 /news/<slug>/)
|
||||
const href = (p.categoryName === "News" ? "/news/" : "/blog/") + p.slug + "/";
|
||||
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 ? (
|
||||
<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>
|
||||
{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 }}>
|
||||
<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>
|
||||
</div>
|
||||
) : null}
|
||||
{showExcerpt ? (
|
||||
<p
|
||||
className="elementor-post__excerpt"
|
||||
style={{ color: excerptColor, marginBottom: excSpacing }}
|
||||
dangerouslySetInnerHTML={{ __html: postExcerpt(p) }}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="px-4 pt-3.5 pb-[18px]">
|
||||
<h3 className="mt-0 mb-1.5 text-[17px] leading-[1.4]">{p.title}</h3>
|
||||
{p.publishedAt ? <time className="text-[13px] text-muted-foreground">{String(p.publishedAt).slice(0, 10)}</time> : null}
|
||||
{p.excerpt ? <p className="mt-2 mb-0 line-clamp-2 text-sm text-muted-foreground">{p.excerpt}</p> : null}
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** # 组件 social-icons widget(图标行) */
|
||||
/** # 函数 文章卡摘要:excerpt 优先,空则取正文纯文本前 N 词(WP wp_trim_words 语义) */
|
||||
function postExcerpt(p: PostCardItem): string {
|
||||
return p.excerpt && p.excerpt.trim() ? p.excerpt : postWords(p.content || "", 18);
|
||||
}
|
||||
|
||||
/** # 函数 纯文本取前 N 词(去 HTML 标签后按空白分词,"…" 结尾) */
|
||||
function postWords(html: string, n: number): string {
|
||||
const text = html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
||||
const words = text.split(" ");
|
||||
return words.length <= n ? text : words.slice(0, n).join(" ") + " …";
|
||||
}
|
||||
|
||||
/** # 组件 social-icons widget:忠实 Elementor DOM(widget-social-icons 语义,H16 实测)
|
||||
* .elementor-social-icons-wrapper.elementor-grid(inline-grid + column-gap=icon_spacing,
|
||||
* 由父级 text-align 居中)> a.elementor-icon.elementor-social-icon.elementor-social-icon-<name>
|
||||
|
||||
@@ -113,7 +113,33 @@ const SETTING_KEYS = new Set([
|
||||
"youtube_url",
|
||||
"map",
|
||||
"posts_per_page",
|
||||
"_skin",
|
||||
"posts_include",
|
||||
"posts_include_term_ids",
|
||||
"cards_posts_per_page",
|
||||
"cards_columns",
|
||||
"cards_columns_tablet",
|
||||
"cards_row_gap",
|
||||
"cards_column_gap",
|
||||
"cards_meta_data",
|
||||
"cards_title_tag",
|
||||
"cards_card_padding",
|
||||
"cards_open_new_tab",
|
||||
"cards_excerpt_color",
|
||||
"cards_image_spacing",
|
||||
"cards_title_spacing",
|
||||
"cards_excerpt_length",
|
||||
"cards_excerpt_spacing",
|
||||
"cards_card_border_width",
|
||||
"cards_thumbnail",
|
||||
"cards_show_badge",
|
||||
"cards_show_excerpt",
|
||||
"cards_show_read_more",
|
||||
"cards_read_more_text",
|
||||
"cards_read_more_alignment",
|
||||
"cards_title_color",
|
||||
"cards_card_bg_color",
|
||||
"cards_item_ratio_mobile",
|
||||
"post_type",
|
||||
"eael_ticker_tag_text",
|
||||
// EAEL Infobox / flip-box / 高级标签页 / 数据表 / 评价等(Renderer 对应组件读取)
|
||||
|
||||
Reference in New Issue
Block a user