feat(role): 展示角色人员列表
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
import { unwrapApiResponse, unwrapNullableApiResponse } from '@/utils/api-response'
|
||||
import { request } from '@/utils/request'
|
||||
import type { ApiResponse } from '@/types/common'
|
||||
import type { ApiResponse, PageResult } from '@/types/common'
|
||||
|
||||
import type {
|
||||
BackendCollection,
|
||||
BackendPage,
|
||||
CreateRoleRequest,
|
||||
PermissionResourceNodeDto,
|
||||
RoleDataScopeRequest,
|
||||
RoleDataScopeResponseDto,
|
||||
RoleMemberDto,
|
||||
RoleRecord,
|
||||
RoleResponseDto,
|
||||
UpdateRoleRequest,
|
||||
@@ -21,6 +23,7 @@ function normalizeRole(dto: RoleResponseDto, index: number): RoleRecord {
|
||||
code: dto.code,
|
||||
name: dto.name,
|
||||
description: dto.description ?? '',
|
||||
userCount: dto.userCount ?? 0,
|
||||
color: fallbackColors[index % fallbackColors.length],
|
||||
status: dto.status ?? 'DISABLED',
|
||||
menuPermissionCount: dto.menuPermissionCount ?? 0,
|
||||
@@ -50,6 +53,24 @@ export async function getAssignableRoles(): Promise<RoleRecord[]> {
|
||||
return normalizeCollection(unwrapApiResponse(response)).map(normalizeRole)
|
||||
}
|
||||
|
||||
export async function getRoleMembers(
|
||||
roleId: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
): Promise<PageResult<RoleMemberDto>> {
|
||||
const response = await request.get<ApiResponse<BackendPage<RoleMemberDto>>>(
|
||||
`/v1/roles/${encodeURIComponent(roleId)}/users`,
|
||||
{ params: { page, size: pageSize } },
|
||||
)
|
||||
const result = unwrapApiResponse(response)
|
||||
return {
|
||||
records: result.records ?? result.items ?? result.content ?? [],
|
||||
total: result.total ?? 0,
|
||||
page: result.page ?? page,
|
||||
pageSize: result.size ?? pageSize,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRolePermissionTree(): Promise<PermissionResourceNodeDto[]> {
|
||||
const response = await request.get<ApiResponse<PermissionResourceNodeDto[]>>(
|
||||
'/v1/roles/permission-tree',
|
||||
|
||||
@@ -166,6 +166,7 @@ export interface RoleResponseDto {
|
||||
code: string
|
||||
name: string
|
||||
description?: string | null
|
||||
userCount?: number | null
|
||||
status?: RoleStatus | null
|
||||
menuPermissionCount?: number | null
|
||||
buttonPermissionCount?: number | null
|
||||
@@ -176,6 +177,14 @@ export interface RoleResponseDto {
|
||||
version?: number | null
|
||||
}
|
||||
|
||||
export interface RoleMemberDto {
|
||||
id: string
|
||||
employeeId?: number | null
|
||||
displayName: string
|
||||
employeeNo?: string | null
|
||||
campusName?: string | null
|
||||
}
|
||||
|
||||
export interface CreateRoleRequest {
|
||||
code: string
|
||||
name: string
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getAllDepartments, getCampuses } from '@/api/management/organization'
|
||||
import {
|
||||
createRole,
|
||||
deleteRole,
|
||||
getRoleMembers,
|
||||
getRoleDataScope,
|
||||
getRolePermissionTree,
|
||||
getRolePermissions,
|
||||
@@ -20,6 +21,7 @@ import type {
|
||||
DepartmentRecord,
|
||||
PermissionResourceNodeDto,
|
||||
RoleDataScopeType,
|
||||
RoleMemberDto,
|
||||
RoleRecord,
|
||||
} from '@/api/management/types'
|
||||
import { useLoginStore } from '@/stores/login'
|
||||
@@ -27,6 +29,13 @@ import { useLoginStore } from '@/stores/login'
|
||||
const loginStore = useLoginStore()
|
||||
const loading = ref(false)
|
||||
const roles = ref<RoleRecord[]>([])
|
||||
const selectedRoleId = ref('')
|
||||
const roleMembers = ref<RoleMemberDto[]>([])
|
||||
const membersLoading = ref(false)
|
||||
const memberPage = ref(1)
|
||||
const memberPageSize = 10
|
||||
const memberTotal = ref(0)
|
||||
let memberRequestSequence = 0
|
||||
const keyword = ref('')
|
||||
const formVisible = ref(false)
|
||||
const formSaving = ref(false)
|
||||
@@ -74,6 +83,9 @@ const filteredRoles = computed(() => {
|
||||
role.code.toLocaleLowerCase().includes(query),
|
||||
)
|
||||
})
|
||||
const selectedRole = computed(
|
||||
() => roles.value.find((role) => role.id === selectedRoleId.value) ?? null,
|
||||
)
|
||||
const scopeLabels: Record<RoleDataScopeType, string> = {
|
||||
ALL: '全院',
|
||||
CUSTOM: '指定院区 / 科室',
|
||||
@@ -132,14 +144,62 @@ async function loadRoles() {
|
||||
loading.value = true
|
||||
try {
|
||||
roles.value = await getRoles()
|
||||
const nextRole = roles.value.find((role) => role.id === selectedRoleId.value) ?? roles.value[0]
|
||||
if (nextRole) {
|
||||
await loadRoleMembers(
|
||||
nextRole.id,
|
||||
nextRole.id === selectedRoleId.value ? memberPage.value : 1,
|
||||
)
|
||||
} else {
|
||||
memberRequestSequence += 1
|
||||
selectedRoleId.value = ''
|
||||
roleMembers.value = []
|
||||
memberTotal.value = 0
|
||||
membersLoading.value = false
|
||||
}
|
||||
} catch {
|
||||
memberRequestSequence += 1
|
||||
roles.value = []
|
||||
selectedRoleId.value = ''
|
||||
roleMembers.value = []
|
||||
memberTotal.value = 0
|
||||
membersLoading.value = false
|
||||
ElMessage.error('角色列表加载失败,请检查当前账号权限')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRoleMembers(roleId: string, page = 1) {
|
||||
const requestSequence = ++memberRequestSequence
|
||||
selectedRoleId.value = roleId
|
||||
memberPage.value = page
|
||||
membersLoading.value = true
|
||||
try {
|
||||
const result = await getRoleMembers(roleId, page, memberPageSize)
|
||||
if (requestSequence !== memberRequestSequence) return
|
||||
roleMembers.value = result.records
|
||||
memberTotal.value = result.total
|
||||
memberPage.value = result.page
|
||||
} catch {
|
||||
if (requestSequence !== memberRequestSequence) return
|
||||
roleMembers.value = []
|
||||
memberTotal.value = 0
|
||||
ElMessage.error('角色成员加载失败,请检查当前账号权限')
|
||||
} finally {
|
||||
if (requestSequence === memberRequestSequence) membersLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectRole(role: RoleRecord) {
|
||||
if (selectedRoleId.value === role.id) return
|
||||
void loadRoleMembers(role.id)
|
||||
}
|
||||
|
||||
function handleMemberPageChange(page: number) {
|
||||
if (selectedRole.value) void loadRoleMembers(selectedRole.value.id, page)
|
||||
}
|
||||
|
||||
function isSystemAdmin(role: RoleRecord) {
|
||||
return role.code === 'SYSTEM_ADMIN'
|
||||
}
|
||||
@@ -429,10 +489,21 @@ onMounted(loadRoles)
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="role in filteredRoles" :key="role.id">
|
||||
<tr
|
||||
v-for="role in filteredRoles"
|
||||
:key="role.id"
|
||||
class="role-row"
|
||||
:class="{ 'is-selected': selectedRoleId === role.id }"
|
||||
:aria-selected="selectedRoleId === role.id"
|
||||
tabindex="0"
|
||||
@click="selectRole(role)"
|
||||
@keydown.enter.prevent="selectRole(role)"
|
||||
@keydown.space.prevent="selectRole(role)"
|
||||
>
|
||||
<td>
|
||||
<strong>{{ role.name }}</strong
|
||||
><span v-if="isSystemAdmin(role)" class="builtin-tag">内置</span>
|
||||
<strong>{{ role.name }}</strong>
|
||||
<span v-if="isSystemAdmin(role)" class="builtin-tag">内置</span>
|
||||
<span class="member-count">{{ role.userCount ?? 0 }} 人</span>
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ role.code }}</code>
|
||||
@@ -448,7 +519,7 @@ onMounted(loadRoles)
|
||||
<td>{{ role.menuPermissionCount }}</td>
|
||||
<td>{{ role.buttonPermissionCount }}</td>
|
||||
<td>{{ scopeLabels[role.dataScope] ?? '未配置' }}</td>
|
||||
<td class="actions">
|
||||
<td class="actions" @click.stop>
|
||||
<button
|
||||
v-if="canViewPermissions"
|
||||
class="text-button"
|
||||
@@ -508,6 +579,46 @@ onMounted(loadRoles)
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="selectedRole" class="page-card member-card">
|
||||
<header class="member-heading">
|
||||
<h2>角色人员列表 · {{ selectedRole.name }}</h2>
|
||||
<span>{{ memberTotal }} 人</span>
|
||||
</header>
|
||||
<div v-loading="membersLoading" class="table-scroll">
|
||||
<table class="role-table member-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用户</th>
|
||||
<th>ID</th>
|
||||
<th>工号</th>
|
||||
<th>院区</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="member in roleMembers" :key="member.id">
|
||||
<td>{{ member.displayName }}</td>
|
||||
<td>{{ member.employeeId ?? '—' }}</td>
|
||||
<td>{{ member.employeeNo || '—' }}</td>
|
||||
<td>{{ member.campusName || '—' }}</td>
|
||||
</tr>
|
||||
<tr v-if="!membersLoading && !roleMembers.length">
|
||||
<td colspan="4" class="empty-cell">该角色暂无可查看的成员</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<el-pagination
|
||||
v-if="memberTotal > memberPageSize"
|
||||
class="member-pagination"
|
||||
background
|
||||
layout="total, prev, pager, next"
|
||||
:total="memberTotal"
|
||||
:page-size="memberPageSize"
|
||||
:current-page="memberPage"
|
||||
@current-change="handleMemberPageChange"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="formVisible"
|
||||
:title="roleForm.id ? '编辑角色' : '新增角色'"
|
||||
@@ -702,6 +813,27 @@ onMounted(loadRoles)
|
||||
border-radius: var(--r);
|
||||
box-shadow: var(--sh);
|
||||
}
|
||||
.member-card {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.member-heading {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: baseline;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.member-heading h2 {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-size: 16px;
|
||||
}
|
||||
.member-heading span {
|
||||
color: var(--mut);
|
||||
}
|
||||
.member-pagination {
|
||||
justify-content: flex-end;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.role-toolbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
@@ -741,6 +873,33 @@ onMounted(loadRoles)
|
||||
padding: 12px 10px;
|
||||
border-bottom: 1px solid #eef4f6;
|
||||
}
|
||||
.role-row {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
.role-row:hover td,
|
||||
.role-row.is-selected td {
|
||||
background: #eaf5f8;
|
||||
}
|
||||
.role-row:focus-visible td {
|
||||
box-shadow:
|
||||
inset 0 2px var(--brand),
|
||||
inset 0 -2px var(--brand);
|
||||
}
|
||||
.member-count {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
padding: 3px 9px;
|
||||
color: var(--brand);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
background: #e7f4f7;
|
||||
border-radius: 14px;
|
||||
}
|
||||
.member-table {
|
||||
min-width: 0;
|
||||
}
|
||||
.role-table td code {
|
||||
color: #627c88;
|
||||
font-size: 11px;
|
||||
|
||||
Reference in New Issue
Block a user