feat(rbac): 批量删除联调 + 角色数据权限部门分配 UI

- 用户/组织/资源页批量删除改批量接口 + 失败汇总提示
- 角色页新增「分配部门」(dataScope=custom 时显示,部门树勾选)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-11 17:42:16 +08:00
parent eff4f8b314
commit d37af1b73e
10 changed files with 132 additions and 12 deletions

View File

@ -37,3 +37,8 @@ export function updateOrg(
export function deleteOrg(id: string): Promise<unknown> { export function deleteOrg(id: string): Promise<unknown> {
return http.delete(`/orgs/${id}`) return http.delete(`/orgs/${id}`)
} }
/** 批量删除组织。 */
export function batchDeleteOrgs(ids: string[]): Promise<{ total: number; success: number; failed: string[] }> {
return http.post('/orgs/batch-delete', { ids })
}

View File

@ -49,3 +49,8 @@ export function updateResource(id: string, data: ResourceUpsertData): Promise<Re
export function deleteResource(id: string): Promise<unknown> { export function deleteResource(id: string): Promise<unknown> {
return http.delete(`/resources/${id}`) return http.delete(`/resources/${id}`)
} }
/** 批量删除资源。 */
export function batchDeleteResources(ids: string[]): Promise<{ total: number; success: number; failed: string[] }> {
return http.post('/resources/batch-delete', { ids })
}

View File

@ -60,3 +60,13 @@ export function listAssignedResources(roleCode: string, signal?: AbortSignal): P
export function assignResources(roleCode: string, resourceCodes: string[]): Promise<unknown> { export function assignResources(roleCode: string, resourceCodes: string[]): Promise<unknown> {
return http.put(`/roles/${roleCode}/resources`, { resourceCodes }) return http.put(`/roles/${roleCode}/resources`, { resourceCodes })
} }
/** 角色已分配的数据权限部门 code 列表(data_scope=custom)。 */
export function listAssignedOrgs(roleCode: string, signal?: AbortSignal): Promise<string[]> {
return http.get<string[]>(`/roles/${roleCode}/orgs`, { signal })
}
/** 分配数据权限部门。 */
export function assignOrgs(roleCode: string, orgCodes: string[]): Promise<unknown> {
return http.put(`/roles/${roleCode}/orgs`, { orgCodes })
}

View File

@ -67,6 +67,11 @@ export function deleteUser(id: string): Promise<unknown> {
return http.delete(`/users/${id}`) return http.delete(`/users/${id}`)
} }
/** 批量删除用户。 */
export function batchDeleteUsers(ids: string[]): Promise<{ total: number; success: number; failed: string[] }> {
return http.post('/users/batch-delete', { ids })
}
/** 重置密码。 */ /** 重置密码。 */
export function resetPassword(id: string, password: string): Promise<unknown> { export function resetPassword(id: string, password: string): Promise<unknown> {
return http.patch(`/users/${id}/password`, { password }) return http.patch(`/users/${id}/password`, { password })

View File

@ -46,7 +46,7 @@ import TyForm from '@/components/ty-form.vue'
import { showDialog } from '@/utils/dialog' import { showDialog } from '@/utils/dialog'
import { useRequest } from '@/composables/useRequest' import { useRequest } from '@/composables/useRequest'
import { buildTree, collectDescendants, filterTree, type TreeNode } from '@/utils/tree' import { buildTree, collectDescendants, filterTree, type TreeNode } from '@/utils/tree'
import { createOrg, deleteOrg, listOrgs, updateOrg, type Org } from '@/api/org' import { batchDeleteOrgs, createOrg, deleteOrg, listOrgs, updateOrg, type Org } from '@/api/org'
type OrgNode = TreeNode<Org> type OrgNode = TreeNode<Org>
@ -238,10 +238,14 @@ async function batchDelete(rows: Record<string, any>[]): Promise<void> {
codes.add(String(r.code)) codes.add(String(r.code))
for (const c of collectDescendants(orgs.value, String(r.code))) codes.add(c) for (const c of collectDescendants(orgs.value, String(r.code))) codes.add(c)
} }
const targets = orgs.value.filter((o) => codes.has(o.code)) const ids = orgs.value.filter((o) => codes.has(o.code)).map((o) => o.id)
for (const t of targets) await deleteOrg(t.id) const result = await batchDeleteOrgs(ids)
if (result.failed.length > 0) {
ElMessage.warning(`删除完成:成功 ${result.success} 个,失败 ${result.failed.length} 个`)
} else {
ElMessage.success('删除成功')
}
selectedRows.value = [] selectedRows.value = []
ElMessage.success('删除成功')
await refreshOrgs() await refreshOrgs()
} }
</script> </script>

