Skip to content

Commit e2e55fa

Browse files
feat: resource auth
1 parent 4092955 commit e2e55fa

28 files changed

Lines changed: 736 additions & 189 deletions

File tree

‎ui/src/api/API_README.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,17 @@ API 枚举与类型统一在 `src/api` 范围内管理,相关规则由本文
184184
`ResourceAuthorizationTargetType` 包含资源类型和三个 `_FOLDER` 类型,文件夹类型用于后端
185185
鉴权。包含子资源时传 `include_children: true` 及经过管理权限筛选的 `folder_ids`,
186186
普通资源或仅当前文件夹不传子文件夹 ID。loading、刷新及成功提示由抽屉负责。
187+
188+
用户组视角使用同文件的 `getResourceUserGroupAuthorization` 和
189+
`putResourceUserGroupAuthorization`,请求路径为
190+
`resource_user_group_permission/resource/<target>/resource/<resource>`。分页返回
191+
`ResponsePage<ResourceUserGroupPermission>`(`id`、`name`、`count`、`permission`),
192+
名称查询参数为 `name`,提交 `ResourceUserGroupPermissionPayload[]`,对象 ID 字段为
193+
`user_group_id`;文件夹生效范围参数与用户授权一致。
194+
195+
System 资源管理的用户授权维护在 `admin/system/resource-management/resource-authorization.ts`,
196+
使用 `/system/workspace/<workspaceId>/resource_management/resource/<target>/resource/<resource>`。
197+
查询与保存签名、分页类型、响应类型及提交类型与 Workspace 用户授权保持一致;工作空间 ID
198+
由接口内部读取,loading 由组件管理。`ResourceAuthorizationDrawer` 内部通过
199+
`isSystemResource()` 选择完整的用户授权 API 对象,作为该抽屉的范围选择例外;用户组仍使用
200+
现有 Workspace 接口,不推测 System 用户组路径。
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { get, put } from '../../core/request'
2+
import type { ParamsPage, ResponsePage } from '../../core/types'
3+
import type { Dict, ResourceAuthorizationTargetType, ResourceUserPermission, ResourceUserPermissionPayload } from '@/api/types'
4+
import { getWorkspaceId } from '@/utils/resource-context'
5+
6+
const getPrefix = () => `/system/workspace/${getWorkspaceId()}/resource_management`
7+
8+
/** 获取 System 资源管理中指定资源或文件夹的用户权限分页列表。 */
9+
const getResourceAuthorization = (targetId: string, resource: ResourceAuthorizationTargetType, page: ParamsPage, query?: Dict<unknown>) => {
10+
return get<ResponsePage<ResourceUserPermission>>(
11+
`${getPrefix()}/resource/${targetId}/resource/${resource}/${page.currentPage}/${page.pageSize}`,
12+
query,
13+
)
14+
}
15+
16+
/** 更新 System 资源管理中的用户权限及文件夹生效范围。 */
17+
const putResourceAuthorization = (targetId: string, resource: ResourceAuthorizationTargetType, permissions: ResourceUserPermissionPayload[]) => {
18+
return put<ResourceUserPermissionPayload[], ResourceUserPermissionPayload[]>(
19+
`${getPrefix()}/resource/${targetId}/resource/${resource}`,
20+
permissions,
21+
)
22+
}
23+
24+
export default { getResourceAuthorization, putResourceAuthorization }

‎ui/src/api/admin/workspace/resource-authorization.ts‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { get, put } from '../core/request'
22
import type { ParamsPage, ResponsePage } from '../core/types'
3-
import type { Dict, ResourceAuthorizationTargetType, ResourceUserPermission, ResourceUserPermissionPayload } from '@/api/types'
3+
import type {
4+
Dict,
5+
ResourceUserGroupPermission,
6+
ResourceUserGroupPermissionPayload,
7+
ResourceAuthorizationTargetType,
8+
ResourceUserPermission,
9+
ResourceUserPermissionPayload,
10+
} from '@/api/types'
411
import { getWorkspaceId } from '@/utils/resource-context'
512

