pagetype的sidebar实现app优先级覆盖机制

This commit is contained in:
jingrow 2025-10-28 19:40:11 +08:00
parent 6ce935f34a
commit b06109f675
2 changed files with 169 additions and 4 deletions

View File

@ -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'

View File

@ -0,0 +1,151 @@
// 侧边栏相关组件覆盖机制,优先级:
// 1) pagetype 视图下的 panel 组件entity 专属)
// - @apps/*/frontend/src/views/pagetype/<entity>/form/panel/<Comp>.vue
// - /src/views/pagetype/<entity>/form/panel/<Comp>.vue
// 2) 核心路径的 panel 组件(源路径级覆盖)
// - @apps/*/frontend/src/core/components/form/panel/<Comp>.vue
// - /src/core/components/form/panel/<Comp>.vue
// 组件名 <Comp>FormPanel.vue / ImageSection.vue / AttachmentSection.vue / TagSection.vue
declare const __APPS_ORDER__: string[]
type AsyncComponentLoader = () => Promise<any>
// 核心路径扫描
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)] -> entriespagetype 覆盖)
// - coreIndex[(compName)] -> entries核心路径覆盖
const viewIndex: Record<string, Entry[]> = {}
const coreIndex: Record<string, Entry[]> = {}
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<any | null> {
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
}
}