feat(role): 展示角色人员列表
This commit is contained in:
@@ -1,13 +1,15 @@
|
|||||||
import { unwrapApiResponse, unwrapNullableApiResponse } from '@/utils/api-response'
|
import { unwrapApiResponse, unwrapNullableApiResponse } from '@/utils/api-response'
|
||||||
import { request } from '@/utils/request'
|
import { request } from '@/utils/request'
|
||||||
import type { ApiResponse } from '@/types/common'
|
import type { ApiResponse, PageResult } from '@/types/common'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
BackendCollection,
|
BackendCollection,
|
||||||
|
BackendPage,
|
||||||
CreateRoleRequest,
|
CreateRoleRequest,
|
||||||
PermissionResourceNodeDto,
|
PermissionResourceNodeDto,
|
||||||
RoleDataScopeRequest,
|
RoleDataScopeRequest,
|
||||||
RoleDataScopeResponseDto,
|
RoleDataScopeResponseDto,
|
||||||
|
RoleMemberDto,
|
||||||
RoleRecord,
|
RoleRecord,
|
||||||
RoleResponseDto,
|
RoleResponseDto,
|
||||||
UpdateRoleRequest,
|
UpdateRoleRequest,
|
||||||
@@ -21,6 +23,7 @@ function normalizeRole(dto: RoleResponseDto, index: number): RoleRecord {
|
|||||||
code: dto.code,
|
code: dto.code,
|
||||||
name: dto.name,
|
name: dto.name,
|
||||||
description: dto.description ?? '',
|
description: dto.description ?? '',
|
||||||
|
userCount: dto.userCount ?? 0,
|
||||||
color: fallbackColors[index % fallbackColors.length],
|
color: fallbackColors[index % fallbackColors.length],
|
||||||
status: dto.status ?? 'DISABLED',
|
status: dto.status ?? 'DISABLED',
|
||||||
menuPermissionCount: dto.menuPermissionCount ?? 0,
|
menuPermissionCount: dto.menuPermissionCount ?? 0,
|
||||||
@@ -50,6 +53,24 @@ export async function getAssignableRoles(): Promise<RoleRecord[]> {
|
|||||||
return normalizeCollection(unwrapApiResponse(response)).map(normalizeRole)
|
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[]> {
|
export async function getRolePermissionTree(): Promise<PermissionResourceNodeDto[]> {
|
||||||
const response = await request.get<ApiResponse<PermissionResourceNodeDto[]>>(
|
const response = await request.get<ApiResponse<PermissionResourceNodeDto[]>>(
|
||||||
'/v1/roles/permission-tree',
|
'/v1/roles/permission-tree',
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ export interface RoleResponseDto {
|
|||||||
code: string
|
code: string
|
||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
|
userCount?: number | null
|
||||||
status?: RoleStatus | null
|
status?: RoleStatus | null
|
||||||
menuPermissionCount?: number | null
|
menuPermissionCount?: number | null
|
||||||
buttonPermissionCount?: number | null
|
buttonPermissionCount?: number | null
|
||||||
@@ -176,6 +177,14 @@ export interface RoleResponseDto {
|
|||||||
version?: number | null
|
version?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RoleMemberDto {
|
||||||
|
id: string
|
||||||
|
employeeId?: number | null
|
||||||
|
displayName: string
|
||||||
|
employeeNo?: string | null
|
||||||
|
campusName?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateRoleRequest {
|
export interface CreateRoleRequest {
|
||||||
code: string
|
code: string
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getAllDepartments, getCampuses } from '@/api/management/organization'
|
|||||||
import {
|
import {
|
||||||
createRole,
|
createRole,
|
||||||
deleteRole,
|
deleteRole,
|
||||||
|
getRoleMembers,
|
||||||
getRoleDataScope,
|
getRoleDataScope,
|
||||||
getRolePermissionTree,
|
getRolePermissionTree,
|
||||||
getRolePermissions,
|
getRolePermissions,
|
||||||
@@ -20,6 +21,7 @@ import type {
|
|||||||
DepartmentRecord,
|
DepartmentRecord,
|
||||||
PermissionResourceNodeDto,
|
PermissionResourceNodeDto,
|
||||||
RoleDataScopeType,
|
RoleDataScopeType,
|
||||||
|
RoleMemberDto,
|
||||||
RoleRecord,
|
RoleRecord,
|
||||||
} from '@/api/management/types'
|
} from '@/api/management/types'
|
||||||
import { useLoginStore } from '@/stores/login'
|
import { useLoginStore } from '@/stores/login'
|
||||||
@@ -27,6 +29,13 @@ import { useLoginStore } from '@/stores/login'
|
|||||||
const loginStore = useLoginStore()
|
const loginStore = useLoginStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const roles = ref<RoleRecord[]>([])
|
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 keyword = ref('')
|
||||||
const formVisible = ref(false)
|
const formVisible = ref(false)
|
||||||
const formSaving = ref(false)
|
const formSaving = ref(false)
|
||||||
@@ -74,6 +83,9 @@ const filteredRoles = computed(() => {
|
|||||||
role.code.toLocaleLowerCase().includes(query),
|
role.code.toLocaleLowerCase().includes(query),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
const selectedRole = computed(
|
||||||
|
() => roles.value.find((role) => role.id === selectedRoleId.value) ?? null,
|
||||||
|
)
|
||||||
const scopeLabels: Record<RoleDataScopeType, string> = {
|
const scopeLabels: Record<RoleDataScopeType, string> = {
|
||||||
ALL: '全院',
|
ALL: '全院',
|
||||||
CUSTOM: '指定院区 / 科室',
|
CUSTOM: '指定院区 / 科室',
|
||||||
@@ -132,14 +144,62 @@ async function loadRoles() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
roles.value = await getRoles()
|
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 {
|
} catch {
|
||||||
|
memberRequestSequence += 1
|
||||||
roles.value = []
|
roles.value = []
|
||||||
|
selectedRoleId.value = ''
|
||||||
|
roleMembers.value = []
|
||||||
|
memberTotal.value = 0
|
||||||
|
membersLoading.value = false
|
||||||
ElMessage.error('角色列表加载失败,请检查当前账号权限')
|
ElMessage.error('角色列表加载失败,请检查当前账号权限')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
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) {
|
function isSystemAdmin(role: RoleRecord) {
|
||||||
return role.code === 'SYSTEM_ADMIN'
|
return role.code === 'SYSTEM_ADMIN'
|
||||||
}
|
}
|
||||||
@@ -429,10 +489,21 @@ onMounted(loadRoles)
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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>
|
<td>
|
||||||
<strong>{{ role.name }}</strong
|
<strong>{{ role.name }}</strong>
|
||||||
><span v-if="isSystemAdmin(role)" class="builtin-tag">内置</span>
|
<span v-if="isSystemAdmin(role)" class="builtin-tag">内置</span>
|
||||||
|
<span class="member-count">{{ role.userCount ?? 0 }} 人</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<code>{{ role.code }}</code>
|
<code>{{ role.code }}</code>
|
||||||
@@ -448,7 +519,7 @@ onMounted(loadRoles)
|
|||||||
<td>{{ role.menuPermissionCount }}</td>
|
<td>{{ role.menuPermissionCount }}</td>
|
||||||
<td>{{ role.buttonPermissionCount }}</td>
|
<td>{{ role.buttonPermissionCount }}</td>
|
||||||
<td>{{ scopeLabels[role.dataScope] ?? '未配置' }}</td>
|
<td>{{ scopeLabels[role.dataScope] ?? '未配置' }}</td>
|
||||||
<td class="actions">
|
<td class="actions" @click.stop>
|
||||||
<button
|
<button
|
||||||
v-if="canViewPermissions"
|
v-if="canViewPermissions"
|
||||||
class="text-button"
|
class="text-button"
|
||||||
@@ -508,6 +579,46 @@ onMounted(loadRoles)
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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
|
<el-dialog
|
||||||
v-model="formVisible"
|
v-model="formVisible"
|
||||||
:title="roleForm.id ? '编辑角色' : '新增角色'"
|
:title="roleForm.id ? '编辑角色' : '新增角色'"
|
||||||
@@ -702,6 +813,27 @@ onMounted(loadRoles)
|
|||||||
border-radius: var(--r);
|
border-radius: var(--r);
|
||||||
box-shadow: var(--sh);
|
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 {
|
.role-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -741,6 +873,33 @@ onMounted(loadRoles)
|
|||||||
padding: 12px 10px;
|
padding: 12px 10px;
|
||||||
border-bottom: 1px solid #eef4f6;
|
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 {
|
.role-table td code {
|
||||||
color: #627c88;
|
color: #627c88;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|||||||
Reference in New Issue
Block a user