/** * 画个标 · 海报素材库加载器(懒加载) * 素材库由 scripts/build-posters.js 构建拆分为: * - js/data/poster-builtin.js 轻量索引(sources 元数据 + 分类索引,无全量内容) * - js/data/posters/{id}.js 每源一个数据文件(HGB_PosterSource_{id} = {items:[...]}) * 本模块负责:按源懒加载 + 缓存 + 同步读已加载源 + 预加载核心源 * * 与图标素材库(iconlib.js)的差异: * - 海报素材为全彩/位图/模板,保留原始宽高比,渲染不再归一化到 24 视口 * - items 字段而非 icons;每条含 type/category/categoryCn/workspaces 归类标签 */ const HGB_PosterLib = (function () { 'use strict'; const VERSION = '20260823e'; // 与 poster.html 中 poster-builtin.js?v= 版本联动 const cache = {}; // sid -> {items:[...]} const pending = {}; // sid -> Promise function fileURL(sid) { return 'js/data/posters/' + sid + '.js?v=' + VERSION; } function loadScript(url) { return new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = url; s.onload = resolve; s.onerror = function () { reject(new Error('海报素材源加载失败: ' + url)); }; document.head.appendChild(s); }); } /** 加载单个源(缓存 + 并发去重) */ function loadSource(sid) { if (cache[sid]) return Promise.resolve(cache[sid]); if (pending[sid]) return pending[sid]; pending[sid] = loadScript(fileURL(sid)).then(() => { const data = window['HGB_PosterSource_' + sid]; if (!data || !data.items) throw new Error('海报素材源不存在: ' + sid); cache[sid] = data; delete pending[sid]; return data; }).catch((e) => { delete pending[sid]; throw e; }); return pending[sid]; } /** 并行加载多个源(全部成功才 resolve) */ function loadSources(ids) { return Promise.all((ids || []).map(loadSource)); } /** 并行加载多个源(容忍单个失败,返回已成功加载的) */ function preload(ids) { return Promise.allSettled((ids || []).map(loadSource)).then((rs) => rs.filter(r => r.status === 'fulfilled').map(r => r.value)); } /** 加载全部源 */ function loadAll() { const ids = Object.keys((typeof HGB_PosterBuiltin !== 'undefined' ? HGB_PosterBuiltin.sources : {}) || {}); return preload(ids); } /** 同步读取已加载源(未加载返回 null) */ function getSource(sid) { return cache[sid] || null; } /** 是否已加载 */ function loaded(sid) { return !!cache[sid]; } /** 已加载的全部源 id */ function loadedIds() { return Object.keys(cache); } /** 从已加载源按 名称/id 找素材(返回 {id,name,svg,type,...} 或 null) */ function findPoster(ref) { if (!ref || typeof ref !== 'string') return null; const name = ref.trim(); const parts = name.split(':'); if (parts.length === 2) { const src = cache[parts[0]]; const hit = src && (src.items || []).find(i => i.name === parts[1] || i.id === name); if (hit) return hit; } const lower = name.toLowerCase(); for (const sid of Object.keys(cache)) { const hit = (cache[sid].items || []).find(i => i.id === name || i.name === name || String(i.name).toLowerCase() === lower || String(i.name).toLowerCase().includes(lower)); if (hit) return hit; } return null; } /** 搜索已加载源(词根包含匹配),返回完整素材对象 */ function searchLoaded(keyword, limit) { const kw = String(keyword || '').toLowerCase(); const out = []; for (const sid of Object.keys(cache)) { for (const it of cache[sid].items || []) { if (it.name && it.name.toLowerCase().includes(kw)) { out.push({ id: sid + ':' + it.name, name: it.name, svg: it.svg, source: sid, type: it.type }); if (out.length >= (limit || 60)) return out; } } } return out; } /** 搜索索引(全量 id/name,无内容,用于筛选展示) */ function searchIndex(keyword, limit) { const kw = String(keyword || '').toLowerCase(); const out = []; const idx = (typeof HGB_PosterBuiltin !== 'undefined' && HGB_PosterBuiltin.itemIndex) || []; for (const it of idx) { if (it.name && it.name.toLowerCase().includes(kw)) { out.push({ id: it.id, name: it.name, source: String(it.id).split(':')[0], category: it.category }); if (out.length >= (limit || 20)) return out; } } return out; } /** 按 id 反查来源 id */ function sourceOf(fullId) { return String(fullId || '').split(':')[0]; } return { VERSION, loadSource, loadSources, preload, loadAll, getSource, loaded, loadedIds, findPoster, searchLoaded, searchIndex, sourceOf }; })();