参与者管理- PD模块内容补充

This commit is contained in:
Cheng Zhou
2026-02-28 14:33:57 +08:00
parent af0c1b4b7d
commit 9b36a42e6f
14 changed files with 855 additions and 35 deletions
+171 -1
View File
@@ -253,7 +253,47 @@
</el-tab-pane>
<el-tab-pane :label="TEXT.modules.subjectDetail.tabs.pd" name="pd">
<StateEmpty :description="TEXT.modules.subjectDetail.emptyPd" />
<div class="tab-actions">
<el-button type="primary" :disabled="isReadOnly" @click="openPdDialog()">
{{ TEXT.modules.subjectDetail.newPd }}
</el-button>
</div>
<el-table :data="pdItems" v-loading="loadingPds" style="width: 100%">
<el-table-column prop="pd_no" :label="TEXT.common.fields.pdNo" width="130" />
<el-table-column prop="pd_type" :label="TEXT.common.fields.pdType" min-width="140" />
<el-table-column prop="pd_level" :label="TEXT.common.fields.pdLevel" min-width="120">
<template #default="scope">{{ displayEnum(TEXT.enums.pdLevel, scope.row.pd_level) }}</template>
</el-table-column>
<el-table-column prop="description" :label="TEXT.common.fields.pdDescription" min-width="240">
<template #default="scope">{{ scope.row.description || TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column prop="status" :label="TEXT.common.fields.status" min-width="100">
<template #default="scope">{{ displayEnum(TEXT.enums.pdStatus, scope.row.status) }}</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="150">
<template #default="scope">
<el-button
link
type="primary"
size="small"
:disabled="isReadOnly"
@click="openPdDialog(scope.row)"
>
{{ TEXT.common.actions.edit }}
</el-button>
<el-button
link
type="danger"
size="small"
:disabled="isReadOnly"
@click="removePd(scope.row)"
>
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</el-table-column>
</el-table>
<StateEmpty v-if="!loadingPds && pdItems.length === 0" :description="TEXT.modules.subjectDetail.emptyPd" />
</el-tab-pane>
</el-tabs>
</el-card>
@@ -342,6 +382,36 @@
<el-button type="primary" :disabled="isReadOnly" @click="saveAe">{{ TEXT.common.actions.save }}</el-button>
</template>
</el-dialog>
<el-dialog append-to=".layout-main .content-wrapper" v-model="pdDialogVisible" :title="TEXT.modules.subjectDetail.dialogPd" width="560px">
<el-form :model="pdForm" label-width="100px">
<el-form-item :label="TEXT.common.fields.pdNo">
<el-input v-model="pdForm.pd_no" disabled :placeholder="TEXT.modules.subjectDetail.pdNoAuto" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.pdType" required>
<el-select v-model="pdForm.pd_type" filterable allow-create default-first-option clearable>
<el-option v-for="item in pdTypeOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item :label="TEXT.common.fields.pdLevel" required>
<el-select v-model="pdForm.pd_level">
<el-option v-for="option in pdLevelOptions" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
<el-form-item :label="TEXT.common.fields.status" required>
<el-select v-model="pdForm.status">
<el-option v-for="option in pdStatusOptions" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
<el-form-item :label="TEXT.common.fields.pdDescription">
<el-input v-model="pdForm.description" type="textarea" :rows="3" />
</el-form-item>
</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>
</template>
</el-dialog>
</div>
</template>
@@ -355,6 +425,7 @@ import { fetchSites } from "../../api/sites";
import { listSubjectHistories, createSubjectHistory, updateSubjectHistory, deleteSubjectHistory } from "../../api/subjectHistories";
import { fetchVisits, createVisit, updateVisit, deleteVisit } from "../../api/visits";
import { fetchAes, createAe, updateAe, deleteAe } from "../../api/aes";
import { listSubjectPds, createSubjectPd, updateSubjectPd, deleteSubjectPd } from "../../api/subjectPds";
import { displayDate, displayEnum, displayText } from "../../utils/display";
import StateEmpty from "../../components/StateEmpty.vue";
import { TEXT } from "../../locales";
@@ -461,6 +532,22 @@ const aeSeriousnessOptions = [
{ value: "IV", label: TEXT.enums.aeSeriousness.IV },
{ value: "V", label: TEXT.enums.aeSeriousness.V },
];
const pdItems = ref<any[]>([]);
const loadingPds = ref(false);
const pdDialogVisible = ref(false);
const pdEditingId = ref<string | null>(null);
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 }));
const pdForm = reactive({
pd_no: "",
pd_type: "",
pd_level: "GENERAL",
description: "",
status: "OPEN",
});
const normalizeSeriousness = (value?: string | null) => {
if (!value) return "I";
const normalized = String(value).trim().toUpperCase();
@@ -905,6 +992,88 @@ const removeAe = async (row: any) => {
}
};
const loadPds = async () => {
if (!studyId || !subjectId) return;
loadingPds.value = true;
try {
const { data } = (await listSubjectPds(studyId, subjectId)) as any;
const list = Array.isArray(data) ? data : data.items || [];
const getPdSeq = (pdNo: string) => {
const text = String(pdNo || "");
const matched = text.match(/^PD(\d+)$/i);
if (!matched) return Number.POSITIVE_INFINITY;
const num = Number(matched[1]);
return Number.isFinite(num) ? num : Number.POSITIVE_INFINITY;
};
pdItems.value = list.sort((a: any, b: any) => getPdSeq(a?.pd_no) - getPdSeq(b?.pd_no));
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
} finally {
loadingPds.value = false;
}
};
const openPdDialog = (row?: any) => {
if (isReadOnly.value) {
ElMessage.warning("中心已停用");
return;
}
pdEditingId.value = row?.id || null;
Object.assign(pdForm, {
pd_no: row?.pd_no || "",
pd_type: row?.pd_type || "",
pd_level: row?.pd_level || "GENERAL",
description: row?.description || "",
status: row?.status || "OPEN",
});
pdDialogVisible.value = true;
};
const savePd = async () => {
if (isReadOnly.value) {
ElMessage.warning("中心已停用");
return;
}
if (!studyId || !subjectId) return;
if (!pdForm.pd_type?.trim()) {
ElMessage.error(TEXT.common.messages.required);
return;
}
try {
const basePayload = {
pd_type: pdForm.pd_type.trim(),
pd_level: pdForm.pd_level,
description: pdForm.description?.trim() || null,
status: pdForm.status,
};
if (pdEditingId.value) {
await updateSubjectPd(studyId, subjectId, pdEditingId.value, basePayload);
} else {
await createSubjectPd(studyId, subjectId, { subject_id: subjectId, ...basePayload });
}
pdDialogVisible.value = false;
loadPds();
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
}
};
const removePd = async (row: any) => {
if (isReadOnly.value) {
ElMessage.warning("中心已停用");
return;
}
if (!studyId || !subjectId) return;
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
if (!ok) return;
try {
await deleteSubjectPd(studyId, subjectId, row.id);
loadPds();
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
}
};
const goBack = () => router.push("/subjects");
watch(
@@ -920,6 +1089,7 @@ onMounted(async () => {
loadHistories();
loadVisits();
loadAes();
loadPds();
});
</script>