完善项目权限前置依赖保存校验
- 以接口权限定义中的 prerequisite_permissions 作为前置依赖单一事实源,生成运行时前置权限映射\n- 保存项目角色权限前预检缺失前置权限与受影响下游权限,未确认时返回 409 调整详情\n- 确认保存时自动合并补齐前置权限与取消下游权限,并提供有效权限矩阵读取接口\n- 前端角色权限编辑改为保存时执行依赖校验,弹窗展示需补充前置权限和受影响下游权限\n- 新增编辑权限取消按钮,清理权限选择即时确认弹窗残留\n- 补充后端接口测试、前端权限编辑测试与优化设计文档
This commit is contained in:
@@ -10,11 +10,11 @@ vi.mock("@/api/projectPermissions", () => ({
|
||||
fetchApiOperations: vi.fn().mockResolvedValue({ data: { operations: [] } }),
|
||||
fetchPermissionTemplates: vi.fn().mockResolvedValue({
|
||||
data: [
|
||||
{ category: "PM", name: "PM", description: "项目负责人,统筹项目全局,协调进度、资源与关键决策。" },
|
||||
{ category: "CRA", name: "CRA", description: "负责各中心临床监查执行,跟进现场质量、数据和问题闭环。" },
|
||||
{ category: "CTA", name: "CTA", description: "负责合同、药品及相关项目事务管理,保障执行支持与物资协同。" },
|
||||
{ category: "QA", name: "QA", description: "负责医学审核与稽查,关注质量风险、合规性和医学一致性。" },
|
||||
{ category: "PV", name: "PV", description: "负责药物警戒相关工作,跟踪安全性事件并支持风险评估。" },
|
||||
{ category: "QA", name: "QA", description: "负责医学审核与稽查,关注质量风险、合规性和医学一致性。" },
|
||||
{ category: "CTA", name: "CTA", description: "负责合同、药品及相关项目事务管理,保障执行支持与物资协同。" },
|
||||
{ category: "CRA", name: "CRA", description: "负责各中心临床监查执行,跟进现场质量、数据和问题闭环。" },
|
||||
{ category: "PM", name: "PM", description: "项目负责人,统筹项目全局,协调进度、资源与关键决策。" },
|
||||
],
|
||||
}),
|
||||
}));
|
||||
@@ -96,7 +96,7 @@ describe("ApiEndpointPermissions.vue", () => {
|
||||
expect(source).not.toContain(':label="role"');
|
||||
});
|
||||
|
||||
it("sorts matrix role columns with the role list order", async () => {
|
||||
it("pins PM first in matrix role columns even when templates return PM last", async () => {
|
||||
const wrapper = mount(ApiEndpointPermissions, {
|
||||
props: {
|
||||
project: { id: "test-id", name: "Test Project" },
|
||||
@@ -117,7 +117,7 @@ describe("ApiEndpointPermissions.vue", () => {
|
||||
});
|
||||
await vi.dynamicImportSettled();
|
||||
|
||||
expect((wrapper.vm as any).roles).toEqual(["PM", "CRA", "CTA", "QA", "PV"]);
|
||||
expect((wrapper.vm as any).roles).toEqual(["PM", "PV", "QA", "CTA", "CRA"]);
|
||||
});
|
||||
|
||||
it("sorts project permission modules by sidebar menu order", async () => {
|
||||
|
||||
@@ -20,6 +20,7 @@ describe("PermissionTemplateSelector", () => {
|
||||
it("reads current project permissions by role key, not by role label", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain('v-for="template in sortedTemplates"');
|
||||
expect(source).toContain('<span class="card-name">{{ template.name }}</span>');
|
||||
expect(source).toContain("refreshKey?: number;");
|
||||
expect(source).toContain("watch(() => props.refreshKey, loadTemplates);");
|
||||
@@ -32,6 +33,15 @@ describe("PermissionTemplateSelector", () => {
|
||||
expect(source).toContain("emit('edit-role', template.category!)");
|
||||
});
|
||||
|
||||
it("sorts role cards with the shared role template order", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain("useRoleTemplateMeta");
|
||||
expect(source).toContain("compareRolesByTemplateOrder");
|
||||
expect(source).toContain("const sortedTemplates = computed");
|
||||
expect(source).toContain("compareRolesByTemplateOrder(a.category, b.category)");
|
||||
});
|
||||
|
||||
it("does not keep hard-coded preset role labels or icons", () => {
|
||||
const source = readSource();
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<div class="template-grid">
|
||||
<div
|
||||
v-for="template in templates"
|
||||
v-for="template in sortedTemplates"
|
||||
:key="template.id"
|
||||
class="template-card"
|
||||
:class="{ inactive: template.category && !isRoleActive(template.category) }"
|
||||
@@ -61,12 +61,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { computed, ref, onMounted, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import {
|
||||
fetchPermissionTemplates,
|
||||
type PermissionTemplate,
|
||||
} from "@/api/projectPermissions";
|
||||
import { useRoleTemplateMeta } from "@/composables/useRoleTemplateMeta";
|
||||
|
||||
const props = defineProps<{
|
||||
studyId: string;
|
||||
@@ -80,6 +81,16 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const templates = ref<PermissionTemplate[]>([]);
|
||||
const { compareRolesByTemplateOrder } = useRoleTemplateMeta();
|
||||
|
||||
const sortedTemplates = computed(() =>
|
||||
[...templates.value].sort((a, b) => {
|
||||
if (a.category && b.category) return compareRolesByTemplateOrder(a.category, b.category);
|
||||
if (a.category) return -1;
|
||||
if (b.category) return 1;
|
||||
return 0;
|
||||
})
|
||||
);
|
||||
|
||||
const roleIcon = (category: string | null) => {
|
||||
const icons: Record<string, string> = {
|
||||
|
||||
Reference in New Issue
Block a user