feat: 在用户列表展示角色信息

- 分页批量补全用户关联角色名称,避免逐行查询

- 支持多角色标签、溢出数量和完整信息提示
This commit is contained in:
2026-08-07 12:47:04 +08:00
parent 4b52d85512
commit 6ad004da9b
3 changed files with 190 additions and 1 deletions

View File

@@ -139,7 +139,51 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
@Override @Override
@LogRecord("分页查询") @LogRecord("分页查询")
protected Page<SysAccount> queryPage(Page<SysAccount> page, QueryWrapper queryWrapper) { protected Page<SysAccount> queryPage(Page<SysAccount> page, QueryWrapper queryWrapper) {
return service.getMapper().paginateWithRelations(page, queryWrapper); Page<SysAccount> result = service.getMapper().paginateWithRelations(page, queryWrapper);
fillRoleNames(result.getRecords());
return result;
}
/**
* 按当前分页内的角色 ID 批量补全角色名称。
*
* @param accounts 当前页账号
*/
private void fillRoleNames(List<SysAccount> accounts) {
if (accounts == null || accounts.isEmpty()) {
return;
}
Set<BigInteger> roleIds = accounts.stream()
.map(SysAccount::getRoleIds)
.filter(java.util.Objects::nonNull)
.flatMap(Collection::stream)
.filter(java.util.Objects::nonNull)
.collect(Collectors.toCollection(LinkedHashSet::new));
if (roleIds.isEmpty()) {
accounts.forEach(account -> account.setRoleNames(List.of()));
return;
}
Map<BigInteger, String> roleNameMap = sysRoleService.listByIds(roleIds).stream()
.filter(role -> role.getId() != null && StringUtil.hasText(role.getRoleName()))
.collect(Collectors.toMap(
SysRole::getId,
SysRole::getRoleName,
(first, ignored) -> first
));
accounts.forEach(account -> {
List<BigInteger> accountRoleIds = account.getRoleIds();
if (accountRoleIds == null || accountRoleIds.isEmpty()) {
account.setRoleNames(List.of());
return;
}
List<String> roleNames = accountRoleIds.stream()
.map(roleNameMap::get)
.filter(StringUtil::hasText)
.distinct()
.collect(Collectors.toList());
account.setRoleNames(roleNames);
});
} }
@Override @Override

View File

