mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 16:56:40 +08:00
优化用户信息展示和权限显示逻辑
This commit is contained in:
+152
-149
@@ -123,173 +123,176 @@ 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' : '']">
|
<!-- 用户头像和基本信息 -->
|
||||||
<template v-slot:prepend>
|
<VCardItem :class="[user.is_superuser ? 'admin-header' : '']">
|
||||||
<div class="position-relative mr-4">
|
<template v-slot:prepend>
|
||||||
<VAvatar
|
<div class="position-relative mr-4">
|
||||||
size="72"
|
<VAvatar
|
||||||
rounded="lg"
|
size="72"
|
||||||
:class="[
|
rounded="lg"
|
||||||
user.is_superuser ? 'admin-avatar' : 'border-4 bg-surface',
|
|
||||||
!user.is_active ? 'grayscale-50 opacity-90' : '',
|
|
||||||
]"
|
|
||||||
:style="user.is_superuser ? 'border: 4px solid rgba(var(--v-theme-warning), 0.3);' : ''"
|
|
||||||
>
|
|
||||||
<VImg :src="user.avatar || avatar1" :alt="user.name" />
|
|
||||||
<div
|
|
||||||
v-if="!user.is_active"
|
|
||||||
class="position-absolute d-flex align-center justify-center rounded-lg bg-surface-variant opacity-20"
|
|
||||||
style="inset: 0"
|
|
||||||
>
|
|
||||||
<VIcon icon="mdi-account-lock" color="white" />
|
|
||||||
</div>
|
|
||||||
</VAvatar>
|
|
||||||
<div v-if="user.is_superuser" class="admin-crown">
|
|
||||||
<VIcon icon="mdi-crown" color="warning" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<VCardTitle class="pa-0 d-flex flex-column">
|
|
||||||
<div class="d-flex flex-column mb-1">
|
|
||||||
<div class="d-flex align-center">
|
|
||||||
<span
|
|
||||||
:class="[
|
:class="[
|
||||||
'text-h6 font-weight-bold truncate',
|
user.is_superuser ? 'admin-avatar' : 'border-4 bg-surface',
|
||||||
user.is_superuser ? 'text-warning' : '',
|
!user.is_active ? 'grayscale-50 opacity-90' : '',
|
||||||
!user.is_active ? 'text-medium-emphasis' : '',
|
|
||||||
]"
|
]"
|
||||||
|
:style="user.is_superuser ? 'border: 4px solid rgba(var(--v-theme-warning), 0.3);' : ''"
|
||||||
>
|
>
|
||||||
{{ displayName }}
|
<VImg :src="user.avatar || avatar1" :alt="user.name" />
|
||||||
<VIcon
|
<div
|
||||||
v-if="user.nickname || user.settings?.nickname"
|
v-if="!user.is_active"
|
||||||
icon="mdi-format-quote-close"
|
class="position-absolute d-flex align-center justify-center rounded-lg bg-surface-variant opacity-20"
|
||||||
size="x-small"
|
style="inset: 0"
|
||||||
color="info"
|
>
|
||||||
class="animate-pulse"
|
<VIcon icon="mdi-account-lock" color="white" />
|
||||||
/>
|
</div>
|
||||||
</span>
|
</VAvatar>
|
||||||
|
<div v-if="user.is_superuser" class="admin-crown">
|
||||||
|
<VIcon icon="mdi-crown" color="warning" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap gap-1 overflow-auto">
|
</template>
|
||||||
<VChip v-if="user.is_superuser" size="x-small" color="error" variant="outlined" label>{{
|
|
||||||
t('user.admin')
|
<VCardTitle class="pa-0 d-flex flex-column">
|
||||||
}}</VChip>
|
<div class="d-flex flex-column mb-1">
|
||||||
<VChip v-else size="x-small" label>{{ t('user.normal') }}</VChip>
|
<div class="d-flex align-center">
|
||||||
<VChip size="x-small" :color="user.is_active ? 'success' : 'grey'" variant="tonal" label>
|
<span
|
||||||
{{ user.is_active ? t('user.active') : t('user.inactive') }}
|
:class="[
|
||||||
</VChip>
|
'text-h6 font-weight-bold truncate',
|
||||||
<VChip v-if="user.is_otp" size="x-small" color="info" variant="tonal" label>2FA</VChip>
|
user.is_superuser ? 'text-warning' : '',
|
||||||
|
!user.is_active ? 'text-medium-emphasis' : '',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ displayName }}
|
||||||
|
<VIcon
|
||||||
|
v-if="user.nickname || user.settings?.nickname"
|
||||||
|
icon="mdi-format-quote-close"
|
||||||
|
size="x-small"
|
||||||
|
color="info"
|
||||||
|
class="animate-pulse"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap gap-1 overflow-auto">
|
||||||
|
<VChip v-if="user.is_superuser" size="x-small" color="error" variant="outlined" label>{{
|
||||||
|
t('user.admin')
|
||||||
|
}}</VChip>
|
||||||
|
<VChip v-else size="x-small" label>{{ t('user.normal') }}</VChip>
|
||||||
|
<VChip size="x-small" :color="user.is_active ? 'success' : 'grey'" variant="tonal" label>
|
||||||
|
{{ user.is_active ? t('user.active') : t('user.inactive') }}
|
||||||
|
</VChip>
|
||||||
|
<VChip v-if="user.is_otp" size="x-small" color="info" variant="tonal" label>2FA</VChip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 移动端订阅数据信息 -->
|
<!-- 移动端订阅数据信息 -->
|
||||||
<div v-if="isMobile" class="d-flex gap-5 mt-2">
|
<div v-if="isMobile" class="d-flex gap-5 mt-2">
|
||||||
<div class="d-flex align-center">
|
<div class="d-flex align-center">
|
||||||
<VIcon size="x-small" icon="mdi-movie-outline" color="primary" class="mr-1" />
|
<VIcon size="x-small" icon="mdi-movie-outline" color="primary" class="mr-1" />
|
||||||
<span class="text-body-2">{{ movieSubscriptions }}</span>
|
<span class="text-body-2">{{ movieSubscriptions }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<VIcon size="x-small" icon="mdi-television-classic" color="primary" class="mr-1" />
|
||||||
|
<span class="text-body-2">{{ tvShowSubscriptions }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-center">
|
</VCardTitle>
|
||||||
<VIcon size="x-small" icon="mdi-television-classic" color="primary" class="mr-1" />
|
|
||||||
<span class="text-body-2">{{ tvShowSubscriptions }}</span>
|
<!-- 头部操作按钮 -->
|
||||||
|
<template v-slot:append>
|
||||||
|
<div :class="['d-flex', isMobile ? 'position-absolute top-2 right-2' : '']">
|
||||||
|
<VBtn
|
||||||
|
icon
|
||||||
|
size="small"
|
||||||
|
:color="user.is_superuser ? 'warning' : 'primary'"
|
||||||
|
variant="text"
|
||||||
|
class="opacity-70 hover:opacity-100 transition-opacity"
|
||||||
|
@click.stop="editUser"
|
||||||
|
>
|
||||||
|
<VIcon icon="mdi-pencil" />
|
||||||
|
</VBtn>
|
||||||
|
|
||||||
|
<VBtn
|
||||||
|
v-if="props.user.id != currentLoginUserId && currentUserIsSuperuser"
|
||||||
|
icon
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
class="opacity-70 hover:opacity-100 transition-opacity"
|
||||||
|
@click.stop="removeUser"
|
||||||
|
>
|
||||||
|
<VIcon icon="mdi-delete" />
|
||||||
|
</VBtn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</VCardTitle>
|
</VCardItem>
|
||||||
|
|
||||||
<!-- 头部操作按钮 -->
|
<!-- 权限显示 -->
|
||||||
<template v-slot:append>
|
<div v-if="!user.is_superuser && user.permissions" class="d-flex flex-wrap gap-1 px-7 pb-3">
|
||||||
<div :class="['d-flex', isMobile ? 'position-absolute top-2 right-2' : '']">
|
<VChip v-if="user.permissions.discovery" size="x-small" color="purple" variant="outlined" label>
|
||||||
<VBtn
|
{{ t('dialog.userAddEdit.permissions.discovery') }}
|
||||||
icon
|
</VChip>
|
||||||
size="small"
|
<VChip v-if="user.permissions.search" size="x-small" color="blue" variant="outlined" label>
|
||||||
:color="user.is_superuser ? 'warning' : 'primary'"
|
{{ t('dialog.userAddEdit.permissions.search') }}
|
||||||
variant="text"
|
</VChip>
|
||||||
class="opacity-70 hover:opacity-100 transition-opacity"
|
<VChip v-if="user.permissions.subscribe" size="x-small" color="green" variant="outlined" label>
|
||||||
@click.stop="editUser"
|
{{ t('dialog.userAddEdit.permissions.subscribe') }}
|
||||||
>
|
</VChip>
|
||||||
<VIcon icon="mdi-pencil" />
|
<VChip v-if="user.permissions.manage" size="x-small" color="orange" variant="outlined" label>
|
||||||
</VBtn>
|
{{ t('dialog.userAddEdit.permissions.manage') }}
|
||||||
|
</VChip>
|
||||||
<VBtn
|
</div>
|
||||||
v-if="props.user.id != currentLoginUserId && currentUserIsSuperuser"
|
|
||||||
icon
|
|
||||||
size="small"
|
|
||||||
color="error"
|
|
||||||
variant="text"
|
|
||||||
class="opacity-70 hover:opacity-100 transition-opacity"
|
|
||||||
@click.stop="removeUser"
|
|
||||||
>
|
|
||||||
<VIcon icon="mdi-delete" />
|
|
||||||
</VBtn>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</VCardItem>
|
|
||||||
|
|
||||||
<!-- 权限显示 -->
|
|
||||||
<div v-if="!user.is_superuser && user.permissions" class="d-flex flex-wrap gap-1 px-7 pb-3">
|
|
||||||
<VChip v-if="user.permissions.discovery" size="x-small" color="purple" variant="outlined" label>
|
|
||||||
{{ t('dialog.userAddEdit.permissions.discovery') }}
|
|
||||||
</VChip>
|
|
||||||
<VChip v-if="user.permissions.search" size="x-small" color="blue" variant="outlined" label>
|
|
||||||
{{ t('dialog.userAddEdit.permissions.search') }}
|
|
||||||
</VChip>
|
|
||||||
<VChip v-if="user.permissions.subscribe" size="x-small" color="green" variant="outlined" label>
|
|
||||||
{{ t('dialog.userAddEdit.permissions.subscribe') }}
|
|
||||||
</VChip>
|
|
||||||
<VChip v-if="user.permissions.manage" size="x-small" color="orange" variant="outlined" label>
|
|
||||||
{{ t('dialog.userAddEdit.permissions.manage') }}
|
|
||||||
</VChip>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 独立的邮箱显示 -->
|
<!-- 独立的邮箱显示 -->
|
||||||
<VDivider class="mx-4" />
|
<VDivider class="mx-4" />
|
||||||
|
<div>
|
||||||
|
<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" />
|
||||||
|
<span class="text-body-2 truncate">{{ user.email || t('user.noEmail') }}</span>
|
||||||
|
</VCardText>
|
||||||
|
|
||||||
<VCardText class="d-flex align-center py-2 px-4 text-medium-emphasis">
|
<!-- PC端显示订阅统计信息 -->
|
||||||
<VIcon icon="mdi-email-outline" size="small" color="primary" class="mr-2 opacity-70" />
|
<VCardText v-if="!isMobile" class="px-4 pt-0 pb-4">
|
||||||
<span class="text-body-2 truncate">{{ user.email || t('user.noEmail') }}</span>
|
<div rounded="lg" class="d-flex justify-space-around">
|
||||||
</VCardText>
|
<div class="d-flex align-center gap-3">
|
||||||
|
<VAvatar
|
||||||
<!-- PC端显示订阅统计信息 -->
|
tile
|
||||||
<VCardText v-if="!isMobile" class="px-4 pt-0 pb-4">
|
rounded="lg"
|
||||||
<div rounded="lg" class="d-flex justify-space-around">
|
size="large"
|
||||||
<div class="d-flex align-center gap-3">
|
class="mr-1"
|
||||||
<VAvatar
|
:class="user.is_superuser ? 'admin-stats-container' : 'user-stats-container'"
|
||||||
tile
|
>
|
||||||
rounded="lg"
|
<div :class="['d-flex align-center justify-center rounded-lg w-10 h-10']">
|
||||||
size="large"
|
<VIcon :color="user.is_superuser ? 'warning' : 'primary'" icon="mdi-movie-outline" size="20" />
|
||||||
class="mr-1"
|
</div>
|
||||||
:class="user.is_superuser ? 'admin-stats-container' : 'user-stats-container'"
|
</VAvatar>
|
||||||
>
|
<div class="d-flex flex-column">
|
||||||
<div :class="['d-flex align-center justify-center rounded-lg w-10 h-10']">
|
<span class="text-lg text-medium-emphasis font-weight-bold">{{ movieSubscriptions }}</span>
|
||||||
<VIcon :color="user.is_superuser ? 'warning' : 'primary'" icon="mdi-movie-outline" size="20" />
|
<span class="text-caption text-medium-emphasis">{{ t('user.movieSubscriptions') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center gap-3">
|
||||||
|
<VAvatar
|
||||||
|
tile
|
||||||
|
rounded="lg"
|
||||||
|
size="large"
|
||||||
|
class="mr-1"
|
||||||
|
:class="user.is_superuser ? 'admin-stats-container' : 'user-stats-container'"
|
||||||
|
>
|
||||||
|
<div :class="['d-flex align-center justify-center rounded-lg w-10 h-10']">
|
||||||
|
<VIcon :color="user.is_superuser ? 'warning' : 'primary'" icon="mdi-television-classic" />
|
||||||
|
</div>
|
||||||
|
</VAvatar>
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<span class="text-lg text-medium-emphasis">{{ tvShowSubscriptions }}</span>
|
||||||
|
<span class="text-caption text-medium-emphasis">{{ t('user.tvSubscriptions') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</VAvatar>
|
|
||||||
<div class="d-flex flex-column">
|
|
||||||
<span class="text-lg text-medium-emphasis font-weight-bold">{{ movieSubscriptions }}</span>
|
|
||||||
<span class="text-caption text-medium-emphasis">{{ t('user.movieSubscriptions') }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-center gap-3">
|
</VCardText>
|
||||||
<VAvatar
|
</div>
|
||||||
tile
|
|
||||||
rounded="lg"
|
|
||||||
size="large"
|
|
||||||
class="mr-1"
|
|
||||||
:class="user.is_superuser ? 'admin-stats-container' : 'user-stats-container'"
|
|
||||||
>
|
|
||||||
<div :class="['d-flex align-center justify-center rounded-lg w-10 h-10']">
|
|
||||||
<VIcon :color="user.is_superuser ? 'warning' : 'primary'" icon="mdi-television-classic" />
|
|
||||||
</div>
|
|
||||||
</VAvatar>
|
|
||||||
<div class="d-flex flex-column">
|
|
||||||
<span class="text-lg text-medium-emphasis">{{ tvShowSubscriptions }}</span>
|
|
||||||
<span class="text-caption text-medium-emphasis">{{ t('user.tvSubscriptions') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
</VCard>
|
||||||
|
|
||||||
<!-- 用户编辑弹窗 -->
|
<!-- 用户编辑弹窗 -->
|
||||||
|
|||||||
@@ -19,10 +19,23 @@ const route = useRoute()
|
|||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
// 获取用户权限信息
|
// 获取用户权限信息
|
||||||
const userPermissions = computed(() => ({
|
const userPermissions = computed(() => {
|
||||||
is_superuser: userStore.superUser,
|
// 确保用户已认证且信息已加载
|
||||||
...userStore.permissions,
|
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(() => {
|
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 {
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ function logout() {
|
|||||||
|
|
||||||
// 清除登录状态信息
|
// 清除登录状态信息
|
||||||
authStore.logout()
|
authStore.logout()
|
||||||
|
userStore.reset()
|
||||||
// 重定向到登录页面或其他适当的页面
|
// 重定向到登录页面或其他适当的页面
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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!',
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ export default {
|
|||||||
networkError: '登录失败,请检查网络连接!',
|
networkError: '登录失败,请检查网络连接!',
|
||||||
authFailure: '登录失败,请检查用户名、密码或双重验证是否正确!',
|
authFailure: '登录失败,请检查用户名、密码或双重验证是否正确!',
|
||||||
permissionDenied: '登录失败,您没有权限访问!',
|
permissionDenied: '登录失败,您没有权限访问!',
|
||||||
|
noPermission: '登录失败,您没有任何功能权限,请联系管理员!',
|
||||||
serverError: '登录失败,服务器错误!',
|
serverError: '登录失败,服务器错误!',
|
||||||
loginFailed: '登录失败',
|
loginFailed: '登录失败',
|
||||||
checkCredentials: '请检查用户名、密码或双重验证码是否正确!',
|
checkCredentials: '请检查用户名、密码或双重验证码是否正确!',
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ export default {
|
|||||||
authFailure: '登錄失敗,請檢查用戶名、密碼或雙重驗證是否正確!',
|
authFailure: '登錄失敗,請檢查用戶名、密碼或雙重驗證是否正確!',
|
||||||
permissionDenied: '登錄失敗,您沒有權限訪問!',
|
permissionDenied: '登錄失敗,您沒有權限訪問!',
|
||||||
serverError: '登錄失敗,服務器錯誤!',
|
serverError: '登錄失敗,服務器錯誤!',
|
||||||
|
noPermission: '登錄失敗,您沒有任何功能權限,請聯繫管理員!',
|
||||||
loginFailed: '登錄失敗',
|
loginFailed: '登錄失敗',
|
||||||
checkCredentials: '請檢查用戶名、密碼或雙重驗證碼是否正確!',
|
checkCredentials: '請檢查用戶名、密碼或雙重驗證碼是否正確!',
|
||||||
},
|
},
|
||||||
|
|||||||
+36
-9
@@ -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
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user