613
const getPrefix = () => {
@@ -23,4 +30,24 @@ const putResourceAuthorization = (targetId: string, resource: ResourceAuthorizat
2330
)
2431
}
2532

26-
export default { getResourceAuthorization, putResourceAuthorization }
33+
/** 获取指定资源或文件夹的用户组权限分页列表。 */
34+
const getResourceUserGroupAuthorization = (targetId: string, resource: ResourceAuthorizationTargetType, page: ParamsPage, query?: Dict<unknown>) => {
35+
return get<ResponsePage<ResourceUserGroupPermission>>(
36+
`${getPrefix()}/resource_user_group_permission/resource/${targetId}/resource/${resource}/${page.currentPage}/${page.pageSize}`,
37+
query,
38+
)
39+
}
40+
41+
/** 更新资源的用户组权限,可应用到有管理权限的子文件夹及资源。 */
42+
const putResourceUserGroupAuthorization = (
43+
targetId: string,
44+
resource: ResourceAuthorizationTargetType,
45+
permissions: ResourceUserGroupPermissionPayload[],
46+
) => {
47+
return put<ResourceUserGroupPermissionPayload[], ResourceUserGroupPermissionPayload[]>(
48+
`${getPrefix()}/resource_user_group_permission/resource/${targetId}/resource/${resource}`,
49+
permissions,
50+
)
51+
}
52+
53+
export default { getResourceAuthorization, putResourceAuthorization, getResourceUserGroupAuthorization, putResourceUserGroupAuthorization }

‎ui/src/api/types/resource-authorization.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,21 @@ export interface ResourceUserPermissionPayload {
2525
folder_ids?: string[]
2626
}
2727