View File

@ -46,7 +46,7 @@ import TyForm from '@/components/ty-form.vue'
import { showDialog } from '@/utils/dialog' import { showDialog } from '@/utils/dialog'
import { useRequest } from '@/composables/useRequest' import { useRequest } from '@/composables/useRequest'
import { buildTree, collectDescendants, filterTree, type TreeNode } from '@/utils/tree' import { buildTree, collectDescendants, filterTree, type TreeNode } from '@/utils/tree'
import { createResource, deleteResource, listResources, updateResource, type Resource, type ResourceUpsertData } from '@/api/resource' import { batchDeleteResources, createResource, deleteResource, listResources, updateResource, type Resource, type ResourceUpsertData } from '@/api/resource'
type ResourceNode = TreeNode<Resource> type ResourceNode = TreeNode<Resource>
@ -288,10 +288,14 @@ async function batchDelete(rows: Record<string, any>[]): Promise<void> {
codes.add(String(r.code)) codes.add(String(r.code))
for (const c of collectDescendants(resources.value, String(r.code))) codes.add(c) for (const c of collectDescendants(resources.value, String(r.code))) codes.add(c)
} }
const targets = resources.value.filter((o) => codes.has(o.code)) const ids = resources.value.filter((o) => codes.has(o.code)).map((o) => o.id)
for (const t of targets) await deleteResource(t.id) const result = await batchDeleteResources(ids)
if (result.failed.length > 0) {
ElMessage.warning(`删除完成:成功 ${result.success} 个,失败 ${result.failed.length} 个`)
} else {
ElMessage.success('删除成功')
}
selectedRows.value = [] selectedRows.value = []
ElMessage.success('删除成功')
await refreshResources() await refreshResources()
} }
</script> </script>

View File

@ -0,0 +1,57 @@
<template>
<el-tree
v-if="ready"
ref="treeRef"
class="assign-tree"
:data="tree"
node-key="code"
show-checkbox
:props="{ label: 'name', children: 'children' }"
default-expand-all
/>
</template>
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue'
import { useRequest } from '@/composables/useRequest'
import { listOrgs } from '@/api/org'
import { listAssignedOrgs } from '@/api/role'
import { buildTree, type TreeNode } from '@/utils/tree'
import type { Org } from '@/api/org'
defineOptions({ name: 'AssignOrgsForm' })
const props = defineProps<{ roleCode: string }>()
interface TreeApi {
getCheckedKeys: (leafOnly?: boolean) => (string | number)[]
setCheckedKeys: (keys: (string | number)[], leafOnly?: boolean) => void
}
const treeRef = ref<TreeApi>()
const { data: orgList } = useRequest((signal: AbortSignal) => listOrgs(signal))
const { data: assigned } = useRequest((signal: AbortSignal) => listAssignedOrgs(props.roleCode, signal))
const tree = computed(() => buildTree(orgList.value ?? []) as TreeNode<Org>[])
const ready = computed(() => orgList.value !== undefined && assigned.value !== undefined)
watch(ready, async (r) => {
if (r) {
await nextTick()
treeRef.value?.setCheckedKeys(assigned.value ?? [], true)
}
})
function submit(): { orgCodes: string[] } {
const checked = treeRef.value?.getCheckedKeys(true) ?? []
return { orgCodes: [...new Set(checked.map(String))] }
}
defineExpose({ submit })
</script>
<style scoped lang="scss">
.assign-tree {
@apply max-h-[420px] overflow-y-auto;
}
</style>

View File

