pagetype列表页支持_list,_list_toolbar,_list_filterbar,_list_actions这些部分的覆盖
This commit is contained in:
parent
3937bd1824
commit
d29a27411f
@ -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 = ''
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user