完善权限缓存与监控指标

This commit is contained in:
Cheng Zhou
2026-05-19 16:03:27 +08:00
parent f8a959b801
commit e95eeed90e
14 changed files with 603 additions and 335 deletions
+309 -222
View File
@@ -1,202 +1,197 @@
<template>
<div class="permission-mgmt-page">
<div class="permission-mgmt-shell unified-shell" v-loading="pageLoading">
<el-tabs v-model="activeTab" class="mgmt-tabs">
<div class="perm-page" v-loading="pageLoading">
<!--
标签页一项目权限配置
-->
<el-tab-pane label="项目权限配置" name="project">
<div class="project-tab-layout">
<!-- 项目选择栏 -->
<div class="project-selector-bar unified-action-bar">
<div class="filter-form">
<span class="selector-label">选择项目</span>
<el-select
v-model="selectedStudyId"
placeholder="请选择项目"
filterable
style="width: 320px"
@change="onStudyChange"
>
<el-option
v-for="s in studies"
:key="s.id"
:label="`${s.code} · ${s.name}`"
:value="s.id"
/>
</el-select>
<div class="filter-spacer" />
<template v-if="selectedStudyId && projectSubTab === 'api'">
<el-button type="default" @click="templateDrawerVisible = true">
<el-icon><Files /></el-icon>
模板管理
</el-button>
<el-button type="primary" :loading="saving" :disabled="!dirty" @click="save">
<el-icon><Check /></el-icon>
保存
</el-button>
</template>
<template v-if="selectedStudyId && projectSubTab === 'members'">
<el-button type="primary" @click="openAddMember">
添加成员
</el-button>
</template>
</div>
</div>
<!-- 未选项目时的空态 -->
<div v-if="!selectedStudyId" class="empty-hint">
<el-empty description="请先选择一个项目以配置权限" />
</div>
<!-- 已选项目子标签 -->
<template v-else>
<el-tabs v-model="projectSubTab" class="project-sub-tabs">
<!-- 接口级权限 -->
<el-tab-pane label="接口级权限" name="api">
<PermissionTemplateSelector
:study-id="selectedStudyId"
:current-permissions="currentPermissionsForTemplate"
:active-roles="activeRolesInStudy"
@edit-role="openRoleEditor"
/>
<el-divider />
<ApiEndpointPermissions
:project="selectedStudy"
:matrix="apiMatrix"
@update="onApiMatrixUpdate"
/>
</el-tab-pane>
<!-- 成员管理 -->
<el-tab-pane label="成员管理" name="members">
<el-table :data="memberRows" v-loading="membersLoading" stripe>
<el-table-column prop="full_name" label="姓名" min-width="140" />
<el-table-column label="项目角色" min-width="140">
<template #default="{ row }">
<el-select
v-model="row.role_in_study"
size="small"
style="width: 130px"
@change="(val: string) => updateMemberRole(row.id, val)"
:disabled="!canEditMember(row) || !row.is_active || row.effectiveStatus === 'DISABLED_GLOBAL'"
>
<el-option v-for="(label, val) in activeRoleLabels" :key="val" :label="label" :value="val" />
</el-select>
</template>
</el-table-column>
<el-table-column label="状态" width="110">
<template #default="{ row }">
<el-tooltip v-if="row.effectiveStatus === 'DISABLED_GLOBAL'" content="请前往账号管理启用该账号" placement="top">
<el-tag type="danger">全局已禁用</el-tag>
</el-tooltip>
<el-tag v-else :type="row.is_active ? 'success' : 'danger'">
{{ row.is_active ? "启用" : "已禁用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="added_at" label="加入时间" min-width="160">
<template #default="{ row }">{{ displayDateTime(row.added_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="160" align="center">
<template #default="{ row }">
<el-button
link
:type="row.is_active ? 'warning' : 'primary'"
size="small"
:disabled="!canEditMember(row) || row.effectiveStatus === 'DISABLED_GLOBAL'"
@click="toggleMemberActive(row)"
>
{{ row.is_active ? "停用" : "启用" }}
</el-button>
<el-button link type="danger" size="small" :disabled="!canEditMember(row)" @click="deleteMember(row)">
移除
</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</template>
</div>
</el-tab-pane>
<!--
标签页二系统级权限
-->
<el-tab-pane label="系统级权限" name="system">
<div class="system-tab-layout">
<el-alert
title="系统级权限为只读展示,所有系统管理操作当前仅限 ADMIN 角色执行。"
type="info"
:closable="false"
show-icon
style="margin-bottom: 16px"
<!--
项目权限配置
-->
<template v-if="activeTab === 'project'">
<div class="perm-header">
<div class="perm-header-left">
<h2 class="perm-title">项目权限配置</h2>
<span class="perm-subtitle">为每个项目的角色配置接口级权限</span>
</div>
<div class="perm-header-actions">
<el-select
v-model="selectedStudyId"
placeholder="选择项目"
filterable
style="width: 280px"
@change="onStudyChange"
>
<el-option
v-for="s in studies"
:key="s.id"
:label="`${s.code} · ${s.name}`"
:value="s.id"
/>
<div v-for="(items, moduleKey) in systemPermissionsByModule" :key="moduleKey" class="system-module-block">
<div class="system-module-header">
<span class="system-module-title">{{ systemModuleLabels[moduleKey] || moduleKey }}</span>
<el-tag size="small" type="info">{{ items.length }} 项操作</el-tag>
</el-select>
</div>
</div>
<div class="perm-body">
<div v-if="!selectedStudyId" class="perm-empty">
<el-empty description="请先选择一个项目以配置权限" />
</div>
<template v-else>
<el-tabs v-model="projectSubTab" class="project-sub-tabs">
<el-tab-pane label="角色权限" name="api">
<div class="sub-tab-toolbar">
<div class="sub-tab-spacer" />
<el-button @click="templateDrawerVisible = true">
<el-icon><Files /></el-icon>
模板管理
</el-button>
<el-button type="primary" :loading="saving" :disabled="!dirty" @click="save">
<el-icon><Check /></el-icon>
保存
</el-button>
</div>
<el-table :data="items" border size="small" class="system-perm-table">
<el-table-column prop="description" label="操作描述" min-width="180" />
<el-table-column prop="permission_key" label="权限标识" min-width="220">
<PermissionTemplateSelector
:study-id="selectedStudyId"
:current-permissions="currentPermissionsForTemplate"
:active-roles="activeRolesInStudy"
@edit-role="openRoleEditor"
/>
<el-divider />
<ApiEndpointPermissions
:project="selectedStudy"
:matrix="apiMatrix"
@update="onApiMatrixUpdate"
/>
</el-tab-pane>
<el-tab-pane label="成员管理" name="members">
<div class="sub-tab-toolbar">
<div class="sub-tab-spacer" />
<el-button type="primary" @click="openAddMember">添加成员</el-button>
</div>
<el-table :data="memberRows" v-loading="membersLoading" stripe class="full-table">
<el-table-column prop="full_name" label="姓名" min-width="140" />
<el-table-column label="项目角色" min-width="140">
<template #default="{ row }">
<code class="perm-key">{{ row.permission_key }}</code>
<el-select
v-model="row.role_in_study"
size="small"
style="width: 130px"
@change="(val: string) => updateMemberRole(row.id, val)"
:disabled="!canEditMember(row) || !row.is_active || row.effectiveStatus === 'DISABLED_GLOBAL'"
>
<el-option v-for="(label, val) in activeRoleLabels" :key="val" :label="label" :value="val" />
</el-select>
</template>
</el-table-column>
<el-table-column prop="action" label="操作类型" width="100">
<el-table-column label="状态" width="110">
<template #default="{ row }">
<el-tag :type="opTagType(row.action)" size="small">
{{ opActionLabel(row.action) }}
<el-tooltip v-if="row.effectiveStatus === 'DISABLED_GLOBAL'" content="请前往账号管理启用该账号" placement="top">
<el-tag type="danger">全局已禁用</el-tag>
</el-tooltip>
<el-tag v-else :type="row.is_active ? 'success' : 'danger'">
{{ row.is_active ? "启用" : "已禁用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="授权角色" width="120">
<el-table-column prop="added_at" label="加入时间" min-width="160">
<template #default="{ row }">{{ displayDateTime(row.added_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="160" align="center">
<template #default="{ row }">
<el-tag v-for="r in row.roles" :key="r" size="small" type="danger" style="margin-right: 4px">{{ r }}</el-tag>
<el-button
link
:type="row.is_active ? 'warning' : 'primary'"
size="small"
:disabled="!canEditMember(row) || row.effectiveStatus === 'DISABLED_GLOBAL'"
@click="toggleMemberActive(row)"
>
{{ row.is_active ? "停用" : "启用" }}
</el-button>
<el-button link type="danger" size="small" :disabled="!canEditMember(row)" @click="deleteMember(row)">
移除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-if="systemLoading" class="empty-hint">
<el-skeleton :rows="6" animated />
</div>
<div v-else-if="!systemPermissions.length" class="empty-hint">
<el-empty description="暂无系统级权限数据" />
</div>
</div>
</el-tab-pane>
</el-tab-pane>
</el-tabs>
</template>
</div>
</template>
<!--
标签页三权限监控
-->
<el-tab-pane label="权限监控" name="monitoring">
<div class="monitoring-tab-layout">
<div class="unified-action-bar" style="margin-bottom: 12px">
<div class="filter-form">
<div class="filter-spacer" />
<el-button @click="loadMonitoringData" :loading="monitoringLoading">
<el-icon><RefreshRight /></el-icon>
刷新
</el-button>
<el-button @click="doResetMetrics" :loading="resetting">重置指标</el-button>
</div>
<!--
系统级权限
-->
<template v-else-if="activeTab === 'system'">
<div class="perm-header">
<div class="perm-header-left">
<h2 class="perm-title">系统级权限</h2>
<span class="perm-subtitle">只读展示所有系统管理操作仅限 ADMIN 角色执行</span>
</div>
</div>
<div class="perm-body">
<div v-if="systemLoading" class="perm-empty">
<el-skeleton :rows="8" animated />
</div>
<div v-else-if="!systemPermissions.length" class="perm-empty">
<el-empty description="暂无系统级权限数据" />
</div>
<template v-else>
<div v-for="(items, moduleKey) in systemPermissionsByModule" :key="moduleKey" class="system-module-block">
<div class="system-module-header">
<span class="system-module-title">{{ systemModuleLabels[moduleKey] || moduleKey }}</span>
<el-tag size="small" type="info" round>{{ items.length }} 项操作</el-tag>
</div>
<PermissionMonitoring
:metrics="metrics"
:health="health"
:alerts="alerts"
@refresh="loadMonitoringData"
/>
<el-table :data="items" border size="small" class="system-perm-table">
<el-table-column prop="description" label="操作描述" min-width="180" />
<el-table-column prop="permission_key" label="权限标识" min-width="220">
<template #default="{ row }">
<code class="perm-key">{{ row.permission_key }}</code>
</template>
</el-table-column>
<el-table-column prop="action" label="操作类型" width="100">
<template #default="{ row }">
<el-tag :type="opTagType({ operation_key: row.permission_key, action: row.action })" size="small">
{{ opActionLabel({ operation_key: row.permission_key, action: row.action }) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="授权角色" width="120">
<template #default="{ row }">
<el-tag v-for="r in row.roles" :key="r" size="small" type="danger" style="margin-right: 4px">{{ r }}</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
</template>
</div>
</template>
<!--
权限监控
-->
<template v-else-if="activeTab === 'monitoring'">
<div class="perm-header">
<div class="perm-header-left">
<h2 class="perm-title">权限监控</h2>
<span class="perm-subtitle">实时监控权限使用情况与异常告警</span>
</div>
<div class="perm-header-actions">
<el-button @click="loadMonitoringData" :loading="monitoringLoading">
<el-icon><RefreshRight /></el-icon>
刷新
</el-button>
<el-button @click="doResetMetrics" :loading="resetting">重置指标</el-button>
</div>
</div>
<div class="perm-body">
<div class="perm-body-padded">
<PermissionMonitoring
:metrics="metrics"
:health="health"
:alerts="alerts"
@refresh="loadMonitoringData"
/>
</div>
</div>
</template>
</el-tabs>
</div>
<!--
模板管理抽屉
@@ -316,7 +311,7 @@
:model-value="roleEditorDraft[op.operation_key] ?? false"
@change="(v: boolean) => roleEditorDraft[op.operation_key] = v"
/>
<el-tag :type="opTagType(op.action)" size="small" class="op-action-tag">{{ opActionLabel(op.action) }}</el-tag>
<el-tag :type="opTagType(op)" size="small" class="op-action-tag">{{ opActionLabel(op) }}</el-tag>
<span class="op-desc">{{ op.description || op.operation_key }}</span>
</div>
</div>
@@ -460,7 +455,19 @@ const ROLE_RANK: Record<string, number> = {
};
// ── 顶层标签 ──
const activeTab = ref<"project" | "system" | "monitoring">("project");
const tabFromPath = (): "project" | "system" | "monitoring" => {
const p = route.path;
if (p.endsWith("/system")) return "system";
if (p.endsWith("/monitoring")) return "monitoring";
return "project";
};
const activeTab = computed({
get: () => tabFromPath(),
set: (tab: "project" | "system" | "monitoring") => {
const pathMap = { system: "/admin/permissions/system", project: "/admin/permissions/project", monitoring: "/admin/permissions/monitoring" };
router.push({ path: pathMap[tab], query: route.query });
},
});
const pageLoading = ref(false);
// ═══════════════════════════════════════════
@@ -928,11 +935,25 @@ const ROLE_EDITOR_MODULE_LABELS: Record<string, string> = {
const roleEditorModuleLabel = (mod: string) => ROLE_EDITOR_MODULE_LABELS[mod] || mod;
const opActionLabel = (action: string) =>
({ create: "创建", read: "读取", update: "更新", delete: "删除", export: "导出", write: "写入" }[action] ?? action);
const getOpVerb = (op: { operation_key: string; action: string }): string => {
const suffix = op.operation_key.split(":").pop() || "";
if (suffix === "create") return "create";
if (suffix === "read" || suffix === "list") return "read";
if (suffix === "update") return "update";
if (suffix === "delete") return "delete";
if (suffix === "export") return "export";
return op.action;
};
const opTagType = (action: string) =>
({ create: "success", read: "info", update: "warning", delete: "danger", export: "", write: "success" } as Record<string, string>)[action] ?? "info";
const opActionLabel = (op: { operation_key: string; action: string }) => {
const verb = getOpVerb(op);
return ({ create: "创建", read: "读取", update: "更新", delete: "删除", export: "导出", write: "写入" }[verb] ?? verb);
};
const opTagType = (op: { operation_key: string; action: string }) => {
const verb = getOpVerb(op);
return (({ create: "success", read: "info", update: "warning", delete: "danger", export: "", write: "success" } as Record<string, string>)[verb] ?? "info");
};
const roleEditorGrouped = computed(() => {
const search = roleEditorSearch.value.toLowerCase();
@@ -1017,9 +1038,9 @@ const saveRoleEditor = async () => {
// ═══════════════════════════════════════════
// 懒加载 & 初始化
// ═══════════════════════════════════════════
watch(activeTab, (tab) => {
if (tab === "system" && !systemPermissions.value.length) loadSystemPermissions();
if (tab === "monitoring" && !metrics.value) loadMonitoringData();
watch(() => route.path, (path) => {
if (path.endsWith("/system") && !systemPermissions.value.length) loadSystemPermissions();
if (path.endsWith("/monitoring") && !metrics.value) loadMonitoringData();
});
onMounted(async () => {
@@ -1034,71 +1055,137 @@ onMounted(async () => {
await Promise.all([loadPermissionData(), loadMembers(), loadActiveRoles()]);
loadCandidates();
}
const tab = tabFromPath();
if (tab === "system" && !systemPermissions.value.length) loadSystemPermissions();
if (tab === "monitoring" && !metrics.value) loadMonitoringData();
});
</script>
<style scoped>
.permission-mgmt-page { padding: 20px; }
/* ── 页面容器:铺满,无 padding ── */
.perm-page {
display: flex;
flex-direction: column;
margin: -6px -8px;
min-height: calc(100vh - 52px);
min-height: calc(100dvh - 52px);
background: #f5f7fa;
}
.permission-mgmt-shell {
/* ── 顶部标题栏 ── */
.perm-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 28px 16px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
border-bottom: 1px solid #e8edf3;
flex-shrink: 0;
gap: 16px;
}
.mgmt-tabs { padding: 0 20px 20px; }
.project-tab-layout,
.system-tab-layout,
.monitoring-tab-layout { padding-top: 4px; }
.project-selector-bar { margin-bottom: 16px; }
.selector-label {
font-size: 14px;
color: #606266;
white-space: nowrap;
.perm-header-left {
display: flex;
flex-direction: column;
gap: 3px;
}
.project-sub-tabs { margin-top: 4px; }
.perm-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #1a1f2e;
line-height: 1.3;
}
.empty-hint { padding: 60px 0; }
.perm-subtitle {
font-size: 13px;
color: #8c96a8;
}
.filter-form {
.perm-header-actions {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
flex-shrink: 0;
}
.filter-spacer { flex: 1; }
/* ── 内容区:可滚动 ── */
.perm-body {
flex: 1;
overflow-y: auto;
padding: 20px 0 20px;
}
.system-module-block { margin-bottom: 24px; }
.perm-empty {
padding: 80px 28px;
display: flex;
justify-content: center;
}
.perm-body-padded {
padding: 0 28px;
}
/* ── 项目权限:子 tabs ── */
.project-sub-tabs {
background: #fff;
border-radius: 0;
padding: 0 28px 24px;
}
:deep(.project-sub-tabs .el-tabs__header) {
margin-bottom: 16px;
}
.sub-tab-toolbar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.sub-tab-spacer { flex: 1; }
.full-table { width: 100%; }
/* ── 系统级权限:模块块 ── */
.system-module-block {
background: #fff;
border-radius: 0;
padding: 20px 28px;
margin-bottom: 12px;
border-bottom: 1px solid #eef0f4;
}
.system-module-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
margin-bottom: 14px;
}
.system-module-title {
font-size: 14px;
font-size: 15px;
font-weight: 600;
color: #303133;
color: #1a1f2e;
}
.system-perm-table { width: 100%; }
.perm-key {
font-family: monospace;
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 12px;
background: #f5f7fa;
padding: 2px 6px;
border-radius: 3px;
color: #606266;
background: #f0f4f8;
padding: 2px 8px;
border-radius: 4px;
color: #4a5568;
}
/* ── 抽屉内部 ── */
.filter-spacer { flex: 1; }
.drawer-toolbar {
display: flex;
align-items: center;
@@ -1117,6 +1204,7 @@ onMounted(async () => {
border-top: 1px solid #edf2f8;
}
/* ── 生效角色列表 ── */
.active-roles-list {
display: flex;
flex-direction: column;
@@ -1157,6 +1245,7 @@ onMounted(async () => {
.template-name { font-weight: 500; }
/* ── 角色编辑器 ── */
.role-editor-layout {
display: flex;
height: calc(100vh - 200px);
@@ -1230,9 +1319,7 @@ onMounted(async () => {
margin-bottom: 16px;
}
.role-editor-module {
margin-bottom: 20px;
}
.role-editor-module { margin-bottom: 20px; }
.role-editor-module-header {
display: flex;