diff --git a/web/static/css/style.css b/web/static/css/style.css index e85c9b6f..30022588 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -36249,16 +36249,88 @@ html[data-theme="dark"] .form-group select { position: relative; } -.rbac-summary-toggle { - margin-left: 4px; - vertical-align: middle; +.rbac-summary-count { + display: block; + margin-top: 3px; + color: var(--text-primary); + font-size: 0.9375rem; + font-weight: 700; + line-height: 1.2; } -.rbac-effective-permissions { - margin: 0 16px 12px; +.rbac-summary-count--interactive { + display: inline-flex; + align-items: center; + margin-top: 3px; + padding: 0 0 1px; + border: none; + border-bottom: 1px dashed color-mix(in srgb, var(--text-secondary) 55%, transparent); + border-radius: 0; + background: none; + color: var(--text-secondary); + font-size: 0.9375rem; + font-weight: 700; + line-height: 1.2; + cursor: pointer; + transition: color 0.15s ease, border-color 0.15s ease; +} + +.rbac-summary-count--interactive:hover { + color: var(--text-primary); + border-bottom-color: color-mix(in srgb, var(--text-secondary) 80%, transparent); +} + +.rbac-summary-count--interactive[aria-expanded="true"] { + color: var(--text-primary); + border-bottom-color: var(--text-secondary); + border-bottom-style: solid; +} + +.rbac-summary-count--interactive:focus-visible { + outline: 2px solid color-mix(in srgb, var(--text-secondary) 35%, transparent); + outline-offset: 3px; +} + +.rbac-permissions-popover { + position: fixed; + z-index: 220; + width: min(380px, calc(100vw - 32px)); +} + +.rbac-permissions-popover[hidden] { + display: none !important; +} + +.rbac-permissions-popover-inner { + position: relative; + overflow: hidden; border: 1px solid var(--border-color); - border-radius: 8px; + border-radius: 10px; background: var(--bg-primary); + box-shadow: var(--shadow-md, 0 8px 24px rgba(15, 23, 42, 0.12)); +} + +.rbac-permissions-popover-inner::before { + content: ''; + position: absolute; + top: -5px; + left: var(--rbac-popover-arrow-left, 18px); + width: 10px; + height: 10px; + background: var(--bg-primary); + border-left: 1px solid var(--border-color); + border-top: 1px solid var(--border-color); + transform: rotate(45deg); +} + +.rbac-permissions-popover-count { + color: var(--text-muted); + font-size: 0.75rem; + font-weight: 500; +} + +.rbac-permissions-popover .rbac-effective-permissions-body { + max-height: min(320px, calc(100vh - 180px)); } .rbac-effective-permissions-head, @@ -36744,6 +36816,11 @@ html[data-theme="dark"] .form-group select { font-size: 0.875rem; } +.rbac-panel-detail .rbac-selected-summary .rbac-summary-count--interactive { + margin-top: 2px; + font-size: 0.875rem; +} + .rbac-panel-detail .rbac-detail-head { min-height: 0; padding: 8px 14px; diff --git a/web/static/js/rbac.js b/web/static/js/rbac.js index cefe0931..43b840ac 100644 --- a/web/static/js/rbac.js +++ b/web/static/js/rbac.js @@ -251,43 +251,120 @@ function resolveUserEffectiveAccess(user) { return { userRoles, effectivePermissions, permissionsByModule, effectiveScope, assignmentCount }; } -function toggleRbacEffectivePermissions() { - rbacState.showEffectivePermissions = !rbacState.showEffectivePermissions; - renderRbacEffectivePermissions(selectedRbacUser()); +let rbacPermissionsPopoverDocBound = false; + +function positionRbacPermissionsPopover() { + const popover = document.getElementById('rbac-permissions-popover'); + const btn = document.querySelector('.rbac-summary-count--interactive'); + if (!popover || !btn || popover.hidden) return; + const rect = btn.getBoundingClientRect(); + const width = Math.min(380, window.innerWidth - 32); + let left = rect.left; + if (left + width > window.innerWidth - 16) left = window.innerWidth - width - 16; + left = Math.max(16, left); + const top = Math.min(rect.bottom + 8, window.innerHeight - 16); + popover.style.width = `${width}px`; + popover.style.left = `${left}px`; + popover.style.top = `${top}px`; + const arrowLeft = Math.min(Math.max(rect.left + rect.width / 2 - left - 5, 12), width - 22); + popover.style.setProperty('--rbac-popover-arrow-left', `${arrowLeft}px`); } -function renderRbacEffectivePermissions(user) { - const box = document.getElementById('rbac-effective-permissions'); - if (!box) return; - if (!user || !rbacState.showEffectivePermissions) { - box.hidden = true; - box.innerHTML = ''; - return; +function onRbacPermissionsPopoverReposition() { + if (rbacState.showEffectivePermissions) positionRbacPermissionsPopover(); +} + +function setRbacPermissionsPopoverOpen(open) { + rbacState.showEffectivePermissions = open; + const popover = document.getElementById('rbac-permissions-popover'); + const btn = document.querySelector('.rbac-summary-count--interactive'); + if (popover) popover.hidden = !open; + if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false'); + if (open) { + requestAnimationFrame(() => positionRbacPermissionsPopover()); } +} + +function closeRbacPermissionsPopover() { + setRbacPermissionsPopoverOpen(false); +} + +function toggleRbacEffectivePermissions(ev) { + if (ev) { + ev.preventDefault(); + ev.stopPropagation(); + } + const opening = !rbacState.showEffectivePermissions; + if (opening) renderRbacEffectivePermissionsContent(selectedRbacUser()); + setRbacPermissionsPopoverOpen(opening); +} + +function onRbacPermissionsPopoverPointerDown(ev) { + if (!rbacState.showEffectivePermissions) return; + const popover = document.getElementById('rbac-permissions-popover'); + const btn = document.querySelector('.rbac-summary-count--interactive'); + if (popover && popover.contains(ev.target)) return; + if (btn && btn.contains(ev.target)) return; + closeRbacPermissionsPopover(); +} + +function bindRbacPermissionsPopoverDismiss() { + if (rbacPermissionsPopoverDocBound) return; + rbacPermissionsPopoverDocBound = true; + document.addEventListener('mousedown', onRbacPermissionsPopoverPointerDown); + document.addEventListener('keydown', ev => { + if (ev.key === 'Escape' && rbacState.showEffectivePermissions) closeRbacPermissionsPopover(); + }); + window.addEventListener('resize', onRbacPermissionsPopoverReposition); + window.addEventListener('scroll', onRbacPermissionsPopoverReposition, true); +} + +function renderRbacEffectivePermissionsContent(user) { + const popover = document.getElementById('rbac-permissions-popover'); + if (!popover || !user) return; const access = resolveUserEffectiveAccess(user); const modules = Object.keys(access.permissionsByModule).sort(); if (!modules.length) { - box.hidden = false; - box.innerHTML = `
${rbacT('rbac.empty.noEffectivePermissions', '暂无有效权限')}${rbacEscape(rbacT('rbac.empty.assignRoleFirst', '分配角色后即可查看权限明细'))}
`; + popover.innerHTML = `
${rbacT('rbac.empty.noEffectivePermissions', '暂无有效权限')}${rbacEscape(rbacT('rbac.empty.assignRoleFirst', '分配角色后即可查看权限明细'))}
`; return; } - box.hidden = false; - box.innerHTML = ` -
- ${rbacEscape(rbacT('rbac.effectivePermissionsDetail', '有效权限明细'))} - -
-
- ${modules.map(module => ` -
-

${rbacEscape(rbacPermissionModuleLabel(module))}${access.permissionsByModule[module].length}

-
- ${access.permissionsByModule[module].sort().map(key => `${rbacEscape(rbacPermissionLabel(key))}`).join('')} -
-
`).join('')} + popover.innerHTML = ` +
+
+ ${rbacEscape(rbacT('rbac.effectivePermissionsDetail', '有效权限明细'))} + ${access.effectivePermissions.size} +
+
+ ${modules.map(module => ` +
+

${rbacEscape(rbacPermissionModuleLabel(module))}${access.permissionsByModule[module].length}

+
+ ${access.permissionsByModule[module].sort().map(key => `${rbacEscape(rbacPermissionLabel(key))}`).join('')} +
+
`).join('')} +
`; } +function renderRbacEffectivePermissions(user) { + const popover = document.getElementById('rbac-permissions-popover'); + const btn = document.querySelector('.rbac-summary-count--interactive'); + if (!popover) return; + if (!user) { + popover.hidden = true; + popover.innerHTML = ''; + rbacState.showEffectivePermissions = false; + if (btn) btn.setAttribute('aria-expanded', 'false'); + return; + } + if (btn) btn.setAttribute('aria-expanded', rbacState.showEffectivePermissions ? 'true' : 'false'); + popover.hidden = !rbacState.showEffectivePermissions; + if (rbacState.showEffectivePermissions) { + renderRbacEffectivePermissionsContent(user); + requestAnimationFrame(() => positionRbacPermissionsPopover()); + } +} + function rbacRoleIsSystem(role) { return !!(role && (role.isSystem || role.is_system)); } @@ -306,6 +383,7 @@ async function initPlatformRbacPage() { if (typeof window !== 'undefined' && window.i18nReady) await window.i18nReady; const typeInput = document.getElementById('rbac-assignment-type'); if (typeInput) rbacState.assignmentResourceType = typeInput.value || 'conversation'; + bindRbacPermissionsPopoverDismiss(); await rbacRun(loadPlatformRbac, rbacT('rbac.errors.loadFailed', '加载失败')); initRbacSelects(); } @@ -562,12 +640,17 @@ function renderRbacRoles() { } else { const access = resolveUserEffectiveAccess(user); const scope = rbacScopeInfo(access.effectiveScope); - const permissionsToggle = access.effectivePermissions.size - ? ` ` - : ''; + const permissionCount = access.effectivePermissions.size; + const permissionCountHtml = permissionCount + ? `` + : `${permissionCount}`; summary.innerHTML = `
${rbacT('rbac.roles', '角色')}${access.userRoles.length}
-
${rbacT('rbac.effectivePermissions', '有效权限')}${access.effectivePermissions.size}${permissionsToggle}
+
+ ${rbacT('rbac.effectivePermissions', '有效权限')} + ${permissionCountHtml} + +
${rbacT('rbac.effectiveScope', '有效资源范围')}${rbacEscape(scope.label)}
${rbacT('rbac.metricAssignments', '资源授权')}${access.assignmentCount}
`; diff --git a/web/templates/index.html b/web/templates/index.html index eaee8184..ee293e24 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -4219,7 +4219,6 @@
-