/** * 一次性数据构建脚本:把 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);