polish(approval-grants-ui): show granter name, fix note cell rendering, tighten layout

This commit is contained in:
matevip 2026-05-27 15:09:11 +08:00
parent b34de4e078
commit 5f2adf15f6
4 changed files with 77 additions and 7 deletions

View File

@ -15,6 +15,7 @@ import vip.mate.approval.grant.repository.ApprovalGrantMapper;
import vip.mate.approval.grant.repository.ApprovalResolutionLogMapper;
import vip.mate.approval.grant.service.ApprovalGrantService;
import vip.mate.auth.model.UserEntity;
import vip.mate.auth.repository.UserMapper;
import vip.mate.auth.service.AuthService;
import vip.mate.common.result.R;
import vip.mate.exception.MateClawException;
@ -57,6 +58,7 @@ public class ApprovalGrantController {
private final ApprovalGrantMapper grantMapper;
private final ApprovalResolutionLogMapper resolutionMapper;
private final AuthService authService;
private final UserMapper userMapper;
private final WorkspaceService workspaceService;
// Create
@ -139,7 +141,39 @@ public class ApprovalGrantController {
wrapper.eq(ApprovalGrant::getGrantedBy, actorId);
}
Page<ApprovalGrant> pageObj = new Page<>(boundedPage, boundedSize);
return R.ok(grantMapper.selectPage(pageObj, wrapper));
IPage<ApprovalGrant> result = grantMapper.selectPage(pageObj, wrapper);
fillGranterNames(result.getRecords());
return R.ok(result);
}
/**
* Batch-loads the display name (nickname username fallback) for every
* unique {@code grantedBy} id on the page and writes it into the entity's
* transient {@code grantedByName} field. One round-trip via
* {@code selectBatchIds} rather than N queries; the field stays null when
* the source user has since been deleted.
*/
private void fillGranterNames(java.util.List<ApprovalGrant> records) {
if (records == null || records.isEmpty()) {
return;
}
java.util.Set<Long> userIds = new java.util.HashSet<>();
for (ApprovalGrant g : records) {
if (g.getGrantedBy() != null) userIds.add(g.getGrantedBy());
}
if (userIds.isEmpty()) return;
java.util.Map<Long, String> idToName = userMapper.selectBatchIds(userIds).stream()
.collect(java.util.stream.Collectors.toMap(
UserEntity::getId,
u -> u.getNickname() != null && !u.getNickname().isEmpty()
? u.getNickname()
: u.getUsername(),
(a, b) -> a));
for (ApprovalGrant g : records) {
if (g.getGrantedBy() != null) {
g.setGrantedByName(idToName.get(g.getGrantedBy()));
}
}
}
// Active summary (chip "(N)")

View File

@ -52,6 +52,15 @@ public class ApprovalGrant {
private Long grantedBy;
/**
* Display name of the granter (nickname username). Not persisted; the
* controller fills it in after {@code selectPage} by batch-loading the
* touched user ids so the UI doesn't need a separate /users call for a
* snowflake name lookup. Null when the user no longer exists.
*/
@TableField(exist = false)
private String grantedByName;
private LocalDateTime grantedAt;
private Integer revoked;

View File

@ -1043,6 +1043,8 @@ export interface ApprovalGrant {
grantKind: GrantKind
expireAt: string | null
grantedBy: string
/** Display name (nickname → username) of the granter; null if the user was deleted. */
grantedByName?: string | null
grantedAt: string
revoked: number
revokedBy: string | null

View File

@ -104,22 +104,35 @@
</td>
<td>{{ t(`approval.grant.kind.${kindI18nKey(row.grantKind)}`) }}</td>
<td class="muted">{{ formatDate(row.expireAt) }}</td>
<td>{{ row.grantedBy }}</td>
<td class="note-cell" :title="row.note || ''">{{ row.note }}</td>
<td>
<span v-if="row.grantedByName" :title="`#${row.grantedBy}`">{{ row.grantedByName }}</span>
<span v-else class="muted">#{{ row.grantedBy }}</span>
</td>
<td>
<span class="note-cell" :title="row.note || ''">{{ row.note }}</span>
</td>
<td class="col-actions">
<button
v-if="row.revoked === 0"
class="action-btn danger"
class="row-action-btn row-action-btn--danger"
:title="t('approval.grant.revokeBtn')"
@click="confirmRevoke(row)"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"
<svg width="13" height="13" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
<span>{{ t('approval.grant.revokeBtn') }}</span>
</button>
<span v-else class="muted">{{ t('common.revoked') }}</span>
<span v-else class="revoked-pill">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="9"/>
<line x1="5.6" y1="5.6" x2="18.4" y2="18.4"/>
</svg>
{{ t('common.revoked') }}
</span>
</td>
</tr>
</tbody>
@ -610,7 +623,19 @@ onMounted(loadGrants)
overflow-x: auto;
}
.rules-table {
min-width: 900px;
min-width: 1100px; /* enough headroom so the kind / granter / date columns
don't collapse into vertical-text mode on a narrow viewport */
}
/* Prevent narrow columns from wrapping into stacked Chinese glyphs. */
.rules-table th,
.rules-table td {
white-space: nowrap;
}
/* Note cell is the one cell where wrapping is fine it's already
ellipsis-truncated by .note-cell. Keep this explicit so adding nowrap to
the wider scope above doesn't suppress the ellipsis. */
.rules-table td .note-cell {
white-space: nowrap;
}
/* Pagination row right-aligned beneath the table. McPagination provides