v0.10.27: /product/$slug 产品单模板一比一(模板2741/29972 补录、Woo 图廊/meta/tabs/相关产品组件、page_css 130 篇、动态按钮落地)+ css-translate 共享模块
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user