Files
MoviePilot-Frontend/src/pages/appcenter.vue
DDSRem e72f9a8374 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
2026-04-09 07:59:40 +08:00

133 lines
3.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { NavMenu } from '@/@layouts/types'
import { getNavMenus } from '@/router/i18n-menu'
import { usePluginSidebarNavStore, useUserStore } from '@/stores'
import { useI18n } from 'vue-i18n'
import { filterPluginSidebarNavEntries } from '@/utils/pluginSidebarNav'
import { filterMenusByPermission } from '@/utils/permission'
// 国际化
const { t } = useI18n()
const userStore = useUserStore()
const pluginSidebarNavStore = usePluginSidebarNavStore()
// 获取用户权限信息
const userPermissions = computed(() => ({
is_superuser: userStore.superUser,
...userStore.permissions,
}))
// 应用分组以header分组
const appGroups = ref<Record<string, NavMenu[]>>({})
// 根据header属性对应用进行分类含插件侧栏项与桌面端侧栏一致
async function categorizeApps() {
const allMenus = getNavMenus(t)
const filteredMenus = filterMenusByPermission(allMenus, userPermissions.value)
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]
}
const groupedMenus: Record<string, NavMenu[]> = {}
menus.forEach(menu => {
const header = menu.header || t('appcenter.others')
if (!groupedMenus[header]) {
groupedMenus[header] = []
}
groupedMenus[header].push(menu)
})
appGroups.value = groupedMenus
}
onMounted(() => {
categorizeApps()
})
</script>
<template>
<div class="app-settings-container">
<VContainer>
<!-- 遍历所有分组 -->
<div v-for="(apps, header) in appGroups" :key="header" class="mb-3">
<VListSubheader class="ps-1">
{{ header }}
</VListSubheader>
<!-- 分组内容 - 使用卡片包装 -->
<VCard variant="flat" class="settings-section-card">
<VList lines="one" class="settings-list">
<VListItem
v-for="(app, appIndex) in apps"
:key="`${header}-${appIndex}-${String(app.to)}`"
:to="app.to || ''"
color="primary"
class="settings-list-item"
rounded="0"
>
<template #prepend>
<VAvatar size="42" color="primary" variant="text" class="me-3">
<VIcon :icon="app.icon as string" size="24"></VIcon>
</VAvatar>
</template>
<VListItemTitle class="font-weight-medium">
{{ app.full_title || app.title }}
</VListItemTitle>
<VListItemSubtitle v-if="app.description">
{{ app.description }}
</VListItemSubtitle>
<template #append>
<VIcon icon="mdi-chevron-right"></VIcon>
</template>
</VListItem>
</VList>
</VCard>
</div>
</VContainer>
</div>
</template>
<style lang="scss" scoped>
.app-settings-container {
margin-block: 0;
margin-inline: auto;
max-inline-size: 960px;
}
.settings-section-card {
overflow: hidden;
backdrop-filter: blur(10px);
background-color: rgb(var(--v-theme-surface));
box-shadow: 0 2px 8px rgba(0, 0, 0, 8%);
}
.settings-list {
padding: 0;
}
.settings-list-item {
padding-block: 8px;
padding-inline: 12px;
transition: background-color 0.2s;
&:not(:last-child) {
border-block-end: 1px solid rgba(var(--v-border-color), 0.12);
}
&:hover {
background-color: rgba(var(--v-theme-primary), 0.05);
}
}
</style>