diff --git a/apps/jingrow/frontend/src/core/registry/pagetypeOverride.ts b/apps/jingrow/frontend/src/core/registry/pagetypeOverride.ts index a1ff10a..b3513f8 100644 --- a/apps/jingrow/frontend/src/core/registry/pagetypeOverride.ts +++ b/apps/jingrow/frontend/src/core/registry/pagetypeOverride.ts @@ -1,65 +1,108 @@ -// 基于约定路径自动解析 pagetype 详情覆盖组件 -// 约定:/src/views/pagetype//.vue +// 基于约定路径自动解析 pagetype 覆盖组件 +// 详情:/src/views/pagetype//.vue +// 工具栏:/src/views/pagetype//_toolbar.vue + +// 由 Vite define 注入,类型声明便于 TS +declare const __APPS_ORDER__: string[] type AsyncComponentLoader = () => Promise // 扫描所有可能的覆盖组件(仅限 .vue) -// 使用 eager: false 延迟加载,生产环境下可按需分包 -const allPagetypeViews: Record = import.meta.glob( - '/src/views/pagetype/**/**.vue' -) +// - 扫描所有 apps 下的前端视图:@apps/*/frontend/src/views/pagetype +// - 同时扫描当前应用 src 下的视图:/src/views/pagetype +// 说明:@apps 别名与 devServer.fs.allow 已在 vite.config.ts 放行 +const globAllApps = import.meta.glob('@apps/*/frontend/src/views/pagetype/**/**.vue') +const globLocal = import.meta.glob('/src/views/pagetype/**/**.vue') + +type SourceEntry = { + loader: AsyncComponentLoader + appName: string + fullPath: string +} + +const allPagetypeViews: Record = {} function getPathSegments(path: string): string[] { - // 统一分隔符 return path.split('/').filter(Boolean) } +function extractAppName(absPath: string): string { + // 形如 @apps//frontend/src/... + const parts = absPath.split('/') + const idx = parts.indexOf('apps') + if (idx >= 0 && idx + 1 < parts.length) { + return parts[idx + 1] + } + // 本地 src 视为 jingrow(核心) + return 'jingrow' +} + +function appsOrder(): string[] { + return Array.isArray(__APPS_ORDER__) && __APPS_ORDER__.length > 0 ? __APPS_ORDER__ : ['jingrow'] +} + +function rankByAppsOrder(appName: string): number { + const order = appsOrder() + const idx = order.indexOf(appName) + return idx >= 0 ? idx : -1 +} + +for (const [p, loader] of Object.entries(globAllApps)) { + const appName = extractAppName(p) + allPagetypeViews[p] = { loader: loader as AsyncComponentLoader, appName, fullPath: p } +} +for (const [p, loader] of Object.entries(globLocal)) { + const appName = extractAppName(p) + allPagetypeViews[p] = { loader: loader as AsyncComponentLoader, appName, fullPath: p } +} + +function sortByPriority(a: string, b: string): number { + const ra = rankByAppsOrder(allPagetypeViews[a].appName) + const rb = rankByAppsOrder(allPagetypeViews[b].appName) + // apps.txt 靠后优先 => 更大的索引优先 + if (ra !== rb) return rb - ra + // 同一应用下:路径更短优先,其次字母序 + if (a.length !== b.length) return a.length - b.length + return a.localeCompare(b) +} + /** * 解析并返回指定 pagetype 的详情覆盖组件 * @param pagetypeSlug 路由中的原始 pagetype(保持小写与下划线) */ export async function resolvePagetypeDetailOverride(pagetypeSlug: string): Promise { if (!pagetypeSlug) return null - // URL 使用短横线,文件夹使用下划线,与后端保持一致 const targetHyphen = pagetypeSlug.toLowerCase() const targetUnderscore = targetHyphen.replace(/-/g, '_') - // 匹配形如 views/pagetype//.vue 的文件 const candidates = Object.keys(allPagetypeViews).filter((file) => { const segs = getPathSegments(file) const len = segs.length if (len < 5) return false - // 路径应为 views/pagetype//.vue const fileName = segs[len - 1] const folderName = segs[len - 2] const baseName = fileName.replace(/\.vue$/i, '') - // 需要同时满足:位于 views/pagetype 下,且末两级目录名与目标一致(支持下划线形式) return ( - segs[1] === 'views' && - segs[2] === 'pagetype' && - folderName === targetUnderscore && + segs.includes('views') && + segs.includes('pagetype') && + folderName === targetUnderscore && baseName === targetUnderscore ) }) if (candidates.length === 0) return null - // 若有多个命中,按路径长度最短优先(更接近根的优先),其次按字母序 - candidates.sort((a, b) => { - if (a.length !== b.length) return a.length - b.length - return a.localeCompare(b) - }) + candidates.sort(sortByPriority) const pick = candidates[0] try { - const mod = await allPagetypeViews[pick]() + const mod = await allPagetypeViews[pick].loader() return mod?.default ?? mod } catch (_e) { return null } } - /** * 解析并返回指定 pagetype 的工具栏覆盖组件 * 约定文件名:/src/views/pagetype//_toolbar.vue @@ -73,32 +116,27 @@ export async function resolvePagetypeToolbarOverride(pagetypeSlug: string): Prom const segs = getPathSegments(file) const len = segs.length if (len < 5) return false - // 路径应为 views/pagetype//_toolbar.vue const fileName = segs[len - 1] const folderName = segs[len - 2] const baseName = fileName.replace(/\.vue$/i, '') return ( - segs[1] === 'views' && - segs[2] === 'pagetype' && - folderName === targetUnderscore && + segs.includes('views') && + segs.includes('pagetype') && + folderName === targetUnderscore && baseName === `${targetUnderscore}_toolbar` ) }) if (candidates.length === 0) return null - candidates.sort((a, b) => { - if (a.length !== b.length) return a.length - b.length - return a.localeCompare(b) - }) + candidates.sort(sortByPriority) const pick = candidates[0] try { - const mod = await allPagetypeViews[pick]() + const mod = await allPagetypeViews[pick].loader() return mod?.default ?? mod } catch (_e) { return null } } - diff --git a/apps/jingrow/frontend/vite.config.ts b/apps/jingrow/frontend/vite.config.ts index 3a93e8c..cac89da 100644 --- a/apps/jingrow/frontend/vite.config.ts +++ b/apps/jingrow/frontend/vite.config.ts @@ -5,6 +5,27 @@ import { fileURLToPath, URL } from 'node:url' import Icons from 'unplugin-icons/vite' import IconsResolver from 'unplugin-icons/resolver' import Components from 'unplugin-vue-components/vite' +import fs from 'node:fs' +import path from 'node:path' + +// 读取 apps.txt 确定应用优先级(靠后优先) +function loadAppsOrder(appsDir: string) { + const appsTxt = path.join(appsDir, 'apps.txt') + try { + const content = fs.readFileSync(appsTxt, 'utf-8') + return content + .split(/\r?\n/) + .map((s) => s.trim()) + .filter(Boolean) + } catch { + return ['jingrow'] + } +} + +// 计算本工程中的 apps 目录(当前文件位于 apps/jingrow/frontend/vite.config.ts) +const currentDir = fileURLToPath(new URL('.', import.meta.url)) +const appsDir = path.resolve(currentDir, '..', '..') +const APPS_ORDER = loadAppsOrder(appsDir) export default defineConfig({ plugins: [ @@ -24,7 +45,9 @@ export default defineConfig({ ], resolve: { alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)) + '@': fileURLToPath(new URL('./src', import.meta.url)), + // 跨 app 访问源码(相对计算得到的 apps 目录) + '@apps': appsDir } }, server: { @@ -33,6 +56,10 @@ export default defineConfig({ strictPort: true, open: false, cors: true, + fs: { + // 放行 monorepo apps 目录,便于 import.meta.glob 跨应用扫描 + allow: [appsDir] + }, allowedHosts: ['code.jingrow.com'], proxy: { '/api/action': { @@ -58,6 +85,8 @@ export default defineConfig({ }, define: { // 确保环境变量在构建时可用 - __APP_VERSION__: JSON.stringify(process.env.npm_package_version) + __APP_VERSION__: JSON.stringify(process.env.npm_package_version), + // 注入 apps.txt 的应用顺序到前端 + __APPS_ORDER__: JSON.stringify(APPS_ORDER) } })