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>
<!-- 如果是单页模式直接显示单页详情组件 -->
<SinglePageDetail v-if="isSinglePage" />
<!-- 列表页整体覆盖 -->
<component :is="listOverrideComponent || 'div'" v-else-if="listOverrideComponent" />
<div v-else class="page">
<!-- 头部 AI 智能体列表一致的结构 -->
<div class="page-header">
@ -8,7 +10,27 @@
<h2>{{ t(title) }}</h2>
</div>
<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" />
</div>
<!-- 活跃过滤条件标签 -->
@ -65,13 +87,24 @@
<i class="fa fa-trash"></i>
{{ t('Delete Selected') }} ({{ selectedKeys.length }})
</button>
</template>
</div>
</div>
<div class="page-content">
<!-- 过滤栏 -->
<!-- 过滤栏覆盖组件 -->
<component
v-if="filterBarComponent"
:is="filterBarComponent"
:context="{
fields: metaFields,
filters,
onFilterChange
}"
/>
<!-- 默认过滤栏 -->
<FilterBar
v-if="!isSinglePage && metaFields.length > 0"
v-else-if="!isSinglePage && metaFields.length > 0"
:fields="metaFields"
v-model="filters"
@filter-change="onFilterChange"
@ -112,15 +145,32 @@
</div>
</div>
<div class="card-actions">
<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>
<!-- 操作列覆盖组件卡片视图 -->
<component
v-if="actionsComponent"
:is="actionsComponent"
:context="{
row,
entity,
openDetail,
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>
@ -166,15 +216,32 @@
<template v-else>{{ formatDisplayValue(row[col.key], col.key) }}</template>
</div>
<div class="col-actions">
<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>
<!-- 操作列覆盖组件 -->
<component
v-if="actionsComponent"
:is="actionsComponent"
:context="{
row,
entity,
openDetail,
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>
@ -188,7 +255,7 @@
</template>
<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 { NInput, NPagination, useMessage } from 'naive-ui'
import axios from 'axios'
@ -198,6 +265,12 @@ import { usePageTypeSlug } from '@/shared/utils/slug'
import { isSinglePageType } from '@/shared/utils/pagetype'
import SinglePageDetail from './SinglePageDetail.vue'
import FilterBar from '@/core/components/FilterBar.vue'
import {
resolvePagetypeListOverride,
resolvePagetypeListToolbarOverride,
resolvePagetypeListFilterBarOverride,
resolvePagetypeListActionsOverride
} from '@/core/registry/pagetypeOverride'
const route = useRoute()
const router = useRouter()
@ -207,6 +280,12 @@ const message = useMessage()
const { pagetypeSlug, entity } = usePageTypeSlug(route)
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 searchQuery = ref('')
@ -626,6 +705,24 @@ async function loadData() {
}
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()
//
if (!isSinglePage.value) {
@ -641,6 +738,24 @@ watch([page], () => {
// entity
watch(() => route.params.entity, async (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
searchQuery.value = ''

View File

@ -1,6 +1,10 @@
// 基于约定路径自动解析 pagetype 覆盖组件
// 详情:/src/views/pagetype/<pagetype>/<pagetype>.vue
// 工具栏:/src/views/pagetype/<pagetype>/<pagetype>_toolbar.vue
// 详情页:/src/views/pagetype/<pagetype>/<pagetype>.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
declare const __APPS_ORDER__: string[]
@ -62,12 +66,21 @@ function sortByPriority(a: string, b: string): number {
return a.localeCompare(b)
}
// 预索引:按 entity 分组,分别索引 detail 与 toolbar 候选,并在构建时一次性排序
type Indexed = { detail: string[]; toolbar: string[] }
// 预索引:按 entity 分组,分别索引 detail、toolbar、list、listToolbar、filterbar、actions 候选,并在构建时一次性排序
type Indexed = {
detail: string[]
toolbar: string[]
list: string[]
listToolbar: string[]
filterbar: string[]
actions: string[]
}
const indexedByEntity: Record<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]
}
@ -81,17 +94,41 @@ for (const file of Object.keys(allPagetypeViews)) {
const baseName = fileName.replace(/\.vue$/i, '')
const entity = folderName
const bucket = ensureIndexed(entity)
// 详情页覆盖:<pagetype>.vue
if (baseName === folderName) {
bucket.detail.push(file)
} else if (baseName === `${folderName}_toolbar`) {
}
// 详情页工具栏覆盖:<pagetype>_toolbar.vue
else if (baseName === `${folderName}_toolbar`) {
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)
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
}
}