@@ -31,6 +31,12 @@ public class SysAccount extends SysAccountBase {
) )
private List<BigInteger> roleIds; private List<BigInteger> roleIds;
/**
* 账号关联的角色名称,仅用于列表展示。
*/
@Column(ignore = true)
private List<String> roleNames;
@RelationManyToMany(joinTable = "tb_sys_account_position" @RelationManyToMany(joinTable = "tb_sys_account_position"
, joinSelfColumn = "account_id" , joinSelfColumn = "account_id"
, joinTargetColumn = "position_id" , joinTargetColumn = "position_id"
@@ -51,6 +57,24 @@ public class SysAccount extends SysAccountBase {
this.roleIds = roleIds; this.roleIds = roleIds;
} }
/**
* 获取账号关联的角色名称。
*
* @return 角色名称列表
*/
public List<String> getRoleNames() {
return roleNames;
}
/**
* 设置账号关联的角色名称。
*
* @param roleNames 角色名称列表
*/
public void setRoleNames(List<String> roleNames) {
this.roleNames = roleNames;
}
public List<BigInteger> getPositionIds() { public List<BigInteger> getPositionIds() {
return positionIds; return positionIds;
} }

View File

@@ -20,6 +20,8 @@ import {
ElMessageBox, ElMessageBox,
ElTable, ElTable,
ElTableColumn, ElTableColumn,
ElTag,
ElTooltip,
} from 'element-plus'; } from 'element-plus';
import { api } from '#/api/request'; import { api } from '#/api/request';
@@ -47,6 +49,7 @@ const selectedRows = ref<any[]>([]);
const batchActionLoading = ref(false); const batchActionLoading = ref(false);
const dictStore = useDictStore(); const dictStore = useDictStore();
const selectedCount = computed(() => selectedRows.value.length); const selectedCount = computed(() => selectedRows.value.length);
const maxVisibleRoleCount = 2;
const headerButtons = [ const headerButtons = [
{ {
key: 'create', key: 'create',
@@ -68,6 +71,30 @@ const headerButtons = [
function initDict() { function initDict() {
dictStore.fetchDictionary('dataStatus'); dictStore.fetchDictionary('dataStatus');
} }
function getRoleNames(row: any): string[] {
if (!Array.isArray(row?.roleNames)) {
return [];
}
const roleNames: unknown[] = row.roleNames;
return [
...new Set(
roleNames
.filter(
(roleName: unknown) =>
roleName !== null &&
roleName !== undefined &&
String(roleName).trim().length > 0,
)
.map(String),
),
];
}
function getVisibleRoleNames(row: any) {
return getRoleNames(row).slice(0, maxVisibleRoleCount);
}
function getHiddenRoleCount(row: any) {
return Math.max(getRoleNames(row).length - maxVisibleRoleCount, 0);
}
const handleSearch = (params: string) => { const handleSearch = (params: string) => {
pageDataRef.value.setQuery({ keyword: params.trim() }); pageDataRef.value.setQuery({ keyword: params.trim() });
}; };
@@ -353,6 +380,58 @@ function isAdmin(data: any) {
{{ row.nickname }} {{ row.nickname }}
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn
prop="roleIds"
align="center"
min-width="260"
:label="$t('sysAccount.roleIds')"
>
<template #default="{ row }">
<ElTooltip
v-if="getRoleNames(row).length > 0"
placement="top"
popper-class="sys-account-role-tooltip"
:show-after="300"
>
<template #content>
<div class="sys-account-role-tooltip__content">
<ElTag
v-for="roleName in getRoleNames(row)"
:key="roleName"
effect="plain"
type="info"
>
{{ roleName }}
</ElTag>
</div>
</template>
<div
class="sys-account-role-summary"
tabindex="0"
:aria-label="getRoleNames(row).join('、')"
>
<ElTag
v-for="roleName in getVisibleRoleNames(row)"
:key="roleName"
class="sys-account-role-tag"
effect="plain"
type="info"
>
{{ roleName }}
</ElTag>
<ElTag
v-if="getHiddenRoleCount(row) > 0"
class="sys-account-role-more"
effect="plain"
type="info"
>
+{{ getHiddenRoleCount(row) }}
</ElTag>
</div>
</ElTooltip>
<span v-else class="sys-account-role-empty">-</span>
</template>
</ElTableColumn>
<ElTableColumn <ElTableColumn
prop="mobile" prop="mobile"
align="center" align="center"
@@ -464,6 +543,48 @@ function isAdmin(data: any) {
align-items: center; align-items: center;
} }
.sys-account-role-summary {
display: flex;
gap: var(--space-1);
align-items: center;
justify-content: center;
min-width: 0;
outline: none;
}
.sys-account-role-summary:focus-visible {
border-radius: var(--radius-control);
box-shadow: 0 0 0 2px hsl(var(--primary) / 24%);
}
.sys-account-role-tag {
max-width: 88px;
}
.sys-account-role-tag :deep(.el-tag__content) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sys-account-role-more {
flex: none;
}
.sys-account-role-empty {
color: hsl(var(--text-muted));
}
:global(.sys-account-role-tooltip) {
max-width: 320px;
}
:global(.sys-account-role-tooltip .sys-account-role-tooltip__content) {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
}
.sys-account-batch-inline__actions :deep(.el-button) { .sys-account-batch-inline__actions :deep(.el-button) {
height: 32px; height: 32px;
min-height: 32px; min-height: 32px;