v0.10.23: 落地页收口(toggle/内嵌模板 27143/posts cards/info-box)+ dev 3311 修复

This commit is contained in:
bigcode
2026-11-02 16:15:14 +08:00
parent 07774c715f
commit 864a737c58
13 changed files with 605 additions and 83 deletions
+76 -4
View File
@@ -10,24 +10,78 @@ import { useEffect, useState } from "react";
import { createFileRoute, Link, notFound, redirect } from "@tanstack/react-router";
import { getLayoutData, Layout, type LayoutData } from "~/components/website/Layout";
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 { Button } from "~/components/ui/button";
import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator } from "~/components/ui/breadcrumb";
import { ProductCard } from "~/components/website/ProductCard";
import { isLangCode, LANG_NAMES, pickContent, type CatchAllData } from "~/components/website/catchall-shared";
import {
getEmbeddedTemplate,
getPublishedContent,
getPostCategoryBySlug,
getProductChildCategory,
hasProductSlug,
listPosts,
listProducts,
loadPostsWidgetsData,
resolveRedirect,
type PostCardItem,
type ProductListResult,
} from "~/server/data";
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("/$")({
// 归档分页用 ?page=N
validateSearch: (search: Record<string, unknown>): { page?: number } => {
@@ -111,7 +165,7 @@ export const Route = createFileRoute("/$")({
const cat = await getPostCategoryBySlug(last).catch(() => null);
if (cat) {
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 渲染)
@@ -120,7 +174,12 @@ export const Route = createFileRoute("/$")({
// 语言前缀首页等内容为空时的兜底
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 }) => {
const langLabel = loaderData?.lang ? ` (${LANG_NAMES[loaderData.lang] || loaderData.lang})` : "";
@@ -249,7 +308,20 @@ function CatchAllPage() {
</div>
</div>
) : 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">
<InquiryForm formKey="contact-us-form" productInterest={c.type === "post" ? "" : c.title} title="Get in Touch" />
</section>