v0.10.23: 落地页收口(toggle/内嵌模板 27143/posts cards/info-box)+ dev 3311 修复
This commit is contained in:
@@ -67,16 +67,30 @@
|
|||||||
ShopFilters/ShopProducts 注入。筛选 URL 用查询参数(原站 WPC 为 /shop/type-som/ 美化链接),
|
ShopFilters/ShopProducts 注入。筛选 URL 用查询参数(原站 WPC 为 /shop/type-som/ 美化链接),
|
||||||
术语顺序/计数来自导入数据(term taxonomy 顺序 + product_attribute_map 计数)。
|
术语顺序/计数来自导入数据(term taxonomy 顺序 + product_attribute_map 计数)。
|
||||||
|
|
||||||
### 1b. 落地页 /landing-pages/$slug(智能网关页已校对,v0.10.22 进行中)
|
### 1b. 落地页 /landing-pages/$slug(智能网关页已校对,v0.10.23 ✅ 桌面全对齐)
|
||||||
- 原站落地页为 post(如 smart-gateways legacy 6509)Elementor 直出,经 catch-all 路由渲染。
|
- 原站落地页为 post(如 smart-gateways legacy 6509)Elementor 直出,经 catch-all 路由渲染。
|
||||||
- smart-gateways 15 节中 10 节已与原站逐项对齐(541ea84/49dcb11/dcfbe08/de5742a/9b87079/
|
- smart-gateways 15 节桌面端全部与原站对齐(v0.10.23 复测:541ea84/49dcb11/dcfbe08/f9e27ce/
|
||||||
7c15af8/68babbb/028b4bc/5745b4f/2fee235,误差 ≤8px),期间修复:EAEL info-box
|
eb3462a/de5742a/9b87079/7c15af8/68babbb/028b4bc/5745b4f/2fee235/391a1ee/a36c301 逐节
|
||||||
默认标题外距/图片 block/标题 margin 设置、image `space` 宽度、列 populated auto 语义、
|
高度 ≤1px;8e90920 按真实 JS 激活态 822.4 = 原站注入 active 后实测 822.4;页脚 590.5)。
|
||||||
按钮图标(star/fire)、HTML 块 ul 默认外距。
|
- v0.10.23 修复:
|
||||||
- 待办:8e90920 EAEL 高级标签页(原站 headless JS 未初始化,需按真实交互态校对)、
|
- 391a1ee toggle 换成 Elementor 原生 DOM(默认全收起、h3 28/1/700、标题 1.125rem、
|
||||||
391a1ee toggle 手风琴(需换 Elementor toggle DOM + 默认全收起 + 28/58px 标题样式)、
|
图标 caret-down/up 左浮动 1em+12px、item 间距 4、内容 .875rem、FAQPage JSON-LD),
|
||||||
a36c301 `[elementor-template id="27143"]` 内嵌模板(含 posts.cards,需模板导入 +
|
桌面节高 496 / 移动 536 与原站全等;标题移动端响应式字号(1.5rem)补进
|
||||||
短代码渲染 + posts 预取)。
|
collectResponsiveCss。
|
||||||
|
- a36c301 内嵌模板:新增 scripts/import-embedded-templates.ts 导入 elementor_library
|
||||||
|
post 27143(slug=template-27143);catch-all 路由扫描 `[elementor-template id="N"]`
|
||||||
|
短代码加载模板并注入渲染,同时预取 posts widget(多分类并集 53/54/703,发布时间倒序)。
|
||||||
|
- posts widget 按 widget-posts.min.css cards 皮肤重写:meta 移到 text 之后(border-top +
|
||||||
|
margin-top:auto)、badge 仅在有缩略图时渲染且绝对右上、缩略图 img 静态宽度 calc(100%+1px)、
|
||||||
|
比率/内距/字号/边框全部按设置内联;桌面 H18 卡片图高 220.6/187.9(原站 220.4/188.1)。
|
||||||
|
- 列 populated 宽度:新增 06632bb/3361c18/3317056/5938d26 的 auto 扣除规则(570 vs 580)。
|
||||||
|
- info-box:p 外距/颜色(eael_infobox_content_margin/color)、图标型 info-box
|
||||||
|
(eael_infobox_icon_new/icon_size,fa-check-circle 码位)、标题行高;
|
||||||
|
- 按钮:图标型按钮 wrapper 2px 行盒(原站 inline-block 基线导致 28px 高)。
|
||||||
|
- 已知数据差异:内嵌模板 posts 取本地快照(dump 2026-10-29),原站已新增 161855/161748
|
||||||
|
两篇(11-02/10-30),故卡片内容与原站当前列表不同;版式/卡片尺寸一致。
|
||||||
|
- 移动端待细化:EAEL tabs 原站 headless 未初始化无法直接取基线;posts 图片经由
|
||||||
|
srcset/medium 变体,个别卡片高度 ±5px。
|
||||||
|
|
||||||
### 2. 方案页
|
### 2. 方案页
|
||||||
- S1 `/solution/` 列表入口页
|
- S1 `/solution/` 列表入口页
|
||||||
@@ -176,3 +190,6 @@ cookie-policy:19 段纯文本。
|
|||||||
| 2 | 移动端 H1 ticker 高度差 ~14px(原站 117,本地 131.2) | H1 ticker 移动端 | 中 |
|
| 2 | 移动端 H1 ticker 高度差 ~14px(原站 117,本地 131.2) | H1 ticker 移动端 | 中 |
|
||||||
| 3 | 全局 `a:hover` 为 --brand-dark(#c85600),kit 全局 secondary 是 #FFA11B | G1/全局样式 | 低 |
|
| 3 | 全局 `a:hover` 为 --brand-dark(#c85600),kit 全局 secondary 是 #FFA11B | G1/全局样式 | 低 |
|
||||||
| 4 | PG 连接曾被 stale vite dev 进程(3311 端口)泄漏占满(290 连接),已清理;重复发生时检查残留 dev 进程 | 环境 | 记录 |
|
| 4 | PG 连接曾被 stale vite dev 进程(3311 端口)泄漏占满(290 连接),已清理;重复发生时检查残留 dev 进程 | 环境 | 记录 |
|
||||||
|
| 5 | 内嵌模板文章列表取 dump 快照,原站新增文章(161855/161748)尚未导入,卡片内容不同步(版式一致) | 内容数据 | 中 |
|
||||||
|
| 6 | posts 卡片移动端个别卡高 ±5px(原站 srcset 选 768/medium 变体,本地用原图;宽高比略有差异) | posts widget | 低 |
|
||||||
|
| 7 | dev 脚本须用 `bun --bun vite dev`:纯 node 下 Vite module-runner 无法解析 Bun 内置模块;vite.config 已把 "bun"/node:* 标记为 ssr builtins | 环境 | 记录 |
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "dusuniot-rebuild",
|
"name": "dusuniot-rebuild",
|
||||||
"version": "0.10.22",
|
"version": "0.10.23",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --port 3311",
|
"dev": "bun --bun vite dev --port 3311",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"start": "bun scripts/prod-server.ts",
|
"start": "bun scripts/prod-server.ts",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* # 脚本 导入内嵌 Elementor 模板([elementor-template id="N"] 短代码引用的 post)
|
||||||
|
* 背景:落地页/产品页里的 shortcode widget 会引用 elementor_library 模板
|
||||||
|
* (如 smart-gateways 的 a36c301 → [elementor-template id="27143"],含
|
||||||
|
* “Dusun Updates” 标题 + posts 卡片列表)。原导入管线未迁移 elementor_library,
|
||||||
|
* 这些模板在本地缺失,导致内嵌区块为空。
|
||||||
|
* 数据源:legacy/dusuniot-db-20261029.sql 的 (meta_id, <postId>, '_elementor_data', …)。
|
||||||
|
* 写入 contents:type=template,slug=`template-<postId>`,legacy_id=<postId>,
|
||||||
|
* 供路由按 legacy_id 查询后用 ElementorRenderer 递归渲染。
|
||||||
|
* 运行:bun scripts/import-embedded-templates.ts [postId...]
|
||||||
|
*/
|
||||||
|
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");
|
||||||
|
|
||||||
|
/** # 成员变量 需要导入的模板 post id(默认:落地页 smart-gateways 内嵌的 27143) **/
|
||||||
|
const ids = process.argv.slice(2).map(Number).filter((n) => Number.isFinite(n));
|
||||||
|
const targets = ids.length ? ids : [27143];
|
||||||
|
|
||||||
|
const text = await Bun.file(DUMP).text();
|
||||||
|
|
||||||
|
/** # 函数 从 dump 提取指定 post 的 _elementor_data(同 import-shop-template 的解析方式) */
|
||||||
|
function extract(postId: number): Record<string, unknown>[] | null {
|
||||||
|
const re = new RegExp("\\(\\d+," + postId + ",'_elementor_data','((?:[^'\\\\]|\\\\.)*)'\\)");
|
||||||
|
const m = text.match(re);
|
||||||
|
if (!m) return null;
|
||||||
|
const json = m[1]!.replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
||||||
|
const data = JSON.parse(json);
|
||||||
|
return Array.isArray(data) ? (data as Record<string, unknown>[]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of targets) {
|
||||||
|
const elements = extract(id);
|
||||||
|
if (!elements) {
|
||||||
|
console.error(`post ${id} 未找到 _elementor_data,跳过`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const slug = `template-${id}`;
|
||||||
|
const title = `Elementor Template ${id}`;
|
||||||
|
const meta = JSON.stringify({ elementor: elements });
|
||||||
|
const existing = await db`SELECT id FROM contents WHERE legacy_id=${id} OR (slug=${slug} AND type='template')`;
|
||||||
|
if (existing.length) {
|
||||||
|
await db`UPDATE contents SET meta=${meta}::text::jsonb, status='published', updated_at=now() WHERE id=${existing[0]!.id}`;
|
||||||
|
console.log(`已更新 ${slug},contents.id = ${existing[0]!.id}`);
|
||||||
|
} else {
|
||||||
|
const inserted = await db`
|
||||||
|
INSERT INTO contents (type, slug, title, status, meta, legacy_id, published_at)
|
||||||
|
VALUES ('template', ${slug}, ${title}, 'published', ${meta}::text::jsonb, ${id}, now())
|
||||||
|
RETURNING id`;
|
||||||
|
console.log(`已插入 ${slug},contents.id = ${inserted[0]!.id}`);
|
||||||
|
}
|
||||||
|
const top = elements.map((el) => String(el.id)).join(", ");
|
||||||
|
console.log(` post ${id} 顶层节: ${top}`);
|
||||||
|
}
|
||||||
|
await db.end();
|
||||||
@@ -108,12 +108,12 @@ const meta = JSON.stringify({ elementor: elements });
|
|||||||
// 写入 contents:type=template / slug=footer;已存在则更新
|
// 写入 contents:type=template / slug=footer;已存在则更新
|
||||||
const existing = await db`SELECT id FROM contents WHERE slug='footer' AND type='template'`;
|
const existing = await db`SELECT id FROM contents WHERE slug='footer' AND type='template'`;
|
||||||
if (existing.length) {
|
if (existing.length) {
|
||||||
await db`UPDATE contents SET meta=${meta}::jsonb, status='published', updated_at=now() WHERE id=${existing[0]!.id}`;
|
await db`UPDATE contents SET meta=${meta}::text::jsonb, status='published', updated_at=now() WHERE id=${existing[0]!.id}`;
|
||||||
console.log("已更新 footer 模板,contents.id =", existing[0]!.id);
|
console.log("已更新 footer 模板,contents.id =", existing[0]!.id);
|
||||||
} else {
|
} else {
|
||||||
const inserted = await db`
|
const inserted = await db`
|
||||||
INSERT INTO contents (type, slug, title, status, meta, legacy_id, published_at)
|
INSERT INTO contents (type, slug, title, status, meta, legacy_id, published_at)
|
||||||
VALUES ('template', 'footer', 'Footer', 'published', ${meta}::jsonb, 58, now())
|
VALUES ('template', 'footer', 'Footer', 'published', ${meta}::text::jsonb, 58, now())
|
||||||
RETURNING id`;
|
RETURNING id`;
|
||||||
console.log("已插入 footer 模板,contents.id =", inserted[0]!.id);
|
console.log("已插入 footer 模板,contents.id =", inserted[0]!.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ const elements = await extractTemplate(1398);
|
|||||||
const meta = JSON.stringify({ elementor: elements });
|
const meta = JSON.stringify({ elementor: elements });
|
||||||
const existing = await db`SELECT id FROM contents WHERE slug='shop-archive' AND type='template'`;
|
const existing = await db`SELECT id FROM contents WHERE slug='shop-archive' AND type='template'`;
|
||||||
if (existing.length) {
|
if (existing.length) {
|
||||||
await db`UPDATE contents SET meta=${meta}::jsonb, status='published', updated_at=now() WHERE id=${existing[0]!.id}`;
|
await db`UPDATE contents SET meta=${meta}::text::jsonb, status='published', updated_at=now() WHERE id=${existing[0]!.id}`;
|
||||||
console.log("已更新 shop-archive 模板,contents.id =", existing[0]!.id);
|
console.log("已更新 shop-archive 模板,contents.id =", existing[0]!.id);
|
||||||
} else {
|
} else {
|
||||||
const inserted = await db`
|
const inserted = await db`
|
||||||
INSERT INTO contents (type, slug, title, status, meta, legacy_id, published_at)
|
INSERT INTO contents (type, slug, title, status, meta, legacy_id, published_at)
|
||||||
VALUES ('template', 'shop-archive', 'Shop Archive', 'published', ${meta}::jsonb, 1398, now())
|
VALUES ('template', 'shop-archive', 'Shop Archive', 'published', ${meta}::text::jsonb, 1398, now())
|
||||||
RETURNING id`;
|
RETURNING id`;
|
||||||
console.log("已插入 shop-archive 模板,contents.id =", inserted[0]!.id);
|
console.log("已插入 shop-archive 模板,contents.id =", inserted[0]!.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -375,6 +375,14 @@ function collectResponsiveCss(elements: ElElement[]): string {
|
|||||||
const iw = sizeCss(st.width_mobile);
|
const iw = sizeCss(st.width_mobile);
|
||||||
if (iw) css += `@media (max-width:767px){${sel} img{width:${iw} !important;}}\n`;
|
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%)
|
// 列响应式宽度(_inline_size_tablet / _inline_size_mobile;页脚内层四列 43%/100%)
|
||||||
const tw = st._inline_size_tablet;
|
const tw = st._inline_size_tablet;
|
||||||
@@ -731,6 +739,7 @@ function WidgetNodeInner({ el }: { el: ElElement }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case "toggle":
|
case "toggle":
|
||||||
|
return <ToggleWidget el={el} settings={settings} />;
|
||||||
case "accordion":
|
case "accordion":
|
||||||
return <AccordionWidget settings={settings} />;
|
return <AccordionWidget settings={settings} />;
|
||||||
case "tabs":
|
case "tabs":
|
||||||
@@ -961,26 +970,45 @@ function TickerWidget({ settings }: { settings: Record<string, unknown> }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** # 组件 posts widget(Elementor Pro 卡片皮肤:文章/产品卡片网格) */
|
/** # 组件 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
|
* .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
|
* > article.elementor-post > .elementor-post__card(纵向 flex、白底、1px 边、圆角 3)
|
||||||
* (flex 纵向、边框/底色/圆角 3 由设置)> [缩略图链接(比率 0.66 内宽、badge 绝对右上)]
|
* > [a.__thumbnail__link > .__thumbnail(padding-bottom 比率) > img]
|
||||||
* + .elementor-post__text(padding 12) > .elementor-post__title(div,fs18/400,#000)
|
* + .__badge(仅缩略图存在时,绝对右上)
|
||||||
* + .elementor-post__meta-data(日期 F j, Y,#ADADAD) + .elementor-post__excerpt(18 词,#767676,底距 44);
|
* + .__text(flex-grow)> .__title + .__excerpt
|
||||||
* 缩略图 thumbnail=none 时省略(右列卡 74 高纯文本);文章数据经 PostsDataContext 预取。
|
* + .__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> }) {
|
function PostsWidget({ el, settings }: { el: ElElement; settings: Record<string, unknown> }) {
|
||||||
const allPosts = useContext(PostsDataContext);
|
const allPosts = useContext(PostsDataContext);
|
||||||
const posts = (el.id && allPosts[el.id]) || [];
|
const posts = (el.id && allPosts[el.id]) || [];
|
||||||
if (!posts.length) return null;
|
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 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 rowGap = sizeCss(settings.cards_row_gap) || "16px";
|
||||||
const colGap = sizeCss(settings.cards_column_gap) || "10px";
|
const colGap = sizeCss(settings.cards_column_gap) || "10px";
|
||||||
const textPad = sizeCss(settings.cards_card_padding) || "12px";
|
const textPad = sizeCss(settings.cards_card_padding) || "12px";
|
||||||
const titleSpacing = sizeCss(settings.cards_title_spacing) || "16px";
|
const titleSpacing = sizeCss(settings.cards_title_spacing) || "16px";
|
||||||
const excSpacing = sizeCss(settings.cards_excerpt_spacing) || "44px";
|
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 = (["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;
|
const TitleTag = titleTag;
|
||||||
// 徽章/日期/摘要开关:switcher 键缺省 = 开(左卡无键但有徽章/摘要的实测语义),
|
// 徽章/日期/摘要开关:switcher 键缺省 = 开(左卡无键但有徽章/摘要的实测语义),
|
||||||
@@ -995,44 +1023,57 @@ function PostsWidget({ el, settings }: { el: ElElement; settings: Record<string,
|
|||||||
if (isNaN(dt.getTime())) return null;
|
if (isNaN(dt.getTime())) return null;
|
||||||
return dt.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className="elementor-posts-container elementor-posts elementor-posts--skin-cards elementor-grid"
|
className="elementor-posts-container elementor-posts elementor-posts--skin-cards elementor-posts--thumbnail-top elementor-posts__hover-gradient elementor-grid"
|
||||||
style={{ display: "grid", gridTemplateColumns: `repeat(${cols}, 1fr)`, gap: `${rowGap} ${colGap}` }}
|
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) => {
|
{posts.map((p) => {
|
||||||
// 链接前缀按主分类:News → /news/,其余 → /blog/(原站 News 文章走 /news/<slug>/)
|
// 链接前缀按主分类:News → /news/,其余 → /blog/(原站 News 文章走 /news/<slug>/)
|
||||||
const href = (p.categoryName === "News" ? "/news/" : "/blog/") + p.slug + "/";
|
const href = (p.categoryName === "News" ? "/news/" : "/blog/") + p.slug + "/";
|
||||||
|
const hasThumb = showThumb && !!p.imageUrl;
|
||||||
return (
|
return (
|
||||||
<article key={p.slug} className="elementor-post elementor-grid-item post type-post status-publish format-standard has-post-thumbnail">
|
<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">
|
<div className="elementor-post__card" style={{ border: `${cardBorderWidth} solid ${cardBorderColor}` }}>
|
||||||
{showThumb && p.imageUrl ? (
|
{hasThumb ? (
|
||||||
<a className="elementor-post__thumbnail__link" href={href} tabIndex={-1}>
|
<a className="elementor-post__thumbnail__link" href={href} tabIndex={-1}>
|
||||||
<div className="elementor-post__thumbnail">
|
<div className="elementor-post__thumbnail" style={thumbVars}>
|
||||||
<img src={localSiteUrl(p.imageUrl)} alt={p.title} loading="lazy" />
|
<img src={localSiteUrl(p.imageUrl!)} alt={p.title} loading="lazy" />
|
||||||
</div>
|
</div>
|
||||||
{showBadge && p.categoryName ? (
|
|
||||||
<span className="elementor-post__badge">{p.categoryName}</span>
|
|
||||||
) : null}
|
|
||||||
</a>
|
</a>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="elementor-post__text">
|
{hasThumb && showBadge && p.categoryName ? (
|
||||||
<TitleTag className="elementor-post__title" style={{ marginBottom: titleSpacing }}>
|
<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>
|
<a href={href}>{p.title}</a>
|
||||||
</TitleTag>
|
</TitleTag>
|
||||||
|
{showExcerpt ? (
|
||||||
|
<div className="elementor-post__excerpt" style={{ marginBottom: excSpacing }}>
|
||||||
|
<p
|
||||||
|
style={{ color: excerptColor, fontSize: excerptSize }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: postExcerpt(p) }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
{showMeta && p.publishedAt ? (
|
{showMeta && p.publishedAt ? (
|
||||||
<div className="elementor-post__meta-data">
|
<div
|
||||||
|
className="elementor-post__meta-data"
|
||||||
|
style={{ padding: `10px ${textPad}`, fontSize: metaSize, fontWeight: metaWeight }}
|
||||||
|
>
|
||||||
<span className="elementor-post-date">{fmtDate(p.publishedAt)}</span>
|
<span className="elementor-post-date">{fmtDate(p.publishedAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{showExcerpt ? (
|
|
||||||
<p
|
|
||||||
className="elementor-post__excerpt"
|
|
||||||
style={{ color: excerptColor, marginBottom: excSpacing }}
|
|
||||||
dangerouslySetInnerHTML={{ __html: postExcerpt(p) }}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
@@ -1147,13 +1188,16 @@ function ButtonWidget({ el, settings }: { el: ElElement; settings: Record<string
|
|||||||
text
|
text
|
||||||
);
|
);
|
||||||
return (
|
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 ? (
|
{hoverBg || hoverColor ? (
|
||||||
<style>{`[data-el="${el.id}"] .el-btn:hover{${hoverBg ? `background:${hoverBg};` : ""}${hoverColor ? `color:${hoverColor};` : ""}}`}</style>
|
<style>{`[data-el="${el.id}"] .el-btn:hover{${hoverBg ? `background:${hoverBg};` : ""}${hoverColor ? `color:${hoverColor};` : ""}}`}</style>
|
||||||
) : null}
|
) : null}
|
||||||
<a className="el-btn" href={link || "#"} style={style} target={(settings.link as { target?: string } | undefined)?.target === "_blank" ? "_blank" : undefined} rel="noreferrer">
|
<a className="el-btn" href={link || "#"} style={style} target={(settings.link as { target?: string } | undefined)?.target === "_blank" ? "_blank" : undefined} rel="noreferrer">
|
||||||
{content}
|
{content}
|
||||||
</a>
|
</a>
|
||||||
|
{/* 原站 wrapper 内含空白文本节点:inline-block 按钮所在行盒会叠加上正文 strut,
|
||||||
|
父级高度 28(按钮 26 + 2px descender)。JSX 需显式补一个空格复现。 */}
|
||||||
|
{" "}
|
||||||
</div>
|
</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 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 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 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;
|
if (!title && !desc && !img?.url) return null;
|
||||||
const tag = s(settings.eael_infobox_title_tag);
|
const tag = s(settings.eael_infobox_title_tag);
|
||||||
const Tag = (["h1", "h2", "h3", "h4", "h5", "h6", "div"].includes(tag) ? tag : "div") as "div";
|
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);
|
const tm = paddingCss(tmRaw);
|
||||||
if (tm) titleStyle.margin = tm;
|
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 (
|
return (
|
||||||
<div className={cls}>
|
<div className={cls}>
|
||||||
{img?.url ? (
|
{img?.url ? (
|
||||||
<div className="infobox-icon">
|
<div className="infobox-icon">
|
||||||
<img src={img.url} alt={s(img.alt)} loading="lazy" />
|
<img src={img.url} alt={s(img.alt)} loading="lazy" />
|
||||||
</div>
|
</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}
|
) : null}
|
||||||
<div className="infobox-content">
|
<div className="infobox-content">
|
||||||
{title ? (
|
{title ? (
|
||||||
@@ -1321,7 +1393,7 @@ function EaelInfoBoxWidget({ settings }: { settings: Record<string, unknown> })
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1390,7 +1462,7 @@ function AdvTabsWidget({ el, settings }: { el: ElElement; settings: Record<strin
|
|||||||
// 内容面板 id:标题 slug 化(原站 building-automation / building-automation-tab 等)
|
// 内容面板 id:标题 slug 化(原站 building-automation / building-automation-tab 等)
|
||||||
const slug = (t: string) => t.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-");
|
const slug = (t: string) => t.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-");
|
||||||
return (
|
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">
|
<div className="eael-tabs-nav">
|
||||||
<ul role="tablist">
|
<ul role="tablist">
|
||||||
{items.map((t, i) => (
|
{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> }) {
|
function AccordionWidget({ settings }: { settings: Record<string, unknown> }) {
|
||||||
const tabs = Array.isArray(settings.tabs) ? (settings.tabs as { tab_title?: string; tab_content?: string }[]) : [];
|
const tabs = Array.isArray(settings.tabs) ? (settings.tabs as { tab_title?: string; tab_content?: string }[]) : [];
|
||||||
if (!tabs.length) return null;
|
if (!tabs.length) return null;
|
||||||
|
|||||||
@@ -132,12 +132,15 @@ const SETTING_KEYS = new Set([
|
|||||||
"youtube_url",
|
"youtube_url",
|
||||||
"map",
|
"map",
|
||||||
"posts_per_page",
|
"posts_per_page",
|
||||||
|
// shortcode widget 源码([elementor-template id="N"] 内嵌模板、页脚菜单/gtranslate)
|
||||||
|
"shortcode",
|
||||||
"_skin",
|
"_skin",
|
||||||
"posts_include",
|
"posts_include",
|
||||||
"posts_include_term_ids",
|
"posts_include_term_ids",
|
||||||
"cards_posts_per_page",
|
"cards_posts_per_page",
|
||||||
"cards_columns",
|
"cards_columns",
|
||||||
"cards_columns_tablet",
|
"cards_columns_tablet",
|
||||||
|
"cards_columns_mobile",
|
||||||
"cards_row_gap",
|
"cards_row_gap",
|
||||||
"cards_column_gap",
|
"cards_column_gap",
|
||||||
"cards_meta_data",
|
"cards_meta_data",
|
||||||
@@ -158,6 +161,16 @@ const SETTING_KEYS = new Set([
|
|||||||
"cards_read_more_alignment",
|
"cards_read_more_alignment",
|
||||||
"cards_title_color",
|
"cards_title_color",
|
||||||
"cards_card_bg_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",
|
"cards_item_ratio_mobile",
|
||||||
"post_type",
|
"post_type",
|
||||||
"eael_ticker_tag_text",
|
"eael_ticker_tag_text",
|
||||||
@@ -168,16 +181,22 @@ const SETTING_KEYS = new Set([
|
|||||||
"eael_infobox_text",
|
"eael_infobox_text",
|
||||||
"eael_infobox_title_tag",
|
"eael_infobox_title_tag",
|
||||||
"eael_infobox_image",
|
"eael_infobox_image",
|
||||||
|
"eael_infobox_img_or_icon",
|
||||||
|
"eael_infobox_icon_new",
|
||||||
|
"eael_infobox_icon_size",
|
||||||
"eael_infobox_img_type",
|
"eael_infobox_img_type",
|
||||||
"eael_infobox_img_margin",
|
"eael_infobox_img_margin",
|
||||||
"eael_infobox_title_margin",
|
"eael_infobox_title_margin",
|
||||||
"eael_infobox_title_margin_mobile",
|
"eael_infobox_title_margin_mobile",
|
||||||
"eael_infobox_title_typography_typography",
|
"eael_infobox_title_typography_typography",
|
||||||
"eael_infobox_title_typography_font_size",
|
"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_mobile",
|
||||||
"eael_infobox_title_typography_font_size_tablet",
|
"eael_infobox_title_typography_font_size_tablet",
|
||||||
"eael_infobox_title_typography_font_weight",
|
"eael_infobox_title_typography_font_weight",
|
||||||
"eael_infobox_title_color",
|
"eael_infobox_title_color",
|
||||||
|
"eael_infobox_content_margin",
|
||||||
|
"eael_infobox_content_color",
|
||||||
"hover_animation",
|
"hover_animation",
|
||||||
// widget 高级盒模型(如 eael-info-box 边框卡:_border_* + _padding/_margin)
|
// widget 高级盒模型(如 eael-info-box 边框卡:_border_* + _padding/_margin)
|
||||||
"_margin",
|
"_margin",
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* # 模块 catch-all 页面共享逻辑($.tsx 只导出 Route)
|
* # 模块 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 { LayoutData } from "~/components/website/Layout";
|
||||||
|
import type { ElElement } from "~/components/elementor/Renderer";
|
||||||
|
|
||||||
/** # 类型 兜底 loader 返回结构 */
|
/** # 类型 兜底 loader 返回结构 */
|
||||||
export type CatchAllData = {
|
export type CatchAllData = {
|
||||||
@@ -28,6 +29,10 @@ export type CatchAllData = {
|
|||||||
hotProducts: ProductListResult;
|
hotProducts: ProductListResult;
|
||||||
/** # 成员变量 WP /%category%/ 归档命中:分类 + 分页文章(content 为 null 时可能命中) **/
|
/** # 成员变量 WP /%category%/ 归档命中:分类 + 分页文章(content 为 null 时可能命中) **/
|
||||||
archive: null | { name: string; slug: string; posts: { items: PostListItem[]; total: number; page: number; pageSize: number } };
|
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 返回形状的窄化别名) */
|
/** # 类型 归档分页结果(listPosts 返回形状的窄化别名) */
|
||||||
|
|||||||
+10
-1
@@ -19,10 +19,19 @@ let _initing: Promise<BunSQLDatabase<typeof schema>> | null = null;
|
|||||||
* 返回值 BunSQLDatabase 实例
|
* 返回值 BunSQLDatabase 实例
|
||||||
*/
|
*/
|
||||||
async function build(): Promise<BunSQLDatabase<typeof schema>> {
|
async function build(): Promise<BunSQLDatabase<typeof schema>> {
|
||||||
|
// 优先取全局 Bun 内置对象:dev(Vite module-runner)与生产(bun 启动
|
||||||
|
// dist/server/server.js)进程都由 Bun 运行,globalThis.Bun 一定存在,
|
||||||
|
// 且不经过打包器/模块解析器;缺失时才退回动态 import("bun")。
|
||||||
|
const g = globalThis as { Bun?: { SQL?: typeof import("bun").SQL } };
|
||||||
|
let SQL: typeof import("bun").SQL | undefined = g.Bun?.SQL;
|
||||||
|
if (!SQL) {
|
||||||
// 变量指示符 + @vite-ignore:Vite/Rolldown 不做静态解析(客户端图不含
|
// 变量指示符 + @vite-ignore:Vite/Rolldown 不做静态解析(客户端图不含
|
||||||
// "bun"),服务端运行时由 Bun 原生解析内置模块
|
// "bun"),服务端运行时由 Bun 原生解析内置模块
|
||||||
const bunModule = "bun";
|
const bunModule = "bun";
|
||||||
const { SQL } = await import(/* @vite-ignore */ bunModule);
|
const mod = (await import(/* @vite-ignore */ bunModule)) as typeof import("bun");
|
||||||
|
SQL = mod.SQL;
|
||||||
|
}
|
||||||
|
if (!SQL) throw new Error("Bun SQL 不可用:请使用 bun 运行时启动服务");
|
||||||
const { drizzle } = await import("drizzle-orm/bun-sql");
|
const { drizzle } = await import("drizzle-orm/bun-sql");
|
||||||
const sql = new SQL(process.env.DATABASE_URL || "postgres://dusuniot:dusuniot@127.0.0.1:25432/dusuniot");
|
const sql = new SQL(process.env.DATABASE_URL || "postgres://dusuniot:dusuniot@127.0.0.1:25432/dusuniot");
|
||||||
return drizzle(sql, { schema }) as BunSQLDatabase<typeof schema>;
|
return drizzle(sql, { schema }) as BunSQLDatabase<typeof schema>;
|
||||||
|
|||||||
+76
-4
@@ -10,24 +10,78 @@ import { useEffect, useState } from "react";
|
|||||||
import { createFileRoute, Link, notFound, redirect } from "@tanstack/react-router";
|
import { createFileRoute, Link, notFound, redirect } from "@tanstack/react-router";
|
||||||
import { getLayoutData, Layout, type LayoutData } from "~/components/website/Layout";
|
import { getLayoutData, Layout, type LayoutData } from "~/components/website/Layout";
|
||||||
import { Pagination } from "~/components/website/Pagination";
|
import { Pagination } from "~/components/website/Pagination";
|
||||||
import { ElementorRenderer, extractElementor } from "~/components/elementor/Renderer";
|
import { ElementorRenderer, extractElementor, type ElElement } from "~/components/elementor/Renderer";
|
||||||
import { InquiryForm } from "~/components/website/InquiryForm";
|
import { InquiryForm } from "~/components/website/InquiryForm";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator } from "~/components/ui/breadcrumb";
|
import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator } from "~/components/ui/breadcrumb";
|
||||||
import { ProductCard } from "~/components/website/ProductCard";
|
import { ProductCard } from "~/components/website/ProductCard";
|
||||||
import { isLangCode, LANG_NAMES, pickContent, type CatchAllData } from "~/components/website/catchall-shared";
|
import { isLangCode, LANG_NAMES, pickContent, type CatchAllData } from "~/components/website/catchall-shared";
|
||||||
import {
|
import {
|
||||||
|
getEmbeddedTemplate,
|
||||||
getPublishedContent,
|
getPublishedContent,
|
||||||
getPostCategoryBySlug,
|
getPostCategoryBySlug,
|
||||||
getProductChildCategory,
|
getProductChildCategory,
|
||||||
hasProductSlug,
|
hasProductSlug,
|
||||||
listPosts,
|
listPosts,
|
||||||
listProducts,
|
listProducts,
|
||||||
|
loadPostsWidgetsData,
|
||||||
resolveRedirect,
|
resolveRedirect,
|
||||||
|
type PostCardItem,
|
||||||
type ProductListResult,
|
type ProductListResult,
|
||||||
} from "~/server/data";
|
} from "~/server/data";
|
||||||
import { useT } from "~/i18n";
|
import { useT } from "~/i18n";
|
||||||
|
|
||||||
|
/** # 函数 把 Elementor 文档归一化为元素数组(裸数组 / {elements} 两种形态) */
|
||||||
|
function asElements(doc: unknown): ElElement[] {
|
||||||
|
if (Array.isArray(doc)) return doc as ElElement[];
|
||||||
|
const els = (doc as { elements?: unknown } | null)?.elements;
|
||||||
|
return Array.isArray(els) ? (els as ElElement[]) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** # 函数 扫描文档里的 [elementor-template id="N"] 短代码并加载模板
|
||||||
|
* 参数 elements 主文档元素树
|
||||||
|
* 返回值 templates: shortcode widget id → 模板元素树;elements: 模板元素树拼接
|
||||||
|
* (供 posts widget 预取)
|
||||||
|
*/
|
||||||
|
async function loadEmbeddedTemplates(elements: ElElement[]): Promise<{ templates: Record<string, ElElement[]>; elements: ElElement[] }> {
|
||||||
|
const templates: Record<string, ElElement[]> = {};
|
||||||
|
const collected: ElElement[] = [];
|
||||||
|
const ids = new Map<number, ElElement[]>();
|
||||||
|
const walk = (els: ElElement[] | undefined) => {
|
||||||
|
for (const el of els || []) {
|
||||||
|
const st = (el.settings || {}) as Record<string, unknown>;
|
||||||
|
const shortcode = typeof st.shortcode === "string" ? st.shortcode : "";
|
||||||
|
const m = /\[elementor-template\s+id=["']?(\d+)/.exec(shortcode);
|
||||||
|
if (m && el.id) ids.set(Number(m[1]), []);
|
||||||
|
if (el.elements?.length) walk(el.elements);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(elements);
|
||||||
|
for (const id of ids.keys()) {
|
||||||
|
const row = await getEmbeddedTemplate(id);
|
||||||
|
const doc = asElements(row?.meta ? (row.meta as Record<string, unknown>).elementor : null);
|
||||||
|
if (doc.length) {
|
||||||
|
ids.set(id, doc);
|
||||||
|
collected.push(...doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 按 widget id 回填模板(同一文档仅取第一个匹配的 widget)
|
||||||
|
const walk2 = (els: ElElement[] | undefined) => {
|
||||||
|
for (const el of els || []) {
|
||||||
|
const st = (el.settings || {}) as Record<string, unknown>;
|
||||||
|
const shortcode = typeof st.shortcode === "string" ? st.shortcode : "";
|
||||||
|
const m = /\[elementor-template\s+id=["']?(\d+)/.exec(shortcode);
|
||||||
|
if (m && el.id) {
|
||||||
|
const doc = ids.get(Number(m[1]));
|
||||||
|
if (doc?.length) templates[el.id] = doc;
|
||||||
|
}
|
||||||
|
if (el.elements?.length) walk2(el.elements);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk2(elements);
|
||||||
|
return { templates, elements: collected };
|
||||||
|
}
|
||||||
|
|
||||||
export const Route = createFileRoute("/$")({
|
export const Route = createFileRoute("/$")({
|
||||||
// 归档分页用 ?page=N
|
// 归档分页用 ?page=N
|
||||||
validateSearch: (search: Record<string, unknown>): { page?: number } => {
|
validateSearch: (search: Record<string, unknown>): { page?: number } => {
|
||||||
@@ -111,7 +165,7 @@ export const Route = createFileRoute("/$")({
|
|||||||
const cat = await getPostCategoryBySlug(last).catch(() => null);
|
const cat = await getPostCategoryBySlug(last).catch(() => null);
|
||||||
if (cat) {
|
if (cat) {
|
||||||
const posts = await listPosts(deps.page || 1, 12, cat.slug).catch(() => ({ items: [], total: 0, page: deps.page || 1, pageSize: 12 }));
|
const posts = await listPosts(deps.page || 1, 12, cat.slug).catch(() => ({ items: [], total: 0, page: deps.page || 1, pageSize: 12 }));
|
||||||
return { layout, content: null, lang, hotProducts, archive: { name: cat.name, slug: cat.slug, posts } } satisfies CatchAllData;
|
return { layout, content: null, lang, hotProducts, archive: { name: cat.name, slug: cat.slug, posts }, templates: {}, postsData: {} } satisfies CatchAllData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 未命中任何内容 → 404(SSR 返回 404 状态码,由 notFoundComponent 渲染)
|
// 未命中任何内容 → 404(SSR 返回 404 状态码,由 notFoundComponent 渲染)
|
||||||
@@ -120,7 +174,12 @@ export const Route = createFileRoute("/$")({
|
|||||||
// 语言前缀首页等内容为空时的兜底
|
// 语言前缀首页等内容为空时的兜底
|
||||||
if (!content) throw notFound();
|
if (!content) throw notFound();
|
||||||
|
|
||||||
return { layout, content, lang, hotProducts, archive: null } satisfies CatchAllData;
|
// 内嵌 Elementor 模板([elementor-template id="N"])+ posts widget 数据预取
|
||||||
|
const mainEls = asElements(extractElementor(content.meta));
|
||||||
|
const embedded = await loadEmbeddedTemplates(mainEls);
|
||||||
|
const postsData = await loadPostsWidgetsData([...mainEls, ...embedded.elements]);
|
||||||
|
|
||||||
|
return { layout, content, lang, hotProducts, archive: null, templates: embedded.templates, postsData } satisfies CatchAllData;
|
||||||
},
|
},
|
||||||
head: ({ loaderData }) => {
|
head: ({ loaderData }) => {
|
||||||
const langLabel = loaderData?.lang ? ` (${LANG_NAMES[loaderData.lang] || loaderData.lang})` : "";
|
const langLabel = loaderData?.lang ? ` (${LANG_NAMES[loaderData.lang] || loaderData.lang})` : "";
|
||||||
@@ -249,7 +308,20 @@ function CatchAllPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<ElementorRenderer doc={elementor} />
|
<ElementorRenderer
|
||||||
|
doc={elementor}
|
||||||
|
postsData={data.postsData}
|
||||||
|
shortcodes={
|
||||||
|
Object.keys(data.templates || {}).length
|
||||||
|
? Object.fromEntries(
|
||||||
|
Object.entries(data.templates).map(([id, doc]) => [
|
||||||
|
id,
|
||||||
|
<ElementorRenderer key={id} doc={doc} postsData={data.postsData} />,
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
<section className="mx-auto mt-10 mb-14 max-w-[860px] px-4">
|
<section className="mx-auto mt-10 mb-14 max-w-[860px] px-4">
|
||||||
<InquiryForm formKey="contact-us-form" productInterest={c.type === "post" ? "" : c.title} title="Get in Touch" />
|
<InquiryForm formKey="contact-us-form" productInterest={c.type === "post" ? "" : c.title} title="Get in Touch" />
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+50
-5
@@ -4,6 +4,8 @@
|
|||||||
* 全部走索引 + 稳定分页,供 Server Function / SSR loader 使用。
|
* 全部走索引 + 稳定分页,供 Server Function / SSR loader 使用。
|
||||||
*/
|
*/
|
||||||
import { db } from "~/db";
|
import { db } from "~/db";
|
||||||
|
// 仅类型引用(编译期擦除):避免与渲染器产生运行期循环依赖
|
||||||
|
import type { ElElement } from "~/components/elementor/Renderer";
|
||||||
import {
|
import {
|
||||||
contents,
|
contents,
|
||||||
categories,
|
categories,
|
||||||
@@ -119,6 +121,39 @@ export async function getShopTemplate() {
|
|||||||
return getPublishedContent("shop-archive", "template");
|
return getPublishedContent("shop-archive", "template");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** # 函数 内嵌 Elementor 模板([elementor-template id="N"] 短代码引用的 post)
|
||||||
|
* 由 scripts/import-embedded-templates.ts 以 slug=`template-<legacyId>` 落库。
|
||||||
|
* 参数 legacyId WP 模板 post id(如 27143)
|
||||||
|
* 返回值 内容行(meta.elementor 为模板元素树);不存在返回 null
|
||||||
|
*/
|
||||||
|
export async function getEmbeddedTemplate(legacyId: number) {
|
||||||
|
return getPublishedContent(`template-${legacyId}`, "template").catch(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** # 函数 收集 Elementor 文档中 posts widget 的查询并预取文章
|
||||||
|
* posts widget(skin cards)按 settings.posts_include_term_ids(WP 术语 legacy_id)+
|
||||||
|
* cards_posts_per_page 查询最新文章;结果以 widget id 为键返回给 ElementorRenderer。
|
||||||
|
* 参数 elements Elementor 文档(裸元素数组,可含多篇文档拼接后的元素)
|
||||||
|
* 返回值 widgetId → 文章卡数组
|
||||||
|
*/
|
||||||
|
export async function loadPostsWidgetsData(elements: ElElement[] | undefined): Promise<Record<string, PostCardItem[]>> {
|
||||||
|
const out: Record<string, PostCardItem[]> = {};
|
||||||
|
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;
|
||||||
|
// 多术语取并集按发布时间倒序(27143:53/54/703)
|
||||||
|
if (termIds.length) out[el.id] = await listPostsByTermsLegacy(termIds.map(Number), perPage).catch(() => []);
|
||||||
|
}
|
||||||
|
if (el.elements?.length) await walk(el.elements);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await walk(elements);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/** # 类型 产品筛选状态:每个筛选字段已选值列表 */
|
/** # 类型 产品筛选状态:每个筛选字段已选值列表 */
|
||||||
export type FilterState = Record<string, string[]>;
|
export type FilterState = Record<string, string[]>;
|
||||||
|
|
||||||
@@ -598,12 +633,22 @@ export type PostCardItem = {
|
|||||||
* 返回值 文章卡数组(标题/摘要/发布时间/特色图/首个分类名)
|
* 返回值 文章卡数组(标题/摘要/发布时间/特色图/首个分类名)
|
||||||
*/
|
*/
|
||||||
export async function listPostsByTermLegacy(legacyTermId: number, limit: number) {
|
export async function listPostsByTermLegacy(legacyTermId: number, limit: number) {
|
||||||
const [cat] = await db
|
return listPostsByTermsLegacy([legacyTermId], limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** # 函数 按多个 WP 术语 legacy_id 取最新文章(posts widget 多分类并集)
|
||||||
|
* Elementor posts widget 的 posts_include_term_ids 可含多个分类,按发布时间倒序
|
||||||
|
* 取并集(原站落地页 27143:terms 53/54/703 → Blog/Case Study/News,最新 4 篇)。
|
||||||
|
* 参数 legacyTermIds WP 术语 id 列表;limit 取条数
|
||||||
|
* 返回值 文章卡数组(去重后按发布时间倒序)
|
||||||
|
*/
|
||||||
|
export async function listPostsByTermsLegacy(legacyTermIds: number[], limit: number) {
|
||||||
|
const catRows = await db
|
||||||
.select({ id: categories.id })
|
.select({ id: categories.id })
|
||||||
.from(categories)
|
.from(categories)
|
||||||
.where(eq(categories.legacyId, legacyTermId))
|
.where(inArray(categories.legacyId, legacyTermIds));
|
||||||
.limit(1);
|
if (!catRows.length) return [] as PostCardItem[];
|
||||||
if (!cat) return [] as PostCardItem[];
|
const catIds = catRows.map((c) => c.id);
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
slug: contents.slug,
|
slug: contents.slug,
|
||||||
@@ -620,7 +665,7 @@ export async function listPostsByTermLegacy(legacyTermId: number, limit: number)
|
|||||||
and(
|
and(
|
||||||
eq(contents.type, "post"),
|
eq(contents.type, "post"),
|
||||||
eq(contents.status, "published"),
|
eq(contents.status, "published"),
|
||||||
sql`EXISTS (SELECT 1 FROM content_terms ct WHERE ct.content_id=${contents.id} AND ct.category_id=${cat.id})`,
|
sql`EXISTS (SELECT 1 FROM content_terms ct WHERE ct.content_id=${contents.id} AND ct.category_id IN (${sql.join(catIds.map((id) => sql`${id}`), sql`, `)}))`,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.orderBy(desc(contents.publishedAt))
|
.orderBy(desc(contents.publishedAt))
|
||||||
|
|||||||
+127
-20
@@ -83,6 +83,7 @@
|
|||||||
}
|
}
|
||||||
.fa-chevron-up::before { content: "\f077"; }
|
.fa-chevron-up::before { content: "\f077"; }
|
||||||
.fa-comment-dots::before { content: "\f4ad"; }
|
.fa-comment-dots::before { content: "\f4ad"; }
|
||||||
|
.fa-check-circle::before { content: "\f058"; }
|
||||||
|
|
||||||
/* # 成员变量 Tailwind 主题令牌(与原 website.css 配色一一对应) **/
|
/* # 成员变量 Tailwind 主题令牌(与原 website.css 配色一一对应) **/
|
||||||
@theme {
|
@theme {
|
||||||
@@ -351,8 +352,9 @@
|
|||||||
.eael-advance-tabs .eael-tabs-nav > ul li .eael-tab-title {
|
.eael-advance-tabs .eael-tabs-nav > ul li .eael-tab-title {
|
||||||
margin: unset; font-size: unset; line-height: unset; font-family: unset; font-weight: unset; color: unset;
|
margin: unset; font-size: unset; line-height: unset; font-family: unset; font-weight: unset; color: unset;
|
||||||
}
|
}
|
||||||
|
.eael-tabs-content { display: flex; flex: 1 1 auto; overflow: hidden; }
|
||||||
.eael-advance-tabs .eael-tabs-content > div { display: none; opacity: 0; }
|
.eael-advance-tabs .eael-tabs-content > div { display: none; opacity: 0; }
|
||||||
.eael-advance-tabs .eael-tabs-content > div.active { display: block; width: 100%; opacity: 1; }
|
.eael-advance-tabs .eael-tabs-content > div.active { display: block; width: 100%; padding: 1em; opacity: 1; }
|
||||||
|
|
||||||
/* ===== Elementor icon-list(widget-icon-list.min.css 语义;H13 链接条实测 2026-11-01)
|
/* ===== Elementor icon-list(widget-icon-list.min.css 语义;H13 链接条实测 2026-11-01)
|
||||||
* inline 视图:ul flex wrap + 负边距 -8px 与 li 8px 侧边距对冲(实测 ul margin 0 -8px);
|
* inline 视图:ul flex wrap + 负边距 -8px 与 li 8px 侧边距对冲(实测 ul margin 0 -8px);
|
||||||
@@ -379,10 +381,26 @@
|
|||||||
[data-el="ef0fcd9"] .eael-tabs-nav > ul li.active,
|
[data-el="ef0fcd9"] .eael-tabs-nav > ul li.active,
|
||||||
[data-el="ef0fcd9"] .eael-tabs-nav > ul li:hover { color: #ED6C00; border-bottom-color: #ED6C00; }
|
[data-el="ef0fcd9"] .eael-tabs-nav > ul li:hover { color: #ED6C00; border-bottom-color: #ED6C00; }
|
||||||
[data-el="ef0fcd9"] .eael-tabs-nav > ul li .eael-tab-title { font-family: "Roboto", sans-serif; }
|
[data-el="ef0fcd9"] .eael-tabs-nav > ul li .eael-tab-title { font-family: "Roboto", sans-serif; }
|
||||||
[data-el="ef0fcd9"] .eael-tab-content-item {
|
[data-el="ef0fcd9"] .eael-advance-tabs .eael-tabs-content > .eael-tab-content-item.active {
|
||||||
color: #333333; padding: 16px 0 0; border-top: 1px solid #C7CBDA;
|
color: #333333; padding: 16px 0 0; border-top: 1px solid #C7CBDA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 落地页 8e90920 adv-tabs 生成规则(post-6509.css ca60c10;1440 实测 2026-11-02)
|
||||||
|
* li 背景 #F4F5F8(globals 26331af)+ 文字 #333 + 底边 3px;hover/active 文字与底边
|
||||||
|
* 变主色 #ED6C00;内容面板 color #333 + padding 16px 0 0 + 顶边 1px #C7CBDA。
|
||||||
|
* 容器 active-caret-on:激活项不显示小三角。 */
|
||||||
|
[data-el="ca60c10"] .eael-tabs-nav > ul li {
|
||||||
|
background-color: #F4F5F8; color: #333333;
|
||||||
|
border-style: solid; border-width: 0 0 3px; border-color: #F4F5F8;
|
||||||
|
}
|
||||||
|
[data-el="ca60c10"] .eael-tabs-nav > ul li.active,
|
||||||
|
[data-el="ca60c10"] .eael-tabs-nav > ul li:hover { color: #ED6C00; border-bottom-color: #ED6C00; }
|
||||||
|
[data-el="ca60c10"] .eael-advance-tabs .eael-tabs-content > .eael-tab-content-item.active {
|
||||||
|
color: #333333; padding: 16px 0 0; border-top: 1px solid #C7CBDA;
|
||||||
|
}
|
||||||
|
.eael-advance-tabs.active-caret-on .eael-tabs-nav > ul li.active:after { display: none; }
|
||||||
|
.eael-advance-tabs .eael-tabs-nav > ul li.active:after { display: block; }
|
||||||
|
|
||||||
/* ===== 首页 H13 Difference Maker(post-18.css 对应块;1436 实测 2026-11-01)
|
/* ===== 首页 H13 Difference Maker(post-18.css 对应块;1436 实测 2026-11-01)
|
||||||
* 楼体大图 = 列 bf3d0ba 背景(bottom left + 100% auto,组件内联输出,post-18 生成规则
|
* 楼体大图 = 列 bf3d0ba 背景(bottom left + 100% auto,组件内联输出,post-18 生成规则
|
||||||
* background_size initial → 100% auto);icon-list 悬停主色(globals text_color_hover);
|
* background_size initial → 100% auto);icon-list 悬停主色(globals text_color_hover);
|
||||||
@@ -420,31 +438,63 @@
|
|||||||
* padding 48/48/16 + 白底 + 圆角 6 + 边框 #E0E5F9 均设置驱动 */
|
* padding 48/48/16 + 白底 + 圆角 6 + 边框 #E0E5F9 均设置驱动 */
|
||||||
[data-el="65282de"] > div, [data-el="cc3e8ca"] > div { width: auto !important; }
|
[data-el="65282de"] > div, [data-el="cc3e8ca"] > div { width: auto !important; }
|
||||||
|
|
||||||
/* ===== Elementor posts widget(skin cards 基础;H18 实测 2026-11-01)
|
/* ===== Elementor posts widget(skin cards 基础;H18 + 模板 27143 实测 2026-11-02)
|
||||||
* 卡 = flex 纵向(h 520 由节点 custom_css .ele-post-dif .elementor-post 注入);
|
* 移植 widget-posts.min.css cards 皮肤:article 纵向 flex;__card 白底/圆角 3/
|
||||||
* 缩略图比率 0.66×内宽(img 绝对定位铺满 object-fit fill)、badge 绝对右上
|
* overflow hidden/纵向 flex;缩略图容器用 padding-bottom 比率撑高、img 静态
|
||||||
* (bg rgba(255,255,255,.7)/12px/圆角 4,自定义样式经 custom_css 注入);
|
* 宽度 calc(100%+1px)(高度按原图比例);hover-gradient 覆盖渐变;badge 绝对
|
||||||
* 标题 18/400/#000、meta 16/#ADADAD/padding 10 12、摘要在 H18 组件内联 */
|
* 右上(默认 #69727d 胶囊,H18 由 custom_css 覆盖为白底圆角 4);
|
||||||
.elementor-post__card {
|
* text flex-grow;meta 在 text 之后(border-top + margin-top:auto)。
|
||||||
display: flex; flex-direction: column; overflow: hidden;
|
* 各尺寸/颜色由 PostsWidget 按设置内联(padding/字号/边框),此处只留结构。 */
|
||||||
border-radius: 3px;
|
.elementor-posts-container:not(.elementor-posts-masonry) { align-items: stretch; }
|
||||||
|
/* 列数:Elementor grid-4 / tablet-2 / mobile-1(变量由 PostsWidget 内联) */
|
||||||
|
.elementor-posts-container.elementor-posts--skin-cards {
|
||||||
|
grid-template-columns: repeat(var(--ep-cols, 4), 1fr);
|
||||||
}
|
}
|
||||||
.elementor-post__thumbnail__link { display: block; position: relative; margin: 0 0 20px; }
|
@media (max-width: 1024px) {
|
||||||
.elementor-post__thumbnail { position: relative; overflow: hidden; padding-bottom: 66%; }
|
.elementor-posts-container.elementor-posts--skin-cards {
|
||||||
.elementor-post__thumbnail img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: fill; }
|
grid-template-columns: repeat(var(--ep-cols-t, 2), 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.elementor-posts-container.elementor-posts--skin-cards {
|
||||||
|
grid-template-columns: repeat(var(--ep-cols-m, 1), 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.elementor-posts .elementor-post { display: flex; flex-direction: column; }
|
||||||
|
.elementor-post__card {
|
||||||
|
display: flex; flex-direction: column; position: relative; overflow: hidden;
|
||||||
|
width: 100%; min-height: 100%; border-radius: 3px; background-color: #fff;
|
||||||
|
}
|
||||||
|
.elementor-post__thumbnail__link { display: block; position: relative; width: 100%; margin: 0 0 20px; }
|
||||||
|
.elementor-post__thumbnail {
|
||||||
|
position: relative; overflow: hidden;
|
||||||
|
padding-bottom: calc(var(--ep-ratio, 0.66) * 100%);
|
||||||
|
}
|
||||||
|
.elementor-post__thumbnail img { display: block; width: calc(100% + 1px); max-width: none; height: auto; }
|
||||||
.elementor-post__badge {
|
.elementor-post__badge {
|
||||||
position: absolute; top: 0; right: 0; z-index: 1;
|
position: absolute; top: 0; right: 0; z-index: 1; margin: 20px;
|
||||||
font-size: 12px; line-height: 1; padding: 7.2px 14.4px;
|
background: #69727d; color: #fff; border-radius: 999px;
|
||||||
|
font-size: 12px; font-weight: 400; line-height: 1; padding: 0.6em 1.2em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.elementor-post__text { padding: 0 12px; flex-grow: 1; }
|
.elementor-post__text { flex-grow: 1; }
|
||||||
.elementor-post__title { font-size: 18px; line-height: 27px; font-weight: 400; color: #000; }
|
.elementor-post__title { font-size: 18px; line-height: 27px; font-weight: 400; color: #000; margin: 0; }
|
||||||
.elementor-post__title a { color: inherit; text-decoration: none; }
|
.elementor-post__title a { color: inherit; text-decoration: none; }
|
||||||
.elementor-post__meta-data {
|
.elementor-post__meta-data {
|
||||||
display: block; margin: 0; padding: 10px 12px;
|
display: block; margin: 0; border-top: 1px solid #eaeaea;
|
||||||
font-size: 16px; color: #ADADAD;
|
color: #adadad; font-size: 12px; line-height: 1.3;
|
||||||
|
}
|
||||||
|
.elementor-post__excerpt p { color: #777; font-size: 14px; line-height: 1.5em; margin: 0; }
|
||||||
|
/* 缩略图 hover 渐变(elementor-posts__hover-gradient):盖住图片与比率留白区 */
|
||||||
|
.elementor-posts__hover-gradient .elementor-post__card .elementor-post__thumbnail__link:after {
|
||||||
|
content: ""; position: absolute; inset: 0; width: 100%; height: 100%;
|
||||||
|
background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.35), transparent 75%);
|
||||||
|
background-repeat: no-repeat; opacity: 1; transition: all 0.3s ease-out;
|
||||||
|
}
|
||||||
|
.elementor-posts__hover-gradient .elementor-post__card:hover .elementor-post__thumbnail__link:after { opacity: 0.5; }
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.elementor-post__thumbnail { padding-bottom: calc(var(--ep-ratio-m, var(--ep-ratio, 0.66)) * 100%); }
|
||||||
}
|
}
|
||||||
.elementor-post__excerpt { font-size: 16px; line-height: 1.7; color: #333; }
|
|
||||||
/* 右列卡(a22c185 .ele-post-dif-2):纯文本卡 bg #F4F5F8、标题链接右侧箭头图标
|
/* 右列卡(a22c185 .ele-post-dif-2):纯文本卡 bg #F4F5F8、标题链接右侧箭头图标
|
||||||
* 与 44px 两行钳制均由 custom_css 注入;此处理品牌底色/箭头 hover 位移 */
|
* 与 44px 两行钳制均由 custom_css 注入;此处理品牌底色/箭头 hover 位移 */
|
||||||
[data-el="a22c185"] .elementor-post__card { background-color: #F4F5F8; }
|
[data-el="a22c185"] .elementor-post__card { background-color: #F4F5F8; }
|
||||||
@@ -530,6 +580,8 @@
|
|||||||
.eael-infobox.icon-on-left { display: flex; position: relative; }
|
.eael-infobox.icon-on-left { display: flex; position: relative; }
|
||||||
.eael-infobox.icon-on-left .infobox-content { padding-left: 15px; flex: 1 1 auto; min-width: 0; }
|
.eael-infobox.icon-on-left .infobox-content { padding-left: 15px; flex: 1 1 auto; min-width: 0; }
|
||||||
.eael-infobox .infobox-icon img { transition: all 300ms ease-in-out 0s; }
|
.eael-infobox .infobox-icon img { transition: all 300ms ease-in-out 0s; }
|
||||||
|
/* 图标型 info-box:图标包容器行高归零,行盒不再附加字体 descender(df5b95b 实测 26px) */
|
||||||
|
.eael-infobox .infobox-icon .infobox-icon-wrap { display: flex; align-items: center; line-height: 0; }
|
||||||
.eael-infobox .infobox-content .infobox-title-section { display: flex; flex-direction: column; }
|
.eael-infobox .infobox-content .infobox-title-section { display: flex; flex-direction: column; }
|
||||||
.eael-infobox .infobox-content .infobox-title-section .title {
|
.eael-infobox .infobox-content .infobox-title-section .title {
|
||||||
width: max-content; max-width: 100%; overflow-wrap: break-word; word-break: break-word;
|
width: max-content; max-width: 100%; overflow-wrap: break-word; word-break: break-word;
|
||||||
@@ -538,6 +590,12 @@
|
|||||||
.eael-infobox.icon-on-top .infobox-icon img { display: block; width: 100%; height: auto; }
|
.eael-infobox.icon-on-top .infobox-icon img { display: block; width: 100%; height: auto; }
|
||||||
.eael-infobox.icon-on-left .infobox-content .title { line-height: 1; margin: 0 0 10px 0; }
|
.eael-infobox.icon-on-left .infobox-content .title { line-height: 1; margin: 0 0 10px 0; }
|
||||||
.eael-infobox .infobox-content p { margin: 0 0 15px; }
|
.eael-infobox .infobox-content p { margin: 0 0 15px; }
|
||||||
|
/* 内容段落生成规则(EAEL:eael_infobox_content_margin/color 落到 p;570df98
|
||||||
|
* 实测 margin 10px 0 0、color #969696;未设置时保持 EAEL 默认 0 0 15px) */
|
||||||
|
.eael-infobox .infobox-content .infobox-text p {
|
||||||
|
margin: var(--ib-p-top, 0) var(--ib-p-right, 0) var(--ib-p-bottom, 15px) var(--ib-p-left, 0);
|
||||||
|
color: var(--ib-p-color, inherit);
|
||||||
|
}
|
||||||
/* 内容里的 ul/ol:hello-elementor 主题 reset(H14 药丸列表实测 margin 0,
|
/* 内容里的 ul/ol:hello-elementor 主题 reset(H14 药丸列表实测 margin 0,
|
||||||
* UA 默认 1em 上下外距会撑高内容 16px);列表样式与内边距由节点 custom_css 定义 */
|
* UA 默认 1em 上下外距会撑高内容 16px);列表样式与内边距由节点 custom_css 定义 */
|
||||||
.eael-infobox .infobox-content ul,
|
.eael-infobox .infobox-content ul,
|
||||||
@@ -720,6 +778,48 @@
|
|||||||
[data-el="fd97340"] .elementor-tab-content { border-width: 0; }
|
[data-el="fd97340"] .elementor-tab-content { border-width: 0; }
|
||||||
[data-el="fd97340"] .elementor-tab-title.elementor-active { color: #ED6C00; }
|
[data-el="fd97340"] .elementor-tab-title.elementor-active { color: #ED6C00; }
|
||||||
|
|
||||||
|
/* ===== Elementor 原生 toggle widget(widget-toggle.min.css 移植 + 846aeb5 生成规则)
|
||||||
|
* 原站实测(1440):条目高 58(标题 padding 15 + 行高 28)、间距 4;标题 h3 28px/1/700
|
||||||
|
* #333、背景 #F0F1F5;标题链接 1.125rem/1 #000;图标左浮动 1em、右距 12;内容
|
||||||
|
* padding 15 / .875rem,默认 display:none,展开加 elementor-active。
|
||||||
|
* 主题 h3 字号由 hello-elementor 提供(1.75rem=28px、700),此处显式写出以脱离宿主 reset。 */
|
||||||
|
.elementor-tab-title a { color: inherit; }
|
||||||
|
.elementor-toggle { text-align: start; }
|
||||||
|
.elementor-toggle .elementor-tab-title {
|
||||||
|
border-block-end: 1px solid #d5d8dc;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
margin: 0;
|
||||||
|
outline: none;
|
||||||
|
padding: 15px;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.elementor-toggle .elementor-tab-title .elementor-toggle-icon { display: inline-block; width: 1em; }
|
||||||
|
.elementor-toggle .elementor-tab-title .elementor-toggle-icon svg { height: 1em; margin-inline-start: -5px; width: 1em; fill: currentColor; }
|
||||||
|
.elementor-toggle .elementor-tab-title .elementor-toggle-icon.elementor-toggle-icon-left { float: left; text-align: left; }
|
||||||
|
.elementor-toggle .elementor-tab-title .elementor-toggle-icon.elementor-toggle-icon-right { float: right; text-align: right; }
|
||||||
|
.elementor-toggle .elementor-tab-title .elementor-toggle-icon-closed { display: block; }
|
||||||
|
.elementor-toggle .elementor-tab-title .elementor-toggle-icon-opened { display: none; }
|
||||||
|
.elementor-toggle .elementor-tab-title.elementor-active { border-block-end: none; }
|
||||||
|
.elementor-toggle .elementor-tab-title.elementor-active .elementor-toggle-icon-closed { display: none; }
|
||||||
|
.elementor-toggle .elementor-tab-title.elementor-active .elementor-toggle-icon-opened { display: block; }
|
||||||
|
.elementor-toggle .elementor-tab-content { border-block-end: 1px solid #d5d8dc; display: none; padding: 15px; }
|
||||||
|
.elementor-toggle .elementor-tab-content.elementor-active { display: block; }
|
||||||
|
/* 标题链接:生成规则 font-size 1.125rem(移动端 1rem),行高继承标题 1 */
|
||||||
|
.elementor-toggle .elementor-toggle-title { font-size: 1.125rem; color: #000; }
|
||||||
|
.elementor-toggle .elementor-toggle-icon { margin-inline-end: 12px; }
|
||||||
|
.elementor-toggle .elementor-toggle-item:not(:last-child) { margin-block-end: 4px; }
|
||||||
|
/* 生成规则:title/content border-width 0、标题背景 #F0F1F5、内容背景 #FFFFFF */
|
||||||
|
.elementor-toggle .elementor-tab-title { border-width: 0; border-color: #FFFFFF; background-color: #F0F1F5; }
|
||||||
|
.elementor-toggle .elementor-tab-content { border-width: 0; border-block-end-color: #FFFFFF; background-color: #FFFFFF; }
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.elementor-toggle .elementor-tab-title { padding: 12px; }
|
||||||
|
.elementor-toggle .elementor-tab-content { padding: 12px 10px; font-size: 0.875rem; }
|
||||||
|
.elementor-toggle .elementor-toggle-title { font-size: 1rem; }
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== 页脚 Elementor 模板 post 58(线上 1436 实测 + post-58.css,2026-11-02)
|
/* ===== 页脚 Elementor 模板 post 58(线上 1436 实测 + post-58.css,2026-11-02)
|
||||||
* 结构:46d7afa 四列链接(桌面) / d834160(html_tag=footer) 返回顶部 + 分隔线 +
|
* 结构:46d7afa 四列链接(桌面) / d834160(html_tag=footer) 返回顶部 + 分隔线 +
|
||||||
* 28c7f0b8 品牌区三列 + a428bb4 版权条。以下为「仅存生成 CSS」与 DOM 语义补丁 */
|
* 28c7f0b8 品牌区三列 + a428bb4 版权条。以下为「仅存生成 CSS」与 DOM 语义补丁 */
|
||||||
@@ -1012,3 +1112,10 @@ body.dialog-prevent-scroll { overflow: hidden; }
|
|||||||
[data-el="71f7329"] > div,
|
[data-el="71f7329"] > div,
|
||||||
[data-el="2ec9a62"] > div,
|
[data-el="2ec9a62"] > div,
|
||||||
[data-el="7bdf4c0"] > div { width: auto !important; }
|
[data-el="7bdf4c0"] > div { width: auto !important; }
|
||||||
|
|
||||||
|
/* Landing 智能网关 028b4bc/5745b4f:列 06632bb/3317056 padding 20 + margin-right 10,
|
||||||
|
* populated 按 auto 扣除 margin(原站内层节与信息框宽 570;100% 时为 580) */
|
||||||
|
[data-el="06632bb"] > div,
|
||||||
|
[data-el="3361c18"] > div,
|
||||||
|
[data-el="3317056"] > div,
|
||||||
|
[data-el="5938d26"] > div { width: auto !important; }
|
||||||
|
|||||||
+8
-1
@@ -16,13 +16,20 @@ export default defineConfig({
|
|||||||
rolldownOptions: { external: ["bun"] },
|
rolldownOptions: { external: ["bun"] },
|
||||||
},
|
},
|
||||||
// 客户端环境用 stub 解析 "bun"(db 模块经 server fn 依赖链进入客户端图,
|
// 客户端环境用 stub 解析 "bun"(db 模块经 server fn 依赖链进入客户端图,
|
||||||
// 动态 import("bun") 只在服务端执行)
|
// 动态 import("bun") 只在服务端执行);SSR 环境把 "bun" 标记为外部模块,
|
||||||
|
// 由 Vite dev 进程(bun)原生解析 —— 否则 dev 下 module-runner 报 ERR_MODULE_NOT_FOUND
|
||||||
environments: {
|
environments: {
|
||||||
client: {
|
client: {
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: [{ find: /^bun$/, replacement: path.resolve(here, "src/stubs/bun-client.ts") }],
|
alias: [{ find: /^bun$/, replacement: path.resolve(here, "src/stubs/bun-client.ts") }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ssr: {
|
||||||
|
// builtins:把 "bun" 声明为该运行时环境的内置模块,dev 下由 bun 原生
|
||||||
|
// 解析(不进入 module-runner 的依赖图,否则报 ERR_MODULE_NOT_FOUND)。
|
||||||
|
// 注意:builtins 是覆写语义,必须带上 node:*,否则 node 内置模块失效。
|
||||||
|
resolve: { external: ["bun"], builtins: ["bun", /^node:/] },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
tsConfigPaths({ projects: ["./tsconfig.json"] }),
|
tsConfigPaths({ projects: ["./tsconfig.json"] }),
|
||||||
|
|||||||
Reference in New Issue
Block a user