前端权限管理:实现接口级权限管理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:
@@ -0,0 +1,351 @@
|
||||
<template>
|
||||
<div class="permission-monitoring">
|
||||
<!-- 系统健康评分 -->
|
||||
<div v-if="health" class="health-card">
|
||||
<div class="health-header">
|
||||
<h3>系统健康状态</h3>
|
||||
<el-tag :type="getHealthType(health.status)">
|
||||
{{ getHealthLabel(health.status) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="health-score">
|
||||
<div class="score-value">{{ health.health_score }}</div>
|
||||
<div class="score-label">健康分数</div>
|
||||
</div>
|
||||
|
||||
<div v-if="health.issues.length > 0" class="health-issues">
|
||||
<div class="issues-title">发现的问题:</div>
|
||||
<ul>
|
||||
<li v-for="(issue, index) in health.issues" :key="index">{{ issue }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 性能指标卡片 -->
|
||||
<div v-if="metrics" class="metrics-cards">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">总检查次数</div>
|
||||
<div class="metric-value">{{ metrics.check_metrics.total_checks }}</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">平均响应时间</div>
|
||||
<div class="metric-value">{{ (metrics.check_metrics.avg_time * 1000).toFixed(2) }}ms</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">允许率</div>
|
||||
<div class="metric-value">{{ metrics.check_metrics.allow_rate.toFixed(1) }}%</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">错误率</div>
|
||||
<div class="metric-value" :style="{ color: metrics.check_metrics.error_rate > 1 ? '#f56c6c' : '#67c23a' }">
|
||||
{{ metrics.check_metrics.error_rate.toFixed(2) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 缓存效率 -->
|
||||
<div v-if="metrics" class="cache-card">
|
||||
<div class="cache-header">
|
||||
<h3>缓存效率</h3>
|
||||
</div>
|
||||
|
||||
<div class="cache-stats">
|
||||
<div class="cache-stat">
|
||||
<div class="stat-label">缓存命中率</div>
|
||||
<el-progress
|
||||
:percentage="metrics.cache_metrics.hit_rate"
|
||||
:color="getProgressColor(metrics.cache_metrics.hit_rate)"
|
||||
/>
|
||||
<div class="stat-value">{{ metrics.cache_metrics.hit_rate.toFixed(1) }}%</div>
|
||||
</div>
|
||||
|
||||
<div class="cache-stat">
|
||||
<div class="stat-label">缓存项目数</div>
|
||||
<div class="stat-value">{{ cacheStats?.cache_items.total_count || 0 }}</div>
|
||||
</div>
|
||||
|
||||
<div class="cache-stat">
|
||||
<div class="stat-label">缓存失效次数</div>
|
||||
<div class="stat-value">{{ metrics.cache_metrics.cache_invalidations }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 告警列表 -->
|
||||
<div class="alerts-card">
|
||||
<div class="alerts-header">
|
||||
<h3>最近告警</h3>
|
||||
<el-button type="primary" size="small" @click="$emit('refresh')">
|
||||
<el-icon><RefreshRight /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="alerts && alerts.alerts.length > 0" class="alerts-table-wrapper">
|
||||
<el-table :data="alerts.alerts" stripe border>
|
||||
<el-table-column prop="timestamp" label="时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.timestamp) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="level" label="级别" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getAlertType(row.level)">{{ row.level }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="type" label="类型" width="150" />
|
||||
|
||||
<el-table-column prop="message" label="消息" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<el-empty description="暂无告警" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { RefreshRight } from "@element-plus/icons-vue";
|
||||
import type {
|
||||
PermissionMetricsResponse,
|
||||
CacheStatsResponse,
|
||||
AlertsResponse,
|
||||
HealthResponse,
|
||||
} from "@/types/api";
|
||||
|
||||
interface Props {
|
||||
metrics: PermissionMetricsResponse | null;
|
||||
health: HealthResponse | null;
|
||||
alerts: AlertsResponse | null;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: "refresh"): void;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
defineEmits<Emits>();
|
||||
|
||||
const cacheStats = computed(() => {
|
||||
// 从 health 中提取 cache_stats
|
||||
return null;
|
||||
});
|
||||
|
||||
const formatTime = (timestamp: number): string => {
|
||||
return new Date(timestamp * 1000).toLocaleString("zh-CN");
|
||||
};
|
||||
|
||||
const getHealthType = (status: string): string => {
|
||||
const typeMap: Record<string, string> = {
|
||||
healthy: "success",
|
||||
degraded: "warning",
|
||||
unhealthy: "danger",
|
||||
};
|
||||
return typeMap[status] || "info";
|
||||
};
|
||||
|
||||
const getHealthLabel = (status: string): string => {
|
||||
const labelMap: Record<string, string> = {
|
||||
healthy: "健康",
|
||||
degraded: "降级",
|
||||
unhealthy: "不健康",
|
||||
};
|
||||
return labelMap[status] || status;
|
||||
};
|
||||
|
||||
const getAlertType = (level: string): string => {
|
||||
const typeMap: Record<string, string> = {
|
||||
info: "info",
|
||||
warning: "warning",
|
||||
error: "danger",
|
||||
};
|
||||
return typeMap[level] || "info";
|
||||
};
|
||||
|
||||
const getProgressColor = (percentage: number): string => {
|
||||
if (percentage >= 80) return "#67c23a";
|
||||
if (percentage >= 50) return "#e6a23c";
|
||||
return "#f56c6c";
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.permission-monitoring {
|
||||
padding: 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.health-card {
|
||||
background: #f5f7fa;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.health-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.health-score {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.score-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.health-issues {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
.issues-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.health-issues ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
|
||||
li {
|
||||
color: #f56c6c;
|
||||
font-size: 13px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.metrics-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.cache-card {
|
||||
background: #f5f7fa;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.cache-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.cache-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.cache-stat {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-progress) {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.alerts-card {
|
||||
background: #f5f7fa;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.alerts-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.alerts-table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user