Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ui/src/api/API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ API 枚举与类型统一在 `src/api` 范围内管理,相关规则由本文
`resource_user_permission/resource/<target>/resource/<resource>`;与 System 用户视角的
`user_resource_permission` 区分。分页使用 `ParamsPage`,直接返回
`ResponsePage<ResourceUserPermission>`;提交 `ResourceUserPermissionPayload[]`。
查询和更新接口内部调用 `getWorkspaceId()` 获取路由工作空间,调用方不传工作空间 ID。
`ResourceAuthorizationTargetType` 包含资源类型和三个 `_FOLDER` 类型,文件夹类型用于后端
鉴权。包含子资源时传 `include_children: true` 及经过管理权限筛选的 `folder_ids`,
普通资源或仅当前文件夹不传子文件夹 ID。loading、刷新及成功提示由抽屉负责。
21 changes: 16 additions & 5 deletions ui/src/api/admin/system/resource-authorization.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,43 @@
import { get, put } from '../core/request'
import type { Dict, ResourceAuthorizationType, ResourcePermissionItem, ResourcePermissionPayload } from '@/api/types'

/** 系统管理资源授权 */
/** 系统管理用户资源授权 */
const prefix = (workspaceId: string) => `/workspace/${workspaceId}/user_resource_permission`
const groupPrefix = (workspaceId: string) => `/workspace/${workspaceId}/user_group_resource_permission`

/** 获取指定空间、指定用户、指定资源类型的权限列表。 */
const getUserResourcePermissions = (workspaceId: string, userId: string, resource: ResourceAuthorizationType, query?: Dict<unknown>) => {
return get<ResourcePermissionItem[]>(`${prefix(workspaceId)}/user/${userId}/resource/${resource}`, query)
}

