移除QA预设角色并收紧角色模板编辑

This commit is contained in:
Cheng Zhou
2026-05-26 11:32:14 +08:00
parent 6c2f61eebd
commit 3c168565ce
26 changed files with 387 additions and 119 deletions
@@ -47,10 +47,30 @@ describe("permission management custom roles", () => {
expect(source).toContain("const roleLabel = (role: string) => ROLE_LABELS[role] || role;");
expect(source).toContain("const templateRoleName = (row: PermissionTemplate) => row.category ? roleLabel(row.category) : row.name;");
expect(source).toContain("name: templateRoleName(row), description: row.description ?? \"\",");
expect(source).toContain("roleDisplayName(row)");
});
it("locks role type for preset edits and creates custom roles only", () => {
const source = readSource();
const dialogStart = source.indexOf("<!-- 新增/编辑角色对话框 -->");
const dialogEnd = source.indexOf("<!-- 新增成员对话框 -->", dialogStart);
const dialogSource = source.slice(dialogStart, dialogEnd);
expect(dialogSource).toContain(':disabled="Boolean(editingTemplateId)"');
expect(source).toContain("const openCreateTemplate = () =>");
expect(source).toContain("const openEditTemplate = (row: PermissionTemplate) =>");
expect(source).toContain('template_type: "CUSTOM"');
expect(dialogSource).toContain('<el-option label="预设角色" value="ROLE" :disabled="!editingTemplateId" />');
expect(dialogSource).toContain('<el-option label="自定义角色" value="CUSTOM" :disabled="Boolean(editingTemplateId)" />');
expect(dialogSource).not.toContain('<el-option label="场景角色" value="SCENARIO" />');
expect(dialogSource).not.toContain('label="分类"');
expect(dialogSource).not.toContain('label="推荐角色"');
expect(dialogSource).not.toContain('label="标签"');
expect(dialogSource).not.toContain("如 PM / CRA / PV");
expect(dialogSource).not.toContain("如 PM,CRA");
expect(dialogSource).not.toContain("可选标签");
});
it("keeps member role selects bound to role keys used by the permission matrix", () => {
const source = readSource();
@@ -61,6 +81,18 @@ describe("permission management custom roles", () => {
expect(source).toContain("await addMember(selectedStudyId.value, addMemberForm);");
});
it("does not offer QA as a preset project permission role", () => {
const source = readSource();
const allRolesStart = source.indexOf("const ALL_ROLES = [");
const allRolesEnd = source.indexOf("];", allRolesStart);
const allRolesSource = source.slice(allRolesStart, allRolesEnd);
expect(allRolesSource).not.toContain('key: "QA"');
expect(source).not.toContain('QA: "QA"');
expect(source).not.toContain("QA: 60");
});
it("submits only the selected role from role editor saves", () => {
const source = readSource();
@@ -69,6 +101,24 @@ describe("permission management custom roles", () => {
expect(source).not.toContain("const payload = flattenMatrix(apiMatrix.value, { includePm: isAdmin.value });");
});
it("submits only editable metadata when saving an existing role template", () => {
const source = readSource();
const updateBuilderStart = source.indexOf("const buildTemplateUpdatePayload = () =>");
const createBuilderStart = source.indexOf("const buildTemplateCreatePayload = ()");
const submitStart = source.indexOf("const submitTemplateForm = async () =>", createBuilderStart);
const updateBuilderSource = source.slice(updateBuilderStart, createBuilderStart);
const createBuilderSource = source.slice(createBuilderStart, submitStart);
expect(updateBuilderSource).toContain("name: templateForm.value.name");
expect(updateBuilderSource).toContain("description: templateForm.value.description || undefined");
expect(updateBuilderSource).not.toContain("template_type");
expect(updateBuilderSource).not.toContain("permissions");
expect(updateBuilderSource).not.toContain("category");
expect(createBuilderSource).toContain("permissions: {}");
expect(source).toContain("const payload = buildTemplateUpdatePayload();");
expect(source).toContain("const payload = buildTemplateCreatePayload();");
});
it("renders setup_config as a Chinese module name in the role editor", () => {
const source = readSource();
@@ -385,21 +385,11 @@
<el-input v-model="templateForm.description" type="textarea" :rows="2" placeholder="可选描述" />
</el-form-item>
<el-form-item label="角色类型" prop="template_type">
<el-select v-model="templateForm.template_type" style="width: 100%">
<el-option label="预设角色" value="ROLE" />
<el-option label="场景角色" value="SCENARIO" />
<el-option label="自定义角色" value="CUSTOM" />
<el-select v-model="templateForm.template_type" style="width: 100%" :disabled="Boolean(editingTemplateId)">
<el-option label="预设角色" value="ROLE" :disabled="!editingTemplateId" />
<el-option label="自定义角色" value="CUSTOM" :disabled="Boolean(editingTemplateId)" />
</el-select>
</el-form-item>
<el-form-item label="分类">
<el-input v-model="templateForm.category" placeholder="如 PM / CRA / PV" />
</el-form-item>
<el-form-item label="推荐角色">
<el-input v-model="templateForm.recommended_roles" placeholder="如 PM,CRA" />
</el-form-item>
<el-form-item label="标签">
<el-input v-model="templateForm.tags" placeholder="可选标签" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="templateDialogVisible = false">取消</el-button>
@@ -461,6 +451,7 @@ import {
fetchActiveRoles,
updateActiveRoles,
type PermissionTemplate,
type PermissionTemplateCreate,
type SystemPermissionItem,
} from "@/api/projectPermissions";
import { addMember, listMemberCandidates, listMembers, removeMember, updateMember } from "@/api/members";
@@ -483,7 +474,7 @@ const canManageSelectedProject = computed(() => isAdmin.value || isSelectedProje
const ROLE_LABELS: Record<string, string> = {
PM: "项目负责人", CRA: "CRA", PV: "PV",
MEDICAL_REVIEW: "医学审核", IMP: "药品管理员", QA: "QA",
MEDICAL_REVIEW: "医学审核", IMP: "药品管理员",
};
const roleLabel = (role: string) => ROLE_LABELS[role] || role;
@@ -495,7 +486,7 @@ const activeRoleLabels = computed(() => {
return result;
});
const ROLE_RANK: Record<string, number> = {
ADMIN: 100, PM: 80, QA: 60, PV: 50, MEDICAL_REVIEW: 50, CRA: 40, IMP: 40,
ADMIN: 100, PM: 80, PV: 50, MEDICAL_REVIEW: 50, CRA: 40, IMP: 40,
};
type MemberRoleRow = { user_id: string; role_in_study: string; user?: any };
const canAssignProjectRole = (role: string) => isAdmin.value || (ROLE_RANK[role] ?? 0) < ROLE_RANK.PM;
@@ -778,7 +769,6 @@ const ALL_ROLES = [
{ key: "PV", label: "PV", icon: "🔍", desc: "访视和参与者管理人员" },
{ key: "MEDICAL_REVIEW", label: "医学审核", icon: "🏥", desc: "医学审核人员" },
{ key: "IMP", label: "药品管理员", icon: "📦", desc: "物资和设备管理人员" },
{ key: "QA", label: "QA", icon: "✅", desc: "质量检查和审计人员" },
];
const activeRolesDraft = ref<string[]>([]);
@@ -898,25 +888,31 @@ const resetTemplateForm = () => {
templateFormRef.value?.clearValidate();
};
const buildTemplateUpdatePayload = () => ({
name: templateForm.value.name,
description: templateForm.value.description || undefined,
});
const buildTemplateCreatePayload = (): PermissionTemplateCreate => ({
...templateForm.value,
description: templateForm.value.description || undefined,
category: templateForm.value.category || undefined,
recommended_roles: templateForm.value.recommended_roles || undefined,
tags: templateForm.value.tags || undefined,
permissions: {},
});
const submitTemplateForm = async () => {
const valid = await templateFormRef.value?.validate().catch(() => false);
if (!valid) return;
templateSaving.value = true;
try {
const payload = {
...templateForm.value,
description: templateForm.value.description || undefined,
category: templateForm.value.category || undefined,
recommended_roles: templateForm.value.recommended_roles || undefined,
tags: templateForm.value.tags || undefined,
permissions: editingTemplateId.value
? (templates.value.find((t) => t.id === editingTemplateId.value)?.permissions ?? {})
: {},
};
if (editingTemplateId.value) {
const payload = buildTemplateUpdatePayload();
await updatePermissionTemplate(editingTemplateId.value, payload);
ElMessage.success("角色已更新");
} else {
const payload = buildTemplateCreatePayload();
await createPermissionTemplate(payload);
ElMessage.success("角色已创建");
}
@@ -35,4 +35,12 @@ describe("ProjectMembers user directory access", () => {
expect(source).toContain(':disabled="!canAssignRole(\'ADMIN\')"');
expect(source).toContain(':disabled="!canEditMember(scope.row)');
});
it("does not offer QA as a project member role preset", () => {
const source = readProjectMembers();
expect(source).not.toContain('value="QA"');
expect(source).not.toContain("canAssignRole('QA')");
expect(source).not.toContain("QA: 60");
});
});
@@ -46,7 +46,6 @@
<el-option :label="TEXT.enums.userRole.PV" value="PV" :disabled="!canAssignRole('PV')" />
<el-option :label="TEXT.enums.userRole.MEDICAL_REVIEW" value="MEDICAL_REVIEW" :disabled="!canAssignRole('MEDICAL_REVIEW')" />
<el-option :label="TEXT.enums.userRole.IMP" value="IMP" :disabled="!canAssignRole('IMP')" />
<el-option :label="TEXT.enums.userRole.QA" value="QA" :disabled="!canAssignRole('QA')" />
<el-option :label="TEXT.enums.userRole.ADMIN" value="ADMIN" :disabled="!canAssignRole('ADMIN')" />
</el-select>
<span v-if="scope.row.effectiveStatus === 'DISABLED_GLOBAL'" class="hint">{{ TEXT.modules.adminProjectMembers.disabledGlobalDesc }}</span>
@@ -86,7 +85,6 @@
<el-option :label="TEXT.enums.userRole.PV" value="PV" :disabled="!canAssignRole('PV')" />
<el-option :label="TEXT.enums.userRole.MEDICAL_REVIEW" value="MEDICAL_REVIEW" :disabled="!canAssignRole('MEDICAL_REVIEW')" />
<el-option :label="TEXT.enums.userRole.IMP" value="IMP" :disabled="!canAssignRole('IMP')" />
<el-option :label="TEXT.enums.userRole.QA" value="QA" :disabled="!canAssignRole('QA')" />
<el-option :label="TEXT.enums.userRole.ADMIN" value="ADMIN" :disabled="!canAssignRole('ADMIN')" />
</el-select>
</el-form-item>
@@ -133,7 +131,6 @@ const projectRole = computed(() => project.value?.role_in_study || "");
const roleRank: Record<string, number> = {
ADMIN: 100,
PM: 80,
QA: 60,
PV: 50,
MEDICAL_REVIEW: 50,
CRA: 40,