diff --git a/src/components/cards/UserCard.vue b/src/components/cards/UserCard.vue index a3f94049..bf6e6227 100644 --- a/src/components/cards/UserCard.vue +++ b/src/components/cards/UserCard.vue @@ -123,173 +123,176 @@ onMounted(() => { 'transition-transform duration-300 hover:-translate-y-1', !props.user.is_active ? 'opacity-85 bg-surface-lighten-1' : '', ]" + class="flex flex-column" @click="userEditDialog = true" > - - - - - -
-
- + + + + + +
+
+ + {{ displayName }} + + +
+
+ {{ + t('user.admin') + }} + {{ t('user.normal') }} + + {{ user.is_active ? t('user.active') : t('user.inactive') }} + + 2FA +
-
- -
-
- - {{ movieSubscriptions }} + +
+
+ + {{ movieSubscriptions }} +
+
+ + {{ tvShowSubscriptions }} +
-
- - {{ tvShowSubscriptions }} + + + + + - - - - - -
- - {{ t('dialog.userAddEdit.permissions.discovery') }} - - - {{ t('dialog.userAddEdit.permissions.search') }} - - - {{ t('dialog.userAddEdit.permissions.subscribe') }} - - - {{ t('dialog.userAddEdit.permissions.manage') }} - + +
+ + {{ t('dialog.userAddEdit.permissions.discovery') }} + + + {{ t('dialog.userAddEdit.permissions.search') }} + + + {{ t('dialog.userAddEdit.permissions.subscribe') }} + + + {{ t('dialog.userAddEdit.permissions.manage') }} + +
- +
+ + + {{ user.email || t('user.noEmail') }} + - - - {{ user.email || t('user.noEmail') }} - - - - -
-
- -
- + + +
+
+ +
+ +
+
+
+ {{ movieSubscriptions }} + {{ t('user.movieSubscriptions') }} +
+
+
+ +
+ +
+
+
+ {{ tvShowSubscriptions }} + {{ t('user.tvSubscriptions') }}
- -
- {{ movieSubscriptions }} - {{ t('user.movieSubscriptions') }}
-
- -
- -
-
-
- {{ tvShowSubscriptions }} - {{ t('user.tvSubscriptions') }} -
-
-
-
+ +
diff --git a/src/layouts/components/Footer.vue b/src/layouts/components/Footer.vue index 0c5e770f..fe15c8a5 100644 --- a/src/layouts/components/Footer.vue +++ b/src/layouts/components/Footer.vue @@ -19,10 +19,23 @@ const route = useRoute() const userStore = useUserStore() // 获取用户权限信息 -const userPermissions = computed(() => ({ - is_superuser: userStore.superUser, - ...userStore.permissions, -})) +const userPermissions = computed(() => { + // 确保用户已认证且信息已加载 + if (!userStore || userStore.userID === -1) { + return { + is_superuser: false, + discovery: false, + search: false, + subscribe: false, + manage: false, + } + } + + return { + is_superuser: userStore.superUser, + ...userStore.permissions, + } +}) // 获取导航菜单 const navMenus = computed(() => { @@ -41,7 +54,42 @@ const currentMenu = ref(getMenuPathFromRoute(route.path)) // 过滤出底部菜单项 const footerMenus = computed(() => { - return navMenus.value.filter((menu: NavMenu) => menu.footer === true) + // 获取所有有权限的菜单 + const allAuthorizedMenus = navMenus.value + + // 优先获取有 footer: true 属性的菜单 + const footerMenusWithProperty = allAuthorizedMenus.filter((menu: NavMenu) => menu.footer === true) + + // 设置期望的底部菜单数量(不包括"更多"按钮) + // 一般来说,底部导航栏显示 3-4 个主要功能比较合适 + const expectedFooterMenuCount = 3 + + // 如果有 footer 属性的菜单已经足够,优先显示它们 + if (footerMenusWithProperty.length >= expectedFooterMenuCount) { + return footerMenusWithProperty.slice(0, expectedFooterMenuCount) + } + + // 如果不够,从没有 footer 属性或 footer 为 false 的菜单中补充 + // 优先选择一些常用的功能菜单 + const nonFooterMenus = allAuthorizedMenus.filter( + (menu: NavMenu) => + menu.footer !== true && + // 排除已经在 footerMenusWithProperty 中的菜单 + !footerMenusWithProperty.some(footerMenu => footerMenu.to === menu.to), + ) + + // 计算还需要多少个菜单 + const needCount = expectedFooterMenuCount - footerMenusWithProperty.length + + // 合并菜单:优先显示有 footer 属性的,然后按菜单定义顺序添加其他菜单 + let finalMenus = [...footerMenusWithProperty, ...nonFooterMenus.slice(0, needCount)] + + // 确保至少有一个菜单显示,如果都没有权限,则显示第一个有权限的菜单 + if (finalMenus.length === 0 && allAuthorizedMenus.length > 0) { + finalMenus = [allAuthorizedMenus[0]] + } + + return finalMenus }) // 监听路由变化来更新currentMenu @@ -209,8 +257,8 @@ const showDynamicButton = computed(() => { .footer-card-content { position: relative; - padding-block: 6px; - padding-inline: 8px; + padding-block: 4px; + padding-inline: 6px; } .footer-btn-group { diff --git a/src/layouts/components/UserProfile.vue b/src/layouts/components/UserProfile.vue index 05131f6e..184c7376 100644 --- a/src/layouts/components/UserProfile.vue +++ b/src/layouts/components/UserProfile.vue @@ -63,6 +63,7 @@ function logout() { // 清除登录状态信息 authStore.logout() + userStore.reset() // 重定向到登录页面或其他适当的页面 router.push('/login') } diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index f2b00d77..62c3b6fd 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -137,6 +137,7 @@ export default { networkError: 'Login failed, please check your network connection!', authFailure: 'Login failed, please check your username, password or two-factor authentication!', permissionDenied: 'Login failed, you do not have permission to access!', + noPermission: 'Login failed, you have no functional permissions, please contact the administrator!', serverError: 'Login failed, server error!', loginFailed: 'Login Failed', checkCredentials: 'Please check your username, password or two-factor authentication code!', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index e6b0965c..0ddbeb30 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -137,6 +137,7 @@ export default { networkError: '登录失败,请检查网络连接!', authFailure: '登录失败,请检查用户名、密码或双重验证是否正确!', permissionDenied: '登录失败,您没有权限访问!', + noPermission: '登录失败,您没有任何功能权限,请联系管理员!', serverError: '登录失败,服务器错误!', loginFailed: '登录失败', checkCredentials: '请检查用户名、密码或双重验证码是否正确!', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index 365217cb..bd81c7ed 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -139,6 +139,7 @@ export default { authFailure: '登錄失敗,請檢查用戶名、密碼或雙重驗證是否正確!', permissionDenied: '登錄失敗,您沒有權限訪問!', serverError: '登錄失敗,服務器錯誤!', + noPermission: '登錄失敗,您沒有任何功能權限,請聯繫管理員!', loginFailed: '登錄失敗', checkCredentials: '請檢查用戶名、密碼或雙重驗證碼是否正確!', }, diff --git a/src/pages/login.vue b/src/pages/login.vue index 798c79f9..5e2d2872 100644 --- a/src/pages/login.vue +++ b/src/pages/login.vue @@ -11,6 +11,8 @@ import { urlBase64ToUint8Array } from '@/@core/utils/navigator' import { SUPPORTED_LOCALES, SupportedLocale } from '@/types/i18n' import { getCurrentLocale, setI18nLanguage } from '@/plugins/i18n' import { useTheme } from 'vuetify' +import { getNavMenus } from '@/router/i18n-menu' +import { filterMenusByPermission } from '@/utils/permission' // 国际化 const { t } = useI18n() @@ -19,6 +21,9 @@ const authStore = useAuthStore() //用户 Store const userStore = useUserStore() +// 获取有权限的菜单 +const navMenus = getNavMenus() + // 表单 const form = ref({ username: '', @@ -111,9 +116,15 @@ async function subscribeForPushNotifications() { } // 登录后处理 -async function afterLogin(superuser: boolean) { - // 跳转到首页或回原始页面 - router.push(authStore.originalPath ?? '/') +async function afterLogin(superuser: boolean, userPayload: userState, filteredMenus: any[]) { + // 如果有原始路径,优先跳转到原始路径 + if (authStore.originalPath && authStore.originalPath !== '/') { + router.push(authStore.originalPath) + } else { + // 跳转到第一个有权限的菜单 + router.push(filteredMenus[0].to) + } + // 订阅推送通知 if (superuser) await subscribeForPushNotifications() // 登录按钮 loading @@ -147,11 +158,6 @@ function login() { }, }) .then((response: any) => { - const authPayLoad: authState = { - token: response.access_token, - remember: form.value.remember, - } - const userPayload: userState = { superUser: response.super_user, userID: response.user_id, @@ -161,11 +167,32 @@ function login() { permissions: response.permissions, } + // 在保存用户信息之前检查权限 + const userPermissions = { + is_superuser: userPayload.superUser, + ...userPayload.permissions, + } + + const filteredMenus = filterMenusByPermission(navMenus, userPermissions) + // 如果用户没有任何可用菜单,拒绝登录 + if (filteredMenus.length === 0) { + // 显示错误信息 + errorMessage.value = t('login.noPermission') + loading.value = false + return + } + + // 权限检查通过,保存用户信息 + const authPayLoad: authState = { + token: response.access_token, + remember: form.value.remember, + } + authStore.login(authPayLoad) userStore.loginUser(userPayload) // 登录后处理 - afterLogin(userPayload.superUser) + afterLogin(userPayload.superUser, userPayload, filteredMenus) }) .catch((error: any) => { // 登录失败,显示错误提示 diff --git a/src/router/index.ts b/src/router/index.ts index 1eb628f6..ec4501d6 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -224,15 +224,18 @@ function abortAllControllers() { } // 路由导航守卫 -router.beforeEach((to: any, from: any, next: any) => { +router.beforeEach(async (to: any, from: any, next: any) => { // 认证 Store const authStore = useAuthStore() // 总是记录非login路由 if (to.fullPath != '/login') authStore.originalPath = to.fullPath const isAuthenticated = authStore.token !== null + if (to.meta.requiresAuth && !isAuthenticated) { + // 用户未登录,重定向到登录页 next('/login') } else { + // 清理所有中止控制器 abortAllControllers() next() }