From 6c5e5770c694cb2914a6092ffad69519c50870e1 Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Sat, 28 Feb 2026 16:41:20 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=8F=E4=BC=98=E5=8C=96-=E7=AB=8B=E9=A1=B9?= =?UTF-8?q?=E9=85=8D=E7=BD=AE-=E9=A2=84=E8=A7=88=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E4=B8=8B=E2=80=9C=E6=9B=B4=E6=96=B0=E2=80=9D=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/crud/study_setup_config.py | 53 +- frontend/src/views/admin/ProjectDetail.vue | 733 +++++++++++++++++++-- 2 files changed, 697 insertions(+), 89 deletions(-) diff --git a/backend/app/crud/study_setup_config.py b/backend/app/crud/study_setup_config.py index 4007c7b0..8f31a877 100644 --- a/backend/app/crud/study_setup_config.py +++ b/backend/app/crud/study_setup_config.py @@ -1,4 +1,5 @@ import uuid +from copy import deepcopy from datetime import datetime from sqlalchemy import delete as sa_delete, select @@ -13,7 +14,7 @@ from app.schemas.study_setup_config import StudySetupConfigData def _to_storage(data: StudySetupConfigData | dict) -> dict: if isinstance(data, StudySetupConfigData): return data.model_dump(mode="json") - return data + return deepcopy(data) async def get_by_study(db: AsyncSession, study_id: uuid.UUID) -> StudySetupConfig | None: @@ -76,31 +77,45 @@ async def publish( return None, False if expected_version is not None and expected_version != existing.version: return None, True + publish_payload = _to_storage(existing.config or {}) next_version = existing.version + 1 - next_display_version_stmt = select(func.max(StudySetupConfigVersion.display_version)).where( - StudySetupConfigVersion.study_id == study_id + latest_snapshot_stmt = ( + select(StudySetupConfigVersion) + .where(StudySetupConfigVersion.study_id == study_id) + .order_by(StudySetupConfigVersion.version.desc(), StudySetupConfigVersion.published_at.desc()) + .limit(1) ) - display_version_result = await db.execute(next_display_version_stmt) - current_max_display_version = display_version_result.scalar_one_or_none() or 0 - next_display_version = current_max_display_version + 1 + latest_snapshot_result = await db.execute(latest_snapshot_stmt) + latest_snapshot = latest_snapshot_result.scalar_one_or_none() + should_create_snapshot = not latest_snapshot or (latest_snapshot.config or {}) != publish_payload + existing.version = next_version existing.publish_status = "PUBLISHED" - existing.published_config = existing.config + existing.published_config = _to_storage(publish_payload) existing.published_by = published_by existing.published_at = datetime.utcnow() existing.saved_by = published_by existing.updated_at = datetime.utcnow() - snapshot = StudySetupConfigVersion( - study_setup_config_id=existing.id, - study_id=study_id, - version=next_version, - display_version=next_display_version, - source_version=next_version - 1, - config=existing.config, - published_by=published_by, - published_at=existing.published_at, - ) - db.add(snapshot) + + if should_create_snapshot: + next_display_version_stmt = select(func.max(StudySetupConfigVersion.display_version)).where( + StudySetupConfigVersion.study_id == study_id + ) + display_version_result = await db.execute(next_display_version_stmt) + current_max_display_version = display_version_result.scalar_one_or_none() or 0 + next_display_version = current_max_display_version + 1 + snapshot = StudySetupConfigVersion( + study_setup_config_id=existing.id, + study_id=study_id, + version=next_version, + display_version=next_display_version, + source_version=next_version - 1, + config=_to_storage(publish_payload), + published_by=published_by, + published_at=existing.published_at, + ) + db.add(snapshot) + if auto_commit: await db.commit() await db.refresh(existing) @@ -143,7 +158,7 @@ async def rollback_to_version( return None, False, True existing.version = existing.version + 1 - existing.config = target.config + existing.config = _to_storage(target.config or {}) existing.saved_by = saved_by existing.publish_status = "DRAFT" existing.updated_at = datetime.utcnow() diff --git a/frontend/src/views/admin/ProjectDetail.vue b/frontend/src/views/admin/ProjectDetail.vue index 3cf6c942..7866909b 100644 --- a/frontend/src/views/admin/ProjectDetail.vue +++ b/frontend/src/views/admin/ProjectDetail.vue @@ -43,7 +43,7 @@
- 保存配置 + 保存配置 发布版本
-
+
{{ item.label }} - {{ item.value || "-" }} + + {{ item.value || "-" }} + + 已更新 + +
@@ -378,9 +394,25 @@
-
+
{{ item.label }} - {{ item.value || "-" }} + + {{ item.value || "-" }} + + 已更新 + +
@@ -398,9 +430,25 @@
-
+
{{ item.label }} - {{ item.value || "-" }} + + {{ item.value || "-" }} + + 已更新 + +
@@ -446,9 +494,25 @@
-
+
{{ item.label }} - {{ item.value || "-" }} + + {{ item.value || "-" }} + + 已更新 + +
@@ -462,7 +526,18 @@ @@ -471,34 +546,100 @@
开始: - {{ resolveProjectMilestoneStart(scope.row) || "-" }} + + {{ resolveProjectMilestoneStart(scope.row) || "-" }} + + 已更新 + +
结束: - {{ resolveProjectMilestoneEnd(scope.row) || "-" }} + + {{ resolveProjectMilestoneEnd(scope.row) || "-" }} + + 已更新 + +
耗时: - {{ formatProjectMilestoneDuration(scope.row) }} + + {{ formatProjectMilestoneDuration(scope.row) }} + + 已更新 + +
@@ -525,7 +666,18 @@ :disabled="!canEditSetup" class="w-full" /> - {{ currentSetupDraft.enrollmentPlan.startDate || "-" }} + + {{ currentSetupDraft.enrollmentPlan.startDate || "-" }} + + 已更新 + +
{{ getFieldError("enrollmentPlan.startDate") }}
~ @@ -538,7 +690,18 @@ :disabled="!canEditSetup" class="w-full" /> - {{ currentSetupDraft.enrollmentPlan.endDate || "-" }} + + {{ currentSetupDraft.enrollmentPlan.endDate || "-" }} + + 已更新 + +
{{ getFieldError("enrollmentPlan.endDate") }}
@@ -553,7 +716,18 @@ 季度 - {{ enrollmentPlanCycleLabel }} + + {{ enrollmentPlanCycleLabel }} + + 已更新 + +
计划入组总数 @@ -566,7 +740,18 @@ controls-position="right" class="w-full" /> - {{ toDisplayNumber(currentSetupDraft.enrollmentPlan.totalTarget) }} + + {{ toDisplayNumber(currentSetupDraft.enrollmentPlan.totalTarget) }} + + 已更新 + +
{{ getFieldError("enrollmentPlan.totalTarget") }}
@@ -620,7 +805,18 @@ class="month-target-input" @update:model-value="setEnrollmentPeriodTarget(scope.row.cells[month - 1].key, $event as number | null)" /> - {{ displayEnrollmentPeriodTarget(scope.row.cells[month - 1].key) }} + + {{ displayEnrollmentPeriodTarget(scope.row.cells[month - 1].key) }} + + 已更新 + + - @@ -656,7 +852,18 @@ class="month-target-input" @update:model-value="setEnrollmentPeriodTarget(scope.row.cells[columnIndex].key, $event as number | null)" /> - {{ displayEnrollmentPeriodTarget(scope.row.cells[columnIndex].key) }} + + {{ displayEnrollmentPeriodTarget(scope.row.cells[columnIndex].key) }} + + 已更新 + + - @@ -668,27 +875,82 @@ @@ -735,7 +997,18 @@ :disabled="!canEditSetup" class="w-full" /> - {{ selectedSiteEnrollmentPlan?.startDate || "-" }} + + {{ selectedSiteEnrollmentPlan?.startDate || "-" }} + + 已更新 + + ~
@@ -747,7 +1020,18 @@ :disabled="!canEditSetup" class="w-full" /> - {{ selectedSiteEnrollmentPlan?.endDate || "-" }} + + {{ selectedSiteEnrollmentPlan?.endDate || "-" }} + + 已更新 + +
@@ -761,7 +1045,18 @@ 季度 - {{ enrollmentPlanCycleLabel }} + + {{ enrollmentPlanCycleLabel }} + + 已更新 + +
中心计划总数 @@ -774,7 +1069,18 @@ controls-position="right" class="w-full" /> - {{ toDisplayNumber(selectedSiteEnrollmentPlan?.target) }} + + {{ toDisplayNumber(selectedSiteEnrollmentPlan?.target) }} + + 已更新 + +
@@ -841,7 +1147,18 @@ class="month-target-input" @update:model-value="setSiteEnrollmentPeriodTarget(selectedSiteEnrollmentPlanIndex, scope.row.cells[month - 1].key, $event as number | null)" /> - {{ displaySiteEnrollmentPeriodTarget(selectedSiteEnrollmentPlan, scope.row.cells[month - 1].key) }} + + {{ displaySiteEnrollmentPeriodTarget(selectedSiteEnrollmentPlan, scope.row.cells[month - 1].key) }} + + 已更新 + + - @@ -877,7 +1194,18 @@ class="month-target-input" @update:model-value="setSiteEnrollmentPeriodTarget(selectedSiteEnrollmentPlanIndex, scope.row.cells[columnIndex].key, $event as number | null)" /> - {{ displaySiteEnrollmentPeriodTarget(selectedSiteEnrollmentPlan, scope.row.cells[columnIndex].key) }} + + {{ displaySiteEnrollmentPeriodTarget(selectedSiteEnrollmentPlan, scope.row.cells[columnIndex].key) }} + + 已更新 + + - @@ -890,22 +1218,66 @@ @@ -943,7 +1315,18 @@ @@ -955,12 +1338,34 @@ @@ -1949,41 +2354,183 @@ const currentStepErrors = computed(() => { return grouped[section] || []; }); -const projectInfoItems = computed(() => [ - { label: "项目名称", value: form.value.name || project.value?.name || "-" }, - { label: "项目全称", value: form.value.project_full_name || project.value?.project_full_name || "-" }, - { label: "申办方", value: form.value.sponsor || project.value?.sponsor || "-" }, - { label: "项目编号", value: form.value.code || project.value?.code || "-" }, - { label: "方案号", value: form.value.protocol_no || project.value?.protocol_no || "-" }, - { label: "组长单位", value: form.value.lead_unit || project.value?.lead_unit || "-" }, - { label: "主要研究者", value: form.value.principal_investigator || project.value?.principal_investigator || "-" }, - { label: "主PM", value: form.value.main_pm || project.value?.main_pm || "-" }, +type DisplayInfoItem = { + label: string; + value: string | number | null | undefined; + updated?: boolean; +}; + +const shouldShowPreviewUpdateMark = computed(() => isPublishedView.value && !isFirstPublish.value); +const currentProjectPublishSnapshot = computed(() => buildProjectPublishSnapshot()); +const normalizeCompareValue = (value: unknown): string => serializeDiffValue(value); +const isPreviewValueUpdated = (currentValue: unknown, previousValue: unknown): boolean => { + if (!shouldShowPreviewUpdateMark.value) return false; + return normalizeCompareValue(currentValue) !== normalizeCompareValue(previousValue); +}; +const normalizeIdentityText = (value: unknown): string => String(value ?? "").trim(); +const resolveRowCompareTarget = ( + rows: T[] | null | undefined, + row: T | null | undefined, + index: number, + identityBuilder: (item: T) => string +): T | null => { + const sourceRows = Array.isArray(rows) ? rows : []; + if (!row || !sourceRows.length) return null; + const rowId = normalizeIdentityText(row.id); + if (rowId) { + const matchedById = sourceRows.find((item) => normalizeIdentityText(item.id) === rowId); + if (matchedById) return matchedById; + } + const rowIdentity = identityBuilder(row); + if (rowIdentity) { + const matchedByIdentity = sourceRows.filter((item) => identityBuilder(item) === rowIdentity); + if (matchedByIdentity.length === 1) return matchedByIdentity[0]; + if (matchedByIdentity.length > 1) { + return matchedByIdentity[index] || matchedByIdentity[0]; + } + } + return sourceRows[index] || null; +}; +const isProjectSnapshotFieldUpdated = (field: keyof ProjectPublishSnapshot): boolean => + isPreviewValueUpdated(currentProjectPublishSnapshot.value[field], projectPublishCompareBase.value?.[field]); +const isProjectMilestoneFieldUpdated = ( + row: ProjectMilestoneDraft, + index: number, + field: "name" | "owner" | "status" | "remark" | "startDate" | "endDate" | "duration" +): boolean => { + const baseRow = resolveRowCompareTarget( + publishCompareBase.value.projectMilestones, + row, + index, + (item) => `${normalizeIdentityText(item.name)}|${normalizeIdentityText(item.owner)}` + ); + if (field === "startDate") return isPreviewValueUpdated(resolveProjectMilestoneStart(row), resolveProjectMilestoneStart(baseRow || undefined)); + if (field === "endDate") return isPreviewValueUpdated(resolveProjectMilestoneEnd(row), resolveProjectMilestoneEnd(baseRow || undefined)); + if (field === "duration") { + return isPreviewValueUpdated( + resolveProjectMilestoneDurationDays(row), + resolveProjectMilestoneDurationDays(baseRow || undefined) + ); + } + return isPreviewValueUpdated(row[field], baseRow?.[field]); +}; +const publishedEnrollmentPlanParsed = computed(() => + parseEnrollmentStageBreakdown(publishCompareBase.value.enrollmentPlan.stageBreakdown) +); +const isEnrollmentPlanFieldUpdated = (field: "startDate" | "endDate" | "totalTarget" | "cycle"): boolean => { + if (field === "cycle") { + return isPreviewValueUpdated(enrollmentPlanCycle.value, publishedEnrollmentPlanParsed.value.cycle); + } + return isPreviewValueUpdated(setupDraft.enrollmentPlan[field], publishCompareBase.value.enrollmentPlan[field]); +}; +const isProjectEnrollmentPeriodUpdated = (key: string | null): boolean => { + if (!key) return false; + const cycle = enrollmentPlanCycle.value; + const localValue = enrollmentTargetsByCycle.value[cycle]?.[key]; + const serverValue = publishedEnrollmentPlanParsed.value.targetsByCycle[cycle]?.[key]; + return isPreviewValueUpdated(localValue, serverValue); +}; +const isSiteMilestoneFieldUpdated = ( + row: SiteMilestoneDraft, + index: number, + field: "milestone" | "planDate" | "owner" | "status" | "remark" +): boolean => { + const baseRow = resolveRowCompareTarget( + publishCompareBase.value.siteMilestones, + row, + index, + (item) => `${normalizeIdentityText(item.milestone)}|${normalizeIdentityText(item.owner)}` + ); + return isPreviewValueUpdated(row[field], baseRow?.[field]); +}; +const isMonitoringStrategyFieldUpdated = ( + row: MonitoringStrategyDraft, + index: number, + field: "strategyType" | "detail" | "frequency" | "updatedAt" +): boolean => { + const baseRow = resolveRowCompareTarget( + publishCompareBase.value.monitoringStrategies, + row, + index, + (item) => + `${normalizeIdentityText(item.strategyType)}|${normalizeIdentityText(item.detail)}|${normalizeIdentityText(item.frequency)}` + ); + return isPreviewValueUpdated(row[field], baseRow?.[field]); +}; +const publishedSiteEnrollmentPlanMap = computed(() => { + const map = new Map(); + publishCompareBase.value.siteEnrollmentPlans.forEach((row) => { + const siteId = normalizeSiteId(row.siteId); + if (!siteId) return; + map.set(siteId, row); + }); + return map; +}); +const getPublishedSiteEnrollmentPlan = (siteId: string | null | undefined): SiteEnrollmentPlanDraft | null => { + const normalizedSiteId = normalizeSiteId(siteId || ""); + if (!normalizedSiteId) return null; + return publishedSiteEnrollmentPlanMap.value.get(normalizedSiteId) || null; +}; +const isSelectedSiteEnrollmentFieldUpdated = (field: "startDate" | "endDate" | "target" | "cycle"): boolean => { + if (field === "cycle") return isEnrollmentPlanFieldUpdated("cycle"); + const currentPlan = selectedSiteEnrollmentPlan.value; + if (!currentPlan) return false; + const basePlan = getPublishedSiteEnrollmentPlan(currentPlan.siteId); + return isPreviewValueUpdated(currentPlan[field], basePlan?.[field]); +}; +const isSelectedSiteEnrollmentPeriodUpdated = (key: string | null): boolean => { + if (!key || !selectedSiteEnrollmentPlan.value) return false; + const cycle = enrollmentPlanCycle.value; + const currentTargets = getSiteEnrollmentTargetsByCycle(selectedSiteEnrollmentPlan.value); + const basePlan = getPublishedSiteEnrollmentPlan(selectedSiteEnrollmentPlan.value.siteId); + const baseTargets = basePlan ? parseSiteEnrollmentStageBreakdown(basePlan.stageBreakdown) : createEmptyEnrollmentTargetsByCycle(); + return isPreviewValueUpdated(currentTargets[cycle]?.[key], baseTargets[cycle]?.[key]); +}; +const isCenterConfirmPlanFieldUpdated = (siteId: string, field: "target" | "startDate" | "endDate"): boolean => { + const currentPlan = siteEnrollmentPlanMap.value.get(normalizeSiteId(siteId)); + const basePlan = getPublishedSiteEnrollmentPlan(siteId); + return isPreviewValueUpdated(currentPlan?.[field], basePlan?.[field]); +}; + +const projectInfoItems = computed(() => [ + { label: "项目名称", value: form.value.name || project.value?.name || "-", updated: isProjectSnapshotFieldUpdated("name") }, + { label: "项目全称", value: form.value.project_full_name || project.value?.project_full_name || "-", updated: isProjectSnapshotFieldUpdated("project_full_name") }, + { label: "申办方", value: form.value.sponsor || project.value?.sponsor || "-", updated: isProjectSnapshotFieldUpdated("sponsor") }, + { label: "项目编号", value: form.value.code || project.value?.code || "-", updated: isProjectSnapshotFieldUpdated("code") }, + { label: "方案号", value: form.value.protocol_no || project.value?.protocol_no || "-", updated: isProjectSnapshotFieldUpdated("protocol_no") }, + { label: "组长单位", value: form.value.lead_unit || project.value?.lead_unit || "-", updated: isProjectSnapshotFieldUpdated("lead_unit") }, + { label: "主要研究者", value: form.value.principal_investigator || project.value?.principal_investigator || "-", updated: isProjectSnapshotFieldUpdated("principal_investigator") }, + { label: "主PM", value: form.value.main_pm || project.value?.main_pm || "-", updated: isProjectSnapshotFieldUpdated("main_pm") }, ]); -const researchInfoItems = computed(() => [ - { label: "研究分期", value: form.value.research_analysis || project.value?.research_analysis || "-" }, - { label: "研究产品", value: form.value.research_product || project.value?.research_product || "-" }, - { label: "对照产品", value: form.value.control_product || project.value?.control_product || "-" }, - { label: "适应症", value: form.value.indication || project.value?.indication || "-" }, - { label: "研究人群", value: form.value.research_population || project.value?.research_population || "-" }, - { label: "研究设计", value: form.value.research_design || project.value?.research_design || "-" }, +const researchInfoItems = computed(() => [ + { label: "研究分期", value: form.value.research_analysis || project.value?.research_analysis || "-", updated: isProjectSnapshotFieldUpdated("research_analysis") }, + { label: "研究产品", value: form.value.research_product || project.value?.research_product || "-", updated: isProjectSnapshotFieldUpdated("research_product") }, + { label: "对照产品", value: form.value.control_product || project.value?.control_product || "-", updated: isProjectSnapshotFieldUpdated("control_product") }, + { label: "适应症", value: form.value.indication || project.value?.indication || "-", updated: isProjectSnapshotFieldUpdated("indication") }, + { label: "研究人群", value: form.value.research_population || project.value?.research_population || "-", updated: isProjectSnapshotFieldUpdated("research_population") }, + { label: "研究设计", value: form.value.research_design || project.value?.research_design || "-", updated: isProjectSnapshotFieldUpdated("research_design") }, ]); -const executionInfoItems = computed(() => [ - { label: "计划开始日期", value: form.value.plan_start_date || project.value?.plan_start_date || "-" }, - { label: "计划结束日期", value: form.value.plan_end_date || project.value?.plan_end_date || "-" }, - { label: "项目状态", value: statusLabel(form.value.status || project.value?.status || "DRAFT") }, - { label: "计划中心数", value: toDisplayNumber(form.value.planned_site_count) }, - { label: "计划入组数", value: toDisplayNumber(setupDraft.enrollmentPlan.totalTarget) }, +const executionInfoItems = computed(() => [ + { label: "计划开始日期", value: form.value.plan_start_date || project.value?.plan_start_date || "-", updated: isProjectSnapshotFieldUpdated("plan_start_date") }, + { label: "计划结束日期", value: form.value.plan_end_date || project.value?.plan_end_date || "-", updated: isProjectSnapshotFieldUpdated("plan_end_date") }, + { label: "项目状态", value: statusLabel(form.value.status || project.value?.status || "DRAFT"), updated: isProjectSnapshotFieldUpdated("status") }, + { label: "计划中心数", value: toDisplayNumber(form.value.planned_site_count), updated: isProjectSnapshotFieldUpdated("planned_site_count") }, + { + label: "计划入组数", + value: toDisplayNumber(setupDraft.enrollmentPlan.totalTarget), + updated: isEnrollmentPlanFieldUpdated("totalTarget"), + }, ]); -const summaryInfoItems = computed(() => [ - { label: "访视总数", value: toDisplayNumber(form.value.visit_total) }, - { label: "访视间隔(天)", value: toDisplayNumber(form.value.visit_interval_days) }, - { label: "窗口期起始偏移", value: toDisplayNumber(form.value.visit_window_start_offset) }, - { label: "窗口期结束偏移", value: toDisplayNumber(form.value.visit_window_end_offset) }, - { label: "方案摘要说明", value: form.value.summary_note || "-" }, - { label: "研究目标摘要", value: form.value.objective_note || "-" }, +const summaryInfoItems = computed(() => [ + { label: "访视总数", value: toDisplayNumber(form.value.visit_total), updated: isProjectSnapshotFieldUpdated("visit_total") }, + { label: "访视间隔(天)", value: toDisplayNumber(form.value.visit_interval_days), updated: isProjectSnapshotFieldUpdated("visit_interval_days") }, + { label: "窗口期起始偏移", value: toDisplayNumber(form.value.visit_window_start_offset), updated: isProjectSnapshotFieldUpdated("visit_window_start_offset") }, + { label: "窗口期结束偏移", value: toDisplayNumber(form.value.visit_window_end_offset), updated: isProjectSnapshotFieldUpdated("visit_window_end_offset") }, + { label: "方案摘要说明", value: form.value.summary_note || "-", updated: isProjectSnapshotFieldUpdated("summary_note") }, + { label: "研究目标摘要", value: form.value.objective_note || "-", updated: isProjectSnapshotFieldUpdated("objective_note") }, ]); const rules = ref({ @@ -4027,8 +4574,28 @@ const removeVersion = async (targetVersion: number) => { } }; +const ensureCanSaveDraftAction = (): boolean => { + if (!project.value) { + ElMessage.warning("项目信息尚未加载完成,请稍后重试"); + return false; + } + if (!canManageSetup.value) { + ElMessage.warning("当前角色无权保存立项配置"); + return false; + } + if (project.value.is_locked) { + ElMessage.warning("项目已锁定,无法保存配置"); + return false; + } + if (isPublishedView.value) { + ElMessage.info("当前为预览视图,请先切换到草稿后再保存"); + return false; + } + return true; +}; + const handlePrimarySaveConfig = async () => { - if (!canSaveDraftAction.value) return; + if (!ensureCanSaveDraftAction()) return; if (incompleteSteps.value.length > 0) { try { await ElMessageBox.confirm( @@ -4820,6 +5387,26 @@ onBeforeUnmount(() => { color: #11264d; } +.updated-value-wrap { + display: inline-block; + position: relative; + vertical-align: middle; +} + +.field-updated-tag { + position: absolute; + left: calc(100% + 6px); + top: 50%; + transform: translateY(-50%); + margin: 0 !important; + z-index: 1; + pointer-events: none; + --el-tag-bg-color: #fff8eb; + --el-tag-border-color: #ecd2a3; + --el-tag-text-color: #9a6b0d; + font-weight: 600; +} + .enrollment-sep { color: #8da3c7; font-weight: 600; @@ -5307,6 +5894,12 @@ onBeforeUnmount(() => { padding-top: 4px; } +.info-value-wrap { + display: inline-block; + position: relative; + vertical-align: middle; +} + .setup-info-tabs { margin-top: -8px; }