diff --git a/src/@layouts/types.d.ts b/src/@layouts/types.d.ts index 3aba2622..e4220639 100644 --- a/src/@layouts/types.d.ts +++ b/src/@layouts/types.d.ts @@ -1,5 +1,6 @@ import type { Component, Ref, VNode } from 'vue' import type { RouteLocationRaw } from 'vue-router' +import type { UserPermissionKey } from '@/utils/permission' import { ContentWidth, FooterType, NavbarType } from './enums' export interface UserConfig { @@ -119,6 +120,7 @@ export interface NavLink extends NavLinkProps, Partial { badgeContent?: string badgeClass?: string disable?: boolean + permission?: UserPermissionKey } export interface NavMenuTabItem { diff --git a/src/App.vue b/src/App.vue index f9f926e9..4168de46 100644 --- a/src/App.vue +++ b/src/App.vue @@ -98,7 +98,7 @@ const startHeartbeat = () => { heartbeatInterval = window.setInterval(async () => { try { if (isLogin.value) { - await api.get('dashboard/cpu') + await api.get('system/ping') } } catch (error) { console.warn('Heartbeat request failed:', error) diff --git a/src/components/FileBrowser.vue b/src/components/FileBrowser.vue index e842d3bc..81f87d49 100644 --- a/src/components/FileBrowser.vue +++ b/src/components/FileBrowser.vue @@ -7,6 +7,8 @@ import { storageIconDict } from '@/api/constants' import type { AxiosInstance } from 'axios' import { useDynamicButton } from '@/composables/useDynamicButton' import { usePWA } from '@/composables/usePWA' +import { useUserStore } from '@/stores' +import { buildUserPermissionContext, hasPermission } from '@/utils/permission' // LocalStorage keys const SORT_KEY = 'fileBrowser.sort' @@ -41,6 +43,10 @@ const props = defineProps({ const emit = defineEmits(['pathchanged']) const route = useRoute() const { appMode } = usePWA() +const userStore = useUserStore() +const canManage = computed(() => + hasPermission(buildUserPermissionContext(userStore.superUser, userStore.permissions), 'manage'), +) const toolbarRef = ref | null>(null) const fileIcons = { @@ -136,11 +142,12 @@ function openNewFolderDialog() { toolbarRef.value?.openNewFolderDialog() } -const showFloatingNewFolderAction = computed(() => route.path === '/filemanager') +const showFloatingNewFolderAction = computed(() => route.path === '/filemanager' && canManage.value) useDynamicButton({ icon: 'mdi-folder-plus-outline', onClick: openNewFolderDialog, + permission: 'manage', show: computed(() => appMode.value && showFloatingNewFolderAction.value), }) diff --git a/src/components/cards/MediaCard.vue b/src/components/cards/MediaCard.vue index 76947be3..a0af8dce 100644 --- a/src/components/cards/MediaCard.vue +++ b/src/components/cards/MediaCard.vue @@ -10,7 +10,7 @@ import router from '@/router' import { useUserStore, useGlobalSettingsStore } from '@/stores' import { useI18n } from 'vue-i18n' import { mediaTypeDict } from '@/api/constants' -import { hasPermission } from '@/utils/permission' +import { buildUserPermissionContext, hasPermission } from '@/utils/permission' import { openSharedDialog } from '@/composables/useSharedDialog' import { getCachedMediaExistsStatus, @@ -45,6 +45,9 @@ const globalSettings = globalSettingsStore.globalSettings // 用户 Store const userStore = useUserStore() +const userPermissions = computed(() => buildUserPermissionContext(userStore.superUser, userStore.permissions)) +const canSearch = computed(() => hasPermission(userPermissions.value, 'search')) +const canSubscribe = computed(() => hasPermission(userPermissions.value, 'subscribe')) // 提示框 const $toast = useToast() @@ -143,7 +146,7 @@ async function querySites() { // 查询用户选中的站点 async function querySelectedSites() { try { - const result: { [key: string]: any } = await api.get('system/setting/IndexerSites') + const result: { [key: string]: any } = await api.get('system/setting/public/IndexerSites') selectedSites.value = result.data?.value ?? [] } catch (error) { console.log(error) @@ -336,12 +339,11 @@ async function checkSubscribe(season: number | null) { // 查询订阅弹窗规则 async function queryDefaultSubscribeConfig() { - // 非管理员不显示 - if (!userStore.superUser) return false + if (!canSubscribe.value) return false try { let subscribe_config_url = '' - if (props.media?.type === '电影') subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig' - else subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig' + if (props.media?.type === '电影') subscribe_config_url = 'system/setting/public/DefaultMovieSubscribeConfig' + else subscribe_config_url = 'system/setting/public/DefaultTvSubscribeConfig' const result: { [key: string]: any } = await api.get(subscribe_config_url) if (result.data?.value) return result.data.value.show_edit_dialog } catch (error) { @@ -534,7 +536,7 @@ onBeforeUnmount(() => {
{ /> // 加载目录设置 async function loadDirectories() { try { - const result: { [key: string]: any } = await api.get('system/setting/Directories') + const result: { [key: string]: any } = await api.get('system/setting/public/Directories') directories.value = result.data?.value ?? [] } catch (error) { console.log(error) diff --git a/src/components/dialog/AddSubtitleDownloadDialog.vue b/src/components/dialog/AddSubtitleDownloadDialog.vue index 2ee3354f..de1ebfbf 100644 --- a/src/components/dialog/AddSubtitleDownloadDialog.vue +++ b/src/components/dialog/AddSubtitleDownloadDialog.vue @@ -63,7 +63,7 @@ const buttonText = computed(() => // 加载目录设置 async function loadDirectories() { try { - const result: { [key: string]: any } = await api.get('system/setting/Directories') + const result: { [key: string]: any } = await api.get('system/setting/public/Directories') directories.value = result.data?.value ?? [] } catch (error) { console.log(error) diff --git a/src/components/dialog/ForkSubscribeDialog.vue b/src/components/dialog/ForkSubscribeDialog.vue index f9b97caa..fda5d47a 100644 --- a/src/components/dialog/ForkSubscribeDialog.vue +++ b/src/components/dialog/ForkSubscribeDialog.vue @@ -51,7 +51,7 @@ function toggleExpand() { // 加载follow用户列表 async function queryFollowUsers() { try { - const result: { [key: string]: any } = await api.get('system/setting/FollowSubscribers') + const result: { [key: string]: any } = await api.get('system/setting/public/FollowSubscribers') followUsers.value = result.data?.value ?? [] } catch (error) { console.log(error) diff --git a/src/components/dialog/PluginMarketSettingDialog.vue b/src/components/dialog/PluginMarketSettingDialog.vue index 2d5314e9..73943741 100644 --- a/src/components/dialog/PluginMarketSettingDialog.vue +++ b/src/components/dialog/PluginMarketSettingDialog.vue @@ -108,7 +108,7 @@ function switchEditorMode(mode: EditorMode | undefined) { /** 加载插件市场仓库配置。 */ async function queryMarketRepoSetting() { try { - const result: { [key: string]: any } = await api.get('system/setting/PLUGIN_MARKET') + const result: { [key: string]: any } = await api.get('system/setting/public/PLUGIN_MARKET') if (result && result.data && result.data.value) { repoList.value = parseRepoInput(result.data.value).repos syncTextFromList() diff --git a/src/components/dialog/ReorganizeDialog.vue b/src/components/dialog/ReorganizeDialog.vue index 86417089..669bb7ef 100644 --- a/src/components/dialog/ReorganizeDialog.vue +++ b/src/components/dialog/ReorganizeDialog.vue @@ -175,7 +175,7 @@ let episodeGroupQueryTimer: ReturnType | undefined // 查询存储 async function loadStorages() { try { - const result: { [key: string]: any } = await api.get('system/setting/Storages') + const result: { [key: string]: any } = await api.get('system/setting/public/Storages') storages.value = result.data?.value ?? [] } catch (error) { @@ -292,7 +292,7 @@ const directories = ref([]) // 查询目录 async function loadDirectories() { try { - const result: { [key: string]: any } = await api.get('system/setting/Directories') + const result: { [key: string]: any } = await api.get('system/setting/public/Directories') directories.value = result.data?.value ?? [] } catch (error) { console.log(error) @@ -848,7 +848,7 @@ async function requestManualTransfer( // 加载剧集格式规则配置状态,用于决定是否允许自动推荐。 async function loadEpisodeFormatRuleConfiguration() { try { - const result: { [key: string]: any } = await api.get('system/setting/EpisodeFormatRuleTable') + const result: { [key: string]: any } = await api.get('system/setting/public/EpisodeFormatRuleTable') episodeFormatRuleConfigured.value = Boolean(result.data?.value?.length) } catch (error) { console.log(error) diff --git a/src/components/dialog/SearchBarDialog.vue b/src/components/dialog/SearchBarDialog.vue index 08319a75..e0e78375 100644 --- a/src/components/dialog/SearchBarDialog.vue +++ b/src/components/dialog/SearchBarDialog.vue @@ -7,7 +7,7 @@ import { useUserStore, useGlobalSettingsStore } from '@/stores' import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue' import { useI18n } from 'vue-i18n' import { useDisplay } from 'vuetify' -import { hasPermission, filterMenusByPermission } from '@/utils/permission' +import { buildUserPermissionContext, hasPermission, filterMenusByPermission } from '@/utils/permission' // 显示器宽度 const display = useDisplay() @@ -30,41 +30,29 @@ const userStore = useUserStore() const globalSettingsStore = useGlobalSettingsStore() const globalSettings = globalSettingsStore.globalSettings -// 超级用户 -const superUser = userStore.superUser - // 当前用户名 const userName = userStore.userName +const userPermissions = computed(() => buildUserPermissionContext(userStore.superUser, userStore.permissions)) // 权限检查 const hasSearchPermission = computed(() => { - return hasPermission( - { - is_superuser: userStore.superUser, - ...userStore.permissions, - }, - 'search', - ) + return hasPermission(userPermissions.value, 'search') +}) + +const hasDiscoveryPermission = computed(() => { + return hasPermission(userPermissions.value, 'discovery') }) const hasSubscribePermission = computed(() => { - return hasPermission( - { - is_superuser: userStore.superUser, - ...userStore.permissions, - }, - 'subscribe', - ) + return hasPermission(userPermissions.value, 'subscribe') }) const hasManagePermission = computed(() => { - return hasPermission( - { - is_superuser: userStore.superUser, - ...userStore.permissions, - }, - 'manage', - ) + return hasPermission(userPermissions.value, 'manage') +}) + +const hasAdminPermission = computed(() => { + return hasPermission(userPermissions.value, 'admin') }) // 是否显示合集搜索项(当SEARCH_SOURCE包含themoviedb时显示) @@ -140,6 +128,7 @@ function getMenus(): NavMenu[] { to: item.to, header: item.header, admin: item.admin, + permission: item.permission, }), ) // 设置标签页 @@ -152,6 +141,7 @@ function getMenus(): NavMenu[] { to: `/setting?tab=${item.tab}`, header: '', admin: true, + permission: 'admin', description: item.description, }), ) @@ -159,12 +149,6 @@ function getMenus(): NavMenu[] { return menus } -// 获取用户权限信息 -const userPermissions = computed(() => ({ - is_superuser: userStore.superUser, - ...userStore.permissions, -})) - // 匹配的菜单列表 const matchedMenuItems = computed(() => { if (!searchWord.value) return [] @@ -202,7 +186,7 @@ async function fetchInstalledPlugins() { // 匹配的插件列表 const matchedPluginItems = computed(() => { if (!searchWord.value) return [] - if (!hasManagePermission.value) return [] + if (!hasAdminPermission.value) return [] const lowerWord = (searchWord.value as string).toLowerCase() return pluginItems.value.filter((item: Plugin) => { if (!item.plugin_name && !item.plugin_desc) return false @@ -222,7 +206,7 @@ async function fetchSubscribes() { // 从接口加载用户站点偏好设置 const loadUserSitePreferences = async () => { try { - const result = await api.get('system/setting/IndexerSites') + const result = await api.get('system/setting/public/IndexerSites') if (result && result.data && result.data.value) { selectedSites.value = result.data.value return @@ -259,7 +243,7 @@ const matchedSubscribeItems = computed(() => { if (!hasSubscribePermission.value) return [] const lowerWord = (searchWord.value as string).toLowerCase() return SubscribeItems.value.filter((item: Subscribe) => { - return (item.name.toLowerCase().includes(lowerWord) && (superUser || userName === item.username)) || false + return (item.name.toLowerCase().includes(lowerWord) && (userStore.superUser || userName === item.username)) || false }) }) @@ -276,7 +260,7 @@ function searchSites(sites: number[]) { // 搜索资源 function searchTorrent() { - if (!searchWord.value) return + if (!searchWord.value || !hasSearchPermission.value) return // 记录搜索词 saveRecentSearches(searchWord.value) // 跳转到搜索页面 @@ -296,7 +280,7 @@ function searchTorrent() { // 搜索字幕资源 function searchSubtitle() { - if (!searchWord.value) return + if (!searchWord.value || !hasSearchPermission.value) return saveRecentSearches(searchWord.value) router.push({ path: '/resource', @@ -314,7 +298,7 @@ function searchSubtitle() { // 跳转媒体搜索页面 function searchMedia(searchType: string) { // 搜索类型 media/person - if (!searchWord.value) return + if (!searchWord.value || !hasDiscoveryPermission.value) return saveRecentSearches(searchWord.value) router.push({ path: '/browse/media/search', @@ -395,7 +379,7 @@ onMounted(() => { searchWordInput.value?.focus() }, 500) // 根据权限加载不同的数据 - if (hasManagePermission.value) { + if (hasAdminPermission.value) { fetchInstalledPlugins() } if (hasSubscribePermission.value) { @@ -437,58 +421,60 @@ onMounted(() => { - - {{ t('common.media') }} - + + hasPermission(buildUserPermissionContext(userStore.superUser, userStore.permissions), 'admin'), +) // 显示器宽度 const display = useDisplay() @@ -128,6 +134,8 @@ async function loadDownloaderSetting() { // 加载规则组 async function queryFilterRuleGroups() { + if (!canAdmin.value) return + try { const result: { [key: string]: any } = await api.get('system/setting/UserFilterRuleGroups') filterRuleGroups.value = result.data?.value ?? [] @@ -163,6 +171,8 @@ async function updateSubscribeInfo() { // 设置用户设置的默认订阅规则 async function saveDefaultSubscribeConfig() { + if (!canAdmin.value) return + try { let subscribe_config_url = '' if (props.type === '电影') subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig' @@ -183,8 +193,8 @@ async function saveDefaultSubscribeConfig() { async function queryDefaultSubscribeConfig() { try { let subscribe_config_url = '' - if (props.type === '电影') subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig' - else subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig' + if (props.type === '电影') subscribe_config_url = 'system/setting/public/DefaultMovieSubscribeConfig' + else subscribe_config_url = 'system/setting/public/DefaultTvSubscribeConfig' const result: { [key: string]: any } = await api.get(subscribe_config_url) @@ -260,7 +270,7 @@ async function removeSubscribe() { // 查询下载目录 async function loadDownloadDirectories() { try { - const result: { [key: string]: any } = await api.get('system/setting/Directories') + const result: { [key: string]: any } = await api.get('system/setting/public/Directories') if (result.success && result.data?.value) { downloadDirectories.value = result.data.value } diff --git a/src/components/workflow/FilterTorrentsAction.vue b/src/components/workflow/FilterTorrentsAction.vue index 1b97f008..a9407696 100644 --- a/src/components/workflow/FilterTorrentsAction.vue +++ b/src/components/workflow/FilterTorrentsAction.vue @@ -4,8 +4,14 @@ import { FilterRuleGroup } from '@/api/types' import { Handle, Position } from '@vue-flow/core' import { useI18n } from 'vue-i18n' import { qualityOptions, resolutionOptions, effectOptions } from '@/api/constants' +import { useUserStore } from '@/stores' +import { buildUserPermissionContext, hasPermission } from '@/utils/permission' const { t } = useI18n() +const userStore = useUserStore() +const canAdmin = computed(() => + hasPermission(buildUserPermissionContext(userStore.superUser, userStore.permissions), 'admin'), +) defineProps({ id: { @@ -23,6 +29,8 @@ const filterRuleGroups = ref([]) // 加载规则组 async function queryFilterRuleGroups() { + if (!canAdmin.value) return + try { const result: { [key: string]: any } = await api.get('system/setting/UserFilterRuleGroups') filterRuleGroups.value = result.data?.value ?? [] diff --git a/src/components/workflow/ScanFileAction.vue b/src/components/workflow/ScanFileAction.vue index 39fdd52b..93885852 100644 --- a/src/components/workflow/ScanFileAction.vue +++ b/src/components/workflow/ScanFileAction.vue @@ -22,7 +22,7 @@ const storages = ref([]) // 查询存储 async function loadStorages() { - const result: { [key: string]: any } = await api.get('system/setting/Storages') + const result: { [key: string]: any } = await api.get('system/setting/public/Storages') storages.value = result.data?.value ?? [] } diff --git a/src/components/workflow/SendMessageAction.vue b/src/components/workflow/SendMessageAction.vue index cab6252d..ea0e25cb 100644 --- a/src/components/workflow/SendMessageAction.vue +++ b/src/components/workflow/SendMessageAction.vue @@ -3,8 +3,14 @@ import api from '@/api' import { NotificationConf } from '@/api/types' import { Handle, Position } from '@vue-flow/core' import { useI18n } from 'vue-i18n' +import { useUserStore } from '@/stores' +import { buildUserPermissionContext, hasPermission } from '@/utils/permission' const { t } = useI18n() +const userStore = useUserStore() +const canAdmin = computed(() => + hasPermission(buildUserPermissionContext(userStore.superUser, userStore.permissions), 'admin'), +) defineProps({ id: { @@ -22,6 +28,8 @@ const notifications = ref([]) // 调用API查询通知渠道设置 async function loadNotificationSetting() { + if (!canAdmin.value) return + try { const result: { [key: string]: any } = await api.get('system/setting/Notifications') notifications.value = result.data?.value ?? [] diff --git a/src/composables/useDynamicButton.ts b/src/composables/useDynamicButton.ts index b2c8d13b..c6db6fed 100644 --- a/src/composables/useDynamicButton.ts +++ b/src/composables/useDynamicButton.ts @@ -12,6 +12,7 @@ import { type ComputedRef, type Ref, } from 'vue' +import type { UserPermissionKey } from '@/utils/permission' // 声明全局变量类型 declare global { @@ -29,6 +30,7 @@ export interface DynamicButtonMenuItem { titleParams?: Record icon?: string color?: string + permission?: UserPermissionKey action: () => void } @@ -57,11 +59,12 @@ export function useDynamicButton(options: { icon: MaybeRefValue onClick?: () => void menuItems?: MaybeRefValue + permission?: UserPermissionKey show?: MaybeRefValue autoRegister?: boolean // 是否自动注册,默认为true }) { // 提取配置 - const { icon, onClick, menuItems, show, autoRegister = true } = options + const { icon, onClick, menuItems, permission, show, autoRegister = true } = options // 动态按钮相关 const registerDynamicButton = inject<((button: any) => void) | null>('registerDynamicButton', null) @@ -81,6 +84,7 @@ export function useDynamicButton(options: { return { icon: resolvedIcon.value, action: onClick || (() => {}), + permission, show: resolvedShow.value, menuItems: buttonMenuItems && buttonMenuItems.length > 0 ? buttonMenuItems : undefined, } @@ -174,7 +178,7 @@ export function useDynamicButton(options: { cleanupDynamicButton() }) - watch([resolvedIcon, resolvedShow, resolvedMenuItems], () => { + watch([resolvedIcon, resolvedShow, resolvedMenuItems, () => permission], () => { if (!componentActive.value) return setupDynamicButton() diff --git a/src/composables/useDynamicHeaderTab.ts b/src/composables/useDynamicHeaderTab.ts index 965b7f88..88c85145 100644 --- a/src/composables/useDynamicHeaderTab.ts +++ b/src/composables/useDynamicHeaderTab.ts @@ -1,5 +1,6 @@ import type { ComputedRef, Ref } from 'vue' import { useTabStateRestore } from '@/composables/useStateRestore' +import type { UserPermissionKey } from '@/utils/permission' // 动态标签页相关类型 interface DynamicHeaderTabButton { @@ -9,6 +10,7 @@ interface DynamicHeaderTabButton { size?: string class?: string action?: () => void + permission?: UserPermissionKey show?: boolean | ComputedRef loading?: boolean | ComputedRef dataAttr?: string // 用于VMenu定位的data属性 diff --git a/src/composables/useSetupWizard.ts b/src/composables/useSetupWizard.ts index 9ac4a6b7..8be39b39 100644 --- a/src/composables/useSetupWizard.ts +++ b/src/composables/useSetupWizard.ts @@ -1619,7 +1619,7 @@ export function useSetupWizard() { // 加载存储设置 async function loadStorageSettings() { try { - const result: { [key: string]: any } = await api.get('system/setting/Directories') + const result: { [key: string]: any } = await api.get('system/setting/public/Directories') if (result.success && result.data?.value && result.data.value.length > 0) { const directory = result.data.value[0] wizardData.value.storage.downloadPath = directory.download_path || '' diff --git a/src/layouts/components/DefaultLayout.vue b/src/layouts/components/DefaultLayout.vue index 92c92000..934768ad 100644 --- a/src/layouts/components/DefaultLayout.vue +++ b/src/layouts/components/DefaultLayout.vue @@ -16,7 +16,14 @@ import { NavMenu } from '@/@layouts/types' import { useDisplay } from 'vuetify' import { useI18n } from 'vue-i18n' import { useRoute, useRouter } from 'vue-router' -import { filterMenusByPermission } from '@/utils/permission' +import { + buildUserPermissionContext, + filterItemsByPermission, + filterMenusByPermission, + hasItemPermission, + hasPermission, + type UserPermissionKey, +} from '@/utils/permission' import { onUnreadMessage } from '@/utils/badge' import { usePullDownGesture } from '@/composables/usePullDownGesture' import { usePWA } from '@/composables/usePWA' @@ -41,17 +48,12 @@ const themeLayout = ref(readThemeCustomizerSettings().layout) const userStore = useUserStore() const pluginSidebarNavStore = usePluginSidebarNavStore() -// 响应式的超级用户状态 -const superUser = computed(() => userStore.superUser) - // ShortcutBar 引用 const shortcutBarRef = ref | null>(null) // 获取用户权限信息 -const userPermissions = computed(() => ({ - is_superuser: userStore.superUser, - ...userStore.permissions, -})) +const userPermissions = computed(() => buildUserPermissionContext(userStore.superUser, userStore.permissions)) +const canAdmin = computed(() => hasPermission(userPermissions.value, 'admin')) // 开始菜单项 const startMenus = ref([]) @@ -112,6 +114,7 @@ interface DynamicHeaderTabButton { size?: string class?: string action?: () => void + permission?: UserPermissionKey show?: boolean | ComputedRef loading?: boolean | ComputedRef dataAttr?: string @@ -196,10 +199,19 @@ const hasDynamicHeaderTab = computed(() => { // 水平布局下动态标签页会并入顶部导航三级菜单,不再额外显示标签页栏。 const showDynamicHeaderTab = computed(() => hasDynamicHeaderTab.value && !showHorizontalThemeNav.value) -const visibleHorizontalHeaderButtons = computed(() => { - if (!showHorizontalThemeNav.value || !hasDynamicHeaderTab.value) return [] +const visibleDynamicHeaderButtons = computed(() => { + if (!hasDynamicHeaderTab.value) return [] - return (dynamicHeaderTab.value?.appendButtons ?? []).filter(button => resolveMaybeRefValue(button.show, true) !== false) + const visibleButtons = (dynamicHeaderTab.value?.appendButtons ?? []).filter( + button => resolveMaybeRefValue(button.show, true) !== false, + ) + return filterItemsByPermission(visibleButtons, userPermissions.value) +}) + +const visibleHorizontalHeaderButtons = computed(() => { + if (!showHorizontalThemeNav.value) return [] + + return visibleDynamicHeaderButtons.value }) // 在组件销毁时清理 @@ -227,7 +239,7 @@ const canUsePullGesture = () => { // 检查是否在dashboard页面 const isDashboard = route.path === '/dashboard' || route.path === '/' // 检查是否是管理员 - const isAdmin = superUser.value + const isAdmin = canAdmin.value // 检查插件快速访问面板是否已显示 const quickAccessOpen = showPluginQuickAccess.value // 检查是否离线 @@ -323,6 +335,12 @@ function resolveHeaderButtonLoading(button: DynamicHeaderTabButton) { return resolveMaybeRefValue(button.loading, false) } +function handleHeaderButtonClick(button: DynamicHeaderTabButton) { + if (!hasItemPermission(button, userPermissions.value)) return + + button.action?.() +} + function getHorizontalTabIcon(tab: DynamicHeaderTabItem) { const icon = tab.icon?.trim() @@ -366,7 +384,7 @@ function applyPendingHorizontalTab() { // 处理未读消息事件 function handleUnreadMessage(count: number) { - if (superUser.value && count > 0) { + if (canAdmin.value && count > 0) { // 延迟一点时间确保组件已渲染 setTimeout(() => { if (shortcutBarRef.value && typeof shortcutBarRef.value.openMessageDialog === 'function') { @@ -480,7 +498,7 @@ onMounted(async () => { class="theme-navbar-row d-flex h-14 align-center mx-1" :class="{ 'theme-navbar-row--horizontal': showHorizontalThemeNav }" > -
@@ -650,17 +668,16 @@ onMounted(async () => { @update:model-value="handleTabChange" >