@ -19,8 +19,9 @@
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import type { CrudAction, JsonSchema, PageQuery, TableColumn } from '@/types/crud' import type { CrudAction, JsonSchema, PageQuery, TableColumn } from '@/types/crud'
import { showDialog } from '@/utils/dialog' import { showDialog } from '@/utils/dialog'
import { assignResources, createRole, deleteRole, listRoles, updateRole, type RoleListQuery } from '@/api/role' import { assignOrgs, assignResources, createRole, deleteRole, listRoles, updateRole, type RoleListQuery } from '@/api/role'
import AssignResourcesForm from './assign-resources-form.vue' import AssignResourcesForm from './assign-resources-form.vue'
import AssignOrgsForm from './assign-orgs-form.vue'
const columns: TableColumn[] = [ const columns: TableColumn[] = [
{ prop: 'name', label: '角色名称', minWidth: 140 }, { prop: 'name', label: '角色名称', minWidth: 140 },
@ -72,6 +73,7 @@ const actions: CrudAction[] = [
'edit', 'edit',
'delete', 'delete',
{ label: '分配权限', type: 'warning', onClick: (row) => assignAction(row) }, { label: '分配权限', type: 'warning', onClick: (row) => assignAction(row) },
{ label: '分配部门', type: 'primary', show: (row) => row.dataScope === 'custom', onClick: (row) => assignOrgsAction(row) },
] ]
function toListQuery(query: PageQuery): RoleListQuery { function toListQuery(query: PageQuery): RoleListQuery {
@ -103,4 +105,16 @@ async function assignAction(row: Record<string, any>): Promise<void> {
await assignResources(String(row.code), result.resourceCodes) await assignResources(String(row.code), result.resourceCodes)
ElMessage.success('权限已保存') ElMessage.success('权限已保存')
} }
async function assignOrgsAction(row: Record<string, any>): Promise<void> {
const result = await showDialog({
component: AssignOrgsForm,
props: { roleCode: String(row.code) },
title: `分配数据权限部门(${row.name})`,
width: '520px',
})
if (result == null) return
await assignOrgs(String(row.code), result.orgCodes)
ElMessage.success('部门已保存')
}
</script> </script>

View File

@ -39,6 +39,7 @@ import { showDialog } from '@/utils/dialog'
import { useRequest } from '@/composables/useRequest' import { useRequest } from '@/composables/useRequest'
import { import {
assignUserRoles, assignUserRoles,
batchDeleteUsers,
createUser, createUser,
deleteUser, deleteUser,
listUsers, listUsers,
@ -167,10 +168,12 @@ const crudRef = ref()
async function batchDelete(rows: Record<string, any>[]): Promise<void> { async function batchDelete(rows: Record<string, any>[]): Promise<void> {
await ElMessageBox.confirm(`确认删除选中的 ${rows.length} 个用户?`, '提示', { type: 'warning' }) await ElMessageBox.confirm(`确认删除选中的 ${rows.length} 个用户?`, '提示', { type: 'warning' })
for (const r of rows) { const result = await batchDeleteUsers(rows.map((r) => String(r.id)))
await deleteUser(String(r.id)) if (result.failed.length > 0) {
ElMessage.warning(`删除完成:成功 ${result.success} 个,失败 ${result.failed.length} 个`)
} else {
ElMessage.success('删除成功')
} }
ElMessage.success('删除成功')
crudRef.value?.reload() crudRef.value?.reload()
} }

13
typed-router.d.ts vendored
View File

@ -121,6 +121,13 @@ declare module 'vue-router/auto-routes' {
Record<never, never>, Record<never, never>,
| never | never
>, >,
'/system/role/assign-orgs-form': RouteRecordInfo<
'/system/role/assign-orgs-form',
'/system/role/assign-orgs-form',
Record<never, never>,
Record<never, never>,
| never
>,
'/system/role/assign-resources-form': RouteRecordInfo< '/system/role/assign-resources-form': RouteRecordInfo<
'/system/role/assign-resources-form', '/system/role/assign-resources-form',
'/system/role/assign-resources-form', '/system/role/assign-resources-form',
@ -253,6 +260,12 @@ declare module 'vue-router/auto-routes' {
views: views:
| never | never
} }
'src/pages/system/role/assign-orgs-form.vue': {
routes:
| '/system/role/assign-orgs-form'
views:
| never
}
'src/pages/system/role/assign-resources-form.vue': { 'src/pages/system/role/assign-resources-form.vue': {
routes: routes:
| '/system/role/assign-resources-form' | '/system/role/assign-resources-form'