/** 更新指定空间、指定用户、指定资源类型的权限列表。 */
const putUserResourcePermissions = (workspaceId: string, userId: string, resource: ResourceAuthorizationType, permissions: ResourcePermissionPayload[]) => {
const putUserResourcePermissions = (
workspaceId: string,
userId: string,
resource: ResourceAuthorizationType,
permissions: ResourcePermissionPayload[],
) => {
return put<ResourcePermissionPayload[], ResourcePermissionPayload[]>(`${prefix(workspaceId)}/user/${userId}/resource/${resource}`, permissions)
}

/** 系统管理用户组资源授权 */
const groupPrefix = (workspaceId: string) => `/workspace/${workspaceId}/user_group_resource_permission`

/** 获取指定空间、指定用户组、指定资源类型的权限列表。 */
const getUserGroupResourcePermissions = (workspaceId: string, userId: string, resource: ResourceAuthorizationType, query?: Dict<unknown>) => {
return get<ResourcePermissionItem[]>(`${groupPrefix(workspaceId)}/user_group/${userId}/resource/${resource}`, query)
}

/** 更新指定空间、指定用户组、指定资源类型的权限列表。 */
const putUserGroupResourcePermissions = (workspaceId: string, userId: string, resource: ResourceAuthorizationType, permissions: ResourcePermissionPayload[]) => {
const putUserGroupResourcePermissions = (
workspaceId: string,
userId: string,
resource: ResourceAuthorizationType,
permissions: ResourcePermissionPayload[],
) => {
return put<ResourcePermissionPayload[], ResourcePermissionPayload[]>(
`${groupPrefix(workspaceId)}/user_group/${userId}/resource/${resource}`,
permissions,
)
}


export default { getUserResourcePermissions, putUserResourcePermissions, getUserGroupResourcePermissions, putUserGroupResourcePermissions }
33 changes: 15 additions & 18 deletions ui/src/api/admin/workspace/resource-authorization.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
import { get, put } from '../core/request'
import type { ParamsPage, ResponsePage } from '../core/types'
import type { Dict, ResourceAuthorizationTargetType, ResourceUserPermission, ResourceUserPermissionPayload } from '@/api/types'
import { getWorkspaceId } from '@/utils/resource-context'

const prefix = (workspaceId: string, targetId: string, resource: ResourceAuthorizationTargetType) =>
`/workspace/${workspaceId}/resource_user_permission/resource/${targetId}/resource/${resource}`

const getPrefix = () => {
const workspaceId = getWorkspaceId()
return `/workspace/${workspaceId}`
}
/** 获取指定资源或文件夹的用户权限分页列表。 */
const getResourceAuthorization = (
workspaceId: string,
targetId: string,
resource: ResourceAuthorizationTargetType,
page: ParamsPage,
query?: Dict<unknown>,
) => {
return get<ResponsePage<ResourceUserPermission>>(`${prefix(workspaceId, targetId, resource)}/${page.currentPage}/${page.pageSize}`, query)
const getResourceAuthorization = (targetId: string, resource: ResourceAuthorizationTargetType, page: ParamsPage, query?: Dict<unknown>) => {
return get<ResponsePage<ResourceUserPermission>>(
`${getPrefix()}/resource_user_permission/resource/${targetId}/resource/${resource}/${page.currentPage}/${page.pageSize}`,
query,
)
}

/** 更新资源的用户权限,可同时应用到有管理权限的子文件夹及资源。 */
const putResourceAuthorization = (
workspaceId: string,
targetId: string,
resource: ResourceAuthorizationTargetType,
permissions: ResourceUserPermissionPayload[],
) => {
return put<ResourceUserPermissionPayload[], ResourceUserPermissionPayload[]>(prefix(workspaceId, targetId, resource), permissions)
const putResourceAuthorization = (targetId: string, resource: ResourceAuthorizationTargetType, permissions: ResourceUserPermissionPayload[]) => {
return put<ResourceUserPermissionPayload[], ResourceUserPermissionPayload[]>(
`${getPrefix()}/resource_user_permission/resource/${targetId}/resource/${resource}`,
permissions,
)
}

export default { getResourceAuthorization, putResourceAuthorization }
12 changes: 8 additions & 4 deletions ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,10 @@ Workspace 的文件夹虚拟树业务组件。组件根据当前资源上下文
新 ID 写入路由,但会清除已有的 `folderId`,避免刷新页面后恢复到旧文件夹。隐藏“全部”入口的
移动目录树继续以调用方传入的 `v-model` 为准。

可编辑文件夹的菜单提供“资源授权”,根据对应资源模块的 `folderAuth(folder.id)` 控制入口。
点击后复用 `ResourceAuthorizationDrawer`,传入 Workspace 资源授权 API、当前 `source` 和
原始完整文件夹子树;搜索过滤不缩减子资源授权范围。只读选择场景不挂载授权抽屉。

```vue
<script setup lang="ts">
import { RESOURCE_TYPE } from '@/api/enums'
Expand Down Expand Up @@ -1320,12 +1324,12 @@ import WorkspaceRelationTags from '@/components/business/workspace-relation-tags
手动导入 `business/resource-authorization-drawer/index.vue`。调用方传入完整 `api`
(`typeof ResourceAuthorizationApi`)和 `type`(`ResourceAuthorizationTargetType`),不由抽屉
根据路由路径选择接口。当前后端的资源用户授权统一使用
`api/admin/workspace/resource-authorization.ts`。通过 `open(id, folder?, workspaceId?)` 打开;
System 场景显式传入资源所属工作空间,Workspace 场景可读取当前工作空间
`api/admin/workspace/resource-authorization.ts`。通过 `open(id, folder?)` 打开;
接口请求与文件夹鉴权均直接通过 `getWorkspaceId()` 读取路由中的工作空间,无需额外传入或保存
`isFolder`、`isRootFolder` 标记文件夹及根目录,传入文件夹类型也可识别文件夹。

支持姓名、用户名、权限及商业版本角色搜索,跨页选择和单人、批量授权;搜索后清空选择。
根目录隐藏“不授权”,返回的“不授权”按“查看”展示。包含子资源时必须传入完整文件夹子树,
抽屉根据目标工作空间筛选有管理权限的文件夹 ID;没有可管理文件夹时禁用该范围。
抽屉根据当前路由工作空间筛选有管理权限的文件夹 ID;没有可管理文件夹时禁用该范围。
`PermissionConfigDialog` 只收集权限和范围,保存成功才关闭并刷新,失败保留配置。
保存成功触发 `refresh`,打开和关闭后统一重置临时数据,过期查询不写回
保存成功触发 `refresh`,关闭后统一重置临时数据
19 changes: 11 additions & 8 deletions ui/src/components/business/folder-tree/VirtualizedTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@ interface DropTargetInfo {
siblings?: FolderStat[]
}

const props = withDefaults(defineProps<{ canEdit?: boolean; currentNodeKey?: string; data?: FolderTreeNode[]; draggable?: boolean; filterText?: string }>(), {
canEdit: true,
currentNodeKey: '',
data: () => [],
draggable: false,
filterText: '',
})
const props = withDefaults(
defineProps<{ canEdit?: boolean; currentNodeKey?: string; data?: FolderTreeNode[]; draggable?: boolean; filterText?: string }>(),
{
canEdit: true,
currentNodeKey: '',
data: () => [],
draggable: false,
filterText: '',
},
)

const emit = defineEmits<{
create: [folder: FolderItem]
Expand Down Expand Up @@ -153,9 +156,9 @@ function handleAfterDrop() {
--el-scrollbar-hover-opacity: 0.5;
--el-scrollbar-hover-bg-color: var(--el-text-color-secondary);
scrollbar-color: transparent transparent;
scrollbar-gutter: stable;
scrollbar-width: thin;


&::-webkit-scrollbar {
background-color: transparent;
height: 6px;
Expand Down
14 changes: 14 additions & 0 deletions ui/src/components/business/folder-tree/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
import { computed, defineAsyncComponent, onMounted, ref, useTemplateRef } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import FolderApi from '@/api/admin/workspace/folder'
import ResourceAuthorizationApi from '@/api/admin/workspace/resource-authorization'
import type { FolderSource, FolderItem } from '@/api/types'
import { FOLDER_SORT, type FolderSort } from './types'
import { FOLDER_ENTRIES, FOLDER_ENTRY_ID } from '@/constants'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import VirtualizedTree from './VirtualizedTree.vue'
import FolderFormDialog from './FolderFormDialog.vue'
import ResourceAuthorizationDrawer from '@/components/business/resource-authorization-drawer/index.vue'
import { useStore } from '@/stores'
import { getWorkspaceId } from '@/utils/resource-context'

Expand Down Expand Up @@ -214,6 +216,13 @@ function handleOpenEditFolder(folder: FolderItem) {
folderFormDialogRef.value?.open(folder.id, folder)
}

/* 文件夹资源授权 */
const resourceAuthorizationDrawerRef = useTemplateRef<InstanceType<typeof ResourceAuthorizationDrawer>>('resourceAuthorizationDrawerRef')

function handleOpenResourceAuthorization(folder: FolderItem) {
resourceAuthorizationDrawerRef.value?.open(folder.id, folder)
}

// 创建后选中新文件夹;编辑当前文件夹时同步最新信息。
function handleFolderRefresh(folder: FolderItem, isEdit: boolean) {
return loadFolders().then(() => {
Expand Down Expand Up @@ -410,6 +419,10 @@ defineExpose({ refresh: loadFolders, openCreate: handleOpenCreateFolder })
<template #icon><MkIcon name="icon_move2_outlined" /></template>
<span>移动到</span>
</MkDropdownItem>
<MkDropdownItem @click="handleOpenResourceAuthorization(row)">
<template #icon><MkIcon name="icon_passkeys_outlined" /></template>
<span>资源授权</span>
</MkDropdownItem>
<MkDropdownItem divided @click="handleDeleteFolder(row)">
<template #icon><MkIcon name="icon_delete-trash_outlined" /></template>
<span>删除</span>
Expand All @@ -420,5 +433,6 @@ defineExpose({ refresh: loadFolders, openCreate: handleOpenCreateFolder })

<FolderFormDialog ref="folderFormDialogRef" :title="formTitle" :source="props.source" @refresh="handleFolderRefresh" />
<MoveToDialog v-if="canEdit" ref="moveToDialogRef" :loading="moveSubmitting" :source="props.source" @submit="handleMoveFolder" />
<ResourceAuthorizationDrawer v-if="canEdit" ref="resourceAuthorizationDrawerRef" :api="ResourceAuthorizationApi" :type="source" is-folder />
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,7 @@ const permission = ref<ResourcePermission>()
const includeChildren = ref(false)
const scopeOnly = ref(false)

function resetData() {
permission.value = undefined
includeChildren.value = false
scopeOnly.value = false
}

function open(currentPermission?: ResourcePermission) {
resetData()
permission.value = currentPermission
scopeOnly.value = Boolean(currentPermission)
visible.value = true
Expand All @@ -38,11 +31,17 @@ function close() {
visible.value = false
}

function resetData() {
permission.value = undefined
includeChildren.value = false
scopeOnly.value = false
}

defineExpose({ open, close })
</script>

<template>
<MkDialog v-model="visible" :title="scopeOnly ? '生效资源' : '配置权限'" width="500" :show-close="!loading" @closed="resetData">
<MkDialog v-model="visible" :title="scopeOnly ? '生效资源' : '配置权限'" :show-close="!loading" @closed="resetData">
<el-radio-group v-if="!scopeOnly" v-model="permission" class="vertical-radio-group" :disabled="loading">
<el-radio v-for="option in options" :key="option.value" :value="option.value">
<p>{{ option.label }}</p>
Expand All @@ -51,12 +50,12 @@ defineExpose({ open, close })
</el-radio-group>
<template v-if="isFolder">
<template v-if="!scopeOnly">
<el-divider />
<el-divider class="my-4!" />
<h6 class="mb-2">生效资源</h6>
</template>
<el-radio-group v-model="includeChildren" class="vertical-radio-group" :disabled="loading">
<el-radio :value="false">仅当前文件夹</el-radio>
<el-radio :value="true" :disabled="!canIncludeChildren">包含所有有管理权限的子文件夹及资源</el-radio>
<el-radio :value="false">仅当前资源</el-radio>
<el-radio :value="true" :disabled="!canIncludeChildren">包含所有子文件夹</el-radio>
</el-radio-group>
</template>
<template #footer>
Expand Down
45 changes: 16 additions & 29 deletions ui/src/components/business/resource-authorization-drawer/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ const searchFields = computed(() => [

/* 查询与跨页选择 */
const drawerVisible = ref(false)
const workspaceId = ref('')
const targetId = ref('')
const folderData = ref<FolderItem>()
const loading = ref(false)
Expand All @@ -62,25 +61,21 @@ const selectedUsers = ref<ResourceUserPermission[]>([])
const paginationConfig = ref({ currentPage: 1, pageSize: 20, total: 0 })
const searchQuery = ref<Dict<unknown>>()
const tableRef = useTemplateRef('tableRef')
let requestId = 0
let sessionId = 0

function loadPermissions() {
if (!targetId.value) return Promise.resolve()
const currentRequestId = ++requestId
loading.value = true
return props.api
.getResourceAuthorization(workspaceId.value, targetId.value, authorizationType.value, paginationConfig.value, searchQuery.value)
.getResourceAuthorization(targetId.value, authorizationType.value, paginationConfig.value, searchQuery.value)
.then(({ records, total }) => {
if (currentRequestId !== requestId) return
permissionUsers.value = records.map((user) => ({
...user,
permission: props.isRootFolder && user.permission === RESOURCE_PERMISSION.NOT_AUTH ? RESOURCE_PERMISSION.VIEW : user.permission,
}))
paginationConfig.value.total = total
})
.finally(() => {
if (currentRequestId === requestId) loading.value = false
loading.value = false
})
}

Expand All @@ -100,17 +95,17 @@ function handleSelectionChange(selection: unknown[]) {
selectedUsers.value = selection as ResourceUserPermission[]
}

/* 子资源范围:使用目标工作空间鉴权,不依赖路由路径推断。 */
/* 子资源范围:直接使用路由中的工作空间鉴权。 */
const managedFolderIds = computed(() => {
const permissions = folderPermissions.value
if (!folderData.value || !permissions) return []
const canManageAll = hasPermission(
[RoleConstants.ADMIN, new Role(RoleConstants.WORKSPACE_MANAGE.name, workspaceId.value), permissions.auth.getWorkspaceManageFlagPermission()],
[RoleConstants.ADMIN, new Role(RoleConstants.WORKSPACE_MANAGE.name, getWorkspaceId()), permissions.auth.getWorkspaceManageFlagPermission()],
undefined,
{ workspaceId: workspaceId.value },
{ workspaceId: getWorkspaceId() },
)
function collectFolders(folder: FolderItem): string[] {
const canManage = canManageAll || hasPermission(buildBaseResourcePermission(permissions!.edit, folder.id, workspaceId.value))
const canManage = canManageAll || hasPermission(buildBaseResourcePermission(permissions!.edit, folder.id, getWorkspaceId()))
return [...(canManage ? [folder.id] : []), ...(folder.children ?? []).flatMap(collectFolders)]
}
return collectFolders(folderData.value)
Expand Down Expand Up @@ -139,7 +134,6 @@ function handlePermissionChange(value: string | number | boolean | undefined, us

function submitPermissions(permission: ResourcePermission, includeChildren: boolean) {
if (submitting.value || !pendingUserIds.value.length || (includeChildren && !managedFolderIds.value.length)) return
const currentSessionId = sessionId
const permissions: ResourceUserPermissionPayload[] = pendingUserIds.value.map((userId) => ({
user_id: userId,
permission,
Expand All @@ -148,9 +142,8 @@ function submitPermissions(permission: ResourcePermission, includeChildren: bool
}))
submitting.value = true
return props.api
.putResourceAuthorization(workspaceId.value, targetId.value, authorizationType.value, permissions)
.putResourceAuthorization(targetId.value, authorizationType.value, permissions)
.then(() => {
if (currentSessionId !== sessionId) return
MsgSuccess('提交成功')
configDialogRef.value?.close()
pendingUserIds.value = []
Expand All @@ -159,16 +152,19 @@ function submitPermissions(permission: ResourcePermission, includeChildren: bool
return loadPermissions()
})
.finally(() => {
if (currentSessionId === sessionId) submitting.value = false
submitting.value = false
})
}

/* 抽屉生命周期 */
function open(id: string, folder?: FolderItem) {
targetId.value = id
folderData.value = folder ? cloneDeep(folder) : undefined
drawerVisible.value = true
loadPermissions()
}

function resetData() {
sessionId += 1
requestId += 1
targetId.value = ''
workspaceId.value = ''
folderData.value = undefined
permissionUsers.value = []
searchQuery.value = undefined
Expand All @@ -179,20 +175,11 @@ function resetData() {
clearSelection()
}

function open(id: string, folder?: FolderItem, resourceWorkspaceId?: string) {
resetData()
targetId.value = id
workspaceId.value = resourceWorkspaceId ?? folder?.workspace_id ?? getWorkspaceId() ?? 'default'
folderData.value = folder ? cloneDeep(folder) : undefined
drawerVisible.value = true
loadPermissions()
}

defineExpose({ open })
</script>

<template>
<MkDrawer v-model="drawerVisible" title="资源授权" size="850" :show-close="!submitting" @closed="resetData">
<MkDrawer v-model="drawerVisible" title="资源授权" :size="920" :show-close="!submitting" @closed="resetData">
<div class="flex-between mb-4 gap-4">
<el-button type="primary" :disabled="!selectedUsers.length || loading || submitting" @click="handleOpenBatchConfig">配置权限</el-button>
<MkComplexSearch :fields="searchFields" @change="handleSearch" />
Expand Down
Loading
Loading