From b06109f675da35b53201499afe0a7d17a3c67134 Mon Sep 17 00:00:00 2001 From: jingrow Date: Tue, 28 Oct 2025 19:40:11 +0800 Subject: [PATCH] =?UTF-8?q?pagetype=E7=9A=84sidebar=E5=AE=9E=E7=8E=B0app?= =?UTF-8?q?=E4=BC=98=E5=85=88=E7=BA=A7=E8=A6=86=E7=9B=96=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/core/pagetype/GenericDetailPage.vue | 22 ++- .../src/core/registry/sidebarOverride.ts | 151 ++++++++++++++++++ 2 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 apps/jingrow/frontend/src/core/registry/sidebarOverride.ts diff --git a/apps/jingrow/frontend/src/core/pagetype/GenericDetailPage.vue b/apps/jingrow/frontend/src/core/pagetype/GenericDetailPage.vue index 57c3c57..0df0825 100644 --- a/apps/jingrow/frontend/src/core/pagetype/GenericDetailPage.vue +++ b/apps/jingrow/frontend/src/core/pagetype/GenericDetailPage.vue @@ -260,10 +260,24 @@ import { useRoute, useRouter } from 'vue-router' import { NButton, NSpace, NIcon, useMessage, NLayout, NLayoutSider, NLayoutContent } from 'naive-ui' import FieldRenderer from '@/core/components/form/FieldRenderer.vue' import { Icon } from '@iconify/vue' -import FormPanel from '@/core/components/form/panel/FormPanel.vue' -import ImageSection from '@/core/components/form/panel/ImageSection.vue' -import AttachmentSection from '@/core/components/form/panel/AttachmentSection.vue' -import TagSection from '@/core/components/form/panel/TagSection.vue' +import { resolveSidebarPanel } from '@/core/registry/sidebarOverride' +import { defineAsyncComponent } from 'vue' +const FormPanel = defineAsyncComponent(async () => { + const mod = await resolveSidebarPanel(entity.value, 'FormPanel.vue') + return mod || (await import('@/core/components/form/panel/FormPanel.vue')) +}) +const ImageSection = defineAsyncComponent(async () => { + const mod = await resolveSidebarPanel(entity.value, 'ImageSection.vue') + return mod || (await import('@/core/components/form/panel/ImageSection.vue')) +}) +const AttachmentSection = defineAsyncComponent(async () => { + const mod = await resolveSidebarPanel(entity.value, 'AttachmentSection.vue') + return mod || (await import('@/core/components/form/panel/AttachmentSection.vue')) +}) +const TagSection = defineAsyncComponent(async () => { + const mod = await resolveSidebarPanel(entity.value, 'TagSection.vue') + return mod || (await import('@/core/components/form/panel/TagSection.vue')) +}) import axios from 'axios' import { t } from '@/shared/i18n' import { get_session_api_headers } from '@/shared/api/auth' diff --git a/apps/jingrow/frontend/src/core/registry/sidebarOverride.ts b/apps/jingrow/frontend/src/core/registry/sidebarOverride.ts new file mode 100644 index 0000000..fac3115 --- /dev/null +++ b/apps/jingrow/frontend/src/core/registry/sidebarOverride.ts @@ -0,0 +1,151 @@ +// 侧边栏相关组件覆盖机制,优先级: +// 1) pagetype 视图下的 panel 组件(entity 专属) +// - @apps/*/frontend/src/views/pagetype//form/panel/.vue +// - /src/views/pagetype//form/panel/.vue +// 2) 核心路径的 panel 组件(源路径级覆盖) +// - @apps/*/frontend/src/core/components/form/panel/.vue +// - /src/core/components/form/panel/.vue +// 组件名 :FormPanel.vue / ImageSection.vue / AttachmentSection.vue / TagSection.vue + +declare const __APPS_ORDER__: string[] + +type AsyncComponentLoader = () => Promise + +// 核心路径扫描 +const globCoreApps = import.meta.glob('@apps/*/frontend/src/core/**/**.vue') +const globCoreLocal = import.meta.glob('/src/core/**/**.vue') +// pagetype 视图下的 panel 扫描 +const globViewApps = import.meta.glob('@apps/*/frontend/src/views/pagetype/**/form/panel/**.vue') +const globViewLocal = import.meta.glob('/src/views/pagetype/**/form/panel/**.vue') + +type Entry = { + loader: AsyncComponentLoader + appName: string + fullPath: string + key?: string // 核心源路径键(相对 src) + entity?: string // 视图下的 entity + compName: string // 组件名(文件名) +} + +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 +} + +function extractAppName(p: string): string { + const s = p.replace(/\\/g, '/').replace(/^\/@fs\//, '/') + if (s.startsWith('/src/')) return 'jingrow' + const reApps1 = /(?:^|\/)apps\/([^/]+)\/frontend\// + const reApps2 = /(?:^|\/)@apps\/([^/]+)\/frontend\// + const m1 = s.match(reApps1) + if (m1 && m1[1]) return m1[1] + const m2 = s.match(reApps2) + if (m2 && m2[1]) return m2[1] + return 'jingrow' +} + +function toKey(p: string): string | null { + const s = p.replace(/\\/g, '/').replace(/^\/@fs\//, '/') + const idx = s.indexOf('/src/') + if (idx >= 0) return s.substring(idx + '/src/'.length) + return null +} + +function parseCompName(p: string): string { + const s = p.replace(/\\/g, '/') + const parts = s.split('/') + const last = parts[parts.length - 1] || '' + return last +} + +function parseEntityFromViewPath(p: string): string | null { + const s = p.replace(/\\/g, '/') + const parts = s.split('/') + const idx = parts.indexOf('pagetype') + if (idx >= 0 && idx + 1 < parts.length) return parts[idx + 1] + return null +} + +const allEntries: Entry[] = [] + +// 核心路径 entries +for (const [p, loader] of Object.entries(globCoreApps)) { + const key = toKey(p) + if (!key) continue + allEntries.push({ loader: loader as AsyncComponentLoader, appName: extractAppName(p), fullPath: p, key, compName: parseCompName(p) }) +} +for (const [p, loader] of Object.entries(globCoreLocal)) { + const key = toKey(p) + if (!key) continue + allEntries.push({ loader: loader as AsyncComponentLoader, appName: extractAppName(p), fullPath: p, key, compName: parseCompName(p) }) +} + +// 视图路径 entries(带 entity) +for (const [p, loader] of Object.entries(globViewApps)) { + const entity = parseEntityFromViewPath(p) + if (!entity) continue + allEntries.push({ loader: loader as AsyncComponentLoader, appName: extractAppName(p), fullPath: p, entity, compName: parseCompName(p) }) +} +for (const [p, loader] of Object.entries(globViewLocal)) { + const entity = parseEntityFromViewPath(p) + if (!entity) continue + allEntries.push({ loader: loader as AsyncComponentLoader, appName: extractAppName(p), fullPath: p, entity, compName: parseCompName(p) }) +} + +// 索引: +// - viewIndex[(entity, compName)] -> entries(pagetype 覆盖) +// - coreIndex[(compName)] -> entries(核心路径覆盖) +const viewIndex: Record = {} +const coreIndex: Record = {} + +for (const e of allEntries) { + if (e.entity) { + const key = `${e.entity}:${e.compName}` + if (!viewIndex[key]) viewIndex[key] = [] + viewIndex[key].push(e) + } else if (e.key) { + const key = e.compName + if (!coreIndex[key]) coreIndex[key] = [] + coreIndex[key].push(e) + } +} + +function sortEntries(list: Entry[]) { + list.sort((a, b) => { + const ra = rankByAppsOrder(a.appName) + const rb = rankByAppsOrder(b.appName) + if (ra !== rb) return rb - ra + const aIsExternal = a.fullPath.includes('/apps/') || a.fullPath.includes('@apps/') + const bIsExternal = b.fullPath.includes('/apps/') || b.fullPath.includes('@apps/') + if (aIsExternal !== bIsExternal) return aIsExternal ? -1 : 1 + if (a.fullPath.length !== b.fullPath.length) return a.fullPath.length - b.fullPath.length + return a.fullPath.localeCompare(b.fullPath) + }) +} + +for (const k of Object.keys(viewIndex)) sortEntries(viewIndex[k]) +for (const k of Object.keys(coreIndex)) sortEntries(coreIndex[k]) + +// 解析侧栏面板组件:优先 entity 下的视图 panel,其次核心路径 panel +export async function resolveSidebarPanel(entity: string, compName: string): Promise { + const entityKey = String(entity).toLowerCase().replace(/-/g, '_') + const name = compName.endsWith('.vue') ? compName : `${compName}.vue` + let bucket = viewIndex[`${entityKey}:${name}`] + if ((!bucket || bucket.length === 0)) { + bucket = coreIndex[name] + } + if (!bucket || bucket.length === 0) return null + try { + const mod = await bucket[0].loader() + return mod?.default ?? mod + } catch { + return null + } +} + +