feat(plugin): 侧栏全页 AppPage、多 nav_key 联邦加载与 sidebar_nav 缓存

- 新增路由 plugin-app 与壳页,按 nav_key 尝试 AppPage{Pascal}/AppPage/Page
- DefaultLayout 与 appcenter 合并插件侧栏项;plugin/sidebar_nav 经 Pinia 去重缓存
- 工具 pluginSidebarNav、联邦 loader 与文档/示例更新;登出时清空侧栏缓存

Made-with: Cursor
This commit is contained in:
DDSRem
2026-04-09 07:59:40 +08:00
parent 9cf782eb5b
commit e72f9a8374
16 changed files with 449 additions and 21 deletions
+17 -10
View File
@@ -1,15 +1,16 @@
<script setup lang="ts">
import { NavMenu } from '@/@layouts/types'
import { getNavMenus } from '@/router/i18n-menu'
import { useUserStore } from '@/stores'
import { usePluginSidebarNavStore, useUserStore } from '@/stores'
import { useI18n } from 'vue-i18n'
import { filterPluginSidebarNavEntries } from '@/utils/pluginSidebarNav'
import { filterMenusByPermission } from '@/utils/permission'
// 国际化
const { t } = useI18n()
// 从 Store 中获取用户信息
const userStore = useUserStore()
const pluginSidebarNavStore = usePluginSidebarNavStore()
// 获取用户权限信息
const userPermissions = computed(() => ({
@@ -20,14 +21,22 @@ const userPermissions = computed(() => ({
// 应用分组(以header分组)
const appGroups = ref<Record<string, NavMenu[]>>({})
// 根据header属性对应用进行分类
function categorizeApps() {
// 获取所有菜单并根据权限过滤
// 根据header属性对应用进行分类(含插件侧栏项,与桌面端侧栏一致)
async function categorizeApps() {
const allMenus = getNavMenus(t)
const filteredMenus = filterMenusByPermission(allMenus, userPermissions.value)
const menus = filteredMenus.filter((item: NavMenu) => !item.footer)
let menus = filteredMenus.filter((item: NavMenu) => !item.footer)
await pluginSidebarNavStore.ensureSidebarNav()
if (pluginSidebarNavStore.items.length > 0) {
const pluginNavMenus = filterPluginSidebarNavEntries(
pluginSidebarNavStore.items,
t,
userPermissions.value,
).map(e => e.navMenu)
menus = [...menus, ...pluginNavMenus]
}
// 按header属性分组
const groupedMenus: Record<string, NavMenu[]> = {}
menus.forEach(menu => {
@@ -38,11 +47,9 @@ function categorizeApps() {
groupedMenus[header].push(menu)
})
// 将分组结果赋值给响应式变量
appGroups.value = groupedMenus
}
// 页面加载时对应用进行分类
onMounted(() => {
categorizeApps()
})
@@ -60,7 +67,7 @@ onMounted(() => {
<VList lines="one" class="settings-list">
<VListItem
v-for="(app, appIndex) in apps"
:key="appIndex"
:key="`${header}-${appIndex}-${String(app.to)}`"
:to="app.to || ''"
color="primary"
class="settings-list-item"
+50
View File
@@ -0,0 +1,50 @@
<script setup lang="ts">
import type { Component } from 'vue'
import api from '@/api'
import { loadRemoteAppPageComponent } from '@/utils/federationLoader'
const route = useRoute()
const pluginId = computed(() => route.params.pluginId as string)
const navKey = computed(() => (route.params.navKey as string) || 'main')
const RemoteView = shallowRef<Component | null>(null)
const loadError = ref(false)
watch(
[pluginId, navKey],
async ([pid, nk]) => {
loadError.value = false
if (!pid) {
RemoteView.value = null
return
}
try {
RemoteView.value = (await loadRemoteAppPageComponent(pid, nk)) as Component
} catch (e) {
console.error(e)
RemoteView.value = null
loadError.value = true
}
},
{ immediate: true },
)
</script>
<template>
<div class="plugin-app-page">
<VAlert v-if="loadError" type="error" class="ma-4" title="组件加载错误">
无法加载插件全页组件多入口时请暴露 AppPage AppPage{Pascal}见文档并确认插件已启用
</VAlert>
<VSkeletonLoader v-else-if="!RemoteView" class="ma-4" type="article, article, article" />
<component
v-else
:is="RemoteView"
:key="`${pluginId}-${navKey}`"
:api="api"
:nav-key="navKey"
:plugin-id="pluginId"
@action="() => {}"
/>
</div>
</template>