优化前后端权限一致性
This commit is contained in:
@@ -13,10 +13,16 @@ describe("permission project role model", () => {
|
||||
expect(source).toContain("study.currentStudyRole");
|
||||
expect(source).toContain("study.currentPermissions");
|
||||
expect(source).toContain('"faq.edit": "faq:update"');
|
||||
expect(source).toContain('"fees.contract.create": "fees_contracts:create"');
|
||||
expect(source).toContain('"fees.contract.update": "fees_contracts:update"');
|
||||
expect(source).toContain('"fees.contract.delete": "fees_contracts:delete"');
|
||||
expect(source).toContain('"documents.create": "documents:create"');
|
||||
expect(source).toContain('"documents.delete": "documents:delete"');
|
||||
expect(source).toContain("isApiPermissionAllowed");
|
||||
expect(source).toContain("projectPermissions.value?.[projectRole.value]?.[operationKey]");
|
||||
expect(source).not.toContain("projectPermissions.value?.roles");
|
||||
expect(source).not.toContain('"faq.edit": "etmf');
|
||||
expect(source).not.toContain('"ADMIN", "PM"');
|
||||
expect(source).not.toContain('"fees.contract.write": "fees_contracts:create"');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,11 @@ const PERMISSIONS: Record<string, string[]> = {
|
||||
"project.members.manage": ["ADMIN"],
|
||||
"site.manage": ["ADMIN"],
|
||||
"site.cra.bind": ["ADMIN"],
|
||||
"fees.contract.write": ["ADMIN"],
|
||||
"fees.contract.create": ["ADMIN"],
|
||||
"fees.contract.update": ["ADMIN"],
|
||||
"fees.contract.delete": ["ADMIN"],
|
||||
"documents.create": ["ADMIN"],
|
||||
"documents.delete": ["ADMIN"],
|
||||
};
|
||||
|
||||
const REASONS: Record<string, string> = {
|
||||
@@ -32,7 +36,11 @@ const REASONS: Record<string, string> = {
|
||||
"project.members.manage": TEXT.modules.permissions.projectMembersManage,
|
||||
"site.manage": TEXT.modules.permissions.siteManage,
|
||||
"site.cra.bind": TEXT.modules.permissions.siteCraBind,
|
||||
"fees.contract.write": TEXT.modules.permissions.feeContractsWrite,
|
||||
"fees.contract.create": TEXT.modules.permissions.feeContractsWrite,
|
||||
"fees.contract.update": TEXT.modules.permissions.feeContractsWrite,
|
||||
"fees.contract.delete": TEXT.modules.permissions.feeContractsWrite,
|
||||
"documents.create": TEXT.modules.permissions.default,
|
||||
"documents.delete": TEXT.modules.permissions.default,
|
||||
};
|
||||
|
||||
export const usePermission = () => {
|
||||
@@ -63,7 +71,11 @@ export const usePermission = () => {
|
||||
"project.members.manage": "project_members:update",
|
||||
"site.manage": "sites:update",
|
||||
"site.cra.bind": "sites:update",
|
||||
"fees.contract.write": "fees_contracts:create",
|
||||
"fees.contract.create": "fees_contracts:create",
|
||||
"fees.contract.update": "fees_contracts:update",
|
||||
"fees.contract.delete": "fees_contracts:delete",
|
||||
"documents.create": "documents:create",
|
||||
"documents.delete": "documents:delete",
|
||||
"fees.attachment.delete": "fees_attachments:delete",
|
||||
"file.attachment.delete": "attachments:delete",
|
||||
"audit.export.read": "audit_logs:read",
|
||||
|
||||
@@ -6,11 +6,20 @@ describe("project route permissions", () => {
|
||||
expect(getProjectRoutePermission("/project/overview")).toEqual({ operationKey: "project_overview:read" });
|
||||
expect(getProjectRoutePermission("/project/milestones")).toEqual({ operationKey: "project_milestones:read" });
|
||||
expect(getProjectRoutePermission("/finance/contracts")).toEqual({ operationKey: "finance_contracts:read" });
|
||||
expect(getProjectRoutePermission("/finance/contracts/new")).toEqual({ operationKey: "finance_contracts:create" });
|
||||
expect(getProjectRoutePermission("/finance/contracts/abc/edit")).toEqual({ operationKey: "finance_contracts:update" });
|
||||
expect(getProjectRoutePermission("/drug/shipments")).toEqual({ operationKey: "drug_shipments:read" });
|
||||
expect(getProjectRoutePermission("/startup/feasibility/new")).toEqual({ operationKey: "ethics:read" });
|
||||
expect(getProjectRoutePermission("/startup/kickoff/new")).toEqual({ operationKey: "startup_auth:read" });
|
||||
expect(getProjectRoutePermission("/drug/shipments/new")).toEqual({ operationKey: "drug_shipments:create" });
|
||||
expect(getProjectRoutePermission("/drug/shipments/abc/edit")).toEqual({ operationKey: "drug_shipments:update" });
|
||||
expect(getProjectRoutePermission("/subjects/new")).toEqual({ operationKey: "subjects:create" });
|
||||
expect(getProjectRoutePermission("/subjects/abc/edit")).toEqual({ operationKey: "subjects:update" });
|
||||
expect(getProjectRoutePermission("/startup/feasibility/new")).toEqual({ operationKey: "ethics:create" });
|
||||
expect(getProjectRoutePermission("/startup/feasibility/abc/edit")).toEqual({ operationKey: "ethics:update" });
|
||||
expect(getProjectRoutePermission("/startup/kickoff/new")).toEqual({ operationKey: "startup_auth:create" });
|
||||
expect(getProjectRoutePermission("/startup/kickoff/abc/edit")).toEqual({ operationKey: "startup_auth:update" });
|
||||
expect(getProjectRoutePermission("/knowledge/medical-consult/abc")).toEqual({ operationKey: "faq:read" });
|
||||
expect(getProjectRoutePermission("/knowledge/notes/new")).toEqual({ operationKey: "knowledge_notes:read" });
|
||||
expect(getProjectRoutePermission("/knowledge/notes/new")).toEqual({ operationKey: "knowledge_notes:create" });
|
||||
expect(getProjectRoutePermission("/knowledge/notes/abc/edit")).toEqual({ operationKey: "knowledge_notes:update" });
|
||||
});
|
||||
|
||||
it("rejects project routes when the role lacks operation permission", () => {
|
||||
|
||||
@@ -6,6 +6,13 @@ export type ProjectRoutePermission = {
|
||||
};
|
||||
|
||||
const routePermissions: Array<{ prefixes: string[]; permission: ProjectRoutePermission }> = [
|
||||
{ prefixes: ["/fees/contracts/new"], permission: { operationKey: "fees_contracts:create" } },
|
||||
{ prefixes: ["/finance/contracts/new"], permission: { operationKey: "finance_contracts:create" } },
|
||||
{ prefixes: ["/drug/shipments/new"], permission: { operationKey: "drug_shipments:create" } },
|
||||
{ prefixes: ["/startup/feasibility/new", "/startup/ethics/new"], permission: { operationKey: "ethics:create" } },
|
||||
{ prefixes: ["/startup/kickoff/new", "/startup/training/new"], permission: { operationKey: "startup_auth:create" } },
|
||||
{ prefixes: ["/subjects/new"], permission: { operationKey: "subjects:create" } },
|
||||
{ prefixes: ["/knowledge/notes/new"], permission: { operationKey: "knowledge_notes:create" } },
|
||||
{ prefixes: ["/project/overview"], permission: { operationKey: "project_overview:read" } },
|
||||
{ prefixes: ["/project/milestones"], permission: { operationKey: "project_milestones:read" } },
|
||||
{ prefixes: ["/fees/contracts"], permission: { operationKey: "fees_contracts:read" } },
|
||||
@@ -23,6 +30,18 @@ const routePermissions: Array<{ prefixes: string[]; permission: ProjectRoutePerm
|
||||
{ prefixes: ["/knowledge/notes", "/knowledge/support-files", "/knowledge/instruction-files"], permission: { operationKey: "knowledge_notes:read" } },
|
||||
];
|
||||
|
||||
const editRoutePermissions: Array<{ pattern: RegExp; permission: ProjectRoutePermission }> = [
|
||||
{ pattern: /^\/fees\/contracts\/[^/]+\/edit$/, permission: { operationKey: "fees_contracts:update" } },
|
||||
{ pattern: /^\/finance\/contracts\/[^/]+\/edit$/, permission: { operationKey: "finance_contracts:update" } },
|
||||
{ pattern: /^\/drug\/shipments\/[^/]+\/edit$/, permission: { operationKey: "drug_shipments:update" } },
|
||||
{ pattern: /^\/startup\/feasibility\/[^/]+\/edit$/, permission: { operationKey: "ethics:update" } },
|
||||
{ pattern: /^\/startup\/ethics\/[^/]+\/edit$/, permission: { operationKey: "ethics:update" } },
|
||||
{ pattern: /^\/startup\/kickoff\/[^/]+\/edit$/, permission: { operationKey: "startup_auth:update" } },
|
||||
{ pattern: /^\/startup\/training\/[^/]+\/edit$/, permission: { operationKey: "startup_auth:update" } },
|
||||
{ pattern: /^\/subjects\/[^/]+\/edit$/, permission: { operationKey: "subjects:update" } },
|
||||
{ pattern: /^\/knowledge\/notes\/[^/]+\/edit$/, permission: { operationKey: "knowledge_notes:update" } },
|
||||
];
|
||||
|
||||
export const projectRouteLandingPaths = [
|
||||
"/project/overview",
|
||||
"/project/milestones",
|
||||
@@ -44,6 +63,11 @@ export const projectRouteLandingPaths = [
|
||||
|
||||
export const getProjectRoutePermission = (path: string): ProjectRoutePermission | null => {
|
||||
const normalized = path || "";
|
||||
for (const item of editRoutePermissions) {
|
||||
if (item.pattern.test(normalized)) {
|
||||
return item.permission;
|
||||
}
|
||||
}
|
||||
for (const item of routePermissions) {
|
||||
if (item.prefixes.some((prefix) => normalized === prefix || normalized.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`))) {
|
||||
return item.permission;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSource = () => readFileSync(resolve(__dirname, "./DocumentList.vue"), "utf8");
|
||||
|
||||
describe("DocumentList permissions", () => {
|
||||
it("guards create and delete actions with document operation permissions", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain('can("documents.create")');
|
||||
expect(source).toContain('can("documents.delete")');
|
||||
expect(source).toContain(":disabled=\"!canCreate\"");
|
||||
expect(source).toContain("v-if=\"canDelete\"");
|
||||
});
|
||||
});
|
||||
@@ -22,7 +22,7 @@
|
||||
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
|
||||
</div>
|
||||
<div class="filter-spacer"></div>
|
||||
<el-button type="primary" @click="openCreate" class="header-action-btn">
|
||||
<el-button type="primary" :disabled="!canCreate" @click="openCreate" class="header-action-btn">
|
||||
<el-icon class="el-icon--left"><Plus /></el-icon>
|
||||
{{ TEXT.modules.fileVersionManagement.newDocument }}
|
||||
</el-button>
|
||||
@@ -75,7 +75,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.actions" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="isAdmin" link type="danger" :disabled="isInactiveSite(row.site_id)" @click.stop="confirmDelete(row)">
|
||||
<el-button v-if="canDelete" link type="danger" :disabled="isInactiveSite(row.site_id)" @click.stop="confirmDelete(row)">
|
||||
{{ TEXT.common.actions.delete }}
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -128,6 +128,7 @@ import { fetchDocuments, createDocument, deleteDocument } from "../../api/docume
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { usePermission } from "../../utils/permission";
|
||||
import type { DocumentSummary } from "../../types/documents";
|
||||
import type { Site } from "../../types/api";
|
||||
import { displayEnum, displayText } from "../../utils/display";
|
||||
@@ -137,6 +138,7 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
const study = useStudyStore();
|
||||
const auth = useAuthStore();
|
||||
const { can } = usePermission();
|
||||
const loading = ref(false);
|
||||
const items = ref<DocumentSummary[]>([]);
|
||||
const sites = ref<Site[]>([]);
|
||||
@@ -173,6 +175,8 @@ const siteActiveMap = computed(() => {
|
||||
const isAdmin = computed(() => {
|
||||
return !!auth.user?.is_admin;
|
||||
});
|
||||
const canCreate = computed(() => can("documents.create"));
|
||||
const canDelete = computed(() => can("documents.delete"));
|
||||
const isInactiveSite = (siteId?: string | null) => !!siteId && siteActiveMap.value[siteId] === false;
|
||||
const documentRowClass = ({ row }: { row: DocumentSummary }) =>
|
||||
`${row?.id ? "clickable-row" : ""}${isInactiveSite(row?.site_id) ? " row-inactive" : ""}`.trim();
|
||||
@@ -259,6 +263,10 @@ const resetFilters = () => {
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
if (!canCreate.value) {
|
||||
ElMessage.warning("权限不足");
|
||||
return;
|
||||
}
|
||||
createForm.doc_no = "";
|
||||
createForm.title = "";
|
||||
createForm.scope_type = "GLOBAL";
|
||||
@@ -268,6 +276,10 @@ const openCreate = () => {
|
||||
};
|
||||
|
||||
const submitCreate = async () => {
|
||||
if (!canCreate.value) {
|
||||
ElMessage.warning("权限不足");
|
||||
return;
|
||||
}
|
||||
if (!createFormRef.value) return;
|
||||
await createFormRef.value.validate(async (valid) => {
|
||||
if (!valid || !trialId.value) return;
|
||||
@@ -305,6 +317,10 @@ const handleRowClick = (row: DocumentSummary) => {
|
||||
};
|
||||
|
||||
const confirmDelete = async (row: DocumentSummary) => {
|
||||
if (!canDelete.value) {
|
||||
ElMessage.warning("权限不足");
|
||||
return;
|
||||
}
|
||||
if (!row?.id) return;
|
||||
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
|
||||
@@ -116,6 +116,7 @@ import { fetchSites } from "../../api/sites";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
import { isApiPermissionAllowed } from "../../utils/apiPermissionValue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -134,7 +135,12 @@ const siteActiveMap = computed(() => {
|
||||
const shipmentId = computed(() => route.params.shipmentId as string | undefined);
|
||||
const isEdit = computed(() => !!shipmentId.value);
|
||||
const studyId = computed(() => study.currentStudy?.id || "");
|
||||
const isReadOnly = computed(() => isEdit.value && !!form.center_id && siteActiveMap.value[form.center_id] === false);
|
||||
const projectRole = computed(() => study.currentStudyRole || (study.currentStudy as any)?.role_in_study || "");
|
||||
const canMutate = computed(() => {
|
||||
const operationKey = isEdit.value ? "drug_shipments:update" : "drug_shipments:create";
|
||||
return isApiPermissionAllowed(study.currentPermissions?.[projectRole.value]?.[operationKey]);
|
||||
});
|
||||
const isReadOnly = computed(() => !canMutate.value || (isEdit.value && !!form.center_id && siteActiveMap.value[form.center_id] === false));
|
||||
|
||||
const form = reactive({
|
||||
center_id: "",
|
||||
@@ -195,6 +201,10 @@ const load = async () => {
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
if (!canMutate.value) {
|
||||
ElMessage.warning("权限不足");
|
||||
return;
|
||||
}
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
|
||||
@@ -127,7 +127,7 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
const study = useStudyStore();
|
||||
const { can } = usePermission();
|
||||
const canWrite = computed(() => can("fees.contract.write"));
|
||||
const canWrite = computed(() => can("fees.contract.update"));
|
||||
|
||||
const contractId = route.params.contractId as string;
|
||||
const loading = ref(false);
|
||||
|
||||
@@ -222,6 +222,7 @@ import StateError from "../../components/StateError.vue";
|
||||
import StateLoading from "../../components/StateLoading.vue";
|
||||
import FeeAttachmentPanel from "../../components/fees/FeeAttachmentPanel.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
import { isApiPermissionAllowed } from "../../utils/apiPermissionValue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -238,8 +239,6 @@ const siteActiveMap = computed(() => {
|
||||
});
|
||||
return map;
|
||||
});
|
||||
const isReadOnly = computed(() => isEdit.value && !!form.center_id && siteActiveMap.value[form.center_id] === false);
|
||||
|
||||
const formRef = ref();
|
||||
const form = reactive({
|
||||
id: "",
|
||||
@@ -258,6 +257,12 @@ const paymentErrors = ref<Record<string, string>[]>([]);
|
||||
|
||||
const contractId = computed(() => route.params.contractId as string | undefined);
|
||||
const isEdit = computed(() => !!contractId.value);
|
||||
const projectRole = computed(() => study.currentStudyRole || (study.currentStudy as any)?.role_in_study || "");
|
||||
const canMutate = computed(() => {
|
||||
const operationKey = isEdit.value ? "fees_contracts:update" : "fees_contracts:create";
|
||||
return isApiPermissionAllowed(study.currentPermissions?.[projectRole.value]?.[operationKey]);
|
||||
});
|
||||
const isReadOnly = computed(() => !canMutate.value || (isEdit.value && !!form.center_id && siteActiveMap.value[form.center_id] === false));
|
||||
|
||||
const attachmentGroups = [
|
||||
{
|
||||
@@ -409,6 +414,10 @@ const validatePayments = () => {
|
||||
|
||||
const submit = async () => {
|
||||
if (!study.currentStudy?.id) return;
|
||||
if (!canMutate.value) {
|
||||
ElMessage.warning("权限不足");
|
||||
return;
|
||||
}
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
|
||||
</div>
|
||||
<div class="filter-spacer"></div>
|
||||
<el-button type="primary" :disabled="!canWrite" @click="goNew" class="header-action-btn">
|
||||
<el-button type="primary" :disabled="!canCreate" @click="goNew" class="header-action-btn">
|
||||
<el-icon class="el-icon--left"><Plus /></el-icon>
|
||||
{{ TEXT.modules.feeContracts.newTitle }}
|
||||
</el-button>
|
||||
@@ -158,7 +158,7 @@
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
:disabled="!canWrite || isInactiveSite(scope.row.center_id)"
|
||||
:disabled="!canDelete || isInactiveSite(scope.row.center_id)"
|
||||
@click.stop="remove(scope.row)"
|
||||
>
|
||||
{{ TEXT.common.actions.delete }}
|
||||
@@ -192,7 +192,8 @@ import { TEXT } from "../../locales";
|
||||
const router = useRouter();
|
||||
const study = useStudyStore();
|
||||
const { can } = usePermission();
|
||||
const canWrite = computed(() => can("fees.contract.write"));
|
||||
const canCreate = computed(() => can("fees.contract.create"));
|
||||
const canDelete = computed(() => can("fees.contract.delete"));
|
||||
|
||||
const loading = ref(false);
|
||||
const errorMessage = ref("");
|
||||
@@ -295,7 +296,7 @@ const onRowClick = (row: any) => {
|
||||
};
|
||||
|
||||
const remove = async (row: any) => {
|
||||
if (!row?.id || !canWrite.value) return;
|
||||
if (!row?.id || !canDelete.value) return;
|
||||
if (isInactiveSite(row?.center_id)) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
|
||||
@@ -61,6 +61,7 @@ import { fetchSites } from "../../api/sites";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
import { isApiPermissionAllowed } from "../../utils/apiPermissionValue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -71,6 +72,11 @@ const sites = ref<any[]>([]);
|
||||
const contractId = computed(() => route.params.contractId as string | undefined);
|
||||
const isEdit = computed(() => !!contractId.value);
|
||||
const studyId = computed(() => study.currentStudy?.id || "");
|
||||
const projectRole = computed(() => study.currentStudyRole || (study.currentStudy as any)?.role_in_study || "");
|
||||
const canMutate = computed(() => {
|
||||
const operationKey = isEdit.value ? "finance_contracts:update" : "finance_contracts:create";
|
||||
return isApiPermissionAllowed(study.currentPermissions?.[projectRole.value]?.[operationKey]);
|
||||
});
|
||||
|
||||
const form = reactive({
|
||||
site_name: "",
|
||||
@@ -88,7 +94,7 @@ const siteActiveMap = computed(() => {
|
||||
});
|
||||
return map;
|
||||
});
|
||||
const isReadOnly = computed(() => isEdit.value && !!form.site_name && siteActiveMap.value[form.site_name] === false);
|
||||
const isReadOnly = computed(() => !canMutate.value || (isEdit.value && !!form.site_name && siteActiveMap.value[form.site_name] === false));
|
||||
|
||||
const loadSites = async () => {
|
||||
if (!studyId.value) return;
|
||||
@@ -119,6 +125,10 @@ const load = async () => {
|
||||
|
||||
const submit = async () => {
|
||||
if (!studyId.value) return;
|
||||
if (!canMutate.value) {
|
||||
ElMessage.warning("权限不足");
|
||||
return;
|
||||
}
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
|
||||
@@ -120,6 +120,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { createSubject, getSubject, updateSubject } from "../../api/subjects";
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import { TEXT } from "../../locales";
|
||||
import { isApiPermissionAllowed } from "../../utils/apiPermissionValue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -137,7 +138,12 @@ const siteActiveMap = computed(() => {
|
||||
const subjectId = computed(() => route.params.subjectId as string | undefined);
|
||||
const isEdit = computed(() => !!subjectId.value);
|
||||
const studyId = computed(() => study.currentStudy?.id || "");
|
||||
const isReadOnly = computed(() => isEdit.value && !!form.site_id && siteActiveMap.value[form.site_id] === false);
|
||||
const projectRole = computed(() => study.currentStudyRole || (study.currentStudy as any)?.role_in_study || "");
|
||||
const canMutate = computed(() => {
|
||||
const operationKey = isEdit.value ? "subjects:update" : "subjects:create";
|
||||
return isApiPermissionAllowed(study.currentPermissions?.[projectRole.value]?.[operationKey]);
|
||||
});
|
||||
const isReadOnly = computed(() => !canMutate.value || (isEdit.value && !!form.site_id && siteActiveMap.value[form.site_id] === false));
|
||||
|
||||
const form = reactive({
|
||||
subject_no: "",
|
||||
@@ -180,6 +186,10 @@ const load = async () => {
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
if (!canMutate.value) {
|
||||
ElMessage.warning("权限不足");
|
||||
return;
|
||||
}
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user