小优化-立项配置-预览模式下“更新”显示
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
</div>
|
||||
|
||||
<div class="setup-toolbar-actions">
|
||||
<el-button type="success" plain :loading="primarySaveLoading" @click="handlePrimarySaveConfig">保存配置</el-button>
|
||||
<el-button type="success" plain :loading="primarySaveLoading" :disabled="isPublishedView" @click="handlePrimarySaveConfig">保存配置</el-button>
|
||||
<el-button type="warning" plain :loading="publishConfirmLoading" @click="publishConfigNow">发布版本</el-button>
|
||||
<el-button
|
||||
type="info"
|
||||
@@ -358,9 +358,25 @@
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div v-for="item in projectInfoItems" :key="`project-${item.label}`" class="info-item">
|
||||
<div
|
||||
v-for="item in projectInfoItems"
|
||||
:key="`project-${item.label}`"
|
||||
class="info-item"
|
||||
:class="{ 'is-updated': item.updated && shouldShowPreviewUpdateMark }"
|
||||
>
|
||||
<span class="info-label">{{ item.label }}</span>
|
||||
<span class="info-value">{{ item.value || "-" }}</span>
|
||||
<span class="info-value-wrap">
|
||||
<span class="info-value">{{ item.value || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="item.updated && shouldShowPreviewUpdateMark"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -378,9 +394,25 @@
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div v-for="item in researchInfoItems" :key="`research-${item.label}`" class="info-item">
|
||||
<div
|
||||
v-for="item in researchInfoItems"
|
||||
:key="`research-${item.label}`"
|
||||
class="info-item"
|
||||
:class="{ 'is-updated': item.updated && shouldShowPreviewUpdateMark }"
|
||||
>
|
||||
<span class="info-label">{{ item.label }}</span>
|
||||
<span class="info-value">{{ item.value || "-" }}</span>
|
||||
<span class="info-value-wrap">
|
||||
<span class="info-value">{{ item.value || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="item.updated && shouldShowPreviewUpdateMark"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -398,9 +430,25 @@
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div v-for="item in executionInfoItems" :key="`execution-${item.label}`" class="info-item">
|
||||
<div
|
||||
v-for="item in executionInfoItems"
|
||||
:key="`execution-${item.label}`"
|
||||
class="info-item"
|
||||
:class="{ 'is-updated': item.updated && shouldShowPreviewUpdateMark }"
|
||||
>
|
||||
<span class="info-label">{{ item.label }}</span>
|
||||
<span class="info-value">{{ item.value || "-" }}</span>
|
||||
<span class="info-value-wrap">
|
||||
<span class="info-value">{{ item.value || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="item.updated && shouldShowPreviewUpdateMark"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -446,9 +494,25 @@
|
||||
</el-form>
|
||||
|
||||
<div v-else class="info-grid">
|
||||
<div v-for="item in summaryInfoItems" :key="item.label" class="info-item">
|
||||
<div
|
||||
v-for="item in summaryInfoItems"
|
||||
:key="item.label"
|
||||
class="info-item"
|
||||
:class="{ 'is-updated': item.updated && shouldShowPreviewUpdateMark }"
|
||||
>
|
||||
<span class="info-label">{{ item.label }}</span>
|
||||
<span class="info-value">{{ item.value || "-" }}</span>
|
||||
<span class="info-value-wrap">
|
||||
<span class="info-value">{{ item.value || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="item.updated && shouldShowPreviewUpdateMark"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
@@ -462,7 +526,18 @@
|
||||
<el-table :data="currentSetupDraft.projectMilestones" class="ctms-table setup-milestone-table">
|
||||
<el-table-column label="里程碑" min-width="180">
|
||||
<template #default="scope">
|
||||
<span class="milestone-cell-text">{{ scope.row.name || "-" }}</span>
|
||||
<span class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ scope.row.name || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="isProjectMilestoneFieldUpdated(scope.row, scope.$index, 'name')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计划时间" width="240">
|
||||
@@ -471,34 +546,100 @@
|
||||
<div class="milestone-plan-line">
|
||||
<span class="milestone-plan-dot" />
|
||||
<span class="milestone-plan-label">开始:</span>
|
||||
<span>{{ resolveProjectMilestoneStart(scope.row) || "-" }}</span>
|
||||
<span class="updated-value-wrap">
|
||||
<span>{{ resolveProjectMilestoneStart(scope.row) || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="isProjectMilestoneFieldUpdated(scope.row, scope.$index, 'startDate')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div class="milestone-plan-line">
|
||||
<span class="milestone-plan-dot" />
|
||||
<span class="milestone-plan-label">结束:</span>
|
||||
<span>{{ resolveProjectMilestoneEnd(scope.row) || "-" }}</span>
|
||||
<span class="updated-value-wrap">
|
||||
<span>{{ resolveProjectMilestoneEnd(scope.row) || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="isProjectMilestoneFieldUpdated(scope.row, scope.$index, 'endDate')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div class="milestone-plan-line">
|
||||
<span class="milestone-plan-dot" />
|
||||
<span class="milestone-plan-label">耗时:</span>
|
||||
<span>{{ formatProjectMilestoneDuration(scope.row) }}</span>
|
||||
<span class="updated-value-wrap">
|
||||
<span>{{ formatProjectMilestoneDuration(scope.row) }}</span>
|
||||
<el-tag
|
||||
v-if="isProjectMilestoneFieldUpdated(scope.row, scope.$index, 'duration')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" min-width="160">
|
||||
<template #default="scope">
|
||||
<span class="milestone-cell-text">{{ normalizeOwnerDisplay(scope.row.owner) }}</span>
|
||||
<span class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ normalizeOwnerDisplay(scope.row.owner) }}</span>
|
||||
<el-tag
|
||||
v-if="isProjectMilestoneFieldUpdated(scope.row, scope.$index, 'owner')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="150">
|
||||
<template #default="scope">
|
||||
<span class="milestone-cell-text">{{ scope.row.status || "-" }}</span>
|
||||
<span class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ scope.row.status || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="isProjectMilestoneFieldUpdated(scope.row, scope.$index, 'status')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="220">
|
||||
<template #default="scope">
|
||||
<span class="milestone-cell-text">{{ scope.row.remark || "-" }}</span>
|
||||
<span class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ scope.row.remark || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="isProjectMilestoneFieldUpdated(scope.row, scope.$index, 'remark')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="!isPublishedView" label="操作" width="130">
|
||||
@@ -525,7 +666,18 @@
|
||||
:disabled="!canEditSetup"
|
||||
class="w-full"
|
||||
/>
|
||||
<span v-else class="enrollment-inline-value">{{ currentSetupDraft.enrollmentPlan.startDate || "-" }}</span>
|
||||
<span v-else class="updated-value-wrap">
|
||||
<span class="enrollment-inline-value">{{ currentSetupDraft.enrollmentPlan.startDate || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="isEnrollmentPlanFieldUpdated('startDate')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
<div v-if="getFieldError('enrollmentPlan.startDate')" class="field-error">{{ getFieldError("enrollmentPlan.startDate") }}</div>
|
||||
</div>
|
||||
<span class="enrollment-sep">~</span>
|
||||
@@ -538,7 +690,18 @@
|
||||
:disabled="!canEditSetup"
|
||||
class="w-full"
|
||||
/>
|
||||
<span v-else class="enrollment-inline-value">{{ currentSetupDraft.enrollmentPlan.endDate || "-" }}</span>
|
||||
<span v-else class="updated-value-wrap">
|
||||
<span class="enrollment-inline-value">{{ currentSetupDraft.enrollmentPlan.endDate || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="isEnrollmentPlanFieldUpdated('endDate')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
<div v-if="getFieldError('enrollmentPlan.endDate')" class="field-error">{{ getFieldError("enrollmentPlan.endDate") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -553,7 +716,18 @@
|
||||
<el-radio-button label="month">月</el-radio-button>
|
||||
<el-radio-button label="quarter">季度</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span v-else class="enrollment-inline-value">{{ enrollmentPlanCycleLabel }}</span>
|
||||
<span v-else class="updated-value-wrap">
|
||||
<span class="enrollment-inline-value">{{ enrollmentPlanCycleLabel }}</span>
|
||||
<el-tag
|
||||
v-if="isEnrollmentPlanFieldUpdated('cycle')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div class="enrollment-inline-group total-target-group">
|
||||
<span class="enrollment-inline-label">计划入组总数</span>
|
||||
@@ -566,7 +740,18 @@
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
<span v-else class="enrollment-inline-value">{{ toDisplayNumber(currentSetupDraft.enrollmentPlan.totalTarget) }}</span>
|
||||
<span v-else class="updated-value-wrap">
|
||||
<span class="enrollment-inline-value">{{ toDisplayNumber(currentSetupDraft.enrollmentPlan.totalTarget) }}</span>
|
||||
<el-tag
|
||||
v-if="isEnrollmentPlanFieldUpdated('totalTarget')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
<div v-if="getFieldError('enrollmentPlan.totalTarget')" class="field-error">{{ getFieldError("enrollmentPlan.totalTarget") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -620,7 +805,18 @@
|
||||
class="month-target-input"
|
||||
@update:model-value="setEnrollmentPeriodTarget(scope.row.cells[month - 1].key, $event as number | null)"
|
||||
/>
|
||||
<span v-else class="milestone-cell-text">{{ displayEnrollmentPeriodTarget(scope.row.cells[month - 1].key) }}</span>
|
||||
<span v-else class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ displayEnrollmentPeriodTarget(scope.row.cells[month - 1].key) }}</span>
|
||||
<el-tag
|
||||
v-if="isProjectEnrollmentPeriodUpdated(scope.row.cells[month - 1].key)"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
<span v-else class="month-empty">-</span>
|
||||
</template>
|
||||
@@ -656,7 +852,18 @@
|
||||
class="month-target-input"
|
||||
@update:model-value="setEnrollmentPeriodTarget(scope.row.cells[columnIndex].key, $event as number | null)"
|
||||
/>
|
||||
<span v-else class="milestone-cell-text">{{ displayEnrollmentPeriodTarget(scope.row.cells[columnIndex].key) }}</span>
|
||||
<span v-else class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ displayEnrollmentPeriodTarget(scope.row.cells[columnIndex].key) }}</span>
|
||||
<el-tag
|
||||
v-if="isProjectEnrollmentPeriodUpdated(scope.row.cells[columnIndex].key)"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
<span v-else class="month-empty">-</span>
|
||||
</template>
|
||||
@@ -668,27 +875,82 @@
|
||||
<el-table :data="currentSetupDraft.siteMilestones" class="ctms-table">
|
||||
<el-table-column label="里程碑" min-width="220">
|
||||
<template #default="scope">
|
||||
<span class="milestone-cell-text">{{ scope.row.milestone || "-" }}</span>
|
||||
<span class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ scope.row.milestone || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="isSiteMilestoneFieldUpdated(scope.row, scope.$index, 'milestone')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计划日期" width="180">
|
||||
<template #default="scope">
|
||||
<span class="milestone-cell-text">{{ scope.row.planDate || "-" }}</span>
|
||||
<span class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ scope.row.planDate || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="isSiteMilestoneFieldUpdated(scope.row, scope.$index, 'planDate')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" min-width="160">
|
||||
<template #default="scope">
|
||||
<span class="milestone-cell-text">{{ normalizeOwnerDisplay(scope.row.owner) }}</span>
|
||||
<span class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ normalizeOwnerDisplay(scope.row.owner) }}</span>
|
||||
<el-tag
|
||||
v-if="isSiteMilestoneFieldUpdated(scope.row, scope.$index, 'owner')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="150">
|
||||
<template #default="scope">
|
||||
<span class="milestone-cell-text">{{ scope.row.status || "未开始" }}</span>
|
||||
<span class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ scope.row.status || "未开始" }}</span>
|
||||
<el-tag
|
||||
v-if="isSiteMilestoneFieldUpdated(scope.row, scope.$index, 'status')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="180">
|
||||
<template #default="scope">
|
||||
<span class="milestone-cell-text">{{ scope.row.remark || "-" }}</span>
|
||||
<span class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ scope.row.remark || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="isSiteMilestoneFieldUpdated(scope.row, scope.$index, 'remark')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="!isPublishedView" label="操作" width="130">
|
||||
@@ -735,7 +997,18 @@
|
||||
:disabled="!canEditSetup"
|
||||
class="w-full"
|
||||
/>
|
||||
<span v-else class="enrollment-inline-value">{{ selectedSiteEnrollmentPlan?.startDate || "-" }}</span>
|
||||
<span v-else class="updated-value-wrap">
|
||||
<span class="enrollment-inline-value">{{ selectedSiteEnrollmentPlan?.startDate || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="isSelectedSiteEnrollmentFieldUpdated('startDate')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<span class="enrollment-sep">~</span>
|
||||
<div class="field-cell enrollment-date-cell">
|
||||
@@ -747,7 +1020,18 @@
|
||||
:disabled="!canEditSetup"
|
||||
class="w-full"
|
||||
/>
|
||||
<span v-else class="enrollment-inline-value">{{ selectedSiteEnrollmentPlan?.endDate || "-" }}</span>
|
||||
<span v-else class="updated-value-wrap">
|
||||
<span class="enrollment-inline-value">{{ selectedSiteEnrollmentPlan?.endDate || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="isSelectedSiteEnrollmentFieldUpdated('endDate')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="enrollment-inline-group">
|
||||
@@ -761,7 +1045,18 @@
|
||||
<el-radio-button label="month">月</el-radio-button>
|
||||
<el-radio-button label="quarter">季度</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span v-else class="enrollment-inline-value">{{ enrollmentPlanCycleLabel }}</span>
|
||||
<span v-else class="updated-value-wrap">
|
||||
<span class="enrollment-inline-value">{{ enrollmentPlanCycleLabel }}</span>
|
||||
<el-tag
|
||||
v-if="isSelectedSiteEnrollmentFieldUpdated('cycle')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div class="enrollment-inline-group total-target-group">
|
||||
<span class="enrollment-inline-label">中心计划总数</span>
|
||||
@@ -774,7 +1069,18 @@
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
<span v-else class="enrollment-inline-value">{{ toDisplayNumber(selectedSiteEnrollmentPlan?.target) }}</span>
|
||||
<span v-else class="updated-value-wrap">
|
||||
<span class="enrollment-inline-value">{{ toDisplayNumber(selectedSiteEnrollmentPlan?.target) }}</span>
|
||||
<el-tag
|
||||
v-if="isSelectedSiteEnrollmentFieldUpdated('target')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -841,7 +1147,18 @@
|
||||
class="month-target-input"
|
||||
@update:model-value="setSiteEnrollmentPeriodTarget(selectedSiteEnrollmentPlanIndex, scope.row.cells[month - 1].key, $event as number | null)"
|
||||
/>
|
||||
<span v-else class="milestone-cell-text">{{ displaySiteEnrollmentPeriodTarget(selectedSiteEnrollmentPlan, scope.row.cells[month - 1].key) }}</span>
|
||||
<span v-else class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ displaySiteEnrollmentPeriodTarget(selectedSiteEnrollmentPlan, scope.row.cells[month - 1].key) }}</span>
|
||||
<el-tag
|
||||
v-if="isSelectedSiteEnrollmentPeriodUpdated(scope.row.cells[month - 1].key)"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
<span v-else class="month-empty">-</span>
|
||||
</template>
|
||||
@@ -877,7 +1194,18 @@
|
||||
class="month-target-input"
|
||||
@update:model-value="setSiteEnrollmentPeriodTarget(selectedSiteEnrollmentPlanIndex, scope.row.cells[columnIndex].key, $event as number | null)"
|
||||
/>
|
||||
<span v-else class="milestone-cell-text">{{ displaySiteEnrollmentPeriodTarget(selectedSiteEnrollmentPlan, scope.row.cells[columnIndex].key) }}</span>
|
||||
<span v-else class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ displaySiteEnrollmentPeriodTarget(selectedSiteEnrollmentPlan, scope.row.cells[columnIndex].key) }}</span>
|
||||
<el-tag
|
||||
v-if="isSelectedSiteEnrollmentPeriodUpdated(scope.row.cells[columnIndex].key)"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
<span v-else class="month-empty">-</span>
|
||||
</template>
|
||||
@@ -890,22 +1218,66 @@
|
||||
<el-table :data="currentSetupDraft.monitoringStrategies" class="ctms-table">
|
||||
<el-table-column label="监查类型" min-width="180">
|
||||
<template #default="scope">
|
||||
<span class="milestone-cell-text">{{ scope.row.strategyType || "-" }}</span>
|
||||
<span class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ scope.row.strategyType || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="isMonitoringStrategyFieldUpdated(scope.row, scope.$index, 'strategyType')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="策略详情" min-width="430">
|
||||
<template #default="scope">
|
||||
<span class="milestone-cell-text">{{ scope.row.detail || "-" }}</span>
|
||||
<span class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ scope.row.detail || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="isMonitoringStrategyFieldUpdated(scope.row, scope.$index, 'detail')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="监查次数" width="140">
|
||||
<template #default="scope">
|
||||
<span class="milestone-cell-text">{{ scope.row.frequency || "-" }}</span>
|
||||
<span class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ scope.row.frequency || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="isMonitoringStrategyFieldUpdated(scope.row, scope.$index, 'frequency')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="更新时间" width="180">
|
||||
<template #default="scope">
|
||||
<span class="milestone-cell-text">{{ scope.row.updatedAt || "-" }}</span>
|
||||
<span class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ scope.row.updatedAt || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="isMonitoringStrategyFieldUpdated(scope.row, scope.$index, 'updatedAt')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="!isPublishedView" label="操作" width="130">
|
||||
@@ -943,7 +1315,18 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="计划入组数" width="140">
|
||||
<template #default="scope">
|
||||
<span class="milestone-cell-text">{{ toDisplayNumber(scope.row.target) }}</span>
|
||||
<span class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ toDisplayNumber(scope.row.target) }}</span>
|
||||
<el-tag
|
||||
v-if="isCenterConfirmPlanFieldUpdated(scope.row.siteId, 'target')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="120">
|
||||
@@ -955,12 +1338,34 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="入组开始时间" width="150">
|
||||
<template #default="scope">
|
||||
<span class="milestone-cell-text">{{ scope.row.startDate || "-" }}</span>
|
||||
<span class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ scope.row.startDate || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="isCenterConfirmPlanFieldUpdated(scope.row.siteId, 'startDate')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="入组结束时间" width="150">
|
||||
<template #default="scope">
|
||||
<span class="milestone-cell-text">{{ scope.row.endDate || "-" }}</span>
|
||||
<span class="updated-value-wrap">
|
||||
<span class="milestone-cell-text">{{ scope.row.endDate || "-" }}</span>
|
||||
<el-tag
|
||||
v-if="isCenterConfirmPlanFieldUpdated(scope.row.siteId, 'endDate')"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
class="field-updated-tag"
|
||||
>
|
||||
已更新
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -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<ProjectPublishSnapshot>(() => 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 = <T extends { id?: string }>(
|
||||
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<string, SiteEnrollmentPlanDraft>();
|
||||
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<DisplayInfoItem[]>(() => [
|
||||
{ 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<DisplayInfoItem[]>(() => [
|
||||
{ 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<DisplayInfoItem[]>(() => [
|
||||
{ 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<DisplayInfoItem[]>(() => [
|
||||
{ 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<FormRules>({
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user