前端权限管理:实现接口级权限管理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
@@ -0,0 +1,222 @@
<template>
<div class="api-permissions">
<!-- 搜索和筛选 -->
<div class="api-permissions-toolbar">
<el-input
v-model="searchText"
placeholder="搜索端点..."
clearable
style="width: 250px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-select v-model="filterModule" placeholder="筛选模块" clearable style="width: 150px">
<el-option label="全部模块" value="" />
<el-option
v-for="module in uniqueModules"
:key="module"
:label="module"
:value="module"
/>
</el-select>
<el-select v-model="filterMethod" placeholder="筛选方法" clearable style="width: 120px">
<el-option label="全部方法" value="" />
<el-option label="GET" value="GET" />
<el-option label="POST" value="POST" />
<el-option label="PATCH" value="PATCH" />
<el-option label="DELETE" value="DELETE" />
</el-select>
</div>
<!-- 权限矩阵表格 -->
<div class="api-permissions-table-wrapper">
<el-table :data="filteredEndpoints" border stripe>
<el-table-column prop="endpoint_key" label="端点" width="250">
<template #default="{ row }">
<div class="endpoint-cell">
<el-tag :type="getMethodType(row.method)" size="small">
{{ row.method }}
</el-tag>
<span class="endpoint-path">{{ row.path }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="module" label="模块" width="100" />
<el-table-column
v-for="role in roles"
:key="role"
:label="role"
:width="100"
align="center"
>
<template #default="{ row }">
<el-checkbox
:model-value="isEndpointAllowed(row.endpoint_key, role)"
@change="(val) => onPermissionChange(row.endpoint_key, role, val as boolean)"
/>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import { Search } from "@element-plus/icons-vue";
import type { ApiEndpointPermissionsResponse } from "@/types/api";
interface Endpoint {
endpoint_key: string;
method: string;
path: string;
module: string;
}
interface Props {
project: any;
matrix: ApiEndpointPermissionsResponse | null;
}
interface Emits {
(e: "update", matrix: ApiEndpointPermissionsResponse): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const searchText = ref("");
const filterModule = ref("");
const filterMethod = ref("");
// 从矩阵中提取角色列表
const roles = computed(() => {
if (!props.matrix) return [];
return Object.keys(props.matrix).sort();
});
// 从矩阵中提取端点列表
const endpoints = computed(() => {
if (!props.matrix) return [];
const endpointSet = new Set<string>();
Object.values(props.matrix).forEach((rolePerms) => {
Object.keys(rolePerms).forEach((endpoint) => {
endpointSet.add(endpoint);
});
});
return Array.from(endpointSet)
.map((endpoint_key) => {
const [method, ...pathParts] = endpoint_key.split(":");
const path = pathParts.join(":");
const module = extractModule(path);
return {
endpoint_key,
method,
path,
module,
};
})
.sort((a, b) => a.endpoint_key.localeCompare(b.endpoint_key));
});
// 提取模块名称
const extractModule = (path: string): string => {
const match = path.match(/\/(\w+)/);
return match ? match[1] : "unknown";
};
// 获取唯一的模块列表
const uniqueModules = computed(() => {
return [...new Set(endpoints.value.map((e) => e.module))].sort();
});
// 过滤后的端点列表
const filteredEndpoints = computed(() => {
return endpoints.value.filter((endpoint) => {
const matchSearch =
!searchText.value ||
endpoint.endpoint_key.toLowerCase().includes(searchText.value.toLowerCase()) ||
endpoint.path.toLowerCase().includes(searchText.value.toLowerCase());
const matchModule = !filterModule.value || endpoint.module === filterModule.value;
const matchMethod = !filterMethod.value || endpoint.method === filterMethod.value;
return matchSearch && matchModule && matchMethod;
});
});
// 检查端点是否允许
const isEndpointAllowed = (endpoint_key: string, role: string): boolean => {
if (!props.matrix || !props.matrix[role]) return false;
return props.matrix[role][endpoint_key]?.allowed ?? false;
};
// 获取方法的标签类型
const getMethodType = (method: string): string => {
const typeMap: Record<string, string> = {
GET: "info",
POST: "success",
PATCH: "warning",
DELETE: "danger",
};
return typeMap[method] || "info";
};
// 权限变更处理
const onPermissionChange = (endpoint_key: string, role: string, allowed: boolean) => {
if (!props.matrix) return;
const updatedMatrix: ApiEndpointPermissionsResponse = JSON.parse(JSON.stringify(props.matrix));
if (!updatedMatrix[role]) {
updatedMatrix[role] = {};
}
updatedMatrix[role][endpoint_key] = { allowed };
emit("update", updatedMatrix);
};
</script>
<style scoped lang="scss">
.api-permissions {
padding: 20px 0;
}
.api-permissions-toolbar {
display: flex;
gap: 15px;
margin-bottom: 20px;
align-items: center;
}
.api-permissions-table-wrapper {
overflow-x: auto;
}
.endpoint-cell {
display: flex;
align-items: center;
gap: 10px;
:deep(.el-tag) {
min-width: 50px;
text-align: center;
}
}
.endpoint-path {
font-family: monospace;
font-size: 12px;
color: #606266;
}
</style>