pagetype列表页支持_list,_list_toolbar,_list_filterbar,_list_actions这些部分的覆盖

This commit is contained in:
jingrow 2025-11-01 15:46:30 +08:00
parent 3937bd1824
commit d29a27411f
2 changed files with 257 additions and 29 deletions

View File

@ -1,6 +1,8 @@
<template> <template>
<!-- 如果是单页模式直接显示单页详情组件 --> <!-- 如果是单页模式直接显示单页详情组件 -->
<SinglePageDetail v-if="isSinglePage" /> <SinglePageDetail v-if="isSinglePage" />
<!-- 列表页整体覆盖 -->
<component :is="listOverrideComponent || 'div'" v-else-if="listOverrideComponent" />
<div v-else class="page"> <div v-else class="page">
<!-- 头部 AI 智能体列表一致的结构 --> <!-- 头部 AI 智能体列表一致的结构 -->
<div class="page-header"> <div class="page-header">
@ -8,7 +10,27 @@
<h2>{{ t(title) }}</h2> <h2>{{ t(title) }}</h2>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="filters"> <!-- 工具栏覆盖组件 -->
<component
v-if="toolbarComponent"
:is="toolbarComponent"
:context="{
entity,
filters,
searchQuery,
viewMode,
selectedKeys,
loading,
rows,
reload,
createRecordHandler,
handleDeleteSelected,
router,
t
}"
/>
<template v-else>
<div class="filters">
<n-input v-model:value="searchQuery" :placeholder="t('Search')" clearable style="width: 200px" /> <n-input v-model:value="searchQuery" :placeholder="t('Search')" clearable style="width: 200px" />
</div> </div>
<!-- 活跃过滤条件标签 --> <!-- 活跃过滤条件标签 -->
@ -65,13 +87,24 @@
<i class="fa fa-trash"></i> <i class="fa fa-trash"></i>
{{ t('Delete Selected') }} ({{ selectedKeys.length }}) {{ t('Delete Selected') }} ({{ selectedKeys.length }})
</button> </button>
</template>
</div> </div>
</div> </div>
<div class="page-content"> <div class="page-content">
<!-- 过滤栏 --> <!-- 过滤栏覆盖组件 -->
<component
v-if="filterBarComponent"
:is="filterBarComponent"
:context="{
fields: metaFields,
filters,
onFilterChange
}"
/>
<!-- 默认过滤栏 -->
<FilterBar <FilterBar
v-if="!isSinglePage && metaFields.length > 0" v-else-if="!isSinglePage && metaFields.length > 0"
:fields="metaFields" :fields="metaFields"
v-model="filters" v-model="filters"
@filter-change="onFilterChange" @filter-change="onFilterChange"
@ -112,15 +145,32 @@
</div> </div>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<button class="action-btn" @click.stop="openDetail(row.name)" :title="t('View')"> <!-- 操作列覆盖组件卡片视图 -->
<i class="fa fa-eye"></i> <component
</button> v-if="actionsComponent"
<button class="action-btn" @click.stop="editRecord(row)" :title="t('Edit')"> :is="actionsComponent"
<i class="fa fa-edit"></i> :context="{
</button> row,
<button class="action-btn" @click.stop="deleteRecord(row.name)" :title="t('Delete')"> entity,
<i class="fa fa-trash"></i> openDetail,
</button> editRecord,
deleteRecord,
router,
t
}"
/>
<!-- 默认操作按钮 -->
<template v-else>
<button class="action-btn" @click.stop="openDetail(row.name)" :title="t('View')">
<i class="fa fa-eye"></i>
</button>
<button class="action-btn" @click.stop="editRecord(row)" :title="t('Edit')">
<i class="fa fa-edit"></i>
</button>
<button class="action-btn" @click.stop="deleteRecord(row.name)" :title="t('Delete')">
<i class="fa fa-trash"></i>
</button>
</template>
</div> </div>
</div> </div>
</div> </div>
@ -166,15 +216,32 @@
<template v-else>{{ formatDisplayValue(row[col.key], col.key) }}</template> <template v-else>{{ formatDisplayValue(row[col.key], col.key) }}</template>
</div> </div>
<div class="col-actions"> <div class="col-actions">
<button class="action-btn" @click.stop="openDetail(row.name)" :title="t('View')"> <!-- 操作列覆盖组件 -->
<i class="fa fa-eye"></i> <component
</button> v-if="actionsComponent"
<button class="action-btn" @click.stop="editRecord(row)" :title="t('Edit')"> :is="actionsComponent"
<i class="fa fa-edit"></i> :context="{
</button> row,
<button class="action-btn" @click.stop="deleteRecord(row.name)" :title="t('Delete')"> entity,
<i class="fa fa-trash"></i> openDetail,
</button> editRecord,
deleteRecord,
router,
t
}"
/>
<!-- 默认操作按钮 -->
<template v-else>
<button class="action-btn" @click.stop="openDetail(row.name)" :title="t('View')">
<i class="fa fa-eye"></i>
</button>
<button class="action-btn" @click.stop="editRecord(row)" :title="t('Edit')">
<i class="fa fa-edit"></i>
</button>
<button class="action-btn" @click.stop="deleteRecord(row.name)" :title="t('Delete')">
<i class="fa fa-trash"></i>
</button>
</template>
</div> </div>
</div> </div>
</div> </div>
@ -188,7 +255,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref, computed, watch } from 'vue' import { onMounted, ref, computed, watch, shallowRef, markRaw } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { NInput, NPagination, useMessage } from 'naive-ui' import { NInput, NPagination, useMessage } from 'naive-ui'
import axios from 'axios' import axios from 'axios'
@ -198,6 +265,12 @@ import { usePageTypeSlug } from '@/shared/utils/slug'
import { isSinglePageType } from '@/shared/utils/pagetype' import { isSinglePageType } from '@/shared/utils/pagetype'
import SinglePageDetail from './SinglePageDetail.vue' import SinglePageDetail from './SinglePageDetail.vue'
import FilterBar from '@/core/components/FilterBar.vue' import FilterBar from '@/core/components/FilterBar.vue'
import {
resolvePagetypeListOverride,
resolvePagetypeListToolbarOverride,
resolvePagetypeListFilterBarOverride,
resolvePagetypeListActionsOverride
} from '@/core/registry/pagetypeOverride'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@ -207,6 +280,12 @@ const message = useMessage()
const { pagetypeSlug, entity } = usePageTypeSlug(route) const { pagetypeSlug, entity } = usePageTypeSlug(route)
const title = computed(() => entity.value) const title = computed(() => entity.value)
//
const listOverrideComponent = shallowRef<any | null>(null)
const toolbarComponent = shallowRef<any | null>(null)
const filterBarComponent = shallowRef<any | null>(null)
const actionsComponent = shallowRef<any | null>(null)
// //
const isSinglePage = ref(false) const isSinglePage = ref(false)
const searchQuery = ref('') const searchQuery = ref('')
@ -626,6 +705,24 @@ async function loadData() {
} }
onMounted(async () => { onMounted(async () => {
//
const listComp = await resolvePagetypeListOverride(pagetypeSlug.value)
listOverrideComponent.value = listComp ? markRaw(listComp) : null
const listToolbarComp = await resolvePagetypeListToolbarOverride(pagetypeSlug.value)
toolbarComponent.value = listToolbarComp ? markRaw(listToolbarComp) : null
const listFilterBarComp = await resolvePagetypeListFilterBarOverride(pagetypeSlug.value)
filterBarComponent.value = listFilterBarComp ? markRaw(listFilterBarComp) : null
const listActionsComp = await resolvePagetypeListActionsOverride(pagetypeSlug.value)
actionsComponent.value = listActionsComp ? markRaw(listActionsComp) : null
//
if (listOverrideComponent.value) {
return
}
await loadMeta() await loadMeta()
// //
if (!isSinglePage.value) { if (!isSinglePage.value) {
@ -641,6 +738,24 @@ watch([page], () => {
// entity // entity
watch(() => route.params.entity, async (newEntity, oldEntity) => { watch(() => route.params.entity, async (newEntity, oldEntity) => {
if (newEntity !== oldEntity) { if (newEntity !== oldEntity) {
//
const listComp = await resolvePagetypeListOverride(String(newEntity))
listOverrideComponent.value = listComp ? markRaw(listComp) : null
const listToolbarComp = await resolvePagetypeListToolbarOverride(String(newEntity))
toolbarComponent.value = listToolbarComp ? markRaw(listToolbarComp) : null
const listFilterBarComp = await resolvePagetypeListFilterBarOverride(String(newEntity))
filterBarComponent.value = listFilterBarComp ? markRaw(listFilterBarComp) : null
const listActionsComp = await resolvePagetypeListActionsOverride(String(newEntity))
actionsComponent.value = listActionsComp ? markRaw(listActionsComp) : null
//
if (listOverrideComponent.value) {
return
}
// //
page.value = 1 page.value = 1
searchQuery.value = '' searchQuery.value = ''

View File

@ -1,6 +1,10 @@
// 基于约定路径自动解析 pagetype 覆盖组件 // 基于约定路径自动解析 pagetype 覆盖组件
// 详情:/src/views/pagetype/<pagetype>/<pagetype>.vue // 详情页:/src/views/pagetype/<pagetype>/<pagetype>.vue
// 工具栏:/src/views/pagetype/<pagetype>/<pagetype>_toolbar.vue // 详情页工具栏:/src/views/pagetype/<pagetype>/<pagetype>_toolbar.vue
// 列表页:/src/views/pagetype/<pagetype>/<pagetype>_list.vue
// 列表页工具栏:/src/views/pagetype/<pagetype>/<pagetype>_list_toolbar.vue
// 列表页过滤栏:/src/views/pagetype/<pagetype>/<pagetype>_list_filterbar.vue
// 列表页操作列:/src/views/pagetype/<pagetype>/<pagetype>_list_actions.vue
// 由 Vite define 注入,类型声明便于 TS // 由 Vite define 注入,类型声明便于 TS
declare const __APPS_ORDER__: string[] declare const __APPS_ORDER__: string[]
@ -62,12 +66,21 @@ function sortByPriority(a: string, b: string): number {
return a.localeCompare(b) return a.localeCompare(b)
} }
// 预索引:按 entity 分组,分别索引 detail 与 toolbar 候选,并在构建时一次性排序 // 预索引:按 entity 分组,分别索引 detail、toolbar、list、listToolbar、filterbar、actions 候选,并在构建时一次性排序
type Indexed = { detail: string[]; toolbar: string[] } type Indexed = {
detail: string[]
toolbar: string[]
list: string[]
listToolbar: string[]
filterbar: string[]
actions: string[]
}
const indexedByEntity: Record<string, Indexed> = {} const indexedByEntity: Record<string, Indexed> = {}
function ensureIndexed(entity: string): Indexed { function ensureIndexed(entity: string): Indexed {
if (!indexedByEntity[entity]) indexedByEntity[entity] = { detail: [], toolbar: [] } if (!indexedByEntity[entity]) {
indexedByEntity[entity] = { detail: [], toolbar: [], list: [], listToolbar: [], filterbar: [], actions: [] }
}
return indexedByEntity[entity] return indexedByEntity[entity]
} }
@ -81,17 +94,41 @@ for (const file of Object.keys(allPagetypeViews)) {
const baseName = fileName.replace(/\.vue$/i, '') const baseName = fileName.replace(/\.vue$/i, '')
const entity = folderName const entity = folderName
const bucket = ensureIndexed(entity) const bucket = ensureIndexed(entity)
// 详情页覆盖:<pagetype>.vue
if (baseName === folderName) { if (baseName === folderName) {
bucket.detail.push(file) bucket.detail.push(file)
} else if (baseName === `${folderName}_toolbar`) { }
// 详情页工具栏覆盖:<pagetype>_toolbar.vue
else if (baseName === `${folderName}_toolbar`) {
bucket.toolbar.push(file) bucket.toolbar.push(file)
} }
// 列表页覆盖:<pagetype>_list.vue
else if (baseName === `${folderName}_list`) {
bucket.list.push(file)
}
// 列表页工具栏覆盖:<pagetype>_list_toolbar.vue
else if (baseName === `${folderName}_list_toolbar`) {
bucket.listToolbar.push(file)
}
// 列表页过滤栏覆盖:<pagetype>_list_filterbar.vue
else if (baseName === `${folderName}_list_filterbar`) {
bucket.filterbar.push(file)
}
// 列表页操作列覆盖:<pagetype>_list_actions.vue
else if (baseName === `${folderName}_list_actions`) {
bucket.actions.push(file)
}
} }
// 候选按优先级排序 // 候选按优先级排序
Object.values(indexedByEntity).forEach(({ detail, toolbar }) => { Object.values(indexedByEntity).forEach(({ detail, toolbar, list, listToolbar, filterbar, actions }) => {
detail.sort(sortByPriority) detail.sort(sortByPriority)
toolbar.sort(sortByPriority) toolbar.sort(sortByPriority)
list.sort(sortByPriority)
listToolbar.sort(sortByPriority)
filterbar.sort(sortByPriority)
actions.sort(sortByPriority)
}) })
/** /**
@ -132,3 +169,79 @@ export async function resolvePagetypeToolbarOverride(pagetypeSlug: string): Prom
} }
} }
/**
* pagetype
* /src/views/pagetype/<name>/<name>_list.vue
*/
export async function resolvePagetypeListOverride(pagetypeSlug: string): Promise<any | null> {
if (!pagetypeSlug) return null
const targetHyphen = pagetypeSlug.toLowerCase()
const targetUnderscore = targetHyphen.replace(/-/g, '_')
const bucket = indexedByEntity[targetUnderscore]
if (!bucket || bucket.list.length === 0) return null
const pick = bucket.list[0]
try {
const mod = await allPagetypeViews[pick].loader()
return mod?.default ?? mod
} catch (_e) {
return null
}
}
/**
* pagetype
* /src/views/pagetype/<name>/<name>_list_filterbar.vue
*/
export async function resolvePagetypeListFilterBarOverride(pagetypeSlug: string): Promise<any | null> {
if (!pagetypeSlug) return null
const targetHyphen = pagetypeSlug.toLowerCase()
const targetUnderscore = targetHyphen.replace(/-/g, '_')
const bucket = indexedByEntity[targetUnderscore]
if (!bucket || bucket.filterbar.length === 0) return null
const pick = bucket.filterbar[0]
try {
const mod = await allPagetypeViews[pick].loader()
return mod?.default ?? mod
} catch (_e) {
return null
}
}
/**
* pagetype
* /src/views/pagetype/<name>/<name>_list_toolbar.vue
*/
export async function resolvePagetypeListToolbarOverride(pagetypeSlug: string): Promise<any | null> {
if (!pagetypeSlug) return null
const targetHyphen = pagetypeSlug.toLowerCase()
const targetUnderscore = targetHyphen.replace(/-/g, '_')
const bucket = indexedByEntity[targetUnderscore]
if (!bucket || bucket.listToolbar.length === 0) return null
const pick = bucket.listToolbar[0]
try {
const mod = await allPagetypeViews[pick].loader()
return mod?.default ?? mod
} catch (_e) {
return null
}
}
/**
* pagetype
* /src/views/pagetype/<name>/<name>_list_actions.vue
*/
export async function resolvePagetypeListActionsOverride(pagetypeSlug: string): Promise<any | null> {
if (!pagetypeSlug) return null
const targetHyphen = pagetypeSlug.toLowerCase()
const targetUnderscore = targetHyphen.replace(/-/g, '_')
const bucket = indexedByEntity[targetUnderscore]
if (!bucket || bucket.actions.length === 0) return null
const pick = bucket.actions[0]
try {
const mod = await allPagetypeViews[pick].loader()
return mod?.default ?? mod
} catch (_e) {
return null
}
}