v0.10.27: /product/$slug 产品单模板一比一(模板2741/29972 补录、Woo 图廊/meta/tabs/相关产品组件、page_css 130 篇、动态按钮落地)+ css-translate 共享模块

This commit is contained in:
bigcode
2026-11-03 05:18:10 +08:00
parent d016be03ae
commit 35d154870a
9 changed files with 472 additions and 277 deletions
+198 -6
View File
@@ -10,6 +10,7 @@ import { getLatestPosts, getLatestProducts, type LatestItem, type PostCardItem }
import TickerBar from "~/components/website/TickerBar";
import { Cf7Form604 } from "~/components/website/Cf7Form604";
import { localSiteUrl } from "~/utils/media";
import { ProductCard, type ProductCardData } from "~/components/website/ProductCard";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "~/components/ui/accordion";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
@@ -270,6 +271,27 @@ function normalizeDoc(doc: ElDocument): ElElement[] {
/** # 成员变量 posts widget 文章数据上下文(loader 预取,经 widget id 分发) **/
const PostsDataContext = createContext<Record<string, PostCardItem[]>>({});
/** # 类型 Woo 产品单页上下文(模板 2741 的 Woo widget 数据) */
export type WooProductContext = {
/** # 成员变量 产品标题/SKU/短描述(HTML) **/
title: string;
sku: string;
shortDescription: string;
/** # 成员变量 分类(meta Category 链接) **/
categories: { name: string; slug: string }[];
/** # 成员变量 图廊:主图 + 缩略图(已解析 WP 尺寸变体) **/
mainImage: { url: string; alt?: string };
thumbs: { url: string; alt?: string }[];
/** # 成员变量 属性表(Additional information) **/
attributes: { attr: string; value: string }[];
/** # 成员变量 产品自身 elementor 文档(Description 标签页内容)与 pageCss **/
productDoc?: ElDocument;
productPageCss?: string;
/** # 成员变量 相关产品(woocommerce-products 卡片数据) **/
related: ProductCardData[];
};
const WooContext = createContext<WooProductContext | null>(null);
/** # 成员变量 WordPress 短代码渲染上下文(widget id → 预渲染节点;页脚 menu-2/gtranslate) **/
const ShortcodesContext = createContext<Record<string, ReactNode>>({});
@@ -292,11 +314,14 @@ export function ElementorRenderer({
shortcodes,
vars,
pageCss,
woo,
}: {
doc: ElDocument;
postsData?: Record<string, PostCardItem[]>;
shortcodes?: Record<string, ReactNode>;
vars?: RendererVars;
/** # 成员变量 Woo 产品单页上下文(产品模板 2741 渲染时传入) **/
woo?: WooProductContext | null;
/** # 成员变量 页面级生成 CSS(原站 post-<id>.css 翻译成 data-el 选择器,scripts/import-page-css.ts 导入) **/
pageCss?: string;
}) {
@@ -307,12 +332,14 @@ export function ElementorRenderer({
<VarsContext.Provider value={vars || {}}>
<PostsDataContext.Provider value={postsData || {}}>
<ShortcodesContext.Provider value={shortcodes || {}}>
<div className="content-html">
{css ? <style dangerouslySetInnerHTML={{ __html: css }} /> : null}
{elements.map((el, i) => (
<ElementNode key={el.id || i} el={el} />
))}
</div>
<WooContext.Provider value={woo || null}>
<div className="content-html">
{css ? <style dangerouslySetInnerHTML={{ __html: css }} /> : null}
{elements.map((el, i) => (
<ElementNode key={el.id || i} el={el} />
))}
</div>
</WooContext.Provider>
</ShortcodesContext.Provider>
</PostsDataContext.Provider>
</VarsContext.Provider>
@@ -779,6 +806,18 @@ function WidgetNodeInner({ el }: { el: ElElement }) {
return <SocialIconsWidget settings={settings} />;
case "eael-contact-form-7":
return <ContactFormWidget el={el} settings={settings} />;
case "woocommerce-product-images":
return <WooProductImages />;
case "woocommerce-product-title":
return <WooProductTitle />;
case "woocommerce-product-meta":
return <WooProductMeta />;
case "woocommerce-product-short-description":
return <WooProductShortDescription />;
case "woocommerce-product-data-tabs":
return <WooProductDataTabs />;
case "woocommerce-products":
return <WooProductsWidget />;
case "video":
return <VideoWidget settings={settings} />;
case "counter":
@@ -2029,6 +2068,159 @@ function VideoWidget({ settings }: { settings: Record<string, unknown> }) {
);
}
/** # 组件 Woo 产品图廊(woocommerce-product-images)
* 取证(dsgw-210-a-1,1440):图廊 484 宽、margin 0 0 32px;主图 482×482(600x600 变体);
* 缩略条 margin 0 -4px、每行 4 张(li 25%)。图片 URL 已在 loader 解析为变体。
*/
function WooProductImages() {
const woo = useContext(WooContext);
if (!woo) return null;
return (
<div className="woo-gallery">
<style>{WOO_CSS}</style>
<div className="woo-gallery-wrapper">
<div className="woo-gallery-image">
<img src={woo.mainImage.url} alt={woo.mainImage.alt || woo.title} width={600} height={600} />
</div>
</div>
<ol className="woo-thumbs">
{woo.thumbs.map((t, i) => (
<li key={i}>
<img src={t.url} alt={t.alt || woo.title} width={100} height={100} loading="lazy" />
</li>
))}
</ol>
</div>
);
}
/** # 组件 Woo 产品标题(24px/600 #333,product_title) */
function WooProductTitle() {
const woo = useContext(WooContext);
if (!woo) return null;
return <h1 className="woo-title">{woo.title}</h1>;
}
/** # 组件 Woo 产品 meta(Model: SKU / Category: 链接,inline 两项、span pad 0 10px) */
function WooProductMeta() {
const woo = useContext(WooContext);
if (!woo) return null;
return (
<div className="woo-meta">
{woo.sku ? (
<span className="woo-meta-item">
<span className="woo-meta-label">Model: </span>
<span className="woo-meta-value">{woo.sku}</span>
</span>
) : null}
{woo.categories.length > 0 ? (
<span className="woo-meta-item">
<span className="woo-meta-label">Category:</span>
<span className="woo-meta-value">
{woo.categories.map((c, i) => (
<a key={c.slug} href={`/collection/${c.slug}/`}>
{i > 0 ? ", " : ""}
{c.name}
</a>
))}
</span>
</span>
) : null}
</div>
);
}
/** # 组件 Woo 产品短描述(16px/32 #333) */
function WooProductShortDescription() {
const woo = useContext(WooContext);
if (!woo?.shortDescription) return null;
return <div className="woo-short-desc" dangerouslySetInnerHTML={{ __html: localSiteUrl(woo.shortDescription) }} />;
}
/** # 组件 Woo 产品数据标签页(woocommerce-product-data-tabs)
* 原站 tabs 栏 display:none(JS 未初始化态),Description 面板直接显示产品自身
* elementor 文档;Additional information 面板为属性表(隐藏)。
*/
function WooProductDataTabs() {
const woo = useContext(WooContext);
if (!woo) return null;
const groups: { attr: string; values: string[] }[] = [];
for (const a of woo.attributes) {
const g = groups.find((x) => x.attr === a.attr);
if (g) g.values.push(a.value);
else groups.push({ attr: a.attr, values: [a.value] });
}
return (
<div className="woo-tabs">
<style>{WOO_CSS}</style>
<ul className="woo-tabs-list">
<li className="woo-tab-active"><a href="#tab-description">Description</a></li>
<li><a href="#tab-additional_information">Additional information</a></li>
</ul>
<div className="woo-tab-panel woo-tab-description">
{woo.productDoc ? (
<ElementorRenderer doc={woo.productDoc} pageCss={woo.productPageCss} />
) : null}
</div>
<div className="woo-tab-panel woo-tab-addinfo" style={{ display: "none" }}>
<h2>Additional information</h2>
<table className="woo-attrs">
<tbody>
{groups.map((g) => (
<tr key={g.attr}>
<th>{g.attr}</th>
<td>{g.values.join(", ")}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
/** # 组件 Woo 相关产品(woocommerce-products,related;4 列卡片) */
function WooProductsWidget() {
const woo = useContext(WooContext);
if (!woo || !woo.related.length) return null;
return (
<div className="woo-related">
<div className="woo-related-grid">
{woo.related.map((p) => (
<ProductCard key={p.id} p={p} />
))}
</div>
</div>
);
}
/** # 常量 Woo 产品组件样式(取证 2026-11-03 dsgw-210-a-1 @1440) */
const WOO_CSS = `
.woo-gallery { margin: 0 0 32px; }
.woo-gallery .woo-gallery-image img { display: block; width: 100%; height: auto; }
.woo-gallery .woo-thumbs { list-style: none; display: flex; flex-wrap: wrap; margin: 6px -4px 0; padding: 0; }
.woo-gallery .woo-thumbs li { width: 25%; padding: 4px; box-sizing: border-box; list-style: none; }
.woo-gallery .woo-thumbs img { display: block; width: 100%; height: auto; border-radius: 3px; cursor: pointer; opacity: 0.85; }
.woo-title { margin: 0; font-size: 24px; font-weight: 600; line-height: 24px; color: #333; }
.woo-meta { display: flex; flex-wrap: wrap; margin: 16px -10px 0; font-size: 16px; line-height: 24px; color: #333; }
.woo-meta .woo-meta-item { padding: 0 10px; }
.woo-meta .woo-meta-label { font-weight: 700; }
.woo-meta .woo-meta-value a { color: #333; text-decoration: none; }
.woo-meta .woo-meta-value a:hover { color: #ED6C00; }
.woo-short-desc { margin-top: 20px; font-size: 16px; line-height: 32px; color: #333; }
.woo-short-desc p { margin: 0 0 10px; }
.woo-tabs-list { list-style: none; display: none; margin: 0; padding: 0; }
.woo-tabs-list li { display: inline-block; }
.woo-tabs-list li a { display: inline-block; padding: 12.8px 19.2px; font-size: 16px; font-weight: 700; line-height: 16px; color: #515151; }
.woo-tab-description { display: block; }
.woo-attrs { width: 100%; border-collapse: collapse; }
.woo-attrs th, .woo-attrs td { border-top: 1px solid rgba(0,0,0,0.06); padding: 10px 16px; text-align: left; font-size: 15px; }
.woo-related-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; }
@media (max-width: 767px) {
.woo-related-grid { grid-template-columns: repeat(2, 1fr); }
}
`;
/** # 函数 从任意 meta 对象中安全提取 elementor 文档(含类型收窄) */
export function extractElementor(meta: unknown): ElDocument | undefined {
if (!meta || typeof meta !== "object") return undefined;