feat: refine subject visits and project workflows
Add early termination visit workflow with ordering, non-applicable visit handling, visit window display, and medication adherence support. Extend monitoring visit issue template fields, site scoping, setup draft project info handling, login security UI, attachment behavior, and related tests/migrations.
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSubjectDetail = () => readFileSync(resolve(__dirname, "./SubjectDetail.vue"), "utf8");
|
||||
|
||||
describe("SubjectDetail medication adherence", () => {
|
||||
it("adds a medication adherence tab with editable actual count", () => {
|
||||
const source = readSubjectDetail();
|
||||
|
||||
expect(source).toContain('name="medicationAdherence"');
|
||||
expect(source).toContain("actual_medication_count");
|
||||
expect(source).toContain("保存用药次数");
|
||||
expect(source).toContain("实际用药次数为试验期间实际药物暴露次数");
|
||||
expect(source).toContain("75%~125% 范围内(包含边界值)");
|
||||
});
|
||||
|
||||
it("uses baseline and termination visit actual dates for adherence calculation", () => {
|
||||
const source = readSubjectDetail();
|
||||
|
||||
expect(source).toContain("const medicationStartDate = computed(() => detail.baseline_date || \"\");");
|
||||
expect(source).toContain('new Set(["终止治疗", "提前终止"])');
|
||||
expect(source).toContain("return Math.floor((end.getTime() - start.getTime()) / 86400000) + 1;");
|
||||
});
|
||||
|
||||
it("marks adherence outside 75 to 125 percent as abnormal", () => {
|
||||
const source = readSubjectDetail();
|
||||
|
||||
expect(source).toContain("if (rate < 75 || rate > 125) return \"danger\";");
|
||||
expect(source).not.toContain("if (rate < 80 || rate > 120) return \"danger\";");
|
||||
});
|
||||
|
||||
it("provides a dedicated early termination workflow", () => {
|
||||
const source = readSubjectDetail();
|
||||
|
||||
expect(source).toContain("openEarlyTerminationDialog");
|
||||
expect(source).toContain("createEarlyTerminationVisit");
|
||||
expect(source).toContain("return \"不适用\";");
|
||||
expect(source).toContain("getVisitStatusLabel");
|
||||
expect(source).toContain("ElMessage.success(TEXT.common.messages.saveSuccess);");
|
||||
expect(source).toContain("提前终止已保存,刷新数据失败,请手动刷新页面");
|
||||
expect(source).toContain("提前终止表示受试者在方案最后一个计划访视窗口期前停止治疗");
|
||||
expect(source).toContain("validateEarlyTerminationDateInput");
|
||||
expect(source).toContain("提前终止日期必须早于最后计划访视窗口期开始日");
|
||||
expect(source).toContain("getVisitPlannedDateLabel");
|
||||
expect(source).toContain("getVisitWindowRangeLabel");
|
||||
expect(source).toContain("~");
|
||||
expect(source).toContain("row.window_start === row.planned_date && row.window_end === row.planned_date");
|
||||
expect(source).toContain("return \"不适用\";");
|
||||
});
|
||||
});
|
||||
@@ -88,9 +88,15 @@
|
||||
|
||||
<el-card class="unified-shell subject-shell subject-tabs-shell">
|
||||
<div class="subject-tabs-action">
|
||||
<el-button type="primary" :disabled="isReadOnly" @click="currentTabAction.onClick">
|
||||
<el-button 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>
|
||||
<el-button v-if="adherenceEditing && activeTab === 'medicationAdherence'" :disabled="isReadOnly" @click="adherenceEditing = false">
|
||||
{{ TEXT.common.actions.cancel }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane :label="TEXT.modules.subjectDetail.tabs.history" name="history">
|
||||
@@ -131,7 +137,14 @@
|
||||
<el-table :data="visitItems" v-loading="loadingVisits" style="width: 100%" class="subject-detail-table" table-layout="fixed">
|
||||
<el-table-column prop="visit_code" :label="TEXT.common.fields.visitCode" show-overflow-tooltip />
|
||||
<el-table-column prop="planned_date" :label="TEXT.common.fields.plannedDate" show-overflow-tooltip>
|
||||
<template #default="scope">{{ displayDate(scope.row.planned_date) }}</template>
|
||||
<template #default="scope">
|
||||
<div class="visit-planned-cell">
|
||||
<span>{{ getVisitPlannedDateLabel(scope.row) }}</span>
|
||||
<span v-if="getVisitWindowRangeLabel(scope.row)" class="visit-window-range">
|
||||
{{ getVisitWindowRangeLabel(scope.row) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="actual_date" :label="TEXT.common.fields.actualDate" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
@@ -151,7 +164,7 @@
|
||||
:type="getVisitStatusTagType(getVisitStatus(scope.row, visitEditingRowId === scope.row.id ? visitRowDraft.actual_date : undefined))"
|
||||
effect="light"
|
||||
>
|
||||
{{ displayEnum(TEXT.enums.visitStatus, getVisitStatus(scope.row, visitEditingRowId === scope.row.id ? visitRowDraft.actual_date : undefined)) }}
|
||||
{{ getVisitStatusLabel(scope.row, visitEditingRowId === scope.row.id ? visitRowDraft.actual_date : undefined) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -205,6 +218,43 @@
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="用药依从性" name="medicationAdherence">
|
||||
<div class="adherence-panel">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="开始用药日期">
|
||||
{{ displayDate(medicationStartDate) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="结束用药日期">
|
||||
{{ displayDate(medicationEndDate) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="结束日期来源">
|
||||
{{ medicationEndVisitLabel }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="理论用药次数">
|
||||
{{ expectedMedicationCountText }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="实际用药次数">
|
||||
<el-input-number
|
||||
v-if="adherenceEditing"
|
||||
v-model="adherenceForm.actual_medication_count"
|
||||
:min="0"
|
||||
:controls="false"
|
||||
size="small"
|
||||
class="adherence-count-input"
|
||||
:disabled="isReadOnly"
|
||||
/>
|
||||
<span v-else>{{ actualMedicationCountText }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="用药依从性">
|
||||
<el-tag :type="adherenceRateTagType" effect="light">{{ medicationAdherenceRateText }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="adherence-hint">
|
||||
用药依从性 = 实际用药次数 ÷ 理论用药次数 × 100%。实际用药次数为试验期间实际药物暴露次数;理论用药次数 =(结束用药日期 - 开始用药日期 + 1)× 1次/天。试验期间的用药依从性原则上要求在 75%~125% 范围内(包含边界值)。
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="TEXT.modules.subjectDetail.tabs.ae" name="ae">
|
||||
<el-table :data="aeItems" v-loading="loadingAes" style="width: 100%" class="subject-detail-table" table-layout="fixed">
|
||||
<el-table-column prop="onset_date" :label="TEXT.common.fields.occurDate" show-overflow-tooltip>
|
||||
@@ -347,6 +397,35 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-if="earlyTerminationDialogVisible"
|
||||
append-to=".layout-main .content-wrapper"
|
||||
v-model="earlyTerminationDialogVisible"
|
||||
title="提前终止"
|
||||
width="560px"
|
||||
>
|
||||
<el-form :model="earlyTerminationForm" label-width="110px">
|
||||
<el-form-item label="提前终止日期" required>
|
||||
<el-date-picker v-model="earlyTerminationForm.termination_date" type="date" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
<el-form-item label="提前终止原因" required>
|
||||
<el-input v-model="earlyTerminationForm.reason" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.common.fields.remark">
|
||||
<el-input v-model="earlyTerminationForm.notes" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="early-termination-hint">
|
||||
提前终止表示受试者在方案最后一个计划访视窗口期前停止治疗;日期进入最后访视窗口期后,应录入正常终止治疗访视。
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="earlyTerminationDialogVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :loading="earlyTerminationSaving" :disabled="isReadOnly" @click="saveEarlyTermination">
|
||||
{{ TEXT.common.actions.save }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-if="aeDialogVisible" append-to=".layout-main .content-wrapper" v-model="aeDialogVisible" :title="TEXT.modules.subjectDetail.dialogAe" width="560px">
|
||||
<el-form :model="aeForm" label-width="100px">
|
||||
<el-form-item :label="TEXT.common.fields.title" required>
|
||||
@@ -426,7 +505,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { getSubject, updateSubject } from "../../api/subjects";
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import { listSubjectHistories, createSubjectHistory, updateSubjectHistory, deleteSubjectHistory } from "../../api/subjectHistories";
|
||||
import { fetchVisits, createVisit, updateVisit, deleteVisit } from "../../api/visits";
|
||||
import { fetchVisits, createEarlyTerminationVisit, 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";
|
||||
@@ -439,7 +518,7 @@ const subjectId = route.params.subjectId as string;
|
||||
const studyId = study.currentStudy?.id || "";
|
||||
const resolveTabFromQuery = () => {
|
||||
const tab = typeof route.query.tab === "string" ? route.query.tab : "";
|
||||
return ["history", "visits", "ae", "pd"].includes(tab) ? tab : "history";
|
||||
return ["history", "visits", "medicationAdherence", "ae", "pd"].includes(tab) ? tab : "history";
|
||||
};
|
||||
|
||||
const loading = ref(false);
|
||||
@@ -452,6 +531,7 @@ const detail = reactive<any>({
|
||||
enrollment_date: "",
|
||||
baseline_date: "",
|
||||
completion_date: "",
|
||||
actual_medication_count: null as number | null,
|
||||
drop_reason: "",
|
||||
});
|
||||
|
||||
@@ -483,6 +563,8 @@ const historyForm = reactive({
|
||||
const visitItems = ref<any[]>([]);
|
||||
const loadingVisits = ref(false);
|
||||
const visitDialogVisible = ref(false);
|
||||
const earlyTerminationDialogVisible = ref(false);
|
||||
const earlyTerminationSaving = ref(false);
|
||||
const visitEditingId = ref<string | null>(null);
|
||||
const visitEditingRowId = ref<string | null>(null);
|
||||
const visitRowDraft = reactive({
|
||||
@@ -497,7 +579,17 @@ const visitForm = reactive<any>({
|
||||
actual_date: "",
|
||||
notes: "",
|
||||
});
|
||||
const earlyTerminationForm = reactive({
|
||||
termination_date: "",
|
||||
reason: "",
|
||||
notes: "",
|
||||
});
|
||||
const isFirstVisit = computed(() => visitForm.visit_code === "V1");
|
||||
const adherenceEditing = ref(false);
|
||||
const adherenceSaving = ref(false);
|
||||
const adherenceForm = reactive({
|
||||
actual_medication_count: null as number | null,
|
||||
});
|
||||
|
||||
const aeItems = ref<any[]>([]);
|
||||
const loadingAes = ref(false);
|
||||
@@ -572,6 +664,14 @@ const normalizeSeriousness = (value?: string | null) => {
|
||||
return "I";
|
||||
};
|
||||
|
||||
const getErrorMessage = (error: any, fallback: string) => {
|
||||
const detail = error?.response?.data?.detail;
|
||||
if (typeof error?.response?.data?.message === "string") return error.response.data.message;
|
||||
if (typeof detail === "string") return detail;
|
||||
if (typeof detail?.message === "string") return detail.message;
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const loadSites = async () => {
|
||||
if (!studyId) return;
|
||||
try {
|
||||
@@ -763,6 +863,17 @@ const openVisitDialog = (row?: any) => {
|
||||
visitDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const openEarlyTerminationDialog = () => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
earlyTerminationForm.termination_date = "";
|
||||
earlyTerminationForm.reason = "";
|
||||
earlyTerminationForm.notes = "";
|
||||
earlyTerminationDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const startVisitRowEdit = (row: any) => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
@@ -803,6 +914,84 @@ const parseDate = (value?: string | null) => {
|
||||
return date;
|
||||
};
|
||||
|
||||
const parseDateAtNoon = (value?: string | null) => {
|
||||
if (!value) return null;
|
||||
const date = new Date(`${value}T12:00:00`);
|
||||
if (isNaN(date.getTime())) return null;
|
||||
return date;
|
||||
};
|
||||
|
||||
const getVisitWindowStartDateText = (row: any) => row?.window_start || row?.planned_date || "";
|
||||
|
||||
const getVisitPlannedDateLabel = (row: any) => {
|
||||
if (String(row?.visit_code || "").trim() === "提前终止") return "不适用";
|
||||
return displayDate(row?.planned_date);
|
||||
};
|
||||
|
||||
const getVisitWindowRangeLabel = (row: any) => {
|
||||
if (String(row?.visit_code || "").trim() === "提前终止") return "";
|
||||
if (!row?.window_start || !row?.window_end) return "";
|
||||
if (row.window_start === row.planned_date && row.window_end === row.planned_date) return "";
|
||||
return `${displayDate(row.window_start)} ~ ${displayDate(row.window_end)}`;
|
||||
};
|
||||
|
||||
const lastPlannedVisitWindowStartText = computed(() => {
|
||||
const excludedCodes = new Set(["筛选访视", "基线访视", "提前终止"]);
|
||||
const candidates = visitItems.value
|
||||
.filter((item) => !excludedCodes.has(String(item?.visit_code || "").trim()))
|
||||
.map((item) => getVisitWindowStartDateText(item))
|
||||
.filter(Boolean)
|
||||
.sort();
|
||||
return candidates[candidates.length - 1] || "";
|
||||
});
|
||||
|
||||
const validateEarlyTerminationDateInput = () => {
|
||||
const boundary = lastPlannedVisitWindowStartText.value;
|
||||
if (!boundary || !earlyTerminationForm.termination_date) return true;
|
||||
if (earlyTerminationForm.termination_date >= boundary) {
|
||||
ElMessage.error(`提前终止日期必须早于最后计划访视窗口期开始日(${boundary})`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const formatCount = (value: number | null | undefined) => (value === null || value === undefined ? TEXT.common.fallback : String(value));
|
||||
const medicationStartDate = computed(() => detail.baseline_date || "");
|
||||
const medicationEndVisit = computed(() => {
|
||||
const targetLabels = new Set(["终止治疗", "提前终止"]);
|
||||
return (
|
||||
visitItems.value.find((item) => targetLabels.has(String(item?.visit_code || "").trim()) && item?.actual_date) || null
|
||||
);
|
||||
});
|
||||
const medicationEndDate = computed(() => medicationEndVisit.value?.actual_date || "");
|
||||
const medicationEndVisitLabel = computed(() => medicationEndVisit.value?.visit_code || "未记录终止治疗/提前终止实际日期");
|
||||
const expectedMedicationCount = computed(() => {
|
||||
const start = parseDateAtNoon(medicationStartDate.value);
|
||||
const end = parseDateAtNoon(medicationEndDate.value);
|
||||
if (!start || !end || end < start) return null;
|
||||
return Math.floor((end.getTime() - start.getTime()) / 86400000) + 1;
|
||||
});
|
||||
const expectedMedicationCountText = computed(() => formatCount(expectedMedicationCount.value));
|
||||
const actualMedicationCount = computed(() => detail.actual_medication_count ?? null);
|
||||
const actualMedicationCountText = computed(() => formatCount(actualMedicationCount.value));
|
||||
const medicationAdherenceRate = computed(() => {
|
||||
const expected = expectedMedicationCount.value;
|
||||
const actual = actualMedicationCount.value;
|
||||
if (!expected || actual === null || actual === undefined) return null;
|
||||
return (actual / expected) * 100;
|
||||
});
|
||||
const medicationAdherenceRateText = computed(() => {
|
||||
const rate = medicationAdherenceRate.value;
|
||||
if (rate === null) return "无法计算";
|
||||
return `${rate.toFixed(1)}%`;
|
||||
});
|
||||
const adherenceRateTagType = computed(() => {
|
||||
const rate = medicationAdherenceRate.value;
|
||||
if (rate === null) return "info";
|
||||
if (rate < 75 || rate > 125) return "danger";
|
||||
return "success";
|
||||
});
|
||||
|
||||
const getVisitStatus = (row: any, actualDateOverride?: string) => {
|
||||
if (row?.status === "CANCELLED") return "CANCELLED";
|
||||
const actualDate = parseDate(actualDateOverride ?? row?.actual_date);
|
||||
@@ -840,6 +1029,14 @@ const getVisitStatusTagType = (status: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getVisitStatusLabel = (row: any, actualDateOverride?: string) => {
|
||||
const status = getVisitStatus(row, actualDateOverride);
|
||||
if (status === "CANCELLED" && String(row?.notes || "").includes("提前终止")) {
|
||||
return "不适用";
|
||||
}
|
||||
return displayEnum(TEXT.enums.visitStatus, status);
|
||||
};
|
||||
|
||||
const getAeTypeKey = (row: any) => {
|
||||
if (row?.is_susar) return "susar";
|
||||
if (row?.is_sae) return "sae";
|
||||
@@ -896,6 +1093,46 @@ const saveVisit = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const saveEarlyTermination = async () => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId) return;
|
||||
if (!earlyTerminationForm.termination_date) {
|
||||
ElMessage.error("请填写提前终止日期");
|
||||
return;
|
||||
}
|
||||
if (!earlyTerminationForm.reason.trim()) {
|
||||
ElMessage.error("请填写提前终止原因");
|
||||
return;
|
||||
}
|
||||
if (!validateEarlyTerminationDateInput()) return;
|
||||
earlyTerminationSaving.value = true;
|
||||
try {
|
||||
await createEarlyTerminationVisit(studyId, subjectId, {
|
||||
termination_date: earlyTerminationForm.termination_date,
|
||||
reason: earlyTerminationForm.reason.trim(),
|
||||
notes: earlyTerminationForm.notes || null,
|
||||
}, {
|
||||
suppressErrorMessage: true,
|
||||
});
|
||||
earlyTerminationDialogVisible.value = false;
|
||||
ElMessage.success(TEXT.common.messages.saveSuccess);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(getErrorMessage(e, TEXT.common.messages.saveFailed));
|
||||
earlyTerminationSaving.value = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await Promise.all([loadSubject(), loadVisits()]);
|
||||
} catch {
|
||||
ElMessage.warning("提前终止已保存,刷新数据失败,请手动刷新页面");
|
||||
} finally {
|
||||
earlyTerminationSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const removeVisit = async (row: any) => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
@@ -1032,6 +1269,11 @@ const currentTabAction = computed(() => {
|
||||
switch (activeTab.value) {
|
||||
case "visits":
|
||||
return { label: TEXT.common.actions.newVisit, onClick: () => openVisitDialog() };
|
||||
case "medicationAdherence":
|
||||
if (adherenceEditing.value) {
|
||||
return { label: "保存用药次数", loading: adherenceSaving.value, onClick: () => saveMedicationAdherence() };
|
||||
}
|
||||
return { label: "编辑", loading: false, onClick: () => startMedicationAdherenceEdit() };
|
||||
case "ae":
|
||||
return { label: TEXT.common.actions.newAe, onClick: () => openAeDialog() };
|
||||
case "pd":
|
||||
@@ -1042,6 +1284,36 @@ const currentTabAction = computed(() => {
|
||||
}
|
||||
});
|
||||
|
||||
const startMedicationAdherenceEdit = () => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
adherenceForm.actual_medication_count = detail.actual_medication_count ?? null;
|
||||
adherenceEditing.value = true;
|
||||
};
|
||||
|
||||
const saveMedicationAdherence = async () => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId) return;
|
||||
adherenceSaving.value = true;
|
||||
try {
|
||||
await updateSubject(studyId, subjectId, {
|
||||
actual_medication_count: adherenceForm.actual_medication_count ?? null,
|
||||
});
|
||||
adherenceEditing.value = false;
|
||||
ElMessage.success(TEXT.common.messages.saveSuccess);
|
||||
await loadSubject();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
adherenceSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const savePd = async () => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
@@ -1168,12 +1440,47 @@ onMounted(async () => {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.subject-overview-descriptions :deep(.el-descriptions__table) {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.visit-planned-cell {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.visit-window-range {
|
||||
color: #8a97ab;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.adherence-panel {
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.adherence-count-input {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.adherence-hint {
|
||||
margin-top: 12px;
|
||||
color: #6b778c;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.early-termination-hint {
|
||||
margin-top: 10px;
|
||||
color: #6b778c;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.subject-overview-descriptions :deep(.el-descriptions__label) {
|
||||
width: 140px;
|
||||
min-width: 140px;
|
||||
|
||||
Reference in New Issue
Block a user