/** * 插件语言与 manifest 初始化(供 core shared admin-lang 与 addon-loader 共用) */ export interface AddonManifest { key: string version?: string sharedVersion?: string views?: Record components?: Record layout?: string langBase?: string } let prodManifests: Record = {} let installedAddonKeys: string[] = [] let prodReady = false let prodInitPromise: Promise | null = null const addonModuleCache: Record | null>> = {} const ADMIN_PREFIX = '/admin' export function adminUrl(relative: string) { if (!relative) return relative if (relative.startsWith('http') || relative.startsWith('/admin/')) return relative if (relative.startsWith('/assets/')) return `${ADMIN_PREFIX}${relative}` if (relative.startsWith('./')) return `${ADMIN_PREFIX}/assets/addons/${relative.slice(2)}` return `${ADMIN_PREFIX}/${relative.replace(/^\//, '')}` } function addonAssetUrl(addon: string, ...segments: string[]) { const rel = ['assets', 'addons', addon, ...segments].join('/').replace(/\/+/g, '/') return `${ADMIN_PREFIX}/${rel}` } export function resolveLangFile(app: string, path: string): string { if (path === '/') return 'index' let view = path.replace(/^(\/admin\/|\/site\/|\/)/, '').replace(/\.vue$/, '') if (view.startsWith('views/')) view = view.slice('views/'.length) if (app) { if (view.startsWith(`${app}/`)) view = view.slice(app.length + 1) else if (view === app) view = 'index' } return view.replaceAll('/', '.') } export function resolveRouteAddon(route: { meta: Record; matched: Array<{ meta: Record }> }): string { if (route.meta.addon) return String(route.meta.addon) for (let i = route.matched.length - 1; i >= 0; i--) { const addon = route.matched[i].meta.addon if (addon) return String(addon) } return '' } export function resolveRouteView(route: { meta: Record; matched: Array<{ meta: Record }>; path: string }): string { if (route.meta.view) return String(route.meta.view) for (let i = route.matched.length - 1; i >= 0; i--) { const view = route.matched[i].meta.view if (view) return String(view) } return route.path } function unwrapLangPack(pack: unknown): Record { if (!pack || typeof pack !== 'object') return {} const obj = pack as Record if (obj.default && typeof obj.default === 'object') { return unwrapLangPack(obj.default) } const result: Record = {} for (const [key, value] of Object.entries(obj)) { if (key === 'default' || key === '__esModule') continue if (typeof value === 'string') result[key] = value } return Object.keys(result).length ? result : (obj as Record) } function flattenLangPack(file: string, pack: Record): Record { const data: Record = {} for (const [key, value] of Object.entries(pack)) { if (typeof value !== 'string') continue data[`${file}.${key}`] = value if (file === 'common') data[key] = value } return data } export function inferAddonFromPath(path: string, knownAddons?: string[]): string { const view = path.replace(/^(\/admin\/|\/site\/|\/)/, '') const first = view.split('/').filter(Boolean)[0] if (!first) return '' const keys = knownAddons?.length ? knownAddons : getInstalledAddonKeys() return keys.includes(first) ? first : '' } async function isAddonEntryAvailable(key: string): Promise { try { const res = await fetch(adminUrl(`/assets/addons/${key}/index.js`), { method: 'HEAD' }) if (!res.ok) return false const ct = (res.headers.get('content-type') || '').toLowerCase() return !ct.includes('text/html') } catch { return false } } export function isAddonInstalled(addon: string): boolean { return !!addon && getInstalledAddonKeys().includes(addon) } export async function loadAddonModule(addon: string): Promise | null> { if (!(addon in addonModuleCache)) { addonModuleCache[addon] = (async () => { try { const url = adminUrl(`/assets/addons/${addon}/index.js`) return await import(/* @vite-ignore */ url) as Record } catch { return null } })() } return addonModuleCache[addon] } /** 清除插件模块缓存(布局切换时调用,确保样式重新注入) */ export function clearAddonModuleCache() { for (const key of Object.keys(addonModuleCache)) { delete addonModuleCache[key] } } export function isAddonProdReady() { return prodReady } export async function ensureAddonProdReady() { if (!prodReady) await initAddonManifests() } export async function initAddonManifests(keys?: string[]) { if (import.meta.env.DEV) { prodReady = true return } if (prodInitPromise) return prodInitPromise prodInitPromise = (async () => { prodManifests = {} installedAddonKeys = [] let addonKeys = keys if (!addonKeys?.length) { try { const indexUrl = adminUrl('/assets/addons/index.json') const res = await fetch(indexUrl) if (res.ok) { const data = await res.json() addonKeys = Array.isArray(data.keys) ? data.keys : [] } } catch { addonKeys = [] } } const verifiedKeys: string[] = [] for (const key of addonKeys || []) { if (!(await isAddonEntryAvailable(key))) continue verifiedKeys.push(key) try { const url = adminUrl(`/assets/addons/${key}/manifest.json`) const res = await fetch(url) if (res.ok) { prodManifests[key] = await res.json() } } catch { // skip missing manifest } } installedAddonKeys = verifiedKeys prodReady = true })() return prodInitPromise } export function getInstalledAddonKeys(): string[] { if (installedAddonKeys.length) return installedAddonKeys return Object.keys(prodManifests) } export function getProdManifests(): Record { return prodManifests } export async function preloadAllAddonLangs( merge: (locale: string, messages: Record) => void ) { if (import.meta.env.DEV) return if (!prodReady) await initAddonManifests() for (const addon of getInstalledAddonKeys()) { try { const mod = await loadAddonModule(addon) if (!mod) continue const langs = mod.langs as Record> | undefined if (!langs) continue for (const [locale, files] of Object.entries(langs)) { const merged: Record = {} for (const [file, pack] of Object.entries(files || {})) { Object.assign(merged, flattenLangPack(file, unwrapLangPack(pack))) } if (Object.keys(merged).length) merge(locale, merged) } } catch { // skip broken addon module } } } export function registerAddonManifest(manifest: AddonManifest) { prodManifests[manifest.key] = manifest prodReady = true } export async function loadAddonLang(addon: string, locale: string, file: string): Promise> { if (import.meta.env.DEV) { try { const messages = await import(/* @vite-ignore */ `@/addon/${addon}/lang/${locale}/${file}.json`) return messages.default || {} } catch { return {} } } if (!prodReady) await initAddonManifests() try { const mod = await loadAddonModule(addon) if (mod) { const langs = mod.langs as Record>> | undefined const pack = langs?.[locale]?.[file] const messages = unwrapLangPack(pack) if (Object.keys(messages).length) return messages } } catch { // fallback to static lang files } const url = addonAssetUrl(addon, 'lang', locale, `${file}.json`) try { const res = await fetch(url) if (res.ok) return await res.json() } catch { // ignore } return {} }