feat(rbac): 批量删除联调 + 角色数据权限部门分配 UI
- 用户/组织/资源页批量删除改批量接口 + 失败汇总提示 - 角色页新增「分配部门」(dataScope=custom 时显示,部门树勾选) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
eff4f8b314
commit
d37af1b73e
@ -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 })
|
||||||
|
}
|
||||||
|
|||||||
@ -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 })
|
||||||
|
}
|
||||||
|
|||||||
@ -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 })
|
||||||
|
}
|
||||||
|
|||||||
@ -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 })
|
||||||
|
|||||||
@ -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)
|
||||||
selectedRows.value = []
|
if (result.failed.length > 0) {
|
||||||
|
ElMessage.warning(`删除完成:成功 ${result.success} 个,失败 ${result.failed.length} 个`)
|
||||||
|
} else {
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
|
}
|
||||||
|
selectedRows.value = []
|
||||||
await refreshOrgs()
|
await refreshOrgs()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -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)
|
||||||
selectedRows.value = []
|
if (result.failed.length > 0) {
|
||||||
|
ElMessage.warning(`删除完成:成功 ${result.success} 个,失败 ${result.failed.length} 个`)
|
||||||
|
} else {
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
|
}
|
||||||
|
selectedRows.value = []
|
||||||
await refreshResources()
|
await refreshResources()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
57
src/pages/system/role/assign-orgs-form.vue
Normal file
57
src/pages/system/role/assign-orgs-form.vue
Normal 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>
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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
13
typed-router.d.ts
vendored
@ -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'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user