权限管理:接入项目级权限矩阵

新增项目级角色权限矩阵,覆盖 PM、CRA、PV、IMP、QA 等项目角色,并通过迁移表持久化各业务模块的读写权限。

后端将参与者、合同费用、物资、启动、里程碑、风险问题、监查稽查、FAQ、共享库、文件版本等接口接入矩阵校验,修复审计日志导出权限和登录 502 的依赖导入问题。

前端新增权限管理页面和项目路由权限映射,按矩阵控制导航显示、直接访问拦截、操作按钮启停,并限制管理后台仅 admin 和授权 PM 可进入。

补充后端权限归一化与接口静态测试、前端路由/权限工具/权限管理页面/工作台等回归测试。
This commit is contained in:
Cheng Zhou
2026-05-13 08:58:03 +08:00
parent 77e842637d
commit 939fc70532
63 changed files with 1952 additions and 387 deletions
+27 -11
View File
@@ -30,12 +30,15 @@
<el-tag v-else type="success" effect="plain" round>未锁定</el-tag>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="240" align="center">
<el-table-column :label="TEXT.common.labels.actions" width="176" align="center">
<template #default="scope">
<div class="action-row">
<el-tooltip :content="TEXT.modules.adminProjects.members" placement="top">
<el-button link type="primary" :icon="User" class="action-btn" @click="goMembers(scope.row)" />
</el-tooltip>
<el-tooltip content="权限管理" placement="top">
<el-button link type="primary" :icon="Key" class="action-btn" @click="goPermissions(scope.row)" />
</el-tooltip>
<el-tooltip :content="TEXT.modules.adminProjects.sites" placement="top">
<el-button link type="primary" :icon="OfficeBuilding" class="action-btn" @click="goSites(scope.row)" />
</el-tooltip>
@@ -68,7 +71,7 @@
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
import { User, OfficeBuilding, ArrowRight, Delete, Lock } from "@element-plus/icons-vue";
import { User, OfficeBuilding, ArrowRight, Delete, Lock, Key } from "@element-plus/icons-vue";
import { fetchStudies, deleteStudy, lockStudy } from "../../api/studies";
import type { Study } from "../../types/api";
import ProjectForm from "./ProjectForm.vue";
@@ -108,6 +111,10 @@ const goSites = (row: Study) => {
router.push(`/admin/projects/${row.id}/sites`);
};
const goPermissions = (row: Study) => {
router.push(`/admin/projects/${row.id}/permissions`);
};
const handleDelete = async (study: Study) => {
try {
// 第一次确认
@@ -197,8 +204,9 @@ const handleLockToggle = async (study: Study) => {
}
};
const enterStudy = (row: Study) => {
const enterStudy = async (row: Study) => {
studyStore.setCurrentStudy(row);
await studyStore.loadCurrentStudyPermissions().catch(() => {});
router.push("/project/overview");
};
@@ -244,7 +252,8 @@ onMounted(() => {
.action-row {
display: inline-flex;
align-items: center;
gap: 10px;
gap: 4px;
justify-content: center;
white-space: nowrap;
}
@@ -263,20 +272,27 @@ onMounted(() => {
}
.action-btn {
font-size: 18px;
padding: 4px;
margin: 0 2px;
transition: all 0.3s;
width: 28px;
height: 28px;
min-height: 28px;
font-size: 16px;
padding: 0;
margin: 0;
border-radius: 4px;
transition: background-color 0.2s, color 0.2s, transform 0.2s;
}
.action-row :deep(.el-button + .el-button) {
margin-left: 0;
}
.action-btn:hover {
transform: scale(1.1);
transform: translateY(-1px);
background-color: #f3f4f6;
border-radius: 4px;
}
.enter-btn {
font-size: 18px;
font-size: 16px;
font-weight: bold;
}
</style>