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:
bigcode
2026-11-02 00:27:15 +08:00
parent f614d10c94
commit 27fa9e605f
8 changed files with 297 additions and 44 deletions
+1 -1
View File
@@ -52,7 +52,7 @@
| H15 | Trusted By 世界领先品牌 | heading + text + 3 行×5 列 logo 格(9 图 + 6 空占位) + 世界地图底图 | ✅ v0.10.16(审计仅剩口径/亚像素 3 项:heading 40/600/#000 + _padding 左右 10%、text 20px/_margin 顶 10/_padding 9rem、space_between_widgets=0 列内间距 0(scoped 覆盖 kit-spacing)、三行 logo 列 populated flex 纵向垂直居中 + 扣 margin(scoped auto,272-16=256)、列 margin 8px + 边框 #474DFF26 设置驱动、空占位列裸渲染(原站无 populated 盒,margin/边框 0)、logo 图 254 宽 text-align center(Elementor image widget 默认);节 margin-top 3.5%+padding 底 4.5rem;随此版块:ColumnNode 空列裸渲染、populated 语义辨析(H4 溢出=基线 100%,H11/H14/H15 扣减=scoped auto)——曾试基线改 auto 致 H4 -7.7 回归后回滚) |
| H16 | Developer Ecosystem | heading ×2 + 内嵌 section 两卡(heading+text+button/social-icons+image) | ✅ v0.10.17(审计 0 差异:节 ab35847 bg #F4F5F8 + heading 40/600/#000(_padding 10%)+副题 20px/_margin 顶 -6;两卡 680×528.4 白底圆角 6 边框 #E0E5F9、padding 48/48/16、margin 12(populated 扣减 scoped)、卡标题 24/700/#0D152E、文本 18px/_padding 3rem、Join Now 白底橙字 12px 56px、图 544×282 居中;随此版块:SocialIconsWidget 忠实重写(wrapper grid gap 10/a 44×44 官方底色 linkedin #0077B5/youtube #CD201F/facebook #3B5998/圆角 10%/白色字形)+FA Brands 字体子集(public/fonts/fa-brands-400.woff2 + 三码位 content 规则)+elementor-animation-shrink 通用类) |
| H17 | News & Updates 标题 | heading | ⬜ |
| H18 | 最新博文 *2 | posts widget ×2(标题行 + 列卡) | ⬜ |
| H18 | 最新博文 ×2 | 8bc6b68:左 posts 卡×2(3025217)+右 Latest blogs>>标题(1c96d68)+posts 列表×5(a22c185) | ✅ v0.10.18(双端节内几何/样式全等(sec 521):左 skin-cards grid 335×2/gap 16 10、卡 520 高(custom_css .ele-post-dif)、缩略图比率 0.66×内宽+img 铺满、NEWS 徽章(uppercase/白 70%/12px/圆角 4,绝对右上)、标题 18/400/#000+mb16、日期 F j,Y/#ADADAD/padding 10 12、摘要 18 词/#767676/底距 44(空摘要回退正文 18 词);右列卡 grid 630、卡 bg #F4F5F8/圆角 3、纯标题 44px 两行钳制+右侧箭头图标(custom_css);Latest blogs>> 标题 24px+#E0E5F9 下划线+橙线图背景;随此版块:PostsWidget 忠实重写(PostsDataContext 预取架构:loader 按 widget 收集 posts_include_term_ids→listPostsByTermLegacy)、posts cards 基础 CSS、slim 白名单 cards_* 全键、**数据修复** scripts/repair-content-terms.ts(从 legacy dump wp_term_relationships 补 1162 条分类关联,Blog 302/News 22——原导入仅 61/609 篇有关联)) |
| H19 | 定制 CTA | button(大按钮) | ⬜ |
## 四、其他核心页型版块(自上而下,概览)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "dusuniot-rebuild",
"version": "0.10.17",
"version": "0.10.18",
"private": true,
"type": "module",
"scripts": {
+55
View File
@@ -0,0 +1,55 @@
/**
* # 脚本 修复 posts 的分类关联(content_terms)
* 背景:导入管线只落了部分文章的分类(609 篇中 548 篇无关联),首页 H18 posts widget
* (按 Blog/News 分类查最新文章)因此查不到数据。原站完整关系在 legacy 镜像
* dusuniot-db-20261029.sql 的 wp_term_relationships INSERT 段中((post_id,term_id,0) 行)。
* 做法:从 dump 提取全部 (wp_post_id, wp_term_id) 对 → 经 contents.legacy_id /
* categories.legacy_id 映射为本库 id → 补插缺失的 content_terms(幂等)。
* 运行:bun scripts/repair-content-terms.ts
*/
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");
// 1) 提取 dump 中的关系对
const text = await Bun.file(DUMP).text();
const pairs = new Set<string>();
const re = /\((\d+),(\d+),0\)/g;
for (const m of text.matchAll(re)) {
pairs.add(`${m[1]}:${m[2]}`);
}
console.log("dump 关系对:", pairs.size);
// 2) 本库映射表:legacyId → id
const contents = await db`SELECT id, legacy_id FROM contents WHERE legacy_id IS NOT NULL`;
const contentMap = new Map<number, number>(contents.map((r) => [Number(r.legacy_id), Number(r.id)]));
const cats = await db`SELECT id, legacy_id FROM categories WHERE legacy_id IS NOT NULL`;
const catMap = new Map<number, number>(cats.map((r) => [Number(r.legacy_id), Number(r.id)]));
// 3) 已有关联集合(幂等)
const existing = await db`SELECT content_id, category_id FROM content_terms`;
const have = new Set(existing.map((r) => `${r.content_id}:${r.category_id}`));
// 4) 补插
let inserted = 0;
for (const p of pairs) {
const [wpPost, wpTerm] = p.split(":").map(Number);
const cid = contentMap.get(wpPost);
const tid = catMap.get(wpTerm);
if (!cid || !tid) continue;
if (have.has(`${cid}:${tid}`)) continue;
await db`INSERT INTO content_terms (content_id, category_id) VALUES (${cid}, ${tid})`;
have.add(`${cid}:${tid}`);
inserted++;
}
console.log(`补插 ${inserted} 条 content_terms`);
// 5) 验证:Blog(53)/News(54) 分类下的文章数
const check = await db`
SELECT ca.name, COUNT(*) AS n FROM content_terms ct
JOIN categories ca ON ca.id = ct.category_id AND ca.legacy_id IN (53, 54)
JOIN contents c ON c.id = ct.content_id AND c.type = 'post' AND c.status = 'published'
GROUP BY ca.name`;
console.log("验证:", JSON.stringify(check));
await db.end();
+91 -29
View File
@@ -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 (
<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 + "/"}
<div
className="elementor-posts-container elementor-posts elementor-posts--skin-cards elementor-grid"
style={{ display: "grid", gridTemplateColumns: `repeat(${cols}, 1fr)`, gap: `${rowGap} ${colGap}` }}
>
{p.image ? (
<div className="aspect-video overflow-hidden bg-[#f2f5f9]">
<img src={p.image} alt="" loading="lazy" className="h-full w-full object-cover" />
{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}
<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}
{showExcerpt ? (
<p
className="elementor-post__excerpt"
style={{ color: excerptColor, marginBottom: excSpacing }}
dangerouslySetInnerHTML={{ __html: postExcerpt(p) }}
/>
) : null}
</div>
</a>
))}
</div>
</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>
+26
View File
@@ -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 对应组件读取)
+34 -4
View File
@@ -4,10 +4,38 @@
*/
import { createFileRoute, Link } from "@tanstack/react-router";
import { getLayoutData, Layout } from "~/components/website/Layout";
import { ElementorRenderer, extractElementor } from "~/components/elementor/Renderer";
import { ElementorRenderer, extractElementor, type ElElement } from "~/components/elementor/Renderer";
import { ProductCard } from "~/components/website/ProductCard";
import { Button } from "~/components/ui/button";
import { getHomePage, getProductCategoryTree, listProducts, listPosts } from "~/server/data";
import { getHomePage, getProductCategoryTree, listProducts, listPosts, listPostsByTermLegacy, type PostCardItem } from "~/server/data";
/** # 函数 收集 Elementor 文档中 posts widget 的查询并预取文章(H18)
* posts widget(skin cards)按 settings.posts_include_term_ids(WP 术语 legacy_id)+
* cards_posts_per_page 查询最新文章,结果按 widget id 传给渲染器。
* 参数 meta 首页 contents.meta
* 返回值 widgetId → 文章卡数组的映射
*/
async function loadPostsWidgets(meta: Record<string, unknown> | null): Promise<Record<string, PostCardItem[]>> {
const out: Record<string, PostCardItem[]> = {};
if (!meta) return out;
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;
if (termIds.length) {
out[el.id] = await listPostsByTermLegacy(Number(termIds[0]), perPage).catch(() => []);
}
}
await walk(el.elements);
}
};
const elementor = (meta.elementor ?? null) as { elements?: ElElement[] } | ElElement[] | null;
// 文档可能为裸数组或 {elements} 包装两种形态(extractElementor 同款兼容)
await walk(Array.isArray(elementor) ? elementor : elementor?.elements);
return out;
}
export const Route = createFileRoute("/")({
loader: async () => {
@@ -20,7 +48,9 @@ export const Route = createFileRoute("/")({
listPosts(1, 3).catch(() => ({ items: [], total: 0, page: 1, pageSize: 3 })),
]);
const meta = (home?.meta ?? null) as Record<string, unknown> | null;
return { layout, home, meta, cats, products, posts };
// Elementor posts widget 预取(H18:左右两个 posts widget)
const postsData = await loadPostsWidgets(meta);
return { layout, home, meta, cats, products, posts, postsData };
},
head: ({ loaderData }) => ({
meta: [
@@ -43,7 +73,7 @@ function HomePage() {
return (
<Layout layout={data.layout}>
{elementor ? (
<ElementorRenderer doc={elementor} />
<ElementorRenderer doc={elementor} postsData={data.postsData} />
) : (
<FallbackHome cats={data.cats} products={data.products} posts={data.posts} />
)}
+48
View File
@@ -560,3 +560,51 @@ export const getLatestPosts = createServerFn({ method: "GET" }).handler(async ()
.limit(8);
return rows.map((r) => ({ title: r.title, url: "/blog/" + r.slug + "/" }));
});
/** # 类型 文章卡数据(Elementor posts widget 骨架卡) **/
export type PostCardItem = {
slug: string;
title: string;
excerpt: string | null;
publishedAt: Date | null;
imageUrl: string | null;
content: string | null;
categoryName: string | null;
};
/** # Server Function 按 WP 术语 legacy_id 取最新文章(Elementor posts widget 查询)
* 首页 H18:posts_include=terms + posts_include_term_ids=[54](News 分类 legacy 54)等;
* 经 categories.legacy_id 反查本库分类,再经 content_terms 取其下最新发布文章。
* 参数 legacyTermId WP 术语 id(legacy_id);limit 取条数
* 返回值 文章卡数组(标题/摘要/发布时间/特色图/首个分类名)
*/
export async function listPostsByTermLegacy(legacyTermId: number, limit: number) {
const [cat] = await db
.select({ id: categories.id })
.from(categories)
.where(eq(categories.legacyId, legacyTermId))
.limit(1);
if (!cat) return [] as PostCardItem[];
const rows = await db
.select({
slug: contents.slug,
title: contents.title,
excerpt: contents.excerpt,
publishedAt: contents.publishedAt,
imageUrl: media.url,
content: contents.html,
categoryName: sql<string | null>`(SELECT cat.name FROM content_terms ct JOIN categories cat ON cat.id = ct.category_id WHERE ct.content_id = ${contents.id} AND cat.taxonomy = 'category' LIMIT 1)`,
})
.from(contents)
.leftJoin(media, eq(media.id, contents.featuredMediaId))
.where(
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})`,
),
)
.orderBy(desc(contents.publishedAt))
.limit(Math.max(1, Math.min(20, limit)));
return rows as PostCardItem[];
}
+32
View File
@@ -372,6 +372,38 @@
* 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-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; }
.elementor-post__badge {
position: absolute; top: 0; right: 0; z-index: 1;
font-size: 12px; line-height: 1; padding: 7.2px 14.4px;
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__title a { color: inherit; text-decoration: none; }
.elementor-post__meta-data {
display: block; margin: 0; padding: 10px 12px;
font-size: 16px; color: #ADADAD;
}
.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; }
[data-el="a22c185"] .elementor-post__text { padding: 0 18px; }
/* H18 两列 populated 扣水平 margin(右列 680-16=664,margin 左 1rem 设置驱动) */
[data-el="1621f99"] > div, [data-el="5db48f4"] > div { width: auto !important; }
/* ===== 首页 H2 产品线入口(post-18.css 对应块;1436px 实测 2026-10-31)
* 外层 section:白→#F4F5F8 线性渐变(180deg)+ 1360 容器(SectionNode 背景已渲染);
* 中列 0.5rem 内边距;内层 e38a898 六列均分 224px,列内容 wrap padding 10px(高 107);