const { request, downloadFile, readFile } = require('./wx-promisify') const localFonts = require('../../assets/fonts') const localDefaultConfig = require('../../assets/default') const fontBufferCache = new Map() const MAX_FONT_CACHE = 4 function normalizePath(path, baseUrl) { if (!path) { return '' } if (/^https?:\/\//i.test(path)) { return path } if (path.startsWith('//')) { return `https:${path}` } if (path.startsWith('/')) { return `${baseUrl}${path}` } return `${baseUrl}/${path}` } function normalizeFontItem(item, baseUrl) { const path = item.path || item.url || '' const normalizedPath = normalizePath(path, baseUrl) const filename = item.filename || normalizedPath.split('/').pop() || `${item.name || 'font'}.ttf` return { id: item.id || `${item.category || '默认'}/${item.name || filename}`, name: item.name || filename.replace(/\.[^.]+$/, ''), category: item.category || '默认', filename, path, url: normalizedPath, } } function normalizeManifest(fonts, baseUrl) { if (!Array.isArray(fonts)) { return [] } return fonts .map((item) => normalizeFontItem(item, baseUrl)) .filter((item) => item.url) } function normalizeDefaultConfig(config) { const payload = config && typeof config === 'object' ? config : {} const selectedRaw = Array.isArray(payload.selectedFontIds) ? payload.selectedFontIds : (Array.isArray(payload.selectedFonts) ? payload.selectedFonts : []) const favoriteRaw = Array.isArray(payload.favoriteFontIds) ? payload.favoriteFontIds : (Array.isArray(payload.favoriteFonts) ? payload.favoriteFonts : []) const selectedFontIds = Array.from( new Set( selectedRaw .map((item) => String(item || '').trim()) .filter(Boolean), ), ) const favoriteFontIds = Array.from( new Set( favoriteRaw .map((item) => String(item || '').trim()) .filter(Boolean), ), ) const result = { selectedFontIds, favoriteFontIds, } if (typeof payload.inputText === 'string') { result.inputText = payload.inputText } if (typeof payload.textColor === 'string') { result.textColor = payload.textColor } if (payload.fontSize !== undefined && payload.fontSize !== null && payload.fontSize !== '') { const fontSize = Number(payload.fontSize) if (Number.isFinite(fontSize)) { result.fontSize = fontSize } } if (payload.letterSpacing !== undefined && payload.letterSpacing !== null && payload.letterSpacing !== '') { const letterSpacing = Number(payload.letterSpacing) if (Number.isFinite(letterSpacing)) { result.letterSpacing = letterSpacing } } return result } function buildDefaultConfigUrl(manifestUrl, baseUrl) { const manifest = String(manifestUrl || '').trim() if (manifest) { const withoutHash = manifest.split('#')[0] const [pathPart] = withoutHash.split('?') const slashIndex = pathPart.lastIndexOf('/') if (slashIndex >= 0) { return `${pathPart.slice(0, slashIndex + 1)}default.json` } } return normalizePath('/miniprogram/assets/default.json', baseUrl) } async function loadFontsManifest(options = {}) { const app = getApp() const manifestUrl = options.manifestUrl || app.globalData.fontsManifestUrl const baseUrl = options.baseUrl || app.globalData.fontsBaseUrl if (Array.isArray(app.globalData.fonts) && app.globalData.fonts.length > 0) { return app.globalData.fonts } try { const response = await request({ url: manifestUrl, method: 'GET', timeout: 10000, }) if (response.statusCode < 200 || response.statusCode >= 300) { throw new Error(`获取字体清单失败,状态码: ${response.statusCode}`) } const fonts = normalizeManifest(response.data, baseUrl) if (!fonts.length) { throw new Error('字体清单为空') } app.globalData.fonts = fonts return fonts } catch (error) { console.warn('远程字体清单加载失败,回退到本地清单:', error) const fallbackFonts = normalizeManifest(localFonts, baseUrl) app.globalData.fonts = fallbackFonts return fallbackFonts } } async function loadDefaultConfig(options = {}) { const app = getApp() const manifestUrl = options.manifestUrl || app.globalData.fontsManifestUrl const baseUrl = options.baseUrl || app.globalData.fontsBaseUrl const defaultConfigUrl = options.defaultConfigUrl || app.globalData.defaultConfigUrl || buildDefaultConfigUrl(manifestUrl, baseUrl) if (app.globalData.defaultConfig && typeof app.globalData.defaultConfig === 'object') { return app.globalData.defaultConfig } try { const response = await request({ url: defaultConfigUrl, method: 'GET', timeout: 10000, }) if (response.statusCode < 200 || response.statusCode >= 300) { throw new Error(`获取默认配置失败,状态码: ${response.statusCode}`) } const remoteConfig = normalizeDefaultConfig(response.data) app.globalData.defaultConfig = remoteConfig return remoteConfig } catch (error) { console.warn('远程 default.json 加载失败,回退到本地默认配置:', error) const fallbackConfig = normalizeDefaultConfig(localDefaultConfig) app.globalData.defaultConfig = fallbackConfig return fallbackConfig } } function setLruCache(key, value) { if (fontBufferCache.has(key)) { fontBufferCache.delete(key) } fontBufferCache.set(key, value) while (fontBufferCache.size > MAX_FONT_CACHE) { const firstKey = fontBufferCache.keys().next().value fontBufferCache.delete(firstKey) } } async function loadFontBuffer(fontItem) { const cacheKey = fontItem.id if (fontBufferCache.has(cacheKey)) { const cached = fontBufferCache.get(cacheKey) setLruCache(cacheKey, cached) return cached } if (!fontItem.url) { throw new Error('字体地址为空') } const downloadRes = await downloadFile({ url: fontItem.url }) if (downloadRes.statusCode < 200 || downloadRes.statusCode >= 300) { throw new Error(`字体下载失败,状态码: ${downloadRes.statusCode}`) } const readRes = await readFile(downloadRes.tempFilePath) const result = { tempFilePath: downloadRes.tempFilePath, buffer: readRes.data, } setLruCache(cacheKey, result) return result } function listCategories(fonts) { const set = new Set(fonts.map((font) => font.category || '默认')) return ['全部', '收藏', ...Array.from(set)] } module.exports = { loadFontsManifest, loadDefaultConfig, loadFontBuffer, listCategories, }