mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-28 19:47:49 +08:00
Improve frontend dialog and layout behavior
This commit is contained in:
Vendored
+4
-1
@@ -1,6 +1,6 @@
|
|||||||
import type { Component, Ref, VNode } from 'vue'
|
import type { Component, Ref, VNode } from 'vue'
|
||||||
import type { RouteLocationRaw } from 'vue-router'
|
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'
|
import { ContentWidth, FooterType, NavbarType } from './enums'
|
||||||
|
|
||||||
export interface UserConfig {
|
export interface UserConfig {
|
||||||
@@ -124,6 +124,7 @@ export interface NavLink extends NavLinkProps, Partial<AclProperties> {
|
|||||||
badgeClass?: string
|
badgeClass?: string
|
||||||
disable?: boolean
|
disable?: boolean
|
||||||
permission?: UserPermissionKey
|
permission?: UserPermissionKey
|
||||||
|
feature?: UserPermissionFeatureKey
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavMenuTabItem {
|
export interface NavMenuTabItem {
|
||||||
@@ -131,6 +132,8 @@ export interface NavMenuTabItem {
|
|||||||
icon?: string
|
icon?: string
|
||||||
tab: string
|
tab: string
|
||||||
description?: string
|
description?: string
|
||||||
|
permission?: UserPermissionKey
|
||||||
|
feature?: UserPermissionFeatureKey
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavMenu extends NavLink {
|
export interface NavMenu extends NavLink {
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ function getMenus(): NavMenu[] {
|
|||||||
header: item.header,
|
header: item.header,
|
||||||
admin: item.admin,
|
admin: item.admin,
|
||||||
permission: item.permission,
|
permission: item.permission,
|
||||||
|
feature: item.feature,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
// 设置标签页
|
// 设置标签页
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ import { useDisplay } from 'vuetify'
|
|||||||
import avatar1 from '@images/avatars/avatar-1.png'
|
import avatar1 from '@images/avatars/avatar-1.png'
|
||||||
import { useUserStore } from '@/stores'
|
import { useUserStore } from '@/stores'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import {
|
||||||
|
USER_PERMISSION_FEATURES,
|
||||||
|
buildDefaultFeaturePermissions,
|
||||||
|
normalizeUserPermissions,
|
||||||
|
type UserPermissionCategoryKey,
|
||||||
|
type UserPermissionFeatureKey,
|
||||||
|
type UserPermissions,
|
||||||
|
} from '@/utils/permission'
|
||||||
|
|
||||||
// 多语言支持
|
// 多语言支持
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -65,14 +73,6 @@ 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,
|
||||||
@@ -88,6 +88,7 @@ const userForm = ref<ExtendedUser>({
|
|||||||
search: true,
|
search: true,
|
||||||
subscribe: true,
|
subscribe: true,
|
||||||
manage: false,
|
manage: false,
|
||||||
|
features: buildDefaultFeaturePermissions(),
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
wechat_userid: null,
|
wechat_userid: null,
|
||||||
@@ -127,34 +128,115 @@ const permissionOptions = [
|
|||||||
description: t('dialog.userAddEdit.permissions.manageDesc'),
|
description: t('dialog.userAddEdit.permissions.manageDesc'),
|
||||||
icon: 'mdi-cog-outline',
|
icon: 'mdi-cog-outline',
|
||||||
},
|
},
|
||||||
]
|
] as const
|
||||||
|
|
||||||
|
const activePermissionCategory = ref<UserPermissionCategoryKey>('discovery')
|
||||||
|
|
||||||
// 权限状态计算属性
|
// 权限状态计算属性
|
||||||
const userPermissions = computed({
|
const userPermissions = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
const permissions = userForm.value.permissions as UserPermissions
|
return normalizeUserPermissions(userForm.value.permissions as Partial<UserPermissions>)
|
||||||
return {
|
|
||||||
discovery: permissions?.discovery ?? true,
|
|
||||||
search: permissions?.search ?? true,
|
|
||||||
subscribe: permissions?.subscribe ?? true,
|
|
||||||
manage: permissions?.manage ?? false,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
set: (value: UserPermissions) => {
|
set: (value: UserPermissions) => {
|
||||||
userForm.value.permissions = value
|
userForm.value.permissions = value
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 切换权限状态
|
const permissionFeatureOptions = computed(() =>
|
||||||
function togglePermission(key: keyof UserPermissions) {
|
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
|
const currentPermissions = userPermissions.value
|
||||||
userPermissions.value = {
|
userPermissions.value = {
|
||||||
...currentPermissions,
|
...currentPermissions,
|
||||||
[key]: !currentPermissions[key],
|
[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) {
|
function changeAvatar(file: Event) {
|
||||||
const fileReader = new FileReader()
|
const fileReader = new FileReader()
|
||||||
const { files } = file.target as HTMLInputElement
|
const { files } = file.target as HTMLInputElement
|
||||||
@@ -182,19 +264,19 @@ function changeAvatar(file: Event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置默认头像
|
/** 将头像恢复为系统默认头像。 */
|
||||||
function resetDefaultAvatar() {
|
function resetDefaultAvatar() {
|
||||||
currentAvatar.value = avatar1
|
currentAvatar.value = avatar1
|
||||||
$toast.success(t('dialog.userAddEdit.resetAvatarSuccess'))
|
$toast.success(t('dialog.userAddEdit.resetAvatarSuccess'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 还原当前头像
|
/** 还原为用户当前已保存的头像。 */
|
||||||
function restoreCurrentAvatar() {
|
function restoreCurrentAvatar() {
|
||||||
currentAvatar.value = userForm.value.avatar
|
currentAvatar.value = userForm.value.avatar
|
||||||
$toast.success(t('dialog.userAddEdit.restoreAvatarSuccess'))
|
$toast.success(t('dialog.userAddEdit.restoreAvatarSuccess'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询用户信息
|
/** 从接口查询当前编辑用户的信息并回填表单。 */
|
||||||
async function fetchUserInfo() {
|
async function fetchUserInfo() {
|
||||||
try {
|
try {
|
||||||
userForm.value = await api.get(`user/${props.username}`)
|
userForm.value = await api.get(`user/${props.username}`)
|
||||||
@@ -210,7 +292,7 @@ async function fetchUserInfo() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用API 新增用户
|
/** 调用接口创建新用户。 */
|
||||||
async function addUser() {
|
async function addUser() {
|
||||||
if (isAdding.value) {
|
if (isAdding.value) {
|
||||||
$toast.error(t('dialog.userAddEdit.creatingUser', { name: userForm.value.name }))
|
$toast.error(t('dialog.userAddEdit.creatingUser', { name: userForm.value.name }))
|
||||||
@@ -256,7 +338,7 @@ async function addUser() {
|
|||||||
isAdding.value = false
|
isAdding.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用API更新用户信息
|
/** 调用接口更新当前用户信息。 */
|
||||||
async function updateUser() {
|
async function updateUser() {
|
||||||
if (isUpdating.value) {
|
if (isUpdating.value) {
|
||||||
$toast.error(t('dialog.userAddEdit.updatingUser', { name: userForm.value.name }))
|
$toast.error(t('dialog.userAddEdit.updatingUser', { name: userForm.value.name }))
|
||||||
@@ -367,7 +449,7 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VDialog scrollable max-width="40rem" :fullscreen="!display.mdAndUp.value">
|
<VDialog scrollable max-width="64rem" :fullscreen="!display.mdAndUp.value">
|
||||||
<VCard>
|
<VCard>
|
||||||
<VCardItem :class="props.oper === 'add' ? 'py-3' : 'py-2'">
|
<VCardItem :class="props.oper === 'add' ? 'py-3' : 'py-2'">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
@@ -572,43 +654,144 @@ onMounted(() => {
|
|||||||
<span>{{ t('dialog.userAddEdit.permissions.title') }}</span>
|
<span>{{ t('dialog.userAddEdit.permissions.title') }}</span>
|
||||||
</VDivider>
|
</VDivider>
|
||||||
<!-- 权限设置 -->
|
<!-- 权限设置 -->
|
||||||
<div v-if="canControl">
|
<div v-if="canControl" class="user-permission-editor">
|
||||||
<VRow>
|
<div class="permission-section-header">
|
||||||
<VCol v-for="option in permissionOptions" :key="option.key" cols="6">
|
<div>
|
||||||
<VCard
|
<div class="text-subtitle-1 font-weight-medium">
|
||||||
:color="userPermissions[option.key as keyof UserPermissions] ? 'primary' : 'surface'"
|
{{ t('dialog.userAddEdit.permissions.categoryTitle') }}
|
||||||
:variant="userPermissions[option.key as keyof UserPermissions] ? 'tonal' : 'outlined'"
|
</div>
|
||||||
class="cursor-pointer transition-all h-full"
|
<div class="text-caption text-medium-emphasis">
|
||||||
@click="togglePermission(option.key as keyof UserPermissions)"
|
{{ t('dialog.userAddEdit.permissions.categoryHint') }}
|
||||||
hover
|
</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">
|
<VCheckboxBtn
|
||||||
<VAvatar
|
:model-value="isFeatureEnabled(feature.key)"
|
||||||
:color="userPermissions[option.key as keyof UserPermissions] ? 'primary' : 'surface-variant'"
|
:disabled="!userPermissions[activePermissionCategory]"
|
||||||
size="40"
|
color="primary"
|
||||||
class="me-3"
|
density="compact"
|
||||||
>
|
class="permission-feature-item__check"
|
||||||
<VIcon :icon="option.icon" />
|
@click.stop
|
||||||
</VAvatar>
|
@update:model-value="togglePermissionFeature(feature.key)"
|
||||||
<div class="flex-grow-1">
|
/>
|
||||||
<div class="text-subtitle-1 font-weight-medium d-flex align-center">
|
<span class="permission-feature-item__icon">
|
||||||
{{ option.title }}
|
<VIcon :icon="feature.icon" size="20" />
|
||||||
<VIcon
|
</span>
|
||||||
v-if="userPermissions[option.key as keyof UserPermissions]"
|
<span class="permission-feature-item__body">
|
||||||
icon="mdi-check-circle"
|
<span class="permission-feature-item__title">{{ feature.title }}</span>
|
||||||
color="primary"
|
<span class="permission-feature-item__desc">{{ feature.description }}</span>
|
||||||
size="small"
|
</span>
|
||||||
class="ms-2"
|
</div>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-caption text-medium-emphasis">
|
|
||||||
{{ option.description }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</div>
|
</div>
|
||||||
</VForm>
|
</VForm>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
@@ -642,3 +825,273 @@ onMounted(() => {
|
|||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
</template>
|
</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 {
|
.filter-buttons-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-btn-mobile {
|
.filter-btn-mobile {
|
||||||
@@ -732,10 +732,23 @@ onMounted(() => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
block-size: auto;
|
block-size: 56px;
|
||||||
min-block-size: 48px;
|
min-block-size: 56px;
|
||||||
|
min-inline-size: 0;
|
||||||
padding-block: 4px;
|
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 {
|
.filter-icon {
|
||||||
@@ -744,20 +757,20 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.filter-label {
|
.filter-label {
|
||||||
|
display: -webkit-box;
|
||||||
|
overflow: hidden;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.15;
|
||||||
|
max-inline-size: 100%;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (width <= 600px) {
|
@media (width <= 600px) {
|
||||||
.filter-buttons-grid {
|
.filter-buttons-grid {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-label {
|
|
||||||
overflow: hidden;
|
|
||||||
max-inline-size: 100%;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
type ComputedRef,
|
type ComputedRef,
|
||||||
type Ref,
|
type Ref,
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import type { UserPermissionKey } from '@/utils/permission'
|
import type { UserPermissionFeatureKey, UserPermissionKey } from '@/utils/permission'
|
||||||
|
|
||||||
// 声明全局变量类型
|
// 声明全局变量类型
|
||||||
declare global {
|
declare global {
|
||||||
@@ -31,6 +31,7 @@ export interface DynamicButtonMenuItem {
|
|||||||
icon?: string
|
icon?: string
|
||||||
color?: string
|
color?: string
|
||||||
permission?: UserPermissionKey
|
permission?: UserPermissionKey
|
||||||
|
feature?: UserPermissionFeatureKey
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
action: () => void
|
action: () => void
|
||||||
}
|
}
|
||||||
@@ -61,11 +62,12 @@ export function useDynamicButton(options: {
|
|||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
menuItems?: MaybeRefValue<DynamicButtonMenuItem[] | undefined>
|
menuItems?: MaybeRefValue<DynamicButtonMenuItem[] | undefined>
|
||||||
permission?: UserPermissionKey
|
permission?: UserPermissionKey
|
||||||
|
feature?: UserPermissionFeatureKey
|
||||||
show?: MaybeRefValue<boolean>
|
show?: MaybeRefValue<boolean>
|
||||||
autoRegister?: boolean // 是否自动注册,默认为true
|
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)
|
const registerDynamicButton = inject<((button: any) => void) | null>('registerDynamicButton', null)
|
||||||
@@ -79,6 +81,7 @@ export function useDynamicButton(options: {
|
|||||||
const resolvedShow = computed(() => resolveMaybeRef(show, true))
|
const resolvedShow = computed(() => resolveMaybeRef(show, true))
|
||||||
const resolvedMenuItems = computed(() => resolveMaybeRef(menuItems))
|
const resolvedMenuItems = computed(() => resolveMaybeRef(menuItems))
|
||||||
|
|
||||||
|
/** 根据当前响应式配置生成可注册的动态按钮对象。 */
|
||||||
function buildDynamicButton() {
|
function buildDynamicButton() {
|
||||||
const buttonMenuItems = resolvedMenuItems.value
|
const buttonMenuItems = resolvedMenuItems.value
|
||||||
|
|
||||||
@@ -86,12 +89,13 @@ export function useDynamicButton(options: {
|
|||||||
icon: resolvedIcon.value,
|
icon: resolvedIcon.value,
|
||||||
action: onClick || (() => {}),
|
action: onClick || (() => {}),
|
||||||
permission,
|
permission,
|
||||||
|
feature,
|
||||||
show: resolvedShow.value,
|
show: resolvedShow.value,
|
||||||
menuItems: buttonMenuItems && buttonMenuItems.length > 0 ? buttonMenuItems : undefined,
|
menuItems: buttonMenuItems && buttonMenuItems.length > 0 ? buttonMenuItems : undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册动态按钮
|
/** 在当前页面激活时注册动态按钮。 */
|
||||||
function setupDynamicButton() {
|
function setupDynamicButton() {
|
||||||
if (!componentActive.value) return
|
if (!componentActive.value) return
|
||||||
|
|
||||||
@@ -133,7 +137,7 @@ export function useDynamicButton(options: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取消注册动态按钮
|
/** 清理当前页面注册过的动态按钮。 */
|
||||||
function cleanupDynamicButton() {
|
function cleanupDynamicButton() {
|
||||||
if (unregisterDynamicButton && dynamicButtonRegistered.value) {
|
if (unregisterDynamicButton && dynamicButtonRegistered.value) {
|
||||||
unregisterDynamicButton()
|
unregisterDynamicButton()
|
||||||
@@ -147,7 +151,7 @@ export function useDynamicButton(options: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 暴露方法:手动打开对话框
|
/** 手动触发动态按钮主操作。 */
|
||||||
function openDialog() {
|
function openDialog() {
|
||||||
onClick?.()
|
onClick?.()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ComputedRef, Ref } from 'vue'
|
import type { ComputedRef, Ref } from 'vue'
|
||||||
import { useTabStateRestore } from '@/composables/useStateRestore'
|
import { useTabStateRestore } from '@/composables/useStateRestore'
|
||||||
import type { UserPermissionKey } from '@/utils/permission'
|
import type { UserPermissionFeatureKey, UserPermissionKey } from '@/utils/permission'
|
||||||
|
|
||||||
// 动态标签页相关类型
|
// 动态标签页相关类型
|
||||||
interface DynamicHeaderTabButton {
|
interface DynamicHeaderTabButton {
|
||||||
@@ -11,6 +11,7 @@ interface DynamicHeaderTabButton {
|
|||||||
class?: string
|
class?: string
|
||||||
action?: () => void
|
action?: () => void
|
||||||
permission?: UserPermissionKey
|
permission?: UserPermissionKey
|
||||||
|
feature?: UserPermissionFeatureKey
|
||||||
show?: boolean | ComputedRef<boolean>
|
show?: boolean | ComputedRef<boolean>
|
||||||
loading?: boolean | ComputedRef<boolean>
|
loading?: boolean | ComputedRef<boolean>
|
||||||
dataAttr?: string // 用于VMenu定位的data属性
|
dataAttr?: string // 用于VMenu定位的data属性
|
||||||
@@ -20,6 +21,8 @@ interface DynamicHeaderTabItem {
|
|||||||
title: string
|
title: string
|
||||||
icon?: string
|
icon?: string
|
||||||
tab: string
|
tab: string
|
||||||
|
permission?: UserPermissionKey
|
||||||
|
feature?: UserPermissionFeatureKey
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DynamicHeaderTabConfig {
|
interface DynamicHeaderTabConfig {
|
||||||
@@ -30,6 +33,7 @@ interface DynamicHeaderTabConfig {
|
|||||||
onUpdateModelValue?: (value: string) => void
|
onUpdateModelValue?: (value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 提供页面动态头部标签的注册、状态恢复和注销能力。 */
|
||||||
export function useDynamicHeaderTab() {
|
export function useDynamicHeaderTab() {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
@@ -37,7 +41,7 @@ export function useDynamicHeaderTab() {
|
|||||||
const registerDynamicHeaderTab = inject<(tab: DynamicHeaderTabConfig) => void>('registerDynamicHeaderTab')
|
const registerDynamicHeaderTab = inject<(tab: DynamicHeaderTabConfig) => void>('registerDynamicHeaderTab')
|
||||||
const unregisterDynamicHeaderTab = inject<() => void>('unregisterDynamicHeaderTab')
|
const unregisterDynamicHeaderTab = inject<() => void>('unregisterDynamicHeaderTab')
|
||||||
|
|
||||||
// 注册动态标签页
|
/** 注册当前页面的动态头部标签配置。 */
|
||||||
const registerHeaderTab = (config: {
|
const registerHeaderTab = (config: {
|
||||||
items: DynamicHeaderTabItem[] | ComputedRef<DynamicHeaderTabItem[]> | Ref<DynamicHeaderTabItem[]>
|
items: DynamicHeaderTabItem[] | ComputedRef<DynamicHeaderTabItem[]> | Ref<DynamicHeaderTabItem[]>
|
||||||
modelValue: Ref<string>
|
modelValue: Ref<string>
|
||||||
@@ -184,7 +188,7 @@ export function useDynamicHeaderTab() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取消注册
|
/** 取消当前页面注册的动态头部标签。 */
|
||||||
const unregisterHeaderTab = () => {
|
const unregisterHeaderTab = () => {
|
||||||
if (unregisterDynamicHeaderTab) {
|
if (unregisterDynamicHeaderTab) {
|
||||||
unregisterDynamicHeaderTab()
|
unregisterDynamicHeaderTab()
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
filterMenusByPermission,
|
filterMenusByPermission,
|
||||||
hasItemPermission,
|
hasItemPermission,
|
||||||
hasPermission,
|
hasPermission,
|
||||||
|
type UserPermissionFeatureKey,
|
||||||
type UserPermissionKey,
|
type UserPermissionKey,
|
||||||
} from '@/utils/permission'
|
} from '@/utils/permission'
|
||||||
import { usePullDownGesture } from '@/composables/usePullDownGesture'
|
import { usePullDownGesture } from '@/composables/usePullDownGesture'
|
||||||
@@ -119,6 +120,7 @@ interface DynamicHeaderTabButton {
|
|||||||
class?: string
|
class?: string
|
||||||
action?: () => void
|
action?: () => void
|
||||||
permission?: UserPermissionKey
|
permission?: UserPermissionKey
|
||||||
|
feature?: UserPermissionFeatureKey
|
||||||
show?: boolean | ComputedRef<boolean>
|
show?: boolean | ComputedRef<boolean>
|
||||||
loading?: boolean | ComputedRef<boolean>
|
loading?: boolean | ComputedRef<boolean>
|
||||||
dataAttr?: string
|
dataAttr?: string
|
||||||
@@ -128,6 +130,8 @@ interface DynamicHeaderTabItem {
|
|||||||
title: string
|
title: string
|
||||||
icon?: string
|
icon?: string
|
||||||
tab: string
|
tab: string
|
||||||
|
permission?: UserPermissionKey
|
||||||
|
feature?: UserPermissionFeatureKey
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DynamicHeaderTab {
|
interface DynamicHeaderTab {
|
||||||
@@ -160,6 +164,8 @@ const unregisterDynamicHeaderTab = () => {
|
|||||||
/** 更新当前动态标签页选中值,并通知注册页面同步状态。 */
|
/** 更新当前动态标签页选中值,并通知注册页面同步状态。 */
|
||||||
const handleTabChange = (newValue: string) => {
|
const handleTabChange = (newValue: string) => {
|
||||||
if (dynamicHeaderTab.value) {
|
if (dynamicHeaderTab.value) {
|
||||||
|
if (!visibleDynamicHeaderTabItems.value.some(item => item.tab === newValue)) return
|
||||||
|
|
||||||
dynamicHeaderTab.value.modelValue = newValue
|
dynamicHeaderTab.value.modelValue = newValue
|
||||||
// 通知注册的页面更新值
|
// 通知注册的页面更新值
|
||||||
if (dynamicHeaderTab.value.onUpdateModelValue) {
|
if (dynamicHeaderTab.value.onUpdateModelValue) {
|
||||||
@@ -193,13 +199,15 @@ watch(
|
|||||||
{ immediate: false },
|
{ immediate: false },
|
||||||
)
|
)
|
||||||
|
|
||||||
// 当前路由是否注册了动态标签页。
|
const visibleDynamicHeaderTabItems = computed(() => {
|
||||||
const hasDynamicHeaderTab = computed(() => {
|
if (!dynamicHeaderTab.value || dynamicHeaderTab.value.routePath !== route.path) return []
|
||||||
return (
|
|
||||||
dynamicHeaderTab.value && dynamicHeaderTab.value.items.length > 0 && dynamicHeaderTab.value.routePath === route.path
|
return filterItemsByPermission(dynamicHeaderTab.value.items, userPermissions.value)
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 当前路由是否注册了动态标签页。
|
||||||
|
const hasDynamicHeaderTab = computed(() => visibleDynamicHeaderTabItems.value.length > 0)
|
||||||
|
|
||||||
// 水平布局下动态标签页会并入顶部导航三级菜单,不再额外显示标签页栏。
|
// 水平布局下动态标签页会并入顶部导航三级菜单,不再额外显示标签页栏。
|
||||||
const showDynamicHeaderTab = computed(() => hasDynamicHeaderTab.value && !showHorizontalThemeNav.value)
|
const showDynamicHeaderTab = computed(() => hasDynamicHeaderTab.value && !showHorizontalThemeNav.value)
|
||||||
|
|
||||||
@@ -384,10 +392,10 @@ function getHorizontalNavTabs(item: NavMenu): DynamicHeaderTabItem[] {
|
|||||||
const targetPath = normalizeMenuPath(item.to)
|
const targetPath = normalizeMenuPath(item.to)
|
||||||
|
|
||||||
if (targetPath && isHorizontalNavActive(item) && hasDynamicHeaderTab.value) {
|
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
|
const pending = pendingHorizontalTab.value
|
||||||
if (normalizeMenuPath(route.path) !== pending.path) return
|
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
|
if (!tabExists) return
|
||||||
|
|
||||||
handleTabChange(pending.tab)
|
handleTabChange(pending.tab)
|
||||||
@@ -673,7 +681,7 @@ onMounted(async () => {
|
|||||||
<template #dynamic-header-tab>
|
<template #dynamic-header-tab>
|
||||||
<div v-if="showDynamicHeaderTab">
|
<div v-if="showDynamicHeaderTab">
|
||||||
<HeaderTab
|
<HeaderTab
|
||||||
:items="dynamicHeaderTab!.items"
|
:items="visibleDynamicHeaderTabItems"
|
||||||
:model-value="dynamicHeaderTab!.modelValue"
|
:model-value="dynamicHeaderTab!.modelValue"
|
||||||
@update:model-value="handleTabChange"
|
@update:model-value="handleTabChange"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ interface DynamicButton {
|
|||||||
icon: string
|
icon: string
|
||||||
action: () => void
|
action: () => void
|
||||||
permission?: DynamicButtonMenuItem['permission']
|
permission?: DynamicButtonMenuItem['permission']
|
||||||
|
feature?: DynamicButtonMenuItem['feature']
|
||||||
show: boolean
|
show: boolean
|
||||||
routePath?: string // 添加路径属性,用于标识哪个路由注册的
|
routePath?: string // 添加路径属性,用于标识哪个路由注册的
|
||||||
menuItems?: DynamicButtonMenuItem[]
|
menuItems?: DynamicButtonMenuItem[]
|
||||||
|
|||||||
@@ -2487,6 +2487,44 @@ export default {
|
|||||||
subscribeDesc: 'Manage movie and TV show subscriptions',
|
subscribeDesc: 'Manage movie and TV show subscriptions',
|
||||||
manage: 'Manage',
|
manage: 'Manage',
|
||||||
manageDesc: 'Access download management and site management etc.',
|
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: {
|
searchBar: {
|
||||||
|
|||||||
@@ -2442,6 +2442,44 @@ export default {
|
|||||||
subscribeDesc: '管理电影和电视剧订阅',
|
subscribeDesc: '管理电影和电视剧订阅',
|
||||||
manage: '管理',
|
manage: '管理',
|
||||||
manageDesc: '访问下载管理和站点管理等功能',
|
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: {
|
searchBar: {
|
||||||
|
|||||||
@@ -2441,6 +2441,44 @@ export default {
|
|||||||
subscribeDesc: '管理電影和電視劇訂閱',
|
subscribeDesc: '管理電影和電視劇訂閱',
|
||||||
manage: '管理',
|
manage: '管理',
|
||||||
manageDesc: '存取下載管理和站點管理等功能',
|
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: {
|
searchBar: {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useGlobalSettingsStore } from '@/stores'
|
import { useGlobalSettingsStore } from '@/stores'
|
||||||
import type { NavMenu, NavMenuTabItem } from '@/@layouts/types'
|
import type { NavMenu, NavMenuTabItem } from '@/@layouts/types'
|
||||||
import type { Composer } from 'vue-i18n'
|
import type { Composer } from 'vue-i18n'
|
||||||
|
import { PERMISSION_FEATURE } from '@/utils/permission'
|
||||||
|
|
||||||
/** 构建当前语言与全局模式对应的主导航菜单。 */
|
/** 构建当前语言与全局模式对应的主导航菜单。 */
|
||||||
export function getNavMenus(t: Composer['t']): NavMenu[] {
|
export function getNavMenus(t: Composer['t']): NavMenu[] {
|
||||||
@@ -28,6 +29,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
|||||||
header: t('menu.start'),
|
header: t('menu.start'),
|
||||||
admin: false,
|
admin: false,
|
||||||
permission: 'search',
|
permission: 'search',
|
||||||
|
feature: PERMISSION_FEATURE.SEARCH_RESOURCE,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('navItems.recommend'),
|
title: t('navItems.recommend'),
|
||||||
@@ -38,6 +40,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
|||||||
admin: false,
|
admin: false,
|
||||||
footer: true,
|
footer: true,
|
||||||
permission: 'discovery',
|
permission: 'discovery',
|
||||||
|
feature: PERMISSION_FEATURE.DISCOVERY_RECOMMEND,
|
||||||
tabs: getRecommendTabs(t),
|
tabs: getRecommendTabs(t),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -49,6 +52,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
|||||||
admin: false,
|
admin: false,
|
||||||
footer: true,
|
footer: true,
|
||||||
permission: 'discovery',
|
permission: 'discovery',
|
||||||
|
feature: PERMISSION_FEATURE.DISCOVERY_EXPLORE,
|
||||||
tabs: getDiscoverTabs(t),
|
tabs: getDiscoverTabs(t),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -61,6 +65,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
|||||||
admin: false,
|
admin: false,
|
||||||
footer: false,
|
footer: false,
|
||||||
permission: 'subscribe',
|
permission: 'subscribe',
|
||||||
|
feature: PERMISSION_FEATURE.SUBSCRIBE_MOVIE,
|
||||||
tabs: getSubscribeMovieTabs(t),
|
tabs: getSubscribeMovieTabs(t),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -73,6 +78,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
|||||||
admin: false,
|
admin: false,
|
||||||
footer: false,
|
footer: false,
|
||||||
permission: 'subscribe',
|
permission: 'subscribe',
|
||||||
|
feature: PERMISSION_FEATURE.SUBSCRIBE_TV,
|
||||||
tabs: getSubscribeTvTabs(t),
|
tabs: getSubscribeTvTabs(t),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -85,6 +91,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
|||||||
admin: true,
|
admin: true,
|
||||||
footer: false,
|
footer: false,
|
||||||
permission: 'manage',
|
permission: 'manage',
|
||||||
|
feature: PERMISSION_FEATURE.MANAGE_WORKFLOW,
|
||||||
tabs: getWorkflowTabs(t),
|
tabs: getWorkflowTabs(t),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -96,6 +103,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
|||||||
header: t('menu.subscribe'),
|
header: t('menu.subscribe'),
|
||||||
admin: false,
|
admin: false,
|
||||||
permission: 'subscribe',
|
permission: 'subscribe',
|
||||||
|
feature: PERMISSION_FEATURE.SUBSCRIBE_CALENDAR,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('navItems.downloadManager'),
|
title: t('navItems.downloadManager'),
|
||||||
@@ -105,6 +113,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
|||||||
header: t('menu.organize'),
|
header: t('menu.organize'),
|
||||||
admin: false,
|
admin: false,
|
||||||
permission: 'manage',
|
permission: 'manage',
|
||||||
|
feature: PERMISSION_FEATURE.MANAGE_DOWNLOADING,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('navItems.mediaOrganize'),
|
title: t('navItems.mediaOrganize'),
|
||||||
@@ -114,6 +123,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
|||||||
header: t('menu.organize'),
|
header: t('menu.organize'),
|
||||||
admin: true,
|
admin: true,
|
||||||
permission: 'manage',
|
permission: 'manage',
|
||||||
|
feature: PERMISSION_FEATURE.MANAGE_HISTORY,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('navItems.fileManager'),
|
title: t('navItems.fileManager'),
|
||||||
@@ -123,6 +133,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
|||||||
header: t('menu.organize'),
|
header: t('menu.organize'),
|
||||||
admin: true,
|
admin: true,
|
||||||
permission: 'manage',
|
permission: 'manage',
|
||||||
|
feature: PERMISSION_FEATURE.MANAGE_FILEMANAGER,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('navItems.pluginManager'),
|
title: t('navItems.pluginManager'),
|
||||||
@@ -142,6 +153,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] {
|
|||||||
header: t('menu.system'),
|
header: t('menu.system'),
|
||||||
admin: true,
|
admin: true,
|
||||||
permission: 'manage',
|
permission: 'manage',
|
||||||
|
feature: PERMISSION_FEATURE.MANAGE_SITE,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('navItems.userManager'),
|
title: t('navItems.userManager'),
|
||||||
|
|||||||
+37
-8
@@ -2,7 +2,14 @@ import { createRouter, createWebHashHistory } from 'vue-router'
|
|||||||
import { configureNProgress } from '@/api/nprogress'
|
import { configureNProgress } from '@/api/nprogress'
|
||||||
import { useAuthStore, usePluginSidebarNavStore, useUserStore } from '@/stores'
|
import { useAuthStore, usePluginSidebarNavStore, useUserStore } from '@/stores'
|
||||||
import { setNavigatingState as setRequestNavigatingState } from '@/utils/requestOptimizer'
|
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
|
// Nprogress
|
||||||
configureNProgress()
|
configureNProgress()
|
||||||
@@ -45,6 +52,7 @@ const router = createRouter({
|
|||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
permission: 'discovery',
|
permission: 'discovery',
|
||||||
|
feature: PERMISSION_FEATURE.DISCOVERY_RECOMMEND,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -54,6 +62,7 @@ const router = createRouter({
|
|||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
permission: 'discovery',
|
permission: 'discovery',
|
||||||
|
feature: PERMISSION_FEATURE.DISCOVERY_EXPLORE,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -63,6 +72,7 @@ const router = createRouter({
|
|||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
permission: 'search',
|
permission: 'search',
|
||||||
|
feature: PERMISSION_FEATURE.SEARCH_RESOURCE,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -73,6 +83,7 @@ const router = createRouter({
|
|||||||
keepAliveKey: 'subscribe-movie',
|
keepAliveKey: 'subscribe-movie',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
permission: 'subscribe',
|
permission: 'subscribe',
|
||||||
|
feature: PERMISSION_FEATURE.SUBSCRIBE_MOVIE,
|
||||||
subType: '电影',
|
subType: '电影',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -84,6 +95,7 @@ const router = createRouter({
|
|||||||
keepAliveKey: 'subscribe-tv',
|
keepAliveKey: 'subscribe-tv',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
permission: 'subscribe',
|
permission: 'subscribe',
|
||||||
|
feature: PERMISSION_FEATURE.SUBSCRIBE_TV,
|
||||||
subType: '电视剧',
|
subType: '电视剧',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -93,6 +105,7 @@ const router = createRouter({
|
|||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
permission: 'subscribe',
|
permission: 'subscribe',
|
||||||
|
feature: PERMISSION_FEATURE.SUBSCRIBE_SHARE,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -102,6 +115,7 @@ const router = createRouter({
|
|||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
permission: 'manage',
|
permission: 'manage',
|
||||||
|
feature: PERMISSION_FEATURE.MANAGE_WORKFLOW,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -111,6 +125,7 @@ const router = createRouter({
|
|||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
permission: 'subscribe',
|
permission: 'subscribe',
|
||||||
|
feature: PERMISSION_FEATURE.SUBSCRIBE_CALENDAR,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -120,6 +135,7 @@ const router = createRouter({
|
|||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
permission: 'manage',
|
permission: 'manage',
|
||||||
|
feature: PERMISSION_FEATURE.MANAGE_DOWNLOADING,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -129,6 +145,7 @@ const router = createRouter({
|
|||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
permission: 'manage',
|
permission: 'manage',
|
||||||
|
feature: PERMISSION_FEATURE.MANAGE_HISTORY,
|
||||||
hideFooter: true,
|
hideFooter: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -139,6 +156,7 @@ const router = createRouter({
|
|||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
permission: 'manage',
|
permission: 'manage',
|
||||||
|
feature: PERMISSION_FEATURE.MANAGE_SITE,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -193,6 +211,7 @@ const router = createRouter({
|
|||||||
keepAliveByFullPath: true,
|
keepAliveByFullPath: true,
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
permission: 'discovery',
|
permission: 'discovery',
|
||||||
|
feature: PERMISSION_FEATURE.DISCOVERY_EXPLORE,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -217,6 +236,7 @@ const router = createRouter({
|
|||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
permission: 'discovery',
|
permission: 'discovery',
|
||||||
|
feature: PERMISSION_FEATURE.DISCOVERY_EXPLORE,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -226,6 +246,7 @@ const router = createRouter({
|
|||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
permission: 'manage',
|
permission: 'manage',
|
||||||
|
feature: PERMISSION_FEATURE.MANAGE_FILEMANAGER,
|
||||||
hideFooter: true,
|
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) {
|
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') {
|
if (to.name !== 'plugin-app') {
|
||||||
return undefined
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pluginId = String(to.params.pluginId || '')
|
const pluginId = String(to.params.pluginId || '')
|
||||||
@@ -279,7 +303,12 @@ async function getRoutePermission(to: any): Promise<UserPermissionKey | undefine
|
|||||||
await pluginSidebarNavStore.ensureSidebarNav()
|
await pluginSidebarNavStore.ensureSidebarNav()
|
||||||
|
|
||||||
const navItem = pluginSidebarNavStore.items.find(item => item.plugin_id === pluginId && item.nav_key === navKey)
|
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')
|
next('/login')
|
||||||
} else if (to.meta.requiresAuth) {
|
} else if (to.meta.requiresAuth) {
|
||||||
const routePermission = await getRoutePermission(to)
|
const routePermission = await getRoutePermission(to)
|
||||||
if (!routePermission) {
|
if (!routePermission.permission && !routePermission.feature) {
|
||||||
next()
|
next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const allowed = hasPermission(
|
const allowed = hasItemPermission(
|
||||||
buildUserPermissionContext(userStore.superUser, userStore.permissions),
|
|
||||||
routePermission,
|
routePermission,
|
||||||
|
buildUserPermissionContext(userStore.superUser, userStore.permissions),
|
||||||
)
|
)
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
setRequestNavigatingState(false)
|
setRequestNavigatingState(false)
|
||||||
|
|||||||
+198
-14
@@ -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 {
|
export interface UserPermissions {
|
||||||
discovery: boolean // 发现权限
|
discovery: boolean // 发现权限
|
||||||
search: boolean // 搜索权限
|
search: boolean // 搜索权限
|
||||||
subscribe: boolean // 订阅权限
|
subscribe: boolean // 订阅权限
|
||||||
manage: boolean // 管理权限
|
manage: boolean // 管理权限
|
||||||
admin?: boolean // 管理员权限,仅用于前端入口标识,实际由 is_superuser 决定
|
admin?: boolean // 管理员权限,仅用于前端入口标识,实际由 is_superuser 决定
|
||||||
|
features?: UserPermissionFeatureMap // 功能级权限,缺省功能默认继承分类权限
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserPermissionKey = keyof UserPermissions
|
|
||||||
export type UserPermissionContext = UserPermissions & { is_superuser?: boolean; [key: string]: unknown }
|
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 {
|
export function buildUserPermissionContext(isSuperuser: boolean, permissions: Partial<UserPermissions> = {}): UserPermissionContext {
|
||||||
return {
|
return {
|
||||||
is_superuser: isSuperuser,
|
is_superuser: isSuperuser,
|
||||||
...DEFAULT_PERMISSIONS,
|
...normalizeUserPermissions(permissions),
|
||||||
...permissions,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +187,7 @@ export const DEFAULT_PERMISSIONS: UserPermissions = {
|
|||||||
subscribe: true,
|
subscribe: true,
|
||||||
manage: false,
|
manage: false,
|
||||||
admin: false,
|
admin: false,
|
||||||
|
features: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 管理员权限配置
|
// 管理员权限配置
|
||||||
@@ -36,9 +197,10 @@ export const ADMIN_PERMISSIONS: UserPermissions = {
|
|||||||
subscribe: true,
|
subscribe: true,
|
||||||
manage: true,
|
manage: true,
|
||||||
admin: true,
|
admin: true,
|
||||||
|
features: buildDefaultFeaturePermissions(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 权限检查函数
|
/** 检查用户是否拥有指定权限分类。 */
|
||||||
export function hasPermission(userPermissions: any, permission: UserPermissionKey): boolean {
|
export function hasPermission(userPermissions: any, permission: UserPermissionKey): boolean {
|
||||||
// 如果用户是超级用户,拥有所有权限
|
// 如果用户是超级用户,拥有所有权限
|
||||||
if (userPermissions?.is_superuser === true) {
|
if (userPermissions?.is_superuser === true) {
|
||||||
@@ -55,31 +217,53 @@ export function hasPermission(userPermissions: any, permission: UserPermissionKe
|
|||||||
return permissions[permission] === true
|
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 {
|
export function hasAnyPermission(userPermissions: any, permissionList: UserPermissionKey[]): boolean {
|
||||||
return permissionList.some(permission => hasPermission(userPermissions, permission))
|
return permissionList.some(permission => hasPermission(userPermissions, permission))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否有所有权限
|
/** 批量检查是否拥有全部权限分类。 */
|
||||||
export function hasAllPermissions(userPermissions: any, permissionList: UserPermissionKey[]): boolean {
|
export function hasAllPermissions(userPermissions: any, permissionList: UserPermissionKey[]): boolean {
|
||||||
return permissionList.every(permission => hasPermission(userPermissions, permission))
|
return permissionList.every(permission => hasPermission(userPermissions, permission))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统一检查带 permission 字段的入口,避免菜单、按钮、快捷入口各自实现判断。
|
/** 统一检查带 permission / feature 字段的入口,避免菜单、按钮、快捷入口各自实现判断。 */
|
||||||
export function hasItemPermission(item: PermissionProtectedItem, userPermissions: any): boolean {
|
export function hasItemPermission(item: PermissionProtectedItem, userPermissions: any): boolean {
|
||||||
if (!item.permission) {
|
if (item.permission && !hasPermission(userPermissions, item.permission)) {
|
||||||
return true
|
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[] {
|
export function filterItemsByPermission<T extends PermissionProtectedItem>(items: T[], userPermissions: any): T[] {
|
||||||
return items.filter(item => hasItemPermission(item, userPermissions))
|
return items.filter(item => hasItemPermission(item, userPermissions))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据权限过滤菜单项
|
/** 根据权限过滤菜单项。 */
|
||||||
export function filterMenusByPermission(menus: any[], userPermissions: any): any[] {
|
export function filterMenusByPermission(menus: any[], userPermissions: any): any[] {
|
||||||
return filterItemsByPermission(menus, userPermissions)
|
return filterItemsByPermission(menus, userPermissions)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Composer } from 'vue-i18n'
|
|||||||
import type { NavMenu } from '@/@layouts/types'
|
import type { NavMenu } from '@/@layouts/types'
|
||||||
import type { PluginSidebarNavItem } from '@/api/types'
|
import type { PluginSidebarNavItem } from '@/api/types'
|
||||||
import { pluginSidebarSectionToHeaderKey } from '@/router/i18n-menu'
|
import { pluginSidebarSectionToHeaderKey } from '@/router/i18n-menu'
|
||||||
import { filterMenusByPermission } from '@/utils/permission'
|
import { buildPluginPermissionFeatureKey, filterMenusByPermission } from '@/utils/permission'
|
||||||
|
|
||||||
export type PluginNavMenuEntry = {
|
export type PluginNavMenuEntry = {
|
||||||
navMenu: NavMenu & { permission?: string }
|
navMenu: NavMenu & { permission?: string }
|
||||||
@@ -31,6 +31,7 @@ export function navMenuFromPluginSidebarItem(
|
|||||||
},
|
},
|
||||||
header,
|
header,
|
||||||
permission: item.permission ?? undefined,
|
permission: item.permission ?? undefined,
|
||||||
|
feature: buildPluginPermissionFeatureKey(item.plugin_id, item.nav_key),
|
||||||
} as NavMenu & { permission?: string }
|
} as NavMenu & { permission?: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user