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
14 changes: 14 additions & 0 deletions ui/src/api/API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,17 @@ API 枚举与类型统一在 `src/api` 范围内管理,相关规则由本文
`ResourceAuthorizationTargetType` 包含资源类型和三个 `_FOLDER` 类型,文件夹类型用于后端
鉴权。包含子资源时传 `include_children: true` 及经过管理权限筛选的 `folder_ids`,
普通资源或仅当前文件夹不传子文件夹 ID。loading、刷新及成功提示由抽屉负责。

用户组视角使用同文件的 `getResourceUserGroupAuthorization` 和
`putResourceUserGroupAuthorization`,请求路径为
`resource_user_group_permission/resource/<target>/resource/<resource>`。分页返回
`ResponsePage<ResourceUserGroupPermission>`(`id`、`name`、`count`、`permission`),
名称查询参数为 `name`,提交 `ResourceUserGroupPermissionPayload[]`,对象 ID 字段为
`user_group_id`;文件夹生效范围参数与用户授权一致。

System 资源管理的用户授权维护在 `admin/system/resource-management/resource-authorization.ts`,
使用 `/system/workspace/<workspaceId>/resource_management/resource/<target>/resource/<resource>`。
查询与保存签名、分页类型、响应类型及提交类型与 Workspace 用户授权保持一致;工作空间 ID
由接口内部读取,loading 由组件管理。`ResourceAuthorizationDrawer` 内部通过
`isSystemResource()` 选择完整的用户授权 API 对象,作为该抽屉的范围选择例外;用户组仍使用
现有 Workspace 接口,不推测 System 用户组路径。
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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 getPrefix = () => `/system/workspace/${getWorkspaceId()}/resource_management`

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

/** 更新 System 资源管理中的用户权限及文件夹生效范围。 */
const putResourceAuthorization = (targetId: string, resource: ResourceAuthorizationTargetType, permissions: ResourceUserPermissionPayload[]) => {
return put<ResourceUserPermissionPayload[], ResourceUserPermissionPayload[]>(
`${getPrefix()}/resource/${targetId}/resource/${resource}`,
permissions,
)
}

export default { getResourceAuthorization, putResourceAuthorization }
31 changes: 29 additions & 2 deletions ui/src/api/admin/workspace/resource-authorization.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { get, put } from '../core/request'
import type { ParamsPage, ResponsePage } from '../core/types'
import type { Dict, ResourceAuthorizationTargetType, ResourceUserPermission, ResourceUserPermissionPayload } from '@/api/types'
import type {
Dict,
ResourceUserGroupPermission,
ResourceUserGroupPermissionPayload,
ResourceAuthorizationTargetType,
ResourceUserPermission,
ResourceUserPermissionPayload,
} from '@/api/types'
import { getWorkspaceId } from '@/utils/resource-context'

const getPrefix = () => {
Expand All @@ -23,4 +30,24 @@ const putResourceAuthorization = (targetId: string, resource: ResourceAuthorizat
)
}

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

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

export default { getResourceAuthorization, putResourceAuthorization, getResourceUserGroupAuthorization, putResourceUserGroupAuthorization }
15 changes: 15 additions & 0 deletions ui/src/api/types/resource-authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ export interface ResourceUserPermissionPayload {
folder_ids?: string[]
}

/** 指定资源下的用户组及其权限。 */
export interface ResourceUserGroupPermission {
id: string
name: string
count: number
permission: ResourcePermission
}

export interface ResourceUserGroupPermissionPayload {
user_group_id: string
permission: ResourcePermission
include_children?: boolean
folder_ids?: string[]
}