28+
/** 指定资源下的用户组及其权限。 */
29+
export interface ResourceUserGroupPermission {
30+
id: string
31+
name: string
32+
count: number
33+
permission: ResourcePermission
34+
}
35+
36+
export interface ResourceUserGroupPermissionPayload {
37+
user_group_id: string
38+
permission: ResourcePermission
39+
include_children?: boolean
40+
folder_ids?: string[]
41+
}
42+
2843
export interface ResourcePermissionItem {
2944
auth_target_type: ResourceAuthorizationType
3045
children?: ResourcePermissionItem[]

‎ui/src/components/COMPONENT_README.md‎

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,13 @@ src/components/
5151
│ │ └── ModelParamsDialog.vue # 模型参数动态表单弹窗
5252
│ ├── workspace-dropdown/
5353
│ │ └── index.vue # 工作空间选择下拉框
54-
│ ├── resource-authorization-drawer/ # 资源用户授权抽屉与权限配置弹窗
54+
│ ├── resource-authorization-drawer/ # 资源用户组、用户授权抽屉与权限配置弹窗
5555
│ │ ├── index.vue
56+
│ │ ├── UserAuthorization.vue # 按用户查询与授权
57+
│ │ ├── types.ts # 内部公共 Props 与权限选项类型
58+
│ │ ├── user-group/
59+
│ │ │ ├── UserGroupAuthorization.vue # 按用户组查询与授权
60+
│ │ │ └── UserGroupMembersDrawer.vue # 用户组成员查询抽屉
5661
│ │ └── PermissionConfigDialog.vue
5762
│ └── workspace-relation-tags/
5863
│ └── index.vue # 标签及关联工作空间展示
@@ -1321,15 +1326,34 @@ import WorkspaceRelationTags from '@/components/business/workspace-relation-tags
13211326

13221327
### ResourceAuthorizationDrawer
13231328

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

1331-
支持姓名、用户名、权限及商业版本角色搜索,跨页选择和单人、批量授权;搜索后清空选择。
1338+
入口 `index.vue` 只管理抽屉、标签切换和公共资源权限上下文;`UserGroupAuthorization.vue`
1339+
与 `UserAuthorization.vue` 分别管理各自的查询、分页、选择、单项及批量保存,复用
1340+
`PermissionConfigDialog`。仅挂载当前标签组件,切换后重新查询并重置临时状态;子组件通过
1341+
`v-model:submitting` 同步保存状态,通过 `refresh` 通知入口转发刷新。
1342+
默认打开“按用户组”,展示用户组名称、成员数和操作权限,支持名称搜索;成员数链接打开共享
1343+
`UserGroupMembersDrawer`。原用户授权放在“按用户”,保留姓名、用户名、权限及商业版本角色搜索。
1344+
两个标签均支持跨页选择、单项和批量授权;切换标签重置搜索、分页及选择,保存期间禁止切换。
1345+
查询仅应用最新响应,避免快速切换标签后串用用户与用户组数据;搜索后清空选择。
13321346
根目录隐藏“不授权”,返回的“不授权”按“查看”展示。包含子资源时必须传入完整文件夹子树,
13331347
抽屉根据当前路由工作空间筛选有管理权限的文件夹 ID;没有可管理文件夹时禁用该范围。
13341348
`PermissionConfigDialog` 只收集权限和范围,保存成功才关闭并刷新,失败保留配置。
1335-
保存成功触发 `refresh`,关闭后统一重置临时数据。
1349+
保存成功触发 `refresh`,关闭动画结束后重置临时数据并触发 `closed`,供资源 Action 卸载按需挂载的抽屉。
1350+
1351+
### UserGroupMembersDrawer
1352+
1353+
手动导入 `business/resource-authorization-drawer/user-group/UserGroupMembersDrawer.vue`,通过 `open(userGroup)` 传入
1354+
`SystemUserGroup`,按其 `workspace_id` 查询成员。支持用户名、姓名搜索和分页,角色使用
1355+
`MkTagGroup` 展示;供系统资源授权页面和资源授权抽屉共同复用。
1356+
1357+
资源授权抽屉内部公共展示 Props 放在同目录 `types.ts`,权限选项类型从
1358+
`RESOURCE_PERMISSION_OPTIONS` 派生;用户和用户组 API Props 分别保留完整 API 对象类型,
1359+
用户组组件只接收具备用户组方法的 Workspace API。

‎ui/src/components/business/folder-tree/index.vue‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import { computed, defineAsyncComponent, onMounted, ref, useTemplateRef } from 'vue'
33
import { useRoute, useRouter } from 'vue-router'
44
import FolderApi from '@/api/admin/workspace/folder'
5-
import ResourceAuthorizationApi from '@/api/admin/workspace/resource-authorization'
65
import type { FolderSource, FolderItem } from '@/api/types'
76
import { FOLDER_SORT, type FolderSort } from './types'
87
import { FOLDER_ENTRIES, FOLDER_ENTRY_ID } from '@/constants'
@@ -433,6 +432,6 @@ defineExpose({ refresh: loadFolders, openCreate: handleOpenCreateFolder })
433432

434433
<FolderFormDialog ref="folderFormDialogRef" :title="formTitle" :source="props.source" @refresh="handleFolderRefresh" />
435434
<MoveToDialog v-if="canEdit" ref="moveToDialogRef" :loading="moveSubmitting" :source="props.source" @submit="handleMoveFolder" />
436-
<ResourceAuthorizationDrawer v-if="canEdit" ref="resourceAuthorizationDrawerRef" :api="ResourceAuthorizationApi" :type="source" is-folder />
435+
<ResourceAuthorizationDrawer v-if="canEdit" ref="resourceAuthorizationDrawerRef" :type="source" is-folder />
437436
</div>
438437
</template>

‎ui/src/components/business/resource-authorization-drawer/PermissionConfigDialog.vue‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
<script setup lang="ts">
22
import { ref } from 'vue'
3-
import type { OptionItem, ResourcePermission } from '@/api/types'
3+
import type { ResourcePermission } from '@/api/types'
4+
5+
import type { ResourcePermissionOption } from './types'
46
57
const props = defineProps<{
6-
options: (OptionItem<ResourcePermission> & { description: string })[]
8+
options: ResourcePermissionOption[]
79
isFolder: boolean
810
canIncludeChildren: boolean
911
loading: boolean
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
<script setup lang="ts">
2+
import { computed, onMounted, ref, useTemplateRef } from 'vue'
3+
import type ResourceAuthorizationApi from '@/api/admin/workspace/resource-authorization'
4+
import type SystemResourceAuthorizationApi from '@/api/admin/system/resource-management/resource-authorization'
5+
import { RESOURCE_PERMISSION } from '@/api/enums'
6+
import type { Dict, ResourcePermission, ResourceUserPermission } from '@/api/types'
7+
import { MsgSuccess } from '@/utils/message'
8+
import MkTable from '@/components/global/mk-table/index.vue'
9+
import PermissionConfigDialog from './PermissionConfigDialog.vue'
10+
import { useStore } from '@/stores'
11+
12+
import type { ResourceAuthorizationContentProps, ResourcePermissionOption } from './types'
13+
14+
interface Props extends ResourceAuthorizationContentProps {
15+
api: typeof ResourceAuthorizationApi | typeof SystemResourceAuthorizationApi
16+
permissionOptions: ResourcePermissionOption[]
17+
}
18+
19+
const props = defineProps<Props>()
20+
const emit = defineEmits<{ refresh: [] }>()
21+
const submitting = defineModel<boolean>('submitting', { required: true })
22+
23+
/* 查询与跨页选择 */
24+
const { auth } = useStore()
25+
const searchFields = computed(() => [
26+
{ label: '姓名', value: 'nick_name' },
27+
{ label: '用户名', value: 'username' },
28+
{ label: '权限', value: 'permission', multiple: true, options: props.permissionOptions },
29+
...(auth.isEE || auth.isPE ? [{ label: '角色', value: 'role' }] : []),
30+
])
31+
const loading = ref(false)
32+
const permissionUsers = ref<ResourceUserPermission[]>([])
33+
const selectedUsers = ref<ResourceUserPermission[]>([])
34+
const paginationConfig = ref({ currentPage: 1, pageSize: 20, total: 0 })
35+
const searchQuery = ref<Dict<unknown>>()
36+
const tableRef = useTemplateRef('tableRef')
37+
38+
function loadPermissions() {
39+
if (!props.targetId) return Promise.resolve()
40+
loading.value = true
41+
return props.api
42+
.getResourceAuthorization(props.targetId, props.type, paginationConfig.value, searchQuery.value)
43+
.then(({ records, total }) => {
44+
permissionUsers.value = records.map((subject) => ({
45+
...subject,
46+
permission: props.isRootFolder && subject.permission === RESOURCE_PERMISSION.NOT_AUTH ? RESOURCE_PERMISSION.VIEW : subject.permission,
47+
}))
48+
paginationConfig.value.total = total
49+
})
50+
.finally(() => {
51+
loading.value = false
52+
})
53+
}
54+
55+
function clearSelection() {
56+
selectedUsers.value = []
57+
tableRef.value?.clearSelection()
58+
}
59+
60+
function handleSearch(query?: Dict<unknown>) {
61+
searchQuery.value = query
62+
paginationConfig.value.currentPage = 1
63+
clearSelection()
64+
loadPermissions()
65+
}
66+
67+
function handleSelectionChange(selection: unknown[]) {
68+
selectedUsers.value = selection as ResourceUserPermission[]
69+
}
70+
71+
/* 单项与批量保存:确认前不修改行数据,失败时保留待提交配置。 */
72+
const configDialogRef = useTemplateRef<InstanceType<typeof PermissionConfigDialog>>('configDialogRef')
73+
const pendingUserIds = ref<string[]>([])
74+
75+
function handleOpenBatchConfig() {
76+
if (!selectedUsers.value.length || submitting.value) return
77+
pendingUserIds.value = selectedUsers.value.map(({ id }) => id)
78+
configDialogRef.value?.open()
79+
}
80+
81+
function handlePermissionChange(value: string | number | boolean | undefined, subject: ResourceUserPermission) {
82+
const permission = props.editablePermissionOptions.find((option) => option.value === value)?.value
83+
if (!permission || permission === subject.permission || submitting.value) return
84+
pendingUserIds.value = [subject.id]
85+
if (props.isFolder) {
86+
configDialogRef.value?.open(permission)
87+
} else {
88+
submitPermissions(permission, false)
89+
}
90+
}
91+
92+
function submitPermissions(permission: ResourcePermission, includeChildren: boolean) {
93+
if (submitting.value || !pendingUserIds.value.length || (includeChildren && !props.managedFolderIds.length)) return
94+
const permissionScope = {
95+
permission,
96+
include_children: includeChildren,
97+
...(includeChildren ? { folder_ids: [...props.managedFolderIds] } : {}),
98+
}
99+
submitting.value = true
100+
return props.api
101+
.putResourceAuthorization(
102+
props.targetId,
103+
props.type,
104+
pendingUserIds.value.map((id) => ({ user_id: id, ...permissionScope })),
105+
)
106+
.then(() => {
107+
MsgSuccess('提交成功')
108+
configDialogRef.value?.close()
109+
pendingUserIds.value = []
110+
clearSelection()
111+
emit('refresh')
112+
return loadPermissions()
113+
})
114+
.finally(() => {
115+
submitting.value = false
116+
})
117+
}
118+
119+
/* 挂载当前标签时查询权限。 */
120+
onMounted(() => loadPermissions())
121+
</script>
122+
123+
<template>
124+
<div>
125+
<div class="flex-between mb-4 gap-4">
126+
<el-button type="primary" :disabled="!selectedUsers.length || loading || submitting" @click="handleOpenBatchConfig">配置权限</el-button>
127+
<MkComplexSearch :fields="searchFields" @change="handleSearch" />
128+
</div>
129+
<MkTable
130+
ref="tableRef"
131+
v-loading="loading || submitting"
132+
v-model:pagination-config="paginationConfig"
133+
:data="permissionUsers"
134+
:max-table-height="260"
135+
@current-change="loadPermissions"
136+
@size-change="loadPermissions"
137+
@selection-change="handleSelectionChange"
138+
>
139+
<el-table-column type="selection" width="40" reserve-selection />
140+
<el-table-column prop="nick_name" label="姓名" min-width="120" show-overflow-tooltip />
141+
<el-table-column prop="username" label="用户名" min-width="120" show-overflow-tooltip />
142+
<el-table-column v-if="auth.isEE || auth.isPE" label="角色" width="160">
143+
<template #default="{ row }">
144+
<MkTagGroup v-if="row.role_name?.length" :tags="row.role_name" />
145+
<span v-else>-</span>
146+
</template>
147+
</el-table-column>
148+
<el-table-column label="权限" min-width="340">
149+
<template #default="{ row }">
150+
<el-radio-group :model-value="row.permission" :disabled="submitting" @change="handlePermissionChange($event, row)">
151+
<el-radio v-for="option in editablePermissionOptions" :key="option.value" :value="option.value" class="mr-4!">{{
152+
option.label
153+
}}</el-radio>
154+
</el-radio-group>
155+
</template>
156+
</el-table-column>
157+
</MkTable>
158+
<PermissionConfigDialog
159+
ref="configDialogRef"
160+
:options="editablePermissionOptions"
161+
:is-folder="isFolder"
162+
:can-include-children="managedFolderIds.length > 0"
163+
:loading="submitting"
164+
@submit="submitPermissions"
165+
/>
166+
</div>
167+
</template>

0 commit comments

Comments
 (0)