前端权限管理:实现接口级权限管理UI和监控仪表板

新增功能:
- 新增 ApiPermissions.vue 主页面,支持模块级/接口级权限切换
- 新增 ProjectPermissionsModule.vue 组件,展示模块级权限矩阵
- 新增 ApiEndpointPermissions.vue 组件,展示接口级权限矩阵
- 新增 PermissionMonitoring.vue 组件,展示权限系统监控仪表板
- 更新 projectPermissions.ts API 客户端,支持接口级权限和监控API
- 新增权限相关的 TypeScript 类型定义
- 更新路由配置,添加接口级权限管理路由
- 更新国际化文本,添加权限管理相关的中文文本

API 客户端新增函数:
- fetchApiEndpointPermissions: 获取接口级权限矩阵
- updateApiEndpointPermissions: 更新接口级权限矩阵
- fetchPermissionMetrics: 获取权限检查指标
- fetchCacheStats: 获取缓存统计
- fetchPermissionAlerts: 获取告警列表
- fetchPermissionHealth: 获取系统健康状态
- resetPermissionMetrics: 重置指标
- clearPermissionAlerts: 清除告警

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Cheng Zhou
2026-05-14 09:19:07 +08:00
parent 1247b64e91
commit 387d035b08
8 changed files with 1107 additions and 2 deletions
+279
View File
@@ -0,0 +1,279 @@
<template>
<div class="permission-page">
<div class="permission-shell unified-shell" v-loading="loading">
<!-- 顶部操作栏 -->
<div class="permission-header unified-action-bar">
<div class="permission-title">
<el-icon><Key /></el-icon>
<h2>权限管理</h2>
</div>
<div class="permission-project-meta">
<span class="meta-item">
<span class="meta-label">项目编号</span>
<strong>{{ project?.code || "-" }}</strong>
</span>
<span class="meta-separator" />
<span class="meta-item">
<span class="meta-label">项目名称</span>
<strong>{{ project?.name || "-" }}</strong>
</span>
</div>
<div class="permission-actions">
<el-button @click="resetMetrics" :loading="resetting">
<el-icon><RefreshRight /></el-icon>
刷新指标
</el-button>
<el-button type="primary" :loading="saving" :disabled="!dirty" @click="save">
<el-icon><Check /></el-icon>
保存
</el-button>
</div>
</div>
<!-- 标签页模块级权限 vs 接口级权限 -->
<el-tabs v-model="activeTab">
<!-- 标签1模块级权限现有 -->
<el-tab-pane label="模块级权限" name="module">
<ProjectPermissionsModule
:project="project"
:matrix="moduleMatrix"
@update="onModuleMatrixUpdate"
/>
</el-tab-pane>
<!-- 标签2接口级权限新增 -->
<el-tab-pane label="接口级权限" name="api">
<ApiEndpointPermissions
:project="project"
:matrix="apiMatrix"
@update="onApiMatrixUpdate"
/>
</el-tab-pane>
<!-- 标签3权限监控新增 -->
<el-tab-pane label="权限监控" name="monitoring">
<PermissionMonitoring
:metrics="metrics"
:health="health"
:alerts="alerts"
@refresh="loadMonitoringData"
/>
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { Key, RefreshRight, Check } from "@element-plus/icons-vue";
import type { Study } from "@/types/api";
import type {
ProjectRolePermissionsResponse,
ApiEndpointPermissionsResponse,
PermissionMetricsResponse,
CacheStatsResponse,
AlertsResponse,
HealthResponse,
} from "@/types/api";
import {
fetchProjectRolePermissions,
updateProjectRolePermissions,
fetchApiEndpointPermissions,
updateApiEndpointPermissions,
fetchPermissionMetrics,
fetchCacheStats,
fetchPermissionAlerts,
fetchPermissionHealth,
resetPermissionMetrics,
} from "@/api/projectPermissions";
import { useStudyStore } from "@/store/study";
import ProjectPermissionsModule from "@/components/ProjectPermissionsModule.vue";
import ApiEndpointPermissions from "@/components/ApiEndpointPermissions.vue";
import PermissionMonitoring from "@/components/PermissionMonitoring.vue";
const route = useRoute();
const studyStore = useStudyStore();
const activeTab = ref<"module" | "api" | "monitoring">("module");
const loading = ref(false);
const saving = ref(false);
const resetting = ref(false);
const project = computed(() => studyStore.currentStudy);
const studyId = computed(() => route.params.id as string);
// 权限数据
const moduleMatrix = ref<ProjectRolePermissionsResponse | null>(null);
const apiMatrix = ref<ApiEndpointPermissionsResponse | null>(null);
// 监控数据
const metrics = ref<PermissionMetricsResponse | null>(null);
const health = ref<HealthResponse | null>(null);
const alerts = ref<AlertsResponse | null>(null);
// 脏值检测
const dirty = ref(false);
const loadPermissionData = async () => {
if (!studyId.value) return;
loading.value = true;
try {
const [moduleRes, apiRes] = await Promise.all([
fetchProjectRolePermissions(studyId.value),
fetchApiEndpointPermissions(studyId.value),
]);
moduleMatrix.value = moduleRes;
apiMatrix.value = apiRes;
dirty.value = false;
} catch (error) {
ElMessage.error("加载权限数据失败");
console.error(error);
} finally {
loading.value = false;
}
};
const loadMonitoringData = async () => {
if (!studyId.value) return;
try {
const [metricsRes, healthRes, alertsRes] = await Promise.all([
fetchPermissionMetrics(),
fetchPermissionHealth(),
fetchPermissionAlerts(undefined, 20),
]);
metrics.value = metricsRes;
health.value = healthRes;
alerts.value = alertsRes;
} catch (error) {
ElMessage.error("加载监控数据失败");
console.error(error);
}
};
const onModuleMatrixUpdate = (newMatrix: ProjectRolePermissionsResponse) => {
moduleMatrix.value = newMatrix;
dirty.value = true;
};
const onApiMatrixUpdate = (newMatrix: ApiEndpointPermissionsResponse) => {
apiMatrix.value = newMatrix;
dirty.value = true;
};
const save = async () => {
if (!studyId.value || !dirty.value) return;
saving.value = true;
try {
// 保存当前活跃标签的权限
if (activeTab.value === "module" && moduleMatrix.value) {
await updateProjectRolePermissions(studyId.value, {
roles: moduleMatrix.value.roles,
});
} else if (activeTab.value === "api" && apiMatrix.value) {
await updateApiEndpointPermissions(studyId.value, apiMatrix.value);
}
dirty.value = false;
ElMessage.success("权限已保存");
} catch (error) {
ElMessage.error("保存权限失败");
console.error(error);
} finally {
saving.value = false;
}
};
const resetMetrics = async () => {
resetting.value = true;
try {
await resetPermissionMetrics();
await loadMonitoringData();
ElMessage.success("指标已重置");
} catch (error) {
ElMessage.error("重置指标失败");
console.error(error);
} finally {
resetting.value = false;
}
};
onMounted(() => {
loadPermissionData();
});
</script>
<style scoped lang="scss">
.permission-page {
padding: 20px;
}
.permission-shell {
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.permission-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
border-bottom: 1px solid #ebeef5;
gap: 20px;
}
.permission-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
h2 {
margin: 0;
font-size: 18px;
}
}
.permission-project-meta {
display: flex;
align-items: center;
gap: 20px;
flex: 1;
font-size: 14px;
color: #606266;
}
.meta-item {
display: flex;
align-items: center;
gap: 8px;
}
.meta-label {
color: #909399;
}
.meta-separator {
width: 1px;
height: 20px;
background: #dcdfe6;
}
.permission-actions {
display: flex;
gap: 10px;
}
:deep(.el-tabs) {
padding: 20px;
}
</style>