mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 15:36:49 +08:00
新增用户权限管理功能
This commit is contained in:
@@ -0,0 +1,109 @@
|
|||||||
|
# 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. 默认权限配置确保新用户有基本的使用权限
|
||||||
@@ -183,6 +183,21 @@ onMounted(() => {
|
|||||||
</VChip>
|
</VChip>
|
||||||
<VChip v-if="user.is_otp" size="x-small" color="info" variant="tonal" label>2FA</VChip>
|
<VChip v-if="user.is_otp" size="x-small" color="info" variant="tonal" label>2FA</VChip>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 权限显示 -->
|
||||||
|
<div v-if="!user.is_superuser && user.permissions" class="d-flex flex-wrap gap-1 mt-1">
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- 移动端订阅数据信息 -->
|
<!-- 移动端订阅数据信息 -->
|
||||||
@@ -294,9 +309,10 @@ onMounted(() => {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
inline-size: 100%;
|
||||||
top: 0;
|
inset-block-start: 0;
|
||||||
padding: 8px 12px;
|
padding-block: 8px;
|
||||||
|
padding-inline: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-header {
|
.admin-header {
|
||||||
@@ -326,10 +342,12 @@ onMounted(() => {
|
|||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
70% {
|
70% {
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
@@ -340,19 +358,21 @@ onMounted(() => {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
animation: float 3s ease-in-out infinite;
|
animation: float 3s ease-in-out infinite;
|
||||||
top: -10px;
|
|
||||||
left: -6px;
|
|
||||||
transform: rotate(-25deg);
|
|
||||||
filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 40%));
|
filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 40%));
|
||||||
|
inset-block-start: -10px;
|
||||||
|
inset-inline-start: -6px;
|
||||||
|
transform: rotate(-25deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes float {
|
@keyframes float {
|
||||||
0% {
|
0% {
|
||||||
transform: rotate(-25deg) translateY(0);
|
transform: rotate(-25deg) translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
transform: rotate(-25deg) translateY(-3px);
|
transform: rotate(-25deg) translateY(-3px);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
transform: rotate(-25deg) translateY(0);
|
transform: rotate(-25deg) translateY(0);
|
||||||
}
|
}
|
||||||
@@ -368,6 +388,7 @@ onMounted(() => {
|
|||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1.2);
|
transform: scale(1.2);
|
||||||
|
|||||||
@@ -65,6 +65,14 @@ interface ExtendedUser extends User {
|
|||||||
nickname?: string
|
nickname?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 权限类型定义
|
||||||
|
interface UserPermissions {
|
||||||
|
discovery: boolean // 发现权限
|
||||||
|
search: boolean // 搜索权限
|
||||||
|
subscribe: boolean // 订阅权限
|
||||||
|
manage: boolean // 管理权限
|
||||||
|
}
|
||||||
|
|
||||||
// 用户编辑表单数据
|
// 用户编辑表单数据
|
||||||
const userForm = ref<ExtendedUser>({
|
const userForm = ref<ExtendedUser>({
|
||||||
id: 0,
|
id: 0,
|
||||||
@@ -75,7 +83,12 @@ const userForm = ref<ExtendedUser>({
|
|||||||
is_superuser: false,
|
is_superuser: false,
|
||||||
avatar: avatar1,
|
avatar: avatar1,
|
||||||
is_otp: false,
|
is_otp: false,
|
||||||
permissions: {},
|
permissions: {
|
||||||
|
discovery: true,
|
||||||
|
search: true,
|
||||||
|
subscribe: true,
|
||||||
|
manage: false,
|
||||||
|
},
|
||||||
settings: {
|
settings: {
|
||||||
wechat_userid: null,
|
wechat_userid: null,
|
||||||
telegram_userid: null,
|
telegram_userid: null,
|
||||||
@@ -86,6 +99,59 @@ const userForm = ref<ExtendedUser>({
|
|||||||
nickname: '', // 昵称字段
|
nickname: '', // 昵称字段
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 权限选项
|
||||||
|
const permissionOptions = [
|
||||||
|
{
|
||||||
|
key: 'discovery',
|
||||||
|
title: t('dialog.userAddEdit.permissions.discovery'),
|
||||||
|
description: t('dialog.userAddEdit.permissions.discoveryDesc'),
|
||||||
|
icon: 'mdi-star-outline',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'search',
|
||||||
|
title: t('dialog.userAddEdit.permissions.search'),
|
||||||
|
description: t('dialog.userAddEdit.permissions.searchDesc'),
|
||||||
|
icon: 'mdi-magnify',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'subscribe',
|
||||||
|
title: t('dialog.userAddEdit.permissions.subscribe'),
|
||||||
|
description: t('dialog.userAddEdit.permissions.subscribeDesc'),
|
||||||
|
icon: 'mdi-rss',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'manage',
|
||||||
|
title: t('dialog.userAddEdit.permissions.manage'),
|
||||||
|
description: t('dialog.userAddEdit.permissions.manageDesc'),
|
||||||
|
icon: 'mdi-cog-outline',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 权限状态计算属性
|
||||||
|
const userPermissions = computed({
|
||||||
|
get: () => {
|
||||||
|
const permissions = userForm.value.permissions as UserPermissions
|
||||||
|
return {
|
||||||
|
discovery: permissions?.discovery ?? true,
|
||||||
|
search: permissions?.search ?? true,
|
||||||
|
subscribe: permissions?.subscribe ?? true,
|
||||||
|
manage: permissions?.manage ?? false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set: (value: UserPermissions) => {
|
||||||
|
userForm.value.permissions = value
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 切换权限状态
|
||||||
|
function togglePermission(key: keyof UserPermissions) {
|
||||||
|
const currentPermissions = userPermissions.value
|
||||||
|
userPermissions.value = {
|
||||||
|
...currentPermissions,
|
||||||
|
[key]: !currentPermissions[key],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 更新头像
|
// 更新头像
|
||||||
function changeAvatar(file: Event) {
|
function changeAvatar(file: Event) {
|
||||||
const fileReader = new FileReader()
|
const fileReader = new FileReader()
|
||||||
@@ -164,6 +230,10 @@ async function addUser() {
|
|||||||
}
|
}
|
||||||
userForm.value.password = newPassword.value
|
userForm.value.password = newPassword.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置权限数据
|
||||||
|
userForm.value.permissions = userPermissions.value
|
||||||
|
|
||||||
isAdding.value = true
|
isAdding.value = true
|
||||||
startNProgress()
|
startNProgress()
|
||||||
try {
|
try {
|
||||||
@@ -216,8 +286,10 @@ async function updateUser() {
|
|||||||
isUpdating.value = true
|
isUpdating.value = true
|
||||||
startNProgress()
|
startNProgress()
|
||||||
try {
|
try {
|
||||||
// 确保昵称保存,使用一个临时变量存储完整数据
|
// 确保昵称和权限保存,使用一个临时变量存储完整数据
|
||||||
const userData = { ...userForm.value }
|
const userData = { ...userForm.value }
|
||||||
|
// 确保权限数据正确传递
|
||||||
|
userData.permissions = userPermissions.value
|
||||||
|
|
||||||
const result: { [key: string]: any } = await api.put('user/', userData)
|
const result: { [key: string]: any } = await api.put('user/', userData)
|
||||||
|
|
||||||
@@ -235,6 +307,10 @@ async function updateUser() {
|
|||||||
if (oldAvatar !== currentAvatar.value && isCurrentUser.value) {
|
if (oldAvatar !== currentAvatar.value && isCurrentUser.value) {
|
||||||
userStore.setAvatar(currentAvatar.value)
|
userStore.setAvatar(currentAvatar.value)
|
||||||
}
|
}
|
||||||
|
// 如果是当前登录用户,更新权限信息
|
||||||
|
if (isCurrentUser.value) {
|
||||||
|
userStore.setPermissions(userPermissions.value)
|
||||||
|
}
|
||||||
emit('save')
|
emit('save')
|
||||||
} else {
|
} else {
|
||||||
if (oldUserName !== currentUserName.value) {
|
if (oldUserName !== currentUserName.value) {
|
||||||
@@ -473,6 +549,67 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
|
<VDivider class="my-10" v-if="canControl">
|
||||||
|
<span>{{ t('dialog.userAddEdit.permissions.title') }}</span>
|
||||||
|
</VDivider>
|
||||||
|
<!-- 权限设置 -->
|
||||||
|
<div v-if="canControl" class="mt-4">
|
||||||
|
<div class="mb-4">
|
||||||
|
<VBtn
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
@click="userPermissions = { discovery: true, search: true, subscribe: true, manage: false }"
|
||||||
|
class="me-2"
|
||||||
|
>
|
||||||
|
{{ t('dialog.userAddEdit.permissions.presetNormal') }}
|
||||||
|
</VBtn>
|
||||||
|
<VBtn
|
||||||
|
variant="outlined"
|
||||||
|
color="warning"
|
||||||
|
size="small"
|
||||||
|
@click="userPermissions = { discovery: true, search: true, subscribe: true, manage: true }"
|
||||||
|
>
|
||||||
|
{{ t('dialog.userAddEdit.permissions.presetAdmin') }}
|
||||||
|
</VBtn>
|
||||||
|
</div>
|
||||||
|
<VRow>
|
||||||
|
<VCol v-for="option in permissionOptions" :key="option.key" cols="12" md="6">
|
||||||
|
<VCard
|
||||||
|
:color="userPermissions[option.key as keyof UserPermissions] ? 'primary' : 'surface'"
|
||||||
|
:variant="userPermissions[option.key as keyof UserPermissions] ? 'tonal' : 'outlined'"
|
||||||
|
class="cursor-pointer transition-all"
|
||||||
|
@click="togglePermission(option.key as keyof UserPermissions)"
|
||||||
|
hover
|
||||||
|
>
|
||||||
|
<VCardText class="d-flex align-center pa-4">
|
||||||
|
<VAvatar
|
||||||
|
:color="userPermissions[option.key as keyof UserPermissions] ? 'primary' : 'surface-variant'"
|
||||||
|
size="40"
|
||||||
|
class="me-3"
|
||||||
|
>
|
||||||
|
<VIcon :icon="option.icon" />
|
||||||
|
</VAvatar>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="text-subtitle-1 font-weight-medium d-flex align-center">
|
||||||
|
{{ option.title }}
|
||||||
|
<VIcon
|
||||||
|
v-if="userPermissions[option.key as keyof UserPermissions]"
|
||||||
|
icon="mdi-check-circle"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
class="ms-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-medium-emphasis">
|
||||||
|
{{ option.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</div>
|
||||||
</VForm>
|
</VForm>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardActions class="pt-3">
|
<VCardActions class="pt-3">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { getNavMenus } from '@/router/i18n-menu'
|
|||||||
import { NavMenu } from '@/@layouts/types'
|
import { NavMenu } from '@/@layouts/types'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { filterMenusByPermission } from '@/utils/permission'
|
||||||
|
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
const appMode = inject('pwaMode')
|
const appMode = inject('pwaMode')
|
||||||
@@ -23,6 +24,12 @@ const userStore = useUserStore()
|
|||||||
// 是否超级用户
|
// 是否超级用户
|
||||||
let superUser = userStore.superUser
|
let superUser = userStore.superUser
|
||||||
|
|
||||||
|
// 获取用户权限信息
|
||||||
|
const userPermissions = computed(() => ({
|
||||||
|
is_superuser: userStore.superUser,
|
||||||
|
...userStore.permissions,
|
||||||
|
}))
|
||||||
|
|
||||||
// 开始菜单项
|
// 开始菜单项
|
||||||
const startMenus = ref<NavMenu[]>([])
|
const startMenus = ref<NavMenu[]>([])
|
||||||
|
|
||||||
@@ -42,7 +49,8 @@ const systemMenus = ref<NavMenu[]>([])
|
|||||||
const getMenuList = (header: string) => {
|
const getMenuList = (header: string) => {
|
||||||
// 使用国际化菜单
|
// 使用国际化菜单
|
||||||
const menus = getNavMenus()
|
const menus = getNavMenus()
|
||||||
return menus.filter((item: NavMenu) => item.header === header && (superUser || !item.admin))
|
const filteredMenus = filterMenusByPermission(menus, userPermissions.value, t)
|
||||||
|
return filteredMenus.filter((item: NavMenu) => item.header === header)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回上一页
|
// 返回上一页
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { getNavMenus } from '@/router/i18n-menu'
|
|||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import { NavMenu } from '@/@layouts/types'
|
import { NavMenu } from '@/@layouts/types'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useUserStore } from '@/stores'
|
||||||
|
import { filterMenusByPermission } from '@/utils/permission'
|
||||||
|
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
const appMode = inject('pwaMode') && display.mdAndDown.value
|
const appMode = inject('pwaMode') && display.mdAndDown.value
|
||||||
@@ -13,8 +15,20 @@ const isEnglish = computed(() => locale.value === 'en-US')
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
// 用户Store
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 获取用户权限信息
|
||||||
|
const userPermissions = computed(() => ({
|
||||||
|
is_superuser: userStore.superUser,
|
||||||
|
...userStore.permissions,
|
||||||
|
}))
|
||||||
|
|
||||||
// 获取导航菜单
|
// 获取导航菜单
|
||||||
const navMenus = computed(() => getNavMenus())
|
const navMenus = computed(() => {
|
||||||
|
const allMenus = getNavMenus()
|
||||||
|
return filterMenusByPermission(allMenus, userPermissions.value, t)
|
||||||
|
})
|
||||||
|
|
||||||
// 根据当前路径获取匹配的菜单路径
|
// 根据当前路径获取匹配的菜单路径
|
||||||
function getMenuPathFromRoute(path: string): string {
|
function getMenuPathFromRoute(path: string): string {
|
||||||
|
|||||||
@@ -3,12 +3,28 @@ import * as Mousetrap from 'mousetrap'
|
|||||||
import SearchBarDialog from '@/components/dialog/SearchBarDialog.vue'
|
import SearchBarDialog from '@/components/dialog/SearchBarDialog.vue'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useUserStore } from '@/stores'
|
||||||
|
import { hasPermission } from '@/utils/permission'
|
||||||
|
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// 用户Store
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const searchDialog = ref(false)
|
const searchDialog = ref(false)
|
||||||
|
|
||||||
|
// 检查是否有搜索权限
|
||||||
|
const hasSearchPermission = computed(() => {
|
||||||
|
return hasPermission(
|
||||||
|
{
|
||||||
|
is_superuser: userStore.superUser,
|
||||||
|
...userStore.permissions,
|
||||||
|
},
|
||||||
|
'search',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
// 注册快捷键
|
// 注册快捷键
|
||||||
Mousetrap.bind(['command+k', 'ctrl+k'], openSearchDialog)
|
Mousetrap.bind(['command+k', 'ctrl+k'], openSearchDialog)
|
||||||
|
|
||||||
@@ -28,7 +44,7 @@ const metaKey = computed(() => (isMac() ? '⌘+K' : 'Ctrl+K'))
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 👉 Search Icon -->
|
<!-- 👉 Search Icon -->
|
||||||
<div class="d-flex align-center cursor-pointer ms-lg-n2" style="user-select: none">
|
<div v-if="hasSearchPermission" class="d-flex align-center cursor-pointer ms-lg-n2" style="user-select: none">
|
||||||
<IconBtn @click="openSearchDialog">
|
<IconBtn @click="openSearchDialog">
|
||||||
<VIcon icon="ri-search-line" />
|
<VIcon icon="ri-search-line" />
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
@@ -38,7 +54,7 @@ const metaKey = computed(() => (isMac() ? '⌘+K' : 'Ctrl+K'))
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 搜索弹窗 -->
|
<!-- 搜索弹窗 -->
|
||||||
<SearchBarDialog v-model="searchDialog" v-if="searchDialog" @close="searchDialog = false" />
|
<SearchBarDialog v-model="searchDialog" v-if="searchDialog && hasSearchPermission" @close="searchDialog = false" />
|
||||||
</template>
|
</template>
|
||||||
<style type="scss" scoped>
|
<style type="scss" scoped>
|
||||||
.meta-key {
|
.meta-key {
|
||||||
|
|||||||
@@ -1518,6 +1518,19 @@ export default {
|
|||||||
saveUserInfo: '保存用户信息',
|
saveUserInfo: '保存用户信息',
|
||||||
cannotDeleteCurrentUser: '不能删除当前登录用户',
|
cannotDeleteCurrentUser: '不能删除当前登录用户',
|
||||||
deleteUser: '删除用户',
|
deleteUser: '删除用户',
|
||||||
|
permissions: {
|
||||||
|
title: '权限设置',
|
||||||
|
presetNormal: '普通用户',
|
||||||
|
presetAdmin: '管理员',
|
||||||
|
discovery: '发现',
|
||||||
|
discoveryDesc: '访问推荐和探索功能',
|
||||||
|
search: '搜索',
|
||||||
|
searchDesc: '使用搜索功能和查看搜索结果',
|
||||||
|
subscribe: '订阅',
|
||||||
|
subscribeDesc: '管理电影和电视剧订阅',
|
||||||
|
manage: '管理',
|
||||||
|
manageDesc: '访问系统管理和设置功能',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
searchBar: {
|
searchBar: {
|
||||||
search: '搜索',
|
search: '搜索',
|
||||||
|
|||||||
+14
-4
@@ -3,20 +3,30 @@ import { NavMenu } from '@/@layouts/types'
|
|||||||
import { getNavMenus } from '@/router/i18n-menu'
|
import { getNavMenus } from '@/router/i18n-menu'
|
||||||
import { useUserStore } from '@/stores'
|
import { useUserStore } from '@/stores'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { filterMenusByPermission } from '@/utils/permission'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
// 从 Store 中获取superuser信息
|
// 从 Store 中获取用户信息
|
||||||
const superUser = useUserStore().superUser
|
const userStore = useUserStore()
|
||||||
|
const superUser = userStore.superUser
|
||||||
|
|
||||||
|
// 获取用户权限信息
|
||||||
|
const userPermissions = computed(() => ({
|
||||||
|
is_superuser: userStore.superUser,
|
||||||
|
...userStore.permissions,
|
||||||
|
}))
|
||||||
|
|
||||||
// 应用分组(以header分组)
|
// 应用分组(以header分组)
|
||||||
const appGroups = ref<Record<string, NavMenu[]>>({})
|
const appGroups = ref<Record<string, NavMenu[]>>({})
|
||||||
|
|
||||||
// 根据header属性对应用进行分类
|
// 根据header属性对应用进行分类
|
||||||
function categorizeApps() {
|
function categorizeApps() {
|
||||||
// 获取可见的菜单项
|
// 获取所有菜单并根据权限过滤
|
||||||
const menus = getNavMenus().filter((item: NavMenu) => (!item.admin || superUser) && !item.footer)
|
const allMenus = getNavMenus()
|
||||||
|
const filteredMenus = filterMenusByPermission(allMenus, userPermissions.value, t)
|
||||||
|
const menus = filteredMenus.filter((item: NavMenu) => !item.footer)
|
||||||
|
|
||||||
// 按header属性分组
|
// 按header属性分组
|
||||||
const groupedMenus: Record<string, NavMenu[]> = {}
|
const groupedMenus: Record<string, NavMenu[]> = {}
|
||||||
|
|||||||
+4
-3
@@ -1,5 +1,6 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type { userState } from '@/stores/types'
|
import type { userState } from '@/stores/types'
|
||||||
|
import { DEFAULT_PERMISSIONS } from '@/utils/permission'
|
||||||
|
|
||||||
export const useUserStore = defineStore('user', {
|
export const useUserStore = defineStore('user', {
|
||||||
state: (): userState => ({
|
state: (): userState => ({
|
||||||
@@ -8,7 +9,7 @@ export const useUserStore = defineStore('user', {
|
|||||||
userName: '',
|
userName: '',
|
||||||
avatar: '',
|
avatar: '',
|
||||||
level: 1,
|
level: 1,
|
||||||
permissions: {},
|
permissions: DEFAULT_PERMISSIONS,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 全局持久化
|
// 全局持久化
|
||||||
@@ -31,7 +32,7 @@ export const useUserStore = defineStore('user', {
|
|||||||
this.level = level
|
this.level = level
|
||||||
},
|
},
|
||||||
setPermissions(permissions: object) {
|
setPermissions(permissions: object) {
|
||||||
this.permissions = permissions
|
this.permissions = { ...DEFAULT_PERMISSIONS, ...permissions }
|
||||||
},
|
},
|
||||||
loginUser(payload: userState) {
|
loginUser(payload: userState) {
|
||||||
this.setSuperUser(payload.superUser)
|
this.setSuperUser(payload.superUser)
|
||||||
@@ -47,7 +48,7 @@ export const useUserStore = defineStore('user', {
|
|||||||
this.setUserName('')
|
this.setUserName('')
|
||||||
this.setAvatar('')
|
this.setAvatar('')
|
||||||
this.setLevel(1)
|
this.setLevel(1)
|
||||||
this.setPermissions({})
|
this.setPermissions(DEFAULT_PERMISSIONS)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// 权限类型定义
|
||||||
|
export interface UserPermissions {
|
||||||
|
discovery: boolean // 发现权限
|
||||||
|
search: boolean // 搜索权限
|
||||||
|
subscribe: boolean // 订阅权限
|
||||||
|
manage: boolean // 管理权限
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认权限配置
|
||||||
|
export const DEFAULT_PERMISSIONS: UserPermissions = {
|
||||||
|
discovery: true,
|
||||||
|
search: true,
|
||||||
|
subscribe: true,
|
||||||
|
manage: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管理员权限配置
|
||||||
|
export const ADMIN_PERMISSIONS: UserPermissions = {
|
||||||
|
discovery: true,
|
||||||
|
search: true,
|
||||||
|
subscribe: true,
|
||||||
|
manage: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 权限检查函数
|
||||||
|
export function hasPermission(userPermissions: any, permission: keyof UserPermissions): boolean {
|
||||||
|
// 如果用户是超级用户,拥有所有权限
|
||||||
|
if (userPermissions?.is_superuser === true) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查具体权限
|
||||||
|
const permissions = userPermissions || {}
|
||||||
|
return permissions[permission] === true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量权限检查
|
||||||
|
export function hasAnyPermission(userPermissions: any, permissionList: (keyof UserPermissions)[]): boolean {
|
||||||
|
return permissionList.some(permission => hasPermission(userPermissions, permission))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有所有权限
|
||||||
|
export function hasAllPermissions(userPermissions: any, permissionList: (keyof UserPermissions)[]): boolean {
|
||||||
|
return permissionList.every(permission => hasPermission(userPermissions, permission))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据权限过滤菜单项
|
||||||
|
export function filterMenusByPermission(menus: any[], userPermissions: any, t?: any): any[] {
|
||||||
|
return menus.filter(menu => {
|
||||||
|
// 如果是超级用户且菜单需要管理员权限,允许访问
|
||||||
|
if (menu.admin && 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 需要管理员权限但用户不是超级用户
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user