diff --git a/PERMISSION_SYSTEM.md b/PERMISSION_SYSTEM.md deleted file mode 100644 index 7e66cf1a..00000000 --- a/PERMISSION_SYSTEM.md +++ /dev/null @@ -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. 默认权限配置确保新用户有基本的使用权限 diff --git a/src/components/cards/MediaCard.vue b/src/components/cards/MediaCard.vue index 20568121..a2dd1592 100644 --- a/src/components/cards/MediaCard.vue +++ b/src/components/cards/MediaCard.vue @@ -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(() => {

- + +
diff --git a/src/components/dialog/UserAddEditDialog.vue b/src/components/dialog/UserAddEditDialog.vue index 914d6e91..2973933c 100644 --- a/src/components/dialog/UserAddEditDialog.vue +++ b/src/components/dialog/UserAddEditDialog.vue @@ -553,7 +553,7 @@ onMounted(() => { {{ t('dialog.userAddEdit.permissions.title') }} -
+
([]) 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) } diff --git a/src/layouts/components/Footer.vue b/src/layouts/components/Footer.vue index 41ce7224..0c5e770f 100644 --- a/src/layouts/components/Footer.vue +++ b/src/layouts/components/Footer.vue @@ -27,7 +27,7 @@ const userPermissions = computed(() => ({ // 获取导航菜单 const navMenus = computed(() => { const allMenus = getNavMenus() - return filterMenusByPermission(allMenus, userPermissions.value, t) + return filterMenusByPermission(allMenus, userPermissions.value) }) // 根据当前路径获取匹配的菜单路径 diff --git a/src/layouts/components/UserProfile.vue b/src/layouts/components/UserProfile.vue index ee7736b7..05131f6e 100644 --- a/src/layouts/components/UserProfile.vue +++ b/src/layouts/components/UserProfile.vue @@ -373,7 +373,7 @@ onUnmounted(() => {
- {{ superUser ? t('user.admin') : t('user.normalUser') }} + {{ superUser ? t('user.admin') : t('user.normal') }} {{ userName }} diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index c28ba8f1..999bd115 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -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', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index 648c6f52..db0d9bbf 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -1520,6 +1520,19 @@ export default { saveUserInfo: '保存用戶信息', cannotDeleteCurrentUser: '不能刪除當前登錄用戶', deleteUser: '刪除用戶', + permissions: { + title: '權限設置', + presetNormal: '普通用戶', + presetAdmin: '管理員', + discovery: '發現', + discoveryDesc: '存取推薦和探索功能', + search: '搜索', + searchDesc: '使用搜索功能和查看搜索結果', + subscribe: '訂閱', + subscribeDesc: '管理電影和電視劇訂閱', + manage: '管理', + manageDesc: '存取系統管理和設置功能', + }, }, searchBar: { search: '搜索', diff --git a/src/pages/appcenter.vue b/src/pages/appcenter.vue index 3ed0d067..c2c853b9 100644 --- a/src/pages/appcenter.vue +++ b/src/pages/appcenter.vue @@ -25,7 +25,7 @@ const appGroups = ref>({}) 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属性分组 diff --git a/src/router/i18n-menu.ts b/src/router/i18n-menu.ts index 78046ebd..f385b5d3 100644 --- a/src/router/i18n-menu.ts +++ b/src/router/i18n-menu.ts @@ -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', }, ] } diff --git a/src/utils/permission.ts b/src/utils/permission.ts index b00256cd..0d7f3dbf 100644 --- a/src/utils/permission.ts +++ b/src/utils/permission.ts @@ -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) }) } diff --git a/src/views/discover/MediaDetailView.vue b/src/views/discover/MediaDetailView.vue index 6afc3157..8ea32498 100644 --- a/src/views/discover/MediaDetailView.vue +++ b/src/views/discover/MediaDetailView.vue @@ -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(() => {