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
+13 -75
View File
@@ -15,6 +15,7 @@
* 用法:bun scripts/import-page-css.ts <slug> [<slug> ...] (缺省 = 全部方案落地页)
*/
import { SQL } from "bun";
import { collectDocIds, filterCssForIds } from "../src/utils/css-translate";
/** # 常量 方案落地页 slug 全集(缺省导入目标) **/
const SOLUTION_SLUGS = [
@@ -37,87 +38,24 @@ const SOLUTION_SLUGS = [
/** # 常量 原站页面 CSS 地址模板 **/
const CSS_URL = (pid: number) => `https://www.dusuniot.com/wp-content/uploads/elementor/css/post-${pid}.css`;
/** # 函数 提取文档全部元素 id 集合(含嵌套;顶层为数组或 {elements} 包装均可) **/
function collectIds(el: unknown, out: Set<string> = new Set()): Set<string> {
if (Array.isArray(el)) {
for (const x of el) collectIds(x, out);
return out;
}
if (!el || typeof el !== "object") return out;
const e = el as { id?: unknown; elements?: unknown[] };
if (typeof e.id === "string") out.add(e.id);
for (const c of Array.isArray(e.elements) ? e.elements : []) collectIds(c, out);
return out;
}
/** # 函数 把选择器中的 elementor-element-<id> 改写为 data-el 属性选择器
* 规则保留条件:含文档 id,或引用 .ele-<自定类>(页面自定义类,渲染器把 _css_classes
* 落到节点 class 上,如 .ele-sol-review-box / .ele-secondDep-dif);其余丢弃
*/
function translateSelector(sel: string, ids: Set<string>, pid: number): string | null {
const idIn = [...sel.matchAll(/elementor-element-(\w+)/g)].map((m) => m[1]);
const hasDocId = idIn.some((id) => ids.has(id));
const hasCustomCls = /\.ele-[a-z][\w-]*/i.test(sel) && !/elementor-element-/.test(sel);
if (!hasDocId && !hasCustomCls) return null;
let out = sel;
for (const id of new Set(idIn)) {
if (!ids.has(id)) continue;
out = out.split(`.elementor-element.elementor-element-${id}`).join(`[data-el="${id}"]`);
out = out.split(`.elementor-element-${id}`).join(`[data-el="${id}"]`);
}
// 剥页面作用域前缀(.elementor-<pid> 空格相连)与多余空白
out = out.replace(new RegExp(`\\.elementor-${pid}\\s*`, "g"), "");
return out.replace(/\s+/g, " ").trim();
}
/** # 函数 解析 CSS 顶层规则(含 @media 嵌套块),返回过滤改写后的规则串
* 用花括号配对扫描,避免依赖完整 CSS 解析器
*/
function filterCss(css: string, ids: Set<string>, pid: number): string {
let out = "";
let i = 0;
const n = css.length;
while (i < n) {
// 跳过注释
if (css.startsWith("/*", i)) {
const end = css.indexOf("*/", i + 2);
i = end === -1 ? n : end + 2;
continue;
}
const brace = css.indexOf("{", i);
if (brace === -1) break;
const sel = css.slice(i, brace).trim();
// 找配对右括号
let depth = 1;
let j = brace + 1;
while (j < n && depth > 0) {
if (css[j] === "{") depth++;
else if (css[j] === "}") depth--;
j++;
}
const body = css.slice(brace + 1, j - 1);
if (sel.startsWith("@media") || sel.startsWith("@supports")) {
const inner = filterCss(body, ids, pid);
if (inner) out += `${sel}{${inner}}\n`;
} else if (sel.startsWith("@keyframes") || sel.startsWith("@font-face")) {
// 全局块整体保留(字体/动画与节点无关)
out += `${sel}{${body}}\n`;
} else {
const parts = sel.split(",").map((p) => translateSelector(p, ids, pid)).filter((p): p is string => !!p);
if (parts.length) out += `${parts.join(",")}{${body}}\n`;
}
i = j;
}
return out;
}
const slugs = process.argv.slice(2).length ? process.argv.slice(2) : SOLUTION_SLUGS;
const sql0 = new SQL("postgres://dusuniot:dusuniot@127.0.0.1:25432/dusuniot", { max: 1 });
// CONTENT_TYPE 环境变量支持产品(product)等其它内容类型;"-all" = 该类型全部已发布内容
const contentType = process.env.CONTENT_TYPE || "post";
let slugs = process.argv.slice(2).length ? process.argv.slice(2) : SOLUTION_SLUGS;
if (slugs.length === 1 && slugs[0] === "-all") {
const rows0 = await sql0`select slug from contents where type = ${contentType} and status = ${"published"}`;
slugs = rows0.map((r: any) => r.slug);
}
await sql0.end();
const sql = new SQL("postgres://dusuniot:dusuniot@127.0.0.1:25432/dusuniot", { max: 1 });
/** 已导入过的跳过(断点续传;meta.page_css 键存在即视为完成) */
let done = 0;
let skipped = 0;
for (const slug of slugs) {
const rows = await sql`select id, legacy_id, meta::text as meta from contents where slug = ${slug} and type = ${"post"} limit 1`;
const rows = await sql`select id, legacy_id, meta::text as meta from contents where slug = ${slug} and type = ${contentType} limit 1`;
const row = rows[0];
if (!row) {
skipped++;
@@ -128,7 +66,7 @@ for (const slug of slugs) {
skipped++;
continue;
}
const ids = collectIds(meta.elementor);
const ids = collectDocIds(meta.elementor);
const pid = row.legacy_id;
if (!pid) {
skipped++;
@@ -150,7 +88,7 @@ for (const slug of slugs) {
skipped++;
continue;
}
const pageCss = filterCss(raw, ids, pid);
const pageCss = filterCssForIds(raw, ids, pid);
meta.page_css = pageCss;
// ::text::jsonb 双重转换:Bun SQL 绑定字符串直接 ::jsonb 会把整串存成 JSON 字符串值
await sql`update contents set meta = ${JSON.stringify(meta)}::text::jsonb where id = ${row.id}`;
+33
View File
@@ -0,0 +1,33 @@
/**
* # 脚本 为全部已发布产品抓 post-<legacy_id>.css → products.meta.page_css
* 与 import-page-css 相同的翻译(css-translate);断点续传(已有 page_css 键跳过)。
* 用法:bun scripts/import-product-css.ts
*/
import { SQL } from "bun";
import { collectDocIds, filterCssForIds } from "../src/utils/css-translate";
const sql = new SQL("postgres://dusuniot:dusuniot@127.0.0.1:25432/dusuniot", { max: 1 });
const rows = await sql`select id, legacy_id, meta::text as meta from products where status = ${"published"} and legacy_id is not null`;
let done = 0, skipped = 0, fail = 0;
for (const row of rows) {
const meta = JSON.parse(row.meta);
if (meta.page_css !== undefined) { skipped++; continue; }
const ids = collectDocIds(meta.elementor);
let raw: string | null = null;
for (let a = 0; a < 3 && raw === null; a++) {
try {
const res = await fetch(`https://www.dusuniot.com/wp-content/uploads/elementor/css/post-${row.legacy_id}.css`, { headers: { "User-Agent": "Mozilla/5.0" } });
if (res.ok) raw = await res.text();
else if (res.status === 404) break;
} catch {
await new Promise((r) => setTimeout(r, 300));
}
}
if (raw === null) { fail++; continue; }
meta.page_css = filterCssForIds(raw, ids, row.legacy_id);
await sql`update products set meta = ${JSON.stringify(meta)}::text::jsonb where id = ${row.id}`;
done++;
if (done % 25 === 0) console.log(`... ${done}`);
}
console.log("imported:", done, "skipped:", skipped, "fail:", fail);
await sql.end();