export interface ResourcePermissionItem {
auth_target_type: ResourceAuthorizationType
children?: ResourcePermissionItem[]
Expand Down
40 changes: 32 additions & 8 deletions ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,13 @@ src/components/
│ │ └── ModelParamsDialog.vue # 模型参数动态表单弹窗
│ ├── workspace-dropdown/
│ │ └── index.vue # 工作空间选择下拉框
│ ├── resource-authorization-drawer/ # 资源用户授权抽屉与权限配置弹窗
│ ├── resource-authorization-drawer/ # 资源用户组、用户授权抽屉与权限配置弹窗
│ │ ├── index.vue
│ │ ├── UserAuthorization.vue # 按用户查询与授权
│ │ ├── types.ts # 内部公共 Props 与权限选项类型
│ │ ├── user-group/
│ │ │ ├── UserGroupAuthorization.vue # 按用户组查询与授权
│ │ │ └── UserGroupMembersDrawer.vue # 用户组成员查询抽屉
│ │ └── PermissionConfigDialog.vue
│ └── workspace-relation-tags/
│ └── index.vue # 标签及关联工作空间展示
Expand Down Expand Up @@ -1321,15 +1326,34 @@ import WorkspaceRelationTags from '@/components/business/workspace-relation-tags

### ResourceAuthorizationDrawer

手动导入 `business/resource-authorization-drawer/index.vue`。调用方传入完整 `api`
(`typeof ResourceAuthorizationApi`)和 `type`(`ResourceAuthorizationTargetType`),不由抽屉
根据路由路径选择接口。当前后端的资源用户授权统一使用
`api/admin/workspace/resource-authorization.ts`。通过 `open(id, folder?)` 打开;
接口请求与文件夹鉴权均直接通过 `getWorkspaceId()` 读取路由中的工作空间,无需额外传入或保存。
手动导入 `business/resource-authorization-drawer/index.vue`,调用方传入
`type`(`ResourceAuthorizationTargetType`),通过 `open(id, folder?)` 打开,不再传入 API。
抽屉内部使用 `isSystemResource()` 判断:用户授权在 System 资源管理中选择
`api/admin/system/resource-management/resource-authorization.ts`,其他范围选择 Workspace 授权接口。
用户组授权继续使用 Workspace 的 `resource_user_group_permission` 接口,尚无独立 System 接口。
两个标签子组件只调用入口传入的完整 API 对象,不自行判断资源范围。
接口请求与文件夹鉴权均通过 `getWorkspaceId()` 读取路由中的工作空间。
`isFolder`、`isRootFolder` 标记文件夹及根目录,传入文件夹类型也可识别文件夹。

支持姓名、用户名、权限及商业版本角色搜索,跨页选择和单人、批量授权;搜索后清空选择。
入口 `index.vue` 只管理抽屉、标签切换和公共资源权限上下文;`UserGroupAuthorization.vue`
与 `UserAuthorization.vue` 分别管理各自的查询、分页、选择、单项及批量保存,复用
`PermissionConfigDialog`。仅挂载当前标签组件,切换后重新查询并重置临时状态;子组件通过
`v-model:submitting` 同步保存状态,通过 `refresh` 通知入口转发刷新。
默认打开“按用户组”,展示用户组名称、成员数和操作权限,支持名称搜索;成员数链接打开共享
`UserGroupMembersDrawer`。原用户授权放在“按用户”,保留姓名、用户名、权限及商业版本角色搜索。
两个标签均支持跨页选择、单项和批量授权;切换标签重置搜索、分页及选择,保存期间禁止切换。
查询仅应用最新响应,避免快速切换标签后串用用户与用户组数据;搜索后清空选择。
根目录隐藏“不授权”,返回的“不授权”按“查看”展示。包含子资源时必须传入完整文件夹子树,
抽屉根据当前路由工作空间筛选有管理权限的文件夹 ID;没有可管理文件夹时禁用该范围。
`PermissionConfigDialog` 只收集权限和范围,保存成功才关闭并刷新,失败保留配置。
保存成功触发 `refresh`,关闭后统一重置临时数据。
保存成功触发 `refresh`,关闭动画结束后重置临时数据并触发 `closed`,供资源 Action 卸载按需挂载的抽屉。

### UserGroupMembersDrawer

手动导入 `business/resource-authorization-drawer/user-group/UserGroupMembersDrawer.vue`,通过 `open(userGroup)` 传入
`SystemUserGroup`,按其 `workspace_id` 查询成员。支持用户名、姓名搜索和分页,角色使用
`MkTagGroup` 展示;供系统资源授权页面和资源授权抽屉共同复用。

资源授权抽屉内部公共展示 Props 放在同目录 `types.ts`,权限选项类型从
`RESOURCE_PERMISSION_OPTIONS` 派生;用户和用户组 API Props 分别保留完整 API 对象类型,
用户组组件只接收具备用户组方法的 Workspace API。
3 changes: 1 addition & 2 deletions ui/src/components/business/folder-tree/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
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'
Expand Down Expand Up @@ -433,6 +432,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 />
<ResourceAuthorizationDrawer v-if="canEdit" ref="resourceAuthorizationDrawerRef" :type="source" is-folder />
</div>
</template>
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { OptionItem, ResourcePermission } from '@/api/types'
import type { ResourcePermission } from '@/api/types'

import type { ResourcePermissionOption } from './types'

const props = defineProps<{
options: (OptionItem<ResourcePermission> & { description: string })[]
options: ResourcePermissionOption[]
isFolder: boolean
canIncludeChildren: boolean
loading: boolean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<script setup lang="ts">
import { computed, onMounted, ref, useTemplateRef } from 'vue'
import type ResourceAuthorizationApi from '@/api/admin/workspace/resource-authorization'
import type SystemResourceAuthorizationApi from '@/api/admin/system/resource-management/resource-authorization'
import { RESOURCE_PERMISSION } from '@/api/enums'
import type { Dict, ResourcePermission, ResourceUserPermission } from '@/api/types'
import { MsgSuccess } from '@/utils/message'
import MkTable from '@/components/global/mk-table/index.vue'
import PermissionConfigDialog from './PermissionConfigDialog.vue'
import { useStore } from '@/stores'

import type { ResourceAuthorizationContentProps, ResourcePermissionOption } from './types'

interface Props extends ResourceAuthorizationContentProps {
api: typeof ResourceAuthorizationApi | typeof SystemResourceAuthorizationApi
permissionOptions: ResourcePermissionOption[]
}

const props = defineProps<Props>()
const emit = defineEmits<{ refresh: [] }>()
const submitting = defineModel<boolean>('submitting', { required: true })

/* 查询与跨页选择 */
const { auth } = useStore()
const searchFields = computed(() => [
{ label: '姓名', value: 'nick_name' },
{ label: '用户名', value: 'username' },
{ label: '权限', value: 'permission', multiple: true, options: props.permissionOptions },
...(auth.isEE || auth.isPE ? [{ label: '角色', value: 'role' }] : []),
])
const loading = ref(false)
const permissionUsers = ref<ResourceUserPermission[]>([])
const selectedUsers = ref<ResourceUserPermission[]>([])
const paginationConfig = ref({ currentPage: 1, pageSize: 20, total: 0 })
const searchQuery = ref<Dict<unknown>>()
const tableRef = useTemplateRef('tableRef')

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

function clearSelection() {
selectedUsers.value = []
tableRef.value?.clearSelection()
}

function handleSearch(query?: Dict<unknown>) {
searchQuery.value = query
paginationConfig.value.currentPage = 1
clearSelection()
loadPermissions()
}

function handleSelectionChange(selection: unknown[]) {
selectedUsers.value = selection as ResourceUserPermission[]
}

/* 单项与批量保存:确认前不修改行数据,失败时保留待提交配置。 */
const configDialogRef = useTemplateRef<InstanceType<typeof PermissionConfigDialog>>('configDialogRef')
const pendingUserIds = ref<string[]>([])

function handleOpenBatchConfig() {
if (!selectedUsers.value.length || submitting.value) return
pendingUserIds.value = selectedUsers.value.map(({ id }) => id)
configDialogRef.value?.open()
}

function handlePermissionChange(value: string | number | boolean | undefined, subject: ResourceUserPermission) {
const permission = props.editablePermissionOptions.find((option) => option.value === value)?.value
if (!permission || permission === subject.permission || submitting.value) return
pendingUserIds.value = [subject.id]
if (props.isFolder) {
configDialogRef.value?.open(permission)
} else {
submitPermissions(permission, false)
}
}

function submitPermissions(permission: ResourcePermission, includeChildren: boolean) {
if (submitting.value || !pendingUserIds.value.length || (includeChildren && !props.managedFolderIds.length)) return
const permissionScope = {
permission,
include_children: includeChildren,
...(includeChildren ? { folder_ids: [...props.managedFolderIds] } : {}),
}
submitting.value = true
return props.api
.putResourceAuthorization(
props.targetId,
props.type,
pendingUserIds.value.map((id) => ({ user_id: id, ...permissionScope })),
)
.then(() => {
MsgSuccess('提交成功')
configDialogRef.value?.close()
pendingUserIds.value = []
clearSelection()
emit('refresh')
return loadPermissions()
})
.finally(() => {
submitting.value = false
})
}

/* 挂载当前标签时查询权限。 */
onMounted(() => loadPermissions())
</script>

<template>
<div>
<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" />
</div>
<MkTable
ref="tableRef"
v-loading="loading || submitting"
v-model:pagination-config="paginationConfig"
:data="permissionUsers"
:max-table-height="260"
@current-change="loadPermissions"
@size-change="loadPermissions"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="40" reserve-selection />
<el-table-column prop="nick_name" label="姓名" min-width="120" show-overflow-tooltip />
<el-table-column prop="username" label="用户名" min-width="120" show-overflow-tooltip />
<el-table-column v-if="auth.isEE || auth.isPE" label="角色" width="160">
<template #default="{ row }">
<MkTagGroup v-if="row.role_name?.length" :tags="row.role_name" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="权限" min-width="340">
<template #default="{ row }">
<el-radio-group :model-value="row.permission" :disabled="submitting" @change="handlePermissionChange($event, row)">
<el-radio v-for="option in editablePermissionOptions" :key="option.value" :value="option.value" class="mr-4!">{{
option.label
}}</el-radio>
</el-radio-group>
</template>
</el-table-column>
</MkTable>
<PermissionConfigDialog
ref="configDialogRef"
:options="editablePermissionOptions"
:is-folder="isFolder"
:can-include-children="managedFolderIds.length > 0"
:loading="submitting"
@submit="submitPermissions"
/>
</div>
</template>
Loading
Loading