feat(perm/frontend): PM 视角的权限管理界面与个人有效权限接入

- store/study 与 router 改为读取 /api-permissions/me 拉取当前用户在
  当前项目的有效权限,菜单与路由守卫据此判定,并在 PM 项目缺省时
  主动 ensureDefaultPmStudy。
- api/projectPermissions 暴露 fetchMyApiEndpointPermissions,并允许
  调用方关闭统一错误提示,便于在批量项目权限拉取场景下静默失败。
- 项目管理:Projects 页根据每个项目的有效权限决定项目入口的可点击
  状态,缺权限时禁用项目名跳转;ProjectDetail 据 setup_config 写权限
  渲染保存/回填/清空/发布按钮,并在无读取权限时提示并退出页面。
- 权限管理:PermissionManagement 切换为按角色提交,Admin 视角才能
  编辑 PM 行;按系统模块美化系统级权限展示并标注 PM 可访问项;项目
  成员表的角色下拉受 PM 等级约束,自定义角色入口下线。
- 监控:PermissionMonitoring/PermissionAccessLogs 增加 isAdmin 与
  showSecurityLog 入参,仅 ADMIN 才能看到底层安全日志与统计。
- ApiEndpointPermissions 表格根据当前用户角色禁用不可写行,避免
  误操作 PM 行;MODULE_LABELS 增加 setup_config 的中文展示。
- 测试同步覆盖以上行为:新版 router/Layout/Projects/PermissionManagement
  /PermissionAccessLogs/PermissionMonitoring/PermissionIpLocations 等
  组件均补足检测,并按需为相关组件提供 pinia + localStorage stub。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cheng Zhou
2026-05-25 12:38:54 +08:00
parent 747dd55225
commit 934d114a29
22 changed files with 501 additions and 145 deletions
@@ -89,7 +89,7 @@
</div>
<el-tag effect="dark" size="small" round>{{ terminalLines.length }} </el-tag>
</div>
<div class="log-action-item log-action-security" @click="openSecurityLogDialog">
<div v-if="showSecurityLog" class="log-action-item log-action-security" @click="openSecurityLogDialog">
<div class="log-action-icon security">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</div>
@@ -159,6 +159,11 @@ import type {
} from "@/types/api";
import { Search as SearchIcon } from "@element-plus/icons-vue";
const props = withDefaults(defineProps<{ showSecurityLog?: boolean }>(), {
showSecurityLog: false,
});
const showSecurityLog = computed(() => props.showSecurityLog);
const MetricIconVisit = () => h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, [
h("path", { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" }),
h("circle", { cx: "12", cy: "12", r: "3" }),
@@ -368,6 +373,7 @@ const loadData = async (options: { silent?: boolean } = {}) => {
};
const loadSecurityData = async (options: { silent?: boolean } = {}) => {
if (!showSecurityLog.value) return;
if (!options.silent) securityLoading.value = true;
try {
const res = await fetchSecurityAccessLogs({ status_min: 400, page: 1, page_size: 80 });
@@ -393,13 +399,13 @@ const startRealtimePolling = () => {
stopRealtimePolling();
realtimeTimer.value = window.setInterval(() => {
loadData({ silent: true });
loadSecurityData({ silent: true });
if (showSecurityLog.value) loadSecurityData({ silent: true });
}, REALTIME_POLL_INTERVAL_MS);
};
const refresh = () => {
loadData();
loadSecurityData();
if (showSecurityLog.value) loadSecurityData();
};
const openInterfaceLogDialog = () => {
@@ -409,6 +415,7 @@ const openInterfaceLogDialog = () => {
};
const openSecurityLogDialog = () => {
if (!showSecurityLog.value) return;
securityLogDialogVisible.value = true;
loadSecurityData({ silent: true });
scrollSecurityTerminalToBottom();