补齐业务路由与页面权限控制
This commit is contained in:
@@ -51,7 +51,7 @@
|
||||
:quote-item="quoteReply"
|
||||
:member-map="memberMap"
|
||||
:user-map="userMap"
|
||||
:allow-attachments="true"
|
||||
:allow-attachments="canUploadReplyAttachment"
|
||||
@submit="submitReply"
|
||||
@clear-quote="clearQuote"
|
||||
/>
|
||||
@@ -107,7 +107,10 @@ import { listMembers } from "../api/members";
|
||||
import { fetchAttachments, uploadAttachment } from "../api/attachments";
|
||||
import { displayDateTime, displayUser, getMemberDisplayName, getUserDisplayName } from "../utils/display";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { useStudyStore } from "../store/study";
|
||||
import { usePermission } from "../utils/permission";
|
||||
import { getAttachmentPermissionKey } from "../utils/attachmentPermissions";
|
||||
import { isApiPermissionAllowed } from "../utils/apiPermissionValue";
|
||||
import FaqItemForm from "../components/FaqItemForm.vue";
|
||||
import ThreadComposer from "../components/ThreadComposer.vue";
|
||||
import ThreadList from "../components/ThreadList.vue";
|
||||
@@ -115,6 +118,7 @@ import { TEXT } from "../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const auth = useAuthStore();
|
||||
const study = useStudyStore();
|
||||
const item = ref<any>(null);
|
||||
const loading = ref(false);
|
||||
const categories = ref<any[]>([]);
|
||||
@@ -136,6 +140,14 @@ const canReply = computed(() => {
|
||||
return can("faq.reply");
|
||||
});
|
||||
|
||||
const canUploadReplyAttachment = computed(() => {
|
||||
if (!item.value?.study_id) return false;
|
||||
if (auth.user?.is_admin) return true;
|
||||
const role = study.currentStudyRole || (study.currentStudy as any)?.role_in_study;
|
||||
const operationKey = getAttachmentPermissionKey("faq_replies", "create");
|
||||
return !!role && !!operationKey && isApiPermissionAllowed(study.currentPermissions?.[role]?.[operationKey]);
|
||||
});
|
||||
|
||||
const canDeleteReply = (reply: any) => {
|
||||
if (reply.is_deleted) return false;
|
||||
if (auth.user?.is_admin) return true;
|
||||
@@ -273,7 +285,7 @@ const submitReply = async () => {
|
||||
quote_reply_id: quoteReply.value?.id || null,
|
||||
});
|
||||
const created = data as any;
|
||||
if (replyFiles.value.length && created?.id) {
|
||||
if (canUploadReplyAttachment.value && replyFiles.value.length && created?.id) {
|
||||
try {
|
||||
for (const file of replyFiles.value) {
|
||||
if (!file.raw) continue;
|
||||
|
||||
@@ -362,7 +362,7 @@ const uploadRules: FormRules = {
|
||||
};
|
||||
|
||||
const roleOptions = computed(() => {
|
||||
const fallbackRoles = ["CRA", "PM", "IMP", "PV", "MEDICAL_REVIEW"];
|
||||
const fallbackRoles = ["CRA", "PM", "CTA", "PV", "QA"];
|
||||
const roles = new Set(members.value.map((m) => m.role_in_study).filter(Boolean));
|
||||
const values = roles.size ? Array.from(roles) : fallbackRoles;
|
||||
return values.map((value) => ({
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<ModulePlaceholder
|
||||
:title="TEXT.modules.audit.title"
|
||||
:subtitle="TEXT.modules.audit.subtitle"
|
||||
:list-title="TEXT.modules.audit.listTitle"
|
||||
:empty-description="TEXT.modules.audit.emptyDescription"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ModulePlaceholder from "../../components/ModulePlaceholder.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
</script>
|
||||
@@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<ModulePlaceholder
|
||||
:title="TEXT.modules.monitoringAudit.title"
|
||||
:subtitle="TEXT.modules.monitoringAudit.subtitle"
|
||||
:list-title="TEXT.modules.monitoringAudit.listTitle"
|
||||
:empty-description="TEXT.modules.monitoringAudit.emptyDescription"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ModulePlaceholder from "../../components/ModulePlaceholder.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
</script>
|
||||
@@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<ModulePlaceholder
|
||||
:title="TEXT.modules.monitoring.title"
|
||||
:subtitle="TEXT.modules.monitoring.subtitle"
|
||||
:list-title="TEXT.modules.monitoring.listTitle"
|
||||
:empty-description="TEXT.modules.monitoring.emptyDescription"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ModulePlaceholder from "../../components/ModulePlaceholder.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readRiskIssueMonitoringVisits = () => readFileSync(resolve(__dirname, "./RiskIssueMonitoringVisits.vue"), "utf8");
|
||||
|
||||
describe("RiskIssueMonitoringVisits permissions", () => {
|
||||
it("gates monitoring visit issue actions by concrete API permissions", () => {
|
||||
const source = readRiskIssueMonitoringVisits();
|
||||
|
||||
expect(source).toContain("useAuthStore");
|
||||
expect(source).toContain("isSystemAdmin");
|
||||
expect(source).toContain("getProjectRole");
|
||||
expect(source).toContain("isApiPermissionAllowed");
|
||||
[
|
||||
"monitoring_issues:list",
|
||||
"monitoring_issues:read",
|
||||
"monitoring_issues:create",
|
||||
"monitoring_issues:update",
|
||||
"monitoring_issues:delete",
|
||||
].forEach((operationKey) => {
|
||||
expect(source).toContain(operationKey);
|
||||
});
|
||||
expect(source).toContain('v-if="canCreateIssue"');
|
||||
expect(source).toContain('v-if="canListIssues"');
|
||||
expect(source).toContain('v-if="canReadIssue"');
|
||||
expect(source).toContain('v-if="canUpdateIssue"');
|
||||
expect(source).toContain('v-if="canDeleteIssue"');
|
||||
expect(source).toContain("const canSaveIssue = computed(() => (formMode.value === \"edit\" ? canUpdateIssue.value : canCreateIssue.value));");
|
||||
});
|
||||
});
|
||||
@@ -105,7 +105,7 @@
|
||||
</div>
|
||||
<div class="template-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-button class="toolbar-button toolbar-button-primary" type="primary" @click="openCreateDialog">
|
||||
<el-button v-if="canCreateIssue" class="toolbar-button toolbar-button-primary" type="primary" @click="openCreateDialog">
|
||||
<el-icon><Plus /></el-icon>
|
||||
<span>新增</span>
|
||||
</el-button>
|
||||
@@ -117,7 +117,14 @@
|
||||
<el-icon><CircleCheck /></el-icon>
|
||||
<span>一键完成</span>
|
||||
</el-button>
|
||||
<el-button class="toolbar-button toolbar-button-success" type="success" plain :loading="exporting" @click="handleExportExcel">
|
||||
<el-button
|
||||
v-if="canListIssues"
|
||||
class="toolbar-button toolbar-button-success"
|
||||
type="success"
|
||||
plain
|
||||
:loading="exporting"
|
||||
@click="handleExportExcel"
|
||||
>
|
||||
<el-icon><Download /></el-icon>
|
||||
<span>导出</span>
|
||||
</el-button>
|
||||
@@ -126,6 +133,7 @@
|
||||
<span>问题导入模板</span>
|
||||
</el-button>
|
||||
<el-upload
|
||||
v-if="canCreateIssue"
|
||||
ref="importUploadRef"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
@@ -166,12 +174,12 @@
|
||||
<el-table-column prop="recommendation" label="建议" min-width="190" show-overflow-tooltip />
|
||||
<el-table-column prop="center_query" label="中心质疑" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="center_latest_reply" label="中心最新回复" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="140" align="center">
|
||||
<el-table-column v-if="canReadIssue || canUpdateIssue || canDeleteIssue" label="操作" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="action-cell">
|
||||
<el-button class="action-btn" link type="primary" size="small" @click="openViewDialog(row)">查看</el-button>
|
||||
<el-button class="action-btn" link type="primary" size="small" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button class="action-btn" link type="danger" size="small" @click="removeIssue(row)">删除</el-button>
|
||||
<el-button v-if="canReadIssue" class="action-btn" link type="primary" size="small" @click="openViewDialog(row)">查看</el-button>
|
||||
<el-button v-if="canUpdateIssue" class="action-btn" link type="primary" size="small" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button v-if="canDeleteIssue" class="action-btn" link type="danger" size="small" @click="removeIssue(row)">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -403,7 +411,7 @@
|
||||
<template #footer>
|
||||
<div class="editor-footer">
|
||||
<el-button @click="formDialogVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submitForm">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button type="primary" :loading="saving" :disabled="!canSaveIssue" @click="submitForm">{{ TEXT.common.actions.save }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
@@ -460,9 +468,12 @@ import {
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import type { Site } from "../../types/api";
|
||||
import { isApiPermissionAllowed } from "../../utils/apiPermissionValue";
|
||||
import { displayDateTime } from "../../utils/display";
|
||||
import { getProjectRole, isSystemAdmin } from "../../utils/roles";
|
||||
|
||||
interface MonitoringIssueRow {
|
||||
id: string;
|
||||
@@ -499,6 +510,7 @@ interface MonitoringIssueRow {
|
||||
}
|
||||
type DateRange = [string, string] | [];
|
||||
|
||||
const auth = useAuthStore();
|
||||
const study = useStudyStore();
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
@@ -515,6 +527,19 @@ const viewIssue = ref<MonitoringIssueRow | null>(null);
|
||||
const sitesLoading = ref(false);
|
||||
const siteOptions = ref<Array<Pick<Site, "id" | "name" | "is_active">>>([]);
|
||||
const siteMap = ref<Record<string, string>>({});
|
||||
const projectRole = computed(() => getProjectRole(study.currentStudy, study.currentStudyRole));
|
||||
const canUseApiPermission = (operationKey: string) => {
|
||||
if (isSystemAdmin(auth.user)) return true;
|
||||
const role = projectRole.value;
|
||||
if (!role) return false;
|
||||
return isApiPermissionAllowed(study.currentPermissions?.[role]?.[operationKey]);
|
||||
};
|
||||
const canListIssues = computed(() => canUseApiPermission("monitoring_issues:list"));
|
||||
const canReadIssue = computed(() => canUseApiPermission("monitoring_issues:read"));
|
||||
const canCreateIssue = computed(() => canUseApiPermission("monitoring_issues:create"));
|
||||
const canUpdateIssue = computed(() => canUseApiPermission("monitoring_issues:update"));
|
||||
const canDeleteIssue = computed(() => canUseApiPermission("monitoring_issues:delete"));
|
||||
const canSaveIssue = computed(() => (formMode.value === "edit" ? canUpdateIssue.value : canCreateIssue.value));
|
||||
|
||||
const currentProjectCenter = computed(() => study.currentStudy?.name || study.currentStudy?.project_full_name || "");
|
||||
|
||||
@@ -802,6 +827,7 @@ const resetFiltersForStudy = () => {
|
||||
};
|
||||
|
||||
const openCreateDialog = () => {
|
||||
if (!canCreateIssue.value) return;
|
||||
resetCreateForm();
|
||||
formMode.value = "create";
|
||||
editingIssueId.value = "";
|
||||
@@ -810,7 +836,7 @@ const openCreateDialog = () => {
|
||||
|
||||
const openEditDialog = async (row: MonitoringIssueRow) => {
|
||||
const studyId = study.currentStudy?.id;
|
||||
if (!studyId || !row?.id) return;
|
||||
if (!studyId || !row?.id || !canUpdateIssue.value) return;
|
||||
try {
|
||||
const { data } = await getMonitoringVisitIssue(studyId, row.id);
|
||||
const issue = (data || row) as MonitoringIssueRow;
|
||||
@@ -825,7 +851,7 @@ const openEditDialog = async (row: MonitoringIssueRow) => {
|
||||
|
||||
const openViewDialog = async (row: MonitoringIssueRow) => {
|
||||
const studyId = study.currentStudy?.id;
|
||||
if (!studyId || !row?.id) return;
|
||||
if (!studyId || !row?.id || !canReadIssue.value) return;
|
||||
try {
|
||||
const { data } = await getMonitoringVisitIssue(studyId, row.id);
|
||||
viewIssue.value = (data || row) as MonitoringIssueRow;
|
||||
@@ -837,7 +863,7 @@ const openViewDialog = async (row: MonitoringIssueRow) => {
|
||||
|
||||
const submitForm = async () => {
|
||||
const studyId = study.currentStudy?.id;
|
||||
if (!studyId) return;
|
||||
if (!studyId || !canSaveIssue.value) return;
|
||||
const ok = await createFormRef.value?.validate().catch(() => false);
|
||||
if (!ok) return;
|
||||
|
||||
@@ -891,7 +917,7 @@ const submitForm = async () => {
|
||||
|
||||
const removeIssue = async (row: MonitoringIssueRow) => {
|
||||
const studyId = study.currentStudy?.id;
|
||||
if (!studyId || !row?.id) return;
|
||||
if (!studyId || !row?.id || !canDeleteIssue.value) return;
|
||||
const confirmed = await ElMessageBox.confirm(`确认删除问题「${row.issue_no}」?`, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!confirmed) return;
|
||||
|
||||
@@ -914,7 +940,7 @@ const getFilename = (header?: string | null) => {
|
||||
|
||||
const handleExportExcel = async () => {
|
||||
const studyId = study.currentStudy?.id;
|
||||
if (!studyId) return;
|
||||
if (!studyId || !canListIssues.value) return;
|
||||
if (filteredItems.value.length === 0) {
|
||||
ElMessage.warning("暂无可导出数据");
|
||||
return;
|
||||
@@ -964,7 +990,7 @@ const handleExportExcel = async () => {
|
||||
|
||||
const onImportChange = async (uploadFile: UploadFile) => {
|
||||
const studyId = study.currentStudy?.id;
|
||||
if (!studyId) return;
|
||||
if (!studyId || !canCreateIssue.value) return;
|
||||
const file = uploadFile.raw;
|
||||
if (!file) return;
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ import { useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { CircleCheck, Location, Search, Warning } from "@element-plus/icons-vue";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { fetchAes } from "../../api/aes";
|
||||
import { fetchRiskIssueAes } from "../../api/aes";
|
||||
import { fetchSubjects } from "../../api/subjects";
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import { displayDate, displayEnum } from "../../utils/display";
|
||||
@@ -166,7 +166,7 @@ const load = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await Promise.all([loadSites(studyId), loadSubjects(studyId)]);
|
||||
const { data } = await fetchAes(studyId);
|
||||
const { data } = await fetchRiskIssueAes(studyId);
|
||||
items.value = (Array.isArray(data) ? data : data.items || []).sort((a: any, b: any) =>
|
||||
String(b.updated_at || "").localeCompare(String(a.updated_at || ""))
|
||||
);
|
||||
|
||||
@@ -69,7 +69,7 @@ export const STAGE_ORDER: Array<{
|
||||
completedKey: StageCompletionKey;
|
||||
}> = [
|
||||
{ key: "institution_initiation_status", label: "机构立项", completedKey: "institution_initiation_completed_at" },
|
||||
{ key: "ethics_status", label: "伦理审批", completedKey: "ethics_completed_at" },
|
||||
{ key: "ethics_status", label: "伦理记录", completedKey: "ethics_completed_at" },
|
||||
{ key: "contract_sign_status", label: "合同签署", completedKey: "contract_sign_completed_at" },
|
||||
{ key: "startup_status", label: "启动", completedKey: "startup_completed_at" },
|
||||
{ key: "enrollment_status", label: "入组", completedKey: "enrollment_completed_at" },
|
||||
|
||||
@@ -48,4 +48,45 @@ describe("SubjectDetail medication adherence", () => {
|
||||
expect(source).toContain("row.window_start === row.planned_date && row.window_end === row.planned_date");
|
||||
expect(source).toContain("return \"不适用\";");
|
||||
});
|
||||
|
||||
it("gates participant subresource mutations by concrete API permissions", () => {
|
||||
const source = readSubjectDetail();
|
||||
|
||||
expect(source).toContain("isApiPermissionAllowed");
|
||||
expect(source).toContain("getProjectRole");
|
||||
expect(source).toContain("isSystemAdmin");
|
||||
[
|
||||
"subjects:update",
|
||||
"subject_histories:create",
|
||||
"subject_histories:update",
|
||||
"subject_histories:delete",
|
||||
"visits:create",
|
||||
"visits:update",
|
||||
"visits:delete",
|
||||
"subject_aes:create",
|
||||
"subject_aes:list",
|
||||
"subject_aes:update",
|
||||
"subject_aes:delete",
|
||||
"subject_pds:create",
|
||||
"subject_pds:list",
|
||||
"subject_pds:update",
|
||||
"subject_pds:delete",
|
||||
].forEach((operationKey) => {
|
||||
expect(source).toContain(operationKey);
|
||||
});
|
||||
expect(source).toContain("v-if=\"currentTabAction.allowed\"");
|
||||
expect(source).toContain("v-if=\"canUpdateHistory || canDeleteHistory\"");
|
||||
expect(source).toContain("v-if=\"canUpdateVisit || canDeleteVisit\"");
|
||||
expect(source).toContain("v-if=\"canUpdateAe || canDeleteAe\"");
|
||||
expect(source).toContain("v-if=\"canUpdatePd || canDeletePd\"");
|
||||
expect(source).toContain("const canSaveHistory = computed(() => (historyEditingId.value ? canUpdateHistory.value : canCreateHistory.value));");
|
||||
expect(source).toContain("const canSaveVisit = computed(() => (visitEditingId.value ? canUpdateVisit.value : canCreateVisit.value));");
|
||||
expect(source).toContain("const canSaveAe = computed(() => (aeEditingId.value ? canUpdateAe.value : canCreateAe.value));");
|
||||
expect(source).toContain("const canSavePd = computed(() => (pdEditingId.value ? canUpdatePd.value : canCreatePd.value));");
|
||||
expect(source).toContain("const canCreateAe = computed(() => canUseApiPermission(\"subject_aes:create\"));");
|
||||
expect(source).toContain("const canUpdateAe = computed(() => canUseApiPermission(\"subject_aes:update\"));");
|
||||
expect(source).toContain("const canDeleteAe = computed(() => canUseApiPermission(\"subject_aes:delete\"));");
|
||||
expect(source).toContain("const canListAe = computed(() => canUseApiPermission(\"subject_aes:list\"));");
|
||||
expect(source).toContain("const canListPd = computed(() => canUseApiPermission(\"subject_pds:list\"));");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
v-if="subjectEditing"
|
||||
type="primary"
|
||||
:loading="subjectSaving"
|
||||
:disabled="isReadOnly"
|
||||
:disabled="isReadOnly || !canUpdateSubject"
|
||||
@click="saveSubjectEdit"
|
||||
>
|
||||
{{ TEXT.common.actions.save }}
|
||||
@@ -18,7 +18,7 @@
|
||||
<el-button v-if="subjectEditing" :disabled="isReadOnly" @click="cancelSubjectEdit">
|
||||
{{ TEXT.common.actions.cancel }}
|
||||
</el-button>
|
||||
<el-button v-else type="primary" :disabled="isReadOnly" @click="startSubjectEdit">
|
||||
<el-button v-else-if="canUpdateSubject" type="primary" :disabled="isReadOnly" @click="startSubjectEdit">
|
||||
{{ TEXT.common.actions.edit }}
|
||||
</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
@@ -91,10 +91,22 @@
|
||||
|
||||
<el-card class="unified-shell subject-shell subject-tabs-shell">
|
||||
<div class="subject-tabs-action">
|
||||
<el-button type="primary" :loading="currentTabAction.loading" :disabled="isReadOnly" @click="currentTabAction.onClick">
|
||||
<el-button
|
||||
v-if="currentTabAction.allowed"
|
||||
type="primary"
|
||||
:loading="currentTabAction.loading"
|
||||
:disabled="isReadOnly"
|
||||
@click="currentTabAction.onClick"
|
||||
>
|
||||
{{ currentTabAction.label }}
|
||||
</el-button>
|
||||
<el-button v-if="activeTab === 'visits'" type="warning" plain :disabled="isReadOnly" @click="openEarlyTerminationDialog">
|
||||
<el-button
|
||||
v-if="activeTab === 'visits' && canCreateVisit"
|
||||
type="warning"
|
||||
plain
|
||||
:disabled="isReadOnly"
|
||||
@click="openEarlyTerminationDialog"
|
||||
>
|
||||
提前终止
|
||||
</el-button>
|
||||
<el-button v-if="adherenceEditing && activeTab === 'medicationAdherence'" :disabled="isReadOnly" @click="adherenceEditing = false">
|
||||
@@ -108,9 +120,10 @@
|
||||
<template #default="scope">{{ displayDate(scope.row.record_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="content" :label="TEXT.common.fields.content" show-overflow-tooltip />
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="140">
|
||||
<el-table-column v-if="canUpdateHistory || canDeleteHistory" :label="TEXT.common.labels.actions" width="140">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="canUpdateHistory"
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@@ -120,6 +133,7 @@
|
||||
{{ TEXT.common.actions.edit }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canDeleteHistory"
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@@ -183,10 +197,10 @@
|
||||
<span v-else>{{ scope.row.notes || TEXT.common.fallback }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="140">
|
||||
<el-table-column v-if="canUpdateVisit || canDeleteVisit" :label="TEXT.common.labels.actions" width="140">
|
||||
<template #default="scope">
|
||||
<template v-if="visitEditingRowId === scope.row.id">
|
||||
<el-button link type="primary" size="small" :disabled="isReadOnly" @click="saveVisitRow(scope.row)">
|
||||
<el-button v-if="canUpdateVisit" link type="primary" size="small" :disabled="isReadOnly" @click="saveVisitRow(scope.row)">
|
||||
{{ TEXT.common.actions.save }}
|
||||
</el-button>
|
||||
<el-button link size="small" :disabled="isReadOnly" @click="cancelVisitRow">
|
||||
@@ -195,6 +209,7 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button
|
||||
v-if="canUpdateVisit"
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@@ -204,6 +219,7 @@
|
||||
{{ TEXT.common.actions.edit }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canDeleteVisit"
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@@ -244,7 +260,7 @@
|
||||
:controls="false"
|
||||
size="small"
|
||||
class="adherence-count-input"
|
||||
:disabled="isReadOnly"
|
||||
:disabled="isReadOnly || !canUpdateSubject"
|
||||
/>
|
||||
<span v-else>{{ actualMedicationCountText }}</span>
|
||||
</el-descriptions-item>
|
||||
@@ -283,9 +299,10 @@
|
||||
<template #default="scope">{{ displayText(scope.row.outcome, TEXT.enums.aeOutcome) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" :label="TEXT.common.fields.remark" show-overflow-tooltip />
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="140">
|
||||
<el-table-column v-if="canUpdateAe || canDeleteAe" :label="TEXT.common.labels.actions" width="140">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="canUpdateAe"
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@@ -295,6 +312,7 @@
|
||||
{{ TEXT.common.actions.edit }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canDeleteAe"
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@@ -324,9 +342,10 @@
|
||||
<el-table-column prop="status" :label="TEXT.common.fields.status">
|
||||
<template #default="scope">{{ displayEnum(TEXT.enums.pdStatus, scope.row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="140">
|
||||
<el-table-column v-if="canUpdatePd || canDeletePd" :label="TEXT.common.labels.actions" width="140">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="canUpdatePd"
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@@ -336,6 +355,7 @@
|
||||
{{ TEXT.common.actions.edit }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canDeletePd"
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@@ -365,7 +385,7 @@
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="historyDialogVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :disabled="isReadOnly" @click="saveHistory">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button type="primary" :disabled="isReadOnly || !canSaveHistory" @click="saveHistory">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -396,7 +416,7 @@
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visitDialogVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :disabled="isReadOnly" @click="saveVisit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button type="primary" :disabled="isReadOnly || !canSaveVisit" @click="saveVisit">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -423,7 +443,7 @@
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="earlyTerminationDialogVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :loading="earlyTerminationSaving" :disabled="isReadOnly" @click="saveEarlyTermination">
|
||||
<el-button type="primary" :loading="earlyTerminationSaving" :disabled="isReadOnly || !canCreateVisit" @click="saveEarlyTermination">
|
||||
{{ TEXT.common.actions.save }}
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -464,7 +484,7 @@
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="aeDialogVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :disabled="isReadOnly" @click="saveAe">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button type="primary" :disabled="isReadOnly || !canSaveAe" @click="saveAe">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -494,7 +514,7 @@
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="pdDialogVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :disabled="isReadOnly" @click="savePd">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button type="primary" :disabled="isReadOnly || !canSavePd" @click="savePd">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -504,6 +524,7 @@
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { getSubject, updateSubject } from "../../api/subjects";
|
||||
import { fetchSites } from "../../api/sites";
|
||||
@@ -512,13 +533,23 @@ import { fetchVisits, createEarlyTerminationVisit, createVisit, updateVisit, del
|
||||
import { fetchAes, createAe, updateAe, deleteAe } from "../../api/aes";
|
||||
import { listSubjectPds, createSubjectPd, updateSubjectPd, deleteSubjectPd } from "../../api/subjectPds";
|
||||
import { displayDate, displayEnum, displayText } from "../../utils/display";
|
||||
import { isApiPermissionAllowed } from "../../utils/apiPermissionValue";
|
||||
import { getProjectRole, isSystemAdmin } from "../../utils/roles";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
const study = useStudyStore();
|
||||
const subjectId = route.params.subjectId as string;
|
||||
const studyId = study.currentStudy?.id || "";
|
||||
const projectRole = computed(() => getProjectRole(study.currentStudy, study.currentStudyRole));
|
||||
const canUseApiPermission = (operationKey: string) => {
|
||||
if (isSystemAdmin(auth.user)) return true;
|
||||
const role = projectRole.value;
|
||||
if (!role) return false;
|
||||
return isApiPermissionAllowed(study.currentPermissions?.[role]?.[operationKey]);
|
||||
};
|
||||
const resolveTabFromQuery = () => {
|
||||
const tab = typeof route.query.tab === "string" ? route.query.tab : "";
|
||||
return ["history", "visits", "medicationAdherence", "ae", "pd"].includes(tab) ? tab : "history";
|
||||
@@ -552,12 +583,17 @@ const subjectForm = reactive({
|
||||
const siteMap = ref<Record<string, string>>({});
|
||||
const siteActiveMap = ref<Record<string, boolean>>({});
|
||||
const isReadOnly = computed(() => !!detail.site_id && siteActiveMap.value[detail.site_id] === false);
|
||||
const canUpdateSubject = computed(() => canUseApiPermission("subjects:update"));
|
||||
const activeTab = ref(resolveTabFromQuery());
|
||||
|
||||
const historyItems = ref<any[]>([]);
|
||||
const loadingHistory = ref(false);
|
||||
const historyDialogVisible = ref(false);
|
||||
const historyEditingId = ref<string | null>(null);
|
||||
const canCreateHistory = computed(() => canUseApiPermission("subject_histories:create"));
|
||||
const canUpdateHistory = computed(() => canUseApiPermission("subject_histories:update"));
|
||||
const canDeleteHistory = computed(() => canUseApiPermission("subject_histories:delete"));
|
||||
const canSaveHistory = computed(() => (historyEditingId.value ? canUpdateHistory.value : canCreateHistory.value));
|
||||
const historyForm = reactive({
|
||||
record_date: "",
|
||||
content: "",
|
||||
@@ -570,6 +606,10 @@ const earlyTerminationDialogVisible = ref(false);
|
||||
const earlyTerminationSaving = ref(false);
|
||||
const visitEditingId = ref<string | null>(null);
|
||||
const visitEditingRowId = ref<string | null>(null);
|
||||
const canCreateVisit = computed(() => canUseApiPermission("visits:create"));
|
||||
const canUpdateVisit = computed(() => canUseApiPermission("visits:update"));
|
||||
const canDeleteVisit = computed(() => canUseApiPermission("visits:delete"));
|
||||
const canSaveVisit = computed(() => (visitEditingId.value ? canUpdateVisit.value : canCreateVisit.value));
|
||||
const visitRowDraft = reactive({
|
||||
actual_date: "",
|
||||
notes: "",
|
||||
@@ -590,6 +630,7 @@ const earlyTerminationForm = reactive({
|
||||
const isFirstVisit = computed(() => visitForm.visit_code === "V1");
|
||||
const adherenceEditing = ref(false);
|
||||
const adherenceSaving = ref(false);
|
||||
const canEditAdherence = computed(() => canUpdateSubject.value);
|
||||
const adherenceForm = reactive({
|
||||
actual_medication_count: null as number | null,
|
||||
});
|
||||
@@ -598,6 +639,11 @@ const aeItems = ref<any[]>([]);
|
||||
const loadingAes = ref(false);
|
||||
const aeDialogVisible = ref(false);
|
||||
const aeEditingId = ref<string | null>(null);
|
||||
const canListAe = computed(() => canUseApiPermission("subject_aes:list"));
|
||||
const canCreateAe = computed(() => canUseApiPermission("subject_aes:create"));
|
||||
const canUpdateAe = computed(() => canUseApiPermission("subject_aes:update"));
|
||||
const canDeleteAe = computed(() => canUseApiPermission("subject_aes:delete"));
|
||||
const canSaveAe = computed(() => (aeEditingId.value ? canUpdateAe.value : canCreateAe.value));
|
||||
const aeCausalityOptions = [
|
||||
{ value: "肯定有关", label: TEXT.enums.aeCausality["肯定有关"] },
|
||||
{ value: "很可能有关", label: TEXT.enums.aeCausality["很可能有关"] },
|
||||
@@ -636,6 +682,11 @@ const pdItems = ref<any[]>([]);
|
||||
const loadingPds = ref(false);
|
||||
const pdDialogVisible = ref(false);
|
||||
const pdEditingId = ref<string | null>(null);
|
||||
const canListPd = computed(() => canUseApiPermission("subject_pds:list"));
|
||||
const canCreatePd = computed(() => canUseApiPermission("subject_pds:create"));
|
||||
const canUpdatePd = computed(() => canUseApiPermission("subject_pds:update"));
|
||||
const canDeletePd = computed(() => canUseApiPermission("subject_pds:delete"));
|
||||
const canSavePd = computed(() => (pdEditingId.value ? canUpdatePd.value : canCreatePd.value));
|
||||
const pdTypeOptions = ["知情同意", "排除标准", "药物使用", "入排标准", "访视流程", "检验检查", "其他"];
|
||||
const pdLevelOptions = Object.entries(TEXT.enums.pdLevel).map(([value, label]) => ({ value, label }));
|
||||
const pdStatusOptions = Object.entries(TEXT.enums.pdStatus).map(([value, label]) => ({ value, label }));
|
||||
@@ -717,6 +768,10 @@ const startSubjectEdit = () => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!canUpdateSubject.value) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
subjectForm.screening_date = detail.screening_date || "";
|
||||
subjectForm.consent_date = detail.consent_date || "";
|
||||
subjectForm.enrollment_date = detail.enrollment_date || "";
|
||||
@@ -735,6 +790,10 @@ const saveSubjectEdit = async () => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!canUpdateSubject.value) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId) return;
|
||||
subjectSaving.value = true;
|
||||
try {
|
||||
@@ -776,6 +835,11 @@ const openHistoryDialog = (row?: any) => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
const allowed = row?.id ? canUpdateHistory.value : canCreateHistory.value;
|
||||
if (!allowed) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
historyEditingId.value = row?.id || null;
|
||||
historyForm.record_date = row?.record_date || "";
|
||||
historyForm.content = row?.content || "";
|
||||
@@ -787,6 +851,10 @@ const saveHistory = async () => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!canSaveHistory.value) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId) return;
|
||||
try {
|
||||
const payload = {
|
||||
@@ -811,6 +879,10 @@ const removeHistory = async (row: any) => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!canDeleteHistory.value) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId) return;
|
||||
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
@@ -856,6 +928,11 @@ const openVisitDialog = (row?: any) => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
const allowed = row?.id ? canUpdateVisit.value : canCreateVisit.value;
|
||||
if (!allowed) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
visitEditingId.value = row?.id || null;
|
||||
if (row) {
|
||||
visitForm.actual_date = row.actual_date || "";
|
||||
@@ -876,6 +953,10 @@ const openEarlyTerminationDialog = () => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!canCreateVisit.value) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
earlyTerminationForm.termination_date = "";
|
||||
earlyTerminationForm.reason = "";
|
||||
earlyTerminationForm.notes = "";
|
||||
@@ -887,6 +968,10 @@ const startVisitRowEdit = (row: any) => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!canUpdateVisit.value) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
visitEditingRowId.value = row?.id || null;
|
||||
visitRowDraft.actual_date = row?.actual_date || "";
|
||||
visitRowDraft.notes = row?.notes || "";
|
||||
@@ -901,6 +986,10 @@ const saveVisitRow = async (row: any) => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!canUpdateVisit.value) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId || !row?.id) return;
|
||||
try {
|
||||
const payload = {
|
||||
@@ -1072,6 +1161,10 @@ const saveVisit = async () => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!canSaveVisit.value) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId) return;
|
||||
try {
|
||||
if (visitEditingId.value) {
|
||||
@@ -1106,6 +1199,10 @@ const saveEarlyTermination = async () => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!canCreateVisit.value) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId) return;
|
||||
if (!earlyTerminationForm.termination_date) {
|
||||
ElMessage.error("请填写提前终止日期");
|
||||
@@ -1146,6 +1243,10 @@ const removeVisit = async (row: any) => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!canDeleteVisit.value) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId) return;
|
||||
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
@@ -1159,6 +1260,10 @@ const removeVisit = async (row: any) => {
|
||||
|
||||
const loadAes = async () => {
|
||||
if (!studyId || !subjectId) return;
|
||||
if (!canListAe.value) {
|
||||
aeItems.value = [];
|
||||
return;
|
||||
}
|
||||
loadingAes.value = true;
|
||||
try {
|
||||
const { data } = (await fetchAes(studyId, { subject_id: subjectId })) as any;
|
||||
@@ -1175,6 +1280,11 @@ const openAeDialog = (row?: any) => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
const allowed = row?.id ? canUpdateAe.value : canCreateAe.value;
|
||||
if (!allowed) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
aeEditingId.value = row?.id || null;
|
||||
Object.assign(aeForm, {
|
||||
term: row?.term || "",
|
||||
@@ -1194,6 +1304,10 @@ const saveAe = async () => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!canSaveAe.value) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId) return;
|
||||
try {
|
||||
const payload = {
|
||||
@@ -1225,6 +1339,10 @@ const removeAe = async (row: any) => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!canDeleteAe.value) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
if (!studyId) return;
|
||||
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
@@ -1238,6 +1356,10 @@ const removeAe = async (row: any) => {
|
||||
|
||||
const loadPds = async () => {
|
||||
if (!studyId || !subjectId) return;
|
||||
if (!canListPd.value) {
|
||||
pdItems.value = [];
|
||||
return;
|
||||
}
|
||||
loadingPds.value = true;
|
||||
try {
|
||||
const { data } = (await listSubjectPds(studyId, subjectId)) as any;
|
||||
@@ -1262,6 +1384,11 @@ const openPdDialog = (row?: any) => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
const allowed = row?.id ? canUpdatePd.value : canCreatePd.value;
|
||||
if (!allowed) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
pdEditingId.value = row?.id || null;
|
||||
Object.assign(pdForm, {
|
||||
pd_no: row?.pd_no || "",
|
||||
@@ -1276,19 +1403,19 @@ const openPdDialog = (row?: any) => {
|
||||
const currentTabAction = computed(() => {
|
||||
switch (activeTab.value) {
|
||||
case "visits":
|
||||
return { label: TEXT.common.actions.newVisit, onClick: () => openVisitDialog() };
|
||||
return { label: TEXT.common.actions.newVisit, loading: false, allowed: canCreateVisit.value, onClick: () => openVisitDialog() };
|
||||
case "medicationAdherence":
|
||||
if (adherenceEditing.value) {
|
||||
return { label: "保存用药次数", loading: adherenceSaving.value, onClick: () => saveMedicationAdherence() };
|
||||
return { label: "保存用药次数", loading: adherenceSaving.value, allowed: canEditAdherence.value, onClick: () => saveMedicationAdherence() };
|
||||
}
|
||||
return { label: "编辑", loading: false, onClick: () => startMedicationAdherenceEdit() };
|
||||
return { label: "编辑", loading: false, allowed: canEditAdherence.value, onClick: () => startMedicationAdherenceEdit() };
|
||||
case "ae":
|
||||
return { label: TEXT.common.actions.newAe, onClick: () => openAeDialog() };
|
||||
return { label: TEXT.common.actions.newAe, loading: false, allowed: canCreateAe.value, onClick: () => openAeDialog() };
|
||||
case "pd":
|
||||
return { label: TEXT.modules.subjectDetail.newPd, onClick: () => openPdDialog() };
|
||||
return { label: TEXT.modules.subjectDetail.newPd, loading: false, allowed: canCreatePd.value, onClick: () => openPdDialog() };
|
||||
case "history":
|
||||
default:
|
||||
return { label: TEXT.common.actions.newHistory, onClick: () => openHistoryDialog() };
|
||||
return { label: TEXT.common.actions.newHistory, loading: false, allowed: canCreateHistory.value, onClick: () => openHistoryDialog() };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1297,6 +1424,10 @@ const startMedicationAdherenceEdit = () => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!canEditAdherence.value) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
adherenceForm.actual_medication_count = detail.actual_medication_count ?? null;
|
||||
adherenceEditing.value = true;
|
||||
};
|
||||
@@ -1306,6 +1437,10 @@ const saveMedicationAdherence = async () => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!canEditAdherence.value) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId) return;
|
||||
adherenceSaving.value = true;
|
||||
try {
|
||||
@@ -1327,6 +1462,10 @@ const savePd = async () => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!canSavePd.value) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId) return;
|
||||
if (!pdForm.pd_type?.trim()) {
|
||||
ElMessage.error(TEXT.common.messages.required);
|
||||
@@ -1356,6 +1495,10 @@ const removePd = async (row: any) => {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!canDeletePd.value) {
|
||||
ElMessage.warning(TEXT.common.messages.noPermission);
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId) return;
|
||||
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
|
||||
Reference in New Issue
Block a user