优化用户信息展示和权限显示逻辑

This commit is contained in:
jxxghp
2025-06-11 12:56:10 +08:00
parent 052d6edd13
commit 3c051b8698
8 changed files with 251 additions and 166 deletions
+5 -2
View File
@@ -123,8 +123,10 @@ onMounted(() => {
'transition-transform duration-300 hover:-translate-y-1', 'transition-transform duration-300 hover:-translate-y-1',
!props.user.is_active ? 'opacity-85 bg-surface-lighten-1' : '', !props.user.is_active ? 'opacity-85 bg-surface-lighten-1' : '',
]" ]"
class="flex flex-column"
@click="userEditDialog = true" @click="userEditDialog = true"
> >
<div class="flex-grow">
<!-- 用户头像和基本信息 --> <!-- 用户头像和基本信息 -->
<VCardItem :class="[user.is_superuser ? 'admin-header' : '']"> <VCardItem :class="[user.is_superuser ? 'admin-header' : '']">
<template v-slot:prepend> <template v-slot:prepend>
@@ -242,10 +244,10 @@ onMounted(() => {
{{ t('dialog.userAddEdit.permissions.manage') }} {{ t('dialog.userAddEdit.permissions.manage') }}
</VChip> </VChip>
</div> </div>
</div>
<!-- 独立的邮箱显示 --> <!-- 独立的邮箱显示 -->
<VDivider class="mx-4" /> <VDivider class="mx-4" />
<div>
<VCardText class="d-flex align-center py-2 px-4 text-medium-emphasis"> <VCardText class="d-flex align-center py-2 px-4 text-medium-emphasis">
<VIcon icon="mdi-email-outline" size="small" color="primary" class="mr-2 opacity-70" /> <VIcon icon="mdi-email-outline" size="small" color="primary" class="mr-2 opacity-70" />
<span class="text-body-2 truncate">{{ user.email || t('user.noEmail') }}</span> <span class="text-body-2 truncate">{{ user.email || t('user.noEmail') }}</span>
@@ -290,6 +292,7 @@ onMounted(() => {
</div> </div>
</div> </div>
</VCardText> </VCardText>
</div>
</VCard> </VCard>
<!-- 用户编辑弹窗 --> <!-- 用户编辑弹窗 -->
+53 -5
View File
@@ -19,10 +19,23 @@ const route = useRoute()
const userStore = useUserStore() const userStore = useUserStore()
// 获取用户权限信息 // 获取用户权限信息
const userPermissions = computed(() => ({ 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, is_superuser: userStore.superUser,
...userStore.permissions, ...userStore.permissions,
})) }
})
// 获取导航菜单 // 获取导航菜单
const navMenus = computed(() => { const navMenus = computed(() => {
@@ -41,7 +54,42 @@ const currentMenu = ref<string>(getMenuPathFromRoute(route.path))
// 过滤出底部菜单项 // 过滤出底部菜单项
const footerMenus = computed(() => { 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 // 监听路由变化来更新currentMenu
@@ -209,8 +257,8 @@ const showDynamicButton = computed(() => {
.footer-card-content { .footer-card-content {
position: relative; position: relative;
padding-block: 6px; padding-block: 4px;
padding-inline: 8px; padding-inline: 6px;
} }
.footer-btn-group { .footer-btn-group {
+1
View File
@@ -63,6 +63,7 @@ function logout() {
// 清除登录状态信息 // 清除登录状态信息
authStore.logout() authStore.logout()
userStore.reset()
// 重定向到登录页面或其他适当的页面 // 重定向到登录页面或其他适当的页面
router.push('/login') router.push('/login')
} }
+1
View File
@@ -137,6 +137,7 @@ export default {
networkError: 'Login failed, please check your network connection!', networkError: 'Login failed, please check your network connection!',
authFailure: 'Login failed, please check your username, password or two-factor authentication!', authFailure: 'Login failed, please check your username, password or two-factor authentication!',
permissionDenied: 'Login failed, you do not have permission to access!', 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!', serverError: 'Login failed, server error!',
loginFailed: 'Login Failed', loginFailed: 'Login Failed',
checkCredentials: 'Please check your username, password or two-factor authentication code!', checkCredentials: 'Please check your username, password or two-factor authentication code!',
+1
View File
@@ -137,6 +137,7 @@ export default {
networkError: '登录失败,请检查网络连接!', networkError: '登录失败,请检查网络连接!',
authFailure: '登录失败,请检查用户名、密码或双重验证是否正确!', authFailure: '登录失败,请检查用户名、密码或双重验证是否正确!',
permissionDenied: '登录失败,您没有权限访问!', permissionDenied: '登录失败,您没有权限访问!',
noPermission: '登录失败,您没有任何功能权限,请联系管理员!',
serverError: '登录失败,服务器错误!', serverError: '登录失败,服务器错误!',
loginFailed: '登录失败', loginFailed: '登录失败',
checkCredentials: '请检查用户名、密码或双重验证码是否正确!', checkCredentials: '请检查用户名、密码或双重验证码是否正确!',
+1
View File
@@ -139,6 +139,7 @@ export default {
authFailure: '登錄失敗,請檢查用戶名、密碼或雙重驗證是否正確!', authFailure: '登錄失敗,請檢查用戶名、密碼或雙重驗證是否正確!',
permissionDenied: '登錄失敗,您沒有權限訪問!', permissionDenied: '登錄失敗,您沒有權限訪問!',
serverError: '登錄失敗,服務器錯誤!', serverError: '登錄失敗,服務器錯誤!',
noPermission: '登錄失敗,您沒有任何功能權限,請聯繫管理員!',
loginFailed: '登錄失敗', loginFailed: '登錄失敗',
checkCredentials: '請檢查用戶名、密碼或雙重驗證碼是否正確!', checkCredentials: '請檢查用戶名、密碼或雙重驗證碼是否正確!',
}, },
+36 -9
View File
@@ -11,6 +11,8 @@ import { urlBase64ToUint8Array } from '@/@core/utils/navigator'
import { SUPPORTED_LOCALES, SupportedLocale } from '@/types/i18n' import { SUPPORTED_LOCALES, SupportedLocale } from '@/types/i18n'
import { getCurrentLocale, setI18nLanguage } from '@/plugins/i18n' import { getCurrentLocale, setI18nLanguage } from '@/plugins/i18n'
import { useTheme } from 'vuetify' import { useTheme } from 'vuetify'
import { getNavMenus } from '@/router/i18n-menu'
import { filterMenusByPermission } from '@/utils/permission'
// 国际化 // 国际化
const { t } = useI18n() const { t } = useI18n()
@@ -19,6 +21,9 @@ const authStore = useAuthStore()
//用户 Store //用户 Store
const userStore = useUserStore() const userStore = useUserStore()
// 获取有权限的菜单
const navMenus = getNavMenus()
// 表单 // 表单
const form = ref({ const form = ref({
username: '', username: '',
@@ -111,9 +116,15 @@ async function subscribeForPushNotifications() {
} }
// 登录后处理 // 登录后处理
async function afterLogin(superuser: boolean) { async function afterLogin(superuser: boolean, userPayload: userState, filteredMenus: any[]) {
// 跳转到首页或回原始页面 // 如果有原始路径,优先跳转到原始路径
router.push(authStore.originalPath ?? '/') if (authStore.originalPath && authStore.originalPath !== '/') {
router.push(authStore.originalPath)
} else {
// 跳转到第一个有权限的菜单
router.push(filteredMenus[0].to)
}
// 订阅推送通知 // 订阅推送通知
if (superuser) await subscribeForPushNotifications() if (superuser) await subscribeForPushNotifications()
// 登录按钮 loading // 登录按钮 loading
@@ -147,11 +158,6 @@ function login() {
}, },
}) })
.then((response: any) => { .then((response: any) => {
const authPayLoad: authState = {
token: response.access_token,
remember: form.value.remember,
}
const userPayload: userState = { const userPayload: userState = {
superUser: response.super_user, superUser: response.super_user,
userID: response.user_id, userID: response.user_id,
@@ -161,11 +167,32 @@ function login() {
permissions: response.permissions, 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) authStore.login(authPayLoad)
userStore.loginUser(userPayload) userStore.loginUser(userPayload)
// 登录后处理 // 登录后处理
afterLogin(userPayload.superUser) afterLogin(userPayload.superUser, userPayload, filteredMenus)
}) })
.catch((error: any) => { .catch((error: any) => {
// 登录失败,显示错误提示 // 登录失败,显示错误提示
+4 -1
View File
@@ -224,15 +224,18 @@ function abortAllControllers() {
} }
// 路由导航守卫 // 路由导航守卫
router.beforeEach((to: any, from: any, next: any) => { router.beforeEach(async (to: any, from: any, next: any) => {
// 认证 Store // 认证 Store
const authStore = useAuthStore() const authStore = useAuthStore()
// 总是记录非login路由 // 总是记录非login路由
if (to.fullPath != '/login') authStore.originalPath = to.fullPath if (to.fullPath != '/login') authStore.originalPath = to.fullPath
const isAuthenticated = authStore.token !== null const isAuthenticated = authStore.token !== null
if (to.meta.requiresAuth && !isAuthenticated) { if (to.meta.requiresAuth && !isAuthenticated) {
// 用户未登录,重定向到登录页
next('/login') next('/login')
} else { } else {
// 清理所有中止控制器
abortAllControllers() abortAllControllers()
next() next()
} }