未知(继上次中断)

This commit is contained in:
Cheng Zhou
2026-02-04 10:52:34 +08:00
parent 8e258d21a7
commit 737f84bf54
99 changed files with 5497 additions and 2143 deletions
+121 -46
View File
@@ -1,27 +1,9 @@
<template>
<div class="page">
<div class="page-header">
<div>
<h1 class="page-title">{{ TEXT.modules.subjectDetail.title }}</h1>
<p class="page-subtitle">{{ TEXT.modules.subjectDetail.subtitle }}</p>
</div>
<div class="actions">
<template v-if="subjectEditing">
<el-button type="primary" :loading="subjectSaving" @click="saveSubjectEdit">{{ TEXT.common.actions.save }}</el-button>
<el-button @click="cancelSubjectEdit">{{ TEXT.common.actions.cancel }}</el-button>
</template>
<template v-else>
<el-button type="primary" :disabled="isReadOnly" @click="startSubjectEdit">
{{ TEXT.common.actions.edit }}
</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</template>
</div>
</div>
<el-card v-loading="loading">
<el-descriptions :column="2" border>
<el-descriptions-item :label="TEXT.common.fields.subjectNo">{{ detail.subject_no || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.modules.subjectManagement.screeningNo">{{ detail.subject_no || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.site">{{ siteMap[detail.site_id] || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.status">
<el-select v-if="subjectEditing" v-model="subjectForm.status" size="small">
@@ -202,10 +184,23 @@
</el-table-column>
<el-table-column prop="term" :label="TEXT.common.fields.title" min-width="140" />
<el-table-column prop="seriousness" :label="TEXT.common.fields.seriousness" width="120">
<template #default="scope">{{ displayEnum(TEXT.enums.aeSeriousness, scope.row.seriousness) }}</template>
<template #default="scope">
<el-tag
:type="getAeSeriousnessType(normalizeSeriousness(scope.row.seriousness))"
size="small"
effect="light"
class="seriousness-tag"
>
{{ displayEnum(TEXT.enums.aeSeriousness, normalizeSeriousness(scope.row.seriousness)) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="causality" :label="TEXT.common.fields.causality" width="120">
<template #default="scope">{{ displayText(scope.row.causality, TEXT.enums.aeCausality) }}</template>
</el-table-column>
<el-table-column prop="outcome" :label="TEXT.common.fields.outcome" width="120">
<template #default="scope">{{ displayText(scope.row.outcome, TEXT.enums.aeOutcome) }}</template>
</el-table-column>
<el-table-column prop="causality" :label="TEXT.common.fields.causality" width="120" />
<el-table-column prop="outcome" :label="TEXT.common.fields.outcome" width="120" />
<el-table-column prop="description" :label="TEXT.common.fields.remark" min-width="200" />
<el-table-column :label="TEXT.common.labels.actions" width="200">
<template #default="scope">
@@ -254,9 +249,9 @@
<el-form :model="visitForm" label-width="100px">
<template v-if="!visitEditingId">
<el-form-item :label="TEXT.common.fields.visitCode" required>
<el-input v-model="visitForm.visit_code" />
<el-input v-model="visitForm.visit_code" disabled />
</el-form-item>
<el-form-item :label="TEXT.common.fields.plannedDate">
<el-form-item :label="TEXT.common.fields.plannedDate" :required="isFirstVisit">
<el-date-picker v-model="visitForm.planned_date" type="date" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.windowStart">
@@ -291,22 +286,18 @@
</el-form-item>
<el-form-item :label="TEXT.common.fields.seriousness" required>
<el-select v-model="aeForm.seriousness">
<el-option :label="TEXT.enums.aeSeriousness.SERIOUS" value="SERIOUS" />
<el-option :label="TEXT.enums.aeSeriousness.NON_SERIOUS" value="NON_SERIOUS" />
</el-select>
</el-form-item>
<el-form-item :label="TEXT.common.fields.severity" required>
<el-select v-model="aeForm.severity">
<el-option :label="TEXT.enums.aeSeverity.MILD" value="MILD" />
<el-option :label="TEXT.enums.aeSeverity.MODERATE" value="MODERATE" />
<el-option :label="TEXT.enums.aeSeverity.SEVERE" value="SEVERE" />
<el-option v-for="option in aeSeriousnessOptions" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
<el-form-item :label="TEXT.common.fields.causality">
<el-input v-model="aeForm.causality" />
<el-select v-model="aeForm.causality" clearable>
<el-option v-for="option in aeCausalityOptions" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
<el-form-item :label="TEXT.common.fields.outcome">
<el-input v-model="aeForm.outcome" />
<el-select v-model="aeForm.outcome" clearable>
<el-option v-for="option in aeOutcomeOptions" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
<el-form-item :label="TEXT.common.fields.remark">
<el-input v-model="aeForm.description" type="textarea" :rows="3" />
@@ -330,7 +321,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 { displayDate, displayEnum } from "../../utils/display";
import { displayDate, displayEnum, displayText } from "../../utils/display";
import StateEmpty from "../../components/StateEmpty.vue";
import { TEXT } from "../../locales";
@@ -393,20 +384,62 @@ const visitForm = reactive<any>({
actual_date: "",
notes: "",
});
const isFirstVisit = computed(() => visitForm.visit_code === "V1");
const aeItems = ref<any[]>([]);
const loadingAes = ref(false);
const aeDialogVisible = ref(false);
const aeEditingId = ref<string | null>(null);
const aeCausalityOptions = [
{ value: "肯定有关", label: TEXT.enums.aeCausality["肯定有关"] },
{ value: "很可能有关", label: TEXT.enums.aeCausality["很可能有关"] },
{ value: "可能有关", label: TEXT.enums.aeCausality["可能有关"] },
{ value: "可能无关", label: TEXT.enums.aeCausality["可能无关"] },
{ value: "无关", label: TEXT.enums.aeCausality["无关"] },
{ value: "待评估", label: TEXT.enums.aeCausality["待评估"] },
];
const aeOutcomeOptions = [
{ value: "痊愈", label: TEXT.enums.aeOutcome["痊愈"] },
{ value: "好转", label: TEXT.enums.aeOutcome["好转"] },
{ value: "持续", label: TEXT.enums.aeOutcome["持续"] },
{ value: "恶化", label: TEXT.enums.aeOutcome["恶化"] },
{ value: "死亡", label: TEXT.enums.aeOutcome["死亡"] },
{ value: "失访", label: TEXT.enums.aeOutcome["失访"] },
];
const aeForm = reactive({
term: "",
onset_date: "",
seriousness: "NON_SERIOUS",
severity: "MILD",
seriousness: "I",
causality: "",
outcome: "",
description: "",
});
const aeSeriousnessOptions = [
{ value: "I", label: TEXT.enums.aeSeriousness.I },
{ value: "II", label: TEXT.enums.aeSeriousness.II },
{ value: "III", label: TEXT.enums.aeSeriousness.III },
{ value: "IV", label: TEXT.enums.aeSeriousness.IV },
{ value: "V", label: TEXT.enums.aeSeriousness.V },
];
const normalizeSeriousness = (value?: string | null) => {
if (!value) return "I";
const normalized = String(value).trim().toUpperCase();
const digitMap: Record<string, string> = {
"1": "I",
"2": "II",
"3": "III",
"4": "IV",
"5": "V",
};
const legacyMap: Record<string, string> = {
SERIOUS: "IV",
NON_SERIOUS: "II",
};
if (digitMap[normalized]) return digitMap[normalized];
if (legacyMap[normalized]) return legacyMap[normalized];
if (["I", "II", "III", "IV", "V"].includes(normalized)) return normalized;
return "I";
};
const loadSites = async () => {
if (!studyId) return;
@@ -487,7 +520,7 @@ const loadHistories = async () => {
if (!studyId || !subjectId) return;
loadingHistory.value = true;
try {
const { data } = await listSubjectHistories(studyId, subjectId);
const { data } = (await listSubjectHistories(studyId, subjectId)) as any;
historyItems.value = Array.isArray(data) ? data : data.items || [];
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
@@ -551,8 +584,17 @@ const loadVisits = async () => {
if (!studyId || !subjectId) return;
loadingVisits.value = true;
try {
const { data } = await fetchVisits(studyId, subjectId);
visitItems.value = Array.isArray(data) ? data : data.items || [];
const { data } = (await fetchVisits(studyId, subjectId)) as any;
const items = Array.isArray(data) ? data : data.items || [];
visitItems.value = items.sort((a: any, b: any) => {
const getNum = (code: any) => {
const text = String(code || "");
if (!text.startsWith("V")) return Number.POSITIVE_INFINITY;
const num = Number(text.slice(1));
return Number.isFinite(num) ? num : Number.POSITIVE_INFINITY;
};
return getNum(a?.visit_code) - getNum(b?.visit_code);
});
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
} finally {
@@ -560,6 +602,21 @@ const loadVisits = async () => {
}
};
const getNextVisitCode = () => {
const used = new Set<number>();
visitItems.value.forEach((item) => {
const code = String(item?.visit_code || "");
if (!code.startsWith("V")) return;
const num = Number(code.slice(1));
if (Number.isFinite(num) && num > 0) {
used.add(num);
}
});
let next = 1;
while (used.has(next)) next += 1;
return `V${next}`;
};
const openVisitDialog = (row?: any) => {
if (isReadOnly.value) {
ElMessage.warning("中心已停用");
@@ -570,7 +627,7 @@ const openVisitDialog = (row?: any) => {
visitForm.actual_date = row.actual_date || "";
visitForm.notes = row.notes || "";
} else {
visitForm.visit_code = "";
visitForm.visit_code = getNextVisitCode();
visitForm.planned_date = "";
visitForm.window_start = "";
visitForm.window_end = "";
@@ -657,6 +714,22 @@ const getVisitStatusTagType = (status: string) => {
}
};
const getAeSeriousnessType = (seriousness: string) => {
switch (seriousness) {
case "I":
return "info";
case "II":
return "success";
case "III":
return "warning";
case "IV":
case "V":
return "danger";
default:
return "info";
}
};
const saveVisit = async () => {
if (isReadOnly.value) {
ElMessage.warning("中心已停用");
@@ -671,6 +744,10 @@ const saveVisit = async () => {
};
await updateVisit(studyId, subjectId, visitEditingId.value, payload);
} else {
if (visitForm.visit_code === "V1" && !visitForm.planned_date) {
ElMessage.error("首次新增访视必须填写计划访视日期");
return;
}
const payload = {
subject_id: subjectId,
visit_code: visitForm.visit_code,
@@ -707,7 +784,7 @@ const loadAes = async () => {
if (!studyId || !subjectId) return;
loadingAes.value = true;
try {
const { data } = await fetchAes(studyId, { subject_id: subjectId });
const { data } = (await fetchAes(studyId, { subject_id: subjectId })) as any;
aeItems.value = Array.isArray(data) ? data : data.items || [];
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
@@ -725,8 +802,7 @@ const openAeDialog = (row?: any) => {
Object.assign(aeForm, {
term: row?.term || "",
onset_date: row?.onset_date || "",
seriousness: row?.seriousness || "NON_SERIOUS",
severity: row?.severity || "MILD",
seriousness: normalizeSeriousness(row?.seriousness),
causality: row?.causality || "",
outcome: row?.outcome || "",
description: row?.description || "",
@@ -747,7 +823,6 @@ const saveAe = async () => {
term: aeForm.term,
onset_date: aeForm.onset_date || null,
seriousness: aeForm.seriousness,
severity: aeForm.severity,
causality: aeForm.causality || null,
outcome: aeForm.outcome || null,
description: aeForm.description || null,