mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-07-13 08:22:18 +08:00
Improve frontend dialog and layout behavior
This commit is contained in:
5
src/@layouts/types.d.ts
vendored
5
src/@layouts/types.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
import type { Component, Ref, VNode } from 'vue'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
import type { UserPermissionKey } from '@/utils/permission'
|
||||
import type { UserPermissionFeatureKey, UserPermissionKey } from '@/utils/permission'
|
||||
import { ContentWidth, FooterType, NavbarType } from './enums'
|
||||
|
||||
export interface UserConfig {
|
||||
@@ -124,6 +124,7 @@ export interface NavLink extends NavLinkProps, Partial<AclProperties> {
|
||||
badgeClass?: string
|
||||
disable?: boolean
|
||||
permission?: UserPermissionKey
|
||||
feature?: UserPermissionFeatureKey
|
||||
}
|
||||
|
||||
export interface NavMenuTabItem {
|
||||
@@ -131,6 +132,8 @@ export interface NavMenuTabItem {
|
||||
icon?: string
|
||||
tab: string
|
||||
description?: string
|
||||
permission?: UserPermissionKey
|
||||
feature?: UserPermissionFeatureKey
|
||||
}
|
||||
|
||||
export interface NavMenu extends NavLink {
|
||||
|
||||
@@ -153,6 +153,7 @@ function getMenus(): NavMenu[] {
|
||||
header: item.header,
|
||||
admin: item.admin,
|
||||
permission: item.permission,
|
||||
feature: item.feature,
|
||||
}),
|
||||
)
|
||||
// 设置标签页
|
||||
|
||||
@@ -7,6 +7,14 @@ import { useDisplay } from 'vuetify'
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
USER_PERMISSION_FEATURES,
|
||||
buildDefaultFeaturePermissions,
|
||||
normalizeUserPermissions,
|
||||
type UserPermissionCategoryKey,
|
||||
type UserPermissionFeatureKey,
|
||||
type UserPermissions,
|
||||
} from '@/utils/permission'
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
@@ -65,14 +73,6 @@ interface ExtendedUser extends User {
|
||||
nickname?: string
|
||||
}
|
||||
|
||||
// 权限类型定义
|
||||
interface UserPermissions {
|
||||
discovery: boolean // 发现权限
|
||||
search: boolean // 搜索权限
|
||||
subscribe: boolean // 订阅权限
|
||||
manage: boolean // 管理权限
|
||||
}
|
||||
|
||||
// 用户编辑表单数据
|
||||
const userForm = ref<ExtendedUser>({
|
||||
id: 0,
|
||||
@@ -88,6 +88,7 @@ const userForm = ref<ExtendedUser>({
|
||||
search: true,
|
||||
subscribe: true,
|
||||
manage: false,
|
||||
features: buildDefaultFeaturePermissions(),
|
||||
},
|
||||
settings: {
|
||||
wechat_userid: null,
|
||||
@@ -127,34 +128,115 @@ const permissionOptions = [
|
||||
description: t('dialog.userAddEdit.permissions.manageDesc'),
|
||||
icon: 'mdi-cog-outline',
|
||||
},
|
||||
]
|
||||
] as const
|
||||
|
||||
const activePermissionCategory = ref<UserPermissionCategoryKey>('discovery')
|
||||
|
||||
// 权限状态计算属性
|
||||
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,
|
||||
}
|
||||
return normalizeUserPermissions(userForm.value.permissions as Partial<UserPermissions>)
|
||||
},
|
||||
set: (value: UserPermissions) => {
|
||||
userForm.value.permissions = value
|
||||
},
|
||||
})
|
||||
|
||||
// 切换权限状态
|
||||
function togglePermission(key: keyof UserPermissions) {
|
||||
const permissionFeatureOptions = computed(() =>
|
||||
USER_PERMISSION_FEATURES.map(feature => ({
|
||||
...feature,
|
||||
title: t(feature.titleKey),
|
||||
description: t(feature.descriptionKey),
|
||||
})),
|
||||
)
|
||||
|
||||
const activePermissionOption = computed(() =>
|
||||
permissionOptions.find(option => option.key === activePermissionCategory.value) ?? permissionOptions[0],
|
||||
)
|
||||
|
||||
const activePermissionFeatures = computed(() =>
|
||||
permissionFeatureOptions.value.filter(feature => feature.permission === activePermissionCategory.value),
|
||||
)
|
||||
|
||||
const enabledCategoryCount = computed(
|
||||
() => permissionOptions.filter(option => userPermissions.value[option.key]).length,
|
||||
)
|
||||
|
||||
const enabledFeatureCount = computed(
|
||||
() =>
|
||||
permissionFeatureOptions.value.filter(
|
||||
feature => userPermissions.value[feature.permission] && isFeatureEnabled(feature.key),
|
||||
).length,
|
||||
)
|
||||
|
||||
/** 切换当前查看的权限分类。 */
|
||||
function selectPermissionCategory(key: UserPermissionCategoryKey) {
|
||||
activePermissionCategory.value = key
|
||||
}
|
||||
|
||||
/** 切换权限分类启用状态,并聚焦到该分类的功能列表。 */
|
||||
function togglePermission(key: UserPermissionCategoryKey) {
|
||||
const currentPermissions = userPermissions.value
|
||||
userPermissions.value = {
|
||||
...currentPermissions,
|
||||
[key]: !currentPermissions[key],
|
||||
}
|
||||
activePermissionCategory.value = key
|
||||
}
|
||||
|
||||
// 更新头像
|
||||
/** 判断功能项是否已启用,缺省功能按启用处理以兼容旧权限数据。 */
|
||||
function isFeatureEnabled(key: UserPermissionFeatureKey) {
|
||||
return userPermissions.value.features?.[key] !== false
|
||||
}
|
||||
|
||||
/** 切换单个功能项的启用状态。 */
|
||||
function togglePermissionFeature(key: UserPermissionFeatureKey) {
|
||||
userPermissions.value = {
|
||||
...userPermissions.value,
|
||||
features: {
|
||||
...(userPermissions.value.features ?? {}),
|
||||
[key]: !isFeatureEnabled(key),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** 统计指定分类下已经勾选的功能数量。 */
|
||||
function getEnabledFeatureCount(permission: UserPermissionCategoryKey) {
|
||||
return permissionFeatureOptions.value.filter(
|
||||
feature => feature.permission === permission && isFeatureEnabled(feature.key),
|
||||
).length
|
||||
}
|
||||
|
||||
/** 返回指定分类下的功能总数。 */
|
||||
function getFeatureTotalCount(permission: UserPermissionCategoryKey) {
|
||||
return permissionFeatureOptions.value.filter(feature => feature.permission === permission).length
|
||||
}
|
||||
|
||||
/** 判断指定分类是否处于功能部分勾选状态。 */
|
||||
function isPermissionPartiallySelected(permission: UserPermissionCategoryKey) {
|
||||
const enabledCount = getEnabledFeatureCount(permission)
|
||||
const totalCount = getFeatureTotalCount(permission)
|
||||
return enabledCount > 0 && enabledCount < totalCount
|
||||
}
|
||||
|
||||
/** 批量设置当前分类下的所有功能项。 */
|
||||
function setCategoryFeatures(permission: UserPermissionCategoryKey, enabled: boolean) {
|
||||
const nextFeatures = { ...(userPermissions.value.features ?? {}) }
|
||||
for (const feature of permissionFeatureOptions.value.filter(item => item.permission === permission)) {
|
||||
nextFeatures[feature.key] = enabled
|
||||
}
|
||||
userPermissions.value = {
|
||||
...userPermissions.value,
|
||||
features: nextFeatures,
|
||||
}
|
||||
}
|
||||
|
||||
/** 将当前分类下的功能项恢复为默认全选状态。 */
|
||||
function resetCategoryFeatures(permission: UserPermissionCategoryKey) {
|
||||
setCategoryFeatures(permission, true)
|
||||
}
|
||||
|
||||
/** 校验并读取用户上传的新头像。 */
|
||||
function changeAvatar(file: Event) {
|
||||
const fileReader = new FileReader()
|
||||
const { files } = file.target as HTMLInputElement
|
||||
@@ -182,19 +264,19 @@ function changeAvatar(file: Event) {
|
||||
}
|
||||
}
|
||||
|
||||
// 重置默认头像
|
||||
/** 将头像恢复为系统默认头像。 */
|
||||
function resetDefaultAvatar() {
|
||||
currentAvatar.value = avatar1
|
||||
$toast.success(t('dialog.userAddEdit.resetAvatarSuccess'))
|
||||
}
|
||||
|
||||
// 还原当前头像
|
||||
/** 还原为用户当前已保存的头像。 */
|
||||
function restoreCurrentAvatar() {
|
||||
currentAvatar.value = userForm.value.avatar
|
||||
$toast.success(t('dialog.userAddEdit.restoreAvatarSuccess'))
|
||||
}
|
||||
|
||||
// 查询用户信息
|
||||
/** 从接口查询当前编辑用户的信息并回填表单。 */
|
||||
async function fetchUserInfo() {
|
||||
try {
|
||||
userForm.value = await api.get(`user/${props.username}`)
|
||||
@@ -210,7 +292,7 @@ async function fetchUserInfo() {
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API 新增用户
|
||||
/** 调用接口创建新用户。 */
|
||||
async function addUser() {
|
||||
if (isAdding.value) {
|
||||
$toast.error(t('dialog.userAddEdit.creatingUser', { name: userForm.value.name }))
|
||||
@@ -256,7 +338,7 @@ async function addUser() {
|
||||
isAdding.value = false
|
||||
}
|
||||
|
||||
// 调用API更新用户信息
|
||||
/** 调用接口更新当前用户信息。 */
|
||||
async function updateUser() {
|
||||
if (isUpdating.value) {
|
||||
$toast.error(t('dialog.userAddEdit.updatingUser', { name: userForm.value.name }))
|
||||
@@ -367,7 +449,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog scrollable max-width="40rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog scrollable max-width="64rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem :class="props.oper === 'add' ? 'py-3' : 'py-2'">
|
||||
<template #prepend>
|
||||
@@ -572,43 +654,144 @@ onMounted(() => {
|
||||
<span>{{ t('dialog.userAddEdit.permissions.title') }}</span>
|
||||
</VDivider>
|
||||
<!-- 权限设置 -->
|
||||
<div v-if="canControl">
|
||||
<VRow>
|
||||
<VCol v-for="option in permissionOptions" :key="option.key" cols="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 h-full"
|
||||
@click="togglePermission(option.key as keyof UserPermissions)"
|
||||
hover
|
||||
<div v-if="canControl" class="user-permission-editor">
|
||||
<div class="permission-section-header">
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-medium">
|
||||
{{ t('dialog.userAddEdit.permissions.categoryTitle') }}
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
{{ t('dialog.userAddEdit.permissions.categoryHint') }}
|
||||
</div>
|
||||
</div>
|
||||
<VChip size="small" color="primary" variant="tonal">
|
||||
{{
|
||||
t('dialog.userAddEdit.permissions.summary', {
|
||||
categories: enabledCategoryCount,
|
||||
features: enabledFeatureCount,
|
||||
})
|
||||
}}
|
||||
</VChip>
|
||||
</div>
|
||||
|
||||
<div class="permission-category-grid">
|
||||
<div
|
||||
v-for="option in permissionOptions"
|
||||
:key="option.key"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="permission-category-option"
|
||||
:class="{
|
||||
'is-active': activePermissionCategory === option.key,
|
||||
'is-enabled': userPermissions[option.key],
|
||||
'is-partial': isPermissionPartiallySelected(option.key),
|
||||
}"
|
||||
@click="selectPermissionCategory(option.key)"
|
||||
@keydown.enter="selectPermissionCategory(option.key)"
|
||||
@keydown.space.prevent="selectPermissionCategory(option.key)"
|
||||
>
|
||||
<span class="permission-category-option__icon">
|
||||
<VIcon :icon="option.icon" size="20" />
|
||||
</span>
|
||||
<span class="permission-category-option__body">
|
||||
<span class="permission-category-option__title-row">
|
||||
<span class="permission-category-option__title">{{ option.title }}</span>
|
||||
<span class="permission-category-option__count">
|
||||
{{ getEnabledFeatureCount(option.key) }}/{{ getFeatureTotalCount(option.key) }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="permission-category-option__desc">{{ option.description }}</span>
|
||||
</span>
|
||||
<VBtn
|
||||
:icon="userPermissions[option.key] ? 'mdi-check-circle' : 'mdi-circle-outline'"
|
||||
:color="userPermissions[option.key] ? 'primary' : 'secondary'"
|
||||
variant="text"
|
||||
size="small"
|
||||
class="permission-category-option__toggle"
|
||||
@click.stop="togglePermission(option.key)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="permission-feature-panel">
|
||||
<div class="permission-feature-panel__header">
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-medium">
|
||||
{{ activePermissionOption.title }}{{ t('dialog.userAddEdit.permissions.featureTitleSuffix') }}
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
{{
|
||||
userPermissions[activePermissionCategory]
|
||||
? t('dialog.userAddEdit.permissions.featureHint')
|
||||
: t('dialog.userAddEdit.permissions.disabledCategoryHint')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="permission-feature-panel__actions">
|
||||
<VBtn
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
:disabled="!userPermissions[activePermissionCategory]"
|
||||
@click="setCategoryFeatures(activePermissionCategory, true)"
|
||||
>
|
||||
{{ t('dialog.userAddEdit.permissions.selectAll') }}
|
||||
</VBtn>
|
||||
<VBtn
|
||||
size="small"
|
||||
variant="outlined"
|
||||
:disabled="!userPermissions[activePermissionCategory]"
|
||||
@click="setCategoryFeatures(activePermissionCategory, false)"
|
||||
>
|
||||
{{ t('dialog.userAddEdit.permissions.clear') }}
|
||||
</VBtn>
|
||||
<VBtn
|
||||
size="small"
|
||||
variant="outlined"
|
||||
:disabled="!userPermissions[activePermissionCategory]"
|
||||
@click="resetCategoryFeatures(activePermissionCategory)"
|
||||
>
|
||||
{{ t('dialog.userAddEdit.permissions.default') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="permission-feature-list">
|
||||
<div
|
||||
v-for="feature in activePermissionFeatures"
|
||||
:key="feature.key"
|
||||
role="checkbox"
|
||||
:aria-checked="isFeatureEnabled(feature.key)"
|
||||
:aria-disabled="!userPermissions[activePermissionCategory]"
|
||||
:tabindex="userPermissions[activePermissionCategory] ? 0 : -1"
|
||||
class="permission-feature-item"
|
||||
:class="{
|
||||
'is-enabled': isFeatureEnabled(feature.key),
|
||||
'is-disabled': !userPermissions[activePermissionCategory],
|
||||
}"
|
||||
@click="userPermissions[activePermissionCategory] && togglePermissionFeature(feature.key)"
|
||||
@keydown.enter="userPermissions[activePermissionCategory] && togglePermissionFeature(feature.key)"
|
||||
@keydown.space.prevent="userPermissions[activePermissionCategory] && togglePermissionFeature(feature.key)"
|
||||
>
|
||||
<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>
|
||||
<VCheckboxBtn
|
||||
:model-value="isFeatureEnabled(feature.key)"
|
||||
:disabled="!userPermissions[activePermissionCategory]"
|
||||
color="primary"
|
||||
density="compact"
|
||||
class="permission-feature-item__check"
|
||||
@click.stop
|
||||
@update:model-value="togglePermissionFeature(feature.key)"
|
||||
/>
|
||||
<span class="permission-feature-item__icon">
|
||||
<VIcon :icon="feature.icon" size="20" />
|
||||
</span>
|
||||
<span class="permission-feature-item__body">
|
||||
<span class="permission-feature-item__title">{{ feature.title }}</span>
|
||||
<span class="permission-feature-item__desc">{{ feature.description }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -642,3 +825,273 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-permission-editor {
|
||||
--permission-editor-radius: min(var(--app-surface-radius, 8px), 8px);
|
||||
--permission-editor-border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
|
||||
--permission-editor-muted-border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
--permission-editor-panel-bg: rgb(var(--v-theme-surface));
|
||||
--permission-editor-hover-bg: rgba(var(--v-theme-primary), 0.04);
|
||||
--permission-editor-active-bg: rgba(var(--v-theme-primary), 0.1);
|
||||
--permission-editor-selected-bg: rgba(var(--v-theme-primary), 0.08);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.permission-section-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.permission-category-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(15.5rem, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.permission-category-option,
|
||||
.permission-feature-item {
|
||||
border: var(--permission-editor-muted-border);
|
||||
border-radius: var(--permission-editor-radius);
|
||||
background: var(--permission-editor-panel-bg);
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
cursor: pointer;
|
||||
transition: border-color 0.18s ease, background-color 0.18s ease, opacity 0.18s ease;
|
||||
}
|
||||
|
||||
.permission-category-option {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: flex-start;
|
||||
min-block-size: 5.75rem;
|
||||
padding: 0.875rem;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.permission-category-option:hover,
|
||||
.permission-feature-item:hover:not(.is-disabled) {
|
||||
border-color: rgba(var(--v-theme-primary), 0.36);
|
||||
background: var(--permission-editor-hover-bg);
|
||||
}
|
||||
|
||||
.permission-category-option:focus-visible,
|
||||
.permission-feature-item:focus-visible {
|
||||
outline: 2px solid rgba(var(--v-theme-primary), 0.72);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.permission-category-option.is-active {
|
||||
border-color: rgba(var(--v-theme-primary), 0.72);
|
||||
background: var(--permission-editor-active-bg);
|
||||
}
|
||||
|
||||
.permission-category-option.is-enabled {
|
||||
border-color: rgba(var(--v-theme-primary), 0.34);
|
||||
}
|
||||
|
||||
.permission-category-option.is-partial .permission-category-option__count {
|
||||
color: rgb(var(--v-theme-warning));
|
||||
background: rgba(var(--v-theme-warning), 0.12);
|
||||
}
|
||||
|
||||
.permission-category-option__icon,
|
||||
.permission-feature-item__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
border-radius: var(--app-control-radius, 6px);
|
||||
background: rgba(var(--v-theme-primary), 0.1);
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
.permission-category-option__icon {
|
||||
inline-size: 2.25rem;
|
||||
block-size: 2.25rem;
|
||||
margin-inline-end: 0.75rem;
|
||||
}
|
||||
|
||||
.permission-category-option__body,
|
||||
.permission-feature-item__body {
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.permission-category-option__title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.5rem;
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.permission-category-option__title,
|
||||
.permission-feature-item__title {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.permission-category-option__count {
|
||||
flex: 0 0 auto;
|
||||
border-radius: var(--app-control-radius, 6px);
|
||||
padding: 0.125rem 0.5rem;
|
||||
background: rgba(var(--v-theme-primary), 0.1);
|
||||
color: rgb(var(--v-theme-primary));
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.permission-category-option__desc,
|
||||
.permission-feature-item__desc,
|
||||
.permission-feature-item__scope {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.permission-category-option__desc,
|
||||
.permission-feature-item__desc {
|
||||
display: block;
|
||||
overflow: visible;
|
||||
line-height: 1.35;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.permission-category-option__toggle {
|
||||
margin-inline-start: 0.25rem;
|
||||
}
|
||||
|
||||
.permission-feature-panel {
|
||||
overflow: hidden;
|
||||
border: var(--permission-editor-border);
|
||||
border-radius: var(--permission-editor-radius);
|
||||
background: rgb(var(--v-theme-surface));
|
||||
}
|
||||
|
||||
.permission-feature-panel__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border-block-end: var(--permission-editor-muted-border);
|
||||
}
|
||||
|
||||
.permission-feature-panel__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.permission-feature-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.625rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.permission-feature-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto minmax(0, 1fr);
|
||||
align-items: flex-start;
|
||||
min-block-size: 4rem;
|
||||
padding: 0.625rem 0.875rem;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.permission-feature-item.is-disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.permission-feature-item.is-disabled .permission-feature-item__icon {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.permission-feature-item.is-disabled .permission-feature-item__title,
|
||||
.permission-feature-item.is-disabled .permission-feature-item__desc {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
}
|
||||
|
||||
.permission-feature-item.is-enabled {
|
||||
background: var(--permission-editor-selected-bg);
|
||||
}
|
||||
|
||||
.permission-feature-item__check {
|
||||
margin-inline-end: 0.5rem;
|
||||
}
|
||||
|
||||
.permission-feature-item__icon {
|
||||
inline-size: 2rem;
|
||||
block-size: 2rem;
|
||||
margin-inline-end: 0.75rem;
|
||||
}
|
||||
|
||||
.permission-feature-item__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.permission-feature-item__scope {
|
||||
max-inline-size: 8rem;
|
||||
margin-inline-start: 1rem;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
|
||||
}
|
||||
|
||||
@media (width <= 960px) {
|
||||
.permission-category-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.permission-feature-list {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.permission-section-header,
|
||||
.permission-feature-panel__header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.permission-feature-panel__actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.user-permission-editor {
|
||||
gap: 0.875rem;
|
||||
}
|
||||
|
||||
.permission-category-option {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
min-block-size: 3.75rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.permission-category-option__icon,
|
||||
.permission-category-option__desc {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.permission-category-option__toggle {
|
||||
margin-inline-start: 0.5rem;
|
||||
}
|
||||
|
||||
.permission-feature-item {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.permission-feature-item__icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -722,7 +722,7 @@ onMounted(() => {
|
||||
.filter-buttons-grid {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.filter-btn-mobile {
|
||||
@@ -732,10 +732,23 @@ onMounted(() => {
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
border-radius: 8px;
|
||||
block-size: auto;
|
||||
min-block-size: 48px;
|
||||
block-size: 56px;
|
||||
min-block-size: 56px;
|
||||
min-inline-size: 0;
|
||||
padding-block: 4px;
|
||||
padding-inline: 0;
|
||||
padding-inline: 4px;
|
||||
}
|
||||
|
||||
.filter-btn-mobile :deep(.v-btn__content) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
inline-size: 100%;
|
||||
max-inline-size: 100%;
|
||||
min-inline-size: 0;
|
||||
line-height: 1.15;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.filter-icon {
|
||||
@@ -744,20 +757,20 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.15;
|
||||
max-inline-size: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
text-align: center;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.filter-buttons-grid {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
overflow: hidden;
|
||||
max-inline-size: 100%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
type ComputedRef,
|
||||
type Ref,
|
||||
} from 'vue'
|
||||
import type { UserPermissionKey } from '@/utils/permission'
|
||||
import type { UserPermissionFeatureKey, UserPermissionKey } from '@/utils/permission'
|
||||
|
||||
// 声明全局变量类型
|
||||
declare global {
|
||||
@@ -31,6 +31,7 @@ export interface DynamicButtonMenuItem {
|
||||
icon?: string
|
||||
color?: string
|
||||
permission?: UserPermissionKey
|
||||
feature?: UserPermissionFeatureKey
|
||||
disabled?: boolean
|
||||
action: () => void
|
||||
}
|
||||
@@ -61,11 +62,12 @@ export function useDynamicButton(options: {
|
||||
onClick?: () => void
|
||||
menuItems?: MaybeRefValue<DynamicButtonMenuItem[] | undefined>
|
||||
permission?: UserPermissionKey
|
||||
feature?: UserPermissionFeatureKey
|
||||
show?: MaybeRefValue<boolean>
|
||||
autoRegister?: boolean // 是否自动注册,默认为true
|
||||
}) {
|
||||
// 提取配置
|
||||
const { icon, onClick, menuItems, permission, show, autoRegister = true } = options
|
||||
const { icon, onClick, menuItems, permission, feature, show, autoRegister = true } = options
|
||||
|
||||
// 动态按钮相关
|
||||
const registerDynamicButton = inject<((button: any) => void) | null>('registerDynamicButton', null)
|
||||
@@ -79,6 +81,7 @@ export function useDynamicButton(options: {
|
||||
const resolvedShow = computed(() => resolveMaybeRef(show, true))
|
||||
const resolvedMenuItems = computed(() => resolveMaybeRef(menuItems))
|
||||
|
||||
/** 根据当前响应式配置生成可注册的动态按钮对象。 */
|
||||
function buildDynamicButton() {
|
||||
const buttonMenuItems = resolvedMenuItems.value
|
||||
|
||||
@@ -86,12 +89,13 @@ export function useDynamicButton(options: {
|
||||
icon: resolvedIcon.value,
|
||||
action: onClick || (() => {}),
|
||||
permission,
|
||||
feature,
|
||||
show: resolvedShow.value,
|
||||
menuItems: buttonMenuItems && buttonMenuItems.length > 0 ? buttonMenuItems : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// 注册动态按钮
|
||||
/** 在当前页面激活时注册动态按钮。 */
|
||||
function setupDynamicButton() {
|
||||
if (!componentActive.value) return
|
||||
|
||||
@@ -133,7 +137,7 @@ export function useDynamicButton(options: {
|
||||
})
|
||||
}
|
||||
|
||||
// 取消注册动态按钮
|
||||
/** 清理当前页面注册过的动态按钮。 */
|
||||
function cleanupDynamicButton() {
|
||||
if (unregisterDynamicButton && dynamicButtonRegistered.value) {
|
||||
unregisterDynamicButton()
|
||||
@@ -147,7 +151,7 @@ export function useDynamicButton(options: {
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露方法:手动打开对话框
|
||||
/** 手动触发动态按钮主操作。 */
|
||||
function openDialog() {
|
||||
onClick?.()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import { useTabStateRestore } from '@/composables/useStateRestore'
|
||||
import type { UserPermissionKey } from '@/utils/permission'
|
||||
import type { UserPermissionFeatureKey, UserPermissionKey } from '@/utils/permission'
|
||||
|
||||
// 动态标签页相关类型
|
||||
interface DynamicHeaderTabButton {
|
||||
@@ -11,6 +11,7 @@ interface DynamicHeaderTabButton {
|
||||
class?: string
|
||||
action?: () => void
|
||||
permission?: UserPermissionKey
|
||||
feature?: UserPermissionFeatureKey
|
||||
show?: boolean | ComputedRef<boolean>
|
||||
loading?: boolean | ComputedRef<boolean>
|
||||
dataAttr?: string // 用于VMenu定位的data属性
|
||||
@@ -20,6 +21,8 @@ interface DynamicHeaderTabItem {
|
||||
title: string
|
||||
icon?: string
|
||||
tab: string
|
||||
permission?: UserPermissionKey
|
||||
feature?: UserPermissionFeatureKey
|
||||
}
|
||||
|
||||
interface DynamicHeaderTabConfig {
|
||||
@@ -30,6 +33,7 @@ interface DynamicHeaderTabConfig {
|
||||
onUpdateModelValue?: (value: string) => void
|
||||
}
|
||||
|
||||
/** 提供页面动态头部标签的注册、状态恢复和注销能力。 */
|
||||
export function useDynamicHeaderTab() {
|
||||
const route = useRoute()
|
||||
|
||||
@@ -37,7 +41,7 @@ export function useDynamicHeaderTab() {
|
||||
const registerDynamicHeaderTab = inject<(tab: DynamicHeaderTabConfig) => void>('registerDynamicHeaderTab')
|
||||
const unregisterDynamicHeaderTab = inject<() => void>('unregisterDynamicHeaderTab')
|
||||
|
||||
// 注册动态标签页
|
||||
/** 注册当前页面的动态头部标签配置。 */
|
||||
const registerHeaderTab = (config: {
|
||||
items: DynamicHeaderTabItem[] | ComputedRef<DynamicHeaderTabItem[]> | Ref<DynamicHeaderTabItem[]>
|
||||
modelValue: Ref<string>
|
||||
@@ -184,7 +188,7 @@ export function useDynamicHeaderTab() {
|
||||
})
|
||||
}
|
||||
|
||||
// 取消注册
|
||||
/** 取消当前页面注册的动态头部标签。 */
|
||||
const unregisterHeaderTab = () => {
|
||||
if (unregisterDynamicHeaderTab) {
|
||||
unregisterDynamicHeaderTab()
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
filterMenusByPermission,
|
||||
hasItemPermission,
|
||||
hasPermission,
|
||||
type UserPermissionFeatureKey,
|
||||
type UserPermissionKey,
|
||||
} from '@/utils/permission'
|
||||
import { usePullDownGesture } from '@/composables/usePullDownGesture'
|
||||
@@ -119,6 +120,7 @@ interface DynamicHeaderTabButton {
|
||||
class?: string
|
||||
action?: () => void
|
||||
permission?: UserPermissionKey
|
||||
feature?: UserPermissionFeatureKey
|
||||
show?: boolean | ComputedRef<boolean>
|
||||
loading?: boolean | ComputedRef<boolean>
|
||||
dataAttr?: string
|
||||
@@ -128,6 +130,8 @@ interface DynamicHeaderTabItem {
|
||||
title: string
|
||||
icon?: string
|
||||
tab: string
|
||||
permission?: UserPermissionKey
|
||||
feature?: UserPermissionFeatureKey
|
||||
}
|
||||
|
||||
interface DynamicHeaderTab {
|
||||
@@ -160,6 +164,8 @@ const unregisterDynamicHeaderTab = () => {
|
||||
/** 更新当前动态标签页选中值,并通知注册页面同步状态。 */
|
||||
const handleTabChange = (newValue: string) => {
|
||||
if (dynamicHeaderTab.value) {
|
||||
if (!visibleDynamicHeaderTabItems.value.some(item => item.tab === newValue)) return
|
||||
|
||||
dynamicHeaderTab.value.modelValue = newValue
|
||||
// 通知注册的页面更新值
|
||||
if (dynamicHeaderTab.value.onUpdateModelValue) {
|
||||
@@ -193,13 +199,15 @@ watch(
|
||||
{ immediate: false },
|
||||
)
|
||||
|
||||
// 当前路由是否注册了动态标签页。
|
||||
const hasDynamicHeaderTab = computed(() => {
|
||||
return (
|
||||
dynamicHeaderTab.value && dynamicHeaderTab.value.items.length > 0 && dynamicHeaderTab.value.routePath === route.path
|
||||
)
|
||||
const visibleDynamicHeaderTabItems = computed(() => {
|
||||
if (!dynamicHeaderTab.value || dynamicHeaderTab.value.routePath !== route.path) return []
|
||||
|
||||
return filterItemsByPermission(dynamicHeaderTab.value.items, userPermissions.value)
|
||||
})
|
||||
|
||||
// 当前路由是否注册了动态标签页。
|
||||
const hasDynamicHeaderTab = computed(() => visibleDynamicHeaderTabItems.value.length > 0)
|
||||
|
||||
// 水平布局下动态标签页会并入顶部导航三级菜单,不再额外显示标签页栏。
|
||||
const showDynamicHeaderTab = computed(() => hasDynamicHeaderTab.value && !showHorizontalThemeNav.value)
|
||||
|
||||
@@ -384,10 +392,10 @@ function getHorizontalNavTabs(item: NavMenu): DynamicHeaderTabItem[] {
|
||||
const targetPath = normalizeMenuPath(item.to)
|
||||
|
||||
if (targetPath && isHorizontalNavActive(item) && hasDynamicHeaderTab.value) {
|
||||
return dynamicHeaderTab.value?.items ?? []
|
||||
return visibleDynamicHeaderTabItems.value
|
||||
}
|
||||
|
||||
return item.tabs ?? []
|
||||
return filterItemsByPermission(item.tabs ?? [], userPermissions.value)
|
||||
}
|
||||
|
||||
/** 在目标页面注册动态标签后应用此前暂存的标签切换。 */
|
||||
@@ -397,7 +405,7 @@ function applyPendingHorizontalTab() {
|
||||
const pending = pendingHorizontalTab.value
|
||||
if (normalizeMenuPath(route.path) !== pending.path) return
|
||||
|
||||
const tabExists = dynamicHeaderTab.value?.items.some(item => item.tab === pending.tab)
|
||||
const tabExists = visibleDynamicHeaderTabItems.value.some(item => item.tab === pending.tab)
|
||||
if (!tabExists) return
|
||||
|
||||
handleTabChange(pending.tab)
|
||||
@@ -673,7 +681,7 @@ onMounted(async () => {
|
||||
<template #dynamic-header-tab>
|
||||
<div v-if="showDynamicHeaderTab">
|
||||
<HeaderTab
|
||||
:items="dynamicHeaderTab!.items"
|
||||
:items="visibleDynamicHeaderTabItems"
|
||||
:model-value="dynamicHeaderTab!.modelValue"
|
||||
@update:model-value="handleTabChange"
|
||||
>
|
||||
|
||||
@@ -119,6 +119,7 @@ interface DynamicButton {
|
||||
icon: string
|
||||
action: () => void
|
||||
permission?: DynamicButtonMenuItem['permission']
|
||||
feature?: DynamicButtonMenuItem['feature']
|
||||
show: boolean
|
||||
routePath?: string // 添加路径属性,用于标识哪个路由注册的
|
||||
menuItems?: DynamicButtonMenuItem[]
|
||||
|
||||
@@ -2487,6 +2487,44 @@ export default {
|
||||
subscribeDesc: 'Manage movie and TV show subscriptions',
|
||||
manage: 'Manage',
|
||||
manageDesc: 'Access download management and site management etc.',
|
||||
categoryTitle: 'Permission Categories',
|
||||
categoryHint: 'Enable a category first, then choose the concrete features available in it',
|
||||
summary: '{categories} categories / {features} features enabled',
|
||||
featureTitleSuffix: ' Features',
|
||||
featureHint: 'Disabled features are hidden from navigation, app center, and direct route access',
|
||||
disabledCategoryHint: 'This category is disabled, so its features are not accessible',
|
||||
selectAll: 'Select All',
|
||||
clear: 'Clear',
|
||||
default: 'Default',
|
||||
featureScope: 'Feature scope',
|
||||
features: {
|
||||
recommend: 'Recommendations',
|
||||
explore: 'Explore',
|
||||
resourceSearch: 'Resource Search',
|
||||
movieSubscribe: 'Movie Subscriptions',
|
||||
tvSubscribe: 'TV Subscriptions',
|
||||
calendar: 'Subscription Calendar',
|
||||
subscribeShare: 'Subscription Share',
|
||||
workflow: 'Workflow',
|
||||
downloading: 'Download Manager',
|
||||
history: 'Media Organize',
|
||||
fileManager: 'File Manager',
|
||||
site: 'Site Manager',
|
||||
},
|
||||
featureDescriptions: {
|
||||
recommend: 'Recommendation page and ranking content',
|
||||
explore: 'Explore page, discovery sources, and media detail browsing',
|
||||
resourceSearch: 'Site resource search and result page',
|
||||
movieSubscribe: 'Movie subscription list and popular movies',
|
||||
tvSubscribe: 'TV subscription list, popular shows, and share tab',
|
||||
calendar: 'Subscription schedule and calendar view',
|
||||
subscribeShare: 'Subscription share list access',
|
||||
workflow: 'Workflow list and shared workflows',
|
||||
downloading: 'Current download task management',
|
||||
history: 'Organize history and import records',
|
||||
fileManager: 'File browsing and file management',
|
||||
site: 'Site list, status, and maintenance',
|
||||
},
|
||||
},
|
||||
},
|
||||
searchBar: {
|
||||
|
||||
@@ -2442,6 +2442,44 @@ export default {
|
||||
subscribeDesc: '管理电影和电视剧订阅',
|
||||
manage: '管理',
|
||||
manageDesc: '访问下载管理和站点管理等功能',
|
||||
categoryTitle: '权限分类',
|
||||
categoryHint: '先启用权限分类,再选择该分类下允许访问的具体功能',
|
||||
summary: '已启用 {categories} 类 / {features} 项功能',
|
||||
featureTitleSuffix: '功能',
|
||||
featureHint: '取消勾选后,该功能会从导航、应用中心和路由访问中隐藏',
|
||||
disabledCategoryHint: '当前分类未启用,功能项将随分类一起不可访问',
|
||||
selectAll: '全选',
|
||||
clear: '清空',
|
||||
default: '默认',
|
||||
featureScope: '功能范围',
|
||||
features: {
|
||||
recommend: '推荐',
|
||||
explore: '探索',
|
||||
resourceSearch: '资源搜索',
|
||||
movieSubscribe: '电影订阅',
|
||||
tvSubscribe: '电视剧订阅',
|
||||
calendar: '订阅日历',
|
||||
subscribeShare: '订阅分享',
|
||||
workflow: '工作流',
|
||||
downloading: '下载管理',
|
||||
history: '媒体整理',
|
||||
fileManager: '文件管理',
|
||||
site: '站点管理',
|
||||
},
|
||||
featureDescriptions: {
|
||||
recommend: '推荐页与榜单内容入口',
|
||||
explore: '探索页、媒体发现与详情浏览',
|
||||
resourceSearch: '站点资源搜索与资源结果页',
|
||||
movieSubscribe: '电影订阅列表和热门电影',
|
||||
tvSubscribe: '电视剧订阅列表、热门剧集和分享标签',
|
||||
calendar: '订阅排期与日历视图',
|
||||
subscribeShare: '订阅分享列表访问',
|
||||
workflow: '工作流列表与分享入口',
|
||||
downloading: '当前下载任务管理',
|
||||
history: '整理历史与入库记录',
|
||||
fileManager: '文件浏览和文件管理',
|
||||
site: '站点列表、站点状态和站点维护',
|
||||
},
|
||||
},
|
||||
},
|
||||
searchBar: {
|
||||
|
||||
@@ -2441,6 +2441,44 @@ export default {
|
||||
subscribeDesc: '管理電影和電視劇訂閱',
|
||||
manage: '管理',
|
||||
manageDesc: '存取下載管理和站點管理等功能',
|
||||
categoryTitle: '權限分類',
|
||||
categoryHint: '先啟用權限分類,再選擇該分類下允許存取的具體功能',
|
||||
summary: '已啟用 {categories} 類 / {features} 項功能',
|
||||
featureTitleSuffix: '功能',
|
||||
featureHint: '取消勾選後,該功能會從導覽、應用中心和路由存取中隱藏',
|
||||
disabledCategoryHint: '目前分類未啟用,功能項將隨分類一起不可存取',
|
||||
selectAll: '全選',
|
||||
clear: '清空',
|
||||
default: '預設',
|
||||
featureScope: '功能範圍',
|
||||
features: {
|
||||
recommend: '推薦',
|
||||
explore: '探索',
|
||||
resourceSearch: '資源搜索',
|
||||
movieSubscribe: '電影訂閱',
|
||||
tvSubscribe: '電視劇訂閱',
|
||||
calendar: '訂閱日曆',
|
||||
subscribeShare: '訂閱分享',
|
||||
workflow: '工作流',
|
||||
downloading: '下載管理',
|
||||
history: '媒體整理',
|
||||
fileManager: '文件管理',
|
||||
site: '站點管理',
|
||||
},
|
||||
featureDescriptions: {
|
||||
recommend: '推薦頁與榜單內容入口',
|
||||
explore: '探索頁、媒體發現與詳情瀏覽',
|
||||
resourceSearch: '站點資源搜索與資源結果頁',
|
||||
movieSubscribe: '電影訂閱列表和熱門電影',
|
||||
tvSubscribe: '電視劇訂閱列表、熱門劇集和分享標籤',
|
||||
calendar: '訂閱排期與日曆視圖',
|
||||
subscribeShare: '訂閱分享列表存取',
|
||||
workflow: '工作流列表與分享入口',
|
||||
downloading: '目前下載任務管理',
|
||||
history: '整理歷史與入庫記錄',
|
||||
fileManager: '文件瀏覽和文件管理',
|
||||
site: '站點列表、站點狀態和站點維護',
|
||||
},
|
||||
},
|
||||
},
|
||||
searchBar: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useGlobalSettingsStore } from '@/stores'
|
||||
import type { NavMenu, NavMenuTabItem } from '@/@layouts/types'
|
||||
import type { Composer } from 'vue-i18n'
|
||||
import { PERMISSION_FEATURE } from '@/utils/permission'
|
||||
|
||||
/** 构建当前语言与全局模式对应的主导航菜单。 */
|
||||
export function getNavMenus(t: Composer['t']): NavMenu[] {
|
||||
@@ -28,6 +29,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
||||
header: t('menu.start'),
|
||||
admin: false,
|
||||
permission: 'search',
|
||||
feature: PERMISSION_FEATURE.SEARCH_RESOURCE,
|
||||
},
|
||||
{
|
||||
title: t('navItems.recommend'),
|
||||
@@ -38,6 +40,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
||||
admin: false,
|
||||
footer: true,
|
||||
permission: 'discovery',
|
||||
feature: PERMISSION_FEATURE.DISCOVERY_RECOMMEND,
|
||||
tabs: getRecommendTabs(t),
|
||||
},
|
||||
{
|
||||
@@ -49,6 +52,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
||||
admin: false,
|
||||
footer: true,
|
||||
permission: 'discovery',
|
||||
feature: PERMISSION_FEATURE.DISCOVERY_EXPLORE,
|
||||
tabs: getDiscoverTabs(t),
|
||||
},
|
||||
{
|
||||
@@ -61,6 +65,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
||||
admin: false,
|
||||
footer: false,
|
||||
permission: 'subscribe',
|
||||
feature: PERMISSION_FEATURE.SUBSCRIBE_MOVIE,
|
||||
tabs: getSubscribeMovieTabs(t),
|
||||
},
|
||||
{
|
||||
@@ -73,6 +78,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
||||
admin: false,
|
||||
footer: false,
|
||||
permission: 'subscribe',
|
||||
feature: PERMISSION_FEATURE.SUBSCRIBE_TV,
|
||||
tabs: getSubscribeTvTabs(t),
|
||||
},
|
||||
{
|
||||
@@ -85,6 +91,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
||||
admin: true,
|
||||
footer: false,
|
||||
permission: 'manage',
|
||||
feature: PERMISSION_FEATURE.MANAGE_WORKFLOW,
|
||||
tabs: getWorkflowTabs(t),
|
||||
},
|
||||
{
|
||||
@@ -96,6 +103,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
||||
header: t('menu.subscribe'),
|
||||
admin: false,
|
||||
permission: 'subscribe',
|
||||
feature: PERMISSION_FEATURE.SUBSCRIBE_CALENDAR,
|
||||
},
|
||||
{
|
||||
title: t('navItems.downloadManager'),
|
||||
@@ -105,6 +113,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
||||
header: t('menu.organize'),
|
||||
admin: false,
|
||||
permission: 'manage',
|
||||
feature: PERMISSION_FEATURE.MANAGE_DOWNLOADING,
|
||||
},
|
||||
{
|
||||
title: t('navItems.mediaOrganize'),
|
||||
@@ -114,6 +123,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
||||
header: t('menu.organize'),
|
||||
admin: true,
|
||||
permission: 'manage',
|
||||
feature: PERMISSION_FEATURE.MANAGE_HISTORY,
|
||||
},
|
||||
{
|
||||
title: t('navItems.fileManager'),
|
||||
@@ -123,6 +133,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
||||
header: t('menu.organize'),
|
||||
admin: true,
|
||||
permission: 'manage',
|
||||
feature: PERMISSION_FEATURE.MANAGE_FILEMANAGER,
|
||||
},
|
||||
{
|
||||
title: t('navItems.pluginManager'),
|
||||
@@ -142,6 +153,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
||||
header: t('menu.system'),
|
||||
admin: true,
|
||||
permission: 'manage',
|
||||
feature: PERMISSION_FEATURE.MANAGE_SITE,
|
||||
},
|
||||
{
|
||||
title: t('navItems.userManager'),
|
||||
|
||||
@@ -2,7 +2,14 @@ import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import { configureNProgress } from '@/api/nprogress'
|
||||
import { useAuthStore, usePluginSidebarNavStore, useUserStore } from '@/stores'
|
||||
import { setNavigatingState as setRequestNavigatingState } from '@/utils/requestOptimizer'
|
||||
import { buildUserPermissionContext, hasPermission, type UserPermissionKey } from '@/utils/permission'
|
||||
import {
|
||||
buildPluginPermissionFeatureKey,
|
||||
buildUserPermissionContext,
|
||||
hasItemPermission,
|
||||
PERMISSION_FEATURE,
|
||||
type PermissionProtectedItem,
|
||||
type UserPermissionKey,
|
||||
} from '@/utils/permission'
|
||||
|
||||
// Nprogress
|
||||
configureNProgress()
|
||||
@@ -45,6 +52,7 @@ const router = createRouter({
|
||||
keepAlive: true,
|
||||
requiresAuth: true,
|
||||
permission: 'discovery',
|
||||
feature: PERMISSION_FEATURE.DISCOVERY_RECOMMEND,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -54,6 +62,7 @@ const router = createRouter({
|
||||
keepAlive: true,
|
||||
requiresAuth: true,
|
||||
permission: 'discovery',
|
||||
feature: PERMISSION_FEATURE.DISCOVERY_EXPLORE,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -63,6 +72,7 @@ const router = createRouter({
|
||||
keepAlive: true,
|
||||
requiresAuth: true,
|
||||
permission: 'search',
|
||||
feature: PERMISSION_FEATURE.SEARCH_RESOURCE,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -73,6 +83,7 @@ const router = createRouter({
|
||||
keepAliveKey: 'subscribe-movie',
|
||||
requiresAuth: true,
|
||||
permission: 'subscribe',
|
||||
feature: PERMISSION_FEATURE.SUBSCRIBE_MOVIE,
|
||||
subType: '电影',
|
||||
},
|
||||
},
|
||||
@@ -84,6 +95,7 @@ const router = createRouter({
|
||||
keepAliveKey: 'subscribe-tv',
|
||||
requiresAuth: true,
|
||||
permission: 'subscribe',
|
||||
feature: PERMISSION_FEATURE.SUBSCRIBE_TV,
|
||||
subType: '电视剧',
|
||||
},
|
||||
},
|
||||
@@ -93,6 +105,7 @@ const router = createRouter({
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
permission: 'subscribe',
|
||||
feature: PERMISSION_FEATURE.SUBSCRIBE_SHARE,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -102,6 +115,7 @@ const router = createRouter({
|
||||
keepAlive: true,
|
||||
requiresAuth: true,
|
||||
permission: 'manage',
|
||||
feature: PERMISSION_FEATURE.MANAGE_WORKFLOW,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -111,6 +125,7 @@ const router = createRouter({
|
||||
keepAlive: true,
|
||||
requiresAuth: true,
|
||||
permission: 'subscribe',
|
||||
feature: PERMISSION_FEATURE.SUBSCRIBE_CALENDAR,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -120,6 +135,7 @@ const router = createRouter({
|
||||
keepAlive: true,
|
||||
requiresAuth: true,
|
||||
permission: 'manage',
|
||||
feature: PERMISSION_FEATURE.MANAGE_DOWNLOADING,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -129,6 +145,7 @@ const router = createRouter({
|
||||
keepAlive: true,
|
||||
requiresAuth: true,
|
||||
permission: 'manage',
|
||||
feature: PERMISSION_FEATURE.MANAGE_HISTORY,
|
||||
hideFooter: true,
|
||||
},
|
||||
},
|
||||
@@ -139,6 +156,7 @@ const router = createRouter({
|
||||
keepAlive: true,
|
||||
requiresAuth: true,
|
||||
permission: 'manage',
|
||||
feature: PERMISSION_FEATURE.MANAGE_SITE,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -193,6 +211,7 @@ const router = createRouter({
|
||||
keepAliveByFullPath: true,
|
||||
requiresAuth: true,
|
||||
permission: 'discovery',
|
||||
feature: PERMISSION_FEATURE.DISCOVERY_EXPLORE,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -217,6 +236,7 @@ const router = createRouter({
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
permission: 'discovery',
|
||||
feature: PERMISSION_FEATURE.DISCOVERY_EXPLORE,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -226,6 +246,7 @@ const router = createRouter({
|
||||
keepAlive: true,
|
||||
requiresAuth: true,
|
||||
permission: 'manage',
|
||||
feature: PERMISSION_FEATURE.MANAGE_FILEMANAGER,
|
||||
hideFooter: true,
|
||||
},
|
||||
},
|
||||
@@ -264,13 +285,16 @@ const router = createRouter({
|
||||
],
|
||||
})
|
||||
|
||||
async function getRoutePermission(to: any): Promise<UserPermissionKey | undefined> {
|
||||
async function getRoutePermission(to: any): Promise<PermissionProtectedItem> {
|
||||
if (to.meta.permission) {
|
||||
return to.meta.permission as UserPermissionKey
|
||||
return {
|
||||
permission: to.meta.permission as UserPermissionKey,
|
||||
feature: to.meta.feature,
|
||||
}
|
||||
}
|
||||
|
||||
if (to.name !== 'plugin-app') {
|
||||
return undefined
|
||||
return {}
|
||||
}
|
||||
|
||||
const pluginId = String(to.params.pluginId || '')
|
||||
@@ -279,7 +303,12 @@ async function getRoutePermission(to: any): Promise<UserPermissionKey | undefine
|
||||
await pluginSidebarNavStore.ensureSidebarNav()
|
||||
|
||||
const navItem = pluginSidebarNavStore.items.find(item => item.plugin_id === pluginId && item.nav_key === navKey)
|
||||
return (navItem?.permission || undefined) as UserPermissionKey | undefined
|
||||
if (!navItem) return {}
|
||||
|
||||
return {
|
||||
permission: (navItem.permission || undefined) as UserPermissionKey | undefined,
|
||||
feature: buildPluginPermissionFeatureKey(pluginId, navKey),
|
||||
}
|
||||
}
|
||||
|
||||
// 路由导航守卫
|
||||
@@ -299,15 +328,15 @@ router.beforeEach(async (to: any, from: any, next: any) => {
|
||||
next('/login')
|
||||
} else if (to.meta.requiresAuth) {
|
||||
const routePermission = await getRoutePermission(to)
|
||||
if (!routePermission) {
|
||||
if (!routePermission.permission && !routePermission.feature) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
const allowed = hasPermission(
|
||||
buildUserPermissionContext(userStore.superUser, userStore.permissions),
|
||||
const allowed = hasItemPermission(
|
||||
routePermission,
|
||||
buildUserPermissionContext(userStore.superUser, userStore.permissions),
|
||||
)
|
||||
if (!allowed) {
|
||||
setRequestNavigatingState(false)
|
||||
|
||||
@@ -1,22 +1,182 @@
|
||||
// 权限类型定义
|
||||
export type UserPermissionCategoryKey = 'discovery' | 'search' | 'subscribe' | 'manage'
|
||||
export type UserPermissionKey = UserPermissionCategoryKey | 'admin'
|
||||
export type UserPermissionFeatureKey = string
|
||||
export type UserPermissionFeatureMap = Record<UserPermissionFeatureKey, boolean>
|
||||
|
||||
export interface UserPermissionFeatureOption {
|
||||
key: UserPermissionFeatureKey
|
||||
permission: UserPermissionCategoryKey
|
||||
titleKey: string
|
||||
descriptionKey: string
|
||||
icon: string
|
||||
path?: string
|
||||
}
|
||||
|
||||
export interface UserPermissions {
|
||||
discovery: boolean // 发现权限
|
||||
search: boolean // 搜索权限
|
||||
subscribe: boolean // 订阅权限
|
||||
manage: boolean // 管理权限
|
||||
admin?: boolean // 管理员权限,仅用于前端入口标识,实际由 is_superuser 决定
|
||||
features?: UserPermissionFeatureMap // 功能级权限,缺省功能默认继承分类权限
|
||||
}
|
||||
|
||||
export type UserPermissionKey = keyof UserPermissions
|
||||
export type UserPermissionContext = UserPermissions & { is_superuser?: boolean; [key: string]: unknown }
|
||||
export type PermissionProtectedItem = { permission?: UserPermissionKey }
|
||||
export type PermissionProtectedItem = { permission?: UserPermissionKey; feature?: UserPermissionFeatureKey }
|
||||
|
||||
// 构造权限检查上下文,统一超级管理员标记与功能权限字段。
|
||||
export const PERMISSION_FEATURE = {
|
||||
DISCOVERY_RECOMMEND: 'discovery.recommend',
|
||||
DISCOVERY_EXPLORE: 'discovery.explore',
|
||||
SEARCH_RESOURCE: 'search.resource',
|
||||
SUBSCRIBE_MOVIE: 'subscribe.movie',
|
||||
SUBSCRIBE_TV: 'subscribe.tv',
|
||||
SUBSCRIBE_CALENDAR: 'subscribe.calendar',
|
||||
SUBSCRIBE_SHARE: 'subscribe.share',
|
||||
MANAGE_WORKFLOW: 'manage.workflow',
|
||||
MANAGE_DOWNLOADING: 'manage.downloading',
|
||||
MANAGE_HISTORY: 'manage.history',
|
||||
MANAGE_FILEMANAGER: 'manage.filemanager',
|
||||
MANAGE_SITE: 'manage.site',
|
||||
} as const
|
||||
|
||||
export const USER_PERMISSION_FEATURES: UserPermissionFeatureOption[] = [
|
||||
{
|
||||
key: PERMISSION_FEATURE.DISCOVERY_RECOMMEND,
|
||||
permission: 'discovery',
|
||||
titleKey: 'dialog.userAddEdit.permissions.features.recommend',
|
||||
descriptionKey: 'dialog.userAddEdit.permissions.featureDescriptions.recommend',
|
||||
icon: 'mdi-star-outline',
|
||||
path: '/recommend',
|
||||
},
|
||||
{
|
||||
key: PERMISSION_FEATURE.DISCOVERY_EXPLORE,
|
||||
permission: 'discovery',
|
||||
titleKey: 'dialog.userAddEdit.permissions.features.explore',
|
||||
descriptionKey: 'dialog.userAddEdit.permissions.featureDescriptions.explore',
|
||||
icon: 'mdi-apple-safari',
|
||||
path: '/discover',
|
||||
},
|
||||
{
|
||||
key: PERMISSION_FEATURE.SEARCH_RESOURCE,
|
||||
permission: 'search',
|
||||
titleKey: 'dialog.userAddEdit.permissions.features.resourceSearch',
|
||||
descriptionKey: 'dialog.userAddEdit.permissions.featureDescriptions.resourceSearch',
|
||||
icon: 'mdi-magnify',
|
||||
path: '/resource',
|
||||
},
|
||||
{
|
||||
key: PERMISSION_FEATURE.SUBSCRIBE_MOVIE,
|
||||
permission: 'subscribe',
|
||||
titleKey: 'dialog.userAddEdit.permissions.features.movieSubscribe',
|
||||
descriptionKey: 'dialog.userAddEdit.permissions.featureDescriptions.movieSubscribe',
|
||||
icon: 'mdi-movie-open-outline',
|
||||
path: '/subscribe/movie',
|
||||
},
|
||||
{
|
||||
key: PERMISSION_FEATURE.SUBSCRIBE_TV,
|
||||
permission: 'subscribe',
|
||||
titleKey: 'dialog.userAddEdit.permissions.features.tvSubscribe',
|
||||
descriptionKey: 'dialog.userAddEdit.permissions.featureDescriptions.tvSubscribe',
|
||||
icon: 'mdi-television',
|
||||
path: '/subscribe/tv',
|
||||
},
|
||||
{
|
||||
key: PERMISSION_FEATURE.SUBSCRIBE_CALENDAR,
|
||||
permission: 'subscribe',
|
||||
titleKey: 'dialog.userAddEdit.permissions.features.calendar',
|
||||
descriptionKey: 'dialog.userAddEdit.permissions.featureDescriptions.calendar',
|
||||
icon: 'mdi-calendar',
|
||||
path: '/calendar',
|
||||
},
|
||||
{
|
||||
key: PERMISSION_FEATURE.SUBSCRIBE_SHARE,
|
||||
permission: 'subscribe',
|
||||
titleKey: 'dialog.userAddEdit.permissions.features.subscribeShare',
|
||||
descriptionKey: 'dialog.userAddEdit.permissions.featureDescriptions.subscribeShare',
|
||||
icon: 'mdi-share-variant',
|
||||
path: '/subscribe-share',
|
||||
},
|
||||
{
|
||||
key: PERMISSION_FEATURE.MANAGE_WORKFLOW,
|
||||
permission: 'manage',
|
||||
titleKey: 'dialog.userAddEdit.permissions.features.workflow',
|
||||
descriptionKey: 'dialog.userAddEdit.permissions.featureDescriptions.workflow',
|
||||
icon: 'mdi-state-machine',
|
||||
path: '/workflow',
|
||||
},
|
||||
{
|
||||
key: PERMISSION_FEATURE.MANAGE_DOWNLOADING,
|
||||
permission: 'manage',
|
||||
titleKey: 'dialog.userAddEdit.permissions.features.downloading',
|
||||
descriptionKey: 'dialog.userAddEdit.permissions.featureDescriptions.downloading',
|
||||
icon: 'mdi-download-outline',
|
||||
path: '/downloading',
|
||||
},
|
||||
{
|
||||
key: PERMISSION_FEATURE.MANAGE_HISTORY,
|
||||
permission: 'manage',
|
||||
titleKey: 'dialog.userAddEdit.permissions.features.history',
|
||||
descriptionKey: 'dialog.userAddEdit.permissions.featureDescriptions.history',
|
||||
icon: 'mdi-folder-play-outline',
|
||||
path: '/history',
|
||||
},
|
||||
{
|
||||
key: PERMISSION_FEATURE.MANAGE_FILEMANAGER,
|
||||
permission: 'manage',
|
||||
titleKey: 'dialog.userAddEdit.permissions.features.fileManager',
|
||||
descriptionKey: 'dialog.userAddEdit.permissions.featureDescriptions.fileManager',
|
||||
icon: 'mdi-folder-multiple-outline',
|
||||
path: '/filemanager',
|
||||
},
|
||||
{
|
||||
key: PERMISSION_FEATURE.MANAGE_SITE,
|
||||
permission: 'manage',
|
||||
titleKey: 'dialog.userAddEdit.permissions.features.site',
|
||||
descriptionKey: 'dialog.userAddEdit.permissions.featureDescriptions.site',
|
||||
icon: 'mdi-web',
|
||||
path: '/site',
|
||||
},
|
||||
]
|
||||
|
||||
/** 构造功能级权限默认值,新建用户默认不收窄已拥有的分类权限。 */
|
||||
export function buildDefaultFeaturePermissions(enabled = true): UserPermissionFeatureMap {
|
||||
return Object.fromEntries(USER_PERMISSION_FEATURES.map(feature => [feature.key, enabled]))
|
||||
}
|
||||
|
||||
/** 构造插件导航的功能权限键,供动态插件入口复用。 */
|
||||
export function buildPluginPermissionFeatureKey(pluginId: string, navKey = 'main'): UserPermissionFeatureKey {
|
||||
return `plugin.${pluginId}.${navKey}`
|
||||
}
|
||||
|
||||
/** 判断传入值是否为可安全读取的普通对象。 */
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
/** 规整用户权限数据,兼容没有 features 字段的历史用户。 */
|
||||
export function normalizeUserPermissions(permissions: Partial<UserPermissions> | null | undefined = {}): UserPermissions {
|
||||
const permissionData = permissions ?? {}
|
||||
const rawFeatures = isRecord(permissionData.features) ? permissionData.features : {}
|
||||
const features = Object.fromEntries(
|
||||
Object.entries(rawFeatures).filter(([, value]) => typeof value === 'boolean'),
|
||||
) as UserPermissionFeatureMap
|
||||
|
||||
return {
|
||||
discovery: permissionData.discovery ?? DEFAULT_PERMISSIONS.discovery,
|
||||
search: permissionData.search ?? DEFAULT_PERMISSIONS.search,
|
||||
subscribe: permissionData.subscribe ?? DEFAULT_PERMISSIONS.subscribe,
|
||||
manage: permissionData.manage ?? DEFAULT_PERMISSIONS.manage,
|
||||
admin: permissionData.admin ?? DEFAULT_PERMISSIONS.admin,
|
||||
features,
|
||||
}
|
||||
}
|
||||
|
||||
/** 构造权限检查上下文,统一超级管理员标记、分类权限与功能权限字段。 */
|
||||
export function buildUserPermissionContext(isSuperuser: boolean, permissions: Partial<UserPermissions> = {}): UserPermissionContext {
|
||||
return {
|
||||
is_superuser: isSuperuser,
|
||||
...DEFAULT_PERMISSIONS,
|
||||
...permissions,
|
||||
...normalizeUserPermissions(permissions),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +187,7 @@ export const DEFAULT_PERMISSIONS: UserPermissions = {
|
||||
subscribe: true,
|
||||
manage: false,
|
||||
admin: false,
|
||||
features: {},
|
||||
}
|
||||
|
||||
// 管理员权限配置
|
||||
@@ -36,9 +197,10 @@ export const ADMIN_PERMISSIONS: UserPermissions = {
|
||||
subscribe: true,
|
||||
manage: true,
|
||||
admin: true,
|
||||
features: buildDefaultFeaturePermissions(),
|
||||
}
|
||||
|
||||
// 权限检查函数
|
||||
/** 检查用户是否拥有指定权限分类。 */
|
||||
export function hasPermission(userPermissions: any, permission: UserPermissionKey): boolean {
|
||||
// 如果用户是超级用户,拥有所有权限
|
||||
if (userPermissions?.is_superuser === true) {
|
||||
@@ -55,31 +217,53 @@ export function hasPermission(userPermissions: any, permission: UserPermissionKe
|
||||
return permissions[permission] === true
|
||||
}
|
||||
|
||||
// 批量权限检查
|
||||
/** 检查用户是否拥有指定功能权限,缺省功能项默认放行以兼容历史数据。 */
|
||||
export function hasFeaturePermission(
|
||||
userPermissions: any,
|
||||
feature?: UserPermissionFeatureKey,
|
||||
permission?: UserPermissionKey,
|
||||
): boolean {
|
||||
if (!feature) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (userPermissions?.is_superuser === true) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (permission && !hasPermission(userPermissions, permission)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const features = isRecord(userPermissions?.features) ? userPermissions.features : {}
|
||||
return features[feature] !== false
|
||||
}
|
||||
|
||||
/** 批量检查是否拥有任一权限分类。 */
|
||||
export function hasAnyPermission(userPermissions: any, permissionList: UserPermissionKey[]): boolean {
|
||||
return permissionList.some(permission => hasPermission(userPermissions, permission))
|
||||
}
|
||||
|
||||
// 检查是否有所有权限
|
||||
/** 批量检查是否拥有全部权限分类。 */
|
||||
export function hasAllPermissions(userPermissions: any, permissionList: UserPermissionKey[]): boolean {
|
||||
return permissionList.every(permission => hasPermission(userPermissions, permission))
|
||||
}
|
||||
|
||||
// 统一检查带 permission 字段的入口,避免菜单、按钮、快捷入口各自实现判断。
|
||||
/** 统一检查带 permission / feature 字段的入口,避免菜单、按钮、快捷入口各自实现判断。 */
|
||||
export function hasItemPermission(item: PermissionProtectedItem, userPermissions: any): boolean {
|
||||
if (!item.permission) {
|
||||
return true
|
||||
if (item.permission && !hasPermission(userPermissions, item.permission)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return hasPermission(userPermissions, item.permission)
|
||||
return hasFeaturePermission(userPermissions, item.feature, item.permission)
|
||||
}
|
||||
|
||||
// 根据权限过滤带 permission 字段的入口
|
||||
/** 根据权限过滤带 permission / feature 字段的入口。 */
|
||||
export function filterItemsByPermission<T extends PermissionProtectedItem>(items: T[], userPermissions: any): T[] {
|
||||
return items.filter(item => hasItemPermission(item, userPermissions))
|
||||
}
|
||||
|
||||
// 根据权限过滤菜单项
|
||||
/** 根据权限过滤菜单项。 */
|
||||
export function filterMenusByPermission(menus: any[], userPermissions: any): any[] {
|
||||
return filterItemsByPermission(menus, userPermissions)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Composer } from 'vue-i18n'
|
||||
import type { NavMenu } from '@/@layouts/types'
|
||||
import type { PluginSidebarNavItem } from '@/api/types'
|
||||
import { pluginSidebarSectionToHeaderKey } from '@/router/i18n-menu'
|
||||
import { filterMenusByPermission } from '@/utils/permission'
|
||||
import { buildPluginPermissionFeatureKey, filterMenusByPermission } from '@/utils/permission'
|
||||
|
||||
export type PluginNavMenuEntry = {
|
||||
navMenu: NavMenu & { permission?: string }
|
||||
@@ -31,6 +31,7 @@ export function navMenuFromPluginSidebarItem(
|
||||
},
|
||||
header,
|
||||
permission: item.permission ?? undefined,
|
||||
feature: buildPluginPermissionFeatureKey(item.plugin_id, item.nav_key),
|
||||
} as NavMenu & { permission?: string }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user