统一项目权限读取模型
1、合并列表与详情读取权限,统一使用 subjects:read、project_members:read、monitoring_issues:read 等业务读取 key。 2、增加历史读取权限 key 的兼容映射,确保存量项目权限覆盖值不会丢失。 3、同步前端权限工具、路由权限与权限管理页面,避免继续引用已合并的旧权限 key。 4、补充项目角色启停保护与抽屉未保存变更守卫,防止停用 PM 或仍有关联成员的角色。
This commit is contained in:
@@ -231,7 +231,15 @@
|
||||
<!-- ══════════════════════════════════════
|
||||
角色管理抽屉
|
||||
══════════════════════════════════════ -->
|
||||
<el-drawer v-model="templateDrawerVisible" title="角色管理" size="720px" direction="rtl" @open="onTemplateDrawerOpen">
|
||||
<el-drawer
|
||||
v-model="templateDrawerVisible"
|
||||
title="角色管理"
|
||||
size="720px"
|
||||
direction="rtl"
|
||||
:close-on-click-modal="true"
|
||||
:before-close="handleTemplateDrawerBeforeClose"
|
||||
@open="onTemplateDrawerOpen"
|
||||
>
|
||||
<div class="drawer-tabs-shell">
|
||||
<el-tabs v-model="templateDrawerTab" class="drawer-tabs">
|
||||
|
||||
@@ -252,10 +260,10 @@
|
||||
<el-table-column label="权限数" width="80">
|
||||
<template #default="{ row }">{{ countPermissions(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<el-table-column v-if="isAdmin" label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openEditTemplate(row)">编辑</el-button>
|
||||
<el-button link type="danger" :disabled="row.is_system" @click="confirmDeleteTemplate(row)">删除</el-button>
|
||||
<el-button v-if="isAdmin" link type="primary" @click="openEditTemplate(row)">编辑</el-button>
|
||||
<el-button v-if="isAdmin" link type="danger" :disabled="row.is_system" @click="confirmDeleteTemplate(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -279,12 +287,24 @@
|
||||
<div>
|
||||
<div class="active-role-name">{{ role.label }}</div>
|
||||
<div class="active-role-desc">{{ role.desc }}</div>
|
||||
<div v-if="roleActiveStatusLabel(role.key)" class="active-role-meta">
|
||||
{{ roleActiveStatusLabel(role.key) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-switch
|
||||
:model-value="activeRolesDraft.includes(role.key)"
|
||||
@change="(v: boolean) => toggleActiveRole(role.key, v)"
|
||||
/>
|
||||
<el-tooltip
|
||||
:disabled="!roleActiveDisableReason(role.key)"
|
||||
:content="roleActiveDisableReason(role.key)"
|
||||
placement="top"
|
||||
>
|
||||
<span>
|
||||
<el-switch
|
||||
:model-value="activeRolesDraft.includes(role.key)"
|
||||
:disabled="!canToggleActiveRole(role.key)"
|
||||
@change="(v: boolean) => toggleActiveRole(role.key, v)"
|
||||
/>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
@@ -362,7 +382,7 @@
|
||||
|
||||
</el-tabs>
|
||||
<div class="drawer-tab-actions">
|
||||
<el-button v-if="templateDrawerTab === 'list'" type="primary" @click="openCreateTemplate">新增角色</el-button>
|
||||
<el-button v-if="templateDrawerTab === 'list' && isAdmin" type="primary" @click="openCreateTemplate">新增角色</el-button>
|
||||
<el-button v-else-if="templateDrawerTab === 'active'" type="primary" :loading="activeRolesSaving" @click="saveActiveRoles">
|
||||
保存
|
||||
</el-button>
|
||||
@@ -464,6 +484,7 @@ import { useAuthStore } from "@/store/auth";
|
||||
import { displayDateTime } from "@/utils/display";
|
||||
import { isApiPermissionAllowed } from "@/utils/apiPermissionValue";
|
||||
import { isSystemAdmin } from "@/utils/roles";
|
||||
import { useDrawerDirtyGuard } from "@/utils/drawerDirtyGuard";
|
||||
import {
|
||||
compareProjectPermissionModules,
|
||||
projectPermissionModuleLabel,
|
||||
@@ -486,8 +507,8 @@ const selectedProjectPermissionAllowed = (operationKey: string) => {
|
||||
if (!role || !apiMatrix.value) return false;
|
||||
return isApiPermissionAllowed(apiMatrix.value?.[role]?.[operationKey]);
|
||||
};
|
||||
const canListProjectMembers = computed(() => selectedProjectPermissionAllowed("project_members:list"));
|
||||
const canListProjectMemberCandidates = computed(() => selectedProjectPermissionAllowed("project_members:candidates"));
|
||||
const canListProjectMembers = computed(() => selectedProjectPermissionAllowed("project_members:read"));
|
||||
const canListProjectMemberCandidates = computed(() => selectedProjectPermissionAllowed("project_members:read"));
|
||||
const canCreateProjectMember = computed(() => selectedProjectPermissionAllowed("project_members:create"));
|
||||
const canUpdateProjectMembers = computed(() => selectedProjectPermissionAllowed("project_members:update"));
|
||||
const canDeleteProjectMembers = computed(() => selectedProjectPermissionAllowed("project_members:delete"));
|
||||
@@ -775,6 +796,7 @@ const templateSaving = ref(false);
|
||||
const activeRolesDraft = ref<string[]>([]);
|
||||
const activeRolesLoading = ref(false);
|
||||
const activeRolesSaving = ref(false);
|
||||
const activeRolesDirtyGuard = useDrawerDirtyGuard(() => activeRolesDraft.value);
|
||||
|
||||
const roleOptions = computed(() => {
|
||||
const options = templates.value.filter((template) => template.category).map((template) => ({
|
||||
@@ -805,7 +827,8 @@ const loadActiveRoles = async () => {
|
||||
activeRolesLoading.value = true;
|
||||
try {
|
||||
const res = await fetchActiveRoles(selectedStudyId.value);
|
||||
activeRolesDraft.value = res.data.active_roles;
|
||||
activeRolesDraft.value = protectPmActiveRole([...res.data.active_roles]);
|
||||
activeRolesDirtyGuard.syncBaseline();
|
||||
} catch {
|
||||
ElMessage.error("加载生效角色失败");
|
||||
} finally {
|
||||
@@ -813,7 +836,44 @@ const loadActiveRoles = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const activeRoleMemberCounts = computed(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
for (const member of members.value) {
|
||||
if (!member.is_active) continue;
|
||||
const role = member.role_in_study;
|
||||
if (!role) continue;
|
||||
counts[role] = (counts[role] ?? 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
});
|
||||
|
||||
const activeRoleMemberCount = (role: string) => activeRoleMemberCounts.value[role] ?? 0;
|
||||
const isActiveRoleInUse = (role: string) => activeRolesDraft.value.includes(role) && activeRoleMemberCount(role) > 0;
|
||||
const canToggleActiveRole = (role: string) => {
|
||||
if (!isAdmin.value && role === "PM") return false;
|
||||
if (isActiveRoleInUse(role)) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const roleActiveStatusLabel = (role: string) => {
|
||||
if (role === "PM" && !isAdmin.value) return "项目负责人必需";
|
||||
const count = activeRoleMemberCount(role);
|
||||
return count > 0 ? `${count} 名成员使用中` : "";
|
||||
};
|
||||
|
||||
const roleActiveDisableReason = (role: string) => {
|
||||
if (role === "PM" && !isAdmin.value) return "PM 为项目必需角色,项目 PM 不可停用";
|
||||
if (isActiveRoleInUse(role)) return "该角色仍有关联成员,需先调整成员角色后才能停用";
|
||||
return "";
|
||||
};
|
||||
|
||||
const protectPmActiveRole = (roles: string[]) => {
|
||||
if (!isAdmin.value && !roles.includes("PM")) roles.unshift("PM");
|
||||
return roles;
|
||||
};
|
||||
|
||||
const toggleActiveRole = (role: string, active: boolean) => {
|
||||
if (!canToggleActiveRole(role)) return;
|
||||
if (active) {
|
||||
if (!activeRolesDraft.value.includes(role)) activeRolesDraft.value.push(role);
|
||||
} else {
|
||||
@@ -825,7 +885,10 @@ const saveActiveRoles = async () => {
|
||||
if (!canManageSelectedProject.value) return;
|
||||
activeRolesSaving.value = true;
|
||||
try {
|
||||
await updateActiveRoles(selectedStudyId.value, activeRolesDraft.value);
|
||||
const roles = protectPmActiveRole([...activeRolesDraft.value]);
|
||||
await updateActiveRoles(selectedStudyId.value, roles);
|
||||
activeRolesDraft.value = roles;
|
||||
activeRolesDirtyGuard.syncBaseline();
|
||||
await loadPermissionData();
|
||||
ElMessage.success("生效角色已保存");
|
||||
} catch {
|
||||
@@ -996,6 +1059,15 @@ const roleEditorDraft = ref<Record<string, boolean>>({});
|
||||
const roleEditorSaving = ref(false);
|
||||
const roleEditorLoading = ref(false);
|
||||
const allOperations = ref<RoleEditorOp[]>([]);
|
||||
const roleEditorDirtyGuard = useDrawerDirtyGuard(() => roleEditorDraft.value);
|
||||
|
||||
const handleTemplateDrawerBeforeClose = (done: () => void) => {
|
||||
if (activeRolesDirtyGuard.isDirty.value || (editingRole.value && roleEditorDirtyGuard.isDirty.value)) {
|
||||
ElMessage.warning("请先保存或取消编辑");
|
||||
return;
|
||||
}
|
||||
done();
|
||||
};
|
||||
|
||||
const roleEditorModuleLabel = projectPermissionModuleLabel;
|
||||
|
||||
@@ -1009,6 +1081,7 @@ const PERMISSION_SECTION_LABELS: Record<string, string> = {
|
||||
subject_histories: "病史",
|
||||
monitoring_issues: "监查访视问题",
|
||||
project_milestones: "项目里程碑",
|
||||
material_equipments_attachments: "设备管理",
|
||||
startup_initiation: "立项",
|
||||
startup_initiation_attachments: "立项",
|
||||
startup_ethics: "伦理",
|
||||
@@ -1145,6 +1218,7 @@ const initRoleEditor = () => {
|
||||
}
|
||||
}
|
||||
roleEditorDraft.value = draft;
|
||||
roleEditorDirtyGuard.syncBaseline();
|
||||
};
|
||||
|
||||
const saveRoleEditor = async () => {
|
||||
@@ -1160,6 +1234,7 @@ const saveRoleEditor = async () => {
|
||||
apiMatrix.value = res.data;
|
||||
const savedRole = editingRole.value;
|
||||
editingRole.value = "";
|
||||
roleEditorDirtyGuard.syncBaseline();
|
||||
ElMessage.success(`${roleLabel(savedRole)} 权限已保存`);
|
||||
} catch {
|
||||
ElMessage.error("保存失败");
|
||||
@@ -1667,6 +1742,13 @@ onMounted(async () => {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.active-role-meta {
|
||||
font-size: 12px;
|
||||
color: #b45309;
|
||||
margin-top: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.template-name { font-weight: 600; color: #1a2332; }
|
||||
|
||||
/* ── 角色编辑器 ── */
|
||||
|
||||
Reference in New Issue
Block a user