未知(继上次中断)
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user