完善项目级权限与项目管理入口
- 新增项目成员候选用户接口,避免 PM 读取全局用户列表 - 按项目角色和权限矩阵控制项目管理、成员、中心与审计入口 - 合并侧边栏管理后台入口,统一从项目管理进入成员等模块 - 加强成员管理安全约束,禁止 PM 修改自己、系统管理员或更高权限角色 - 修复多项目角色缓存串用问题,并补充前后端回归测试
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<div class="main-content-flat unified-shell">
|
||||
<div class="unified-action-bar actions-only-bar">
|
||||
<div class="filter-spacer"></div>
|
||||
<el-button type="primary" @click="openCreate">
|
||||
<el-button v-if="isAdmin" type="primary" @click="openCreate">
|
||||
{{ TEXT.common.actions.add }}{{ TEXT.modules.adminProjects.projectLabel }}
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -11,7 +11,7 @@
|
||||
<el-table :data="projects" v-loading="loading" stripe class="project-table" table-layout="fixed">
|
||||
<el-table-column prop="name" :label="TEXT.common.fields.projectName" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-link type="primary" @click="goDetail(scope.row)" class="font-medium">{{ scope.row.name }}</el-link>
|
||||
<el-link type="primary" @click="goProject(scope.row)" class="font-medium">{{ scope.row.name }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="code" :label="TEXT.common.fields.projectCode" show-overflow-tooltip>
|
||||
@@ -33,19 +33,22 @@
|
||||
<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-tooltip v-if="canProject(scope.row, 'project_members', 'read')" :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-tooltip v-if="canProject(scope.row, 'project_members', 'write')" 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-tooltip v-if="canProject(scope.row, 'sites', 'read')" :content="TEXT.modules.adminProjects.sites" placement="top">
|
||||
<el-button link type="primary" :icon="OfficeBuilding" class="action-btn" @click="goSites(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="canProject(scope.row, 'audit_export', 'read')" :content="TEXT.menu.auditLogs" placement="top">
|
||||
<el-button link type="primary" :icon="Document" class="action-btn" @click="goAuditLogs(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="TEXT.modules.adminProjects.enter" placement="top">
|
||||
<el-button link type="success" :icon="ArrowRight" class="action-btn enter-btn" @click="enterStudy(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="!scope.row.is_locked" content="锁定项目" placement="top">
|
||||
<el-tooltip v-if="isAdmin && !scope.row.is_locked" content="锁定项目" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="info"
|
||||
@@ -54,7 +57,7 @@
|
||||
@click="handleLockToggle(scope.row)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="TEXT.common.actions.delete" placement="top">
|
||||
<el-tooltip v-if="isAdmin" :content="TEXT.common.actions.delete" placement="top">
|
||||
<el-button link type="danger" :icon="Delete" class="action-btn" @click="handleDelete(scope.row)" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
@@ -68,14 +71,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { User, OfficeBuilding, ArrowRight, Delete, Lock, Key } from "@element-plus/icons-vue";
|
||||
import { User, OfficeBuilding, ArrowRight, Delete, Lock, Key, Document } from "@element-plus/icons-vue";
|
||||
import { fetchStudies, deleteStudy, lockStudy } from "../../api/studies";
|
||||
import type { Study } from "../../types/api";
|
||||
import { fetchProjectRolePermissions } from "../../api/projectPermissions";
|
||||
import type { ProjectRolePermissionsResponse, Study } from "../../types/api";
|
||||
import ProjectForm from "./ProjectForm.vue";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { isSystemAdmin } from "../../utils/roles";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const projects = ref<Study[]>([]);
|
||||
@@ -84,6 +90,9 @@ const formVisible = ref(false);
|
||||
const editingProject = ref<Study | null>(null);
|
||||
const router = useRouter();
|
||||
const studyStore = useStudyStore();
|
||||
const auth = useAuthStore();
|
||||
const isAdmin = computed(() => isSystemAdmin(auth.user));
|
||||
const permissionsByProject = ref<Record<string, ProjectRolePermissionsResponse>>({});
|
||||
|
||||
const loadProjects = async () => {
|
||||
loading.value = true;
|
||||
@@ -91,6 +100,19 @@ const loadProjects = async () => {
|
||||
const { data } = await fetchStudies();
|
||||
const items = (data as any).items || [];
|
||||
projects.value = items;
|
||||
if (!isAdmin.value) {
|
||||
const entries = await Promise.all(
|
||||
items.map(async (item: Study) => {
|
||||
try {
|
||||
const { data: permissions } = await fetchProjectRolePermissions(item.id);
|
||||
return [item.id, permissions] as const;
|
||||
} catch {
|
||||
return [item.id, null] as const;
|
||||
}
|
||||
})
|
||||
);
|
||||
permissionsByProject.value = Object.fromEntries(entries.filter((entry) => entry[1]));
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminProjects.loadFailed);
|
||||
} finally {
|
||||
@@ -99,10 +121,17 @@ const loadProjects = async () => {
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
if (!isAdmin.value) return;
|
||||
editingProject.value = null;
|
||||
formVisible.value = true;
|
||||
};
|
||||
|
||||
const canProject = (row: Study, module: string, action: "read" | "write") => {
|
||||
if (isAdmin.value) return true;
|
||||
const role = row.role_in_study || "";
|
||||
return !!permissionsByProject.value[row.id]?.roles?.[role as keyof ProjectRolePermissionsResponse["roles"]]?.[module]?.[action];
|
||||
};
|
||||
|
||||
const goMembers = (row: Study) => {
|
||||
router.push(`/admin/projects/${row.id}/members`);
|
||||
};
|
||||
@@ -115,7 +144,12 @@ const goPermissions = (row: Study) => {
|
||||
router.push(`/admin/projects/${row.id}/permissions`);
|
||||
};
|
||||
|
||||
const goAuditLogs = async (row: Study) => {
|
||||
await enterStudy(row, "/admin/audit-logs");
|
||||
};
|
||||
|
||||
const handleDelete = async (study: Study) => {
|
||||
if (!isAdmin.value) return;
|
||||
try {
|
||||
// 第一次确认
|
||||
await ElMessageBox.confirm(
|
||||
@@ -165,6 +199,7 @@ const handleDelete = async (study: Study) => {
|
||||
};
|
||||
|
||||
const handleLockToggle = async (study: Study) => {
|
||||
if (!isAdmin.value) return;
|
||||
// 锁定后不可解锁
|
||||
if (study.is_locked) {
|
||||
ElMessage.warning("项目已锁定,不可解锁");
|
||||
@@ -204,16 +239,24 @@ const handleLockToggle = async (study: Study) => {
|
||||
}
|
||||
};
|
||||
|
||||
const enterStudy = async (row: Study) => {
|
||||
const enterStudy = async (row: Study, path = "/project/overview") => {
|
||||
studyStore.setCurrentStudy(row);
|
||||
await studyStore.loadCurrentStudyPermissions().catch(() => {});
|
||||
router.push("/project/overview");
|
||||
router.push(path);
|
||||
};
|
||||
|
||||
const goDetail = (row: Study) => {
|
||||
router.push(`/admin/projects/${row.id}`);
|
||||
};
|
||||
|
||||
const goProject = (row: Study) => {
|
||||
if (isAdmin.value) {
|
||||
goDetail(row);
|
||||
return;
|
||||
}
|
||||
enterStudy(row);
|
||||
};
|
||||
|
||||
const statusLabel = (status: string) =>
|
||||
TEXT.enums.projectStatus[status as keyof typeof TEXT.enums.projectStatus] || status;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user