清理全局角色残留并修复项目权限判断

This commit is contained in:
Cheng Zhou
2026-05-29 10:20:42 +08:00
parent 63457aab11
commit 44d69c2d7b
63 changed files with 739 additions and 443 deletions
@@ -73,7 +73,7 @@
<el-tag v-if="memberRows.length" effect="plain" size="small" round>{{ memberRows.length }} 名成员</el-tag>
</div>
<div class="sub-tab-actions">
<el-button type="primary" @click="openAddMember">
<el-button v-if="canAddProjectMember" type="primary" @click="openAddMember">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px; margin-right: 4px"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="20" y1="8" x2="20" y2="14"/><line x1="23" y1="11" x2="17" y2="11"/></svg>
添加成员
</el-button>
@@ -96,7 +96,7 @@
size="small"
style="width: 130px"
@change="(val: string) => updateMemberRole(row.id, val)"
:disabled="!canEditMember(row) || !row.is_active || row.effectiveStatus === 'DISABLED_GLOBAL'"
:disabled="!canUpdateProjectMember(row) || !row.is_active || row.effectiveStatus === 'DISABLED_GLOBAL'"
>
<el-option
v-for="(label, val) in memberRoleLabels(row)"
@@ -128,12 +128,12 @@
link
:type="row.is_active ? 'warning' : 'primary'"
size="small"
:disabled="!canEditMember(row) || row.effectiveStatus === 'DISABLED_GLOBAL'"
:disabled="!canUpdateProjectMember(row) || row.effectiveStatus === 'DISABLED_GLOBAL'"
@click="toggleMemberActive(row)"
>
{{ row.is_active ? "停用" : "启用" }}
</el-button>
<el-button link type="danger" size="small" :disabled="!canEditMember(row)" @click="deleteMember(row)">
<el-button link type="danger" size="small" :disabled="!canDeleteProjectMember(row)" @click="deleteMember(row)">
移除
</el-button>
</template>
@@ -480,6 +480,18 @@ const isAdmin = computed(() => isSystemAdmin(auth.user));
const selectedProjectRole = computed(() => selectedStudy.value?.role_in_study || "");
const isSelectedProjectPm = computed(() => selectedProjectRole.value === "PM");
const canManageSelectedProject = computed(() => isAdmin.value || isSelectedProjectPm.value);
const selectedProjectPermissionAllowed = (operationKey: string) => {
if (isAdmin.value) return true;
const role = selectedProjectRole.value;
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 canCreateProjectMember = computed(() => selectedProjectPermissionAllowed("project_members:create"));
const canUpdateProjectMembers = computed(() => selectedProjectPermissionAllowed("project_members:update"));
const canDeleteProjectMembers = computed(() => selectedProjectPermissionAllowed("project_members:delete"));
const canAddProjectMember = computed(() => canCreateProjectMember.value && canListProjectMemberCandidates.value);
const { roleLabel, roleDescription, roleIcon, compareRolesByTemplateOrder, loadRoleTemplates } = useRoleTemplateMeta();
const ROLE_RANK: Record<string, number> = {
@@ -495,7 +507,7 @@ const assignableRoleLabels = computed(() => {
return result;
});
const memberRoleLabels = (row: MemberRoleRow) => {
if (canEditMember(row)) return assignableRoleLabels.value;
if (canUpdateProjectMember(row)) return assignableRoleLabels.value;
return {
...assignableRoleLabels.value,
[row.role_in_study]: roleLabel(row.role_in_study),
@@ -574,7 +586,8 @@ const onStudyChange = async (id: string) => {
candidates.value = [];
activeRolesDraft.value = [];
router.replace({ query: { ...route.query, projectId: id } });
await Promise.all([loadPermissionData(), loadMembers(), loadActiveRoles()]);
await loadPermissionData();
await Promise.all([loadMembers(), loadActiveRoles()]);
loadCandidates();
};
@@ -617,13 +630,14 @@ const addMemberRules: FormRules = {
role_in_study: [{ required: true, message: "请选择角色", trigger: "change" }],
};
const canEditMember = (row: MemberRoleRow) => {
if (!canManageSelectedProject.value) return false;
const canMutateProjectMember = (row: MemberRoleRow) => {
if (row.user_id === auth.user?.id) return false;
if (row.user?.is_admin) return false;
if (!isAdmin.value && (ROLE_RANK[row.role_in_study] ?? 0) >= ROLE_RANK.PM) return false;
return true;
};
const canUpdateProjectMember = (row: MemberRoleRow) => canUpdateProjectMembers.value && canMutateProjectMember(row);
const canDeleteProjectMember = (row: MemberRoleRow) => canDeleteProjectMembers.value && canMutateProjectMember(row);
const memberRows = computed(() =>
members.value.map((m) => {
@@ -639,7 +653,7 @@ const availableCandidates = computed(() => {
});
const loadMembers = async () => {
if (!selectedStudyId.value) return;
if (!canListProjectMembers.value || !selectedStudyId.value) return;
membersLoading.value = true;
try {
const { data } = await listMembers(selectedStudyId.value, { limit: 500, include_inactive: true });
@@ -652,7 +666,10 @@ const loadMembers = async () => {
};
const loadCandidates = async () => {
if (!selectedStudyId.value) return;
if (!canListProjectMemberCandidates.value || !selectedStudyId.value) {
candidates.value = [];
return;
}
try {
const { data } = await listMemberCandidates(selectedStudyId.value, { limit: 500 });
candidates.value = data ?? [];
@@ -662,6 +679,7 @@ const loadCandidates = async () => {
};
const openAddMember = () => {
if (!canAddProjectMember.value) return;
addMemberForm.user_id = "";
addMemberForm.role_in_study = Object.keys(assignableRoleLabels.value)[0] || "";
addMemberVisible.value = true;
@@ -670,6 +688,10 @@ const openAddMember = () => {
const resetAddMemberForm = () => { addMemberFormRef.value?.clearValidate(); };
const submitAddMember = async () => {
if (!canAddProjectMember.value) {
ElMessage.warning("无权限添加成员");
return;
}
const valid = await addMemberFormRef.value?.validate().catch(() => false);
if (!valid) return;
addMemberSaving.value = true;
@@ -686,6 +708,12 @@ const submitAddMember = async () => {
};
const updateMemberRole = async (memberId: string, role: string) => {
const row = members.value.find((item) => item.id === memberId);
if (!row || !canUpdateProjectMember(row)) {
ElMessage.warning("无权限更新成员");
loadMembers();
return;
}
try {
await updateMember(selectedStudyId.value, memberId, { role_in_study: role });
ElMessage.success("角色已更新");
@@ -696,6 +724,10 @@ const updateMemberRole = async (memberId: string, role: string) => {
};
const toggleMemberActive = async (row: any) => {
if (!canUpdateProjectMember(row)) {
ElMessage.warning("无权限更新成员");
return;
}
if (row.is_active) {
const ok = await ElMessageBox.confirm("确认停用该成员?", "停用成员", { type: "warning" }).catch(() => null);
if (!ok) return;
@@ -710,6 +742,10 @@ const toggleMemberActive = async (row: any) => {
};
const deleteMember = async (row: any) => {
if (!canDeleteProjectMember(row)) {
ElMessage.warning("无权限移除成员");
return;
}
const ok = await ElMessageBox.confirm("确认将该账号从本项目移除?账号本身不会被删除。", "移除成员", {
type: "warning",
}).catch(() => null);
@@ -973,8 +1009,10 @@ const PERMISSION_SECTION_LABELS: Record<string, string> = {
subject_histories: "病史",
monitoring_issues: "监查访视问题",
project_milestones: "项目里程碑",
startup_initiation: "立项记录",
startup_ethics: "伦理记录",
startup_initiation: "立项",
startup_initiation_attachments: "立项",
startup_ethics: "伦理",
startup_ethics_attachments: "伦理",
precautions: "注意事项",
precautions_attachments: "注意事项",
faq: "医学咨询/FAQ",
@@ -1147,7 +1185,8 @@ onMounted(async () => {
if (initialStudyId) {
selectedStudyId.value = initialStudyId;
if (qSub === "members") projectSubTab.value = "members";
await Promise.all([loadPermissionData(), loadMembers(), loadActiveRoles()]);
await loadPermissionData();
await Promise.all([loadMembers(), loadActiveRoles()]);
loadCandidates();
}