v0.10.21: 产品归档页 C1-C4(原站 /shop/ 模板 1398 + WPC 筛选 + WooCommerce 网格)
- 导入 archive 模板 post 1398(slug=shop-archive),/collection/ 用 ElementorRenderer 直出 hero/两列外壳,按 widget id 注入 ShopFilters/ShopProducts;新增 server function 解决客户端导航时 loader 打不开 db 的问题 - C1 hero:面包屑 Home / Shop(BreadcrumbWidget + vars.breadcrumb 上下文) - C2 筛选:12 组属性(term taxonomy 顺序,scripts/import-attribute-term-order.ts); 长期列表 5 项 + See more,Battery/Operating/Install 默认折叠;URL query 驱动, SoM 筛选实测 22 条;顶部 Filters 标题/已选 chips/Clear all - C3 网格:WooCommerce 归档 DOM(结果计数、orderby、24 卡 4 列、卡片 1:1 图/标题/ Read more);默认排序导入 products.meta.menu_order(scripts/import-product-menu-order.ts), 前三个商品与原站一致(DSOM-040R/110R/130N) - C4 分页:页码/箭头、当前页样式、page2 25–48 条验证 - 修复 Tailwind .columns-4 与 WooCommerce 网格类冲突;app.css 新增 shop 归档 scoped 样式 - 1436 实测:hero 52.8、shop 节 2906.7、筛选区 2798.7、网格 2523.3、卡 257.7×409、 分页 y2855,与原站逐项一致(±6px 内);bun test 11 pass;版本 0.10.21
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* # 脚本 导入产品属性术语顺序(对齐原站 /shop/ 筛选列表顺序)
|
||||
* 原站 WPC 筛选按 wp_term_taxonomy 中的术语顺序展示(取证:pa_product-type 顺序
|
||||
* SoM → IoT Gateway → Wi-Fi Repeater → Router → Beacon,对应 term_id 756/764/845/846/862;
|
||||
* pa_cpu 顺序 PX30 → RK3328 → RK3588 → PX30K …)。本项目导入时按字母序写入,需要修复。
|
||||
* 做法:解析 dump 的 wp_terms(term_id→name)与 wp_term_taxonomy(插入顺序),
|
||||
* 对每个 pa_<属性 slug>,按顺序把 product_attribute_values.sort 设为序号。
|
||||
* 运行:bun scripts/import-attribute-term-order.ts
|
||||
*/
|
||||
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");
|
||||
|
||||
/** # 函数 提取某张表的 INSERT 段文本(支持多段 INSERT) */
|
||||
function tableInserts(lines: string[], table: string): string[] {
|
||||
const out: string[] = [];
|
||||
let buf = "";
|
||||
let on = false;
|
||||
for (const l of lines) {
|
||||
if (l.startsWith(`INSERT INTO \`${table}\` VALUES`)) {
|
||||
on = true;
|
||||
buf = l;
|
||||
} else if (on) {
|
||||
buf += l;
|
||||
}
|
||||
if (on && l.trimEnd().endsWith(";")) {
|
||||
out.push(buf);
|
||||
buf = "";
|
||||
on = false;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** # 函数 解析 SQL 值元组 */
|
||||
function parseTuples(s: string): string[][] {
|
||||
const out: string[][] = [];
|
||||
let p = s.indexOf("(");
|
||||
while (p >= 0 && p < s.length) {
|
||||
const values: string[] = [];
|
||||
let cur = "";
|
||||
let inQ = false;
|
||||
let esc = false;
|
||||
let depth = 0;
|
||||
p++;
|
||||
for (; p < s.length; p++) {
|
||||
const ch = s[p]!;
|
||||
if (esc) { cur += ch; esc = false; continue; }
|
||||
if (inQ) {
|
||||
if (ch === "\\") { esc = true; continue; }
|
||||
if (ch === "'") { inQ = false; continue; }
|
||||
cur += ch;
|
||||
continue;
|
||||
}
|
||||
if (ch === "'") { inQ = true; continue; }
|
||||
if (ch === "(") { depth++; cur += ch; continue; }
|
||||
if (ch === ")") {
|
||||
if (depth === 0) { values.push(cur); cur = ""; p++; break; }
|
||||
depth--;
|
||||
cur += ch;
|
||||
continue;
|
||||
}
|
||||
if (ch === "," && depth === 0) { values.push(cur); cur = ""; continue; }
|
||||
cur += ch;
|
||||
}
|
||||
out.push(values);
|
||||
p = s.indexOf("(", p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const lines = (await Bun.file(DUMP).text()).split("\n");
|
||||
|
||||
// term_id → name
|
||||
const nameById = new Map<number, string>();
|
||||
for (const seg of tableInserts(lines, "wp_terms")) {
|
||||
for (const v of parseTuples(seg)) {
|
||||
if (v[0]) nameById.set(Number(v[0]), v[1] ?? "");
|
||||
}
|
||||
}
|
||||
console.log("terms:", nameById.size);
|
||||
|
||||
// taxonomy → [term names](按 dump 插入顺序)
|
||||
const byTax = new Map<string, string[]>();
|
||||
for (const seg of tableInserts(lines, "wp_term_taxonomy")) {
|
||||
for (const v of parseTuples(seg)) {
|
||||
const taxonomy = v[2] ?? "";
|
||||
const termId = Number(v[1]);
|
||||
if (!taxonomy.startsWith("pa_")) continue;
|
||||
const name = nameById.get(termId);
|
||||
if (!name) continue;
|
||||
const list = byTax.get(taxonomy) ?? [];
|
||||
list.push(name);
|
||||
byTax.set(taxonomy, list);
|
||||
}
|
||||
}
|
||||
|
||||
let updated = 0;
|
||||
for (const [taxonomy, names] of byTax) {
|
||||
const attrSlug = taxonomy.slice(3);
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
const res = await db`
|
||||
UPDATE product_attribute_values v SET sort = ${i}
|
||||
FROM product_attributes a
|
||||
WHERE v.attribute_id = a.id AND a.slug = ${attrSlug} AND lower(trim(v.value)) = lower(trim(${names[i]!}))`;
|
||||
updated += res.count;
|
||||
}
|
||||
console.log(taxonomy, "→", names.length, "terms");
|
||||
}
|
||||
console.log("updated rows:", updated);
|
||||
await db.end();
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* # 脚本 导入产品 menu_order(WooCommerce 默认排序依据)
|
||||
* 原站 /shop/ 默认排序 = wp_posts.menu_order ASC(取证:DSOM-040R=1、DSOM-110R=3、
|
||||
* DSOM-130N=4,与线上商品顺序一致)。本项目 products 表无 menu_order 列,
|
||||
* 故写入 products.meta.menu_order,listProducts 默认排序读取该字段。
|
||||
* 数据源:legacy dump 的 wp_posts 全部 INSERT 段(每段 23 字段,menu_order 为第 20 列)。
|
||||
* 运行:bun scripts/import-product-menu-order.ts
|
||||
*/
|
||||
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");
|
||||
|
||||
/** # 函数 解析 SQL 值元组(仅支持字符串/数字,处理反斜杠转义与括号深度) */
|
||||
function parseTuples(s: string, from: number): string[][] {
|
||||
const out: string[][] = [];
|
||||
let p = from;
|
||||
while (p < s.length) {
|
||||
if (s[p] === ";") break;
|
||||
if (s[p] !== "(") { p++; continue; }
|
||||
const values: string[] = [];
|
||||
let cur = "";
|
||||
let inQ = false;
|
||||
let esc = false;
|
||||
let depth = 0;
|
||||
p++;
|
||||
for (; p < s.length; p++) {
|
||||
const ch = s[p]!;
|
||||
if (esc) { cur += ch; esc = false; continue; }
|
||||
if (inQ) {
|
||||
if (ch === "\\") { esc = true; continue; }
|
||||
if (ch === "'") { inQ = false; continue; }
|
||||
cur += ch;
|
||||
continue;
|
||||
}
|
||||
if (ch === "'") { inQ = true; continue; }
|
||||
if (ch === "(") { depth++; cur += ch; continue; }
|
||||
if (ch === ")") {
|
||||
if (depth === 0) { values.push(cur); cur = ""; p++; break; }
|
||||
depth--;
|
||||
cur += ch;
|
||||
continue;
|
||||
}
|
||||
if (ch === "," && depth === 0) { values.push(cur); cur = ""; continue; }
|
||||
cur += ch;
|
||||
}
|
||||
out.push(values);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const text = await Bun.file(DUMP).text();
|
||||
const rows: string[][] = [];
|
||||
let idx = 0;
|
||||
while (true) {
|
||||
const i = text.indexOf("INSERT INTO `wp_posts`", idx);
|
||||
if (i < 0) break;
|
||||
const start = text.indexOf("(", i);
|
||||
rows.push(...parseTuples(text, start));
|
||||
idx = text.indexOf(";", start);
|
||||
if (idx < 0) break;
|
||||
}
|
||||
const products = rows.filter((v) => v[20] === "product");
|
||||
console.log("dump product rows:", products.length);
|
||||
|
||||
let updated = 0;
|
||||
for (const v of products) {
|
||||
const legacyId = Number(v[0]);
|
||||
const order = Number(v[19] || 0);
|
||||
if (!legacyId) continue;
|
||||
const res = await db`
|
||||
UPDATE products SET meta = COALESCE(meta, '{}'::jsonb) || jsonb_build_object('menu_order', ${order})
|
||||
WHERE legacy_id = ${legacyId}`;
|
||||
if (res.count) updated += res.count;
|
||||
}
|
||||
console.log("updated products:", updated);
|
||||
await db.end();
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* # 脚本 导入产品归档页 Elementor 模板(legacy elementor_library post 1398)
|
||||
* 原站 /shop/ 与产品分类归档共用 archive 模板 post 1398(elementor_location=archive):
|
||||
* - a2ab15c:面包屑 hero(bg #F4F5F8 / padding 0 30 / content 1400)
|
||||
* - 65176b9:两列(20% WPC 筛选 + 80% 标题 + WooCommerce 商品归档)
|
||||
* - 61b1990:移动端归档描述(hide_desktop)
|
||||
* 写入 contents(type=template, slug=shop-archive, meta.elementor),供
|
||||
* collection 路由以 ElementorRenderer 直出外壳,筛选/商品网格按 widget id 注入。
|
||||
* 运行:bun scripts/import-shop-template.ts
|
||||
*/
|
||||
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");
|
||||
|
||||
/** # 函数 从 dump 提取指定 post 的 _elementor_data */
|
||||
async function extractTemplate(postId: number): Promise<Record<string, unknown>[]> {
|
||||
const text = await Bun.file(DUMP).text();
|
||||
const re = new RegExp("\\(\\d+," + postId + ",'_elementor_data','((?:[^'\\\\]|\\\\.)*)'\\)");
|
||||
const m = text.match(re);
|
||||
if (!m) throw new Error(`dump 中未找到 post ${postId} 的 _elementor_data`);
|
||||
const json = m[1]!.replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
||||
const data = JSON.parse(json);
|
||||
if (!Array.isArray(data)) throw new Error("_elementor_data 不是数组");
|
||||
return data as Record<string, unknown>[];
|
||||
}
|
||||
|
||||
const elements = await extractTemplate(1398);
|
||||
const meta = JSON.stringify({ elementor: elements });
|
||||
const existing = await db`SELECT id FROM contents WHERE slug='shop-archive' AND type='template'`;
|
||||
if (existing.length) {
|
||||
await db`UPDATE contents SET meta=${meta}::jsonb, status='published', updated_at=now() WHERE id=${existing[0]!.id}`;
|
||||
console.log("已更新 shop-archive 模板,contents.id =", existing[0]!.id);
|
||||
} else {
|
||||
const inserted = await db`
|
||||
INSERT INTO contents (type, slug, title, status, meta, legacy_id, published_at)
|
||||
VALUES ('template', 'shop-archive', 'Shop Archive', 'published', ${meta}::jsonb, 1398, now())
|
||||
RETURNING id`;
|
||||
console.log("已插入 shop-archive 模板,contents.id =", inserted[0]!.id);
|
||||
}
|
||||
console.log("顶层节:", elements.map((el) => el.id).join(", "));
|
||||
await db.end();
|
||||
Reference in New Issue
Block a user