mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-28 02:51:56 +08:00
在多个组件中实现权限管理功能
This commit is contained in:
@@ -1,109 +0,0 @@
|
||||
# MoviePilot 用户权限控制系统
|
||||
|
||||
## 概述
|
||||
|
||||
本系统实现了精细化的用户权限控制,将用户权限分为四个主要类别:发现、搜索、订阅、管理。用户登录后根据权限显示不同的菜单范围。
|
||||
|
||||
## 权限类型
|
||||
|
||||
### 1. 发现权限 (discovery)
|
||||
- **功能**: 访问推荐和探索功能
|
||||
- **对应菜单**: 推荐、探索
|
||||
- **默认状态**: 启用
|
||||
|
||||
### 2. 搜索权限 (search)
|
||||
- **功能**: 使用搜索功能和查看搜索结果
|
||||
- **对应菜单**: 搜索结果页面
|
||||
- **对应组件**: 顶部搜索栏
|
||||
- **默认状态**: 启用
|
||||
|
||||
### 3. 订阅权限 (subscribe)
|
||||
- **功能**: 管理电影和电视剧订阅
|
||||
- **对应菜单**: 电影订阅、电视剧订阅、工作流、日历
|
||||
- **默认状态**: 启用
|
||||
|
||||
### 4. 管理权限 (manage)
|
||||
- **功能**: 访问系统管理和设置功能
|
||||
- **对应菜单**: 系统设置、用户管理、站点管理、插件管理、文件管理、媒体整理、下载管理
|
||||
- **默认状态**: 禁用
|
||||
|
||||
## 实现细节
|
||||
|
||||
### 权限存储
|
||||
- 权限数据存储在 `userStore.permissions` 中,格式为 JSON 对象
|
||||
- 后端接口支持 `permissions` 字段的存储和读取
|
||||
|
||||
### 权限检查
|
||||
- 超级用户拥有所有权限,无视权限设置
|
||||
- 普通用户根据具体权限配置进行访问控制
|
||||
- 使用 `hasPermission()` 函数进行权限检查
|
||||
|
||||
### 菜单过滤
|
||||
- 使用 `filterMenusByPermission()` 函数根据用户权限过滤菜单项
|
||||
- 支持国际化菜单标题匹配
|
||||
- 自动隐藏无权限访问的菜单项
|
||||
|
||||
### 组件权限控制
|
||||
- 搜索栏根据搜索权限控制显示/隐藏
|
||||
- 各页面根据对应权限进行访问控制
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 新增用户时设置权限
|
||||
在用户编辑对话框中:
|
||||
- 提供权限设置界面
|
||||
- 支持快速预设(普通用户/管理员)
|
||||
- 可单独切换每个权限
|
||||
|
||||
### 2. 编辑现有用户权限
|
||||
- 管理员可以修改其他用户的权限
|
||||
- 用户无法修改自己的权限(防止权限提升)
|
||||
- 权限变更立即生效
|
||||
|
||||
### 3. 权限预设
|
||||
- **普通用户**: 发现✓ 搜索✓ 订阅✓ 管理✗
|
||||
- **管理员**: 发现✓ 搜索✓ 订阅✓ 管理✓
|
||||
|
||||
## 文件修改清单
|
||||
|
||||
### 新增文件
|
||||
- `src/utils/permission.ts` - 权限管理工具函数
|
||||
|
||||
### 修改文件
|
||||
- `src/components/dialog/UserAddEditDialog.vue` - 添加权限设置界面
|
||||
- `src/stores/user.ts` - 添加权限默认值处理
|
||||
- `src/layouts/components/DefaultLayout.vue` - 根据权限过滤菜单
|
||||
- `src/layouts/components/SearchBar.vue` - 根据搜索权限控制显示
|
||||
- `src/layouts/components/Footer.vue` - 底部菜单权限过滤
|
||||
- `src/pages/appcenter.vue` - 应用中心权限过滤
|
||||
- `src/components/cards/UserCard.vue` - 显示用户权限信息
|
||||
- `src/locales/zh-CN.ts` - 添加权限相关国际化文本
|
||||
|
||||
## 权限检查示例
|
||||
|
||||
```typescript
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import { useUserStore } from '@/stores'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 检查是否有搜索权限
|
||||
const hasSearchPermission = hasPermission({
|
||||
is_superuser: userStore.superUser,
|
||||
...userStore.permissions
|
||||
}, 'search')
|
||||
|
||||
// 检查是否有管理权限
|
||||
const hasManagePermission = hasPermission({
|
||||
is_superuser: userStore.superUser,
|
||||
...userStore.permissions
|
||||
}, 'manage')
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 超级用户始终拥有所有权限
|
||||
2. 权限变更需要重新登录或刷新页面生效
|
||||
3. 权限数据以 JSON 格式存储在后端
|
||||
4. 前端权限检查仅用于界面控制,后端仍需进行权限验证
|
||||
5. 默认权限配置确保新用户有基本的使用权限
|
||||
@@ -15,6 +15,7 @@ import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
|
||||
import SubscribeSeasonDialog from '../dialog/SubscribeSeasonDialog.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { mediaTypeDict } from '@/api/constants'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -481,7 +482,13 @@ onBeforeUnmount(() => {
|
||||
</p>
|
||||
<div v-if="props.media?.collection_id" class="mb-3" @click.stop=""></div>
|
||||
<div v-else class="flex align-center justify-between">
|
||||
<IconBtn icon="mdi-magnify" color="white" @click.stop="clickSearch" />
|
||||
<IconBtn
|
||||
v-if="hasPermission({ is_superuser: userStore.superUser, ...userStore.permissions }, 'search')"
|
||||
icon="mdi-magnify"
|
||||
color="white"
|
||||
@click.stop="clickSearch"
|
||||
/>
|
||||
<VSpacer />
|
||||
<IconBtn icon="mdi-heart" :color="isSubscribed ? 'error' : 'white'" @click.stop="handleSubscribe" />
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
@@ -553,7 +553,7 @@ onMounted(() => {
|
||||
<span>{{ t('dialog.userAddEdit.permissions.title') }}</span>
|
||||
</VDivider>
|
||||
<!-- 权限设置 -->
|
||||
<div v-if="canControl" class="mt-4">
|
||||
<div v-if="canControl">
|
||||
<div class="mb-4">
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
|
||||
@@ -49,7 +49,7 @@ const systemMenus = ref<NavMenu[]>([])
|
||||
const getMenuList = (header: string) => {
|
||||
// 使用国际化菜单
|
||||
const menus = getNavMenus()
|
||||
const filteredMenus = filterMenusByPermission(menus, userPermissions.value, t)
|
||||
const filteredMenus = filterMenusByPermission(menus, userPermissions.value)
|
||||
return filteredMenus.filter((item: NavMenu) => item.header === header)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ const userPermissions = computed(() => ({
|
||||
// 获取导航菜单
|
||||
const navMenus = computed(() => {
|
||||
const allMenus = getNavMenus()
|
||||
return filterMenusByPermission(allMenus, userPermissions.value, t)
|
||||
return filterMenusByPermission(allMenus, userPermissions.value)
|
||||
})
|
||||
|
||||
// 根据当前路径获取匹配的菜单路径
|
||||
|
||||
@@ -373,7 +373,7 @@ onUnmounted(() => {
|
||||
</template>
|
||||
<div>
|
||||
<span class="text-primary text-sm font-medium d-block">
|
||||
{{ superUser ? t('user.admin') : t('user.normalUser') }}
|
||||
{{ superUser ? t('user.admin') : t('user.normal') }}
|
||||
</span>
|
||||
<span class="text-high-emphasis text-lg font-weight-bold">
|
||||
{{ userName }}
|
||||
|
||||
@@ -1538,6 +1538,19 @@ export default {
|
||||
saveUserInfo: 'Save User Information',
|
||||
cannotDeleteCurrentUser: 'Cannot delete current logged-in user',
|
||||
deleteUser: 'Delete User',
|
||||
permissions: {
|
||||
title: 'Permission Settings',
|
||||
presetNormal: 'Normal User',
|
||||
presetAdmin: 'Administrator',
|
||||
discovery: 'Discovery',
|
||||
discoveryDesc: 'Access recommendation and exploration features',
|
||||
search: 'Search',
|
||||
searchDesc: 'Use search functionality and view search results',
|
||||
subscribe: 'Subscribe',
|
||||
subscribeDesc: 'Manage movie and TV show subscriptions',
|
||||
manage: 'Manage',
|
||||
manageDesc: 'Access system management and settings',
|
||||
},
|
||||
},
|
||||
searchBar: {
|
||||
search: 'Search',
|
||||
|
||||
@@ -1520,6 +1520,19 @@ export default {
|
||||
saveUserInfo: '保存用戶信息',
|
||||
cannotDeleteCurrentUser: '不能刪除當前登錄用戶',
|
||||
deleteUser: '刪除用戶',
|
||||
permissions: {
|
||||
title: '權限設置',
|
||||
presetNormal: '普通用戶',
|
||||
presetAdmin: '管理員',
|
||||
discovery: '發現',
|
||||
discoveryDesc: '存取推薦和探索功能',
|
||||
search: '搜索',
|
||||
searchDesc: '使用搜索功能和查看搜索結果',
|
||||
subscribe: '訂閱',
|
||||
subscribeDesc: '管理電影和電視劇訂閱',
|
||||
manage: '管理',
|
||||
manageDesc: '存取系統管理和設置功能',
|
||||
},
|
||||
},
|
||||
searchBar: {
|
||||
search: '搜索',
|
||||
|
||||
@@ -25,7 +25,7 @@ const appGroups = ref<Record<string, NavMenu[]>>({})
|
||||
function categorizeApps() {
|
||||
// 获取所有菜单并根据权限过滤
|
||||
const allMenus = getNavMenus()
|
||||
const filteredMenus = filterMenusByPermission(allMenus, userPermissions.value, t)
|
||||
const filteredMenus = filterMenusByPermission(allMenus, userPermissions.value)
|
||||
const menus = filteredMenus.filter((item: NavMenu) => !item.footer)
|
||||
|
||||
// 按header属性分组
|
||||
|
||||
@@ -12,6 +12,7 @@ export function getNavMenus() {
|
||||
header: t('menu.start'),
|
||||
admin: false,
|
||||
footer: true,
|
||||
permission: 'manage',
|
||||
},
|
||||
{
|
||||
title: t('navItems.searchResult'),
|
||||
@@ -19,6 +20,7 @@ export function getNavMenus() {
|
||||
to: '/resource',
|
||||
header: t('menu.start'),
|
||||
admin: false,
|
||||
permission: 'search',
|
||||
},
|
||||
{
|
||||
title: t('navItems.recommend'),
|
||||
@@ -27,6 +29,7 @@ export function getNavMenus() {
|
||||
header: t('menu.discovery'),
|
||||
admin: false,
|
||||
footer: true,
|
||||
permission: 'discovery',
|
||||
},
|
||||
{
|
||||
title: t('navItems.explore'),
|
||||
@@ -35,6 +38,7 @@ export function getNavMenus() {
|
||||
header: t('menu.discovery'),
|
||||
admin: false,
|
||||
footer: true,
|
||||
permission: 'discovery',
|
||||
},
|
||||
{
|
||||
title: t('navItems.movie'),
|
||||
@@ -44,6 +48,7 @@ export function getNavMenus() {
|
||||
header: t('menu.subscribe'),
|
||||
admin: false,
|
||||
footer: false,
|
||||
permission: 'subscribe',
|
||||
},
|
||||
{
|
||||
title: t('navItems.tv'),
|
||||
@@ -53,6 +58,7 @@ export function getNavMenus() {
|
||||
header: t('menu.subscribe'),
|
||||
admin: false,
|
||||
footer: false,
|
||||
permission: 'subscribe',
|
||||
},
|
||||
{
|
||||
title: t('navItems.workflow'),
|
||||
@@ -62,6 +68,7 @@ export function getNavMenus() {
|
||||
header: t('menu.subscribe'),
|
||||
admin: true,
|
||||
footer: false,
|
||||
permission: 'manage',
|
||||
},
|
||||
{
|
||||
title: t('navItems.calendar'),
|
||||
@@ -70,6 +77,7 @@ export function getNavMenus() {
|
||||
to: '/calendar',
|
||||
header: t('menu.subscribe'),
|
||||
admin: false,
|
||||
permission: 'subscribe',
|
||||
},
|
||||
{
|
||||
title: t('navItems.downloadManager'),
|
||||
@@ -77,6 +85,7 @@ export function getNavMenus() {
|
||||
to: '/downloading',
|
||||
header: t('menu.organize'),
|
||||
admin: false,
|
||||
permission: 'manage',
|
||||
},
|
||||
{
|
||||
title: t('navItems.mediaOrganize'),
|
||||
@@ -84,6 +93,7 @@ export function getNavMenus() {
|
||||
to: '/history',
|
||||
header: t('menu.organize'),
|
||||
admin: true,
|
||||
permission: 'manage',
|
||||
},
|
||||
{
|
||||
title: t('navItems.fileManager'),
|
||||
@@ -91,6 +101,7 @@ export function getNavMenus() {
|
||||
to: '/filemanager',
|
||||
header: t('menu.organize'),
|
||||
admin: true,
|
||||
permission: 'manage',
|
||||
},
|
||||
{
|
||||
title: t('navItems.pluginManager'),
|
||||
@@ -98,6 +109,7 @@ export function getNavMenus() {
|
||||
to: '/plugins',
|
||||
header: t('menu.system'),
|
||||
admin: true,
|
||||
permission: 'manage',
|
||||
},
|
||||
{
|
||||
title: t('navItems.siteManager'),
|
||||
@@ -105,6 +117,7 @@ export function getNavMenus() {
|
||||
to: '/site',
|
||||
header: t('menu.system'),
|
||||
admin: true,
|
||||
permission: 'manage',
|
||||
},
|
||||
{
|
||||
title: t('navItems.userManager'),
|
||||
@@ -112,6 +125,7 @@ export function getNavMenus() {
|
||||
to: '/user',
|
||||
header: t('menu.system'),
|
||||
admin: true,
|
||||
permission: 'admin',
|
||||
},
|
||||
{
|
||||
title: t('navItems.settings'),
|
||||
@@ -119,6 +133,7 @@ export function getNavMenus() {
|
||||
to: '/setting',
|
||||
header: t('menu.system'),
|
||||
admin: true,
|
||||
permission: 'admin',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -45,53 +45,19 @@ export function hasAllPermissions(userPermissions: any, permissionList: (keyof U
|
||||
}
|
||||
|
||||
// 根据权限过滤菜单项
|
||||
export function filterMenusByPermission(menus: any[], userPermissions: any, t?: any): any[] {
|
||||
export function filterMenusByPermission(menus: any[], userPermissions: any): any[] {
|
||||
return menus.filter(menu => {
|
||||
// 如果是超级用户且菜单需要管理员权限,允许访问
|
||||
if (menu.admin && userPermissions?.is_superuser) {
|
||||
// 如果是超级用户,拥有所有权限
|
||||
if (userPermissions?.is_superuser) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 如果菜单不需要管理员权限,检查具体权限
|
||||
if (!menu.admin) {
|
||||
// 根据菜单的header判断需要的权限
|
||||
const header = menu.header
|
||||
|
||||
// 使用国际化键名进行匹配
|
||||
if (header === 'menu.discovery' || (t && header === t('menu.discovery'))) {
|
||||
return hasPermission(userPermissions, 'discovery')
|
||||
}
|
||||
if (header === 'menu.start' || (t && header === t('menu.start'))) {
|
||||
return hasPermission(userPermissions, 'search')
|
||||
}
|
||||
if (header === 'menu.subscribe' || (t && header === t('menu.subscribe'))) {
|
||||
return hasPermission(userPermissions, 'subscribe')
|
||||
}
|
||||
if (
|
||||
header === 'menu.system' ||
|
||||
header === 'menu.organize' ||
|
||||
(t && (header === t('menu.system') || header === t('menu.organize')))
|
||||
) {
|
||||
return hasPermission(userPermissions, 'manage')
|
||||
}
|
||||
|
||||
// 兼容中文菜单头
|
||||
switch (header) {
|
||||
case '发现':
|
||||
return hasPermission(userPermissions, 'discovery')
|
||||
case '开始':
|
||||
return hasPermission(userPermissions, 'search')
|
||||
case '订阅':
|
||||
return hasPermission(userPermissions, 'subscribe')
|
||||
case '系统':
|
||||
case '整理':
|
||||
return hasPermission(userPermissions, 'manage')
|
||||
default:
|
||||
return true
|
||||
}
|
||||
// 如果菜单没有权限要求,默认显示
|
||||
if (!menu.permission) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 需要管理员权限但用户不是超级用户
|
||||
return false
|
||||
// 检查用户是否拥有所需权限
|
||||
return hasPermission(userPermissions, menu.permission)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
|
||||
import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -587,7 +588,10 @@ onBeforeMount(() => {
|
||||
</div>
|
||||
<div class="media-actions">
|
||||
<VBtn
|
||||
v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id"
|
||||
v-if="
|
||||
(mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id) &&
|
||||
hasPermission({ is_superuser: userStore.superUser, ...userStore.permissions }, 'search')
|
||||
"
|
||||
variant="tonal"
|
||||
color="info"
|
||||
class="mb-2"
|
||||
|
||||
Reference in New Issue
Block a user