按业务模块拆分附件权限

This commit is contained in:
Cheng Zhou
2026-05-28 10:53:00 +08:00
parent d2b41ae454
commit 31fcb7e6f2
7 changed files with 473 additions and 57 deletions
@@ -3,7 +3,7 @@
<div class="header">
<span>{{ headerTitle }}</span>
<AttachmentUploader
v-if="!readonly"
v-if="canCreate"
:study-id="studyId"
:entity-type="entityType"
:entity-id="entityId"
@@ -68,9 +68,10 @@ import { formatFileSize } from "./attachmentUtils";
import { useAuthStore } from "../../store/auth";
import { useStudyStore } from "../../store/study";
import { isSystemAdmin } from "../../utils/roles";
import { usePermission } from "../../utils/permission";
import { listMembers } from "../../api/members";
import { displayDateTime, displayUser, getMemberDisplayName, getUserDisplayName } from "../../utils/display";
import { getAttachmentPermissionKey } from "../../utils/attachmentPermissions";
import { isApiPermissionAllowed } from "../../utils/apiPermissionValue";
import { TEXT } from "../../locales";
const props = defineProps<{
@@ -85,7 +86,6 @@ const attachments = ref<any[]>([]);
const loading = ref(false);
const auth = useAuthStore();
const study = useStudyStore();
const permission = usePermission();
const members = ref<any[]>([]);
const previewVisible = ref(false);
const previewUrl = ref("");
@@ -96,6 +96,20 @@ const previewError = ref("");
const previewLoading = ref(false);
const headerTitle = computed(() => props.title || TEXT.common.labels.attachments);
const currentRolePermissions = computed(() => {
const role = study.currentStudyRole || (study.currentStudy as any)?.role_in_study;
return role ? study.currentPermissions?.[role] : null;
});
const canUseAttachmentPermission = (action: "create" | "read" | "delete") => {
if (isSystemAdmin(auth.user)) return true;
const operationKey = getAttachmentPermissionKey(props.entityType, action);
if (!operationKey) return false;
return isApiPermissionAllowed(currentRolePermissions.value?.[operationKey]);
};
const canCreate = computed(() => !props.readonly && canUseAttachmentPermission("create"));
const load = async () => {
if (!props.studyId || !props.entityId) return;
loading.value = true;
@@ -205,8 +219,7 @@ const preview = (row: any) => {
const canDelete = (row: any) => {
if (props.readonly) return false;
const action = props.entityType === "knowledge_note" ? "shared.library.write" : "file.attachment.delete";
return isSystemAdmin(auth.user) || permission.can(action);
return canUseAttachmentPermission("delete");
};
const remove = async (row: any) => {
@@ -7,6 +7,7 @@
<div v-if="group.description" class="group-desc">{{ group.description }}</div>
</div>
<el-upload
v-if="canCreate"
:http-request="(options: any) => doUpload(group.key, options)"
:show-file-list="false"
:limit="1"
@@ -77,16 +78,17 @@
</template>
<script setup lang="ts">
import { onMounted, reactive, ref, watch } from "vue";
import { computed, onMounted, reactive, ref, watch } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { fetchAttachments, uploadAttachment, deleteAttachment } from "../../api/attachments";
import { listMembers } from "../../api/members";
import { useAuthStore } from "../../store/auth";
import { useStudyStore } from "../../store/study";
import { isSystemAdmin } from "../../utils/roles";
import { usePermission } from "../../utils/permission";
import { formatFileSize } from "../attachments/attachmentUtils";
import { displayUser, getMemberDisplayName, getUserDisplayName } from "../../utils/display";
import { getAttachmentPermissionKey } from "../../utils/attachmentPermissions";
import { isApiPermissionAllowed } from "../../utils/apiPermissionValue";
import StateEmpty from "../StateEmpty.vue";
import StateLoading from "../StateLoading.vue";
import { TEXT } from "../../locales";
@@ -111,7 +113,6 @@ const grouped = reactive<Record<string, any[]>>({});
const progressMap = reactive<Record<string, number>>({});
const auth = useAuthStore();
const study = useStudyStore();
const permission = usePermission();
const members = ref<any[]>([]);
const previewVisible = ref(false);
const previewUrl = ref("");
@@ -123,6 +124,20 @@ const previewLoading = ref(false);
const isUploading = (key: string) => (progressMap[key] || 0) > 0 && (progressMap[key] || 0) < 100;
const currentRolePermissions = computed(() => {
const role = study.currentStudyRole || (study.currentStudy as any)?.role_in_study;
return role ? study.currentPermissions?.[role] : null;
});
const canUseAttachmentPermission = (action: "create" | "read" | "delete") => {
if (isSystemAdmin(auth.user)) return true;
const operationKey = getAttachmentPermissionKey(`${props.entityType}_contract`, action);
if (!operationKey) return false;
return isApiPermissionAllowed(currentRolePermissions.value?.[operationKey]);
};
const canCreate = computed(() => !props.readonly && canUseAttachmentPermission("create"));
const loadMembers = async () => {
const studyId = study.currentStudy?.id;
if (!studyId) return;
@@ -271,7 +286,7 @@ const download = (row: any) => {
const canDelete = (row: any) => {
if (props.readonly) return false;
return isSystemAdmin(auth.user) || permission.can("fees.attachment.delete");
return canUseAttachmentPermission("delete");
};
const remove = async (row: any) => {
@@ -0,0 +1,20 @@
import { describe, expect, it } from "vitest";
import { getAttachmentPermissionKey } from "./attachmentPermissions";
describe("attachment permission mapping", () => {
it("maps attachment entity types to module attachment permissions", () => {
expect(getAttachmentPermissionKey("contract_fee_contract", "delete")).toBe("fees_contracts_attachments:delete");
expect(getAttachmentPermissionKey("startup_feasibility", "read")).toBe("startup_initiation_attachments:read");
expect(getAttachmentPermissionKey("startup_ethics", "create")).toBe("startup_ethics_attachments:create");
expect(getAttachmentPermissionKey("startup_kickoff_ppt", "read")).toBe("startup_auth_attachments:read");
expect(getAttachmentPermissionKey("training_authorization", "delete")).toBe("startup_auth_attachments:delete");
expect(getAttachmentPermissionKey("drug_shipment", "read")).toBe("drug_shipments_attachments:read");
expect(getAttachmentPermissionKey("precaution", "create")).toBe("precautions_attachments:create");
expect(getAttachmentPermissionKey("faq_replies", "create")).toBe("faq_attachments:create");
});
it("does not return generic attachments permissions", () => {
expect(getAttachmentPermissionKey("unknown", "read")).toBeNull();
expect(getAttachmentPermissionKey("precaution", "read")).not.toBe("attachments:read");
});
});
@@ -0,0 +1,25 @@
export type AttachmentPermissionAction = "create" | "read" | "delete";
const ATTACHMENT_PERMISSION_PREFIXES: Record<string, string> = {
contract_fee_contract: "fees_contracts_attachments",
contract_fee_voucher: "fees_contracts_attachments",
contract_fee_invoice: "fees_contracts_attachments",
startup_feasibility: "startup_initiation_attachments",
startup_ethics: "startup_ethics_attachments",
startup_kickoff: "startup_auth_attachments",
startup_kickoff_minutes: "startup_auth_attachments",
startup_kickoff_signin: "startup_auth_attachments",
startup_kickoff_ppt: "startup_auth_attachments",
training_authorization: "startup_auth_attachments",
drug_shipment: "drug_shipments_attachments",
precaution: "precautions_attachments",
faq_replies: "faq_attachments",
};
export const getAttachmentPermissionKey = (
entityType: string,
action: AttachmentPermissionAction
): string | null => {
const prefix = ATTACHMENT_PERMISSION_PREFIXES[entityType];
return prefix ? `${prefix}:${action}` : null;
};