feat: 龙里小程序第一次上传
This commit is contained in:
81
scripts/audit-css.mjs
Normal file
81
scripts/audit-css.mjs
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 关键工具类抽查:把产物里指定类的实际声明打出来,用于人工核对数值是否与设计稿一致。
|
||||
* 用法:node scripts/audit-css.mjs [distDir]
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const ROOT = path.resolve(process.argv[2] || "dist/build/mp-weixin");
|
||||
const css = fs.readFileSync(path.join(ROOT, "app.wxss"), "utf8");
|
||||
|
||||
/** 取出某个类选择器(含伪元素/伪类变体)的规则体 */
|
||||
function rule(cls, extra = "") {
|
||||
const escaped = cls.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\/g, "\\\\");
|
||||
const re = new RegExp(`\\.${escaped}${extra ? extra.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : ""}\\s*\\{([^}]*)\\}`, "g");
|
||||
const out = [];
|
||||
let m;
|
||||
while ((m = re.exec(css))) out.push(m[1].replace(/\s+/g, " ").trim());
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 找任意「含该子串的选择器」的规则(用于看被重写后的名字) */
|
||||
function ruleByFragment(frag) {
|
||||
const out = [];
|
||||
const re = /([^{}@/]+)\{([^}]*)\}/g;
|
||||
let m;
|
||||
while ((m = re.exec(css))) {
|
||||
if (m[1].includes(frag)) out.push([m[1].trim().slice(0, 90), m[2].replace(/\s+/g, " ").trim().slice(0, 260)]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const groups = [
|
||||
["① 全局变量与骨架", ["page", "button"]],
|
||||
["② 刻度换算(验证 --spacing 与 rem2rpx)", []],
|
||||
["③ 自定义 @utility:页面底 / 渐变 / 遮罩", []],
|
||||
["④ 伪元素装饰", []],
|
||||
["⑤ 设计 Token 生成的原子", ["bg-brand", "text-ink2", "text-ink3", "rounded-card", "rounded-hero", "rounded-panel", "rounded-dialog", "shadow-elev2", "shadow-ccard", "shadow-wcard"]],
|
||||
["⑥ 字号刻度", ["text-hint", "text-cap", "text-body", "text-title", "text-h1", "text-display"]],
|
||||
["⑦ 组合类(自定义 radius token)", []],
|
||||
["⑧ 滤镜 / 背景模糊 / 文字阴影", []],
|
||||
];
|
||||
|
||||
for (const [title] of groups) console.log(`\n===== ${title} =====`);
|
||||
|
||||
console.log("--- page ---");
|
||||
rule("page").forEach((r) => console.log(" " + r.slice(0, 400)));
|
||||
console.log("--- button::after ---");
|
||||
rule("button", ":after").forEach((r) => console.log(" " + r));
|
||||
|
||||
console.log("\n--- --spacing 定义 ---");
|
||||
ruleByFragment("--spacing:").slice(0, 3).forEach(([s, b]) => console.log(" " + s + " { " + b.slice(0, 120) + " }"));
|
||||
console.log("--- 数值刻度样例 gap-2 / px-4 / h-14 / w-11 / py-3 ---");
|
||||
for (const c of ["gap-2", "px-4", "h-14", "w-11", "py-3", "h-7", "gap-3", "mt-auto"]) {
|
||||
console.log(" ." + c + " -> " + (rule(c)[0] || "【缺】"));
|
||||
}
|
||||
|
||||
console.log("\n--- 自定义工具类 ---");
|
||||
for (const c of ["skin-home", "skin-detail", "grad-brand", "grad-cardside", "gradtext-accent", "gradtext-brand", "scrim-tile", "scrim-rec", "deco-notch", "deco-titledot", "deco-underline", "deco-hairline", "deco-handle", "deco-mark", "btn-reset", "grad-divider", "grad-call"]) {
|
||||
const r = rule(c);
|
||||
console.log(" ." + c + " -> " + (r.length ? r.join(" | ").slice(0, 220) : "【缺】"));
|
||||
}
|
||||
console.log(" 伪元素变体:");
|
||||
for (const frag of ["deco-notch:before", "deco-underline:after", "btn-reset:after"]) {
|
||||
ruleByFragment(frag).forEach(([s, b]) => console.log(" " + s + " { " + b.slice(0, 150) + " }"));
|
||||
}
|
||||
|
||||
console.log("\n--- 组合 radius token(rounded-l-card / rounded-r-card / rounded-t-panel)---");
|
||||
for (const c of ["rounded-l-card", "rounded-r-card", "rounded-t-panel", "rounded-t-dialog"]) {
|
||||
console.log(" ." + c + " -> " + (rule(c)[0] || "【缺】"));
|
||||
}
|
||||
|
||||
console.log("\n--- 滤镜 / 模糊 / 文字阴影 ---");
|
||||
ruleByFragment("grayscale").slice(0, 4).forEach(([s, b]) => console.log(" " + s + " { " + b.slice(0, 200) + " }"));
|
||||
ruleByFragment("brightness").slice(0, 3).forEach(([s, b]) => console.log(" " + s + " { " + b.slice(0, 200) + " }"));
|
||||
ruleByFragment("backdrop-blur").slice(0, 3).forEach(([s, b]) => console.log(" " + s + " { " + b.slice(0, 260) + " }"));
|
||||
ruleByFragment("text-shadow").slice(0, 3).forEach(([s, b]) => console.log(" " + s + " { " + b.slice(0, 200) + " }"));
|
||||
ruleByFragment("rotate").slice(0, 3).forEach(([s, b]) => console.log(" " + s + " { " + b.slice(0, 160) + " }"));
|
||||
|
||||
console.log("\n--- --tw-* 变量兜底初始化(weapp-tailwindcss 注入)---");
|
||||
const idx = css.indexOf("--tw-border-style");
|
||||
console.log(" " + css.slice(css.lastIndexOf("}", idx) + 1, idx + 700).replace(/\s+/g, " ").slice(0, 700));
|
||||
133
scripts/build-icons.mjs
Normal file
133
scripts/build-icons.mjs
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 图标生成脚本:微信小程序不支持内联 <svg>,因此把初设图里的 SVG symbol
|
||||
* 编译成 WXSS 的 background-image(base64 data-uri),按「名称 + 色调」生成类名。
|
||||
*
|
||||
* 用法:node scripts/build-icons.mjs
|
||||
* 产出:src/styles/icons.scss
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const ROOT = path.resolve(import.meta.dirname, "..");
|
||||
const OUT = path.join(ROOT, "src/styles/icons.scss");
|
||||
|
||||
/** 颜色 token(与 design-token.scss 保持一致) */
|
||||
const TONES = {
|
||||
ink: "#14181C",
|
||||
ink2: "#6B7280",
|
||||
ink3: "#9CA3AF",
|
||||
white: "#FFFFFF",
|
||||
brand: "#00B578",
|
||||
brandDeep: "#009A67",
|
||||
accent: "#FF8A3D",
|
||||
danger: "#E5484D",
|
||||
};
|
||||
|
||||
/** stroke 型图标:24×24 线性图标,stroke-width 1.9 */
|
||||
const STROKE_ICONS = {
|
||||
pin: '<path d="M12 21s7-6.1 7-11a7 7 0 1 0-14 0c0 4.9 7 11 7 11z"/><circle cx="12" cy="10" r="2.4"/>',
|
||||
arrow: '<path d="M4.5 12h15"/><path d="M13.5 6l6 6-6 6"/>',
|
||||
chev: '<path d="M9.5 5.5 16 12l-6.5 6.5"/>',
|
||||
back: '<path d="M14.5 5.5 8 12l6.5 6.5"/>',
|
||||
phone:
|
||||
'<path d="M4.2 4.4h3.9l1.9 4.8-2.4 1.5a12.2 12.2 0 0 0 5.8 5.8l1.5-2.4 4.8 1.9v3.9a1 1 0 0 1-1.1 1A16.8 16.8 0 0 1 3.2 5.5a1 1 0 0 1 1-1.1z"/>',
|
||||
copy: '<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15.5V5.5a1.5 1.5 0 0 1 1.5-1.5h10"/>',
|
||||
gift: '<path d="M20 12v8.5H4V12"/><path d="M2.5 7.5h19V12h-19z"/><path d="M12 7.5v13"/><path d="M12 7.5H8.6a2.3 2.3 0 1 1 0-4.6C11.6 2.9 12 7.5 12 7.5z"/><path d="M12 7.5h3.4a2.3 2.3 0 1 0 0-4.6C12.4 2.9 12 7.5 12 7.5z"/>',
|
||||
home: '<path d="M4 11.2 12 4.4l8 6.8v8.3a1 1 0 0 1-1 1h-4.2v-6H9.2v6H5a1 1 0 0 1-1-1z"/>',
|
||||
ticket:
|
||||
'<path d="M3.5 9.6a2 2 0 0 1 2-2h13a2 2 0 0 1 2 2v1.2a1.4 1.4 0 0 0 0 2.8v1.2a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2v-1.2a1.4 1.4 0 0 0 0-2.8z"/><path d="M13.6 7.6v2M13.6 13v2"/>',
|
||||
user: '<circle cx="12" cy="8.2" r="3.9"/><path d="M4.6 20.6a7.4 7.4 0 0 1 14.8 0"/>',
|
||||
check: '<path d="M5.5 12.6 10 17l8.5-9"/>',
|
||||
close: '<path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/>',
|
||||
doc: '<path d="M6.5 3.5h7.5l4.5 4.5v12.5h-12z"/><path d="M14 3.5V8h4.5"/><path d="M9 13h6M9 16.5h4.5"/>',
|
||||
shield: '<path d="M12 3.5 19.5 6v5.6c0 4.6-3.6 7.6-7.5 8.9-3.9-1.3-7.5-4.3-7.5-8.9V6z"/><path d="M9.2 12.2l2.2 2.2 3.6-3.6"/>',
|
||||
info: '<circle cx="12" cy="12" r="8.6"/><path d="M12 11v5.2"/><path d="M12 7.8h.01"/>',
|
||||
headset:
|
||||
'<path d="M4.5 15v-3a7.5 7.5 0 0 1 15 0v3"/><path d="M4.5 13.6h2.2v5H5.6a1.1 1.1 0 0 1-1.1-1.1z"/><path d="M19.5 13.6h-2.2v5h1.1a1.1 1.1 0 0 0 1.1-1.1z"/><path d="M17.3 18.6v.6a2 2 0 0 1-2 2h-2.8"/>',
|
||||
logout: '<path d="M15 12H4"/><path d="M11.4 8.4 15 12l-3.6 3.6"/><path d="M15.5 4.5h3a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-3"/>',
|
||||
trash:
|
||||
'<path d="M4.5 7h15"/><path d="M9.5 7V4.8h5V7"/><path d="M6.5 7l.9 13h9.2l.9-13"/><path d="M10.5 10.5v6M13.5 10.5v6"/>',
|
||||
share:
|
||||
'<path d="M12 16V3.8"/><path d="M8.2 7.6 12 3.8l3.8 3.8"/><path d="M5 13.6V19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-5.4"/>',
|
||||
clock: '<circle cx="12" cy="12" r="8.6"/><path d="M12 7.4v4.8l3.2 1.9"/>',
|
||||
map: '<path d="M9 4.5 3.5 6.6v13L9 17.4l6 2.1 5.5-2.1v-13L15 6.6z"/><path d="M9 4.5v12.9M15 6.6v12.9"/>',
|
||||
food: '<path d="M6.6 3.6v5M9.4 3.6v5"/><path d="M8 3.6v16.8"/><path d="M6.6 8.6h2.8"/><path d="M16.4 3.6c1.7 0 2.8 1.3 2.8 3.1 0 1.9-1.1 3.1-2.8 3.1s-2.8-1.2-2.8-3.1c0-1.8 1.1-3.1 2.8-3.1z"/><path d="M16.4 9.8v10.6"/>',
|
||||
bed: '<path d="M3.6 18.6V6.4"/><path d="M3.6 15h16.8v3.6"/><path d="M20.4 15v-2.6a1.6 1.6 0 0 0-1.6-1.6h-7V15"/><circle cx="7.6" cy="12.1" r="1.7"/>',
|
||||
peak: '<path d="M2.8 19.2 9 8.4l3.6 6.2 2.2-3.4 6.4 8z"/>',
|
||||
people:
|
||||
'<circle cx="9.2" cy="8.4" r="3.2"/><path d="M3.6 19.4a5.6 5.6 0 0 1 11.2 0"/><path d="M15.8 5.6a3.2 3.2 0 0 1 0 6"/><path d="M17.4 14.8a5.6 5.6 0 0 1 3 4.6"/>',
|
||||
};
|
||||
|
||||
/** fill 型图标(实心) */
|
||||
const FILL_ICONS = {
|
||||
wx: '<path d="M8.9 3.4C5.2 3.4 2.2 5.9 2.2 9c0 1.8 1 3.4 2.6 4.4l-.7 2.4 2.8-1.4c.6.2 1.3.3 2 .3h.5a5.4 5.4 0 0 1-.2-1.5c0-3 3-5.3 6.8-5.3h.5C16 5.3 12.7 3.4 8.9 3.4z"/><path d="M15.9 8.8c-3.4 0-6.1 2-6.1 4.4 0 1.5.9 2.8 2.3 3.6l-.6 1.9 2.3-1.1c.5.1 1.1.2 1.6.2 3.4 0 6.1-2 6.1-4.6s-2.7-4.4-5.6-4.4z"/>',
|
||||
};
|
||||
|
||||
/** 需要生成的「图标 × 色调」组合(只生成用得到的,控制体积) */
|
||||
const MATRIX = {
|
||||
pin: ["ink3", "brand", "white"],
|
||||
arrow: ["white", "brandDeep", "ink"],
|
||||
chev: ["ink3", "white", "brand", "ink2"],
|
||||
back: ["ink", "white", "brand"],
|
||||
phone: ["white", "brandDeep"],
|
||||
copy: ["brandDeep", "brand"],
|
||||
gift: ["white", "brandDeep", "accent"],
|
||||
home: ["ink3", "brand"],
|
||||
ticket: ["ink3", "brand", "accent", "white", "ink2"],
|
||||
user: ["ink3", "brand", "white", "ink2"],
|
||||
check: ["white", "brand"],
|
||||
close: ["ink3", "white"],
|
||||
doc: ["ink2", "brand"],
|
||||
shield: ["ink2", "white", "brand"],
|
||||
info: ["ink2", "brand"],
|
||||
headset: ["ink2", "brand"],
|
||||
logout: ["ink2", "white"],
|
||||
trash: ["ink2", "danger"],
|
||||
share: ["white"],
|
||||
clock: ["brand", "ink3", "white"],
|
||||
map: ["brandDeep", "white"],
|
||||
food: ["brandDeep", "accent", "white", "brand"],
|
||||
bed: ["accent", "brand", "white", "brandDeep"],
|
||||
peak: ["accent", "brand", "white"],
|
||||
people: ["ink3", "brand", "white", "brandDeep"],
|
||||
wx: ["white"],
|
||||
};
|
||||
|
||||
const buildSvg = (name, body, color, type) => {
|
||||
const style =
|
||||
type === "fill"
|
||||
? `fill='${color}' stroke='none'`
|
||||
: `fill='none' stroke='${color}' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'`;
|
||||
return `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' ${style}>${body}</svg>`;
|
||||
};
|
||||
|
||||
const toDataUri = (svg) => `data:image/svg+xml;base64,${Buffer.from(svg, "utf8").toString("base64")}`;
|
||||
|
||||
const lines = [
|
||||
"/* 由 scripts/build-icons.mjs 生成,请勿手改 */",
|
||||
"",
|
||||
".ai{display:block;flex:none;background-repeat:no-repeat;background-position:center;background-size:100% 100%}",
|
||||
"",
|
||||
];
|
||||
|
||||
let count = 0;
|
||||
for (const [name, tones] of Object.entries(MATRIX)) {
|
||||
const type = FILL_ICONS[name] ? "fill" : "stroke";
|
||||
const body = FILL_ICONS[name] || STROKE_ICONS[name];
|
||||
if (!body) throw new Error(`未定义的图标:${name}`);
|
||||
for (const tone of tones) {
|
||||
const color = TONES[tone];
|
||||
if (!color) throw new Error(`未定义的色调:${tone}`);
|
||||
if (type === "stroke") {
|
||||
// stroke 型图标 stroke-width 随字号缩放会变细,统一用 1.9
|
||||
}
|
||||
const uri = toDataUri(buildSvg(name, body, color, type));
|
||||
lines.push(`.ai-${name}.t-${tone}{background-image:url("${uri}")}`);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
fs.writeFileSync(OUT, lines.join("\n"), "utf8");
|
||||
const kb = (fs.statSync(OUT).size / 1024).toFixed(1);
|
||||
console.log(`icons.scss 已生成:${count} 条规则 / ${kb} KB`);
|
||||
199
scripts/build-mock.mjs
Normal file
199
scripts/build-mock.mjs
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* 一次性数据构建脚本:把 DuohuaDiscovery 的 miniprogram_data.json
|
||||
* 拆成小程序运行期使用的 mock 数据(字段与未来接口同构)。
|
||||
*
|
||||
* 运行:node scripts/build-mock.mjs
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const ROOT = path.resolve(import.meta.dirname, "..");
|
||||
const SRC = "/Users/gleen/OneFeel/YGChatCS/src/pages/DuohuaDiscovery/data/miniprogram_data.json";
|
||||
const OUT = path.join(ROOT, "src/mock");
|
||||
|
||||
const data = JSON.parse(fs.readFileSync(SRC, "utf8"));
|
||||
|
||||
const SECTION_DEFINITIONS = [
|
||||
{
|
||||
key: "tourism",
|
||||
title: "龙里票根经济",
|
||||
subtitle: "领取龙里旅游优惠",
|
||||
coverImage:
|
||||
"https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/longli/miniprogram/attractions/attraction-001/01.jpg",
|
||||
collections: ["attractions", "attraction_subprojects"],
|
||||
supportAssetIds: ["tourism-overview-map", "tourism-guide-qr"],
|
||||
},
|
||||
{
|
||||
key: "lodging",
|
||||
title: "旅居服务",
|
||||
subtitle: "酒店 · 民宿别院 · 旅居公寓 · 露营基地",
|
||||
collections: ["accommodations"],
|
||||
supportAssetIds: ["lodging-location-schematic", "lodging-guide-qr"],
|
||||
},
|
||||
{
|
||||
key: "food",
|
||||
title: "全域美食",
|
||||
subtitle: "特色菜肴 · 地方小吃 · 甜品饮品",
|
||||
collections: ["foods"],
|
||||
supportAssetIds: ["food-guide-qr"],
|
||||
},
|
||||
{
|
||||
key: "specialties",
|
||||
title: "龙里特产",
|
||||
subtitle: "刺梨制品 · 地方食品 · 农产品",
|
||||
collections: ["specialties"],
|
||||
supportAssetIds: [],
|
||||
},
|
||||
];
|
||||
|
||||
const COLLECTIONS = [
|
||||
{ key: "attractions", label: "景点" },
|
||||
{ key: "attraction_subprojects", label: "游乐演艺" },
|
||||
{ key: "accommodations", label: "旅居" },
|
||||
{ key: "foods", label: "美食" },
|
||||
{ key: "specialties", label: "特产" },
|
||||
];
|
||||
|
||||
/** 一期 mock:源数据无营业时间字段,统一占位,待运营方按商户补充 */
|
||||
const MOCK_BUSINESS_HOURS = "09:00–22:00";
|
||||
|
||||
const DROP_FIELDS = [
|
||||
"issues",
|
||||
"source",
|
||||
"source_no",
|
||||
"images",
|
||||
"main_image_object_key",
|
||||
"gallery_object_keys",
|
||||
"local_relative_path",
|
||||
"oss_object_key",
|
||||
"sha256",
|
||||
"source_file",
|
||||
"source_page",
|
||||
"pdf_image_name",
|
||||
"pdf_x0",
|
||||
"pdf_top",
|
||||
"pdf_x1",
|
||||
"pdf_bottom",
|
||||
"conversion_note",
|
||||
"color_mode",
|
||||
];
|
||||
|
||||
const trim = (obj) => {
|
||||
const out = {};
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
if (DROP_FIELDS.includes(k)) continue;
|
||||
if (v === null || v === undefined || v === "") continue;
|
||||
out[k] = v;
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
/** 电话归一:源数据里分隔符混用「—」「–」「-」,统一成半角连字符;另给纯数字供 makePhoneCall */
|
||||
const normalizeContact = (item) => {
|
||||
const raw = item?.contact?.raw || "";
|
||||
if (!raw) return null;
|
||||
const display = raw.replace(/[—–-~~\s]/g, "-").replace(/-+/g, "-").replace(/-$/, "");
|
||||
const dial = display.replace(/\D/g, "");
|
||||
return { raw, display, dial };
|
||||
};
|
||||
|
||||
/** 地址归一:源数据 address 是对象(含经纬度为 null),页面只用一个字符串;无地址返回空串(页面整行隐藏) */
|
||||
const normalizeAddress = (item) => {
|
||||
const addr = item?.address;
|
||||
if (!addr) return "";
|
||||
if (typeof addr === "string") return addr;
|
||||
if (addr.raw || addr.normalized) return addr.raw || addr.normalized;
|
||||
if (addr.township_or_street) {
|
||||
return [addr.province, addr.prefecture, addr.county, addr.township_or_street].filter(Boolean).join("");
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const items = {};
|
||||
const counts = {};
|
||||
|
||||
for (const { key } of COLLECTIONS) {
|
||||
const list = Array.isArray(data[key]) ? data[key] : [];
|
||||
let n = 0;
|
||||
for (const raw of list) {
|
||||
// 硬约束:过滤 enabled === false
|
||||
if (raw.enabled === false) continue;
|
||||
const item = trim(raw);
|
||||
delete item.enabled;
|
||||
delete item.address;
|
||||
item.collection = key;
|
||||
item.business_hours = item.business_hours || MOCK_BUSINESS_HOURS;
|
||||
item.contact = normalizeContact(raw);
|
||||
item.address_text = normalizeAddress(raw);
|
||||
item.badge = item.subtype || item.category || item.type || "";
|
||||
items[raw.id] = item;
|
||||
n += 1;
|
||||
}
|
||||
counts[key] = n;
|
||||
}
|
||||
|
||||
/** 分组目录:过滤 enabled=false / 空分组,按 type_order -> sort_within_type 排序 */
|
||||
const catalog = {};
|
||||
for (const { key } of COLLECTIONS) {
|
||||
const groups = Array.isArray(data?.catalog_by_type?.[key]) ? data.catalog_by_type[key] : [];
|
||||
catalog[key] = groups
|
||||
.map((group) => {
|
||||
const ids = (group.items || [])
|
||||
.filter((it) => items[it.id])
|
||||
.sort((a, b) => (a.sort_within_type ?? 0) - (b.sort_within_type ?? 0))
|
||||
.map((it) => it.id);
|
||||
return {
|
||||
collection: key,
|
||||
type_code: group.type_code,
|
||||
type: group.type,
|
||||
type_order: group.type_order ?? 0,
|
||||
item_count: ids.length,
|
||||
item_ids: ids,
|
||||
};
|
||||
})
|
||||
.filter((group) => group.item_count > 0)
|
||||
.sort((a, b) => a.type_order - b.type_order);
|
||||
}
|
||||
|
||||
const assets = (data.assets || []).map((a) => ({
|
||||
asset_id: a.asset_id,
|
||||
entity_type: a.entity_type,
|
||||
entity_id: a.entity_id,
|
||||
entity_name: a.entity_name,
|
||||
role: a.role,
|
||||
sort_order: a.sort_order ?? 0,
|
||||
oss_url: a.oss_url,
|
||||
width_px: a.width_px,
|
||||
height_px: a.height_px,
|
||||
content_type: a.content_type,
|
||||
}));
|
||||
|
||||
const sections = SECTION_DEFINITIONS.map((def) => {
|
||||
const groups = def.collections.flatMap((c) =>
|
||||
catalog[c].map((g) => ({ ...g, groupId: `${c}-${g.type_code}` }))
|
||||
);
|
||||
const ids = groups.flatMap((g) => g.item_ids);
|
||||
const first = items[ids[0]];
|
||||
return {
|
||||
...def,
|
||||
group_count: groups.length,
|
||||
item_count: ids.length,
|
||||
group_ids: groups.map((g) => g.groupId),
|
||||
};
|
||||
});
|
||||
|
||||
const write = (name, payload) => {
|
||||
const file = path.join(OUT, name);
|
||||
fs.writeFileSync(file, JSON.stringify(payload, null, 0) + "\n", "utf8");
|
||||
const kb = (fs.statSync(file).size / 1024).toFixed(1);
|
||||
console.log(`${name.padEnd(16)} ${kb} KB`);
|
||||
};
|
||||
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
write("sections.json", sections);
|
||||
write("catalog.json", catalog);
|
||||
write("items.json", items);
|
||||
write("assets.json", assets);
|
||||
|
||||
console.log("\n集合条数(已过滤 enabled=false):", counts);
|
||||
console.log("items 总数:", Object.keys(items).length);
|
||||
88
scripts/verify-build.mjs
Normal file
88
scripts/verify-build.mjs
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 产物结构校验:把 dist 里所有 WXML 的 class 抽出来,回查是否都有对应 WXSS 规则。
|
||||
* 用途:Tailwind 改造后确认「模板写的类」与「编译出的样式」没有错配(尤其任意值类会被
|
||||
* weapp-tailwindcss 重写成安全类名,肉眼看不出来)。
|
||||
* 用法:node scripts/verify-build.mjs [distDir]
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const ROOT = path.resolve(process.argv[2] || "dist/build/mp-weixin");
|
||||
|
||||
if (!fs.existsSync(ROOT)) {
|
||||
console.error(`找不到构建产物目录:${ROOT}\n请先执行 npm run build:mp-weixin`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const cssFiles = [];
|
||||
const wxmlFiles = [];
|
||||
(function walk(dir) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) walk(full);
|
||||
else if (entry.name.endsWith(".wxss")) cssFiles.push(full);
|
||||
else if (entry.name.endsWith(".wxml")) wxmlFiles.push(full);
|
||||
}
|
||||
})(ROOT);
|
||||
|
||||
/* ---- 1. 收集 WXSS 中定义过的类名 ---- */
|
||||
const defined = new Set();
|
||||
let cssBytes = 0;
|
||||
for (const file of cssFiles) {
|
||||
const css = fs.readFileSync(file, "utf8");
|
||||
cssBytes += Buffer.byteLength(css);
|
||||
// 选择器里的 .class,可能带反斜杠转义(如 .w-\[10px\])
|
||||
for (const m of css.matchAll(/\.((?:[A-Za-z0-9_-]|\\.)+)/g)) {
|
||||
defined.add(m[1].replace(/\\/g, ""));
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 2. 收集 WXML 里 class 属性用到的 token ---- */
|
||||
const used = new Map();
|
||||
let staticCount = 0;
|
||||
let dynamicCount = 0;
|
||||
|
||||
for (const file of wxmlFiles) {
|
||||
const src = fs.readFileSync(file, "utf8");
|
||||
for (const m of src.matchAll(/class="([^"]*)"/g)) {
|
||||
let value = m[1];
|
||||
if (value.includes("{{")) {
|
||||
// 动态绑定:把三元/对象里的字符串字面量也捞出来一起校验
|
||||
dynamicCount += 1;
|
||||
for (const lit of value.match(/'[^']*'|"[^"]*"/g) || []) value += ` ${lit.slice(1, -1)}`;
|
||||
value = value.replace(/\{\{[^}]*\}\}/g, " ");
|
||||
} else {
|
||||
staticCount += 1;
|
||||
}
|
||||
for (const token of value.split(/\s+/)) {
|
||||
const cls = token.trim();
|
||||
if (!cls) continue;
|
||||
if (!used.has(cls)) used.set(cls, new Set());
|
||||
used.get(cls).add(path.relative(ROOT, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 3. 校验 ---- */
|
||||
const missing = [];
|
||||
for (const [cls, files] of used) {
|
||||
if (!defined.has(cls)) missing.push([cls, [...files].slice(0, 3)]);
|
||||
}
|
||||
|
||||
console.log("产物目录 :", path.relative(process.cwd(), ROOT));
|
||||
console.log("wxss 文件 :", cssFiles.length, `(合计 ${(cssBytes / 1024).toFixed(1)} KB)`);
|
||||
console.log("app.wxss :", `${(fs.statSync(path.join(ROOT, "app.wxss")).size / 1024).toFixed(1)} KB`);
|
||||
console.log("wxml 文件 :", wxmlFiles.length);
|
||||
console.log("class 静态处数 :", staticCount, " 动态处数 :", dynamicCount);
|
||||
console.log("涉及 class :", used.size);
|
||||
console.log("");
|
||||
|
||||
if (missing.length === 0) {
|
||||
console.log("✅ 结构校验通过:所有 class 均有对应样式规则,0 错配");
|
||||
} else {
|
||||
console.log(`❌ 有 ${missing.length} 个 class 找不到样式规则:`);
|
||||
for (const [cls, files] of missing.slice(0, 80)) {
|
||||
console.log(` .${cls} <- ${files.join(", ")}`);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
}
|
||||
Reference in New Issue
Block a user