feat(role): 展示角色人员列表

This commit is contained in:
xy
2026-09-24 20:37:15 +08:00
parent abe7e17582
commit 757145506e
3 changed files with 194 additions and 5 deletions
+22 -1
View File
@@ -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',
+9
View File
@@ -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;