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:
@@ -56,10 +56,10 @@
|
||||
</div>
|
||||
|
||||
<div class="setup-toolbar-actions">
|
||||
<el-button type="success" plain :loading="primarySaveLoading" :disabled="isPublishedView" @click="handlePrimarySaveConfig">保存配置</el-button>
|
||||
<el-button type="info" plain :disabled="isPublishedView || !publishedSetupDraft" @click="handleRefillDraft">一键回填</el-button>
|
||||
<el-button type="danger" plain :disabled="isPublishedView" @click="handleClearDraft">清空草稿</el-button>
|
||||
<el-button type="warning" plain :loading="publishConfirmLoading" @click="publishConfigNow">发布版本</el-button>
|
||||
<el-button v-if="canManageSetup" type="success" plain :loading="primarySaveLoading" :disabled="isPublishedView" @click="handlePrimarySaveConfig">保存配置</el-button>
|
||||
<el-button v-if="canManageSetup" type="info" plain :disabled="isPublishedView || !publishedSetupDraft" @click="handleRefillDraft">一键回填</el-button>
|
||||
<el-button v-if="canManageSetup" type="danger" plain :disabled="isPublishedView" @click="handleClearDraft">清空草稿</el-button>
|
||||
<el-button v-if="canManageSetup" type="warning" plain :loading="publishConfirmLoading" @click="publishConfigNow">发布版本</el-button>
|
||||
<el-button
|
||||
type="info"
|
||||
plain
|
||||
@@ -1912,9 +1912,9 @@ import { ArrowLeft, ArrowRight, Promotion, User, OfficeBuilding, Lock, SuccessFi
|
||||
import {
|
||||
fetchStudyDetail,
|
||||
} from "../../api/studies";
|
||||
import { fetchMyApiEndpointPermissions } from "../../api/projectPermissions";
|
||||
import { listMembers } from "../../api/members";
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import { fetchUsers } from "../../api/users";
|
||||
import type { Site, Study } from "../../types/api";
|
||||
import type {
|
||||
CenterConfirmDraft,
|
||||
@@ -1933,6 +1933,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { TEXT, requiredMessage } from "../../locales";
|
||||
import { isSystemAdmin } from "../../utils/roles";
|
||||
import { isApiPermissionAllowed } from "../../utils/apiPermissionValue";
|
||||
import { groupErrorsBySection, parseFieldPath, type SetupValidationError } from "../../utils/setupFieldLocator";
|
||||
import { buildSetupReadableDiffRows, serializeDiffValue, type SetupDiffRow } from "../../utils/setupDiffRows";
|
||||
import {
|
||||
@@ -2403,7 +2404,21 @@ const setupDraft = reactive<SetupConfigDraft>({
|
||||
|
||||
const storageKey = computed(() => `ctms_setup_config_draft_${String(route.params.projectId || "")}`);
|
||||
const projectDraftStorageKey = computed(() => `ctms_setup_project_draft_${String(route.params.projectId || "")}`);
|
||||
const canManageSetup = computed(() => isSystemAdmin(authStore.user));
|
||||
const projectPermissions = ref<Record<string, Record<string, any>> | null>(null);
|
||||
const hasSetupReadPermission = computed(() => {
|
||||
if (isSystemAdmin(authStore.user)) return true;
|
||||
if (!projectPermissions.value) return false;
|
||||
const rolePerms = Object.values(projectPermissions.value)[0];
|
||||
if (!rolePerms) return false;
|
||||
return isApiPermissionAllowed(rolePerms["setup_config:update"]);
|
||||
});
|
||||
const canManageSetup = computed(() => {
|
||||
if (isSystemAdmin(authStore.user)) return true;
|
||||
if (!projectPermissions.value) return false;
|
||||
const rolePerms = Object.values(projectPermissions.value)[0];
|
||||
if (!rolePerms) return false;
|
||||
return isApiPermissionAllowed(rolePerms["setup_config:update"]);
|
||||
});
|
||||
const isPreviewView = computed(() => setupViewMode.value === "preview");
|
||||
const isPublishedVersionView = computed(() => setupViewMode.value === "published");
|
||||
const isPublishedView = computed(() => isPreviewView.value || isPublishedVersionView.value);
|
||||
@@ -4361,18 +4376,7 @@ const loadMemberDisplayMap = async (studyId: string) => {
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
// Ignore member API errors and fallback to users API below.
|
||||
}
|
||||
try {
|
||||
const { data } = await fetchUsers({ limit: 1000 });
|
||||
const users = (data as any)?.items || [];
|
||||
users.forEach((user: any) => {
|
||||
const userId = String(user?.id || "").trim();
|
||||
if (!userId || nextMap[userId]) return;
|
||||
nextMap[userId] = user?.full_name || user?.username || user?.email || userId;
|
||||
});
|
||||
} catch {
|
||||
// ignore
|
||||
// Ignore member API errors.
|
||||
}
|
||||
memberDisplayMap.value = nextMap;
|
||||
projectMemberOptions.value = Array.from(memberOptionMap.entries())
|
||||
@@ -4384,8 +4388,21 @@ const loadProject = async () => {
|
||||
try {
|
||||
const projectId = route.params.projectId as string;
|
||||
projectPublishCompareFallback.value = null;
|
||||
const { data } = await fetchStudyDetail(projectId);
|
||||
|
||||
const [studyRes, permRes] = await Promise.all([
|
||||
fetchStudyDetail(projectId),
|
||||
fetchMyApiEndpointPermissions(projectId, { suppressErrorMessage: true }).catch(() => ({ data: null })),
|
||||
]);
|
||||
const { data } = studyRes;
|
||||
project.value = data as Study;
|
||||
projectPermissions.value = permRes.data as any;
|
||||
|
||||
if (!hasSetupReadPermission.value) {
|
||||
ElMessage.warning("当前角色无权访问立项配置");
|
||||
router.replace("/admin/projects");
|
||||
return;
|
||||
}
|
||||
|
||||
syncFormFromProjectWithoutSetupLink(project.value);
|
||||
projectSnapshotAtLoad.value = buildProjectPublishSnapshot();
|
||||
clearLocalProjectDraft();
|
||||
|
||||
Reference in New Issue
Block a user