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